/* ============================================================================
   NabGol — CHAT CANVAS  (the middle region: where messages appear)
   ----------------------------------------------------------------------------
   Owns EXACTLY one thing: `#messages.chat-messages` and everything inside it.
   It does not touch the header, the composer, the sheets, the sidebar or the
   floating player — by design, so this file can be re-skinned for a new app
   without touching the shell.

   Why this file exists (measured on 2026-08-28 with tools/_chat_canvas_shot.js,
   the real CSS cascade + the real markdown pipeline, not by reading code):

     1. Body prose rendered at 15px / 1.75 on mobile. chat-flagship-polish.css
        asks for 16.75px / 2.18, but style-mobile.css + chat-mobile-flagship.css
        override it with `!important`. So the tuned scale never reached a phone,
        and the platform's own ">= 16px forever" rule was being broken in the one
        place it matters most — the answer text.
     2. Every <strong> carried a green highlighter gradient. Three bold words in
        one paragraph = three green blocks. That single rule is most of what read
        as "cheap" next to a first-class Persian product.
     3. Every colon-line (.nabgol-mag-section) was mint-green + bold, so a long
        answer shouted five times in green. Hierarchy has to come from weight and
        space, not from colour repeated on every label.
     4. A one-line courtesy reply ("خواهش می‌کنم…") was typeset as a 1.06em bold
        display lead, because .nabgol-mag-lead lands on the first paragraph of
        EVERY message regardless of length.
     5. Three nested boxes: canvas > assistant bubble > list card. The assistant
        bubble is the one that carries no information — only the user's own words
        need to be visually contained.
     6. The date was reprinted on every single bubble, plus five bordered action
        chips, under a four-word reply.
     7. blockquote got a box from chat-flagship-polish.css AND a second box from
        an earlier layer, so notes rendered as a card inside a card.

   Load order: LAST in both templates, after chat-flagship-polish.css and
   chat-mobile-flagship.css. `!important` here is not laziness — the layers below
   claim these same properties with `!important`, so a plain declaration loses.
   Every rule is scoped by `#messages` so the id keeps specificity above them.

   Proof harness (run it before claiming any change here is done):
     node live/tools/_chat_canvas_shot.js <outDir>        # 8 shots + geometry audit
     bash live/tools/_shot_slice.sh <shot.png> 1700       # readable slices
   ========================================================================== */

/* ---------------------------------------------------------------------------
   1) TOKENS — one scale, two themes. No other rule in this file may hard-code
   a colour or a size; a new app re-declares these and inherits the whole skin.
   Contrast is measured against the canvas each theme actually paints
   (dark #0e1621 mobile / #0d1117 desktop, light #ffffff / #f6f8fa).
   --------------------------------------------------------------------------- */
:root {
  /* ink */
  --nbgc-ink: #e8eef6;          /* 13.9:1 on #0e1621 */
  --nbgc-ink-strong: #ffffff;   /* bold runs and section labels */
  --nbgc-ink-soft: #94a3b6;     /*  7.1:1 — meta, captions, quiet chrome */
  --nbgc-ink-faint: #64748b;    /*  4.0:1 — hairline labels only, never prose */
  --nbgc-ink-faint-on-user: #748297; /* 3.5:1 on --nbgc-user-face over the mobile
     dark canvas (#0e1621) — ink-faint itself only reaches 2.87:1 there because the
     tinted user bubble is lighter than the plain canvas it was calibrated for. */

  /* brand */
  --nbgc-accent: #34d399;
  --nbgc-accent-ink: #6ee7b7;   /* accent used AS TEXT on the canvas */
  --nbgc-accent-wash: rgba(52, 211, 153, 0.10);
  --nbgc-accent-edge: rgba(52, 211, 153, 0.28);
  --nbgc-ink-on-accent: #000000; /* an icon painted ON a solid --nbgc-accent
     fill (the composer's send button) — --nbgc-accent itself is a bright
     mint on this theme, so white measures only 1.92:1 there; needs its own
     token because it answers a different question than --nbgc-accent-ink
     ("accent used as text on the canvas") answers. */

  /* surfaces that sit ON the canvas */
  --nbgc-surface: rgba(148, 168, 200, 0.07);
  --nbgc-surface-2: rgba(148, 168, 200, 0.11);
  --nbgc-surface-raised: rgba(148, 168, 200, 0.17); /* "which control in a chrome
     bar is the primary one, without spending green on it?" — surface-2 is the
     HOVER weight of a ghost control, so a control that rests there is
     indistinguishable from any hovered neighbour. This is the step above it:
     the resting/hover surface of the single weighted control in a bar (today
     the header's "new chat" +), so weight can come from surface instead of
     from accent, which the canvas needs more than the chrome does. */
  --nbgc-hairline: rgba(148, 168, 200, 0.16);
  --nbgc-hairline-strong: rgba(148, 168, 200, 0.26);
  /* the header's own surface for the moments it stands ABOVE a panel's scrim
     (chat-header.css section 8). Opaque on purpose: a bar that is still live
     must not read as part of the dimmed page behind it, and a transparent one
     would show the scrim straight through and look disabled while working.
     Same value as the dark mobile canvas, so the lift is invisible until you
     look at what is dimmed and what is not. */
  --nbgc-header-lifted: #0e1621;
  /* the face of a panel that floats ABOVE the page — bottom sheets and the
     header dropdown. Answers a different question than --nbgc-header-lifted:
     that one hides a lift by matching the page, this one has to read as a
     raised surface sitting on top of it. */
  --nbgc-sheet-face: #0f172a;
  --nbgc-sheet-handle: rgba(255, 255, 255, 0.28);
  /* THE SECTION RULE gets its own token, and it is NOT --nbgc-hairline.
     A hairline that is right for the edge of a card is wrong for a rule that
     has to be read as structure across a whole answer — and the two themes do
     not need the same amount of it. This is the token that keeps day and night
     independently tuned instead of one being the other inverted: on the dark
     canvas a light rule at 0.16 alpha already reads, on white the same
     construction needs more, because dark-on-light loses less to the surround.
     Not shared with the header or the composer; nothing outside this region
     draws a structural rule. */
  --nbgc-rule: rgba(148, 168, 200, 0.20);

  /* the user's own words — the only bubble left */
  --nbgc-user-face: rgba(52, 211, 153, 0.15);
  --nbgc-user-edge: rgba(52, 211, 153, 0.30);
  --nbgc-user-ink: #eaf6f0;

  /* type scale — every number below was re-derived when the face changed to
     Nazli on 2026-09-02, because none of them survives a face swap.

     17.5px is not "bigger text". Nazli's Persian body height is 59.0% of the em
     where Vazirmatn RD's is 67.2% (measured: ink of سهصط above the baseline,
     _font_bakeoff_shot.js). The same px is therefore NOT the same size in the
     two faces, and shipping Nazli at 16.5 would have been a silent 12% shrink
     nobody chose. At 17.5px the rendered body height is 10.33px against the old
     16.5px's 11.09px — a deliberate 6.9% REDUCTION, which is the "it looks too
     big" half of the complaint, answered in the only unit that means anything:
     rendered pixels of letter, not the number in the stylesheet.

     lh 1.70 is likewise a measurement, and it is LOWER than the old 1.92 for a
     face that leaves less ink standing: window.__leadingProbe() at 17.5px puts
     Nazli's over-dots 13px above the baseline and its under-dots 4px below, so a
     line of real ink is 17px tall. At lh 1.70 the clear air between one line's
     under-dots and the next line's over-dots is 12.75px — against 13.7px on the
     old face at 1.92. Practically the same air, a smaller number, because the
     number was never the thing. The ratio air/ink lands at 0.75, which is where
     the previous pass argued a Persian paragraph belongs and this pass had no
     reason to move it. */
  /* Left at `normal`, and that is a measurement rather than an omission. Nazli's
     own word space renders 6.03px at 17.5px = 0.345em, which is already a
     generous, ordinary text space — nothing to correct. An intermediate
     candidate (Markazi Text) needed +0.10em here because its newspaper-tight
     0.167em let words run together; adding the same nudge to Nazli made the page
     measurably worse, so the token stays and the value does not.
     Kept as a token rather than a literal so the escape-hatch block below can
     restore the outgoing face's setting in one line with the rest of its pack. */
  --nbgc-wordspace: normal;
  --nbgc-prose: 17.5px;
  --nbgc-lh: 1.7;
  --nbgc-measure: 100%;
  --nbgc-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
  --nbgc-font: "Nazli", "Nazli Fallback", Tahoma, serif;
  /* The canvas owns its own typography. Before this token, answer text had no
     font-family of its own and inherited whichever <body> rule won the cascade
     that day — style-common.css:34, style-mobile.css:4, and
     chat-flagship-polish.css:24 each claim it with a different chain, and that
     last one leads with "IRANSansX", a file that is not on the server at all.
     Inheriting a font is not choosing one.

     Nazli since 2026-09-02, and the reason is a CATEGORY, not a taste.
     Readers reported that "everything looks bold" while body text has been
     font-weight: 400 the whole time — so the complaint was never about weight.
     It is how much black the TYPEFACE leaves per line, measured across six faces
     at weight 400 and 16.5px (_font_bakeoff_shot.js):

       Vazirmatn RD 0.0469   XB Zar 0.0420   XB Niloofar 0.0402
       Markazi Text 0.0399   Roya   0.0361   NAZLI       0.0342

     Nazli is the lightest of all of them — 27% less ink than the face it
     replaces, for the same words at the same rendered size.

     Why no amount of further Vazirmatn tuning could ever have found this: every
     face the project had compared — Vazirmatn, Vazirmatn RD, Shabnam, Sahel,
     Samim, Tanha, Gandom, Parastoo — is a geometric SANS off one designer's
     shared skeleton, so "they all look the same and they all look heavy" was the
     only answer the search could return. Nazli is a NASKH: the free GPL rebuild
     of NAZANIN by Sharif FarsiWeb, i.e. the letterform Persian schoolbooks and
     theses are actually set in. It also agrees with this region's own design
     thesis, written down by an earlier pass: the answer is a DOCUMENT, not a
     chat.

     And it survives the thing a naskh is supposed to fail at. Thin strokes
     normally go soft on a dark ground, which is where most of these students
     read; measured (_zwnj_night_probe.js), night edge sharpness as a share of
     day is 112.4% for Nazli against 95.4% for Vazirmatn RD. It gets crisper on
     the dark canvas where the outgoing face got softer.

     Rejected with reasons, so this is not re-run: XB Zar and XB Niloofar are the
     academic LaTeX standard and genuinely handsome, but 1.40MB and 1.25MB (six
     scripts each) on a server that compresses nothing, and their letters run
     75-76% of the em so matching today's size would mean declaring 13.4px and
     breaking the 16px floor. Markazi Text is variable and only 67KB but is a
     newspaper face, and 15% lighter where Nazli is 27%. Roya (the free Lotus) is
     the runner-up. Noto Naskh, Scheherazade and Amiri are static AND heavier.

     "Nazli Fallback" is the metric-matched local() face from
     assets/vendor/nazli/Nazli-fallback.css; it holds the layout still during the
     swap. The generic tail is `serif`, not `sans-serif`, because on a device
     where nothing else resolves a serif is the nearer miss. This token is the
     only place a font name may appear in this file — chat-header.css and
     chat-composer.css consume it so the whole chat page speaks one voice, and
     the vendor file additionally re-declares the same two .woff under the legacy
     name "Vazirmatn" so the other 22 declarations across the platform follow
     without a single edit. */

  /* ── RHYTHM ────────────────────────────────────────────────────────────────
     ONE modular scale. Every vertical distance inside #messages is a multiple
     of --nbgc-rhythm and nothing else, so the file cannot go back to
     accumulating 1.1em here and 13px there until no two gaps agree.

     32px is not a taste: it is the line box this page already paints. Mobile is
     16.5 x 1.92 = 31.68px, desktop is 17 x 1.90 = 32.30px. The grid was always
     there; before this token the spacing simply ignored it. If the type scale
     or the face ever changes, recompute this number FIRST — every other
     distance in the file follows it automatically.

     The steps below are the only vertical values this file may use. They exist
     as named tokens rather than as inline calc() so that `grep` can prove the
     rule holds (see the gate's rhythm check), and so a future app can retune
     the whole vertical feel by editing one line. */
  /* 31px since 2026-09-02, and it moved for exactly the reason the paragraph
     above says it must: the face changed, so the line box this page paints
     changed. Mobile is now 20 x 1.50 = 30.00 and desktop 20.5 x 1.55 = 31.78;
     the mean rounds to 31, by the same arithmetic that produced 32 from 31.68
     and 32.30. Nothing else in this file was touched to follow it — every gap is
     already calc(var(--nbgc-rhythm) * n), which is the whole point of the token,
     and the one-line edit re-tunes the entire vertical feel. */
  --nbgc-rhythm: 30px;
  --nbgc-r-025: calc(var(--nbgc-rhythm) * 0.25);   /*  7.5px */
  --nbgc-r-05:  calc(var(--nbgc-rhythm) * 0.5);    /* 15px   */
  --nbgc-r-075: calc(var(--nbgc-rhythm) * 0.75);   /* 22.5px */
  --nbgc-r-1:   var(--nbgc-rhythm);                /* 30px   */
  --nbgc-r-15:  calc(var(--nbgc-rhythm) * 1.5);    /* 45px   */
  --nbgc-r-2:   calc(var(--nbgc-rhythm) * 2);      /* 60px   */
  /* HORIZONTAL AND OPTICAL CONSTANTS. Vertical rhythm is a grid; horizontal
     padding is not, and forcing it onto the same multiples would make a table
     cell or a quote indent worse in order to make a number look tidy. They are
     still tokens, not literals, so `grep` can prove no loose px survives in any
     margin or padding in this file. */
  --nbgc-inset: 18px;           /* boxed blocks: note, question, step, pre     */
  --nbgc-inset-sm: 14px;        /* table cells, edit textarea                  */
  --nbgc-indent: 26px;          /* the blockquote's indent-instead-of-a-box    */
  --nbgc-gutter: 12px;          /* the phone canvas's side gutter              */
  --nbgc-avatar-nudge: 2px;     /* optical: drops the 28px avatar onto the
                                   first text line's cap, not its box top      */
  --nbgc-avatar-nudge-sm: 3px;  /* the phone's 24px avatar needs one more px   */

  --nbgc-gap-turn: var(--nbgc-r-15);   /* between two different speakers */
  --nbgc-gap-same: var(--nbgc-r-05);   /* two messages from the same speaker */
  --nbgc-radius: 18px;
  --nbgc-radius-sm: 12px;
}

html[data-theme="light"] {
  --nbgc-ink: #14181d;          /* 15.8:1 on #ffffff */
  --nbgc-ink-strong: #000000;
  --nbgc-ink-soft: #5b6672;     /*  6.4:1 */
  --nbgc-ink-faint: #78838f;    /*  4.5:1 */
  --nbgc-ink-faint-on-user: #78838f; /* the light user-face tint never dips below
     ink-faint's floor, so no separate value is needed here. */

  --nbgc-accent: #15803d;
  --nbgc-accent-ink: #0f6a37;   /*  5.6:1 — the mint greens all fail on white */
  --nbgc-accent-wash: rgba(21, 128, 61, 0.07);
  --nbgc-accent-edge: rgba(21, 128, 61, 0.22);
  --nbgc-ink-on-accent: #ffffff; /* 5.0:1 — this theme's accent is dark enough
     that white already works, unlike the dark theme's bright mint. */

  --nbgc-surface: rgba(15, 23, 42, 0.04);
  --nbgc-surface-2: rgba(15, 23, 42, 0.07);
  --nbgc-surface-raised: rgba(15, 23, 42, 0.11);
  --nbgc-hairline: rgba(15, 23, 42, 0.10);
  --nbgc-hairline-strong: rgba(15, 23, 42, 0.18);
  --nbgc-header-lifted: #ffffff;
  --nbgc-sheet-face: #ffffff;
  --nbgc-sheet-handle: rgba(15, 23, 42, 0.22);
  /* 0.16, not the dark theme's 0.20 mirrored to 0.10: at 0.10 the section rule
     was measured invisible against white in the day-mode comparison shots, and
     the whole structural device evaporated in one of the two themes. */
  --nbgc-rule: rgba(15, 23, 42, 0.16);

  --nbgc-user-face: rgba(21, 128, 61, 0.09);
  --nbgc-user-edge: rgba(21, 128, 61, 0.20);
  --nbgc-user-ink: #12241a;
}

body.desktop-chat {
  /* Same derivation as the mobile pair, re-run at the desktop size: rendered
     body height 0.590 x 18 = 10.62px against the old 17px face's 11.42px, i.e.
     the same deliberate 7% reduction the phone gets. __leadingProbe() at 18px
     reads 13px of over-dot and 4px of under-dot, a 17px band; lh 1.65 leaves
     12.7px of clear air, ratio 0.75 — the same paragraph texture as the phone
     and as the face this replaces. */
  --nbgc-prose: 18px;
  --nbgc-lh: 1.65;
  /* px, not em: this caps .message-bubble, whose own font-size is 14px, so an
     em value silently resolved to 532px instead of the intended 646.

     560px, and the old 646 was wrong — measured 2026-08-30 with
     window.__measureProbe() (live/tools/_canvas_measure.js), which buckets every
     character onto the line box it actually painted on and averages only the
     COMPLETE lines, because the ragged last line biases the naive
     length/lineCount by up to 8%.

       width   em    chars/line
        520   30.6      69.5
        560   32.9      74.9
        646   38.0      86.0   <- what shipped, labelled "~70 characters"
        700   41.2      93.5

     So the file's own comment was off by 16 characters. Two things fell out of
     the measurement that are worth keeping:

     1. A Persian character is NARROWER than a Latin one here, not wider:
        0.427em of average advance against Latin's ~0.5em. The script joins, and
        half the alphabet is a vertical stroke. Every "Persian is wider" instinct
        (this session started with one) points the wrong way.
     2. Because of that, neither "60-75 characters" nor "9-12 words" survives the
        trip from Latin — the character count comes out too permissive and the
        word count too strict (Persian words are short: 3.5 letters on average in
        this product's own prose, so 646px carries 19 words per line).

     What DOES transfer is the em-width of the line, which is what all three
     rules are proxies for. Latin's 66-character optimum is 33em; at 17px that is
     561px, and it lands at 74.9 Persian characters — inside the classic band as
     well. Two independent derivations agreeing on 560 is the reason to trust it.
     A 3-column practice table and the widest diagram card both still fit.

     ── RE-DERIVED 2026-09-02 FOR NAZLI: 560 -> 460 ──
     The paragraph above concluded that what transfers between faces is the
     em-width of the line, not the character count. That was true for the swap it
     was written about — Vazirmatn and Vazirmatn RD have byte-identical advances,
     so both derivations had to agree. Across a change of CATEGORY they stop
     agreeing, and only one of them is still measuring the thing that matters.
     Nazli is a narrow book face: the same Persian sentence is 21% shorter in it
     than in Vazirmatn RD (2385.06px against 3023.19px at 100px). 33em of Nazli
     at 18px is 594px, and __measureProbe() puts 88 characters on that line — far
     outside any reading rule. The em was only ever a proxy for how many words
     the eye crosses per line; the character count is what survives.

       width   em    chars/line   words/line     (Nazli @ 18px)
        440   24.4      65.5         15.0
       *460   25.6      68.0         15.5   <- chosen
        480   26.7      71.5         16.3
        500   27.8      74.9         17.0   <- at the ceiling
        560   31.1      84.3         18.8   <- what 560 now buys: too long

     460px, and it is the middle of the band rather than its edge: 68.0 sits with
     room on both sides where 480 and 500 lean on the upper limit, and 66 is the
     classic optimum this whole rule descends from. window.__canvasAudit()
     confirms the 3-column practice table and the widest diagram card still fit
     at 460 with nothing overflowing — the narrower column costs less than it
     looks like it should, because Nazli sets everything inside it narrower too.

     On the phone the reading width is the phone's, and __measureProbe() reads
     51.5 characters at 334px. Below the band, and correctly so: a 390px screen
     has no 68-character line to give and never did. */
  --nbgc-measure: 460px;        /* 68.0 Persian characters at 18px, measured */
  --nbgc-gap-turn: var(--nbgc-r-2);
  --nbgc-gap-same: var(--nbgc-r-05);
}

/* ── THE WAY BACK ───────────────────────────────────────────────────────────
   The owner asked for two things at once: ship Nazli everywhere, and be able to
   take it back — either wholly, or for particular robots that should not look
   like a textbook. This block is the CSS half of that; the other half is
   includes/nabgol_typeface.php, which decides which font files a page links and
   stamps the same id onto <body data-typeface>.

   HOW TO USE IT
     one robot   -> add  "typeface": "sans"  to that robot's record. No code.
     everything  -> set NABGOL_TYPEFACE_DEFAULT to 'sans'. One line.
   A robot created next year needs no action either way: an absent key resolves
   to the default, so the platform face follows automatically.

   WHY THIS BLOCK RESTORES SIX TOKENS AND NOT JUST THE FONT NAME
   A typeface is not a swappable skin here, and pretending otherwise would ship
   a broken page under the escape hatch. Every number below is a property of the
   face it was derived for:
     - Nazli's Persian body height is 59.0% of the em, Vazirmatn RD's is 67.2%,
       so the SAME px is not the same size — hence a different --nbgc-prose.
     - Their ink bands differ, so the leading that leaves the same air differs.
     - --nbgc-rhythm is round(prose x lh) and follows both.
     - Nazli sets the same sentence 21% shorter, so the column that carries
       60-75 characters is a different width.
     - Nazli's natural word space is wide enough that justification needs no
       help; Vazirmatn's is too, at `normal`.
   Restoring the font name alone would leave a sans face on a naskh's type
   scale, which is a worse page than either choice made properly.

   These are the values this file shipped before 2026-09-02, unchanged, with
   their original derivations recorded in the git history of this file. */
body[data-typeface="sans"] {
  --nbgc-font: "Vazirmatn RD", "Vazirmatn RD Fallback", Tahoma, sans-serif;
  --nbgc-prose: 16.5px;
  --nbgc-lh: 1.92;
  --nbgc-rhythm: 32px;
  --nbgc-wordspace: normal;
}

body.desktop-chat[data-typeface="sans"] {
  --nbgc-prose: 17px;
  --nbgc-lh: 1.9;
  --nbgc-measure: 560px;
}

/* ---------------------------------------------------------------------------
   2) THE CANVAS
   The background stays whatever the shell paints (var(--tg-bg) on mobile,
   var(--bg) on desktop): the middle region must not seam against the header and
   the composer, which this session does not own. The only thing removed is the
   green radial wash — it tinted the top of every conversation.
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) #messages.chat-messages {
  font-family: var(--nbgc-font) !important;
  background-image: none !important;
  display: flex !important;
  flex-direction: column !important;
  align-items: stretch !important;
  row-gap: 0 !important;                /* rhythm comes from .message margins */
  scroll-behavior: smooth;
  scrollbar-width: thin;
  scrollbar-color: var(--nbgc-hairline-strong) transparent;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

body.mobile-chat #messages.chat-messages {
  padding: var(--nbgc-r-05) var(--nbgc-gutter) var(--nbgc-r-075) !important;
}

body.desktop-chat #messages.chat-messages {
  padding: var(--nbgc-r-1) 0 !important;
}

body:is(.desktop-chat, .mobile-chat) #messages.chat-messages::-webkit-scrollbar {
  width: 10px;
}
body:is(.desktop-chat, .mobile-chat) #messages.chat-messages::-webkit-scrollbar-thumb {
  background: var(--nbgc-hairline);
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: 999px;
}
body:is(.desktop-chat, .mobile-chat) #messages.chat-messages:hover::-webkit-scrollbar-thumb {
  background: var(--nbgc-hairline-strong);
  background-clip: content-box;
}
body:is(.desktop-chat, .mobile-chat) #messages.chat-messages::-webkit-scrollbar-track {
  background: transparent;
}

body:is(.desktop-chat, .mobile-chat) #messages .message ::selection {
  background: var(--nbgc-accent-edge);
  color: var(--nbgc-ink-strong);
}

/* ---------------------------------------------------------------------------
   3) TURN RHYTHM
   A transcript reads as turns, not as a stack of equal cards: a new speaker
   gets a full gap, a follow-up from the same speaker gets a small one.
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) #messages .message {
  width: 100% !important;
  max-width: 100% !important;
  margin: 0 !important;
  padding: 0 !important;
  background: none !important;
  border: 0 !important;
  overflow: visible !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .message + .message {
  margin-top: var(--nbgc-gap-turn) !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message.assistant + .message.assistant,
body:is(.desktop-chat, .mobile-chat) #messages .message.user + .message.user {
  margin-top: var(--nbgc-gap-same) !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .message-col,
body:is(.desktop-chat, .mobile-chat) #messages .message-row {
  width: 100% !important;
  max-width: 100% !important;
  min-width: 0 !important;
}

/* WHICH SIDE EACH SPEAKER SITS ON — a product decision, not a CSS accident.
   Owner's call (2026-08-29): **the reader's own message hugs the RIGHT edge; the
   robot's answer, its bubble and its avatar sit on the LEFT.** That is the
   arrangement Persian users are habituated to from WhatsApp and Telegram, and it
   is the one this product shipped with. It is NOT the textbook RTL mirror (which
   would put "mine" on the left), so nobody should "fix" it back later.

   Mechanics, because three layers fight over this:
   - The ROW runs on a PHYSICAL left-to-right axis (`direction: ltr`), so
     flex-start means the physical left for both speakers no matter what
     style-desktop.css:756/759 forces.
   - assistant: `justify-content: flex-start` -> avatar first, at the far left.
   - user: `justify-content: flex-end`       -> bubble pinned to the right.
   - The BODY switches back to `direction: rtl`, which does two jobs at once: the
     Persian text reads right-to-left, and a max-width block (the capped answer)
     sits at its RTL start edge — the right — so the answer's first-read edge
     lands on the canvas edge instead of floating mid-column.

   Avatar also rides the FIRST line, not the bottom of the card: bottom-aligning
   it put the robot mark under the action row, where it read as a stray badge. */
body:is(.desktop-chat, .mobile-chat) #messages .message-col,
body:is(.desktop-chat, .mobile-chat) #messages .message-row {
  direction: ltr !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message-row {
  display: flex !important;
  align-items: flex-start !important;
  gap: 10px !important;
  flex-direction: row !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message.assistant .message-row {
  justify-content: flex-start !important;   /* avatar + answer -> LEFT */
}
body:is(.desktop-chat, .mobile-chat) #messages .message.user .message-row {
  justify-content: flex-end !important;     /* the reader's words -> RIGHT */
}
body:is(.desktop-chat, .mobile-chat) #messages .message-body {
  direction: rtl !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .msg-avatar {
  width: 28px !important;
  height: 28px !important;
  min-width: 28px !important;
  min-height: 28px !important;
  flex: 0 0 28px !important;
  margin: var(--nbgc-avatar-nudge) 0 0 !important;
  /* ── THE AVATAR RIDES WITH ITS ANSWER ─────────────────────────────────────
     The owner's rule (2026-08-29) is that the robot's mark sits on the LEFT of
     its words. It was implemented as "the left edge of the canvas", which was
     the same place while the answer was 646px wide inside a 780px canvas — 106px
     of gap. When the measure was re-derived to 460px for Nazli (2026-09-02) the
     answer moved right and the badge did not: measured 292px between the avatar
     and the first word it belongs to, a 28px mark alone in an empty field. The
     same defect the 2026-08-29 pass was written to fix ("آواتار ۷۵۰px دورتر از
     پاسخِ خودش"), re-created by a change in a different file.

     `margin-inline-start: auto` inside the row's ltr axis eats the free space
     BEFORE the avatar, so the pair [avatar][answer] is pushed to the row's end
     as one unit and the gap between them is the row's own 10px, at every answer
     length and every viewport width. The side does not change, the answer's
     first-read edge does not move, and the number can never drift again because
     it is no longer a number. */
  margin-inline-start: auto !important;
  border-radius: 9px !important;
  background: var(--nbgc-accent-wash) !important;
  color: var(--nbgc-accent-ink) !important;
  border: 1px solid var(--nbgc-accent-edge) !important;
  font-size: 13px !important;
  font-weight: 700 !important;
  line-height: 1 !important;
  box-shadow: none !important;
  overflow: hidden !important;
}

/* The avatar stays on a phone too — it is what marks the LEFT side as the robot's
   (owner's call 2026-08-29). It was hidden here before because 28px + a 12px gap
   is 10% of a 390px reading measure; paid for instead by trimming the canvas
   gutter and the avatar itself, so the line still holds ~40 Persian characters:
   390 - 2x12 gutter - (24 + 8) avatar = 334px of text. */
/* A RUN OF ANSWERS IS ONE BLOCK, SO IT GETS ONE MARK.
   --nbgc-gap-same already binds consecutive messages from the same speaker into
   a group (15px against the 45/60px turn gap). Repeating the badge on every
   member of that group re-announces a speaker who has not changed — measured on
   the real page 2026-09-02: two answers 15px apart, each with its own 28px mark.
   `visibility: hidden` and not `display: none` on purpose: the box stays, so the
   answer column does not shift by 38px halfway down a run, and the first mark of
   the run still sits where the owner's rule puts it. The avatar is
   aria-hidden="true" decoration, so nothing is lost to a screen reader either.
   If anything ever lands between two answers, the sibling selector simply stops
   matching and both marks come back — the safe failure. */
body:is(.desktop-chat, .mobile-chat) #messages .message.assistant + .message.assistant .msg-avatar {
  visibility: hidden !important;
}

body.mobile-chat #messages .msg-avatar {
  width: 24px !important;
  height: 24px !important;
  min-width: 24px !important;
  min-height: 24px !important;
  flex: 0 0 24px !important;
  border-radius: 8px !important;
  font-size: 11px !important;
  margin-top: var(--nbgc-avatar-nudge-sm) !important;
}
body.mobile-chat #messages .message-row {
  gap: 8px !important;
}
/* A user row has no avatar, so its bubble would otherwise stop 32px short of the
   right edge and look misaligned against the answers above it. */
body.mobile-chat #messages .message.user .message-row,
body.desktop-chat #messages .message.user .message-row {
  padding-inline-start: 0 !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .message-body {
  min-width: 0 !important;
  flex: 1 1 auto !important;
}
/* The answer's column is the MEASURE, and it is the same width whether the
   answer is four words or forty lines. That constancy is what lets the avatar
   below park at one x for every message instead of chasing the text. */
body:is(.desktop-chat, .mobile-chat) #messages .message.assistant .message-body {
  width: 100% !important;
  max-width: var(--nbgc-measure) !important;
}
/* Cap the whole answer block, not just its paragraphs: prose, tables, diagrams,
   the action row and the timestamp then share ONE column edge. Capping only
   `.text` left the footer 94px wider than the words above it. */
body:is(.desktop-chat, .mobile-chat) #messages .message.assistant .message-bubble {
  max-width: var(--nbgc-measure) !important;
}
/* The user's own message stays a contained quotation, never a full-width slab —
   and never a WIDER one than the answer either.

   Measured on the real page 2026-09-02, desktop: the answer sat in its 460px
   measure while a long question ran to 577px, i.e. the reader's own words got a
   25% wider column and ~80 characters a line where this file's own derivation
   put the comfortable line at 68. Two speakers, two measures, and the louder one
   belonged to the question rather than to the answer — which is the exact fault
   the saturated green bubble was removed for, still present in geometry after it
   was fixed in colour. `min()` keeps the percentage as the small-screen rule and
   the measure as the ceiling, so the two speakers share one column edge. */
body.mobile-chat #messages .message.user .message-body {
  flex: 0 1 auto !important;
  width: fit-content !important;
  max-width: min(86%, var(--nbgc-measure)) !important;
}
body.desktop-chat #messages .message.user .message-body {
  flex: 0 1 auto !important;
  width: fit-content !important;
  max-width: min(74%, var(--nbgc-measure)) !important;
}

/* ---------------------------------------------------------------------------
   4) THE ONE BUBBLE
   Assistant: no container at all. The answer is a document on the canvas, which
   is what every first-class assistant product does — and it removes one of the
   three nested boxes an answer used to sit in.
   User: one soft brand-tinted card. Not the old saturated gradient with white
   text: a five-line question in white-on-green is the least readable surface in
   the whole product, and it made the user's own words shout louder than the
   answer they were asking about.
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) #messages .message.assistant .message-bubble {
  background: none !important;
  background-image: none !important;
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow: none !important;
  padding: 0 !important;
  overflow: visible !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .message.user .message-bubble {
  background: var(--nbgc-user-face) !important;
  background-image: none !important;
  border: 1px solid var(--nbgc-user-edge) !important;
  border-radius: var(--nbgc-radius) !important;
  /* One tightened corner, and it belongs on the side the bubble is ANCHORED to.
     `end-end` resolves to block-end + inline-end, which under rtl is the BOTTOM
     LEFT — the far corner, pointing away from the edge the bubble is pinned to
     and into the empty canvas, where it reads as an asymmetry rather than as a
     tail. Every RTL messenger this product's readers already use (Telegram,
     WhatsApp, Bale) puts it on the anchored side. `end-start` is that corner
     under rtl: bottom right. */
  border-end-start-radius: 7px !important;
  box-shadow: none !important;
  padding: var(--nbgc-r-05) var(--nbgc-inset) var(--nbgc-r-025) !important;
  overflow: visible !important;
  color: var(--nbgc-user-ink) !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .message.user .message-bubble.has-media {
  padding: var(--nbgc-r-025) !important;
}

/* ---------------------------------------------------------------------------
   5) PROSE — the part the whole product is judged on.
   16.5px / 1.92 on a phone, 17px / 1.9 on a desktop.

   The leading was re-measured rather than re-asserted (window.__leadingProbe,
   live/tools/_canvas_measure.js: real Persian rasterised at the real size, pixel
   rows scanned for actual ink instead of trusting the font's metric tables).
   What came back corrects this file's own previous claim, which said the dots
   COLLIDE below ~1.85. They do not — not at any value anyone would consider:

     ink of one line spans -14px (over-dots: ت ث ن ق, and the alef/lam risers)
     to +4px (under-dots and the connected ج ح چ bowls) at 16.5px, i.e. an ink
     band about 18px tall. Clear air between one line's under-dots and the
     next line's over-dots:
       lh 1.72 -> 10.4px      lh 1.90 -> 13.3px
       lh 1.85 -> 12.5px      lh 1.92 -> 13.7px      lh 2.18 -> 18.0px

   Nothing touches anything. So the choice is texture, not mechanics, and the
   honest reason to hold 1.90/1.92 is the ratio: air ends up ~0.76 of the ink
   band, at the generous end of a comfortable text setting, and a Persian
   paragraph needs to be at that end because its dots ARE the alphabet — ب پ ت ث
   are one skeleton and a dot count. 2.18 (what shipped before) put the air at
   1.0 of the ink band, which is why long answers read as loose lines instead of
   paragraphs. The compact direction's 1.76 (air 0.61) stayed legible in the
   comparison shots and was rejected for what it did to the hierarchy, not for
   what it did to the dots — see the operating plan for that decision.

   These two values are also what makes --nbgc-rhythm exact: 16.5 x 1.92 =
   31.68 and 17 x 1.90 = 32.30, both 32px to the eye. Changing either one means
   recomputing the rhythm token.
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) #messages .message .text {
  max-width: 100% !important;
  font-size: var(--nbgc-prose) !important;
  line-height: var(--nbgc-lh) !important;
  font-weight: 400 !important;
  letter-spacing: 0 !important;
  word-spacing: var(--nbgc-wordspace) !important;
  color: var(--nbgc-ink) !important;
  padding: 0 !important;
  text-wrap: pretty;
  hanging-punctuation: first last;
  overflow-wrap: anywhere !important;
  word-break: normal !important;
  font-variant-numeric: proportional-nums;
  /* The region CLAIMS this property instead of inheriting whatever the chain
     decides. chat-flagship-polish.css asked for `emoji` here, which forces emoji
     presentation onto every character that has one — the ASCII digits, `#` and
     `*` included, because they are keycap bases. That is how `1234567890` inside
     inline code became ten blue tiles (measured 2026-09-02). Claimed, so it
     cannot come back through a `:where()` upstream. */
  font-variant-emoji: normal !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .message.user .text {
  color: var(--nbgc-user-ink) !important;
  max-width: 100% !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .message .text > :first-child {
  margin-top: 0 !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text > :last-child {
  margin-bottom: 0 !important;
}

/* ── WHERE PROSE STOPS: `:not(.nabgol-diagram-shell *)` ─────────────────────
   Every bare-tag rule from here to the end of the list section carries this
   guard, and it means one thing: THE CANVAS STYLES PROSE, NOT THE INSIDE OF A
   COMPONENT. A diagram card is not a paragraph that happens to contain shapes —
   it is a component with its own type scale, its own rhythm and its own ink
   tokens (--nbgd-*), and chat-flagship-polish.css owns all three.

   Left off, these rules reach straight through the card's frame and restyle its
   internals with prose values, with !important, against component rules that
   have none. Measured on the desktop light shell, 2026-08-30
   (tools/_diagram_size_probe.js, `EXPLAIN=compare`):
     · the <h4> naming a compare column took the prose heading rhythm — 32px above
       and 8px below — inside a flex header, making it 92.8px tall for 28px of
       content, three times per card
     · its <ul>/<li> took prose line-height 1.9 and prose list margins, adding
       another ~34px per column
   Total: `compare` stood 597px tall in the chat and 341px in the fullscreen
   viewer — the SAME markup, at a NARROWER width. The viewer was never better
   designed; it simply lives outside `#messages`, so none of this reached it.
   Reproducing that boundary here is what closed most of the gap.

   The guard sits on the SUBJECT of each selector, never on an ancestor: it must
   ask "is the element I am about to style inside a card", not "is some ancestor".
   Prose outside a card is untouched — the `.nabgol-mag-*` rules below are
   class-scoped to shapes only prose emits, so they need no guard. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text p:not(.nabgol-diagram-shell *) {
  margin: 0 0 var(--nbgc-r-05) !important;
  line-height: var(--nbgc-lh) !important;
  font-size: 1em !important;
  font-weight: 400 !important;
  color: var(--nbgc-ink) !important;
  background: none !important;
  border: 0 !important;
  padding: 0 !important;
}

/* ── EVEN LINE LENGTHS, AND WHY NOT BY JUSTIFYING ──────────────────────────
   The owner asked for this in as many words: "equal line lengths, tidy setting,
   standard — not overdone". The obvious answer is text-align: justify. It was
   built, measured, and rejected, and the numbers are the reason.

   Persian cannot be justified well by a browser. The engine stretches WORD
   SPACES; it does not extend kashida, the baseline elongation an Arabic
   typesetter would use, because no shipping engine implements it for this
   script. So every pixel of "equal edges" is bought with white between words.
   Measured on 20 real paragraphs in both shells with
   live/tools/_justify_river_probe.js — every space rect on every painted line,
   compared against the same paragraph with justification switched off:

     text-align: justify        widest word gap 0.95em, worst line-to-line
                                space ratio 1.57. One paragraph in five had a
                                hole in it wide enough to see.
     justify + text-wrap:balance   worse, not better: balance shortens each line,
                                then justify has to stretch further to fill it —
                                widest gap went to 2.45em.
    *text-wrap: balance alone   word spaces stay EXACTLY natural (stretch
                                0.001em) and the lines come out 7.3% apart on
                                average, 9.6% at worst.

   Against 40.3% average raggedness with neither, that last row is the owner's
   request answered — nearly equal lines — while the "not overdone" half is
   answered by there being no stretching at all. balance equalises the lines by
   choosing better BREAK POINTS instead of by inflating the spaces, which is
   what a typesetter would have done and what justification cannot do here.

   The honest limit, recorded rather than discovered later: browsers cap
   text-wrap: balance at a handful of lines and silently fall back to normal
   wrapping above it. Every paragraph in the real lesson content is 2-3 lines, so
   the cap is not reached today; a very long paragraph would simply wrap normally
   rather than break. That is a graceful floor, not a cliff.

   Where it is NOT applied, and why each one:
     .user            the user's own line is short and lives in a bubble
     mag-eyebrow/title/section/kicker/pull
                      headings and labels; balancing a two-word label just moves
                      the word
     table cells      never in the selector; balance fights a fixed column
     diagram internals
                      the standing rule of this file — the canvas styles prose,
                      not the inside of a component

   text-wrap: pretty stays on .text above and still governs everything this rule
   does not name. The two are alternatives, not a pair: both are text-wrap-style
   values, so a paragraph gets one or the other. */
body:is(.desktop-chat, .mobile-chat) #messages .message:not(.user) .text
  :is(p, li):not(.nabgol-diagram-shell *):not(.nabgol-mag-eyebrow):not(.nabgol-mag-title):not(.nabgol-mag-section):not(.nabgol-mag-kicker):not(.nabgol-mag-pull) {
  text-wrap: balance;
}

/* Bold is bold. Nothing else. The green highlighter behind every <strong> was
   the single loudest thing in the old canvas. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text strong:not(.nabgol-diagram-shell *),
body:is(.desktop-chat, .mobile-chat) #messages .message .text b:not(.nabgol-diagram-shell *) {
  font-weight: 700 !important;
  color: var(--nbgc-ink-strong) !important;
  background: none !important;
  background-image: none !important;
  padding: 0 !important;
  border-radius: 0 !important;
  box-shadow: none !important;
}

/* Persian has no real italic. A sloped naskh is a synthetic shear that eats
   legibility, so emphasis becomes a colour shift instead.
   It used to be a colour shift PLUS font-weight 500, which Nazli cannot paint:
   the face has 400 and 700 and nothing between, so CSS font matching resolved
   500 down to 400 and the rule quietly did half of what it claimed. Written as
   400 now, because a stylesheet that names a weight the font does not have is a
   stylesheet nobody can reason about. Colour does the work, and <em> in this
   product's prose is rare enough that it does not need more. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text em:not(.nabgol-diagram-shell *),
body:is(.desktop-chat, .mobile-chat) #messages .message .text i:not(.nabgol-diagram-shell *) {
  font-style: normal !important;
  font-weight: 400 !important;
  color: var(--nbgc-ink-strong) !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .message .text a {
  color: var(--nbgc-accent-ink) !important;
  text-decoration: underline !important;
  text-decoration-thickness: 1px !important;
  text-underline-offset: 0.22em !important;
  text-decoration-color: var(--nbgc-accent-edge) !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text a:hover {
  text-decoration-color: var(--nbgc-accent) !important;
}

/* Headings never reach chat prose (normalizeProse strips markdown '#' by design,
   gated by tools/test_chat_editorial.php) — but if a robot's custom pipeline ever
   emits one, it must land on a restrained scale rather than a 2em shout. All six
   levels collapse into two steps on purpose.

   `:not(.nabgol-diagram-shell *)` is load-bearing, and this is what it costs to
   leave out. A defensive fallback for a heading that "never happens" in prose was
   silently claiming every heading a DIAGRAM CARD writes on purpose — a compare
   card names each column with an <h4>, and this rule handed it 32px of prose
   rhythm above and 8px below, with !important, inside a flex header whose own
   `margin: 0` had no !important to fight back with. Measured 2026-08-30 with
   tools/_diagram_size_probe.js: `.nabgol-compare-head` was 92.8px tall in the chat
   against 52.4px in the fullscreen viewer, for identical 28px content — 40px of
   dead band per column, three columns per card. The viewer escaped it for one
   reason only: it lives outside `#messages`, so this rule never reached it. That
   is most of why "the chat version never reached the fullscreen's level".

   Third time this exact shape has bitten this area: `#messages *` in the
   reduced-motion reset killed the thinking plant, the `:is(img,video,svg)` reset
   killed KaTeX's radical, and this killed the card headers. A blanket declaration
   inside #messages must always ask whether a COMPONENT below it already decided
   the thing being reset. Prose rules stop where a component's own skin begins. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text :is(h1, h2):not(.nabgol-diagram-shell *) {
  margin: var(--nbgc-r-15) 0 var(--nbgc-r-05) !important;
  font-size: 1.3em !important;
  font-weight: 700 !important;
  line-height: 1.45 !important;
  color: var(--nbgc-ink-strong) !important;
  border: 0 !important;
  padding: 0 !important;
  background: none !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text :is(h3, h4, h5, h6):not(.nabgol-diagram-shell *) {
  margin: var(--nbgc-r-1) 0 var(--nbgc-r-025) !important;
  font-size: 1.1em !important;
  font-weight: 700 !important;
  line-height: 1.5 !important;
  color: var(--nbgc-ink-strong) !important;
  border: 0 !important;
  padding: 0 !important;
  background: none !important;
}

/* ---------------------------------------------------------------------------
   6) THE MAGAZINE CLASSES
   chat-markdown.js labels paragraphs by shape: a greeting becomes .eyebrow, a
   short colon-line becomes .section, the closing question becomes .question.
   That inference is right — the prompt in includes/chat_editorial.php asks the
   model for exactly those shapes. What was wrong is that each label also got a
   colour, so one answer carried five different greens.
   Here: hierarchy from WEIGHT, SPACE and — for the section marker — a RULE.
   Colour is spent once, on the closing question, because that is the only
   paragraph asking the reader to act.

   The section rule is the one device this region kept from the three directions
   that were built and compared on 2026-08-30 (see the operating plan). It does
   the job the green label was doing, it can repeat five times in one answer
   without the answer shouting five times, and it costs no colour at all — which
   matters because colour is the thing that breaks first in dark mode.
   --------------------------------------------------------------------------- */

/* Greeting / verdict line: quiet, tight to the paragraph it introduces. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text p.nabgol-mag-eyebrow {
  margin: 0 0 var(--nbgc-r-025) !important;
  font-size: 0.97em !important;
  /* 400, not 600. On Nazli a 600 resolves UP to 700, so this quiet greeting line
     would have come out as the boldest thing near it — the exact defect this
     session was opened to fix, reintroduced by a number nobody re-read after the
     face changed. It is already set apart by being smaller, softer and tight to
     the paragraph below it. */
  font-weight: 400 !important;
  letter-spacing: 0.01em !important;
  color: var(--nbgc-ink-soft) !important;
  background: none !important;
  border: 0 !important;
  padding: 0 !important;
}

/* First paragraph of the message. It is a paragraph, not a headline — a
   four-word "خواهش می‌کنم" used to be typeset as display copy. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text p.nabgol-mag-lead {
  margin: 0 0 var(--nbgc-r-05) !important;
  font-size: 1em !important;
  font-weight: 400 !important;
  line-height: var(--nbgc-lh) !important;
  color: var(--nbgc-ink) !important;
  background: none !important;
  border: 0 !important;
  padding: 0 !important;
}

/* .nabgol-mag-title fires on ANY first paragraph under 52 characters, so it lands
   on "خواهش می‌کنم." just as often as on a real opening hook. It gets a small
   weight lift and nothing else — treating it as a display title is what turned a
   four-word courtesy reply into a headline. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text p.nabgol-mag-title {
  margin: 0 0 var(--nbgc-r-05) !important;
  padding: 0 !important;
  font-size: 1em !important;
  font-weight: 700 !important;   /* two-weight face: 600 painted as 700 anyway */
  line-height: var(--nbgc-lh) !important;
  letter-spacing: 0 !important;
  color: var(--nbgc-ink-strong) !important;
  background: none !important;
  background-image: none !important;
  border: 0 !important;
  border-radius: 0 !important;
}

/* A TITLE WITH NOTHING TO TITLE IS NOT A TITLE.
   The same classifier that gives .nabgol-mag-title its weight lift fires on ANY
   first paragraph under 52 characters — including a message whose entire body
   IS that paragraph. "خواهش می‌کنم. هر وقت سؤال دیگری داشتی همین‌جا بپرس."
   then renders at 600 in ink-strong, which on that screen makes a four-word
   courtesy reply the HEAVIEST text in the whole conversation, louder than the
   answer it is thanking. Seen on the real page 2026-08-30, both shells.

   `:only-child` is the exact test and it needs no :has(): the footer and the
   action row are siblings of .text, not of the paragraph, so a paragraph that is
   the only child of .text is a message with nothing under it to introduce.
   Same reasoning as the conditional lead below — a label is only a label when
   there is something for it to label. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text > p.nabgol-mag-title:only-child {
  font-weight: 400 !important;
  color: var(--nbgc-ink) !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text > p.nabgol-mag-eyebrow:only-child {
  font-weight: 400 !important;
  color: var(--nbgc-ink) !important;
}

/* THE LEAD, AND WHY IT IS CONDITIONAL.
   chat-markdown.js labels the first paragraph of EVERY message .nabgol-mag-lead
   (or .nabgol-mag-title when it is short). That is how "خواهش می‌کنم." became
   display copy in the first magazine pass, and it is why simply re-enabling a
   lead here would repeat that mistake at a larger size.

   A lead only exists inside an ARTICLE. So the rule asks the DOM whether this
   message IS one — does it carry a section label, a step card, a table, a
   diagram, or a display equation? — and only then lets the opening paragraph
   step up. A courtesy reply has none of those and stays a plain sentence. This
   is the "data-driven, not blanket" condition the direction was required to
   satisfy, expressed in the cascade instead of in JavaScript, so it cannot
   drift out of sync with the markup that produces those shapes.

   It lifts .nabgol-mag-lead and NOT .nabgol-mag-title, and that is load-bearing
   rather than fussy: chat-markdown.js already separates the two and they are
   different shapes. A title is a short colon-ended opener ("گام اول: مخرج مشترک
   را پیدا کن.") — a heading, which wants WEIGHT. A lead is the first real body
   paragraph — prose, which wants SIZE and air. The first pass here lifted both,
   and measured on the real scene, four headings silently lost their weight and
   turned into large light sentences.

   :is() wraps the :has() list on purpose: a browser that does not understand
   :has() drops the whole argument instead of throwing the rule away, and the
   lead simply stays a normal paragraph. Degrading to "correct but plain" is the
   only acceptable failure mode for a typographic enhancement. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text:is(
  :has(> p.nabgol-mag-section),
  :has(.nabgol-step),
  :has(table),
  :has(figure.nabgol-diagram),
  :has(.katex-display)
) > p.nabgol-mag-lead:first-child,
body:is(.desktop-chat, .mobile-chat) #messages .message .text:is(
  :has(> p.nabgol-mag-section),
  :has(.nabgol-step),
  :has(table),
  :has(figure.nabgol-diagram),
  :has(.katex-display)
) > p.nabgol-mag-eyebrow:first-child + p.nabgol-mag-lead {
  /* AIR, NOT SIZE — and that is a measurement, not a preference.
     This rule used to set 1.075em. Painted at the desktop scale that is 19.35px
     against the body's 18px, and against the section label's 19.08px: two
     "large" sizes 1.4% apart, which no reader can tell from each other, next to
     a step from the body that no reader can see at all. A size step below ~1.125
     does not read as hierarchy; it reads as an inconsistency, and it cost the
     section label the one thing that made it the loudest object in the answer.
     The lead keeps what actually separates it — a wider gap under it — and the
     file's own thesis holds without exception: hierarchy comes from weight and
     space. Leading follows the region's rhythm for the same reason; 1.85 against
     the body's 1.65 put one paragraph off the 31px grid every other line sits
     on. */
  font-size: 1em !important;
  font-weight: 400 !important;
  line-height: var(--nbgc-lh) !important;
  color: var(--nbgc-ink) !important;
  margin: 0 0 var(--nbgc-r-075) !important;
}

/* Section label ("آنچه می‌دانیم:", "راهنمایی مرحله به مرحله:") — the real
   structure of a NabGol answer. A rule above it, then the label, then a small
   gap so the label visibly belongs to what FOLLOWS it rather than floating
   between two blocks. Distance above (r-15 + the r-05 of padding) is four times
   the distance below: that asymmetry is the whole hierarchy. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text p.nabgol-mag-section,
body:is(.desktop-chat, .mobile-chat) #messages .message .text p.nabgol-mag-kicker,
body:is(.desktop-chat, .mobile-chat) #messages .message .text p.nabgol-mag-pull {
  margin: var(--nbgc-r-15) 0 var(--nbgc-r-025) !important;
  padding: var(--nbgc-r-05) 0 0 !important;
  font-size: 1.06em !important;
  /* ── TWO WEIGHTS, NOT FOUR ────────────────────────────────────────────────
     Nazli has 400 and 700 and nothing between, so every 500 and 600 this file
     used to carry was a number the browser silently rounded (500 down to 400,
     600 up to 700) — a hierarchy that existed only in the stylesheet. Weight is
     now a binary here, and the middle steps are written as what actually paints.

     700 stays on this label, and that is affordable for a reason that had to be
     measured rather than assumed: NAZLI'S BOLD IS LIGHTER THAN THE OLD FACE'S
     REGULAR. Ink coverage at 16.5px — Nazli 700 is 0.0439 against Vazirmatn RD
     400 at 0.0469 (_font_bakeoff_shot.js). So the answer gets lighter overall
     even with every structural label left at full bold, which is why this
     section did not have to trade hierarchy away to fix "everything looks bold".

     The one place 700 is genuinely wrong is the greeting eyebrow, which drops to
     400 and leans on size, space and colour instead — see that rule. Everything
     else that used to sit at 500 is written as 400 and everything at 600 as 700,
     which is what the browser was painting all along; the file no longer names a
     weight this face cannot produce. */
  font-weight: 700 !important;
  line-height: 1.5 !important;
  letter-spacing: 0 !important;
  color: var(--nbgc-ink-strong) !important;
  background: none !important;
  background-image: none !important;
  border: 0 !important;
  border-top: 1px solid var(--nbgc-rule) !important;
  border-radius: 0 !important;
}
/* A rule at the very top of an answer would separate it from the message above,
   not from anything inside it. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text > p.nabgol-mag-section:first-child,
body:is(.desktop-chat, .mobile-chat) #messages .message .text > p.nabgol-mag-kicker:first-child,
body:is(.desktop-chat, .mobile-chat) #messages .message .text > p.nabgol-mag-pull:first-child {
  margin-top: 0 !important;
  padding-top: 0 !important;
  border-top: 0 !important;
}

/* "نکته:" / "توجه:" — a hairline aside, one box only. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text p.nabgol-mag-note {
  margin: var(--nbgc-r-075) 0 !important;
  padding: var(--nbgc-r-05) var(--nbgc-inset) !important;
  font-size: 1em !important;   /* never 0.97em: nested inside a quote the ems
                                  compound and the prose drops under the 16px
                                  floor this platform treats as permanent. */
  font-weight: 400 !important;
  line-height: 1.85 !important;
  color: var(--nbgc-ink) !important;
  background: var(--nbgc-surface) !important;
  border: 0 !important;
  border-inline-start: 2px solid var(--nbgc-hairline-strong) !important;
  border-radius: 0 var(--nbgc-radius-sm) var(--nbgc-radius-sm) 0 !important;
}
html[dir="rtl"] body:is(.desktop-chat, .mobile-chat) #messages .message .text p.nabgol-mag-note {
  border-radius: var(--nbgc-radius-sm) 0 0 var(--nbgc-radius-sm) !important;
}

/* The closing question — "your turn". A NEUTRAL card with an accent edge, never
   a green fill: a green-filled card is what the user's own bubble is, and in the
   first light-mode pass the robot's question and the reader's own message were
   the same object at a glance. One meaning, one surface. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text p.nabgol-mag-question {
  margin: var(--nbgc-r-1) 0 0 !important;
  padding: var(--nbgc-r-05) var(--nbgc-inset) !important;
  font-size: 1em !important;
  font-weight: 400 !important;   /* two-weight face: 500 painted as 400 anyway */
  line-height: 1.8 !important;
  color: var(--nbgc-ink) !important;
  background: var(--nbgc-surface) !important;
  background-image: none !important;
  border: 0 !important;
  border-inline-start: 3px solid var(--nbgc-accent) !important;
  border-radius: var(--nbgc-radius-sm) !important;
}

/* ---------------------------------------------------------------------------
   7) LISTS — no card. A list is prose with markers, and the old rounded panel
   around every <ul> was the third nested box in an answer.
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) #messages .message .text :is(ul, ol):not(.nabgol-diagram-shell *) {
  list-style: none !important;
  margin: var(--nbgc-r-025) 0 var(--nbgc-r-075) !important;
  padding: 0 !important;
  padding-inline-start: 1.55em !important;
  background: none !important;
  background-image: none !important;
  border: 0 !important;
  border-radius: 0 !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text :is(ul, ol) :is(ul, ol):not(.nabgol-diagram-shell *) {
  margin: var(--nbgc-r-025) 0 !important;
  padding-inline-start: 1.25em !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .message .text li:not(.nabgol-diagram-shell *) {
  position: relative !important;
  margin: 0 0 var(--nbgc-r-025) !important;
  padding: 0 !important;
  font-size: 1em !important;
  line-height: var(--nbgc-lh) !important;
  color: var(--nbgc-ink) !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text li:last-child:not(.nabgol-diagram-shell *) {
  margin-bottom: 0 !important;
}

/* Bullet: a small accent dot on the text's first line, no halo.
   ORDER MATTERS in these two blocks. The layers below set the PHYSICAL `right`
   with `!important`, so it has to be reset — but `inset-inline-start` is just an
   alias for `right` under rtl, so writing `right: auto` AFTER it silently threw
   the marker back to offset 0 and painted the dot on top of the first letter
   (seen in the 2026-08-28 shots). Physical resets first, logical last. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text li[data-kind="bullet"]:not(.nabgol-diagram-shell *)::before,
body:is(.desktop-chat, .mobile-chat) #messages .message .text ul > li:not(.nabgol-diagram-shell *)::before {
  right: auto !important;
  left: auto !important;
  content: "" !important;
  position: absolute !important;
  inset-inline-start: -1.05em !important;
  inset-inline-end: auto !important;
  top: calc(0.5 * var(--nbgc-lh) * 1em - 0.19em) !important;
  width: 0.38em !important;
  height: 0.38em !important;
  border-radius: 999px !important;
  background: var(--nbgc-accent) !important;
  box-shadow: none !important;
  display: block !important;
}

/* Ordered: the Persian numeral itself, tabular and quiet — not a filled pill.
   data-n is written by chat-markdown.js; never rely on Latin CSS counters. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text li[data-kind="num"]:not(.nabgol-diagram-shell *)::before,
body:is(.desktop-chat, .mobile-chat) #messages .message .text ol > li:not(.nabgol-diagram-shell *)::before,
body:is(.desktop-chat, .mobile-chat) #messages .message .text li:not([data-kind]):not(.nabgol-diagram-shell *)::before {
  right: auto !important;
  left: auto !important;
  content: attr(data-n) !important;
  position: absolute !important;
  inset-inline-start: -1.55em !important;
  inset-inline-end: auto !important;
  top: 0 !important;
  width: 1.3em !important;
  height: auto !important;
  display: block !important;
  text-align: center !important;
  font-size: 0.94em !important;
  font-weight: 700 !important;
  line-height: var(--nbgc-lh) !important;
  color: var(--nbgc-accent-ink) !important;
  background: none !important;
  border-radius: 0 !important;
  font-variant-numeric: tabular-nums;
}

/* ::marker still carried a mint colour from the layer below; the markers here
   are ::before, so the real one must stay invisible rather than double up. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text li:not(.nabgol-diagram-shell *)::marker {
  content: "" !important;
  color: transparent !important;
}

/* ---------------------------------------------------------------------------
   8) QUOTES, RULES, CODE
   --------------------------------------------------------------------------- */
/* AN INDENT, NOT A BOX, AND NOT A COLOURED BAR EITHER. A quote in a tutoring
   answer is almost always the model's own aside, and it already sits inside an
   answer that has a table, a step card and a closing card competing for edges.
   Indenting it and dropping the ink one step says "quieter, still mine" with no
   new object on the page — and it takes the accent bar off a paragraph that was
   never asking the reader to act, which is what leaves the accent free to mean
   exactly one thing (see the closing question). */
body:is(.desktop-chat, .mobile-chat) #messages .message .text blockquote:not(.nabgol-diagram-shell *) {
  margin: var(--nbgc-r-075) 0 !important;
  padding: 0 !important;
  padding-inline-start: var(--nbgc-indent) !important;
  border: 0 !important;
  background: none !important;
  background-image: none !important;
  border-radius: 0 !important;
  color: var(--nbgc-ink-soft) !important;
  font-size: 1em !important;
  font-weight: 400 !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text blockquote p {
  margin-bottom: var(--nbgc-r-025) !important;
  color: inherit !important;
  font-weight: 400 !important;
}
/* A markdown quote that starts with "نکته:" gets BOTH the quote rule and the
   .nabgol-mag-note rule, i.e. a card inside a bar. Inside a quote the note keeps
   only the type, never a second box. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text blockquote p.nabgol-mag-note,
body:is(.desktop-chat, .mobile-chat) #messages .message .text blockquote p.nabgol-mag-question,
body:is(.desktop-chat, .mobile-chat) #messages .message .text blockquote p.nabgol-mag-section {
  margin: 0 0 var(--nbgc-r-025) !important;
  padding: 0 !important;
  background: none !important;
  border: 0 !important;
  border-radius: 0 !important;
  font-weight: 400 !important;
  color: inherit !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text blockquote p:last-child {
  margin-bottom: 0 !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text blockquote p:last-child {
  margin-bottom: 0 !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .message .text hr {
  border: 0 !important;
  height: 1px !important;
  margin: var(--nbgc-r-1) 0 !important;
  background: var(--nbgc-hairline) !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .message .text :not(pre) > code {
  font-family: var(--nbgc-mono) !important;
  font-size: 0.9em !important;
  padding: 0.1em 0.4em !important;
  border-radius: 6px !important;
  border: 0 !important;
  background: var(--nbgc-surface-2) !important;
  color: var(--nbgc-ink-strong) !important;
  direction: ltr;
  unicode-bidi: isolate;
}

body:is(.desktop-chat, .mobile-chat) #messages .message .text pre {
  margin: var(--nbgc-r-075) 0 !important;
  border-radius: var(--nbgc-radius-sm) !important;
  border: 1px solid var(--nbgc-hairline) !important;
  background: var(--nbgc-surface) !important;
  overflow-x: auto !important;
  max-width: 100% !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text pre code {
  font-family: var(--nbgc-mono) !important;
  font-size: 13.5px !important;
  line-height: 1.75 !important;
  direction: ltr !important;
  text-align: left !important;
  background: none !important;
  padding: var(--nbgc-r-05) var(--nbgc-inset-sm) !important;
  display: block !important;
}

/* ---------------------------------------------------------------------------
   9) MATH (KaTeX renders on the live DOM, after DOMPurify — see script.js)
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) #messages .message .text .katex {
  font-size: 1.06em !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text .katex-display {
  margin: var(--nbgc-r-075) 0 !important;
  padding: var(--nbgc-r-025) 0 !important;
  overflow-x: auto !important;
  overflow-y: hidden !important;
  direction: ltr !important;
  text-align: center !important;
}
/* chat-flagship-polish.css puts `text-align: left !important` on `.katex` so that
   inline math keeps its LTR run. On a display equation that inner rule wins over
   the centered block, so `$$S = 12 \times 7 = 84$$` sat flush against the left
   edge of the column instead of centred under the sentence it answers. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text .katex-display > .katex {
  font-size: 1.12em !important;
  display: block !important;
  text-align: center !important;
}

/* ---------------------------------------------------------------------------
   10) TABLES — hairline rows, no vertical rules, numbers on a tabular grid.
   The scroller and the expand button come from chat-markdown.js; only the skin
   is set here, and the button is pulled out of the first header cell it used to
   sit on top of.
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) #messages .message .text .nabgol-table-scroll {
  margin: var(--nbgc-r-075) 0 !important;
  border: 1px solid var(--nbgc-hairline) !important;
  border-radius: var(--nbgc-radius-sm) !important;
  background: none !important;
  overflow-x: auto !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text table {
  width: 100% !important;
  border-collapse: collapse !important;
  border: 0 !important;
  font-size: 0.96em !important;
  background: none !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text :is(th, td) {
  padding: var(--nbgc-r-025) var(--nbgc-inset-sm) !important;
  border: 0 !important;
  border-bottom: 1px solid var(--nbgc-hairline) !important;
  text-align: start !important;
  line-height: 1.75 !important;
  color: var(--nbgc-ink) !important;
  background: none !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text th {
  font-weight: 700 !important;
  font-size: 0.92em !important;
  letter-spacing: 0.01em !important;
  color: var(--nbgc-ink-soft) !important;
  background: var(--nbgc-surface) !important;
  border-bottom: 1px solid var(--nbgc-hairline-strong) !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text tr:last-child td {
  border-bottom: 0 !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text td.nabgol-table-num,
body:is(.desktop-chat, .mobile-chat) #messages .message .text th.nabgol-table-num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text .nabgol-media-expand-btn {
  inset-block-start: 6px !important;
  inset-inline-end: 6px !important;
  background: var(--nbgc-surface-2) !important;
  border: 1px solid var(--nbgc-hairline) !important;
  color: var(--nbgc-ink-soft) !important;
  box-shadow: none !important;
}

/* ---------------------------------------------------------------------------
   11) MEDIA + DIAGRAMS
   Diagram internals belong to docs/CHAT_GRAPHICS_OPERATING_PLAN.md and its own
   gate — this file only gives the figure consistent outer spacing so it sits in
   the same rhythm as the prose around it.
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) #messages .message .text figure.nabgol-diagram {
  margin: var(--nbgc-r-1) 0 !important;
  max-width: 100% !important;
}
/* `:not(.katex *)` is load-bearing, and this is the bug it fixes.
   KaTeX does not draw a radical with a glyph. It draws the hook, the overbar,
   and every stretchy construction — \overline, \underline, \widehat,
   \xrightarrow, \overbrace, and the big delimiters — as an inline <svg> that
   katex.min.css sizes with `height: inherit`, taking the height its wrapper
   span computed. A blanket `height: auto` here overrode that, the SVG collapsed
   to zero, and the radical sign SILENTLY DISAPPEARED while the numbers under it
   stayed. On the owner's own screen the chat printed "16/25 = 16/25 = 4/5"
   where the full-screen view (which lives outside #messages, and so escaped
   this rule) printed the correct one. Verified restored in all four modes on
   the real page 2026-08-30, with a radical kept permanently in the lab scene
   and in _chat_real_shot.js so it can never regress unseen.

   The reset itself still matters for everything else: an <img> or a diagram
   <svg> arriving with only a width attribute would otherwise stretch. So the
   fix excludes KaTeX's internals rather than dropping the rule. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text :is(img, video, svg):not(.katex *) {
  max-width: 100% !important;
  height: auto !important;
}

/* AND THE SECOND HALF OF THE SAME BUG, which the first half hid.
   Excluding KaTeX from THIS file's reset was necessary and not sufficient:
   style-mobile.css:319 carries its own
     body.mobile-chat .message .text svg:not(.nabgol-diagram-svg) { height: auto !important }
   so on a phone — the shell almost every student actually uses — the radical
   stayed collapsed after the fix above. Measured, not guessed: the matched-rule
   walk over the real cascade reported the svg's height coming from
   style-mobile.css at 0.25px against a 45.83px .hide-tail parent, while the same
   query on the desktop shell already reported 47.22px.

   The repair belongs here rather than in the shell file: this region owns its
   own appearance, this sheet loads last in both templates, and restoring the
   vendor's own declared value is the narrowest possible correction. It is
   deliberately NOT folded into the rule above — that rule is about what this
   file resets, this one is about what a lower layer breaks. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text .katex svg {
  height: inherit !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .attachments,
body:is(.desktop-chat, .mobile-chat) #messages .message .attachment {
  border-radius: var(--nbgc-radius-sm) !important;
  overflow: hidden !important;
}

/* ---------------------------------------------------------------------------
   12) FOOTER — actions under the text's own edge, timestamp whisper-quiet.
   Nothing is removed from the DOM and nothing becomes unreachable: the actions
   stay visible on touch (where there is no hover) and only settle back to a
   quieter opacity on a pointer device.
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) #messages .message .footer {
  display: flex !important;
  flex-direction: row !important;
  direction: rtl !important;      /* style-desktop.css:762 forces ltr, which put
                                     the action row at the far LEFT of a 740px
                                     column — 750px from the text it acts on. */
  align-items: center !important;
  justify-content: flex-start !important;
  gap: 10px !important;
  margin: var(--nbgc-r-025) 0 0 !important;
  padding: 0 !important;
  border: 0 !important;
  border-top: 0 !important;
  background: none !important;
  min-height: 30px !important;
}
/* The GROUP moves to the inline start; the chips inside keep their own ltr axis
   and their `order` values, which tools/test_chat_chrome_flagship.php gates
   (action_dom_order / action_css_order_and_transparent_regen). */
body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions {
  order: -1 !important;
  direction: ltr !important;
  display: flex !important;
  align-items: center !important;
  justify-content: flex-end !important;
  gap: 2px !important;
  margin: 0 !important;
  padding: 0 !important;
  background: none !important;
  border: 0 !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .footer-meta {
  /* ORDER 0, AND IT STAYS THERE. A pass on 2026-09-02 moved this group ahead of
     the action chips, reasoning that a timestamp is part of the record and the
     chips are chrome. The owner looked at it and rejected it: the footer's
     furniture does not move. The only thing that changes here is the order of the
     two spans INSIDE this group. */
  order: 0 !important;
  /* THE TIMESTAMP RUNS LEFT-TO-RIGHT INSIDE ITSELF, AND THAT IS THE OWNER'S CALL.
     The GROUP still sits at the row's inline start — the right edge, next to the
     answer's own first word. Only its two spans read the other way: date on the
     left, time on its right, so scanning the numeral run left-to-right gives the
     big unit first. That matches how the run is actually read: digits are an LTR
     island in a Persian line no matter what direction the box declares, which is
     the same fact that made the un-separated version read time-first.
     `direction`, not `order` or `row-reverse`: the separator below is built from
     LOGICAL properties, so it follows this flip on its own and stays between the
     two groups. */
  direction: ltr !important;
  display: flex !important;
  align-items: center !important;
  gap: var(--nbgc-r-025) !important;
  margin: 0 !important;
  font-size: 12.5px !important;
  line-height: 1.4 !important;
  font-weight: 400 !important;   /* two-weight face: 500 painted as 400 anyway */
  /* ── ink-SOFT, not ink-faint, and opacity CLAIMED ──────────────────────────
     Measured on the real page 2026-09-02, all four theme/shell combinations:
     the timestamp painted between 2.21:1 and 2.63:1. The project's own floor for
     a label is 4.5:1 (ROUTER, «هیچ کنترلی از دسترس خارج نمی‌شود»), so the most
     repeated text in the product was the one line that failed it — twice per
     message, on every message.

     TWO causes, and neither was visible in this file:
     1. --nbgc-ink-faint is not what its comment claimed. #78838f measures
        3.86:1 on white, not 4.5:1, and 3.22:1 on the user bubble's tint.
     2. This rule set a colour but never claimed `opacity`, so three other sheets
        did: chat-flagship-polish.css .62, style-mobile.css .75 and
        chat-mobile-flagship.css .72 !important. The winner multiplied every
        carefully chosen ink by 0.72 — the file's own note two rules below says
        "one opacity, set on the parent", and then set it on the CHILDREN only,
        which is why the reset never reached the number that was doing the harm.

     --nbgc-ink-soft is the token whose own definition already says "meta,
     captions, quiet chrome", and it measures 4.89-5.85:1 (light) and 5.31-7.37:1
     (dark) across canvas AND user tint, phone AND desktop. So the fix needs no
     new token: use the one that was designed for this, at the opacity this file
     says it wants. */
  color: var(--nbgc-ink-soft) !important;
  opacity: 1 !important;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
  letter-spacing: 0.01em !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .footer-meta .msg-date,
body:is(.desktop-chat, .mobile-chat) #messages .message .footer-meta .msg-time {
  color: inherit !important;
  font-size: inherit !important;
  font-weight: inherit !important;
  line-height: inherit !important;
  background: none !important;
  padding: 0 !important;
  border: 0 !important;
  /* style-desktop.css:764-765 gave the date .72 and the time .58, so the two
     halves of one timestamp faded differently. One opacity, set on the parent. */
  opacity: 1 !important;
  /* EACH NUMERAL GROUP IS ITS OWN BIDI ISLAND. Two adjacent digit runs in an RTL
     line are two left-to-right islands with nothing between them, and the
     bidi algorithm is free to merge them: that is how a conversation title
     rendered as «۱۱۴۰۵/۰۶/۱۱» — a list number welded to a year. `isolate`
     makes the boundary a rule of the layout instead of an accident of the
     content.
     And the DIRECTION is claimed here, on the spans, not inherited from the
     group: measured 2026-09-02, with the group set to ltr these two still
     computed rtl, which put the separator on the wrong side of the digits.
     style-desktop.css:819 also forces direction: rtl !important on exactly this
     pair. Whatever wins, the region says what it wants — the same lesson this
     file already had to learn about opacity two blocks up. */
  direction: ltr !important;
  unicode-bidi: isolate !important;
}
/* THE ORDER WAS RIGHT AND STILL READ WRONG.
   The date IS the first thing on the line (measured on the live page: date
   325..378, time 289..318 on a phone — right to left, exactly as the owner
   wants). But a reader does not scan «۱۱:۱۲ ۱۴۰۵/۶/۱۱» right-to-left: digits
   are a left-to-right island, both groups are set in the same tabular figures,
   and 7px of flex gap is the only thing between them. So the eye takes the whole
   run left-to-right and reads the TIME first. The fix is a visible boundary, not
   a different DOM order.

   style-desktop.css:766 already tried this with a "·" — and used
   `margin-right`, a PHYSICAL margin, which in an RTL row pushed the dot to the
   far edge of the line. That is why the separator was removed here rather than
   repaired. Logical margins make it land where it belongs: `::before` sits at
   the span's inline START, which under rtl is its right side, i.e. between the
   date and the time. `margin-inline-end` then opens the air on the dot's left,
   and the row's own 7px gap is the air on its right — symmetrical, and it
   cannot drift when the direction changes. */
body:is(.desktop-chat, .mobile-chat) #messages .message .footer-meta .msg-time::before {
  content: "" !important;
  display: none !important;
}
/* THE DOT HANGS OFF THE DATE, AND CARRIES ITS OWN ISOLATION.
   First attempt put it on .msg-time::before — the time span's inline START,
   which should be its left side now that the group runs ltr. Measured: it
   painted on the RIGHT of the digits instead, because the span's content is a
   neutral character next to a run of Arabic-Indic digits and the bidi algorithm
   is free to resolve that neutral either way. A separator whose side depends on
   what happens to sit beside it is not a separator.
   So: ::after on the DATE (its inline end under ltr = its right side, i.e.
   between the two groups), and `unicode-bidi: isolate` on the pseudo-element
   itself so the dot is its own run and no neighbouring digit can drag it. Air on
   both sides comes from one token — the margin here and the row gap below — so
   the dot is optically centred between the two numbers. */
body:is(.desktop-chat, .mobile-chat) #messages .message .footer-meta .msg-date::after {
  content: "·" !important;
  display: inline !important;
  margin: 0 !important;
  margin-inline-start: var(--nbgc-r-025) !important;
  direction: ltr !important;
  unicode-bidi: isolate !important;
  opacity: 0.5;
  font-variant-numeric: normal;
}

/* Ghost icon buttons. Border + fill on five chips under every message was the
   noisiest chrome in the canvas; the shape now appears on hover/press only. */
body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions > .action-chip,
body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions > button {
  width: 32px !important;
  height: 32px !important;
  min-width: 32px !important;
  min-height: 32px !important;
  max-width: 32px !important;
  padding: 0 !important;
  margin: 0 !important;
  border: 0 !important;
  border-radius: 9px !important;
  background: none !important;
  background-image: none !important;
  box-shadow: none !important;
  color: var(--nbgc-ink-soft) !important;
  opacity: 0.75;
  transition: background-color 0.16s ease, color 0.16s ease, opacity 0.16s ease;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions > .action-chip .ico,
body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions > button .ico {
  width: 16px !important;
  height: 16px !important;
  stroke-width: 1.9 !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions > .action-chip:hover,
body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions > .action-chip:focus-visible {
  background: var(--nbgc-surface-2) !important;
  color: var(--nbgc-ink-strong) !important;
  opacity: 1;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions > .action-chip:active {
  background: var(--nbgc-surface-2) !important;
  transform: scale(0.94);
}

/* ── TOUCH: THE TARGET IS BIGGER THAN THE ICON ──────────────────────────────
   Measured on the real page 2026-09-02, 390px phone: seven 32x32 chips at a 2px
   gap under every answer — a 34px pitch — with «حذف» sitting 2px from «کپی».
   This product enforces a 44px tap floor everywhere else it has one
   (test_controls_always_reachable.js, and the header and composer that gate
   points at), and the row a student touches most often was the one row nobody
   measured.

   40, not 44, and the reason is arithmetic rather than taste: seven controls at
   44 is 308px before a single gap, against 334px of reading column, which
   leaves 26px for six gaps — targets that meet the floor individually and
   overlap each other, which is worse than a smaller clean target. 40x40 with a
   4px gap is 304px: +56% target area and double the separation from the
   destructive action, with the icon itself untouched at 16px. Getting the rest
   of the way to 44 means fewer visible actions on a phone, and how many a phone
   shows is a product decision, not a stylesheet's. */
@media (pointer: coarse) {
  /* Seven 40px targets plus the timestamp do not fit one 334px line, so the
     line is allowed to break rather than to squeeze: the timestamp drops under
     the row instead of the row losing its gaps back. Nothing is hidden and
     nothing shrinks — the only cost is height, on the axis a phone can scroll. */
  body:is(.desktop-chat, .mobile-chat) #messages .message .footer {
    flex-wrap: wrap !important;
    row-gap: var(--nbgc-r-025) !important;
  }
  body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions {
    gap: 4px !important;
  }
  body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions > .action-chip,
  body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions > button {
    width: 40px !important;
    height: 40px !important;
    min-width: 40px !important;
    min-height: 40px !important;
    max-width: 40px !important;
    border-radius: 11px !important;
  }
}
body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions > .action-chip.copied {
  color: var(--nbgc-accent-ink) !important;
  opacity: 1;
}

/* Pointer devices: the row rests at half strength and comes up on hover. Touch
   devices keep it at full strength, because there is no hover to reveal it. */
@media (hover: hover) and (pointer: fine) {
  body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions {
    opacity: 0.55;
    transition: opacity 0.18s ease;
  }
  body:is(.desktop-chat, .mobile-chat) #messages .message:hover .message-actions,
  body:is(.desktop-chat, .mobile-chat) #messages .message:focus-within .message-actions {
    opacity: 1;
  }
  /* The TIMESTAMP does not fade. An icon row that comes up on hover is a
     progressive disclosure; a line of text that rests below its contrast floor
     is not — it is unreadable text with a hover-only remedy, and a hover is not
     a remedy on the device most of these students hold. The ink is already the
     quiet one (--nbgc-ink-soft); it is quiet by colour, at 4.9:1, not by
     dimming. */
}

/* The user's actions live on a tinted card — same ghost treatment, tuned ink. */
body:is(.desktop-chat, .mobile-chat) #messages .message.user .message-actions > .action-chip {
  color: var(--nbgc-ink-soft) !important;
  background: none !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message.user .message-actions > .action-chip:hover {
  background: var(--nbgc-surface-2) !important;
  color: var(--nbgc-user-ink) !important;
}

/* A UI-only assistant turn (buttons, no prose) has nothing to copy or read
   aloud, so it should not print an empty footer under itself. */
body:is(.desktop-chat, .mobile-chat) #messages .message.assistant .message-bubble.ui-only > .footer {
  margin-top: var(--nbgc-avatar-nudge) !important;
}

/* ---------------------------------------------------------------------------
   13) VERSION SWITCH + INLINE EDIT — same hairline language as everything else.
   --------------------------------------------------------------------------- */
/* `:not([hidden])` is load-bearing. appendMessage() renders .version-controls and
   .edit-area on EVERY message and relies on `hidden` / a display rule to keep
   them out of sight; a bare `display: flex !important` here made "قبلی / بعدی"
   appear under every single bubble (caught on the real page, not in the lab). */
body:is(.desktop-chat, .mobile-chat) #messages .message .version-controls:not([hidden]) {
  display: flex !important;
  align-items: center !important;
  gap: 6px !important;
  margin: var(--nbgc-r-025) 0 0 !important;
  padding: 0 !important;
  background: none !important;
  border: 0 !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .version-controls button {
  height: 28px !important;
  padding: 0 var(--nbgc-inset-sm) !important;
  border-radius: 8px !important;
  border: 1px solid var(--nbgc-hairline) !important;
  background: none !important;
  color: var(--nbgc-ink-soft) !important;
  font-family: inherit !important;
  font-size: 12px !important;
  font-weight: 700 !important;   /* two-weight face: 600 painted as 700 anyway */
}
body:is(.desktop-chat, .mobile-chat) #messages .message .version-controls button:hover {
  background: var(--nbgc-surface-2) !important;
  color: var(--nbgc-ink-strong) !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .version-controls .version-badge {
  font-size: 12px !important;
  color: var(--nbgc-ink-faint) !important;
  font-variant-numeric: tabular-nums;
}

/* Belt and braces for every element the app hides with the attribute. */
body:is(.desktop-chat, .mobile-chat) #messages .message [hidden] {
  display: none !important;
}

body:is(.desktop-chat, .mobile-chat) #messages .message .edit-area textarea {
  font-family: inherit !important;
  font-size: var(--nbgc-prose) !important;
  line-height: 1.8 !important;
  color: var(--nbgc-ink) !important;
  background: var(--nbgc-surface) !important;
  border: 1px solid var(--nbgc-hairline-strong) !important;
  border-radius: var(--nbgc-radius-sm) !important;
  padding: var(--nbgc-r-025) var(--nbgc-inset-sm) !important;
}

/* ---------------------------------------------------------------------------
   14) MOTION
   --------------------------------------------------------------------------- */
/* This block used to read `#messages, #messages *`, and that universal
   descendant was a wall violation hiding in plain sight: it matches the
   thinking ritual (#typing, the plant, its signal rings) that this file is
   explicitly forbidden to touch, and it does so in a form no selector-text gate
   catches — the gate looks for `#typing|.typing-indicator|.nabgol-think`, and
   `*` names none of them. Reduced motion still has to be honoured, so the reset
   now names exactly the two things THIS file animates (the arrival of the newest
   message and the streaming caret) plus the region's own smooth scrolling. Any
   other animation inside the region belongs to the file that declared it, and
   that file gets to decide how it behaves under reduced motion. */
@media (prefers-reduced-motion: reduce) {
  body:is(.desktop-chat, .mobile-chat) #messages {
    scroll-behavior: auto !important;
  }
  body:is(.desktop-chat, .mobile-chat) #messages > .message,
  body:is(.desktop-chat, .mobile-chat) #messages .message .text > p:last-child::after,
  body:is(.desktop-chat, .mobile-chat) #messages .message .text > li:last-child::after,
  body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions,
  body:is(.desktop-chat, .mobile-chat) #messages .message .message-actions > .action-chip,
  body:is(.desktop-chat, .mobile-chat) #messages .message .footer-meta {
    transition: none !important;
    animation: none !important;
  }
}

/* ---------------------------------------------------------------------------
   15) THE EMPTY STATE — the first thing anyone sees in the middle region.
   Structure and chip hierarchy belong to includes/starter_chips.php and its gate
   (tools/test_action_buttons_gate.php); only the type and the card are set here.
   The subtitle was 13px, which is the robot's own one-line description — the
   sentence that tells a parent what this tutor is for, set smaller than the
   timestamps. The card also stops being an opaque slab: on the canvas a hairline
   is enough to hold the group together.
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) #messages .empty {
  border: 1px solid var(--nbgc-hairline) !important;
  border-radius: 20px !important;
  background: var(--nbgc-surface) !important;
  box-shadow: none !important;
}
body.mobile-chat #messages .empty {
  padding: var(--nbgc-r-1) var(--nbgc-inset) !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .empty .title {
  font-size: 20px !important;
  font-weight: 700 !important;
  line-height: 1.6 !important;
  letter-spacing: 0 !important;
  color: var(--nbgc-ink-strong) !important;
  margin: 0 0 var(--nbgc-r-025) !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .empty .subtitle {
  font-size: 15.5px !important;
  font-weight: 400 !important;
  line-height: 1.85 !important;
  color: var(--nbgc-ink-soft) !important;
  max-width: 34em !important;
  margin: 0 auto var(--nbgc-r-05) !important;
  text-wrap: pretty;
  /* chat-mobile-flagship.css:206 clamps this to 2 lines at 0.8rem and 0.62 opacity.
     It is the robot's own one-sentence description — the only text that tells a
     parent what this tutor does — and it was being cut off mid-word with "…".
     Nothing else in the empty state competes for the space. */
  display: block !important;
  -webkit-line-clamp: none !important;
  overflow: visible !important;
  opacity: 1 !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .empty .big-icon {
  border-radius: 16px !important;
  box-shadow: none !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .empty .nabgol-free-intro-note {
  font-size: 13px !important;
  line-height: 1.8 !important;
  color: var(--nbgc-ink-faint) !important;
  margin-top: var(--nbgc-r-025) !important;
}
/* Chips: readable label, comfortable target. Colour, order and the
   mission/hero hierarchy stay exactly where their own gate put them. */
body:is(.desktop-chat, .mobile-chat) #messages .empty .suggestion-chip {
  font-size: 14.5px !important;
  line-height: 1.6 !important;
  min-height: 42px !important;
}

/* ---------------------------------------------------------------------------
   16) THE JUMP-TO-LATEST BUTTON floats over the canvas, so it belongs to the
   canvas language even though it lives outside #messages.
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) .chat-main > .scroll-bottom-btn {
  border: 1px solid var(--nbgc-hairline-strong) !important;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.22) !important;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
/* …including its PAINT, which this block used to leave to style-desktop.css:467
   — `rgba(30,30,36,0.8)` with `#e4e4e7` ink and no light variant. Measured on
   the real page 2026-09-02, desktop light: a near-black disc floating in the
   middle of a #f6f8fa canvas, the single darkest object on the screen, sitting
   over the action row of whatever message it happens to cover. That is the
   accent-budget rule («chrome هرگز از محتوا بلندتر حرف نمی‌زند») broken by a
   control that is not even an accent. Same lifted-surface language as every
   other floating control in this region, in both themes; geometry, z-index and
   show/hide behaviour are untouched — they belong to the section that fixed
   them (2026-08-31). */
body.desktop-chat .chat-main > .scroll-bottom-btn {
  background: var(--nbgc-header-lifted) !important;
  color: var(--nbgc-ink) !important;
}
body.desktop-chat .chat-main > .scroll-bottom-btn:hover {
  background: var(--nbgc-surface-raised) !important;
  color: var(--nbgc-ink-strong) !important;
}

/* ---------------------------------------------------------------------------
   17) STEP CARDS (آکاردئون قدم) — theme-aware, and part of the canvas language.
   chat-flagship-polish.css:860 hard-codes rgba(30,41,59,.95) with #f8fafc text
   and has no light variant, so on a white page the steps rendered as three black
   slabs in the middle of the answer (measured on the real page 2026-08-28). The
   card structure and the feature's behaviour belong to
   docs/EDTECH_FEATURES_WALL.md and its gate; only the skin is set here.
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) #messages .message .text .nabgol-step {
  margin: 0 0 var(--nbgc-r-025) !important;
  border-radius: var(--nbgc-radius-sm) !important;
  background: var(--nbgc-surface) !important;
  border: 1px solid var(--nbgc-hairline) !important;
  overflow: hidden !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text .nabgol-step > summary {
  padding: var(--nbgc-r-025) var(--nbgc-inset) !important;
  gap: 10px !important;
  color: var(--nbgc-ink-strong) !important;
  font-size: 1em !important;
  font-weight: 700 !important;   /* two-weight face: 600 painted as 700 anyway */
  line-height: 1.7 !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text .nabgol-step > summary:hover {
  background: var(--nbgc-surface-2) !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text .nabgol-step > summary::after {
  border-right-color: var(--nbgc-ink-soft) !important;
  border-bottom-color: var(--nbgc-ink-soft) !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text .nabgol-step-n {
  width: 1.55em !important;
  height: 1.55em !important;
  background: var(--nbgc-accent-wash) !important;
  border: 1px solid var(--nbgc-accent-edge) !important;
  color: var(--nbgc-accent-ink) !important;
  font-size: 0.86em !important;
  font-weight: 700 !important;
  font-variant-numeric: tabular-nums;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text .nabgol-step-body {
  padding: 0 var(--nbgc-inset) var(--nbgc-r-025) !important;
  color: var(--nbgc-ink) !important;
  font-size: 1em !important;
}
/* پاسخ نهایی — the destination card. An accent edge, not a filled green box. */
body:is(.desktop-chat, .mobile-chat) #messages .message .text .nabgol-step-final {
  border: 1px solid var(--nbgc-accent-edge) !important;
  border-inline-start: 3px solid var(--nbgc-accent) !important;
  background: var(--nbgc-accent-wash) !important;
}
body:is(.desktop-chat, .mobile-chat) #messages .message .text .nabgol-step-final > summary {
  color: var(--nbgc-ink-strong) !important;
}

/* ---------------------------------------------------------------------------
   18) ARRIVAL — the one piece of motion this region owns.

   Scope note, deliberately narrow: the thinking ritual (#typing, the plant with
   its signal rings and its video) is NOT touched here. It was expensive to build
   and it stays exactly as it is; nothing in this section selects it.

   Only the NEWEST message animates — `:last-child`, not every `.message`. That
   is what keeps a history restore cheap: the app appends the whole stored
   transcript inside one synchronous loop, so the intermediate messages never
   survive a painted frame as :last-child and never start an animation at all.
   When the next message arrives the previous one simply loses the selector, and
   because the animation's end state IS the element's natural state, dropping it
   causes no jump.

   Composited properties only (opacity + transform): neither triggers layout, so
   this cannot add a layout shift — asserted by test_chat_canvas_perf_gate.js,
   which measures CLS across a 100-message build. No `will-change`: it would pin
   a compositor layer per message for a 260ms effect.
   --------------------------------------------------------------------------- */
@keyframes nbgc-arrive {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* style-common.css:292 puts `animation: fadeIn 0.25s` on EVERY .message. Measured
   2026-08-29 on the real page: opening a 100-message conversation started 100
   concurrent fade+translate animations in one frame. Same visual result for the
   one message a human is looking at, a hundredth of the work. */
body:is(.desktop-chat, .mobile-chat) #messages > .message {
  animation: none !important;
}

/* `:last-child` does NOT work here and looked like it did: #messages ends with
   the thinking indicator, so the newest message is never the last CHILD.
   `:nth-last-child(1 of .message)` is the precise tool — "last element matching
   .message" — and it lives inside `:is()` so a browser that does not understand
   `of S` ignores that argument instead of throwing the whole rule away. */
body:is(.desktop-chat, .mobile-chat) #messages > .message:is(:last-child, :nth-last-child(1 of .message)) {
  animation: nbgc-arrive 0.26s cubic-bezier(0.22, 1, 0.36, 1) !important;
}

/* The caret says "still writing" INSIDE the answer, where the eye already is.
   `.is-streaming-reply` is set by script.js on the message being streamed and
   removed when it finishes, so this needs no JS of its own. It rides the last
   paragraph rather than the .text block, so it sits at the end of the sentence
   (the LEFT edge in RTL) instead of alone on a new line. A message that ends in
   a table or a diagram simply gets no caret — better than a bar floating under
   a card. */
body:is(.desktop-chat, .mobile-chat) #messages .message.is-streaming-reply .text > p:last-child::after,
body:is(.desktop-chat, .mobile-chat) #messages .message.is-streaming-reply .text > li:last-child::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: 1.05em;
  margin-inline-start: 0.18em;
  vertical-align: -0.16em;
  border-radius: 1px;
  background: var(--nbgc-accent);
  animation: nbgc-caret 1s steps(2, start) infinite;
}
@keyframes nbgc-caret { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }

/* ---------------------------------------------------------------------------
   9) COST CEILING — a long transcript must cost what a short one costs
   ----------------------------------------------------------------------------
   Measured on 2026-09-02 with live/tools/_chat_perf_probe.js (real page, real
   phone metrics, real CPU throttling — not by reading code):

     Every message the conversation has ever held sits in the DOM, because the
     message-record law says a message is a record and restore replays the whole
     transcript. That is right, and it is not the bug. The bug is that NOTHING
     told the browser those messages could be skipped, so every single layout
     pass walked all of them. Typing one letter runs a layout pass — the composer
     autosize sets height:auto and reads scrollHeight — so on a long history the
     cost of one keystroke grew with the number of messages ever exchanged.
     That is the freeze the user reported, and it gets worse forever.

   `content-visibility: auto` is the fix that has no ceiling: the browser skips
   layout, style and paint for any message far enough off-screen, so the cost of
   a keystroke is the cost of what is ON SCREEN — the same at 20 messages and at
   20,000. No windowing, no recycling, no message removed from the DOM: history
   parity, print, in-page search and the record law are all untouched.

   `contain-intrinsic-size: auto <len>` makes the placeholder remember the real
   height each message had the last time it was rendered, so the scrollbar and
   the scroll position never jump. The literal length is only ever used for a
   message that has not been laid out even once — and there is none, because
   script.js only sets data-nbg-skippable AFTER the enhance chain has painted
   and measured that message. That ordering is the whole safety argument:
   KaTeX, hljs and above all the Mermaid polish (which reads real
   getBoundingClientRect values) always run while the message is fully rendered.
   --------------------------------------------------------------------------- */
body:is(.desktop-chat, .mobile-chat) #messages .message[data-nbg-skippable="1"] {
  content-visibility: auto;
  contain-intrinsic-size: auto 260px;
}

/* Printing and "save as PDF" must contain the WHOLE conversation, never just
   the part that happened to be on screen. Chrome renders skipped subtrees when
   printing, but a browser that only half-implements that would silently print
   blank boxes — and a blank page is the kind of defect nobody notices until a
   parent has it in their hand. Turning the property off for print costs one
   line and removes the question. */
@media print {
  body:is(.desktop-chat, .mobile-chat) #messages .message[data-nbg-skippable="1"] {
    content-visibility: visible;
    contain-intrinsic-size: none;
  }
}

/* The kill switch, and the way anything that must see the WHOLE transcript asks
   for it. Same specificity as the rule above and written after it, so it wins.
   Used by nabgolRenderAllMessages() in script.js — history-parity digests, and
   any future measurement that needs every turn laid out at once. */
body.nbg-render-all #messages .message[data-nbg-skippable="1"] {
  content-visibility: visible;
  contain-intrinsic-size: none;
}
