/* ==========================================================================
   Chat Page — Landing / Chat-specific styles
   ========================================================================== */

/* --- Page Layout (flex column, centered) --- */
.chat-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: calc(100vh - 48px);
  /* Pixel-perfect pass (28 Sep): the canvas's page ground. On `.chat-page`, not
     on `body` -- test_browser_design holds the body to `--neutral-100`. */
  background: var(--sim-page);
}

/* The signed-out brand bar lives inside .chat-page, which centres its children,
   so without this it shrinks to the width of its contents while the same bar
   for signed-in users — rendered outside the wrapper — spans the page. Same
   component, two widths, for no reason a reader could see. */
.chat-page > .site-nav {
  align-self: stretch;
}

/* --- Header --- */
/* Bug #820: the second copy of the cap. This header sits directly above
   `.main-layout` and is centred by the same mechanism, so the two line up only
   while they hold the same number. `.main-layout` moved 960px -> 1600px
   (static/css/layout.css); leaving this at 960 would leave the "SmartImmigrate"
   title floating 240px (at 1440) to 320px (at 1920) inboard of the content
   column beneath it — a new visual defect on the same page. Keep the two values
   equal. */
.chat-header {
  /* Pixel-perfect pass (28 Sep): hidden at every width now, not only below the
     breakpoint. The canvas has no page title over the conversation, and at
     desktop it was ~115px repeating the brand bar above it. #820's alignment
     check states `display: none` as a pass. The same #796 handoff as on
     mobile applies: the welcome block carries the heading. */
  display: none;
  width: 100%;
  max-width: 1600px;
  justify-content: space-between;
  align-items: center;
  padding: var(--space-6) var(--space-4) var(--space-3);
}

.header-title h1 {
  font-size: var(--text-2xl);
  margin-bottom: var(--space-1);
  color: var(--neutral-900);
}
.header-title p {
  font-size: var(--text-md);
  color: var(--neutral-600);
}

/* --- Auth Buttons (pre-login) --- */
.header-auth {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* Bug #839. `--action-fill` and not `--primary`: `--primary` is the brand red
   again (design-system.css, alias block), and the nav's sign-in control is not
   this page's primary action — the composer's send button is, and it carries
   `data-primary-action`. Filled ink is the base; the accent belongs to the one
   marked control. Measured on production before this change: "Accesso", the
   paperclip, "Invia" and "Analizza il mio caso" were all `#0E1418`, which is
   four primary actions and therefore none. */
.auth-btn {
  background: var(--action-fill);
  color: #fff;
  border: none;
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-5);
  font-size: var(--text-sm);
  cursor: pointer;
  transition: background var(--transition-base);
  font-family: var(--font-family);
  font-weight: var(--font-medium);
}
.auth-btn:hover { background: var(--action-fill-hover); }

.user-menu {
  display: none;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-sm);
}
.user-menu.visible { display: flex; }
.user-email { color: var(--neutral-600); }

.logout-btn {
  background: none;
  color: var(--neutral-700);
  border: 1px solid var(--neutral-400);
  border-radius: var(--radius-md);
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-sm);
  cursor: pointer;
  font-family: var(--font-family);
}
.logout-btn:hover { background: var(--neutral-200); }

/* --- Chat Container --- */
.chat-container {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

/* --- Bug #980: "New conversation" -----------------------------------------
   One row at the top of the chat column, the control at its end. `hidden`
   while there is no conversation, so a first visit -- the page the #1074 dock
   budget and #792's above-the-fold checks measure -- gives the transcript
   every pixel it had. The button is the design system's ghost `.sim-btn`;
   the only page rule is its width, because below the breakpoint the component
   layer makes every `.sim-btn` full-width and this one is a secondary action
   beside the conversation, not the page's call to action. */
.chat-toolbar {
  display: flex;
  justify-content: flex-end;
  flex: none;
}
.chat-toolbar[hidden] { display: none; }
.chat-toolbar .chat-new-conversation { width: auto; }

/* --- Messages --- */
.messages-wrapper {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
}

.messages {
  flex: 1;
  overflow-y: auto;
  /* Pixel-perfect pass (28 Sep): the conversation is no longer a white card
     inside the page. The bubbles are the surfaces, the transcript is the page
     itself -- the claude.ai shape the owner asked for. */
  background: transparent;
  padding: var(--space-4);
  /* Substory #710: no `max-height: 60vh` any more. On a 900px window it gave
     the transcript 540px and the rest of the screen to margin, which is why the
     composer appeared to eat the page. The transcript now takes whatever the
     frame leaves it (see the desktop frame near the end of this file, and the
     mobile one inside the 760px query). */
  min-height: 300px;
}

/* --- The scope banner (substory #710) ---------------------------------------
   "We are evaluating 4 of 15 programmes": ONE component, the first row of the
   chat column at every width, so it stays put above the transcript while the
   transcript scrolls. The script sets only its text and `hidden`.

   A neutral surface, deliberately: it is an informational fact about the
   interview, not a warning (owner, 2026-09-25: "Neutral surface, not amber").
   Neutral tokens only, the same family as `.msg.status`, and no warning,
   error or accent token. */
.scope-banner {
  /* Pixel-perfect pass (28 Sep): a centred pill, as in the canvas -- still the
     neutral surface the owner asked for, not the canvas's amber. */
  align-self: center;
  max-width: 100%;
  background: var(--sim-surface);
  color: var(--sim-ink-2);
  border: 1px solid var(--sim-line);
  border-radius: var(--radius-full);
  padding: 6px var(--space-3);
  font-size: var(--text-xs);
  font-weight: var(--font-medium);
  text-align: center;
}
.scope-banner[hidden] { display: none; }

/* --- The first minute (substory #796) ---------------------------------------
   The welcome block lives INSIDE `.messages`, so everything here is layout for
   a first entry in the transcript. The controls it contains are the design
   system's (`.sim-btn--ghost --wrap`): nothing below re-declares a component's
   colour, type or touch target, because a page rule that beats the component
   layer is the defect #767 exists to close.

   Every measure is relative or tokenised and every box is allowed to shrink,
   which is what keeps `TestNothingOverflowsAt390` true in Urdu and Hindi as
   well as English. `padding-inline-start` rather than `padding-left` on the
   step list for the same reason: `/ur/` renders `dir=rtl` and the numbers have
   to sit on the correct side of it.

   Bug #820 changed the `100%` below to `min(100%, 70ch)`. The block was designed
   and measured inside `.main-layout`'s old 960px cap, where `100%` of the
   transcript was 644px and the lead paragraph set at a readable measure by
   accident rather than by rule. Widening the cap to 1600px makes `100%` 1284px
   at 1920, and the lead — the first sentences a visitor reads — sets at ~160
   characters per line. That is the same second-order defect the cap change
   already had to fix on `.msg` a few rules down, in an element that did not
   exist when it was fixed there; #796 landed on `main` after this branch was
   cut, so the merge, not either change on its own, is what produces it.

   `70ch` is `.msg`'s measure, deliberately: this block IS the transcript's first
   entry (see the paragraph above), and a welcome that sets wider than the
   answers below it would be the widest text on the page. Measured, the two now
   render the same 630px at every viewport above the breakpoint.

   `min()` only ever narrows, so nothing here regresses: at 390px `100%` is
   326px and still binds, which is what keeps `TestNothingOverflowsAt390` and
   #796's own beta gate reading exactly what they read before. The chips keep
   their own `max-width: 30rem` (480px) and are unaffected either way.

   `test_the_welcome_block_keeps_a_readable_measure` in
   apps/core/tests/test_beta_bug820_desktop_width_e2e.py holds this. */
/* Pixel-perfect pass (28 Sep): the arrival is the page, not a grey card in a
   white card. A large title, a short lead, the three steps in one white
   "How it works" card with numbered discs (the first filled: it is where the
   person is), then "Where do you want to start?" over the four options as
   pills. The numbers are CSS counters, so the list keeps its <ol> semantics
   and `padding-inline-start` keeps them on the correct side in Urdu. */
.welcome {
  padding-block: var(--space-2) 0;
  margin-bottom: var(--space-4);
  max-width: min(100%, 70ch);
}

.welcome-title {
  font-size: 24px;
  font-weight: var(--font-bold);
  line-height: 1.25;
  letter-spacing: -0.02em;
  color: var(--sim-ink);
  margin-bottom: 10px;
}

.welcome-lead {
  font-size: 15px;
  line-height: 1.55;
  color: var(--sim-ink-2);
  margin-bottom: var(--space-5);
}

.welcome-how {
  background: var(--sim-surface);
  border-radius: 16px;
  box-shadow: var(--sim-shadow);
  padding: 18px;
  margin-bottom: var(--space-5);
}

/* "HOW IT WORKS", "WHERE DO YOU WANT TO START?": small section labels.
   `--sim-muted` passes 4.5 on white (5.68) and on the page ground. */
.welcome-kicker {
  font-size: 11px;
  font-weight: var(--font-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sim-muted);
  margin-bottom: var(--space-3);
}
.welcome-kicker--plain {
  font-size: var(--text-sm);
  font-weight: var(--font-medium);
  letter-spacing: 0;
  text-transform: none;
  color: var(--sim-ink-2);
  margin-bottom: 10px;
}

.welcome-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.welcome-step {
  position: relative;
  min-height: 22px;
  padding-inline-start: 34px;
  font-size: 14px;
  line-height: 1.45;
  color: var(--sim-ink);
}
/* Bug #1202: the numeral is a real element, not `::before` with a CSS
   counter. #765's contrast audit measures a pseudo-element's text against its
   ANCESTORS' background, so the white "1" on its ink disc read as white on
   the white card (1.00:1) and turned the beta gate red. As an element with its
   own background it is measured against the disc (ink on white 18.9:1, white
   on ink the same). aria-hidden: the <ol> already says the order. */
.welcome-step-num {
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0;
  inline-size: 22px;
  block-size: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  border: 1.5px solid var(--sim-line-strong);
  border-radius: var(--radius-full);
  background: var(--sim-surface);
  font-size: 11px;
  font-weight: var(--font-bold);
  color: var(--sim-ink-2);
}
.welcome-step:first-child .welcome-step-num {
  background: var(--sim-ink);
  border-color: var(--sim-ink);
  color: #fff;
}
.welcome-step-title {
  display: block;
  font-weight: var(--font-semibold);
}
.welcome-step:not(:first-child) .welcome-step-title { font-weight: var(--font-medium); }
.welcome-step-note {
  display: block;
  color: var(--sim-muted);
}

/* A COLUMN at every width, not only below the breakpoint. The issue asks for
   the chips to wrap into a column at 390px because "Urdu, Hindi and Korean run
   much longer than English", and a row that wraps only when it must puts two
   chips on a line in one locale and one in another — so the layout the beta
   gate measures would not be the layout most visitors see. One per line is
   also how a list of things to choose from reads inside a transcript.
   `max-width` keeps them from stretching to a 700px desktop transcript. */
.welcome-chips {
  display: flex;
  /* Pixel-perfect pass (28 Sep): the canvas wraps the four options as pills
     rather than stacking full-width bars. Above the breakpoint they do; below
     it they stay ONE PER LINE (the query further down), because #796 requires
     it at 390 -- Urdu, Hindi and Korean run long -- and its gate measures
     exactly that (TestNothingOverflowsAt390). */
  flex-wrap: wrap;
  gap: var(--space-2);
  max-width: 30rem;
  margin-bottom: var(--space-4);
}

/* The chip is `.sim-btn --ghost --wrap`; this adds only its minimum width
   inside a flex column, which is layout and not appearance. Without it a long
   Urdu label makes the button its own widest word wide. */
.welcome-chip { min-width: 0; }
/* The pill shape is this page's, the rest (type, target, focus) is still the
   component's; (0,2,0) so it beats `.sim-btn--ghost` without `!important`. */
.welcome-chips .welcome-chip {
  width: auto;   /* below 760px `.sim-btn` is `width: 100%` (design-system.css) */
  border-radius: var(--radius-full);
  background: var(--sim-surface);
  border-color: var(--sim-line-strong);
  padding-inline: 17px;
  font-size: 14px;
}

/* Substory #710. The price on the third step never breaks between the amount
   and its ISO code. */
.welcome-price {
  white-space: nowrap;
  font-weight: var(--font-semibold);
}

/* Substory #710: "or write it in your own words…", the fourth way to start.
   A quieter control than the four options above it (it hands over the composer
   rather than choosing a goal), but still a real button with a full touch
   target. Its space above comes from the chips' own `margin-bottom`, so the
   chips come back up by that much and this one gives it back underneath. */
.welcome-chips:has(+ .welcome-compose) { margin-bottom: 0; }
.welcome-compose {
  display: block;
  min-block-size: var(--sim-tap);
  margin-block-end: var(--space-4);
  padding: var(--space-2) 0;
  background: none;
  border: none;
  color: var(--sim-ink-2);
  font: inherit;
  font-size: 14px;
  text-align: start;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  cursor: pointer;
}
.welcome-compose:hover { color: var(--neutral-900); }

/* --- The affiliation disclosure (substory #796) -----------------------------
   Two copies on a first visit: one under the welcome block and one in the
   page's footer. The footer copy is the one that matters after the first
   message, when the block is gone and the product starts giving immigration
   guidance — which is exactly when not looking like the Government of Canada
   matters (story #793).

   `--sim-ink-2` and not `--sim-muted`. The issue names the risk: "the muted
   token has failed contrast elsewhere on this page". `--sim-muted` does pass
   on both grounds this renders on (5.68 on white, 5.21 on `--sim-soft`), but
   it passes by 0.7, and this is small text at 12px whose ground is a page
   token that may be re-pointed by a future palette change. `--sim-ink-2`
   measures 9.5 on white and 8.7 on `--sim-soft`, so the margin survives a
   ground moving under it. */
.chat-disclosure {
  font-size: var(--text-xs);
  line-height: var(--leading-relaxed);
  color: var(--sim-ink-2);
}

.chat-footer {
  width: 100%;
  padding: var(--space-2) 0 0;
  /* Substory #1257: the footer's own strut sets the height of the lines the
     notice and the links flow on, so it takes the disclosure's type. */
  font-size: var(--text-xs);
  line-height: var(--leading-relaxed);
  color: var(--sim-ink-2);
}

/* --- Substory #1257: the notice and the legal links, in the same footer ------
   Step 3 puts the five legal links INSIDE this footer, after the disclosure and
   at its size; Step 6 lets the collection notice ("Your documents are
   processed with AI in the United States. Privacy Policy") take this line too
   when it does not fit beside the paperclip. It does not: below 760px the
   composer is #1074's dock, capped at 80px with exactly 20px reserved for
   #794's hint row, so a permanent notice row in the dock would push the hint
   past the cap. The footer is the last row of the composer column, directly
   under the dock, so this is still "in the composer column".

   THE BUDGET, MEASURED ON BETA BEFORE THIS CHANGE (main 15a4a3d, 393 wide):
   the footer was 59.6px (8 padding + two 19.8px disclosure lines + the 12px
   `p` margin from base.css). #1074's gate then had 52px of headroom at 393x659
   (visible transcript 367 against a 315 floor) and 11px at 393x410 (118
   against 107). Everything below is sized to that:

   * The disclosure stays a BLOCK of its own. #1074 and #796 sample it on a
     5x5 grid with elementFromPoint; an inline disclosure's rect would cover
     the end of its last line, where the notice's words would answer the hit
     test instead.
   * The notice and the links flow INLINE after it, as one wrapped paragraph,
     so the five links cost the rest of a line instead of a row each. The `p`
     margin goes (12px back).
   * The links reach WCAG 2.2 2.5.8's 24x24 with `padding-block` on an inline
     box, which grows the hit area without growing the line box: a 24px
     inline-flex link would make every line it sits on 24px tall. Measured:
     26px tall, the narrowest ("Terms") 42px wide.
   * Below 760px those lines are 18px (--leading-normal) and the footer has
     no padding above the disclosure (see the max-width: 760px block).

   MEASURED AFTER, same viewports, #1074's own _MEASURE: 393x659 visible
   transcript 347 (en) / 329 (it, three lines of notice and links) against
   315, region below 235 / 253 against 267; 393x410 (notice aside) 116
   against 107, below 217 against 226.

   THE ONE PLACE THE NOTICE STEPS ASIDE: a PORTRAIT phone viewport under
   560px tall (see the media query after the max-width: 760px block). That is
   the Android soft keyboard's shape (#708's resizes-content; #1074's 393x410
   fixture, portrait), where the whole frame below the transcript has 11px
   left and two more lines would take the conversation under #1074's floor.
   The person is typing at that moment, not uploading — and the file chooser
   the paperclip opens dismisses the keyboard, so the viewport is back to full
   height, and the notice back on screen, at the moment documents are chosen.
   `orientation: portrait` is load-bearing: a LANDSCAPE phone (667x375,
   740x360) is under 560px tall with no keyboard at all, and opening the file
   chooser does not change its viewport, so without it the notice would never
   be shown there (review 03, finding 1). In landscape the notice stays and
   the page scrolls to it. iOS overlays its keyboard without resizing the
   viewport, so on iOS the notice never leaves in either orientation. */
/* 4px under the disclosure, not 0: a link's padding box reaches 4px above its
   18px line, and directly under the disclosure that strip answered #1074's
   5x5 hit test on the disclosure's bottom row instead of the disclosure
   (measured 21/25 against a 90% floor at 393x410 with margin 0). */
.chat-footer > .chat-disclosure[data-affiliation-disclosure] {
  margin-block: 0 var(--space-1);
  margin-inline: 0;
}
.chat-footer > .privacy-collection-notice,
.chat-footer > .legal-footer {
  display: inline;
  margin: 0;
  line-height: inherit;
}
.chat-footer > .legal-footer { margin-inline-start: var(--space-1); }
.chat-footer .legal-footer a,
.chat-footer .privacy-collection-notice a {
  display: inline;
  min-block-size: 0;
  min-inline-size: 0;
  padding-block: 6px;
  padding-inline: var(--space-1);
  color: var(--sim-ink-2);
  text-decoration: underline;
  text-underline-offset: 2px;
  white-space: nowrap;
}

/* --- Chat Bubbles --- */
/* Bug #820, second-order effect of the cap change above. `.msg` is a block with
   `max-width: 85%` and no `width`, so a bubble fills 85% of the transcript
   whatever it contains. At the old 960px cap that was a 547px slab; widening
   `.main-layout` to 1600px doubled it to 1091px, so a four-word user message
   rendered as a 1091px block of solid colour and a long answer set at ~145
   characters per line — about double a readable measure, and double what it was.
   Widening the column is the fix; letting the bubbles widen with it is not.

   `width: fit-content` makes a bubble as wide as its content. `min(85%, 70ch)`
   then stops a long answer at ~70 characters instead of letting it grow with the
   column. `min()` only ever narrows: at every viewport the result is <= the old
   85%, so no bubble can come out wider than it is today, and the `max-width: 95%`
   mobile override further down still binds first below 760px.

   Measured on beta with layout.css and this file served over it (/it/, a short
   user message and a long assistant answer injected into the transcript). The
   left-hand number is the widened cap with the *old* `.msg` rule — the state
   this block corrects — not today's 960px production page:

       viewport    transcript   short bubble        long bubble
                                before -> after     before -> after
       1920x1080      1316      1091 -> 257 px      1091 -> 630 px
       1440x900       1156       955 -> 257 px       955 -> 630 px
       1280x800        996       819 -> 257 px       819 -> 630 px
       1024x768        740       602 -> 257 px       602 -> 602 px
       390x780         358       310 -> 257 px       310 -> 310 px

   Rich message content is unaffected, because the constraint is a `max-width`
   and not a `width`: `.msg pre` still scrolls its own overflow (a 1170px code
   line sits in a 630px bubble exactly as it sat in a 547px one) and nothing
   clips at any viewport. `.msg table` is the one visible change — `width: 100%`
   of a fit-content box resolves to the table's own content width, so a small
   three-column table measures 348px at 1920 rather than 547px. A wide table is
   unchanged: it still fills the cap.

   `test_bubbles_do_not_grow_with_the_chat_column` in
   apps/core/tests/test_beta_bug820_desktop_width_e2e.py holds both ends — the
   short bubble far narrower than the transcript, the long one bounded — so this
   cannot silently regress the next time the cap moves.

   The three full-width variants below opt out with `width: auto`. They already
   carry `max-width: 100%` because they are notices rather than bubbles, and
   `.msg.status` / `.msg.registration` centre their text with `text-align`, which
   a fit-content box would silently stop doing. */
.msg {
  margin-bottom: 14px;
  padding: 15px 19px;
  /* Pixel-perfect pass (28 Sep): 20px bubbles above the breakpoint, 18px
     below, each with a 6px tail (the logical corners in the rules below). */
  border-radius: 20px;
  width: fit-content;
  max-width: min(85%, 70ch);
  font-size: 15px;
  line-height: 1.6;
  word-wrap: break-word;
}

/* Bug #1123. A bubble's PARAGRAPH direction follows its own content, not the
   page's. Without this a Latin sentence on /ur/ (or an Urdu one on /it/) kept
   the page's paragraph direction, so the neutral character that closes it --
   full stop, closing quote, parenthesis -- was drawn at the wrong end of its
   run: "... Federal Skilled Worker." rendered as ".Skilled Worker". The UA's
   `unicode-bidi: isolate` on a div shields it from its surroundings; it does
   not make it follow its content. `plaintext` does (UAX #9 rules P2/P3: the
   first strong character decides).

   `unicode-bidi: plaintext`, NOT `dir="auto"` on the bubble, which is what the
   issue's Step 2 literally asks for. `dir="auto"` changes the element's
   `direction`, and #840 pins the bubbles with logical margins
   (`margin-inline-start: auto` above) that resolve against the element's own
   direction -- so a Latin bubble on /ur/ jumped to the other edge of the
   transcript (measured at IG-1). `plaintext` changes how the text inside is
   ordered and leaves `direction`, and therefore the bubble's edge, the page's.

   It is not inherited, and each block is its own bidi paragraph, so it is set
   on every block box renderMarkdown()/DOMPurify can put inside a bubble too
   (PURIFY_CONFIG.ALLOWED_TAGS in templates/chat/landing.html): on `.msg`
   alone the assistant's reply, wrapped in a <p>, stayed broken.
   apps/chat/tests/test_bug1123_bidi_plaintext_rules.py pins this list against
   ALLOWED_TAGS. */
.msg,
.msg :is(p, li, h1, h2, h3, h4, blockquote, pre, td, th) {
  unicode-bidi: plaintext;
}

/* Bug #840. The three bubble rules below — user, assistant, error — used to pin
   themselves with `margin-left`/`margin-right` and square one corner with
   `border-bottom-right-radius`/`border-bottom-left-radius`. Those mean the same
   PIXELS in every direction, so on /ur/ the transcript came out identical to
   /it/ rather than its mirror: the reader's own message sat on the inline-START
   edge with its tail pointing away from the edge it was on, and the assistant
   sat where the reader expects to find themselves. At 390 the two locales
   measured pixel-for-pixel the same.

   Written as the logical mirror, NOT as a `[dir="rtl"]` override. The override
   is the cheaper edit and this repo already teaches it (base.css had one for
   `.item-badge`, deleted by this same fix), but it leaves the physical rule
   standing for the other six locales and has to be kept in step with it by hand
   forever. `margin-inline-start: auto` is one rule that is correct in seven.
   `apps/core/tests/test_beta_bug840_rtl_layout_e2e.py` fails on any
   direction- or locale-scoped rule whose subject is `.msg.user`/`.msg.system`,
   which is what keeps it that way.

   `border-end-end-radius` is the bottom-inline-end corner: bottom-right in LTR,
   bottom-left in RTL. `border-end-start-radius` is its mirror. */

/* Bug #839. Your own message bubble is filled ink, not the brand red: it is
   content you already sent, not the thing to do next. `--action-fill` is the
   same ink it painted before this change — `--primary` is what moved. */
.msg.user {
  background: var(--action-fill);
  color: #fff;
  margin-inline-start: auto;
  max-width: min(85%, 520px);
  border-end-end-radius: 6px;
}

/* Pixel-perfect pass (28 Sep): the assistant is a white surface lifted off
   the page ground, not a grey block in a white card. */
.msg.system {
  background: var(--sim-surface);
  color: var(--sim-ink);
  box-shadow: var(--sim-shadow);
  padding: 19px 22px;
  margin-inline-end: auto;
  border-end-start-radius: 6px;
}

.msg.error {
  background: var(--error-light);
  border: 1px solid var(--error);
  color: var(--error);
  margin-inline-end: auto;
  border-end-start-radius: 6px;
}
.msg.error code {
  background: #FDE8E8;
  padding: 0.1rem 0.3rem;
  border-radius: var(--radius-sm);
  font-size: 0.9em;
  user-select: all;
}

/* The three variants below are notices, not bubbles: they span the transcript by
   design, which is why each already carried `max-width: 100%`. `width: auto`
   opts them back out of `.msg`'s `fit-content` (bug #820) so they keep spanning
   it — without this, `.msg.status` and `.msg.registration` shrink to their text
   and their `text-align: center` has nothing left to centre within. */
.msg.status {
  background: transparent;
  color: var(--sim-muted);
  font-style: italic;
  text-align: center;
  width: auto;
  max-width: 100%;
  font-size: var(--text-sm);
}

.msg.analysis {
  /* Pixel-perfect pass (28 Sep): an analysis is an assistant answer, and it
     wears the assistant's surface -- no tinted slab with an accent rule. */
  background: var(--sim-surface);
  color: var(--sim-ink);
  box-shadow: var(--sim-shadow);
  width: auto;
  max-width: 100%;
}

.msg.registration {
  background: var(--sim-surface);
  border: 1px solid var(--sim-line);
  box-shadow: var(--sim-shadow);
  width: auto;
  max-width: 100%;
  text-align: center;
}
/* Bug #839. `--action-fill` e non `--primary`: la registrazione ne disegna DUE
   insieme (landing.html:1684 e :1688), quindi il marchio qui darebbe due
   accenti nello stesso messaggio. Il fondo lo decide il marcatore, e nessuno di
   questi due lo porta: restano inchiostro. Il `:hover` qui sotto era già stato
   convertito, il fondo a riposo no — erano rossi fermi e neri sotto il mouse. */
.msg.registration .reg-action-btn {
  display: inline-block;
  margin-top: var(--space-2);
  background: var(--action-fill);
  color: #fff;
  border: none;
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-5);
  font-size: var(--text-sm);
  cursor: pointer;
  font-family: var(--font-family);
}
.msg.registration .reg-action-btn:hover { background: var(--action-fill-hover); }

/* --- Pathway Selection Buttons --- */
/* Pixel-perfect pass (28 Sep), and bug #1119: the pathways are reply chips
   under the answer, as in the canvas -- outlined pills on the page ground, not
   a pink panel of filled ink buttons. */
.pathway-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
  margin-bottom: var(--space-4);
}
.pathway-actions p {
  flex-basis: 100%;
  margin: 0;
  font-size: var(--text-sm);
  color: var(--sim-ink-2);
}
/* Bug #839. Stessa ragione di `.reg-action-btn`, in forma peggiore: questo
   bottone lo costruisce un ciclo (landing.html:1643, uno per percorso), quindi
   `var(--primary)` qui non dava "un accento di troppo" ma N accenti — cioè il
   censimento con cui #839 si apre, riscritto nel colore nuovo. */
.pathway-select-btn {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  min-height: var(--sim-tap);
  background: var(--sim-surface);
  color: var(--sim-ink);
  border: 1px solid var(--sim-line-strong);
  border-radius: var(--radius-full);
  padding: var(--space-2) 17px;
  font-size: 14px;
  font-weight: var(--font-medium);
  line-height: 1.35;
  cursor: pointer;
  font-family: var(--font-family);
  text-align: start;   /* Bug #840: was `left`; `start` is `left` in LTR. */
  transition: background var(--transition-base), border-color var(--transition-base);
}
.pathway-select-btn:hover { background: var(--sim-soft); border-color: var(--sim-ink-2); }
.pathway-select-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
/* Bug #1119: a choice already made is a confirmed STATE, not a call to
   action -- soft surface, the ok colour on the text and the check only. */
.pathway-select-btn.selected {
  background: var(--sim-ok-soft);
  color: var(--sim-ok);
  border-color: var(--sim-ok-soft);
  font-weight: var(--font-semibold);
  opacity: 1;
  cursor: default;
  pointer-events: none;
}
.pathway-select-btn.selected::before {
  content: '\2713\00a0';
}
.pathway-select-btn.disabled-greyed {
  background: var(--sim-surface);
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}
/* Substory #954: "how many dependent children", asked above the pathway
   buttons. Logical properties only (RTL, #840). */
.pathway-dependants {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--neutral-600);
}
.pathway-dependants-input {
  inline-size: 5rem;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--neutral-400, #9ca3af);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  font-family: var(--font-family);
}
.pathway-dependants-hint {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--error);
}

/* --- Composer hint (#794) ---
   What the separate "Analyze My Case" button used to occupy. That button was
   permanently greyed and explained nothing; this is the composer explaining
   itself instead, directly beneath the field the person is typing in. Hidden
   until it has something to say, so first paint is unchanged. */
.composer-hint {
  display: none;
  padding: var(--space-2) var(--space-3);
  color: var(--neutral-600);
  font-size: var(--text-sm);
  /* Substory #1074 moved this inside `.input-area` (see the template comment
     there for why). A whole row of the composer, not a fourth control in its
     row — and `flex-basis` rather than `width` because the composer is a flex
     container at every width. Inert while `display: none`, which is how it
     ships and how the composer is measured. */
  flex-basis: 100%;
}
.composer-hint.visible { display: block; }

/* --- Typing Indicator --- */
.typing-indicator {
  display: none;
  padding: var(--space-2) var(--space-4);
  color: var(--neutral-500);
  font-style: italic;
  font-size: var(--text-sm);
}
.typing-indicator.visible { display: block; }

/* --- Input Area --- */
.input-area {
  display: flex;
  /* Substory #1074: `#composer-hint` is the composer's last child now, and it
     is a full-basis row. Nothing else here can wrap — the field shrinks — so
     at rest this changes no layout at any width. */
  flex-wrap: wrap;
  /* Pixel-perfect pass (28 Sep): the composer is ONE pill -- field, paperclip
     and send inside a single white capsule, as on claude.ai and in the canvas.
     The field loses its own box; focus is shown on the capsule instead
     (`:focus-within` below). Logical padding only: the wide side is the side
     the text starts on, which is the right-hand one in Urdu (#840). */
  align-items: flex-end;
  gap: 6px;
  background: var(--sim-surface);
  border: 1px solid var(--sim-line);
  border-radius: 26px;
  padding-block: var(--space-2);
  padding-inline: var(--space-5) var(--space-2);
  box-shadow: var(--sim-shadow);
  transition: border-color var(--transition-base), box-shadow var(--transition-base);
}
.input-area:focus-within {
  /* The field has `outline: none`, so THIS is the keyboard focus indicator:
     an ink edge (9.6:1 on white, 8.7:1 on the page ground). A soft grey ring
     was tried first and measured 1.02:1 against the page -- invisible. */
  border-color: var(--sim-ink-2);
  box-shadow: var(--sim-shadow), 0 0 0 1px var(--sim-ink-2);
}

.input-area textarea {
  flex: 1;
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--sim-ink);
  padding-block: 11px;
  padding-inline: 0;
  font-size: var(--text-md);
  line-height: 1.4;
  font-family: var(--font-family);
  resize: none;
  min-height: var(--sim-tap);
  max-height: 160px;
  outline: none;
}
.input-area textarea::placeholder { color: var(--sim-muted); opacity: 1; }
/* One line at rest (the markup's `rows="3"` is for a page without this sheet);
   landing.html's fitComposer() grows it with the text above the breakpoint. */
.input-area textarea { block-size: var(--sim-tap); }

/* Bug #839. Questa è la regola che dipinge `#send-btn`, ed è (0,1,1): finché
   diceva `var(--primary)` l'accento della landing arrivava da QUI e non dal
   marcatore. Si poteva togliere `data-primary-action` dal bottone e la pagina
   restava identica — il meccanismo era un no-op sulla superficie che conta di
   più. Con `--action-fill` il rosso lo porta il marcatore, e ogni futuro
   `.input-area button` che non sia l'invio nasce inchiostro invece che rosso.
   Il `:hover` a :487 era già stato convertito: la regola si contraddiceva. */
.input-area button {
  background: var(--action-fill);
  color: #fff;
  border: none;
  border-radius: var(--radius-full);
  padding: var(--space-3) var(--space-6);
  font-size: var(--text-md);
  cursor: pointer;
  align-self: flex-end;
  transition: background var(--transition-base);
  font-family: var(--font-family);
}
/* Pixel-perfect pass (28 Sep): send is a round 44px button with an arrow, its
   word moved to `aria-label` (and `title`). Still painted through
   `--action-fill`, which `[data-primary-action]` re-points to the accent
   (#839), so the red keeps coming from the marker and not from here. */
.input-area #send-btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--sim-tap);
  min-inline-size: var(--sim-tap);
  block-size: var(--sim-tap);
  min-block-size: var(--sim-tap);
  padding: 0;
}
.input-area #send-btn .sim-icon {
  inline-size: 18px;
  block-size: 18px;
  stroke-width: 2.3;
}
.input-area button:hover:not(:disabled) { background: var(--action-fill-hover); }
.input-area button:disabled { opacity: 0.55; cursor: not-allowed; }

/* Upload button variant */
/* Bug #830: this block used to carry `font-size: 1.2rem`, and it never once
   took effect. `.input-area button { font-size: var(--text-md) }` (:456 above)
   is (0,1,1) and this selector is (0,1,0), so the glyph has always painted at
   16px — measured on deployed beta at both 390x844 and 1440x900. The dead
   declaration is removed rather than left in place because reading it is how
   the paperclip came to be described as a 19.2px glyph in a 44x44 button when
   it is a 16px glyph in a 44x40 one. The size that matters is set below the
   breakpoint, at a specificity that wins — see `.input-area .upload-btn`. */
.upload-btn {
  padding: var(--space-3);
  line-height: 1;
  align-self: flex-end;
  /* Substory #848: the content is a 20x20 `svg.sim-icon`, not a glyph. As a
     flex item it is centred in the box and `vertical-align` no longer applies,
     so the icon cannot push the line box — and with it the 44px target #830
     measures — taller than the 20px it is. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* Bug #839: the paint moves to `.input-area .upload-btn`, and the reason is the
   one the #830 note above already records for `font-size`. `.upload-btn` is
   (0,1,0) and `.input-area button` is (0,1,1), so the `background:
   var(--neutral-700)` that stood here has never painted a pixel: the paperclip
   has always worn the send button's fill. That is the symptom bug #839 opens
   with — "a utility icon painted like the primary action" — and it was hiding
   behind a declaration that reads as if it had already been fixed.

   A utility icon is not the thing to do next, so it steps down to the system's
   ghost idiom. The ghost is white-with-a-border in design-system.css because it
   sits on grey surfaces; `.input-area` is white, so here the same idea inverts
   to a soft fill with no border. That is not a preference: a border would push
   the phone box to 46px (20px glyph + 24px block padding + 2px border, with
   `box-sizing: border-box` and `min-height: var(--sim-tap)` as a floor), so the
   paperclip would stop lining up with the 44px send button beside it, and
   apps/core/tests/test_beta_bug830_accessible_names_e2e.py measures exactly
   that box. */
/* Pixel-perfect pass (28 Sep): inside the capsule the paperclip is a quiet
   round icon. Its fill is the capsule's own white rather than `transparent`:
   #839 asserts the attach control is a visible, OPAQUE, non-accent fill, and
   white on white reads exactly like the canvas's transparent button. */
.input-area .upload-btn {
  background: var(--sim-surface);
  color: var(--sim-muted);
  /* Here and not on `.upload-btn`: `.input-area button`'s 12/24 padding is
     (0,1,1) and beats a (0,1,0) rule -- the dead-declaration trap #830
     documents above. Zero padding + the 44px minimums = a 44px circle. */
  padding: 0;
  min-inline-size: var(--sim-tap);
  min-block-size: var(--sim-tap);
}
.input-area .upload-btn:hover:not(:disabled) { background: var(--sim-soft); color: var(--sim-ink); }
/* The anchor for the `.sim-count` badge (#804). Set at every width because it
   changes nothing on its own — an unpositioned box and a `position: relative`
   one occupy the same space — and a positioning context that appears only
   inside a media query is the kind of thing that resolves against the viewport
   the one time it is forgotten. */
.upload-btn { position: relative; }
/* Above the breakpoint the badge is not shown: the document panel is right
   there beside the chat, so a count on the paperclip would be a third place
   saying what two already say. "The desktop layout is fine and this issue must
   not touch it." */
.upload-btn .sim-count { display: none; }

#file-input { display: none; }

/* --- Markdown inside chat bubbles --- */
.msg > :first-child { margin-top: 0; }
.msg > :last-child  { margin-bottom: 0; }

.msg h1, .msg h2, .msg h3, .msg h4 {
  margin: 0.5em 0 0.3em;
  line-height: var(--leading-tight);
}
.msg h1:first-child, .msg h2:first-child, .msg h3:first-child { margin-top: 0; }
.msg h1 { font-size: 1.2em; }
.msg h2 {
  font-size: 1.1em;
  color: var(--neutral-700);
  border-bottom: 1px solid var(--neutral-300);
  padding-bottom: 0.2em;
  margin-top: 0.8em;
}
.msg h3 { font-size: 1em; font-weight: var(--font-semibold); color: var(--neutral-700); }
.msg h4 { font-size: 0.9em; color: var(--neutral-600); }
.msg p   { margin: 0.3em 0; }
/* Bug #840: `padding-inline-start`, not `padding-left` — the room a list makes
   for its own bullets and numbers is on the side the line starts on. */
.msg ul, .msg ol { margin: 0.3em 0; padding-inline-start: 1.5em; }
.msg li { margin-bottom: 0.2em; }
.msg strong { font-weight: var(--font-semibold); }
.msg em { font-style: italic; }
.msg code {
  background: var(--neutral-200);
  padding: 0.1em 0.3em;
  border-radius: var(--radius-sm);
  font-size: 0.9em;
  font-family: var(--font-mono);
}
.msg pre {
  background: var(--neutral-200);
  padding: 0.5em 0.8em;
  border-radius: var(--radius-sm);
  overflow-x: auto;
  max-width: 100%;
  margin: 0.4em 0;
}
.msg pre code { background: none; padding: 0; }
.msg hr {
  border: none;
  border-top: 1px solid var(--neutral-300);
  margin: 0.5em 0;
}
.msg table {
  border-collapse: collapse;
  width: 100%;
  margin: 0.3em 0;
  font-size: 0.9em;
  display: block;
  overflow-x: auto;
}
.msg th, .msg td {
  border: 1px solid var(--neutral-300);
  padding: 0.3em 0.5em;
  text-align: start;   /* Bug #840: was `left`; `start` is `left` in LTR. */
}
.msg th { background: var(--neutral-100); font-weight: var(--font-semibold); }
.msg blockquote {
  /* Bug #840: the quote rule and the indent it pairs with were `border-left`
     and `padding-left`, so in Urdu the bar sat at the end of the quoted line
     and the indent opened away from it. */
  border-inline-start: 3px solid var(--primary);
  margin: 0.3em 0;
  padding-inline-start: 0.8em;
  color: var(--neutral-600);
}
.msg a { color: var(--primary); text-decoration: underline; }
.msg a:hover { color: var(--primary-hover); }

/* User messages: links and code override for white text */
.msg.user a { color: #fff; }
.msg.user code { background: rgba(255,255,255,0.2); color: #fff; }

/* --- Connection Status Bar --- */
.connection-status {
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-sm);
  font-weight: var(--font-medium);
  text-align: center;
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  transition: transform 0.3s ease, opacity 0.3s ease;
  transform: translateY(0);
  opacity: 1;
}
.connection-status.hidden {
  transform: translateY(-100%);
  opacity: 0;
  display: none;
}
.connection-status.reconnecting {
  background: var(--warning-light);
  color: #795548;
  border-bottom: 1px solid var(--warning);
  animation: pulse 2s ease-in-out infinite;
}
.connection-status.offline {
  background: var(--error-light);
  color: var(--error);
  border-bottom: 1px solid var(--error);
}
.connection-status.failed {
  background: var(--error-light);
  color: var(--error);
  border-bottom: 1px solid var(--error);
}
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.6; }
}

/* --- Responsive ---
   ONE breakpoint, 760px, and it is the design system's: `design-system.css`
   uses it, `layout.css` collapses the nav at it, and the page used to disagree
   with itself by carrying a second query at 700 right here. Between 700 and 760
   that produced a mobile nav sitting on a desktop layout — a state nobody
   designed, and the state bug #792's 759x800 case is red on. The old
   `@media (max-width: 700px)` block is gone rather than left alongside this
   one; its `.msg` rules moved here unchanged.

   Below the breakpoint the page stops being the desktop compressed and becomes
   an APP FRAME: a fixed-height column where only the transcript scrolls and the
   composer is anchored to the bottom. Bug #792. */
@media (max-width: 760px) {
  /* `dvh`, never `vh`: on iOS `vh` is measured against the viewport with the
     browser bar retracted, so a `100vh` frame puts its own bottom rows behind
     the bar. That is literally the second half of the reported bug — the
     document panel rendering underneath the browser chrome.

     `min-height` has to be neutralised too. The base rule is
     `min-height: calc(100vh - 48px)`, and on iOS that is *larger* than 100dvh,
     so it would win over the height set here and re-open the very overflow the
     frame exists to close. */
  .chat-page {
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
    /* Substory #1074. The composer dock's height, written ONCE because two
       rules need the same number and must never disagree: the dock sets it as
       its `min-block-size`, and `.main-layout` gives exactly it back as
       `padding-block-end` (see both below). A `position: fixed` dock occupies
       no space in the column, so if the reserve and the dock ever drift apart
       the dock paints over `.chat-footer` and #793's affiliation disclosure —
       which is the annexation the issue forbids by name and the beta gate's
       5x5 visibility grid measures.

       44 + 8 + 8: `--sim-tap` is the design system's minimum comfortable touch
       target and `--space-2` is the padding above and below it. Composed from
       tokens rather than written as 60px so the dock follows the target if it
       ever moves. */
    --sim-dock-h: calc(var(--sim-tap) + 2 * var(--space-2));
  }

  /* The frame has to survive the two shapes this page comes in.

     Signed OUT, the brand bar is rendered inside `.chat-page` (see the comment
     at the top of this file), so `.chat-page` alone is the whole page and
     `height: 100dvh` above is exact.

     Signed IN — which is how the bug was reported — `base.html` renders
     `includes/_nav.html` as a SIBLING of `.chat-page`. A `.chat-page` that is
     100dvh tall then starts at y=navHeight, and the composer anchored to its
     bottom lands one nav-height below the fold: the same bug, smaller. So the
     frame is the body, and `.chat-page` takes what the bar leaves. Its
     `height: 100dvh` becomes the flex basis, `min-height: 0` lets it shrink to
     the remainder, and the signed-out case is unaffected because there is
     nothing to shrink against.

     `body { min-height: 100vh }` (base.css) is overridden for the same reason
     the `min-height` above is: on iOS it exceeds 100dvh and would scroll the
     document. Browsers without `:has()` simply drop these three rules and get
     the signed-out frame plus a nav-height document scroll when signed in —
     today's behaviour, not a worse one. */
  body:has(> .chat-page) {
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }
  body:has(> .chat-page) > .site-nav { flex: 0 0 auto; }
  body:has(> .chat-page) > .chat-page { flex: 1 1 auto; }

  /* ~138px of a second "SmartImmigrate" and its subtitle, five pixels under a
     bar that already says both. On a 640px screen that is a fifth of the page
     spent repeating the brand to someone who just read it.

     HANDOFF TO #796: this block holds the page's only `<h1>`
     (`landing.html:73`), so below the breakpoint the chat page has no heading
     element at all. The bar above is a `header`/`nav` with the brand name in
     it, which is not the same thing to a screen reader. That is a known,
     accepted consequence of hiding the duplicate — #796 owns what fills the
     empty transcript on mobile and carries the replacement heading with it.
     Documented handoff, not an oversight. */
  .chat-header { display: none; }

  /* Substory #1257: the lines the collection notice and the legal links flow
     on are set at --leading-normal (18px) below the breakpoint, not the
     disclosure's relaxed 19.8 — the disclosure keeps its own line-height
     through `.chat-disclosure`. Measured at 393x659 in Italian (three such
     lines) this is the difference between 5px and 10px of #1074's headroom.
     The 8px padding above the disclosure goes (the column's 12px gap still
     separates it from the doc bar): it pays for the 4px under the disclosure
     and leaves 4px more of the 11px the keyboard-open frame had. See
     `.chat-footer` above for the whole budget. */
  .chat-footer {
    padding-block-start: 0;
    line-height: var(--leading-normal);
  }

  /* The chain that lets the transcript yield.

     A flex item's default `min-height` is `auto`, i.e. "never smaller than my
     content". With a conversation in it every one of these would refuse to
     shrink, the column would overflow its frame, and the composer would be
     pushed out of the bottom exactly as it is today. Every link from the frame
     down to `.messages` needs `min-height: 0` or none of them do. */
  .chat-container,
  .messages-wrapper { min-height: 0; }

  /* The two values that ARE the bug. `min-height: 300px` is why 360x640 is
     broken on a bare page load, `max-height: 60vh` is why any screen with a
     conversation in it is broken. Both were chosen for a column standing beside
     the document panel; neither survives the breakpoint. `flex: 1` puts the
     transcript on the leftovers, which is what makes it yield instead of
     claiming a fixed share. */
  .messages {
    flex: 1;
    min-height: 0;
    max-height: none;
    /* Pixel-perfect pass (28 Sep): no card, so no inner gutter -- the bubbles
       line up with the composer and the document bar on the frame's 16px. */
    padding-block: var(--space-2);
    padding-inline: 0;
  }

  /* --- Composer: a dock (substory #1074) -----------------------------------
     159px of a 659px phone was the composer. It is 60px now, and the 99px goes
     to the conversation: 240px of visible transcript becomes 367px at 393x659,
     and 32px becomes 118px at 393x410 — where 32px was the transcript panel's
     own padding with no conversation visible inside it at all.

     THREE ROWS BECOME ONE. The stacked composer #792 built (textarea on row
     one, paperclip and action on row two) spent 83 + 8 + 44 on the rows and 24
     more on its own padding. The substory's artboards draw one row — textarea,
     attach, send — and the textarea losing `flex-basis: 100%` is what buys that
     (`flex-wrap` stays, for `#composer-hint`'s row; nothing else can wrap). The cost is named
     rather than hidden: the textarea is ~214px instead of ~345px, so a long
     Italian or Urdu placeholder is clipped where it used to wrap. It is still
     wider than the ~180px the paragraph this one replaces called out as the
     pre-#792 state, and bug #792 records placeholder truncation as deliberately
     unasserted and belonging to the composer's COPY (#794), not to its frame.
     (Superseded by bug #1120: below 760px the placeholder is 14px and every
     locale is asserted to fit the one-line field — see the #1120 rule below.)

     WHY 60px, WHERE THE ISSUE SKETCHES "approximately 76 (maximum 80)". The
     binding constraint is not the cap. At 393x410 — the Definition of Done's
     249px keyboard fixture — the whole region below the transcript has to lose
     75px of its 301, and the 44px document bar, the 59.6px footer, the gaps and
     the frame's own bottom padding all stay, by the same DoD. That leaves 60
     for the dock and 12 for the gap above it, and 44px tap targets inside 8px
     of padding is exactly 60. Nothing in the DoD puts a floor under the
     composer; every clause in it prefers a smaller one.

     WHY `position: fixed`, AND WHY THE FRAME RESERVES ITS HEIGHT. The dock has
     to sit at `bottom: calc(var(--kb, 0px) + env(safe-area-inset-bottom))`:
     that is the issue's instrument for the iOS keyboard, which OVERLAYS the
     page instead of shrinking the layout viewport, so `100dvh` does not move
     and an in-flow composer cannot be told the keyboard is there at all. See
     static/js/keyboard-inset.js for where `--kb` comes from. A fixed element
     occupies no space, so `.chat-page .main-layout` below gives back exactly
     `--sim-dock-h` as `padding-block-end` — otherwise the dock lands on top of
     `.chat-footer`.

     `var(--space-2)` is added to the offset so the card floats clear of the
     screen edge rather than butting against it. It costs the conversation
     nothing: it sits inside the 16px the frame already reserved below the dock.

     `inset-inline`, never `left`/`right`: the product ships Urdu (#840). */
  .input-area {
    position: fixed;
    inset-inline: var(--space-4);
    bottom: calc(var(--kb, 0px) + env(safe-area-inset-bottom, 0px)
                 + var(--space-2));
    /* Above the page, below the document sheet (70) and its backdrop (71) and
       well below the modals (100) — design-system.css writes that order down.
       The sticky bar is 50 and lives at the opposite end of the frame, so the
       two never meet. */
    z-index: 40;
    /* `min-block-size`, not `block-size`: the dock is exactly `--sim-dock-h`
       at rest (44px row + 8px above and below, measured), and it has to be
       able to grow when `#composer-hint` appears as a second row — a fixed
       height would clip the one sentence the composer has to explain itself
       with. It grows UPWARD, because the bottom edge is the anchored one. */
    min-block-size: var(--sim-dock-h);
    /* ...and it may not grow one pixel further than the gap the frame left
       above it. `.chat-page .main-layout` below reserves
       `--space-4 + --space-3 + --sim-dock-h` and the dock sits `--space-2`
       off the bottom, so the clear space between `.chat-footer`'s bottom edge
       and the dock's top edge is exactly `--space-4 + --space-3 − --space-2`
       = 20px, and the dock's ceiling is 60 + 20 = 80 — the Definition of
       Done's cap, arrived at from the frame rather than typed in.
       The comment under `.chat-page .main-layout` used to warn that if the
       reserve and the dock stopped naming the same number the dock would eat
       the footer. Between the red commit and this one it DID: `#composer-hint`
       made the dock 103.5px and it painted over 23.5px of `.chat-footer` and
       over two of the five sample rows of #793's affiliation disclosure. This
       line is why that can no longer happen whatever lands inside the dock —
       the invariant is enforced by the layout, not by arithmetic done once in
       a comment. The hint row below is sized to fit inside it with 3.2px to
       spare, so the clip is a backstop and not a mechanism.
       What the ceiling protects is the FRAME — footer, disclosure, doc route,
       nav. It does not, on its own, protect the composer: `.input-area` is
       `overflow: visible`, so content that exceeds the ceiling does not clip,
       it paints outside the white card and off the bottom of the screen while
       every geometric clause still reads green. Forced with `line-height: 6`
       the dock measured 571-651 (80.0px, capped) with the hint painted at
       615-703 on a 659px screen. That is why `_assert_composer_is_a_dock`
       asserts `scrollHeight <= clientHeight` on every un-scrollable part of
       the band: the ceiling makes the failure quiet, and that assertion is
       what makes it loud again. Raise anything inside this dock and the gate,
       not a screenshot, is what will tell you. */
    max-block-size: calc(var(--sim-dock-h) + var(--space-4) + var(--space-3)
                         - var(--space-2));
    align-items: center;
    /* Pixel-perfect pass (28 Sep): the canvas's capsule, 7 + 44 + 7 + two 1px
       borders = 60 = `--sim-dock-h`, so the dock is exactly as tall as before
       and every #1074 number holds. */
    padding-block: 7px;
    padding-inline: var(--space-4) 7px;
    /* The 8px gap is the ROW's, between the field and the two controls beside
       it. The hint is a second row and pays for its own separation out of its
       line box; an 8px row gap on top of that is 8px of the 20px the dock has
       to grow into, spent on nothing. */
    row-gap: 0;
  }

  /* --- The hint, as a row of the dock (substory #1074 + #794) -------------
     #794's hint is a full-width row of the composer (see the template). In a
     dock capped at 80px that row has 20px to live in, and it arrived using
     35.5px — 19.5px of line box inside 16px of its own padding, plus the
     dock's 8px row gap. 103.5px of composer, and the review that caught it
     measured the dock's top edge at 547.5 against a footer at 511.4-571.0.
     So the row is rebuilt to the budget:

       12px/1.4  = 16.8px of line box, and `--text-sm`/1.5 = 19.5 would fit
                   too — 12px is chosen because the hint is a caption under
                   the field, not a peer of the 16px the field itself must
                   keep for iOS. Written as `var(--text-xs)`, which IS that
                   12px (`variables.css:39` declares `0.75rem` against the
                   `html { font-size: 16px }` at `base.css:8`). The textarea's
                   `16px` two rules below is a literal on purpose and this one
                   is not, and the difference is whether anything would tell
                   you: dropping the FIELD below 16px makes iOS zoom the page
                   on focus, which no test on this branch can see, so that
                   number must not follow a token. This one is checked — a
                   token moved to 16px makes the row 22.4px, the dock's 80px
                   ceiling clips it, and `_assert_composer_is_a_dock`'s
                   overflow clause reds on `scrollHeight > clientHeight`. A
                   token that moves here is caught, so the token is used.
       nowrap    + ellipsis: the height has to be a CONSTANT, not a function
                   of how long the sentence is in the locale the person reads.
                   Measured at 12px against a 337px content box: fr 252px is
                   the longest, then hi 242, it 213, en 198, ur 142, ko 127,
                   zh 100.8 — nothing is ellipsised today, and the declaration
                   is what stops an eighth locale wrapping to two lines and
                   re-breaking the cap silently.
       padding   8px, and an equal NEGATIVE margin. This is the one line worth
                 reading twice. `overflow: hidden` clips at the PADDING box, so
                 the padding is the ink's room — Urdu's Nastaliq needs 33px of
                 ink for a 16.8px line box, measured, and would lose its
                 descenders in a box sized to the line alone. The negative
                 margin gives that room back to the layout, so the row COSTS
                 16.8px and CLIPS at 32.8. With the padding and no margin the
                 dock is 92.8px and over the cap; with `overflow: visible`
                 instead of the padding, Urdu paints outside the card.
       pointer-events: none — the negative top margin makes the row's box
                 overlap the field's bottom 8px, and a caption must not take
                 taps away from the field it is captioning.

     Measured with this rule, hint visible, dock top 574.2 against a footer
     ending at 571.0: band 76.8px in /en/ /it/ /fr/ /ur/ /hi/ /ko/ /zh-hans/,
     at 393x659 and 393x410, with the disclosure, the doc route and the nav
     all at 25/25. */
  .input-area .composer-hint {
    font-size: var(--text-xs);
    line-height: 1.4;
    padding-block: var(--space-2);
    margin-block: calc(-1 * var(--space-2));
    padding-inline: var(--space-1);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    pointer-events: none;
  }

  /* One line, and it stays one line. `rows="3"` is deliberately left in the
     markup: above the breakpoint the composer is still the three-row box it has
     always been, and "the desktop layout is fine and this issue must not touch
     it". The height is capped here, inside the query, and the field scrolls
     internally for a long message exactly as it already did — measured at #792,
     `scrollHeight 594` against `clientHeight 81`.

     `min-block-size` is not decoration: the base rule (`:492`) says
     `min-height: 60px`, and a `min-height` beats a `block-size`, so without
     this line the field renders 60px tall and overflows the dock's own padding.
     Measured that way before it was added. */
  .input-area textarea {
    flex: 1 1 0;
    min-inline-size: 0;
    block-size: var(--sim-tap);
    min-block-size: var(--sim-tap);
    padding-block: 10px;
    padding-inline: 0;
    line-height: 1.5;
    /* 16px exactly, not `var(--text-md)` and never less: below 16px iOS Safari
       zooms the page when the field takes focus, and a zoomed page has no fold
       left to speak of. Written as a literal so a future token change cannot
       quietly drop under the threshold. */
    font-size: 16px;
  }

  /* Bug #1120: the placeholder is one step smaller than the field. The dock
     shows exactly one line (a 44px field with 10px of block padding), and the
     placeholder was never re-measured against it. At 390x844 the Urdu
     sentence is 234px on one line in a 233px content box, so it wraps, and
     its verb lands on a second line that the field cuts in half. Italian did
     the same in a 216px field before the 28 Sep pass widened it.

     14px is the issue's own "reduce the placeholder's font-size by one step".
     A placeholder paints only while the field is empty, so this applies only
     then. The field itself stays 16px, so what the person TYPES keeps the
     size that stops iOS zooming on focus (see the rule above). The
     placeholder's font-size has no part in that. The line-height inherits as
     the unitless 1.5, so it is 21px, inside the one line the field shows.
     Measured on beta at 390x844, keyboard closed and open (336px): every
     shipped locale is one line. The widest are Urdu at 204px and French at
     201px of 233px, so a translation ~14% longer than today's longest still
     fits.

     Two alternatives were rejected. `nowrap` + `text-overflow: ellipsis` is
     not used because in Urdu the ellipsis would eat "بتائیں", *tell*, which
     is the request itself. A shorter string is not used because the copy
     belongs to #710, and a string that fits one locale breaks again on the
     next. apps/chat/tests/test_beta_bug1120_composer_placeholder_e2e.py
     measures all seven locales and holds the 14px floor. */
  .input-area textarea::placeholder {
    font-size: 14px;
  }

  /* `--sim-tap` (design-system.css:52) is the design system's minimum
     comfortable touch target; holding the paperclip to exactly that leaves the
     rest of the row for the action. Taken from the token rather than written as
     44px so the paperclip follows the system if the target ever changes.
     (`.site-nav-menu-btn`, layout.css:110, still hardcodes it — that predates
     this and is #794's neighbourhood, not this bug's.) */
  .input-area .upload-btn {
    flex: 0 0 var(--sim-tap);
    /* Substory #1074: the composer is one row now, so the controls sit beside
       the field rather than under it. `flex-end` was row two's alignment and
       would push the paperclip to the bottom of the dock. */
    align-self: center;
    padding-inline: 0;   /* Bug #840: was padding-left/padding-right, both 0. */
    /* Bug #830: `flex-basis` set the width to 44 and nothing set the height,
       so this was a 44x40 target — measured on deployed beta, and corroborated
       by apps/chat/tests/test_beta_bug982_zero_document_route_e2e.py:39, which
       records h=40 for this button. `min-height` rather than `height` so the
       box can still grow if a locale or a future badge needs the room; it may
       never shrink below the token again.

       The glyph goes to 20px HERE, not in the `.upload-btn` block above,
       because only this selector is specific enough to beat
       `.input-area button { font-size: var(--text-md) }` at :456 — (0,2,0)
       against (0,1,1). That is the whole reason the 1.2rem removed above was
       inert. Written as a literal: `--text-lg` is 18px (variables.css:43), so
       there is no token that resolves to the 20px the issue asks for, and
       pointing at a token that is 2px short would be the same silent miss in
       a new form. With `line-height: 1` and the 12px block padding inherited
       from `.input-area button`, 20 + 12 + 12 lands the box on 44 on its own;
       `min-height` is the floor that survives someone changing the padding. */
    min-height: var(--sim-tap);
    font-size: 20px;
  }
  /* Substory #1074: send sizes to its own word instead of taking the rest of a
     row of its own — `flex: 1` was row two's share, and in a single row it
     would leave the field about 60px. `min-block-size` is the clause that was
     RED on `main`: the button measured 43px, one pixel under the target floor,
     because its height came entirely from 12px of block padding and a 19px
     line box. The padding goes to zero and the floor becomes the token, so the
     box is 44 whatever the locale does to the word inside it.

     Logical, not physical: `min-inline-size` is the axis that swaps in Urdu,
     and #840 counts every physical declaration in this area. */
  .input-area button:not(.upload-btn) {
    flex: 0 0 auto;
    align-self: center;
    min-block-size: var(--sim-tap);
    min-inline-size: var(--sim-tap);
    padding-block: 0;
    padding-inline: 0;
  }

  /* The dock's reserve. `.main-layout` (layout.css:293) already carries the
     frame's bottom edge and its `env(safe-area-inset-bottom)` padding; this
     adds the height the dock no longer takes in the column, plus the gap above
     it. Scoped to `.chat-page` because `--sim-dock-h` is declared there and
     `.main-layout` is on pages that have no dock — and `padding-block-end`
     rather than `padding-bottom` for #840's reason, even though this axis does
     not mirror: the count of physical declarations in this area is the thing
     being reduced. Logical and physical longhands cascade together, and this
     selector is more specific than layout.css's, so it wins.

     If this and `.input-area`'s `min-block-size` ever stop naming the same
     variable, the dock eats the footer. That is the whole reason the number is
     a variable — and, because a variable shared by two rules is still only a
     convention, the reason `.input-area` also carries a `max-block-size`
     written out of the same three tokens as the reserve below. The dock can
     grow into the gap this padding leaves above it and can no longer grow
     past it. */
  .chat-page .main-layout {
    padding-block-end: calc(var(--space-4) + env(safe-area-inset-bottom, 0px)
                            + var(--space-3) + var(--sim-dock-h));
  }

  /* --- Documents: a bottom sheet and a docbar (substory #804) --- */
  /* The panel stops being a row of the column and becomes the sheet's body.
     `.sim-sheet` (design-system.css) supplies the surface, the cap and the
     open/closed transform; everything here is the panel shedding the sidebar
     geometry it wears above the breakpoint.

     `overflow-y: auto` is kept deliberately, not inherited by luck: the sheet
     is capped at 70dvh and the list is unbounded, so this element is the
     scroller. Without it the documents past the cap are unreachable — which is
     the defect on `main`, moved inside the sheet.

     The id raises specificity over both `.doc-panel` rules (components.css:362
     and layout.css:281, the 20dvh strip cap), so source order is not doing the
     work here. */
  .sim-sheet #doc-panel {
    width: auto;
    max-height: none;
    min-height: 0;
    flex: 1 1 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    background: none;
    border-radius: 0;
    box-shadow: none;
    padding: var(--space-2) var(--space-4) var(--space-4);
  }
  /* The sheet header already says "Documents". */
  .sim-sheet #doc-panel h3 { display: none; }

  /* The bar's own presentation is the design system's (`.sim-docbar` there
     turns itself on below this same breakpoint). What belongs to this page is
     only its behaviour as a row of the app frame: `.chat-container` is a flex
     column, so without this the bar is a shrinkable item and a loaded
     transcript squeezes it under its own 44px tap target. */
  .chat-container > .sim-docbar { flex: none; }
  .upload-btn .sim-count { display: inline-flex; }

  /* Upload progress reports inside the bar once there is a bar to report into
     (renderDocbar() in landing.html moves the node, below the breakpoint
     only). Empty it is a nameless gap, so it takes no room until it has
     something to say. */
  .sim-docbar #upload-progress { padding: 0; }
  /* Substory #1074: the same rule, unscoped. Before the first upload of a
     session there is no docbar to move into, so the node renders as a row of
     the column — and empty it is 16px of padding and nothing else, plus the
     12px gap the column puts above it. 28px of a 410px screen, saying nothing.
     The DoD's whole-region accounting at 393x410 does not close with it.

     This hides no upload INFORMATION, which is what the issue forbids: the
     node stays in the DOM — the beta gate asserts exactly that and asserts
     nothing about its height — and `:empty` stops matching the moment it has
     something to say. The line above has applied this reasoning inside the
     docbar since #804; this removes the scope, it does not invent the rule. */
  #upload-progress:empty { display: none; }

  /* --- Bubbles (moved verbatim from the retired 700px block) --- */
  /* 95%, not the base rule's `min(85%, 70ch)`: on a phone the transcript is
     already narrower than a 70ch measure, so the desktop cap would only take
     width away from a column that has none to give. The base rule's
     `width: fit-content` still applies here (bug #820) — a short bubble is as
     wide as its text at 390px too — and 95% is the ceiling it wraps at. */
  /* #796: one option per line on a phone, each still a pill as wide as its
     label -- see `.welcome-chips` above. */
  .welcome-chips {
    flex-direction: column;
    align-items: flex-start;
  }

  .msg {
    max-width: 95%;
    padding: 13px 16px;
    border-radius: 18px;
    line-height: 1.5;
  }
  /* The canvas's measures at 390: the reader's own message stops short of the
     far edge so it reads as a reply, the answer takes a little more. */
  .msg.user { max-width: min(82%, 300px); }
  .msg.system,
  .msg.analysis {
    padding: 15px 17px;
    line-height: 1.55;
  }
  .msg h1 { font-size: 1.1em; }
  .msg h2 { font-size: 1em; }
  .msg h3 { font-size: 0.95em; }
  .msg pre { font-size: 0.85em; }
}

/* Substory #1257: the collection notice steps aside on a PORTRAIT phone
   viewport under 560px tall — the keyboard-open shape. Never in landscape,
   where that height is the whole screen with no keyboard open. Why, and why
   that is safe, is written under `.chat-footer` above. */
@media (max-width: 760px) and (max-height: 559.98px) and (orientation: portrait) {
  .chat-footer > .privacy-collection-notice { display: none; }
}

/* --- Substory #1196: the desktop sidebar and context panel ------------------
   Not rendered below 1400px: the page there is exactly what it was. 1400 and
   not 1200 because #820 measures the ANONYMOUS page at 1280 and 1440 and
   holds the chat column to 60% of the viewport and the field to 50%: with a
   260px sidebar beside the 240px document panel, 1280 gives 57% and 46% (red)
   and 1440 gives 61% and 52%. The canvas is drawn at 1440. */
.chat-sidebar,
.chat-context { display: none; }

@media (min-width: 1400px) {
  .chat-page .main-layout {
    display: grid;
    grid-template-columns: 260px minmax(0, 1fr) auto;
    grid-template-rows: auto minmax(0, 1fr);
    /* `--space-3`, today's gap: at 1440 anonymous the chat column is
       1440 - 32 - 260 - 240 - 24 = 884px (61.4%) and the field ~754px (52.4%)
       against #820's 60% / 50%. A 20px gap leaves 60.3% / 51.3%. */
    column-gap: var(--space-3);
    row-gap: var(--space-4);
  }
  .chat-sidebar {
    grid-column: 1; grid-row: 1 / span 2;
    display: flex; flex-direction: column; gap: var(--space-6);
    min-block-size: 0; overflow-y: auto;
  }
  .chat-page .chat-container { grid-column: 2; grid-row: 1 / span 2; }
  .chat-context {
    grid-column: 3; grid-row: 1;
    inline-size: 336px;
    display: flex; flex-direction: column; gap: var(--space-4);
  }
  /* The document list keeps its own card and its own tests (#804, #820:
     bounded, beside the chat, never to the bottom edge) when there is no
     case: then it is the only place the uploads are shown, at its 240px.
     With a case the context panel says what is owed and links to Documenti,
     where the full list and its actions live -- the canvas draws no list
     here. Kept, it got what the panel left: 119px at 1440x900, one card
     showing, cut (#1196 visual check, 29 Sep). What it carried still
     arrives: each document's outcome in the chat (the batch summary names
     the failed ones and why) and the panel refreshes on every classified or
     failed document; Retry and Remove are on Documenti, one link away. */
  .chat-page #doc-panel { grid-column: 3; grid-row: 1 / span 2; align-self: start; max-block-size: 100%; }
  .chat-page .main-layout:has(.chat-context) #doc-panel { display: none; }
}

.chat-kicker {
  font-size: 11px; font-weight: var(--font-bold); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--sim-muted); margin: 0;
}
.chat-pathway {
  display: flex; flex-direction: column; gap: 8px;
  padding: 15px; background: var(--sim-surface);
  border-radius: 14px; box-shadow: var(--sim-shadow-sm);
}
.chat-pathway__name { font-size: 14px; font-weight: var(--font-semibold); color: var(--sim-ink); line-height: 1.35; margin: 0; }
.chat-pathway__meta { font-size: 12px; color: var(--sim-ink-2); line-height: 1.45; margin: 0; }
.chat-progress {
  block-size: 5px; border-radius: var(--radius-full);
  background: var(--sim-line); overflow: hidden;
}
.chat-progress__fill {
  display: block; block-size: 100%; inline-size: 0;
  background: var(--sim-ok); border-radius: var(--radius-full);
  transition: inline-size .3s ease;
}
.chat-card {
  display: flex; flex-direction: column; gap: 12px;
  padding: 18px; background: var(--sim-surface);
  border-radius: 16px; box-shadow: var(--sim-shadow);
}
.chat-next__title { font-size: 17px; font-weight: var(--font-semibold); line-height: 1.35; letter-spacing: -0.015em; color: var(--sim-ink); margin: 0; }
.chat-next__text { font-size: 13px; line-height: 1.5; color: var(--sim-ink-2); margin: 0; }
.chat-next__text:empty { display: none; }
.chat-next__action { justify-content: center; }
/* `hidden` loses to a component's own `display` (`.sim-btn` is inline-flex,
   `.sim-count` inline-flex): without these the empty action button and a
   badge reading nothing both stayed on screen -- the #1196 harness caught it. */
.chat-next__action[hidden],
.sim-nav__item .sim-count[hidden] { display: none; }
.chat-dossier, .chat-steps { display: flex; flex-direction: column; gap: 10px; padding-inline: 2px; }
.chat-dossier__head { display: flex; align-items: baseline; gap: 8px; margin: 0; }
.chat-dossier__title, .chat-steps__title { font-size: 13px; font-weight: var(--font-semibold); color: var(--sim-ink); margin: 0; }
.chat-dossier__count, .chat-dossier__fields { font-size: 12px; color: var(--sim-ink-2); margin: 0; }
.chat-dossier__chips { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.chat-dossier__chips:empty { display: none; }
.chat-steps { padding-block-start: var(--space-4); border-block-start: 1px solid var(--sim-line); }
.chat-price {
  display: flex; flex-direction: column; gap: 8px;
  padding: 16px; background: var(--sim-soft); border-radius: 14px;
}
.chat-price__head { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 13px; font-weight: var(--font-semibold); color: var(--sim-ink); }
.chat-price__head .sim-icon { inline-size: 15px; block-size: 15px; color: var(--sim-muted); }
.chat-price__text { font-size: 12px; line-height: 1.5; color: var(--sim-ink-2); margin: 0; }
.chat-price__text strong { color: var(--sim-ink); font-weight: var(--font-semibold); }

/* --- The desktop frame (substory #710) --------------------------------------
   Above the breakpoint the page used to be a document that scrolled: the
   transcript was capped at 60vh and the rest of a tall window went to margin,
   while on a 800px one a long conversation pushed the composer below the fold
   (the page measured 869px of 800). Now the page is the same kind of frame the
   mobile block above builds: the viewport's height, and the transcript — the
   one row with `flex: 1` — takes what the header, the banner, the composer and
   the footer leave it, and is the only thing that scrolls.

   The same three `body:has(> .chat-page)` rules as below the breakpoint, for
   the same reason: signed in, the nav is a sibling of `.chat-page`, so the
   BODY has to be the frame. Unlike the mobile frame there is no
   `overflow: hidden`: `.messages` keeps its `min-height: 300px` here, so on a
   window too short to hold the column the page scrolls rather than clipping
   the composer.

   The reading column. #710 caps the conversation at about 700px. The
   transcript panel keeps its width (#820's floors measure the chat column and
   the composer, and they stay), and its inline padding grows instead, so the
   bubbles sit in a centred 700px column: the assistant on its start edge, the
   applicant on its end edge, 700px apart at most. `max()` keeps the ordinary
   `--space-4` wherever the panel is narrower than that. */
@media (min-width: 761px) {
  body:has(> .chat-page) {
    height: 100dvh;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  body:has(> .chat-page) > .site-nav { flex: 0 0 auto; }
  body:has(> .chat-page) > .chat-page { flex: 1 1 auto; }

  .chat-page {
    height: 100dvh;
    min-height: 0;
  }
  .chat-page .main-layout,
  .chat-container,
  .messages-wrapper { min-height: 0; }

  .messages {
    padding-inline: max(var(--space-4), calc((100% - 700px) / 2));
  }

  /* Pixel-perfect pass (28 Sep): the capsule sits on the reading column's
     axis instead of spanning the whole chat column -- a little wider than the
     700px of text, as on claude.ai. 900 and not 700 because #820 holds the
     field to half the viewport at 1280 and 1440
     (test_beta_bug820_desktop_width_e2e.py): measured, 860 left it 706px at
     1440 (49.0%, red); 900 leaves it ~746px. */
  .input-area {
    inline-size: min(100%, 900px);
    margin-inline: auto;
  }
  .chat-footer { text-align: center; }

  /* Bug #1146: #1074's rule, above the breakpoint too. Before the first upload
     of a session the node is empty, and empty it was 16px of padding plus the
     12px column gap above it, between the transcript and the composer: 28px
     of a 768px window saying nothing, on the screen whose fourth starting
     option did not fit. The node stays in the DOM, and `:empty` stops matching
     the moment an upload reports into it. */
  #upload-progress:empty { display: none; }
}

/* --- Bug #1146: the arrival screen on a short desktop window ----------------
   At 1366x768 the four starting options and the rest of the welcome block did
   not fit in the transcript with the composer: `.messages` held 509px of 625
   (en), the family option overhung its edge by 5px, and the "or write it in
   your own words" way to start was below the fold with nothing to say so. The
   page header was already gone (pixel-perfect pass) and the composer pill is
   62px, so what was left to give back was vertical rhythm: the frame's
   padding, the column's gaps and the welcome block's own spacing.

   Spacing only. No content, id, class or data attribute is removed or
   renamed (#710's rule), and no control drops under its 44px target: every
   number here is a margin, a padding or a gap. Scoped to short windows, so a
   tall desktop (1440x900 and up) keeps the canvas's rhythm exactly. 860 and
   not 768 because the original report was at 1512x816. Measured after, on
   the bug's harness: see test_bug1146_arrival_options_fit_e2e.py. */
@media (min-width: 761px) and (max-height: 860px) {
  .chat-page .main-layout { padding-block: var(--space-2); }
  .chat-page .chat-container { gap: var(--space-2); }
  .messages { padding-block: var(--space-2); }
  .chat-footer { padding-block-start: var(--space-1); }

  .welcome { padding-block-start: 0; margin-bottom: var(--space-3); }
  .welcome-title { margin-bottom: 6px; }
  .welcome-lead { margin-bottom: var(--space-3); }
  .welcome-how { padding-block: var(--space-3); margin-bottom: var(--space-3); }
  .welcome-kicker { margin-bottom: var(--space-2); }
  .welcome-kicker--plain { margin-bottom: var(--space-2); }
  .welcome-steps { gap: var(--space-2); }
  .welcome-compose { margin-block-end: var(--space-2); }
}
