/* Front Desk — the voice console on /labs/front-desk.
   Ninth in the numbered labs cascade; the ordinal prefix IS the load order, since
   stylesheet_link_tag :app serves these sorted by path. Keep comment delimiters balanced:
   a comment that closes early leaks prose into the next rule and silently eats it.
   Pinned by test/assets/labs_stylesheets_test.rb. */

/* .lp-head's 40px of bottom padding separates a heading from the prose that follows it on every
   other lab page. Here the orb follows, and it carries its own visual air — a blurred mesh inset
   inside a 208px box — so the measured padding on top of that read as a gap rather than as
   spacing. Scoped to this page's head; the shared rule is untouched. */
.fd-head { padding-bottom: 0; }

/* .lp-subtitle's shared 56ch measure is tuned for every other lab's shorter one- or two-clause
   line; this page's subtitle names four separate capabilities and wrapped to four lines at that
   width, with the last line carrying a single orphaned word. Widened only here, on the head, so
   every other lab page's subtitle keeps the shared 56ch. */
.fd-head .lp-subtitle { max-width: 74ch; }

/* ============================================================
   The orb — the page's subject.

   COLOUR EXCEPTION, deliberate and signed off. The site is monochrome; the one prior departure
   is the soft radial wash on the thesis triad and proof cards (see CLAUDE.md), plus the
   three-hue border ring on .hero-cta-secondary. This uses that same technique — color-mix over
   var(--bg) — tinted to Front Desk's OWN stage hue, so the colour already means "Front Desk"
   in the catalogue and in this page's eyebrow. No new hue enters the system.

   Every value that shapes it is a custom property below, so retuning is arithmetic.
   ============================================================ */
.fd-stage {
  --fd-orb-size: clamp(126px, 18.7vw, 177px);
  --fd-amp: 0;              /* 0-1, written by the controller on each volume-level event */
  /* The aurora's three dials. --fd-period is the under current's resting cycle (rotation during
     a call, the idle drift at rest) — near-still by design on the overlay: at 90s the motion is
     an event you catch, not a loop you watch. The over current always runs at 1.3x this, in the
     other direction, so the two can never sync into a static pattern. --fd-deep is the third
     gold step — a darkness of the existing ink, not a new hue. */
  --fd-period: 90s;
  --fd-deep: color-mix(in srgb, var(--stage-ink, #854D0E) 72%, #000);

  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  /* A small top margin, and only a small one. .fd-head already zeroes .lp-head's own 40px of
     padding below the subtitle, and the orb's blurred mesh carries visual air inside its own box,
     so anything measured on top of both reads as a hole. Every pixel here also pushes the
     transcript further below the fold during a call, which is what actually costs something. */
  margin: clamp(10px, 1.4vw, 20px) 0 clamp(10px, 1.6vw, 18px);
}

/* A <button>, so the reset comes first: it is a control, but nothing about it should look like
   one — the mesh and ring below are the entire affordance. */
.fd-orb {
  position: relative;
  width: var(--fd-orb-size);
  height: var(--fd-orb-size);
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: none;
  border-radius: 50%;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

/* It is aria-hidden and out of the tab order, so it can never take focus by keyboard; this only
   suppresses the ring a mouse press would otherwise leave behind on Safari and Firefox. */
.fd-orb:focus { outline: none; }

/* The lab page's decorative orb wrapper — the same shape as .fd-band-orb (application.css): the
   button reset lives on the WRAPPER, which carries the toggle action, while the .fd-orb inside
   stays a span with no target. Pressing it opens the unified overlay. */
.fd-page-orb {
  /* The decorative orbs run warmer than the overlay's: they are shown BEFORE a call, inviting
     one, so their resting drift cycles in roughly a quarter of the overlay's 90s period — motion
     you notice within a glance, not one you have to wait for (40s tested as too still; 24s is
     the pace the molten-drift mockup was approved at). Inherits down to the layers; the band's
     twin lives on .fd-band-stage in application.css. */
  --fd-period: 24s;
  /* SAME SIZE AS THE BAND'S, declared here rather than on .fd-stage because the overlay's stage
     is `.fd-stage .fd-ov-stage` and must keep the larger clamp — this page's orb sat at the
     stage default (up to 177px) and read as a different object from the homepage's 158px one.
     Values mirror .fd-band-stage in application.css exactly, 96px step included: change one and
     change the other. */
  --fd-orb-size: clamp(112px, 13vw, 158px);
  display: flex;
  justify-content: center;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: transform 140ms ease;
}

.fd-page-orb:hover { transform: translateY(-1px); }
/* aria-hidden and out of the tab order, same as .fd-band-orb — after a press the button holds
   focus for the length of the call, and the global :focus-visible ring would reveal a square box
   around a circle the moment the overlay closes. */
.fd-page-orb:focus { outline: none; }

@media (prefers-reduced-motion: reduce) {
  .fd-page-orb:hover { transform: none; }
}

/* The band steps its orb to 96px at 620px; this page's follows at the same width. */
@media (max-width: 620px) {
  .fd-page-orb { --fd-orb-size: 96px; }
}

/* Hover belongs on the ring alone — brightening the mesh would read as a state change, and the
   mesh's opacity is how connecting/speaking/listening are told apart. */
.fd-orb:hover .fd-orb-ring { border-color: var(--stage-ink, #854D0E); }

/* Two counter-rotating conic currents in the stage's gold family — the "Ember" aurora. Each is
   blurred so its edges dissolve into the other, and each fades itself out toward the rim through
   a radial MASK rather than a painted veil: a veil would have to name a ground colour, and the
   three surfaces do not share one (the homepage band sits on a 2.5% gold wash, not --bg).

   The currents never scale; only the ring does, so the surface cannot appear to pump. The only
   properties that ever animate on them are transform and opacity — the blur is static per layer,
   rasterised once — which is what keeps the whole orb on the compositor on low-end devices.
   test/assets/front_desk_orb_test.rb pins that. */
.fd-orb-under,
.fd-orb-over,
.fd-orb-ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
}

.fd-orb-under,
.fd-orb-over {
  -webkit-mask-image: radial-gradient(closest-side, #000 64%, transparent 100%);
  mask-image: radial-gradient(closest-side, #000 64%, transparent 100%);
  will-change: transform;
  transition: opacity 400ms ease;
}

/* The mixes, blurs and mask above were retuned 2026-08-17 ("stoked ember", picked from a
   design-options pass): the first cut peaked at 31% gold behind 13-18px of blur behind a mask
   fading from 55% out, and most of the pigment never reached the eye — the band's orb read as a
   pale wash. Roughly doubled presence, same hues, same layers. */
.fd-orb-under {
  inset: -6%;
  background: conic-gradient(from 210deg,
    transparent 0deg,
    color-mix(in srgb, var(--stage, #EAB308) 48%, transparent) 80deg,
    color-mix(in srgb, var(--stage-ink, #854D0E) 50%, transparent) 160deg,
    transparent 240deg,
    color-mix(in srgb, var(--stage, #EAB308) 30%, transparent) 310deg,
    transparent 360deg);
  filter: blur(13px);
  animation: fd-rotate var(--fd-period) linear infinite;
}

.fd-orb-over {
  inset: 8%;
  background: conic-gradient(from 40deg,
    transparent 0deg,
    color-mix(in srgb, var(--stage, #EAB308) 60%, transparent) 120deg,
    transparent 210deg,
    color-mix(in srgb, var(--fd-deep) 44%, transparent) 300deg,
    transparent 360deg);
  filter: blur(9px);
  animation: fd-rotate-rev calc(var(--fd-period) * 1.3) linear infinite;
}

/* The hairline edge, and the only thing that carries amplitude. The transition is what turns
   discrete volume events into continuous motion instead of steps. */
.fd-orb-ring {
  border: 1px solid var(--line);
  transform: scale(calc(1 + var(--fd-amp) * 0.14));
  transition: transform 90ms ease-out, border-color 300ms ease;
}

/* ---- states ----
   Every state is told by rotation speed and opacity, never by a keyframed filter, so
   reduced-motion can kill the animation and leave every state still legible.

   Idle DRIFTS rather than rotating in place ("molten drift", 2026-08-17, picked from a
   design-options pass): a soft conic turning around its own centre is nearly invisible — the
   pattern looks the same at every angle, which is why the first idle read as static however the
   period was tuned. What the eye tracks is the gradient's CENTRE moving, so the drift keyframes
   walk each layer's bright lobes off-axis while they turn. Rotation advances a quarter-turn per
   keyframe stop under linear timing, so the spin stays uniform while the translate wanders — ONE
   combined transform timeline per layer, because two simultaneous animations cannot share the
   transform property and the individual translate/rotate properties would fail
   front_desk_orb_test's pinned whitelist.

   The 6s breathe rides on top, half a phase apart per layer, so the surface shimmers rather than
   pumps. This is for the DECORATIVE orbs: the band and the lab page show idle before a call, and
   an inert disc there reads as a logo rather than something you can talk to. The overlay's orb
   wears idle only while hidden (openOverlay goes straight to connecting), so the near-still rule
   the overlay lives by is untouched. */
.fd-orb[data-state="idle"] .fd-orb-under {
  animation: fd-drift var(--fd-period) linear infinite,
             fd-breathe 6s ease-in-out infinite;
}
.fd-orb[data-state="idle"] .fd-orb-over {
  animation: fd-drift-rev calc(var(--fd-period) * 1.3) linear infinite,
             fd-breathe 6s ease-in-out -3s infinite;
}

/* Connecting: the currents spin up AND beat at 1.2s — fd-pulse's period is the SAME interval as
   the connecting pulse in front_desk_audio.js (pulseEvery: 1200), so the sound and the picture
   are one rhythm rather than two things happening at once. Change one and change the other —
   test/assets/front_desk_orb_test.rb pins the pair. The rotation periods stay whole multiples
   of 1.2s so the spin never drifts against the beat. */
.fd-orb[data-state="connecting"] .fd-orb-under {
  animation: fd-rotate 4.8s linear infinite, fd-pulse 1.2s ease-in-out infinite;
}
.fd-orb[data-state="connecting"] .fd-orb-over {
  animation: fd-rotate-rev 6s linear infinite, fd-pulse 1.2s ease-in-out infinite;
}
.fd-orb[data-state="connecting"] .fd-orb-ring { border-color: var(--stage-ink, #854D0E); }

/* Speaking: the currents circulate at twice the resting pace while the ring rides the voice. */
.fd-orb[data-state="speaking"] .fd-orb-under { animation-duration: calc(var(--fd-period) / 2); }
.fd-orb[data-state="speaking"] .fd-orb-over  { animation-duration: calc(var(--fd-period) * 0.65); }
.fd-orb[data-state="speaking"] .fd-orb-ring  { border-color: var(--stage-ink, #854D0E); }

/* Listening is a state, not a measurement: the SDK reports the ASSISTANT's level, never yours.
   So this dims and rests rather than pretending to react. */
.fd-orb[data-state="listening"] .fd-orb-under,
.fd-orb[data-state="listening"] .fd-orb-over { opacity: 0.55; }

/* A failure must never look like a live call. All hue gone, all motion stopped. The filter here
   is a static declaration, not an animation — it changes once, on entering the state. */
.fd-orb[data-state="error"] .fd-orb-under,
.fd-orb[data-state="error"] .fd-orb-over {
  animation: none;
  filter: blur(14px) grayscale(1);
  opacity: 0.3;
}

@keyframes fd-rotate     { to { transform: rotate(360deg); } }
@keyframes fd-rotate-rev { to { transform: rotate(-360deg); } }

/* The idle wander (see the idle comment above for why this exists and why it is one combined
   timeline). Transform only. The translate percentages are of the LAYER's own size, small enough
   that the mask still contains every lobe at full drift. */
@keyframes fd-drift {
  0%   { transform: rotate(0deg)   translate(0, 0); }
  25%  { transform: rotate(90deg)  translate(5%, -4%); }
  50%  { transform: rotate(180deg) translate(-4%, 5%); }
  75%  { transform: rotate(270deg) translate(4%, 4%); }
  100% { transform: rotate(360deg) translate(0, 0); }
}
@keyframes fd-drift-rev {
  0%   { transform: rotate(0deg)    translate(0, 0); }
  25%  { transform: rotate(-90deg)  translate(-5%, 3%); }
  50%  { transform: rotate(-180deg) translate(5%, -3%); }
  75%  { transform: rotate(-270deg) translate(-3%, -5%); }
  100% { transform: rotate(-360deg) translate(0, 0); }
}

/* Opacity only, same discipline as fd-pulse. The floor sits below the pulse's 0.45 deliberately:
   with the stoked-ember mixes above, a 0.55 floor read as no swell at all, and the half-phase
   offset between the layers keeps the deeper wave a shimmer, not a blink. */
@keyframes fd-breathe {
  0%, 100% { opacity: 0.4; }
  50%      { opacity: 1; }
}

/* Opacity only — no filter, no transform — so the connecting beat stays compositor-cheap and
   the static per-layer blur is never re-rasterised mid-pulse. */
@keyframes fd-pulse {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 1; }
}

/* Quieter than .fd-button was: the orb is the thing you look at now, so the control under it
   should not compete with it. */
/* The ring is the same three-hue gradient .hero-cta-secondary carries, by the same two-layer
   background technique: page colour on padding-box, gradient on border-box, so only the border
   ring takes colour and the pill's rounded corner stays clean. A plain `border-image` cannot do
   this — it does not follow border-radius — and a mask-composite ring would erase the label with
   the fill, since a mask applies to the element's text too.

   --fd-call-fill is what keeps "same background as before" literally true. The button was
   `background: none`, so it showed whatever ground it sat on, and its three surfaces do not share
   one: the lab page and Monday Brief sit on --bg, but the homepage band sits on a 2.5% wash of the
   stage hue. The padding-box layer needs an opaque colour, so each ground declares its own fill
   beside itself (see .fd-band in application.css) rather than this file guessing.

   --fd-call-ring is the second layer, kept swappable even though every current .fd-call wears the
   default gradient — a future control that needs a flat colour swaps the property rather than
   rebuilding the two-layer background. NOTE a flat colour still has to be written as a gradient:
   background layers are images, and a bare background-color is always the bottom layer, so it
   cannot take part in border-box. */
.fd-call {
  --fd-call-fill: var(--bg);
  --fd-call-ring: linear-gradient(120deg, #FF6F5E, #22B8CF, #8B5CF6);
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.04em;
  /* No text-transform: the label's casing is authored in the view, so "Use voice mode" can carry
     its capital. A lowercase transform here would silently overrule it and the markup would look
     wrong to anyone reading it. */
  padding: 11px 24px;
  border: 2.5px solid transparent;
  border-radius: 999px;
  background:
    linear-gradient(var(--fd-call-fill), var(--fd-call-fill)) padding-box,
    var(--fd-call-ring) border-box;
  color: var(--ink);
  cursor: pointer;
  transition: transform 140ms ease, filter 140ms ease;
}

/* ---- what's left of the call palette ----
   EVERY call button wears the default three-hue gradient now — the lab page's, the band's and
   Monday Brief's are all bare toggles in the same dress, so the green/red state rings (and the
   setButton that wrote the state attribute, and Monday Brief's static green) are gone: the overlay
   covers the page for the whole of a call, so a state on a page button was never visible while
   it differed from idle. The one flat call colour remaining is the overlay's end circle.

   AN ALIAS, NOT A HEX. --fd-stop is the page's existing --fd-refusal — the same coral the mute
   toggle and the microphone note show — aliased because the names mean different things: a mute
   reporting a "refusal" and a button that ENDS A CALL are not the same statement, and if they
   should ever diverge this is the one line to change. The token's shared definition is further
   down this file, beside the indicators. */
.fd-call,
.fd-ov-end { --fd-stop: var(--fd-refusal); }

/* The hover leaves the fill alone — the old invert filled the pill with ink, which on a button
   whose whole point is the coloured ring read as a different control rather than the same one
   lit up. Nothing here touches `background`, so the ring and the ground both survive.

   Saturating rather than dimming is what makes it obvious: the gradient is the only colour on
   this control, so pushing it is the loudest change available that costs no layout. The label is
   near-black and barely moves under saturate(), which is why it can be applied to the whole
   element instead of the ring alone (the ring is a background layer and cannot be filtered on its
   own). The lift is transform-only, so it never reflows the pill or its neighbours — and no
   box-shadow glow, per the design system's no-shadows rule. */
.fd-call:hover,
.fd-call:focus-visible {
  filter: saturate(1.75);
  transform: translateY(-1px);
}

/* Motion opts out; the colour cue stays, so the control is still obviously hovered. Matches the
   orb's rule above — reduced motion means fewer sensory effects, not fewer affordances. */
@media (prefers-reduced-motion: reduce) {
  .fd-call:hover,
  .fd-call:focus-visible { transform: none; }
}

.fd-call:disabled { opacity: 0.45; cursor: default; }
/* A disabled call is not hoverable — without this the lift and the saturation still fire. */
.fd-call:disabled:hover { filter: none; transform: none; }

/* The note centres under the call button on the lab page; the homepage band leaves it alone and
   stays flush left. */
.fd-stage .fd-note { align-items: center; text-align: center; }

/* No reduced-sound media query exists, so front_desk_audio.js already treats reduced-motion as
   "reduce sensory effects". The orb follows the same rule, and sound and motion opt out together.
   State stays legible through opacity and ring colour, which do not move. */
@media (prefers-reduced-motion: reduce) {
  .fd-orb-under,
  .fd-orb-over { animation: none !important; transition: none; }
  .fd-orb-ring { transform: none; transition: none; }
}

/* A text button, not a bordered one: it is a preference, not an action on the call. */
.fd-mute {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  background: none;
  border: 0;
  padding: 4px 2px;
  cursor: pointer;
  border-bottom: 1px solid transparent;
}

.fd-mute:hover { color: var(--ink); border-bottom-color: var(--line); }

/* No opacity dim any more: the cross and the refusal red carry the muted state, and fading them
   only weakened the signal they exist to send. */

/* Tick and cross, and the same two colours as the microphone row, so the block reads as one status
   stack rather than one styled row above a plain one.

   KEYED ON aria-pressed, which is the mute PREFERENCE — the one thing this row genuinely knows. It
   is not a measurement of audibility and cannot be: our mute flag gates only the three synthesised
   tones and never the assistant's voice, and a muted tab or a turned-down system is invisible to
   every browser API (verified directly — the AudioContext still reaches "running" and playback is
   never refused). So green here means "we are not silencing the tones", not "you will hear the
   agent". Read the microphone row for something measured. */
.fd-mute[aria-pressed="false"]::before { content: "\2713"; margin-right: 6px; }
.fd-mute[aria-pressed="true"]::before  { content: "\2715"; margin-right: 6px; }
.fd-mute[aria-pressed="false"] { color: var(--fd-ready); }
.fd-mute[aria-pressed="true"]  { color: var(--fd-refusal); }

/* Fixed height rather than growing the page: a call fills this over several minutes, and a
   panel that pushes the explainer downwards as it fills moves the reader's place mid-call. */
/* No border or fill: the bubbles already carry their own edges, and a box around them read as a
   second frame inside the page. Vertical padding stays so the first speaker label is not clipped
   against the scroll edge, and a little on the right so bubbles clear the scrollbar. */
/* The height is capped by the VIEWPORT as well as by a figure. A fixed 460px was taller than the
   space left below the orb on a laptop window, so the panel's own bottom sat under the fold — and
   a scroller pinned to its bottom then puts the newest line off-screen, which reads as "it stops
   scrolling" even though the panel is doing exactly what it was told. */
.fd-transcript {
  width: 100%;
  min-height: 160px;
  max-height: min(460px, calc(100vh - 220px));
  overflow-y: auto;
  padding: 14px 8px 14px 0;
}

/* The bubbles themselves come from labs_08_chat_transcript.css (.pg-chat-*), which is a shared
   component despite its prefix — Signal Line already borrows from it too. Front Desk is its third
   consumer, and builds the same markup in JS rather than through the _chat_exchange partial,
   because these arrive live rather than being rendered from a finished transcript.
   labs_08 loads before this file, so the two overrides below win at equal specificity. */

/* A live call has no "customer/agent" framing — it is you and the thing you are talking to. */
.fd-transcript .pg-chat { margin-top: 0; }

/* An interim line is text the transcriber has not committed to yet, so it is shown as provisional
   rather than settled. It firms up in place when the final message replaces it. */
.fd-bubble-interim { opacity: 0.55; }

/* .pg-chat-action-item right-aligns for the Proving Ground, where tool calls belong to the
   right-hand speaker. Here they belong to Front Desk, on the left. */
.fd-tool-chip .pg-chat-action-item { text-align: left; }

/* ---- the tool call's result, inside its chip ---- */

/* The right-hand rail that used to live here is gone — it redrew every call the inline chip had
   already shown, and the site-wide overlay never had one. Its arguments, its wall-clock time and
   its .fd-tool / .fd-tools-list / .fd-tool-json rules went with it; only the RESULT had nowhere
   else to go, so it sits in the chip's own disclosure now, under the arguments. */
.fd-tool-chip-result {
  font-family: var(--font-sans);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ink-faint);
  margin: 8px 0 0;
  overflow-wrap: break-word;
}

/* A failed tool must not read as a quiet success — in dev every call 404s until the tools deploy,
   and that needs to be visible rather than an absence. NOT a new hue: the same red .fd-note and
   .fd-note use for a refusal. */
.fd-tool-chip-result--error { color: #D3686D; }

.fd-note {
  font-size: 13px;
  color: var(--ink-faint);
  margin: 0;
}

/* The note is the pre-press status surface as well as the standing guidance — one paragraph whose
   text the controller swaps, rather than a second element revealed beside it, so the control never
   moves under the cursor at the moment it is being pressed. Same data-tone idiom as the orb's
   states, and the same red: a refusal reads identically wherever it lands.

   NOT a new hue in either direction. The refusal red is .labs-stage-ask's --stage and the ready
   green is .labs-stage-prove's --stage-ink, both already in application.css — the same two families
   the catalogue and the eyebrows read. The green takes the INK step rather than the --stage one
   because this is body text: #34D399 measures 1.9:1 on white and would be decorative at best,
   where #047857 measures 5.5:1 and clears AA. */
/* Two rows, and IDENTICAL on every surface — the homepage band, this lab page and Monday Brief all
   render the one partial and take their type from .fd-note above, so the only thing that differs
   between them is the alignment they inherit from their container (centred under the orb, flush
   left beside the band's button). Spans rather than two <p>s so the pair stays one paragraph to a
   screen reader, with only the first announcing changes. */
.fd-note-state,
.fd-note-cap { display: block; }

/* The block is a column so all three rows stack identically on every surface, and it inherits its
   alignment from whatever contains it — centred under the orb on the lab page, flush left beside
   the band's button. align-items: inherit is what carries that down to the button, which as a flex
   item would otherwise stretch the full width and put its text somewhere the rows are not. */
.fd-note {
  display: flex;
  flex-direction: column;
  align-items: inherit;
  gap: 2px;
  line-height: 1.55;
}

/* Row one, in the block rather than loose above it. The stage used to space this with .fd-stage's
   18px gap; inside the block it is one of three rows and takes the block's own rhythm.

   IT ALSO TAKES THE BLOCK'S TYPE. .fd-mute is mono at 12px with letter-spacing — right for a lone
   control under the orb in the overlay, wrong sitting directly above a sans 13px reading, where the
   two rows read as two different kinds of thing rather than one status stack. Inherited rather than
   restated so it tracks .fd-note if that ever changes.

   The hover underline stays: it is the only row that is clickable, and that is worth saying. */
.fd-note .fd-mute {
  padding: 0;
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  letter-spacing: normal;
}

/* ROW ONE ONLY takes a state colour. The cap line is true whatever the microphone is doing, and
   tinting it would make "four minutes" read as part of the reading. */
.fd-note-state[data-tone="problem"] { color: var(--fd-refusal); }
.fd-note-state[data-tone="ready"]   { color: var(--fd-ready); }
.fd-note-cap { color: var(--ink-faint); }

/* The tick and cross come from the tone, not from JS. Drawing them here means the glyph cannot
   disagree with the colour, and it stays out of textContent — so role="status" announces the
   sentence rather than reading "check mark" in front of it. aria-hidden is unnecessary for the same
   reason: generated content is not part of the accessible name.

   No glyph for the untoned state, deliberately: "your browser will ask" is neither a pass nor a
   fail, and a symbol there would claim a reading that has not been taken yet. */
.fd-note-state[data-tone="ready"]::before   { content: "\2713"; margin-right: 6px; }
.fd-note-state[data-tone="problem"]::before { content: "\2715"; margin-right: 6px; }

/* ============================================================
   The takeover overlay.

   It renders site-wide from the layout (except on /labs/front-desk), so unlike everything above
   this point these rules apply on the marketing homepage. They stay in this file because it already
   owns the .fd-* namespace and because the whole of Front Desk's appearance being in one place is
   worth more than splitting by which page it lands on.

   A persistent header affordance used to live here too — an orb and a "use voice mode" label in
   .nav-right, driving this same controller. It was removed deliberately: a control fixed in the
   header of every page is a widget, and the calls now start from the surfaces that ask for one
   (the homepage band, /labs/front-desk, Monday Brief's "Or call it."). The overlay still opens on
   every page, because a call started from the band has to render somewhere.
   ============================================================ */

/* GREEN, the same --fd-ready the mute toggle and the microphone note read — "live" is a status,
   and status colours already have one definition on this surface (see the token list further
   down, which this class joins). It wore the stage's gold-brown ink for a while, which read as
   a smudge rather than a live light.

   The class keeps its .fd-affordance-* name: it is the overlay's only live indicator now, and
   renaming it would touch the markup and this file for no behaviour. */
.fd-affordance-dot {
  display: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--fd-ready);
}

/* Shown only when a call is genuinely up. Never while connecting, and above all never on a refusal,
   where the entire point is that nothing is live. */
.fd-overlay[data-state="live"] .fd-affordance-dot { display: block; }

@media (prefers-reduced-motion: no-preference) {
  .fd-overlay[data-state="live"] .fd-affordance-dot { animation: fd-live 2s ease-in-out infinite; }
}

@keyframes fd-live { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* ---- the takeover ---- */

.fd-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;                /* above everything, including the sticky header */
  background: var(--bg);
  display: grid;
  /* orb, transcript takes the slack, the summary once a call has ended, then the control bar and
     the disclaimer banner. The summary row is `auto` and the panel carries `hidden` until a call
     ends, so it collapses to nothing for the whole of a live call and the transcript keeps the
     full height. The top row is the stage itself — nothing sits above the call any more. */
  grid-template-rows: auto 1fr auto auto auto;
  /* The bottom padding adds the safe-area inset so the bar's circles clear the iOS home
     indicator — the same env() the Listening Post's composer uses (labs_01, mobile).
     NOTE: the padding clamp is restated in the connecting-state transform on .fd-ov-stage
     below — retune both.
     --fd-pad-x names the horizontal component so the mobile sheet's .fd-ov-bottom-stack
     (labs_09_front_desk.css's mobile block) can counteract it exactly — two independently
     written copies of this clamp() is what produced grey bars down both sides, confirmed live
     (2026-08-17): the bottom-stack was inset by this padding same as everything else in the
     overlay, but the sheet is genuinely full-width, escaping it via position: fixed. */
  --fd-pad-x: clamp(16px, 3vw, 32px);
  padding: clamp(16px, 3vw, 32px) var(--fd-pad-x);
  padding-bottom: calc(clamp(12px, 2vw, 24px) + env(safe-area-inset-bottom, 0px));
  /* The overlay locks the document; containing overscroll here stops iOS rubber-banding the page
     underneath when a swipe runs past the transcript's own scroller. */
  overscroll-behavior: contain;
}

.fd-overlay[hidden] { display: none; }

@media (prefers-reduced-motion: no-preference) {
  .fd-overlay { animation: fd-ov-in 320ms ease both; }
}

@keyframes fd-ov-in { from { opacity: 0; } to { opacity: 1; } }

/* Wraps the timer, the bar and the disclaimer — see the mobile block further down for why
   (.fd-ov-bottom-stack needs to raise all three as one continuous surface above the sheet).
   flex-direction: column rather than plain block flow so the three children never margin-collapse
   with each other, matching how they behaved as separate (non-collapsing) grid items before this
   wrapper existed — their own margin/padding values are unchanged, so spacing doesn't shift.
   Unscoped (not mobile-only): a layout-only rule, invisible on desktop either way. */
.fd-ov-bottom-stack {
  display: flex;
  flex-direction: column;
}

/* THE CONTROL BAR. One centred row of two circular buttons — mute and end — bottom of the
   overlay, ChatGPT-voice-mode shape. The live dot sits absolute-centred above the pair so the
   circles stay symmetric; it is shown only in the live state by the rule further up. */
.fd-ov-bar {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 22px;
  padding: 16px 0 4px;
}

.fd-ov-bar .fd-affordance-dot {
  position: absolute;
  top: 4px;
  left: 50%;
  transform: translateX(-50%);
}

/* The running clock, live-state only (same gating pattern as .fd-affordance-dot above) — a
   sibling of .fd-ov-bar rather than inside it, so it reads as a small status line sitting just
   above the controls rather than a third circle in the row. Tabular numerals so mm:ss does not
   shift width as the seconds tick over. */
.fd-ov-timer {
  display: none;
  font-family: var(--font-mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: var(--ink-faint);
  text-align: center;
  margin: 0 0 2px;
}

.fd-overlay[data-state="live"] .fd-ov-timer { display: block; }

/* WEIGHT AND MOTION, NOT COLOUR — the site's no-colour rule applies here same as everywhere else
   on this page. Fires once, 60s before MAX_DURATION_SECONDS, and holds for the rest of the call. */
.fd-ov-timer[data-warn="true"] { color: var(--ink); font-weight: 700; }

@media (prefers-reduced-motion: no-preference) {
  /* Reuses fd-live (defined above, for the affordance dot) rather than a new keyframe — this and
     the dot are the same kind of statement, "pay attention to this", and do not need two rhythms. */
  .fd-ov-timer[data-warn="true"] { animation: fd-live 2s ease-in-out infinite; }
}

/* The mute becomes a circle HERE ONLY — the shared status block's mute (homepage band, Monday
   Brief, the lab page's note) stays a text row. The button's text ("Sound on"/"Sound off",
   written by renderMute) is hidden at font-size: 0 and stays the accessible name; the visible
   face is the existing ::before tick/cross, which already carries the preference's green/red
   keyed on aria-pressed. */
.fd-overlay .fd-mute {
  width: 54px;
  height: 54px;
  border-radius: 50%;
  border: 1px solid var(--line);
  padding: 0;
  font-size: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* A SPEAKER, NOT THE TEXT ROW'S TICK. The base ::before glyphs (✓/✕) belong beside the words
   "Sound on"/"Sound off" in the note row; alone in a circle a tick reads as "confirm", not
   "sound". A mic glyph was tried and rejected (2026-08-15, Jason's call): this toggle gates the
   synthesised UI tones, not the actual microphone — Vapi owns that — so the speaker is the
   honest shape. Speaker with waves = sound on, speaker with a cross = muted, drawn as a mask
   filled with currentColor so it keeps the same green/red the aria-pressed rules already set. */
.fd-overlay .fd-mute::before {
  content: "";
  width: 20px;
  height: 20px;
  margin-right: 0;
  background-color: currentColor;
  -webkit-mask: var(--fd-mute-icon) center / contain no-repeat;
  mask: var(--fd-mute-icon) center / contain no-repeat;
}

.fd-overlay .fd-mute[aria-pressed="false"] {
  --fd-mute-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 9v6h4l5 4.5V4.5L8 9H4z"/><path d="M15.5 8.5a5 5 0 0 1 0 7" fill="none" stroke="black" stroke-width="1.8" stroke-linecap="round"/><path d="M18 6a9 9 0 0 1 0 12" fill="none" stroke="black" stroke-width="1.8" stroke-linecap="round"/></svg>');
}

.fd-overlay .fd-mute[aria-pressed="true"] {
  --fd-mute-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 9v6h4l5 4.5V4.5L8 9H4z"/><path d="M15.5 9.5l5 5M20.5 9.5l-5 5" fill="none" stroke="black" stroke-width="1.8" stroke-linecap="round"/></svg>');
}

.fd-overlay .fd-mute:hover { border-color: var(--ink); border-bottom-color: var(--ink); }

/* The mute circle appears ONLY on a live call. While connecting the sole meaningful act is
   cancelling (the connecting chime is technically mutable, but a second circle there read as a
   control for a call that does not exist yet), and once ended or refused there is nothing left
   to mute — so in every state but live the bar is the one way out. */
.fd-overlay:not([data-state="live"]) .fd-mute { display: none; }

/* RED IN EVERY STATE. This control's whole job is leaving — cancel, end call, close — so the colour
   is a property of the button rather than of the state it is in. A circle now, matching the mute
   beside it: the state label ships in the three spans below at font-size: 0 (still the accessible
   name, picked by data-state), and the visible face is the ::before cross in the call-stop red. */
.fd-ov-end {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 54px;
  height: 54px;
  font-family: var(--font-mono);
  font-size: 0;
  letter-spacing: 0.03em;
  color: var(--ink);
  background: none;
  border: 1px solid var(--fd-stop);
  border-radius: 999px;
  padding: 0;
  cursor: pointer;
  transition: background-color 140ms ease, border-color 140ms ease;
}

.fd-ov-end::before { content: "\2715"; font-size: 17px; color: var(--fd-stop); }

/* A WASH, NOT AN INVERT. The hover used to fill with ink and flip the label to the page colour,
   which threw the red away at exactly the moment the button was about to be pressed — and filling
   with --fd-stop instead is the contrast failure this whole palette avoids (white on the coral is
   3.3:1, under AA for 13px type). A 12% tint reads as clearly hovered, keeps the ink label at full
   contrast, and lets the ring stay the thing carrying the colour. */
.fd-ov-end:hover,
.fd-ov-end:focus-visible {
  background: color-mix(in srgb, var(--fd-stop) 12%, transparent);
  border-color: var(--fd-stop);
  color: var(--ink);
}

/* RECONNECT — the same circle shape as .fd-ov-end, in --fd-ready (the mute toggle's "sound on"
   green) rather than the stop red: this button is a constructive retry, not a way of leaving.
   Hidden by default; shown only alongside a genuinely dropped call (data-end-reason="dropped" —
   see determineEndReason in the controller). A hard time-limit ending gets no Reconnect at all,
   since the call did what it was supposed to. */
.fd-ov-reconnect {
  display: none;
  align-items: center;
  justify-content: center;
  width: 54px;
  height: 54px;
  font-family: var(--font-mono);
  font-size: 0;
  color: var(--ink);
  background: none;
  border: 1px solid var(--fd-ready);
  border-radius: 999px;
  padding: 0;
  cursor: pointer;
  transition: background-color 140ms ease, border-color 140ms ease;
}

.fd-ov-reconnect::before { content: "\21bb"; font-size: 20px; color: var(--fd-ready); }

.fd-ov-reconnect:hover,
.fd-ov-reconnect:focus-visible {
  background: color-mix(in srgb, var(--fd-ready) 12%, transparent);
}

.fd-overlay[data-state="ended"][data-end-reason="dropped"] .fd-ov-reconnect { display: inline-flex; }

/* One label per overlay state, all three in the markup, CSS picks — the same rule the orb
   follows, so the controller writes only data-state and no copy lives in JS. `ended` and `refused`
   share `close`: both mean the overlay is up with no call behind it.

   A state with no rule here renders the button with NO LABEL AT ALL, so a fifth one needs a line
   below as well as a setOverlayState call. */
.fd-ov-end-text { display: none; }
.fd-overlay[data-state="connecting"] .fd-ov-end-connecting,
.fd-overlay[data-state="live"]       .fd-ov-end-live,
.fd-overlay[data-state="ended"]      .fd-ov-end-close,
.fd-overlay[data-state="refused"]    .fd-ov-end-close { display: inline; }

/* Sits under the orb, because that is where the eye already is while waiting. Sans rather than
   mono: this is a sentence to read, not a reading to take. */
/* The refusal red. NOT a new hue: it is .labs-stage-ask's --stage (application.css), the Listening
   Post's stage colour, and it is already on this page as the first stop of .fd-call's gradient
   ring. Set on .fd-ov-notice unconditionally — setNotice is its only writer, and while it mainly
   holds a problem, it's also the one always-visible surface left for submitMessage's post-call-
   ended confirmation once .fd-ov-message itself goes CSS-hidden. */
/* ONE DEFINITION OF EACH, read by the indicators AND by the call buttons' rings (see the call
   palette above). .fd-call and .fd-ov-end join this list rather than carrying their own hexes so
   "Sound off" and "End the call" cannot drift into two different reds on the same screen. Custom
   properties inherit, but these are declared on a selector list rather than :root because this
   file's first rule must stay .fd-head — see the note on the call palette. */
.fd-affordance-dot,
.fd-note,
.fd-mute,
.fd-ov-notice,
.fd-call,
.fd-ov-end,
.fd-ov-reconnect,
.fd-ov-message-error { --fd-refusal: #FF6F5E; --fd-ready: #047857; }

.fd-ov-notice {
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.5;
  color: var(--fd-refusal);
  text-align: center;
  max-width: 46ch;
  margin: 4px auto 0;
}

/* Same data-tone idiom as .fd-note-state — the "ready" green is submitMessage's own
   confirmation, which is not a problem and should not read as one. Default (no attribute) stays
   the refusal red, so every existing caller (rate limit, mic blocked, dropped, etc) is unchanged. */
.fd-ov-notice[data-tone="ready"] { color: var(--fd-ready); }

/* Where the notice sits, and reading the same way, but state-driven rather than written from JS:
   the call is over, the transcript is still there, and the corner offers `close`. Shared with
   .fd-ov-connecting-line below — same neutral caption, different state, different words — and
   with the two reason-specific lines beside it: the client cannot ask Vapi WHY a call ended (that
   only arrives later, by webhook), so determineEndReason in the controller infers one of
   time-limit/dropped/"" and writes it as data-end-reason alongside data-state="ended", and these
   three rules pick the line to match — exactly one shows for any given ending. */
.fd-ov-ended-line,
.fd-ov-connecting-line,
.fd-ov-time-limit-line,
.fd-ov-dropped-line {
  display: none;
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink-muted);
  text-align: center;
  margin: 4px auto 0;
}

/* data-end-reason="" is the ordinary case — visitor hangup, silence timeout, assistant-initiated
   close — everything that is not a known time-limit or drop. */
.fd-overlay[data-state="ended"][data-end-reason=""] .fd-ov-ended-line { display: block; }
.fd-overlay[data-state="ended"][data-end-reason="time-limit"] .fd-ov-time-limit-line { display: block; }
.fd-overlay[data-state="ended"][data-end-reason="dropped"] .fd-ov-dropped-line { display: block; }
.fd-overlay[data-state="connecting"] .fd-ov-connecting-line { display: block; }

/* Hidden by default and shown only at desktop widths: it labels a key phone keyboards do not
   have, and the control it hints at is right beside it. Absolutely positioned, NOT a flex item —
   in the row it shifts the circle pair off the bar's centre, which drops the live dot (also
   centred on the bar) onto the end circle instead of between the two. */
.fd-ov-esc {
  display: none;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-faint);
}

@media (min-width: 700px) {
  .fd-ov-esc {
    display: inline;
    position: absolute;
    left: calc(50% + 82px);
    top: 50%;
    transform: translateY(-50%);
  }

  /* The 82px above clears the TWO-circle pair (54 + 22 gap + 54, so the end circle's right edge
     sits at 50% + 65px). In every non-live state the mute is hidden and the lone circle's edge
     is at 50% + 27px — the same 17px gap lands at 44. */
  .fd-overlay:not([data-state="live"]) .fd-ov-esc { left: calc(50% + 44px); }
}

/* THE INTERCOM LAUNCHER IS HIDDEN WHILE THE OVERLAY OWNS THE SCREEN. Its iframe carries its own
   z-index in the billions, so it paints over the overlay whatever ours is — and its bottom-right
   corner is exactly where the control bar now sits. The body class is written by
   openOverlay/closeOverlay; the selector list and the !important match the body.lp-demo
   precedent in labs_01_composer_mobile.css. */
body.fd-ov-open .intercom-lightweight-app-launcher,
body.fd-ov-open .intercom-launcher-frame,
body.fd-ov-open #intercom-container .intercom-launcher { display: none !important; }

/* place-CONTENT, not place-items. place-items centres each item inside its own track, and the two
   implicit auto tracks then stretch to fill this 1fr row — which put the tool label half a viewport
   below the prism it is supposed to label. place-content sizes the tracks to their content and
   centres the pair as a block, so the gap below is the 20px it says it is. */
/* The orb sits in its own row at its natural size — .fd-stage already defines --fd-orb-size and the
   mesh colours, and reusing that wrapper is what makes the overlay's orb identical to the lab
   page's rather than a second thing that looks similar. Only the margins differ here. */
.fd-ov-stage {
  /* NOTE: the top margin clamp is restated inside the connecting transform below — retune both. */
  margin: clamp(12px, 3vh, 36px) 0 clamp(8px, 2vh, 20px);
  transition: transform 650ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* While CONNECTING the stage sits at the visual centre of the screen — the transcript row is
   empty until the call connects, so the resting top position leaves a blank middle. A TRANSFORM,
   not a layout change: the grid (and the bar pinned at the bottom) never move, and translateY's
   50% is the stage's own height, so it self-centres at any orb size. The subtracted terms are the
   stage's two top offsets — the overlay's padding clamp and the margin clamp above — restated
   here because calc cannot read them; retune all together. Two transform lines are the vh
   fallback (dvh wins where supported; bare vh overshoots on iOS while the URL bar is collapsed).

   On call-start the state goes `live`, the transform resolves to none, and the transition above
   glides the orb up into its resting place as the greeting lands in the transcript. `refused` is
   deliberately NOT centred: a refusal can arrive mid-call (the connected-but-deaf path) when the
   transcript has content, and a translated stage would paint on top of the bubbles — so a refusal
   glides to the top with its coral notice, and the motion itself draws the eye to the message. */
.fd-overlay[data-state="connecting"] .fd-ov-stage {
  transform: translateY(calc(50vh - 50% - clamp(16px, 3vw, 32px) - clamp(12px, 3vh, 36px)));
  transform: translateY(calc(50dvh - 50% - clamp(16px, 3vw, 32px) - clamp(12px, 3vh, 36px)));
}

/* Position still changes between states; it just stops animating — the same rule the orb's own
   mesh and ring follow. */
@media (prefers-reduced-motion: reduce) {
  .fd-ov-stage { transition: none; }
}

/* The show page caps the transcript at min(460px, 100vh - 220px) because it sits in a scrolling
   document. Here it IS the document, so it fills its grid row instead.

   min-height: 0 is required, not tidy. A grid item's default min-height is auto, which refuses to
   shrink below its content — so without this the panel grows past the viewport and the page scrolls
   instead of the panel, which on a locked overlay means it cannot be scrolled at all. */
.fd-ov-transcript {
  max-height: none;
  height: 100%;
  min-height: 0;
  width: 100%;
  max-width: 780px;
  margin-inline: auto;
  padding-inline: 0;
}

/* THE TYPED PATH into leave_message — live-call only (a visitor who has not yet connected has
   nothing to send, and one who has already hung up has nowhere for it to go), so the whole block
   is gated the same way the mute circle is. SIGNAL-DRIVEN, not a manual toggle: the form itself
   starts `hidden` and only offer_message_form's client-side reveal (revealMessageForm in
   front_desk_controller.js) un-hides it — this CSS only ever handles the outer "is a call live at
   all" gate, never the inner "has the offer happened yet" one. */
.fd-overlay:not([data-state="live"]) .fd-ov-message { display: none; }

.fd-ov-message {
  width: 100%;
  max-width: 420px;
  margin: 10px auto 0;
}

.fd-ov-message-form {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 10px;
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: 8px;
}

/* Restates what the `hidden` attribute already means. Without this, this rule's own class-level
   `display: flex` above beats the UA stylesheet's attribute-based `display: none` for `[hidden]`,
   and the form would render visible the instant it entered the DOM regardless of the attribute —
   the standard trap of giving an element both `hidden` and its own `display` declaration. */
.fd-ov-message-form[hidden] { display: none; }

/* UNCONDITIONAL, outside any media query — these three exist only for the mobile sheet (see the
   max-width: 699px block below), and must default to invisible at every width, not just above it.
   Found live (2026-08-16): without this base rule, revealMessageForm removing the peek tab's
   `hidden` attribute was enough on its own to render it — a bare button/div has UA display values
   the mobile block's own `display: none` reset never reaches outside that media query. The mobile
   rules override this back to flex/block once the width and state both match. */
.fd-ov-message-peek,
.fd-ov-message-scrim,
.fd-ov-message-handle { display: none; }

/* Static guidance the desktop transcript already carries in-conversation — this exists for the
   mobile sheet, which covers the transcript once expanded, but renders on both since it's harmless
   context either way. */
.fd-ov-message-hint {
  font-size: 12.5px;
  color: var(--ink-faint);
  margin: 0 0 2px;
}

.fd-ov-message-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.fd-ov-message-label {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.fd-ov-message-row textarea,
.fd-ov-message-row input {
  font-family: var(--font-sans);
  font-size: 14px;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 8px 10px;
  resize: vertical;
}

.fd-ov-message-row textarea:focus,
.fd-ov-message-row input:focus { outline: none; border-color: var(--ink); }

.fd-ov-message-error {
  font-size: 13px;
  color: var(--fd-refusal);
  margin: 0;
}

.fd-ov-message-submit {
  align-self: flex-end;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--bg);
  background: var(--ink);
  border: 0;
  border-radius: 4px;
  padding: 9px 16px;
  cursor: pointer;
}

.fd-ov-message-submit:hover { opacity: 0.85; }

/* --fd-refusal is declared on the shared token list further down this file (the error text above
   needs it, same red as every other refusal on this page); no separate declaration here. */

/* MOBILE ONLY — below the same 700px cutover .fd-ov-esc already uses further down this file.
   Above it, .fd-ov-message-peek/-scrim/-handle stay [hidden] and data-sheet is never read: the
   inline form behaves exactly as it always has.

   Below it, filling the form in — by voice or by typing — used to push the transcript out of
   view entirely, since .fd-ov-message sat in the grid's own flow at up to ~280px tall on a
   screen that might only be ~600px to begin with. Offering a message now defaults to a peek tab
   (one line, pinned above the control bar) instead; tapping it raises the form as a bottom sheet,
   and the transcript is visible again the instant it's collapsed back down. */
@media (max-width: 699px) {
  /* ONE source of truth for the reserved height, read by both the sheet's `bottom` (below) and
     .fd-ov-bottom-stack's own min-height (further down) — declared on .fd-overlay, the nearest
     ancestor both a descendant of .fd-ov-message and .fd-ov-bottom-stack can actually read a
     custom property from (they're siblings, so a property on either one directly wouldn't reach
     the other). Splitting this into two independently-written numbers is exactly what produced
     the grey gap between them, confirmed live (2026-08-17): the sheet backed off by 150px but
     .fd-ov-bottom-stack was left to size itself to its own content (~128px), leaving the
     difference uncovered for the scrim to show through. One variable means they can't drift
     apart again. */
  .fd-overlay {
    --fd-bottom-stack-reserve: calc(150px + env(safe-area-inset-bottom, 0px));
  }

  .fd-ov-message-peek {
    display: none;
    width: 100%;
    align-items: center;
    gap: 8px;
    padding: 10px 14px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--bg);
    font-family: var(--font-sans);
    font-size: 13px;
    color: var(--ink);
    text-align: left;
    cursor: pointer;
  }
  .fd-ov-message[data-sheet="collapsed"] .fd-ov-message-peek:not([hidden]) { display: flex; }

  /* .labs-stage-talk on the overlay (see _front_desk_overlay.html.erb) supplies --stage — the
     same gold the orb and the lab's own eyebrow use, so this names no new hue. */
  .fd-ov-message-peek-dot {
    width: 6px; height: 6px; border-radius: 50%; background: var(--stage); flex-shrink: 0;
  }
  .fd-ov-message-peek-chevron { margin-left: auto; color: var(--ink-faint); font-size: 11px; }

  .fd-ov-message[data-sheet="collapsed"] .fd-ov-message-form { display: none; }
  .fd-ov-message[data-sheet="expanded"] .fd-ov-message-peek { display: none; }

  /* THE SHEET. Fixed rather than laid out in the grid, so expanding it can never push the
     orb/transcript/bar around. Its `bottom` is NOT 0 — see the comment on that below, which is the
     actual fix for a real-device bug two rounds of tuning padding/spacers around the control bar
     didn't close. */
  .fd-ov-message[data-sheet="expanded"] .fd-ov-message-form {
    display: flex;
    position: fixed;
    left: 0; right: 0;
    z-index: 5;
    max-width: none;
    max-height: min(60vh, 520px);
    max-height: min(60dvh, 520px);
    margin: 0;
    /* The base rule carries no background: in normal flow it never needed one, sitting over the
       overlay's own opaque ground. Fixed and layered over the transcript, it does — found live
       (2026-08-16): without this, transcript text painted straight through the sheet's own field
       labels. */
    background: var(--bg);
    border-radius: 16px 16px 0 0;
    border-bottom: none;
    overflow-y: auto;
    box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.14);
    /* NOT 0. A prior version used bottom: 0 plus reserved space (first padding-bottom, then a
       trailing spacer element) inside the scroll content, trusting the control bar's raised
       z-index and opaque background to paint over the last bit. Found live on a real phone
       (2026-08-16), twice: the submit button still showed through around/behind the bar. Two
       plausible causes and no need to pick one — .fd-ov-bar is a normal in-flow row inset by
       .fd-overlay's own side padding while this sheet is genuinely full-width, so the bar's
       background may not reach as far as the sheet does; separately, this sheet's max-height
       (dvh) and the overlay's own inset: 0 sizing are two independently-computed viewport
       measurements, and iOS Safari's collapsing toolbar (visible in the screenshot — an ordinary
       Safari tab, not fullscreen) is a known source of exactly that kind of drift between
       elements. Anchoring bottom to the bar stack's own fixed height instead means the box
       PHYSICALLY CANNOT reach the bar's screen region — no overlap to paint over, regardless of
       which of those was actually at play. 150px measured live (timer top to disclaimer bottom
       was ~128px, plus margin) — fixed, not viewport-relative, since the bar's own content height
       doesn't change with screen size. Read off --fd-bottom-stack-reserve (declared on
       .fd-ov-bottom-stack below) rather than repeating the literal calc() here — see that rule's
       comment for why these two can never be allowed to drift apart again. */
    bottom: var(--fd-bottom-stack-reserve);
  }

  /* Full-width on the mobile sheet, not desktop's compact right-aligned button — a bigger,
     thumb-friendlier target, and it removes any ambiguity about the button clipping at the
     sheet's right edge. Desktop's align-self: flex-end is untouched outside this media query. */
  .fd-ov-message[data-sheet="expanded"] .fd-ov-message-submit { align-self: stretch; }

  /* The swipe-down-to-collapse handle, and the sheet's visual "this is a drag surface" affordance
     — hidden entirely outside the expanded sheet, since the collapsed peek tab needs no handle. */
  .fd-ov-message-handle { display: none; }
  .fd-ov-message[data-sheet="expanded"] .fd-ov-message-handle {
    display: block;
    width: 34px; height: 4px; border-radius: 2px; background: var(--line);
    margin: 4px auto 10px;
    flex-shrink: 0;
  }

  /* Full-screen rather than clipped to the transcript's own bounds — simpler than computing where
     the transcript ends, and harmless: the bar's raised z-index and opaque background (further
     down) keep it visually and functionally on top regardless. */
  .fd-ov-message-scrim { display: none; }
  .fd-ov-message[data-sheet="expanded"] .fd-ov-message-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 4;
    background: rgba(0, 0, 0, 0.18);
  }

  /* THE NON-NEGOTIABLE PART: ending a call must never be blocked by the sheet. Raising this stack
     above it and giving it one continuous opaque ground means the sheet visually tucks UNDER it
     rather than covering it — the same "persistent bottom bar over a sheet" shape iOS's own apps
     use. ONE element, not three: the timer, the bar and the disclaimer used to each carry this
     individually, and the MARGINS between them — not covered by any of the three's own background
     — let the scrim show through as grey seams, confirmed live (2026-08-17). This base rule is
     what the stack looks like whenever the sheet is NOT open (no scrim exists then, so this is
     mostly a no-op, but harmless either way) — the override just below is what actually matters
     while the sheet is expanded. Nothing here changes layout, only stacking, so desktop — where
     this whole block doesn't apply — is untouched. */
  .fd-ov-bottom-stack {
    position: relative;
    z-index: 6;
    background: var(--bg);
  }

  /* THIRD round on this exact seam (vertical gap, then horizontal bars, now this) — each prior fix
     patched the stack's height or width to match the sheet's, and each time something the stack
     didn't yet account for reopened a gap: this one is .fd-overlay's own padding-bottom (the
     safe-area reserve below the grid's last row), which sits BELOW the stack's box entirely and
     was never the stack's to cover as a normal in-flow element. Confirmed live (2026-08-17):
     elementFromPoint in that gap resolves to the scrim, nothing opaque above it.
     RETIRED the min-height/negative-margin patches above in favour of one structural fix instead
     of a fourth patch: while the sheet is expanded, take this stack OUT of the padded grid flow
     entirely and fix it to the true viewport — left: 0; right: 0; bottom: 0, the exact same
     coordinate system the sheet and scrim already use. A box anchored to the true edges on all
     four sides cannot leave a gap on any of them, which is what every prior round was chasing
     piecemeal. Scoped via the sibling combinator (data-sheet lives on .fd-ov-message, a sibling of
     this stack, not an ancestor) so the NORMAL in-flow behaviour above is completely untouched
     whenever the sheet isn't open — this override only exists for the one state that needs it. */
  .fd-ov-message[data-sheet="expanded"] ~ .fd-ov-bottom-stack {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    /* None of the timer/bar/disclaimer carry their own horizontal inset — they've always relied
       on sitting inside .fd-overlay's own padded content area for that. Now that this box is
       fixed to the true (unpadded) viewport edges, it has to restate that inset itself, or the
       content would jump flush to the screen edges instead of staying where it's always been. */
    padding-left: var(--fd-pad-x);
    padding-right: var(--fd-pad-x);
    /* Same --fd-bottom-stack-reserve the sheet's own `bottom` reads (see that rule) — so this
       box's TOP edge lands exactly on the sheet's bottom edge, not just "close enough": both
       numbers come from the one variable, not two independently-sized boxes trusted to agree.
       justify-content: flex-end keeps the actual timer/circles/disclaimer content packed at the
       bottom of this now-taller box (the base rule above only sets flex-direction, not this), and
       the safe-area padding below pulls that content clear of the home indicator — the empty
       space above the content, inside this box, is what closes the old vertical gap; it was never
       visible as a gap because it's the same white background as everything else here. */
    min-height: var(--fd-bottom-stack-reserve);
    justify-content: flex-end;
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
}

/* The post-call summary, sharing the lab page's .fd-analysis rules and adding only what a
   full-screen surface needs: the transcript's measure, and a scroller of its own.

   ITS OWN max-height IS LOAD-BEARING. The overlay is fixed to the viewport and locks the document's
   scroll, so a panel that simply grew would push its own end off the bottom of the screen with no
   way to reach it. Capped, it scrolls internally and the transcript above keeps the rest.

   A hairline instead of a heading: the panel's own "Measured" and "The read" labels already say what
   it is, and the transcript directly above needs separating from it more than it needs naming. */
.fd-ov-analysis {
  width: 100%;
  max-width: 780px;
  margin: 16px auto 0;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  max-height: 42vh;
  overflow-y: auto;
  min-height: 0;
}

/* NO reduced-motion rule for the overlay's contents, deliberately. The prism this replaced was a
   full-screen moving surface and needed one; the orb is now the only thing that moves, and its own
   @media (prefers-reduced-motion: reduce) block further up this file already stops its mesh
   animation and its ring transform. The fade-in above is already gated on no-preference. */

/* ============================================================
   The tools list.

   .fd-kit-*, NOT .fd-tool-*. Eleven .fd-tool* names are already taken above by the LIVE tool-call
   chips, so reusing that prefix for a static list would restyle the running conversation.
   .dt-table is out for the same class of reason: that is /labs/data's data-grid language, and the
   Proving Ground's bout page already declined it deliberately.
   ============================================================ */

/* The section labels the lab page uses now that the tab bar is gone — mono uppercase in the
   .fd-analysis-label register, one above the toolkit and one above the last-call section. */
.fd-sec-label {
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 34px 0 12px;
}

/* THE SWITCHBOARD: every pill in one wrapped cloud, one blurb below (the ruled two-column
   dl this replaced ran ~900px — taller than a window — for ten tools). The tablist is the
   Proving Ground's controller; these rules only dress it. */
.fd-kit-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 14px;
}

/* The pill wears the page's own stage gold — ink text on a light wash, the same color-mix idiom
   as the band's ground, arriving through .labs-stage-talk on the page body so no hex is named
   here beyond the shared fallbacks. Now a real <button role="tab">: the resting wash stays faint
   (8%) so ten of them read as a set of labels, and only the selected one deepens toward the
   stage hue — state told by weight of the same colour, not a second colour. */
.fd-kit-pill {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.01em;
  color: var(--stage-ink, #854D0E);
  border: 1px solid color-mix(in srgb, var(--stage-ink, #854D0E) 30%, transparent);
  background: color-mix(in srgb, var(--stage, #EAB308) 8%, transparent);
  border-radius: 4px;
  padding: 5px 11px;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  transition: background 140ms ease, border-color 140ms ease;
}

.fd-kit-pill:hover { border-color: var(--stage-ink, #854D0E); }

.fd-kit-pill[aria-selected="true"] {
  background: color-mix(in srgb, var(--stage, #EAB308) 26%, transparent);
  border-color: var(--stage-ink, #854D0E);
}

/* min-height holds three lines (plus the panel's own padding — border-box) so switching between
   a one-line blurb and a three-line one does not bounce the last-call section below. Full width,
   bordered in the resting pill's own faint stage-gold, so the panel reads as what the selected
   pill opened rather than loose prose under the cloud. */
.fd-kit-blurb {
  margin: 0;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--ink-muted);
  border: 1px solid color-mix(in srgb, var(--stage-ink, #854D0E) 30%, transparent);
  border-radius: 4px;
  padding: 12px 16px;
  min-height: calc(3 * 1.6em + 24px);
}

/* ---- post-call analytics ---- */

/* .fd-analysis-*, NOT .fd-tool-* or .fd-kit-*: eleven of the first are taken by the live tool rail
   and six of the second by the static tools table. Same collision, same answer as .fd-kit-* — see
   the note above that block. Nothing here names a hue: the panel inherits --stage / --stage-ink
   from .labs-stage-talk, which sits on .wrap.lp-body so that it actually reaches this far. It used
   to sit on .fd-stage — a sibling — and every var(--stage-ink) below resolved against nothing; see
   the note on that element in show.html.erb for why that failed silently rather than falling back. */

.fd-analysis { margin-top: 26px; }

.fd-analysis-pending {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 22px 0;
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--ink-muted);
}

/* .lp-answer-star carries margin-top: 20px from labs_01, where it is a TRAILING element sitting
   under a finished answer. Here it is the left half of a centred flex row, so that margin drags it
   20px below the line it is meant to sit on — align-items: center then centres a box that is
   mostly empty space. Scoped rather than fixed at source: the Listening Post and /admin/overview
   both still want the original spacing. */
.fd-analysis-pending .lp-answer-star { margin-top: 0; flex: none; }

.fd-analysis-failed {
  padding: 20px 0;
  font-size: 15px;
  color: var(--ink-muted);
}

.fd-analysis-block { margin-bottom: 30px; }

/* THE PANEL'S ONE SERIF LINE, and the reason it opens with prose rather than with six figures.
   Same role and nearly the same setting as .ov-lead p on /admin/overview: the site's human voice,
   used once. Note this is VAPI's summary of the call rather than our own read — the foot says so,
   because the two arrive from different places and only one of them is ours. */
.fd-analysis-verdict {
  font-family: var(--font-serif);
  font-size: clamp(19px, 2.2vw, 24px);
  line-height: 1.4;
  letter-spacing: -0.012em;
  max-width: 46ch;
  text-wrap: balance;
  margin: 0 0 22px;
  color: var(--ink);
}

/* The only field on this panel anyone can act on, so it is the only one with a rule beside it.
   Promoted out of the read's rows — see the partial. */
.fd-analysis-gap {
  border-left: 2px solid var(--stage-ink);
  padding: 12px 0 12px 16px;
  margin: 0 0 26px;
}

.fd-analysis-gap-key {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--stage-ink);
  margin-bottom: 4px;
}

/* Auto-fit rather than a fixed column count: six readings on a laptop, three on a tablet, two on a
   phone, with no breakpoint of their own. Tighter than it was, and the figures are mono now — they
   sit below the verdict rather than opening the panel, so they read as reference. */
.fd-analysis-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  margin-top: 14px;
}

.fd-analysis-stat {
  background: var(--bg);
  padding: 10px 12px;
}

.fd-analysis-stat dt {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.fd-analysis-stat dd {
  margin: 4px 0 0;
  font-family: var(--font-mono);
  font-size: 16px;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.fd-analysis-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 12px;
}

.fd-analysis-chip {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 11.5px;
  padding: 3px 8px;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--ink-muted);
}

/* The same 128px label rail as .lab-index-block on /labs, for the same reason: a label beside its
   prose reads as an index, a label above it reads as a form. */
.fd-analysis-row {
  display: grid;
  grid-template-columns: 128px 1fr;
  gap: 18px;
  padding: 16px 0;
  border-top: 1px solid var(--line);
}

.fd-analysis-row:first-of-type { border-top: none; }

.fd-analysis-label {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding-top: 3px;
}

.fd-analysis-value { font-size: 15px; line-height: 1.65; color: var(--ink); }
.fd-analysis-value p { margin: 8px 0 0; color: var(--ink-muted); }

/* A judgement, so it ships with its word and never as colour alone — the same rule /admin's status
   colours follow. The tone only changes weight and border here; this page has one hue. */
.fd-analysis-mark {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 3px 9px;
  border: 1px solid var(--line);
  color: var(--ink);
}

.fd-analysis-mark[data-tone="positive"] { border-color: var(--stage-ink); color: var(--stage-ink); }
.fd-analysis-mark[data-tone="negative"] { border-color: var(--ink); background: var(--ink); color: var(--bg); }

.fd-analysis-arc {
  margin-left: 10px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--ink-faint);
}

.fd-analysis-list { margin: 0; padding-left: 18px; }
.fd-analysis-list li { margin-bottom: 6px; }

.fd-analysis-evidence {
  display: block;
  font-size: 13.5px;
  color: var(--ink-faint);
}

.fd-analysis-followup { font-style: italic; }

.fd-analysis-quote {
  margin: 18px 0 0;
  padding-left: 16px;
  border-left: 2px solid var(--stage-ink);
  font-family: var(--font-serif);
  font-size: 19px;
  line-height: 1.5;
  color: var(--ink);
}

.fd-analysis-foot {
  font-size: 13px;
  line-height: 1.6;
  color: var(--ink-faint);
  max-width: 68ch;
}

/* The label rail costs more than it earns once the value column is narrower than its measure. */
@media (max-width: 640px) {
  .fd-analysis-row { grid-template-columns: 1fr; gap: 6px; }
}

/* The one line rendered by shared/_front_desk_disclaimer. Quiet mono caption in the .fd-mute
   register: a convention, not a warning. */
.fd-disclaimer {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  text-align: center;
  margin: 10px 0 0;
}

/* On the overlay it is a one-line BANNER under the control bar: full width, a hairline above,
   the last thing on screen before the safe area. nowrap + hidden overflow is the guarantee the
   copy stays one line — which is why the partial's sentence is short. */
.fd-overlay .fd-disclaimer {
  border-top: 1px solid var(--line-soft);
  padding-top: 8px;
  margin: 8px 0 0;
  font-size: 10px;
  white-space: nowrap;
  overflow: hidden;
}

/* Phones give the transcript the room: one step down on the orb, taken on the overlay's stage so
   the shared clamp is untouched everywhere else. */
@media (max-width: 420px) {
  .fd-ov-stage { --fd-orb-size: 112px; }
}
.fd-stage .fd-disclaimer { margin-top: 6px; }
