/* Literary Nightstand, ported from candi's src/style.css. Warm browns and
   charcoal: a lamp on at night, not a UI. Everything reads off these vars so
   the palette is one edit. */
:root {
  --ink: #efe7dc;
  --ink-dim: #a89c8d;
  --ink-faint: #6d6357;
  --bg: #1a1714;
  --bg-raised: #221e1a;
  --bg-sunk: #141110;
  --rule: #332c25;
  --accent: #c98f5a;
  --accent-dim: #7d5a39;
  --measure: 34rem; /* the line length prose is actually readable at */
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
  --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--serif);
  /* The bundle ships no webfont: a 200KB face to save on a night of reading is
     a bad trade against a browser build people open on phone data. */
  -webkit-font-smoothing: antialiased;
}

#app {
  display: flex;
  flex-direction: column;
  height: 100%;
  max-width: var(--measure);
  margin: 0 auto;
  padding: 0 1.25rem;
}

/* ---- prose ---------------------------------------------------------------- */

#prose {
  flex: 1 1 0;
  /* Flex items default to min-height:auto, which refuses to shrink below their
     content — so without this the prose pane will not give space back to a tall
     console, and the two of them together overflow #app's 100% height into a
     region html/body clips with no way to scroll. Half of the bug where the ask
     menu's last group was unreachable (Aaron, 2026-07-31). */
  min-height: 0;
  overflow-y: auto;
  /* The right padding is a gutter for the overlay scrollbar, and the negative
     margin pays for it out of #app's padding so the prose keeps the same
     measure and stays aligned with the console below. Without it macOS draws
     the scrollbar straight over the last vote letter, which is both hard to
     read and hard to click (Aaron, 2026-07-30). scrollbar-gutter would be the
     tidy answer but has no effect where scrollbars are overlaid, which is
     exactly the case that breaks. */
  /* The 2.5rem at the foot is not slack: it has to clear the bottom fade, which
     is 2rem tall. See #prose.more below. */
  padding: 2.5rem 0.85rem 2.5rem 0;
  margin-right: -0.85rem;
  scroll-behavior: smooth;
}

.beat { margin: 0 0 2.25rem; }

/* What she chose, above the prose it produced. Small and dim: it is a receipt,
   not a heading. */
.beat .choice {
  font-family: var(--sans);
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 0.6rem;
}

.beat p {
  font-size: 1.0625rem;
  line-height: 1.72;
  margin: 0 0 1.1em;
  text-wrap: pretty;
}

/* Everything but the newest beat recedes, so the eye lands on what just
   happened without the history being hidden from you. */
.beat:not(:last-child) { color: var(--ink-dim); }

/* A beat that has just happened fades up instead of appearing whole. Ported
   from candi's reader (paragraphReveal, tailwind.config.js) because that one
   already reads well: 0.9s is slow enough to feel lit rather than switched on,
   and 6px of rise is enough to suggest the text settling into place without
   anything visibly sliding.

   An ANIMATION, not a transition, and that distinction is the whole reason
   this version is four rules instead of two hundred lines. `backwards` holds
   the 0% frame through the delay, so a paragraph is hidden from its first
   painted frame with nothing to hide it — no veil class to apply, no
   measurement, no timers, and no way for a forced style flush between insert
   and hide to make the beat flash before it fades.

   The stagger is per CHILD, so the choice line, the state line, the paragraphs
   and the footer all arrive as one wave rather than the prose landing on
   furniture that is already there. 50ms apart and capped at 240ms — the cap is
   the point. Uncapped, a long beat leaves its last paragraph invisible for
   over a second, which stops reading as a bloom and starts reading as a queue.
   Everything is in flight within a quarter second; it is the 0.9s that does
   the work. */
.beat.arriving > * { animation: arrive 0.9s ease-out backwards; }
.beat.arriving > *:nth-child(2) { animation-delay: 50ms; }
.beat.arriving > *:nth-child(3) { animation-delay: 100ms; }
.beat.arriving > *:nth-child(4) { animation-delay: 150ms; }
.beat.arriving > *:nth-child(5) { animation-delay: 200ms; }
.beat.arriving > *:nth-child(n+6) { animation-delay: 240ms; }
@keyframes arrive {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
/* Two children of a beat are DELIBERATELY dim at rest, and an animation ending
   at opacity 1 overrides that for as long as it runs — so the state line rose
   to full and then snapped back to 0.35 the instant it finished, a small bright
   flinch under the choice. `backwards` fill covers the delay, not the tail, so
   the only fix is to land the animation where the element actually lives.
   Measured: child 1 read 1.0 at 900ms and 0.4 at 990ms. */
.beat.arriving > .beatstate { animation-name: arrive-state; }
.beat.arriving > .tier { animation-name: arrive-tier; }
@keyframes arrive-state {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 0.35; transform: none; }
}
@keyframes arrive-tier {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 0.28; transform: none; }
}
/* Asked for less movement, given less movement. The prose still arrives, it
   just stops travelling to get here. */
@media (prefers-reduced-motion: reduce) {
  #prose { scroll-behavior: auto; }
  .beat.arriving > * { animation: none; }
}

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

#console {
  /* Takes what it needs and is ALLOWED TO SHRINK — `flex: none` was the other
     half of the unreachable-menu bug: the say drawer runs three groups and
     fifteen buttons, which is taller than the embed, and an unshrinkable,
     unbounded console simply ran off the bottom of the iframe.
     Capped so the prose never disappears entirely, and scrolled past that.

     The cap reserves a fixed READING WINDOW rather than a fixed share, because
     what the prose needs is a number of LINES, not a percentage. A flat 72%
     left 224px on an 800-tall embed and 179px on a 640-tall phone — seven
     lines and six — of a beat that runs about twenty, so the smaller the
     screen, the less of the passage you could see while answering it. The
     middle term flips that: 18rem is ten lines, held constant everywhere, and
     the console takes whatever is left.
       min 50%  — a floor, so a very short window (landscape phone, ~500px)
                  splits the difference instead of starving the menu to four
                  rows to protect prose nobody has room to read either way.
       max 72%  — the old cap, still the ceiling on tall screens, where the
                  say drawer's natural 637px fits under it anyway.
     Percentages resolve against #app, whose height is definite. */
  flex: 0 1 auto;
  max-height: clamp(50%, calc(100% - 18rem), 72%);
  overflow-y: auto;
  border-top: 1px solid var(--rule);
  background: var(--bg);
  padding: 0.9rem 0 max(1rem, env(safe-area-inset-bottom));
}

#banner {
  font-family: var(--sans);
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--ink-dim);
  background: var(--bg-sunk);
  border-left: 2px solid var(--accent-dim);
  padding: 0.6rem 0.75rem;
  margin-bottom: 0.75rem;
}

nav#menu { display: flex; flex-direction: column; gap: 0.35rem; }

/* Group headers inside a drawer — same contract as the CLI: one per group, and
   the groups arrive contiguous so it never prints twice. */
.group {
  font-family: var(--sans);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0.5rem 0 0.1rem;
}

/* ---- the two kinds of button ------------------------------------------------
   .choice is the FICTION: a thing the character does. Full width, left aligned,
   set as a sentence, because that is what it is — the console is a menu of
   sentences and should read as one.

   .btn is the FURNITURE: done, save, import, delete, undo, back. See further
   down.

   Keeping them apart is the whole point. One class used to do both jobs, and the
   saves panel was the proof: "save this night", "download all", "import a file"
   and "forget this key" arrived as four full-width left-aligned sentences,
   indistinguishable from a menu of things the character could do, in a panel
   where the character does nothing at all. Eight override rules existed to patch
   .choice back into the shape each surface actually wanted — .utility, .rating,
   .keyring-row, .panel-head, .slot, #gate, #landing, #rung — which is a
   taxonomy asking to be written down (Aaron, 2026-07-31). This is it written
   down. */

button.choice {
  display: block;
  width: 100%;
  text-align: left;
  font-family: var(--sans);
  font-size: 0.9375rem;
  line-height: 1.4;
  color: var(--ink);
  background: var(--bg-raised);
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0.7rem 0.85rem;
  cursor: pointer;
  /* Fingers, not mice. */
  min-height: 2.75rem;
}
button.choice:hover { border-color: var(--accent-dim); }
button.choice:active { background: var(--bg-sunk); }
button.choice:disabled { opacity: 0.45; cursor: default; }

/* Keyboard focus, in this palette rather than the browser's. Chrome's default is
   a saturated blue that appears nowhere else on the page, and it lands on the
   first thing anyone sees: both front doors focus their affirmative button on
   open, so the very first frame of the game was a warm brown screen with one
   system-blue rectangle on it.

   Zero specificity via :where(), so the two places that deliberately handle
   their own focus — the freeform input, which swaps the ring for a border
   colour, and #gear — still win without needing !important. :focus-visible and
   not :focus, so a mouse click does not leave a ring behind. */
:where(button, input, a, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The one press that carries the night. ONE per surface — the rule that makes
   .primary mean anything is that there is never a second one to compare it to. */
button.choice.primary {
  border-color: var(--accent-dim);
  color: var(--accent);
}

/* ---- .btn: the furniture ----------------------------------------------------
   Quieter than a choice on every axis that carries weight — smaller, dimmer,
   transparent rather than raised, and only as wide as its label. A control the
   player operates, not a thing the character does.

   Modifiers, all opt-in, because the default should be the common case:
     .block   stretch to the container (stacked panel actions, list rows)
     .primary the one affirmative press on this surface
     .icon    a square for a single glyph
     .grow    equal share of a flex row (side-by-side decision pairs) */
button.btn {
  display: inline-block;
  width: auto;
  text-align: center;
  font-family: var(--sans);
  font-size: 0.8125rem;
  line-height: 1.4;
  color: var(--ink-dim);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0.5rem 0.9rem;
  cursor: pointer;
  /* Still a touch target. Smaller than a choice, never smaller than a thumb. */
  min-height: 2.25rem;
  transition: color 120ms ease, border-color 120ms ease;
}
button.btn:hover { color: var(--ink); border-color: var(--accent-dim); }
button.btn:active { background: var(--bg-sunk); }
button.btn:disabled { opacity: 0.45; cursor: default; }

button.btn.block { display: block; width: 100%; }
button.btn.grow { flex: 1; }
button.btn.primary { color: var(--accent); border-color: var(--accent-dim); }
button.btn.icon {
  flex: 0 0 2.75rem;
  width: 2.75rem;
  padding: 0;
  font-size: 0.9375rem;
}

/* Persistent, quiet: stop / rewind / end the night live here and never move.
   The row holds one of each kind — "End the night here" is the fiction, "undo"
   is the furniture — so it sizes the choices down to meet the buttons rather
   than restyling either. */
.utility { display: flex; gap: 0.35rem; margin-top: 0.45rem; }
.utility button.choice {
  font-size: 0.8125rem;
  padding: 0.5rem 0.7rem;
  min-height: 2.25rem;
  color: var(--ink-dim);
  background: transparent;
}
/* Hold its natural width in a row that shrinks. .choice carries width:100% and
   so has a basis to give up; a .btn is auto-width and gets squeezed first —
   which folded "← undo that" into three stacked words while the story choices
   either side of it kept their full measure. The label is short and fixed, so
   the row should take its space out of the sentences, not out of it. */
.utility button.btn { flex: 0 0 auto; white-space: nowrap; }

/* ---- her own words -------------------------------------------------------- */

/* The end-of-night note shares this input wholesale rather than growing a second
   one. Two text fields that ask the player for a sentence should not look like
   two different affordances, and the only difference between them is which
   question is in the placeholder. NOT `.rating` or any name already spoken for
   further down — see the warning under the thumbs. */
form#freeform, form.night-note { display: flex; gap: 0.35rem; margin-top: 0.45rem; }
form#freeform input, form.night-note input {
  flex: 1;
  font-family: var(--serif);
  font-size: 0.9375rem;
  color: var(--ink);
  background: var(--bg-sunk);
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0.6rem 0.7rem;
}
form#freeform input::placeholder, form.night-note input::placeholder { color: var(--ink-faint); }
form#freeform input:focus, form.night-note input:focus { outline: none; border-color: var(--accent-dim); }

/* ---- thumbs --------------------------------------------------------------- */

/* NOT `.rating` — that name is already a five-column grid further down, and
   taking it turned the thumbs into grid cells 200px tall. */
.verdict { margin-top: 0.4rem; }
.thumbs { display: flex; gap: 0.3rem; }
.thumbs button {
  font-family: var(--sans);
  font-size: 0.8125rem;
  color: var(--ink-faint);
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.2rem 0.4rem;
}
.thumbs button:hover { color: var(--ink); }
.thumbs button[aria-pressed="true"] { color: var(--accent); }
/* Sized in em so the icon tracks the button's own type scale, and filled with
   currentColor so hover and pressed need no rule of their own. */
.thumbs svg { display: block; width: 1.1em; height: 1.1em; fill: currentColor; }
/* A thumbs-down IS a thumbs-up through half a turn. Rotating one path keeps the
   pair from drifting the way two hand-drawn ones would, and the down thumb sits
   a shade lower for it, which is what the eye expects anyway. */
.thumbs svg.down { transform: rotate(180deg); }

/* ---- what was wrong ------------------------------------------------------ */
/* Only ever shown under a thumbs-down, and only just after one. "This beat was
   bad" names no repair; "wrong position, tier 2, key X" names the passage AND
   the edit. The question is asked at the one moment the player still remembers
   the answer (Aaron, 2026-07-31). */

.why-box {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.45rem;
  padding: 0.5rem 0.6rem;
  background: var(--bg-sunk);
  border-left: 2px solid var(--accent-dim);
}
.why-ask {
  font-family: var(--sans);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-right: 0.15rem;
}
.why-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.why-chips button,
.why-skip,
.why {
  font-family: var(--sans);
  font-size: 0.75rem;
  color: var(--ink-dim);
  background: none;
  border: 1px solid var(--rule);
  border-radius: 999px;
  cursor: pointer;
  padding: 0.22rem 0.6rem;
}
.why-chips button:hover, .why-skip:hover, .why:hover { color: var(--ink); border-color: var(--accent-dim); }
.why-chips button[aria-pressed="true"] { color: var(--accent); border-color: var(--accent-dim); }
/* No border and no box: skipping is the quiet option and should not compete
   with the three answers for the same click. */
.why-skip { border-color: transparent; color: var(--ink-faint); margin-left: auto; }

/* Own row: a text field beside three chips wraps into a mess on a phone, and
   this is the branch where somebody is actually typing something. */
.why-note { display: flex; gap: 0.35rem; flex: 1 0 100%; margin-top: 0.1rem; }
.why-note input {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--sans);
  font-size: 0.8125rem;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0.3rem 0.5rem;
}
.why-note input::placeholder { color: var(--ink-faint); }
.why-note input:focus { outline: none; border-color: var(--accent-dim); }
.why-note button {
  font-family: var(--sans);
  font-size: 0.75rem;
  color: var(--ink-dim);
  background: none;
  border: 1px solid var(--rule);
  border-radius: 3px;
  cursor: pointer;
  padding: 0.3rem 0.6rem;
}
.why-note button:hover { color: var(--ink); border-color: var(--accent-dim); }

/* The answer, left on screen. A verdict you cannot see is one you cannot
   correct, and the note is often one second thought from being the useful one. */
.why { margin-top: 0.4rem; color: var(--ink-faint); }

@media (max-width: 30rem) {
  #app { padding: 0 0.9rem; }
  #prose { padding-top: 1.5rem; }
}

/* A gap, marked. Not an error state — the sim did happen and the line above is
   true — but the reader has to be able to tell unwritten from badly written,
   because that is the whole judgement the curation pass wants back. */
.beat.miss p { font-style: italic; color: var(--ink-dim); }
.beat.miss .gap {
  font-family: var(--sans);
  font-style: normal;
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
}

/* Navigation, not choices: two-up so four of them cost one button of height
   instead of four, and quieter than the press that advances the night. */
.openers { display: grid; grid-template-columns: 1fr 1fr; gap: 0.35rem; }
.openers button.choice { background: transparent; color: var(--ink-dim); }
.openers button.choice:hover { color: var(--ink); }

/* A hard cut at the console edge reads as a broken layout rather than a scroll,
   so the bottom fades. Only the bottom: nothing overlaps the top of the pane,
   and fading there dimmed the first line of the opening beat for no reason.
   Only while there IS more below, too — see #prose.more, set by syncFade() in
   main.ts. A fade is a claim that the prose continues, and on a beat that ends
   above the fold it was a false one: the reader saw their last line rendered
   half-transparent with nothing under it, which reads as a rendering fault
   rather than as an invitation to scroll (Aaron, 2026-07-31).

   The fade height and #prose's bottom padding are a pair. The padding must be
   the larger of the two, or the last line of a fully-scrolled pane sits inside
   the gradient and dims even though the reader is as far down as the pane
   goes — which is the same false claim by a different route. 2.5rem against
   2rem. Move one, move the other. */
#prose.more {
  mask-image: linear-gradient(to bottom, #000 calc(100% - 2rem), transparent 100%);
}

.ending { display: flex; flex-direction: column; gap: 0.4rem; }
/* A judgement of the night, not a move inside it — .btn, and the grid gives them
   their width. It read as five more things the character could do while wearing
   .choice, which is the wrong question at the one moment the game asks a real
   one. */
.rating { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.35rem; }
.rating button.btn { width: 100%; }

/* Per-beat footer: which version you are reading, and what you thought of it. */
/* Wraps, and the items align to the top rather than the middle, because one of
   them — the thumbs — grows a panel underneath it when a beat is marked bad.
   Centred and unwrapping, that panel dragged the row's whole height with it and
   pushed itself into a 100px column beside the thumbs. */
.footer { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0.75rem; margin-top: 0.4rem; }
/* A full line of its own. Shrink-to-fit gave the panel a ~100px column beside
   the thumbs and wrapped every chip onto two words. */
.footer .verdict { margin-top: 0; flex: 1 1 100%; min-width: 0; }

.swap { display: flex; gap: 0.2rem; }
.swap button {
  font-family: var(--sans);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0.15rem 0.45rem;
  cursor: pointer;
  min-width: 1.6rem;
}
.swap button:hover { color: var(--ink); }
/* The one you are reading. This was a bare colour shift, on the theory that a
   loud "current" badge is a thumb on the scale — which was wrong: the labels
   are already anonymous, so showing WHICH letter is on screen biases nothing,
   and not showing it clearly left the judge unsure what they were rating
   (Aaron, 2026-07-30). Fill it, don't whisper it. */
.swap button[aria-pressed="true"] {
  color: var(--bg);
  background: var(--accent);
  border-color: var(--accent);
  font-weight: 600;
}

/* A beat you have already judged. Quiet — a rule down the left margin, the same
   accent as the vote pill — because it has to be findable while scrolling
   without competing with the prose you are reading now. */
.beat.judged {
  border-left: 2px solid var(--accent-dim);
  padding-left: 0.9rem;
  margin-left: -1.1rem;
}

/* Fill a missing column, dev builds only. Sits at the right with the vote when
   both are present, so the row reads: which versions exist · which won · write
   the rest. Deliberately unlabelled — naming the model here would put it next
   to prose about to be judged, which is the one thing the shuffle exists to
   prevent. */
.gen { display: flex; gap: 0.2rem; margin-left: auto; }
.gen button {
  font-family: var(--sans);
  font-size: 0.6875rem;
  color: var(--accent);
  background: transparent;
  border: 1px dashed var(--accent-dim);
  border-radius: 3px;
  padding: 0.15rem 0.45rem;
  cursor: pointer;
}
.gen button:hover { border-style: solid; }
.gen-note {
  font-family: var(--sans);
  font-size: 0.6875rem;
  color: var(--ink-faint);
  font-style: italic;
}

/* "See the fallback" under a gap — the other half of the buy decision (entry
   35). Same family as .gen and deliberately quieter: the ＋ spends money, this
   one only looks. Not pushed right, so it sits with the thumbs rather than
   competing with the ＋ for the far edge. */
.peek {
  font-family: var(--sans);
  font-size: 0.6875rem;
  color: var(--ink-faint);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0.15rem 0.45rem;
  cursor: pointer;
}
.peek:hover { color: var(--ink-dim); }
/* Which rung answered, and whether tonight has already spent it. */
.fallback-label {
  font-family: var(--sans);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
  margin-top: 0.6rem;
}
/* Indented and dimmed: this is prose being SHOWN, not prose being read. The
   rule down its left says the same thing a block quote says. */
p.fallback {
  color: var(--ink-dim);
  border-left: 1px solid var(--rule);
  padding-left: 0.7rem;
}

/* The comparative question, only when there is something to compare. */
.vote { display: flex; align-items: center; gap: 0.25rem; margin-left: auto; }
.vote-label {
  font-family: var(--sans);
  font-size: 0.6875rem;
  color: var(--ink-faint);
  margin-right: 0.1rem;
}
.vote button {
  font-family: var(--sans);
  font-size: 0.6875rem;
  color: var(--ink-dim);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0.15rem 0.45rem;
  cursor: pointer;
}
.vote button:hover { color: var(--ink); border-color: var(--accent-dim); }
/* Your vote, and it stays clickable — votes are correctable, so the other
   options must remain live rather than fading out. The old `:disabled` rule
   that greyed the losers is gone with the one-shot behaviour it belonged to. */
.vote button[aria-pressed="true"] {
  color: var(--bg);
  background: var(--accent);
  border-color: var(--accent);
  font-weight: 600;
}

/* A road with thinner prose on it — not a road you cannot take.
   The dash used to mean a wall. It now means a caveat and an offer: the beat
   behind it is written in general rather than for this exact moment, or written
   once and already read tonight, and a key would have it written properly. All
   of them advance the night on a click, so this must read as quieter than a
   normal choice and never as :disabled — that means "not now", and nothing here
   means that.

   The badge is a data attribute rather than four content rules because what it
   says depends on whether a key is set, which is a fact the button knows and
   the stylesheet does not. */
button.choice.dashed {
  color: var(--ink-faint);
  background: transparent;
  border-style: dashed;
  cursor: pointer;
}
button.choice.dashed:hover {
  border-color: var(--accent-dim);
  color: var(--ink);
}
button.choice.dashed::after {
  content: attr(data-badge);
  float: right;
  margin-left: 0.75rem;
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.6;
}
/* Generic is the ordinary case — 15% of menu items — so it wears the lightest
   version of the marking. A gap is rare and worth noticing. */
button.choice.dashed.generic { border-color: color-mix(in srgb, var(--rule) 70%, transparent); }
button.choice.dashed.generic::after { opacity: 0.42; }
/* The continue slot is dashed often — a second and third continue on one key is
   the commonest road to tier 3 — and it is still the loudest thing on the
   screen. The dash marks it; it does not demote it. */
button.choice.dashed.primary { color: var(--accent); border-color: var(--accent-dim); }
button.choice.dashed.generic.primary { border-color: color-mix(in srgb, var(--accent-dim) 70%, transparent); }
button.choice.dashed:disabled { opacity: 0.5; cursor: default; }
button.choice.dashed:disabled::after { content: none; }
/* Cloth-blocked: the one row that DOES mean "not now", which is why it is the
   one dashed row allowed to be :disabled — see the note at the top of this
   block. It keeps its badge, because the badge is the entire point: it names the
   garment. Lower-cased, because it is a garment rather than a status. */
button.choice.dashed.blocked:disabled::after {
  content: attr(data-badge);
  text-transform: none;
  font-style: italic;
  opacity: 0.7;
}
button.choice.dashed.blocked { border-style: dotted; opacity: 0.62; }
button.choice.dashed.blocked:hover { border-color: var(--rule); color: var(--ink-faint); }

/* ---- JIT: the keyring and a beat being written ----------------------------- */

/* The key box lives in the banner, which sits above the menu — so the question
   ("do you want to pay to open this road?") appears where the road was, not on
   a settings screen the player would have had to visit before knowing why. */
.keyring { display: flex; flex-direction: column; gap: 0.6rem; }
.keyring-why { font-size: 0.8125rem; line-height: 1.45; color: var(--ink-dim); }
.keyring-input {
  width: 100%;
  padding: 0.6rem 0.7rem;
  font-family: var(--serif);
  font-size: 0.9375rem;
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 2px;
}
.keyring-input:focus { outline: none; border-color: var(--accent-dim); }
.keyring-row { display: flex; gap: 0.5rem; }

/* The one-time explanation. Slightly larger than the rest of the banner because
   it is the only thing in the game that asks a player to consider spending
   money, and it should read as a paragraph rather than as a status line. */
.keyring-why.pitch { font-size: 0.875rem; color: var(--ink-dim); }
.keyring-link {
  align-self: flex-start;
  font-size: 0.75rem;
  letter-spacing: 0.02em;
  color: var(--accent-dim);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.keyring-link:hover { color: var(--accent); border-bottom-color: var(--accent-dim); }

/* Off is a real setting. See costSwitch() for why it is not just a courtesy. */
.costswitch {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.75rem;
  color: var(--ink-faint);
  cursor: pointer;
}
.costswitch input { accent-color: var(--accent-dim); }

/* Prose arriving a fragment at a time. It gets the ordinary beat treatment minus
   the affordances — no vote row, no thumbs — because it is not a passage yet and
   rating something half-written measures the wrong thing. The caret is the only
   signal that more is coming; a spinner would compete with the words. */
.beat.writing p { color: var(--ink); }
/* :not(.rewriting) because the caret means "more of THIS is coming", and the
   rewrite notice is a finished sentence. A blinking cursor after it would read
   as the notice itself still being typed. */
.beat.writing p:not(.rewriting)::after {
  content: "▍";
  margin-left: 0.1em;
  color: var(--accent-dim);
  animation: blink 1.1s step-end infinite;
}
@keyframes blink { 50% { opacity: 0; } }

/* Why the prose just vanished and started over. Sits where the draft was, in the
   prose pane, because that is where the player is looking when it happens — the
   banner at the bottom of the console was too far from the event to caption it.
   Dim and small: it explains a hiccup, it is not the beat. */
.rewriting {
  font-family: var(--sans);
  font-size: 0.75rem;
  line-height: 1.4;
  color: var(--ink-faint);
  font-style: normal;
  margin-bottom: 0.4rem;
}

/* The receipt on a beat the player bought. Pushed to the far edge of the footer
   so it sits under the last line of prose rather than in the row of controls —
   it is something to notice once, not something to act on. Only ever present on
   beats this browser paid for, and removable entirely (costSwitch). */
.cost {
  margin-left: auto;
  align-self: flex-end;
  font-size: 0.6875rem;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* The community and support asks, after the night only. Links, not buttons —
   they leave the game, and nothing that leaves should dress like a choice. */
.community { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 1.4rem; }

/* Said once, after the night, never beside it. */
.spend {
  margin-top: 0.9rem;
  font-size: 0.75rem;
  letter-spacing: 0.03em;
  color: var(--ink-faint);
}

/* ---- the saves panel ------------------------------------------------------- */

/* Fixed rather than absolute, and over everything: the panel is modal in intent
   even though it is dismissed by a button rather than by a scrim click. Scrim
   dismissal is wrong here — a mis-tap while reaching for a slot would throw away
   the list mid-decision. */
.panel {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1.25rem;
  max-width: var(--measure);
  margin: 0 auto;
  background: var(--bg);
  overflow-y: auto;
}
.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 0.6rem;
}
.panel-title {
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
/* "done" needs nothing here now: .btn is already auto-width and centred. The
   override this replaced existed only to undo .choice's full-bleed. */
/* Tall enough for its own content and no taller. `flex: 1` here made the slot
   list eat every spare pixel in a fixed, full-height panel, so two saved nights
   put four hundred points of nothing between the last slot and "save this
   night" and the panel read as broken (Aaron, 2026-07-31). The panel itself is
   the scroller; the body has no reason to stretch. */
.panel-body { display: flex; flex-direction: column; gap: 0.4rem; flex: 0 1 auto; }
.panel-acts { display: flex; flex-direction: column; gap: 0.4rem; }
/* The one act in this panel that ends something. Held off from the file
   operations above it by a gap of its own — the button already carries a border,
   so the separation is space rather than a second rule. */
.panel-acts .panel-fresh { margin-top: 0.9rem; }

/* The slot row: the whole line loads, the ✕ deletes. The load target is
   deliberately the large one — loading is the common act and deleting is the
   irreversible one, so they should not be the same size.

   Both are .btn now, which is what fixed the ✕ for good rather than by
   specificity arithmetic. The load is `.btn.block` sitting in a flex row so it
   takes the slack; the ✕ is `.btn.icon`, which declares its own 2.75rem basis.
   Neither is fighting a rule that wanted them full-bleed in the first place. */
.slot { display: flex; gap: 0.4rem; align-items: stretch; }
/* A row in a list, so it reads left-aligned like the line of prose it names —
   the one place a .btn wants a choice's alignment. */
.slot .btn.block { flex: 1; width: auto; text-align: left; padding: 0.6rem 0.8rem; }
.slot .btn.icon:hover { color: var(--accent); }

.panel-note {
  font-size: 0.6875rem;
  line-height: 1.5;
  color: var(--ink-faint);
  border-top: 1px solid var(--rule);
  padding-top: 0.7rem;
}

/* A colophon: which build this is, and how big its corpus got. Quiet, monospace
   numerals so two people comparing stamps are comparing the same shapes. */
.panel-build {
  font-family: var(--sans);
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
  opacity: 0.7;
  border-top: 1px solid var(--rule);
  margin-top: 0.4rem;
  padding-top: 0.7rem;
}

/* The gear. Chrome, not choice — it must not read as something the character
   does, which is what it did while it sat in the menu wearing .choice. But the
   first fix overshot: a faint icon floated in the viewport corner was invisible
   AND would cover the prose on a narrow screen. It lives in the console strip
   now, right-aligned above the disclosure, where furniture belongs. */
#gear {
  /* #console is a block, not a flex container, so auto-margin does the
     right-aligning that align-self would not. */
  display: block;
  margin: 0 0 0.5rem auto;
  padding: 0.25rem 0.6rem;
  font-family: var(--sans);
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  color: var(--ink-dim);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 999px;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}
#gear:hover, #gear:focus-visible {
  color: var(--ink);
  border-color: var(--accent-dim);
  outline: none;
}

/* ---- the front door -------------------------------------------------------- */
/* Covers everything, opaque, above the saves panel's z-index: this is the one
   overlay that must not be escapable by a mis-tap or a stray focus ring. */

#gate {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--bg);
  overflow-y: auto;
  /* Safe-area padding as well as its own: on a phone in the itch iframe this is
     the only screen that runs edge to edge. No bottom padding, because .gate-act
     sticks to the bottom of this scrollport and would float above it — the
     bottom inset lives on .gate-act and .gate-privacy instead. */
  padding: max(1.5rem, env(safe-area-inset-top)) 1.25rem 0;
}
.gate-inner { max-width: var(--measure); margin: 0 auto; }

#gate h1 {
  font-size: 1.75rem;
  font-weight: 400;
  letter-spacing: 0.01em;
  margin: 0 0 0.3rem;
}
.gate-sub {
  font-size: 1rem;
  color: var(--ink-dim);
  margin: 0 0 1.75rem;
}

/* Three facts, as a definition list because that is what they are — and it
   gives screen readers the pairing for free. */
.gate-facts { margin: 0 0 1.75rem; }
.gate-facts dt {
  font-family: var(--sans);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-top: 1rem;
}
.gate-facts dd {
  margin: 0.25rem 0 0;
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--ink-dim);
}

/* The party the night starts after. Set as prose, in the reading face, because
   it IS the first page of the story — the passages are simply forbidden to
   repeat it. */
.gate-premise {
  border-left: 2px solid var(--accent-dim);
  padding: 0.1rem 0 0.1rem 1rem;
  margin: 0 0 1.75rem;
}
.gate-premise-label {
  font-family: var(--sans);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 0.6rem;
}
.gate-premise p:not(.gate-premise-label) {
  font-size: 1.0625rem;
  line-height: 1.72;
  margin: 0 0 0.9em;
  text-wrap: pretty;
}
.gate-premise p:last-child { margin-bottom: 0; }

/* The agreement and the button that accepts it, pinned to the bottom of the
   scrollport so the way in is on screen at first paint without the premise
   having to move below the decision that skips it. Opaque, with a rule, because
   the facts and the premise scroll UNDER this rather than beside it. */
.gate-act {
  position: sticky;
  bottom: 0;
  background: var(--bg);
  border-top: 1px solid var(--rule);
  padding: 0.9rem 0 max(0.9rem, env(safe-area-inset-bottom));
}

.gate-confirm {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--ink);
  margin: 0 0 1rem;
}

.gate-row { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
/* Quiet, but a real link — declining has to actually leave, including out of
   the itch iframe (hence target=_top on the anchor). */
.gate-leave {
  font-family: var(--sans);
  font-size: 0.8125rem;
  color: var(--ink-faint);
  text-decoration: none;
  padding: 0.5rem 0;
}
.gate-leave:hover { color: var(--ink-dim); text-decoration: underline; }

/* Sits below the pinned bar at the true end of the document, and carries the
   bottom safe-area inset that #gate gave up so .gate-act could reach the edge. */
.gate-privacy {
  font-size: 0.6875rem;
  line-height: 1.6;
  color: var(--ink-faint);
  border-top: 1px solid var(--rule);
  margin: 1.5rem 0 max(1.5rem, env(safe-area-inset-bottom));
  padding-top: 0.9rem;
}

/* ---- the landing ------------------------------------------------------------ */
/* The second front door: shown on a return visit when a night is waiting. Same
   chrome as #gate deliberately — it is the same kind of screen, arriving in the
   same place — but centred rather than top-aligned, because it is four lines
   rather than a page, and vertically centred four lines do not read as a
   document that failed to load.

   It exists because the offer used to be a button PREPENDED to the live menu:
   you landed inside a fresh night, already begun, with "continue the other one"
   sitting on top of the choices, and both of them wearing .primary. Nothing said
   which night you were looking at, and nothing anywhere said "new". */
#landing {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--bg);
  overflow-y: auto;
  display: flex;
  align-items: center;
  padding: max(1.5rem, env(safe-area-inset-top)) 1.25rem max(1.5rem, env(safe-area-inset-bottom));
}
.landing-inner { max-width: var(--measure); width: 100%; margin: 0 auto; }

#landing h1 {
  font-size: 1.75rem;
  font-weight: 400;
  letter-spacing: 0.01em;
  margin: 0 0 0.3rem;
}
.landing-sub {
  font-size: 1rem;
  color: var(--ink-dim);
  margin: 0 0 1.75rem;
}

/* What is waiting, in the reading face: it is a line of the story, not a row in
   a file manager. Same left rule as .gate-premise, which is the same claim —
   this is the fiction, held apart from the furniture around it. */
.landing-night {
  border-left: 2px solid var(--accent-dim);
  padding: 0.1rem 0 0.1rem 1rem;
  margin: 0 0 1.75rem;
}
.landing-night-label {
  font-family: var(--sans);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 0.5rem;
}
.landing-night p:not(.landing-night-label) {
  font-size: 1.0625rem;
  line-height: 1.6;
  margin: 0;
  color: var(--ink);
}

.landing-row { display: flex; flex-direction: column; gap: 0.5rem; }

/* The front doors are the one surface where furniture is the whole screen, so
   its buttons are sized up to match — a full-width press with room around it,
   rather than the small control a .btn is inside a panel. Scoped to the two
   overlays, which is a surface making a decision about its own scale rather than
   a patch on what a button is. */
#gate button.btn, #landing button.btn, #rung button.btn {
  font-size: 0.9375rem;
  padding: 0.75rem 1rem;
  min-height: 2.75rem;
}

/* ---- a dialogue rung ------------------------------------------------------- */

/* The campaign layer's authored conversations, before the night (web/rung.ts).
   A third full-screen surface, and it borrows the front doors' geometry
   deliberately: the same fixed inset, the same measure, the same button scale,
   because it is the same KIND of thing — a screen that asks one question and
   goes away. What it does not borrow is the vertical centring. A rung is
   PROSE, several paragraphs of it, and centred text that grows downward as you
   read reflows the whole screen on every choice; this one is anchored at the
   top and scrolls like a page. */
#rung {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--bg);
  overflow-y: auto;
  padding: max(2.5rem, env(safe-area-inset-top)) 1.25rem max(2rem, env(safe-area-inset-bottom));
}
.rung-inner { max-width: var(--measure); width: 100%; margin: 0 auto; }

/* Quiet, and in the sans: the title is a label on a scene, not a line of it.
   Nothing above it — no story title, no chapter number — because the prose
   opens by saying where she is. */
.rung-title {
  font-family: var(--sans);
  font-size: 0.6875rem;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 1.5rem;
}

/* The reading face, at the size the night's prose is read at, because it IS
   the night's prose — the fact that a human wrote this one and a model wrote
   the next is not something the page should let the reader feel. */
.rung-prose p {
  font-size: 1.0625rem;
  line-height: 1.7;
  color: var(--ink);
  margin: 0 0 1.1rem;
}
.rung-prose p:last-child { margin-bottom: 0; }

.rung-row {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 1.75rem;
}

/* The answers are left-aligned like the menu's choices, not centred like the
   front doors' two: these are things she SAYS, and a centred sentence reads as
   a button rather than as speech. */
#rung button.btn {
  text-align: left;
  line-height: 1.45;
}

.landing-note {
  font-size: 0.6875rem;
  line-height: 1.6;
  color: var(--ink-faint);
  border-top: 1px solid var(--rule);
  margin: 1.5rem 0 0;
  padding-top: 0.9rem;
}

/* ---- from main: the state line and the tier badge -------------------------- */

/* The state line, under the choice at the head of each beat: the night as it
   stood after that action. Scrolls with the history; costs nothing while
   reading. */
.beatstate {
  font-size: 0.72rem;
  opacity: 0.35;
  letter-spacing: 0.02em;
  /* Clear of the prose below it. The foot-of-beat version needed no such gap
     because the footer supplied one. */
  margin: 0 0 1.2rem;
}
/* Tucked up against the choice so the two read as one header rather than as two
   separate notes — .choice already carries 0.6rem beneath it, which is a
   paragraph break's worth of air between a line and its own subtitle. Scoped to
   the adjacency because the opening beat has no choice above it, and there the
   line is the first thing in the article and must not pull itself upward. */
.beat .choice + .beatstate { margin-top: -0.3rem; }

/* Debug: which tier served the beat. Faint, right-aligned, out of the prose. */
article.beat { position: relative; }
.tier { position: absolute; top: 0.4rem; right: 0.5rem; font-size: 0.7rem; opacity: 0.28; font-variant-numeric: tabular-nums; }

/* ---- the jump panel (dev only) ------------------------------------------------

   A form, not fiction, so it is allowed to look like one: dense rows, a label
   and a control, and none of the measure discipline the prose column keeps. It
   is only ever on Aaron's machine — mountSeekPanel hangs off `dev`, which is
   false on every shipped build — so it costs a shipped player nothing but these
   few hundred bytes.

   The scene buttons sit above the form because a named scene is already a
   verified list of moves: if one of them is what you wanted, searching for it
   again is pure waste, and the layout should say so before you fill anything in. */
.seek-scenes { display: flex; flex-direction: column; gap: 0.3rem; }
.seek-scenes .btn.block { text-align: left; padding: 0.45rem 0.7rem; }
/* The fold-out holding the named moments and the facet form — the panel's
   old face, demoted under the scene ladder (2026-08-09). */
.seek-more { margin-top: 0.6rem; }
.seek-more > summary { cursor: pointer; opacity: 0.7; font-size: 0.85em; padding: 0.2rem 0; }
.seek-more > summary:hover { opacity: 1; }

.seek-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  font-size: 0.75rem;
}
/* Wraps rather than truncating: a garment's name is diegetic ("striped
   blue-and-black bra") and clipping it turns two rows into the same row. */
.seek-label { color: var(--ink-dim); flex: 1 1 auto; min-width: 0; }
.seek-select {
  flex: 0 0 auto;
  max-width: 55%;
  font-family: var(--sans);
  font-size: 0.75rem;
  padding: 0.2rem 0.3rem;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 2px;
}
/* Where the search reports itself. Never empty once looking has started — a
   progress line that vanishes reads as a crash, and this one can legitimately
   run for several seconds. */
.seek-status {
  font-size: 0.6875rem;
  line-height: 1.5;
  color: var(--ink-faint);
  min-height: 1.5em;
}
