/**
 * ATITHI GLASS — the guest app's visual system.
 *
 * Loaded last, after tokens.css and daylight.css, so it owns the palette and the surfaces.
 *
 * ## The idea
 * ONE product, ONE palette — and the palette is the WELCOME SCREEN'S (css/welcome.css). The
 * concierge is the screen a guest lands on immediately after tapping "Let's Get Started", and it
 * has to read as the next page of the same stationery, not as a different application: warm cream
 * ground, deep royal indigo, soft lavender, a restrained champagne accent, fine-line botanical
 * texture at the edges.
 *
 * This replaced the "alpine glass" system — warm sand, a deep-teal anchor, a glacier-turquoise
 * accent, borrowed from the manager dashboard (css/manager.css). That was internally coherent and
 * measured, but it made the two guest-facing screens read as two products, and the guest's first
 * impression is the one that has to survive. The dashboard keeps its own palette: it defines
 * --brand itself and never reads this ramp.
 *
 * ## The token names say what they are
 * Every downstream rule in chat.css / cards.css / orb.css / daylight.css reads --indigo, --violet,
 * --glass, --ink and so on, so re-hueing here re-hues the whole app without touching a downstream
 * file: the names are the seam, not the hues.
 *
 * Those names used to be --teal and --aqua, inherited from the retired alpine palette and left in
 * place through the re-hue on the argument that renaming them would mean a wide diff "to achieve
 * exactly nothing visible". It achieved something worth having: a file that said --teal while
 * painting royal indigo, and --aqua while painting lavender, cost every later reader a lookup and
 * had already misled one audit into believing this palette was teal. The real footprint turned out
 * to be three files (this one, orb.css, daylight.css) and ~115 call sites, all mechanical — the
 * "six stylesheets" estimate was never re-checked after orb.css and daylight.css were split out.
 * admin.html has its own unrelated --teal in an inline <style>; it is a separate cascade and was
 * deliberately left alone.
 *
 * Also kept: the CONTRAST DISCIPLINE. Every ink below was re-measured against the surface it
 * actually lands on rather than swapped by eye, and the ratio is recorded beside it.
 *
 * ## The rules this system holds itself to
 *  1. TWO colours, and only two: the indigo anchor and the champagne/lavender accent. Everything
 *     else is the cream/ivory ramp at different depths. If something needs emphasis it gets a
 *     brighter surface, not a new hue.
 *  2. The dark band is structural. The dashboard's rail is what stopped its cream-on-cream screen
 *     reading as one dull field; the guest app has no rail, so the HERO carries that job. Without
 *     a light/dark division every downstream colour decision has nothing to push against.
 *  3. NO photography in the UI itself. The mountains are drawn (see THE MOUNTAINS), the botanical
 *     texture is a masked line drawing, and both are atmosphere — so nothing ever needs a scrim to
 *     stay readable, and the screen never becomes a photograph with text on it.
 *  4. Surfaces are PAPER, not glass. A pane means "this is a distinct thing you can act on", and it
 *     reads as hotel stationery: a near-opaque ivory face, a hairline, a soft warm shadow. The blur
 *     is still there but it is now a whisper — frosted-glass depth on every card is what made the
 *     old screen read as a generic app chrome rather than as a premium hospitality product.
 *  5. Nothing is pure white. Pure-white cards on a warm ground are what make a warm palette read
 *     as clinical, because the warmth then survives only in the pixels that aren't a card.
 *
 * ## The signature
 * The orb — a real glass sphere lit white from within, indigo glass and nothing else. The
 * voice made visible, and the only bold element on the page. Everything else stays quiet for it.
 */

/* ---------------------------------------------------------------------------------------------
 * Palette. Overrides tokens.css for both the default and the explicit light attribute (light is
 * what index.html stamps before paint). Dark keeps its own deeper canvas below.
 * ------------------------------------------------------------------------------------------- */

:root,
:root[data-theme="light"] {
  color-scheme: light; /* warm ivory ground — form controls must follow */

  /* ---------------------------------------------------------------------------------------
   * THE WELCOME SCREEN'S PALETTE (css/welcome.css), carried into the concierge.
   *
   * Warm cream ground · deep royal indigo anchor · soft lavender · restrained champagne.
   *
   * The four values the welcome screen owns are reproduced EXACTLY, because the two screens are
   * seen four seconds apart and a near-miss is worse than an obvious difference:
   *   #4a2340  the plum     (--w-indigo)   · #ffe4d5  the wash     (--w-lavender)
   *   #fffaf6  the ivory    (--w-ivory)    · #f2603f  the ridge    (--w-gold)
   *
   * What this palette has to do that the welcome screen's does not: hold a DENSE surface. The
   * welcome screen is one card on a photograph; this is a header band, a transcript of bubbles,
   * grounding cards, a pill lane and a dock, all of which need to separate from each other. Two
   * consequences, both deliberate:
   *
   *  1. The GROUND is not the ivory. #fff7f0 as the page ground would leave every ivory card
   *     invisible on it. The ground is the welcome screen's own "behind the card" tone (its
   *     --w-ivory-edge, #f7ece1) taken one step deeper, so a card still reads as an OBJECT laid
   *     on paper. The separation is 1.22:1 against the card face, against 1.46:1 for the old
   *     sand — deliberately less, because on this palette the hairline and the soft warm shadow
   *     do the work that raw value separation used to, which is what makes it stationery rather
   *     than a stack of tiles. If a surface ever looks lost, deepen the GROUND, never whiten
   *     the card.
   *  2. The ridge accent is DECORATIVE ONLY at its true value (#f2603f is 2.9:1 on the card face —
   *     nowhere near readable). Anything champagne-coloured that has to be READ uses the darker
   *     --gold-ink step below. Same split the welcome screen makes for its eyebrow line.
   * ------------------------------------------------------------------------------------- */

  /* The ground. Warm cream, a step below the card face so a card floating on it reads as an
     object (1.25:1) rather than dissolving into it — see note 1 above. --canvas-lift is the same
     paper one step up, and is what the dock and the native select popups are built from. */
  --canvas: #f7e9dd;
  /* One step DEEPER than the card face's bottom stop (#fdf4ee), not equal to it. When these two
     matched, a capability card dissolved into the panel it sat on — the "suggestions aren't
     visible" report — because --card-face fades to exactly the ground it is laid on. The fix is
     the one this file's own palette note prescribes: deepen the GROUND, never whiten the card. */
  --canvas-lift: #fbede4;

  /* ---------------------------------------------------------------------------------------
   * THE ACCENT — soft lavender-indigo. Four steps, each measured for the job it does:
   *     --violet       the FILL (the live dot, the halo). Never a large area.
   *     --violet-lit   the light end — the live dot ON the indigo band, dark-mode ink.
   *     --violet-deep  ICONS and small text on paper: 9.0:1 on the card, 7.4:1 on the ground.
   *     --violet-glow  the halo alpha, carried over from the old ramp's 0.34 unchanged.
   *
   * The names are the old turquoise ramp's and stay that way on purpose — see the file header.
   * ------------------------------------------------------------------------------------- */
  --violet: #c0492b;
  --violet-lit: #ff9b78;
  --violet-deep: #c0492b;
  --violet-glow: rgba(192, 73, 43, 0.32);

  /* THE WORDMARK'S OWN ACCENT — "Vani"'s two grounds (paper, band). Deliberately its own pair,
     not an alias of --violet/--violet-lit: those are shared with the orb cue, the live dot and
     every card icon, so re-tuning them would recolor the whole app's accent system just to make
     one word more orange. --vani skews further from red toward true orange (hue ~24° vs
     --violet's ~11°). Both steps darkened a second pass for a deeper, richer orange —
     --vani-lit is the one guests actually see, since #hero-top .wm-v (below) uses it
     unconditionally, on the photo pre-chat and the solid plum band once chat starts; it stops at
     #e87a30 rather than going darker still because the plum band is very dark (~3% luminance) and
     a color much past this floors out under the 3:1 large-text contrast a darker step would risk.
     --vani has no such ceiling — its ground (cream, the drawer) is light, so it goes darker still. */
  --vani: #9c3a07;
  --vani-lit: #e87a30;

  /* Deep royal indigo — the anchor. The welcome screen's --w-indigo verbatim, and the hue the
     header band, the orb and every primary action are built from. --indigo-lit is the hover step
     and is the welcome CTA's own --w-indigo-lift, so hover is a palette value here too and not
     an eyeballed one; --indigo-ink is its pressed step (--w-indigo-deep). */
  --indigo: #4a2340;
  --indigo-lit: #5c3350;
  --indigo-ink: #36192e;
  /* Ink ON the indigo fill. Named for the fill it sits on, and NOT --accent-ink: js/theme.js sets
     --accent / --accent-ink inline from the tenant's own accent, which beats every stylesheet, so
     a primary action filled with the fixed palette indigo and inked with a tenant-derived colour
     measured 1.8:1 on the old ramp and would do the same here. This one is 13.9:1 on --indigo and
     10.4:1 on the --indigo-lit hover, and no tenant can move it. Warm ivory, not white — the same
     ink the header band uses over the same hue. */
  --on-indigo: #fff3ea;
  --ember: #c2571f;

  /* Champagne. Decorative at full value, readable at --gold-ink — see note 2 above. These four
     override css/tokens.css's bronze ramp, which nothing outside that file reads, so redefining
     them here keeps any future consumer on this palette rather than the retired one. */
  --gold: #f2603f;
  --gold-lit: #ff9b78;
  --gold-deep: #a33b21;
  /* The readable warm accent: 6.3:1 on the card face, 5.9:1 on the ground. Both AA. */
  --gold-ink: #a33b21;
  --gold-glow: rgba(242, 96, 63, 0.3);
  /* The welcome card's wash, verbatim. Icon circles, the guest's own bubbles, quiet washes. */
  --lavender: #ffe4d5;
  --lavender-deep: #ffd9c4;

  /* The massif, redrawn for AtithiVani: three ridgelines in ATMOSPHERIC PERSPECTIVE rather than
     the two hard-edged polygons and a snow triangle this token used to carry. The far layer is
     ridge-far at 0.22 and heavily blurred, the middle is ridge-near at 0.38 and lightly blurred,
     and only the near layer — the header band's own plum, so the two still read as one object —
     is crisp. That blur gradient IS the change: flat polygons at any opacity read as a generated
     decoration, and haze is what makes a range read as distance.

     Same viewBox and same `preserveAspectRatio='none'` as before, so every consumer's sizing is
     untouched. No `%` anywhere inside the data URI: a literal percent that isn't a valid escape
     (`-20%'`, `offset='0%'`) is what breaks these in Safari, so the filter regions are left at
     their defaults and the gradient stops use 0/1 instead. */
  --hero-range: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 260' preserveAspectRatio='none'%3E%3Cdefs%3E%3Cfilter id='h1'%3E%3CfeGaussianBlur stdDeviation='7'/%3E%3C/filter%3E%3Cfilter id='h2'%3E%3CfeGaussianBlur stdDeviation='3'/%3E%3C/filter%3E%3CradialGradient id='sg'%3E%3Cstop offset='0' stop-color='%23F2603F' stop-opacity='.35'/%3E%3Cstop offset='1' stop-color='%23F2603F' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Ccircle cx='890' cy='62' r='85' fill='url(%23sg)'/%3E%3Cpath filter='url(%23h1)' opacity='.22' fill='%23FF9B78' d='M0,168 C150,135 300,148 420,122 C540,96 620,132 760,112 C880,96 980,129 1080,116 C1140,108 1180,119 1200,114 L1200,260 L0,260 Z'/%3E%3Cpath filter='url(%23h2)' opacity='.38' fill='%23F2603F' d='M0,195 C180,161 320,177 460,151 C580,129 680,164 800,145 C920,125 1000,155 1100,143 C1150,137 1180,145 1200,142 L1200,260 L0,260 Z'/%3E%3Cpath opacity='.92' fill='%234A2340' d='M0,221 C160,187 300,205 440,174 C560,150 660,191 820,166 C940,148 1040,182 1140,168 C1170,163 1190,169 1200,166 L1200,260 L0,260 Z'/%3E%3Cellipse cx='440' cy='176' rx='30' ry='7' fill='%23FFF3EA' opacity='.38'/%3E%3Cellipse cx='820' cy='168' rx='22' ry='5' fill='%23FFF3EA' opacity='.3'/%3E%3C/svg%3E");

  --brand: var(--violet);
  --brand-dark: var(--violet-deep);
  --brand-light: var(--violet-lit);
  /* Brand-coloured TEXT. This is the one place the accent becomes champagne rather than lavender:
     it inks .prov (a card's "where this came from" line), and a whisper of gold is what has always
     marked those. --gold-ink, not --gold: the fill value is 2.1:1 on the card. It used to ink
     .cap-pill-book too — see the note on that class for why a gold capability card had to stop. */
  --brand-ink: var(--gold-ink);
  --brand-wash: rgba(192, 73, 43, 0.1);
  --brand-wash-strong: rgba(192, 73, 43, 0.18);
  --accent: var(--indigo);
  --accent-ink: #fff3ea;
  /* --pine / --pine-ink were remapped to the indigo anchor here. Their only consumer anywhere is
     tokens.css's `--ok: var(--pine)`, so that remap's single effect was to paint the status-green
     token brand-indigo in light only — see the SOLID STATUS FILLS note below, which now sets --ok
     directly. Dropped rather than re-pointed at a green: a token with no consumer left is just
     another name a reader has to chase. */
  --on-brand: #fff7f1;

  /* ---------------------------------------------------------------------------------------
   * SURFACES — hotel stationery, not frosted glass.
   *
   * The alphas went UP and the blur went DOWN (0.78 -> 0.9 and 18px -> 12px). On the old sand
   * ground a 0.78-alpha pane with an 18px blur read as a nice frosted tile; on cream it reads as
   * every AI chat product shipped since 2023, which is the one thing this screen must not look
   * like. Near-opaque ivory over a hairline and a soft warm shadow reads as paper. The blur is
   * kept rather than removed so a card scrolling over the mountains still shows depth — it is
   * now a whisper instead of the material itself.
   *
   * NOTHING here is pure white: pure-white cards on a warm ground are exactly what makes a warm
   * palette read as clinical, because the warmth then survives only in the pixels that aren't a
   * card.
   * ------------------------------------------------------------------------------------- */
  --glass: rgba(255, 250, 246, 0.9);
  --glass-2: rgba(255, 250, 246, 0.96);
  --glass-3: #fffcf9;
  /* The hairline is PLUM-tinted, not neutral: it is what carries the card edge now that the
     value separation from the ground is gentler (note 1 at the palette block). */
  --glass-line: rgba(74, 35, 64, 0.1);
  --glass-line-strong: rgba(74, 35, 64, 0.2);
  --blur: 12px;

  /* Surfaces read the glass so every downstream rule in chat/cards inherits the system. */
  --bg: var(--canvas);
  --paper: var(--canvas-lift);
  --paper-2: #fbede4;
  --paper-warm: #fffaf6;
  --surface: #fffaf6;
  --surface-2: #fbede4;
  --surface-3: #f7e9dd;
  --surface-lift: #fffcf9;
  --card-face: linear-gradient(178deg, #fffdfb 0%, #fdf4ee 100%);
  --bubble-in: #fffaf6;
  /* The guest's own words: the welcome card's lavender. It is the accent's quietest form and the
     one place a large area of it is right — a bubble is small and there is only ever one hue of
     them on screen. */
  --bubble-out: var(--lavender);

  /* Ink, indigo-tinted rather than neutral, so the type belongs to the palette instead of sitting
     on top of it. Measured on the card face / on the ground:
       --ink        15.2:1 / 12.9:1   --ink-soft  6.9:1 / 5.8:1   --ink-faint  6.1:1 / 5.1:1
     All three clear AA on both, which is what lets a card and the bare ground use the same inks. */
  --ink: #2a1226;
  --ink-soft: #5b3a54;
  --ink-faint: #7a5a72;
  --hairline: var(--glass-line);
  --hairline-strong: var(--glass-line-strong);

  /* The deep royal indigo band. In the dashboard the anchor is a rail; here it is the hero's
     chrome. It is what gives the page a real light/dark division to push against — without it the
     cream-on-cream screen reads as one dull field. Built from --indigo's own three steps so the band
     and every primary action are visibly the same object family. */
  --header-bg: linear-gradient(168deg, #5c3350 0%, #4a2340 52%, #36192e 100%);
  --header-ink: #fff3ea; /* 13.6:1 on the band's mid stop */
  --header-ink-soft: #e6c9d9; /* 8.9:1 — supporting copy on the band */
  --header-line: rgba(255, 255, 255, 0.14);
  /* The champagne hairline under the band. Decorative: it is 1px tall and reads as the gilt edge
     on hotel stationery, which is exactly the welcome card's ornament doing the same job. */
  --header-rule: linear-gradient(90deg, transparent, var(--gold) 22%, var(--gold-lit) 50%, var(--gold) 78%, transparent);
  --ghost-bg: rgba(255, 250, 246, 0.9);
  --ghost-bg-hover: #fffcf9;
  --ghost-line: rgba(74, 35, 64, 0.2);

  /* Shadows are indigo-tinted, never neutral black — black on cream greys the warmth out of
     everything it touches. Softer and shallower than the old set: on stationery the shadow says
     "this sheet is lying on that one", not "this widget floats". */
  --lift: 0 16px 40px -22px rgba(74, 35, 64, 0.28);
  --shadow-1: 0 1px 2px rgba(74, 35, 64, 0.06);
  --shadow-2: 0 6px 16px -10px rgba(74, 35, 64, 0.16);
  --shadow-3: 0 24px 56px -26px rgba(74, 35, 64, 0.3);

  /* Elevation for objects that need a real contact edge — a card, a popover, a floating pill —
     which --shadow-2/3's wide negative spread doesn't leave them. Two-stop, same recipe as
     .cap-pill's own hand-tuned shadow: a tight stop so the object sits ON the sheet below it, a
     soft wide one so it reads as lifted paper rather than a bordered rectangle. --elev-2 is
     byte-identical to .cap-pill's existing shadow on purpose, so adopting the token there is a
     provable no-op. */
  --elev-1: 0 1px 2px rgba(74, 35, 64, 0.05), 0 6px 14px -12px rgba(74, 35, 64, 0.22);
  --elev-2: 0 1px 2px rgba(74, 35, 64, 0.05), 0 10px 20px -16px rgba(74, 35, 64, 0.28);
  --elev-3: 0 2px 6px rgba(74, 35, 64, 0.06), 0 22px 48px -24px rgba(74, 35, 64, 0.34);

  --scroll-thumb: rgba(74, 35, 64, 0.24);
  --scroll-thumb-hover: rgba(74, 35, 64, 0.4);
  --scroll-track: rgba(74, 35, 64, 0.06);

  /* Status. The three semantic pairs are UNCHANGED — each ink was already cleared against its own
     tint, they carry meaning rather than brand, and a warning that changes hue with the theme is a
     warning a guest has to re-learn. Only --info, which is the palette's own quiet surface rather
     than a semantic state, follows the cream ramp. */
  --ok-bg: #e5f1ea;
  --ok-ink: #1f6647;
  --ok-line: #2f7d5b;
  --warn-bg: #fbf1da;
  --warn-ink: #8f3d10;
  --warn-line: #c2571f;
  --danger-bg: #fbe9e6;
  --danger-ink: #96302f;
  --danger-line: #b23b3b;
  --info-bg: #fbede4;
  --info-ink: var(--ink-soft);
  --info-line: var(--glass-line);

  /* The SOLID status fills, for the few places a status is a filled object rather than a tinted
     surface: .safety-call-cta's tap-to-call pill and the emergency row's border (css/cards.css).
     They are declared HERE, in the same block as the pairs above, because leaving them out is
     what produced a real bug: they fell through to tokens.css, where `--ok: var(--pine)` met this
     file's own `--pine: var(--indigo)` remap and painted the safety card's call button deep
     INDIGO in light — while dark, which tokens.css does re-set, painted the same button green.
     One semantic token, two hue families, on the app's most trust-critical surface.
     Each is its own family's `-line` value, the saturation already measured for an edge, which is
     also the right weight for a small filled pill. White-on-fill measured: --ok 4.99:1,
     --danger 5.86:1 (both AA at any size; the pill is 700-weight besides). */
  --ok: var(--ok-line);
  --warn: var(--warn-line);
  --danger: var(--danger-line);
  --info: var(--violet-deep);

  --focus-ring: 0 0 0 3px rgba(74, 35, 64, 0.45);

  /* Type. The display serif is BACK, and it is the welcome screen's: Cormorant Garamond carries
     the brand and the greeting there, and self-hosted Latin subsets are already shipped
     (css/fonts.css), so the pairing costs no extra bytes. It was retired when this file's earlier
     revision wanted a single sans; that was the decision that left the concierge reading as a
     chat product while the welcome screen read as a hotel. The @font-face unicode-ranges are Latin
     only, so a Bengali or Devanagari greeting still falls through to the Noto faces — which is
     correct, Cormorant has no Bengali. daylight.css already asks for --font-serif on #greeting;
     removing the override below is what lets that rule finally take effect. */

  /* The orb: a glass sphere lit from within, sized to carry the composition rather than float in it.
     `34vmin` measured the wrong things. At 1280x720 it hit the 200px cap inside a 672px pane that
     also holds the band, greeting, sub-line, two status lines, chips and dock — 49px more than
     existed, and the overflow landed as the orb's labels painted over the capability pills. Now
     `34cqi` is the pane's own width (the `pane` container) and `21vh` the vertical budget, so the
     sphere is the largest circle fitting both: 163px in a 480px column, 133px on a 390px phone. */
  /* THE touch floor, used by every control a thumb aims at. 48px is Material's minimum and 4px
     over Apple's 44pt; the dock ran 34-36px and the pill lanes 38px before this existed. */
  --tap: 48px;

  /* ---------------------------------------------------------------------------------------
   * THE DOCK'S FLANKING CONTROLS — the type button and the safety button, at ONE size.
   *
   * They are meant to be twins (see THE TYPE BUTTON below), and they were not: the type button
   * drew a 38px ring inside a 48px target and safety a 40px ring inside a 48x53 column, so the
   * pair read as two different controls at two different weights, and both sat at the touch
   * FLOOR rather than comfortably above it. Measured in the browser before this change:
   * type 48x48 (38px visual), safety 48x53 (40px visual).
   *
   * 54px is the visual control and, being the button's own box too, also the touch target — 6px
   * clear of the 48px floor rather than exactly on it, and inside the 52-56px band a thumb hits
   * without aiming. One token, read by both, so they cannot drift apart again.
   *
   * The mic does NOT read this: it stays the largest object in the row by a wide margin (72px at
   * rest, see THE MIC'S TWO SIZES) because it is the primary control and the whole point of the
   * voice-first dock. 54 was chosen partly so that ratio survives — 72:54 still reads as
   * "one bold control between two quiet ones", where 72:64 would not.
   * ------------------------------------------------------------------------------------- */
  --dock-ctl: 54px;

  /* The mic, sized per mode by the blocks further down (welcome ~96px, active 64px). This base
     value is what a mode-less render gets. */
  --orb-size: 96px;
  /* --orb-fill / --orb-ring / --orb-glow / --orb-label-bg / --orb-label-ink used to live here.
     Nothing read them — the sphere is painted by the rules at THE SIGNATURE below — and they
     still carried the marigold light this palette no longer uses, so they described an orb that
     had not existed for two redesigns. Same mistake as the dead --orb-size override this file's
     own notes call out. Deleted rather than recoloured. */
}

/* Dark mode keeps the same two colours, swapped: the indigo deepens to become the ground, and the
   lavender accent lifts so it still reads as lit against it. The champagne is left alone — gold on
   a dark ground is the one accent that needs no adjustment, and it is what keeps a dark render from
   going cold. */
/*
 * THE PREMIUM DARK PALETTE — a deliberate palette in its own right, not the light theme inverted.
 * Deep navy/near-black ground (never pure black), the brand's own night anchor as the interaction
 * colour (a brighter "lit" step for hover/gradient highlights), a warm coral for icon ink and small
 * accents, and the decorative warm step held to what it was — decorative only, never a fill. Every
 * consumer below reads these same token NAMES the rest of this file already uses, so re-tuning them
 * here re-skins the whole app without touching a downstream rule.
 *
 * These are AtithiVani's NEEL values — the brand system's own night pairing for Sondhya, not a
 * hand-darkened version of the light palette.
 */
:root[data-theme="dark"] {
  --canvas: #161014;
  --canvas-lift: #21171e;
  --brand-dark: #593651;
  /* The soft blue ridge step — the small-accent/border step, not the bright interaction colour. */
  --brand-light: #f59372;
  --brand-ink: #ffb89d;
  /* --violet is the FILL (live dot, halo, orb ripple). It is declared HERE, which it was not
     before: light's fill is a deep coral tuned for cream, and a deep coral on a near-black ground
     is too dim to read as "live". Neel's own accent is the lighter coral, which is what that step
     is for. */
  --violet: #ff9b78;
  /* --violet-deep is the ICON step read by every card glyph and dock control; the palette's own
     light coral is what keeps them legible without competing with the anchor fills. */
  --violet-deep: #ffb89d;
  --accent: #40293b;
  --accent-ink: #fbefe6;
  --lavender: #352331;
  --lavender-deep: #4c3148;
  --glass: rgba(51, 33, 46, 0.72);
  --glass-2: rgba(51, 33, 46, 0.88);
  --glass-3: #33212e;
  --glass-line: rgba(245, 147, 114, 0.16);
  --glass-line-strong: rgba(245, 147, 114, 0.28);
  --card-face: linear-gradient(178deg, #33212e 0%, #2c1d28 100%);
  --surface: #33212e;
  --surface-2: #2c1d28;
  --surface-3: #21171e;
  --surface-lift: #412a3c;
  --paper: #21171e;
  --paper-2: #181116;
  --paper-warm: #33212e;
  --bubble-in: #33212e;
  --bubble-out: #352331;
  --ink: #fbefe6;
  --ink-soft: #fdbda1;
  --ink-faint: #f08161;
  /* The lighter "lit" end of a gradient (the operator-card media header, the orb's own glass sheen)
     reads the brighter coral; --violet-deep above stays the ICON step. */
  --violet-lit: #ffb89d;

  /* THE WORDMARK'S OWN ACCENT, dark theme — see the light block's note. --canvas is near-black
     here, so both grounds (canvas, band) need the bright end; --vani reuses the light theme's
     --vani-lit value (same reuse pattern --violet already follows) and --vani-lit steps a touch
     lighter again for the band. Darkened alongside the light block, same floor logic: the dark
     band (#40293b) is as dark as the light band, so --vani-lit stops at #f08c44 rather than
     matching --vani's drop exactly. */
  --vani: #e87a30;
  --vani-lit: #f08c44;
  --indigo: #40293b;
  --indigo-lit: #593651;
  --indigo-ink: #2d1e29;
  --on-indigo: #fbefe6;
  /* Second pass, lighter and more opaque still than the first (rgba(64, 41, 59,.66)->rgba(33, 23, 30,.86)):
     that pass fixed the band going fully invisible but still left the wave foot (THE BAND'S FOOT,
     below) too close to --canvas to read as its own shape once chat starts and there is no photo
     behind the glass. --indigo-lit at the top stop, --indigo at the bottom — the SAME two steps
     the light theme's own header-bg is built from, one rung up from --canvas instead of down from
     white, so the band is unmistakably a lighter object laid over the page rather than a tint of
     it. The crest on it is the same canonical orange/yellow lockup the light theme shows — there
     is no dark-theme logo (see THE ONE LOGO, below). */
  --header-bg: linear-gradient(180deg, rgba(89, 54, 81, 0.82) 0%, rgba(64, 41, 59, 0.92) 100%);
  --header-ink-soft: #fdbda1;
  --ghost-bg: rgba(251, 239, 230, 0.06);
  --ghost-bg-hover: rgba(251, 239, 230, 0.12);
  --ghost-line: rgba(245, 147, 114, 0.18);
  --focus-ring: 0 0 0 3px rgba(255, 184, 157, 0.5);
  /* Soft, not heavy — lower alpha than the light theme's own shadow set, since a hard black shadow
     over a near-black ground reads as a smudge rather than lift. */
  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.22);
  --shadow-2: 0 6px 18px -8px rgba(0, 0, 0, 0.3);
  --shadow-3: 0 24px 56px -24px rgba(0, 0, 0, 0.4);
  --scroll-thumb: rgba(251, 239, 230, 0.18);
  --scroll-thumb-hover: rgba(251, 239, 230, 0.3);
  --scroll-track: rgba(251, 239, 230, 0.05);

  --header-ink: #fbefe6;
  --header-line: rgba(245, 147, 114, 0.14);
  --hairline: var(--glass-line);
  --hairline-strong: var(--glass-line-strong);

  /* Muted green — a quiet privacy/success indicator, not a bright status-page green. */
  --ok-bg: #142720;
  --ok-ink: #7fd8a8;
  --ok-line: #3e8f63;
  --warn-bg: #2e2411;
  --warn-ink: #f0cd7f;
  --warn-line: #8a6a22;
  --danger-bg: #331b18;
  --danger-ink: #f4a9a1;
  --danger-line: #8c4340;
  --info-bg: #33212e;
  --info-ink: var(--ink-soft);
  --info-line: var(--glass-line);

  /* Same fills as light, on purpose — a filled status pill is a small bright object on a dark
     ground, and dark's own `-line` steps are too dim to work as a fill (they're tuned for drawing
     edges). Holding one fill per status across both themes is also what "a warning does not change
     hue with the theme" asks for. */
  --ok: #3e8f63;
  --warn: #c2571f;
  --danger: #b23b3b;
  --info: var(--violet-deep);
}

/* ---------------------------------------------------------------------------------------------
 * The canvas. One colour, two blooms of the same hue, fixed so nothing parallaxes. No image.
 * ------------------------------------------------------------------------------------------- */

body {
  background: var(--canvas);
  color: var(--ink);
}

body::before {
  content: "";
  position: fixed;
  inset: -20% -10%;
  z-index: -1;
  pointer-events: none;
  background:
    /* A warm champagne deepening low on the right where the hands are, and the welcome screen's
       own lavender wash top-left — its wide-screen background is `radial-gradient(120% 80% at 50%
       0%, --w-lavender, transparent)` over the ivory edge, and this is that move adapted to a
       screen whose top is occupied by the indigo band. The warm one is the GROUND's own hue, not
       the accent: it is what keeps the atmosphere cream rather than violet. Nothing else — the
       ground is a ground. */
    radial-gradient(70% 48% at 88% 88%, rgba(196, 162, 96, 0.16) 0%, transparent 64%),
    radial-gradient(64% 46% at 6% 14%, rgba(192, 73, 43, 0.1) 0%, transparent 62%);
}

/* Dark mode: the same two blooms in the palette's own purple/indigo rather than cream and lavender.
   At the light values above these are warm pale washes, which on a near-black ground read as haze
   over the UI; here they are a faint cinematic lift in the corners the chrome does not occupy. */
:root[data-theme="dark"] body::before {
  background:
    radial-gradient(70% 48% at 88% 88%, rgba(120, 57, 94, 0.16) 0%, transparent 64%),
    radial-gradient(64% 46% at 6% 14%, rgba(245, 147, 114, 0.08) 0%, transparent 62%);
}

/* ---------------------------------------------------------------------------------------------
 * THE BOTANICAL TEXTURE — the welcome card's own line art, at the edges of the concierge.
 *
 * Same asset, same technique: brand/botanical-sprig.svg painted as a CSS MASK (not an <img>), so
 * the champagne tone and the opacity live here next to each other and the path data stays in
 * exactly one file. It is already precached by the service worker for the welcome screen, so this
 * costs no request and nothing offline. No new asset, no new markup, no new element.
 *
 * Two sprigs, on ONE pseudo-element via two mask layers, and where they sit is the whole point:
 *
 *  - Right edge, upper third — clear of the indigo band above it and to the SIDE of the greeting
 *    cluster, which is centred. It is the same corner the welcome card puts its first sprig in.
 *  - Left edge, foot — mostly behind the dock, which is a translucent blurred gradient, so it
 *    reads as texture *in* the paper the dock is made of rather than as a drawing next to it.
 *
 * Nothing is placed in the middle third, which is where the transcript lives — point 4 of the brief
 * this revision answers is that the chat surface stays a clean cream field.
 *
 * ## HOW FAR OFF THE EDGE, and why it is not a round number
 * The overhangs are a FRACTION of each drawing's own width, not a fixed px, because the drawings are
 * clamp()ed and a fixed overhang means a different fragment shows at every viewport size.
 *
 * The right one is 0.62 of its width, and that number was measured rather than judged. #greeting-sub
 * is `max-width: 34ch`, centred, so its longest line grows with the viewport — and the drawing's ink
 * occupies roughly x 10..150 of its 200-wide viewBox, so where the ink starts on screen is
 * `viewport + overhang - 0.95 * width`. At 0.29 (the first attempt) the ink began at x=278 on a
 * 375px screen while the subtitle's glyphs reached x=291: thirteen pixels of leaf behind the last
 * word of the greeting, which is exactly the thing this decoration must never do. Measured with a
 * Range over the text nodes, not the block box — the block box is full-width and would have said
 * "overlap" at any offset.
 *
 * At 0.62 the visible fragment is the stem and mid-leaves clipped by the screen edge. Measured
 * clearance between the subtitle's last glyph and the first ink, welcome mode:
 *
 *     320   360   375   390   414   430
 *      —    26px  34px  39px  48px  54px
 *
 * 320 has no figure because the subtitle is hidden there (the max-height: 620px tier above). 360 is
 * the tightest and it is the smallest width we ship to, which is the right place for the margin to
 * be smallest. Re-measure if the greeting copy, its measure, or the display size changes — these are
 * observations, not guarantees.
 *
 * `opacity: 0.1` matches the welcome card's 0.11 (a hair lower, because the ground here is a step
 * deeper than that card's ivory and the same alpha read a touch stronger on it). `position: fixed`
 * rather than absolute: #chat-pane is a 100dvh flex column that does not scroll as a whole, so
 * fixed means the texture is stable and cannot parallax against the pane it decorates.
 * ------------------------------------------------------------------------------------------- */

body::after {
  /* The two drawing widths. Named, because both the height AND the overhang are derived from them —
     an SVG mask with a viewBox and no width/height has an intrinsic ratio but no intrinsic size, so
     `auto` for the second value is at the engine's discretion. 1.35 is the drawing's own 270/200. */
  --sprig-a: clamp(150px, 40vw, 230px);
  --sprig-b: clamp(126px, 34vw, 196px);

  content: "";
  position: fixed;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  opacity: 0.1;
  background-color: var(--gold);
  -webkit-mask-image: url(/brand/botanical-sprig.svg), url(/brand/botanical-sprig.svg);
  mask-image: url(/brand/botanical-sprig.svg), url(/brand/botanical-sprig.svg);
  -webkit-mask-repeat: no-repeat, no-repeat;
  mask-repeat: no-repeat, no-repeat;
  -webkit-mask-position:
    right calc(var(--sprig-a) * -0.62) top 17%,
    left calc(var(--sprig-b) * -0.4) bottom calc(var(--sprig-b) * -0.34);
  mask-position:
    right calc(var(--sprig-a) * -0.62) top 17%,
    left calc(var(--sprig-b) * -0.4) bottom calc(var(--sprig-b) * -0.34);
  -webkit-mask-size:
    var(--sprig-a) calc(var(--sprig-a) * 1.35),
    var(--sprig-b) calc(var(--sprig-b) * 1.35);
  mask-size:
    var(--sprig-a) calc(var(--sprig-a) * 1.35),
    var(--sprig-b) calc(var(--sprig-b) * 1.35);
}

/*
 * ACTIVE MODE: the texture retreats into the corners.
 *
 * The welcome placement is tuned around the greeting cluster, and in active mode that cluster is
 * gone — the transcript occupies the same band instead. `top 17%` puts the right sprig's ink at
 * y≈146 on a 390x844 phone, and #log starts at 112: the leaves would sit directly behind the first
 * reply, which is the one thing this decoration must never do (and no amount of low opacity makes
 * line art behind body copy a good idea).
 *
 * So in active mode both sprigs move to the extreme corners the conversation cannot reach — the
 * right one drops to the foot beside the dock, the left one stays at the foot — and the whole layer
 * dims by a fifth. The margins still carry the botanical, the middle of the screen is clean cream,
 * and the transcript is unambiguously the subject. Nothing about the asset, the technique or the
 * sizes changes; this is two positions and one opacity.
 *
 * `body:has(...)`, because the mode lives on #app and #app is body's CHILD — the texture is body's
 * own pseudo-element, so there is no descendant selector that can reach it from the mode. The
 * alternative was to move the layer onto #app::after; :has() is the smaller change and keeps the
 * layer on the element Phase 1 put it on.
 */
body:has(#app[data-mode="active"])::after {
  opacity: 0.085;
  /*
   * The right sprig moves to the MIDDLE of the right edge rather than to the foot, and further off
   * screen: 0.74 of its width overhangs, so what shows is ~60px of leaf tips against the margin.
   *
   * The revision before this one sent both sprigs to the bottom corners, and the measured result was
   * that neither was visible at all — the foot of the screen is the dock (a blurred gradient over
   * them) and the top is the indigo band, so a conversation ran on flat cream with the texture hidden
   * under its own chrome at both ends. The brief for this pass calls that "too plain", and it is.
   *
   * Beside the transcript rather than behind it: the ink stays in the outer ~60px, the layer is
   * `z-index: -1` under opaque bubbles, and at 0.085 champagne it is a grain in the paper. The middle
   * of the screen is still clean cream.
   */
  -webkit-mask-position:
    right calc(var(--sprig-a) * -0.74) top 46%,
    left calc(var(--sprig-b) * -0.4) bottom calc(var(--sprig-b) * -0.34);
  mask-position:
    right calc(var(--sprig-a) * -0.74) top 46%,
    left calc(var(--sprig-b) * -0.4) bottom calc(var(--sprig-b) * -0.34);
}

/*
 * ...AND THE ATMOSPHERE COMES OUT FROM BEHIND THE CHROME.
 *
 * body::before's two blooms are placed for the welcome composition: champagne low-right at 88% 88%,
 * lavender high-left at 6% 14%. In active mode those are exactly the two places the guest cannot see
 * — the dock covers the first and the header band covers the second — so the one layer that gives the
 * page depth was doing nothing for the whole conversation.
 *
 * Same two hues, same technique, moved into the band the transcript occupies and softened by a fifth.
 * Very soft and very wide: 0.13 champagne and 0.085 lavender over ~60% of the pane each, which reads
 * as warmth in the paper rather than as a gradient. Nothing is added — this is two positions and two
 * alphas on a layer that already existed.
 */
body:has(#app[data-mode="active"])::before {
  /*
   * THREE WASHES, and they are the welcome photograph's own light rather than a new idea: the milky
   * haze that sits in the valley, the warm cream of the sky, and the muted forest of the conifers in
   * the foreground. Paint order is back to front, as the picture is.
   *
   * Every alpha is between 0.07 and 0.5-on-near-white, i.e. at the threshold where you would notice
   * it missing but never notice it arriving. The lavender bloom of the welcome placement is dropped
   * here on purpose: cream + haze + green is three hues in one field already, and a fourth is what
   * turns atmosphere into a gradient.
   */
  background:
    /* The MILKY HAZE — near-white, wide, high. Over the cream ground it reads as light in the room
       rather than as a colour, which is what keeps the top of the transcript from looking flat. */
    radial-gradient(92% 52% at 50% 26%, rgba(255, 253, 251, 0.5) 0%, transparent 70%),
    /* The warm cream deepening on the right, where the hands are. The GROUND's own hue. */
    radial-gradient(58% 44% at 86% 54%, rgba(196, 162, 96, 0.12) 0%, transparent 66%),
    /* The FOREST — a desaturated pine, low and left, well under the reading line. Green is new to
       this file and deliberately muted to within a step of the ground: at full saturation any green
       on cream reads as a different product. */
    radial-gradient(62% 44% at 10% 80%, rgba(86, 104, 82, 0.085) 0%, transparent 66%);
}

/* Dark mode: the in-conversation atmosphere, recoloured the same way the welcome placement above
   is. The light rule's first stop is a near-white "milky haze" — on a near-black transcript ground
   that is exactly the pale fog a dark theme must not show, so all three become faint indigo/purple
   lifts instead of lightening washes. */
:root[data-theme="dark"] body:has(#app[data-mode="active"])::before {
  background:
    radial-gradient(92% 52% at 50% 26%, rgba(56, 35, 51, 0.4) 0%, transparent 70%),
    radial-gradient(58% 44% at 86% 54%, rgba(120, 57, 94, 0.14) 0%, transparent 66%),
    radial-gradient(62% 44% at 10% 80%, rgba(37, 24, 34, 0.28) 0%, transparent 66%);
}

/*
 * THE MOUNTAIN IS NOT PAINTED HERE. A `body` background layer carrying `hero-mountain.jpg` under a
 * cream wash used to sit at this point in the file, as a SECOND way of drawing the range for active
 * mode. It is deleted, and the reason is worth keeping so it is not reintroduced.
 *
 * There is exactly one mountain layer in this app — `.hero-scene__img`, the real <img> in the hero
 * (see THE HERO SCENE below) — and active mode now keeps it instead of hiding it. Two layers meant
 * two different-looking mountains either side of the `data-mode` flip: a full-strength 187px band
 * confined to the 480px pane, replaced ~64ms later by a washed full-window layer. That is the flash.
 * One layer cannot disagree with itself.
 */

/* Dark mode: the same botanical drawing, dimmer. Champagne line art at light's own 0.1 becomes one
   of the brighter things on a near-black ground, so it keeps a presence just above nothing. */
:root[data-theme="dark"] body::after {
  opacity: 0.05;
}

/* A landscape phone has ~340px of pane and the leaves would land on the composer and the greeting
   both. Texture is the first thing that should go when there is no room for it. */
@media (max-height: 500px) {
  body::after {
    display: none;
  }
}

/* =============================================================================================
 * THE HERO SCENE — Kanchenjunga at first light, as a section of the composition.
 *
 * ## Why a photograph, when this file's own rules said "no photography"
 * That rule was written against the state it replaced: a full-bleed hotel photo behind the WHOLE
 * hero, with the lockup, the language control and the greeting laid on top of it under a scrim.
 * That is a poster with UI on it — every colour decision downstream then has to survive whatever is
 * in the pixels, and nothing reads as designed.
 *
 * A photograph as a BANDED SECTION is a different object. Nothing is laid over it, so nothing needs
 * a scrim; it occupies a fixed slice of the column between the greeting and the capability sheet;
 * and it gives up its height first when there is none to spare. It also does something the drawn
 * massif could not: the drawn one is a two-polygon silhouette, and at any size above a hairline it
 * reads as clip-art rather than as distance. Layered ridges, mist and alpenglow are the whole
 * subject of a premium travel composition and they cannot be faked with two polygons.
 *
 * The drawn massif is NOT deleted. It is exactly right for the one job it still has: the 24px ridge
 * tucked under the band in active mode, where the transcript is the subject and a photograph would
 * be an intrusion. See the ACTIVE MODE block below.
 *
 * ## What the scene has to respect
 *  1. It is a SECTION, not a background. Its top edge dissolves into the cream so it emerges out of
 *     the paper rather than starting on a line, and its foot is overlapped by the capability
 *     sheet's rounded corners — the sheet is stationery laid on the desk over the view.
 *  2. It never carries type. Not the greeting, not the lockup, not a control. That is the whole
 *     reason it needs no scrim and the whole reason the palette below it still holds.
 *  3. It is the FIRST thing to go. A short pane, a landscape phone, or an active conversation all
 *     take it away entirely — see the height ladder and ACTIVE MODE below.
 * ============================================================================================= */

.hero-scene {
  /* Bleed to the pane edges: #hero's own --sp-4 inset is for type, and a photograph inset from the
     column reads as an illustration pasted into a document rather than as a view. */
  position: relative;
  flex: 0 0 auto;
  margin: var(--sp-3) calc(-1 * var(--sp-4)) 0;
  /*
   * THE BAND'S PROPORTION, which is the whole difference between a view and a stripe.
   *
   * Re-measured against the shipped file (img/hero-mountain.jpg is 1080x720 — a 1.5:1 crop, not the
   * 1.706:1 this comment used to claim; the asset was re-baked at some point and this rule never
   * caught up). A ratio WIDER than the file's, as 1.706 was, makes `cover` bind on width and trim the
   * photo's TOP AND BOTTOM to fill it — which on this file meant a slice of the violet upper sky and a
   * slice of the mist-filled valley, the two things that make the shot read as dawn, went first.
   *
   * 1.2:1 is narrower than the file, so `cover` binds on HEIGHT instead: the full photo top-to-bottom
   * survives — sunrise, both lit peaks and the misted valley floor all stay in frame at every width —
   * and the crop it does need falls on the left/right margins, which this file has to spare. It is
   * also simply taller: `max-height: 40vh` is what binds on a short or landscape screen (was 26vh),
   * and on a typical phone the ratio itself now produces close to that — the band goes from a 211px
   * strip to something nearer 310px at 375px wide, because the mountain is the subject of this
   * composition and a strip was giving it up. The extra height comes out of the capability sheet's own
   * scroll (documented below, THE HERO SCENE OCCUPIES THE SLACK), never out of the dock.
   */
  aspect-ratio: 1.2 / 1;
  max-height: 40vh;
  overflow: hidden;
  /*
   * CREAM -> MOUNTAIN -> CREAM. The top edge dissolves into the page so the view emerges out of the
   * paper rather than starting on a line, and the fade is over the photograph's own pale sky, which
   * is what makes the transition read as light rather than as a gradient overlay. 16%, not 30%: the
   * band is taller now, and 30% of a taller box is more of the actual mountain spent on the fade than
   * this photo can afford — 16% covers the same visual hand-off over fewer of its pixels.
   *
   * The foot stays solid. The capability sheet's rounded corners ride up over it (see #cap-panel's
   * negative margin), and a sheet of stationery laid over a window is the composition; a second fade
   * there would leave a soft band under a hard card edge.
   */
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 16%);
  mask-image: linear-gradient(180deg, transparent 0, #000 16%);
}

/* The hero's own bottom padding is air under a greeting; there is a photograph there now, and the
   sheet is what ends the section. Stated on the mode rather than on `#hero` so active mode — which
   has no scene and whose band needs its own foot clearance — is unaffected. */
#app[data-mode="welcome"] #hero {
  padding-bottom: 0;
}

/*
 * The image. With `.hero-scene`'s box now narrower than the file's own 1.5:1 (see THE BAND'S
 * PROPORTION, above), `cover` binds on height and has nothing to trim top-to-bottom — the sunrise, the
 * lit peaks and the misted valley all survive at every width this app ships. What it does trim is the
 * sides, so `center` is right for Y (there is no vertical loss to place) and X stays centred because
 * the file's two focal points — the sun low on the left, the tallest lit peak right-of-centre — sit
 * close enough to the middle that an even side-crop keeps both in frame; measured on the file at the
 * band's tightest real crop (~20% of width per side) neither one is reached.
 *
 * The gradient underneath is the offline state, and it is why the JPEGs can stay out of the
 * service-worker shell: with no network the band paints as this photograph's own dawn — warm cream
 * sky over a cool violet valley — rather than as a hole.
 */
.hero-scene__img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  object-position: center center;
  background: linear-gradient(180deg, #ffe9da 0%, #ffd9c4 44%, #a3859b 100%);
  /*
   * THE GRADE. The raw file's own sky is violet (measured, `filter: none`: sky r150 g118 b173 —
   * blue sits 23 above red), which is why `sepia()` is here at all: dropping it toward zero brings
   * the violet straight back (measured at sepia 0.1-0.18, sky blue-red is still +9 to +11). Some
   * sepia is load-bearing, not a style choice.
   *
   * Everything else this filter used to carry has been cut. Three earlier passes pushed sepia and
   * saturate UP together (0.32/1.45 -> 0.4/1.6) chasing a warmer, more vivid sky and sun — measured
   * against the reference each time, correctly, but the reference kept moving and the cumulative
   * result read as a yellow/cream wash rather than a soft sunrise: saturate(1.6) was amplifying the
   * file's own most-saturated patch (the sun disc) harder than anywhere else in the frame.
   *
   * This pass goes the other way on both axes at once — saturate DOWN, not just less-far-up
   * (1.6 -> 0.85, an actual desaturation of the graded frame, not a smaller boost) — while holding
   * sepia at roughly the same 0.35-0.4 level the violet fix has always needed. Measured:
   *
   *                    sky (r,g,b)      sky b-r    sun core (r,g,b)   sun r-b   darkest-10% (r,g,b)
   *     prior grade    148,117,139        -9         213,181,147        66        26,19,33
   *     this grade     132,113,129        -3         193,171,150        43        31,27,36
   *
   * Sky is now close to neutral (b-r only -3, neither the raw violet nor a warm push in the other
   * direction) rather than the previous grade's own warm lean. The sun's r-b gap — the clearest
   * single number for "how gold is the light source" — drops by a third. The shadow lightens
   * slightly (this pass no longer tries to keep it distinctly indigo — "do not make the image
   * purple" now applies everywhere in the frame, not just the parts a wash starts from) but stays
   * a cool grey-blue, not warm — it does not flip to match the sky/sun's own soft warmth.
   *
   * `contrast(1.08 -> 1.0)`: contrast was the second lever pushing every warm patch further from
   * its neighbours (measured directly in the fourth pass — the sun's punch tracked contrast, not
   * brightness). At 1.0 the sun and sky sit in the frame rather than jumping out of it.
   *
   * SIXTH PASS — the sky/sun numbers above were fine; the residual yellow was somewhere this grade
   * had not measured yet. Profiled the frame in horizontal bands (mean colour per row, "yellowness"
   * = (r+g)/2 - b, which is near zero for a neutral or pink-leaning tone and rises for true yellow):
   * it peaks specifically at the LIT SNOW band (25-45% down the frame, yellowness 22-25), not at the
   * sky (-10) or the mist/foot (-8 to 4). The lit peaks — the part of the image this pass is asked
   * to keep as "the visual focus" — were the actual source, not the sky or the sun disc this and
   * earlier passes kept re-measuring.
   *
   * `sepia(0.35 -> 0.25)`, `saturate(0.85 -> 0.75)`, `contrast(1 -> 0.98)`: a small step down on all
   * three, re-measured on the same band —
   *
   *                    sky b-r    lit-snow yellowness (25%/35% bands)    darkest-10% (r,g,b)
   *     prior grade      -10               22 / 25                          30,26,36
   *     this grade       -14               15 / 21                          31,27,39
   *
   * — the sky moves further from violet (not toward it), the shadow is unchanged (still a cool
   * grey-blue, not warm), and the lit-snow band is the one region that actually shifts: -7 points of
   * yellowness where the earlier passes' own measurements (sky, sun, shadow) had already looked
   * correct. `brightness(0.86 -> 0.88)` came up slightly to compensate for the desaturation reading
   * as darker overall, not as a second, independent brightness change.
   *
   * SEVENTH PASS — the sixth pass's cut landed a little past neutral: `saturate(0.75)` and
   * `contrast(0.98)` together read as muted/grey rather than a soft pastel sunrise, which is its
   * own failure mode this pass corrects for. `sepia` stays exactly where it was (0.25) — the
   * lit-snow band it was cut to fix has not moved, so there was no reason to touch the one number
   * that controls it. `saturate(0.75 -> 0.85)` and `contrast(0.98 -> 1.03)` both come back up, a
   * smaller step than the one that went down:
   *
   *                    sky yellowness    lit-snow yellowness (25%/35%)
   *     sixth pass          -14                  15 / 21
   *     this pass           -17                  19 / 24
   *
   * Sky gets a touch more colour without crossing back toward yellow (more negative, if anything);
   * lit-snow rises but stays well under the fourth pass's 22-25, i.e. short of where "too yellow"
   * was first measured. `brightness(0.88 -> 0.87)`: the added contrast lifts the frame's apparent
   * brightness slightly on its own, so brightness came down a hair to hold the same overall level
   * rather than compounding into "too bright" again.
   */
  filter: sepia(0.25) saturate(0.85) contrast(1.03) brightness(0.87);
}

/* Dark mode: a gentle grade rather than the light theme's correction, plus a directional scrim —
   see `.hero-scene::after`, right below — carries most of the "darker" job. Sepia is DROPPED here
   rather than restated: the light grade uses it to cancel the raw file's own violet sky cast, and
   for a "purple/indigo cinematic atmosphere" that cast is exactly the mood wanted, not a flaw to
   correct. Brightness is barely touched (0.88) so the ridge lines and lit snow the guest is meant
   to recognise the view by stay legible — the scrim, not this filter, is what reads as "dark". */
:root[data-theme="dark"] .hero-scene__img {
  filter: saturate(0.94) contrast(1.02) brightness(0.88);
}

/*
 * THE SCRIM — darker at the top and foot, clearer through the middle where the peaks and the
 * greeting sit, tinted with a whisper of the palette's own indigo rather than neutral black. This
 * is the "sophisticated dark gradient overlay" the mountain wants: atmosphere the UI can sit on,
 * not a filter that flattens the photograph into a silhouette.
 *
 * `.hero-scene` is already `position: relative` (base rule, above) or `position: absolute` (the
 * pre-chat "promoted to background" state, ACTIVE MODE HAS TWO PICTURES below) — either way it is
 * its own stacking context, so this pseudo-element only has to out-rank the plain `<img>` inside
 * it (z-index: auto) to sit above the photograph; #hero-copy's own z-index: 3 (outside this
 * element entirely) still lands above the whole thing.
 */
:root[data-theme="dark"] .hero-scene::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(13, 9, 12, 0.58) 0%,
    rgba(37, 24, 34, 0.22) 26%,
    rgba(37, 24, 34, 0.12) 50%,
    rgba(25, 17, 23, 0.28) 74%,
    rgba(13, 9, 12, 0.62) 100%
  );
}

/* ---------------------------------------------------------------------------------------------
 * THE ORNAMENT — hairline, diamond, hairline, in champagne.
 *
 * The welcome screen's own rule (css/welcome.css .welcome-ornament), at concierge scale. It is what
 * separates the greeting from the view below it without drawing a border, and it is the third place
 * the same rotated-square diamond appears (the privacy line and the reply's list markers are the
 * others) — one ornament, three sizes, rather than three symbols.
 *
 * A rotated square and not a glyph: ✦ / ◆ / ◇ are outside the Latin subsets css/fonts.css ships, so
 * a glyph would fall through to whatever symbol face the OS happens to have, or to a tofu box.
 * ------------------------------------------------------------------------------------------- */

.hero-ornament {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-top: var(--sp-4);
}

/* #brand-ornament reuses this exact component under the wordmark, but only in the one pre-chat
   composition that shows it (see THE LOCKUP'S OWN ORNAMENT, further down this file) — everywhere
   else (welcome mode, the compact header) it stays out of the DOM's visual flow entirely. */
#brand-ornament {
  display: none;
}

.hero-ornament::before,
.hero-ornament::after {
  content: "";
  width: clamp(28px, 9vw, 44px);
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--gold));
}

.hero-ornament::after {
  background: linear-gradient(90deg, var(--gold), transparent);
}

.hero-ornament > span {
  width: 5px;
  height: 5px;
  background: var(--gold);
  transform: rotate(45deg);
}

/* =============================================================================================
 * ACTIVE MODE HAS TWO PICTURES, AND THE CONVERSATION IS THE SWITCH.
 *
 *     PRE-CHAT   header · guest card · MOUNTAIN + the greeting over it · rail · dock
 *     IN CHAT    header · guest card · transcript on cream + botanical  · rail · dock
 *
 * ## Why the state is `#log:empty` and not `data-mode`
 * `data-mode` cannot express this, and trying to make it do so is what produced the reported flash.
 * The app enters ACTIVE mode at boot whenever the URL carries `from=welcome` — which every guest
 * arriving through the welcome screen and the demo picker does (js/app.js main(), and that call is
 * deliberate: "Get Started" promises the concierge, not a second welcome). So the guest was in active
 * mode ~64ms after load, with `.hero-scene` hidden by a `data-mode` rule, and the mountain they saw
 * was just the welcome frame going past on the way. Measured with a MutationObserver on #app
 * (temporary, since removed):
 *
 *     t=454ms  DOMContentLoaded   mode=welcome  .hero-scene display:block   480x187
 *     t=461ms  window.load        mode=welcome  .hero-scene display:block   (the frame they saw)
 *     t=525ms  data-mode->active  mode=active   .hero-scene display:none    0x0
 *
 * One mutation, one rule. No second stylesheet, no class, no media query, no pseudo-element, no image
 * fallback, and the service worker was serving a cache byte-identical to the server (all verified).
 *
 * The real question the picture answers is not "which mode" but "has the guest started talking yet",
 * and the DOM already knows: `#log` is empty until the first row is appended, whether that row comes
 * from a typed turn, a voice turn, a capability card or a replayed session. So `:has(#log:empty)` IS
 * the pre-chat state — it needs no new attribute, no new class and not one line of JavaScript, it
 * cannot disagree with the transcript it is derived from, and it flips at exactly the moment the
 * conversation begins rather than at some point during boot. `:has()` is already this file's technique
 * (see the botanical layer's `body:has(...)`).
 *
 * ## One mountain layer, still
 * The picture is `.hero-scene` — the same <img>, the same `srcset`, the same `object-position: center
 * 42%`, full strength, no overlay. Pre-chat it is promoted from a band in the flow to the BACKGROUND
 * of the hero box, so the greeting rides over it (the reference composition) and the transcript that
 * replaces it is not pushed down by anything. There is no second image, no background-image copy and
 * no new asset anywhere in the app; see the note above THE HERO SCENE for the layer that was deleted
 * to make that true, and why it must not come back.
 * ============================================================================================= */

/* ---- IN CHAT: the picture and the greeting go, the cream paper and its botanical stay ---- */

#app[data-mode="active"]:not(:has(#log:empty)) .hero-scene,
#app[data-mode="active"]:not(:has(#log:empty)) #hero-copy {
  display: none;
}

/* ---- PRE-CHAT: the hero box becomes the picture, and the greeting sits in it ---- */

/*
 * The hero takes the slack and the empty transcript gives it up. Both are `flex: 1` siblings in
 * #chat-pane's column, so without the second half of this the two would share the free space and the
 * picture would be half the size it should be — #log-wrap is a zero-content box here, so it has no
 * business holding any of it. The dock and the rail are untouched: they are `flex: 0 0 auto` and stay
 * exactly where they are in both states, which is what keeps the switch from moving any control.
 */
#app[data-mode="active"]:has(#log:empty) #hero {
  flex: 1 1 auto;
}

#app[data-mode="active"]:has(#log:empty) #log-wrap {
  flex: 0 0 auto;
}

/*
 * ...and #canvas stops competing for it too. It is #hero's `flex: 1` sibling, and the dock inside it
 * carries `margin-top: auto` to sit at the foot — so the slack was landing in that auto margin as
 * 140px of nothing between the rail and the dock, and the picture got half the height it should have.
 * `flex: 0 1 auto` makes #canvas exactly as tall as the rail + dock it holds; the auto margin then
 * resolves to zero and #hero takes everything left. Measured at 1280x720: the picture goes from 26%
 * of the pane to 47%, and the rail and dock do not move — they were already pinned to the foot.
 */
#app[data-mode="active"]:has(#log:empty) #canvas {
  flex: 0 1 auto;
}

/*
 * THE PICTURE, as the hero's background rather than a band in its column.
 *
 * `position: absolute` is what makes it a background layer: it leaves the flow entirely, so it can
 * neither push the greeting down nor take height from anything, and when the first message hides it
 * the layout does not move a pixel — the box it filled was already the height it was going to be.
 * That is the whole difference between this and a hero image, and it is why the transition cannot jump.
 *
 * The insets are NEGATIVE by exactly #hero's own padding, so the photograph bleeds to the pane's
 * edges instead of sitting inset from them like an illustration pasted into a document. #chat-pane
 * clips (`overflow: hidden`), so the bleed can never reach past the column.
 *
 * It fills the box behind the indigo header band too, and that is deliberate rather than sloppy:
 * #hero-top is opaque and carries z-index 4 (css/layout.css), so it simply paints over the top of the
 * picture — which costs nothing and means this rule needs no knowledge of how tall the band is. The
 * greeting is z-index 3 and lands on the picture; the picture is z-index 0.
 *
 * `aspect-ratio` and `max-height` are released because the box now sizes the picture rather than the
 * other way round; `object-fit: cover` on the <img> (base rule) does the rest, so nothing is squashed.
 */
#app[data-mode="active"]:has(#log:empty) .hero-scene,
#app[data-mode="welcome"] .hero-scene {
  /*
   * `display: block` re-states the default on purpose: the height ladder further down this file drops
   * the scene entirely under `@media (max-height: 600px)`, and that rule was written for WELCOME mode,
   * where the scene is a band in the flow competing with the capability sheet for a short column. Here
   * it competes with nothing — the transcript is empty and has given its height up — so on a 320x568
   * phone there are still ~250px for the picture with the greeting centred in it, and dropping it
   * would leave this state as flat cream with a greeting on it. The ladder still governs WELCOME mode
   * and the in-chat state exactly as before.
   */
  display: block;
  position: absolute;
  z-index: 0;
  top: calc(-1 * (env(safe-area-inset-top) + var(--sp-4)));
  right: calc(-1 * var(--sp-4));
  bottom: calc(-1 * var(--sp-5));
  left: calc(-1 * var(--sp-4));
  margin: 0;
  aspect-ratio: auto;
  max-height: none;
  /*
   * THE MASK DISSOLVES THE FOOT; #hero-copy's OWN BACKGROUND CARRIES THE REST OF THE READABILITY
   * JOB NOW — a split that used to live entirely here, in five stops.
   *
   * The base rule fades only the top, because in WELCOME mode the capability sheet's rounded corners
   * ride up over the foot (`#cap-panel { margin-top: -30px }`) and a second fade there would leave a
   * soft band under a hard card edge. Here the picture fills a ~500px box with a greeting centred in
   * it, and the FOOT still dissolves for the same reason: the rail below is a lane of pills on the
   * open page rather than a sheet laid over the photograph, and an undissolved foot would end the
   * picture on a hard line above them.
   *
   * The LOWER HALF used to pale for the second reason — that is where the type lands — but paling the
   * whole lower half to protect two lines of it was more of the mountain given up than the type
   * actually needed. That job now belongs to `#hero-copy`'s own soft radial wash (below), sized to
   * the greeting's own box rather than to the picture's, so a guest who has not scrolled to the
   * greeting yet — the header, the lockup, the language control — sees the photograph at close to
   * full strength. `#greeting` / `#greeting-sub` also carry their own soft white `text-shadow`
   * (css/daylight.css), so this is a second, independent layer of the same discipline, not the only
   * one.
   *
   * THE OPAQUE WINDOW NOW RUNS 12–86%, with NO partial-alpha holding stop in between — two changes
   * from the pass before this one, both aimed at the same complaint: the mountain read as faded
   * through its own upper and middle ground, right where the enlarged lockup and the language pill
   * now sit (see THE LOCKUP, above). A held mid-alpha stop across the whole lower half was tried
   * first and rejected — this photograph's midground is a BRIGHT mist-filled valley with its own
   * faint, layered ridgelines in it, and holding those at partial alpha read as a second, paler
   * mountain hovering under the first rather than as a fade (the valley's own ridges, half-revealed,
   * look like more geography, not air). Full strength for the entire 12–86% window sidesteps that
   * exact failure mode — a fully-rendered misty valley reads as weather, not as a ghost — and leaves
   * the sun, the lit peaks AND the valley below them all at full strength; only the last 14% (well
   * past the crop's treeline, into the seam with the rail below it) still dissolves, for the same
   * reason the foot has always dissolved: an undissolved foot ends the picture on a hard line above
   * the rail rather than letting it settle into the page.
   *
   * NINTH PASS — THE FADE LIVES HERE NOW, in the rule BOTH pre-chat modes share, and that is the
   * actual fix rather than a re-tune. The gradual 48%-onwards fade and the cream veil that used to
   * follow this rule were both scoped `[data-mode="welcome"]` only — but a real guest arrives in
   * `[data-mode="active"]` with an empty log (index.html's entry guard puts anyone coming through
   * /welcome/ straight into active mode), so neither ever applied to the screen a guest sees. What
   * they got was THIS window: full opacity to 86%, then everything dumped in the last 14% — the
   * "image suddenly ends on a hard transition" report, straight from the numbers. Both welcome-only
   * overrides are deleted; this one gradient is now the single source of truth for both modes.
   *
   * WHY SO MANY STOPS: a two-stop fade (`#000 X%, transparent 100%`) has a slope discontinuity at X
   * — opacity is flat, then instantly linear — and the eye reads that corner as a horizontal line
   * across the picture even though no line is drawn. That is the band that was showing up above the
   * greeting. The stops below approximate an ease-in curve: the slope starts near zero at 40% and
   * grows gradually, so there is no corner anywhere for the eye to catch.
   *
   * WHY NO COLOUR OVERLAY: the fade is done ENTIRELY in the mask (alpha), never with a cream or
   * white gradient painted on top. Alpha lets whatever is behind the scene show through — the app's
   * own background — so the picture dissolves INTO the page's existing tone instead of under a wash
   * of a colour picked by hand. That is also why the previous cream `::after` veil is gone rather
   * than merely lightened: a cream overlay on a lavender/peach photograph is exactly the white-fog
   * look this pass removes, and no alpha value makes a wrong-hue overlay right.
   */
  -webkit-mask-image: linear-gradient(
    180deg,
    transparent 0,
    #000 12%,
    #000 40%,
    rgba(0, 0, 0, 0.96) 50%,
    rgba(0, 0, 0, 0.88) 58%,
    rgba(0, 0, 0, 0.76) 66%,
    rgba(0, 0, 0, 0.62) 74%,
    rgba(0, 0, 0, 0.46) 82%,
    rgba(0, 0, 0, 0.3) 90%,
    rgba(0, 0, 0, 0.16) 96%,
    rgba(0, 0, 0, 0.08) 100%
  );
  mask-image: linear-gradient(
    180deg,
    transparent 0,
    #000 12%,
    #000 40%,
    rgba(0, 0, 0, 0.96) 50%,
    rgba(0, 0, 0, 0.88) 58%,
    rgba(0, 0, 0, 0.76) 66%,
    rgba(0, 0, 0, 0.62) 74%,
    rgba(0, 0, 0, 0.46) 82%,
    rgba(0, 0, 0, 0.3) 90%,
    rgba(0, 0, 0, 0.16) 96%,
    rgba(0, 0, 0, 0.08) 100%
  );
}

/*
 * WELCOME'S OWN FOOT FADE, and THE VEIL, both used to live here. Both are GONE, deliberately:
 *
 *   · both were scoped `[data-mode="welcome"]`, so neither ever applied to the screen a real guest
 *     sees (they arrive in `[data-mode="active"]` with an empty log — see the NINTH PASS note on
 *     the shared rule above). Two rules tuned across several passes against a composition nobody
 *     reaches is worse than no rules: they made the shared fade look already-solved.
 *   · the veil was a cream (`rgba(250,243,234,...)`) gradient painted OVER the photograph, which is
 *     the white-fog/snowy-band effect. A hand-picked cream cannot match a lavender/peach photo at
 *     any alpha; fading in the mask instead lets the app's own background show through, which is
 *     what makes the picture dissolve into the page rather than under a wash.
 *
 * The one constraint they existed to respect still holds and is handled by the shared gradient's
 * own tail: `#cap-panel` rides up into the scene by 54px at every breakpoint (`margin-top: -30px`
 * plus the scene's `bottom: calc(-1 * var(--sp-5))` bleed), and its head is `position: sticky` and
 * must stay opaque to mask cards scrolling under it — so the picture has to still be *faintly*
 * present where that head arrives, never already at zero. The shared gradient ends at 0.08 rather
 * than 0 for exactly that reason.
 */

/*
 * THE CROP, for this box specifically. The base rule's `object-position: center 42%` (above) is
 * tuned for the WELCOME band, whose `aspect-ratio: 1.706/1` is fixed regardless of viewport — a
 * wide, short box where `cover` always crops top/bottom and never left/right, so a horizontal
 * position of "center" costs nothing there.
 *
 * This box is the opposite: `position: absolute; inset` fills whatever the flex column leaves it,
 * which on a phone (and on the ≥720px centred column, which is a ~480px-wide pane running nearly
 * the full viewport HEIGHT) is consistently taller than it is wide — well short of the photograph's
 * own 1.5:1, so `cover` crops LEFT/RIGHT here, and "center" was cutting the sun disc (left of frame
 * centre in this photograph) while keeping the empty, paler mist in the middle of the shot. 24% 44%
 * is checked against the full 320-768px width range this app ships (see the PR notes): the sun and
 * a lit ridge both stay in frame at every size, rather than only at the one aspect "center" suited.
 */
#app[data-mode="active"]:has(#log:empty) .hero-scene__img,
#app[data-mode="welcome"] .hero-scene__img {
  object-position: 24% 44%;
}

/*
 * THE GREETING, lower in the picture rather than centred in the leftover space above the rail.
 *
 * `margin-top: auto` (inherited from #hero's base rule, css/layout.css) already pulls #hero-copy
 * down as far as the flex column allows; the change here is `margin-bottom`, which used to be the
 * same `auto` — splitting the leftover space evenly and landing the greeting at its midpoint (51% of
 * the viewport, measured on a 375×812 phone with the enlarged lockup above it). The reference sets
 * the greeting noticeably lower than that, closer to its own bottom third. A small FIXED bottom
 * margin instead of a second `auto` is what moves it there: the two auto edges no longer split the
 * slack in half, so it all collects above the greeting instead, and only a deliberate hairline of
 * air remains below before the seam into the rail. No positioning, no translate, so a long greeting
 * in any language still grows from the middle outwards and #hero-copy keeps its own `min-height: 0`
 * scroll behaviour.
 *
 * `margin-top: auto` is restated explicitly here rather than left to fall back to #hero's base rule
 * (layout.css) — daylight.css's generic `#app[data-mode="active"] #hero-copy` (the IN-CHAT rule,
 * with no `:has()`) sets a fixed `margin-top: var(--sp-2)`, and its selector is more specific than
 * the base rule's bare `#hero-copy`, so leaving this property unset here let that 8px win instead of
 * the auto it needs — measured pulling the greeting UP against the lockup instead of down toward the
 * rail, the opposite of this rule's entire point.
 */
#app[data-mode="active"]:has(#log:empty) #hero-copy,
#app[data-mode="welcome"] #hero-copy {
  margin-top: auto;
  margin-bottom: var(--sp-5);
  padding-top: 0;
}

/* The champagne rule under the sub-line sits closer to it. sp-4 -> sp-2: the ornament is the
   greeting block's own full stop, and 16px of air above a 5px hairline read as a gap between
   two things rather than as the end of one. 8px back to the sheet, at no cost to the mark. */
#app[data-mode="active"]:has(#log:empty) #hero-copy .hero-ornament,
#app[data-mode="welcome"] #hero-copy .hero-ornament {
  margin-top: var(--sp-2);
}

/*
 * THIS WAS WELCOME-ONLY, WRONGLY — the same class of bug as THE FADE and THE LANGUAGE PILL
 * elsewhere in this file. A real guest is in `[data-mode="active"]` with an empty log, not
 * `"welcome"` (see the entry guard in index.html), so every "the greeting is still too low" report
 * across several passes of this rule was measured against a screen nobody actually lands on — the
 * greeting was sitting at the untouched shared floor (`margin-bottom: var(--sp-5)`, 24px, from the
 * rule above) the entire time. The claim this used to make — that active pre-chat "is calibrated
 * for a different crop of the same file" — does not hold up either: both modes already share one
 * `object-position` for `.hero-scene__img` (see THE CROP, below), so there was no actual crop
 * difference to justify the split. Extended to both; the reasoning for the value itself is
 * unchanged from the passes that arrived at it:
 *
 * At the shared floor (24px) the greeting sat 71-95% down #hero (measured at 375x812) — its own
 * bottom third. `vh` rather than a fixed px value: #hero's own height is itself a `vh` floor (56vh,
 * see THE COMPOSITION), so a vh-based margin keeps the greeting at the same PROPORTION of the hero
 * box across breakpoints instead of drifting as that floor's absolute height changes. 6vh, then
 * 8vh: `margin-top: auto` on the shared rule above is what makes extra margin-bottom safe to add
 * without colliding with the language pill — it absorbs whatever slack is left ABOVE the greeting
 * first, and only once that reaches 0 does #hero grow taller to keep the requested gap below (the
 * same flex-grow that gives #hero its 56vh floor) — but that slack is finite, and 10vh, tried
 * first, spent nearly all of it on the smallest phones (gap to the language pill dropped to 8-12px).
 * 8vh keeps a clearly-higher greeting while leaving that gap at a still-modest ~24-28px on those
 * same phones — 768px and desktop have slack to spare either way.
 */
#app[data-mode="active"]:has(#log:empty) #hero-copy,
#app[data-mode="welcome"] #hero-copy {
  /*
   * 8vh -> `clamp(16px, 3vh, 28px)` (24px at 375x812), and the reasoning above is what makes the cut
   * safe rather than what it contradicts. That reasoning is about the greeting's PROPORTION down a
   * hero whose height was a 56vh floor; with the floor down to `min(38vh, 300px)` the hero now sizes
   * to its own content, `margin-top: auto` has no slack left to absorb, and 8vh (65px here) stopped
   * being "the greeting sits in the lower third" and became 65px of empty cream between the ornament
   * and the sheet — on the screen where the sheet is what the guest came for.
   *
   * `clamp`, not a bare vh: the value is now a real gap between two objects rather than a share of a
   * box, so it wants a floor and a ceiling in px.
   *
   * AND THE FLOOR IS 24px BECAUSE OF THE SHEET, not because of taste. #cap-panel rides UP over the
   * foot of the hero by 30px (`margin-top: -30px`, below), and `margin-top: auto` above pins this
   * block to the hero's BOTTOM edge — so any value under 30 puts the greeting's champagne ornament
   * on the stationery instead of on the photograph. Measured at 24px of margin: the ornament landed
   * 5px inside the sheet. The 4.5vh middle arm resolves to ~36px at 375x812, which clears it; the
   * 24px floor only ever binds below ~535px of viewport height, where the sheet stops riding up at
   * all (`@media (max-height: 600px)`) and there is nothing left to clear.
   */
  margin-bottom: clamp(24px, 4.5vh, 46px);
}

/*
 * ...AND THE GREETING IS UN-COLLAPSED, because active mode folds it away by default.
 *
 * css/daylight.css's INTERACTION MODES block sets `opacity: 0; max-height: 0; overflow: hidden` on
 * #greeting/#greeting-sub under `#app[data-mode="active"]` — the 0.35s/0.5s fade that plays when a
 * conversation starts. That is correct and stays; it is simply written against the assumption that
 * active mode always means "talking", which is no longer true for the pre-chat state. Measured
 * without this rule: #hero-copy laid out at 33px with #greeting and #greeting-sub both at height 0,
 * i.e. a picture with only the champagne ornament on it.
 *
 * So the two lines are restored, and the margins are put back to the values daylight.css's own base
 * rules give them (`#greeting { margin: 0 }`, `#greeting-sub { margin: var(--sp-3) auto 0 }`) rather
 * than to numbers invented here — the collapse rule zeroes them, so they have to be re-stated.
 * `max-height` and `opacity` are the transition's own properties, so the fade still plays in both
 * directions as the conversation starts and the state flips.
 *
 * #orb-hint stays folded. It is the mic's language hint ("English · বাংলা · हिन्दी · नेपाली"), which
 * belongs to the welcome screen's invitation to tap, not over a picture with a greeting already on it.
 */
#app[data-mode="active"]:has(#log:empty) #greeting {
  opacity: 1;
  max-height: none;
  margin: 0;
  overflow: visible;
  pointer-events: auto;
}

#app[data-mode="active"]:has(#log:empty) #greeting-sub {
  opacity: 1;
  max-height: none;
  margin: var(--sp-3) auto 0;
  overflow: visible;
  pointer-events: auto;
}

#app[data-mode="active"] #hero {
  padding-bottom: 0;
}

/*
 * ...AND THE DRAWN RIDGE GOES WITH IT.
 *
 * A 24px two-polygon massif was tucked under the band here, at 0.4 opacity, as the last trace of the
 * property's geography once the photograph had gone. It is deleted rather than dimmed: two polygons
 * and a snow triangle at 24px tall is exactly the "decorative element that looks generated" this
 * pass is asked to avoid, and it sat directly above the first reply — the one place on this screen
 * that must be nothing but paper and words. The conversation's cream-and-botanical character comes
 * from body::after, which is a real line drawing and lives in the margins.
 *
 * `--hero-range` (the inline SVG in this file's palette block) is now unused by the guest app; it
 * stays declared because the manager dashboard draws the same range from its own copy of the token
 * and the two surfaces are meant to agree if the ridge is ever wanted back.
 */
#app[data-mode="active"] #hero::before {
  display: none;
}

/* =============================================================================================
 * THE HEADER, OPEN OVER THE PICTURE — pre-chat active mode only.
 *
 * Everywhere else #hero-top is the opaque indigo band (THE HEADER BAND, below): a fixed dark chrome
 * strip is what gives the rest of the app a light/dark division to push against. Here the mountain
 * is already the hero's own background (THE PICTURE, above), and painting an opaque band over its
 * top third would curtain off the photo for no reason — the guest has not started talking yet, and
 * the first thing the screen should say is "this is the view", not "this is an app". So in this one
 * state the band goes transparent, the hamburger and the theme/profile pair float on the photograph
 * as light glass discs, and the lockup drops off the row it shares with them onto its own centred
 * line — the composition the welcome screen itself already uses, one screen later.
 *
 * `display: contents` on #topbar-actions is what makes this possible without moving a single node
 * in index.html: the cluster's own box stops painting and its children (sign-in, language, the
 * hidden select, theme, the profile icon) become direct flex items of .brand-row, so `order` can put
 * language on its own centred line under the lockup while sign-in/theme/profile stay the tight
 * trailing-edge trio they already are everywhere else. Every id-based lookup in js/app.js survives
 * untouched — the elements never move, only the box that used to group them stops being drawn.
 * ============================================================================================= */

#app[data-mode="active"]:has(#log:empty) #hero-top,
#app[data-mode="welcome"] #hero-top {
  background: none;
  box-shadow: none;
}

/* Dark mode's own glass treatment on #hero-top (below, THE HEADER BAND) adds a backdrop-filter
   that this transparent state doesn't reset — left alone, the crisp photo behind the floating
   chrome discs above would blur for no reason. */
:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) #hero-top,
:root[data-theme="dark"] #app[data-mode="welcome"] #hero-top {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

#app[data-mode="active"]:has(#log:empty) #hero-top::before,
#app[data-mode="welcome"] #hero-top::before {
  /* No band, so nothing to gild along its foot. */
  display: none;
}

#app[data-mode="active"]:has(#log:empty) #topbar-actions,
#app[data-mode="welcome"] #topbar-actions {
  display: contents;
}

#app[data-mode="active"]:has(#log:empty) .brand-row,
#app[data-mode="welcome"] .brand-row {
  flex-wrap: wrap;
  justify-content: center;
}

/* Row 1 stays put — hamburger leading, sign-in/theme/profile trailing. `margin-left: auto` on
   whichever of sign-in/theme is actually first in flow (a [hidden] sibling has no box to push
   against) reproduces #topbar-actions's own old `margin-left: auto`, now that its box is gone. */
#app[data-mode="active"]:has(#log:empty) #signin-btn,
#app[data-mode="welcome"] #signin-btn {
  margin-left: auto;
}
#app[data-mode="active"]:has(#log:empty) #signin-btn[hidden] ~ #theme-btn,
#app[data-mode="welcome"] #signin-btn[hidden] ~ #theme-btn {
  margin-left: auto;
}

/* The break after row 1: an empty flex item spanning the full row, so everything from the lockup
   onward wraps to its own line. */
#app[data-mode="active"]:has(#log:empty) .brand-row::before,
#app[data-mode="welcome"] .brand-row::before {
  content: "";
  order: 2;
  flex-basis: 100%;
  height: 0;
}

/* Row 2 — the lockup, MARK OVER WORDMARK rather than side by side. `.brand-mark` gets `flex-basis:
   100%`, which is the same "own line" trick the two breakers use above: nothing else fits beside a
   100%-wide item, so it forces its own row without a third pseudo-element (only ::before/::after
   exist to spend, and both are already the row 1 -> row 2 and row 2 -> row 3 breaks). `.brand-text`
   keeps its original order (3, same as the mark — DOM order, mark then text, is what decides which
   comes first when two items share one order value) and simply has nowhere left to sit but the next
   line, which .brand-row's `justify-content: center` then centres exactly as it does every other
   line in this stack.

   `display: block` on `.brand-text` is new: the narrow-phone container query further down this file
   (`@container pane (max-width: 359px) { .brand-text { display: none } }`) exists for the SHARED row
   this lockup no longer uses — it was written for "Atithi" squeezed to a one-pixel sliver between
   the crest and the chrome cluster on one shared line, which cannot happen once the wordmark has its
   own centred line below the crest. Left alone, that rule's plain-class specificity still wins over
   nothing and silently drops the wordmark at 320-359px — exactly one of this app's checked widths —
   leaving a bare crest with a gap where "Atithi" belongs. This selector's `#app[data-mode=...]`
   prefix outranks it, so restating `display: block` here is the fix, not a new rule. */
#app[data-mode="active"]:has(#log:empty) .brand-mark,
#app[data-mode="active"]:has(#log:empty) .brand-text,
#app[data-mode="welcome"] .brand-mark,
#app[data-mode="welcome"] .brand-text {
  order: 3;
  display: block;
}

/* `flex-basis` wins over `width` for a flex item's used size, so the box that forces the line break
   cannot also be the box that sizes the crest — it has to hand that job to the <img> instead (THE
   CREST, below). Cleared here: the translucent chip / gold ring (`#hero-top .brand-mark`) reads as
   a stray full-width bar once this box is 100% wide, so this state paints the crest bare, the way
   the reference does. */
#app[data-mode="active"]:has(#log:empty) .brand-mark,
#app[data-mode="welcome"] .brand-mark {
  flex-basis: 100%;
  display: flex;
  justify-content: center;
  width: auto;
  height: auto;
  background: none;
  border: 0;
  box-shadow: none;
  overflow: visible;
}

/* The second break, before language — after #brand-ornament's own line (order 4), which already
   forces one, but kept as an explicit safety net rather than relying on that alone. */
#app[data-mode="active"]:has(#log:empty) .brand-row::after,
#app[data-mode="welcome"] .brand-row::after {
  content: "";
  order: 5;
  flex-basis: 100%;
  height: 0;
}

/*
 * THE LANGUAGE PILL — every language on offer, at once, no disclosure to open.
 *
 * Everywhere else this control is a `<details>`: a closed "EN" face and a popover of full endonyms
 * behind it (see THE LANGUAGE POPOVER, further down). That is right for a control tucked beside
 * theme/profile in a compact chrome row. Here language is not one control among several — it is
 * the reference's own second element under the lockup — so the closed face is hidden entirely and
 * `.lang-sheet` (the SAME rows `renderLangSheet()` already builds and wires, one real `<button>` per
 * language, already carrying `.current`) is forced visible and laid out as a horizontal pill instead
 * of waiting on the `[open]` attribute a disclosure needs. No new markup, no new state, no new click
 * handling: `dom.langMenu.open = false` still runs on every row's existing click handler, it simply
 * has nothing left to close.
 *
 * `#lang-menu` itself turns into the flex container (rather than going `display: contents` the way
 * `#topbar-actions` does above): a `<details>` element's UA-native "hide everything but summary
 * while closed" behaviour turns out to run at the `<details>` level even once `display: contents`
 * removes its own box — measured here as `.lang-sheet` computing every override correctly
 * (`position: static`, `order: 6`, a real `margin-top`) and STILL painting at the icon row's own
 * position, as if none of that had taken effect. Making `#lang-menu` a flex box directly sidesteps
 * that: a `<details>` with `display: flex` lays out its two children (the hidden `summary`, the
 * forced-visible `.lang-sheet`) exactly like any other flex container regardless of `[open]`, so its
 * own height is real and its position in `.brand-row`'s wrap (`order`, `flex-basis: 100%`,
 * `margin-top`) is what everything else in this lockup already uses.
 */
#app[data-mode="active"]:has(#log:empty) #lang-menu,
#app[data-mode="welcome"] #lang-menu {
  order: 6;
  flex-basis: 100%;
  margin-top: var(--sp-5);
  display: block;
  /* Text-align, not `justify-content` — a `display: flex` + `justify-content: center` #lang-menu
     was measured landing its one visible child with its LEFT edge at the container's own midpoint
     rather than straddling it (and swapping to `margin-inline: auto` on the child produced an
     equally wrong negative resolved margin) — a `<details>`-as-flex-container quirk stacked on the
     one already noted above it. `text-align: center` sidesteps flex centring for this element
     entirely, and is exactly the right tool besides: `.lang-sheet` below is `inline-flex`, i.e. an
     inline-level box, which is what `text-align` centres. */
  text-align: center;
}

/* WELCOME only: closer still to the gold divider above it. Zeroing this margin only removed the
   pill's OWN extra space — `.brand-row`'s shared `row-gap` (var(--sp-3), the same rhythm every
   other wrapped line in this lockup uses) still held it ~24px below the divider, which still read
   as its own detached row. That row-gap is shared with every other gap in the lockup (crest to
   wordmark, wordmark to divider), so it is not a knob this one gap can turn alone — a negative
   margin on the pill itself is what pulls it in past that shared spacing without touching any of
   the others.
   -10px (14px of clear air) still read as its own row rather than sitting directly under the
   line. -18px measured to ~6px of clear air below the divider's own hairlines (the diamond's own
   span extends a little past them, to ~5px) — close enough to read as "just below", short of an
   actual touch.
   THIS RULE WAS WELCOME-ONLY, AND SHOULD NOT HAVE BEEN: a real guest reaches this lockup through
   `data-mode="active"` with an empty transcript (the `/welcome/` -> "Get Started" flow enters
   ACTIVE mode at boot, per index.html's entry guard), not through `data-mode="welcome"` — that
   mode is only what a bare `?code=` visit with no `from=welcome` param lands in, which is how this
   was tested but is not the path a real guest takes. Confirmed live: a guest on the actual flow
   measured `margin-top: 24px` here — the untouched shared base rule — because this selector never
   matched their `data-mode`. Extended to both.

   -18px (~6px of clear air below the divider's hairlines) later read as too tight — the pill sat
   close enough to the gold divider that the two blurred into one row rather than reading as
   "Atithi, then the divider, then a small gap, then language". -14px (~10px of clear air) is the
   subtle correction: still visibly closer to the divider than the shared row-gap would put it, but
   with enough daylight between them that the divider and the pill each read as their own line. */
#app[data-mode="active"]:has(#log:empty) #lang-menu,
#app[data-mode="welcome"] #lang-menu {
  margin-top: -14px;
}

#app[data-mode="active"]:has(#log:empty) #lang-menu > summary,
#app[data-mode="welcome"] #lang-menu > summary {
  display: none;
}

#app[data-mode="active"]:has(#log:empty) .lang-sheet,
#app[data-mode="welcome"] .lang-sheet {
  position: static;
  display: inline-flex;
  flex-direction: row;
  gap: 2px;
  width: max-content;
  max-width: none;
  padding: 3px;
  border-radius: var(--radius-pill);
  /* Lighter fill still, and the border pulled down to a whisper — the same correction as the
     chrome discs just above (THE GLASS DISCS): a visible white ring around the pill read as its
     own drawn shape rather than a pane of glass. The box-shadow keeps the pill legible against the
     photo without a border doing that job. */
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.12);
  -webkit-backdrop-filter: blur(9px) saturate(130%);
  backdrop-filter: blur(9px) saturate(130%);
  box-shadow: 0 3px 10px -6px rgba(74, 35, 64, 0.3);
  animation: none;
  opacity: 1;
  transform: none;
}

/*
 * THE PILL WAS RENDERING BEHIND THE PHOTO. Root-caused by toggling the real `open` attribute:
 * `#lang-menu` (a closed `<details>`) reports 0 intrinsic height to `getBoundingClientRect()`
 * regardless of the `display: block` set on it above — Chromium's native "collapsed while closed"
 * box measurement for `<details>` overrides an author `display` on the details element ITSELF, even
 * though it does NOT stop an explicitly-displayed child (`.lang-sheet`) from painting. Tried and
 * rejected: `display: flex/grid/table/flow-root/inline-block` on `#lang-menu` all measured the same
 * 0 — `list-item` gave 22.5px, still wrong (that is the marker glyph, not the pill). The effect: with
 * `#lang-menu` measuring 0, `#hero-top`'s own auto height came up short of where `.lang-sheet`
 * actually paints, and the pill ended up outside the z-index:4 box that is supposed to keep it above
 * `.hero-scene`'s z-index:0 photo — confirmed with `elementFromPoint` at the pill's own coordinates
 * returning `.hero-scene__img`, not the pill.
 *
 * `::details-content` is the fix — Chromium's own originating pseudo-element for the collapsible
 * content box, added specifically to make it stylable. Forcing it open here does not affect the
 * disclosure ANYWHERE else in the app: the selector is qualified the same way every other rule in
 * this composition is, so a mid-conversation `<details>` (`data-mode="active"` with a real
 * transcript) is untouched and still opens only on click. Where this selector is unsupported (Safari,
 * Firefox, older Chromium) the language pill falls back to exactly its pre-existing behaviour — this
 * is a progressive fix, not a required one.
 */
#app[data-mode="active"]:has(#log:empty) #lang-menu::details-content,
#app[data-mode="welcome"] #lang-menu::details-content {
  display: block;
  block-size: auto;
  content-visibility: visible;
  overflow: visible;
}

/* Each row collapses from a full-width tap target with a reserved tick to a compact segment — the
   selected fill (below) is what marks "current" now, so the tick this same row wears in the popover
   would be redundant weight here. */
#app[data-mode="active"]:has(#log:empty) .lang-row,
#app[data-mode="welcome"] .lang-row {
  width: auto;
  min-height: auto;
  justify-content: center;
  /* Shrunk from calc(--sp-2 - 2px)/--sp-4 (6px/16px) — the pill was reading as large next to the
     now-smaller lockup above it (THE LOCKUP passes above). */
  padding: 4px var(--sp-3);
  font-size: 11px;
  border-radius: var(--radius-pill);
  font-family: var(--font-indic);
  font-weight: 700;
  color: var(--indigo-ink);
}

#app[data-mode="active"]:has(#log:empty) .lang-row-name,
#app[data-mode="active"]:has(#log:empty) .lang-row-tick,
#app[data-mode="welcome"] .lang-row-name,
#app[data-mode="welcome"] .lang-row-tick {
  display: none;
}

/* `.lang-row-code` — "EN" / "हिं" / "বাং" (js/i18n.js LANG_CODES) — is the SAME short form the
   closed disclosure face shows elsewhere (`.lang-code`, above), now rendered once per row by
   `renderLangSheet()` instead of once for whichever language is current. Hidden by default (next to
   THE LANGUAGE POPOVER's own rules, further down) since the popover context shows the full endonym
   instead. */
#app[data-mode="active"]:has(#log:empty) .lang-row-code,
#app[data-mode="welcome"] .lang-row-code {
  display: inline-block;
}

#app[data-mode="active"]:has(#log:empty) .lang-row.current,
#app[data-mode="welcome"] .lang-row.current {
  background: var(--indigo);
  color: var(--on-indigo);
}

#app[data-mode="active"]:has(#log:empty) .lang-row:not(.current):hover,
#app[data-mode="welcome"] .lang-row:not(.current):hover {
  background: rgba(255, 255, 255, 0.5);
}

/* Dark mode: the popover itself is already dark (--glass-3), so a 0.5-alpha white hover would be
   the brightest thing in it. A purple tint instead, matching .lang-row.current's own accent. */
:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) .lang-row:not(.current):hover,
:root[data-theme="dark"] #app[data-mode="welcome"] .lang-row:not(.current):hover {
  background: color-mix(in srgb, var(--accent) 20%, transparent);
}

/*
 * THE GLASS DISCS. Frosted ivory rather than the band's translucent white-on-indigo: on a bright
 * dawn sky a white-on-white circle disappears, so the fill goes warm instead of the band's cool
 * white, and the ink goes dark instead of the band's warm-ivory light-on-dark. Same shapes, same
 * sizes, same tokens (.chrome-icon-btn / .lang-menu > summary do all the actual button chrome) —
 * only the two values that assumed a dark ground underneath them change.
 *
 * Kept LIGHT on purpose: a first pass ran these near-opaque (0.85) with a heavy 10px blur, which
 * read as a paper chip laid over the photo rather than a disc of the sky itself. Two more cuts
 * followed the same complaint each time (0.85 -> 0.5 -> 0.3 alpha, 6px -> 8px blur) and the discs
 * still read as solid white bubbles with a visible white RING around them — the border was doing
 * exactly what the fill cuts kept trying to undo. This pass drops the fill again (0.3 -> 0.16) and
 * takes the border down to a bare whisper (0.5 -> 0.12): just enough for the eye to find the
 * circle's edge against a sky that varies from near-white to dusk-violet across the frame, not
 * enough to read as a drawn ring. The box-shadow (indigo-tinted, not white) still carries what
 * definition the disc needs, which is why the border can give up almost all of its own.
 */
#app[data-mode="active"]:has(#log:empty) #hero-top .chrome-icon-btn,
#app[data-mode="active"]:has(#log:empty) #hero-top .lang-menu > summary,
#app[data-mode="welcome"] #hero-top .chrome-icon-btn,
#app[data-mode="welcome"] #hero-top .lang-menu > summary {
  background: rgba(255, 252, 246, 0.16);
  border-color: rgba(255, 255, 255, 0.12);
  box-shadow: 0 3px 10px -6px rgba(74, 35, 64, 0.3);
  color: var(--indigo-ink);
  -webkit-backdrop-filter: blur(9px) saturate(130%);
  backdrop-filter: blur(9px) saturate(130%);
}

/* Dark mode: the discs above assume a bright dawn sky behind them, but the mountain is dimmed to
   ~62% brightness in this theme (.hero-scene__img's own dark filter) — a light disc with dark ink
   now sits on a dark photo instead. Dark glass with light ink, matching the rest of this theme's
   floating chrome. */
:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) #hero-top .chrome-icon-btn,
:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) #hero-top .lang-menu > summary,
:root[data-theme="dark"] #app[data-mode="welcome"] #hero-top .chrome-icon-btn,
:root[data-theme="dark"] #app[data-mode="welcome"] #hero-top .lang-menu > summary {
  background: rgba(25, 17, 23, 0.4);
  border-color: rgba(255, 255, 255, 0.16);
  box-shadow: 0 3px 10px -6px rgba(0, 0, 0, 0.5);
  color: var(--header-ink);
}

#app[data-mode="active"]:has(#log:empty) #hero-top .lang-icon,
#app[data-mode="active"]:has(#log:empty) #hero-top .lang-code,
#app[data-mode="active"]:has(#log:empty) #hero-top .stay-glyph,
#app[data-mode="active"]:has(#log:empty) #hero-top #theme-btn .icon,
#app[data-mode="active"]:has(#log:empty) #hero-top .signin-label,
#app[data-mode="active"]:has(#log:empty) #hero-top .signin-btn .icon,
#app[data-mode="welcome"] #hero-top .lang-icon,
#app[data-mode="welcome"] #hero-top .lang-code,
#app[data-mode="welcome"] #hero-top .stay-glyph,
#app[data-mode="welcome"] #hero-top #theme-btn .icon,
#app[data-mode="welcome"] #hero-top .signin-label,
#app[data-mode="welcome"] #hero-top .signin-btn .icon {
  color: var(--indigo-ink);
}

:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) #hero-top .lang-icon,
:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) #hero-top .lang-code,
:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) #hero-top .stay-glyph,
:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) #hero-top #theme-btn .icon,
:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) #hero-top .signin-label,
:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) #hero-top .signin-btn .icon,
:root[data-theme="dark"] #app[data-mode="welcome"] #hero-top .lang-icon,
:root[data-theme="dark"] #app[data-mode="welcome"] #hero-top .lang-code,
:root[data-theme="dark"] #app[data-mode="welcome"] #hero-top .stay-glyph,
:root[data-theme="dark"] #app[data-mode="welcome"] #hero-top #theme-btn .icon,
:root[data-theme="dark"] #app[data-mode="welcome"] #hero-top .signin-label,
:root[data-theme="dark"] #app[data-mode="welcome"] #hero-top .signin-btn .icon {
  color: var(--header-ink);
}

#app[data-mode="active"]:has(#log:empty) #hero-top #brand-name,
#app[data-mode="welcome"] #hero-top #brand-name {
  color: var(--indigo-ink);
  text-shadow: 0 1px 14px rgba(255, 252, 246, 0.7);
}

:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) #hero-top #brand-name,
:root[data-theme="dark"] #app[data-mode="welcome"] #hero-top #brand-name {
  color: var(--header-ink);
  text-shadow: 0 2px 16px rgba(13, 9, 12, 0.6);
}

/* The focus ring reverts to the app's own indigo-on-cream ring. The white ring above (THE CHROME
   ICON BUTTONS) exists only because the opaque indigo band makes an indigo ring invisible; a white
   ring on ivory glass has the same problem in reverse. */
#app[data-mode="active"]:has(#log:empty) #hero-top .lang-menu > summary:focus-visible,
#app[data-mode="active"]:has(#log:empty) #hero-top .chrome-icon-btn:focus-visible,
#app[data-mode="welcome"] #hero-top .lang-menu > summary:focus-visible,
#app[data-mode="welcome"] #hero-top .chrome-icon-btn:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* THE GREETING'S OWN READABILITY WASH — REMOVED. This used to be a soft radial glow sized to
   #hero-copy's own box (the greeting + sub-line only), cut down over several passes (0.55 peak
   alpha -> 0.18, 65%/80% radius -> 110%/150%, final stop 82% -> 100%) chasing the same complaint
   each time: even at 0.18 alpha, a warm-white ellipse clipped by a box only as tall as two lines of
   type still reads as a card face with a soft edge sitting on the mountain — precisely the "white
   rectangle behind the greeting" report. Cutting the alpha further only delayed the same edge; the
   fix is no local surface at all. Readability now rests entirely on #greeting / #greeting-sub's own
   `text-shadow` (css/daylight.css), which was already doing the actual legibility work. */
#app[data-mode="active"]:has(#log:empty) #hero-copy,
#app[data-mode="welcome"] #hero-copy {
  background: none;
}

/* =============================================================================================
 * THE VIEW, CONTINUED — the same mountain photograph, carried past where .hero-scene ends, so the
 * suggestion rail sits ON the view rather than on flat cream handed off from it. A colour-only wash
 * was tried here first and cut: it matched the photograph's own palette but carried none of its
 * texture, so the rail still read as "photo, then a plain tinted panel" rather than one continuous
 * picture — exactly the "the image should cover the suggestion cards too" report.
 *
 * `#canvas::before`, not a second copy of `.hero-scene__img`: this is a decorative repeat of the
 * SAME asset (no new file), confined to being #canvas's own background layer rather than a second
 * photo element competing with .hero-scene for layout or z-index. `#canvas` is given `position:
 * relative` and an explicit `z-index: 0` so it becomes its own stacking context — that is what lets
 * the pseudo-element sit at `z-index: -1` and be GUARANTEED behind every real child of #canvas
 * (#cap-panel, #log-wrap, #composer-dock) regardless of which of them happen to be positioned, rather
 * than relying on DOM order against elements that are not all in the same stacking bucket.
 *
 * THE CROP: #canvas is short and wide (the opposite box shape from #hero, which is narrow and tall),
 * so `cover` crops the photograph's TOP and BOTTOM here instead of its sides — a DIFFERENT slice of
 * the same file than `.hero-scene__img` shows. `24% 88%` keeps the same horizontal anchor and picks
 * a vertical anchor near the misted valley floor .hero-scene's own crop already shows at ITS foot.
 *
 * THE LINE, root-caused: matching alpha at the two layers' shared boundary (see THE SEAM below) was
 * not enough on its own. Two independently-`cover`-fitted crops of one photo, in boxes of very
 * different aspect ratios, do not line up pixel-for-pixel — ridge and snow-line detail that is sharp
 * in `.hero-scene__img` meets a differently-framed, equally sharp crop here, and THAT mismatch is
 * what reads as a seam, regardless of how carefully the alpha either side of it is tuned. `filter:
 * blur(...)`, added below, is the actual fix: it dissolves this layer's own ridge lines and snow
 * edges into soft fields of the photograph's own colour, so there is no sharp geometry left to
 * disagree with the crisp photo above it — the eye reads continuing atmosphere, not a second image.
 * `background-size: cover` already fills the box edge-to-edge, so the blur has no bare/transparent
 * edge to fringe; it only softens the photograph's own detail.
 *
 * THE FADE is done in the mask (alpha), the same discipline as `.hero-scene`'s own (THE MASK, above)
 * and for the same reason: it lets #canvas's own cream ground show through rather than painting a
 * wash of a colour picked by hand over the photo.
 *
 * THE SEAM: `.hero-scene`'s own mask (THE MASK, above) ends its box at 0.08 alpha — this layer's
 * mask STARTS at that same 0.08, not at `transparent`, so there is no alpha gap at the join either,
 * on top of the blur that removes the content mismatch. From that matched start it rises to a real,
 * naturally-visible hold through the rail (0.5 peak — enough that the view reads as present behind
 * the cards, not a whisper), never the photograph's own full strength — the cards themselves carry
 * their own light, tinted, semi-transparent fill (.cap-pill, below), which is where the "readable
 * over a photograph" job actually lives, not a heavier mask here — then eases down through the
 * transcript/dock region, ending at 0.16 rather than fading away entirely, so the view stays
 * perceptible around the mic instead of handing off to flat cream. #composer-dock's own fill is
 * loosened a touch for the same reason (below), so that tail has something to show through.
 * ============================================================================================= */
#app[data-mode="active"]:has(#log:empty) #canvas,
#app[data-mode="welcome"] #canvas {
  position: relative;
  z-index: 0;
}

#app[data-mode="active"]:has(#log:empty) #canvas::before,
#app[data-mode="welcome"] #canvas::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: url("/img/hero-mountain.jpg");
  background-repeat: no-repeat;
  background-size: cover;
  background-position: 24% 88%;
  /* blur(26px) is THE LINE fix (see the note above) — it is what turns a second, differently-cropped
     photo into soft continuing atmosphere. The grade after it is unchanged from `.hero-scene__img`. */
  filter: blur(26px) sepia(0.25) saturate(0.85) contrast(1.03) brightness(0.87);
  -webkit-mask-image: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0.08) 0%,
    rgba(0, 0, 0, 0.28) 22%,
    rgba(0, 0, 0, 0.46) 42%,
    rgba(0, 0, 0, 0.5) 55%,
    rgba(0, 0, 0, 0.4) 70%,
    rgba(0, 0, 0, 0.26) 85%,
    rgba(0, 0, 0, 0.16) 100%
  );
  mask-image: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0.08) 0%,
    rgba(0, 0, 0, 0.28) 22%,
    rgba(0, 0, 0, 0.46) 42%,
    rgba(0, 0, 0, 0.5) 55%,
    rgba(0, 0, 0, 0.4) 70%,
    rgba(0, 0, 0, 0.26) 85%,
    rgba(0, 0, 0, 0.16) 100%
  );
}

/* Dark mode: the same gentle grade `.hero-scene__img` wears (above), plus the same indigo scrim as
   an EXTRA background layer rather than a second pseudo-element (this one is already a `::before`,
   and an element only gets the two) — `background` accepts a stack of images, so the gradient
   paints over the photograph in one declaration. */
:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) #canvas::before,
:root[data-theme="dark"] #app[data-mode="welcome"] #canvas::before {
  background-image:
    linear-gradient(180deg, rgba(13, 9, 12, 0.5) 0%, rgba(37, 24, 34, 0.15) 45%, rgba(13, 9, 12, 0.55) 100%),
    url("/img/hero-mountain.jpg");
  filter: blur(26px) saturate(0.94) contrast(1.02) brightness(0.88);
}

/* THE LOCKUP — a centred emblem, the composition's actual subject rather than a compact header
   badge. `row-gap` keeps the crest and the wordmark reading as ONE lockup (tight); the extra
   `margin-top` below on `.brand-mark` and `#lang-menu` is what gives the icon row and the language
   pill their own generous air, so the tightness is deliberately only between the two lines that are
   meant to read as a single mark. */
#app[data-mode="active"]:has(#log:empty) .brand-row,
#app[data-mode="welcome"] .brand-row {
  /* sp-3 (12px) -> sp-2 (8px). Four wrapped lines means this gap is paid four times, so 4px off it
     is 16px back to the sheet below — and it is the cheapest 16px on the screen, because the tight
     rhythm is what makes the crest, the wordmark and the ornament read as ONE lockup in the first
     place (see the note above). The language pill keeps its own generous air from its `margin-top`,
     which this does not touch. */
  row-gap: var(--sp-2);
}

/* Air above the crest, between it and the icon row. Reduced sp-4 (16px) -> sp-2 (8px): the third
   pass on this lockup asks the crest to sit slightly higher, and since the crest's own top is what
   this margin pushes down, less of it is what moves the crest up without touching anything below. */
#app[data-mode="active"]:has(#log:empty) .brand-mark,
#app[data-mode="welcome"] .brand-mark {
  margin-top: var(--sp-2);
}

/*
 * THE CREST, reduced a third time — clamp(34px, 8.5vw, 50px) still read as slightly large next to
 * the wordmark once that grew (below). -12% at both ends (34->30, 50->44) is smaller than the -23%
 * cut two passes ago: this is a balance correction against the now-larger wordmark, not a repeat of
 * "the crest is oversized" — going further would undersize it the other way. Still above the
 * compact-header floor (36-40px, briefly, since this crest stands alone on its own line and can
 * afford to read a hair smaller than that everywhere except the widest phones).
 */
#app[data-mode="active"]:has(#log:empty) .brand-mark img,
#app[data-mode="welcome"] .brand-mark img {
  /* +12% (30->34, 44->49), +15% (34->39, 49->56), then +15% again (39->45, 56->64): this crest is
     bare (the container above zeroes its background, border and box-shadow for this mode), so it
     sits directly on the mountain scene with nothing framing it — the mark was reading faint
     here. */
  width: clamp(45px, 11.2vw, 64px);
  height: clamp(45px, 11.2vw, 64px);
  /* `drop-shadow`, not `box-shadow`: this crest has no ring or chip behind it in this mode, so the
     shadow has to hug the mark's own silhouette or it reads as a stray box floating on the photo.
     Same dark-indigo ink and low opacity as .welcome-lockup__mark's identical treatment (the other
     place this mark sits directly on a mountain view) — a hairline contact shadow plus a soft short
     lift, not a glow. */
  filter:
    drop-shadow(0 1px 1.5px rgba(20, 14, 40, 0.6))
    drop-shadow(0 0 5px rgba(20, 14, 40, 0.35));
}

/* "Atithi", grown back up — the previous pass's clamp(17px, 4.6vw, 23px) balanced the wordmark
   against the larger 44-64px crest of that pass; against the now-smaller 30-44px crest above, the
   same wordmark read as too quiet. +~17% (17->20, 23->27) puts the wordmark ahead of the crest in
   the lockup's visual weight instead of the two competing as equals. */
#app[data-mode="active"]:has(#log:empty) #brand-name,
#app[data-mode="welcome"] #brand-name {
  font-size: clamp(20px, 5.4vw, 27px);
}

/* THE LOCKUP'S OWN ORNAMENT — the same hairline-diamond-hairline `#hero-copy` wears under the
   greeting (see index.html and the note below), reused here under the wordmark. Hidden by the base
   `.hero-ornament` rule (below THE ORNAMENT) everywhere else: the compact header never had a second
   ornament and should not gain one, so this ID only turns it on for the two compositions the
   reference shows it in. `flex-basis: 100%` gives it — like `.brand-mark` and `.brand-text` — its
   own line in the wrapped `.brand-row`; `order` places that line between the wordmark (3) and the
   language pill (pushed to 6, below). */
#app[data-mode="active"]:has(#log:empty) #brand-ornament,
#app[data-mode="welcome"] #brand-ornament {
  display: flex;
  order: 4;
  flex-basis: 100%;
  margin-top: var(--sp-2);
}

/*
 * THE ORNAMENT, MADE TO READ against a photograph instead of the paper `.hero-ornament` was
 * designed for. The shared rule (THE ORNAMENT, below) sizes its hairlines and diamond for gold-on-
 * cream, where the champagne line only has to beat a few percent of contrast against warm paper —
 * over a busy, similarly-warm sunrise it was reading as a stray mark rather than a deliberate
 * divider. Scoped to `#brand-ornament` alone (an ID selector), not `.hero-ornament` generally, so
 * the greeting's own copy of this component two lines below is untouched.
 *
 * Thicker lines (1px -> 1.5px) and a wider span (clamp(28px,9vw,44px) -> clamp(36px,10vw,54px)) read
 * as "a line drawn with intent" rather than a hairline that could be a rendering artifact; the
 * diamond grows to match (5px -> 7px) so it stays the visual centre of the mark rather than being
 * dwarfed by thicker arms either side of it. Colour and opacity are untouched — `--gold` was already
 * solid, unfaded; the fix was scale, not strength.
 */
#brand-ornament::before,
#brand-ornament::after {
  width: clamp(36px, 10vw, 54px);
  height: 1.5px;
}

#brand-ornament > span {
  width: 7px;
  height: 7px;
}

/* =============================================================================================
 * THE COMPOSITION — how the column divides, per mode.
 *
 * WELCOME, top to bottom:
 *     #hero-top      the transparent glass chrome      fixed
 *     .hero-scene    the view, now #hero's OWN background, full-bleed  fixed
 *     #hero-copy     greeting, sub-line, ornament      rides on the view, lower third
 *     #cap-panel     the capability sheet              TAKES WHAT'S LEFT, scrolls
 *     #composer-dock mic · composer · safety · privacy fixed
 *
 * The load-bearing decision is which region absorbs the leftover height, and this pass MOVES it:
 * it used to be the capability sheet (slack there became more of the menu visible before a scroll),
 * matching pre-chat ACTIVE mode below — because the reference this pass is built against is one
 * continuous photograph behind the greeting, not a band above a menu, and that composition only
 * exists today when #hero itself is the region taking the slack (see THE PICTURE, above THE
 * COMPOSITION). #cap-panel keeps the exact CSS that let it absorb space when there was some
 * (`flex: 1 1 auto; min-height: 0`, below) — it simply has less to absorb now, and scrolls sooner.
 * Nothing about the sheet, its chips or the dock beneath it changed; only which sibling the leftover
 * height goes to did.
 *
 * ACTIVE: the greeting and the view go, #log-wrap takes the slack, and the lane moves BELOW the
 * transcript — `order` on #canvas's flex children, so one DOM order serves both (the menu belongs
 * under the hero when it is the subject; the lane belongs in thumb reach when the conversation is).
 * ============================================================================================= */

#app[data-mode="welcome"] #hero {
  /*
   * Was `flex: 0 0 auto` (content-height only). Pre-chat ACTIVE mode's own version of this rule
   * uses `flex: 1 1 auto` (shrinkable) and it is safe there ONLY because that mode's #canvas holds
   * the compact rail (`#app[data-mode="active"] #cap-panel { flex: 0 0 auto }`, a fixed ~70px pill
   * row) — #hero and #canvas's combined natural content never exceeds the pane, so shrink never
   * actually engages and `1 1` behaves identically to `1 0`.
   *
   * WELCOME's #canvas still holds the full capability SHEET (the headline + the whole chip grid,
   * `flex: 1 1 auto` with no fixed cap — see #cap-panel, below), whose natural content is easily
   * 400-600px on its own. With `1 1 auto` here, #hero's own flex-shrink:1 meant that once
   * #hero-top + #canvas's combined natural size overran the pane, the shrink algorithm took height
   * off BOTH siblings — collapsing #hero-copy to 0 (measured: greeting rendered at 0px, margin-top:
   * auto resolving to 0 because there was no positive space left to distribute) instead of leaving
   * #hero alone and letting #cap-panel give up the height and scroll, which is the one thing in this
   * ladder actually designed to do that.
   *
   * `flex: 1 0 auto` — shrink 0 — is the fix: #hero still GROWS to take positive leftover space
   * (the full-bleed photo), but never shrinks below its own content, so any overflow falls entirely
   * on #canvas (shrink 1) and, inside it, on #cap-panel (shrink 1, `min-height: 0`) rather than on
   * the greeting.
   *
   * THE FLOOR CAME DOWN, 56vh -> `min(38vh, 300px)`, and that is this pass's largest single change.
   *
   * Measured at 375x812 before it: #hero rendered at 454.7px — 56% of the first viewport — against a
   * #cap-panel holding 686px of cards in 278px of box, of which four cards were legible. The
   * photograph was taking the slack from the one region on this screen that says what the product
   * does. The floor is what was handing it over: #hero's own content (chrome + greeting) measures
   * ~423px here, so 56vh was not describing the composition, it was inflating it, and `margin-top:
   * auto` on #hero-copy quietly absorbed the difference as air above the greeting.
   *
   * `min(38vh, 300px)` rather than a bare `38vh`: past ~790px of viewport height 38vh keeps growing
   * and the sheet stops gaining anything from it, so the px arm caps the picture on a tall phone and
   * on desktop while the vh arm still shrinks it on a short one. The floor no longer binds at
   * 375x812 — #hero sizes to its own content now (~355px after the two trims below), which is the
   * outcome this rule wanted all along: the picture takes the space the composition needs and the
   * sheet keeps the rest. It still binds where it was always meant to, on a viewport tall enough
   * that content-height alone would leave the photograph looking like a band.
   *
   * `min-height` is what actually gives `flex-grow: 1` something to grow INTO. Without it,
   * #hero's basis (`auto`, i.e. its own header + greeting content, ~395px measured at 375×812) and
   * #cap-panel's full-sheet basis (~650px) together already exceed most phones' pane height, so the
   * flex algorithm never reaches a positive remainder to distribute — grow never engages, and #hero
   * just renders at its own content size (measured: 49% of viewport). The floor forces the pane into
   * deficit early enough that #cap-panel — designed to give up height and scroll, see THE COMPOSITION
   * above — absorbs it, instead of the outcome being decided by whichever side's content happens to
   * be bigger. Cancelled below `@media (max-height: 600px)`, where `.hero-scene` itself goes and a
   * reserved floor would just be empty cream above the greeting.
   */
  /*
   * GROW 0, and this is the other half of bringing the hero down.
   *
   * `1 0 auto` made #hero the region that absorbs the column's leftover height. With the floor at
   * 56vh that was invisible, because the floor was bigger than any leftover; with the floor at
   * `min(38vh, 300px)` it became the whole story — measured at 430x932, #hero took every spare pixel
   * and rendered at 549.7px (59% of the viewport), WORSE than the 56% this pass exists to fix, while
   * the sheet sat at its own content height with cream under it. The taller the phone, the bigger
   * the photograph and the further the menu fell.
   *
   * So the slack goes back to #canvas (below), and #hero is sized by its own content — the chrome,
   * the lockup, the language pill and the greeting — with the floor underneath it for a short
   * viewport. SHRINK STAYS 0: that is the part the long note above is about, and nothing here
   * changes it. #hero still never gives up height, so a deficit still falls entirely on #cap-panel,
   * which is the one region in this ladder designed to scroll.
   */
  flex: 0 0 auto;
  min-height: min(38vh, 300px);
}

#app[data-mode="welcome"] #canvas {
  /* THE SLACK LIVES HERE AGAIN. It was moved to #hero so the photograph could be one continuous
     full-bleed field behind the greeting — which it still is, because .hero-scene is absolutely
     positioned against #hero's box and fills whatever that box turns out to be. What moving the
     slack actually bought was a photograph that grew without limit on a tall phone (see #hero,
     above); the composition it was meant to protect does not depend on it.
     `1 1 auto`: grow, so leftover height lands in the sheet where the menu can use it, and shrink,
     so #cap-panel still gives up height and scrolls rather than overflowing the pane. */
  flex: 1 1 auto;
  min-height: 0;
}

#app[data-mode="welcome"] #cap-panel {
  order: 1;
  flex: 1 1 auto;
  min-height: 0;
  /* The sheet rides UP over the foot of the view, which is the whole reason its top corners are
     rounded and its top edge is gilt: stationery laid on the desk over the window, not a second
     panel stacked under a photograph. #cap-panel is z-index 1 and the scene is not, so the overlap
     paints the right way round.
   *
   * -30px rather than -22: the brief for this pass asks for the capability section to sit closer to
   * the photograph so the two read as one composition, and 8px more overlap is what that is. It is
   * bounded by the picture, not by taste — the mountain's foot is the mist, and anything past ~34px
   * starts covering the ridge line that gives the valley its depth. */
  margin-top: -30px;
}

#app[data-mode="welcome"] #log-wrap {
  /* Nothing has been said yet, and every path that writes to the log calls enterActiveMode() first
     (js/app.js) — so in welcome mode this is an empty scroll container taking half the screen. */
  display: none;
}

#app[data-mode="active"] #log-wrap {
  order: 1;
}

#app[data-mode="active"] #cap-panel {
  order: 2;
  flex: 0 0 auto;
}

/* SUGGESTIONS STEP ASIDE WHILE THE MIC IS ACTUALLY DOING SOMETHING — requested directly: once a
 * call is connecting/listening/thinking/speaking, the guest wants the transcript to have the
 * screen, not a capability grid competing with it, and the reclaimed height should go to the
 * chat, not sit empty. `#log-wrap` is already `flex: 1` (chat.css), so removing #cap-panel from
 * the flex line is the whole fix — nothing here needs to reach into #log-wrap or resize anything
 * itself.
 *
 * `error` and `idle`/`disabled` are deliberately excluded: a dropped call (error) and no call at
 * all (idle/disabled) both mean the guest is not actively talking, which is exactly when
 * suggestions are supposed to be there — the same "normal / typing -> show, engaged -> hide" split
 * the request describes. Listed positively (five states) rather than as a `:not()` chain against
 * the same three exclusions, so a future state added to js/orb.js defaults to SHOWING suggestions
 * unless it is deliberately added here, not silently hiding them.
 *
 * Scoped to `data-compose="closed"`: switching to type mid-call is the guest choosing the OTHER
 * input method, not ending the call, and this file already suppresses voice's own modifier the
 * same way while composing text (`#app[data-compose="open"] #mute-btn`, THE DOCK'S TWO STATES) —
 * suggestions get the same exemption, not a special case invented for them.
 */
#app[data-compose="closed"] #canvas:has(#orb[data-state="connecting"]) #cap-panel,
#app[data-compose="closed"] #canvas:has(#orb[data-state="listening"]) #cap-panel,
#app[data-compose="closed"] #canvas:has(#orb[data-state="understanding"]) #cap-panel,
#app[data-compose="closed"] #canvas:has(#orb[data-state="thinking"]) #cap-panel,
#app[data-compose="closed"] #canvas:has(#orb[data-state="speaking"]) #cap-panel {
  display: none;
}

/*
 * The dock is LAST in both modes, and it has to say so.
 *
 * `order` defaults to 0, so the moment anything else in #canvas is given a positive order the dock
 * — which is last in the DOM and needs no order to be last — jumps to the front of the flex line.
 * It rendered above the capability sheet, overlapping it by 124px. Stated unconditionally rather
 * than per mode: there is no arrangement of this screen where the dock is not at the foot.
 *
 * The road advisory (js/app.js inserts it before #log-wrap) keeps the implicit 0 on purpose, which
 * puts it at the top of the canvas in both modes — an unprompted warning belongs where it is read.
 */
#composer-dock {
  order: 3;
}

/* =============================================================================================
 * THE DOCK'S TWO STATES — and the mic is the point of the first one.
 *
 *     [type]        [ M I C ]        [help]        #app[data-compose="closed"]  (the default)
 *     [mic]  [ Type a message… ➤ ]  [help]        #app[data-compose="open"]
 *
 * Atithi is a concierge you TALK to. The dock this replaces showed both inputs permanently — a 48px
 * mic beside a 52px field — which is an even-handed arrangement, and even-handed is the wrong answer:
 * the screen said "chat app with a microphone attached", and the mic was the smaller of the two
 * objects in it. The primary interaction has to be the biggest thing in the row.
 *
 * So voice-first is the resting state: the composer is not rendered, the mic is 64px and centred in
 * the slack between two 48px controls, and a keyboard is one tap away on the left. Tapping it hands
 * the middle to the field and the mic steps to 48px on the left, where it stays reachable — the row
 * REARRANGES, nothing appears from nowhere and nothing moves off screen.
 *
 * Nothing here is new machinery: `data-compose` and setComposeOpen are the disclosure contract this
 * dock was built with (js/app.js), and the mode sizes below are the same variable css/orb.css has
 * always read. `flex` and `order` do the arranging, so no control is positioned against another and
 * the row cannot drift with locale length.
 * ============================================================================================= */

/* The row's order is [type] [mic] [composer] [help] in the markup, which is also tab order. `order`
   is only needed for the ONE swap: the mic moves ahead of the type button's slot when typing, so
   `[mic] [field]` reads left to right. */
#app[data-compose="open"] #orb-stage {
  order: -1;
}

/* ---- VOICE-FIRST (the default) ---- */

/* The mic used to take the slack and centre IN it — "centred in the slack" reads as centred on the
 * bar only when #composer-toggle and mute+help weigh the same, which stopped being reliably true
 * once mute became a variable-width text pill rather than a fixed 44px icon: starting a call (mute
 * appearing) shifted the slack's midpoint, and with it the mic, a few px sideways. A guest reported
 * this directly — the mic must never move, not even slightly, when mute shows or hides.
 *
 * So the mic is pinned to the BAR's own centreline instead, absolutely, completely decoupled from
 * every sibling's width. `#dock-bar` is `position: relative` for exactly this (below); `min-height`
 * there reserves the mic's own footprint now that it no longer occupies flex flow to claim it
 * itself. The historical objection to absolute positioning here (a GROWING neighbour — the composer
 * field — could overlap a fixed-position mic) does not apply in this state: `#composer` is
 * `display: none` throughout CLOSED, so there is no growing neighbour to collide with; it only
 * exists in OPEN, where the mic already uses a completely different (flex, left-aligned) layout. */
#app[data-compose="closed"] #orb-stage {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

/* The containing block for the mic's absolute centring above, and the reservation that replaces
 * the flex-flow height it no longer contributes now that it is out of flow. `--orb-size` is
 * already this mode's own token (THE MIC'S TWO SIZES, below), so the two can never drift apart. */
#dock-bar {
  position: relative;
}

#app[data-compose="closed"] #dock-bar {
  min-height: var(--orb-size);
}

/* Type stays flush left by default (the row's plain flex-start). Whichever of mute/help is the
 * FIRST visible control after it absorbs all the leftover width via an auto margin (set on
 * `.mute-btn:not([hidden])` itself, further down — one property, not a second rule competing with
 * it), so that group sits flush at the right edge regardless of mute's own width or visibility —
 * the same "never depend on a variable-width neighbour" fix as the mic's, applied to the side that
 * still needs to react to mute's width, just not by moving anything the guest is looking at. */
#app[data-compose="closed"] #dock-bar:not(:has(.mute-btn:not([hidden]))) #help-btn {
  margin-inline-start: auto;
}

/* No field, and no reserved space for one. `display: none` rather than visibility/height, so the
   textarea is out of tab order too — a focusable field inside a collapsed row is how a keyboard
   guest ends up typing into something they cannot see. */
#app[data-compose="closed"] #composer {
  display: none;
}

/* ---- TYPING ---- */

/* The type button has done its job: the field it opened is now the thing saying "type here", and two
   keyboard affordances in one row is one too many. */
#app[data-compose="open"] #composer-toggle {
  display: none;
}

/* Voice and its modifier have nothing to say while a guest is composing text — the UI polish
 * pass's own requirement, and it holds even mid-call: tapping "type" during a live call still
 * hides mute, because the field now owns the row's whole middle and a status pill competing with
 * it for width is the "bulky bottom sheet" this pass explicitly guards against. `display: none`,
 * not the `hidden` attribute — js/app.js's `hidden` still means "no call to mute" and this is a
 * second, independent reason to hide the same element, layered on top rather than replacing it.
 * Two IDs plus an attribute outranks `.mute-btn:not([hidden])`'s two classes regardless of source
 * order, so this needs no `!important`. */
#app[data-compose="open"] #mute-btn {
  display: none;
}

/* ---- THE MIC'S TWO SIZES ---- */

/*
 * 72px at rest, 56px once typing. It was clamp(80px, 22cqi, 96px) when it was a lone orb in an empty
 * dock, then 52/48 beside a permanent field, then 64/48 beside 38-40px neighbours.
 *
 * Both numbers went UP with `--dock-ctl`, and they had to: the mic is the PRIMARY control, so the
 * ratio between it and the quiet controls bracketing it is the thing being preserved, not the
 * absolute px. At 64 against the new 54 it would have become "the biggest of three buttons"; at 72
 * it is unmistakably the one bold object in the row — 72:54, close to the 64:48 the design was
 * measured at. And 56 rather than 48 in typing mode, so that even with the field taking the middle
 * the mic is never SMALLER than the safety control beside it (48 against 54 was exactly that).
 *
 * Row arithmetic on the narrowest pane we ship to (320px, where #dock-bar drops to an 8px inset and
 * 6px gaps): at rest 54 + 72 + 54 = 180px of controls, so the mic still has slack to centre in;
 * typing 56 + 54 = 110px of controls, leaving ~182px for the field, which still holds the whole
 * "Type a message…" placeholder.
 *
 * Declared on `#app[data-compose]` (one ID + one attribute) so it beats nothing and is beaten by
 * nothing: css/orb.css only ever READS --orb-size.
 */
#app[data-compose="closed"] {
  --orb-size: 72px;
}

#app[data-compose="open"] {
  --orb-size: 56px;
}

/* =============================================================================================
 * THE TYPE BUTTON — the way into the keyboard, and safety's twin.
 *
 * Deliberately the same object as #help-btn at the other end of the row, and now literally so: a
 * `--dock-ctl` squircle with its word beneath it, bare button around both. Two identical quiet
 * controls bracketing one bold one is what makes the mic read as the centre of the dock rather than
 * as the biggest of three buttons.
 *
 * ## What changed, and why the word came back
 * It was a 38px CIRCLE inside a 48px target, icon-only, on the argument that the keyboard glyph is
 * the convention and a word here would put a label under one bracket and not the other. That
 * argument was right about the asymmetry and wrong about which way to fix it: safety keeps its word
 * because a control a guest has to FIND needs a name, and "the way to type instead of talking" is
 * exactly such a control on a screen whose resting state hands the middle to a microphone. So both
 * brackets get a word now, and the asymmetry is gone the other way round.
 *
 * `.composer-toggle-label` therefore stops being visually-hidden. It carries the SHORT string
 * (i18n `typeShort` — "Type", one word, sized for a 54px column); the full "Type instead" is still
 * what `aria-label` announces, which is the same split safety makes between `.help-word` and its
 * own aria-label. See js/app.js applyLangChrome.
 *
 * A SQUIRCLE, not a circle, for the same reason safety is one: the mic is the filled circle in this
 * row, and a ringed circle beside it reads as a second, competing primary. Same paper, same
 * hairline, unmistakably not the mic.
 * ============================================================================================= */

#composer-toggle {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: calc(var(--dock-ctl) * var(--type-scale));
  min-height: calc(var(--dock-ctl) * var(--type-scale));
  height: auto;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  color: var(--ink-soft);
  /* The label's type, matched to safety's word so the two brackets are one object at two positions
     rather than two labels at two sizes. `font: inherit` first, because a <button> does not inherit
     the page's face on its own. */
  font: inherit;
  font-size: calc(10.5px * var(--type-scale));
  font-weight: 600;
  letter-spacing: 0.03em;
  line-height: 1.15;
  white-space: nowrap;
  cursor: pointer;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* The squircle, sized exactly as safety's: `box-sizing: border-box`, 54 minus two 15s = a 24px
   drawing — the app draws its glyphs at 18px inside a 38-40px control, and this is that proportion
   carried up to 54 rather than an 18px glyph rattling around in a bigger box. */
#composer-toggle .icon {
  box-sizing: border-box;
  width: calc(var(--dock-ctl) * var(--type-scale));
  height: calc(var(--dock-ctl) * var(--type-scale));
  padding: calc(15px * var(--type-scale));
  /* --radius-lg, not a bespoke 17px. The squircle proportion this note above describes wants a
     corner closer to a card's than a small control's — --radius (16px) reads noticeably sharper
     at this 54px size, --radius-lg (22px) keeps the soft, rounded-square character while landing
     on the same named tier #dock-bar .help-btn .icon uses just below, so the two brackets of the
     dock share one radius as well as one size token. */
  border-radius: var(--radius-lg);
  background: var(--glass-2);
  border: 1px solid var(--glass-line-strong);
  box-shadow: 0 1px 2px rgba(74, 35, 64, 0.05);
  /* NOT transitioned — both properties resolve through theme-swapped custom properties, which
     Chromium then freezes at the value they started from (see the long note on `.cap-pill`). This
     control was measured painting the LIGHT cream `rgba(255, 250, 244, 0.96)` in dark mode because
     of it. The hover below is a state change and still reads instantly. */
}

/* =============================================================================================
 * THE PUNCHLINE — the company's line, under the company's name.
 *
 * WELCOME SCREEN ONLY. Once a conversation starts the header collapses into its compact indigo
 * band and swaps to the wordmark lockup image; a second line of text there would fight a row whose
 * height the rest of the dock is measured against, and a guest mid-conversation is talking to a
 * concierge, not reading positioning.
 *
 * Quiet by construction: it sits UNDER a serif wordmark, so it takes the sans face at a small size
 * and a soft ink rather than competing for the same voice. `:empty` collapses it to nothing, so a
 * locale with no punchline costs no space instead of leaving a gap under the name.
 * ------------------------------------------------------------------------------------------- */
/* Not flush to the gutter. At 375px the line measured 343px inside a 343px box — not overflowing,
   but touching both edges, which over a photograph reads as text that has been cut off. A slightly
   tighter measure lets it settle onto two balanced lines with air either side; the punchline still
   gets its own line away from the wordmark, which is what `fix(brand)` asked for. */
#brand-punchline {
  /* Air via PADDING, not max-width. `flex-basis: 100%` below is what gives this its own full line
     in `.brand-row`; a max-width shrank the flex item to its measure and the punchline went back
     to sitting BESIDE the wordmark — undoing the whole point of `fix(brand)`. Padding leaves the
     basis at 100% and still keeps the text off both gutters. */
  padding-inline: var(--sp-3);
  text-wrap: balance;
  /* Its own full line in the wrapped `.brand-row`, the same way `.brand-mark`, `.brand-text` and
     `#brand-ornament` each get one. Order 5 puts it after the ornament (4) and before the language
     pill (6). Without `flex-basis: 100%` it is a flex item on a full line and collapses to zero
     width — which is exactly what happened when it lived inside `.brand-text`: every word wrapped
     onto its own line and the paragraph spilled across the language pills. */
  flex-basis: 100%;
  /* Same `order` as the ornament, which the rule further down pushes to 5 — so the lockup reads
     name, strapline, rule, rather than putting a hairline between a name and the line that
     belongs to it. */
  order: 4;
  text-align: center;
  margin: calc(var(--sp-1) * 0.5) 0 0;
  font-family: var(--font-sans);
  /* 12px, not 10.5. This sits over a PHOTOGRAPH, and small text over a sunrise is the one thing
     the welcome screen cannot do — the first draft shipped at 10.5px in `--ink-soft` and was
     barely legible against the bright half of the range. */
  font-size: calc(12px * var(--type-scale));
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1.3;
  /* The wordmark's own ink and glow, one step softer. `--ink-soft` has no contrast against a lit
     sky; `--indigo-ink` under a white glow is exactly how `#brand-name` solves the same problem two
     lines above, so the strapline reads as part of the same lockup rather than as something
     floating over the picture. */
  color: var(--indigo-ink);
  /*
   * FULL OPACITY, AND THE HALO IN THREE STEPS RATHER THAN ONE WIDE ONE.
   *
   * This line is dark ink on a PHOTOGRAPH, and on a wide pane it runs far enough across the hero to
   * cross the dark mountain ridge on the right — where dark-on-dark simply vanishes. The bright
   * half of the range was never the hard case; that is where dark ink does its best work.
   *
   * The single `0 1px 14px` glow could not answer it. A 14px blur behind 12px/600 text is wider
   * than the glyphs themselves, so it spreads to something faint over a large area instead of
   * building an opaque plate right at the strokes — and `opacity: 0.85` on the element scaled the
   * halo down along with the ink, weakening the one thing holding the line up.
   *
   * So: opacity to 1, and the halo stacked — a tight near-opaque core that fills the counters and
   * the gaps between letters, a mid step, and the original wide one kept for the soft edge that
   * ties the line to the lockup above it. Same look on the lit sky, legible on the ridge.
   */
  opacity: 1;
  text-shadow:
    0 0 3px rgba(255, 252, 246, 0.95),
    0 0 7px rgba(255, 252, 246, 0.9),
    0 1px 14px rgba(255, 252, 246, 0.8);
}

/* The rule goes BELOW the strapline. `#brand-ornament`'s own rule sets `order: 4`; this restates
   it at the same weight, later in the file, rather than editing that rule — it is scoped to the
   same two compositions and says why here, where the reason lives. */
#app[data-mode="active"]:has(#log:empty) #brand-ornament,
#app[data-mode="welcome"] #brand-ornament {
  order: 5;
}

#brand-punchline:empty {
  display: none;
}

/* Only the two compositions that draw the centred lockup draw this. Everywhere else the base rule
   below keeps it out of a header that has room for one line. */
#app[data-mode="active"]:has(#log:empty) #brand-punchline,
#app[data-mode="welcome"] #brand-punchline {
  display: block;
}

/* Drawn on the welcome screen and nowhere else. `display: none` is the base state so a mode this
   rule does not name can never surprise us with a second line in the header. */
#brand-punchline {
  display: none;
}

/* On the dark band the soft ink has nothing to sit on; the wordmark's own colour is what this
   line belongs to. */
:root[data-theme="dark"] #brand-punchline {
  color: var(--header-ink);
  opacity: 0.8;
  text-shadow: 0 2px 16px rgba(13, 9, 12, 0.7);
}

/* =============================================================================================
 * MUTE — a small status pill, not a second microphone (the UI polish pass).
 *
 * It used to be a squircle wearing the SAME plain-mic glyph the orb itself wears, just 44px
 * instead of 72 — which is "two competing microphone icons" by construction, not by accident, and
 * a text label under it (a later patch) treated the symptom without removing the second mic. This
 * drops the glyph entirely: a small dot (never mic-shaped) plus a word, in one pill shaped closer
 * to a status chip than to a button that DOES something — which is the point, since what it
 * mostly does is REPORT a state the mic itself is already living.
 *
 * NEUTRAL at rest: --glass-2 paper, --ink-soft ink, a plain grey dot — visually secondary to the
 * mic by construction, the same job the old squircle did, just without pretending to be a mic.
 *
 * ACTIVE (muted) takes --ok's sage/green trio — the SAME tokens the grounding cards already use
 * for "this is fine" — never ember: ember is this file's warning/error hue everywhere else, and a
 * guest choosing to mute themselves on purpose is not an error. --ok is also themed per mode
 * already (light/dark blocks, THE PALETTE), so this needs no dark-mode rule of its own the way the
 * old ember fill did.
 *
 * `:not([hidden])` guard, kept from the original: `[hidden]` is a UA rule at (0,0,1) and a bare
 * `.mute-btn { display: inline-flex }` at (0,1,0) would beat it — see the git history on this
 * block for the bug that shipped once already.
 * ------------------------------------------------------------------------------------------- */
.mute-btn:not([hidden]) {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: calc(6px * var(--type-scale));
  /* `auto`, not a small tuck-in gap: with the mic absolutely centred on the bar (THE MIC'S TWO
     SIZES) rather than centred in the flex slack, nothing needs to advertise where "the mic's own
     modifier" sits any more by proximity — what has to hold instead is that mute+help sit flush at
     the row's RIGHT EDGE regardless of mute's own (variable, text-driven) width, which is exactly
     what one auto margin on the first of the pair does. Only matters while mute is actually
     visible: OPEN mode force-hides it regardless (`#app[data-compose="open"] #mute-btn`, above). */
  margin-inline-start: auto;
  /* 36px, not --tap (48px): a status pill a guest reads more than presses, and the mic beside it
     is still the only 72px object in the row — see THE MIC'S TWO SIZES. Comfortably above the
     24px WCAG 2.5.8 floor and the tap target itself is the whole pill, padding included, not a
     bare glyph shrunk to fit one. */
  height: calc(36px * var(--type-scale));
  padding-inline: calc(12px * var(--type-scale));
  border-radius: var(--radius-pill);
  background: var(--glass-2);
  border: 1px solid var(--glass-line-strong);
  color: var(--ink-soft);
  font: inherit;
  font-size: calc(11.5px * var(--type-scale));
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(74, 35, 64, 0.05);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  /* Background/border/colour, not layout — a state flip, not a resize, so the pill never nudges
     its neighbours when it switches. */
  transition:
    background var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease);
}

.mute-btn:hover {
  background: var(--glass-3);
  border-color: rgba(74, 35, 64, 0.28);
}

.mute-btn:active {
  transform: scale(0.97);
}

/* The one glyph this control keeps — a plain dot, deliberately not mic-shaped, so it can never be
   mistaken for a second microphone at any size. */
.mute-dot {
  width: calc(7px * var(--type-scale));
  height: calc(7px * var(--type-scale));
  border-radius: 50%;
  background: var(--ink-faint);
  flex: 0 0 auto;
  transition: background var(--dur-fast) var(--ease);
}

/* MUTED is a state the guest must not be able to miss — a silenced mic that looks live is
   indistinguishable from a broken one. The pill still fills solid rather than merely swapping the
   dot, same reasoning the old ember rule had; the hue is the only thing that changed. */
.mute-btn[aria-pressed="true"] {
  background: var(--ok-bg);
  border-color: var(--ok-line);
  color: var(--ok-ink);
}

.mute-btn[aria-pressed="true"] .mute-dot {
  background: var(--ok);
}

/* And the mic itself says so, so the state is legible from the object the guest is looking at
   rather than only from the control they just pressed. `data-muted` is written by js/orb.js and
   sits alongside `data-state`: a muted call is still connecting/listening/speaking, it just is not
   hearing the room. The cues keep animating on purpose — the agent may still be talking. */
#orb[data-muted="1"] .orb-glyph,
#orb[data-muted="1"] .orb-glyph svg {
  color: var(--ember);
  opacity: 1;
}

#orb[data-muted="1"] .orb-core {
  box-shadow: 0 16px 34px -12px color-mix(in srgb, var(--ember) 40%, transparent),
    inset 0 0 0 2px color-mix(in srgb, var(--ember) 55%, transparent);
}

#composer-dock:has(#orb[data-muted="1"]) #orb-label {
  color: var(--ember);
  font-weight: 600;
}

#composer-toggle:hover .icon {
  background: var(--glass-3);
  border-color: rgba(74, 35, 64, 0.28);
}

#composer-toggle:active {
  transform: scale(0.97);
}

#composer-toggle:focus-visible {
  outline: none;
  box-shadow: none;
}

#composer-toggle:focus-visible .icon {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* The word under the keyboard glyph. --ink-soft (6.9:1 on the ground) is safety's label ink too, so
   the two brackets are one label at two positions. It is the SHORT string; the full one is the
   button's aria-label — see the note above and js/app.js.

   `max-width` + ellipsis rather than a per-locale size: "লিখুন" and "लिखें" are wider than "Type"
   and a clipped word is better than a control that grows and un-centres the mic. */
.composer-toggle-label {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-soft);
  font-weight: 600;
}

/* ---------------------------------------------------------------------------------------------
 * ...AND WHEN THERE IS NO SLACK TO DISTRIBUTE.
 *
 * A ladder of height tiers, cheapest rung first. Heights, not widths: height is what runs out, since
 * the pane is 100dvh and holds band + greeting + view + menu + dock. The order of sacrifice is a
 * product decision — ornament, then air, then the view, then the sub-line. Never the greeting, never
 * the safety control, never the touch floor.
 * ------------------------------------------------------------------------------------------- */

/* ~1366x768 with browser chrome, and a 640px Android. The ornament is pure decoration and the air
   around the greeting is negotiable; the view gives up a third of its band. */
@media (max-height: 680px) {
  .hero-ornament {
    display: none;
  }
  #hero {
    padding-bottom: var(--sp-3);
  }
  .hero-scene {
    max-height: 17vh;
    margin-top: var(--sp-2);
  }
}

/* Smaller again: the view goes entirely, and the sub-line with it. The greeting alone still says
   everything it has to, and the capability sheet — the thing a guest is here to use — gets all of
   the ~200px the two of them were spending. */
@media (max-height: 600px) {
  /* The plain `.hero-scene` selector above is specificity 10; WELCOME's own full-bleed override
     (`#app[data-mode="welcome"] .hero-scene`, added for the reference composition) is an ID plus an
     attribute plus a class, comfortably higher, and a higher-specificity rule wins regardless of
     which one sits inside a media query — so without this restating the hide with matching
     specificity, that rule's `display: block` would survive under 600px and the picture would never
     go. Same reasoning for #hero's own floor, below: a reserved 56vh with no picture in it is just
     empty cream above the greeting. */
  #app[data-mode="welcome"] .hero-scene {
    display: none;
  }
  #app[data-mode="welcome"] #hero {
    min-height: 0;
  }
  .hero-scene {
    display: none;
  }
  /*
   * ...AND THE SHEET STOPS RIDING UP.
   *
   * The overlap exists to put the sheet's rounded corners over the FOOT OF THE PHOTOGRAPH. With the
   * photograph gone there is nothing under it but the greeting, and #hero-copy carries z-index 3
   * against the sheet's 1 (css/layout.css) — so the sheet slid under the greeting and the two sets of
   * type painted through each other. Verified at 801x598, where the pane is 550px tall: "Good
   * evening, Ananya" and "Here are some things I can help with" occupied the same 20px.
   *
   * It was a latent bug at the old -22px and this pass's -30px made it unmissable. The fix is the
   * honest one: no picture, no overlap.
   */
  #app[data-mode="welcome"] #cap-panel {
    margin-top: 0;
  }
  #app[data-mode="welcome"] #greeting-sub {
    display: none;
  }
  #app[data-mode="welcome"] #greeting {
    font-size: clamp(24px, 5vw, 30px);
  }
  /* THE VIEW CONTINUED GOES WITH THE PICTURE — WELCOME ONLY, and that "only" is load-bearing.
     `.hero-scene` itself only goes dark in WELCOME mode at this height: the plain `.hero-scene {
     display: none }` two rules up is specificity 10, and per the note on it, that is NOT enough to
     beat the ACTIVE-mode variant's own `display: block` rule (THE PICTURE, above this file) — so a
     real guest, who is in `data-mode="active"` with an empty log (see index.html's entry guard),
     keeps the photograph under 600px. This selector used to ALSO hide `#canvas::before` for that
     same active state — which killed the continuation while the photo above it stayed lit, drawing
     exactly the hard line this whole pass exists to remove. Scoped to welcome alone now, so it only
     fires in the one state where the picture it is meant to continue has actually gone. */
  #app[data-mode="welcome"] #canvas::before {
    display: none;
  }
}

/* A landscape phone: ~340px of pane. The greeting shrinks to a line, the band and the stay row give
   up their air, and the privacy note goes — but the mic, the composer and safety all stay exactly
   where they were and exactly the size they were. A control that moves or shrinks between
   orientations is worse than a screen with less decoration on it. */
@media (max-height: 500px) {
  #app[data-mode="welcome"] #greeting {
    font-size: clamp(18px, 3vw, 22px);
  }
  #app[data-mode="welcome"] #hero-top {
    padding-bottom: var(--sp-2);
  }
  #app[data-mode="welcome"] #hero-copy {
    padding-top: var(--sp-2);
  }
  #dock-privacy {
    display: none;
  }
  /* The active lane also gives up its labels here — see the foot of THE ACTIVE LANE below, where it
     has to live to win against that block's own width. */
}

/* The retired full-bleed photo era: the two overlay divs are gone from index.html and #hero::after
   was a film-grain layer over them. The kill rule stays as a floor in case a cached index.html from
   an older deploy is still being served out of the service worker. */
.hero-img,
.hero-scrim,
#app[data-mode="welcome"] #hero::after {
  display: none !important;
  background: none !important;
}

/* ---------------------------------------------------------------------------------------------
 * THE HEADER BAND — and the PROPORTION that makes or breaks this palette.
 *
 * An earlier revision put the anchor colour behind the WHOLE hero, which meant roughly three
 * quarters of the screen was a dark saturated field. The app read as "a coloured app" rather than
 * as a warm one, which is the exact thing the brief rejected — and it is the same trap on indigo as
 * it was on teal.
 *
 * The manager dashboard is the lesson. Its anchor is a RAIL — a narrow strip, maybe 15% of the
 * pixels — and everything else is warm cream carrying ivory cards. The anchor is the anchor, not
 * the atmosphere.
 *
 * So the band shrinks to the chrome (#hero-top: lockup, language, guest strip) and the warm ground
 * takes the rest. The greeting and the orb now sit on cream, and the only large dark object left
 * on the page is the orb itself — which is the point, because it is the signature.
 *
 * The band is also the one piece of chrome shared with the WELCOME screen, in a different form:
 * there, the indigo is the CTA and the champagne is a hairline ornament under the eyebrow. Here the
 * indigo is the band and the champagne is a hairline along its foot (#hero-top::before). Same two
 * colours, same relationship, so the two screens read as one piece of stationery.
 * ------------------------------------------------------------------------------------------- */

/*
 * The hero is a stack of three sections and nothing else — no background, no scrim, no reserved
 * range height. The greeting and the view are SIBLINGS now rather than copy laid over art, which is
 * what retires the whole class of bug this rule used to carry a page of notes about: the subtitle
 * landing on the pale back ridge and turning to mush, the copy having nowhere to go once the band,
 * the greeting and the range all wanted the same 371px.
 */
#hero {
  background: none;
  border: 0;
  color: var(--ink);
}

#hero-top {
  background: var(--header-bg);
  border-radius: 0 0 var(--radius-xl, 28px) var(--radius-xl, 28px);
  box-shadow: 0 14px 30px -24px rgba(74, 35, 64, 0.55);
  color: var(--header-ink);
  /* pull the band out to the pane edges; #hero's own padding would otherwise inset it */
  margin: calc(-1 * (env(safe-area-inset-top) + var(--sp-4))) calc(-1 * var(--sp-4)) 0;
  /* The lockup row sets its own height off the 48px chrome controls, so the band's vertical padding
     is air and nothing else. --sp-1 top and bottom (down from --sp-2, then --sp-3 before that):
     4px of clearance around a row of 48px circles that were never touching the edge anyway, and
     every pixel this gives back is a pixel the transcript gets instead — requested directly, and
     the floor this can shrink to before the band starts to look cramped around the chrome. The
     narrow-pane tier further down already lands here for `padding-bottom` alone — it is now a
     no-op at this width, restated rather than removed since it costs nothing to leave. */
  padding: calc(env(safe-area-inset-top) + var(--sp-1)) var(--sp-4) var(--sp-1);
}

/* Dark mode: glass, not a second solid slab. --header-bg is the same opaque indigo gradient in
   both themes (this file's own palette note), which in chat mode — no mountain behind it — is the
   "solid purple bar" a dark pass has to lose. A translucent blur reads as dark chrome over
   whatever sits behind it (the mountain pre-chat, the dark canvas once a conversation starts). */
:root[data-theme="dark"] #hero-top {
  /* --header-bg, not a second hardcoded gradient: the base rule above already reads that token, and
     the dark palette block now defines it as this theme's own translucent glass — so the only thing
     this rule still has to add is the blur and the softer shadow. One source of truth for the band's
     colour, per theme, instead of a literal here that silently shadowed the token. */
  background: var(--header-bg);
  -webkit-backdrop-filter: blur(20px) saturate(140%);
  backdrop-filter: blur(20px) saturate(140%);
  /* Soft, not heavy — a hard black drop under a translucent band over a near-black page reads as a
     seam rather than as lift. */
  box-shadow: 0 14px 30px -22px rgba(0, 0, 0, 0.5);
}

/*
 * The gilt edge. One champagne hairline along the band's foot, fading out at both ends so it reads
 * as an engraved rule rather than as a border on a box — the welcome card's ornament (hairline,
 * diamond, hairline) doing the same job in the space a header has for it.
 *
 * Inset by the corner radius so it stops where the curve starts: run edge to edge and it would cut
 * across the two rounded corners as a chord, which is the detail that separates a gilt edge from a
 * stray line. `pointer-events: none` because it sits over the band's own padding, where the
 * language control and the overflow summary are.
 *
 * No `position: relative` needed on #hero-top — layout.css already sets it (and a z-index with it,
 * for the two popups that hang below the band). Re-declaring it here would look like this rule's
 * requirement when it is actually the band's own.
 */
#hero-top::before {
  content: "";
  position: absolute;
  right: var(--radius-xl, 28px);
  bottom: 0;
  left: var(--radius-xl, 28px);
  height: 1px;
  pointer-events: none;
  background: var(--header-rule);
  opacity: 0.7;
}

/* =============================================================================================
 * THE BAND'S FOOT — a wave, not a straight edge. CHAT-ACTIVE ONLY.
 *
 * The band's bottom was two 28px corner radii: soft at the corners, dead straight for the 300-odd
 * pixels between them, which is what made it read as a slab laid on the page rather than as chrome
 * the page flows out of. This replaces that straight run with ONE gentle undulation — a single
 * asymmetric curve, 18px at its deepest, not a scallop and not a repeating pattern.
 *
 * Done with a two-layer MASK: the 18px stays 18px at every width because the wave layer is sized
 * in pixels while the rectangle above it takes the remaining height, and `mask-composite` is left
 * at its `add` default so the two layers union into one silhouette.
 *
 * THE MASK IS ON ::after, NOT ON #hero-top, AND THAT IS THE WHOLE POINT OF THIS RULE.
 *
 * A mask clips an element's DESCENDANTS as well as its own paint, and #hero-top is not just a
 * bar — it is the anchor for the two popups that hang BELOW it: the language picker (#lang-menu)
 * and the stay sheet (#stay-sheet, via .stay-anchor). Masking the band itself cut both of them
 * off at the wave, leaving a 15px tab of a menu nobody could read or use. So the band's PAINT
 * moves to a pseudo-element that holds nothing, and the mask goes there.
 *
 * `z-index: -1` puts that layer behind the chrome row without needing a z-index on every child.
 * It is safe to go negative here because #hero-top is `position: relative; z-index: 4`
 * (css/layout.css) and so is already a stacking context — the layer cannot escape behind the
 * hero or the transcript, only behind its own siblings.
 *
 * Scoped to the chat-active state on purpose. Pre-chat and welcome put the band in its
 * TRANSPARENT form over the picture (THE HEADER, OPEN OVER THE PICTURE, above) where there is no
 * edge to soften, and where the extra 18px of foot would push a composition that is already
 * measured against the photo.
 *
 * The gilt hairline moves up to the wave's crest so it still reads as the band's own edge instead
 * of cutting across the curve, and the corner radii go — a radius and a wave on the same edge is
 * two ideas about the same 18px.
 * ============================================================================================= */
#app[data-mode="active"]:not(:has(#log:empty)) #hero-top {
  border-radius: 0;
  /* The wave's own depth, added under the band's existing --sp-1 so no chrome lands in the trough. */
  padding-bottom: calc(var(--sp-1) + 18px);
  /* The ::after below paints the band instead, so this element has nothing of its own to show. */
  background: none;
  /* A straight-edged drop shadow under a wavy edge reads as a second, wrong edge. The wave and the
     gilt hairline are the separation now. (A `filter: drop-shadow` on the layer would follow the
     silhouette, but filters resolve BEFORE masks, so the mask would clip the shadow away again.) */
  box-shadow: none;
}

#app[data-mode="active"]:not(:has(#log:empty)) #hero-top::after {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  background: var(--header-bg);
  -webkit-mask:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 18' preserveAspectRatio='none'%3E%3Cpath fill='%23000' d='M0,0 L1200,0 L1200,6 C950,17 350,-1 0,11 Z'/%3E%3C/svg%3E")
      no-repeat bottom / 100% 18px,
    linear-gradient(#000, #000) no-repeat top / 100% calc(100% - 18px);
  mask:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 18' preserveAspectRatio='none'%3E%3Cpath fill='%23000' d='M0,0 L1200,0 L1200,6 C950,17 350,-1 0,11 Z'/%3E%3C/svg%3E")
      no-repeat bottom / 100% 18px,
    linear-gradient(#000, #000) no-repeat top / 100% calc(100% - 18px);
}

/* Dark's glass band follows the paint onto the layer. Left on #hero-top the blur would resolve
   over the element's full rectangle — a blurred straight edge behind a wavy painted one. */
:root[data-theme="dark"] #app[data-mode="active"]:not(:has(#log:empty)) #hero-top {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

:root[data-theme="dark"] #app[data-mode="active"]:not(:has(#log:empty)) #hero-top::after {
  -webkit-backdrop-filter: blur(20px) saturate(140%);
  backdrop-filter: blur(20px) saturate(140%);
}

#app[data-mode="active"]:not(:has(#log:empty)) #hero-top::before {
  bottom: 18px;
  right: 0;
  left: 0;
}

/* =============================================================================================
 * THE WORDMARK'S WEIGHT CONTRAST — one word, two weights, everywhere it is set as live text.
 *
 * Atithi at 600 in the surrounding ink, Vani at 400 in the accent. That contrast IS the mark:
 * the brand has no camel-case capital and no space, so if both halves render at one weight and
 * one colour the wordmark is simply wrong, not merely plainer.
 *
 * Live text rather than the flat lockup PNG/SVG in the places that can take it, because an <img>
 * cannot inherit a token — and these two spans sit on cream (the drawer), on a photograph (the
 * welcome lockup) and on the plum band (the chat header), each of which needs a different pair
 * of inks. One markup, three grounds, no extra asset.
 * ============================================================================================= */
.wm-a {
  font-weight: 600;
}

.wm-v {
  font-weight: 400;
  /* --vani, NOT --accent and NOT --violet. `color: var(--accent)` here painted Vani the same
     plum as Atithi, which is the whole contrast gone; --violet is the shared accent-orange used
     by the orb/live-dot/icons, and tuning it just for this word would recolor all of those too. */
  color: var(--vani);
}

/* Chrome on the band takes the band's inks. */
#hero-top #brand-name {
  color: var(--header-ink);
}

/* On the band, Vani takes its own LIT step. This is the brand's own rule for an accent on
   surface-inverse (the eyebrow spec: "accent on light grounds and ridge-far on surface-inverse")
   and not a nudge for contrast's sake — --vani is tuned to be read on cream and lands at roughly
   2:1 on the plum band. `--ridge-far` is the brand system's name for this step and does NOT exist
   in this file; an undefined custom property makes the whole declaration invalid at computed-value
   time, so naming it here silently left Vani inheriting the band's ink. */
#hero-top .wm-v {
  color: var(--vani-lit);
}

/* A touch brighter than the band's own --header-ink, and only for the wordmark: the header band is
   now translucent glass with the dimmed mountain showing through it (THE HEADER BAND, above), so
   the one word identifying the product wants slightly more lift than the rest of the chrome ink. */
:root[data-theme="dark"] #hero-top #brand-name {
  color: #fdfaf5;
  text-shadow: 0 1px 10px rgba(10, 7, 9, 0.4);
}

#hero-top .signin-label,
#hero-top .signin-btn .icon {
  color: var(--header-ink-soft);
}

/* Panes on the band are white-translucent glass — ivory glass would disappear into the indigo. The
   stay row is the one exception and is deliberately NOT in this list: it is a sheet of paper laid on
   the band, not a pane cut out of it (see THE STAY CONTEXT below). */
#hero-top .lang-menu > summary,
#hero-top .signin-btn,
#hero-top .chrome-icon-btn,
#hero-top .brand-mark {
  background: rgba(255, 255, 255, 0.1);
  border-color: rgba(255, 255, 255, 0.18);
  box-shadow: none;
}

/* =============================================================================================
 * THE CHROME ICON BUTTONS — one 48px circle, and two jobs.
 *
 * Shared by #menu-btn (the drawer trigger, leading edge of the band) and #theme-btn (trailing edge,
 * beside language). Both are a single glyph on the indigo band and nothing else — no label text at
 * this size, which is why syncThemeButton's own label span is hidden here (see the note below) but
 * still written, so the accessible name never depends on which face of the button is showing.
 *
 * #menu-btn specifically: a <button aria-haspopup="dialog">, not a <summary> — what it opens is a
 * modal panel that inerts the page behind it, and a disclosure's semantics would promise the
 * opposite. It leads the band rather than trailing it because the drawer it opens slides in from
 * the LEFT, and a trigger on the same side as the panel it summons is the one placement that does
 * not send a guest's eye across the whole header to find where the tap went.
 * ============================================================================================= */

.chrome-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: calc(var(--tap) * var(--type-scale));
  height: calc(var(--tap) * var(--type-scale));
  padding: 0;
  border-radius: var(--radius-pill);
  border: 1px solid var(--ghost-line);
  color: var(--header-ink);
  cursor: pointer;
  /* NOT transitioned — see the note on `.cap-pill`. Measured frozen at the light theme's
     `rgba(255, 252, 246, 0.16)` in dark mode. */
}

.chrome-icon-btn:hover {
  background: rgba(255, 255, 255, 0.18);
}

.chrome-icon-btn .icon {
  width: 22px;
  height: 22px;
}

/* #theme-btn is a `.chrome-icon-btn` AND carries syncThemeButton's icon+label pair (js/app.js) —
   the label is what the drawer's row version of this button showed, and it stays in the DOM so the
   accessible name is never in question, just not drawn: a second glyph-and-word button beside the
   hamburger and the language pill is more chrome than a 48px band can wear. */
#theme-btn span:last-child {
  display: none;
}

/* The sun/moon crossfade (js/app.js syncThemeButton, via js/motion-fx.js): for the length of the
   transition there are TWO `.icon` spans in the button, and this is what makes them sit exactly on
   top of one another — centred, stacked — rather than side by side, so the swap reads as one mark
   turning into the other. `#menu-btn`'s single hamburger glyph never has a second `.icon` sibling,
   so this rule never has anything else to affect.
 *
 * Centred by `top`/`left` MATH, not `transform: translate(-50%, -50%)`: js/motion-fx.js's
 * `animate()` writes rotate/scale straight onto this element's `transform` for the crossfade, and
 * that write REPLACES the whole property rather than composing with it — a transform-based centring
 * trick would be overwritten the instant the animation starts, and the icon would visibly jump.
 * `width`/`height` are re-stated here (not left to `.chrome-icon-btn .icon`'s 22px) so this rule is
 * self-contained: the old version relied on that inherited size while ALSO setting `inset: 0`, which
 * is over-constrained for an absolutely-positioned box with an explicit width — the browser resolved
 * it by dropping `left` and anchoring the 22px box to the button's top-right corner instead of its
 * centre. That was the "awkward" rendering: a small icon pinned off to one side of a 48px circle.
 */
#theme-btn {
  position: relative;
}

#theme-btn .icon {
  position: absolute;
  top: calc(50% - 11px);
  left: calc(50% - 11px);
  width: 22px;
  height: 22px;
}

/* Sun vs moon get the same weight and the same neutral ink as every other chrome glyph
   (.chrome-icon-btn's --header-ink) — matching, not distinct, is the point there. The one place
   the two states differ is a warmth wash on the sun, which shows only when the app's OWN theme is
   dark: existing tokens, blended rather than swapped outright, so "distinct" does not tip into
   "a different icon language" for what is still one control in the same row as the hamburger and
   the language globe. */
:root[data-theme="dark"] #theme-btn .icon {
  color: color-mix(in srgb, var(--gold-lit) 45%, var(--header-ink));
}

/* Focus on the band. --focus-ring is a translucent indigo tuned for the cream ground; on the deep
   indigo band it is indigo on indigo and a keyboard guest sees nothing at all. White is the band's
   own ink. */
#hero-top .lang-menu > summary:focus-visible,
#hero-top .chrome-icon-btn:focus-visible,
#hero-top .signin-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.6);
}

/* =============================================================================================
 * THE UTILITY DRAWER — a concierge's utility panel, not an app's navigation menu.
 *
 * Slides in from the LEFT over the conversation, ~78% of the pane wide, so a strip of Atithi stays
 * visible behind the scrim: the drawer is a layer ON the concierge, and a guest should never lose
 * the thread they were in the middle of. (The narrower ~60% variant was tried and rejected — the
 * same five rows had to drop their secondary text to avoid wrapping, which is less legible, not
 * lighter.)
 *
 * `visibility` alongside `transform` rather than `display`: the slide has to animate, and a closed
 * drawer must not be reachable by Tab even in the frame before js/app.js sets `inert` on the panel
 * itself. That same function inerts every OTHER child of #chat-pane while the drawer is open — the
 * browser enforcing the trap rather than a hand-rolled Tab cycle, the same technique
 * js/manager/dialog.js uses against <body>.
 * ============================================================================================= */

/*
 * ABSOLUTE, NOT FIXED — and this is the whole reason the drawer now lives inside #chat-pane.
 *
 * `position: fixed` resolves against the VIEWPORT, so on any screen ≥720px (where layout.css centres
 * #chat-pane as a 460/480px column with a rounded, shadowed edge) the scrim blacked out the entire
 * browser window and the panel slid in from the far left of the monitor — metres from the concierge
 * it belongs to. On a phone the two coincide, which is why it looked correct there and only there.
 *
 * #chat-pane is already `position: relative` (layout.css, for the #market-panel/#login-panel
 * overlays) and already `overflow: hidden`, so it is both the containing block and the clip: the
 * scrim covers exactly the app's own frame, the drawer parks off ITS left edge rather than the
 * window's, and neither can escape the pane's rounded corners.
 */
.drawer-scrim {
  position: absolute;
  inset: 0;
  z-index: 55;
  background: color-mix(in srgb, var(--indigo-ink) 52%, transparent);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

body.drawer-open .drawer-scrim {
  opacity: 1;
}

.utility-drawer {
  /* Absolute against #chat-pane, not fixed against the viewport — see the note on .drawer-scrim. */
  position: absolute;
  inset: 0 auto 0 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  /* ~78% of THE PANE (cqi = the #chat-pane container's inline size), not of the viewport. `78vw`
     read the window, so on a 1280px desktop the panel was ~998px wide inside a 480px column and its
     "78%" meant nothing; on a phone the two happened to agree. At 375px this is 292px; at the 320px
     floor, 250px — still room for "What can Atithi do?" on one line in every shipped locale. */
  width: min(78cqi, 21rem);
  /* 100% of the pane, which is already the dvh-aware height (layout.css sizes #app/#chat-pane with
     100dvh, minus its own margin above 720px) — so this inherits the keyboard-safe height rather
     than re-deriving it and overshooting the pane's rounded foot. */
  height: 100%;
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  background: var(--canvas-lift);
  border-right: 1px solid var(--hairline);
  /* Off-canvas by default. The extra 2% clears its own shadow, so no smudge sits on the closed
     screen's left edge. */
  transform: translateX(-102%);
  visibility: hidden;
  overflow: hidden;
  transition:
    transform var(--dur) var(--ease),
    visibility var(--dur) step-end;
}

body.drawer-open .utility-drawer {
  transform: none;
  visibility: visible;
  box-shadow: var(--shadow-3);
  transition:
    transform var(--dur) var(--ease),
    visibility 0s;
}

/* While a finger is on it the drawer follows the finger, so a transition would fight the drag. */
body.drawer-dragging .utility-drawer {
  transition: none;
}

/*
 * The paper. One botanical sprig in the panel's own bottom corner, at an opacity where it is the
 * grain of the stock rather than a drawing on it — the same asset, the same masking technique and
 * the same intent as the welcome card's corners and the page's own edges (body::after). It is what
 * makes a list of settings read as hotel stationery rather than as a generic nav drawer, and it
 * costs one pseudo-element and no request: brand/botanical-sprig.svg is already precached by the
 * service worker and already painted elsewhere on this screen.
 *
 * Bottom-LEFT and past both edges, under the account group's trailing corner where no label reaches.
 */
.utility-drawer::after {
  content: "";
  position: absolute;
  z-index: 0;
  left: -30px;
  bottom: -44px;
  width: 124px;
  height: 167px; /* the drawing's own 270/200 ratio */
  pointer-events: none;
  opacity: 0.08;
  background-color: var(--gold);
  -webkit-mask: url(/brand/botanical-sprig.svg) no-repeat center / contain;
  mask: url(/brand/botanical-sprig.svg) no-repeat center / contain;
}

:root[data-theme="dark"] .utility-drawer::after {
  opacity: 0.06;
}

/* The head: the wordmark, and the close action. Everything sits above the sprig. */
.drawer-head {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  flex: 0 0 auto;
  padding: var(--sp-4) var(--sp-3) var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--hairline);
}

/* The display serif, at the size the header's own lockup uses — the drawer is a room in the same
   building, so it is signed the same way. */
.drawer-title {
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: 1.1;
  color: var(--indigo);
  letter-spacing: 0.01em;
}

/* The deep indigo that signs the panel in light mode is indigo-on-indigo against the dark canvas
   (1.2:1 — the wordmark all but disappeared). The lit end of the lavender ramp is the same hue with
   the value inverted, which is exactly what --violet-deep/--brand-ink already do elsewhere. */
:root[data-theme="dark"] .drawer-title {
  color: var(--brand-light);
}

/* The crest and the wordmark+tagline block, on one row, so the close button stays vertically
   centred against the pair rather than against the wordmark alone. */
.drawer-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

/* The header's own #brand-mark ring (see daylight.css .brand-mark / glass.css #hero-top
   .brand-mark) at drawer scale: the same champagne hairline framing the same crest, so the panel
   is signed the way the header already is rather than inventing a second badge language. Smaller
   than the header's 36/44px — this sits beside an 18px tagline, not a full chrome band. */
.drawer-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: 9px;
  overflow: hidden;
  background: var(--canvas-lift);
  border: 1px solid rgba(242, 96, 63, 0.5);
}

.drawer-mark img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.drawer-lockup {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

/* Quiet on purpose: the tagline is signage, not a heading. --fs-micro at --ink-soft is the same
   weight the drawer's own group labels sit at, one step below the serif above it. */
.drawer-tagline {
  margin: 0;
  font-size: var(--fs-micro);
  line-height: 1.3;
  color: var(--ink-soft);
}

/*
 * PROPERTY ESSENTIALS. The rows reuse `.drawer-row` for their box, padding and hover, so they line
 * up down the same left edge as the controls below them — but they are facts, not actions (bar the
 * front desk, which is a real tel: link), so the text inside stacks a label over its value instead
 * of sitting on one line.
 *
 * The rows also sit inside ONE card (below) rather than floating loose in the group like the
 * Atithi/Account rows do: these are the facts a guest came looking for, so they read as a single
 * kept reference rather than three more items in a settings list.
 */
#drawer-essentials-rows {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  overflow: hidden;
}

#drawer-essentials-rows .essential-row {
  border-radius: 0;
}

#drawer-essentials-rows .essential-row + .essential-row {
  border-top: 1px solid var(--hairline);
}

.essential-row {
  align-items: flex-start;
  /* .drawer-row is sized for a single line of label; these carry two or three. */
  min-height: 0;
  padding-block: var(--sp-2);
  /* A fact is not a button. The front-desk row IS an <a> and gets the pointer back below. */
  cursor: default;
  text-decoration: none;
}

a.essential-row {
  cursor: pointer;
}

.essential-row .icon {
  /* Optically aligned with the label's cap-height rather than the whole stacked block. The disc
     is bigger than the wordmark-sized icon it replaced, so this no longer needs the 1px nudge the
     plain glyph did. */
  margin-top: 0;
  flex: 0 0 auto;
}

.essential-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  /* Only matters on the front-desk row, which carries a trailing chevron after this: lets the
     chevron sit at the row's far edge instead of crowding the value. A no-op for wifi/checkout,
     which have nothing after them to push away from. */
  flex: 1 1 auto;
}

.essential-label {
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.essential-value {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink);
  /* A network name or a phone number is a string to read character by character, so it must be
     able to break rather than overflow the 250px-at-320px panel. */
  overflow-wrap: anywhere;
}

/* The Wi-Fi password — the one value here a guest copies rather than reads once, so it is set in
   the mono-ish tabular treatment numbers get elsewhere in this app and is explicitly selectable. */
.essential-sub {
  font-size: var(--fs-small);
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
  user-select: text;
}

.drawer-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: calc(var(--tap) * var(--type-scale));
  height: calc(var(--tap) * var(--type-scale));
  padding: 0;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--ink-soft);
  cursor: pointer;
  transition:
    background var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease);
}

.drawer-close:hover {
  background: var(--lavender);
  color: var(--indigo);
}

.drawer-close .icon {
  width: 18px;
  height: 18px;
}

/* The one scroller. Natural-height groups inside it, so nothing nests a second scrollbar. */
.drawer-scroll {
  position: relative;
  z-index: 1;
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--sp-4) var(--sp-2) var(--sp-5);
}

/* Whitespace is the grouping. Four short groups with air between them read as a considered list;
   the same five rows with no gaps read as a dropdown that grew. */
.drawer-group + .drawer-group {
  margin-top: var(--sp-5);
}

/* Sign out is the one row here that ends the session, so it gets the only rule in the panel. */
.drawer-group--account {
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--hairline);
}

/* The champagne eyebrow — --brand-ink, which IS the readable gold and inverts with the theme
   (#a33b21 at 6.3:1 on cream, #ffb89d at 8.4:1 on the dark card). Never --gold: the fill value is
   2.1:1 and would make these labels a swatch rather than a word. */
.drawer-group-label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-2);
  padding-inline: var(--sp-3);
  color: var(--brand-ink);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.drawer-group-name {
  min-width: 0;
}

/* Rows: the label is the target, so each is a full-width 48px row with the icon leading. The labels
   are never hidden at any width — a list of unlabelled glyphs is a guessing game. */
/* The staff-dashboard row is the one `<a>` among these rows (it leaves the app for another
   surface), and every other rule here was written for a <button>. Without this the browser's own
   anchor styling paints an underline through the label — the only row in the drawer wearing one. */
.drawer-row {
  text-decoration: none;
  display: flex;
  align-items: center;
  /* Explicit, because .btn (chat.css) centres its content — without it the icons landed at
     different x positions and the list had no edge to read down. */
  justify-content: flex-start;
  gap: var(--sp-3);
  width: 100%;
  min-height: var(--tap);
  padding: var(--sp-2) var(--sp-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-small);
  font-weight: 600;
  text-align: start;
  cursor: pointer;
  transition:
    background var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

.drawer-row:hover {
  background: var(--lavender);
}

/* Press feedback, and the only movement any row makes. */
.drawer-row:active {
  transform: scale(0.985);
}

/* Spoken replies "on" (js/app.js syncTtsButton toggles `.active`) now reads through the switch
   below alone — the row itself stays the same plain object at rest as every other row, and only
   the one control that actually has two states changes appearance. It used to wash the whole row
   in lavender and set the label bold, which is the "this is the current selection" grammar a
   language picker needs (many rows, one active) applied to a control that is not a picker at all
   (one row, two states) — a guest reads a switch faster than a recolored row, and does not have to
   learn a second meaning for the same lavender the essentials card and every icon disc already use
   for something else. */
.switch {
  position: relative;
  flex: 0 0 auto;
  width: 40px;
  height: 24px;
  border-radius: var(--radius-pill);
  background: var(--hairline-strong);
  transition: background var(--dur-fast) var(--ease);
}

.switch-thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  /* --on-indigo, not --surface: the thumb has to read against BOTH the off track
     (--hairline-strong) and the on track (--violet), in both themes, without flipping tone the
     way --surface does — --on-indigo is the one ivory the palette already keeps fixed across
     themes for exactly that "readable on the fill" job (see its definition, THE ACCENT). */
  background: var(--on-indigo);
  box-shadow: var(--shadow-2);
  transition: transform var(--dur-fast) var(--ease);
}

.drawer-row.active .switch {
  background: var(--violet);
}

/* 40px track, 18px thumb, 3px inset each side: 40 - 18 - 3 - 3 = 16px of travel. */
.drawer-row.active .switch-thumb {
  transform: translateX(16px);
}

/* The leading glyph, in a lavender disc — the same treatment `.drawer-about .cap-item .icon` gives
   the capability list two groups down, so a guest who has opened "What can Atithi do?" once
   already knows this shape means "this row's own icon". One disc language for the whole panel
   instead of a plain glyph here and a dressed one there. */
.drawer-row .icon {
  width: calc(28px * var(--type-scale));
  height: calc(28px * var(--type-scale));
  padding: calc(5px * var(--type-scale));
  border-radius: 50%;
  background: var(--lavender);
  color: var(--violet-deep);
  flex: 0 0 auto;
}

/* Lets the trailing chevron/switch (#about-toggle's chevron, the front-desk row's chevron, and the
   switch below) sit at the row's far edge instead of crowding the label — every other .drawer-row
   has exactly two children (icon, label) and this rule is a no-op for them since there is nothing
   after the label to push away from. */
.about-label,
.tts-label {
  flex: 1 1 auto;
  text-align: start;
}

/* The "this row opens something / goes somewhere" chevron — #about-toggle's own disclosure arrow
   (rotates to point up once open, the same convention the language popover's summary arrow uses)
   and the front-desk row's static "this is a tap-to-call link" hint. Reset back to a plain glyph:
   without this it would inherit the lavender-disc treatment above meant for the row's LEADING
   icon, and two discs on one row reads as a mistake, not emphasis. */
.drawer-row .icon.drawer-row-chevron {
  width: 16px;
  height: 16px;
  padding: 0;
  border-radius: 0;
  background: none;
  color: var(--ink-faint);
  transition: transform var(--dur-fast) var(--ease);
}

#about-toggle[aria-expanded="true"] .drawer-row-chevron {
  transform: rotate(180deg);
}

/*
 * The capability map, expanded in place. It used to be `renderAbout`'s own full-screen overlay
 * sheet (.cap-sheet, with a serif title and a close button); mounted here instead, it is content
 * INSIDE an already-open, already-focus-trapped drawer, so neither of those is needed — `js/app.js`
 * calls `renderAbout` with no `onClose`, which is what drops the close button, and this rule
 * un-does `.cap-sheet`'s own card styling (border/shadow/radius/background) since the drawer
 * itself is already the card.
 */
.drawer-about {
  padding: 0 var(--sp-3) var(--sp-2);
}

.drawer-about .cap-sheet {
  background: none;
  border: 0;
  box-shadow: none;
  border-radius: 0;
  padding: 0;
  max-height: none;
}

.drawer-about .cap-head {
  display: none;
}

.drawer-row--signout .icon {
  color: var(--ink-faint);
}

/* No focus rule of its own, deliberately. The drawer is cream paper, and --focus-ring (layout.css,
   plus the accessibility floor at the foot of this file) is the ring already tuned for exactly that
   ground — translucent indigo in light, lavender in dark. The white ring on the header's
   `.chrome-icon-btn`/`.lang-menu` above is the exception, and it exists only because those controls
   sit on the deep indigo band where an indigo ring is invisible. Adding a second convention here
   would have been the drawer disagreeing with the rest of the app about what focus looks like.
 */

/*
 * REDUCED MOTION. The drawer still has to arrive and leave — a control that never appears is not an
 * accessibility win — so the slide and the fade collapse to ~nothing rather than being removed, and
 * `visibility` keeps its step timing so a closed drawer stays untabbable.
 */
@media (prefers-reduced-motion: reduce) {
  .drawer-scrim,
  .utility-drawer,
  body.drawer-open .utility-drawer {
    transition-duration: 1ms;
  }

  .drawer-row:active {
    transform: none;
  }
}

/* =============================================================================================
 * THE BAND ON A PHONE — and why there is so little left in this ladder.
 *
 * The band used to fight itself for width. Its h1 was the PROPERTY's name (two clamped serif lines
 * on a phone), under a four-line strapline, beside a sign-in pill, a 94px language pill and the
 * overflow button — so a 390px screen rendered "Ati" next to a tower of "YOUR HIMALAYAN TRIP
 * COMPANION" and every tier below was an order-of-sacrifice decision about what to hide.
 *
 * Three of those four pressures are gone. The h1 is one short word ("Atithi") at every width and in
 * every tenant, the strapline and the eyebrow under it are deleted, the sign-in pill is permanently
 * hidden, and the language control is a 62px "◎ EN" disclosure instead of a select sized to the word
 * "English". The lockup now has ~200px of a 390px band for six characters. Nothing has to give.
 * ============================================================================================= */

@container pane (max-width: 480px) {
  /* The mark reads perfectly well at 36, and the 4px is worth more to the row's air than to the
     badge — the one tightening from the old ladder that is still worth having. */
  .brand-mark {
    width: 36px;
    height: 36px;
  }

  .brand-row {
    gap: var(--sp-2);
  }
}

/* On a phone the jump control is a circle, not a 140px labelled pill: it floats over the transcript
   the way every scroll-to-latest affordance does, so the less of a message it can sit on, the better.
   The wording survives as the aria-label (js/app.js), which is what a screen reader reads anyway. */
@container pane (max-width: 600px) {
  #jump-latest {
    width: calc(var(--tap) * var(--type-scale));
    padding: 0;
    justify-content: center;
    border-radius: 50%;
  }
  #jump-latest span:last-child {
    display: none;
  }
}

/* The starter-chip lane's dock dressing went with the lane itself — the dock carries exactly one
   suggestion surface now and it is #cap-rail. See the #chips note in css/orb.css. */

/* Every width: the chrome cluster never compresses, and the lockup is what absorbs the difference. */
.brand-row {
  min-width: 0;
}

/*
 * 320-359px: the wordmark goes, rather than being absorbed down to nothing.
 *
 * "The lockup absorbs the difference" is the right rule until there is no difference left to
 * absorb. Measured at 320px: the hamburger (48) + the crest (36) + the chrome cluster (174) + four
 * 8px gaps leaves the <h1> SIX PIXELS, so the header rendered a one-pixel-wide sliver of a serif
 * "A" between the crest and the globe. That is not a compressed wordmark, it is a rendering
 * artefact that looks like a bug.
 *
 * The crest alone is the honest thing to show at this width — it is the mark on the favicon, the
 * PWA icon and the tenant's own lockup slot, so it is the part a guest recognises, and #brand-name
 * stays in the DOM as the accessible name. The ~44px it frees goes to the row's own gap, which is
 * what was making five controls sit shoulder to shoulder at the one width where thumbs have the
 * least room to miss.
 *
 * 359px matches the tier orb.css and the cap-panel already use for the same "narrowest real phone"
 * case, rather than inventing a fourth breakpoint.
 */
@container pane (max-width: 359px) {
  .brand-text {
    display: none;
  }

  .brand-row {
    gap: var(--sp-3);
  }
}

.signin-btn {
  min-height: calc(var(--tap) * var(--type-scale)); /* was 36px */
}

/* =============================================================================================
 * THE LANGUAGE CONTROL — "◎ EN", and an ivory popover of endonyms, back in the header.
 *
 * Every part of this is drawn by us. What it replaces was a native <select> dressed to look like a
 * pill: the closed face could only ever be one of its own options (so the endonym was pushed out of
 * sight with `color: transparent` and a two-character span laid over it), the globe and the caret
 * were `pointer-events: none` decorations positioned absolutely inside it, and the OPEN face
 * belonged to the operating system — on Android a full-width grey sheet from the foot of the screen,
 * on Windows a square white box with a system-blue highlight. There were ~200 lines across two
 * stylesheets trying to make that not so, including a whole `@supports (appearance: base-select)`
 * block for the one engine family that allows it.
 *
 * The <select> is still the value holder and still fires the change event (see the note in
 * css/chat.css); nothing about the language system moved. It spent one session as a drawer group —
 * this popover is that group's exact CONTENT, moved back to a direct one-tap home in the band,
 * because a control a guest reaches for constantly deserves not to sit behind the hamburger.
 * ============================================================================================= */

.lang-menu {
  position: relative;
  flex: 0 0 auto;
}

.lang-menu > summary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  /* Wide enough for "বাং" — the longest short form any shipped locale has — and no wider. */
  min-width: calc(62px * var(--type-scale));
  height: calc(var(--tap) * var(--type-scale));
  padding-inline: var(--sp-2);
  border: 1px solid var(--ghost-line);
  border-radius: var(--radius-pill);
  color: var(--header-ink);
  cursor: pointer;
  list-style: none; /* Firefox draws a triangle without this */
  transition: background var(--dur-fast) var(--ease);
}

.lang-menu > summary::-webkit-details-marker {
  display: none;
}

.lang-menu > summary:hover {
  background: rgba(255, 255, 255, 0.18);
}

/* The globe. Static, and at 17px rather than the icon default's 19: beside two or three characters
   of tracked type it is a mark, not an illustration. */
.lang-menu .lang-icon {
  width: calc(17px * var(--type-scale));
  height: calc(17px * var(--type-scale));
  opacity: 0.85;
  color: var(--header-ink-soft);
}

/* The code itself — "EN" / "हिं" / "বাং" (js/i18n.js LANG_CODES) — the band's own ink, since it sits
   on the indigo band again rather than the drawer's cream paper. */
.lang-code {
  flex: 0 0 auto;
  color: var(--header-ink);
  font-family: var(--font-indic);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1;
  white-space: nowrap;
}

/* The sheet. Anchored to the summary's LEFT edge (this control now sits right after the hamburger,
   near the pane's left edge, so a right anchor would hang the popover off a 320px screen) — same
   paper, same radius, same shadow and the same open animation as the capability sheet's overlays. */
/* Sized to its CONTENT, not to a fixed panel measure: three short endonyms and a reserved tick are
   never close to 11rem wide, and a popover with that much floor read as an empty settings panel
   rather than a compact language switch. `width: max-content` hugs the longest row (the rows below
   are `width: 100%`, so 100% of a max-content box is exactly that row's own width); the max-width
   is only a safety ceiling for a locale whose endonym runs long, capped to the pane's own width via
   `cqi` (see #chat-pane's `container: pane` in layout.css) so it can never overflow the frame. */
.lang-sheet {
  position: absolute;
  top: calc(100% + var(--sp-2));
  left: 0;
  z-index: 21;
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: max-content;
  min-width: 0;
  max-width: min(9rem, calc(100cqi - var(--sp-6)));
  padding: var(--sp-1);
  border: 1px solid var(--glass-line);
  border-radius: var(--radius);
  background: var(--glass-3);
  box-shadow: var(--shadow-3);
  /* Visible the instant `<details open>` renders it — never gated behind an entrance animation
     completing. The slide below is a progressive enhancement layered on top, not what makes the
     sheet appear; a guest whose device skips, interrupts, or (this control's actual reported bug)
     never finishes ticking it still gets a sheet that is fully there and tappable, which is what
     "reliably opens" means. `lang-sheet-pop-in`, not the shared `sheet-pop-in` #stay-sheet also
     uses, and on purpose: that one still fades from opacity 0, which is exactly the dependency this
     control is being taken off of, and #stay-sheet's own reliability is not this pass's job.
     `translateY` only — never `opacity` — so a stalled or interrupted animation leaves the sheet
     merely 6px out of position for a frame, never invisible. */
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: no-preference) {
  .lang-sheet {
    animation: lang-sheet-pop-in var(--dur) var(--ease-out-quart, var(--ease));
  }
}

@keyframes lang-sheet-pop-in {
  from {
    transform: translateY(-6px);
  }
}

@keyframes sheet-pop-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
}

/* One row per language, and the row is the target: the endonym in its own script, a champagne tick
   on the current one. The tick is RESERVED on the others (hidden, not removed) so choosing a
   language does not shift the list by a glyph's width. */
.lang-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: calc(44px * var(--type-scale));
  padding: var(--sp-2) var(--sp-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-family: var(--font-indic);
  font-size: var(--fs-small);
  font-weight: 500;
  text-align: start;
  cursor: pointer;
  /* NOT transitioned — see the note on `.cap-pill`. The ACTIVE row's fill resolves through
     theme-swapped tokens and was measured frozen at the light theme's indigo in dark mode. */
}

.lang-row:hover {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.lang-row.current {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  font-weight: 700;
}

.lang-row-name {
  flex: 1 1 auto;
  min-width: 0;
}

/* The compact "EN" / "हिं" / "বাং" twin of `.lang-row-name`'s full endonym — see THE LANGUAGE PILL,
   above, for the one composition that shows this instead. Hidden here in the popover, where the row
   is a full-width tap target and the full name is what a guest choosing a language actually reads. */
.lang-row-code {
  display: none;
  font-family: var(--font-indic);
}

.lang-row-tick {
  width: calc(15px * var(--type-scale));
  height: calc(15px * var(--type-scale));
  flex: 0 0 auto;
  color: var(--gold-ink);
  visibility: hidden;
}

.lang-row.current .lang-row-tick {
  visibility: visible;
}

/* Below the band it is the warm ground, so the greeting and the orb's label take PAGE ink.
   #orb-label is the line the guest actually reads under the orb; #orb-hint is a separate,
   usually-empty slot — both pinned so neither can drift onto the wrong ink again. */
/* The greeting cluster's own air. layout.css reserves --sp-5 above it for a hero whose greeting was
   pinned to the foot of a tall band; the greeting sits directly under the chrome now and the pixels
   are worth more to the capability sheet, which is what actually runs out of room. */
#hero-copy {
  padding-top: var(--sp-3);
}

#hero-copy #greeting {
  color: var(--ink);
}
#hero-copy #greeting-sub,
#hero-copy #orb-hint {
  color: var(--ink-soft);
}

/* css/daylight.css's own text-shadow on #greeting/#greeting-sub is a hardcoded WHITE glow, built to
   lift dark ink off a bright sky — the opposite of what light ivory ink over a dark mountain needs.
   A soft dark shadow instead, doing the same legibility job from the other side, and softer than a
   scrimless version would need: the scene now carries its own gradient scrim (.hero-scene::after),
   so this only has to hold the glyph edges rather than darken the whole area behind the text. That
   is also what keeps it from reading as an opaque rectangle behind the greeting. */
:root[data-theme="dark"] #hero-copy #greeting {
  text-shadow: 0 1px 2px rgba(13, 9, 12, 0.6), 0 2px 12px rgba(13, 9, 12, 0.45);
}
:root[data-theme="dark"] #hero-copy #greeting-sub {
  text-shadow: 0 1px 2px rgba(13, 9, 12, 0.65), 0 2px 10px rgba(13, 9, 12, 0.5);
}
#hero-copy #orb-label {
  color: var(--ink-soft);
  background: none;
  border: 0;
  box-shadow: none;
}

#app,
#stage,
#chat-pane,
#log-wrap,
#log {
  background: transparent;
}

/*
 * The transcript is a CLEAN cream field.
 *
 * daylight.css paints #log with an 18px dot grid in the accent. That is graph paper, and graph paper
 * behind a conversation is the visual shorthand for "you are talking to a machine" — the one reading
 * this screen has to avoid. The texture this page wants is the botanical line art at its edges
 * (body::after), which is the same decision the welcome card makes: decoration lives in the margins,
 * never under the words.
 *
 * `background-image`, not `background`, so daylight.css's rule is the only thing overridden and the
 * transparent ground from the block above survives.
 */
#log {
  background-image: none;
}

/* ---------------------------------------------------------------------------------------------
 * The paper material. One mixin-by-hand, applied to every pane so depth is consistent.
 *
 * The selector list, the border and the shadow are unchanged; what changed is the tokens they read
 * (--glass is now near-opaque ivory and --blur a whisper — see SURFACES) and the lit edge below,
 * which is now half what it was. Together that is the whole "premium card language" move: a pane
 * reads as a sheet of ivory stationery with a hairline and a soft warm shadow, not as a frosted
 * tile. Nothing about which elements are panes was touched, because which things a guest can act on
 * did not change.
 * ------------------------------------------------------------------------------------------- */

.chip,
.card,
.bubble,
.btn-ghost,
.signin-btn,
/* #composer is NOT a pane. It is a bare column inside the dock bar now (css/orb.css); the pane is
   #composer-field, the ivory field inside it, which is dressed in THE DOCK block below. Leaving the
   form in this list drew a second bordered box around the field it contains. */
/* #about-panel is not in this list because the id no longer exists: the capability map expands
   inside the utility drawer now (see .drawer-about, "WHAT CAN ATITHI DO?" below) rather than
   opening as its own overlay, so there is no separate pane for this rule to reach. */
#market-panel,
#login-panel,
.road-banner,
.cap-group,
.op-card,
.weather-tile,
.segment,
.progress,
.rate-row,
.cond-card,
.track-card {
  background: var(--glass);
  -webkit-backdrop-filter: blur(var(--blur)) saturate(140%);
  backdrop-filter: blur(var(--blur)) saturate(140%);
  border: 1px solid var(--glass-line);
  box-shadow: var(--shadow-2);
  color: var(--ink);
}

/* The lit top edge. At 0.1 white over a 0.78-alpha pane it was a visible sheen — the tell of a
   frosted-glass system. Over a near-opaque ivory face it only has to keep the pane from reading as
   a flat rectangle, so it drops to 0.05 and stops short: enough that the top of a card catches the
   light, not enough to notice as a gradient. */
/*
 * ...and the elements that carry it need a positioned box for it. ONLY those elements.
 *
 * The three overlay sheets used to be in this list and are not in the `::after` list below, so the
 * `position: relative` they were being given bought nothing — and cost the feature they exist for.
 * css/chat.css makes them `position: absolute; inset: 0` so they cover the pane like a dialog (there
 * is a long note there about why: as in-flow blocks they squeezed the conversation off a 720px
 * laptop). This file loads later at equal specificity, so `relative` won and `inset: 0` resolved to
 * "no offset from where I already am" — the capability sheet rendered as a 329px block ABOVE the
 * header, pushing the whole app down the page, with 1334px of its own content clipped inside it.
 *
 * Measured on the shipped build before this phase: panel top 0, height 329, position `relative`,
 * with the indigo band starting at y=240 underneath it.
 */
.chip,
.card,
.road-banner {
  position: relative;
  /* ABOVE the capability sheet, which is z-index 1 and deliberately rides up 54px into the scene
     (`#cap-panel { margin-top: -30px }` plus the scene's bleed). That ride-up is the intended
     composition — a sheet of stationery laid over a window — but it was landing on top of the
     road advisory and slicing its second line in half: a guest saw a warning triangle above
     "…about 18 min slower via NH 3" with the number cut off, and the dismiss X half-covered.
     Clipped safety text is worse than no safety text, so the advisory wins the overlap. It is
     transient and dismissible; the sheet behind it is not going anywhere. */
  z-index: 2;
}

.chip::after,
.card::after,
.road-banner::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, transparent 34%);
}

/* Brand lockup + header chrome */
.brand-mark {
  background: var(--glass-2);
  border: 1px solid var(--glass-line);
  box-shadow: var(--shadow-2);
}

/* =============================================================================================
 * THE ONE LOGO.
 *
 * The finalized orange/yellow AtithiVani mark (brand/atithivani-mark.svg, and its favicon rung
 * brand/atithivani-favicon.svg) is the only approved logo, in light AND dark theme. There is
 * deliberately no `:root[data-theme="dark"]` swap to a "-dark" file here: the blue Neel cuts in
 * brand/ are not approved for use, and the brand sheet forbids recolouring the supplied artwork.
 * ============================================================================================= */
/*
 * The crest. brand/atithivani-mark.svg is by its own description a full-bleed app-icon badge, and an
 * app icon laid straight on the indigo band reads as exactly that — a tile belonging to none of the
 * objects around it. The welcome screen solves this by framing the same asset in a champagne
 * hairline ring, and this is that ring at header scale. Presentation only: the asset is untouched,
 * so the mark, the favicon and the PWA icons still cannot drift apart.
 */
#hero-top .brand-mark {
  border-color: rgba(242, 96, 63, 0.5);
}
/*
 * "Atithi", in the display serif — the same face, the same weight and the same tracking the welcome
 * screen sets the word in, four seconds earlier. It is the whole lockup now, so it is allowed to be a
 * touch larger than the 19px daylight.css sets for a two-line property name: 21px at a 1.0 scale
 * still leaves the band's chrome cluster untouched, because six characters of serif is 62px.
 */
#brand-name {
  color: var(--ink);
  font-size: calc(21px * var(--type-scale));
  /* Optical, not decorative: at this size a display serif needs the tracking opened a hair rather
     than closed, which is the opposite of what a sans at the same size wants. Matches the welcome
     lockup's own 0.012em. */
  letter-spacing: 0.012em;
  /* One word, one line. The two-line clamp daylight.css sets exists for a tenant's property name,
     which no longer lives here (it is the stay row's first fact). */
  -webkit-line-clamp: 1;
  white-space: nowrap;
}

/* =============================================================================================
 * THE CHAT-STARTED WORDMARK — `#brand-lockup-chat`, the supplied "Atithi / Your AI Travel
 * Companion" image, swapped in for the crest + "Atithi" pair ONLY once the band is the opaque
 * chat-started header (the same `#app[data-mode="active"]:not(:has(#log:empty))` state
 * `.hero-scene`/`#hero-copy` already use, above, to mean "a conversation is actually under way").
 * Welcome and the transparent pre-chat header are untouched: `.brand-mark` and `#brand-name` keep
 * rendering exactly as before there, tenant white-label included.
 * ============================================================================================= */

/* Hidden everywhere by default; the scoped rule below is the only place that turns it on. */
.brand-lockup-chat {
  display: none;
}

/*
 * THE ENDORSED LOCKUP IS THE CHAT-STARTED HEADER. Crest, wordmark and "A PRODUCT OF SARGVISION"
 * as one flat asset, in place of the crest + live-text pair, for as long as a conversation is up.
 *
 * WHY A DERIVED ASSET RATHER THAN THE SUPPLIED FILE. Both canonical lockups are drawn for a light
 * ground: they ink "Atithi" in #4A2340, which is `--indigo` — the same value this band is painted
 * in, and the same value as the crest's own disc. Dropped straight onto the band the word vanishes
 * and the disc dissolves into the bar. `atithivani-lockup-endorsed.svg` is therefore mirrored by
 * `-onband.svg`, which is the same artwork byte for byte apart from three things: the type goes to
 * `--on-indigo`/`--violet-lit`, the endorsement rule and line pick up a cream at reduced alpha, and
 * a 2px cream hairline is laid over the disc — the same separation `.brand-mark`'s CSS ring gives
 * the live crest, done inside the file because page CSS cannot reach into an <img>.
 *
 * ONE ASSET FOR BOTH THEMES. `--header-bg` is deep plum in Sondhya and near-black glass in Neel,
 * and cream-on-coral reads on both, so there is no second swap here. This is the same reasoning
 * index.html records for why the slot was pointed at a "-dark" file in both themes.
 */
#app[data-mode="active"]:not(:has(#log:empty)) .brand-lockup-chat {
  display: block;
  /* Height-driven, not width-driven: the endorsed lockup is 520x150 and the bilingual one 470x150,
     so pinning the height is what keeps the crest the same size across a language switch and lets
     each file take the width its own type needs.
     48, which is the chrome buttons' own diameter — the largest this can be and still sit ON the
     row rather than setting the row's height. It is also as large as the band can make the
     endorsement line: that line is 11px inside a 150-tall frame, so it paints at 48/150*11 = 3.5px
     here, and no height that fits a 74px band gets it to a legible 7. See the note below. */
  width: auto;
  /* 56, up from the chrome row's own 48 — asked for directly, more legible as the one brand object
     left in the band. Taller than the 48px chrome buttons beside it now, so the row grows to fit
     it (align-items: center keeps the buttons centred against the taller lockup) rather than the
     lockup being capped at button height. object-fit: contain + min-width: 0 below still shrink it
     on a narrow pane exactly as before, just from a higher ceiling. */
  height: calc(56px * var(--type-scale));
  /*
   * IT SHRINKS BEFORE THE CHROME ROW DOES. At its full height this asset is 3.47:1 — 166px wide
   * beside a 48px menu button and a 174px action cluster, which is 384px of content in the 343px
   * row a 375px phone gives. The live wordmark it replaced never hit this because text ellipsises
   * and a flat lockup cannot.
   *
   * `min-width: 0` is the whole fix: the element is already `flex: 0 1 auto`, but a replaced
   * element's `min-width: auto` floors it at its intrinsic width, so it refused to give any ground
   * and pushed the stay button off the right edge instead. With the floor removed it takes what is
   * left of the row, and `object-fit: contain` scales the artwork down inside that box rather than
   * squashing it — so the lockup gets smaller on a narrow pane and the chrome never moves.
   */
  min-width: 0;
  object-fit: contain;
  object-position: left center;
}

/* The crest and the live wordmark step aside for it — one brand object in the band, not two. */
#app[data-mode="active"]:not(:has(#log:empty)) .brand-mark,
#app[data-mode="active"]:not(:has(#log:empty)) .brand-text {
  display: none;
}

/*
 * BENGALI GETS THE BILINGUAL LOCKUP. `html[lang]` is written by js/app.js applyLangChrome() on
 * every language change, so this follows the guest's choice with no JS of its own.
 *
 * `content:` rather than a second <img>: it swaps the painted image without touching the element's
 * `alt`, its box, or the entrance animation that may still hold it — and one element means the two
 * lockups can never both be in the layout mid-switch. The intrinsic ratio differs (470x150 against
 * 520x150), which is exactly why the rule above sizes by height and leaves width auto.
 */
:root[lang="bn"] #app[data-mode="active"]:not(:has(#log:empty)) .brand-lockup-chat {
  content: url("/brand/atithivani-lockup-bengali-onband.svg");
}

/* No dark-theme override for either lockup: the same orange/yellow onband files render in both
   themes (THE ONE LOGO, above). */

/*
 * THE ENDORSEMENT LINE IS TEXTURE HERE, NOT TEXT — a measured limit, recorded so nobody spends an
 * afternoon on it again. "A PRODUCT OF SARGVISION" is set at 11px inside the asset's 150-tall
 * frame. Reaching a legible 7px on screen needs the lockup at 150 x 7/11 = 95px; the band is 74px
 * including its padding and the chrome buttons are 48px circles inside it. There is no height that
 * fits the row and renders that line. At the 48px above it paints at 3.5px: a grey dash under the
 * wordmark, which is what the design system means by reserving this lockup for footers, covers and
 * letterheads and keeping it off a product screen.
 *
 * Shipped anyway, at the largest honest size, because it was asked for explicitly. The fix, if the
 * line has to be READ in the band, is a horizontal cut of the endorsement — the rule and the words
 * set beside the wordmark on its own baseline instead of stacked beneath it, which fits ~9px into
 * the same 48px. That is new artwork, so it is not done here.
 */

/* No narrow-pane step for this any more — `object-fit: contain` above degrades it continuously by
   width, which is a better answer than a breakpoint that is right at one pane size and wrong at the
   next. The crest and live wordmark's own phone rules (THE BAND ON A PHONE, below) still apply to
   the pre-chat header, where they are still what is drawn. */

/* =============================================================================================
 * THE PROFILE ICON — "you're staying at…", as one glyph in the chrome row rather than a second
 * band on its own.
 *
 * ## What this replaces
 * A sheet of paper laid across the foot of the band, three lines deep, carrying the guest's name,
 * the property and the dates — legible, but a permanent second row under the brand lockup for a
 * control a guest reads once and does not need to see again. The DETAIL that card carried is not
 * gone: it is the same #stay-sheet this opens (js/app.js renderStaySheet), and the same facts are
 * the button's accessible name (js/app.js renderGuestStrip sets aria-label), so nothing here is a
 * smaller answer to "does Atithi know my stay?" — it is the same answer, read on demand instead of
 * always-on.
 *
 * `.chrome-icon-btn` (see THE CHROME ICON BUTTONS above) does the actual button chrome — the 48px
 * circle, the glass fill, the hover and the focus ring. What is here is specific to THIS icon: the
 * person glyph, and hiding the three text spans a sighted guest no longer reads directly.
 * ============================================================================================= */

/*
 * ...AND IT HAS TO ACTUALLY DISAPPEAR WHEN THERE IS NO STAY.
 *
 * js/app.js's renderGuestStrip() sets `hidden` on this button whenever the bootstrap payload carries
 * no guest facts, which is exactly the anonymous case (POST /api/anon mints an identity, not a
 * booking — every field comes back an empty string). Verified: `guestStrip.hidden === true`.
 *
 * `#hero-top #guest-strip[hidden]` is two IDs plus an attribute, so it outranks `.chrome-icon-btn`'s
 * one class regardless of source order — the same trap daylight.css already documents for
 * `.signin-btn[hidden]`, one selector along.
 */
#hero-top #guest-strip[hidden] {
  display: none;
}

/*
 * THE ANCHOR. Sized to the button alone (`display: inline-flex` around one 44px child adds no box
 * of its own) so `#stay-sheet` below can position off THIS instead of `#hero-top` — see the note in
 * index.html for the bug that fixed. `position: relative` is what makes it a containing block; it
 * has to be a real box for that, which is also why it collapses explicitly when the button is
 * `hidden` rather than being left to size to nothing on its own — a `display: inline-flex` wrapper
 * around a `display: none` child still counts as a flex item and still takes its share of
 * `#topbar-actions`' `gap`, which the button alone never did.
 *
 * `flex: 0 0 auto` carries over the same no-shrink rule `.chrome-icon-btn` sets on the button
 * itself (below) — without it, this new wrapper (not the button inside it) is what a tight row
 * would shrink first, and a squeezed wrapper reads as a squeezed icon.
 */
.stay-anchor {
  position: relative;
  display: inline-flex;
  flex: 0 0 auto;
}

.stay-anchor:has(> #guest-strip[hidden]) {
  display: none;
}

/* The glyph. One, not two: a person, at the same 22px every other chrome icon in the band uses
   (see .chrome-icon-btn .icon) — this control shares one icon language with the hamburger, the
   language globe and the theme glyph now, rather than sizing itself for a three-line card. */
.stay-glyph {
  width: 22px;
  height: 22px;
  color: var(--header-ink);
}

/* The champagne ring — only once there is a stay to ring. `--ghost-line`'s cool white-alpha border
   (the shared .chrome-icon-btn default) reads as "one of the row's plain icon buttons"; a hairline
   of the same accent the welcome ornament and the drawer wordmark use marks this one as carrying
   something the others don't. Border-COLOR only, not a second border: the control keeps its shared
   1px/pill/44px shape, so it still reads as one family with the hamburger, globe and theme icon
   beside it — a different accent, not a different control. */
#hero-top #guest-strip[data-linked="1"] {
  border-color: color-mix(in srgb, var(--gold) 55%, transparent);
}

/* The three facts a sighted guest used to read directly off the card. They are not deleted — they
   are still the button's TEXT, which is exactly what renderGuestStrip's explicit aria-label is built
   from — only no longer painted, since the icon is the whole visible face now. */
.stay-lines {
  display: none;
}

/* NO STAY: the icon stays put ("a blank space is not an answer to does Atithi know my stay") but
   stops claiming to be a control — `data-linked="0"` is set by js/app.js exactly when there is no
   sheet behind it to open. `cursor: default` alone left this indistinguishable from the resolved
   state to a sighted guest (the `aria-label` swap is the only thing a screen reader hears) — dashed,
   unfilled chrome reads as "nothing behind this" at a glance, the visual mirror of `[data-linked="1"]`'s
   gold ring rather than its full glass-and-border treatment. */
#hero-top #guest-strip[data-linked="0"] {
  cursor: default;
  background: transparent;
  border-style: dashed;
  border-color: var(--ghost-line);
}

#hero-top #guest-strip[data-linked="0"] .stay-glyph {
  color: var(--header-ink-soft);
}

#hero-top #guest-strip[data-linked="0"]:hover {
  background: transparent;
}

/* ---------------------------------------------------------------------------------------------
 * THE STAY SHEET. Hotel, where, when, and whether it resolved.
 *
 * A popover on the band, not a modal: it answers a glance, so it must cost a glance to dismiss.
 * Anchored under the profile icon that opens it — RIGHT edge, not full-width between the band's
 * padding — and sized as one compact card (≈288px) rather than the guest-lifecycle switcher this
 * used to also carry: with only the current stay's own five facts left in it (see js/app.js
 * renderStaySheet), a full-bleed panel was answering a glance-sized question with a dialog-sized
 * box.
 *
 * `top`/`right` are against `.stay-anchor` (the button's own wrapper, above), not `#hero-top` —
 * see index.html for why: `#hero-top` grows taller whenever the header's language pill wraps onto
 * its own line (css/glass.css THE LANGUAGE PILL, every pre-first-message screen), and this sheet
 * used to open below THAT full height instead of below the icon, landing over the greeting.
 * `right: 0` on the anchor's own box lines the card's trailing edge up with the icon's, the same
 * place `right: var(--sp-4)` against #hero-top used to land it when the header was a single row.
 * `100cqi` (the #chat-pane container — layout.css `container: pane`) rather than a viewport unit
 * is what keeps this inside the CHATBOT's own frame on a wide screen, where #chat-pane is a
 * centred column narrower than the browser window (the same reason the utility drawer moved off
 * `vw` units) — container queries resolve against #chat-pane regardless of which element inside
 * it establishes the positioning context.
 * ------------------------------------------------------------------------------------------- */

#stay-sheet {
  position: absolute;
  z-index: 22; /* above the band's own chrome, below the overflow sheet's 20-in-its-own-context */
  top: calc(100% + var(--sp-2));
  right: 0;
  left: auto;
  width: min(288px, calc(100cqi - 2 * var(--sp-4)));
  padding: var(--sp-5) var(--sp-4);
  border: 1px solid var(--glass-line-strong);
  border-radius: var(--radius);
  background: var(--glass-3);
  box-shadow: var(--shadow-3);
  color: var(--ink);
  text-align: start;
  /* Clips the corner sprig below to the card's own rounded edge, so it reads as grain in the stock
     rather than a shape floating past it. Nothing else here relied on overflowing this box. */
  overflow: hidden;
  /* Fade + a small upward slide, shared with the language popover (see `.lang-sheet`) — one
     entrance language for every disclosure hanging off this header. `more-in` (this rule's previous
     animation) had no keyframes left anywhere in this file; this was a silent no-op. */
  animation: sheet-pop-in var(--dur) var(--ease-out-quart, var(--ease));
}

#stay-sheet[hidden] {
  display: none; /* [hidden] loses to the display above without this */
}

/*
 * The one piece of place-texture inside it. Same asset, same masking technique, same intent as the
 * utility drawer's own corner (see THE PAPER note above) — what turns "hotel name, dates, a
 * checkmark" from a data readout into something that feels like a keepsake. Bottom-right, under the
 * status row where no text reaches; sized and dimmed enough to be the grain of the card, not a
 * drawing on it.
 */
#stay-sheet::after {
  content: "";
  position: absolute;
  z-index: 0;
  right: -20px;
  bottom: -26px;
  width: 84px;
  height: 113px; /* the drawing's own 270/200 ratio, per the drawer's corner */
  pointer-events: none;
  opacity: 0.08;
  background-color: var(--gold);
  -webkit-mask: url(/brand/botanical-sprig.svg) no-repeat center / contain;
  mask: url(/brand/botanical-sprig.svg) no-repeat center / contain;
}

:root[data-theme="dark"] #stay-sheet::after {
  opacity: 0.06;
}

/* `position: relative` lifts every line above the corner sprig's `z-index: 0` — plain text with no
   stacking context of its own would otherwise paint UNDER an absolutely-positioned sibling that
   comes later in the DOM, per normal painting order. */
#stay-sheet p {
  position: relative;
  z-index: 1;
  margin: 0;
}

/* The hotel is the sheet's headline, so it takes the display serif — the same treatment the lockup
   and the greeting use, and the thing that makes this read as stationery rather than as a tooltip. */
.stay-hotel {
  font-family: var(--font-serif);
  font-size: calc(20px * var(--type-scale));
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: 0.012em;
}

.stay-where {
  margin-top: 2px !important;
  color: var(--ink-soft);
  font-size: var(--fs-small);
}

/* The dates are the fact a guest actually opens this for, so they get the champagne rule above them
   and a little more weight than the line before. */
.stay-dates {
  margin-top: var(--sp-3) !important;
  padding-top: var(--sp-3);
  border-top: 1px solid rgba(242, 96, 63, 0.4);
  font-size: var(--fs-body);
  font-weight: 600;
}

.stay-detail {
  margin-top: 2px !important;
  color: var(--ink-soft);
  font-size: var(--fs-small);
}

/* The status line. Green ink on its own tint, from the palette's semantic pair — the one place in
   this sheet that is not cream, because it is the one line that carries a state rather than a fact. */
.stay-status {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-3) !important;
  padding: 3px var(--sp-3) 3px var(--sp-2);
  border-radius: var(--radius-pill);
  background: var(--ok-bg);
  color: var(--ok-ink); /* 5.6:1 on --ok-bg */
  font-size: var(--fs-micro);
  font-weight: 600;
}

.stay-status .icon {
  width: calc(14px * var(--type-scale));
  height: calc(14px * var(--type-scale));
}

/* ---------------------------------------------------------------------------------------------
 * THE DEMO GUEST SELECTOR — temporary, pre-auth (see js/demo_guest.js / web/demo_guests.py).
 *
 * One row style, one home now: the first-run picker (`#demo-guest-panel`, an overlay sheet styled
 * by the shared #market-panel/#login-panel/#demo-guest-panel rule in chat.css). A
 * second "switch guest" section used to also render this same row style inside the profile
 * popover (js/app.js renderStaySheet); that section is gone — the popover now shows only the
 * CURRENT stay, and switching who you're browsing as is the first-run picker's job alone. The row
 * style stays here because the picker still uses it.
 * ------------------------------------------------------------------------------------------- */

.demo-guest-list {
  list-style: none;
  margin: var(--sp-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.demo-guest-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}

.demo-guest-row:hover {
  background: var(--bubble-out);
  border-color: var(--hairline-strong);
}

.demo-guest-row.current {
  border-color: rgba(242, 96, 63, 0.6);
  background: var(--brand-wash);
}

.demo-guest-row .icon {
  flex: 0 0 auto;
  width: calc(16px * var(--type-scale));
  height: calc(16px * var(--type-scale));
  color: var(--ink-faint);
}

.demo-guest-row.current .icon {
  color: var(--ok-ink);
}

.demo-guest-lines {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.demo-guest-name,
.demo-guest-property,
.demo-guest-when {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.demo-guest-name {
  font-weight: 600;
  color: var(--ink);
  font-size: var(--fs-small);
}

.demo-guest-property {
  color: var(--ink-soft);
  font-size: var(--fs-micro);
}

.demo-guest-when {
  color: var(--ink-faint);
  font-size: var(--fs-micro);
}

/* The first-run picker widens beyond the popover's 22rem stay-sheet cap — for a moment it's the
   whole screen's subject, not a corner disclosure. */
#demo-guest-panel .demo-guest-list {
  max-width: 34rem;
  margin-inline: auto;
}

/*
 * THE NARROWEST BANDS — a 320px SE, a ~344px folded cover screen.
 *
 * REMOVED (was a no-op, now would be a regression): this used to give back the band's last 4px of
 * padding-bottom at this width, back when the base rule's own padding was --sp-2. The base rule
 * has since dropped to --sp-1 on every width — the further compaction pass — so this tier's own
 * --sp-2 would now set the narrowest phones BACK UP to more padding than every wider pane gets,
 * exactly backwards from what a narrow-pane tier is for. Nothing left to shrink here at this width
 * specifically; the profile icon is a fixed 48px circle at every width regardless.
 */

/* ---------------------------------------------------------------------------------------------
 * Type. A display serif for the two lines that carry the brand, a sans for everything a guest has
 * to read at length.
 *
 * The greeting is the concierge's answer to the welcome screen's headline, and it is now set the
 * same way: Cormorant Garamond, 600, with the line gap pulled in and a hair of negative tracking,
 * because a display serif at this size without them reads as a body font that happened to be set
 * large. The sizes and the Indic overrides stay in daylight.css, which is where the per-script
 * decisions live — Bengali and Devanagari greetings deliberately do NOT take the serif (Cormorant
 * has no Bengali, and the @font-face unicode-range means it could never have been used).
 * ------------------------------------------------------------------------------------------- */

#greeting {
  font-weight: 600;
  letter-spacing: -0.008em;
  line-height: 1.05;
  color: var(--ink);
  text-shadow: none;
}
#greeting-sub {
  color: var(--ink-soft);
  text-shadow: none;
}
#orb-hint {
  color: var(--ink-soft);
  letter-spacing: 0.16em;
}

/* =============================================================================================
 * THE CAPABILITY SHEET — the welcome screen's subject.
 *
 * A sheet of ivory stationery with large rounded top corners riding up over the view, carrying a
 * headed menu of concierge cards. It is the same move the welcome screen makes with its own card
 * over its own photograph, which is what makes the two screens read as one piece of stationery.
 *
 * It is ALSO the active conversation's shortcut lane. One element, two states, `#app[data-mode]`
 * apart — not two components, because they hold the same buttons with the same handlers and a second
 * component would be a second thing to keep in step.
 * ============================================================================================= */

#cap-panel {
  position: relative;
  z-index: 1; /* over the scene's foot, so the rounded corners read as paper laid on the view */
  display: flex;
  flex-direction: column;
  min-width: 0;
  /*
   * NO padding above the head, and that is a fix rather than a tightening.
   *
   * The sheet scrolls and its head is `position: sticky; top: 0` — against the PADDING box. With
   * --sp-4 of padding here, that left a 16px strip above the parked head with nothing in it but the
   * scroll container, so cards passing under the head reappeared in it: at any scroll offset a
   * sliver of the previous row ("Compare & book", "Requests & more") floated above the heading. The
   * air the padding was buying is now the head's own top padding, where it moves with the head.
   */
  padding-top: 0;
  border-top: 1px solid var(--glass-line);
  /* --radius-lg (22px), not the bespoke 26px this shipped with. The welcome hero photo/greeting
     above it is the one place on this screen a slightly larger corner earned its own number by
     eye — but --radius-lg is 4px away, not a different shape, and giving this its own literal
     instead of the token closest to it is exactly the kind of one-off the token scale exists to
     prevent. Named-token now: the major-container tier every other large sheet in the app already
     uses (--stay-sheet's popover is one step down, at --radius, since it never rides up over a
     photo the way this does). */
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  /*
   * Not --glass for the BODY of the sheet: this is still a large, opaque area of paper, and a
   * translucent one under the chip grid would let the ridges show through the text. But the sheet's
   * own TOP EDGE now rides up over the welcome hero's full-bleed photo (THE PICTURE, above THE
   * COMPOSITION) rather than over an already-faded band foot, and starting this gradient at fully
   * opaque `#fffdfb 0%` put a hard, flat-coloured edge across still-visible mountain — measured as
   * the "white strip" complaint: the photo's own mask does not reach zero alpha until #cap-panel has
   * already painted over it.
   *
   * So the first ~70px fade from transparent instead of starting solid — long enough to clear the
   * `-30px` overlap (below) with room to spare, short enough that it is finished well before
   * #cap-panel-head's own padding, i.e. never behind actual text. Everything past 70px is unchanged
   * (the same lit-top-to-settled-foot card-face gradient this shipped with), so the sheet still reads
   * as paper once it is actually holding content — only the seam is different.
   */
  background: linear-gradient(
    180deg,
    rgba(255, 253, 251, 0) 0px,
    rgba(255, 253, 251, 0.82) 40px,
    #fffdfb 70px,
    var(--canvas-lift) 100%
  );
  /* Halved (0.4 -> 0.2 alpha): a contact shadow this strong drawn over a photo that is itself fading
     to nothing at the same seam doubled as a second, darker edge — the "hard boundary" the gradient
     above fixes was two effects stacking, not one. */
  box-shadow: 0 -10px 30px -22px rgba(74, 35, 64, 0.2);
  /* The sheet scrolls, not the page. Whichever of the two modes it is in, the region above it
     (the hero, or the transcript) must not move when this overflows. */
  overflow-y: auto;
  overscroll-behavior-y: contain;
  scrollbar-width: none;
  /* Containing block + clip for the botanical layer this sheet carries (#cap-panel::after, below).
     It costs nothing visually on its own: `overflow-y: auto` above already clips this box, so paint
     containment is only restating a boundary that exists — what it adds is that a `fixed` child now
     resolves against THIS box instead of the viewport, which is what stops the texture scrolling
     away with the cards. */
  contain: paint;
}

#cap-panel::-webkit-scrollbar {
  display: none;
}

/* The gilt edge along the sheet's top, inset past the corner radius so it stops where the curve
   starts — the same engraved rule the header band carries along its foot (#hero-top::before), and
   the same reason: it is the one detail that separates stationery from a rounded rectangle. */
#cap-panel::before {
  content: "";
  position: absolute;
  top: 0;
  right: 26px;
  left: 26px;
  height: 1px;
  pointer-events: none;
  background: var(--header-rule);
  opacity: 0.55;
}

/* Same reasoning as #cap-panel's own `border-top: none` above: this gilt rule is drawn at the exact
   seam with the photo in welcome mode, and a line across the image is a border by another name.
   Active mode's compact rail (never over the photo) keeps it. */
#app[data-mode="welcome"] #cap-panel::before {
  display: none;
}

/* ---------------------------------------------------------------------------------------------
 * THE BOTANICAL, CONTINUED ONTO THE SHEET.
 *
 * body::after (THE BOTANICAL TEXTURE, far above) already carries this at the screen's edges, and
 * everything about it — the asset, the mask technique, the champagne tone, the "nothing in the
 * middle third" rule — is reused here rather than reinvented. The one thing it cannot do is reach
 * THIS surface: #cap-panel is opaque paper at `z-index: 1`, so in welcome mode it covers the whole
 * lower half of the screen and the layer underneath it simply never shows. The hero photograph
 * takes the upper half. Between them the tuned texture was invisible in the one mode a guest
 * spends the longest looking at.
 *
 * So the sheet grows its own, on the pseudo-element the gilt edge above did not take.
 *
 * `z-index: -1` is doing precise work: #cap-panel is `position: relative; z-index: 1`, which makes
 * it a stacking context, so a negative child is TRAPPED inside it — painted over the sheet's own
 * cream gradient and under every card, label and control in it, with no way to escape upward onto
 * the transcript or downward behind the sheet. A positive or auto z-index here would paint over the
 * cards' text instead, because the cards are not positioned.
 *
 * TWO SPRIGS, BOTH MOSTLY OFF THE EDGE — the same composition as the body layer: a long overhang so
 * what enters the sheet is the clipped tips of one branch, never a whole drawing sitting in the
 * paper like a sticker. Left at the foot, right below the head. The middle stays empty, which is
 * where the card grid and the "More ways" row live.
 *
 * 0.07, against the body layer's 0.1. Lower because part of this one lies behind CARDS whose faces
 * are only ~55% opaque, so the ink reads through them at closer to full strength than it does on
 * bare ground — matching the body layer's alpha would make the texture louder through a card than
 * beside it, which is backwards. Not lower still: most of each branch falls on the BARE cream below
 * the grid, and 0.055 (tried first) disappeared there. Compared at 0.055 / 0.07 / 0.10 on screen —
 * 0.10 reads as a drawing on the sheet, 0.055 as nothing, 0.07 as grain in the paper.
 * ------------------------------------------------------------------------------------------- */
#cap-panel::after {
  /* Same derivation as body::after's own pair: the width is the knob, the height follows the
     drawing's 270/200, and the overhang is a fraction of the width so the same fragment shows at
     every viewport rather than a different one at each. */
  --sprig-c: clamp(164px, 44vw, 248px);
  --sprig-d: clamp(138px, 37vw, 208px);

  content: "";
  /*
   * FIXED, NOT ABSOLUTE — and this is the whole reason `contain: paint` is on #cap-panel.
   *
   * The sheet is a scroll container. An absolutely positioned pseudo-element inside one is laid
   * out against the padding box but SCROLLS WITH THE CONTENT, so the first version of this simply
   * slid away: expand "More ways I can help", scroll to the foot, and the texture was gone. Paper
   * grain that leaves when you scroll the paper is worse than no grain.
   *
   * Paint containment on the parent makes this element the containing block for a `fixed`
   * descendant AND clips it to the padding box — so `inset: 0` here means the sheet's VISIBLE box
   * at every scroll offset, and the branches stay exactly where they were put. Two properties,
   * no wrapper element, no JS.
   */
  position: fixed;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  opacity: 0.07;
  background-color: var(--gold);
  -webkit-mask-image: url(/brand/botanical-sprig.svg), url(/brand/botanical-sprig.svg);
  mask-image: url(/brand/botanical-sprig.svg), url(/brand/botanical-sprig.svg);
  -webkit-mask-repeat: no-repeat, no-repeat;
  mask-repeat: no-repeat, no-repeat;
  -webkit-mask-position:
    left calc(var(--sprig-c) * -0.46) bottom calc(var(--sprig-c) * -0.28),
    right calc(var(--sprig-d) * -0.58) top 18%;
  mask-position:
    left calc(var(--sprig-c) * -0.46) bottom calc(var(--sprig-c) * -0.28),
    right calc(var(--sprig-d) * -0.58) top 18%;
  -webkit-mask-size:
    var(--sprig-c) calc(var(--sprig-c) * 1.35),
    var(--sprig-d) calc(var(--sprig-d) * 1.35);
  mask-size:
    var(--sprig-c) calc(var(--sprig-c) * 1.35),
    var(--sprig-d) calc(var(--sprig-d) * 1.35);
}

/* ACTIVE MODE: off. The sheet is a ~70px horizontal rail there, not a page of paper — two cropped
   branches inside a strip that short read as clutter beside the transcript, and the body layer's
   own active-mode placement (which moves to the extreme corners, see body:has(...)::after above) is
   already the answer for that composition. */
#app[data-mode="active"] #cap-panel::after {
  display: none;
}

/* Dark mode: the sheet is near-black glass and coral line art on it reads as a smudge rather than
   as grain in paper, so it steps down again. Not removed — the texture is part of the brand in both
   themes, and body::after keeps its own alpha in dark for the same reason. */
:root[data-theme="dark"] #cap-panel::after {
  opacity: 0.045;
}

/*
 * The head. A serif line and a champagne diamond, with a hairline running out to the pane edge —
 * the shape of a section heading on a menu rather than of a label on a widget.
 */
#cap-panel-head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  /* The top padding is the sheet's own air, moved here so it travels with the sticky head — see the
     note on #cap-panel's `padding-top: 0`. */
  padding: var(--sp-4) var(--sp-4) var(--sp-3);
  /* STICKY. The sheet scrolls, and a heading that scrolls away leaves a grid of cards with nothing
     naming it. `top: 0` is against #cap-panel's padding box, so the head parks under the gilt edge.
     The fill is the top stop of the sheet's own gradient, so the cards pass behind it invisibly. */
  position: sticky;
  top: 0;
  z-index: 2;
  background: #fffdfb;
}

:root[data-theme="dark"] #cap-panel-head {
  background: var(--glass-3);
}

/*
 * ...and the SHEET needs the same treatment, which it was not getting.
 *
 * #cap-panel's fill is `linear-gradient(#fffdfb, var(--canvas-lift))` — a literal ivory top stop over
 * a themed bottom one. In dark mode that interpolated from near-WHITE at the top of the sheet to deep
 * indigo at its foot, with dark cards sitting on the pale half: measured
 * `linear-gradient(rgb(255,253,249), rgb(33,26,60))` on the shipped build. A pre-existing bug rather
 * than one this pass introduced — the head above has carried a dark override since it was written,
 * which is precedent for exactly this — but the sheet is the largest surface on the welcome screen and
 * it is not honest to leave it half-lit in a theme the overflow menu offers.
 *
 * The top stop becomes the paper the cards and the head are made of, so the sheet reads as one sheet.
 */
/*
 * Dark elevated surface — the palette's own --glass-2/--surface-2, which now carry a properly dark,
 * reliably-opaque-enough alpha themselves (THE PREMIUM DARK PALETTE, css/tokens block above), so
 * this no longer needs its own hand-picked alpha steps. Still fades IN from transparent over the
 * first 40px, so the mountain's own fade at the seam isn't double-covered, and still blurs what
 * shows through that fade.
 */
:root[data-theme="dark"] #cap-panel {
  background: linear-gradient(180deg, transparent 0%, var(--glass-2) 40px, var(--surface-2) 100%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  backdrop-filter: blur(18px) saturate(140%);
}

#cap-panel-title {
  margin: 0;
  font-family: var(--font-serif);
  font-size: calc(18px * var(--type-scale));
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.008em;
  color: var(--ink);
}

/* Indic scripts do not take the display serif — Cormorant has no Bengali or Devanagari, and the
   @font-face unicode-ranges mean it could never be used for them. Same decision the greeting makes. */
:root[lang="bn"] #cap-panel-title,
:root[lang="hi"] #cap-panel-title,
:root[lang="ne"] #cap-panel-title {
  font-family: var(--font-indic);
  font-size: calc(15px * var(--type-scale));
  font-weight: 700;
}

/* The diamond, and the rule that carries it out to the edge. */
.cap-panel-mark {
  flex: 1 1 auto;
  position: relative;
  height: 5px;
  min-width: var(--sp-4);
}

.cap-panel-mark::before {
  content: "";
  position: absolute;
  top: 2px;
  right: 12px;
  left: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(242, 96, 63, 0.7));
}

.cap-panel-mark::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  width: 5px;
  height: 5px;
  background: var(--gold);
  transform: rotate(45deg);
}

/* =============================================================================================
 * THE SUGGESTIONS TOGGLE — a plain disclosure chevron, the same shape `.drawer-row-chevron`
 * already wears for "this row opens something", not a new control language for one button.
 * `.cap-panel-mark`'s own `flex: 1 1 auto` still does its job with this as a new trailing
 * sibling — the rule stretches to fill whatever room is left and the chevron sits flush at the
 * row's own trailing edge, welcome or active alike.
 * ============================================================================================= */
#cap-panel-toggle {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(28px * var(--type-scale));
  height: calc(28px * var(--type-scale));
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

#cap-panel-toggle:hover {
  background: var(--lavender);
  color: var(--violet-deep);
}

#cap-panel-toggle:active {
  transform: scale(0.94);
}

#cap-panel-toggle .icon {
  width: calc(16px * var(--type-scale));
  height: calc(16px * var(--type-scale));
  transition: transform var(--dur-fast) var(--ease);
}

/* Collapsed points the chevron back up — the same "flip on aria-expanded" convention
   `.drawer-row-chevron` already uses, one attribute driving both the icon and the state. */
#cap-panel-toggle[aria-expanded="false"] .icon {
  transform: rotate(180deg);
}

/* THE COLLAPSE ITSELF. `data-collapsed`, not `hidden`/`display:none` on #cap-panel: the head (and
 * this toggle) must stay on screen so there is a way BACK, which ruled out hiding the section
 * wholesale the way the automatic per-call hide does (SUGGESTIONS STEP ASIDE, above) — that one
 * needs no way back because it reverses itself the moment the call ends.
 *
 * `#app` prefixed on every one of these, not just `#cap-panel[data-collapsed="1"] #cap-rail`:
 * THE WELCOME GRID below sets this same element's `display` through `#app[data-mode="welcome"]
 * #cap-rail` — two IDs plus an attribute, identical specificity to a bare `#cap-panel[...]
 * #cap-rail`, and it is declared AFTER this block, so without the extra ID here it would win by
 * source order alone and the guest's own collapse would silently do nothing in welcome mode. One
 * more ID settles it regardless of where either rule sits in the file. */
#app #cap-panel[data-collapsed="1"] #cap-rail,
#app #cap-panel[data-collapsed="1"] #cap-more,
#app #cap-panel[data-collapsed="1"] #cap-rail-more,
#app #cap-panel[data-collapsed="1"] #cap-desk {
  display: none;
}

/* In ACTIVE mode the sheet's chrome falls away: no paper, no corners, no gilt edge, no head. What is
   left is a lane of shortcuts sitting on the cream between the transcript and the dock. */
#app[data-mode="active"] #cap-panel {
  padding-top: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  overflow: visible;
  /* And the paint containment goes with it. The base rule carries `contain: paint` so the sheet can
     hold a FIXED botanical layer that does not scroll with its cards (see #cap-panel::after) — but
     containment clips to the padding box unconditionally, which would quietly override the
     `overflow: visible` on the line above and undo the whole point of it. The rail has no texture
     to pin (the ::after is `display: none` in this mode), so there is nothing here to keep. */
  contain: none;
}

#app[data-mode="active"] #cap-panel::before {
  display: none;
}

/* `#cap-panel-head` used to disappear outright here too — the whole point of ACTIVE mode is that
 * the sheet's chrome falls away. It stays mounted now, because `#cap-panel-toggle` (the guest's
 * own show/hide, index.html) needs ONE fixed home in both compositions rather than a second copy
 * of itself for this mode. What still falls away is just the WELCOME-only chrome the head used to
 * carry — the serif title and the champagne rule have nothing to introduce here, since the
 * compact rail underneath already says what it is with no heading needed (see
 * #app[data-mode="active"] #cap-panel-title / .cap-panel-mark, below) — so the row shrinks to
 * exactly as tall as the one control left in it. */
#app[data-mode="active"] #cap-panel-head {
  position: static;
  padding: var(--sp-1) var(--sp-4) 0;
  background: none;
  justify-content: flex-end;
}

#app[data-mode="active"] #cap-panel-title,
#app[data-mode="active"] .cap-panel-mark {
  display: none;
}

/* ---------------------------------------------------------------------------------------------
 * THE WELCOME GRID. Two columns of cards, and the last one spans both when the count is odd.
 *
 * A GRID and not the wrapping flex lane it replaces, for one reason that matters: flex-wrap sizes
 * each row to its own content, so a row holding "Permits" and "Roads & weather" was 38px shorter
 * than the row under it and the menu read as a pile rather than as a set. A grid gives every card
 * the same box, which is the whole difference between eight cards and a considered menu of eight.
 *
 * `:last-child:nth-child(odd)` spans the final card across both columns — the reference's own
 * "Ask anything" full-width row, derived from the count rather than hard-coded to an entry, so it
 * keeps working as the signed-in entries come and go (js/app.js renders 6 anonymous, 10-11 resolved).
 * ------------------------------------------------------------------------------------------- */

#app[data-mode="welcome"] #cap-rail,
#app[data-mode="welcome"] #cap-rail-more {
  display: grid;
  /*
   * `minmax(0, 1fr)`, not `1fr`, and it is a bug fix rather than a flourish. `1fr` is
   * `minmax(auto, 1fr)`, and `auto` floors a track at its content's MIN-CONTENT width — for these
   * cards that is 34px of disc + 20px of gaps + 12px of chevron + 18px of padding + the longest word
   * in the title. On a 320px pane each track has 139px to play with and "Handpicked"/"weather" push
   * that floor past it, so the two tracks grew and the right-hand column hung 16px off the pane.
   * Measured after this pass's larger disc and padding made the floor 12px higher than it was.
   *
   * A zero minimum lets the track be exactly half the sheet and the text clip inside it (both title
   * and description are already `-webkit-line-clamp`ed with `overflow: hidden`), which is the
   * behaviour a two-column grid of fixed-width cards has to have.
   */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  /* `1fr` on the implicit rows of an auto-height grid resolves to "every row as tall as the tallest",
     which is the point: `align-items: stretch` alone equalises WITHIN a row, so a row of one-line
     cards sat 16px shorter than the row under it and the menu read as a pile rather than as a set.
     One card needing two lines costs every card 16px, which is the right price for a considered set —
     and is why the i18n `desc` strings are written to a 16-character budget. */
  grid-auto-rows: 1fr;
  /* 10px, not 8. The cards are taller and their surfaces are more present than they were, and a
     set of paper cards needs the ground to show between them or the grid reads as a table. */
  gap: 10px;
  align-items: stretch;
  overflow: visible;
  padding: 0 var(--sp-4) var(--sp-4);
  /* The lane's HORIZONTAL scroll affordances are meaningless in a grid. What replaces the right-edge
     fade is a fade at the FOOT of the sheet (#cap-panel::after), because the axis that runs out of
     room changed with the axis the cards run in. */
  -webkit-mask-image: none;
  mask-image: none;
  scroll-snap-type: none;
}

/* The gap under the four featured cards is the disclosure's, not the rail's — the rail's own
   sp-4 floor would stack with #cap-more's own air and read as a break in the menu. #cap-rail-more
   keeps no bottom padding either: #cap-desk below it carries the sheet's real foot. */
#app[data-mode="welcome"] #cap-rail,
#app[data-mode="welcome"] #cap-rail-more {
  padding-bottom: var(--sp-2);
}

/*
 * THE BOTTOM FADE IS GONE, and the reason is that the overflow it was apologising for is gone too.
 *
 * It existed because thirteen entries do not fit a phone — six rows of 87px in a sheet that has
 * ~278 — and the argument was that a row dissolving into the dock reads as "there is more, scroll"
 * where a row cut flush reads as a clipped layout. The argument is sound; the ratio was not. At
 * 375x812 the mask landed squarely through the middle of the fifth row, so what a guest actually
 * saw was "Book & travel" and "Ask the front desk" as half-dissolved ghost text — which reads as
 * neither of the two things the fade was choosing between. It reads as a rendering bug.
 *
 * The sheet now opens with four contextual cards and a full-width "More ways I can help" row
 * (js/app.js renderCapRail), which fits whole at every width this app ships. Nothing is cut, so
 * nothing needs dissolving; and when the guest opens the row, the sheet scrolls the ordinary way to
 * a real end rather than into a gradient. A fade over a list the guest deliberately expanded would
 * be the same ghost text again, one tap later.
 */
#app[data-mode="welcome"] #cap-panel {
  /* NO border where the sheet meets the photo. The base rule's `border-top: 1px solid
     var(--glass-line)` (#cap-panel, above) is a hairline meant to separate one paper surface from
     another — over a photograph it reads as a drawn line across the image instead, which is the
     "border on the image" this pass removes. Active mode's compact rail keeps the base rule's
     hairline (it never sits over the photo), so this is scoped to welcome only. */
  border-top: none;
}

#app[data-mode="welcome"] #cap-rail > .cap-pill:last-child:nth-child(odd),
#app[data-mode="welcome"] #cap-rail-more > .cap-pill:last-child:nth-child(odd) {
  grid-column: 1 / -1;
}

/* =============================================================================================
 * THE DISCLOSURE — "More ways I can help", and the nine cards behind it.
 *
 * The sheet leads with four contextual cards (js/app.js featuredCapKeys) and keeps the rest one tap
 * away. This is the tap, and the grid under it is what it opens.
 *
 * NOT A CARD, deliberately. Every instinct here says make it a full-width `.cap-pill` so the menu
 * stays one kind of object — and that is what it was, for one build. It measured 87px: #cap-rail is
 * `grid-auto-rows: 1fr`, so the row a one-line control lands in is still as tall as the tallest
 * card in the sheet, and the whole point of this pass was to stop spending the sheet's height on
 * things that are not the menu. So it is its own object, outside the grid: a quiet full-width bar,
 * lighter than a card, that reads as the edge of the menu rather than another item on it.
 *
 * Its own grid for what it reveals, for the same reason and one more: `aria-controls` can then name
 * a single container, which is what a disclosure is supposed to point at.
 * ============================================================================================= */
/*
 * `[hidden]` FIRST, and at ID specificity, because the rule below is the reason it is needed. The
 * UA stylesheet's `[hidden] { display: none }` is specificity 0-1-0 and `#cap-more`'s own
 * `display: flex` is 1-0-0 — so the disclosure ignored the attribute entirely and stayed on screen
 * in ACTIVE mode, orphaned under the conversation's shortcut lane with nothing left to disclose
 * (js/app.js does set `dom.capMore.hidden = true` there; the attribute simply had no effect).
 *
 * Exactly the bug `#cap-rail-more[hidden]` further down already fixes for the grid this opens — the
 * second half of the same mistake, in the element next to it. Unscoped by mode, so `hidden` means
 * hidden wherever it is set rather than only in the mode someone remembered to write a rule for.
 *
 * IT DOES NOT REPLACE THAT ONE. `#cap-rail-more` is listed here for the modes where nothing else
 * sets its `display`, but in WELCOME mode the grid rule is `#app[data-mode="welcome"] #cap-rail-more`
 * — two IDs, which outranks this selector's one. The scoped `[hidden]` rule below is what wins
 * there and is still load-bearing; deleting it as a duplicate brings the 428px collapsed list back.
 */
#cap-more[hidden],
#cap-rail-more[hidden] {
  display: none;
}

#cap-more {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  width: calc(100% - 2 * var(--sp-4));
  margin: 0 var(--sp-4);
  /* 44px: the tap-target floor exactly, and half a card — which is the proportion that makes it
     read as the menu's own footer rather than as a fifth entry. */
  min-height: 44px;
  padding: 0 var(--sp-3);
  border: 0;
  border-radius: 14px;
  background: color-mix(in srgb, var(--lavender, #ffe4d5) 45%, transparent);
  color: var(--ink-soft);
  font: inherit;
  font-size: calc(14px * var(--type-scale));
  font-weight: 600;
  cursor: pointer;
  transition:
    background var(--dur) var(--ease),
    color var(--dur) var(--ease);
}

#cap-more:hover,
#cap-more:focus-visible {
  background: color-mix(in srgb, var(--lavender, #ffe4d5) 75%, transparent);
  color: var(--ink);
}

#cap-more .icon {
  /* The chevron turns to point at what it opens — the one piece of state this control has, and the
     only motion on it. A `transition` rather than an animation, so css/layout.css's blanket
     `@media (prefers-reduced-motion: reduce)` rule — which caps `transition-duration` on `*` —
     already covers it, and this needs no reduced-motion rule of its own. */
  display: flex;
  transition: transform var(--dur) var(--ease);
}

#cap-more[aria-expanded="true"] .icon {
  transform: rotate(180deg);
}

/* What it reveals: the same two-column grid of the same cards, laid out by the same rule as
   #cap-rail (see THE WELCOME GRID above, which names both). Only the padding differs — the gap
   above it belongs to the disclosure, not to the first row of cards. */
#app[data-mode="welcome"] #cap-rail-more {
  padding-top: var(--sp-3);
}

/* `[hidden]`'s UA `display: none` is not enough here — the grid rule above is an ID plus an
   attribute and wins outright, so a collapsed list kept its whole 428px of layout and pushed the
   front-desk line 340px below the dock. Restated at matching specificity so the markup can keep
   saying `hidden` and mean it. */
#app[data-mode="welcome"] #cap-rail-more[hidden] {
  display: none;
}

/*
 * THE NARROW SHEET — 320px, where the heading is what overflows.
 *
 * Measured at 320x812: #cap-panel-head goes 49.6px -> 71.2px because "Here are some things I can
 * help with" wraps to two serif lines, and that 21.6px is most of the 30px by which the sheet then
 * ran past the dock — taking the front-desk line with it.
 *
 * The ornament goes, not the words. `.cap-panel-mark` is the gilt rule and diamond to the right of
 * the heading; it is `aria-hidden` decoration, and dropping it returns its own width plus the
 * flex gap to the heading, which then fits on one line again. The same trade this file already
 * makes for the wordmark at this width (see THE LOCKUP): at 320px a decorative flourish beside
 * type costs the type a line, and the type is the part carrying meaning.
 */
@container pane (max-width: 359px) {
  #app[data-mode="welcome"] .cap-panel-mark {
    display: none;
  }
  #app[data-mode="welcome"] #cap-panel-head {
    padding-bottom: var(--sp-2);
  }
}

/* =============================================================================================
 * THE WAY TO A PERSON — one line under the cards, into the front desk.
 *
 * Quiet on purpose, and quiet in a specific direction: it is smaller and softer than a card, so it
 * never competes with the four above it, but it is a real button with a real tap target rather than
 * a footnote. A guest who wants a human should not have to work out that "Ask the front desk" is
 * hiding inside a disclosure, and they must not reach for the safety pill instead — that control is
 * for "I feel unsafe" and has to keep meaning only that.
 * ============================================================================================= */
#cap-desk {
  margin: 0;
  /* sp-2, not sp-4, at the foot: the dock's own top padding sits directly beneath this line, so a
     16px floor here read as ~30px of dead air — and at 320px it was the 8px that pushed the line
     itself under the dock. The 44px button above it already carries the tap target. */
  padding: 0 var(--sp-4) var(--sp-2);
  text-align: center;
}

.cap-desk-btn {
  /* A link's manners, a button's target: 44px of height for the thumb, none of it drawn. */
  min-height: 44px;
  padding: 0 var(--sp-3);
  border: 0;
  background: none;
  color: var(--ink-soft);
  font: inherit;
  font-size: calc(13px * var(--type-scale));
  line-height: 1.4;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--ink-soft) 35%, transparent);
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color var(--dur) var(--ease);
}

.cap-desk-btn:hover,
.cap-desk-btn:focus-visible {
  color: var(--ink);
  text-decoration-color: currentColor;
}

/* =============================================================================================
 * THE CARD — an item on a hotel's menu of services.
 *
 * A glyph on a lavender disc, the name of the thing, a line saying what it covers, and a chevron.
 * Four parts, and the second line is the one that makes it a card rather than a large tag: "Permits"
 * alone in a 160px box reads as a filter you toggle in a set; "Permits / Rules & paperwork" reads as
 * something being offered to you.
 *
 * PAPER, not glass: an ivory face, an indigo hairline, a soft warm shadow. Not a 0.9-alpha pane with
 * a 12px blur and a 0.7-alpha white border — a white edge on cream reads as a highlight around every
 * single one, which is what made the old lane the loudest thing on the screen.
 * ============================================================================================= */

/*
 * THE WIDTH BUDGET, and where this pass spent it.
 *
 * Two columns in a 375px pane leaves each card 165.5px (16px of sheet padding each side, 10px of
 * gap), and four things have to fit across it: a disc, a gap, two lines of text, and a chevron.
 * Measured at 375:
 *
 *     165.5 − 2 border − 8 pad − 34 disc − 10 gap − 12 chevron − 10 gap − 10 pad = 79.5px of text
 *
 * That is 12px less than the version before this pass, and the trade is deliberate: the cards were
 * asked to be larger and more spacious, and on a two-column grid "larger" can only come out of the
 * text column or out of the card's height. It came out of both — the disc grows 30 -> 34, the padding
 * 6/8 -> 8/10, and the floor 58 -> 70 — because 79px still holds the ~14 characters the i18n `desc`
 * strings are written to on one line, while a card 12px taller is the difference between a tile and
 * something being offered to you.
 *
 * Every number is still small, and for the same reason as before: every pixel the ornament takes
 * comes straight out of the words.
 */
.cap-pill {
  gap: 10px;
  /* The floor, not the height. The grid stretches every card to its row, and the row is set by the
     tallest — so a two-line description anywhere in a row makes that whole row taller. Uniform rows
     are the whole difference between eight cards and a considered menu of eight. */
  min-height: calc(70px * var(--type-scale));
  /* Tighter on the glyph side than the chevron side: the disc carries its own optical margin, so
     matching padding on both edges leaves the card looking left-heavy. */
  padding: var(--sp-3) 10px var(--sp-3) var(--sp-2);
  /*
   * FROSTED GLASS — semi-transparent, tinted from the same warm-ivory/indigo family the rest of this
   * app's glass surfaces already use (--glass/--glass-2/--lavender), so the card reads as its own
   * small pane of the same material rather than a swatch of an unrelated colour. It can afford to be
   * this transparent without the photograph's own ridge lines showing confusingly through it: the
   * layer behind it is now heavily blurred (`#canvas::before`, THE VIEW, CONTINUED, above), so what
   * shows through is soft colour, not competing detail — the same reason the seam above it disappeared.
   *
   * NO BORDER PROPERTY — the edge is an inset box-shadow instead (below), so it costs no layout.
   * The card used to carry no edge at all, on the argument that even a low-alpha 1px line reads as
   * an outline. That held while the face was cool lavender over a warm-ivory sheet: the hue
   * difference alone separated them. The AtithiVani palette put card and sheet in the same peach
   * family, and the composite is now LIGHTER than the sheet at the top and a hair darker at the
   * bottom — the two halves cancel and the card disappears. The ring is the smallest thing that
   * gives it an edge back without touching the fill, which is what the glyph disc and the label
   * contrast are both measured against.
   */
  border: none;
  /* 18, not 16: a taller card wants a slightly larger corner or the radius reads as tighter than it
     was. The sheet it sits on is 26 and the reply card is 22, so the three still step down. */
  border-radius: 18px;
  background: linear-gradient(178deg, rgba(255, 253, 251, 0.6) 0%, rgba(255, 228, 213, 0.52) 100%);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  backdrop-filter: blur(14px) saturate(150%);
  /* Softer than the paper-card shadow it replaces (0.05/0.28 alpha -> 0.04/0.16): a hard-edged
     shadow under a translucent card reads as a glass tile with a paper shadow glued under it. The
     leading `inset` is the card's edge (see NO BORDER PROPERTY above) — a hairline in the plum
     anchor at the same alpha `--hairline` uses elsewhere, so it matches every other seam on the
     sheet. The drop shadow's spread goes -16px -> -12px and 0.16 -> 0.22 for the same reason: at
     -16 on a 20px blur almost nothing reached the page. */
  box-shadow:
    inset 0 0 0 1px rgba(74, 35, 64, 0.1),
    0 1px 2px rgba(74, 35, 64, 0.04),
    0 10px 20px -12px rgba(74, 35, 64, 0.22);
  color: var(--ink);
}

/* Dark mode: this card's fill above is a hardcoded cream gradient with no theme awareness, which
   is a cream tile on a dark canvas. The palette's own elevated-surface tokens instead — a dark,
   reliably-opaque-enough card regardless of what the mountain is doing behind the sheet. A
   hairline stands in for the shadow's own separation job, since a black shadow barely registers
   against a dark ground. */
:root[data-theme="dark"] .cap-pill {
  background: linear-gradient(178deg, var(--glass-2) 0%, var(--surface-2) 100%);
  border: 1px solid var(--glass-line);
  box-shadow: var(--shadow-2);
}

/* The icon disc. `box-sizing: border-box` is what sizes this without touching the SVG: the glyph
   inside is `width: 100%` of the CONTENT box (css/chat.css), so the padding sets the glyph's size and
   the width sets the disc's — 34 minus two 8s leaves an 18px drawing, which is where these
   fine-stroke icons were designed to sit. */
.cap-pill-glyph {
  box-sizing: border-box;
  width: calc(34px * var(--type-scale));
  height: calc(34px * var(--type-scale));
  padding: calc(8px * var(--type-scale));
  border-radius: 50%;
  background: var(--lavender);
  color: var(--violet-deep); /* 9.0:1 on the card face, 7.8:1 on the lavender disc it sits on */
  /* Colour is deliberately NOT transitioned — the same call css/daylight.css records for
     `.pref-chip`, and for the same measured reason: when a transitioned property resolves through a
     custom property that a THEME swaps on an ancestor, Chromium keeps the value it started from. No
     animation is registered and the property never re-resolves, so toggling the theme with cards on
     screen left these discs painted the LIGHT --lavender (#ffe4d5) on a dark card indefinitely.
     Confirmed here by forcing `transition: none`, which snapped them to the correct fill. The hover
     below is a state change and still reads instantly; only the 120ms tween is given up. */
}

/*
 * PER-CATEGORY ACCENTS, dark mode only — one muted hue per topic so the four quick actions read as
 * a differentiated set rather than four identical lavender discs. Dark only on purpose: the light
 * theme's own single-lavender treatment is a deliberate "nothing is the selected one" decision (see
 * the note on .cap-pill-book) and is not this pass's to change.
 *
 * Only the four TOPIC cards get a hue. The rest of the rail (book, tracks, finds, and the signed-in
 * request entries) keeps the neutral lavender disc, so a coloured disc means "this is one of the
 * four things you came here to ask about" rather than just "this is a card".
 *
 * Each pair is a low-alpha wash of the hue plus a lit ink of the same hue — the wash carries the
 * category and the ink carries the contrast, which is what keeps these muted rather than neon.
 */
:root[data-theme="dark"] .cap-pill-permits .cap-pill-glyph {
  background: rgba(255, 155, 120, 0.18);
  color: #ffb89d;
}

:root[data-theme="dark"] .cap-pill-roads .cap-pill-glyph {
  background: rgba(204, 88, 53, 0.18);
  color: #f99f7f;
}

:root[data-theme="dark"] .cap-pill-rooms .cap-pill-glyph {
  background: rgba(62, 143, 99, 0.18);
  color: #7fcf9c;
}

:root[data-theme="dark"] .cap-pill-local .cap-pill-glyph {
  background: rgba(242, 96, 63, 0.18);
  color: #ff9b78;
}

.cap-pill-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1 1 auto;
}

.cap-pill-title {
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: 1.25;
  color: var(--ink);
  /* Two lines, then ellipsis. "Ask the front desk" needs two at 160px; nothing needs three, and a
     third would make one card taller than its neighbour in the grid. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.cap-pill-desc {
  font-size: calc(11px * var(--type-scale));
  font-weight: 400;
  line-height: 1.3;
  color: var(--ink-faint); /* 6.1:1 on the card face */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The chevron. Quiet by construction: 14px at 0.5 opacity is an affordance, and anything louder
   competes with the glyph disc for the eye at the two ends of the same small card. */
.cap-pill-go {
  width: calc(12px * var(--type-scale));
  height: calc(12px * var(--type-scale));
  flex: 0 0 auto;
  color: var(--ink-faint);
  opacity: 0.5;
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}

/* ---------------------------------------------------------------------------------------------
 * ONE INTERACTION TREATMENT, for every card in both modes.
 *
 * A 1px lift, the shadow one step up, the hairline gaining a little indigo, the disc one step deeper.
 * That is all: these are sheets of paper on a sheet of paper, and paper does not scale, glow or
 * bounce. Press returns it to the surface, which is the whole feedback a finger needs.
 *
 * `transform` is on the base rule (not just the hover) so the lift and the press both animate, and
 * `:disabled` is excluded throughout — a card the send guard has switched off must not respond to a
 * hover as though it would do something.
 * ------------------------------------------------------------------------------------------- */

/* `transform` ONLY — the lift and the press still animate, and that is the whole feedback the note
   above describes. The three COLOUR properties are deliberately dropped, the same call
   css/daylight.css records for `.pref-chip` and `.cap-pill-glyph` above: a transitioned property
   resolving through a custom property that a THEME swaps on an ancestor keeps the value it started
   from in Chromium — no animation is registered and it never re-resolves. Measured here: with
   `border-color` transitioned, a dark-mode card's border computed to `rgb(245, 243, 250)` (frozen
   `currentColor`, since the base rule is `border: none`) — a bright white hairline around every
   card, which is the one thing a premium dark surface must not have. Forcing `transition: none`
   snapped it to the correct `rgba(127, 179, 212, 0.16)`, which is what confirmed the cause. */
.cap-pill {
  transition: transform var(--dur-fast) var(--ease);
}

.cap-pill:not(:disabled):hover {
  /* Same frosted family as the resting fill, just a touch more opaque — enough to register as
     "lifted" without snapping back to an opaque paper card. */
  background: linear-gradient(178deg, rgba(255, 253, 251, 0.72) 0%, rgba(255, 228, 213, 0.64) 100%);
  /* One alpha step up on the resting ring, same as every other hover step here. (The `border-color`
     this replaces was inert — the base rule is `border: none`, so there was nothing to colour.) */
  box-shadow:
    inset 0 0 0 1px rgba(74, 35, 64, 0.16),
    0 2px 4px rgba(74, 35, 64, 0.05),
    0 14px 26px -18px rgba(74, 35, 64, 0.22);
  transform: translateY(-1px);
}

/* Dark mode's own lift: the light hover fill above is cream-based and would flash a pale tile on
   press. One step brighter than the resting dark fill, same as every other pane's hover step. */
:root[data-theme="dark"] .cap-pill:not(:disabled):hover {
  background: linear-gradient(178deg, var(--glass-3) 0%, var(--surface-lift) 100%);
  border-color: var(--glass-line-strong);
  box-shadow: var(--shadow-3);
}

.cap-pill:not(:disabled):hover .cap-pill-glyph {
  background: var(--lavender-deep);
}

/* The four topic discs step up in their OWN hue on hover rather than falling back to the neutral
   lavender step above, which would read as the accent being lost on the one interaction that should
   confirm it. Same hues as the resting fills, one alpha step up. */
:root[data-theme="dark"] .cap-pill-permits:not(:disabled):hover .cap-pill-glyph {
  background: rgba(255, 155, 120, 0.3);
}

:root[data-theme="dark"] .cap-pill-roads:not(:disabled):hover .cap-pill-glyph {
  background: rgba(204, 88, 53, 0.3);
}

:root[data-theme="dark"] .cap-pill-rooms:not(:disabled):hover .cap-pill-glyph {
  background: rgba(62, 143, 99, 0.3);
}

:root[data-theme="dark"] .cap-pill-local:not(:disabled):hover .cap-pill-glyph {
  background: rgba(242, 96, 63, 0.3);
}

.cap-pill:not(:disabled):active {
  transform: translateY(0);
  /* The ring stays through the press. Dropping it would make the card lose its edge for exactly as
     long as a finger is on it, which reads as the card vanishing under the tap. */
  box-shadow: inset 0 0 0 1px rgba(74, 35, 64, 0.16), 0 1px 2px rgba(74, 35, 64, 0.05);
}

/* Keyboard focus is the same object as hover plus a ring, so tabbing the lane feels like the same
   control the finger uses. The ring is on the card rather than replacing its shadow: a focus ring
   that removes the lift makes the focused card look pressed. */
.cap-pill:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.cap-pill:not(:disabled):hover .cap-pill-go {
  opacity: 0.85;
  transform: translateX(2px);
}

/*
 * THE NARROWEST SHEET — a 320px SE, a ~344px folded cover screen.
 *
 * Two columns in a 320px pane leaves each card 139px, and after the disc, the gaps, the chevron and
 * the padding that is 59px of text: "Roads & weather" breaks to "Roads &" / "weather" and "Ask the
 * front desk" ends in an ellipsis. The card grew for this pass and the text column paid for it, and
 * at this width that trade stops being worth it.
 *
 * So the ornament gives the width back, in the order it is worth least: the CHEVRON goes first (it
 * is a 0.5-opacity affordance on a card that is entirely tappable, and the whole row already reads as
 * a menu item), then the disc steps back to 30px and the gaps to 8. That is 59px -> 85px of text,
 * which is more than the version before this pass had at this width — the cards stay 70px tall and
 * spacious, and only the decoration is rationed.
 */
@container pane (max-width: 359px) {
  #app[data-mode="welcome"] .cap-pill {
    gap: 8px;
    padding-inline: 6px 8px;
  }

  #app[data-mode="welcome"] .cap-pill-glyph {
    width: calc(30px * var(--type-scale));
    height: calc(30px * var(--type-scale));
    padding: calc(6px * var(--type-scale));
  }

  #app[data-mode="welcome"] .cap-pill-go {
    display: none;
  }
}

/*
 * "BOOK & TRAVEL" IS NOT SPECIAL ANY MORE, and that is the fix.
 *
 * It carried a champagne hairline, a champagne disc and champagne ink, on the reasoning that the one
 * entry the property earns from deserves the brand accent. What it actually looked like in a grid of
 * eight otherwise identical cards was the SELECTED one — a guest reads "the gold card" as "the card
 * I am on" or "the default", and it drew the eye away from Permits and Roads & weather, which are the
 * two things a guest on a hill road actually needs first.
 *
 * So every card now has one surface and one interaction treatment, and the champagne stays where it
 * cannot be mistaken for a state: the sheet's gilt hairline, the header band's rule, the ornaments.
 * The class itself is untouched and still in the markup — tests/e2e drive `.cap-pill-book`, and the
 * marketplace it opens is unchanged.
 */
.cap-pill-book {
  border-color: var(--glass-line);
}

.cap-pill-book .cap-pill-glyph {
  background: var(--lavender);
}

/* =============================================================================================
 * THE ACTIVE LANE. The same cards, recomposed as one horizontal row of glyph-beside-label
 * shortcuts — the shape a lane wants when the conversation is the subject and the lane has ~70px of
 * the screen rather than half of it.
 *
 *     [ ⬡ Permits ] [ ⬡ Roads & weather ] [ ⬡ Rooms ] [ ⬡ Nearby ] →
 *
 * ## Why the label sits BESIDE the glyph now
 * It was stacked under it in a fixed 88px-wide card, which made every label of more than one word
 * wrap: "Roads & weather" was two centred lines at 11px, "Ask the front desk" three. A card whose
 * text has to break to fit is a card a guest reads instead of scanning. Laid out in a row the card
 * sizes to its own label, so every entry is one line at 12.5px — wider cards, more readable, and the
 * lane still holds four of them on a 390px phone with the fifth peeking under the right-edge fade
 * (which is what says "swipe").
 *
 * The description and the chevron still go: a second line is for a menu you are reading, and there is
 * a transcript above this that a guest is reading instead.
 *
 * ## The floors
 * 52px tall — above the 48px touch floor with the glyph and the label centred in it, and it is what
 * keeps the whole lane (including its own padding) at ~70px so the transcript keeps the screen. It
 * scrolls horizontally and ONLY horizontally: the lane is `flex-wrap: nowrap` + `overflow-x: auto`
 * with `overflow-y: hidden` (see #app[data-mode="active"] #cap-rail above), so there is no width at
 * which this becomes two rows.
 * ============================================================================================= */

#app[data-mode="active"] .cap-pill {
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  gap: 9px;
  /* No width: the card is as wide as its own label. `flex: 0 0 auto` is what stops the lane's flex
     layout squeezing them all to fit — they are meant to overflow and scroll. */
  width: auto;
  flex: 0 0 auto;
  min-height: calc(52px * var(--type-scale));
  padding: 0 var(--sp-3) 0 10px;
  border-radius: 15px;
  text-align: start;
}

#app[data-mode="active"] .cap-pill-glyph {
  width: calc(30px * var(--type-scale));
  height: calc(30px * var(--type-scale));
  padding: calc(7px * var(--type-scale));
}

#app[data-mode="active"] .cap-pill-text {
  flex: 0 0 auto;
  align-items: flex-start;
  width: auto;
}

/* One line, never wrapped, never clipped — the card grew to fit it. 12.5px rather than the stacked
   version's 11: this is the size at which a label under a transcript is read rather than deciphered. */
#app[data-mode="active"] .cap-pill-title {
  font-size: calc(12.5px * var(--type-scale));
  line-height: 1.2;
  letter-spacing: 0.005em;
  white-space: nowrap;
  -webkit-line-clamp: 1;
}

#app[data-mode="active"] .cap-pill-desc,
#app[data-mode="active"] .cap-pill-go {
  display: none;
}

/*
 * A LANDSCAPE PHONE, mid-conversation: the lane goes to bare glyphs.
 *
 * Measured at 667x375 before this: band 124, transcript 92, lane 91, dock 68 — the shortcut lane was
 * taking as much of the screen as the answer it sits under, which is the one thing §14 of the brief is
 * explicit about. Bare 48px discs give 27px back to the transcript and stay a full touch target.
 *
 * The entry's name is still the button's accessible name (js/app.js sets `aria-label` from the title,
 * not from the visible span), so this costs a sighted guest a label at the width where labels do not
 * fit, and costs a screen-reader guest nothing. `display: none` on the lane was the alternative and it
 * is worse: a shortcut a guest cannot reach beats a small one only on a spreadsheet.
 *
 * It lives HERE rather than in the height ladder at the top of this file because the rule it has to
 * beat — `#app[data-mode="active"] .cap-pill { width: 78px }` — is in the block above at equal
 * specificity, and later wins. Moving it up there is what made the first attempt a no-op.
 */
@media (max-height: 500px) {
  #app[data-mode="active"] .cap-pill {
    width: calc(var(--tap) * var(--type-scale));
    min-height: calc(var(--tap) * var(--type-scale));
    padding: 0;
    /* The row layout left-aligns its glyph, which in a 48px card with no label is 9px off centre.
       Centring is not cosmetic here: the glyph IS the whole control at this width. */
    justify-content: center;
  }

  #app[data-mode="active"] .cap-pill-text {
    display: none;
  }
}

/* ---------------------------------------------------------------------------------------------
 * Chips, composer, actions.
 * ------------------------------------------------------------------------------------------- */

/* The starter chips stay pills, deliberately: they are whole questions ("Is NH10 open?"), and a
   sentence set in a rounded rectangle reads as a card the guest is meant to study rather than as
   something to tap and be done with. */
.chip {
  color: var(--ink-soft);
  border-radius: var(--radius-pill);
}
.chip:not(:disabled):hover {
  background: var(--glass-3);
  border-color: var(--glass-line-strong);
  color: var(--ink);
}
.chip:disabled {
  opacity: 0.42;
}

#text-input {
  background: transparent;
  border-color: transparent;
  color: var(--ink);
}
#text-input::placeholder {
  color: var(--ink-faint);
}
#text-input:focus {
  border-color: var(--glass-line-strong);
  background: var(--glass);
}

/* The one thing allowed to be a solid accent: the primary action.
 *
 * #jump-latest and .op-book are in this list now. They were not, and css/daylight.css's earlier
 * rule left them white-on-light-accent — 1.9:1 on the old marigold, an unreadable label on the
 * two controls a guest reaches for most. Every primary action is one fill and one ink again. */
/* .pref-submit and .cab-submit join the list rather than keeping copies of their own. They were the
   last two primary actions in the app still filled with `var(--brand)` under a near-black brown ink
   — a different fill, a different ink and a different hover from every other button a guest presses. */
#send-btn,
#jump-latest,
.btn-accent,
.op-call,
.op-book,
.login-submit,
.pref-submit,
.cab-submit {
  /* The fill is the deep INDIGO anchor, not the lighter --violet. It is the hue the header band and
     the orb are already built from — and, more to the point, it is the exact fill the welcome
     screen's "Let's Get Started" button uses, so the first primary action a guest ever taps and
     every one after it are the same object. --violet keeps its small jobs: icons, the live dot, the
     glow. */
  background: var(--indigo);
  /* The fill sits at the dark end of its ramp, so the ink is the band's cream: --on-brand would be
     1.1:1 here. --on-indigo measures 13.9:1 on this fill and 10.4:1 on the --indigo-lit hover — and it
     is tenant-proof, which --accent-ink is not (see the token). */
  color: var(--on-indigo);
  border: 0;
  /* The welcome CTA's own shadow, scaled to a smaller control: two stops, indigo-tinted, so the
     button reads as lifted off the paper rather than as glowing. */
  box-shadow: 0 10px 22px -12px rgba(74, 35, 64, 0.7), 0 2px 5px -2px rgba(74, 35, 64, 0.35);
}
#send-btn:not(:disabled):hover,
#jump-latest:hover,
.btn-accent:not(:disabled):hover,
.op-call:hover,
.op-book:hover,
.login-submit:hover,
.pref-submit:hover,
.cab-submit:hover {
  /* One step LIGHTER, not brighter, and it is the welcome CTA's own hover step: the button reads as
     rising toward the light while --on-indigo stays clear on it (10.4:1). */
  background: var(--indigo-lit);
}

/* =============================================================================================
 * THE CONVERSATION — two objects, and they are deliberately not the same KIND of object.
 *
 * The guest's message is a spoken aside: short, right-aligned, indigo, and it reads as something
 * said. Atithi's reply is a document: left-aligned, ivory, and it reads as something written down
 * and handed over. That asymmetry is the whole design — a guest should never have to check an
 * alignment to know which is which, and two mirrored bubbles in two tints is exactly the generic
 * messaging look the brief rejects.
 *
 * The markup is untouched. js/render.js builds `.row.user > .bubble` and `.row.assistant > .bubble`
 * (plus `.row.error`), and every selector below is one of those three — no new class, no wrapper,
 * no change to the renderer.
 *
 * `.bubble.out` used to be styled here and never existed: no code path has ever produced an `.out`
 * class (the roles are on the ROW, not the bubble), so that rule was painting nothing. Deleted
 * rather than recoloured — the same call this file's own notes make about the dead --orb-size
 * override.
 * ============================================================================================= */

.bubble {
  border-radius: var(--radius-lg);
}

/* ---------------------------------------------------------------------------------------------
 * THE GUEST'S MESSAGE — solid indigo, warm ivory ink.
 *
 * It was a 12%-lavender wash of the card colour (css/daylight.css), which on a cream ground is
 * nearly the same value as Atithi's reply: the two sides of the conversation differed by a tint so
 * slight that alignment was doing all the work. Solid indigo is the same fill as the send button
 * and the welcome screen's CTA — the guest's own words wear the colour of the guest's own actions.
 *
 * Kept compact on purpose: a question is short, and a narrower measure than the reply is what makes
 * the pair read as dialogue rather than as two columns.
 * ------------------------------------------------------------------------------------------- */

.row.user .bubble {
  background: var(--indigo);
  /* 13.9:1 — the same ink the header band and every primary action use over this hue. */
  color: var(--on-indigo);
  /* No hairline. A border on a solid dark fill is a seam, and this pane is not a piece of paper. */
  border-color: transparent;
  /* Softer and tighter than the shared --shadow-2: a dark fill already separates from cream on
     value alone, so the shadow only has to lift it a millimetre. */
  box-shadow: 0 6px 14px -10px rgba(74, 35, 64, 0.55);
  /* The tail. 6px rather than chat.css's 4px — at a 22px radius, 4 reads as a nick in the corner
     and 6 reads as a deliberate point back toward the guest. */
  border-bottom-right-radius: 6px;
}

/* Markdown inside the guest's bubble. The guest types plain text, so in practice only the link and
   code rules can fire — but chat.css inks all of these with --ink / --brand-ink, which are tuned for
   ivory paper and land at 1.1:1 and 1.6:1 on indigo. Anything that does appear stays readable. */
.row.user .bubble strong {
  color: var(--on-indigo);
}

.row.user .bubble a {
  color: #e6c9d9; /* 9.1:1 on the plum fill */
}

.row.user .bubble code {
  background: rgba(255, 255, 255, 0.16);
}

.row.user .bubble li::marker {
  color: rgba(246, 241, 234, 0.7);
}

/* The guest's own meta line sits under a right-aligned bubble, so it aligns with it. `.row.user` is
   already `align-items: flex-end` (chat.css), which handles the block — this is the text inside it,
   which was starting at the meta line's own left edge. */
.row.user .meta {
  justify-content: flex-end;
}

/* The timestamp itself, on both sides of the conversation. One step up from --ink-faint (the meta
   line's colour, which also carries the agent badge and the retry affordance) because a time you
   have to hunt for is not doing its job — and `tabular-nums` so a stamp changing from "9:09" to
   "9:11" cannot shift the line under a bubble. */
.meta-time {
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------------------------
 * ATITHI'S REPLY — a calm sheet of ivory, not a chat bubble.
 *
 * Three moves take it from bubble to card, and the third is the one that does the work:
 *  1. The card FACE (the same gradient every grounding card uses), so a reply and the cards that
 *     justify it are visibly the same material. The reply used to be a flat translucent pane while
 *     the card under it was a gradient — two papers in one answer.
 *  2. NO TAIL. chat.css gives it a 4px bottom-left corner, which is the one detail that says
 *     "speech bubble". A uniform radius says "sheet".
 *  3. More air. --sp-4 all round instead of --sp-3/--sp-4: a document has margins, and this is the
 *     block a guest actually reads at length.
 * ------------------------------------------------------------------------------------------- */

.row.assistant .bubble {
  padding: var(--sp-4);
  background: var(--card-face);
  border-color: var(--glass-line);
  box-shadow: var(--shadow-2);
  /* Overrides chat.css's 4px tail — see move 2 above. */
  border-bottom-left-radius: var(--radius-lg);
}

/* The reply is the one block on this screen read as prose, so it gets prose measure: 1.62 is the
   welcome blurb's own line-height, and the extra leading is what separates "readable" from
   "legible" over four or five lines of an answer about permits. */
.row.assistant .bubble p,
.row.assistant .bubble li {
  line-height: 1.62;
}

/* A heading inside a reply takes the display serif, the way a card headline does. It is the only
   ornament the transcript gets, and markdown h3 is rare enough that it stays an accent. */
.row.assistant .bubble h3 {
  font-family: var(--font-serif);
  font-size: calc(var(--fs-h3) * 1.08);
  font-weight: 600;
  letter-spacing: 0.005em;
  color: var(--ink);
}

/* ---------------------------------------------------------------------------------------------
 * THE RECOMMENDATION LIST — a concierge presenting options, not a bulleted paragraph.
 *
 * A model's answer to "where should I go near Manali" is a markdown list, and it is the single most
 * common shape a reply takes. A round bullet in the accent makes it read as a machine's output; a
 * champagne diamond makes it read as a card on a menu, and it is the SAME rotated square the hero
 * ornament and the privacy line use, so the transcript shares the page's one ornament rather than
 * introducing a symbol of its own.
 *
 * A pseudo-element and a grid, not `::marker` with `content` and not a `list-style-type` string —
 * both of those need a glyph, and ✦ / ◆ are outside the Latin subsets css/fonts.css ships. Drawn, it
 * looks the same in every locale and on every OS.
 *
 * Scoped to a DIRECT child list, so a nested list keeps ordinary markers: two levels of identical
 * diamonds would say the levels are equal when the indent says they are not.
 * ------------------------------------------------------------------------------------------- */

.row.assistant .bubble > ul {
  list-style: none;
  padding-left: 0;
  margin: 0.7em 0;
  display: flex;
  flex-direction: column;
  gap: 0.55em;
}

/*
 * `position: relative` + an absolute marker, and NOT a two-column grid.
 *
 * The grid was the first attempt and it broke the moment real content hit it. A markdown list item is
 * mixed inline flow — `<strong>Solang Valley</strong>` followed by a bare text node " – Adventure
 * activities" — and `display: grid` makes EVERY child, including each anonymous text run, its own grid
 * item. So the bold lead-in took the 1fr column and the description that followed it was placed in the
 * next row's FIRST column, which is 12px wide: it rendered one character per line, straight down the
 * card. Verified in the browser on a real reply.
 *
 * An absolutely-positioned marker over a padded block leaves the item as ordinary inline flow, which is
 * what a sentence needs.
 */
.row.assistant .bubble > ul > li {
  position: relative;
  margin: 0;
  padding-left: 1.4em;
}

.row.assistant .bubble > ul > li::before {
  content: "";
  position: absolute;
  left: 3px;
  /* The diamond sits on the first line's optical centre. 1.62 is the reply's own line-height, so half
     a line minus half the diamond puts it there whatever the type scale does. */
  top: calc((1.62em - 6px) / 2);
  width: 6px;
  height: 6px;
  background: var(--gold);
  transform: rotate(45deg);
}

/* The lead-in of each item — "**Solang Valley** – Adventure activities" — is the one place a reply is
   allowed a colour, and it is the indigo the guest's own words wear rather than the champagne of the
   ornament beside it. Two golds in one line would make the diamond a bullet point again. */
.row.assistant .bubble > ul > li > strong:first-child {
  color: var(--violet-deep); /* 9.0:1 on the card face */
}

/* ---------------------------------------------------------------------------------------------
 * MEASURE. A reply is a document and a question is an aside, so they do not share a width.
 *
 * chat.css caps every row at `min(72ch, 88%)`. On a phone that resolves to 88% for both, which
 * leaves the reply 12% narrower than it needs to be for no reason — nothing is beside it — while the
 * guest's short question is allowed to run nearly edge to edge and stops reading as an aside.
 * ------------------------------------------------------------------------------------------- */

.row.assistant,
.row.error,
.row.system {
  max-width: min(72ch, 95%);
}

.row.user {
  max-width: min(52ch, 82%);
}

/*
 * An error is a reply that went wrong, so it keeps the reply's shape — the semantic danger tint from
 * the palette is what carries the meaning, and it does not need a different silhouette too.
 *
 * The COLOURS are pinned here because they were the last of the pre-cream palette left in the
 * transcript. css/daylight.css inks this bubble with --ember on a 12%-ember wash, and measured on
 * the current ground that is 3.7:1 — under AA, on the one message that has to be read carefully,
 * often by someone whose connection just failed. --ember is also a scene colour on this palette (it
 * is the sun over the range and the road advisory's tint), so using it as error ink made "something
 * broke" share a hue with "here is the weather".
 *
 * The replacement is not a new colour: it is the --danger-bg / --danger-ink pair the palette already
 * carries and the safety control already uses, measured at 7.3:1 on ivory. Same meaning, same
 * family, twice the legibility, and no behaviour touched — js/render.js still builds the identical
 * row with the identical retry affordance.
 */
.row.error .bubble {
  padding: var(--sp-4);
  border-bottom-left-radius: var(--radius-lg);
  background: var(--danger-bg);
  border-color: var(--danger-line);
  color: var(--danger-ink);
}

/* The retry control inherits the bubble's ink rather than the page's, so it stays legible on the
   tint instead of falling back to a colour chosen for cream. */
.row.error .retry-btn {
  color: var(--danger-ink);
  border-color: rgba(150, 48, 47, 0.4);
}

/*
 * THE OVERLAY SHEETS — the capability map, the marketplace, and hotel sign-in.
 *
 * They were a 0.74-alpha DEEP INDIGO slab, and that was a straightforward contrast bug rather than a
 * matter of taste: every rule inside them inks with --ink / --ink-soft (css/chat.css), which are the
 * near-black indigos tuned for ivory paper. Measured on the shipped build, the capability sheet's own
 * headline rendered #241f38 on #1e1442 — 1.1:1. The heading of the sheet a guest opens to find out
 * what the product can do was invisible, and had been since this file made the panels dark.
 *
 * They are cream paper now, like every other surface in the app: a near-opaque ivory sheet with the
 * indigo hairline and the deepest shadow in the system, which is what a modal earns. The inks they
 * already use are the ones measured against exactly this face (--ink 15.2:1, --ink-soft 6.9:1), so
 * the fix is the surface, not thirty text colours.
 *
 * The blur stays: these cover a live transcript, and a sheet you can sense the page behind is what
 * says "this is over your conversation, not instead of it".
 */
#market-panel,
#login-panel {
  background: linear-gradient(180deg, var(--glass-3) 0%, var(--canvas-lift) 100%);
  -webkit-backdrop-filter: blur(30px) saturate(140%);
  backdrop-filter: blur(30px) saturate(140%);
  border-color: var(--glass-line-strong);
  box-shadow: var(--shadow-3);
  color: var(--ink-soft);
}

/* THE SIGN-IN/OTP SHEET STAYS LIGHT, ALWAYS — asked for directly, independent of whether the
   concierge behind it is dark. `#login-panel` is the one container for both steps of the flow
   (hotel+phone, then the OTP itself — js/login.js reuses the same element), so pinning it here
   covers the whole thing.

   Every custom property the dark block redefines (this file's `:root[data-theme="dark"]`, above)
   is re-declared here at its LIGHT value. That is sufficient, not just necessary: a token that is
   only ever an alias for another (`--hairline: var(--glass-line)`, `--paper: var(--canvas-lift)`,
   `--brand: var(--violet)`, ...) is stored UNRESOLVED and substituted at the point of use against
   whichever element is asking — so overriding the token being aliased here also fixes every alias
   of it for anything painted inside this sheet, with no need to re-list the aliases themselves. */
:root[data-theme="dark"] #login-panel {
  color-scheme: light;
  --canvas: #f7e9dd;
  --canvas-lift: #fbede4;
  --brand-dark: #c0492b;
  --brand-light: #ff9b78;
  --brand-ink: #a33b21;
  --violet: #c0492b;
  --violet-deep: #c0492b;
  --violet-lit: #ff9b78;
  --accent: #4a2340;
  --accent-ink: #fff3ea;
  --lavender: #ffe4d5;
  --lavender-deep: #ffd9c4;
  --glass: rgba(255, 250, 246, 0.9);
  --glass-2: rgba(255, 250, 246, 0.96);
  --glass-3: #fffcf9;
  --glass-line: rgba(74, 35, 64, 0.1);
  --glass-line-strong: rgba(74, 35, 64, 0.2);
  --card-face: linear-gradient(178deg, #fffdfb 0%, #fdf4ee 100%);
  --surface: #fffaf6;
  --surface-2: #fbede4;
  --surface-3: #f7e9dd;
  --surface-lift: #fffcf9;
  --paper: #fbede4;
  --paper-2: #fbede4;
  --paper-warm: #fffaf6;
  --bubble-in: #fffaf6;
  --bubble-out: #ffe4d5;
  --ink: #2a1226;
  --ink-soft: #5b3a54;
  --ink-faint: #7a5a72;
  --vani: #9c3a07;
  --vani-lit: #e87a30;
  --indigo: #4a2340;
  --indigo-lit: #5c3350;
  --indigo-ink: #36192e;
  --on-indigo: #fff3ea;
  --header-bg: linear-gradient(168deg, #5c3350 0%, #4a2340 52%, #36192e 100%);
  --header-ink: #fff3ea;
  --header-ink-soft: #e6c9d9;
  --header-line: rgba(255, 255, 255, 0.14);
  --ghost-bg: rgba(255, 250, 246, 0.9);
  --ghost-bg-hover: #fffcf9;
  --ghost-line: rgba(74, 35, 64, 0.2);
  --focus-ring: 0 0 0 3px rgba(74, 35, 64, 0.45);
  --shadow-1: 0 1px 2px rgba(74, 35, 64, 0.06);
  --shadow-2: 0 6px 16px -10px rgba(74, 35, 64, 0.16);
  --shadow-3: 0 24px 56px -26px rgba(74, 35, 64, 0.3);
  --scroll-thumb: rgba(74, 35, 64, 0.24);
  --scroll-thumb-hover: rgba(74, 35, 64, 0.4);
  --scroll-track: rgba(74, 35, 64, 0.06);
  --ok-bg: #e5f1ea;
  --ok-ink: #1f6647;
  --ok-line: #2f7d5b;
  --warn-bg: #fbf1da;
  --warn-ink: #8f3d10;
  --warn-line: #c2571f;
  --danger-bg: #fbe9e6;
  --danger-ink: #96302f;
  --danger-line: #b23b3b;
  --info-bg: #fbede4;
}

/* =============================================================================================
 * "WHAT CAN ATITHI DO?" — once a small card on its own scrim, now content inside the drawer row
 * that asks the question.
 *
 * Before that it was a full-pane sheet with a serif headline, a three-line paragraph and twelve
 * items in four groups — a product brochure that arrived over a conversation a guest was in the
 * middle of (measured at 390x844: 1330px of content in a 620px pane, four screens of scrolling,
 * opened from a one-line menu row). The scrim-and-card version fixed the takeover; mounting the
 * same content inside the drawer (see .drawer-about below) removes the second overlay entirely —
 * one disclosure, not a row that opens a dialog.
 * ============================================================================================= */

.cap-sheet {
  width: min(100%, 22.5rem);
  /* Never taller than three quarters of the pane: the conversation stays visible above and below it,
     which is the whole difference between a card and a takeover. Its own content scrolls past that. */
  max-height: min(75cqh, 30rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-4);
  border: 1px solid var(--glass-line-strong);
  border-radius: var(--radius-lg, 22px);
  background: var(--glass-3);
  box-shadow: var(--shadow-3);
  color: var(--ink-soft);
}

.cap-sheet::-webkit-scrollbar {
  display: none;
}

/* One column, always — the four-across grid (chat.css's base .cap-groups) is for a projector; at
   the drawer's own width (min(78cqi, 21rem), so ≤336px) the groups are a list of three short
   stages, and the horizontal rule under each label is what separates them. */
.drawer-about .cap-groups {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/* The groups lose their card-within-a-card treatment: no fill, no border, no shadow, no padding.
   Three bordered boxes inside the drawer's own bordered panel is the tell of content that used to
   be a page of its own. */
.drawer-about .cap-group {
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
}

.drawer-about .cap-group-label {
  margin-bottom: var(--sp-2);
  color: var(--ink-faint);
}

.drawer-about .cap-list {
  gap: var(--sp-2);
}

/* The item's glyph: a lavender disc and indigo ink, at 26px — chat.css's base square-cornered
   champagne-wash treatment is tuned for a card floating in the transcript, and champagne inking a
   glyph here would make every one of the eight items read as a brand mark rather than a bullet. */
.drawer-about .cap-item .icon {
  width: calc(26px * var(--type-scale));
  height: calc(26px * var(--type-scale));
  padding: calc(5px * var(--type-scale));
  border-radius: 50%;
  background: var(--lavender);
  color: var(--violet-deep);
}

.drawer-about .cap-name {
  font-weight: 600;
  font-size: calc(13px * var(--type-scale));
}

.drawer-about .cap-body {
  font-size: calc(11.5px * var(--type-scale));
  line-height: 1.4;
  color: var(--ink-soft);
}

/* The closing line — how to ask, and in which languages. Above it, the same hairline the group labels
   use, so it reads as the foot of the card rather than as a ninth item. */
.cap-foot {
  margin: 0;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--glass-line);
  font-size: calc(11.5px * var(--type-scale));
  line-height: 1.4;
  color: var(--ink-faint);
}

/* The road advisory keeps its ember tint but becomes glass like everything else. */
.road-banner {
  background: rgba(240, 164, 138, 0.12);
  border-color: rgba(240, 164, 138, 0.3);
}
.road-banner-route {
  color: var(--ink);
}
.road-banner-note {
  color: var(--ink-soft);
}

/*
 * SAFETY — findable without being loud, and part of the hospitality system rather than an alarm.
 *
 * It was the danger TINT as a fill: a pink pill with a saturated red hairline, sitting permanently in
 * the dock next to the mic. That is the right treatment for a warning a guest is being SHOWN; it is
 * the wrong one for a control a guest is being OFFERED, and in a cream-and-indigo concierge it read
 * as the one piece of a different application.
 *
 * So the surface becomes the same ivory paper as everything else in the dock, and the danger colour
 * retreats to the two places that actually locate the control: a soft ember hairline and the ink.
 *
 * This IMPROVES the contrast rather than trading it away — the reason the previous revision reached
 * for the tint + ink pair was that a pale coral measured 4.42:1 on ivory, under AA. --danger-ink is
 * not that coral: on the pink fill it measured 6.5:1, and on this ivory it measures 7.3:1. The one
 * control a guest reaches for when something is wrong is now the more legible of the two, not the
 * less. The weight also drops from 700 to 600: at 13px, 700 was doing the shouting the fill used to.
 *
 * `.safety-btn` / `#safety-btn` stay in the selector list. They are the safety CARD's own call
 * button, and it is deliberately NOT quietened — inside a card that has already told a guest
 * something is wrong, a loud button is correct. It keeps the tint via the rule after this one.
 */
.safety-btn,
#safety-btn,
.help-btn,
#help-btn {
  background: var(--danger-bg);
  border: 1px solid var(--danger-line);
  color: var(--danger-ink);
  -webkit-backdrop-filter: blur(var(--blur));
  backdrop-filter: blur(var(--blur));
}

/* The DOCK's safety control only — scoped so the safety card's own CTA is untouched. */
/* The DOCK's control is dressed in THE DOCK block at the foot of this file — the surface moved onto
   the ring inside the button when the label moved outside it, so the two rules have to be read
   together and it is better to have them in one place. */

/*
 * DARK MODE has to move the INK, not just the surface — and this is the trap the light rule above
 * walks into if it is left alone.
 *
 * --danger-ink (#96302f) is a dark red chosen against a pale danger tint, and the palette does not
 * re-declare it per theme because the tint does not either: the base .help-btn rule pairs the two,
 * so both stay light and the pair keeps working. The moment the dock's control takes --glass-2 as
 * its surface it becomes theme-aware, and in dark mode that is a deep indigo — dark red on it
 * measured 1.83:1, on the one control a guest reaches for when something is wrong.
 *
 * So the dark theme gets the same relationship inverted: a light coral ink (7.9:1 on the dark pane)
 * and a hairline lifted to match (in THE DOCK block). The hue is unchanged — it is still
 * unmistakably the ember control, just lit from the other side.
 */
:root[data-theme="dark"] #dock-bar .help-btn,
:root[data-theme="dark"] #dock-bar .help-btn .icon,
:root[data-theme="dark"] #dock-bar .help-btn .help-label {
  color: #f2a49c;
}
.safety-btn .icon,
#safety-btn .icon,
.help-btn .icon,
#help-btn .icon,
.help-label {
  color: var(--danger-ink);
}

/* The marketplace card's champagne ink is gone with the rest of its special treatment — see the note
   on `.cap-pill-book` above. Every capability card takes --ink for its title and --violet-deep for its
   glyph, like the other seven, so none of them reads as the selected one.
 *
 * `.prov` (a grounding card's provenance line) keeps --brand-ink: that is champagne used as a quiet
 * label colour on a receipt, which is nothing to do with a card being singled out. */
.prov {
  color: var(--brand-ink);
}

/* Cards + their furniture */
.card-title {
  color: var(--ink);
}
.prov,
.card-foot {
  color: var(--ink-faint);
}

/* Form controls inside glass */
.login-select,
.login-input {
  background: var(--glass-2);
  border: 1px solid var(--glass-line);
  color: var(--ink);
}
.login-select:focus,
.login-input:focus {
  border-color: var(--glass-line-strong);
}

/* Native select needs an explicit dark popup or the OS paints white-on-white options. The sign-in
   sheet's hotel picker is the only native select left on this screen — the language control is a
   drawn popover now (see THE LANGUAGE CONTROL above), so the two rules that used to dress its OS
   popup here, including the `@supports not (appearance: base-select)` fallback, went with it. */
.login-select {
  color: var(--ink);
}
.login-select option {
  background: var(--canvas-lift);
  color: var(--ink);
}

/* =============================================================================================
 * THE MIC — a button, not an orb.
 *
 * It was a glass sphere: an opaque indigo body under a specular highlight, a white light burning at
 * its heart, a white hairline rim, an inner shade along the bottom for volume, an indigo halo, and —
 * at rest — an infinite breathing scale plus an expanding "invite" ring (css/orb.css). Six layers
 * and two forever-running animations on the one control that has to read as ordinary.
 *
 * That is the "oversized glowing microphone / neon AI aesthetic" this pass is asked to remove, and
 * the argument for removing it is not only taste: a permanently pulsing object next to a text field
 * is the screen telling a guest that the machine is the point. The composer is the point.
 *
 * So: one flat indigo disc, one hairline, one white glyph, one soft contact shadow. Same hue as the
 * header band and the welcome screen's CTA, so it stays unmistakably the primary action; same size
 * it already was (52 welcome / 48 active — it was never oversized in pixels, only in weight).
 *
 * WHAT IS NOT TOUCHED: the state machine. js/orb.js still drives `data-state`, and every state that
 * carries information keeps its cue — the listening ring, the thinking arc, the speaking pulse, the
 * error tint, the acting ripple, and the disabled treatment below. Only the IDLE animations go,
 * because "idle" is not information.
 * ============================================================================================= */

/* Two colours, not three: the disc is indigo and everything on it is white. A champagne rim or glyph
   would put a third hue on the one object the eye lands on first, and the gold's job on this screen
   is hairlines and texture. So the cue tokens orb.css derives its ring, arc and ripple from stay
   white here. */
#orb {
  --orb-cue: rgba(255, 255, 255, 0.95);
  --orb-cue-soft: rgba(255, 255, 255, 0.42);
}

.orb-core {
  /* ONE fill: the palette's own indigo, lit a step from the top so the disc is not a flat circle of
     ink. The two stops are --indigo-lit and a step below --indigo-ink, which is the same ramp the band
     and every primary action are built from. No specular highlight, no inner core light, no blur. */
  background: linear-gradient(180deg, #5c3350 0%, #2f1528 100%);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  /* One contact shadow and one hairline of inner light along the top edge. That is the whole
     material. The 44px halo went with the glow. */
  box-shadow:
    0 6px 14px -8px rgba(26, 17, 57, 0.5),
    inset 0 1px 1px rgba(255, 255, 255, 0.28);
}

/* Dark mode: the two stops above are LIGHT-palette literals (#3a2a7a is that theme's --indigo-lit),
   so the one control the brief calls the focal point was the only major surface still wearing the
   old ramp. The palette's own purple pair instead — and a restrained glow, which is the one place
   this theme spends one: the mic is the primary action and a purple lift is what marks it as such
   against a near-black dock. Kept to a single wide, low-alpha stop, not a halo. */
/* The mic reads as the SIGNATURE, not as another chrome disc. On Neel the anchor is a mid navy,
   and a mid-navy sphere on a near-black dock is the one thing this control must not be — it was
   the flattest object on the screen. The lit stop takes the theme's accent coral and the shadow
   stop keeps the anchor, so the sphere still belongs to the palette while being the brightest
   thing in the dock, which is what the orb is for. */
:root[data-theme="dark"] .orb-core {
  background: linear-gradient(180deg, #ff9b78 0%, #c0492b 100%);
  box-shadow:
    0 6px 16px -8px rgba(0, 0, 0, 0.5),
    0 0 28px -10px rgba(255, 155, 120, 0.5),
    inset 0 1px 1px rgba(255, 255, 255, 0.24);
}

/* The rim: a quiet white hairline, even all the way round rather than catching the light at one
   corner. A gradient rim is a lit sphere's tell. */
#orb::before {
  background: rgba(255, 255, 255, 0.24);
}

/* The mic glyph — white on the indigo, and no drop-shadow: there is no highlight underneath it to
   define its edges against any more. (orb.css paints it --violet-deep under a WHITE shadow, both tuned
   for the frosted disc this replaces.) */
.orb-glyph {
  color: #fff;
  filter: none;
}
.orb-glyph svg {
  color: #fff;
}

/*
 * AT REST, IT RESTS.
 *
 * orb.css gives the idle state `orb-breath` (an infinite 2.4s scale) and `orb-invite` (an infinite
 * expanding ring). Both are switched off here rather than deleted there, because the same two
 * animations still do real work on `listening` and `speaking`, where a moving control means the
 * pipeline is live. A `data-state` selector is one attribute more specific than orb.css's, so this
 * wins without !important.
 */
#orb[data-state="idle"] .orb-core,
#orb[data-state="idle"] .orb-ring {
  animation: none;
}

#orb[data-state="idle"] .orb-ring {
  opacity: 0;
}

/* Hover/press stay, and stay small: they are the only motion the control has at rest, and they are
   the ones a finger asks for. */
#orb:not([data-state="disabled"]):hover {
  transform: none;
  filter: brightness(1.08);
}

/* Blocked/disabled. orb.css desaturates with `grayscale(.7)` and drops to `opacity:.55`, which on
   this palette turned the control into a pale cream blob with a near-invisible glyph — the reported
   "icon is not clear". Unavailable should read as the same button, unlit: the indigo drops to half
   strength, and the glyph keeps its contrast on it. */
#orb[data-state="disabled"] .orb-core {
  filter: none;
  opacity: 1;
  background: linear-gradient(180deg, rgba(58, 42, 122, 0.45), rgba(36, 26, 82, 0.6));
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.18);
}

/* Dark mode's unlit mic — the same "same button, at half strength" idea as the rule above, in the
   new palette's purple rather than the light theme's indigo, and with no glow (the glow is what
   marks the control as available). This selector carries an id + an attribute, so it correctly
   outranks the enabled `:root[data-theme="dark"] .orb-core` rule further up. */
:root[data-theme="dark"] #orb[data-state="disabled"] .orb-core {
  background: linear-gradient(180deg, rgba(89, 54, 81, 0.4) 0%, rgba(64, 41, 59, 0.52) 100%);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.18);
}
#orb[data-state="disabled"] .orb-glyph {
  opacity: 1;
  color: rgba(246, 241, 234, 0.8);
  filter: none;
}

/* ERROR — the mic must still READ as a mic.
 *
 * `.orb-glyph svg { color: #fff }` above is unconditional, and the error disc in css/orb.css is
 * near-white, so the glyph was white on white: the control the guest is being asked to tap
 * appeared to have vanished. That is the "there is no mic icon" report, and it survived four
 * rounds of looking for it in JS because nothing in JS is wrong — the icon is in the DOM, at full
 * opacity, painted in a colour with no contrast against what is behind it.
 *
 * The disc is tinted properly here too rather than left at orb.css's 22% wash, so the state is
 * legible as a state and not as a rendering fault. Contrast of the glyph against the disc is
 * ~4.8:1; the disc against the dock is not decoration, it is the only cue that the tap will
 * retry.
 */
#orb[data-state="error"] .orb-core {
  background: radial-gradient(
    128% 128% at 32% 24%,
    color-mix(in srgb, var(--ember) 16%, #fff),
    color-mix(in srgb, var(--ember) 40%, #fff) 72%
  );
  box-shadow: 0 16px 34px -12px color-mix(in srgb, var(--ember) 45%, transparent),
    inset 0 2px 2px rgba(255, 255, 255, 0.75);
}

#orb[data-state="error"] .orb-glyph,
#orb[data-state="error"] .orb-glyph svg {
  color: color-mix(in srgb, var(--ember) 88%, #2b1206);
  opacity: 1;
  filter: none;
}

/* The status line is a SIBLING of #dock-bar, not a child of #orb — so css/orb.css's
   `#orb[data-state="error"] #orb-label` has never matched anything. `:has()` on the shared
   ancestor is how the idle state already does it (css/orb.css's own idle rule), and it is what
   makes the reason read as a warning instead of as ordinary muted status text. Three ids, so it
   also outranks css/daylight.css's `#app[data-mode="active"] #orb-label`, which is what was
   actually painting it. */
#composer-dock:has(#orb[data-state="error"]) #orb-label {
  color: var(--ember);
  font-weight: 600;
}
#orb[data-state="disabled"]::before {
  background: rgba(255, 255, 255, 0.16);
}

/* =============================================================================================
 * THE DOCK — the surfaces. css/orb.css owns its layout; this owns what it is made of.
 *
 * The band itself is the heaviest pane in the system: it holds the page down. Inside it, three
 * materials and a clear hierarchy — a solid indigo sphere, an ivory field, and a quiet outlined
 * control. Nothing here is a mode: the composer is permanently visible, so it is dressed once.
 * ============================================================================================= */

#composer-dock {
  /* PAPER, not glass. This was a transparent-to-cream gradient over a 26px blur — the heaviest
     backdrop-filter in the app, on the one surface that is permanently on screen and permanently
     over moving content. Two reasons it is now a flat fill:
       · it read as glass. Rule 4 of this file's own system note says surfaces are paper, and the
         dock was the largest remaining exception to it;
       · a blurred pane the transcript scrolls under is a continuous GPU repaint across the full
         width of the viewport, on the one element guaranteed to be composited every frame.
     --canvas-lift at full strength is the same colour the old gradient reached at its foot, so the
     dock's own face is nearly unchanged; what goes away is the see-through top third.
   *
   * SOFTENED AGAIN, without reopening that decision: `rgba()` at 0.82 alpha, still NO
   * `backdrop-filter`. The GPU cost the note above rejected is specifically a blur SAMPLING moving
   * content every frame — plain alpha over a static composite has none of that, so the dock can
   * read as a touch less solid without paying for glass. 0.82, not lower: this is the one surface
   * with mic/type/help/privacy on it, and "soft" was never asked to mean "hard to read against". */
  background: rgba(251, 237, 228, 0.82);
  border-top: 1px solid rgba(74, 35, 64, 0.08);
}

/* PRE-CHAT ONLY: a touch more transparent than the base 0.82, so the mountain carried behind the
   rail (`#canvas::before`, THE VIEW, CONTINUED) stays naturally perceptible around the mic too,
   instead of being fully covered the moment it reaches the dock. Still a flat alpha, no
   backdrop-filter — the GPU-cost objection above is about a live blur sampling the transcript every
   frame, which this remains free of; a plain alpha change costs nothing extra to composite. Scoped
   to the two states that actually have a photograph behind them (an in-conversation dock has none —
   see #canvas::before's own selectors — so its 0.82 base is untouched). */
#app[data-mode="active"]:has(#log:empty) #composer-dock,
#app[data-mode="welcome"] #composer-dock {
  background: rgba(251, 237, 228, 0.68);
}

/* Dark mode: the same flat-fill discipline (no backdrop-filter — see the note above), just the
   dark glass tint instead of cream. --glass-2 is already the alpha this file uses for every other
   dark pane, so the dock reads as the same material as the cards above it, not a cream leftover. */
:root[data-theme="dark"] #composer-dock {
  background: var(--glass-2);
  border-top: 1px solid var(--glass-line);
}

/* The light pre-chat override above is higher-specificity than the dark rule right above this one
   (three IDs vs. one), so without restating it here dark mode's pre-chat dock would inherit the
   LIGHT cream fill — measured live, and exactly the bug this rule exists to prevent. The value is
   a step lighter than the base dark dock's own --glass-2, for the same reason the light pair steps
   0.82 -> 0.68: the mountain carried behind the rail stays perceptible at the dock too, so the foot
   of the screen reads as glass over the view rather than a separate solid rectangle. */
:root[data-theme="dark"] #app[data-mode="active"]:has(#log:empty) #composer-dock,
:root[data-theme="dark"] #app[data-mode="welcome"] #composer-dock {
  background: var(--glass);
}

/*
 * THE FIELD. An elegant ivory field with a hairline, and the send action inside it — one control
 * with something at its end, rather than a field and a button that happen to be adjacent. It was
 * dressed only in active mode, on the reasoning that a welcome screen has nothing competing with it;
 * with the composer permanently in the dock, "where do I type" has to be answerable at a glance on
 * the first screen too.
 */
#composer-field {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: flex-end;
  gap: 6px;
  padding: 6px 6px 6px var(--sp-4);
  /* A hairline with a whisper of indigo in it rather than the shared --glass-line: this is the one
     surface on the screen a guest is meant to want to touch, and at 0.10 alpha its edge was the
     faintest object in the dock. */
  border: 1px solid rgba(74, 35, 64, 0.14);
  /* Half the ONE-LINE field's height (the 44px send + 6px padding each side + 1px border each side),
     not --radius-pill. At one line the two are the same pill. But 999px clamps to half of whatever
     height the field has, so as the textarea grew the corners grew with it — 59px at the four-line
     cap — and the first/last lines and the bottom-anchored send ran into the curve. Held at the
     one-line value, the corners stay the size of the send button's own, so send sits concentric in
     the bottom-right corner at every height. */
  border-radius: calc((44px * var(--type-scale) + 14px) / 2);
  /* --surface, and the house --shadow-1 rather than a hand-rolled two-stop: the field is the same
     stock as the cards it sits below, which is what makes the dock read as part of the page rather
     than a tray laid over it. It stays the brightest thing in the dock — that job was --glass-3's
     reason for being here, and --surface is the brighter paper in both themes — but it no longer
     needs its own shadow recipe to say so.
   *
   * A thin rgba() skin over --surface, not --surface itself: the field is still the single brightest,
   * most opaque object in the dock (0.92 alpha, well above the dock's own 0.82) — this is the part a
   * guest reads as "type here", so it stays closest to solid paper of anything softened this pass. */
  background: rgba(252, 249, 243, 0.92);
  box-shadow: var(--shadow-1);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}

/* Dark mode: --surface-lift is this theme's brightest paper (see the note above — the field has
   to stay the brightest object in the dock), and the hairline follows the dark glass-line rather
   than the light theme's indigo-on-cream assumption. */
:root[data-theme="dark"] #composer-field {
  background: var(--surface-lift);
  border-color: var(--glass-line-strong);
}

#composer-field:focus-within {
  border-color: rgba(74, 35, 64, 0.34);
  box-shadow: var(--shadow-2);
}

/* Dark mode: the light focus ring above is an indigo darker than the field it would sit on. The
   lit lavender step is this theme's own visible-on-dark accent (see --brand-light's note). */
:root[data-theme="dark"] #composer-field:focus-within {
  border-color: var(--brand-light);
}

/* The textarea is the field's content, not a control of its own: chat.css gives it a border, a
   radius, a fill and its own focus ring, all of which are the field's job now. What it keeps is the
   16px floor (iOS force-zooms a focused field under 16px) and the auto-grow, both in chat.css. */
#text-input {
  border: 0;
  border-radius: 0;
  padding: 0;
  background: none;
  box-shadow: none;
  /* 40 + the field's 6px top and bottom padding = a 52px field, which is 4px TALLER than the mic and
     the safety control beside it. That is deliberate: the composer is the dominant control in this
     dock, and the row still reads as one because the three are centred on it. */
  min-height: calc(40px * var(--type-scale));
  /* The line has to sit on the vertical centre of a 40px box, and the auto-grow in js/app.js sets
     `height` from `scrollHeight` — so the padding has to be even, top and bottom, or the first line
     of a grown field creeps upward. */
  padding-block: calc((40px * var(--type-scale) - (max(16px, var(--fs-body)) * var(--lh-body))) / 2);
  max-height: 6.5em;
}

#text-input:focus {
  border-color: transparent;
  background: transparent;
  box-shadow: none;
}

/* THE SEND BUTTON — the mic's smaller sibling, inside the field.
 *
 * Same hue, same circle, same family; a third of the area and a fraction of the lift, so the pair
 * reads as primary and secondary rather than as two buttons of equal claim. 38px because it sits
 * INSIDE a 48px control: it is not a target a thumb aims at across the screen, it is the end of the
 * field the thumb is already in, and the field's own 48px box is what the touch floor applies to.
 *
 * The empty-input behaviour is untouched: nothing here adds a disabled state or changes when the
 * button is live, which is js/app.js's business. */
/* 44, not 40: it is the one control in the dock that was under the touch floor (measured 40x40 at
   320x568), and it is the action that COMMITS a message — the least forgiving thing in the row to
   have to aim at. The field grows to 56px around it, which also makes the composer unambiguously the
   dominant control in the typing state. */
#send-btn {
  position: relative;
  width: calc(44px * var(--type-scale));
  height: calc(44px * var(--type-scale));
  flex: 0 0 auto;
  box-shadow: 0 5px 12px -7px rgba(74, 35, 64, 0.6);
}

/*
 * ...and its TARGET is bigger than its circle.
 *
 * 38px is the largest circle that fits inside a 48px field with breathing room, and it is the right
 * visual weight — a send button the same size as the mic would claim equal primacy. But 38px is under
 * both touch floors, and "the field around it is 48px" is not an answer: a tap in the field's padding
 * focuses the textarea, it does not send.
 *
 * So the hit area is grown past the paint with a transparent pseudo-element: 44x46, which clears
 * Apple's floor. Asymmetric on purpose — 4px vertically, 3px horizontally — because the horizontal
 * growth eats into the 6-8px gap between the composer and the safety control, and that gap is the one
 * clearance in this dock that exists for a safety reason (see the note in css/orb.css). 3px of it is
 * the most this is allowed to take.
 */
#send-btn::after {
  content: "";
  position: absolute;
  inset: -4px -3px;
  border-radius: var(--radius-pill);
}

#send-btn .icon {
  width: calc(18px * var(--type-scale));
  height: calc(18px * var(--type-scale));
}

/*
 * THE CHARACTER COUNTER, out of the flow.
 *
 * chat.css keeps it as a laid-out-but-`visibility: hidden` sibling below #composer-row so that
 * revealing it near the 500-character limit cannot shift the field mid-sentence. That was right when
 * the composer was a band of its own; inside the dock ROW it made the middle column 17px taller than
 * the mic and the safety control beside it, so 17px of every screen was reserved for a label that is
 * hidden 95% of the time — permanently, to avoid a one-off shift at character 420.
 *
 * Absolute against #composer, so it costs nothing until it appears and shifts nothing when it does.
 * It surfaces just above the field's right end on its own ivory chip, because at that point it is
 * over the transcript (or the capability lane) and a bare 11px numeral on cream would be unreadable.
 */
#composer {
  position: relative;
}

#char-count {
  position: absolute;
  right: 10px;
  bottom: calc(100% + 3px);
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  /* Same paper as the field it labels — see THE FIELD above. */
  border: 1px solid var(--hairline);
  background: var(--surface);
  box-shadow: var(--shadow-1);
  line-height: 1.4;
}

/*
 * SAFETY, in the dock. The ivory paper every other surface is made of, with the danger colour
 * retreating to the two places that actually locate the control: a soft ember hairline and the ink.
 * See the long SAFETY note above for why the tint-as-fill treatment was wrong here and why this is
 * the more legible of the two (7.3:1, against 6.5:1 on the pink fill it replaced).
 *
 * The surface is on the RING, not on the button (css/orb.css): the button is bare so its word can sit
 * outside the circle. So the base .help-btn rule above — which fills the whole control with the
 * danger tint — has to be undone here as well as replaced.
 */
#dock-bar .help-btn {
  background: none;
  border: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/*
 * A ROUNDED SQUARE, not a circle, and a quieter one than it was.
 *
 * The mic is a filled indigo circle. This was a ringed circle of the same diameter beside it, on the
 * reasoning that "filled = primary, ringed = secondary" reads as one family — and it does, but it
 * also made the two controls the same OBJECT at two weights, which is what left safety reading as a
 * second, competing primary action. A squircle is a different object: the same paper, the same
 * hairline discipline, unmistakably not the mic.
 *
 * The ember stays where it locates the control — the shield glyph and, at a third of its old alpha,
 * the hairline — and comes off the box. That is the whole "premium and subtle, not a loud red button"
 * move, and it costs nothing in contrast: --danger-ink measures 7.3:1 on this paper (the label's own
 * ink is set in the SAFETY block above and is unchanged).
 */
#dock-bar .help-btn .icon {
  /* --dock-ctl, the same token the type button reads, so the two brackets cannot drift apart again.
     54 minus two 15s = a 24px drawing, the app's 18px glyph proportion carried up to this size. */
  width: calc(var(--dock-ctl) * var(--type-scale));
  height: calc(var(--dock-ctl) * var(--type-scale));
  padding: calc(15px * var(--type-scale));
  /* --radius-lg — see #composer-toggle .icon's note; same size, same shape, same tier. */
  border-radius: var(--radius-lg);
  background: var(--glass-2);
  border: 1px solid rgba(150, 48, 47, 0.22);
  box-shadow: 0 1px 2px rgba(74, 35, 64, 0.05);
}

#dock-bar .help-btn:hover .icon {
  background: var(--glass-3);
  border-color: rgba(150, 48, 47, 0.45);
}

/* The word under the shield. It was --danger-ink at 600, which on a 9.5px label read as a warning
   rather than as a name; --ink-soft (6.9:1 on the paper) makes it a label again. The MEANING has not
   moved — the glyph above it is still ember, and the full "Help · I feel unsafe" is still the
   button's aria-label (js/app.js), which is what a screen reader announces. */
#dock-bar .help-btn .help-label {
  color: var(--ink-soft);
  font-weight: 600;
}

:root[data-theme="dark"] #dock-bar .help-btn .icon {
  border-color: rgba(242, 164, 156, 0.4);
}

:root[data-theme="dark"] #dock-bar .help-btn:hover .icon {
  border-color: rgba(242, 164, 156, 0.7);
}

/* The privacy line's diamond — the hero ornament's, at 4px. */
.dock-privacy-mark {
  width: 4px;
  height: 4px;
  flex: 0 0 auto;
  background: var(--gold);
  transform: rotate(45deg);
  opacity: 0.85;
}

/* ---------------------------------------------------------------------------------------------
 * Kill the white text halos. They existed to hold type legible over a bright photograph; with the
 * photograph gone they are just smudge.
 * ------------------------------------------------------------------------------------------- */

#brand-name,
#greeting,
#greeting-sub,
#orb-hint,
#orb-label,
.hero-langs,
#hero-copy * {
  text-shadow: none;
}

/* ---------------------------------------------------------------------------------------------
 * Accessibility floor. Visible focus on glass, and motion kept opt-out.
 * ------------------------------------------------------------------------------------------- */

:where(button, a, select, textarea, input):focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

@media (prefers-reduced-motion: reduce) {
  body::before {
    animation: none;
  }
}


/* ── The mute pill's word ─────────────────────────────────────────────────────────────────────
 *
 * The pill itself (THE MUTE PILL, above) already sets the type — size, weight, colour, the lot —
 * because the word is no longer a caption under a glyph, it is half of what the pill IS. This is
 * just the swap: "Mute" at rest, "Muted" once pressed, one word visible at a time. */
.mute-label {
  display: inline;
  line-height: 1;
}

.mute-btn .mute-word-off,
.mute-btn[aria-pressed="true"] .mute-word-on {
  display: none;
}

.mute-btn[aria-pressed="true"] .mute-word-off {
  display: inline;
}
