/* Digital Dream AI — product shell styles. Every value comes from styles/tokens.css.
   Round r2026-08-22-03: the shell is the approved v2 pastel-lilac twilight, and the Home + dream-capture
   surfaces are built from the reference set at the 390x844 viewport. */
*, *::before, *::after { box-sizing: border-box; }
html { height: 100%; background: var(--dd-backdrop-base); scroll-padding-top: var(--dd-s6); }
body {
  margin: 0; min-height: 100dvh; color: var(--dd-text);
  font-family: var(--dd-font-body); font-size: var(--dd-fs-body); line-height: var(--dd-lh-body);
  /* the canvas lives on html (and in the first-paint block, which TS-39 pins). Once html carries a background, a
     background on body paints in the root stacking context OVER every negative-z-index layer — which is what made
     the reference washes and the diagonal sheet invisible. body is transparent from here on. */
  background: transparent;
  -webkit-font-smoothing: antialiased; text-wrap: pretty;
}
[hidden] { display: none !important; }
/* DF-32: the icon/gradient sprite must stay renderable. display:none (the [hidden] attribute) on an outermost svg
   removes its subtree from rendering, and a paint-server reference into such a subtree behaves as if the target
   did not exist — the Home crescent lost its url(#moon-grad) fill and vanished. Zero size hides the sprite without
   detaching its defs. */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* Atmosphere — 1:1 with the reference's own two washes. They are ROTATED ellipses
   (washA: centre 330,165 radii 210x180 rotate 135deg; washB: centre 72,704 radii 180x165 rotate -35deg), which a
   CSS radial-gradient cannot express — the previous approximation used unrotated percentage ellipses. Each wash is
   now its own rotated layer with the reference's radii, colours and stops, anchored to the corner it belongs to.
   Both sit on a fixed, pointer-transparent layer behind everything: no layout box, no shift (M-WV-03). */
body::before {
  content: ""; position: fixed; inset: 0; z-index: -2; pointer-events: none;
  background: linear-gradient(160deg, var(--dd-backdrop-base) 0%, var(--dd-backdrop-sheet) 100%);
}

.wash { position: fixed; z-index: -1; pointer-events: none; border-radius: 50%; }
.wash--a { width: 420px; height: 360px; right: -150px; top: -15px; transform: rotate(135deg);
  background: radial-gradient(closest-side, var(--dd-wash-a) 0%, transparent 100%); }
.wash--b { width: 360px; height: 330px; left: -108px; bottom: -25px; transform: rotate(-35deg);
  background: radial-gradient(closest-side, var(--dd-wash-b) 0%, transparent 100%); }
/* the controlled star field: pearl-gold, dusty blue, lilac and blush, never rainbow confetti */
/* the controlled star field, 1:1 with the reference: the box is the reference's own 294..390 x 0..130 region,
   pinned to the top-right corner so every shape keeps its exact offset from that corner at any viewport */
.sky { position: fixed; top: 0; right: 0; width: 96px; height: 130px; z-index: -1; pointer-events: none; }

.skip { position: absolute; left: var(--dd-s4); top: -100px; padding: var(--dd-s3) var(--dd-s4); background: var(--dd-surface); color: var(--dd-text); border: 1px solid var(--dd-border); border-radius: var(--dd-r1); z-index: 1000; }
.skip:focus-visible { top: var(--dd-s4); }
:focus-visible { outline: 3px solid var(--dd-focus); outline-offset: 3px; }
/* a heading is focused programmatically so a screen reader lands on the new screen; it is never in the tab order,
   so it must not paint a keyboard ring */
[tabindex="-1"]:focus { outline: none; }

/* screen: one grid cell, every state stacks in the same area so swaps never move layout */
.screen {
  display: grid; place-items: start center; min-height: 100dvh;
  padding: max(var(--dd-s7), env(safe-area-inset-top)) max(var(--dd-s5), env(safe-area-inset-right))
           max(calc(var(--dd-s7) + var(--dd-control-h)), env(safe-area-inset-bottom)) max(var(--dd-s5), env(safe-area-inset-left));
}
/* F-02, F-03 (browser-proven at 320 px / German / 200% text / a 240-character unbroken token): a grid item's
   automatic minimum size is its min-content width, so one long localised word or one unbreakable user string could
   push the whole column past the viewport. Zero-min on every shared item plus a declared wrapping policy for text
   roles keeps the page one-dimensional without shrinking type or forking a locale. */
.state, .state > *, .card, .card > *, .action, .footnote, .doclist, .doclist__item, .doclist__btn { min-width: 0; }
.title, .lede, .body, .action__label, .action__hint, .card__note, .notice p, .footnote p, .transcript { overflow-wrap: anywhere; }
.state { grid-area: 1 / 1; width: var(--dd-content-w); display: grid; gap: var(--dd-s4); align-content: start; }

/* loader */
/* First-load motion policy (DF-18, TS-39): the loader asset is painted with the document, so on a fast start its
   animation would begin and end inside a window nobody can read — the flash the owner reported. src/app.js releases
   the animation only after loaderAnimateAfterMs (src/config/ui-timing.js); until then the crescent is a still,
   branded first screen. Pausing costs nothing: no animation frames are produced while the state is paused. */
html:not([data-loader="animate"]) .moon-loader * { animation-play-state: paused; }
/* Paused at the first keyframe the stars sit at 0.12 opacity, which reads as an empty sky. The still screen uses the
   same value the reduced-motion screen uses, so the calm frame is legible and both static views look identical. */
html:not([data-loader="animate"]) .moon-loader .ml-star { opacity: 0.7; }
/* DF-29: the loader crescent and the Home emblem are the same moon, so they must be the same moon on screen. The
   canvas width comes from --dd-moon-loader-w (derived from --dd-moon-box), and this padding puts the crescent's
   centre exactly where Home puts its emblem's centre: the brand line, the grid gap and half the emblem box, minus
   the distance from the top of the loader svg to the crescent centre. The moon therefore stays put through the
   handover instead of jumping and resizing. */
.state--loader { justify-items: center; z-index: 1;
  padding-top: calc(var(--dd-brand-box) + var(--dd-s4) + var(--dd-moon-box) / 2 - var(--dd-moon-loader-centre)); }
/* DF-26, DF-28: the loader is a splash over the shell, not one of the stacked states. Every state view lives in the
   same grid cell (grid-area: 1 / 1), and the declared 3 s window keeps the loader on screen while the shell already
   dispatched the skeleton, so the owner saw the skeleton's disc and boxes through the crescent. The rule is
   structural rather than a list of state names: while #loader is not [hidden], every sibling state view is invisible
   — including screens added in later rounds — and the loader stacks above them. visibility keeps their boxes, so
   the handover to the skeleton or to Home still shifts nothing (M-WV-03). */
#loader:not([hidden]) ~ .state { visibility: hidden; }
/* Handover exception (DF-33, DF-34): while the splash dissolves onto Home, ONLY Home may paint under it. The
   skeleton must never become visible here — that is DF-28 (a bare disc showing through the splash) coming back, so
   the exception names #content and nothing else. The splash stops intercepting taps the moment it starts, so the
   mic is live immediately. */
#loader.is-handoff:not([hidden]) ~ #content { visibility: visible; }
#loader.is-handoff { pointer-events: none; }
/* DF-25: the loader is inline SVG, so the browser painted it as it parsed — the halo disc, then a full moon, then
   the crescent — which read as three different objects assembling. Modules are deferred, so src/app.js runs exactly
   when parsing is done: it writes data-loader, and that is the one moment the finished drawing may appear. Before it
   the shell shows the painted canvas and the label, so nothing about first paint regresses. */
/* F-04: the loader visual was width-driven only, so compact-height landscape (568x320) left ~11 px between the
   label and the edge. The canvas is now bounded by the available height too. Geometry is untouched — one canonical
   viewBox, one aspect ratio; only the wrapper's budget changes, and the handover measures live rects, so the
   landing stays exact at any size. */
.moon-loader { width: min(var(--dd-moon-loader-w), 52vh); height: auto; display: block; opacity: 0; transform: translateX(var(--dd-moon-loader-nudge)); }
html[data-loader] .moon-loader { opacity: 1; transition: opacity var(--dd-dur-trans) var(--dd-ease-out); }
/* Moon-loader motion and paint. It used to be duplicated inside both the asset and its inline copy; one place is
   the rule (SINGLE_CODEBASE_RULES), so the shipped SVG is geometry only and every timing lives here:
   6.2s / 7s / 5.4s internal drift, 4.8s stars, 3.2s trace travel, 13.5s reveal — the approved values, unchanged. */
@keyframes ml-b1 { 0%, 100% { transform: translate(0, 0) scale(1); } 33% { transform: translate(26px, 18px) scale(1.08); } 66% { transform: translate(-14px, 34px) scale(0.95); } }
@keyframes ml-b2 { 0%, 100% { transform: translate(0, 0) scale(1.05); } 50% { transform: translate(-30px, -24px) scale(0.9); } }
@keyframes ml-b3 { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(24px, -30px); } }
@keyframes ml-twinkle { 0%, 100% { opacity: 0.12; transform: scale(0.7); } 50% { opacity: 0.95; transform: scale(1.25); } }
@keyframes ml-travel { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -72; } }
@keyframes ml-reveal { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }
.moon-loader .ml-b1 { animation: ml-b1 6.2s ease-in-out infinite; transform-origin: center; }
.moon-loader .ml-b2 { animation: ml-b2 7s ease-in-out infinite; transform-origin: center; }
.moon-loader .ml-b3 { animation: ml-b3 5.4s ease-in-out infinite; transform-origin: center; }
.moon-loader .ml-star { transform-box: fill-box; transform-origin: center; animation: ml-twinkle 4.8s ease-in-out infinite; }
.moon-loader .ml-s2 { animation-delay: 0.8s; }
.moon-loader .ml-s3 { animation-delay: 1.7s; }
.moon-loader .ml-s4 { animation-delay: 2.6s; }
.moon-loader .ml-s5 { animation-delay: 3.3s; }
/* the traced path reads dusty blue → lilac → blush → pearl-gold so its direction is legible; it stays an
   indeterminate startup indicator and never reports a percentage */
.moon-loader .ml-trace { fill: none; stroke: url(#ml-trace-gradient); stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 6 12; opacity: 0.85; animation: ml-travel 3.2s linear infinite; }
.moon-loader .ml-reveal-path { animation: ml-reveal 13.5s cubic-bezier(0.18, 0.68, 0.22, 1) forwards; }
.loader__label { margin: 0; color: var(--dd-text-muted); font-size: var(--dd-fs-body); text-align: center; }

/* skeleton (.sk) — the same boxes Home paints, so the swap never shifts layout (TS-08).
   The sweep is a transform on an overlay, not an animated background-position: a moving background repaints
   every box on the main thread every frame, which on a mid-range Android is main-thread work spent on a
   placeholder (WV-TBT/WV-INP lever "long tasks, main-thread work during input"). A translated overlay is
   composited, so the skeleton costs no main-thread paint while the shell is still starting. */
.sk { position: relative; overflow: hidden; border-radius: var(--dd-r1); background: var(--dd-surface-2); }
.sk::after { content: ""; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent 0%, var(--dd-surface) 50%, transparent 100%);
  animation: dd-sk-sweep 1.4s linear infinite; }
.sk--brand { height: var(--dd-brand-box); width: 96px; }
.sk--moon { height: var(--dd-moon-box); width: var(--dd-moon-box); border-radius: 50%; justify-self: center; }
/* F-01: the title and the lede placeholders reserve height by laying out the real localized string in the real
   typography, invisibly — no fixed line count. Every other role keeps its declared control geometry, which does not
   depend on text. */
.sk--title { min-height: var(--dd-title-box); width: 100%; }
.sk--lede { min-height: var(--dd-lede-box); width: 100%; }
/* the ghost must carry EVERY property that affects line breaking, or it reserves a different number of lines than
   the role it stands for — overflow-wrap included (it changes both min-content size and the break opportunities) */
.sk__ghost { display: block; color: transparent; visibility: hidden; overflow-wrap: anywhere; }
.sk--title .sk__ghost { font-family: var(--dd-font-display); font-size: var(--dd-fs-title); line-height: var(--dd-lh-title); font-weight: 760; letter-spacing: -0.01em; text-wrap: balance; }
.sk--lede .sk__ghost { font-size: var(--dd-fs-lead); line-height: var(--dd-lh-body); }
.sk--mic { height: var(--dd-mic-ring); width: var(--dd-mic-ring); border-radius: 50%; justify-self: center; }
.sk--button { height: var(--dd-control-h); width: 100%; border-radius: var(--dd-pill); }
@keyframes dd-sk-sweep { to { transform: translateX(100%); } }

/* type */
/* every product paragraph carries its own rhythm through the grid gap, never through UA margins — the
   skeleton predicts the same boxes, so a stray 1em margin would shift the swap (TS-08) */
.brand, .action, .topbar, .card__head, .card__meta, .card__note, .assure, .notice__head { margin: 0; }
/* DF-45: the mark's viewBox (22 44 16 17) already carries the reference's own vertical relation to the wordmark, so
   the box is anchored to the text baseline rather than to the centre of the line box. Centred, the star landed 3.9 px
   below where 01_HOME.svg draws it (the reference puts its centre 5 px above the DREAM baseline, centring left it
   1.1 px above). Both margins are the reference's own numbers, not tuning: the viewBox bottom (y 61) sits 1 px below
   the DREAM baseline (y 60), and its top (y 44) sits 4 px above the column top (y 48) — the reference really does let
   the violet dot (y 47..51) rise past the column edge. Together they put the mark's box at 44..61 and the wordmark
   baseline at 60, which is the reference verbatim. The wordmark drops to line-height 1 for the same reason: with the
   body's 1.6 the half-leading above the cap made the TEXT the tallest thing over the baseline, so it — not the
   reference geometry — decided where the line sat, and the whole lockup rode 2 px low. */
.brand { display: flex; align-items: baseline; gap: 6px; height: var(--dd-brand-box); }
.brand__mark { width: 16px; height: 17px; flex: none; align-self: baseline; margin: -4px 0 -1px; }
.brand__dot { fill: var(--dd-accent); opacity: 0.5; }
.brand__word { font-size: var(--dd-fs-eyebrow); font-weight: 750; letter-spacing: var(--dd-track-eyebrow); color: var(--dd-text-muted); text-transform: uppercase; line-height: 1; }
.title { margin: 0; font-family: var(--dd-font-display); font-size: var(--dd-fs-title); line-height: var(--dd-lh-title); font-weight: 760; min-height: var(--dd-title-box); text-wrap: balance; letter-spacing: -0.01em; }
/* every capture screen's title is 30 px and left-aligned in the references; Home's is 32 px and centred */
.state--capture .title, .state--error .title { font-size: var(--dd-fs-title-capture); }
/* DF-47: Home and its skeleton carry the Home subtitle size. Declared as a custom-property override on the subtree
   rather than a font-size rule, so everything derived from it follows in lockstep — .lede, --dd-lede-box (the
   min-height reservation) and the skeleton's own ghost — and the swap cannot start reserving a different box from
   the one the content fills. */
.state--content, .state--skeleton { --dd-fs-lead: var(--dd-fs-lead-home); }
.lede { margin: 0; min-height: var(--dd-lede-box); font-size: var(--dd-fs-lead); color: var(--dd-text-muted); }
.body { margin: 0; min-height: var(--dd-body-box); color: var(--dd-text-muted); max-width: 65ch; }
/* HOME + SKELETON vertical rhythm — 1:1 with uploads/01_HOME.svg (see the gap tokens). One gap value for the whole
   column was the F-01 cause: the skeleton and Home agreed with each other but neither agreed with the reference. */
.state--content, .state--skeleton { gap: 0; }
.state--content .moon, .sk--moon { margin-top: var(--dd-gap-brand-moon); }
.state--content .title, .sk--title { margin-top: var(--dd-gap-moon-title); }
.state--content .lede, .sk--lede { margin-top: var(--dd-gap-title-lede); }
.state--content .mic, .sk--mic { margin-top: var(--dd-gap-lede-mic); }
.state--content .action { margin-top: var(--dd-gap-mic-action); }
.state--content .btn--secondary { margin-top: var(--dd-gap-action-cta); }
.sk--button { margin-top: calc(var(--dd-gap-mic-action) + var(--dd-gap-action-cta) + var(--dd-fs-body) * var(--dd-lh-body) + var(--dd-fs-small) * var(--dd-lh-body) + 3px); }
.state--content .footnote { margin-top: var(--dd-gap-cta-footnote); }
.state--content { text-align: center; }
.state--content .brand { justify-content: flex-start; text-align: left; }

/* Home emblem — 1:1 with uploads/01_HOME.svg. Every value below is the reference's own; nothing is styled by taste. */
.moon { width: var(--dd-moon-box); height: var(--dd-moon-box); display: block; justify-self: center; overflow: visible; }
.moon__halo { fill: var(--dd-brand-halo); opacity: 0.44; }
.moon__cut { fill: var(--dd-moon-cut); }
.moon__shadow { flood-color: var(--dd-brand-plum); flood-opacity: 0.16; }
/* gradient stops take stop-color, not fill: with `fill` the stop falls back to black and the moon paints as a dark
   disc — the artefact this round removed everywhere else */
.moon__sheen { fill: var(--dd-sheen); }
.g-s1 { stop-color: var(--dd-brand-blue); }
.g-s2 { stop-color: var(--dd-brand-violet); }
.g-s3 { stop-color: var(--dd-brand-blush); }
.g-s4 { stop-color: var(--dd-brand-gold-light); }
.sky__gold { fill: var(--dd-brand-gold); }
.sky__lilac { fill: var(--dd-accent); opacity: 0.42; }
.sky__blue { fill: var(--dd-brand-blue-deep); opacity: 0.45; }
.sky__blush { fill: var(--dd-brand-blush); opacity: 0.72; }
/* the reference gives the sky field and the emblem's own shapes slightly different opacities — kept exactly */
.sky .sky__gold { opacity: 0.95; }
.moon .sky__lilac { opacity: 0.5; }
.moon .sky__blue { opacity: 0.55; }
/* the mic outline, 1:1 with the reference: stroke #2D2142 at 2.6, no fill. The `.title` fragment welded to this
   selector in an earlier round made it match nothing, so the glyph fell back to a solid black fill. */
.mic__glyph, .btn__glyph { fill: none; stroke: currentColor; stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; }
/* The JS-disabled fallback must never paint while scripting is on. Browsers hide <noscript> themselves, but its
   content is raw text and print/PNG/PPTX rasterisers have been seen to lay it out anyway; this states the intent. */
noscript { display: none; }
.btn__glyph { stroke-width: 3; }
.emblem__mic { fill: none; color: var(--dd-text-muted); stroke: currentColor; stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; }
.emblem__slash { stroke: var(--dd-error-mark); stroke-width: 3; stroke-linecap: round; }
.iconbtn svg { fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; color: var(--dd-text); }

/* controls */
.btn { min-height: var(--dd-control-h); padding: 0 var(--dd-s5); border: 0; border-radius: var(--dd-pill);
  font: inherit; font-weight: 700; cursor: pointer; touch-action: manipulation; color: var(--dd-on-primary);
  transition: opacity var(--dd-dur-micro) var(--dd-ease-out), transform var(--dd-dur-micro) var(--dd-ease-out); }
/* Shadow — 1:1 with the reference filter: feDropShadow dy=10 stdDeviation=15 means a CSS blur radius of 30 (twice
   the deviation), not 15. Every disc and card the reference filters carries exactly this. */
.btn--primary { min-height: var(--dd-cta-h); width: 100%; background: linear-gradient(90deg, var(--dd-primary) 0%, var(--dd-primary-2) 100%); box-shadow: 0 10px 30px var(--dd-shadow-soft); }
.btn--secondary { width: 100%; background: var(--dd-surface-2); border: 1px solid var(--dd-border); display: inline-flex; align-items: center; justify-content: center; gap: var(--dd-s2); font-weight: 750; }
.btn--ghost { background: transparent; color: var(--dd-text); border: 1px solid var(--dd-border); }
.btn__glyph { width: 18px; height: 26px; flex: none; }
.btn:active { transform: scale(0.98); }
/* DF-55: an <a> wearing .btn is not a <button>. It brings a UA underline, aligns its label to the inline start and,
   being a block box rather than a button's centred anonymous one, drops the label at the TOP of the pill instead of
   its middle — the embedded escape action was the first anchor in the product to wear .btn and painted underlined,
   left-aligned and 33 px high in a 56 px pill. Scoped to a.btn so the pixel-verified <button> geometry is untouched. */
a.btn { display: inline-flex; align-items: center; justify-content: center; text-align: center; text-decoration: none; }
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.iconbtn { width: var(--dd-control-min); height: var(--dd-control-min); min-height: var(--dd-control-min); padding: 0; border-radius: 50%;
  background: var(--dd-surface-2); border: 1px solid var(--dd-border); display: grid; place-items: center; cursor: pointer; }
.iconbtn svg { width: 20px; height: 20px; }
.topbar { display: flex; justify-content: flex-start; }

/* Home: one dominant voice action, one secondary text route */
.mic { width: var(--dd-mic-ring); height: var(--dd-mic-ring); min-height: var(--dd-mic-ring); padding: 0; border-radius: 50%;
  background: var(--dd-mic-face); border: 1px solid var(--dd-border); box-shadow: 0 10px 30px var(--dd-shadow-soft);
  display: grid; place-items: center; cursor: pointer; justify-self: center; }
.mic__core { width: var(--dd-mic-core); height: var(--dd-mic-core); border-radius: 50%; display: grid; place-items: center;
  background: linear-gradient(90deg, var(--dd-mic-core-1) 0%, var(--dd-mic-core-2) 100%); }
.mic__glyph { width: 32px; height: 46px; }
.action { display: grid; gap: 3px; justify-items: center; }
.action__label { font-weight: 700; }
.action__hint { font-size: var(--dd-fs-small); color: var(--dd-text-muted); }
.footnote { display: grid; gap: 13px; justify-items: center; }
.footnote hr { width: 148px; height: 1px; margin: 0; border: 0; background: var(--dd-border); }
.footnote p { margin: 0; font-size: var(--dd-fs-micro); color: var(--dd-text-muted); text-align: center; }

/* card: the near-solid reading and input surface. Long text never sits on the gradient. */
.card { background: var(--dd-surface); border: 1px solid var(--dd-border); border-radius: var(--dd-r3);
  padding: var(--dd-s5); display: grid; gap: var(--dd-s4); box-shadow: 0 10px 30px var(--dd-shadow-soft); }
.card__head { display: flex; align-items: center; gap: var(--dd-s2); }
.card__eyebrow { font-size: var(--dd-fs-eyebrow-card); font-weight: 800; letter-spacing: var(--dd-track-eyebrow-card); color: var(--dd-text-muted); text-transform: uppercase; }
.card__head .card__eyebrow, .card__head .state-label { margin-right: auto; }
.state-label { font-size: var(--dd-fs-small); font-weight: 700; }
.timer { font-family: var(--dd-font-mono); font-size: var(--dd-fs-small); color: var(--dd-text-muted); font-variant-numeric: tabular-nums; }
.dot { width: 10px; height: 10px; border-radius: 50%; flex: none; background: var(--dd-brand-gold); }
.card__meta { display: flex; justify-content: space-between; gap: var(--dd-s3); border-top: 1px solid var(--dd-border); padding-top: var(--dd-s3);
  font-size: var(--dd-fs-micro); color: var(--dd-text-muted); }
.card__note { margin: 0; font-size: var(--dd-fs-micro); color: var(--dd-text-muted); text-align: center; }

/* the waveform bars: 5 px on a 14 px step — the reference's own geometry */
.wave { display: flex; align-items: center; justify-content: center; gap: 9px; height: 76px; }
/* Recording can run for minutes, so its motion is the longest-lived animation in the product. Only the live
   centre group animates — the outer bars keep their declared heights. Seven concurrent compositor animations
   instead of twenty-one, with the same read: the middle of the waveform is where the voice is. */
.wave__bar { width: 5px; height: 68px; border-radius: var(--dd-pill); background: var(--dd-text-muted); opacity: 0.5;
  transform-origin: center; transform: scaleY(var(--h)); }
.wave__bar.is-live { background: var(--dd-accent); opacity: 1;
  animation: dd-wave 1.1s var(--dd-ease-out) infinite alternate; animation-delay: calc(var(--i) * -90ms); }
@keyframes dd-wave { from { transform: scaleY(calc(var(--h) * 0.68)); } to { transform: scaleY(var(--h)); } }
.stop { width: 116px; height: 116px; min-height: 116px; padding: 0; border-radius: 50%; border: 0; cursor: pointer; justify-self: center;
  background: linear-gradient(90deg, var(--dd-mic-core-1) 0%, var(--dd-mic-core-2) 100%); box-shadow: 0 10px 30px var(--dd-shadow-soft);
  display: grid; place-items: center; }
.stop__glyph { width: 30px; height: 30px; border-radius: 7px; background: var(--dd-text); }

/* transcript + text capture */
/* the transcript is read at 17 px in the reference; the text-capture field is written at 16 px */
.transcript { width: 100%; min-height: 168px; padding: 0; margin: 0; font: inherit; font-size: var(--dd-fs-read); line-height: var(--dd-lh-read);
  color: var(--dd-text); background: transparent; border: 0; resize: none; }
#capture-text .transcript { font-size: var(--dd-fs-body); }
.transcript::placeholder { color: var(--dd-text-muted); }
.transcript:focus-visible { outline-offset: 6px; }
.assure { display: flex; align-items: center; gap: var(--dd-s2); font-size: var(--dd-fs-micro); color: var(--dd-text-muted); }
.assure .dot { background: var(--dd-success); }

/* CAPTURE + DENIED vertical rhythm and geometry — 1:1 with uploads/02..05. Reference y positions: back button 48,
   title baseline 145 (30 px), lede 176 (15 px), card 226 on Recording and 218 on Transcript/Text, the primary CTA
   635 (56 px), the secondary 707 (52 px). Card heights are the reference's (252 / 320 / 356) so the actions below
   them land where the reference puts them. */
.state--capture { gap: 0; }
.state--capture .title { margin-top: 23.5px; }
.state--capture .lede { margin-top: 2.4px; }
.state--capture .card { margin-top: 33.75px; }
/* DF-46: the card is a grid AND carries the reference's min-height, so `align-content` defaulted to stretch and the
   252 px floor was divided among the interior rows by the browser rather than declared by the design. The head row
   inflated from 22.4 to 39.3 px, which pushed the status dot and the two labels 6.2 px BELOW the reference while
   pulling the waveform 11.6 px ABOVE it (bar centre 344.4 against the reference's 356) — the interior drifted in two
   directions at once, and every position moved again whenever the min-height or the note's wrapping changed.
   The rows are packed at the top now and each one carries the reference's own distance, exactly as every other
   capture offset in this file is declared. Reference (02_CAPTURE_RECORDING.svg): card content top 251, dot 251..261
   sitting ON the 14 px labels' baseline (261), waveform bars centred on 356, note baseline 432. */
#capture-record .card { margin-top: 41.75px; min-height: 252px; gap: 0; align-content: start; }
/* the reference draws the dot's bottom exactly on the label baseline — a relation, not a coordinate, so it survives
   any font. line-height:1 for the same reason as .brand__word (DF-45): a row the reference positions in absolute
   coordinates must not carry the body's half-leading, or the text metrics decide where the row sits. */
#capture-record .card__head { align-items: baseline; line-height: 1; margin-left: -4px; gap: 11px; }
#capture-record .wave { margin-top: 53px; }
#capture-record .card__note { margin-top: 23.8px; }
#capture-transcript .card { min-height: 320px; }
#capture-transcript .transcript { min-height: 152px; }
/* the reference sets this title on two lines; reserving both keeps everything below it on the reference's y */
#mic-denied .title { min-height: calc(var(--dd-fs-title-capture) * var(--dd-lh-title) * 2); }
/* DF-46 (second occurrence): the text card is the one other place the floor really is taller than the content \u2014 88.2 px
   of slack that `align-content: stretch` was spreading through the gaps. Measured: the transcript card's content is
   447 px against a 320 px floor, so it has no slack and is deliberately left alone. Here the slack goes where a
   writing surface's spare height belongs \u2014 into the writing area \u2014 by making the textarea the only flexible row.
   Two rows, not three: the field's <label> is .visually-hidden, which is position:absolute and therefore not a grid
   item at all — a three-row template handed the slack to the meta line instead of the field.
   This is the canonical pattern, not a reference measurement: 04_CAPTURE_TEXT is not in the package, so the card box
   (356 px, recorded) is held and only the distribution inside it is decided here. */
#capture-text .card { min-height: 356px; grid-template-rows: 1fr auto; }
#capture-record .stop { margin-top: 53px; }
#capture-record .action { margin-top: 12px; }
#capture-record .btn--secondary { margin-top: 29px; }
#capture-transcript .assure { margin-top: 25px; }
#capture-transcript .btn--primary { margin-top: 50px; }
#capture-text .btn--primary { margin-top: 61px; }
#capture-transcript .btn--secondary, #capture-text .btn--secondary, #mic-denied .btn--secondary { margin-top: var(--dd-s4); }
#mic-denied { gap: 0; }
#mic-denied .title { margin-top: 43.5px; }
#mic-denied .lede { margin-top: 18.75px; }
#mic-denied .emblem { margin-top: 27.75px; }
#mic-denied .notice { margin-top: 27px; }
#mic-denied .notice { min-height: 112px; gap: 13px; align-content: start; }
#mic-denied .btn--primary { margin-top: 68px; }
/* the denied emblem is a 140 px disc on the surface-2 fill with the same soft shadow as every other reference disc */
.emblem { width: var(--dd-moon-box); height: var(--dd-moon-box); border-radius: 50%; justify-self: center;
  background: var(--dd-surface-2); border: 1px solid var(--dd-border); box-shadow: 0 10px 30px var(--dd-shadow-soft);
  display: grid; place-items: center; }
/* the two micro marks the references draw as rings, not as filled dots */
.assure__mark { width: 12px; height: 12px; flex: none; }
.assure__disc { fill: var(--dd-success); }
.assure__tick { fill: none; stroke: var(--dd-on-accent); stroke-width: 1.4; }
.notice__mark { width: 16px; height: 16px; flex: none; }
.notice__ring { fill: var(--dd-error-soft); stroke: var(--dd-error-mark); }
.notice__minus { stroke: var(--dd-error-mark); stroke-width: 1.8; stroke-linecap: round; }
.emblem svg { width: 84px; height: 84px; }
.notice { display: grid; gap: var(--dd-s2); }
.notice__head { display: flex; align-items: center; gap: var(--dd-s2); font-weight: 700; font-size: var(--dd-fs-small); }
.notice p { margin: 0; font-size: var(--dd-fs-small); color: var(--dd-text-muted); }

/* Continuity across the handover: the splash moons dissolve INTO the kruglyak Home already has, which is therefore
   the one thing that stays — so it must not take part in Home's fade, or it blinks out exactly when it should read
   as continuous. Home fades in around it. */
.state--content { animation: dd-fade var(--dd-dur-trans) var(--dd-ease-out); }
/* during the splash handover the arrival is driven per element by src/app.js; the CSS fade would stack on top of it */
.state--content.is-arriving { animation: none; }
.state--content .moon { animation: none; }
.state--error, .state--capture { animation: dd-fade var(--dd-dur-trans) var(--dd-ease-out); }
@keyframes dd-fade { from { opacity: 0; } to { opacity: 1; } }

/* error */
.state--error .title { color: var(--dd-text); }
.error__icon { display: inline-block; width: 1em; height: 1em; margin-right: var(--dd-s2); vertical-align: -0.1em; fill: var(--dd-error-mark); }
.error__body { color: var(--dd-error); }

/* probe log (emulated flow, QA surface inside the console) */
.probe { display: grid; gap: var(--dd-s2); }
.probe__row { display: grid; grid-template-columns: auto 1fr; gap: var(--dd-s3); align-items: baseline; padding: var(--dd-s3) var(--dd-s4); background: var(--dd-surface); border: 1px solid var(--dd-border); border-radius: var(--dd-r2); }
.tag { font-size: var(--dd-fs-small); font-weight: 600; padding: 2px var(--dd-s2); border-radius: var(--dd-pill); border: 1px solid var(--dd-border); color: var(--dd-text-muted); white-space: nowrap; }
.tag--emulated { color: var(--dd-accent); }
.tag--ok { color: var(--dd-success); }
.tag--fail { color: var(--dd-error); }
.probe__transcript { width: 100%; min-height: calc(var(--dd-fs-body) * var(--dd-lh-body) * 3); padding: var(--dd-s3) var(--dd-s4); font: inherit; color: var(--dd-text); background: var(--dd-surface-2); border: 1px solid var(--dd-border); border-radius: var(--dd-r1); resize: vertical; }
label { display: block; font-size: var(--dd-fs-small); color: var(--dd-text-muted); margin-bottom: var(--dd-s1); }

/* foundation console (QA surface, not product UI) */
/* F-05: the QA entry point used to be a fixed button, which at 200% text intersected the primary action on three
   fixtures. It now flows at the end of the document — the same decision DF-06 made for the console sheet itself:
   a QA surface never overlays a product action. */
.console-toggle { display: block; margin: var(--dd-s5) max(var(--dd-s4), env(safe-area-inset-right)) var(--dd-s5) auto;
  min-height: var(--dd-control-min); background: var(--dd-surface); }
.console { position: relative; width: 100%; overflow: visible; padding: var(--dd-s4) max(var(--dd-s4), env(safe-area-inset-right)) calc(var(--dd-s7) + var(--dd-control-h)) max(var(--dd-s4), env(safe-area-inset-left));
  background: var(--dd-surface-2); border-top: 1px solid var(--dd-border); z-index: 90; display: grid; gap: var(--dd-s4); }
.console h2 { margin: 0; font-size: var(--dd-fs-body); }
.console__group { display: grid; gap: var(--dd-s2); }
.console__row { display: flex; flex-wrap: wrap; gap: var(--dd-s2); }
.console .btn { min-height: var(--dd-control-min); padding: 0 var(--dd-s4); }
select, input[type="checkbox"] { font: inherit; min-height: var(--dd-control-min); color: var(--dd-text); background: var(--dd-surface); border: 1px solid var(--dd-border); border-radius: var(--dd-r1); padding: 0 var(--dd-s3); }
input[type="checkbox"] { width: 24px; min-height: 24px; accent-color: var(--dd-accent); }
.statuslist { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--dd-s1); font-size: var(--dd-fs-small); }
.statuslist li { display: flex; justify-content: space-between; gap: var(--dd-s3); }
.eventlog { margin: 0; padding: 0; list-style: none; font-size: var(--dd-fs-small); font-variant-numeric: tabular-nums; color: var(--dd-text-muted); max-height: 12rem; overflow: auto; }

/* reduced motion: OS setting or the QA override on <html data-motion="reduced"> */
@media (prefers-reduced-motion: reduce) {
  .sk, .state--content, .state--error, .state--capture, .btn { animation: none !important; transition: none !important; }
  .sk::after { animation: none !important; opacity: 0 !important; }
  .wave__bar { animation: none !important; }
  .moon-loader .ml-b1, .moon-loader .ml-b2, .moon-loader .ml-b3, .moon-loader .ml-star, .moon-loader .ml-trace, .moon-loader .ml-reveal-path { animation: none !important; }
  .moon-loader .ml-reveal-path { stroke-dashoffset: 0 !important; }
  html[data-loader] .moon-loader { transition: none !important; }
  .moon-loader .ml-star { opacity: 0.7 !important; }
}
html[data-motion="reduced"] .sk, html[data-motion="reduced"] .state--content, html[data-motion="reduced"] .state--error, html[data-motion="reduced"] .state--capture, html[data-motion="reduced"] .btn { animation: none !important; transition: none !important; }
html[data-motion="reduced"] .sk::after { animation: none !important; opacity: 0 !important; }
html[data-motion="reduced"] .wave__bar { animation: none !important; }
html[data-motion="reduced"] .moon-loader .ml-b1, html[data-motion="reduced"] .moon-loader .ml-b2, html[data-motion="reduced"] .moon-loader .ml-b3,
html[data-motion="reduced"] .moon-loader .ml-star, html[data-motion="reduced"] .moon-loader .ml-trace, html[data-motion="reduced"] .moon-loader .ml-reveal-path { animation: none !important; }
html[data-motion="reduced"] .moon-loader .ml-reveal-path { stroke-dashoffset: 0 !important; }
html[data-motion="reduced"][data-loader] .moon-loader { transition: none !important; }
html[data-motion="reduced"] .moon-loader .ml-star { opacity: 0.7 !important; }

@media (min-width: 768px) { .screen { padding-left: max(var(--dd-s7), env(safe-area-inset-left)); padding-right: max(var(--dd-s7), env(safe-area-inset-right)); } }

/* ---- legal hub + account (r2026-08-22-05, V4 §3/§10): tokens only, 48 px controls, no colour-only state ---- */
.home__legal { margin-top: var(--dd-s3); width: auto; align-self: center; }
.state--legal { align-items: stretch; text-align: left; }
.state--legal .title--compact { font-size: calc(var(--dd-fs-title) * 0.82); min-height: 0; margin-top: var(--dd-s2); }
.state--legal .lede--compact { min-height: 0; }
.body--meta { min-height: 0; font-size: var(--dd-fs-small); color: var(--dd-text-muted); }
.doclist { list-style: none; margin: var(--dd-s3) 0 0; padding: 0; display: grid; gap: var(--dd-s2); }
.doclist__item { margin: 0; }
.doclist__btn { width: 100%; min-height: var(--dd-control-min); display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: var(--dd-s2); text-align: left; background: var(--dd-surface); border: 1px solid var(--dd-border); border-radius: var(--dd-r3); padding: var(--dd-s2) var(--dd-s3); color: var(--dd-text); font: inherit; cursor: pointer; }
.doclist__btn:focus-visible { outline: 3px solid var(--dd-accent); outline-offset: 2px; }
.doclist__title { font-weight: 600; }
/* DF-49: the version line may wrap. It was nowrap, which made it the row's min-content floor — at 200% text on a
   320 px screen the list could not shrink and pushed the page 8.7 px wide. The rule above gives the row a zero
   minimum; this gives the text the wrapping policy that has to come with it (styles/app.css line 59 states the
   policy: zero-min PLUS a declared wrapping behaviour, or the row just overflows its own box instead of the page). */
.doclist__meta { font-size: var(--dd-fs-small); color: var(--dd-text-muted); }
.account { margin-top: var(--dd-s4); padding: var(--dd-s3); display: grid; gap: var(--dd-s2); }
.account .console__row { flex-wrap: wrap; gap: var(--dd-s2); }
.account .console__row .btn { width: auto; flex: 1 1 10rem; }
.account__email { flex: 1 1 12rem; min-height: var(--dd-control-min); border-radius: var(--dd-r1); }
.btn--danger { background: var(--dd-surface-2); border: 1px solid var(--dd-error); color: var(--dd-error); }
.warn { border-color: var(--dd-error); padding: var(--dd-s3); display: grid; gap: var(--dd-s2); }
.warn .body { min-height: 0; }
#account-state[data-identity-state="anonymous_pending"]::before, #account-state[data-identity-state="link_pending"]::before { content: "… "; }
#account-state[data-identity-state="linked"]::before { content: "✓ "; }
#account-state[data-identity-state="anonymous_failed"]::before, #account-state[data-identity-state="link_failed"]::before { content: "! "; }
#legal-doc { display: grid; gap: var(--dd-s2); }
#legal-doc .btn--ghost { width: auto; justify-self: start; }
.legal__body { display: grid; gap: var(--dd-s2); color: var(--dd-text); max-width: 65ch; }
.legal__body h3 { margin: var(--dd-s2) 0 0; font-size: var(--dd-fs-lead); }
.legal__body p { margin: 0; }
.legal__body ul { margin: 0; padding-left: 1.2rem; }
.job { min-height: 0; }
.job[data-job-stage="accepted"]::before, .job[data-job-stage="running"]::before, .job[data-job-stage="recovered"]::before { content: "… "; }
.job[data-job-stage="needs_retry"]::before, .job[data-job-stage="failed"]::before { content: "! "; }
.job[data-job-stage="completed"]::before { content: "✓ "; }
