:root {
  --black: #000000;
  --white: #ffffff;
  --magenta: #ff00ff;
  --serif: "Playfair Display SC", Georgia, "Times New Roman", serif;
  --sans: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  background: var(--white);
  color: var(--black);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

button { font-family: inherit; }

/* ---------- screens ---------- */

.screen {
  display: none;
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  /* dvh tracks the real visible viewport (keyboard, browser chrome);
     falls back to the line above on browsers that don't support it —
     invalid values are ignored, not applied, so this is additive only. */
  height: 100dvh;
}

.screen--active { display: block; }

.screen--chrome { background: var(--white); }

/* ---------- top / bottom bars ---------- */

.bar {
  position: fixed;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px 28px;
  z-index: 5;
}

.bar--top { top: 0; }
.bar--bottom { bottom: 0; }

.mark {
  display: block;
  width: 28px;
  height: 28px;
  border-radius: 3px;
}

.mark--ghost {
  width: 30px;
  height: 30px;
  border-radius: 0;
}

.wordmark {
  font-family: var(--serif);
  color: var(--magenta);
  font-size: 14px;
  letter-spacing: 0.05em;
}

/* C9.5: a real Home link, but only where it's actually safe (Gate, Send,
   Mirror — never Compose or Release, where it could abandon an active
   composition or an unpaid resolution). Reset to look identical to the
   plain .wordmark span it replaces; only cursor/hover signal it's live. */
/* Real bug, found 2026-09-04: `font: inherit` is a shorthand that resets
   font-family/size/weight together, not just the button-chrome
   properties this rule actually wants to reset — it was silently
   overwriting .wordmark's own serif/14px with whatever the <header>
   inherits (a plain system sans-serif at 16px), so the button variant of
   this wordmark (Gate, Send, Mirror) rendered visibly different from the
   plain-<span> variant (Compose) despite sharing the same .wordmark
   class. Resetting only what needs resetting fixes it without fighting
   .wordmark's own declarations. */
button.wordmark--nav {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
  transition: opacity 0.18s ease;
}
button.wordmark--nav:hover,
button.wordmark--nav:focus-visible {
  opacity: 0.7;
}

.coords, .time {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--black);
  font-variant-numeric: tabular-nums;
}

/* dark chrome variant — gate screen */
.screen--dark { background: var(--black); }
.screen--dark .coords,
.screen--dark .time { color: var(--white); }

/* ---------- home (1-A port) — white/black instrument, scoped to
   #screen-home only so the Mirror screen (which shares .bar/.wordmark/
   .mark) is untouched. ROOM 1 COMPOSITION/CROSSING/CONDUCT PASS
   (2026-09-30): background flipped from black to white — magenta is
   restrained to the crossing moment (the CTA border) rather than
   carrying the whole screen's atmosphere, per direct instruction. Text
   color now falls through to .home-line's own black default below,
   no override needed. */

/* FINAL VISUAL COMPOSITION PASS (2026-09-20): was clamp(20px, 4vw, 56px)
   — that uniform gap is what made ENTER float in its own hole and the
   lower three read as a separate footer. Tightened outer rhythm; Block
   A's own internal spacing is restored separately below via
   .home-block-a so the title/tagline relationship doesn't collapse. */
#screen-home .home-main {
  gap: clamp(14px, 2.4vw, 28px);
  overflow-y: auto;
  /* Home-as-product pass: the old fixed upward shift (tuned for a
     wordmark/tagline/button-only layout) clipped the wordmark's own top
     edge off-screen the moment real copy made this column taller —
     there was no scroll to recover it. Dropped rather than re-tuned:
     with six lines of real content, true vertical centering (plus a
     scroll fallback for genuinely short viewports) is more honest than
     another fixed offset that will just break again at the next content
     change. */
}

#screen-home .home-line--wordmark {
  font-family: var(--serif);
  font-weight: 700;
  color: var(--magenta);
  /* Demoted 2026-09-04: this used to be the dominant spatial fact of the
     page (clamp up to 112px) with the actual line sized as an
     afterthought underneath it — backwards hierarchy for a page whose
     one job is the line, not the brand name. Now matches the same quiet
     register every other room's own header wordmark already uses
     (.wordmark, 14px) instead of a unique giant treatment only Home had.
     ROOM 1 3+1+3 PASS (2026-09-30): the wordmark is the one place
     magenta stays at rest — quiet brand bookend, not the field. The CTA
     below carries magenta only as its own hover/active signal, not a
     resting border. */
  font-size: 14px;
  letter-spacing: 0.05em;
  max-width: calc(100vw - 48px);
  overflow-wrap: normal;
}

/* ROOM 1 3+1+3 PASS (2026-09-30): the CTA is now the shared, compact
   .btn-conduct as-is — no Home-specific size/border/hover override.
   Magenta only ever appears here via the shared button's own
   :hover/:focus-visible rule below (see base .btn-conduct) — the active
   crossing signal, not a resting field. */
#screen-home .home-cta-label {
  display: block;
}

/* ---------- home / mirror main ---------- */

.home-main {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  padding: 0 24px;
  text-align: center;
}

.home-line {
  margin: 0;
  color: var(--black);
}

.home-line--top {
  font-family: var(--serif);
  /* Promoted 2026-09-04 to the dominant spatial fact of the page — this
     is the one thing Home actually needs someone to read; the wordmark
     inherited its old clamp size instead of the other way around. Sized
     back down same day — dominant relative to the wordmark, not
     shouting. */
  font-size: clamp(22px, 4vw, 44px);
  line-height: 1.2;
  font-weight: 700;
  letter-spacing: 0.02em;
  max-width: calc(100vw - 48px);
}

/* CLOSE ONE THING PASS (2026-09-20): .home-score groups the lower three
   as one compact descending score, own tighter gap distinct from
   .home-main's wider top-level rhythm — same grouping idea as Room 2's
   .gate-composition. */
/* FINAL VISUAL COMPOSITION PASS (2026-09-20): Block A's own generous
   rhythm, independent of .home-main's now-tighter outer gap — this is
   what keeps title/tagline from collapsing now that the outer gap
   shrank. Same column width as .home-score below (both max-width:320px)
   so the two blocks read as one aligned instrument. */
.home-block-a {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(16px, 3vw, 28px);
  /* No max-width here — .home-line--top already carries its own wider
     allowance (calc(100vw - 48px)) so the title stays one line; a
     narrower wrapper max-width would force it to wrap and undercut the
     one thing this pass has to protect. .home-score below still matches
     .home-line--context's own 320px column on its own terms. */
}
.home-score {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* COMPOSITION ALIGNMENT PASS (2026-09-21): 10px -> 16px. The three
     lines here are now full sentence-weight beats (two of the three
     wrap), not the old terse one-line-each score this gap was tuned
     for — at 10px they read as one compressed paragraph instead of
     three distinct beats mirroring .home-block-a's own 1/2/3 above.
     CALLABLE CONDUCT PASS (2026-09-22): 16px -> clamp(16px, 3vw, 28px),
     a literal, complete mirror of .home-block-a's own gap above — per
     direct instruction, the lower three beats now read at the exact
     same rhythm as the upper three, not a deliberately tighter one. */
  gap: clamp(16px, 3vw, 28px);
  max-width: 320px;
  /* CALLABLE CONDUCT PASS (2026-09-22): the -12px negative margin that
     used to pull this block up toward ENTER is removed — per direct
     instruction, the ENTER-to-line-5 gap now equals the tagline-to-
     ENTER gap above (both .home-main's own clamp(14px,2.4vw,28px)),
     instead of reading visibly shorter than it. */
}
.home-line--bottom {
  font-size: 12px;
  letter-spacing: 0.14em;
  opacity: 0.65;
}
/* Quiet closing bookend, mirroring the wordmark's own quiet register at
   the top — trails off rather than closing with equal weight to
   position 6 above it. */
.home-line--bottom-quiet {
  opacity: 0.4;
}
/* ROOM 1 3+1+3 PASS (2026-09-30): magenta on the closing seal, testing
   whether it "composes cleanly" per direct instruction — echoes the
   wordmark's own magenta at the opposite bookend, still small/quiet
   (12px, 0.4-equivalent opacity via 0.55 on a color with more inherent
   presence than black), not a second loud statement. Scoped to
   #screen-home; .home-line--bottom-quiet is Room 1-only regardless. */
#screen-home .home-line--bottom-quiet {
  color: var(--magenta);
  opacity: 0.55;
}

/* Plain, quiet, functional — sits between the tagline's poetry and the
   fine-print time caption. Deliberately not styled to compete with
   either; it's the one place actual mechanism/cost facts live on Home. */
.home-line--context {
  font-family: var(--sans);
  font-size: 14px;
  letter-spacing: 0.02em;
  opacity: 0.7;
  max-width: 320px;
}

/* Only present at all once an account is known to exist — anonymous
   entry stays exactly as quiet as it already was. */
/* Only ever set right before a genuine resolution-loss return to Home
   (see conduct()'s expired catch branch in app.js) — the [hidden]
   attribute is the only thing gating it, so an ordinary fresh arrival at
   Home never shows this. */
.home-status {
  font-family: var(--sans);
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--white);
  opacity: 0.6;
  max-width: 320px;
  margin: 0;
}
/* Only ever rendered inside #screen-home (see app.js), which is now a
   white surface — the class default above still assumes the old black
   Home background, so it's overridden here rather than changed at the
   source and risking an unseen dark-screen user of the same class. */
#screen-home .home-status { color: var(--black); }

.home-vault-link {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--white);
  opacity: 0.6;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: opacity 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.home-vault-link:hover,
.home-vault-link:focus-visible {
  opacity: 1;
  color: var(--magenta);
  border-bottom-color: var(--magenta);
}

/* Continuity pass: the persistent way back to a saved Card, present in
   every room where leaving doesn't risk abandoning an active/unpaid
   composition (Gate, Card, Send, Mirror — never Compose or Release,
   same safety boundary the wordmark-nav link already uses). Positioned
   absolutely so it can't disturb any room's existing flex centering or
   space-between layout — it doesn't participate in that flow at all,
   just overlays the same top-left corner every room's .bar already
   reserves via its own padding. Always visible, not gated on auth state:
   /vault already handles both the signed-out and signed-in case
   honestly, so hiding the door until you're recognized was the actual
   discoverability problem, not a feature. */
.bar-vault-link {
  position: absolute;
  top: 50%;
  left: 28px;
  transform: translateY(-50%);
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.08em;
  /* Black by default — Send (.screen--life) and Mirror (.screen--chrome)
     are both white-background rooms, same as Card. Only Gate is dark. */
  color: var(--black);
  opacity: 0.5;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: opacity 0.18s ease, color 0.18s ease, border-color 0.18s ease;
  z-index: 6;
}
.bar-vault-link:hover,
.bar-vault-link:focus-visible {
  opacity: 1;
  color: var(--magenta);
  border-bottom-color: var(--magenta);
}
.screen--dark .bar-vault-link {
  color: var(--white);
  opacity: 0.6;
}

/* Legibility pass: one uniform header placement rule for every room that
   uses .bar--top (Gate, Compose, Release, Send, Mirror), instead of the
   room-by-room special-casing this used to be. The wordmark true-centers
   everywhere, regardless of whether a Vault link or ghost-mark sits
   beside it — previously Gate/Compose centered the wordmark+mark as a
   pair (off from true 50%) while Send/Mirror left it pinned to the flex-
   start edge, two different placements for the same element. Taking the
   wordmark out of flex flow entirely and centering it absolutely means
   every room's header reads identically: Vault (if present) at the far
   left, wordmark dead-center, mark/action at the far right. */
.bar--top {
  justify-content: flex-end;
}
.bar--top .wordmark {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}


.btn-conduct {
  font-family: var(--serif);
  font-size: 18px;
  letter-spacing: 0.04em;
  background: var(--black);
  color: var(--white);
  border: 2px solid var(--white);
  padding: 22px 40px;
  cursor: pointer;
  transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease, opacity 0.18s ease;
}
.btn-conduct:hover,
.btn-conduct:focus-visible {
  background: var(--magenta);
  color: var(--white);
  border-color: var(--magenta);
}
.btn-conduct:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.btn-conduct:disabled:hover {
  background: var(--black);
  border-color: var(--white);
}

/* Room 4 final surgical pass (2026-09-11): the music control is now a
   real, visible text action — discoverable without becoming visually
   dominant, per direct instruction that it must no longer hide behind
   the previous tiny-dot control. Same quiet register .release-terms-link
   already established (sans, small, dimmed-until-hover), slightly more
   present at rest (0.55 vs. terms' 0.4) so it reads as clickable without
   competing with #btn-release for the room's own center of gravity.
   CALLABLE CONDUCT PASS (2026-09-22): resting opacity dropped further
   (0.55 -> 0.06) and a transparent border added, per direct instruction
   to "basically hide it for now" — kept, not cut, discoverable on
   hover/focus (opacity 1 + a real magenta outline appears then) rather
   than removed outright. The transparent border at rest reserves the
   same box size so the outline appearing on hover doesn't shift layout.
   Folded inline as a parenthetical right after "LEAVE CHANGED." in this
   same pass — display:inline-block (button default) already sits fine
   mid-sentence; the .release-audio-mark--atmosphere top-margin modifier
   this used to need as a standalone block is no longer applied (left in
   place below, orphaned, same convention as elsewhere in this file). The
   parens themselves are generated (::before/::after), not literal
   characters in the markup — app.js's setChopinLabel() replaces this
   button's textContent wholesale on every play/pause toggle, which would
   wipe real parens but leaves CSS-generated ones untouched; they inherit
   this element's own color/opacity automatically, dimming and revealing
   in lockstep with the label itself. */
/* Pulled out of the score entirely and pinned to the corner, per direct
   instruction — it read as an extra, too-loud beat in the vertical
   stack ("almost like a blob there"). position:fixed removes it from
   .release-main's flow regardless of its DOM position. */
.release-audio-mark {
  position: fixed;
  left: 16px;
  bottom: 16px;
  z-index: 5;
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--white);
  opacity: 0.3;
  background: none;
  border: 1px solid transparent;
  border-radius: 2px;
  cursor: pointer;
  padding: 4px 8px;
  transition: opacity 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.release-audio-mark:hover,
.release-audio-mark:focus-visible {
  opacity: 1;
  color: var(--magenta);
  border-color: var(--magenta);
}
.release-audio-mark.is-playing {
  opacity: 1;
  color: var(--magenta);
}
/* FINAL COMMIT-SURFACE CLEANUP (2026-09-20): extra separation on top of
   .release-main's own flex gap, for when this was its own standalone
   block below the seven beats. No longer applied (CALLABLE CONDUCT
   PASS, 2026-09-22 — Chopin folded inline into the LEAVE CHANGED. line
   instead); left in place, unused, same retired-not-deleted convention
   already used elsewhere in this codebase. */
.release-audio-mark--atmosphere {
  margin-top: 20px;
}

/* Quiet, non-interactive supporting line — same register as the music
   control at rest, but never brightens (nothing to hover; it's just
   text), so it reads as clearly subordinate to both #btn-release above
   it and the music control below it. */
.release-quiet-line {
  font-family: var(--sans);
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--white);
  opacity: 0.5;
  margin: 0;
}

/* ROOM 4 HINGE PASS (2026-09-30): the old .release-line--conduction/
   .release-tc-inline pair that used to live here is gone — its line
   ("THE CONDUCT STAYS EXACT. (T&C)") is retired; T&C moved to its own
   quiet line beside Stripe (see index.html), reusing the app's existing
   .release-terms-link. .release-line--movement (the lower three's own
   stronger "click," repurposed to carry "Held as a Composium Card.") is
   defined once, further down — see the ONE PIECE PASS section. */

/* Was .btn-conduct-sub, the button's own price sub-span — no longer
   needed now that Room 4's button copy is one flat "Release it · €4"
   label (cleanup pass, 2026-09-09); removed rather than left orphaned,
   since nothing else in the codebase used this class. */

/* ---------- Room 7 / final score ---------- */
/* The room adds no explanation. It gives the already-resolved object one
   clean final field, with Conduct as the load-bearing note. Long values
   stay in ordinary document flow so the ending remains usable on small
   screens instead of clipping against a fixed composition. */
.mirror-score {
  width: 100%;
  min-height: 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(14px, 2.2vh, 24px);
  padding: clamp(48px, 7vh, 80px) 24px;
  text-align: center;
  overflow-y: auto;
}

.mirror-score-field {
  width: min(100%, 720px);
}

.mirror-score-label {
  margin: 0 0 5px;
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--magenta);
}

.mirror-score-value {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(15px, 2.2vw, 19px);
  line-height: 1.42;
  color: var(--black);
  opacity: 0.72;
  overflow-wrap: anywhere;
}
.mirror-score-value:empty,
.mirror-score-address:empty,
.mirror-score-destination:empty {
  display: none;
}

.mirror-score-field--tone .mirror-score-value {
  font-size: clamp(17px, 2.7vw, 22px);
  opacity: 0.82;
}

.mirror-score-field--composition .mirror-score-value {
  max-width: 650px;
  margin-inline: auto;
  opacity: 0.78;
}

.mirror-score-field--conduct {
  padding: clamp(10px, 1.8vh, 18px) 0;
}

.mirror-score-conduct {
  max-width: 720px;
  margin-inline: auto;
  font-size: clamp(28px, 5vw, 46px);
  line-height: 1.16;
  font-weight: 700;
  letter-spacing: 0.005em;
  opacity: 1;
}

.mirror-score-address,
.mirror-score-destination {
  margin: 0;
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.mirror-score-address {
  color: var(--black);
  opacity: 0.5;
}

.mirror-score-destination {
  color: var(--magenta);
  opacity: 1;
}

.mirror-actions {
  display: flex;
  justify-content: center;
  margin-top: clamp(4px, 1vh, 10px);
}

.mirror-choice {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.14em;
  color: var(--black);
  background: none;
  border: 1px solid var(--black);
  padding: 12px 28px;
  cursor: pointer;
  transition: border-color 0.18s ease, color 0.18s ease;
}
.mirror-choice:hover { border-color: var(--magenta); color: var(--magenta); }
.mirror-choice:disabled { opacity: 0.5; cursor: default; pointer-events: none; }

/* ---------- gate ---------- */

.gate-main {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Room 2 — Listen (2026-09-14): .gate-main now has exactly one direct
     child, .gate-score (the seven-line score is one continuous object,
     not several independently-spaced ones) — center, not space-between,
     is what actually centers a single flex child; space-between would
     leave it pinned to the top of the padded area instead. */
  justify-content: center;
  gap: 20px;
  /* Same failure mode Home already had and got fixed 2026-09-02: a fixed
     negative translateY tuned for shorter content clips the top of the
     page off-screen the moment real content makes this column taller.
     Top padding clears the fixed header instead of fighting it with an
     offset that only works for one specific content length; overflow-y
     is the fallback for genuinely short viewports rather than relying on
     centering alone. */
  /* Bottom padding widened 2026-09-04 — the footer is position:fixed
     (doesn't reserve its own flow space), so 48px left object 5 sitting
     almost flush against it. 84px clears the footer's own ~50px height
     with real breathing room on top. */
  padding: 100px 24px 84px;
  overflow-y: auto;
  text-align: center;
  max-width: 480px;
  margin: 0 auto;
}

.gate-gps-status {
  font-size: 13px;
  letter-spacing: 0.06em;
  color: var(--white);
  margin: 0;
}
/* Only occupies space (and the flex gap around it) once there's actually
   something to say — the default Room Two state has nothing in it. */
.gate-gps-status:empty { display: none; }
.gate-gps-status.is-granted { color: var(--magenta); }
.gate-gps-status.is-blocked { color: var(--white); opacity: 0.6; }

/* Shared with Room 4 (Release) — its own single line ("It's ready.")
   uses .gate-obj--1 too. Room 2's own copy no longer uses .gate-obj at
   all as of the Room 2 — Listen pass (2026-09-14; see .gate-score-line
   below instead) — kept here only because Room 4 still depends on it. */
.gate-obj {
  font-family: var(--serif);
  color: var(--white);
  margin: 0;
  max-width: 420px;
}
.gate-obj--1,
.gate-obj--4 {
  font-size: clamp(22px, 3.4vw, 30px);
  font-weight: 700;
  line-height: 1.3;
}

/* CLOSE ONE THING PASS (2026-09-20): replaces the old .gate-score/
   .gate-score-line coded-fragment score with a single human-facing
   composition — choose the thing, read what's about to happen, cross.
   Same tight vertical rhythm as the score it replaces. */
.gate-composition {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* ROOM 2 CLARITY PASS (2026-09-30): 18px -> 14px, per direct
     instruction to read the field and its crossing as one bonded
     operation rather than a looser stack of independent beats. */
  gap: 14px;
}
.gate-thing-label {
  font-family: var(--sans);
  font-size: 13px;
  letter-spacing: 0.1em;
  color: var(--white);
  opacity: 0.85;
  margin: 0;
}
/* ROOM 2 CLARITY PASS (2026-09-30): was a single-line, center-aligned
   text input styled to look like a short "name it" field — per direct
   instruction, the field must now read as "put the messy thing here,"
   not "fill out this form." Converted to a real multi-line textarea in
   index.html (same id/classes, same .life-auth-email base for its
   white/bordered/focus-magenta look); left-aligned, sized to be visibly
   larger than a form control at rest, with modest JS auto-growth (see
   app.js) capped by max-height/overflow-y below rather than growing
   without limit — matches the field's own 200-char server-side ceiling
   (routes/sessions.js), so in practice this cap is rarely if ever hit. */
.gate-thing-input {
  width: 100%;
  max-width: 420px;
  min-height: 110px;
  max-height: 220px;
  padding: 16px 18px;
  font-size: 17px;
  line-height: 1.5;
  text-align: left;
  resize: none;
  overflow-y: auto;
}
.gate-line {
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.5;
  color: var(--white);
  opacity: 0.85;
  margin: 0;
  max-width: 420px;
}
/* ROOM 2 CLARITY PASS (2026-09-30): replaces the old differentiated
   three-beat treatment (.gate-ai-notice-line at 12px/0.55,
   .gate-line--commit at full 1.0 opacity, .gate-line--release with its
   own +14px break) — per direct instruction, these three operating
   facts must read as one compact, clearly-secondary block, not three
   large detached visual beats. One uniform quiet register for all
   three; only the text itself (ONE THING · FIVE MINUTES · ONE CLEAR
   MOVE. stays capitalized, the other two don't) carries any remaining
   distinction. See .gate-facts below for the tight wrapper gap. */
.gate-line--facts {
  font-size: 12px;
  opacity: 0.6;
}
.gate-facts {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  margin-top: 4px;
}
/* Tightened from 14px — per direct instruction, the field and
   START THE COMPOSITION should read as one operation, not two
   separately-spaced beats. */
.gate-consent {
  margin-top: 6px;
}

/* The deliberate acknowledgment gate — Compose stays disabled until this
   is checked (app.js), so this is the actual "applicable confirmation"
   Terms/Conditions describe, not just ambient copy. */
.gate-consent {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  text-align: left;
  cursor: pointer;
  max-width: 420px;
}
/* Room 2 handshake pass (2026-09-10): the checkbox is still the real
   control app.js binds to (#gate-consent-checkbox, unchanged onchange
   wiring) — only hidden from view, not removed, so "Enter" (the pill
   below) reads as the single visible action instead of sitting next to a
   second, literal checkbox. Standard clip-based hide, not display:none,
   so it stays keyboard-focusable and toggleable via the label. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.gate-consent-body {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
/* Room 2 legibility pass (2026-09-11): quieter, not reworded — only its
   visual weight drops relative to the CTA pill below it, per direct
   instruction to keep this line but make it clearly secondary.
   FIRST AUTHORITY VALVE (2026-09-16), finishing pass: the copy itself
   did change this time — the old single line conflated writing (sent to
   AI) with location (not sent to AI, captured separately at entry) into
   one sentence; see index.html's own comment above .gate-score for the
   full reasoning. This class now carries the composition/AI fact only. */
.gate-consent-text {
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.5;
  color: var(--white);
  opacity: 0.55;
}
/* FIRST AUTHORITY VALVE (2026-09-16), finishing pass: the location fact,
   split out of .gate-consent-text above into its own class — a distinct
   fact (what happens to your location), not the same claim restyled, so
   it never reads as "also processed by AI" the way one shared sentence
   did. Identical visual tier on purpose: this is not a warning, it's the
   same register as everything else in this consent body. */
.gate-location-text {
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.5;
  color: var(--white);
  opacity: 0.55;
}
/* FIRST AUTHORITY VALVE (2026-09-16): same quiet tier as
   .gate-consent-text above — a distinct fact (what this act authorizes),
   not a restatement of what data gets processed, so its own class rather
   than reusing that one, kept at the identical visual weight on purpose:
   this is not a warning, it's the same register as everything else in
   this consent body. Sits after .gate-entry-action in source order, so
   it reads immediately under the actual crossing action, not before it. */
.gate-crossing-scope {
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.5;
  color: var(--white);
  opacity: 0.55;
}
/* Styled as a full pill button, but it's still just a child of the same
   <label> as the checkbox above it — clicking it toggles that one input
   via native label behavior, not a second control or a second handler.
   Squared off 2026-09-10 — matches the hard-edged geometry every other
   primary action on the site already uses (.btn-conduct has none). */
/* MARKET-TEST SURFACE PASS (2026-09-21): was a filled magenta pill —
   the one primary crossing on the site not built from .btn-conduct's
   outline family (white border on black, fills magenta on hover). Room
   1's ENTER and Room 4's COMMIT THIS CONDUCT both use that family;
   reskinned to match exactly so Room 2 doesn't read as a different
   product mid-flow. Mechanism unchanged — same <span> inside the same
   <label>/checkbox app.js binds to. */
.gate-entry-action {
  display: block;
  font-family: var(--serif);
  font-size: 18px;
  letter-spacing: 0.04em;
  font-weight: 700;
  color: var(--white);
  background: var(--black);
  border: 2px solid var(--white);
  padding: 22px 40px;
  margin-top: 6px;
  text-align: center;
  transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease, opacity 0.18s ease;
}

/* CTA MEANING PASS (2026-09-21): the crossing's own meaning, stated
   quietly inside the pill itself — inherits .gate-entry-action's own
   color (white / white-on-hover) at reduced opacity, so it dims/lights
   with the pill as one object rather than needing its own hover rule. */
.gate-entry-subline {
  display: block;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.06em;
  opacity: 0.6;
  margin-top: 6px;
}

.gate-ai-notice {
  display: block;
  font-family: var(--sans);
  font-size: 10px;
  line-height: 1.4;
  font-weight: 400;
  letter-spacing: normal;
  color: var(--black);
  opacity: 0.6;
  margin-top: 8px;
}

/* The connecting thread between objects 1-4 — one continuous field, not
   four paragraphs stacked with equal empty space between them. */
.gate-divider {
  display: block;
  width: 1px;
  height: 20px;
  background: rgba(255, 255, 255, 0.25);
}

/* Room 2 handshake pass (2026-09-10): dropped the bordered-box treatment
   this modifier used to carry — six flat statements plus a boxed form
   module read like two different registers stacked on one screen. The
   pill (.gate-entry-action) is still the primary affordance; this class
   now only exists as the .is-entering hook app.js already toggles. */
.gate-consent--entry {
  transition: opacity 0.18s ease;
}
.gate-consent--entry:hover .gate-entry-action,
.gate-consent--entry:focus-within .gate-entry-action {
  background: var(--magenta);
  color: var(--white);
  border-color: var(--magenta);
}
.gate-consent--entry.is-entering {
  opacity: 0.45;
  pointer-events: none;
}


/* ---------- composition room ---------- */

#screen-composition { background: var(--black); }

/* Room 3 — Final UI Cleanup (2026-09-14), per direct instruction: the
   phase word (bring/decide/establish/...) is gone — "the notation +
   question is the conductor." setRoomCue() in app.js no longer writes
   to this element at all, so it stays permanently empty and collapses
   via the :empty rule just below. Left in the DOM rather than removed
   (no HTML restructuring unless necessary) — this rule is now inert,
   not deleted, in case the element is ever repurposed. */
.room-phase {
  display: block;
  text-align: center;
  font-family: var(--sans);
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--magenta);
  opacity: 0.55;
  margin: 0 0 6px;
}
.room-phase:empty { display: none; }
/* Visual-fix commit (2026-09-14): bundled here as necessary
   infrastructure, not a standalone product decision — recede-on-type
   (dim the phase word once real writing exists, toggled in app.js) is
   what "Room 3 phase/cue readability" refers to; there is no readability
   defect to fix independent of this mechanism, since nothing recedes
   without it. This mechanism was never independently committed/decided
   before now. The specific opacity value (0.3) is the actual fix — an
   earlier, never-committed version of this same mechanism used 0.12,
   confirmed live as unreadable at 13px. Flagged here rather than
   silently bundled; split further on request. */
.room-phase.is-receded { opacity: 0.3; transition: opacity 0.4s ease; }

/* CLOSE ONE THING PASS (2026-09-20): the Page 2 chosen thing, carried
   visibly into Room 3 — quieter than .room-phase (this isn't a prompt,
   it's a standing reminder of what's being closed), never recedes on
   typing the way .room-phase/.room-cue do, since it should stay legible
   the whole time rather than fading once writing starts.
   ROOM 3 OBJECT-COMPOSITION PASS (2026-09-30): promoted from a quiet
   13px/0.5-opacity sans caption to the room's own serif register — per
   direct instruction, this must read as "the object Composium is
   currently carrying," not faint metadata above a blank page. Wrapped
   in .room-thing with its own THE THING label (below); still never
   recedes on typing. */
.room-thing {
  margin: 0 0 10px;
}
.room-thing:has(.room-anchor:empty) { display: none; }
.room-thing-label {
  display: block;
  text-align: center;
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--black);
  opacity: 0.45;
  margin: 0 0 4px;
}
.room-anchor {
  display: block;
  text-align: center;
  font-family: var(--serif);
  font-weight: 700;
  font-size: clamp(15px, 2.4vw, 19px);
  line-height: 1.35;
  color: var(--black);
  opacity: 0.92;
  margin: 0;
}
.room-anchor:empty { display: none; }

/* Anchored to the screen's own edges, not centered by a vh-height +
   transform. That combination is what destabilized under the iOS
   keyboard: vh doesn't track the keyboard-shrunk visual viewport, and a
   transform-centered box has no way to respond when its reference
   height changes — it just recenters around a new, wrong midpoint. Edge
   insets on a position:fixed/dvh-height parent track the real viewport
   directly, so the writing surface holds still instead. */
/* Inset enough to clear the fixed header above it (see .room-bar), and
   bounded with a visible edge so the writing surface reads as a
   contained app surface rather than the page itself — real-device
   feedback was that an edge-to-edge white-on-white box with no border
   didn't visually resolve as a surface at all. The thin border still
   does that job; the drop shadow (C5) stays dropped — that's what made
   this read as a floating UI panel/card rather than the room itself. A
   plane with an edge, not a card on a page. Padding widened for more
   breathing room around the material, and the whole surface fades in on
   arrival (see room-arrive below) instead of hard-cutting into view, so
   entering already reads as "something is happening" rather than a page
   load.
   Room 3 handshake pass (2026-09-10): corners squared back off (was
   border-radius:14px, added 2026-09-04) per this pass's explicit
   hard/square geometry requirement — the border alone already does the
   "this is a contained surface" job the rounding was added for. */
.room {
  position: absolute;
  top: 92px;
  right: 24px;
  /* .composium-footer is fixed, ~50px tall, white text built for a black
     surface — real-device finding: the room's white box was extending
     close enough to the bottom edge that the footer overlapped onto it,
     where white-on-white is invisible rather than merely close. This
     clears it outright rather than tightening the margin further. */
  bottom: 64px;
  left: 24px;
  max-width: 640px;
  margin: 0 auto;
  background: var(--white);
  border: 1px solid rgba(0, 0, 0, 0.08);
  display: flex;
  flex-direction: column;
  /* ROOM 3 SMALL UI ENCLOSURE PASS (2026-09-20): this was documented as
     letting .room-field shrink, but min-height:0 only overrides
     flexbox's default min-height:auto on the flex ITEM that needs to
     shrink — setting it on the container (here) does nothing for that.
     .room-field itself never had it — see its own rule below, where the
     real fix now lives (confirmed the bug at 320x480: RESOLVE was pushed
     past the bottom edge entirely). Left here too, still harmless. */
  min-height: 0;
  padding: 32px 36px;
  animation: room-arrive 0.4s ease both;
}

/* Widened on desktop 2026-09-04 — 640px read cramped on larger screens;
   mobile (the max-width: 640px block further down) is untouched. */
@media (min-width: 900px) {
  .room {
    max-width: 800px;
  }
}
@media (prefers-reduced-motion: reduce) {
  .room { animation: none; }
}
@keyframes room-arrive {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Tightened (C5): this header zone previously claimed as much vertical
   weight as the writing surface itself — a real "section" with its own
   border. Quieter border, less padding, so it recedes into the room
   instead of framing it.
   ROOM 3 UI HIERARCHY PASS (2026-09-30): renamed from .room-top,
   restructured from a single-item flex row into the column that groups
   position/time with the current prompt directly beneath it — per
   direct instruction, "position/time → current compositional question"
   must read as one state, not two headings competing for attention. The
   dashed border moves with it, still doing the same job of separating
   this header stack from the writing surface below, just from
   underneath the (now two-line) group instead of underneath the timer
   alone. */
.room-position {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 6px;
  padding-bottom: 8px;
  border-bottom: 1px dashed rgba(0, 0, 0, 0.08);
}

/* Room 3 cue (2026-09-09) — one live cue at a time, aimed at surfacing
   more real Matter, never at asking for the answer. Replaces the dead
   Listen/Compose/Conduct stage indicator (same slot, same quiet
   black-at-low-opacity convention .room-placeholder already uses);
   deliberately not magenta — that color reads as an active control/CTA
   everywhere else in the product, and this is ambient aim, not a CTA.
   Progressed by setRoomCue() in app.js from state.secondsLeft alone.
   Room 3 — Compose (2026-09-14): uppercase transform, applied
   unconditionally regardless of the underlying cue text — a CSS
   transform, not a data change, same pattern .room-phase already uses
   just below; app.js's ROOM_CUES text stays naturally cased in source.
   SEVEN CONDUCTIVE LINES (2026-09-20): the original reason cited for
   this transform (matching Room 2's own "N-X ·" notation) no longer
   applies — neither Room 2 nor Room 3's cue text carries that notation
   anymore. The uppercase treatment itself is kept as-is, now simply the
   room's existing quiet visual register, not tied to a notation that no
   longer exists. Size/opacity untouched. */
.room-cue {
  display: block;
  text-align: center;
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--black);
  opacity: 0.45;
  /* ROOM 3 UI HIERARCHY PASS (2026-09-30): 8px -> 3px — bonds this to
     .room-timer directly above it inside their shared .room-position,
     per direct instruction not to let position/time and the current
     prompt read as two separated, competing headings. */
  margin: 3px 0 0;
}
.room-cue:empty { display: none; }
/* Visual-fix commit (2026-09-14): same mechanism/fix as .room-phase
   above — see that rule's comment. */
.room-cue.is-receded { opacity: 0.28; transition: opacity 0.4s ease; }

/* Only occupies space once resolution has actually failed and there's
   something to say — matches .gate-gps-status's collapse-when-empty. */
.composition-error {
  font-size: 12px;
  letter-spacing: 0.03em;
  color: var(--magenta);
  margin: 0 0 16px;
}
.composition-error:empty { display: none; }

/* Room 3 final surgical pass (2026-09-11): magenta again, per direct
   instruction ("Timer is magenta") — supersedes C4.1's quiet-black
   treatment above. Full opacity throughout, not just during the final-20s
   urgency state below, which still layers its own pulse animation on top
   of this same color. */
.room-timer {
  font-size: 16px;
  color: var(--magenta);
  opacity: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.08em;
}

/* Final 20 seconds: the same number starts carrying the weight of a live
   set actually ending — no new element, just the truth of what's left. */
.room-timer.is-ending {
  opacity: 1;
  animation: room-timer-pulse 1s ease-in-out infinite;
}
@keyframes room-timer-pulse {
  0%, 100% { opacity: 1; color: var(--magenta); }
  50% { opacity: 0.55; color: #ff33ff; }
}

.room-field {
  position: relative;
  flex: 1;
  /* ROOM 3 SMALL UI ENCLOSURE PASS (2026-09-20): this is the flex ITEM
     .room's own min-height:0 comment thought it was fixing — flexbox's
     default min-height:auto on a flex item means it can't shrink below
     its content's natural minimum, which on a short viewport pushed
     RESOLVE off the bottom of the screen entirely (confirmed at
     320x480). This is what actually lets it compress. */
  min-height: 0;
  margin: 24px 0;
}

/* Font-size raised 17px -> 19px (C5, matches .room-input below) — more
   presence for the one thing the room is actually for, a small but real
   part of "more breathing room / stronger relationship between cursor
   and space." */
/* UI/BRAND CONVERGENCE PASS (2026-09-21): 19px -> 13px. This was sized to
   match .room-input (typed text), correct when this element was empty —
   but once it carries a real orientation sentence, matching the typed-
   text size let a momentary instruction outrank .room-cue itself (12px,
   this room's own actual operating-register voice for the live rotating
   prompt). No caret-alignment risk: this and real typed text are never
   visible at once (.is-hidden fires on first keystroke), so decoupling
   the sizes costs nothing. */
.room-placeholder {
  position: absolute;
  top: 0;
  left: 0;
  font-size: 13px;
  color: var(--black);
  opacity: 0.35;
  pointer-events: none;
  /* Short enough that the placeholder is gone before the caret and the
     first typed character settle in — a longer fade here was leaving a
     visible overlap between the placeholder and real input. */
  transition: opacity 0.08s ease;
}
.room-placeholder.is-hidden { opacity: 0; }

/* C4.1 — no copy added, just a slow, quiet breath on the one thing
   already occupying the empty room, so the surface reads as already
   present/attentive rather than a static, inert form field. Scoped to
   :not(.is-hidden) so it cleanly hands off to the instant fade above the
   moment real typing begins, instead of fighting it (a running CSS
   animation otherwise keeps winning over a plain opacity rule regardless
   of selector specificity). */
@media (prefers-reduced-motion: no-preference) {
  .room-placeholder:not(.is-hidden) {
    animation: room-placeholder-breathe 3.6s ease-in-out infinite;
  }
}
@keyframes room-placeholder-breathe {
  0%, 100% { opacity: 0.28; }
  50% { opacity: 0.42; }
}

.room-input {
  width: 100%;
  height: 100%;
  min-height: 0;
  resize: none;
  border: none;
  outline: none;
  background: transparent;
  color: var(--black);
  font-family: var(--sans);
  font-size: 19px;
  line-height: 1.65;
  caret-color: var(--magenta);
}
/* Resolution has begun — the same dimming language the room already uses
   for its placeholder/support text, not a new visual idea. */
.room-input:disabled {
  opacity: 0.5;
}

.room-bottom {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 8px;
  padding-top: 10px;
  border-top: 1px dashed rgba(0, 0, 0, 0.08);
}

/* C5: was a solid black filled button — the one element in the room that
   read as a conventional "Submit" control, competing with the fact that
   resolution is already automatic (early Conduct). Recast as a quiet
   text affordance, same register as the Card's own "Save" — present and
   fully functional (same id, same click handler, same mechanics), but no
   longer shaped like the thing you're supposed to press to make
   something happen.
   ROOM 3 UI HIERARCHY PASS (2026-09-30): 0.45 opacity/no border read as
   disabled rather than quiet, per direct instruction. Raised resting
   opacity and weight, added a visible underline so it reads as a
   deliberate exit from the composition — still restrained (no fill, no
   box), not the conventional Submit button C5 deliberately moved away
   from. Same id, same click handler, same mechanics; only contrast
   changed. */
.btn-conduct-room {
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.1em;
  background: none;
  color: var(--black);
  opacity: 0.75;
  border: none;
  border-bottom: 1px solid rgba(0, 0, 0, 0.35);
  padding: 6px 2px 4px;
  cursor: pointer;
  transition: opacity 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.btn-conduct-room:hover { opacity: 1; color: var(--magenta); border-bottom-color: var(--magenta); }

/* ROOM 3 WRITING-SURFACE UTILITY PASS (2026-09-21): a quiet local
   clipboard utility tucked into the writing surface's own bottom-right
   corner — deliberately smaller/quieter than .btn-conduct-room (RESOLVE)
   so it never reads as a second primary action. [hidden] (toggled in
   app.js off the same input listener that already drives the
   placeholder/cue-recede behavior) covers "hidden when empty"; disabled
   is set alongside it for the same condition, belt and suspenders since
   this sits inside a focusable text field a person may tab past. */
.room-copy-btn {
  position: absolute;
  right: 0;
  bottom: 6px;
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.08em;
  background: none;
  border: none;
  color: var(--black);
  opacity: 0.3;
  cursor: pointer;
  padding: 4px 2px;
  transition: opacity 0.18s ease, color 0.18s ease;
}
.room-copy-btn:hover,
.room-copy-btn:focus-visible { opacity: 0.75; color: var(--magenta); }
.room-copy-btn:disabled { cursor: default; }

/* ---------- release (the crossing) ----------
   Restores .stripe-main/.stripe-eyebrow, removed in be3cd06 alongside
   screen-stripe itself, under names matching the restored room's new
   id — same rules, unchanged. */

.release-main {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* ROOM 4 COMMIT SURFACE PASS (2026-09-20): plain `center` plus
     overflow-y:auto isn't enough on its own — confirmed live (390x600):
     Chromium's centered-flex-overflow behavior only ever makes the END
     overflow reachable by scroll, not the START, so the closure heading
     stayed permanently clipped above an unreachable scrollTop no matter
     how far the box scrolled. `safe center` centers when content fits
     and falls back to start-alignment (so scrollTop:0 is the real top)
     when it doesn't — the standard fix for this exact bug, not a
     workaround. This pass added real height (resolved line, caption, two
     lines that used to live inside the button) with no corresponding
     overflow fallback before now — .gate-main/.room both already handle
     their own short-viewport case, this brings Room 4 in line. */
  justify-content: safe center;
  gap: 24px;
  /* Matches .gate-main's own values exactly, same reason: .bar is
     position:fixed (top:0, ~60-70px effective height) and
     .composium-footer is fixed (~50px), and #screen-release is one of
     the screens that shows it (body:has(#screen-release.screen--active)
     .composium-footer below). Confirmed live: without this, scrolled
     content at either end visually passed underneath the fixed header —
     overflow-y:auto/safe-center alone only fix scroll reachability, not
     the fixed elements sitting on top of what scrolls into their space. */
  padding: 100px 24px 84px;
  overflow-y: auto;
  text-align: center;
  max-width: 480px;
  margin: 0 auto;
}

/* ROOM 4 HINGE PASS (2026-09-30): recognition → authorization → release,
   per direct instruction. Position 1 of the room's own 3+1+3 — quiet
   orientation that this is the result of the composition, same
   label-above-object convention Room 3's THE THING already established
   (.room-thing-label), recolored for this room's dark surface. */
.release-orientation {
  display: block;
  text-align: center;
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--white);
  opacity: 0.45;
  margin: 0;
}
/* ROOM 4 COMMIT SURFACE PASS (2026-09-20): #release-resolved-line reuses
   .release-conduct-ready's existing large-serif treatment unchanged (see
   its rule just below) — this only adds the collapse-when-empty
   convention already used throughout the app (.room-cue:empty,
   .gate-gps-status:empty, etc.) for the one known path where
   state.card may not be set yet — see renderResolvedLine()'s comment in
   app.js. */
#release-resolved-line:empty { display: none; }
/* Position 2: the dominant object of the room, unchanged by this pass —
   it was already the room's largest, most legible element; what was
   wrong was the clutter competing around it, not its own size. No
   truncation, blur, or copy-disabling of any kind — full resolved text,
   always. */
.release-conduct-ready {
  max-width: 100%;
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(22px, 4vw, 30px);
  font-weight: 700;
  line-height: 1.25;
  color: var(--white);
}
/* Center 1 — the crossing. ROOM 4 HINGE PASS (2026-09-30): dropped the
   permanent magenta border and the two-tier label/sub split (Stripe/
   security copy now lives in its own quiet line below the button, see
   index.html) — one flat "RELEASE AS A CONDUCT · €4" action, magenta
   only on hover/focus via the shared .btn-conduct rule (below), same
   restraint convention Rooms 1-3 already established: magenta as the
   active signal, not a resting field. Compact by default now (no
   Room-specific font-size override — inherits .btn-conduct's own 18px),
   not the "giant theatrical portal" this room must avoid per direct
   instruction. */
.release-control {
  width: min(100%, 420px);
  padding: 20px 22px;
  line-height: 1.3;
}
.release-control-label {
  display: block;
}
/* Center 1's own quiet, secondary group — Stripe/security fact and T&C,
   deliberately plain .release-quiet-line (see index.html), not its own
   bespoke styling; this is support copy for the crossing above it, not a
   fourth beat competing with it. */
/* Lower 3 — what release creates. ROOM 4 HINGE PASS (2026-09-30):
   replaces the old five-statement pile-up (IT MOVES WHEN YOU DO / THIS
   IS THE LINE YOU ARE CHOOSING TO STAND BEHIND / RELEASED AS A COMPOSIUM
   CARD / THE CONDUCT STAYS EXACT / THE COMPOSITION ENDS HERE...) with
   three consequences under real hierarchy, mirroring Room 1's own
   context/top/bottom-quiet rhythm: a quiet lead fact, one stronger
   serif "click" in the middle (.release-line--movement, repurposed
   in-place — its existing bold-serif treatment already was this room's
   strongest lower beat, just under different words), and a quieter
   closing line stating plainly that movement stays the human's choice. */
.release-score {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}
.release-line--seal {
  opacity: 0.4;
}
.release-contract-fact {
  margin: 0;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.1em;
  line-height: 1.4;
  color: var(--white);
  opacity: 0.85;
}
.release-agreement {
  margin: 0;
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.02em;
  line-height: 1.4;
  color: var(--white);
  opacity: 0.55;
}
.release-agreement .release-terms-link { opacity: 1; }

/* Quiet, clearly interactive — same dimmed/hover-to-magenta convention
   used elsewhere in the app, just on white (Room 4 is
   .screen--dark) instead of black. Own line, own element: not a
   parenthetical inside the sentence above, so it never renders with
   browser-default blue/underline (no rule existed for the old inline
   <a>, which is exactly what this replaces). */
/* Room 4 final surgical pass (2026-09-11): opacity dropped 0.5 -> 0.4 —
   with the music control now a real, visible text action of its own
   (.release-audio-mark, resting at 0.55), Terms needs to read as
   clearly the quietest of the two, per direct instruction that it stay
   "the smallest/subtlest text action" on the page.
   CALLABLE CONDUCT PASS (2026-09-22): that ordering has reversed — Chopin
   is now the one deliberately hidden control (opacity 0.06, see
   .release-audio-mark above), so Terms no longer needs to read as the
   quieter of the two. Bumped up instead, per direct instruction ("a bit
   larger and more visible"): 12px -> 14px, opacity 0.4 -> 0.65. Explicit
   font-weight:400 added — this link now sits inside .release-line--
   conduction (bold), which font-weight otherwise inherits by default
   unlike font-family/font-size; without this it silently picked up that
   bold weight the first time this class was ever nested in a bold
   parent. */
.release-terms-link {
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 400;
  letter-spacing: 0.08em;
  color: var(--white);
  opacity: 0.65;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: opacity 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.release-terms-link:hover,
.release-terms-link:focus-visible {
  opacity: 1;
  color: var(--magenta);
  border-bottom-color: var(--magenta);
}

/* ---------- loop / processing ---------- */

#screen-loop { background: var(--black); }

.loop-wrap {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  gap: 14px;
}

.loop-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--magenta);
  animation: loop-pulse 1s ease-in-out infinite;
}
.loop-dot:nth-child(2) { animation-delay: 0.15s; }
.loop-dot:nth-child(3) { animation-delay: 0.3s; }

/* The landing beat ("LAND") — the instant resolution is confirmed, the
   three pulsing dots settle into one steady white point before the Card
   appears. Same markup, no new elements: the other two dots fade out,
   the first stops pulsing and turns white. The point has landed. */
.loop-wrap.is-landed .loop-dot {
  animation: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.loop-wrap.is-landed .loop-dot:first-child {
  opacity: 1;
  background: #ffffff;
  transform: scale(1.4);
  transition: transform 0.25s ease, background 0.15s ease;
}

@keyframes loop-pulse {
  0%, 100% { opacity: 0.25; transform: scale(0.8); }
  50% { opacity: 1; transform: scale(1.15); }
}

/* ---------- card screen ---------- */
/* #screen-card and its own .card-main wrapper removed (Room 5 — Conduct A,
   2026-09-15) along with the screen itself — /card/:click_id is the one
   canonical Card surface now. .composium-card (the SVG's own viewport-
   relative width/aspect-ratio sizing) removed here too (Room 5 — Make the
   Visible Card Boringly Stable, 2026-09-15) along with the SVG it sized —
   card.html's visible Card is ordinary block-flow HTML now (see its own
   .card-doc rules); the rules below this point (.card-brand through
   .card-source-mark) are shared with card.html's offscreen #export-card
   SVG (TAKE IT's own renderer, never visible) via this same stylesheet
   and are untouched. */

/* Source Mark — quietest text on the Card, deliberately: subordinate to
   the composition above it. Display-only reference, not the full
   click_id, not a digest, not provider/model — see routes/card.js for
   what the mark can resolve to. The separate footer wordmark this used
   to sit below (.card-wordmark) was removed 2026-09-10 as a duplicate of
   the top brand text, which now reads COMPOSIUM.APP itself. */
.card-source-mark {
  fill: #ffffff;
  opacity: 0.35;
  font-family: var(--sans);
  font-size: 9px;
  letter-spacing: 0.08em;
}

/* Same mark as .card-source-mark, carried onto Life and Mirror (both
   white screens, hence black instead of white) — one visible thread
   proving this is the same sealed object moving through the rest of the
   transaction, not a fresh screen with no memory of the Card that made it. */
.object-mark {
  color: #000000;
  opacity: 0.3;
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 0.08em;
  margin: 0;
}

/* Demoted 2026-09-04 (27px -> 16px): Card identity should be clear but
   subordinate to the actual resolved content — Tone now owns the first
   recognition moment, instead of the brand header. */
.card-brand {
  fill: var(--magenta);
  font-family: var(--serif);
  font-size: 16px;
}

/* Room 5 final surgical pass (2026-09-11): three intentional chunks, not
   a taper from one giant line to one tiny leftover line, per direct
   instruction. Tone (recognition) and Conduct (the forward charge —
   "go now") both bold and full-opacity, bookending Composition (the
   concrete movement now in front of the user) as the measured, regular-
   weight middle. Conduct previously ended the Card at 15px/400/0.75
   opacity — a fading coda; it now lands with as much real weight as
   Tone, deliberately, since "go now" is not the quietest thing on the
   Card. Sizes kept close together (24/20/22) rather than a steep decay,
   so all three read as deliberate objects, not a hierarchy of
   diminishing importance. See .card-label just below for the small
   TONE/COMPOSITION/CONDUCT structural markers each chunk now carries. */
.card-label {
  font-family: var(--sans);
  color: #ff00ff;
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  opacity: 0.55;
  margin: 0 0 4px;
}

/* All three: overflow-wrap so a single long word can't push past the
   Card's own border. font-size here is the base/normal size (scale 1.0)
   — card-layout.js sets an inline font-size that can compress this down
   for genuinely dense Cards, measuring real rendered height at each
   trial rather than trusting a fixed per-section ceiling. max-height
   here is a generous backstop only — large enough to never constrain a
   real, successfully-fitted layout, there only to contain a pathological
   string if the JS layout pass fails to run at all. */
/* Room 5 Card Receiving Object pass (2026-09-15), per direct instruction:
   Composium-dominant hierarchy — supersedes the 2026-09-14 Conduct-
   dominant build immediately above. Composium (the resolved synthesis
   of Tone+Composition+Conduct together) is now the Card's one hero
   line — full weight, full opacity, largest text on the Card. Tone,
   Composition, and Conduct all become its supporting anatomy: Conduct
   keeps a modest edge over Tone/Composition (bold, a "hard line," per
   direct instruction) since it's still the strongest of the three, but
   none of the three compete with Composium itself. Values match card-
   layout.js's own BASE_FONT_SIZE, which this file's own comment
   requires stay in sync.
   Room 5 — Finish the Canonical Card (2026-09-15): 26 -> 30, per direct
   instruction that this gap needed to read as significantly greater
   authority, not a modest size step — widens the ratio over Conduct
   (17px, the largest of the three constituents) from ~1.53x to ~1.76x. */
.card-composium {
  font-family: var(--serif);
  color: #ffffff;
  font-weight: 700;
  font-size: 30px;
  line-height: 1.3;
  margin: 0;
  overflow-wrap: break-word;
  max-height: 400px;
  overflow: hidden;
}

.card-tone {
  font-family: var(--serif);
  color: #ffffff;
  font-weight: 400;
  font-size: 15px;
  line-height: 1.4;
  opacity: 0.7;
  margin: 0;
  overflow-wrap: break-word;
  max-height: 400px;
  overflow: hidden;
}

.card-composition {
  font-family: var(--serif);
  color: #ffffff;
  font-weight: 400;
  font-size: 15px;
  line-height: 1.5;
  opacity: 0.65;
  margin: 0;
  overflow-wrap: break-word;
  max-height: 400px;
  overflow: hidden;
}

.card-conduct {
  font-family: var(--serif);
  color: #ffffff;
  font-weight: 700;
  font-size: 17px;
  line-height: 1.4;
  opacity: 0.85;
  margin: 0;
  overflow-wrap: break-word;
  max-height: 400px;
  overflow: hidden;
}

/* ---------- send (room 6 — where the Card leaves) ----------
   White, like the Card screen this follows — one continuation, not a cut
   to an unrelated black menu. One question, one set of real destinations
   — no recap of the Card itself, the composer already has it. */

#screen-life { background: var(--white); }

/* Room 6 Conduct Payload pass (2026-09-11): conduct_payload is now the
   dominant object in the room — the resolved commit itself, not a
   quiet preview of something else shown above it. Bold serif, centered,
   the same weight register the old .life-composition (removed) used to
   carry. white-space:pre-wrap preserves the payload's own real line
   breaks (a composium.app or substituted Card-URL line Carmelita may
   have written). */
.life-payload {
  font-family: var(--serif);
  font-weight: 700;
  font-size: clamp(19px, 3.2vw, 27px);
  line-height: 1.4;
  letter-spacing: 0.01em;
  color: var(--black);
  white-space: pre-wrap;
  max-width: 480px;
  margin: 0;
  text-align: center;
}
.life-payload[hidden] { display: none; }

/* Shared small text-link register — used by NOT YET below. */
.life-inline-link {
  background: none;
  border: none;
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.03em;
  color: var(--black);
  opacity: 0.75;
  cursor: pointer;
  padding: 2px 4px;
  text-decoration: underline;
}
.life-inline-link:hover { opacity: 1; color: var(--magenta); }

/* Room 6 Final Cut (2026-09-14): endpoint tiles, unchanged register from
   the prior pass (quiet mark+label, not an app grid) — now rendered
   inline inside each route's own .life-port-endpoints instead of one
   shared panel elsewhere on the page. */
.life-route-mark {
  display: grid;
  grid-template-columns: 16px minmax(0, 1fr);
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: 6px;
  row-gap: 2px;
  background: none;
  border: none;
  padding: 3px 4px;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.03em;
  color: var(--black);
  opacity: 0.6;
  text-align: left;
  transition: opacity 0.18s ease, color 0.18s ease;
}
.life-route-mark-icon { display: flex; grid-column: 1; grid-row: 1 / span 2; }
.life-route-mark-icon svg { display: block; width: 16px; height: 16px; }
.life-route-mark-label { grid-column: 2; grid-row: 1; }
.life-route-mark-cue {
  grid-column: 2;
  grid-row: 2;
  font-size: 9px;
  line-height: 1.25;
  letter-spacing: 0;
  opacity: 0.66;
}
.life-route-mark:hover,
.life-route-mark.is-selected {
  opacity: 1;
  color: var(--magenta);
}
/* A quiet visual suggestion only — resting opacity raised above its
   siblings' 0.6, never colored like the active/selected state above,
   never disables or auto-fires the other choices. */
.life-route-mark.is-recommended { opacity: 0.9; }
/* Each mark is the whole action now — briefly disabled while its own
   run() is in flight, so a second tap can't fire a second crossing. */
.life-route-mark:disabled { opacity: 0.3; cursor: default; pointer-events: none; }
/* Honest "the port is real, this destination isn't" tiles — dimmer
   than a real door at rest, a dashed outline instead of hover/selected
   color, so nothing here can read as a working channel before it is one. */
.life-route-mark.is-placeholder {
  opacity: 0.35;
  border: 1px dashed rgba(0, 0, 0, 0.3);
  border-radius: 3px;
}
.life-route-mark.is-placeholder:hover { opacity: 0.5; color: var(--black); }
.life-port-note {
  font-family: var(--sans);
  font-size: 11px;
  color: var(--black);
  opacity: 0.5;
  margin: 2px 0 0;
}

/* ROOM 6 — quiet orientation: names the choice at the field without
   changing the six existing destinations or their route behavior. */

/* Room 6 scroll column: keep the fixed header clear and long Conducts
   reachable by scrolling. Route panels are absolute and do not add flow. */
.life-main {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* ROOM 6 — ROUTE-TRAY ANCHOR: expanding the endpoint tray used to push
     this column from safe-centered layout into start alignment, visibly
     shifting the Conduct field. Start alignment keeps its origin fixed
     whether the tray is closed or open. */
  justify-content: flex-start;
  gap: 20px;
  /* Reserve scroll room beneath the fixed Conduct action and footer so
     the open carrier tray can be brought fully clear of both. */
  padding: 80px 16px calc(144px + env(safe-area-inset-bottom));
  text-align: center;
  overflow-y: auto;
}

.life-instruction {
  flex: 0 0 auto;
  margin: 0 0 8px;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.12em;
  line-height: 1.3;
}
.life-instruction::after {
  content: "";
  display: block;
  width: 28px;
  height: 2px;
  margin: 7px auto 0;
  background: var(--magenta);
}

/* ROOM 6 — HARD AXIS CUT (2026-09-17): the prior row/column grid did not
   establish six node centers from one geometric origin and radius. This
   field now anchors every center from its actual 50%/50% origin; route
   panels remain anchored to their circles without changing the field. */
.life-sigil {
  --axis-r: 212px;
  --axis-diagonal: calc(var(--axis-r) * cos(45deg));
  --axis-node-size: 88px;
  position: relative;
  flex: 0 0 auto;
  width: calc(var(--axis-r) + var(--axis-r) + var(--axis-node-size) + 20px);
  height: 570px;
  max-width: 100%;
  margin-inline: auto;
  isolation: isolate;
}
.life-port-slot {
  position: absolute;
  /* The center is a positioned z-index:1 layer. Keep every existing
     route control above it so the Conduct surface cannot intercept taps. */
  z-index: 2;
  left: calc(50% + var(--axis-x, 0px));
  top: calc(50% + var(--axis-y, 0px));
  width: max-content;
  height: max-content;
  transform: translate(-50%, -50%);
}
.life-port-slot--structure { --axis-x: 0px; --axis-y: calc(0px - var(--axis-r)); }
.life-port-slot--signal { --axis-x: calc(0px - var(--axis-diagonal)); --axis-y: calc(0px - var(--axis-diagonal)); }
.life-port-slot--ground { --axis-x: calc(0px - var(--axis-r)); --axis-y: 0px; }
.life-port-slot--movement { --axis-x: var(--axis-diagonal); --axis-y: calc(0px - var(--axis-diagonal)); }
.life-port-slot--capital { --axis-x: var(--axis-r); --axis-y: 0px; }
/* Keep SHARE IT below the Conduct object; the other five route centers
   and the Conduct origin remain unchanged. */
.life-port-slot--life { --axis-x: 0px; --axis-y: var(--axis-r); }
/* The center: a compact, hard, black object — the product itself, not a
   floating line of text. Deliberately small and concentrated, not a
   scaled-up hero block; visual authority here comes from contrast
   (solid black against the white room) and material weight (real
   padding, a real edge), the same register the Card's own black-on-
   magenta identity already established, not a new visual language. */
.life-conduct-center {
  position: absolute;
  left: 50%;
  top: 50%;
  /* ROOM 6 — page-axis correction: the rendered black object read ~5px
     left of the viewport midpoint; correct the object only, not its orbit. */
  transform: translate(calc(-50% + 5px), -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: 100%;
  max-width: 300px;
  min-width: 200px;
  background: #000000;
  border-radius: 16px;
  padding: 22px 24px 18px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.16);
  box-sizing: border-box;
  z-index: 1;
}
.life-center-label {
  font-family: var(--sans);
  color: var(--magenta);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0.9;
  margin: 0 0 2px;
}
.life-composium {
  font-family: var(--serif);
  font-weight: 700;
  font-size: clamp(16px, 2.2vw, 20px);
  line-height: 1.32;
  color: #ffffff;
  max-width: 260px;
  margin: 0;
  text-align: center;
  overflow-wrap: break-word;
}
.life-composium:empty { display: none; }
.life-provenance {
  font-family: var(--sans);
  font-size: 9px;
  letter-spacing: 0.08em;
  color: #ffffff;
  opacity: 0.62;
  margin: 4px 0 0;
}

/* CUT 2A — LOCK THE CONDUCT FIELD (2026-09-16), per direct instruction:
   COPY CONDUCT re-added — bordered like the existing Room 7 EXIT
   control, sized to read as this
   room's own default action, not as dominant as the solid-black center
   object itself. */
.life-copy-conduct-btn {
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--white);
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.65);
  padding: 8px 14px;
  cursor: pointer;
  margin-top: 2px;
  transition: border-color 0.18s ease, color 0.18s ease, background 0.18s ease;
}
.life-copy-conduct-btn:hover,
.life-copy-conduct-btn:focus-visible { border-color: var(--magenta); color: var(--black); background: var(--magenta); }
.life-copy-conduct-btn:disabled { opacity: 0.5; cursor: default; pointer-events: none; }
.life-copy-conduct-btn.is-selected { background: var(--magenta); border-color: var(--magenta); color: var(--black); }

/* Room 6 — Conduct 6.2 (2026-09-16): the quiet persistent exit — see its
   own comment in index.html. Deliberately more present than Send to
   Composium above (this is the room's one real door, always reachable)
   but still quiet relative to the six ports and the center — per direct
   instruction, "it must not dominate the six routes." */
.life-moved-link {
  /* ROOM 6 — persistent Conduct action: keep the declared crossing
     visible above the fixed footer while the field scrolls beneath. */
  position: fixed;
  left: 50%;
  bottom: calc(56px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 3;
  width: min(360px, calc(100vw - 32px));
  margin: 0;
  background: var(--black);
  border: 1px solid var(--black);
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--white);
  opacity: 1;
  cursor: pointer;
  padding: 14px 20px;
  transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.life-moved-link:hover,
.life-moved-link:focus-visible { background: var(--magenta); color: var(--black); border-color: var(--magenta); }
.life-moved-link:disabled {
  background: var(--black);
  border-color: var(--black);
  color: var(--white);
  cursor: default;
  opacity: 0.4;
}

/* Room 6 — Conduct 6.1 (2026-09-16): each of the six routes now lives
   directly in its own named sigil slot (see .life-sigil above) — one
   route per slot, so the old .life-routes stacking wrapper (three routes
   sharing one container) no longer has a job; each .life-route-port
   renders straight into its .life-port-slot parent. Ports are round and
   soft, deliberately not rectangular menu tiles or equal dashboard
   cards — quiet at rest, popping visibly when selected (see .is-active
   below), receding further than before when a sibling is open, since a
   selected port is now meant to visually dominate its five siblings, not
   just sit at the same weight with a colored border. */
.life-route-port {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  transition: opacity 0.22s ease;
}
/* Route becomes active (selected) or quietly recedes while a sibling
   route is open — never disabled, never removed, per direct
   instruction: "recommendation must never remove user choice." */
.life-route-port.is-receding { opacity: 0.35; }
.life-port-btn {
  width: 88px;
  height: 88px;
  border-radius: 50%;
  background: var(--white);
  border: 1.5px solid rgba(0, 0, 0, 0.16);
  color: var(--black);
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.25;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 8px;
  cursor: pointer;
  opacity: 0.7;
  box-sizing: border-box;
  transition: opacity 0.22s ease, transform 0.22s ease, border-color 0.22s ease, background 0.22s ease, color 0.22s ease, box-shadow 0.22s ease;
}
.life-port-btn:hover {
  opacity: 1;
  border-color: var(--magenta);
  color: var(--magenta);
}
/* The pop: selected keeps its own spatial identity (still the same
   slot, same direction) but visibly activates — filled magenta, scaled
   up, a real shadow — the one thing on the page besides the center
   itself that reads as "engaged," per direct instruction. */
.life-route-port.is-active .life-port-btn {
  opacity: 1;
  background: var(--magenta);
  border-color: var(--magenta);
  color: #ffffff;
  box-shadow: 0 6px 18px rgba(255, 0, 255, 0.28);
}
/* Room 6 — Conduct 6.2 (2026-09-16), per direct instruction: "if there is
   no genuine resource-flow Conduct yet, keep the coordinate
   constitutionally visible but clearly inactive/unavailable." Dashed
   border and a lower resting opacity than even a normal dormant port —
   distinct at rest, before any click, not just once its own (still
   honestly placeholder-labeled) endpoints are opened. Currently applies
   to Capital only (see room6RouteHasRealCarrier() in app.js), derived
   automatically from the same route/channel data everywhere else in this
   file already trusts, not hardcoded to one route id. */
.life-port-btn.is-unavailable {
  border-style: dashed;
  border-color: rgba(0, 0, 0, 0.22);
  opacity: 0.45;
}
.life-port-btn.is-unavailable:hover { opacity: 0.6; border-color: rgba(0, 0, 0, 0.35); color: var(--black); }
.life-route-port.is-active .life-port-btn.is-unavailable {
  background: none;
  color: var(--black);
  box-shadow: none;
}

/* Room 6 — Conduct 6.2 (2026-09-16), real defect found by this pass's
   own geometry-lock requirement (see .life-sigil's own comment above):
   position:absolute, anchored to .life-route-port (the nearest
   positioned ancestor), expanding outward — away from the center, never
   toward it or toward the row a mirrored sibling shares. This removes
   endpoints from grid flow entirely, so opening them can never shift any
   port's own anchor position or the center. Direction is per-slot (see
   the .life-port-slot--* rules below): structure opens upward, life
   downward, signal/ground further left, movement/capital further
   right — each port's own endpoints grow away from the sigil's own
   center, never across it.
   SUPERSEDED (45-MINUTE PUBLISH CUT, 2026-09-16), per direct instruction:
   this whole block is now unused — no .life-port-endpoints element is
   ever created (renderRoom6Routes() in app.js no longer builds one per
   port). STRUCTURE's own upward expansion, predicted as a real risk
   right in this comment, was confirmed live this cut actually sitting
   behind the fixed header at its real 4-item length; endpoints now
   render into the single .life-carrier-tray below the sigil instead
   (normal document flow, structurally unable to overlap the header).
   Kept in place, not deleted, same convention as .home-line--wordmark. */
/* ROOM 6 — ROUTE ACCEPTANCE (2026-09-17): each panel is attached to
   its own selected port and removed from document flow. The panel's
   outward placement leaves the center object and other route circles
   stationary when destinations open. */
.life-port-endpoints {
  position: absolute;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 4px;
  width: 208px;
  max-width: min(208px, calc(100vw - 24px));
  padding: 8px;
  box-sizing: border-box;
  background: var(--white);
  border: 1px solid rgba(0, 0, 0, 0.18);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.12);
  z-index: 6;
}
.life-port-endpoints:empty { display: none; }
.life-port-slot--structure .life-port-endpoints {
  left: calc(100% + 56px);
  right: auto;
  top: 50%;
  bottom: auto;
  transform: translateY(-100%);
  width: 280px;
  max-width: min(280px, calc(100vw - 24px));
  margin: 0;
}
.life-port-slot--life .life-port-endpoints {
  top: 50%;
  left: 100%;
  right: auto;
  transform: none;
  margin: 0 0 0 8px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.life-port-slot--signal .life-port-endpoints {
  right: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-right: 8px;
}
.life-port-slot--ground .life-port-endpoints {
  right: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-right: 8px;
}
.life-port-slot--movement .life-port-endpoints,
.life-port-slot--capital .life-port-endpoints {
  left: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-left: 8px;
}
.life-port-slot--capital .life-port-endpoints { width: 260px; }
.life-route-mark {
  width: 100%;
  min-height: 26px;
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  align-items: baseline;
  column-gap: 8px;
  padding: 3px 5px;
  border: 0;
  background: transparent;
  color: var(--black);
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.life-route-mark:hover,
.life-route-mark:focus-visible { color: var(--magenta); background: rgba(255, 0, 255, 0.06); }
.life-route-mark--static { cursor: default; }
.life-route-mark-label { font-size: 12px; line-height: 1.2; }
.life-route-mark-cue { font-size: 9px; line-height: 1.3; opacity: 0.62; overflow-wrap: anywhere; }
.life-route-mark-action { font-size: 10px; line-height: 1.2; letter-spacing: 0.06em; }
.life-route-mark-behavior { font-size: 8px; line-height: 1.25; opacity: 0.56; }
.life-route-booking { border-top: 0; }
.life-port-note {
  font-family: var(--sans);
  font-size: 10px;
  color: var(--black);
  opacity: 0.5;
  margin: 0;
  max-width: 160px;
}

/* Room 6 — mobile keeps the same route geometry while using a slightly
   wider radius on standard 390px phones. */
/* Room 6 — Conduct 6.1 (2026-09-16), per direct instruction: mobile
   preserves the same sigil topology at real size, not a collapse to a
   generic seven-item list — the center stays the visual hinge, all six
   directions stay individually placed, just compressed. Ports shrink
   (88px -> 64px, still comfortably touch-sized) and the side columns
   narrow accordingly; the center card also narrows slightly so the
   whole sigil fits without horizontal scroll on a real narrow viewport
   (confirmed live at 390px — see this pass's own verification). */
@media (max-width: 640px) {
  .life-main { padding-top: 128px; }
  .life-sigil {
    --axis-node-size: 56px;
    --axis-r: min(132px, calc(50vw - 44px));
    --axis-diagonal: calc(var(--axis-r) * cos(45deg));
    width: calc(var(--axis-r) + var(--axis-r) + var(--axis-node-size) + 16px);
    height: 327px;
  }
  .life-port-slot--life { --axis-y: var(--axis-r); }
  .life-port-btn {
    width: var(--axis-node-size);
    height: var(--axis-node-size);
    font-size: 9px;
    padding: 4px;
  }
  .life-conduct-center {
    width: clamp(120px, calc(100vw - 256px), 134px);
    min-width: 0;
    max-width: 134px;
    padding: 14px 14px 12px;
  }
  .life-copy-conduct-btn { width: 100%; padding: 8px 4px; font-size: 9px; }
  .life-composium { font-size: 14px; line-height: 1.28; }
  .life-port-endpoints { width: 116px; max-width: calc(100vw - 24px); }
  .life-port-slot--structure .life-port-endpoints {
    left: 50%;
    top: auto;
    bottom: calc(100% + 8px);
    transform: translateX(-50%);
    width: min(208px, calc(100vw - 24px));
    margin: 0;
  }
  .life-port-slot--signal .life-port-endpoints {
    left: calc(8px - 50vw + var(--axis-diagonal) + 28px);
    right: auto;
    top: auto;
    bottom: calc(100% + 8px);
    transform: none;
    margin: 0;
  }
  .life-port-slot--signal .life-route-mark { min-height: 30px; padding: 2px 5px; }
  .life-port-slot--signal .life-route-mark-cue { line-height: 1.2; }
  .life-port-slot--ground .life-port-endpoints {
    right: auto;
    left: calc(8px - (50vw - var(--axis-r) - 28px));
    top: 50%;
    transform: translateY(-50%);
    margin: 0;
  }
  .life-port-slot--movement .life-port-endpoints {
    left: 0;
    right: auto;
    top: auto;
    bottom: calc(100% + 8px);
    transform: none;
    margin: 0;
  }
  .life-port-slot--capital .life-port-endpoints {
    left: calc(36px - 50vw - var(--axis-r));
    right: auto;
    top: calc(100% + 92px);
    transform: none;
    margin: 0;
    width: min(220px, calc(100vw - 24px));
  }
  .life-port-slot--life .life-port-endpoints {
    left: 100%;
    right: auto;
    top: 50%;
    transform: none;
    margin: 0 0 0 8px;
    width: min(180px, calc(50vw - 40px));
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 360px) {
  .life-sigil {
    --axis-r: 108px;
    --axis-diagonal: calc(var(--axis-r) * cos(45deg));
    --axis-node-size: 44px;
    width: calc(var(--axis-r) + var(--axis-r) + var(--axis-node-size) + 16px);
    height: 284px;
  }
  .life-port-slot--life { --axis-y: var(--axis-r); }
  .life-port-slot--life .life-port-endpoints { width: min(120px, calc(50vw - 40px)); }
  .life-conduct-center {
    width: 120px;
    max-width: 120px;
    min-width: 0;
    padding: 10px 8px 8px;
  }
  .life-copy-conduct-btn { padding: 7px 3px; font-size: 8px; }
  .life-composium { font-size: 12px; line-height: 1.25; }
}

/* Inline sign-in — appears only when Save needs an account it doesn't
   have yet. Same visual language as the Vault page's own login form
   (input#login-email/.btn-vault), not a new idiom. */
/* ROOM 6 — ROUTE AUTHORIZATION (2026-09-18): the proposed outward route,
   shown before anything is recorded. Layout only — the fields themselves
   reuse .life-auth/.life-auth-email below, unchanged. */
.life-authorize {
  width: 100%;
  max-width: 440px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.life-authorize[hidden] { display: none; }

.life-authorize-label {
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.18em;
  color: var(--magenta);
  margin: 0;
}

.life-authorize-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
}

.life-authorize-name {
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 0.14em;
  color: rgba(0, 0, 0, 0.45);
}

.life-authorize-note {
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.5;
  color: rgba(0, 0, 0, 0.55);
  margin: 0;
  text-align: center;
}

/* Scoped so the shared .life-auth width container stacks the three route
   fields here without altering it anywhere else it may be reused. */
.life-authorize .life-auth {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.life-auth {
  width: 100%;
  max-width: 440px;
}

.life-auth-form {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.life-auth-email {
  flex: 1;
  min-width: 180px;
  font-family: var(--sans);
  font-size: 14px;
  padding: 12px 14px;
  border: 1px solid var(--black);
  background: var(--white);
  color: var(--black);
}
.life-auth-email:focus { outline: none; border-color: var(--magenta); }

.life-auth-submit {
  font-family: var(--serif);
  font-size: 14px;
  letter-spacing: 0.04em;
  background: var(--black);
  color: var(--white);
  border: 1px solid var(--black);
  padding: 12px 22px;
  cursor: pointer;
  transition: background 0.18s ease, border-color 0.18s ease;
}
.life-auth-submit:hover { background: var(--magenta); border-color: var(--magenta); }
.life-auth-submit:disabled { opacity: 0.5; cursor: default; }

/* ---------- void ---------- */

.screen--void { background: var(--white); }

/* ---------- ownership footer ----------
   Hidden by default; enabled per-screen below. Allow-listed on purpose —
   only the black in-transaction screens that don't already carry a
   closing statement of their own (Card's embedded wordmark, Conducted's
   own line, Mirror's own time/coords footer). Room Three's own layout is
   untouched; this sits in the black margin around the room, not in it. */
.composium-footer {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 4;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 18px 16px;
  pointer-events: none;
}

.composium-footer-link {
  font-size: 10px;
  letter-spacing: 0.05em;
  color: var(--white);
  opacity: 0.4;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  pointer-events: auto;
  transition: opacity 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.composium-footer-link:hover,
.composium-footer-link:focus-visible {
  opacity: 1;
  color: var(--magenta);
  border-bottom-color: var(--magenta);
}

/* Home is excluded deliberately — pure pre-boundary arrival, before any
   crossing has happened, carries none of the operative-interior chrome. */
body:has(#screen-gate.screen--active) .composium-footer,
body:has(#screen-composition.screen--active) .composium-footer,
body:has(#screen-release.screen--active) .composium-footer,
body:has(#screen-life.screen--active) .composium-footer {
  display: flex;
}

/* Life is white (it continues the Card, not the black in-transaction
   screens) — the footer's white-on-dark text needs the same inversion
   here or it disappears against that background. */
body:has(#screen-life.screen--active) .composium-footer-link {
  color: var(--black);
}
/* Restated at equal specificity so it still wins over the base rule above
   during hover/focus — the base selector's #screen-life id would
   otherwise outrank .composium-footer-link:hover's plain class rule and
   silently cancel the hover color. */
body:has(#screen-life.screen--active) .composium-footer-link:hover,
body:has(#screen-life.screen--active) .composium-footer-link:focus-visible {
  color: var(--magenta);
}

/* ---------- legal overlay ---------- */

/* Product path pass (2026-09-10): OPEN -> READ -> X -> exact same place.
   Every .js-legal-link click opens the real legal page inside this fixed
   sheet (see app.js) instead of navigating away — active composition
   state is structurally unreachable from inside the iframe, so there is
   nothing to lose regardless of what's clicked inside it. */
.legal-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.legal-overlay[hidden] { display: none; }
.legal-overlay-frame {
  position: relative;
  width: 100%;
  max-width: 720px;
  height: 100%;
  max-height: 800px;
  background: var(--white);
  border: 1px solid rgba(0, 0, 0, 0.1);
}
.legal-overlay-close {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 1;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--white);
  color: var(--black);
  border: 1px solid var(--black);
  font-family: var(--sans);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  transition: border-color 0.18s ease, color 0.18s ease;
}
.legal-overlay-close:hover { border-color: var(--magenta); color: var(--magenta); }
.legal-overlay-iframe {
  width: 100%;
  height: 100%;
  border: none;
}

/* ---------- responsive ---------- */

@media (max-width: 640px) {
  .room { top: 78px; right: 14px; bottom: 60px; left: 14px; padding: 24px 20px; }
  /* .room-placeholder removed from this rule (2026-09-21) — now decoupled
     from .room-input's own size (see its base rule's own comment), so it
     no longer needs to track it responsively either; .room-cue, the same
     register it now matches, has no mobile override either. */
  .room-input { font-size: 15px; }
  .bar { padding: 16px 18px; }
  .btn-conduct { padding: 18px 28px; font-size: 16px; }
  .legal-overlay { padding: 0; }
  .legal-overlay-frame { max-width: none; max-height: none; border: none; }
}

/* A software keyboard can reduce the visual viewport far below a phone's
   physical height. Keep the same Room 3 hierarchy and controls, but return
   space from chrome/margins to the flexible writing field so the live cue,
   current writing, and RESOLVE remain visible together. Desktop and normal
   mobile viewports retain the rules above unchanged. */
@media (max-width: 640px) and (max-height: 520px) {
  .room { top: 60px; bottom: 50px; padding: 12px 16px; }
  /* ROOM 3 OBJECT-COMPOSITION PASS (2026-09-30): keyboard-open state —
     the thing stays visible but collapses to one truncated line rather
     than claiming multiple lines of the tightest vertical budget on
     screen; .room-field's own flex:1 is what actually returns that
     space to the writing surface once these header lines compress. */
  .room-thing { margin-bottom: 2px; }
  .room-thing-label { font-size: 10px; margin-bottom: 2px; }
  .room-anchor {
    font-size: 13px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* ROOM 3 UI HIERARCHY PASS (2026-09-30): .room-top renamed
     .room-position (see base rule) — margin-top moved from .room-cue
     (now the second line inside .room-position, not a standalone
     element after it) onto .room-position itself; .room-cue keeps only
     its own tight 3px bond to the timer above it. */
  .room-position { margin-top: 2px; padding-bottom: 4px; }
  .room-cue { font-size: 11px; }
  .room-field { margin: 10px 0; }
  .room-bottom { margin-top: 2px; padding-top: 4px; }
  .btn-conduct-room { padding: 4px 2px; }

  /* ROOM 2 CLARITY PASS (2026-09-30): same keyboard-open strategy as
     Room 3 just above — .gate-composition becomes the flex:1 child that
     actually returns space to the field, .gate-thing-input becomes the
     flex:1 child within it that claims that space, and every sibling
     compresses to its smallest legible size so the field stays "the
     dominant usable area" per direct instruction, not just a field that
     merely avoids growing further. */
  .gate-main { padding: 60px 16px 40px; }
  .gate-composition { flex: 1; min-height: 0; width: 100%; gap: 8px; }
  .gate-thing-label { font-size: 11px; }
  .gate-composition > .gate-line { font-size: 12px; line-height: 1.35; }
  .gate-thing-input { flex: 1; min-height: 60px; max-height: none; padding: 10px 14px; }
  .gate-consent { margin-top: 4px; }
  .gate-entry-action { padding: 14px 24px; font-size: 16px; }
  .gate-facts { gap: 2px; margin-top: 2px; }
  .gate-line--facts { font-size: 10px; }
}


/* ---------- ONE PIECE PASS (2026-09-25) ----------
   The seven rooms are one composition. Matter enters, Conduct resolves,
   the exact Conduct becomes the melody, and the remaining rooms carry it.
   These are compositional emphasis corrections only; no routing or state
   mechanics change here. */
#screen-home .home-line--top {
  font-size: clamp(28px, 5vw, 54px);
  line-height: 1.08;
  max-width: 720px;
}
#screen-home .home-line--context {
  max-width: 460px;
  line-height: 1.55;
}
#screen-home .home-score {
  max-width: 520px;
}
.gate-thing-label {
  font-family: var(--serif);
  font-size: clamp(18px, 2.8vw, 24px);
  font-weight: 700;
  letter-spacing: 0.02em;
  opacity: 1;
}
/* ROOM 2 CLARITY PASS (2026-09-30): the .gate-thing-input sizing that
   used to live here (max-width/min-height/padding/font-size/line-height,
   tuned for a 54px single-line field) is now set once, in place, on the
   base rule above — this textarea's own real size, not a late override
   fighting an earlier one. */
.release-main {
  max-width: 680px;
}
/* ROOM 4 HINGE PASS (2026-09-30): repurposed in place, CSS unchanged —
   this was already the lower group's strongest beat (bold-leaning serif,
   echoing #release-resolved-line's own register, sized down so it
   recedes rather than competing with the resolved line and button
   above). Per direct instruction, one of the lower three should be the
   stronger "click," analogous to Room 1's own strong lower position;
   this rule already was that, just under different words each pass
   ("IT MOVES WHEN YOU DO." / "Held as a Composium Card." / "READY TO
   MOVE." now) — the Card fact moved to the quiet lead line instead,
   this pass. */
.release-line--movement {
  font-family: var(--serif);
  font-size: clamp(16px, 2.4vw, 21px);
  font-weight: 400;
  letter-spacing: 0.01em;
  opacity: 0.82;
}
.release-conduct-ready {
  max-width: 620px;
}
.life-center-label {
  opacity: 1;
}
