/* ABSENCE
   An observation instrument. Monochrome interface, two type voices, six motion
   verbs, square corners, no decoration. The black hole supplies all the colour
   in the frame; the interface never competes with it.
   See docs/absence/DESIGN_SYSTEM.md. */

/* ------------------------------------------------------------- the type --- */
/* Both voices are bundled, subset to latin, and served from this directory.
 *
 * They were not, until the project moved from Windows to Linux and the title
 * quietly changed shape. Not one family named in either stack below is
 * installed on a stock Arch/KDE machine — not Inter Tight, not Segoe UI, not
 * Cascadia Mono, not Consolas — so both stacks fell through to the CSS generics
 * and rendered in Noto Sans and Noto Sans Mono. The title asks for weight 320,
 * which is an off-grid value that only means anything on a variable face; on a
 * static fallback it snaps to the nearest cut instead of interpolating.
 *
 * `block` rather than `swap`: the display voice is spent exactly once, on the
 * title, and a face that changes halfway through a 1.5s per-letter resolve is
 * worse than one that arrives a frame late. Both files are local and under
 * 45KB, and the entry gate holds the sequence until the viewer commits, so the
 * block period is never reached in practice.
 * Licences and provenance: fonts/PROVENANCE.md. */
@font-face{
  font-family: "Inter Tight";
  src: url("../fonts/inter-tight-var-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: block;
}
@font-face{
  font-family: "JetBrains Mono";
  src: url("../fonts/jetbrains-mono-var-latin.woff2") format("woff2");
  font-weight: 100 800;
  font-style: normal;
  font-display: block;
}

/* ------------------------------------------------------- the boot floor --- */
/* index.html hides the whole interface inline, before this file is fetched, so
 * that a navigation cannot expose a half-painted document. This is the other
 * half of that contract: the moment this stylesheet applies, the hiding comes
 * off and the rules below own visibility again — every one of these elements is
 * already invisible here until the app says otherwise (the deck is opacity 0,
 * the HUD's contents are gated behind body.ui-*, the startup layer's beats have
 * not fired). No script is involved in either direction, so nothing can strand
 * the page half-hidden. */
#hud,#deck,#lab,#startup,#frame,#reveal{ visibility: visible; }

:root{
  --ink-100: rgba(255,255,255,1);
  --ink-72:  rgba(255,255,255,.72);
  --ink-46:  rgba(255,255,255,.46);
  --ink-28:  rgba(255,255,255,.28);
  --ink-16:  rgba(255,255,255,.16);
  --ink-08:  rgba(255,255,255,.08);
  --ink-04:  rgba(255,255,255,.04);

  --ember:     #f2a65a;
  --ember-dim: rgba(242,166,90,.32);
  --alarm:     #e8674f;

  --panel: rgba(6,7,9,.72);
  --panel-solid: rgba(5,6,8,.94);

  --font-display: "Inter Tight","Inter","Helvetica Neue","Segoe UI Variable Display","Segoe UI",system-ui,sans-serif;
  /* The bundled faces lead both stacks now. `ui-monospace` used to, and it is a
     generic — it resolves to whatever the host calls its UI monospace and can
     never be the face this interface was measured against. The host names are
     kept behind them: if a machine genuinely has Cascadia Mono or SF Mono, the
     bundled file is still what loads, which is the point of bundling it. */
  --font-mono: "JetBrains Mono",ui-monospace,"Cascadia Mono","SF Mono",Consolas,monospace;

  --t-micro: 120ms;
  --t-control: 220ms;
  --t-panel: 420ms;
  --t-cine: 900ms;
  --ease: cubic-bezier(.16,1,.3,1);
  --ease-io: cubic-bezier(.65,0,.35,1);

  --shadow-r: 200px;

  --gut: 26px;
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
}

*{ box-sizing: border-box; }

html,body{
  margin:0; padding:0; width:100%; height:100%;
  background:#000; overflow:hidden; overscroll-behavior:none;
}
body{
  font-family: var(--font-mono);
  color: var(--ink-72);
  -webkit-font-smoothing: antialiased;
  position: fixed; inset: 0;
  touch-action: none;
  font-variant-numeric: tabular-nums;
}

#view{
  position: fixed; inset: 0;
  width:100%; height:100%;
  display:block; background:#000;
  touch-action: none;
  z-index: 1;
}

/* ---------------------------------------------------------------- frame -- */

#frame{
  position: fixed; inset: 0;
  pointer-events: none; z-index: 6;
  opacity: 0;
  transition: opacity var(--t-cine) var(--ease);
}
body.ui-frame #frame{ opacity: 1; }
.edge{ position:absolute; background: var(--ink-16); }
.edge-t, .edge-b{ height:1px; left: calc(var(--gut) + var(--safe-l)); right: calc(var(--gut) + var(--safe-r)); transform: scaleX(0); }
.edge-l, .edge-r{ width:1px;  top: calc(var(--gut) + var(--safe-t)); bottom: calc(var(--gut) + var(--safe-b)); transform: scaleY(0); }
.edge-t{ top: calc(var(--gut) + var(--safe-t)); transform-origin: left; }
.edge-b{ bottom: calc(var(--gut) + var(--safe-b)); transform-origin: right; }
.edge-l{ left: calc(var(--gut) + var(--safe-l)); transform-origin: bottom; }
.edge-r{ right: calc(var(--gut) + var(--safe-r)); transform-origin: top; }
/* ALIGN: the frame draws itself rather than fading in */
body.ui-frame .edge-t{ transform: scaleX(1); transition: transform 1100ms var(--ease) 0ms; }
body.ui-frame .edge-r{ transform: scaleY(1); transition: transform 1100ms var(--ease) 120ms; }
body.ui-frame .edge-b{ transform: scaleX(1); transition: transform 1100ms var(--ease) 240ms; }
body.ui-frame .edge-l{ transform: scaleY(1); transition: transform 1100ms var(--ease) 360ms; }

/* ------------------------------------------------------------------ hud -- */

#hud{
  position: fixed; inset: 0;
  pointer-events: none; z-index: 10;
  transition: opacity var(--t-cine) var(--ease);
}
body.hud-off #hud{ opacity: 0; }
body.hud-off #frame{ opacity: 0; }

.micro{
  font-size: 8.5px; letter-spacing: .22em;
  color: var(--ink-28); text-transform: uppercase;
}

/* RESOLVE — the one entrance used by every interface element */
.resolve{ opacity:0; filter: blur(6px); transform: translateY(4px); }
#identity, #elapsed, #telemetry{
  position: absolute;
  opacity: 0; filter: blur(7px);
  transition: opacity 900ms var(--ease), filter 900ms var(--ease), transform 900ms var(--ease);
}
#identity{ left: calc(var(--gut) + var(--safe-l) + 22px); top: calc(var(--gut) + var(--safe-t) + 22px); transform: translateY(6px); }
#elapsed{ right: calc(var(--gut) + var(--safe-r) + 22px); top: calc(var(--gut) + var(--safe-t) + 22px); text-align: right; transform: translateY(6px); }
#telemetry{ left: calc(var(--gut) + var(--safe-l) + 22px); bottom: calc(var(--gut) + var(--safe-b) + 20px); transform: translateY(6px); }

body.ui-identity  #identity{ opacity:1; filter:none; transform:none; }
body.ui-elapsed   #elapsed{ opacity:1; filter:none; transform:none; }
body.ui-telemetry #telemetry{ opacity:1; filter:none; transform:none; }

.ident-name{
  font-family: var(--font-display);
  font-weight: 200;
  font-size: 27px;
  letter-spacing: .46em;
  color: var(--ink-100);
  line-height: 1;
  padding-left: .06em;
}
.ident-sub{
  margin-top: 11px;
  font-size: 8.5px; letter-spacing: .30em; color: var(--ink-46);
}
.ident-state{
  margin-top: 13px;
  display: flex; align-items: center; gap: 8px;
  font-size: 9px; letter-spacing: .24em; color: var(--ink-72);
}
.state-dot{
  width: 4px; height: 4px; background: var(--ink-28);
  transition: background var(--t-control) var(--ease), box-shadow var(--t-control) var(--ease);
}
.ident-state.live .state-dot{
  background: var(--ember);
  box-shadow: 0 0 9px var(--ember-dim);
}
.ident-state.live{ color: var(--ink-100); }

#elapsed .clock{
  margin-top: 9px;
  font-size: 19px; letter-spacing: .10em; color: var(--ink-100);
  font-variant-numeric: tabular-nums;
}

.tel-head{ margin-bottom: 10px; }
#telemetry{ font-size: 10px; }
.trow{
  display: flex; align-items: baseline; gap: 14px;
  padding: 3px 0; white-space: nowrap;
  border-top: 1px solid var(--ink-08);
}
.trow:first-of-type{ border-top: 0; }
.tlabel{ width: 168px; color: var(--ink-46); letter-spacing: .16em; font-size: 8.5px; }
.tval{ color: var(--ink-100); letter-spacing: .06em; font-size: 11px; }

#toast{
  position: absolute; top: calc(var(--gut) + var(--safe-t) + 30px); left: 50%;
  transform: translateX(-50%) translateY(-6px);
  font-size: 9px; letter-spacing: .24em; color: var(--ink-100);
  background: var(--panel-solid);
  border: 1px solid var(--ink-16);
  padding: 9px 18px;
  opacity: 0; pointer-events: none;
  transition: opacity var(--t-control) var(--ease), transform var(--t-control) var(--ease);
}
#toast.show{ opacity: 1; transform: translateX(-50%) translateY(0); }

#hint{
  position: absolute; bottom: calc(var(--gut) + var(--safe-b) + 22px); left: 50%;
  transform: translateX(-50%);
  font-size: 8.5px; letter-spacing: .26em; color: var(--ink-28);
  white-space: nowrap; opacity: 0;
  transition: opacity 1200ms var(--ease);
}
body.ui-hint #hint{ animation: hintPass 11s var(--ease) forwards; }
@keyframes hintPass{ 0%{opacity:0} 12%{opacity:1} 78%{opacity:1} 100%{opacity:0} }
body.ui-hint.touch-layout #hint{ display: none; }

/* --------------------------------------------------------------- controls */

.ctl{
  position: relative;
  display: block; width: 100%;
  font-family: var(--font-mono);
  text-align: left;
  background: transparent;
  border: 0;
  border-top: 1px solid var(--ink-08);
  border-radius: 0;
  padding: 11px 2px 12px;
  min-height: 44px;
  color: var(--ink-72);
  cursor: pointer;
  transition: color var(--t-micro) var(--ease), background var(--t-micro) var(--ease);
}
.ctl:hover{ color: var(--ink-100); background: var(--ink-04); }
.ctl:active{ background: var(--ink-08); }
.ctl:focus-visible{ outline: 1px solid var(--ink-100); outline-offset: 2px; }
.ctl-label{
  display: block;
  font-size: 9.5px; letter-spacing: .22em;
}
.ctl-sub{
  display: block; margin-top: 4px;
  font-size: 8px; letter-spacing: .10em; color: var(--ink-28);
  text-transform: lowercase;
}
.ctl-state{
  position: absolute; right: 2px; top: 11px;
  font-size: 8px; letter-spacing: .18em; color: var(--ink-28);
  transition: color var(--t-control) var(--ease);
}
.ctl.on{ color: var(--ink-100); }
.ctl.on .ctl-state{ color: var(--ink-72); }
.ctl.on .ctl-label{ font-weight: 500; }

/* LOCK — an active control carries a solid mark in the left gutter */
.ctl::before{
  content: ''; position: absolute; left: -12px; top: 12px; bottom: 12px;
  width: 2px; background: var(--ink-100);
  transform: scaleY(0); transform-origin: center;
  transition: transform var(--t-control) var(--ease);
}
.ctl.on::before{ transform: scaleY(1); }

/* ALIGN — a rule that grows with real transition progress */
.ctl-progress{
  position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: var(--ink-46);
  transform: scaleX(0); transform-origin: left;
  transition: opacity var(--t-control) var(--ease);
}
.ctl-progress.aligning{ background: var(--ember); }

.ctl-lead{ border-top: 0; padding-top: 4px; }
.ctl-lead .ctl-label{ font-size: 10.5px; letter-spacing: .26em; }
.ctl-lead.on::before{ background: var(--ember); }
.ctl-lead.on .ctl-state{ color: var(--ember); }

.ctl-min{ border-top: 0; text-align: center; padding: 12px 4px; }
.ctl-min .ctl-label{ font-size: 9px; letter-spacing: .18em; }
.ctl-min::before{ left: 0; right: 0; top: auto; bottom: 0; width: auto; height: 2px;
  transform: scaleX(0); transition: transform var(--t-control) var(--ease); }
.ctl-min.on::before{ transform: scaleX(1); }

.ctl-hold{ border-top: 0; }
.ctl-hold .ctl-label{ font-size: 9px; letter-spacing: .20em; }
body.revealing .ctl-hold{ color: var(--ink-100); background: var(--ink-08); }
/* while structure is exposed, the deck recedes so the mode owns the frame */
body.revealing #deck{ opacity: .42; }
body.revealing #deck:hover, body.revealing #deck:focus-within{ opacity: 1; }
/* the plate supersedes the runtime telemetry, in the same corner: two readouts
   of the same numbers in one place is clutter, not information */
body.revealing #telemetry{ opacity: 0; }
#deck, #telemetry{ transition: opacity var(--t-cine) var(--ease); }

.grid2{ display: grid; grid-template-columns: 1fr 1fr; gap: 0 16px; }
.grid3{ display: grid; grid-template-columns: repeat(3,1fr); gap: 1px; background: var(--ink-08); border: 1px solid var(--ink-08); }
.grid3 .ctl-min{ background: rgba(0,0,0,.35); }

.seg{
  display: grid; grid-template-columns: repeat(4,1fr);
  border: 1px solid var(--ink-16);
}
.seg-item{
  font-family: var(--font-mono);
  font-size: 8px; letter-spacing: .12em;
  padding: 10px 2px; min-height: 40px;
  background: transparent; color: var(--ink-46);
  border: 0; border-left: 1px solid var(--ink-08);
  cursor: pointer;
  transition: color var(--t-micro) var(--ease), background var(--t-micro) var(--ease);
}
.seg-item:first-child{ border-left: 0; }
.seg-item:hover{ color: var(--ink-100); background: var(--ink-04); }
.seg-item:focus-visible{ outline: 1px solid var(--ink-100); outline-offset: -1px; }
/* active = luminance + a rule + a label change, never a fill; a render setting
   must never be the brightest object in the frame */
.seg-item{ position: relative; }
.seg-item.on{ color: var(--ink-100); }
.seg-item::before{
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px;
  background: var(--ink-100); transform: scaleX(0);
  transition: transform var(--t-control) var(--ease);
}
.seg-item.on::before{ transform: scaleX(1); }

.sect{ padding: 0 0 18px; }
.sect[data-sect="observation"] .sect-head{ color: var(--ink-46); }
.sect-head{
  margin: 0 0 8px;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 8.5px; letter-spacing: .34em;
  color: var(--ink-28); text-transform: uppercase;
}
.sect-note{
  margin-top: 8px;
  font-size: 8px; letter-spacing: .10em; color: var(--ink-28);
}

/* ----------------------------------------------------------------- deck -- */

#deck{
  position: fixed;
  right: calc(var(--gut) + var(--safe-r) + 22px);
  /* clears the observation clock, which owns the top-right corner */
  top: calc(var(--gut) + var(--safe-t) + 124px);
  bottom: calc(var(--gut) + var(--safe-b) + 20px);
  width: 258px;
  z-index: 14;
  display: flex; flex-direction: column;
  pointer-events: auto;
  opacity: 0; transform: translateX(14px); filter: blur(7px);
  transition: opacity 900ms var(--ease), transform 900ms var(--ease), filter 900ms var(--ease);
}
body.ui-deck #deck{ opacity: 1; transform: none; filter: none; }
body.hud-off #deck{ opacity: 0; pointer-events: none; }

.deck-handle{
  display: flex; align-items: center; gap: 10px;
  width: 100%;
  background: transparent; border: 0;
  border-bottom: 1px solid var(--ink-16);
  padding: 0 0 12px; margin-bottom: 16px;
  color: var(--ink-46); cursor: pointer;
  font-family: var(--font-display);
  font-size: 9px; letter-spacing: .34em;
  min-height: 30px;
}
.deck-handle:hover{ color: var(--ink-100); }
.deck-handle:focus-visible{ outline: 1px solid var(--ink-100); outline-offset: 3px; }
.handle-grip{ width: 14px; height: 1px; background: currentColor; flex: none; }

.deck-body{
  flex: 1; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: thin; scrollbar-color: var(--ink-16) transparent;
  padding-left: 12px; margin-left: -12px;
}
.deck-body::-webkit-scrollbar{ width: 3px; }
.deck-body::-webkit-scrollbar-thumb{ background: var(--ink-16); }

/* RETRACT */
body.deck-collapsed .deck-body{
  max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height var(--t-panel) var(--ease-io), opacity var(--t-micro) var(--ease);
}

.keys-toggle{
  display: none;
  width: 100%; min-height: 30px; margin-top: 10px;
  background: transparent; border: 0; border-top: 1px solid var(--ink-08);
  color: var(--ink-28); cursor: pointer;
  font-family: var(--font-mono); font-size: 7.5px; letter-spacing: .22em;
  text-align: left; padding: 9px 0 0;
}
.keys-toggle:hover{ color: var(--ink-72); }
.keys-toggle:focus-visible{ outline: 1px solid var(--ink-100); outline-offset: 2px; }
@media (max-height: 780px){ .keys-toggle{ display: block; } }
body.touch-layout .keys-toggle{ display: none; }

.deck-keys{
  display: flex; flex-wrap: wrap; gap: 4px 12px;
  padding-top: 14px; border-top: 1px solid var(--ink-08);
  font-size: 7.5px; letter-spacing: .12em; color: var(--ink-28);
}
.deck-keys .key b{
  color: var(--ink-46); font-weight: 500; margin-right: 5px; letter-spacing: .14em;
}

/* ------------------------------------------------------------------ lab -- */

#lab{
  position: fixed;
  left: calc(var(--gut) + var(--safe-l) + 22px);
  top: calc(var(--gut) + var(--safe-t) + 22px);
  bottom: calc(var(--gut) + var(--safe-b) + 20px);
  width: 342px;
  z-index: 16;
  display: flex; flex-direction: column;
  background: var(--panel);
  backdrop-filter: blur(14px) saturate(.6);
  -webkit-backdrop-filter: blur(14px) saturate(.6);
  border: 1px solid var(--ink-16);
  /* REVEAL — a wipe from the anchored edge, not a fade */
  clip-path: inset(0 0 0 0);
  transition: clip-path var(--t-panel) var(--ease), opacity var(--t-panel) var(--ease);
}
#lab.closed{
  clip-path: inset(0 100% 0 0);
  opacity: 0; pointer-events: none;
}
body.hud-off #lab{ opacity: 0; pointer-events: none; }

.lab-head{
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 18px 20px 14px;
  border-bottom: 1px solid var(--ink-16);
}
.lab-title{
  font-family: var(--font-display); font-weight: 200;
  font-size: 20px; letter-spacing: .42em; color: var(--ink-100);
}
.lab-sub{ margin-top: 7px; font-size: 8px; letter-spacing: .14em; color: var(--ink-28); }
.lab-actions{ display: flex; gap: 8px; }
.lab-actions .ctl-min{
  width: auto; padding: 9px 12px; border: 1px solid var(--ink-16);
  min-height: 36px;
}

.lab-body{
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: 4px 20px 22px;
  scrollbar-width: thin; scrollbar-color: var(--ink-16) transparent;
}
.lab-body::-webkit-scrollbar{ width: 3px; }
.lab-body::-webkit-scrollbar-thumb{ background: var(--ink-16); }

.lab-group{ padding: 18px 0 6px; border-top: 1px solid var(--ink-08); }
.lab-group:first-child{ border-top: 0; }
.lab-group-head{
  position: sticky; top: 0; z-index: 2;
  background: linear-gradient(180deg, rgba(5,6,8,.97) 62%, rgba(5,6,8,0));
  padding: 10px 0 12px;
  margin: 0 0 6px;
  font-family: var(--font-display); font-weight: 400;
  font-size: 8.5px; letter-spacing: .34em; color: var(--ink-72);
  display: flex; flex-direction: column; gap: 5px;
}
.lab-group-note{
  font-family: var(--font-mono);
  font-size: 8px; letter-spacing: .06em; color: var(--ink-28);
  text-transform: none;
}

.prow{ padding: 0 0 17px; }
.phead{ display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
.plabel{ font-size: 8.5px; letter-spacing: .18em; color: var(--ink-46); cursor: pointer; }
.prow.nondefault .plabel{ color: var(--ink-72); }
.prow.nondefault .plabel::after{ content: ' ·'; color: var(--ember); }
.pval{
  font-size: 10.5px; letter-spacing: .04em; color: var(--ink-100);
  font-variant-numeric: tabular-nums;
}

.ptrack{ position: relative; height: 34px; margin-top: 6px; }
.ptrack::before{
  content: ''; position: absolute; left: 0; right: 0; top: 17px; height: 1px;
  background: var(--ink-16);
}
.pfill{
  position: absolute; left: 0; right: 0; top: 17px; height: 1px;
  background: var(--ink-72); transform-origin: left; transform: scaleX(0);
  pointer-events: none;
}
.prow.active .pfill, .prow.focused .pfill{ background: var(--ink-100); }

.ptrack input[type="range"]{
  -webkit-appearance: none; appearance: none;
  position: absolute; inset: 0;
  width: 100%; height: 34px;
  background: transparent; cursor: pointer; margin: 0;
  touch-action: pan-y;
}
.ptrack input[type="range"]::-webkit-slider-runnable-track{ height: 34px; background: transparent; }
.ptrack input[type="range"]::-webkit-slider-thumb{
  -webkit-appearance: none; appearance: none;
  width: 3px; height: 20px; background: var(--ink-100);
  border: 0; border-radius: 0; margin-top: 7px;
  box-shadow: 0 0 0 14px rgba(0,0,0,.001);
}
.ptrack input[type="range"]::-moz-range-track{ height: 34px; background: transparent; }
.ptrack input[type="range"]::-moz-range-thumb{
  width: 3px; height: 20px; background: var(--ink-100); border: 0; border-radius: 0;
}
.ptrack input[type="range"]:focus-visible{ outline: 1px solid var(--ink-46); outline-offset: 0; }
.prow.focused .plabel, .prow.focused .pval{ color: var(--ink-100); }
#lab:focus{ outline: none; }
.prow.active .ptrack input[type="range"]::-webkit-slider-thumb{ height: 28px; margin-top: 3px; }

/* Explanations are disclosed, not permanent. A paragraph under every control
   turns the console into a document; the words stay one hover or focus away. */
.phelp{
  margin: 0;
  font-size: 8.5px; line-height: 1.7; letter-spacing: .02em;
  color: var(--ink-46);
  max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height var(--t-control) var(--ease-io),
              opacity var(--t-micro) var(--ease),
              margin var(--t-control) var(--ease-io);
}
.prow:hover .phelp, .prow.focused .phelp, .prow.active .phelp{
  max-height: 90px; opacity: 1; margin-top: 9px;
}
.pdiagram{
  margin: 0; max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height var(--t-control) var(--ease-io),
              opacity var(--t-micro) var(--ease), margin var(--t-control) var(--ease-io);
}
.prow:hover .pdiagram, .prow.focused .pdiagram, .prow.active .pdiagram{
  max-height: 70px; opacity: 1; margin-top: 9px;
}
.pdiagram svg{ width: 100%; max-width: 190px; height: auto; overflow: visible; }
.d-solid{ fill: var(--ink-16); stroke: var(--ink-46); stroke-width: .8; }
.d-dash{ fill: none; stroke: var(--ink-28); stroke-width: .8; stroke-dasharray: 2 3; }
.d-line{ stroke: var(--ink-16); stroke-width: .8; }
.d-bright{ fill: none; stroke: var(--ink-100); stroke-width: 1.4; }
.d-cap{ fill: var(--ink-28); font-family: var(--font-mono); font-size: 5px; letter-spacing: .1em; text-anchor: middle; }

/* --------------------------------------------------------------- reveal -- */

/* ------------------------------------------------------------ hold cue --- */

/* HOLD FEEDBACK — inside the structural readout, not over the image.
   It was a pill floating in the frame. The image is the artwork and this mode
   is no longer allowed to put anything on it, cue included. */
.hold-cue{
  flex: none;
  display: flex; align-items: center; gap: 10px;
  margin: 2px 0 9px;
}
.hold-track{ position: relative; flex: 1 1 auto; height: 1px; background: var(--ink-16); }
.hold-fill{
  position: absolute; inset: 0; background: var(--ember);
  transform: scaleX(0); transform-origin: left;
  will-change: transform;
}
.hold-state{
  flex: none; font-size: 7.5px; letter-spacing: .20em; color: var(--ink-46);
  min-width: 62px; text-align: right;
}

/* THE FIELD LINES.
   The seven traced light paths, outside the black hole, curving around the
   accretion disk. This is the only thing Reveal draws over the image: the
   labels, leaders, crossing marks and guide circles that used to sit here are
   gone from the composition rather than faded out. */
#reveal{
  position: fixed; inset: 0; z-index: 8;
  pointer-events: none;
}
#revealPlot{
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; overflow: visible;
}
#revealPlot .ray{
  fill: none;
  stroke: var(--ink-100);
  stroke-width: 1;
  stroke-linejoin: round; stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
#revealPlot .ray.back{ stroke-width: .75; }
#revealPlot .ray.front{ stroke-width: 1; }

/* the bounded diagram in the deck */
/* The structural view is a fixed composition, never a scroll: it lays itself out
   to the height available, and the diagram is the element that gives way. */
.deck-view[data-view="structure"]{
  display: none;
  height: 100%; min-height: 0;
  overflow: hidden;
}
#deck[data-view="structure"] .deck-view[data-view="structure"]{ display: flex; }
.deck-view[data-view="structure"] .sect{
  display: flex; flex-direction: column; min-height: 0; width: 100%;
  padding-bottom: 4px;
}
.plot-wrap{
  flex: 1 1 auto; min-height: 96px;
  display: flex; align-items: center; justify-content: center;
  margin: 2px 0 9px;
}
#structurePlot{
  display: block; width: auto; height: 100%;
  max-width: 100%; max-height: 232px;
  aspect-ratio: 1 / 1;
  background: rgba(0,0,0,.30);
  border: 1px solid var(--ink-08);
}
/* All seven at equal weight: there is no selected path, so there is no dimmed
   remainder. The ones that end on the horizon are marked, because that is a
   fact about them rather than a selection. */
#structurePlot .d-ray{
  fill: none; stroke: var(--ink-72); stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
#structurePlot .d-ray.captured{ stroke: var(--ember-dim); }
/* The horizon is black. Filled at 10% white it was the lightest solid in the
   plot -- lighter than the panel behind it and lighter than every path -- which
   is the wrong way round for the one object in this diagram that is defined by
   letting nothing out. */
#structurePlot .d-shadow{ fill: #000; stroke: var(--ink-72); stroke-width: 1; }
#structurePlot .d-photon{ fill: none; stroke: var(--ink-28); stroke-width: 1; stroke-dasharray: 2 3; }
#structurePlot .d-isco{ fill: none; stroke: var(--ink-16); stroke-width: 1; stroke-dasharray: 1 4; }
/* fill comes from the gradients in renderDiagram; the colour is set here */
#structurePlot .d-disk rect{ color: var(--ember-dim); }
#structurePlot .d-disk stop{ stop-color: var(--ember-dim); }
#structurePlot .d-cross{ fill: var(--ember); }

.sgrid{
  display: grid; grid-template-columns: 1fr 1fr; gap: 1px 12px;
  flex: none;
}
.sgrid > div{ display: flex; justify-content: space-between; gap: 8px; min-width: 0; }
.sgrid.wide{ grid-column: 1 / -1; }
.sgrid > div.wide{ grid-column: 1 / -1; }
.sgrid span{ font-size: 7.5px; letter-spacing: .10em; color: var(--ink-28); line-height: 1.95; }
.sgrid b{
  font-size: 8.5px; font-weight: 400; color: var(--ink-72);
  font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* THE PATH LEDGER — seven rows, no controls.
   Tabular figures so the impact parameters form a column the eye can run down,
   and fixed layout so a long fate cannot push the crossings off the panel. */
/* The ledger is a table of seven short rows, and a table has a readable
   measure. On a tablet in portrait the structural drawer is the full 820px
   wide, which stretched b/b_c to the far left, FATE to the middle and CROSSES
   to the far right with 250px of nothing between them: each row read as three
   unrelated fragments rather than one line. Cap the measure and centre it, so
   the columns stay together at any drawer width. */
.ledger{
  flex: none; width: 100%; max-width: 520px; margin: 2px auto 0;
  border-collapse: collapse; table-layout: fixed;
  font-variant-numeric: tabular-nums;
}
.ledger caption{
  caption-side: top; text-align: left;
  font-size: 7.5px; letter-spacing: .14em; color: var(--ink-28);
  padding-bottom: 4px;
}
.ledger th{
  font-size: 7px; letter-spacing: .12em; color: var(--ink-28); font-weight: 400;
  text-align: left; padding: 0 0 3px;
  border-bottom: 1px solid var(--ink-08);
}
.ledger th:last-child, .ledger td:last-child{ text-align: right; }
.ledger td{
  font-size: 9px; letter-spacing: .04em; color: var(--ink-72);
  padding: 2px 0; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.ledger td.lb{ color: var(--ink-100); width: 30%; font-size: 9.5px; }
.ledger td.lf{ width: 42%; }
/* Not ember. In a mode that drains the image to a fifth of its colour, the most
   saturated pixels in the frame were two table cells. The ember budget belongs
   to the black hole and to the one control that is being held. */
.ledger td.lf.cap{ color: var(--ink-100); }
.ledger td.lc{ width: 28%; color: var(--ink-46); }
/* The "beyond the plot" marker, in a slot every value reserves so that adding
   it to one row cannot shove that row's digits out of the column. */
.ledger .cx{ display: inline-block; width: .62em; text-align: left; color: var(--ink-28); }

/* The two steps fitSidecar() can take when the diagram alone cannot make room.
   Both are last resorts and neither is a scrollbar. */
.deck-view[data-view="structure"].tight .ledger td{ padding: 0; }
.deck-view[data-view="structure"].tight .smeans{ line-height: 1.3; gap: 0; }
.deck-view[data-view="structure"].tight .sradii{ margin-top: 5px; padding-top: 4px; }
.deck-view[data-view="structure"].tight .hold-cue{ margin: 0 0 5px; }
.deck-view[data-view="structure"].no-plot .plot-wrap{ display: none; }
/* The very last resort, for windows shorter than anything anyone designs for:
   the sentences go and the numbers stay. They are in REVEAL_STRUCTURE.md and
   they are the least load-bearing thing on the panel. */
.deck-view[data-view="structure"].minimal .smeans{ display: none; }
/* For windows too short to hold the panel by any arrangement. The observer
   block and the radii are stated elsewhere in the deck; the seven paths are
   not stated anywhere else, so they are what survives. */
.deck-view[data-view="structure"].bare .sgrid,
.deck-view[data-view="structure"].bare .sradii{ display: none; }
.deck-view[data-view="structure"].bare .ledger caption{ display: none; }
.deck-view[data-view="structure"].bare .ledger td,
.deck-view[data-view="structure"].bare .ledger th{ font-size: 7px; line-height: 1.1; }
.deck-view[data-view="structure"].bare .sect-head{ display: none; }
/* On a very short window the rail's 124px top offset — which exists to clear the
   observation clock — is most of the screen. The clock moves aside instead. */
@media (max-height: 560px){
  #deck{ top: calc(var(--gut) + var(--safe-t) + 58px); }
  .deck-view[data-view="structure"] .sect-head{ margin-bottom: 4px; }
}

.sradii{
  flex: none; margin-top: 8px; padding-top: 7px;
  border-top: 1px solid var(--ink-08);
  font-size: 7.5px; letter-spacing: .08em; color: var(--ink-28); line-height: 1.7;
}
.sradii b{ color: var(--ink-72); font-weight: 400; }
.srow b{ color: var(--ink-72); font-weight: 400; font-variant-numeric: tabular-nums; }
/* What the named boundaries mean. This is the prose the annotations used to
   carry over the picture; it is legible here and costs the image nothing. */
.smeans{
  flex: none; margin-top: 6px;
  display: flex; flex-direction: column; gap: 2px;
  font-size: 7.5px; letter-spacing: .06em; color: var(--ink-28); line-height: 1.55;
}
.smeans b{ color: var(--ink-46); font-weight: 400; letter-spacing: .14em; }
/* --------------------------------------------------------------- startup -- */

/* The layer takes the pointer only once click-anywhere-to-skip is armed.
 *
 * It used to take it for the whole sequence, and `.stmt` below is a full-width
 * invisible block across the lower middle of the screen — directly over the
 * entry gate's SOUND control. The gate drops `pointer-events` 900ms before it
 * drops `hidden` (`#prelude.gone`), and in that window a click aimed at SOUND
 * lands on `#lineA` instead. This is the unfixed twin of the `#titleBlock`
 * defect from 015e929, whose comment reads "it sat over BEGIN OBSERVATION and
 * swallowed clicks aimed at it — an automated run hit exactly that and retried
 * 47 times". Fixed at the layer rather than at the caption, because giving
 * `.stmt` alone `pointer-events: none` would move the interceptor onto
 * `#startup` itself, which is `inset: 0`.
 *
 * `body.skippable` is added 900ms into a RUNNING sequence (startup.js), which
 * is also exactly when `#startupSkip` inside this layer becomes visible — so
 * the layer is inert precisely while it has nothing to offer. */
#startup{
  position: fixed; inset: 0; z-index: 30;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
  background: transparent;
}
#startup[hidden]{ display: none; }
body.startup.skippable #startup{ pointer-events: auto; }

/* The statements are annotation, not titling. Spending the display voice here
   would make the title an enlargement of the captions rather than an arrival.
   pointer-events: none for the same reason #titleBlock has it — this box is
   invisible, full-width, and sits over the entry gate's controls. */
.stmt{
  pointer-events: none;
  position: absolute;
  left: 50%; top: 69%;
  transform: translate(-50%,-50%);
  text-align: center;
  width: min(1080px, 88vw);
}
.stmt .line{
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: clamp(10.5px, 1.05vw, 15px);
  letter-spacing: .34em;
  line-height: 2.5;
  color: var(--ink-100);
  padding-left: .34em;
  text-shadow: 0 0 16px rgba(0,0,0,.85);
  opacity: 0;
  filter: blur(7px);
  transform: scale(1.006);
}
/* RESOLVE — micro-focus and tracking settle, no typewriter, no glitch */
.stmt .line.in{
  animation: resolveLine 1700ms var(--ease) forwards;
}
@keyframes resolveLine{
  0%   { opacity:0; filter: blur(7px); letter-spacing: .52em; transform: scale(1.006); }
  55%  { opacity:1; }
  100% { opacity:1; filter: blur(0);   letter-spacing: .34em; transform: scale(1); }
}
#stmt1.out, #stmt2.out{
  animation: recede 1100ms var(--ease) forwards;
}
@keyframes recede{
  0%   { opacity:1; filter: blur(0); }
  100% { opacity:0; filter: blur(9px); }
}

/* LIGHT is magnified once, as the critical arc emerges, and never loops.
   Lensing magnifies and arcs; it does not italicise, so there is no skew. */
.lensed{ display: inline-block; }
.lensed.bend{ animation: lensMagnify 1700ms var(--ease-io) forwards; }
@keyframes lensMagnify{
  0%   { transform: scaleX(1);    letter-spacing: 0; }
  45%  { transform: scaleX(1.06); letter-spacing: .04em; }
  100% { transform: scaleX(1);    letter-spacing: 0; }
}

#titleBlock{
  position: absolute;
  left: 50%; top: 48%;
  transform: translate(-50%,-50%);
  text-align: center;
  width: min(1400px, 94vw);
  /* It is a caption, and it is 94vw wide across the middle of the screen. While
     the entry gate is up it sat over BEGIN OBSERVATION and swallowed clicks
     aimed at it — an automated run hit exactly that and retried 47 times. A
     visitor gets no retries. */
  pointer-events: none;
}
#titleBlock.out{ animation: recede 1400ms var(--ease) forwards; }

/* The word is bitten through by the shadow it is named for: inside the photon
   capture radius the letters are mostly absent. The radius is the real one,
   written from the renderer each frame, so the mask tracks the observer. */
#title{
  margin: 0;
  font-family: var(--font-display);
  /* 320 was tuned on Windows against Segoe UI Variable Display — an optical
     DISPLAY cut whose hairline is finer than any text grotesque reaches. That
     face exists on no machine here and never did on Linux, and against the
     bundled Inter Tight the same number renders visibly heavier than the
     lockup that was accepted. So it was matched by MEASUREMENT rather than by
     the number. The accepted title's stems are 5px at a 122px cap height; at
     172px Inter Tight gives 4px at weight 100, 5px at 120, 6px at 140, 8px at
     200, 11px at 320. 120 is the match, and an art-direction pass that measured
     the accepted frame independently arrived at the same target.
     One residual is honest and recorded: Inter Tight's caps are wider than
     Segoe UI Variable Display's, so the lockup is wider than the Windows one at
     the same size. That is the face, not the weight.
     See test-results/TYPOGRAPHY.md. */
  font-weight: 120;
  font-size: clamp(52px, 12.5vw, 172px);
  letter-spacing: .19em;
  line-height: .96;
  color: #fff;
  padding-left: .19em;
  display: flex; justify-content: center;
  -webkit-mask-image: radial-gradient(circle at 50% 50%,
    rgba(0,0,0,.20) 0,
    rgba(0,0,0,.20) calc(var(--shadow-r) - 16px),
    rgba(0,0,0,1)   calc(var(--shadow-r) + 34px));
  mask-image: radial-gradient(circle at 50% 50%,
    rgba(0,0,0,.20) 0,
    rgba(0,0,0,.20) calc(var(--shadow-r) - 16px),
    rgba(0,0,0,1)   calc(var(--shadow-r) + 34px));
}
#title span{
  display: inline-block;
  opacity: 0;
  transform: translateY(.16em) scaleY(1.14);
  filter: blur(16px);
}
/* each letter resolves out of the dark on its own beat */
#title.in span{ animation: letterResolve 1500ms var(--ease) forwards; }
#title.in span:nth-child(1){ animation-delay: 0ms; }
#title.in span:nth-child(2){ animation-delay: 85ms; }
#title.in span:nth-child(3){ animation-delay: 170ms; }
#title.in span:nth-child(4){ animation-delay: 255ms; }
#title.in span:nth-child(5){ animation-delay: 340ms; }
#title.in span:nth-child(6){ animation-delay: 425ms; }
#title.in span:nth-child(7){ animation-delay: 510ms; }
@keyframes letterResolve{
  0%   { opacity:0; transform: translateY(.16em) scaleY(1.14); filter: blur(16px); }
  100% { opacity:1; transform: none; filter: blur(0); }
}

#descriptor{
  margin-top: 44px;
  opacity: 0;
  display: flex; flex-direction: column; gap: 9px; align-items: center;
}
#descriptor.in{ animation: descIn 1500ms var(--ease) forwards; }
@keyframes descIn{
  0%   { opacity:0; letter-spacing: .44em; }
  100% { opacity:1; letter-spacing: .34em; }
}
.desc-a{
  font-size: clamp(9px, .92vw, 12px); letter-spacing: .34em;
  /* The lockup works on contrast: a firm caption anchoring a dissolving title.
     Windows was resolving the mono stack to a heavier platform face, so
     bundling JetBrains Mono at its default 400 quietly took half the caption's
     brightest ink away at the same moment the title gained some — the two were
     converging, which is the opposite of the relationship that was accepted. */
  font-weight: 500;
  color: var(--ink-100);
  text-shadow: 0 0 16px rgba(0,0,0,.92), 0 0 5px rgba(0,0,0,.85);
}


#startupSkip{
  position: absolute;
  right: calc(var(--gut) + var(--safe-r) + 22px);
  bottom: calc(var(--gut) + var(--safe-b) + 22px);
  font-family: var(--font-mono);
  font-size: 8px; letter-spacing: .30em;
  color: var(--ink-28);
  background: transparent; border: 1px solid var(--ink-08);
  padding: 11px 18px; min-height: 40px;
  cursor: pointer; opacity: 0;
  transition: opacity var(--t-panel) var(--ease), color var(--t-micro) var(--ease), border-color var(--t-micro) var(--ease);
}
body.skippable #startupSkip{ opacity: 1; }
#startupSkip:hover{ color: var(--ink-100); border-color: var(--ink-28); }
#startupSkip:focus-visible{ outline: 1px solid var(--ink-100); outline-offset: 2px; }
body:not(.startup) #startupSkip{ display: none; }

/* ----------------------------------------------------------------- fault -- */

#fault{
  position: fixed; inset: 0; z-index: 60;
  background: rgba(0,0,0,.9);
  display: flex; align-items: center; justify-content: center;
}
#fault[hidden]{ display: none; }
.fault-card{
  max-width: 460px; padding: 30px 34px;
  border: 1px solid var(--ink-16);
  background: var(--panel-solid);
  text-align: center;
}
.fault-kicker{ font-size: 8px; letter-spacing: .40em; color: var(--alarm); }
.fault-title{
  margin-top: 14px;
  font-family: var(--font-display); font-weight: 200;
  font-size: 22px; letter-spacing: .34em; color: var(--ink-100);
}
.fault-msg{
  margin-top: 14px; font-size: 9.5px; line-height: 1.85;
  color: var(--ink-46); word-break: break-word;
}
.fault-actions{ margin-top: 22px; display: flex; gap: 10px; justify-content: center; }
.fault-actions .ctl-min{ width: auto; padding: 11px 18px; border: 1px solid var(--ink-16); }

/* -------------------------------------------------------- mobile console -- */

#mobileBar{ display: none; }

body.touch-layout #deck{
  left: 0; right: 0; top: auto;
  bottom: 0; width: auto;
  padding: 0;
  transform: none;
}
body.touch-layout.ui-deck #deck{ transform: none; }

body.touch-layout .deck-handle{ display: none; }

body.touch-layout .deck-body{
  order: 1;
  max-height: 0;
  padding: 0 18px;
  background: var(--panel-solid);
  border-top: 1px solid var(--ink-16);
  overflow: hidden;
  transition: max-height var(--t-panel) var(--ease-io);
}
body.touch-layout.panel-open .deck-body{
  max-height: min(46vh, 380px);
  overflow-y: auto;
  padding: 18px 18px 10px;
}
/* one drawer at a time: show only the section the console asked for */
body.touch-layout .deck-body .sect{ display: none; }
body.touch-layout #deck[data-open="viewpoint"] .sect[data-sect="viewpoint"],
body.touch-layout #deck[data-open="render"] .sect[data-sect="render"]{ display: block; }
body.touch-layout .deck-keys{ display: none; }

body.touch-layout #mobileBar{
  order: 2;
  display: grid;
  /* two rows of three: a readable label and a comfortable thumb target, which
     one row of six on a 390px screen could not give */
  grid-template-columns: repeat(3, 1fr);
  background: var(--panel-solid);
  border-top: 1px solid var(--ink-16);
  padding-bottom: var(--safe-b);
  padding-left: var(--safe-l);
  padding-right: var(--safe-r);
}
body.touch-layout #mobileBar .tile:nth-child(3n+1){ border-left: 0; }
body.touch-layout #mobileBar .tile:nth-child(n+4){ border-top: 1px solid var(--ink-08); }

/* on a touch layout the audio drawer is the deck's audio state */
body.touch-layout #deck[data-open="audio"] .deck-view[data-view="audio"],
body.touch-layout #deck[data-open="structure"] .deck-view[data-view="structure"]{ display: block; }
body.touch-layout .deck-view[data-view="structure"] .sect{ display: block; }
/* On touch the drawer is the sidecar, and it still may not scroll: the plot is
   the element that gives way, and the readout stays whole. */
body.touch-layout.revealing.panel-open .deck-body{
  /* 45vh was tried, to stop the sheet evicting its own subject on a phone --
     at 52vh a 740px screen is left a 200px strip of black hole above a full
     page of numbers. It cut the last two lines of the readout off at 360x740,
     so it is not available: the readout being whole is the harder promise. */
  max-height: min(52vh, 420px);
  overflow: hidden;
}
body.touch-layout .deck-view[data-view="structure"]{ max-height: 100%; }
body.touch-layout #structurePlot{ max-height: 118px; }
/* On a short phone the sheet's 52vh is 385px and the readout does not fit in
   it: the caption was being cut off, which the visibility check caught only
   once it started asking the document what is actually on screen. The diagram
   gives way further rather than the readout losing a line. */
@media (max-height: 800px){
  body.touch-layout #structurePlot{ max-height: 84px; }
  body.touch-layout .ledger td{ padding: 1px 0; }
  body.touch-layout .ledger caption{ padding-bottom: 2px; }
  body.touch-layout .hold-cue{ margin: 0 0 6px; }
  body.touch-layout .smeans{ line-height: 1.4; }
}
body.touch-layout .sgrid span{ line-height: 1.75; }
body.touch-layout #deck[data-open="audio"] .deck-back{ display: none; }
body.touch-layout .deck-view[data-view="audio"] .sect[data-sect="audio"]{ display: block; }

/* HUD RECOVERY — outside the deck by construction, so no rule that disables the
   deck can disable the way back into it. */
.hud-restore{
  position: fixed; left: 50%; bottom: calc(10px + var(--safe-b));
  transform: translateX(-50%);
  display: none; align-items: center; gap: 8px;
  padding: 10px 18px; min-height: 44px;
  background: rgba(0,0,0,.55);
  border: 1px solid var(--ink-16);
  color: var(--ink-46);
  font-family: var(--font-mono); font-size: 8px; letter-spacing: .26em;
  z-index: 20; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
body.hud-off .hud-restore{ display: flex; }
.restore-grip{ width: 16px; height: 1px; background: currentColor; }
.hud-restore:active{ color: var(--ink-100); }
.hud-restore:focus-visible{ outline: 1px solid var(--ink-100); outline-offset: 2px; }

/* touch targets: the segmented render control is the one place a 40px row
   survived from the desktop layout */
body.touch-layout .seg-item{ min-height: 44px; }
.tile:focus-visible{ outline: 1px solid var(--ink-100); outline-offset: -2px; }
.tile{
  position: relative;
  background: transparent; border: 0;
  border-left: 1px solid var(--ink-08);
  color: var(--ink-46);
  font-family: var(--font-mono);
  padding: 13px 2px 12px;
  min-height: 60px;
  cursor: pointer;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none; user-select: none;
}
.tile:first-child{ border-left: 0; }
.tile-label{ font-size: 9.5px; letter-spacing: .08em; }
.tile-state{ font-size: 8px; letter-spacing: .06em; color: var(--ink-28); min-height: 9px; }
.tile.on{ color: var(--ink-100); }
.tile.on .tile-state{ color: var(--ink-72); }
.tile::before{
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: var(--ink-100); transform: scaleX(0);
  transition: transform var(--t-control) var(--ease);
}
.tile.on::before{ transform: scaleX(1); }
.tile-cine.on::before{ background: var(--ember); }
.tile-reveal.on::before{ background: var(--ember); }
.tile:focus-visible{ outline: 1px solid var(--ink-100); outline-offset: -2px; }

body.touch-layout #lab{
  left: 0; right: 0; top: auto; width: auto;
  bottom: 0; max-height: 78vh;
  border-left: 0; border-right: 0; border-bottom: 0;
  clip-path: inset(0 0 0 0);
  transform: translateY(0);
  transition: transform var(--t-panel) var(--ease), opacity var(--t-panel) var(--ease);
}
body.touch-layout #lab.closed{
  clip-path: inset(0 0 0 0);
  transform: translateY(102%);
  opacity: 0;
}
body.touch-layout .lab-body{ padding-bottom: calc(28px + var(--safe-b)); }

body.touch-layout #telemetry{
  bottom: auto; top: calc(var(--gut) + var(--safe-t) + 118px);
  font-size: 9.5px;
}
/* the hottest part of the render sits directly under the top-of-screen readouts
   on a phone; a soft scrim is the only thing that keeps them legible */
body.touch-layout #hud::before{
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 300px;
  background: linear-gradient(180deg, rgba(0,0,0,.62) 0%, rgba(0,0,0,.34) 48%, rgba(0,0,0,0) 100%);
  pointer-events: none;
}
body.touch-layout .tlabel{ width: 132px; font-size: 7.5px; }
body.touch-layout .tval{ font-size: 10px; }
body.touch-layout .ident-name{ font-size: 19px; letter-spacing: .40em; }
body.touch-layout .ident-sub{ font-size: 7.5px; letter-spacing: .22em; }
body.touch-layout #elapsed .clock{ font-size: 14px; }
body.touch-layout #identity,
body.touch-layout #telemetry{ left: calc(var(--gut) + var(--safe-l) + 12px); }
body.touch-layout #elapsed{ right: calc(var(--gut) + var(--safe-r) + 12px); }
body.touch-layout #identity,
body.touch-layout #elapsed{ top: calc(var(--gut) + var(--safe-t) + 14px); }
body.touch-layout #elapsed .clock{ font-size: 13px; letter-spacing: .06em; }
body.touch-layout #elapsed .micro{ font-size: 7.5px; letter-spacing: .18em; }
body.touch-layout{ --gut: 12px; }

@media (max-width: 420px){
  body.touch-layout .tile-label{ font-size: 8.5px; letter-spacing: .02em; }
  body.touch-layout .tile-cine .tile-label{ font-size: 8px; }
  body.touch-layout #telemetry .trow:nth-child(n+5){ display: none; }
}

/* landscape phones: the image is precious, so the console gets out of the way */
@media (orientation: landscape) and (max-height: 480px){
  body.touch-layout #telemetry{ display: none; }
  body.touch-layout #mobileBar .tile{ min-height: 48px; padding: 8px 2px; }
  body.touch-layout.panel-open .deck-body{ max-height: 42vh; }
}

/* short viewports: the deck compacts rather than scrolling away its own tail */
@media (min-width: 861px) and (max-height: 900px){
  .sect{ padding-bottom: 13px; }
  .sect-head{ margin-bottom: 6px; }
  .ctl{ padding: 9px 2px 10px; min-height: 40px; }
  .ctl-sub{ margin-top: 3px; }
  .seg-item{ padding: 9px 2px; min-height: 36px; }
  .deck-keys{ padding-top: 10px; gap: 3px 10px; }
}
@media (min-width: 861px) and (max-height: 780px){
  /* The key list is the only place the shortcuts are written down, so on a short
     viewport it is collapsed behind its own control rather than deleted: twelve
     seconds into a 1280x720 session there used to be no statement of any hotkey
     anywhere, including the one that brings the interface back. */
  .deck-keys{ display: none; }
  body.keys-open .deck-keys{ display: flex; }
  .ctl-sub{ display: none; }
  #deck{ top: calc(var(--gut) + var(--safe-t) + 112px); }
}

/* -------------------------------------------------------- deck routing --- */
/* AUDIO is a state of the deck, not a panel over it and not a scroll to the
   bottom of one. Both views occupy the same region; only one is present. */

.deck-view{
  transition: opacity 200ms var(--ease), transform 260ms var(--ease);
}
.deck-view{ display: none; }
#deck:not([data-view]) .deck-view[data-view="observation"],
#deck[data-view="observation"] .deck-view[data-view="observation"],
#deck[data-view="audio"] .deck-view[data-view="audio"],
#deck[data-view="structure"] .deck-view[data-view="structure"]{
  display: block;
  animation: deckEnter 260ms var(--ease) both;
}
@keyframes deckEnter{
  from{ opacity: 0; transform: translateX(10px); }
  to  { opacity: 1; transform: none; }
}

.deck-back{
  display: flex; align-items: center; gap: 9px;
  width: 100%; min-height: 34px;
  padding: 0 0 12px; margin-bottom: 16px;
  background: transparent; border: 0;
  border-bottom: 1px solid var(--ink-16);
  color: var(--ink-46); cursor: pointer;
  font-family: var(--font-display);
  font-size: 9px; letter-spacing: .34em;
  transition: color var(--t-micro) var(--ease);
}
.deck-back:hover{ color: var(--ink-100); }
.deck-back:focus-visible{ outline: 1px solid var(--ink-100); outline-offset: 3px; }
.back-mark{
  width: 14px; height: 1px; background: currentColor; flex: none;
  position: relative;
}
.back-mark::before{
  content: ''; position: absolute; left: 0; top: -2px;
  width: 5px; height: 5px;
  border-left: 1px solid currentColor; border-bottom: 1px solid currentColor;
  transform: rotate(45deg); transform-origin: left center;
}

/* --------------------------------------------------- immediate feedback --- */
/* The renderer may be spending 70ms of every frame on the main thread. Hover
   and press must not queue behind it, so both are opacity on a promoted layer
   rather than a background repaint, and both resolve in well under 100ms. */

.ctl, .seg-item, .tile, .deck-handle, .deck-back{ position: relative; }
.ctl::after, .seg-item::after, .tile::after{
  content: ''; position: absolute; inset: 0;
  background: var(--ink-100);
  opacity: 0; pointer-events: none;
  will-change: opacity;
  transition: opacity 80ms linear;
}
.ctl:hover::after, .seg-item:hover::after, .tile:hover::after{ opacity: .045; }
.ctl.pressed::after, .seg-item.pressed::after, .tile.pressed::after{
  opacity: .10; transition-duration: 0ms;
}
/* the background repaint the overlay replaces */
.ctl:hover{ background: transparent; }
.ctl:active{ background: transparent; }
.seg-item:hover{ background: transparent; }

/* Containment: a telemetry write must not invalidate layout for the whole HUD. */
#telemetry, #elapsed, #identity, .sect, .prow, .deck-view{
  contain: layout style;
}
.tval, .clock{ contain: layout style paint; }

/* ------------------------------------------------------------- prelude --- */
/* Not a splash screen: the renderer is already live behind this, holding its
   own first frame. Nothing here may look like a web control. */

#prelude{
  position: fixed; inset: 0; z-index: 40;
  display: grid; place-items: center;
  background: radial-gradient(120% 90% at 50% 52%, rgba(0,0,0,.72) 0%, rgba(0,0,0,.94) 60%, #000 100%);
  opacity: 1;
  transition: opacity 900ms var(--ease);
}
#prelude[hidden]{ display: none; }

/* The press is acknowledged in the instant it lands.
   The sequence itself waits for the audio device to start -- up to about a
   second on a cold context, measured at 1207 ms on the test machine -- because
   picture and sound have to begin together. A gate that shows nothing at all
   during that second reads as a press that did not work, and there was no rule
   for this class at all: `prelude-going` was being set on the body and styled
   by nobody. Now the withdrawal begins immediately and the wait happens inside
   an authored transition instead of inside a stall. */
body.prelude-going #preludeBegin,
body.prelude-going .prelude-sub{
  opacity: 0; transform: translateY(-5px);
  transition: opacity 240ms var(--ease), transform 420ms var(--ease);
}
body.prelude-going .prelude-rule{
  width: min(420px, 74vw);
  transition: width 900ms var(--ease-io);
}
body.prelude-going .prelude-mark{
  opacity: .5;
  transition: opacity 760ms var(--ease);
}
/* The cold-start wait. The mark holds instead of fading, because the sequence
   has not begun yet and pretending otherwise would be a lie about the state. */
.prelude-wait{
  position: absolute; left: 50%; transform: translateX(-50%);
  margin-top: 26px;
  font-size: 8.5px; letter-spacing: .30em; color: var(--ink-28);
  opacity: 0; transition: opacity 700ms var(--ease);
  pointer-events: none; white-space: nowrap;
}
body.prelude-waiting .prelude-wait{ opacity: 1; animation: waitPulse 2.6s var(--ease-io) infinite; }
body.prelude-waiting .prelude-mark{ opacity: .82; }
@keyframes waitPulse{ 0%,100%{ opacity: .34; } 50%{ opacity: 1; } }
#prelude.gone{ opacity: 0; pointer-events: none; }

.prelude-body{
  display: grid; justify-items: center; gap: 0;
  padding: 0 24px;
  text-align: center;
}

.prelude-mark{
  font-family: var(--font-display);
  font-size: clamp(26px, 4.4vw, 44px);
  font-weight: 200;
  letter-spacing: .52em;
  text-indent: .52em;
  color: var(--ink-72);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 1600ms var(--ease), transform 1600ms var(--ease), color 1600ms var(--ease);
}
#prelude.ready .prelude-mark{ opacity: 1; transform: none; }

.prelude-rule{
  width: 0; height: 1px; margin: 22px 0 18px;
  background: var(--ink-16);
  transition: width 1400ms var(--ease) 260ms;
}
#prelude.ready .prelude-rule{ width: min(230px, 46vw); }

.prelude-sub{
  font-size: 8.5px; letter-spacing: .28em; color: var(--ink-28);
  opacity: 0; transition: opacity 1200ms var(--ease) 520ms;
}
#prelude.ready .prelude-sub{ opacity: 1; }

#preludeBegin{
  margin-top: 46px;
  min-height: 46px; padding: 15px 30px;
  background: transparent;
  border: 1px solid var(--ink-16);
  color: var(--ink-72);
  font-family: var(--font-mono);
  font-size: 9.5px; letter-spacing: .30em;
  cursor: pointer;
  opacity: 0; transform: translateY(4px);
  transition: opacity 1000ms var(--ease) 900ms, transform 1000ms var(--ease) 900ms,
              color var(--t-control) var(--ease), border-color var(--t-control) var(--ease),
              background-color var(--t-control) var(--ease);
}
#prelude.ready #preludeBegin{ opacity: 1; transform: none; }
#preludeBegin:hover{ color: var(--ink-100); border-color: var(--ink-46); background: var(--ink-04); }
#preludeBegin:focus-visible{ outline: 1px solid var(--ink-100); outline-offset: 3px; }

#preludeSound{
  margin-top: 20px;
  display: inline-flex; align-items: center; gap: 9px;
  min-height: 34px; padding: 8px 12px;
  background: transparent; border: 0;
  color: var(--ink-28);
  font-family: var(--font-mono);
  font-size: 8px; letter-spacing: .22em;
  cursor: pointer;
  opacity: 0;
  transition: opacity 1000ms var(--ease) 1150ms, color var(--t-control) var(--ease);
}
#prelude.ready #preludeSound{ opacity: 1; }
#preludeSound:hover{ color: var(--ink-72); }
#preludeSound:focus-visible{ outline: 1px solid var(--ink-46); outline-offset: 2px; }
.prelude-sound-dot{
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--ink-16);
  transition: background-color var(--t-control) var(--ease), box-shadow var(--t-control) var(--ease);
}
#preludeSound.on{ color: var(--ink-46); }
#preludeSound.on .prelude-sound-dot{
  background: var(--ember); box-shadow: 0 0 8px var(--ember-dim);
}
.prelude-sound-state{ color: inherit; }

/* while the gate is up nothing behind it may take a pointer */
body.prelude-open #deck, body.prelude-open #hud, body.prelude-open #startupSkip{
  pointer-events: none;
}
body.prelude-open #startupSkip{ opacity: 0; }

/* ------------------------------------------------------------- the cut --- */
/* Reloading is part of the experience, because people reload.
 *
 * Measured on this machine with a compositor screencast: the screen goes black,
 * and then — 777 ms into the navigation, after the black — Chrome presents the
 * OUTGOING document one more time, at the window size it had before, the deck
 * and the telemetry and the running black hole all back for a frame. It reads
 * as a glitch because it is one: the wrong interface, at the wrong size, after
 * the picture had already ended.
 *
 * The page cannot stop the compositor doing that. What it can do is make sure
 * the frame that gets re-presented is already black, and open the new document
 * on black too, so the two blacks are the same black and the seam is gone.
 *
 * The lift is a CSS animation, not a scripted one, deliberately: the first
 * paint of a reload happens long before the module graph has run, and on a cold
 * shader cache it can be a minute before anything else is ready. Darkness that
 * depends on a script is darkness that arrives late. */
#veil{
  position: fixed; inset: 0; z-index: 90;
  background: #000;
  pointer-events: none;
  opacity: 0;
  /* The failsafe, and ONLY the failsafe: hold absolute black, then give up and
     lift anyway. A gate that depends on a script to open is a gate that stays
     shut when the script is what failed. */
  animation: veilHold 3200ms linear both;
}
@keyframes veilHold{ 0%{ opacity: 1; } 62%{ opacity: 1; } 100%{ opacity: 0; } }

/* The real lift, taken when the entry gate is armed and there is something
   composed to look at. On a warm load that is inside the first frame and the
   viewer never learns this existed; on a slow one it holds the darkness for as
   long as the darkness is the honest thing to show. */
body.arrived #veil{ animation: veilLift 1000ms var(--ease) both; }
@keyframes veilLift{ from{ opacity: 1; } to{ opacity: 0; } }

/* A fault outranks the arrival. The veil is z-index 90 and #fault is 60, so
   without this a WebGL failure in the first second — which is exactly when
   faults happen — puts the alertdialog behind opaque black while fatal() has
   already moved focus into it. */
body.fault #veil{ display: none; }

/* Leaving.
 *
 * This is the floor, not the effect: cancel the arrival animation and stand at
 * full black. The 180ms fade over it is applied inline by main.js, because
 * swapping one CSS animation for another on the same element does NOT restart
 * the clock in Chrome — measured, the replacement animation reports itself as
 * running and never advances — and a departure that silently does nothing is
 * the exact defect this whole section exists to remove. If the script is not
 * there to fade it, the rule below still cuts to black. */
body.departing #veil{ animation: none; opacity: 1; }

@media (prefers-reduced-motion: reduce){
  #veil{ animation: veilHold 900ms linear both; }
  body.arrived #veil{ animation: veilLift 220ms linear both; }
}

/* ------------------------------------------------------- audio detail ---- */
/* Disclosed only while sound is running: two controls, no panel. */

.arow{
  display: grid; grid-template-columns: 42px 1fr 26px; align-items: center; gap: 8px;
  margin: 14px 0 6px;
}
.arow .micro{ font-size: 7.5px; }
.aval{ font-size: 8px; color: var(--ink-46); text-align: right; }

.arow input[type="range"]{
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 26px; margin: 0;
  background: transparent; cursor: pointer; touch-action: pan-y;
}
.arow input[type="range"]::-webkit-slider-runnable-track{
  height: 1px; margin-top: 12px; background: var(--ink-16);
}
.arow input[type="range"]::-webkit-slider-thumb{
  -webkit-appearance: none; appearance: none;
  width: 3px; height: 14px; background: var(--ink-72);
  border: 0; border-radius: 0; margin-top: -6px;
  box-shadow: 0 0 0 12px rgba(0,0,0,.001);
}
.arow input[type="range"]::-moz-range-track{ height: 1px; background: var(--ink-16); }
.arow input[type="range"]::-moz-range-thumb{
  width: 3px; height: 14px; background: var(--ink-72); border: 0; border-radius: 0;
}
.arow input[type="range"]:focus-visible{ outline: 1px solid var(--ink-46); outline-offset: 4px; }
.arow input[type="range"]:hover::-webkit-slider-thumb{ background: var(--ink-100); }

.sect[data-sect="audio"] .sect-note{ margin-top: 6px; }

/* ------------------------------------------------------- reduced motion --- */

@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 120ms !important;
  }
  /* ...except the veil, which is not an animation. It is the thing that stops a
     reload showing the wrong document, and collapsing it to 0.01ms hands
     precisely the viewers who asked for less motion the one flash the whole
     mechanism exists to remove. It is shortened above, not abolished. */
  #veil{ animation-duration: 900ms !important; }
  body.arrived #veil{ animation-duration: 220ms !important; }
  .stmt .line, #title span, #descriptor{ filter: none !important; }
  .stmt .line.in{ animation: none; opacity: 1; filter: none; letter-spacing: .40em; transform: none; }
  #title.in span{ animation: none; opacity: 1; transform: none; filter: none; }
  #descriptor.in{ animation: none; opacity: 1; }
  .lensed{ animation: none; }
  #identity, #elapsed, #telemetry, #deck{ filter: none !important; }
  /* the duration override above does not clear delays, and a gate the viewer
     cannot see for a second is a broken gate */
  .prelude-mark, .prelude-rule, .prelude-sub, #preludeBegin, #preludeSound{
    transition-delay: 0ms !important;
  }
}
