/* Exit Code — the game. Palette and base in ../tokens.css.
 *
 * Two worlds share this page. The chrome -- masthead, editor, console, board --
 * is your desk, and it does not change. The stage is the night's own place, and
 * it is a different one every day. Only the stage may take a day's light.
 */

/* ---------------------------------------------------------------- masthead */

/*
 * Two bands, not one row. The rail on top is the desk -- the mark, where you
 * can go, who is playing -- and it is identical on every page and every night.
 * The band under it is tonight, and nothing in it survives the date changing.
 *
 * They used to share a row, which put a 24px mark and a 34px title shoulder to
 * shoulder and left the mark floating at the middle of a three-line block,
 * belonging to neither line.
 */
.masthead {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--rule);
}

.rail {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: 1.6rem;
}

.masthead-night {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: var(--s3);
  flex-wrap: wrap;
}

.titles { display: flex; flex-direction: column; gap: var(--s1); }

h1 {
  margin: 0;
  font-size: var(--t-display);
  font-weight: 600;
  letter-spacing: -.02em;
  text-transform: uppercase;
}
/* Leading the title, subordinate to it -- the same relationship the board's
   rows have always had between "003" and the night it names. */
.day-no { color: var(--faint); font-weight: 400; font-variant-numeric: tabular-nums; }

.counter { display: flex; flex-direction: column; align-items: flex-end; }
.counter b { font-size: var(--t-figure); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
/* The direct child, not any span. `#count` lives inside the <b>, so the
   descendant form matched it too and drew the number you are counting at 11px
   against the 24px "/ 7" it is counted against. */
.counter > span { font-size: var(--t-label); letter-spacing: .16em; text-transform: uppercase; color: var(--faint); }

/* ------------------------------------------------------------------ layout */

.layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s4);
  padding: var(--s4);
  max-width: 96rem;
  margin: 0 auto;
  align-items: start;
}

.pane { display: flex; flex-direction: column; gap: var(--s3); min-width: 0; }

.pane-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s2);
  border-bottom: 1px solid var(--rule);
  padding-bottom: var(--s1);
}

h2 {
  margin: 0;
  font-size: var(--t-hint);
  letter-spacing: .18em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--muted);
}

.hint, .clock { font-size: var(--t-hint); color: var(--faint); letter-spacing: .06em; }
.clock { font-variant-numeric: tabular-nums; color: var(--sodium); }

/* -------------------------------------------------------------- left pane */

.scene-prose { display: flex; flex-direction: column; gap: var(--s2); }

/* Measured at 85 characters across, which is wider than anything anybody
   reads willingly. The night's own voice was the worst-set text on the page. */
.scene-prose p {
  margin: 0;
  max-width: 68ch;
  font-family: var(--serif);
  font-size: var(--t-prose);
  line-height: 1.58;
  color: var(--text);
}

/* No label above it. The masthead already says which night this is, and serif
   prose sitting under it reads as the opening without being announced -- while
   a fifth identical micro-label made the only human writing on the page look
   like one more section of chrome. */
.scene-prose { margin-top: var(--s1); }

.vocab { display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
.vocab-head { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; }

/* The names are chips you click now, not a list you read: clicking drops the
   bare identifier into the caret. They stay quieter than the verbs above --
   the verb is the amber, clickable primary; a name is the thing you put in it,
   so it lifts to amber only on hover, when it is the thing you are choosing. */
.vocab p { margin: 0; display: flex; flex-wrap: wrap; gap: var(--s1); }

.name-chip {
  font-family: var(--mono);
  font-size: var(--t-hint);
  text-transform: none;
  letter-spacing: 0;
  color: var(--text);
  background: var(--sunk);
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: .1rem .4rem;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}
.name-chip:hover { color: var(--sodium); border-color: var(--sodium-d); }

/* ------------------------------------------------------- the verb reference */

.verbs {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
}

.verb {
  width: 100%;
  text-align: left;
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--t-hint);
  color: var(--sodium);
  border: 0;
  background: var(--sunk);
  padding: .3rem .5rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* `look(thing=None)`: the parameter name is what a player cannot guess, the
   default only says "optional". Both worth keeping, not equally worth reading. */
.verb .dflt { color: var(--faint); }
.verb:hover .dflt { color: var(--muted); }

.verb:hover { background: var(--desk); }
.verb:focus-visible { outline: 2px solid var(--sodium); outline-offset: -2px; }

.verb-doc {
  margin: 0;
  min-height: 1.5em;
  font-family: var(--serif);
  font-size: var(--t-prose-sm);
  color: var(--muted);
}



.lbl {
  font-size: var(--t-label);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--faint);
}

.editor-head { margin-top: var(--s2); position: relative; }

.editor {
  display: grid;
  grid-template-columns: 2.6rem minmax(0, 1fr);
  background: var(--sunk);
  border: 1px solid var(--rule);
  min-height: 15rem;
}

.gutter {
  padding: var(--s2) var(--s1) var(--s2) 0;
  text-align: right;
  color: var(--faint);
  font-size: var(--t-code);
  line-height: 1.65;
  font-variant-numeric: tabular-nums;
  white-space: pre;
  overflow: hidden;
  border-right: 1px solid var(--rule);
  user-select: none;
}

#code {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--t-code);
  line-height: 1.65;
  padding: var(--s2);
  resize: vertical;
  min-height: 15rem;
  tab-size: 4;
}

/* Inset, because the editor fills a panel that already has an edge and a ring
   outside it would read as a second border. Same exception the verb grid
   takes, and the same ring -- it used to be `outline: none` plus a 1px inset
   box-shadow, which is a private focus indicator for the one control on this
   page you spend the whole night inside. */
#code:focus-visible { outline: 2px solid var(--sodium); outline-offset: -2px; }

.controls { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }

button {
  appearance: none;
  font: inherit;
  font-size: var(--t-control);
  letter-spacing: .1em;
  text-transform: uppercase;
  border: 1px solid var(--rule-lit);
  background: var(--desk);
  color: var(--text);
  padding: .45rem .9rem;
  cursor: pointer;
  transition: border-color 120ms ease, color 120ms ease, background 120ms ease;
}

button:hover:not(:disabled) { border-color: var(--sodium); color: var(--sodium); }
button:disabled { opacity: .4; cursor: default; }

button.primary { border-color: var(--sodium-d); color: var(--sodium); }
button.primary:hover:not(:disabled) { background: var(--sodium); color: var(--ground); }

/* ------------------------------------------------- story ↔ code toggle */

.mode-toggle { display: inline-flex; border: 1px solid var(--rule-lit); }
.mode-toggle .mode-btn {
  border: 0;
  padding: .28rem .7rem;
  font-size: var(--t-hint);
  letter-spacing: .12em;
  background: transparent;
  color: var(--faint);
}
.mode-toggle .mode-btn + .mode-btn { border-left: 1px solid var(--rule); }
.mode-toggle .mode-btn[aria-pressed="true"],
.mode-toggle .mode-btn[aria-pressed="true"]:hover {
  background: var(--sodium);
  color: var(--ground);
}

/* Two registers for the same UI: code helpers vanish in Story, the plain
   phrasing vanishes in Code. applyMode() sets the body class before either is
   shown, so neither flashes. */
body.story-mode .code-only { display: none; }
body.code-mode .story-only { display: none; }

/* Lines a Story program used that the night did not recognise: shown here in
   the night's register, never as a traceback. */
/* The night's own voice -- serif, per Two Voices -- not the machine's mono. */
.skips { margin-top: var(--s2); font-family: var(--serif); font-size: var(--t-prose-sm); color: var(--muted); }
.skips p { margin: 0 0 .25rem; color: var(--faint); }
.skips ul { margin: 0; padding-left: 1.1rem; }
.skips li { margin: .1rem 0; }

kbd {
  font: inherit;
  /* No size of its own. It used to be .9em, which is fine inside a 12.5px
     button and is 10.37px inside an 11.5px hint -- under this project's own
     floor, in the two keycaps that tell you how to get out of the editor.
     A relative size cannot promise a floor; `opacity` was already doing the
     work of keeping the cap quieter than the sentence around it. */
  opacity: .7;
  margin-left: .35rem;
}

.status { font-size: var(--t-hint); color: var(--faint); }
.status.busy { color: var(--sodium); }

/* Tab indents rather than moving on, which would make the editor a place you
   can get into and not out of. Escape arms the next Tab; this says so, and
   lights up once it is armed so the state is visible and not just believed. */
.esc-hint {
  margin-left: auto;
  font-size: var(--t-hint);
  color: var(--faint);
  transition: color 120ms ease;
}

.esc-hint kbd { margin-left: 0; opacity: 1; color: var(--muted); }
.esc-hint.armed, .esc-hint.armed kbd { color: var(--sodium); }

/* Announced, not shown. Same trick the storyline board's rare toggle uses:
   still in the accessibility tree, out of the layout. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: 0; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.console {
  margin: 0;
  min-height: 3rem;
  max-height: 12rem;
  overflow: auto;
  padding: var(--s2);
  background: var(--sunk);
  border: 1px solid var(--rule);
  font-size: var(--t-control);
  line-height: 1.55;
  color: var(--muted);
  white-space: pre-wrap;
}

.console .out { color: var(--green); }
.console .err { color: var(--red); }

/* The night's own line, inside the machine's box. Serif because a day wrote it
   and the machine did not -- the console was rendering it in mono, which put
   the wood's voice in the terminal's mouth. */
.console .went-on {
  margin: var(--s2) 0 0;
  font-family: var(--serif);
  font-size: var(--t-prose-sm);
  line-height: 1.5;
  color: var(--text);
  white-space: normal;
}

/* Said once per stumble, quietly, at the only moment anybody doubts it. */
.console .no-loss {
  margin: var(--s1) 0 0;
  font-size: var(--t-hint);
  letter-spacing: .06em;
  color: var(--faint);
  white-space: normal;
}

/* ------------------------------------------------------------- floor plan */

/* Pinned. The editor is a long way down the left column, so without this you
   hit Run and are looking at the wrong half of the page while it plays. */
/*
 * One scroll region on this page, not two.
 *
 * This was sticky, capped at the viewport, with overflow-y: auto -- which is
 * the textbook tall-sticky-sidebar recipe and it cost more than it bought.
 * The panel moved for two hundred pixels, froze, and from then on its content
 * only moved if you scrolled inside it: a second scrollbar down the right
 * edge, the seventh way out clipped behind it, and a slide back up at the
 * bottom of the page where the sticky range ran out of container. It also
 * needed an opaque background and a z-index to stop it painting over the
 * ways-out board, and `block: "nearest"` on the ending because the card kept
 * arriving inside overflow nobody had scrolled.
 *
 * The moment that actually needs this panel on screen is pressing Run, and
 * showStage() already puts it there. So it scrolls with the page like
 * everything else.
 */
.stage {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  padding-bottom: var(--s2);
}

.plan-wrap { border: 1px solid var(--rule); background: var(--sunk); }

/* One rule for every night, the generic map included -- there is exactly one
   `viewBox="0 0 480 300"` svg inside each frame and it fills the frame's
   width. Width is what drives: `height: auto` takes the height off the
   viewBox's own ratio, so the drawing lands on the frame exactly and there is
   nothing left over.
 *
 * There used to be a `max-height: 46vh` here as well, and on any window taller
 * than it was wide the two constraints fought: the height cap won, the svg
 * element stayed full width, and `xMidYMid meet` centred a drawing narrower
 * than the box it sat in. At 1406x781 that was 83 dead pixels -- 12% of the
 * frame -- as a band of nothing down both sides of every night. */
.plan-wrap svg { display: block; width: 100%; height: auto; max-height: var(--plan-cap); }

/* What the plan gives up room *to*, which is not what it used to be.
 *
 * The Short-Window Rule asked the plan, the caption, the transport and the
 * ending to fit one screen, and gave the plan a flat 33vh to make it happen.
 * Measured on a 1516x674 window, the column wants 844px: the cap was buying
 * an unbroken rule at 170px short, and charging the night half its size for
 * it. It was also charging most on exactly the windows that had the most room
 * to spare sideways -- a wide, short laptop screen put a 356px night in the
 * middle of a 712px column.
 *
 * So the plan gives way to what has to be *beside* it while a night plays --
 * the pane's head, the caption and the transport, 178px of them measured with
 * a three-line caption, reserved as 12rem for slack. The ending card and the
 * ways-out board are not in that list: they arrive after the run and both
 * already walk themselves onto the screen (`el.ending.scrollIntoView`), so
 * reserving room for them all night costs the night and buys them nothing.
 *
 * A calc rather than a breakpoint, because there was never anything special
 * about 720px -- the constraint is continuous and now says so. On any window
 * tall enough for the column to drive the size, this resolves larger than the
 * column and does nothing at all. */
:root { --plan-cap: calc(100vh - 12rem); }

/* `width: 100%` with `align-self`, never `margin-inline: auto`. The frame is a
   flex item in `.stage`'s column, and an auto inline margin there switches it
   off stretch sizing and onto shrink-to-fit -- which for an svg carrying no
   width attribute is the CSS replaced-element default of 300px, so the whole
   night rendered at 300x188 however much room it had. */
.plan-wrap {
  width: 100%;
  max-width: calc(var(--plan-cap) * 480 / 300);
  align-self: center;
}

.help-q {
  font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--t-control);
  line-height: 1;
  color: var(--faint);
  background: none;
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: 0.25rem 0.5rem;
  cursor: pointer;
}
.help-q:hover { color: var(--sodium); border-color: var(--sodium); }
.help-q:focus-visible { outline: 2px solid var(--sodium); outline-offset: 2px; }
.help-q[aria-expanded="true"] { color: var(--sodium); border-color: var(--sodium); }

/* How-to popover: the sanctioned Popover shadow -- floating over the page,
   dismissible, gone when you are done with it. */
.how-pop {
  position: absolute;
  top: calc(100% + var(--s1));
  right: 0;
  z-index: 3;
  width: min(30rem, 90%);
  background: var(--desk);
  border: 1px solid var(--rule-lit);
  border-radius: 2px;
  padding: var(--s3);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
}
.how-title { font-family: var(--mono); font-size: var(--t-label); letter-spacing: 0.18em;
             color: var(--muted); margin-bottom: var(--s2); }
.how-pop ol { list-style: none; display: grid; gap: var(--s2); margin: 0;
              font-family: var(--mono); font-size: var(--t-base); line-height: 1.6;
              color: var(--muted); }
.how-pop li { display: flex; gap: var(--s2); align-items: baseline; }
.how-pop code { font-size: var(--t-code); color: var(--text); }
.how-try { flex: none; font-family: var(--mono); font-size: var(--t-control);
           color: var(--sodium); background: none; cursor: pointer;
           border: 1px solid var(--rule-lit); border-radius: 2px;
           padding: 0.15rem 0.45rem; }
.how-try:hover { border-color: var(--sodium); }

.floor { fill: var(--desk); stroke: var(--rule-lit); stroke-width: 1.5; }
.desk  { fill: var(--sunk); stroke: var(--rule-lit); stroke-width: 1; }

svg .tag {
  fill: var(--faint);
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: .14em;
}

svg .tag.micro { font-size: 7.5px; }

/* The leaf slides up the wall and the doorway lights up behind it. Rotating a
   door open in plan view collides with everything standing near it. */
.doorway { fill: var(--sunk); transition: fill 300ms ease, fill-opacity 300ms ease; fill-opacity: 1; }
.doorway.open { fill: var(--sodium); fill-opacity: .4; }

.door {
  fill: var(--rule-lit);
  stroke: var(--muted);
  stroke-width: 1;
  transition: transform var(--m-move);
}

.door.open { transform: translateY(-70px); }

.reader { fill: var(--red); transition: fill 300ms ease; }
.reader.green { fill: var(--green); }

.window { fill: var(--sunk); stroke: var(--rule-lit); stroke-width: 1.5; }
.lift { fill: var(--sunk); stroke: var(--rule); stroke-width: 1.5; stroke-dasharray: 3 2; }

.keypad { fill: var(--sunk); stroke: var(--rule-lit); stroke-width: 1; transition: stroke 300ms ease; }
.keypad.known { stroke: var(--sodium); }

.camera { fill: var(--green); transition: fill 300ms ease, opacity 300ms ease; }
.camera.covered { fill: var(--faint); opacity: .4; }

.prop { fill: var(--rule); transition: fill 300ms ease; }
.prop.touched { fill: var(--sodium-d); }

.alarm { fill: var(--sunk); stroke: var(--red); stroke-width: 1.5; transition: fill 300ms ease; }
.alarm.pulled { fill: var(--red); }

#alarm-group.pulled { animation: klaxon 900ms ease-in-out infinite; }
@keyframes klaxon { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }

/* `r` is in here because night 004 shrinks the figure when you get in the
   water, and a radius that snaps reads as a different dot arriving rather
   than as the same person going under. */
.figure { stroke-width: 1.5;
          transition: opacity 300ms ease, fill 300ms ease, r var(--m-move); }
.guard-figure { fill: var(--desk); stroke: var(--muted); }
.you-figure   { fill: var(--sodium); stroke: var(--sodium); fill-opacity: .22; }

/* Scoped to the drawing. Unscoped, this leaked onto the ways-out board, whose
   rows also carry a .name -- six HTML spans inheriting a size meant for a
   figure's caption inside a 480x300 viewBox, rendering at a literal 8px. The
   `fill` was always the tell: it does nothing at all to an HTML span. */
svg .name {
  fill: var(--muted);
  font-family: var(--mono);
  font-size: 8px;
  letter-spacing: .12em;
}

#guard { transition: transform var(--m-move), opacity 300ms ease; }

/* You walk to the window and THEN go out of it. Fading while still moving
   reads as nothing having happened at all. */
#you, #badge {
  transition: transform var(--m-move), opacity 320ms ease 300ms;
}

/* The corridor was the only night anybody walked across. The five stages built
   after it place the figure with the same `translate()` every tick and had no
   transition on it at all, so on nights 002-006 you did not cross the room --
   you were simply somewhere else on the next frame, 620ms after being here.
   The torch and the phone's glow move with you for the same reason: they are
   light you are carrying, and light that arrives after its owner is a bug.
 *
 * Gated on .placed because none of these groups carries a resting position in
 * the markup the way #you does. The first paint is where the night starts, not
 * a move to it; ungated, every one of them would slide in from the corner of
 * the viewBox on the opening frame. render() adds the class after painting. */
.plan-wrap.placed #r2-you,
.plan-wrap.placed #r3-you,
.plan-wrap.placed #r4-you,
.plan-wrap.placed #r5-you,
.plan-wrap.placed #r6-you {
  transition: transform var(--m-move);
}

/* Opacity is the torch going out, and it must not wait for the walk. */
.plan-wrap.placed #r3-cone,
.plan-wrap.placed #r3-glow {
  transition: transform var(--m-move), opacity 300ms ease;
}

#guard.asleep .guard-figure { fill-opacity: .35; stroke: var(--faint); }
#guard.gone { opacity: .15; }
#you.gone, #badge.gone { opacity: 0; }

svg .zzz { fill: var(--sodium); font-family: var(--mono); font-size: 9px; opacity: 0; transition: opacity 300ms ease; }
#guard.asleep .zzz { opacity: 1; }

.badge-chip { fill: var(--sodium); stroke: var(--sodium); stroke-width: .8; fill-opacity: .5; }

.meter-bg { fill: var(--rule); }
.meter { fill: var(--sodium-d); transition: width 380ms ease; }
#r3-you.lost { opacity: .55; }
#r3-you.lost .name { letter-spacing: .04em; }
.trust { fill: var(--sodium); }
.susp  { fill: var(--red); }

.lampglow { transition: opacity 400ms ease; }

/* -------------------------------------------------- the generic map ---- */

/* Sizing lives with `.plan-wrap svg`, above -- the map is one of the frames.
   Its own copy of the cap outlived the shared rule and would have put the
   letterbox back on every undrawn night. */

.link { stroke: var(--rule-lit); stroke-width: 1.2; stroke-dasharray: 3 4; }

.place circle { fill: var(--sunk); stroke: var(--rule-lit); stroke-width: 1.5; transition: all 300ms ease; }
.place text { fill: var(--faint); }
.place.exit circle { stroke: var(--green); }
.place.bad circle { stroke: var(--red); }
.place.climb circle { stroke-dasharray: 2 2; }
.place.on circle { fill: var(--sodium); fill-opacity: .25; stroke: var(--sodium); }
.place.on text { fill: var(--sodium); }

.plan-wrap.placed #map-you { transition: transform var(--m-move); }

.gauges {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s3);
  padding: var(--s2) var(--s3);
  border-top: 1px solid var(--rule);
}

.gauge { display: flex; align-items: center; gap: var(--s2); font-size: var(--t-hint); }
.g-k { color: var(--faint); letter-spacing: .12em; }
.g-v { color: var(--sodium); font-variant-numeric: tabular-nums; }
.bar { width: 5rem; height: 5px; background: var(--rule); display: inline-block; }
.bar i { display: block; height: 100%; background: var(--sodium); }
/* No transition on that width. The whole gauge block is rebuilt with
   innerHTML on every frame, so the bar is a new element with no previous
   value to travel from -- the rule has never once fired. */

.lamps { display: flex; flex-wrap: wrap; gap: .3rem; margin-left: auto; }

.lamp {
  font-size: var(--t-label);
  letter-spacing: .1em;
  padding: .1rem .4rem;
  border: 1px solid var(--rule);
  color: var(--faint);
  transition: color 300ms ease, border-color 300ms ease;
}

.lamp.lit { color: var(--sodium); border-color: var(--sodium-d); }

/* ---------------------------------------------------------- day picker */
/*
 * One quiet line rather than a tab per night. Tabs do not survive a pool that
 * grows every day, and the archive was always home.html.
 *
 * It lives in the rail. Inside the stage it sat above the picture and read as
 * a caption on it rather than as somewhere to go.
 */

.days {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-right: auto;
  font-size: var(--t-label);
  letter-spacing: .12em;
  text-transform: uppercase;
  white-space: nowrap;
}

.days a {
  color: var(--muted);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px;
  transition: color 120ms ease, border-color 120ms ease;
}

.days a:hover { color: var(--sodium); border-color: var(--sodium-d); }

/* Loud enough to be worth reading, because it is the one thing on this page
   that can be wrong about what night it is. It only appears when the night on
   screen is genuinely not tonight -- arriving at tonight through the board's
   own ?day= link used to be greeted with it. */
.days-note { color: var(--sodium); }

/* A day the corridor cannot depict gets its own state, plainly. */
/* `display: grid` below is author-origin and beats the UA's [hidden] rule, so
   without this every mapped day drew an empty bordered box under its gauges.
   Four other components in this file already say it; this one was missed. */
.readout[hidden] { display: none; }

.readout {
  border: 1px solid var(--rule);
  background: var(--sunk);
  padding: var(--s3);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: .2rem var(--s3);
}

.read { display: flex; justify-content: space-between; gap: var(--s2); font-size: var(--t-control); }
.read .k { color: var(--muted); }
.read .v.on { color: var(--green); }
.read .v.off { color: var(--faint); }
.read .v.num { color: var(--sodium); font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------- caption */

.caption { display: flex; flex-direction: column; gap: var(--s1); min-height: 4.4rem; }

.caption-line { display: flex; align-items: baseline; gap: var(--s2); }
.marker { display: block; flex: none; color: var(--sodium); }

.caption code.resumed { color: var(--faint); }

.caption code {
  font-family: var(--mono);
  font-size: var(--t-code);
  color: var(--text);
}

.tickno {
  margin-left: auto;
  font-size: var(--t-hint);
  color: var(--faint);
  letter-spacing: .1em;
  font-variant-numeric: tabular-nums;
}

.said {
  margin: 0;
  font-family: var(--serif);
  font-size: var(--t-prose);
  color: var(--muted);
  padding-left: var(--s3);
  border-left: 1px solid var(--rule);
}

.transport { display: flex; gap: var(--s1); }
.transport button {
  display: inline-flex;
  align-items: center;
  padding: .3rem .7rem;
  font-size: var(--t-hint);
}
.transport button svg { display: block; }

/* ---------------------------------------------------------------- ending */

.ending[hidden] { display: none; }

/* Offered only when a run resolved nothing and there is a board to send to.
   Quiet -- it is an invitation, not a failure notice; the amber is on the one
   button, the way it is everywhere else. */
.propose[hidden] { display: none; }
.propose {
  margin-top: var(--s3);
  padding: var(--s3);
  border: 1px solid var(--rule);
  border-radius: 2px;
  background: var(--sunk);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}
.propose-lead {
  margin: 0;
  font-family: var(--serif);
  font-size: var(--t-prose-sm);
  line-height: 1.5;
  color: var(--muted);
}
.propose-row { display: flex; gap: var(--s2); flex-wrap: wrap; }
.propose-row[hidden] { display: none; }
.propose-note,
.propose input {
  flex: 1;
  min-width: 12rem;
  background: var(--ground);
  border: 1px solid var(--rule);
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--t-code);
  padding: .4rem .55rem;
}
.propose input:focus { border-color: var(--sodium-d); }
.propose button {
  flex: none;
  font-size: var(--t-control);
  letter-spacing: .06em;
  color: var(--ground);
  background: var(--sodium);
  border: 0;
  border-radius: 2px;
  padding: .4rem .8rem;
  cursor: pointer;
}
/* Keep the text dark on hover: the generic button:hover flips color to sodium,
   which on this button's sodium-d hover background is amber on amber (1.7:1). */
.propose button:hover:not(:disabled) { background: var(--sodium-d); color: var(--ground); }
.propose button:disabled { opacity: .6; cursor: default; }
.propose-done { margin: 0; font-size: var(--t-prose-sm); color: var(--green); }
.propose-done[hidden] { display: none; }

.ending {
  border: 1px solid var(--sodium-d);
  background: var(--desk);
  padding: var(--s3);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  animation: resolve 560ms cubic-bezier(.16, 1, .3, 1) both;
}

.ending-label {
  font-size: var(--t-hint);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--sodium);
}

.ending-text {
  margin: 0;
  font-family: var(--serif);
  font-size: var(--t-prose);
  line-height: 1.6;
}

@keyframes surface {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}

/* The night answering. Its own keyframe rather than `surface`, because this is
   the one moment the whole session is remembered by and it had been borrowing
   a generic 4px nudge. Still restrained -- ten pixels, no bounce, no scale --
   but it decelerates hard out of the held beat, so it reads as something
   settling into place rather than something that was already there. */
@keyframes resolve {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* ----------------------------------------------------------------- board */

/* The stage scrolls internally, so the ways out sit at the bottom of it and
   are reached by scrolling the panel rather than the page. */
.board-head { margin-top: var(--s2); }

.board { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }

.board li {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: .3rem 0;
  border-bottom: 1px solid var(--rule);
  color: var(--faint);
  font-size: var(--t-code);
}

.board li:last-child { border-bottom: 0; }
.board li.found { color: var(--text); }
/* The mark for one way out: a square, filled once you have found it, and the
   same square on the game page and the board page because it is one
   vocabulary. It was a BLACK SQUARE / LIGHT SHADE character pair, which meant
   it inherited whatever those glyphs look like in the player's font. */
.glyph {
  display: block;
  flex: none;
  width: 12px;
  height: 12px;
  border: 1px solid var(--rule);
  border-radius: 2px;
}
.board li.found .glyph { background: var(--sodium-d); border-color: var(--sodium-d); }

.board li .num { color: var(--faint); font-variant-numeric: tabular-nums; }
.board li .rare {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  margin-left: auto;
  color: var(--sodium);
}
.board li.found.is-rare .glyph { background: var(--sodium); border-color: var(--sodium); }

/* ------------------------------------------------------------------ boot */

.boot {
  position: fixed;
  inset: 0;
  background: var(--ground);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  z-index: 10;
  text-align: center;
  padding: var(--s3);
}

.boot p { margin: 0; color: var(--sodium); letter-spacing: .06em; }
.boot .boot-note { color: var(--faint); font-size: var(--t-control); letter-spacing: 0; }
html:not([data-booting]) .boot { display: none; }

/* Only ever on screen when boot failed. The thing to press comes before the
   reason, because the reason is for whoever you report it to, not for you. */
.boot-retry { margin-top: var(--s1); }
.boot .boot-why { max-width: 32rem; font-size: var(--t-hint); opacity: .75; }

@media (max-width: 62rem) {
  .layout { grid-template-columns: minmax(0, 1fr); }
}

/* --------------------------------------------------------------- who you are
 *
 * The identity strip and the door. Same sodium-at-night world as the rest;
 * nothing here should feel like a product signup, because it isn't one.
 */

.who { position: relative; flex: none; }

.who-name {
  font-family: var(--mono);
  font-size: var(--t-control);
  letter-spacing: .04em;
  color: var(--muted);
  background: none;
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: var(--s1) var(--s2);
  cursor: pointer;
  transition: color 200ms ease, border-color 200ms ease;
}
.who-name:hover { color: var(--text); border-color: var(--rule-lit); }

.who-menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--s1));
  z-index: 40;
  width: 20rem;
  padding: var(--s3);
  background: var(--desk);
  border-radius: 2px;
  box-shadow: var(--shadow-popover);
}
.who-menu[hidden] { display: none; }

.who-key-label, .who-note {
  margin: 0;
  font-size: var(--t-label);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--faint);
}
.who-note { text-transform: none; letter-spacing: 0; line-height: 1.5; margin-top: var(--s1); }

.who-key {
  margin: var(--s1) 0 0;
  font-size: var(--t-prose);
  color: var(--sodium);
  user-select: all;
}

.who-progress {
  margin-top: var(--s3);
  padding-top: var(--s2);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: var(--s1);
}

.who-day {
  display: flex;
  justify-content: space-between;
  gap: var(--s2);
  /* Bled out to the card's padding, so the hover reads as the whole row. */
  padding: .2rem .35rem;
  margin: 0 -.35rem;
  border-radius: 2px;
  font-size: var(--t-control);
  color: var(--faint);
  text-decoration: none;
}
.who-day:hover { background: var(--sunk); color: var(--text); }
.who-day.played { color: var(--text); }
/* The night you are already on. It stays a link -- going to where you are is
   harmless -- but it should not look like somewhere else to go. */
.who-day.here { color: var(--sodium); }
.who-day-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.who-day-count { font-variant-numeric: tabular-nums; flex: none; }

.who-out {
  margin-top: var(--s3);
  width: 100%;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .06em;
  color: var(--faint);
  background: none;
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: var(--s1);
  cursor: pointer;
}
.who-out:hover { color: var(--red); border-color: var(--red); }

/* ------------------------------------------------------------------- the door */

.gate {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s4);
  background: var(--scrim);
}
.gate[hidden] { display: none; }

.gate-card {
  width: min(28rem, 100%);
  padding: var(--s4);
  background: var(--desk);
  border: 1px solid var(--rule-lit);
  border-radius: 3px;
}

/* The mark itself, where an "Exit Code" kicker used to sit above the heading.
   A modal is the one place the rail is not on screen, so this is the only
   place in the product where the logo is doing identity work rather than
   navigation -- which is a better job for it than labelling a question. */
.gate-mark { display: block; margin-bottom: var(--s3); }
.gate-mark .mark-frame { stroke: var(--rule-lit); }
.gate-mark .mark-light { fill: var(--sodium); }

.gate-card h2 {
  margin: 0 0 var(--s2);
  font-family: var(--serif);
  font-size: var(--t-headline);
  font-weight: 400;
  letter-spacing: .01em;
}

.gate-note {
  margin: 0 0 var(--s3);
  color: var(--muted);
  font-size: var(--t-code);
  line-height: 1.6;
}

.gate-form { display: flex; gap: var(--s2); }

.gate-form input {
  flex: 1;
  min-width: 0;
  font-family: var(--mono);
  font-size: var(--t-base);
  color: var(--text);
  background: var(--sunk);
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: var(--s2);
}
/* The border warming is the resting affordance; the ring is what tells you
   where the keyboard is. It used to switch the ring off and keep only the
   border, which is --rule going to --sodium-d: two dark tones, on the two
   fields that are the whole of the login. */
.gate-form input:focus { border-color: var(--sodium-d); }
.gate-form input::placeholder { color: var(--faint); }

.gate-form button {
  flex: none;
  font-family: var(--mono);
  font-size: var(--t-code);
  color: var(--ground);
  background: var(--sodium);
  border: 1px solid var(--sodium);
  border-radius: 2px;
  padding: var(--s2) var(--s3);
  cursor: pointer;
}
/* The colour has to be restated. The bare `button:hover` rule sets it to
   --sodium, which lands sodium text on a sodium-d fill at 1.8:1 -- the single
   worst contrast anywhere in the project, and only reachable with a mouse. */
.gate-form button:hover {
  background: var(--sodium-d);
  border-color: var(--sodium-d);
  color: var(--ground);
}

#gate-return button {
  color: var(--muted);
  background: none;
  border-color: var(--rule-lit);
}
#gate-return button:hover { color: var(--text); background: none; border-color: var(--text); }

.gate-or {
  margin: var(--s3) 0;
  text-align: center;
  font-size: var(--t-label);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--faint);
}

.gate-error {
  margin: var(--s3) 0 0;
  font-size: var(--t-control);
  color: var(--red);
}
.gate-error[hidden] { display: none; }

/* The way past the door. Quiet, and deliberately not a button-looking button:
   it is the third option, not a competing one. */
.gate-skip {
  display: block;
  width: 100%;
  margin-top: var(--s3);
  padding-top: var(--s3);
  border: 0;
  border-top: 1px solid var(--rule);
  border-radius: 0;
  background: none;
  color: var(--muted);
  font-size: var(--t-hint);
  letter-spacing: .1em;
}
.gate-skip:hover:not(:disabled) { color: var(--sodium); background: none; }

/* ------------------------------------------------------------- first run
 *
 * The product's own claim, stated once, beside the editor. Shown only to
 * somebody with nothing on this night and never shown twice.
 */

.first-run {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  margin-top: var(--s3);
  padding: var(--s3);
  border: 1px solid var(--sodium-d);
  background: var(--desk);
  animation: surface 400ms ease both;
}

.first-run[hidden] { display: none; }

.first-run p {
  margin: 0;
  flex: 1;
  font-family: var(--serif);
  font-size: var(--t-prose-sm);
  line-height: 1.5;
  color: var(--text);
}

.first-run b { font-weight: 400; color: var(--sodium); }
.first-run button { flex: none; font-size: var(--t-hint); }

/* ----------------------------------------------------------------- levels
 *
 * How much of the loop you wrote. Not a difficulty setting and not a menu --
 * the engine reads it off your program, so this is a label, never a control.
 */

.ending-line {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  flex-wrap: wrap;
}

.ending-level {
  flex: none;
  padding: 1px var(--s2);
  border: 1px solid var(--rule-lit);
  border-radius: 2px;
  font-size: var(--t-label);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--faint);
}
.ending-level.plan { color: var(--sodium); border-color: var(--sodium-d); }

.board .planned {
  display: inline-flex;
  margin-left: var(--s1);
  color: var(--sodium-d);
}
.rare svg, .planned svg { display: block; }

/* The way back to a program you already ran. `margin-left: auto` on the first
   one only, so the rare and planned marks stay beside the title where they
   describe it and the controls gather at the right edge where a control
   belongs. Not `--faint`: faint is reserved for things that are not yet true,
   and this one is true or it is not rendered at all. */
.board .route {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: .3rem;
  padding: .15rem .35rem;
  background: none;
  border: 1px solid transparent;
  border-radius: 2px;
  color: var(--muted);
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}
.board .route:first-of-type { margin-left: auto; }
.board .route + .route { margin-left: var(--s1); }
.board .route:hover { color: var(--sodium); border-color: var(--sodium-d); }
.board .route:focus-visible {
  outline: 2px solid var(--sodium);
  outline-offset: 2px;
}
.board .route svg { display: block; }
.board .route-level {
  font-size: var(--t-label);
  letter-spacing: .16em;
  text-transform: uppercase;
}

/* Two ways to play. A label, not a control panel: the bare `button` rule
   uppercases and letter-spaces everything, which turned two short sentences
   into shouting, and 28 state keys laid out flat took more room than the
   editor. */

.levels { margin-top: var(--s3); }
/* Label left, hint right, same shape as every other section head here. */
.levels .lbl { display: inline; }
.levels-hint { margin-left: var(--s2); }

/* One card wide now: Script leads alone and Plan moved into the fold below. */
.level-row { display: flex; gap: var(--s2); margin-top: var(--s1); }

.level {
  flex: 1;
  min-width: 0;
  text-align: left;
  font-family: var(--mono);
  font-size: var(--t-hint);
  text-transform: none;
  letter-spacing: 0;
  line-height: 1.45;
  background: var(--sunk);
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: .4rem .55rem;
  cursor: pointer;
  transition: border-color 200ms ease;
}
.level:hover { border-color: var(--sodium-d); background: var(--sunk); }

.level b {
  display: block;
  font-size: var(--t-label);
  font-weight: 400;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--sodium);
}
.level span { display: block; color: var(--faint); }
.level code { font-size: var(--t-hint); color: var(--muted); }

/* The opt-in second way to play. Same disclosure vocabulary as state() below
   it -- a drawn caret, not a typed glyph -- and its own note and button live
   inside, so the first screen shows only the script. */
.more-play { margin-top: var(--s2); }
/* Sentence case, matching the state() disclosure right below it -- a 46-char
   label set in all-caps reads as a shout and worse than a heading, and this
   one is a full phrase, not a two-word section head. */
.more-play > summary {
  font-size: var(--t-code);
  letter-spacing: 0;
  color: var(--muted);
  cursor: pointer;
  list-style: none;
  padding: .3rem 0;
}
.more-play > summary::-webkit-details-marker { display: none; }
.more-play > summary::before {
  content: "";
  display: inline-block;
  width: .4em; height: .4em;
  margin: 0 .85em 0 .15em;
  vertical-align: .08em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 160ms ease;
}
.more-play[open] > summary::before { transform: rotate(45deg); }
.more-play > summary:hover { color: var(--muted); }
.more-play .more-note {
  margin: var(--s1) 0 var(--s2);
  font-family: var(--serif);
  font-size: var(--t-prose-sm);
  line-height: 1.5;
  color: var(--muted);
}
.more-play .level { margin-bottom: var(--s2); }

.state-keys { margin-top: var(--s2); }

.state-keys summary {
  font-size: var(--t-label);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--faint);
  cursor: pointer;
  list-style: none;
  /* It was 17.6px tall, under WCAG 2.2's 24px target minimum, and it is a
     block rather than a word inside a sentence, so no inline exception. */
  padding: .3rem 0;
}
.state-keys summary::-webkit-details-marker { display: none; }
/* Drawn, not typed. It was BLACK RIGHT-POINTING SMALL TRIANGLE and its
   downward twin, two characters that sit on different baselines and jump
   when the details opens. */
.state-keys summary::before {
  content: "";
  display: inline-block;
  width: .4em;
  height: .4em;
  /* The rotated square only fills its diagonal, so the optical gap is smaller
     than the margin says. Measured, not guessed. */
  margin: 0 .85em 0 .15em;
  vertical-align: .08em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 160ms ease;
}
.state-keys[open] summary::before { transform: rotate(45deg); }
.state-keys summary:hover { color: var(--muted); }
.state-keys summary:focus-visible { color: var(--muted); }
.state-keys summary code { text-transform: none; letter-spacing: 0; }
.state-keys summary b { font-weight: 400; color: var(--faint); }

/* Even open, it cannot take the column over. */
.state-keys p {
  margin: var(--s1) 0 0;
  max-height: 6.5rem;
  overflow-y: auto;
  font-size: var(--t-hint);
  line-height: 1.6;
  color: var(--faint);
  word-break: break-word;
}

/* ------------------------------------------------------------------ the mark
 *
 * A doorway with the light on the other side of it. Top-left on every page,
 * and it goes home -- which is where a logo goes, and it saves the masthead a
 * button that never fitted anywhere.
 */

.mark {
  display: flex;
  align-items: center;
  gap: var(--s2);
  text-decoration: none;
  flex: none;
}
.mark svg { display: block; overflow: visible; }
.mark-frame {
  fill: none;
  stroke: var(--rule-lit);
  stroke-width: 1.6;
  transition: stroke 200ms ease;
}
.mark-light { fill: var(--sodium-d); transition: fill 200ms ease; }
.mark:hover .mark-frame { stroke: var(--muted); }
.mark:hover .mark-light { fill: var(--sodium); }

.mark-word {
  font-size: var(--t-label);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--faint);
  white-space: nowrap;
}
.mark:hover .mark-word { color: var(--muted); }

@media (max-width: 46rem) {
  .mark-word { display: none; }
}

/* The night band leads with the title block, so that is what flexes. */
.masthead-night .titles { flex: 1; min-width: 0; }

/* A killed program is not an engine error, and the console must not read like
   one: what the player needs to know is that nothing they typed was lost. */
.console .note {
  display: block;
  margin-top: var(--s1);
  color: var(--faint);
}

/* ------------------------------------------------------------ the stages
   One hand-built scene per released night. Every colour here is a token;
   every word is set in the svg tag faces; the machine speaks in hairlines. */

/* 002 · the records room */
.r2-strip { fill: var(--rule); transition: fill 300ms ease; }
.r2-strip.lit { fill: var(--sodium-d); }
.r2-bay { fill: var(--sunk); stroke: var(--rule); }
.r2-bay.df { stroke: var(--sodium-d); }
.r2-front { fill: var(--desk); stroke: var(--rule-lit); transition: transform 300ms ease; }
.waves { fill: none; stroke: var(--muted); stroke-width: 1; }

/* 003 · a wood at night */
.r3-ground { fill: var(--ground); }
.r3-trunks line { stroke: var(--rule); stroke-width: 3; }
.r3-path { fill: var(--rule-lit); opacity: .18; }
.r3-stream { fill: none; stroke: var(--rule-lit); stroke-width: 2;
             stroke-dasharray: 1 7; stroke-linecap: round; }
.r3-fence { stroke: var(--rule-lit); stroke-width: 1.5; stroke-dasharray: 6 5; }
.r3-deadfall { fill: var(--rule); stroke: var(--rule-lit); }
.star { fill: var(--text); }
#r3-stars { transition: opacity 900ms ease; }
#r3-cone { fill: var(--sodium); transition: opacity 300ms ease; }
#r3-glow { fill: var(--sodium); opacity: 0; transition: opacity 300ms ease; }

/* 004 · the pool at closing time
   Two hairlines make the tank read as a basin: the deck edge and the water.
   The Hairline Rule doing the work a fill cannot -- a pool is a hole, and a
   hole is drawn with edges. A third inset line for the tile read as nested
   boxes instead, which is the failure mode of the same idea overdone. */
.r4-deck { fill: none; stroke: var(--rule); }
.r4-pool { fill: var(--sunk); stroke: var(--rule-lit); }
/* The one lit lane. This is the stage's own light, which is the day's to set,
   and it is the whole look of a municipal pool at this hour: a dark hall with
   a single lane on. Kept under a tenth, because at .09 over --sunk it stopped
   being light in water and became a brown band lying on top of it. */
.r4-lit { fill: var(--sodium); opacity: .07; }
/* Ropes, not a grid: dashed because a lane rope is floats on a line. At
   --rule they were invisible against the water they float on. */
.r4-lanes line { stroke: var(--rule-lit); stroke-dasharray: 5 6; }
.r4-lanes line.lit { stroke: var(--sodium-d); }
.r4-ladder line { stroke: var(--rule-lit); stroke-width: 1.5; stroke-linecap: round; }
.r4-office { fill: var(--rule); transition: fill 300ms ease; }
.r4-office.lit { fill: var(--sodium-d); opacity: .55; }
.r4-changing { fill: var(--desk); stroke: var(--rule); }
/* The dark arrives with the shutter, not forty milliseconds behind it. */
#r4-shutter { transition: height var(--m-move), opacity var(--m-move); }
#r4-room { transition: opacity var(--m-move); }
#r4-you.swim circle { r: 8; }

/* 005 · the locksmith's cage */
.r5-cage { fill: var(--sunk); stroke: var(--rule-lit); stroke-width: 1.5; }
.r5-cagedoor { fill: var(--desk); stroke: var(--rule-lit);
               transition: transform var(--m-move); }
.r5-slat { stroke: var(--rule-lit); stroke-width: 5; }
.r5-shaft { fill: var(--sodium); opacity: .12; transition: opacity 300ms ease; }
.r5-drawer { fill: var(--desk); stroke: var(--rule-lit); transition: transform 300ms ease; }
.r5-spark path { stroke: var(--sodium); stroke-width: 1.5; fill: none; }
#r5-led { animation: r5-pulse 1.6s ease-in-out infinite; }
#r5-led.off { fill: var(--rule-lit); stroke: var(--rule-lit); animation: none; }
@keyframes r5-pulse {
  0%, 100% { opacity: .35; }
  50%      { opacity: 1; }
}

/* 006 · the long wave */
.r6-scale { fill: none; stroke: var(--rule-lit); stroke-width: 1.5; }
.r6-tick { stroke: var(--rule-lit); }
.r6-cgtick { stroke: var(--green); stroke-width: 2; }
.r6-needle { stroke: var(--sodium); stroke-width: 2.5;
             transition: transform var(--m-sweep); }
.r6-needle.sig { stroke: var(--text); stroke-width: 1.5; }
.r6-valve { fill: var(--sodium-d); transition: fill 300ms ease; }
#r6-valves.off .r6-valve { fill: var(--rule-lit); }
.r6-jar { fill: var(--sunk); stroke: var(--rule-lit); }
.r6-acid { fill: var(--sodium-d); opacity: .85;
           transition: y 380ms ease, height 380ms ease; }
.r6-wheel { fill: none; stroke: var(--rule-lit); stroke-width: 2; }
.r6-carrier { fill: none; stroke: var(--sodium); stroke-width: 1.5;
              stroke-dasharray: 5 4; animation: r6-hum 1.2s linear infinite; }
@keyframes r6-hum {
  to { stroke-dashoffset: -18; }
}
.r6-drop { stroke: var(--rule-lit); stroke-width: 1; }
.r6-window { fill: var(--ground); stroke: var(--rule); }
.r6-spout { fill: var(--rule-lit); }
