/* 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.
   ============================================================ */

/* THE MODULE IS A TWO-COLUMN DECK (2026-09-04) — reusing this site's own /audit deck (.ar-deck,
   site_07_forms.css:134-139: 46fr 54fr, gap 56px, stacking at 980px) rather than inventing a new
   ratio. Left column (46%) is .fd-stage, the orb-led call cluster; right column (54%) is
   .fd-module-toolkit — the wider share serves the Toolkit's wrapping pill rows the same way
   .ar-deck's wide pane serves its report-cover reproduction, while the call cluster is compact
   enough not to need more. `> div { min-width: 0 }` is the same idiom .ar-deck/.ct-deck use so
   neither column's content can stretch its own grid track.

   WIDTH 100% IS LOAD-BEARING, not decoration — see the identical note on .dt-stats-row in
   site_03_hero.css. `main.lp-page` is a flex column and `.wrap`'s `margin: 0 auto` beats
   `align-items: stretch`, so a `.wrap` shrinks to its own content instead of filling the measure
   unless something inside it forces the width. Without this the deck (and everything underneath
   it, since it's now the widest thing `.wrap.lp-body` holds) can silently narrow to whatever its
   content needs rather than spanning the page like the rest of this site. */
.fd-module {
  width: 100%;
  display: grid;
  grid-template-columns: 46fr 54fr;
  gap: 56px;
  align-items: start;
  background: var(--bg);
  margin: clamp(10px, 1.4vw, 20px) 0 0;
}
.fd-module > div { min-width: 0; }

/* Same breakpoint and gap-step both .ar-deck and .ct-deck use (site_07_forms.css), so this page's
   stacking behaviour matches the site's established convention rather than picking a new number. */
@media (max-width: 980px) {
  .fd-module { grid-template-columns: 1fr; gap: 48px; }
}

.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, var(--clay)) 72%, #000);

  /* THIS BASE RULE MUST STAY THE OVERLAY'S LAYOUT, NOT THE LAB PAGE'S. `.fd-ov-stage`
     (_front_desk_overlay.html.erb) is `class="fd-stage fd-ov-stage"` — it shares this exact
     selector so the custom properties above reach it too — so any layout property written HERE
     (display, grid-*, align-items, gap, padding, margin) reapplies inside the live-call takeover
     as well. The console's grid belongs on `.fd-module .fd-stage` below instead, which the
     overlay's `.fd-ov-stage` is never a descendant of. Keep it that way. */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  margin: clamp(10px, 1.4vw, 20px) 0 clamp(10px, 1.6vw, 18px);
}

/* NO .fd-module .fd-stage LAYOUT RULE HERE, DELIBERATELY — see the base .fd-stage rule's own
   comment above. The plain centred flex column it already carries for the overlay's sake is
   exactly the orb-led shape this page wants too, so there is nothing to override and nothing new
   to scope. `.fd-module-desc` just needs to read as a centred paragraph within that column. */
.fd-module-desc {
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink2);
  margin: 0;
  max-width: 40ch;
  text-align: center;
}

/* 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 (site_03_hero.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 in site_03_hero.css. */
  --fd-period: 24s;
  /* NO LONGER MATCHED TO THE BAND'S SIZE (2026-09-04). It used to be — deliberately, clamp(112px,
     13vw, 158px), so this page's orb read as the same object as the homepage band's. That was the
     wrong call for this page specifically: this is Front Desk's own dedicated surface, where the
     orb should be unmistakably the dominant element rather than matching a small inline icon
     elsewhere on the site. Grown past even .fd-stage's own pre-restructure default (clamp(126px,
     18.7vw, 177px), above) — it now leads a 46%-width column on its own, so it can afford to be
     larger than either. Tune the clamp's max against the built column rather than trusting this
     number; the homepage band's .fd-band-orb in site_03_hero.css is UNCHANGED and still uses its
     own 158px clamp — this is the one deliberate departure between the two orbs. */
  --fd-orb-size: clamp(140px, 15vw, 190px);
  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, matching the
   homepage band's own mobile floor. No grid to collapse here any more (see the base .fd-stage
   rule's comment) — the deck itself already stacked to one column at 980px, well above this
   width, and .fd-stage's plain flex column keeps working unchanged all the way down. */
@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, var(--clay)); }

/* 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, var(--ember)) 48%, transparent) 80deg,
    color-mix(in srgb, var(--stage-ink, var(--clay)) 50%, transparent) 160deg,
    transparent 240deg,
    color-mix(in srgb, var(--stage, var(--ember)) 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, var(--ember)) 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, var(--clay)); }

/* 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, var(--clay)); }

/* 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 site_03_hero.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);
  /* The three rays, in order: a person, the translation, the machine. It was three
     bespoke hues; it is the spectrum now, and it is the same drawing the prism
     makes at the top of every page. */
  --fd-call-ring: linear-gradient(120deg, var(--ember), var(--peri), var(--cobalt));
  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 READS AS A CENTRED ROW HERE (2026-09-04) — the mute toggle, the connection reading and
   the "capped at" line sit in one line under the chips, centred under the orb-led column above
   them rather than left-aligned (that was Option 4's chips-left/status-right split row, which no
   longer exists). Reverts to a stack below 620px, purely for width — three short phrases side by
   side get cramped in a narrow single-column deck. Scoped through `.fd-module .fd-stage` — not
   bare `.fd-stage` — so the overlay's `.fd-ov-stage` (which never renders a `.fd-note` at all, but
   shares the `.fd-stage` class) can't be reached by accident, even though it provably wouldn't be. */
.fd-module .fd-stage .fd-note {
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 14px;
  text-align: center;
  flex-wrap: wrap;
}
@media (max-width: 620px) {
  .fd-module .fd-stage .fd-note { flex-direction: column; 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. */
/* THE HIT AREA IS 44px; THE MARK IS STILL 21. This is the note's first row, one of
   three stacked lines, and making the control itself 44px tall would put a gap in
   the middle of a block whose whole job is to read as one stack. So the padding
   buys the target and the negative margin gives the layout back — the visual
   rhythm is unchanged and a thumb still gets a real target.
   Found by measuring at 390px: it was the only control on any of the six pages
   under the floor, and it is the mute, which a caller reaches for mid-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;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-block: -11px;
}

.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: 13px;
  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: var(--miss); }

.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. */
  /* Seven rows, one per child: stage, transcript, message, audit hand-off, summary,
     what-happens-next, bottom stack. Every row but the transcript's is `auto` and every one of
     them carries either `hidden` or a display:none state gate, so each collapses to nothing until
     its own moment and the transcript keeps the slack. .fd-ov-next joined on 2026-09-03 and
     .fd-ov-audit on 2026-09-04 — an implicit row would have behaved identically, but a template
     that stops naming its children is one nobody can read against the markup. */
  grid-template-rows: auto 1fr auto auto 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;
}

/* THE OVERLAY'S ORB IS THE HOMEPAGE BAND'S ORB, AND THE BAND'S IS GOLD.
   Prism II remapped .labs-stage-talk from gold onto the warm end of the spectrum
   (--ember/--clay), which repainted this orb while .fd-band (site_03_hero.css)
   kept the original pair — so the object a visitor presses and the object that
   then fills the screen were two different colours. This restores the pair the
   orb was drawn for, and the two values are .fd-band's exactly: change one and
   change the other.

   TWO CLASSES, NOT ONE. .labs-stage-talk lives in application.css, whose compiled
   logical path is tailwind.css and therefore loads AFTER every labs_* file — at
   equal specificity it would win. The compound selector is what makes this apply.
   .labs-stage-talk stays on both because the analytics panel below reads
   --stage/--stage-ink too, and it must resolve there or its declarations drop
   whole rather than falling back.

   THE LAB PAGE IS THE SECOND SELECTOR (2026-09-03). /labs/front-desk carried the
   remapped ember for months, so the page a visitor lands on to meet the Front
   Desk showed a different orb from the band on the homepage and from the overlay
   that opens out of both — and the toolkit pills, their blurb panel and the
   analysis labels below all read --stage/--stage-ink, so they went ember with it.
   One rule paints all of them. The eyebrow above sits OUTSIDE .lp-body and so is
   out of reach here; it takes the same ink from .eyebrow-gold in application.css.

   The pair itself is --fd-gold/--fd-gold-ink, declared once on :root in
   application.css — it used to be two literal hexes here and two more in
   .fd-band (site_03_hero.css), each with a comment asking the next reader to
   change the other. */
/* The lab hero joined this list on 2026-09-03. Its band sits directly above the
   orb, so an ember hero over a gold page would be a new seam in the one place the
   "one object's colour" argument is easiest to see.
   THE FRONT DESK CARD ON /labs AND /customer-intelligence STAYS EMBER, so lab six
   is the one whose page does not match its own catalogue hue. That mismatch
   predates this band and is left alone on purpose rather than fixed halfway. */
.fd-overlay.labs-stage-talk,
.lp-body.labs-stage-talk,
.lab-hero.labs-stage-talk {
  --stage: var(--fd-gold);
  --stage-ink: var(--fd-gold-ink);
}
/* --stage-lt and --stage-draw are here and not in the list above because only the
   hero has a gradient with a third stop to end on and a line drawing to stroke.
   Sand is the warm light already on the spectrum and the draw is the same 58%
   let-down of the ink that every .labs-stage-* rule uses, so no new hue enters and
   the mark is not left stroking the Talk stage's clay on a gold ground. */
.lab-hero.labs-stage-talk {
  --stage-lt: var(--sand);
  --stage-draw: color-mix(in srgb, var(--fd-gold-ink) 58%, var(--bg));
}

.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 ("Interface sounds on"/"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 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.

   THE ACCESSIBLE NAME CARRIES THE SCOPE, BECAUSE THE GLYPH CANNOT (2026-09-03). Both available
   shapes overclaim: a mic says it gates the microphone and a speaker says it silences the other
   party, and this gates neither — only the three synthesised tones. A design critique found the
   real cost of that: somebody on a call in public presses the one control that looks like "make
   this stop making noise", keeps hearing the agent, and has been misled at the moment they most
   wanted out. renderMute now writes "Interface sounds on"/"off" rather than "Sound on"/"off", so
   the speaker is scoped by the label instead of standing alone as a claim. The glyph decision
   above is deliberately NOT reversed — between two wrong shapes it is still the better one.

   The control a caller actually expects in this slot is a mic mute, which Vapi does expose
   (vapi.setMuted). That is a real feature with its own state, reconnect and aria questions, not a
   relabel, and it is the open follow-up here. */
.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: 21px; color: var(--fd-ready); }

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

/* Dropped calls, AND refusals (2026-09-03). The refusal copy has always ended "then try again" —
   allow the microphone in your site settings, then try again — while this selector named the
   dropped ending only, so the screen gave an instruction and rendered no control to carry it out.
   The only way back was `close`, then finding the button on the page again. reconnect() is just
   start(), so it is as safe from a refusal as from a drop: if the cause has not been fixed the
   gate refuses again, in place, which is a far better answer than a dead end. A hard time-limit
   ending still gets none — that call did what it was supposed to. */
.fd-overlay[data-state="ended"][data-end-reason="dropped"] .fd-ov-reconnect,
.fd-overlay[data-state="refused"] .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. */
/* STATUS, NOT IDENTITY. A refusal and a ready state are not a position on the
   spectrum, so these take the system's own status tokens rather than a stage hue,
   and both always ship with a word beside them. They were #FF6F5E and #047857. */
.fd-affordance-dot,
.fd-note,
.fd-mute,
.fd-ov-notice,
.fd-call,
.fd-ov-end,
.fd-ov-reconnect,
.fd-ov-message-error { --fd-refusal: var(--miss); --fd-ready: var(--pass); }

.fd-ov-notice {
  font-family: var(--font-sans);
  font-size: 15px;
  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: 15px;
  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: 12px;
  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 OR ended, never before a call. The outer gate used to
   be live-only, on the reasoning that "one who has already hung up has nowhere for it to go".
   That half was wrong: Labs::FrontDeskController#message authorizes on the session having claimed
   the call id, not on a live call, so a message typed after the call ends saves through exactly
   the same path — which is what lets .fd-ov-next's second button offer it as the alternative to
   booking. A visitor who has not connected at all still has nothing to send, so that half stands.

   SIGNAL-DRIVEN, not a manual toggle: the form itself starts `hidden` and is un-hidden only by
   revealMessageForm — from offer_message_form mid-call, or from offerMessage once ended. This CSS
   only ever handles the outer "could a message go anywhere" gate, never the inner "has it been
   offered yet" one, so widening it reveals nothing on its own. */
.fd-overlay:not([data-state="live"]):not([data-state="ended"]) .fd-ov-message { display: none; }

/* THE AUDIT HAND-OFF — see the block's own note in shared/_front_desk_overlay.html.erb.

   Visibility is the `hidden` attribute alone, set by revealAuditPanel and cleared by resetPanels.
   No state gate beside it, unlike .fd-ov-message and .fd-ov-next: those two are also reachable
   from the ended state, and this one is only ever revealed by a tool firing mid-call, so the
   attribute is the whole of the truth and a second writer would be something to keep in step.

   A row rather than a column, because unlike .fd-ov-next these are not two competing asks: one
   line of context and one control. It wraps at narrow widths rather than shrinking the pill,
   which has to clear 44px at every width like every other CTA on the site. */
.fd-ov-audit {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 8px 14px;
  width: 100%;
  max-width: 520px;
  margin: 12px auto 0;
  text-align: center;
}

/* 15px, the ladder's `.small`, not 14 — the twelve sizes are the whole set and 14 is not one of
   them (labs_type_scale_test pins it). Same size as the pill beside it; --ink-muted rather than
   weight is what makes it the supporting half of the pair. */
.fd-ov-audit-line {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.4;
  color: var(--ink-muted);
}

/* THE SAME PAIR AS .fd-ov-next-book, AND THE FIRST DRAFT OF THIS RULE GOT IT WRONG. Painting the
   pill in the surface's own gold reads well as an argument — the audit is the Front Desk's ask
   rather than the site's — and it is unbuildable: --fd-gold is a LIGHT HUE, fixed across both
   themes, and its text partner --fd-gold-ink brightens to #E2AC33 in dark. Measured, gold-ink on
   gold is about 3.8:1 in light (under AA at 15px bold) and about 1.05:1 in dark, which is a pill
   with no words on it. This is the whole reason a light hue may never carry type.

   --fill-primary/--on-fill instead, so the two asks on this surface are one object at two
   moments: the same pill, mid-call and after it. */
.fd-ov-audit-open {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 11px 22px;
  border-radius: 999px;
  background: var(--fill-primary);
  color: var(--on-fill);
  font-family: var(--font-sans);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  transition: background-color 140ms ease;
}

.fd-ov-audit-open:hover,
.fd-ov-audit-open:focus-visible { background: var(--fill-primary-hi); }

/* WHAT HAPPENS NEXT — see the block's own note in shared/_front_desk_overlay.html.erb. Shown on
   `ended` only, the same state-picks-the-copy idiom as .fd-ov-ended-line above, so nothing here
   is written from JS and it cannot appear mid-call. Not on `refused`: no call happened there.

   Column at every width rather than a row that wraps: the two asks are not peers — one books
   time, the other declines to — and side by side at 390px they became two half-width boxes of
   equal weight, which is the choice the hierarchy is trying not to offer. */
.fd-ov-next { display: none; }
.fd-overlay[data-state="ended"] .fd-ov-next {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  width: 100%;
  max-width: 420px;
  margin: 14px auto 0;
}

/* The primary. A filled pill, because it is the one thing on this surface asking for a decision,
   and every CTA on the site is a pill that clears 44px — this is the same object, on a surface
   that happens to be a takeover. --fill-primary/--on-fill rather than the stage gold: booking is
   the SITE's ask, not the Front Desk's, and it reads the same here as it does in the footer. */
.fd-ov-next-book {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 11px 24px;
  border-radius: 999px;
  background: var(--fill-primary);
  color: var(--on-fill);
  font-family: var(--font-sans);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  /* COLOUR TRANSITIONS WITH THE FILL, and it did not until 2026-09-06. This button demotes to
     underlined text while the message form is open and returns to a pill when it closes, and only
     background-color was transitioned — so on the way back the type snapped to --on-fill white
     instantly while the cobalt faded in over 140ms, leaving white text on a near-white ground for
     the whole of it. A flash of invisible label, on the one control that is the site's ask. */
  transition: background-color 140ms ease, color 140ms ease;
}

.fd-ov-next-book:hover,
.fd-ov-next-book:focus-visible { background: var(--fill-primary-hi); }

/* The secondary, and deliberately not a second pill: a declining option that looks as pressable
   as the one above it is a fork, not a hierarchy. Underlined text at the same 44px target.

   THE BOOKING TAKES THIS TREATMENT TOO ONCE THE FORM IS OPEN (2026-09-06, Jason), which is the
   same argument one state further on. The form's own submit became the site's cobalt pill when
   this surface moved onto the Prism II form vocabulary, and it lands directly above the booking
   pill: two filled cobalt asks, same size, same colour, stacked, with nothing saying which one
   the visitor came here to press. Inside an open form the submit is the ask and a calendar is
   the distraction, so the booking steps back rather than the submit stepping down. It returns to
   a filled pill the moment the form closes, because with no form on screen it IS the ask. */
.fd-ov-next-message,
.fd-overlay[data-state="ended"] .fd-ov-message:has(.fd-ov-message-form:not([hidden])) ~ .fd-ov-next .fd-ov-next-book {
  min-height: 44px;
  padding: 8px 12px;
  border: 0;
  background: none;
  font-family: var(--font-sans);
  font-size: 15px;
  font-weight: 400;
  color: var(--ink-muted);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color 140ms ease;
}

.fd-ov-next-message:hover,
.fd-ov-next-message:focus-visible,
.fd-overlay[data-state="ended"] .fd-ov-message:has(.fd-ov-message-form:not([hidden])) ~ .fd-ov-next .fd-ov-next-book:hover,
.fd-overlay[data-state="ended"] .fd-ov-message:has(.fd-ov-message-form:not([hidden])) ~ .fd-ov-next .fd-ov-next-book:focus-visible {
  background: none;
  color: var(--ink);
}

/* Once the form is open, the button that opened it is a control that does nothing — and on a phone
   it would sit behind the sheet it just raised. .fd-ov-message precedes .fd-ov-next in the
   overlay's DOM order, so a sibling combinator reaches it; if those two are ever reordered this
   rule stops matching and the button simply stays visible, which is the harmless direction. */
.fd-overlay[data-state="ended"] .fd-ov-message:has(.fd-ov-message-form:not([hidden])) ~ .fd-ov-next .fd-ov-next-message {
  display: none;
}

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

/* NO RADIUS. Rounded is reserved for actions on this site, and a panel holding a form
   is not one. The mobile sheet below keeps its 16px top corners and its drag handle
   keeps 2px, and both are deliberate: those say the surface can be dragged, which is
   function rather than decoration. */
.fd-ov-message-form {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 10px;
  padding: 14px;
  /* --line2, NOT --line. The fields inside carry --field-line at 3.4:1 because a control's
     boundary has to clear 3:1, and the panel around them was on the decorative hairline at about
     1.15:1 — so the container was fainter than its own contents and the group read as four
     floating boxes rather than one panel. This is a decorative edge and the 1.4.11 floor does not
     apply to it, but it does have to hold its own children visually. */
  border: 1px solid var(--line2);
}

/* 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,
.fd-ov-message-close { 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: 13px;
  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: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* Lowercase beside an uppercase label, at the same size and colour — a difference in voice rather
   than in strength, which is exactly how .form-optional reads it on /audit. NO opacity: that file
   records measuring --ink3 at 6.53:1 and an opacity:.75 on top of it compositing down to 3.65:1,
   under the 4.5 required at 12px, in a way that still passed in dark mode and looked deliberate. */
.fd-ov-message-optional {
  margin-left: 6px;
  letter-spacing: 0.04em;
  text-transform: none;
}

/* The reassurance under the email. Same size and colour as the hint above the form, because it is
   the same kind of speech: the surface talking, not the visitor being labelled. */
.fd-ov-message-note {
  margin: 6px 0 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ink-faint);
}

/* THE PAINT COMES FROM site_07_forms.css NOW, AND ONLY THE SIZE IS HERE (2026-09-06).
   These controls carry .form-input / .form-textarea in the markup, so the square corners,
   the --field-line boundary, the placeholder colour, the hover border and the invalid
   state all arrive from the one place the site defines a field. That file's own header
   says why: a second copy of the field vocabulary drifts one input at a time, and this
   was that second copy — 15px type, a 4px radius and the decorative --line at about
   1.2:1 where a control's boundary needs 3:1.

   THIS FORM IS DELIBERATELY SMALLER THAN /audit AND /contact (Jason), which is the only
   reason anything is written here at all. Those give a field 48px and 12px 14px; this one
   is compact at 44px, and 44 rather than the 42 it measured because that is the floor on
   anything pressed and this form's real home is the mobile sheet, under a thumb.

   SPECIFICITY IS THE MECHANISM: this selector is (0,1,1) and .form-input is (0,1,0), so
   these two declarations win and everything deleted above falls through to site_07. Adding
   a colour back here would win too, silently, which is what the test guards. */
.fd-ov-message-row textarea,
.fd-ov-message-row input {
  min-height: 44px;
  padding: 10px 12px;
}

/* Two lines, where /audit's is six. Same reason as the fields above: this is a note left
   in passing during a call, not a considered message typed on a page built for it. */
.fd-ov-message-row textarea { min-height: 66px; }

.fd-ov-message-row textarea:focus-visible,
.fd-ov-message-row input:focus-visible {
  /* :focus-visible, NOT :focus. On :focus this removed the outline for KEYBOARD users too and left
     a 1px border-colour change as the only indicator on the one form in this surface — invisible
     against the resting border at a glance. The border change stays as the visual refinement; the
     ring stays for anyone arriving by Tab. */
  /* --fill-primary, not the stage ink. site_07 gives every field on the site the same ring
     as the composer's ask pill, so there is one answer to "where am I" rather than one per
     component — and the submit below is now that same cobalt, so a gold ring here would be
     the only one of its kind on a form. */
  outline: 2px solid var(--fill-primary);
  outline-offset: 2px;
  border-color: var(--ink);
}

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

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

/* THE SITE'S PILL, ONE SIZE DOWN. The button carries .btn.primary in the markup, so the
   999px radius, the cobalt fill, the reversed type and the 44px floor all come from
   site_00_foundation. It was a near-black 12px mono rectangle at 37px tall, sitting 60px
   above the cobalt Calendly pill in the same panel — two button vocabularies in one
   overlay. press_states_test has listed this selector among the site's PILLS the whole
   time, so the press state was already a pill's while the paint was not.

   TWO CLASSES, NOT ONE, AND THAT IS LOAD-BEARING. .btn declares font-size: 16px at
   (0,1,0) in a file that loads AFTER this one, so a single-class override here loses on
   source order and the button silently renders at the full site size. Scoping to the form
   makes it (0,2,0) and it wins. The mobile sheet's align-self: stretch is untouched. */
.fd-ov-message-submit { align-self: flex-end; }

.fd-ov-message-form .fd-ov-message-submit {
  font-size: 13px;
  padding: 10px 18px;
}

/* The in-flight state. Dimmed rather than restyled, so the button does not change size mid-press
   and shift the row under the visitor's finger. :disabled is what actually blocks the second
   press; this is what says so. */
.fd-ov-message-submit.is-busy { opacity: 0.72; cursor: progress; }

/* --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));
  }

  /* --field-line, for the same reason the fields inside take it: this tab is an interactive
     control and the ONLY way into the sheet on a phone, and it was drawn on the decorative
     hairline at about 1.15:1 — the exact failure the field border was moved off. */
  .fd-ov-message-peek {
    display: none;
    width: 100%;
    align-items: center;
    gap: 8px;
    /* 44px, measured at 43 in headless Chrome at 428x926 before this line existed. Padding plus a
       13px line box landed one pixel short, which no amount of reading the rule would have shown
       — and this tab is the ONLY way back into the sheet on a phone, so it is the last control on
       the surface that should be under the floor. */
    min-height: 44px;
    padding: 10px 14px;
    border: 1px solid var(--field-line);
    /* 999px, not the 10px it carried. Rounded is reserved for actions on this site and this is
       one, so a radius is allowed — but 10 belonged to nothing, and the pill is what this design
       system means by "pressable". */
    border-radius: 999px;
    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: 12px; }

  .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 color-mix(in srgb, var(--ink) 14%, transparent);
    /* 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. */
  /* The sheet's visible close, beside the drag handle. 44px like everything pressed here, and it
     is a real button so Tab reaches it and a screen reader announces it — the handle it sits next
     to is aria-hidden and pointer-only, and the peek tab that opened the sheet is display:none
     while the sheet is open, so before this there was no exit in the accessibility tree at all.
     Positioned rather than in flow so it cannot push the hint and the first field down. */
  .fd-ov-message[data-sheet="expanded"] .fd-ov-message-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 4px;
    right: 8px;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--ink-muted);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
  }
  .fd-ov-message[data-sheet="expanded"] .fd-ov-message-close:hover,
  .fd-ov-message[data-sheet="expanded"] .fd-ov-message-close:focus-visible { color: var(--ink); }

  .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: color-mix(in srgb, var(--ink) 18%, transparent);
  }

  /* 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: 12px;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 34px 0 12px;
}

/* THE DECK'S RIGHT COLUMN (2026-09-04) — a left hairline marks the seam with .fd-stage instead of
   the top one this used to carry when the toolkit sat stacked below the call row. Neither .ar-deck
   nor .ct-deck (site_07_forms.css) draws a rule between their own two columns, relying on gap
   alone — this one keeps a visible divider anyway, since the picked design wanted a clear seam
   between the centred orb cluster and this left-aligned tool list; a deliberate departure from
   that precedent, not an oversight. Overrides .fd-sec-label's own 34px top margin, which stays
   as-is for "Your last call" below (still unboxed, still wants that much air above it). */
.fd-module-toolkit {
  border-left: 1px solid var(--line);
  padding-left: 28px;
}
.fd-module-toolkit .fd-sec-label { margin-top: 0; }

/* Once the deck stacks (.fd-module's own 980px breakpoint, above), a left rule reads as a stray
   mark rather than a seam between columns — the divider flips to the top, same as it was before
   this page had two columns at all. */
@media (max-width: 980px) {
  .fd-module-toolkit {
    border-left: none;
    padding-left: 0;
    border-top: 1px solid var(--line);
    padding-top: 24px;
  }
}

/* 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. */
/* Two labelled groups down the page rather than one wrapping row of ten — see the markup's note
   and Labs::FrontDesk::ToolGuide::GROUPS. The wrappers are role="presentation", so this is purely
   visual grouping over a single flat tab sequence. */
.fd-kit-pills {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin: 0 0 14px;
}

.fd-kit-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

/* The group's name, in the data voice at the mono floor. --ink-muted rather than the stage ink:
   it names a set, it is not one of the things in it, and giving it the same gold as the tabs put
   a ninth gold object in a row of eight. */
.fd-kit-group-label {
  flex-basis: 100%;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* 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-flex;
  align-items: center;
  /* 44px, like every other thing on this site a thumb has to find. It was 31px (12px type in 5px
     of padding) — the smallest target on the page, eight of them, wrapping to several rows on a
     phone. Padding does the work rather than a fixed height so a wrapped label still fits. */
  min-height: 44px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.01em;
  color: var(--stage-ink, var(--clay));
  border: 1px solid color-mix(in srgb, var(--stage-ink, var(--clay)) 30%, transparent);
  background: color-mix(in srgb, var(--stage, var(--ember)) 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, var(--clay)); }

/* SELECTION USED TO MAKE THE LABEL HARDER TO READ, which is the wrong way round for the one tab
   whose blurb is on screen. The only difference between states was the wash — 8% to 26% of the
   stage gold, about 1.6:1 between the two fills — and deepening it dropped the selected label from
   5.7:1 to 3.6:1 in light and 2.8:1 in dark, i.e. under AA for 12px type, while the nine you had
   not picked stayed comfortably above it. So the state was told almost entirely by a 1px border,
   and told at the cost of the thing being selected.
   The wash therefore stays at the resting value and the state is carried by the border alone
   (2026-09-04: dropped the bold weight and the 2px left rule — Jason — a full-strength border on
   its own reads clearly enough without either). */
.fd-kit-pill[aria-selected="true"] {
  border-color: var(--stage-ink, var(--clay));
}

/* 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: 15px;
  line-height: 1.6;
  color: var(--ink-muted);
  border: 1px solid color-mix(in srgb, var(--stage-ink, var(--clay)) 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: 13px;
  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-sans);
  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: 12px;
  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: 12px;
  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: 12px;
  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: 12px;
  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: 12px;
  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: 12px;
  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: 13px;
  color: var(--ink-faint);
}

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

/* THE ONE PLACE IN /labs THAT KEEPS THE SERIF (Prism II, 2026-09-01). Newsreader
   appears in exactly three roles on this site — the triad's human reading, every
   quotation, and About's opening line — and that scoping is what makes the face
   mean "a human is speaking" rather than "you are in a decorative mood". This
   renders the CALLER'S OWN WORDS, lifted out of a transcript, which is a
   quotation. Every other serif in the labs stylesheets was a heading or a verdict
   and moved to the display face when the rest of the site did. */
.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: 12px;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  text-align: center;
  margin: 10px 0 0;
}

/* NOT IN THE REFUSED STATE (2026-09-03), where no AI has spoken and none is going to: a blocked
   microphone, no input device, an insecure origin or a spent rate limit. "AI can make mistakes."
   under a screen explaining that the call never started is a caption about something that did not
   happen, sitting directly beneath the sentence a visitor actually needs to read. It is a
   convention for a transcript, and in that state there is no transcript. */
.fd-overlay[data-state="refused"] .fd-disclaimer { display: none; }

/* 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: 12px;
  white-space: nowrap;
  overflow: hidden;
  /* IT IS A <p>, AND application.css CAPS EVERY <p> AT 64ch. So this "full width" banner was
     461px inside a 1376px stack, with its hairline stopping a third of the way across and the
     centred text centred inside that third — measured live at 1440px, and it had been shipping
     that way. A measure is right for prose and wrong for a rule that is meant to span the
     surface; nothing raises, and at phone widths, where the cap never binds, it looks correct. */
  max-width: none;
}

/* 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; }

/* ============================================================
   THE TRANSCRIPT'S BUBBLES
   ============================================================
   .pg-chat is a shared component with four consumers — the Proving Ground's bout and
   exhibit, Signal Line, this overlay, and /admin/calls. This one declaration is scoped to
   .fd-chat, which the overlay's JS and the admin transcript both carry, so the other two
   are untouched. .fd-chat and not .fd-transcript: that one owns the overlay panel's
   max-height and overflow, and the admin page's full transcript must not become a
   scroller.

   THE SIDES ARE INVERTED HERE, WHICH IS WHY ONLY THE CUSTOMER SIDE IS WRITTEN. In labs_08
   "customer" means the LEFT column and "agent" the RIGHT — sides, not roles — and
   front_desk_controller.js maps assistant→customer, user→agent, so Front Desk sits left
   and the visitor right.

   THE VISITOR IS DELIBERATELY NOT PAINTED HERE. labs_08 already gives its agent side the
   ice wash and the cobalt hairline, measured per theme through --pg-bubble-tint (55% light,
   36% dark), and on this surface that side IS the visitor. So the visitor's bubble is
   labs_08's own rule, inherited. Writing a second copy of that wash in this file is the
   drift this block used to be: two measurements of one thing, kept in step by hand.

   THE PERSON TYPING IS THE ONE IN COLOUR (2026-09-06), which is the chat convention and
   not the spectrum's. This block previously read the other way — cool on the machine, warm
   on the person — and paired ice against apricot: two tints of equal weight and opposite
   temperature, competing down the page. That pairing is retired. The Front Desk now takes
   the page's warm grey and no hue at all, so the transcript has one colour in it, on the
   side a reader's eye follows.

   --bg2 needs no dark step: it is a ground token and steps itself (#F5F1E9 → #1C1915),
   unlike a light hue mixed against --bg, which is what forced the two dark blocks here
   before. The 3px tail corner is still not touched — it follows the side, not the speaker,
   and that is what makes the two columns read as two people. */
.fd-chat .pg-chat-bubble-customer { background: var(--bg2); }
