/**
 * The mic — Atithi's voice-first hero. Same state machine as before (js/orb.js drives `data-state`
 * on #orb): one infinite animation at a time, one label at a time. Every animated cue has a static
 * equivalent under prefers-reduced-motion.
 *
 * ## Colour is the primary signal now; motion is the second one
 * This file used to hold the opposite rule — "idle/listening/speaking read the cue colour, error
 * reads ember, disabled greys out, never a new hue per state" — and leaned on shape and rhythm to
 * separate the live states. In practice that meant listening and speaking drew the same four bars
 * in the same colour, and the guest was asked to tell "I am hearing you" from "I am answering you"
 * by comparing a taper (46/58/70/82 against 34/58/82/100) and a sweep direction, on a 72px control,
 * without a reference to compare against. Two states that a guest cannot name at a glance are one
 * state as far as the interface is concerned.
 *
 * So the live states now carry a hue as well: listening cool, speaking warm champagne, connecting a
 * quiet desaturated step, thinking the palette default. Every one of those is drawn from the
 * existing two-colour palette (indigo/lavender and champagne) — the rule that survives is "no THIRD
 * hue", not "no hue per state". The motion distinctions are all still there and unchanged; they are
 * simply no longer the only thing carrying the meaning. Error still reads ember and disabled still
 * greys out, exactly as before.
 *
 * The base cue pair is `--orb-cue` / `--orb-cue-soft`, with the per-state pairs beside them below.
 * css/glass.css repaints the disc itself (a deep indigo sphere with near-white cues); the ramp this
 * file defaults to dresses the bare light-theme orb.
 */

/* ---------------------------------------------------------------------------------------------
 * The mic button. (#orb-stage itself is laid out with #dock-bar, further down this file.)
 * ------------------------------------------------------------------------------------------- */

#orb {
  /* State cue colour — the ring, the thinking arc, the acting ripple and the halo, in one pair
     instead of four literals. The look that actually ships paints the sphere deep teal and its
     cues white (css/glass.css overrides these two); the gold below is what the bare light-theme
     orb in this file was built for, and it stays the default so the two layers keep agreeing. */
  --orb-cue: var(--violet);
  --orb-cue-soft: var(--violet-glow);

  position: relative;
  width: var(--orb-size);
  height: var(--orb-size);
  flex-shrink: 0;
  border: 0;
  padding: 0;
  background: none;
  border-radius: 50%;
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: transform var(--dur) var(--ease-out-quart);
  -webkit-tap-highlight-color: transparent;
}

#orb:not([data-state="disabled"]):hover {
  transform: scale(1.03);
}

#orb:not([data-state="disabled"]):active {
  transform: scale(0.97);
}

/* The gold gradient hairline rim (the mask trick: paint a 1.5px ring, punch out the middle). */
#orb::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  padding: 1.5px;
  background: linear-gradient(150deg, #fff, var(--violet-lit) 55%, rgba(255, 255, 255, 0.4));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
  z-index: 2;
}

/* The frosted-glass disc — a bright radial highlight, a backdrop blur, a soft lift and a gold
   halo. This is the mockup's `.speak`, mapped onto the app's existing .orb-core layer so the
   state animations keep working. */
.orb-core {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(128% 128% at 32% 24%, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.55) 60%);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  backdrop-filter: blur(10px) saturate(140%);
  box-shadow: 0 16px 34px -10px rgba(18, 41, 45, 0.35), 0 0 46px -8px var(--orb-cue-soft),
    inset 0 2px 2px rgba(255, 255, 255, 0.9), inset 0 -3px 8px rgba(18, 41, 45, 0.1);
  transition: box-shadow var(--dur) var(--ease), filter var(--dur) var(--ease), opacity var(--dur) var(--ease);
}

/* The one animated ring — a cue-coloured circle that scales up and fades. */
.orb-ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid var(--orb-cue-soft);
  opacity: 0;
  pointer-events: none;
}

/* The mic glyph — deep gold on the frosted disc. Large by construction (34%), so it clears the
   3:1 large-element contrast bar. */
.orb-glyph {
  position: relative;
  z-index: 1;
  width: 34%;
  height: 34%;
  color: var(--violet-deep);
  filter: drop-shadow(0 1px 1px rgba(255, 255, 255, 0.65));
  transition: opacity var(--dur) var(--ease), color var(--dur) var(--ease);
}

.orb-glyph svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* =============================================================================================
 * THE STATE CUE — integrated around the mic, not apart from it ("Option B" in review).
 *
 * Every one of the four voice states now reads on the button itself: bars or dots flanking it
 * (listening / understanding / speaking) or a ring drawn around it (thinking). There is no
 * separate stage, no overlay, no blur, no card — #orb-stage is the only object on screen, exactly
 * as it was before this feature existed; what changes is what appears alongside it while
 * `data-state` holds one of the four live values. The button never changes shape or loses its
 * glyph, so it stays recognisable as the control that ends the call.
 *
 * `:has()` is how every rule below reaches #orb from its siblings (the flanks, the halo) —
 * `#orb-stage:has(#orb[data-state="..."])` is the same pattern the dock already used for
 * `#composer-dock:has(#orb[data-state="idle"])` just below, so #orb's `data-state` stays the one
 * source of truth js/orb.js writes to; nothing new needs mirroring onto a second element.
 * ============================================================================================= */

/* THE VOICE STATUS LINE — a small badge, not another section of the dock. It used to sit ABOVE the
 * whole bar, full width, centred with `text-align: center` — which is only the mic's own
 * centreline when #composer-toggle and mute+Help happen to weigh the same. They do not whenever
 * mute is visible, which is exactly when this label has something to say (connecting/listening/
 * thinking/answering) — so a bar-centred line drifted off the mic it was reporting on the moment
 * it was needed most.
 *
 * Anchored to `#orb-stage` instead (`position: absolute`, `left: 50%`, a plain `translateX(-50%)`)
 * — the box the mic itself is centred WITHIN (THE MIC'S TWO SIZES, css/glass.css), not the bar it
 * sits in. That box's own centreline is the mic's centreline by construction, regardless of how
 * wide #composer-toggle/#mute-btn/#help-btn are on either side, so this needs no coordination with
 * any of their widths. `#orb-stage` already carries `position: relative` for the flanks/halo
 * below, so no new positioning context is introduced.
 *
 * Out of flow entirely — idle/connecting/error/disabled cost the dock no height, same promise the
 * old `:empty` rule made, just kept a different way. What it floats INTO is `#composer-dock`'s own
 * reserved top padding (THE DOCK, above) — never `#cap-panel`'s territory, which sits flush against
 * the dock with zero clearance of its own the moment the transcript is empty. Kept deliberately
 * tiny (one line, --fs-micro, a 3px vertical pad) so that reservation can stay small too: this is a
 * badge attached to the mic, not a card floating over the transcript. `pointer-events: none` keeps
 * it inert — it is a status, never a control. */
#orb-label {
  position: absolute;
  left: 50%;
  bottom: calc(100% + var(--sp-1));
  transform: translateX(-50%);
  z-index: 1;
  margin: 0;
  padding: calc(3px * var(--type-scale)) calc(9px * var(--type-scale));
  border-radius: var(--radius-pill);
  background: var(--glass-2);
  border: 1px solid var(--glass-line-strong);
  box-shadow: 0 1px 2px rgba(74, 35, 64, 0.05);
  font-size: var(--fs-micro);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  pointer-events: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  transition: color var(--dur) var(--ease);
}

#orb-label:empty {
  display: none;
}

/*
 * ...and IDLE counts as empty. "Tap and ask me anything" is a caption for a mic glyph, on a dock
 * that already says what it is — the one line the reference composition does not carry either.
 * Connecting/error/disabled all have something to say and keep their strings; this is scoped to
 * idle alone.
 */
#composer-dock:has(#orb[data-state="idle"]) #orb-label {
  display: none;
}

/*
 * ...except once, on this device's very first visit: js/orb.js arms a ~2s idle timer and stamps
 * `data-hint="1"` on this same element if the guest still hasn't tapped. That one attribute has to
 * outrank the blanket idle suppression two rules up — same element, so it wins purely on selector
 * specificity (0,2,1 vs 0,2,0) without needing `!important`. orb.js clears the attribute itself on
 * the first tap or state change, so no companion "un-hint" rule is needed here.
 */
#composer-dock:has(#orb[data-state="idle"]) #orb-label[data-hint="1"] {
  display: block;
}

/*
 * The language hint ("English · বাংলা · हिन्दी · नेपाली") is not drawn in the dock any more.
 *
 * It advertised which languages the VOICE understands, and it did that in the one place the dock
 * cannot spare: a permanent composer, a status line and the privacy note already share this band,
 * and a fourth line of centred copy there is what makes a dock consume the screen. The element and
 * the string stay (js/orb.js still writes it) — the header's language control is what a guest
 * actually uses to change language, and it is 40px away.
 */
#orb-hint {
  display: none;
}

/* #orb-stage is the containing block every cue below hangs off — the flanks sit either side of the
   button, the halo sits centred over it — without disturbing its own centring in the voice-first
   dock or the 56px/left-edge position it takes when the composer opens (css/glass.css). */
#orb-stage {
  position: relative;

  /* PER-STATE CUE COLOUR. Motion alone was carrying the difference between receiving and replying,
     and it could not: both states drew the same four bars, in the same --violet-deep, from the same
     shared rule, differing only in taper (46/58/70/82 against 34/58/82/100), sweep direction and
     rhythm. Those are real distinctions and they stay — but they ask a guest to compare two
     animations they cannot see side by side, from across a room, mid-conversation. Hue does not
     need comparing.
       · listening — the cool end. Atithi is taking something IN.
       · speaking  — the warm champagne end. Atithi is giving something BACK.
     One is cool, one is warm, and that reads peripherally without being studied, which is the whole
     requirement. Both stay inside the two-colour palette (indigo/lavender and champagne); no third
     hue is introduced for either.

     Declared on the STAGE, not on #orb. The flanks are siblings of the button inside this element,
     not children of it, so a custom property set on #orb does not inherit to them — it resolves on
     the button and comes back empty on the bars, which makes `background: var(--orb-cue-listen)`
     an invalid declaration and paints them transparent. The stage is the nearest ancestor the disc
     and both flanks actually share. */
  --orb-cue-listen: var(--violet-deep);
  --orb-cue-listen-glow: var(--violet-glow);
  --orb-cue-speak: var(--gold-deep);
  --orb-cue-speak-glow: var(--gold-glow);
}

/* The champagne family is deliberately NOT re-stepped for dark mode anywhere else in the palette
   (gold reads as gold on either ground), but --gold-deep is the member chosen for contrast against
   CREAM, and these bars sit on the dark dock in dark mode. --gold-lit is the same hue at the step
   the dark theme already uses wherever champagne has to carry against a dark surface. */
:root[data-theme="dark"] #orb-stage {
  --orb-cue-speak: var(--gold-lit);
}

/* ---------------------------------------------------------------------------------------------
 * THE FLANKS — listening, understanding, speaking. Two small waveforms either side of the mic, or
 * two small groups of dots; never both at once, absolutely positioned off the stage's centre line
 * so they add no width and the button never moves.
 * ------------------------------------------------------------------------------------------- */

.orb-flank {
  position: absolute;
  top: 50%;
  /* Fits the wider of the two faces (four bars + three gaps, ~0.385 * --orb-size); dots are
     narrower and simply sit flush to the mic-facing edge within it (justify-content, below). Both
     faces are `inset: 0` on THIS box, so it is the one place that must carry an explicit width —
     without it `inset: 0` has nothing to resolve against and collapses to zero. */
  width: calc(var(--orb-size) * 0.4);
  height: calc(var(--orb-size) * 0.42);
  opacity: 0;
  pointer-events: none;
  /* Behind #mute-btn/#help-btn, not in front of them. A positioned element with no z-index still
     paints ABOVE ordinary in-flow siblings regardless of DOM order — on the narrowest panes, where
     the mic (now pinned to the bar's own centre, THE MIC'S TWO SIZES) sits closer to mute+help
     than the flank's own reach, that meant a "listening"/"speaking" waveform washing across the
     mute pill's text instead of stopping at it. -1 drops the flank one tier below ordinary content
     in the stacking order, so mute/help's own (opaque) surfaces cover it wherever they overlap —
     the flank still animates in full behind them, it just no longer paints over a control's label. */
  z-index: -1;
  transform: translateY(-50%);
  transition: opacity var(--dur) var(--ease);
}

.orb-flank--left {
  right: calc(50% + var(--orb-size) / 2 + var(--sp-2));
}

.orb-flank--right {
  left: calc(50% + var(--orb-size) / 2 + var(--sp-2));
}

/* Bars — listening and speaking. Same family in both — same colour, same pill shape, same
   button-flanking position — so the two still read as one system. What tells them apart is
   deliberately everything ELSE: width, taper, direction and rhythm, all set per state below,
   because "receiving" and "replying" are different gestures and a mic that moved identically for
   both was the thing that read as one animation wearing two names.
   Each fixed static height is the still silhouette a reduced-motion guest sees; the base values
   here are speaking's (a steep taper — see the state rules for why listening's is flatter). */
.flank-bars {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: calc(var(--orb-size) * 0.055);
  opacity: 0;
  transition: opacity var(--dur) var(--ease), width var(--dur) var(--ease);
}

.orb-flank--left .flank-bars { justify-content: flex-end; }
.orb-flank--right .flank-bars { justify-content: flex-start; }

.flank-bars > span {
  width: calc(var(--orb-size) * 0.055);
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--violet-deep);
  transform-origin: center center;
  filter: drop-shadow(0 0 5px var(--violet-glow));
  transition: width var(--dur) var(--ease), height var(--dur) var(--ease);
}

/* The still silhouette — tallest nearest the mic, tapering outward, so the pair reads as one shape
   spreading from the button. `--flank-i` counts OUTWARD FROM THE MIC on each side (0 = innermost),
   which is what makes speaking's centre-out timing (further down) trivial: delay grows with
   distance from the button in both directions at once. */
.orb-flank--left .flank-bars > span:nth-child(1),
.orb-flank--right .flank-bars > span:nth-child(4) { height: 34%; --flank-i: 3; }
.orb-flank--left .flank-bars > span:nth-child(2),
.orb-flank--right .flank-bars > span:nth-child(3) { height: 58%; --flank-i: 2; }
.orb-flank--left .flank-bars > span:nth-child(3),
.orb-flank--right .flank-bars > span:nth-child(2) { height: 82%; --flank-i: 1; }
.orb-flank--left .flank-bars > span:nth-child(4),
.orb-flank--right .flank-bars > span:nth-child(1) { height: 100%; --flank-i: 0; }

/* `--sweep-i` counts ACROSS the whole row, left to right, ignoring the mic in the middle — 0 at
   the far left, 7 at the far right. Listening's only use for it, further down: a wave that arrives
   from one side and crosses the button, which is what "receiving" looks like and centre-out
   timing (`--flank-i`) cannot produce on its own. */
.orb-flank--left .flank-bars > span:nth-child(1) { --sweep-i: 0; }
.orb-flank--left .flank-bars > span:nth-child(2) { --sweep-i: 1; }
.orb-flank--left .flank-bars > span:nth-child(3) { --sweep-i: 2; }
.orb-flank--left .flank-bars > span:nth-child(4) { --sweep-i: 3; }
.orb-flank--right .flank-bars > span:nth-child(1) { --sweep-i: 4; }
.orb-flank--right .flank-bars > span:nth-child(2) { --sweep-i: 5; }
.orb-flank--right .flank-bars > span:nth-child(3) { --sweep-i: 6; }
.orb-flank--right .flank-bars > span:nth-child(4) { --sweep-i: 7; }

/* Dots — understanding. Quieter than the bars either side of it: opacity and a 2px settle instead
   of a scaleY sweep, so "parsing what you said" reads calmer than receiving or replying without a
   fourth, unrelated loader shape. */
.flank-dots {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: calc(var(--orb-size) * 0.09);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

.orb-flank--left .flank-dots { justify-content: flex-end; }
.orb-flank--right .flank-dots { justify-content: flex-start; }

.flank-dots > span {
  width: calc(var(--orb-size) * 0.09);
  height: calc(var(--orb-size) * 0.09);
  border-radius: 50%;
  background: var(--violet-deep);
  opacity: 0.5;
  filter: drop-shadow(0 0 3px var(--violet-glow));
}

.orb-flank--left .flank-dots > span:nth-child(1) { --dot-i: 1; }
.orb-flank--left .flank-dots > span:nth-child(2) { --dot-i: 0; }
.orb-flank--right .flank-dots > span:nth-child(1) { --dot-i: 0; }
.orb-flank--right .flank-dots > span:nth-child(2) { --dot-i: 1; }

/* ---------------------------------------------------------------------------------------------
 * THE HALO — thinking. A ring drawn AROUND the button rather than beside it: "processing" reads as
 * the mic itself working, not as a second object next to it. A faint full track (the path) plus
 * one comet-shaped arc carries the motion, so the state has real presence without ever closing
 * into a solid spinner ring — the comet's own gradient fades to transparent before it completes
 * the circle.
 * ------------------------------------------------------------------------------------------- */

.orb-halo {
  position: absolute;
  top: 50%;
  left: 50%;
  /* 1.32x read as a separate ring floating apart from the mic (review feedback) — a 16%-of-radius
     gap on a 72px disc is ~5.8px, plainly a second object. 1.05x plus the ring's own ~1.5-2px
     border closes that to a hairline, so the two read as one connected shape instead. Still a
     percentage of --orb-size, not a fixed px, so it scales with the mic on every breakpoint and
     under stage mode's --type-scale, exactly as it did before. */
  width: calc(var(--orb-size) * 1.05);
  height: calc(var(--orb-size) * 1.05);
  transform: translate(-50%, -50%);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease);
}

.orb-halo .orbit-track {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* Was `--hairline-strong` — a neutral 20%-alpha grey built for borders on the ivory paper, not
     for tracing a ring that sits mostly OFF the paper, against the mixed backgrounds behind the
     dock. It read as barely there (review). This is the same indigo the comet arc itself is drawn
     in, just faded rather than a different, unrelated hue — darker and more legible, still a thin
     line rather than a filled shape. */
  border: 1.5px solid color-mix(in srgb, var(--violet-deep) 45%, transparent);
}

.orb-halo .orbit-comet {
  position: absolute;
  inset: 0;
  border-radius: 50%;
}

/* The arc itself: a conic gradient (indigo head fading through the lavender accent to nothing)
   punched into a thin ring via a mask, rather than a filled wedge. */
.orb-halo .orbit-comet::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* The head/tail/glow are variables so a state can re-tint the arc without restating the mask and
     gradient geometry. Thinking keeps the palette default below; connecting overrides them. */
  background: conic-gradient(
    from 0deg,
    var(--comet-head, var(--violet-deep)) 0deg,
    var(--comet-tail, var(--violet)) 55deg,
    transparent 105deg 360deg
  );
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2.5px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2.5px));
  filter: drop-shadow(0 0 4px var(--comet-glow, var(--violet-glow)));
}

/* The leading fleck — the one place this cue spends champagne gold, and it spends it small: a 5px
   dot marking the comet's bright end, restrained decoration rather than a third cue colour. */
.orb-halo .orbit-comet::after {
  content: "";
  position: absolute;
  top: -1.5px;
  left: 50%;
  width: 5px;
  height: 5px;
  margin-left: -2.5px;
  border-radius: 50%;
  background: var(--gold);
  box-shadow: 0 0 4px var(--gold-lit);
}

/* ---------------------------------------------------------------------------------------------
 * States. `data-state` on #orb is the single source of truth (js/orb.js); the four live states
 * below are reached from #orb-stage via `:has()`, idle/connecting/error/disabled (further down)
 * read `#orb[data-state]` directly as they always have.
 * ------------------------------------------------------------------------------------------- */

/* Idle — a calm breath and a single faint gold invite ring. The ONE infinite animation. */
#orb[data-state="idle"] .orb-core {
  animation: orb-breath var(--orb-pulse-dur) var(--ease-out-quart) infinite;
}

#orb[data-state="idle"] .orb-ring {
  animation: orb-invite var(--orb-pulse-dur) var(--ease-out-quart) infinite;
}

/* Connecting — dim slightly, still, no ring. */
#orb[data-state="connecting"] .orb-core {
  opacity: 0.85;
  filter: saturate(0.9);
}

/* Every cue below is suppressed while the composer is open: the field occupies the row the flanks
   and halo would otherwise sit in, and the mic itself has shrunk and moved to the row's left edge,
   so their centring math (built for the voice-first row) would no longer line up with it anyway.
   Folded into each state selector rather than a separate `!important` override, so nothing has to
   out-specify a 2-ID `:has()` rule to win. */

/* Listening: RECEIVING. One wave crosses the whole row left to right, through `--sweep-i` — the
   near opposite of speaking's centre-out timing just below, which is the point: a guest should
   tell the two apart without reading the label. Deliberately calmer everywhere else too — thinner
   bars, a flatter taper (so no single bar dominates), and a shallow ~20% pulse — because
   "listening" is Atithi holding still to hear, not performing.

   A second, quieter cue rides along on the mic itself: `.orb-ring` is otherwise idle-only (glass.css
   switches idle's OWN use of it off, just above), so it is free here rather than a new element —
   TWO of it, in fact (index.html's `.orb-ring--2`), staggered half a cycle apart. A single ring
   spends most of its time near its faded ends and only briefly near its visible peak, which read
   as "not really there" rather than calm — the same construction Siri/Assistant use for a
   listening cue, so at nearly any instant one of the two rings is in its visible half.

   `--violet-lit` rather than `--orb-cue-soft`: the cue token resolves to near-white on the shipped
   indigo disc (glass.css's own "two colours, not three" rule for the disc's OTHER cues), which
   read as barely-there here. `--violet-lit` is the same soft indigo/lavender already doing the
   waveform's fill (`--violet-deep`) and glow (`--violet-glow`) a few lines up — an existing-palette
   colour, just a step lighter so a thin line of it actually shows against the dark disc. Opacity
   is carried on the element (the keyframe), not the colour's own alpha, so the 0.55 peak below is
   the true peak, not a colour swatch guess. */
#orb[data-state="listening"] .orb-ring {
  border-color: var(--violet-lit);
  animation: orb-listen-ripple var(--orb-pulse-dur) var(--ease-out-quart) infinite;
}

/* The stagger. Every other property (colour, keyframe, duration) comes from the shared rule just
   above — `.orb-ring--2` still matches `.orb-ring` — so this is the ONE thing that makes it a
   second, offset ring rather than a duplicate sitting exactly on the first. */
#orb[data-state="listening"] .orb-ring--2 {
  animation-delay: calc(var(--orb-pulse-dur) / -2);
}

#app[data-compose="closed"] #orb-stage:has(#orb[data-state="listening"]) .orb-flank,
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="listening"]) .flank-bars {
  opacity: 1;
}

/* Thinner than the shared default (0.055), and a flatter taper than speaking's steep one — 46/58
   /70/82% against speaking's 34/58/82/100. Calm reads as understated proportions, not just slow
   motion. The cool cue is stated here rather than inherited from `.flank-bars > span`, which is
   also what speaking overrides — the shared rule's own colour is now only a fallback for any state
   that shows bars without naming a cue. */
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="listening"]) .flank-bars > span {
  width: calc(var(--orb-size) * 0.042);
  background: var(--orb-cue-listen);
  filter: drop-shadow(0 0 5px var(--orb-cue-listen-glow));
}

#app[data-compose="closed"] #orb-stage:has(#orb[data-state="listening"]) .orb-flank--left .flank-bars > span:nth-child(1),
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="listening"]) .orb-flank--right .flank-bars > span:nth-child(4) { height: 46%; }
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="listening"]) .orb-flank--left .flank-bars > span:nth-child(2),
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="listening"]) .orb-flank--right .flank-bars > span:nth-child(3) { height: 58%; }
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="listening"]) .orb-flank--left .flank-bars > span:nth-child(3),
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="listening"]) .orb-flank--right .flank-bars > span:nth-child(2) { height: 70%; }
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="listening"]) .orb-flank--left .flank-bars > span:nth-child(4),
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="listening"]) .orb-flank--right .flank-bars > span:nth-child(1) { height: 82%; }

#app[data-compose="closed"] #orb-stage:has(#orb[data-state="listening"]) .flank-bars > span {
  animation: orb-bar-sweep 900ms var(--ease) infinite;
  animation-delay: calc(var(--sweep-i) * 95ms);
}

/* Understanding: the bars settle into quiet dots either side — same object, softer pulse, so
   "parsing the request" reads calmer than "receiving" it. */
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="understanding"]) .orb-flank,
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="understanding"]) .flank-dots {
  opacity: 1;
}

#app[data-compose="closed"] #orb-stage:has(#orb[data-state="understanding"]) .flank-dots > span {
  animation: orb-dot-settle 1100ms var(--ease) infinite;
  animation-delay: calc(var(--dot-i) * 180ms);
}

/* Thinking — a calm incomplete orbit around the button: a faint static track plus one comet arc,
   never a full rotating ring and never a spinner. Slow (3.4s/turn) on purpose. Its own geometry
   already separates it from listening and speaking, which draw bars rather than a ring, so it keeps
   the palette's default comet tint. */
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="thinking"]) .orb-halo {
  opacity: 1;
}

#app[data-compose="closed"] #orb-stage:has(#orb[data-state="thinking"]) .orbit-comet {
  animation: orb-orbit-rotate 3.4s linear infinite;
}

/* Connecting — its own cue, where it used to be a second selector on thinking's rules above.
   The argument for sharing was that "Atithi is working on it" is one idea whether a socket is
   opening or a request is being reasoned about. It is not one idea to the guest it happens to:
   thinking follows something they said and is worth waiting through, while connecting is the app
   still getting ready and is the state where a stall means "tap it again" rather than "be patient".
   Rendering them identically meant the mic had no way to say which was happening.

   So: a gathering pulse, drawn inward rather than travelling around. Same halo element, no new DOM
   and no second arc — only the keyframe and the tint differ. The tint is deliberately the quietest
   thing on the disc, a desaturated step toward the faint ink, because connecting is the one state
   that is not about the guest's request at all. It also keeps its own dim/desaturated disc rule
   further up this file, which is what it always had. */
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="connecting"]) .orb-halo {
  opacity: 1;
  --comet-head: color-mix(in srgb, var(--violet) 55%, var(--ink-faint));
  --comet-tail: color-mix(in srgb, var(--violet) 25%, transparent);
  --comet-glow: transparent;
}

#app[data-compose="closed"] #orb-stage:has(#orb[data-state="connecting"]) .orbit-comet {
  animation: orb-gather 1.6s var(--ease-out-quart) infinite;
}

/* Speaking: TRANSMITTING. Same bars, same colour, same button — the family stays one system — but
   everything about the motion says "outputting" instead of "receiving": the width and taper are
   the shared default above (steeper — 34/58/82/100 — so the inner bars stand up taller against
   the shorter outer ones, more pronounced than listening's flattened profile), the timing runs
   from the mic OUTWARDS on both sides via `--flank-i` (centre-driven, the opposite of listening's
   one-way `--sweep-i`), and the pulse itself is a richer, fuller swing than a plain scaleY sweep —
   see `orb-bar-pulse-speak` below. */
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="speaking"]) .orb-flank,
#app[data-compose="closed"] #orb-stage:has(#orb[data-state="speaking"]) .flank-bars {
  opacity: 1;
}

#app[data-compose="closed"] #orb-stage:has(#orb[data-state="speaking"]) .flank-bars > span {
  animation: orb-bar-pulse-speak 620ms var(--ease) infinite;
  animation-delay: calc(var(--flank-i) * 90ms);
  /* The warm half of the pair — see PER-STATE CUE COLOUR at the top of this file. This is the one
     change that makes speaking legible against listening without watching the motion: the taper
     and centre-out timing above are unchanged, they simply no longer have to carry the distinction
     alone. */
  background: var(--orb-cue-speak);
  filter: drop-shadow(0 0 5px var(--orb-cue-speak-glow));
}

/* Error — an ember-tinted disc; the label carries the reason. */
#orb[data-state="error"] .orb-core {
  background: radial-gradient(128% 128% at 32% 24%, #fff, color-mix(in srgb, var(--ember) 22%, #fff) 70%);
  box-shadow: 0 16px 34px -12px color-mix(in srgb, var(--ember) 45%, transparent),
    inset 0 2px 2px rgba(255, 255, 255, 0.9);
}

#orb[data-state="error"] .orb-glyph {
  color: var(--ember);
}

#orb[data-state="error"] #orb-label {
  color: var(--ember);
}

/* Disabled — no guest link / no worklet. Greyed, no motion, not clickable. */
#orb[data-state="disabled"] {
  cursor: default;
}

#orb[data-state="disabled"] .orb-core {
  filter: grayscale(0.7);
  opacity: 0.55;
  box-shadow: 0 8px 20px -12px rgba(18, 41, 45, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.8);
  animation: none;
}

#orb[data-state="disabled"] .orb-glyph {
  opacity: 0.6;
  color: var(--ink-faint);
}

/* Acting — the "voice that acts" moment: a single one-shot ripple as a grounded card lands. */
#orb.acting::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid var(--orb-cue);
  animation: orb-ripple 620ms var(--ease-out-quart) 1;
  pointer-events: none;
  z-index: 2;
}

/* ---------------------------------------------------------------------------------------------
 * Keyframes. Kept few and calm.
 * ------------------------------------------------------------------------------------------- */

@keyframes orb-breath {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.035); }
}

@keyframes orb-invite {
  0% { opacity: 0.5; transform: scale(0.98); }
  70%, 100% { opacity: 0; transform: scale(1.14); }
}

/* Listening's ripple — a sonar-ping shape rather than idle's own smooth fade-out (`orb-invite`,
   just above): it starts AT the disc's own edge, invisible, rises to a genuinely visible 0.55 by a
   fifth of the way in, HOLDS in the 0.3-0.55 band for the next half of the cycle (rather than
   immediately fading, which is what made the first version read as a single brief flash), then
   expands the rest of the way to ~11px past the disc (0.72 × --orb-size's own 36px radius on a
   72px mic — inside the 10-15px brief) as it finally fades. The border itself stays the base
   rule's constant 2px throughout — only opacity and scale move here. Run twice, staggered (the
   state rule above), so one of the two is always somewhere in that long visible middle. */
@keyframes orb-listen-ripple {
  0% { opacity: 0; transform: scale(1); }
  20% { opacity: 0.55; transform: scale(1.08); }
  65% { opacity: 0.3; transform: scale(1.22); }
  100% { opacity: 0; transform: scale(1.32); }
}

@keyframes orb-ripple {
  0% { opacity: 0.55; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.5); }
}

/* Listening's pulse — scaleY only (composites, never triggers layout), and shallow: ~20% of a
   bar's own height either side of rest. Paired with the flatter taper and the `--sweep-i` delay
   above, this is what keeps listening reading as calm rather than merely slow. */
@keyframes orb-bar-sweep {
  0%, 100% { transform: scaleY(0.82); }
  50% { transform: scaleY(1); }
}

/* Speaking's pulse — a fuller swing (roughly double listening's amplitude) with a light opacity
   lift at the peak, so a bar visibly brightens as it rises rather than just stretching. That
   opacity component is the "richer" half of the brief; the amplitude is the "stronger" half. */
@keyframes orb-bar-pulse-speak {
  0%, 100% { transform: scaleY(0.5); opacity: 0.85; }
  50% { transform: scaleY(1.08); opacity: 1; }
}

/* Understanding's dots — a quiet settle rather than listening/speaking's sweep: opacity and a 2px
   lift, staggered per dot (flank-dots > span's --dot-i, above). Ends where it starts, so the single
   reduced-motion iteration lands on a calm, evenly-dim rest look for both dots at once. */
@keyframes orb-dot-settle {
  0%, 100% { opacity: 0.5; transform: translateY(0) scale(0.88); }
  42% { opacity: 0.95; transform: translateY(-2px) scale(1); }
}

/* Thinking's comet — a plain rotation. The shape stays an incomplete arc throughout (the conic
   gradient on .orbit-comet::before fades to transparent well before 360deg), so a full turn never
   reads as a closed spinner ring. */
@keyframes orb-orbit-rotate {
  to { transform: rotate(360deg); }
}

/* Connecting's gathering pulse. It draws INWARD — starting a little wide and faint, closing onto
   the disc — which is the opposite reading to thinking's arc travelling around it, and that is the
   point: one is "reaching for something", the other is "turning something over". A slow quarter
   rotation rides along so the arc is not a static shape being scaled, without ever looking like
   the full 360 that thinking owns. */
@keyframes orb-gather {
  0% {
    transform: scale(1.14) rotate(0deg);
    opacity: 0.15;
  }
  55% {
    transform: scale(1) rotate(60deg);
    opacity: 0.85;
  }
  100% {
    transform: scale(0.94) rotate(90deg);
    opacity: 0;
  }
}

/* ---------------------------------------------------------------------------------------------
 * The capability lane — the SKELETON only. This file owns the horizontal-scroll mechanics (the
 * lane it is in ACTIVE mode); css/glass.css owns the card material and the welcome grid.
 * ------------------------------------------------------------------------------------------- */

#cap-rail {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: nowrap;
  gap: var(--sp-2);
  align-items: stretch;
  overflow-x: auto;
  /* Horizontally only, stated rather than inferred: an element cannot be `visible` on one axis and
     clipped on the other, so leaving this to compute made a single-row lane one stray pixel away
     from scrolling vertically too. */
  overflow-y: hidden;
  /* Trailing room so the last card comes to rest clear of the fade below, instead of ending flush
     against the pane border where it reads as clipped rather than as scrolled. */
  padding: var(--sp-2) var(--sp-6) var(--sp-2) var(--sp-4);
  scrollbar-width: none;
  /* It is a scroll lane carrying eight-plus cards in a 480px column, so it has to *look* like one.
     The right edge dissolves instead of guillotining a card mid-word — which is what made "Nearby"
     read as a rendering bug — and the cards snap. The left edge is left crisp on purpose: a fade
     there would lie about there being something before the first card whenever the lane is at rest. */
  -webkit-mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - var(--sp-6)), transparent 100%);
  mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - var(--sp-6)), transparent 100%);
  scroll-snap-type: x proximity;
  scroll-padding-inline-start: var(--sp-4);
  scroll-behavior: smooth;
}

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

#cap-rail:empty {
  display: none;
}

.cap-pill {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  scroll-snap-align: start;
  min-height: var(--tap); /* the touch floor, in the lane a thumb uses most */
  font: inherit;
  cursor: pointer;
  text-align: start;
  transition: background var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}

.cap-pill:not(:disabled):active {
  transform: scale(0.98);
}

.cap-pill:disabled {
  opacity: 0.45;
  cursor: default;
  box-shadow: none;
}

/* =============================================================================================
 * THE DOCK — the one part of the screen a thumb actually lives in.
 *
 * ONE ROW, three zones, in reading order:
 *
 *     [ MIC ]  [ Type your message…                    ➤ ]  [ HELP ]
 *
 * and one quiet line of reassurance under it. That is the whole foot of the app.
 *
 * ## What changed, and why the old arrangement had to go
 * The previous dock was [keyboard-toggle] [MIC, absolutely centred] [safety], with the composer
 * COLLAPSED behind the toggle. Three problems, and the third is the one that decided it:
 *
 *  1. The screen showed one way in and hid the other. A guest who would rather type had to find a
 *     "Type instead" pill first — on the surface whose whole promise is "ask me anything".
 *  2. Opening the composer hid the capability lane (there was a rule for it), so choosing to type
 *     cost the guest every suggestion on the screen.
 *  3. Absolute centring cannot hold a field. The mic was centred on the bar and its neighbours
 *     capped at `calc(50% - --orb-size/2 - gap)`, which is a layout for two labels either side of a
 *     fixed object. With a growing composer between them the arithmetic has nothing to solve for.
 *
 * So the mic joins the flow as the FIRST zone, the composer takes the slack, and safety anchors the
 * end. Three flex children with one `gap`: the row cannot drift with locale length, because nothing
 * in it is positioned against anything else.
 *
 * ## What is unchanged, deliberately
 *  · The 48px touch floor on every control, and the deliberate gap between the composer and safety:
 *    activating safety sends an intent to the property, so a slipped thumb must not reach it.
 *  · Safety is never disabled — js/app.js excludes it from setInputsEnabled on purpose.
 *  · The mic's state machine, its five visual states and every string. Only its size and its
 *    position in the row are this file's business.
 * ============================================================================================= */

#composer-dock {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--sp-1);
  /* --sp-1, both sides — back to the fully compacted value. It briefly went up to --sp-6 to
   * reserve clearance for #orb-label (which floats ABOVE #dock-bar, out of flow) against
   * #cap-panel sitting flush against the dock with zero clearance of its own whenever the
   * transcript was empty. That conflict is gone at the source now: #cap-panel is hidden outright
   * for every state #orb-label actually has something to say for (css/glass.css, SUGGESTIONS STEP
   * ASIDE), so the two can no longer occupy the same moment, and the reservation this comment used
   * to justify would just be unused air above the mic the rest of the time. What clearance the
   * label still needs against real transcript content it gets from `#log`'s own bottom padding
   * (--sp-5, chat.css) — a guest is never scrolled with the last message flush against the dock
   * either. */
  padding-top: var(--sp-1);
  padding-bottom: calc(var(--sp-1) + env(safe-area-inset-bottom));
  /* The fill, the blur and the top hairline are css/glass.css's — it owns every surface. */
}

#dock-bar {
  display: flex;
  align-items: center;
  /* --sp-4/--sp-3, up from --sp-3/--sp-2. The two flanking controls grew to 54px (--dock-ctl) and
     they are the row's outermost objects, so the inset is what keeps them from reading as pinned to
     the screen edge — and on a phone the edge is where a palm rests. The narrow-pane tier below
     gives some of it back, which is the only place this row has ever had to negotiate. */
  gap: var(--sp-3);
  padding-inline: var(--sp-4);
}

/* The mic — zone one, and the only bold object in the dock. */
#orb-stage {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
}

/* The composer — zone two, and the only zone that flexes. `min-width: 0` is load-bearing: a flex
   item's default `min-width: auto` is its content's intrinsic minimum, and a textarea's is wide
   enough to push safety off a 320px screen. */
#composer {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  /* chat.css dresses this as a docked band of its own (padding + --paper fill + a bottom safe-area
     inset). Inside the bar it is just a column holding the field and the counter. */
  padding: 0;
  background: none;
}

#composer-row {
  display: flex;
  align-items: flex-end;
}

/*
 * Safety — zone three. A ringed circle with its word beneath it, the same shape the mic is and the
 * same shape the reference gives it: filled circle = primary, ringed circle = secondary, and a guest
 * reads the pair as one family rather than as a sphere next to a box.
 *
 * The BUTTON is bare — no fill, no border, no shadow. Those live on the circle inside it (dressed in
 * css/glass.css), so the word sits outside the ring where there is room for it. A 48px pill holding
 * both a shield and "Help" is a pill with a 9px label crushed against its edge.
 *
 * The touch target is the whole 48x~53 column, which clears both floors. The full
 * "Help · I feel unsafe" is always the button's aria-label (js/app.js) — what a screen reader
 * announces does not depend on how much room the word had.
 */
.help-btn {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  /* --dock-ctl (54px), not --tap (48px): this is the dock's flanking-control size, shared with the
     type button at the other end of the row, and it is deliberately ABOVE the touch floor rather
     than exactly on it. See the token's own note in css/glass.css. --tap remains the floor every
     other control in the app is held to. */
  width: calc(var(--dock-ctl) * var(--type-scale));
  min-height: calc(var(--dock-ctl) * var(--type-scale));
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  /* 10.5px, up from 9.5. The one visible word on the one control a guest reaches for when something
     is wrong should not be the smallest type on the screen. */
  font-size: calc(10.5px * var(--type-scale));
  font-weight: 600;
  letter-spacing: 0.03em;
  line-height: 1.15;
  white-space: nowrap;
  cursor: pointer;
}

/* The ring. `box-sizing: border-box` so the padding sets the glyph and the width sets the shape —
   the size and the squircle radius are css/glass.css's (THE DOCK), which owns every surface; what
   stays here is the box model and the transition. */
.help-btn .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));
  border-radius: 50%;
  /* The transition this comment above still refers to is GONE, deliberately: both properties
     resolve through custom properties css/glass.css swaps per theme, and Chromium freezes a
     transitioned property that does so at the value it started from — measured painting the LIGHT
     cream `rgba(255, 250, 244, 0.96)` on this control in dark mode. See the long note on
     `.cap-pill` in css/glass.css. The hover is a state change and still reads instantly. */
}

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

/* One visible half of the label, at every width: the short "Help" under the shield. The long half is
   what the aria-label carries. `:has()` is the guard for a locale whose string has no "·" — there
   .help-word is empty and .help-phrase holds the whole label, so hiding the phrase unconditionally
   would leave a wordless pill. */
.help-label {
  display: inline-flex;
  align-items: baseline;
  gap: 0.34em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

.help-label:has(.help-word:not(:empty)) .help-phrase,
.help-label:has(.help-word:not(:empty)) .help-sep {
  display: none;
}

.help-word:empty {
  display: none;
}

/*
 * The text/voice disclosure is BACK, and it is the first control in the row.
 *
 * It was `display: none` here for one release, while the dock showed both inputs at once. That dock
 * gave the mic and the field equal weight, which on a product whose primary interaction is voice made
 * the mic the smaller of the two objects in the row. The resting state is voice-first again — see THE
 * DOCK'S TWO STATES in css/glass.css, which owns the arrangement and the button's surface.
 *
 * Nothing about the element or its handler changed across either revision; only whether it was
 * reachable. This is why it was kept rather than deleted.
 */

/*
 * The starter chips, retired from the dock for the same reason — with one more.
 *
 * They were three concrete typed questions ("Is NH10 open?") in a lane directly under the
 * capability lane. That is TWO suggestion lanes stacked above the keyboard, spending ~120px of a
 * phone twice on one job, and the brief this phase implements is explicit that there is exactly one
 * suggestion lane and it is not at the bottom of the screen. #cap-rail is that lane, and its
 * "Permits" / "Roads & weather" / "Rooms" cards ask the same questions these chips did.
 *
 * js/render.js still renders them into #chips on every language change — nothing about that path
 * changed, so restoring the lane is one line if it is ever wanted back.
 */
#chips {
  display: none;
}

/* The privacy line. One line, quiet, centred, under the controls. */
#dock-privacy {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  margin: 0;
  padding: 0 var(--sp-4);
  font-size: calc(10.5px * var(--type-scale));
  color: var(--ink-faint);
  letter-spacing: 0.01em;
}

/* ---------------------------------------------------------------------------------------------
 * NARROW COLUMN — keyed to #chat-pane (a `pane` query container, see layout.css), not the viewport.
 *
 * A media query here asks about the screen, and nearly every decision in this file is about the
 * COLUMN: the pane is a 460-480px box at every viewport from 720px up, so `@media (max-width: 820px)`
 * both never fired on a laptop that needed it and fired on a phone where it was wrong.
 *
 * There is very little left in this ladder, and that is the point of the row above it. The three
 * zones are `[fixed] [flexible] [fixed]` with one gap, so the composer absorbs every width the pane
 * has and nothing has to be given up — the old dock spent four @container tiers hiding labels and
 * shaving padding because it was solving `calc(50% - --orb-size/2 - gap)` for a label.
 * ------------------------------------------------------------------------------------------- */

/* The tightest panes we ship to (a 320px SE, a ~344px folded cover screen). The bar gives back 4px of
   its inset and 2px of each gap — 12px to the field, which is the flexible zone. Nothing else gives
   way: measured at 320, that leaves [52 mic][192 field][48 safety] with 125px of typing room and the
   placeholder fitting whole, so there was no case for shrinking a control.

   Deliberately NOT an `--orb-size` override. css/glass.css declares the mic's size per MODE
   (`#app[data-mode="welcome"]`, two IDs) and loads after this file, so a single-ID `#app` rule here
   cannot win — an earlier revision of this ladder carried exactly that override and it had been dead
   since the day glass.css took over the sizing. If the mic ever does need to shrink on a narrow pane,
   it belongs beside the mode sizes in glass.css. */
@container pane (max-width: 359px) {
  #dock-bar {
    padding-inline: var(--sp-2);
    gap: 6px;
  }
}

/*
 * Stage/presentation mode multiplies every control by 1.45. The composer is the flexible zone, so
 * the row still holds — it just has less to give the field, and the privacy line is the first thing
 * that is not worth its space on a projector.
 */
:root[data-stage="1"] #dock-privacy {
  display: none;
}
