/* ============================================================
   Voice Portal — Sensei's place (the Tutor home around the room)
   The 床の間 (Tokonoma) shell + the journey map, ported from the
   picked demos (design-mockups/voice-portal/, PR #737 + premium
   pass) into the app as an overlay AROUND the live room:
     app < #voicePortalOverlay (9600) > the room, SEATED inside it
   (the 4900-under-5000 ladder above was true until S3R reseated the
   room into .vp-stage; the portal is the outer surface now — see the
   z-index ladder at the top of css/chat-mode.css, and note that a
   number read off a comment is how the dock rail spent a week behind
   the letter panel.)
   One house: the room's shoji slides open INSIDE this lobby and the
   lantern orb travels between them. Everything scoped to the
   overlay; the app's own chrome is opted out (cards rest).
   Desktop-only, localhost-gated (same gate as the room).
   ============================================================ */

/* The paper rises OVER whatever is up — the hall (z 9500) included — in one
   calm motion: opacity + a small settle, compositor-only (no blur of the app
   beneath, no reflow). Every geometric change (scroll lock, chrome hide, hall
   dismissal) waits under body.voice-portal-covered, added only once the paper
   is fully opaque, so nothing ever shifts in view. */
#voicePortalOverlay {
  position: fixed; inset: 0; z-index: 9600;
  display: none;
  background: var(--color-paper);
  color: var(--color-ink);
  font-family: var(--font-ui);
  -webkit-font-smoothing: antialiased;
  overflow-y: auto;
  scrollbar-width: thin;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.4s var(--ease-settle, ease),
              transform 0.45s var(--ease-settle, ease),
              filter 0.5s ease;
}
/* the room's 3px hairline, on every scroller in this file. ⛔ BOTH HALVES OR
   NEITHER: `::-webkit-scrollbar` is Chrome/Safari, `scrollbar-width` is what
   Firefox reads. The room styled its record to a hairline and the portal drew
   the platform's full-fat bar three inches away, on the same screen. */
#voicePortalOverlay::-webkit-scrollbar { width: 3px; }
#voicePortalOverlay::-webkit-scrollbar-track { background: transparent; }
#voicePortalOverlay::-webkit-scrollbar-thumb { background: rgba(44, 44, 44, 0.08); border-radius: 3px; }
#voicePortalOverlay.visible { opacity: 1; transform: translateY(0); }
/* ------------------------------------------------------------
   THE ROOM AS THE ENTRY (S2, owner model 2026-07-26)

   The room does NOT nest inside this overlay. It stays a fixed,
   viewport-anchored layer and the rail becomes a left INSET on
   it, so everything measured inside the room keeps meaning what
   it meant: the bounded transcript column, the dock rail, the
   bottom anchor, the clip-path box. Nesting would have cost all
   five, plus the overlay's transform makes it a containing block
   for the room's fixed children.

   So while the room is live the PAPER steps aside and the rail
   stays. (The portal cannot duck beneath the room: the chat
   overlay lives inside #app, whose stacking context sits at z 10,
   while the portal is a body-level layer — any portal opacity
   paints OVER the room. First live walk 2026-07-11.) Instead the
   overlay goes transparent and inert, and only the rail keeps
   its paint and its pointer events.
   ------------------------------------------------------------ */
body.voice-portal-open { --vp-rail-w: 244px; }
body.voice-portal-open.vp-rail-collapsed { --vp-rail-w: 0px; }

body.chat-mode #voicePortalOverlay {
  background: transparent; pointer-events: none; overflow: hidden;
  /* ⛔ the entrance transition above is REPEATED here on purpose. A second
     `transition` shorthand replaces the first rather than adding to it, and
     since S3R the room is up from the moment the portal opens — so this rule
     applies on the very first frame and used to leave the overlay with
     `transition-property: background` alone. The paper's own 0.4s rise was
     dead: the rail snapped into place at frame one while the room behind it
     took 0.6s to open, and Sensei's place looked like it loaded in two
     halves. Whatever this rule transitions, it carries opacity + transform. */
  transition: background 0.4s ease,
              opacity 0.4s var(--ease-settle, ease),
              transform 0.45s var(--ease-settle, ease);
}
body.chat-mode #voicePortalOverlay .vp-shell { background: transparent; }
body.chat-mode #voicePortalOverlay .vp-main { opacity: 0; pointer-events: none; transition: opacity 0.3s ease; }
body.chat-mode #voicePortalOverlay .vp-rail { pointer-events: auto; }
/* ⛔ LOAD-BEARING since the room moved inside (2026-07-27). The overlay above
   turns pointer-events OFF for itself and everything under it, which was
   harmless while the room was a separate body-level layer — it is now a CHILD,
   so without this the entire room would be click-through: the bar, the hold,
   the ＋, the transcript's reading doors, all of it dead. Any new element
   seated in the stage needs the same line. */
body.chat-mode #voicePortalOverlay .chat-mode-overlay { pointer-events: auto; }

/* ⛔ AND SO DOES EVERY NEW THING SEATED IN THE OVERLAY. The rule above is not a
   quirk of the rail — the overlay turns pointer-events OFF for itself and
   everything under it, so ANY element appended to it is born click-through.
   Both of these rendered perfectly and were dead to a real mouse: the row menu,
   and the echo's Undo (which the CDP suite "passed" for two rounds because
   el.click() bypasses pointer-events entirely — only elementFromPoint catches
   it). If you append to #voicePortalOverlay, you add a line here. */
body.chat-mode #voicePortalOverlay .vp-menu,
body.chat-mode #voicePortalOverlay .vp-echo { pointer-events: auto; }

/* collapsed: the rail slides off the left edge and the room goes full bleed.
   ⛔ AND IT LEAVES THE TAB ORDER WITH IT. transform moves a thing without
   hiding it, so every button in a rail the learner had put away kept its seat
   in the walk: Tab from the room landed on controls nobody could see. The
   visibility flip is DELAYED by the slide's own 0.34s on the way out and
   instant on the way back, so the rail is never invisible while it is still
   travelling. ⛔ ONE complete transition list per rule — a second shorthand
   replaces the first rather than adding to it. */
body.vp-rail-collapsed #voicePortalOverlay .vp-rail {
  transform: translateX(-100%); visibility: hidden;
  transition: transform 0.34s var(--ease-settle, ease), visibility 0s 0.34s;
}

/* THE PANEL TOGGLE, second position. One control, one glyph, one corner: it
   lives in the rail's top row while the rail is out, and against the viewport's
   top-left once the rail has gone. It is NOT a way out of Sensei's place —
   leaving is the room's own ✕, top right. (Until 2026-07-26 this corner held a
   ✕ that closed the whole portal, so hiding the history quit the room instead.
   Hide and leave are different acts and may not share a glyph.) */
.vp-rail-handle { display: none; }
/* Same key, other seat — so the same spec (see .vp-rail-x below for the
   measurement and the reference block). Its hover never inked the mark, only
   the wash: half of one control wearing half a dress. */
body.voice-portal-open.vp-rail-collapsed .vp-rail-handle {
  display: flex; align-items: center; justify-content: center;
  position: fixed; left: 14px; top: 14px;
  width: 30px; height: 30px; z-index: 9601; pointer-events: auto;
  background: rgba(253, 252, 249, 0); border: 1px solid rgba(44, 44, 44, 0);
  border-radius: 9px; cursor: pointer; padding: 0;
  color: var(--color-stone); opacity: 0.68;
  /* ⛔ IT ARRIVES WITH THE RAIL, not 340ms ahead of it. display:none→flex is not
     animatable and opacity was never in the transition list, so the handle
     painted at full strength on frame one — on top of a rail still three
     hundred milliseconds from leaving — and vanished at frame one on the way
     back, leaving 340ms with neither control on screen. display:none restarts
     the animation on every arrival, which is exactly the behaviour wanted here.
     ⛔ The base opacity stays 0.68 and the keyframe carries only `from`, so
     reduce lands the end state by construction. */
  animation: vpHandleIn 0.34s var(--ease-settle, ease);
  transition: color 0.2s, background 0.24s var(--ease-settle, ease),
              border-color 0.24s var(--ease-settle, ease),
              box-shadow 0.24s var(--ease-settle, ease), transform 0.12s;
}
@keyframes vpHandleIn { from { opacity: 0; } }
/* flat on hover, for the reason recorded at .vp-rail-x below */
#voicePortalOverlay .vp-rail-handle:hover {
  color: var(--color-ink); opacity: 1;
  background: rgba(253, 252, 249, 0.97); border-color: rgba(44, 44, 44, 0.10);
}
#voicePortalOverlay .vp-rail-handle:active { transform: scale(0.94); }
/* 30 → 44×44 (touch floor). Nothing sits within 14px of this corner, so the
   expander is symmetric. */
.vp-rail-handle::after { content: ''; position: absolute; inset: -7px; }
@media (prefers-reduced-motion: reduce) {
  #voicePortalOverlay .vp-rail { transition: none; }
  /* the collapsed rule outranks the base one, so its own list has to be killed
     by name or the transform would still travel under reduce */
  body.vp-rail-collapsed #voicePortalOverlay .vp-rail { transition: none; }
  #voicePortalOverlay .vp-rail-x,
  body.voice-portal-open.vp-rail-collapsed .vp-rail-handle {
    transition: color 0.2s, opacity 0.2s; animation: none;
  }
  #voicePortalOverlay .vp-rail-x:active,
  #voicePortalOverlay .vp-rail-handle:active { transform: none; }
}
/* once covered: lock the page scroll and step the app chrome out (all of this
   happens beneath opaque paper — invisible, so nothing can shake) */
body.voice-portal-covered { overflow: hidden; }
body.voice-portal-covered .ink-wash-nav,
body.voice-portal-covered .hero-top-bar,
body.voice-portal-covered .app-branding-topleft,
body.voice-portal-covered .intro-mode-indicator { display: none !important; }
/* (.grid-filter-bar / .grid-search-bar / .nav-tools-row dropped 2026-07-28 —
   the free-text filter chrome was retired with the lookup palette, #772/#773;
   nothing in js/ or index.html has emitted those classes since.) */

#voicePortalOverlay, #voicePortalOverlay * { box-sizing: border-box; }
#voicePortalOverlay [hidden] { display: none !important; }
/* dragging over text is the one place the OS gets to choose a colour in this
   product, and its answer is a flat system blue. The hall already refused it
   (css/home-hall.css) with the app's indigo at 0.18 — same ink here. */
#voicePortalOverlay ::selection { background: rgba(61, 90, 128, 0.18); }

/* Japanese always Klee One; English always Inter. Never let them drift.
   ⛔ THE `#chatModeOverlay .ces-lobby …` HALVES ARE GONE (2026-07-28). #cesLobby
   is a real socket in index.html, but the only thing any JS writes to it is
   emptiness (chat-mode.js renderDoorCard: `seat.textContent = ''`); the landing
   seats in .chat-landing-head. A DESCENDANT of an always-empty node cannot
   match, so those halves were dressing nothing. */
/* ⛔ THE LETTER PANEL IS BODY-LEVEL, so it needs saying twice. Everything in it
   is portal-built (letterSheet, the record, the notebook page) but it lives
   OUTSIDE #voicePortalOverlay — so the letter's kept phrases fell back to the
   body's Noto Sans and its inline Japanese to a Latin serif, in the one place
   the learner reads Sensei's own hand. Same scope pairing as --vpl-hand above. */
#voicePortalOverlay .jp,
.chat-thread-panel .jp { font-family: var(--font-jp); }
#voicePortalOverlay .romaji,
.chat-thread-panel .romaji { font-family: var(--font-ui); font-style: italic; color: var(--color-romaji); }
/* Japanese inside prose keeps the ink-brown of every other JP token */
#voicePortalOverlay .vp-rec-sec .jp,
#voicePortalOverlay .jy-proof-name .jp,
#voicePortalOverlay .jy-proof-read .jp { color: var(--color-jp-text); }

/* ---- native fidelity: opt out of the app's raised-button chrome (cards rest;
   the portal's own hover rules re-enable their gentle motion further down) ----

   ⛔ SCOPED TO THE PORTAL'S OWN FURNITURE, NEVER "everything under this ID"
   (2026-07-27). This read `#voicePortalOverlay button:hover { transform: none }`,
   which was true for as long as the room was a SEPARATE overlay. The moment the
   room was seated inside the portal (one screen, ensureShell/seatRoom) that
   blanket became a rule over every button in the room — and it carries an ID,
   so at (1,1,1) it outranks the room's own (0,3,0) hover rules and simply won.
   Two controls are centred BY transform, so killing their transform dropped
   them half their own size down and right, out from under the cursor:
     · #chatStartBtn (Talk to Sensei / Resume) — measured 73px × 22px
     · #chatPauseBtn (the hold)                — measured 32px × 32px
   Which then FLICKERED: the button moves off the pointer, :hover ends, it snaps
   back, :hover fires again. The owner caught it within a minute.
   chat-mode.css already warned about exactly this ("Every transform on a centred
   dock control must restate the centering") — the warning was right and the
   defence was specificity, which an ID walks straight through.
   The lesson is the general one: a rule scoped to a CONTAINER'S ID annexes
   whatever is later moved into that container. Scope to what you own.
   The portal owns the rail, the reference surfaces and the handle. The landing
   is portal content sitting in the room, so it is named in its seat there —
   ONE seat, `.chat-landing-head` (the `.ces-lobby` half went 2026-07-28: that
   socket is only ever emptied, so nothing inside it can match).

   ⛔ AND THE LIST HAS TO GROW WITH THE PORTAL — the exact mirror of the bug
   above (owner, 2026-07-27: "make the menu options not shake as I hover between
   them"). That one was scoped too WIDE and annexed the room's buttons; this was
   scoped too NARROW, and the row menu and the echo are appended to the OVERLAY
   rather than into .vp-rail or .vp-main — so they fell straight back under the
   global `button:hover` (components.css:385), which lifts 2px and drops a
   6px/20px card shadow. Measured: each item rose to y 371 from 373 and gained
   `0 6px 20px`, so crossing from Rename to Remove made both jump. They also wore
   a `0 4px 15px` at REST — a card shadow on a row inside a menu that already has
   one. ⭐ Both halves of this trap are the same question: WHICH ELEMENTS DOES
   THIS SELECTOR ACTUALLY REACH TODAY? Ask it again every time the portal grows
   a new surface, and add it here. */
#voicePortalOverlay :is(.vp-rail, .vp-main, .vp-menu, .vp-echo) button,
#voicePortalOverlay > .vp-rail-handle,
#chatModeOverlay .chat-landing-head button { box-shadow: none; }
#voicePortalOverlay :is(.vp-rail, .vp-main, .vp-menu, .vp-echo) button:hover,
#voicePortalOverlay > .vp-rail-handle:hover,
#chatModeOverlay .chat-landing-head button:hover { transform: none; box-shadow: none; }

/* ── ONE FOCUS RING, AND IT IS THE ROOM'S (2026-07-28) ──────────────────────
   The room draws `outline: 2px solid var(--color-accent); outline-offset: 2px`
   on each of its controls. The portal drew nothing of its own and fell through
   to the app's global mark (css/base.css) — a 2px outline PLUS a 4px indigo
   box-shadow halo. So the keyboard indicator changed shape at the rail seam:
   tab across from a rail row into the room and the same act wore two marks.
   The room's is the one that stays, because it is the mark drawn beside the
   speech; the halo is app chrome from a different screen.
   base.css is deliberately untouched — it is app-wide and not S2's to re-rule.

   ⛔ SCOPED TO THE PORTAL'S OWN FURNITURE, exactly like the opt-out above, and
   for the recorded reason (:139-170). `#voicePortalOverlay` CONTAINS the room
   now, and an ID walks straight through the room's own (0,3,0) rules — which
   would flatten the two controls that declare a different offset on purpose
   (.chat-dock-hold at 3px, .cu-dock-close at 1px). Same list as the opt-out;
   it grows with the portal, never with a wildcard. */
#voicePortalOverlay :is(.vp-rail, .vp-main, .vp-menu, .vp-echo) :is(button, [role="button"], a):focus-visible,
#voicePortalOverlay > .vp-rail-handle:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
  box-shadow: none;              /* the global 4px halo — the room draws no halo */
}
/* …but a card must not DIP when you reach it by keyboard: the line above is
   also what would strip its resting shadow. (1,3,1) beats the (1,3,0) above. */
#voicePortalOverlay :is(.vp-rail, .vp-main) :is(button, a).vp-card:focus-visible { box-shadow: var(--shadow-rest); }

/* The letter and the record render in the ROOM's side panel, not in this
   overlay (one room, one conversation surface — S3R). The portal still BUILDS
   them, so their ink has to reach where they are mounted; both token blocks
   name that seam explicitly. */
#voicePortalOverlay, .chat-thread-panel, #chatModeOverlay {
  /* portal-local semantic tints, all derived from the token palette.
     #chatModeOverlay is here because the LANDING renders inside the room
     (owner ruling 2026-07-26) and its shelf reads these — without them a
     pill's background resolves to nothing and paints white on white. */
  /* (--vp-solid deleted 2026-07-29, S9 — the last matcha in Sensei's place.
     S7 took --rel-similar out of the status vocabulary (it is the word-graph's
     relation hue, and a green "solid" chip made a machine state look like a
     semantic edge) and left it on loan to .jy-tl-dot until S9 rebuilt that
     timeline. The shelf is earned end to end now, so the dot is gold and the
     token has no consumer left. The eviction is total, and the colour law
     enforces it as such: any matcha in either file is red.) */
  /* (--vp-forming / --vp-growing deleted 2026-07-28, S7: the chips they dressed
     now grade in ink, so both tokens had zero consumers. design-mockups keeps
     its own copies in design-mockups/voice-portal/portal.css.) */
  --vp-watching: var(--color-faint);
  --vp-line: rgba(44, 44, 44, 0.07);
  --vp-line-strong: rgba(44, 44, 44, 0.12);
}

/* ⛔ THE LANTERN ORB IS RETIRED (2026-07-27, owner ruling). Its own heading
   said what was wrong with it: "chat-mode v18's lantern, lobby-sized" — the
   portal was carrying a COPY of a mark the room had already replaced with the
   ensō (law #747). Two orbs for one room, and pressing Talk swapped one for
   the other in the same seat, which is the loudest half of why the place read
   as two places. The room's ensō is the one mark now, at rest and in the talk;
   the landing draws no orb at all (lobbyNode, voice-portal.js).
   .vp-orb-mini below is NOT part of this: it is the rail brand's small avatar
   beside "Sensei", a different object doing a different job, and it stays. */
.vp-orb-mini {
  position: relative; width: 30px; height: 30px; border-radius: 50%;
  background: radial-gradient(circle at 38% 33%, #f4efe6 0%, #e6dcc9 40%, #cabda3 100%);
  box-shadow: inset 0 1px 1px rgba(255,252,244,0.6), inset 0 -2px 4px rgba(120,105,78,0.14);
  flex-shrink: 0;
}
.vp-orb-mini::before {
  content: ''; position: absolute; top: 22%; left: 24%; width: 30%; height: 28%; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,253,247,0.85), rgba(255,253,247,0) 70%);
}

/* ------------------------------------------------------------
   TYPE SCALE
   ------------------------------------------------------------ */
.vp-eyebrow { font-family: var(--font-ui); font-size: var(--rm-caption); letter-spacing: 0.14em; text-transform: uppercase; color: var(--color-stone); opacity: 0.55; }
.vp-h1 { font-family: var(--font-ui); font-size: 26px; font-weight: 600; letter-spacing: -0.01em; color: var(--color-ink); }
.vp-h2 { font-family: var(--font-ui); font-size: var(--rm-en); font-weight: 600; letter-spacing: 0.01em; color: var(--color-ink); }
.vp-body { font-family: var(--font-ui); font-size: var(--rm-en); line-height: 1.6; color: var(--color-stone); }
.vp-small { font-family: var(--font-ui); font-size: var(--rm-support); line-height: 1.5; color: var(--color-stone); }
.vp-faint { color: var(--color-stone); opacity: 0.62; }

.vp-anchor { display: inline-flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.vp-anchor .k { font-family: var(--font-jp); font-size: var(--rm-jp); color: var(--color-jp-text); }
.vp-anchor .r { font-family: var(--font-ui); font-style: italic; font-size: var(--rm-support); color: var(--color-romaji); }
.vp-anchor .g { font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone); opacity: 0.8; }

/* ------------------------------------------------------------
   RESTING CARD + ACTIONS
   ------------------------------------------------------------ */
.vp-card { background: var(--color-card); border: 1px solid var(--vp-line); border-radius: var(--radius-lg); box-shadow: var(--shadow-rest); padding: 18px 20px; }
#voicePortalOverlay a.vp-card, #voicePortalOverlay button.vp-card {
  display: block; text-align: left; cursor: pointer; width: 100%;
  transition: border-color var(--duration) var(--ease-settle), transform var(--duration) var(--ease-settle);
}
#voicePortalOverlay a.vp-card:hover, #voicePortalOverlay button.vp-card:hover { border-color: var(--vp-line-strong); transform: translateY(-1px); }

.vp-btn {
  font-family: var(--font-ui); font-size: var(--rm-support); font-weight: 600;
  border-radius: var(--radius-full); padding: 12px 30px; border: 1px solid transparent; cursor: pointer;
  transition: filter var(--duration) var(--ease-settle), transform 0.12s var(--ease-settle), background var(--duration), border-color var(--duration);
  display: inline-flex; align-items: center; gap: 9px;
}
/* (--primary and --ghost deleted 2026-07-28: both are mockup-era variants that
   the live portal never emits — the only construction of either is in
   design-mockups/voice-portal/portal.js, which loads its OWN portal.css.
   .vp-btn--quiet below is live; so is .vp-add-btn.ghost further down, which is
   a different class despite the word.) */
.vp-btn--quiet { background: transparent; color: var(--color-stone); border-color: transparent; padding: 8px 12px; font-weight: 500; }
/* the quiet second verdict inside a word row — one class, because five copies
   of `style="padding:6px 12px;font-size:12.5px"` were invisible to the type law
   and drifted from every token the surface uses (S8) */
.vp-btn--row { padding: 6px 12px; font-size: var(--rm-support); }
#voicePortalOverlay .vp-btn--quiet:hover { color: var(--color-ink); }
.vp-btn .chev { font-size: 15px; line-height: 1; opacity: 0.85; }

/* weight 400 since the Muji cut (owner pick 2026-07-28) — the ask is content,
   not a heading; margin-inline centres the 46ch cap on the landing's one axis */
.vp-focus-lead { font-family: var(--font-ui); font-size: 18px; font-weight: 400; line-height: 1.45; color: var(--color-ink); max-width: 46ch; margin-inline: auto; }
.vp-focus-lead .jp { color: var(--color-jp-text); }
.vp-focus-lead.long { font-size: var(--rm-en); }

/* chips */
.vp-chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-ui); font-size: var(--rm-caption); font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 3px 9px 3px 8px; border-radius: var(--radius-full); border: 1px solid currentColor;
}
.vp-chip .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
/* HOW SOLIDLY YOU KNOW IT — graded neutrals, one wash (S7, owner pick B).
   Four hues said four unrelated things (matcha=relation, amber=earned,
   indigo=interactive) about ONE ordered scale. The scale is ordered, so ink
   WEIGHT carries it: ink for solid, stone for the middle two, faint for
   watching — over a single background so the row reads as one family.
   The border and .dot are currentColor, so they follow without a rule. */
.vp-chip.solid   { color: var(--color-ink);   background: rgba(44,44,44,0.04); }
.vp-chip.forming { color: var(--color-stone); background: rgba(140,133,120,0.08); }
.vp-chip.growing { color: var(--color-stone); background: rgba(140,133,120,0.08); }
.vp-chip.watching{ color: var(--vp-watching);background: rgba(140,133,120,0.08); }

/* the earned moment (gold — scarce, only when a win landed) */
.vp-won { position: relative; border-left: 2px solid var(--color-gold); padding-left: 14px; }
.vp-won-seal { display: inline-flex; align-items: center; gap: 7px; font-family: var(--font-ui); font-size: var(--rm-control); font-weight: 600; letter-spacing: 0.04em; color: var(--color-amber-deep); }
.vp-missed { border-left: 2px solid var(--vp-line-strong); padding-left: 14px; }

/* nav */
.vp-nav-item {
  display: inline-flex; align-items: center; gap: 9px;
  font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone);
  padding: 8px 12px; border-radius: var(--radius); cursor: pointer; background: none; border: none;
  transition: color var(--duration), background var(--duration);
}
.vp-nav-item .seal { font-family: var(--font-jp); font-size: 13px; opacity: 0.68; }
#voicePortalOverlay .vp-nav-item:hover { color: var(--color-ink); }
.vp-nav-item.active { color: var(--color-accent); }
.vp-nav-item.active .seal { opacity: 0.9; }
/* the row grows a second line ONLY when it has something to say, so a quiet
   queue keeps the menu a plain list of four words */
.vp-nav-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; text-align: left; }
.vp-nav-item.has-status { align-items: flex-start; padding-top: 7px; padding-bottom: 7px; }
.vp-nav-item.has-status .seal { margin-top: 1px; }
.vp-nav-status {
  font-family: var(--font-ui); font-size: var(--rm-control); font-weight: 600; letter-spacing: 0.01em;
  color: var(--color-stone); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* ⛔ THE TONE CLASSES CARRY NO COLOUR (S7, owner pick B, 2026-07-28). Five
   states wore five hues — amber, indigo, matcha, dimmed stone — and between
   them they spent the whole palette on what the MACHINE is doing. The words
   already differ ("6 built to review" / "2 being built"); the ink does not.
   The classes STAY in the DOM: they are the JS writers' output and the ember's
   only hooks. Every tone now inherits the base .vp-nav-status stone.
   the ember — the ONE status colour: a small mark meaning "something waits for
   YOU" (built cards to review, refreshes to judge, candidates to activate).
   Text never wears the hue; as a graphic it clears WCAG 1.4.11 at 4.12:1
   worst-case on this paper. Everything the machine is merely doing stays ink. */
.vp-nav-status.ready::before, .vp-nav-status.refresh::before, .vp-nav-status.build::before {
  content: ''; display: inline-block; width: 5px; height: 5px; border-radius: 50%;
  background: var(--color-amber-deep); margin-right: 6px; vertical-align: 1.5px;
}
/* neutral, not amber: the status line beside it already carries the ember, and
   two warm marks on one nav row would say "waiting" twice. */
.vp-nav-badge {
  font-family: var(--font-ui); font-size: var(--rm-caption); font-weight: 700;
  min-width: 16px; height: 16px; padding: 0 4px; border-radius: var(--radius-full);
  background: rgba(44,44,44,0.07); color: var(--color-stone); display: inline-grid; place-items: center;
}

.vp-surface { animation: vpSurfaceIn 0.4s var(--ease-brush) both; }
@keyframes vpSurfaceIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* The other half of the swap. The arrival has had an entrance since the portal
   was built; the departure was a hard cut in the same frame, so a surface
   change read as a flicker rather than a move. The outgoing node is lifted out
   of the flow and laid over the arriving one for 90ms.
   ⛔ NOTHING TRAVELS: the entrance carries the 6px rise, and a ghost that also
   moved would make two things travel in opposite directions over one 400ms.
   Opacity only, and fast — an exit that outlasts its arrival is a smear.
   ⛔ SOURCE ORDER IS LOAD-BEARING. Re-appending a node restarts its CSS
   animations, so the ghost would otherwise fade straight back IN under
   .vp-surface's rule; both are (0,1,0), so this must stay below it.
   ⛔ NO reduce-motion rule here on purpose — the ghost is never CREATED under
   reduce (voice-portal.js), which is the version of this that cannot strand
   anything invisible.
   ⛔ THE OFFSETS ARE .vp-main's OWN PADDING (40 / 44 / 44, declared below).
   An absolute child measures the PADDING box, so a bare `inset: 0` would seat
   the ghost 40px above and 44px left of the content it is replacing. Move that
   padding and move these with it. */
.vp-swap-ghost {
    position: absolute;
    top: 40px;
    left: 44px;
    right: 44px;
    pointer-events: none;
    animation: vpSurfaceOut 90ms ease-out both;
}
@keyframes vpSurfaceOut { from { opacity: 1; } to { opacity: 0; } }

.vp-section-title { display: flex; align-items: baseline; gap: 10px; margin: 0 0 4px; }
.vp-section-title .lead { font-family: var(--font-ui); font-size: var(--rm-support); font-weight: 600; color: var(--color-ink); }
.vp-section-title .sub { font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone); opacity: 0.6; }
/* the title's breathing dot — the room's one signal that a worker is at it right
   now. THE MOTION IS THE SIGNAL, not the hue (S7): "being made" is the machine
   working, and the machine never gets the ember. Stone, still breathing. */
.vp-section-title .d { width: 6px; height: 6px; border-radius: 50%; background: var(--color-stone); align-self: center; animation: vplWriting 2.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .vp-section-title .d { animation: none; opacity: 0.6; } }

/* history rows */

.vp-rec-strip { background: var(--color-card-bright); }
.vp-rec-strip-row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.vp-rec-facts { font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone); font-variant-numeric: tabular-nums; }
.vp-rec-facts b { color: var(--color-ink); font-weight: 600; }

/* per-session record */
.vp-rec-head { margin-bottom: 8px; }
.vp-rec-verdict { margin-top: 14px; padding: 10px 0 10px 14px; }
.vp-rec-sec { padding: 20px 0; border-top: 1px solid var(--vp-line); }
.vp-rec-sec-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
.vp-rec-sec-head .l { font-family: var(--font-ui); font-size: var(--rm-support); font-weight: 600; letter-spacing: 0.03em; color: var(--color-ink); }
.vp-rec-sec-head .s { font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone); opacity: 0.6; }
/* (.vp-touch-chips/.vp-touch-chip/.vp-touch-move/.vp-register/.vp-register-label
   deleted 2026-07-28 — mockup-era record furniture; no live renderer emits any
   of them.) */
/* two plain facts about the deck, said in ink (S7): "already in your deck" and
   "nothing waiting" are the machine reporting, not a call to act. */
.vp-indeck { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-ui); font-size: var(--rm-control); font-weight: 600; color: var(--color-stone); }
.vp-allcaught { background: var(--color-card-bright); border-color: var(--vp-line-strong); }
.vp-list { margin: 0; padding-left: 18px; font-family: var(--font-ui); font-size: var(--rm-support); line-height: 1.65; color: var(--color-stone); }
.vp-list li { margin: 3px 0; }
.vp-list li .jp { font-family: var(--font-jp); color: var(--color-jp-text); }
.vp-list--faint li { opacity: 0.78; }
.vp-back { padding-left: 0; margin-bottom: 2px; }

/* the surface's own name — the hall's .wall-section grammar. The romaji is
   FAINT, never the composer's amber: amber means "sound" everywhere else in
   the app, and this is a title, not something to say. */
.vp-words-head { display: flex; align-items: baseline; gap: 12px; margin: 0 0 6px; }
.vp-words-head .jp { font-family: var(--font-jp); font-size: 24px; font-weight: 600;
  color: var(--color-ink); line-height: 1; font-feature-settings: 'palt' 1; }
.vp-words-head .rom { font-family: var(--font-ui); font-style: italic;
  font-size: var(--rm-control); letter-spacing: 0.04em; color: var(--color-faint); }
.vp-words-head .en { font-family: var(--font-ui); font-size: var(--rm-caption);
  font-weight: 600; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-faint); }
.vp-words-head .rule { flex: 1; height: 1px; align-self: center; margin-left: 4px;
  background: linear-gradient(90deg, rgba(44,44,44,0.10) 0%, rgba(44,44,44,0.07) 55%, transparent 96%); }

/* WORD ROWS — ONE BASELINE (S8, owner pick B 2026-07-29).
   Two tracks: everything the row says, and — only where there is one — a
   right-hand fact about the word out loud. The old 104px face column beside a
   flex body broke every word longer than four characters over three lines and
   left the body half empty; the word now opens line 1 INSIDE the body, so a
   long word has the whole row.
   NO BORDERS. Space and the section titles carry the structure — a hairline
   per row turned a list of nine into a ledger of nine, and this surface is
   Sensei's, not an invoice. */
/* ⛔ THE GAP LIVES IN THE CELL, NOT BETWEEN THE TRACKS. A column-gap is spent
   whether or not the second track holds anything, so every row without a
   right-hand fact — most of the queue — gave up 24px of its own measure to an
   empty column. It moved into .vp-word-side's padding: side-less rows get the
   whole track, side rows read exactly as they did. */
.vp-word { display: grid; grid-template-columns: minmax(0, 1fr) max-content;
  column-gap: 0; align-items: start; padding: 15px 0; }
/* a ready row IS the door — the whole row taps through; its only affordance is
   the quiet look-line and a resting hover, never a chip or a filled button */
a.vp-word--tap { text-decoration: none; color: inherit; cursor: pointer;
  margin: 0 -14px; padding-left: 14px; padding-right: 14px; border-radius: var(--radius-lg);
  transition: background var(--duration) var(--ease-settle); }
#voicePortalOverlay a.vp-word--tap:hover { background: var(--color-card-bright); }
/* the door line lives in the row's right column (S8) — no auto-margin: the cell seats it */
.vp-look { font-family: var(--font-ui); font-size: var(--rm-support); font-weight: 600;
  color: var(--color-accent); white-space: nowrap; opacity: 0.8; transition: opacity var(--duration); }
a.vp-word--tap:hover .vp-look { opacity: 1; text-decoration: underline; text-underline-offset: 3px; }
/* (.vp-pill + .vp-pill.refresh deleted 2026-07-28, S7 — the last two survivors
   of a set no live renderer emits (the word-row states are .vp-status-chip
   below). They were held only so shoot_elevation_t2's `.vp-pill.refresh` probe
   would not go silently empty; that probe moved to .vp-nav-status, the live
   successor, in the same commit.) */
/* (.vp-word-face deleted 2026-07-29, S8 — the 104px column is gone; line 1
   lives in the body now.) */
.vp-word-l1 { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.vp-word-jp { font-family: var(--font-jp); font-size: 20px; line-height: 1.25;
  color: var(--color-jp-text); font-feature-settings: 'palt' 1; }
.vp-word-reading { font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone); opacity: 0.75; }
.vp-word-romaji { font-family: var(--font-ui); font-style: italic; font-size: var(--rm-control); color: var(--color-romaji); }
/* the frequency BAND, in the walk's voice: a word, never a number (S8) */
.vp-word-band { font-style: italic; }
.vp-word-body { min-width: 0; }
/* the right-hand cell — one quiet phrase and a day, stone at two weights.
   ⛔ NO GOLD ON THIS AXIS: forty-five earned rows would be wallpaper, and the
   ember only means anything while it is scarce. */
.vp-word-side { text-align: right; padding-top: 3px; padding-left: 24px; }
.vp-word-state { font-family: var(--font-ui); font-size: var(--rm-control);
  color: var(--color-stone); opacity: 0.92; white-space: nowrap; }
.vp-word-date { font-family: var(--font-ui); font-size: var(--rm-control);
  color: var(--color-stone); opacity: 0.62; margin-top: 2px; }
.vp-spoken-more { font-family: var(--font-ui); font-size: var(--rm-control);
  color: var(--color-stone); opacity: 0.62; padding: 10px 0 0; cursor: pointer; }
#voicePortalOverlay .vp-spoken-more:hover { opacity: 0.85; }
.vp-word-gloss { font-family: var(--font-ui); font-size: var(--rm-support); font-weight: 600; color: var(--color-ink); margin-top: 4px; max-width: 52ch; }
.vp-word-why { font-family: var(--font-ui); font-size: var(--rm-support); line-height: 1.55; color: var(--color-stone); margin-top: 5px; max-width: 56ch; }
/* Why a build died. Reads as a note pinned to the row, not an alarm: the design
   has no red, and a billing block is information the owner acts on calmly. The
   left rule is the only weight it carries — and it is .vp-missed's stone
   hairline, not gold (S7 earned-only law: failure never wears the win colour). */
.vp-word-failwhy {
    font-family: var(--font-ui); font-size: var(--rm-support); line-height: 1.55;
    color: var(--color-ink); margin-top: 6px; max-width: 56ch;
    padding: 5px 0 5px 10px; border-left: 2px solid var(--vp-line-strong);
}
.vp-word-meta { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
.vp-status-chip { font-family: var(--font-ui); font-size: var(--rm-caption); font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; padding: 3px 9px; border-radius: var(--radius-full); }
/* THE CYCLE IS ONE WASH, WEIGHT GRADES IT (S7, owner pick B). These five are
   rungs on one ladder, and hue-coding them made pending→live read as five
   unrelated facts. One stone wash under all of them; ink weight says how far
   along (stone early, ink once it is worth a look). */
.vp-status-chip.pending  { color: var(--color-stone); background: rgba(140,133,120,0.1); }
.vp-status-chip.approved { color: var(--color-stone); background: rgba(140,133,120,0.1); }
.vp-status-chip.ready    { color: var(--color-ink); background: rgba(140,133,120,0.1); }
/* building = the letters rail's breathing dot — the room's one signal for
   "being made", and the BREATH is what says it (the gold left with S7) */
.vp-status-chip.building { color: var(--color-stone); background: rgba(140,133,120,0.1); display: inline-flex; align-items: center; gap: 6px; }
.vp-status-chip.building .d { width: 6px; height: 6px; border-radius: 50%; background: var(--color-stone); animation: vplWriting 2.4s ease-in-out infinite; }
/* (.vp-status-chip.blessed + .live deleted 2026-07-29, S8 — the cycle's last
   two rungs, the owner's yes and the card on the wall, had one emitter between
   them (historyChip) and it had no caller anywhere. Both rungs are still SAID
   on this surface: 'Publishing soon' and 'On your wall' are section titles,
   which is where the state has lived since the rows went bare.) */
.vp-add-btn {
  font-family: var(--font-ui); font-size: var(--rm-support); font-weight: 600; color: #fff; background: var(--color-accent);
  border: none; border-radius: var(--radius-full); padding: 7px 16px; cursor: pointer;
  transition: filter var(--duration), transform 0.12s;
}
#voicePortalOverlay .vp-add-btn:hover { filter: brightness(1.06); }
#voicePortalOverlay .vp-add-btn:active { transform: scale(0.97); }
.vp-add-btn[disabled] { background: transparent; color: var(--color-accent); border: 1px solid rgba(61,90,128,0.3); cursor: default; }
/* ghost = a look back at a settled wave, not a call to act */
.vp-add-btn.ghost { background: transparent; color: var(--color-accent); border: 1px solid rgba(61,90,128,0.3); }
/* (.vp-note, .vp-whisper, .vp-whisper-label and the three .vp-shelf-arc rules
   deleted 2026-07-28 — the shelf's own furniture, and the shelf went with the
   landing redesign. Only design-mockups/voice-portal/portal.js still builds
   them, against its own stylesheet.) */

/* ------------------------------------------------------------
   LETTERS — the correspondence (文箱) + Sensei's notebook.
   The sheet is the portal's ONE serif surface: the letter is
   written, everything around it is UI. Red ink (--vpl-shu) is
   the teacher's hand and appears at most twice on a sheet
   (one ◎, one corrected shape). Gold marks milestone letters
   only (earned-only law).
   ------------------------------------------------------------ */
/* ⛔ THE LETTER'S OWN VOICES — FOUR SIZES, NOT FIFTEEN (S12 phase 2, owner
   pick C 2026-07-29). The sheet carried fifteen raw values between 10.5 and
   20px, most of them a half-pixel apart. Half a pixel is under what the eye
   resolves at reading distance, so those steps were variety, not hierarchy —
   and the room next to it had been down to four voices since July.
   The letter does NOT simply join the room's scale: it is written where
   everything around it is UI, and it needs a reading size the room has no name
   for (15.5, between --rm-en 15 and --rm-jp 19) and a line-height the room's
   32px baseline pin cannot give it. So it mints its own four and borrows the
   room's --rm-support / --rm-caption for the two sizes it already shared.
   The pairing of scopes is the rule for everything in this block: the panel is
   a body-level node, so a token declared for the overlay alone reaches nothing
   the learner is reading. */
#voicePortalOverlay, .chat-thread-panel {
  --vpl-hand: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, serif;
  --vpl-shu: #A8402C; /* the red ink */
  --vpl-body: 15.5px; /* the written body voice */
  --vpl-lh: 1.85;     /* the hand's one line-height */
  --vpl-jp: 18px;     /* Japanese displayed on the sheet */
  --vpl-kept: 20px;   /* the kept phrase — the sheet's crown */
  --vpl-meta: 12px;   /* datelines, letter romaji, small marks */
}
/* ------------------------------------------------------------
   THE SESSION RAIL — the history beside the room. A session is
   the entity here; its letter is one of its facets, so the rail
   lists sessions and never letters.
   ------------------------------------------------------------ */
/* ⛔ .vp-rail-cta (the blue New session hand) RETIRED 2026-07-26, owner call.
   The room's own bar carries the one hand that starts a talk; a second accent
   button in the corner competed with it and did something else besides (load
   an empty room — you still had to press the bar). The Sensei mark above the
   nav is the way back to an empty room, and it wears the label now. */

.vp-srail { display: flex; flex-direction: column; min-height: 0; flex: 1; margin-top: 18px; }
.vp-srail-head {
  display: flex; align-items: baseline; gap: 7px; padding: 0 10px 7px;
  font-family: var(--font-ui); font-size: var(--rm-caption); font-weight: 600;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--color-stone); opacity: 0.92;
}
/* ⛔ 1, NOT 0.7. It nests inside the head, so its own 0.7 rode the head's and
   the number measured 2.27:1 — the least legible text in the rail was the one
   fact the rail exists to state. At 1 it simply wears the head's 0.92. */
.vp-srail-count { font-weight: 500; letter-spacing: 0; opacity: 1; }
.vp-srail-list { overflow-y: auto; scrollbar-width: thin; min-height: 0; margin: 0 -4px; padding: 0 4px 4px; }
.vp-srail-list::-webkit-scrollbar { width: 3px; }
.vp-srail-list::-webkit-scrollbar-track { background: transparent; }
.vp-srail-list::-webkit-scrollbar-thumb { background: rgba(44, 44, 44, 0.08); border-radius: 3px; }
/* ⛔ .vp-srail-empty and .vp-srail-aside RETIRED 2026-07-27 (owner). The empty
   state captioned an empty list ("Your sessions will collect here.") and the
   fold put the room's own quarantine bookkeeping on screen as "▸ 4 set aside",
   which reads as four broken sessions rather than four non-sessions. Both gone;
   the archive door below replaces the fold's job of keeping the rail short. */
.vp-srail-more {
  display: block; width: 100%; text-align: left; background: none; border: none;
  font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone);
  padding: 10px; margin-top: 2px; cursor: pointer; opacity: 0.92;
  transition: color var(--duration), opacity var(--duration);
}
/* the way out of a kept session: the rail's own ink, never a second blue hand */
.vp-srail-new {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: left;
  background: none; border: none; cursor: pointer;
  font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone);
  padding: 8px 10px; margin-bottom: 2px; border-radius: var(--radius);
  transition: color var(--duration), background var(--duration);
}
.vp-srail-new .p { font-size: 12px; opacity: 0.68; line-height: 1; }
#voicePortalOverlay .vp-srail-new:hover { color: var(--color-ink); background: rgba(44,44,44,0.035); }
#voicePortalOverlay .vp-srail-new:hover .p { opacity: 0.85; }
/* on the landing it is where you already are, so it stops inviting */
.vp-srail-new.on { color: var(--color-accent); }
.vp-srail-new.on .p { opacity: 0.9; }
#voicePortalOverlay .vp-srail-more:hover { opacity: 1; color: var(--color-ink); }

.vp-srow {
  display: block; width: 100%; text-align: left; background: none; border: none;
  border-left: 2px solid transparent; border-radius: 0 5px 5px 0;
  padding: 8px 10px; cursor: pointer;
}
.vp-srow:hover { background: var(--color-card-bright); }
.vp-srow.on { background: var(--color-card-bright); border-left-color: var(--color-accent); }
.vp-srow.aside { opacity: 0.5; }
/* the two reference doors while Sensei is talking: they say no, and look it.
   0.38 SURVIVES THE S7 CONTRAST SWEEP — RULED 2026-07-28 with the owner's pick:
   this is a refusal state, deliberately sub-floor, and WCAG exempts disabled
   controls (1.4.3). Raising it would make a door that does nothing look open. */
.vp-nav-item.waiting { opacity: 0.38; cursor: default; }
#voicePortalOverlay .vp-nav-item.waiting:hover { background: none; }
.vp-srow-when {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone);
}
/* ⛔ GOLD WORN AS INK, not gold as paint. This 5px circle is the ONLY sign a
   letter is unread — a graphical object under WCAG 1.4.11, floor 3:1 — and
   #C9A96E on the rail's own gradient measures 2.14 at the top and 1.92 at the
   bottom, i.e. it fails hardest exactly where the rows are. --color-amber-deep
   is the same family and already the portal's own (the 済 seal, the ready
   status): 4.60 → 4.12.
   S7 (owner pick B, 2026-07-28) made this dot the type specimen for THE EMBER
   FAMILY — the one working use of the gold/amber family, a ≤5px mark meaning
   "something waits for YOU". Unchanged here; the nav ::before, .jy-nudge .d and
   .vpj-dot-s.nudge were brought TO it. */
.vp-srow-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--color-amber-deep, #8A6E35); flex: none; }
.vp-srow-fold { font-size: var(--rm-caption); opacity: 0.92; }
.vp-srow-title {
  font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-ink); margin-top: 2px;
  line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.vp-srow.on .vp-srow-title { font-weight: 500; }
.vp-srow-title .jp { font-size: var(--rm-support); }
.vp-srow-untitled { color: var(--color-stone); opacity: 0.92; font-style: italic; }
/* how much of the open night was HIS — one quiet line under the night's own
   name, in the fold marker's voice (S9). 0.92 is the room's ruled stone for
   text this size on this paper. */
.vp-srow-turns { font-family: var(--font-ui); font-size: var(--rm-caption); color: var(--color-stone); opacity: 0.92; margin-top: 2px; }
.vp-srow-state,
.vp-srow.writing .vp-srow-title {
  display: flex; align-items: center; gap: 7px; margin-top: 3px;
  font-family: var(--font-ui); font-size: var(--rm-control); font-style: italic; color: var(--color-stone);
}
/* ⛔ FAILURE IS NOT ABSENCE, and it wears its own line. The record could not be
   read, so the rail says that instead of rendering a learner with no history.
   Quiet, but it has to be READ: 0.92 stone is the room's own ruled number for
   text this size on this paper (css/chat-mode.css, twice, both citing 4.5:1). */
.vp-srail-fail {
  padding: 2px 10px 12px;
  font-family: var(--font-ui); font-size: var(--rm-support);
  color: var(--color-stone); opacity: 0.92;
}
/* the archive door's own quiet voice — the RECIPE, not the selector. Extending
   .vp-srail-more would give the one door in the rail a second identity. */
.vp-srail-retry {
  display: block; text-align: left; background: none; border: none;
  font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone);
  padding: 8px 0 0; margin: 0; cursor: pointer; opacity: 0.92;
  text-decoration: underline; text-underline-offset: 3px;
  transition: color var(--duration), opacity var(--duration);
}
#voicePortalOverlay .vp-srail-retry:hover { opacity: 1; color: var(--color-ink); }
/* a line the record could not read is not a line nobody wrote */
.vp-srail-warn {
  padding: 0 10px 7px;
  font-family: var(--font-ui); font-size: var(--rm-caption);
  color: var(--color-stone); opacity: 0.92;
}
/* ⛔ THE RECORD LANDS ON ONE CROSSFADE. The rail's rows and the room's landing
   both wait for the same read, so they arrive together rather than each popping
   in when its own fetch happened to answer. The base opacity is UNTOUCHED and
   the keyframe carries only the `from` — so killing the animation lands the end
   state by construction (reference_reduced_motion_must_land, and the reduce
   block at the foot of this file). It sits below .vp-surface deliberately: the
   landing wears both classes and this is the one that wins on its first paint.
   ⛔ In the SEAT the landing needs a twin: `.chat-landing-head .vp-lobby`
   (chat-mode.css) kills `animation` at (0,2,0) to suppress the surface
   entrance, which silently out-ranked this rule too — the landing popped while
   the rail faded. The seat re-arms exactly this crossfade at seat specificity
   (S6, 2026-07-28); change one and you must change both. */
.vp-record-in { animation: vpRecordIn 0.22s ease-out; }
@keyframes vpRecordIn { from { opacity: 0; } }
.vp-srow.writing { cursor: default; }
/* stone since S7 — the vplWriting breath says "right now"; the hue would have
   said "for you", which a worker mid-write is not. */
.vp-srow-state .d, .vp-srow.writing .d {
  width: 6px; height: 6px; border-radius: 50%; background: var(--color-stone);
  animation: vplWriting 2.4s ease-in-out infinite;
}
@keyframes vplWriting { 0%, 100% { opacity: 0.3; } 50% { opacity: 0.9; } }
@media (prefers-reduced-motion: reduce) {
  .vp-srow-state .d, .vp-srow.writing .d { animation: none; opacity: 0.6; }
}

/* ------------------------------------------------------------
   THE SESSION PAGE is GONE (S3R, 2026-07-26)

   It was a SECOND conversation renderer: the portal drew a past
   session's turns in its own markup (.vps-conv/.vps-msg) with its
   own bar, and Resume then tore that down and opened the real
   room — empty. Two renderers of one thing is how a resume ends
   up looking like a fresh start.

   There is now ONE conversation surface: the room's #chatTranscript,
   painted by the room's own appendMessage. A session is a THREAD
   loaded into it. Its dress lives in css/chat-mode.css beside the
   live talk it has to match, because it IS that talk, stopped.
   ------------------------------------------------------------ */

/* The letter is the app's one WRITTEN artifact, and it speaks in one voice:
   its Japanese joins the serif the English already speaks (owner pick B,
   2026-07-29). Until now the sheet declared no family for its Japanese at all
   and the panel scope rule above handed it Klee — so a single sheet carried a
   written English beside a UI Japanese, which is the seam a reader feels
   without being able to name.
   ⛔ The scope is the SHEET alone. The notebook under it is clinical, the
   record blocks are UI, and everywhere else in Sensei's place Japanese stays
   Klee — the app's own face. Noto Serif JP 400 is already on the fonts line
   (index.html); the weight stays 400 here as it is everywhere on this paper. */
#voicePortalOverlay .vpl-sheet .jp,
.chat-thread-panel .vpl-sheet .jp { font-family: 'Noto Serif JP', 'Hiragino Mincho ProN', serif; }

.vpl-sheet {
  max-width: 620px;
  background: linear-gradient(180deg, rgba(255,255,255,0.55), rgba(255,255,255,0) 130px), var(--color-card-bright);
  border: 1px solid var(--vp-line); border-radius: 3px;
  /* the sheet's own two-layer weight, in the app's sumi (44,44,44). It read
     (44,42,40) — two points off, invisible as a difference and unchosen as a
     decision. The SHAPE and the alphas are the letter's and stay. */
  box-shadow: 0 1px 2px rgba(44, 44, 44, 0.04), 0 14px 40px -28px rgba(44, 44, 44, 0.18);
  padding: 46px 54px 40px;
  animation: vplSheetIn 0.5s var(--ease-settle) both;
}
/* milestone paper: a touch warmer — the one earned-gold surface */
.vpl-sheet.milestone { background: linear-gradient(180deg, rgba(255,255,255,0.5), rgba(255,255,255,0) 130px), #FAF6EC; }
@keyframes vplSheetIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

.vpl-dateline { display: flex; justify-content: space-between; align-items: baseline; font-family: var(--font-ui); font-size: var(--vpl-meta); color: var(--color-stone); margin-bottom: 28px; }
.vpl-tag { font-family: var(--vpl-hand); font-style: italic; font-size: var(--vpl-meta); color: var(--color-stone); }
.vpl-tag.gold { font-family: var(--font-ui); font-style: normal; font-size: var(--rm-caption); letter-spacing: 0.06em; color: var(--color-gold, #C9A96E); }
.vpl-salutation { font-family: var(--vpl-hand); font-size: var(--vpl-jp); color: var(--color-ink); margin-bottom: 20px; }
.vpl-body p { font-family: var(--vpl-hand); font-size: var(--vpl-body); line-height: var(--vpl-lh); color: var(--color-ink); opacity: 0.92; margin: 0 0 15px; max-width: 58ch; }
.vpl-body p .jp, .vpl-ps p .jp { font-size: var(--vpl-body); color: var(--color-jp-text); }
.vpl-body p .romaji, .vpl-ps p .romaji { font-family: var(--vpl-hand); }

/* the indigo quote-rail = MATERIAL (S12 dress ruling 2026-07-29), never state */
.vpl-kept { display: flex; gap: 10px; margin: 4px 0 18px 26px; padding-left: 16px; border-left: 2px solid rgba(61,90,128,0.26); }
.vpl-maru { color: var(--vpl-shu); font-size: var(--vpl-body); padding-top: 4px; opacity: 0.9; }
/* the one line-height that is NOT --vpl-lh: 20px of Japanese at 1.85 opens a
   gap its own romaji sub falls into, and the kept phrase is one block */
.vpl-kept-jp { font-size: var(--vpl-kept); line-height: 1.7; color: var(--color-jp-text); }
.vpl-kept-sub { font-family: var(--vpl-hand); font-style: italic; font-size: var(--rm-support); color: var(--color-stone); margin-top: 4px; }

.vpl-corr { margin: 4px 0 18px 26px; padding-left: 16px; border-left: 2px solid rgba(168,64,44,0.22); }
.vpl-corr-slip .jp {
  font-size: var(--vpl-jp); color: var(--color-stone);
  text-decoration: line-through; text-decoration-color: rgba(168,64,44,0.55); text-decoration-thickness: 1px;
}
.vpl-corr-slip .romaji { font-size: var(--vpl-meta); }
.vpl-corr-shape { margin-top: 5px; }
.vpl-corr-shape .jp { font-size: var(--vpl-jp); color: var(--vpl-shu); }
.vpl-corr-shape .r { font-family: var(--font-ui); font-style: italic; font-size: var(--vpl-meta); color: var(--vpl-shu); opacity: 0.85; }
.vpl-corr-shape .g { font-family: var(--vpl-hand); font-style: italic; font-size: var(--rm-support); color: var(--vpl-shu); }

.vpl-sig { font-family: var(--vpl-hand); font-size: var(--vpl-body); color: var(--color-ink); text-align: right; margin-top: 28px; }
.vpl-sig .jp { font-size: var(--vpl-meta); color: var(--color-stone); margin-left: 6px; }
.vpl-ps { border-top: 1px solid var(--vp-line); margin-top: 26px; padding-top: 16px; }
.vpl-ps-label { font-family: var(--vpl-hand); font-style: italic; font-size: var(--rm-support); color: var(--color-stone); margin-bottom: 5px; }
.vpl-ps p { font-family: var(--vpl-hand); font-size: var(--vpl-body); line-height: var(--vpl-lh); color: var(--color-ink); opacity: 0.9; margin: 0; max-width: 58ch; }

.vpl-words { border-top: 1px solid var(--vp-line); margin-top: 26px; padding-top: 18px; }
.vpl-word { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 7px; }
.vpl-word .jp { font-size: var(--vpl-jp); color: var(--color-jp-text); }
.vpl-word .romaji { font-size: var(--vpl-meta); }
.vpl-word-gloss { font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone); }
.vpl-keep { margin-left: auto; font-size: var(--vpl-meta); color: var(--color-accent); }

/* the record — a facet of the session now, not a fold under its letter */
.vpl-record { max-width: 620px; }

/* the notebook — the ONE admin dress (jy-admin) holds the dated pages */
.vpl-nb { max-width: 640px; }
.vpl-nb-entry { margin-top: 18px; }
.vpl-nb-entry:first-of-type { margin-top: 14px; }
/* ⛔ THE CLINICAL REGISTER IS NOT WRITTEN IN SENSEI'S HAND (S12 phase 2,
   2026-07-29). The notebook page wore the letter's serif one size down, inside
   a dashed admin box — a private clinical note dressed as correspondence, and
   the one thing on this panel that made the hand look like a font choice
   rather than a voice. It speaks the admin block's own grammar now (Inter, the
   room's support and caption voices), which is what everything else behind
   this fold already speaks. The hand belongs to the letter alone.
   ⛔ AND THE DATE IS A DATE. The row is a flex pair — the day against the
   Teacher's meta, which is a sentence — and in the smaller clinical face at a
   narrow width the day was squeezed into a two-line column ("Jul" over "29").
   It never shrinks and it never wraps; the gap that lets the pair breathe is
   the fix, not a smaller day. */
.vpl-nb-date {
  display: flex; justify-content: space-between; align-items: baseline; gap: 14px;
  font-family: var(--font-ui); font-size: var(--rm-support); font-weight: 600; color: var(--color-ink);
  border-bottom: 1px solid var(--vp-line); padding-bottom: 5px; margin-bottom: 9px;
}
.vpl-nb-date > span:first-child { flex-shrink: 0; white-space: nowrap; }
.vpl-nb-date .meta { font-family: var(--font-ui); font-size: var(--rm-caption); font-weight: 400; color: var(--color-stone); }
.vpl-nb-p { font-family: var(--font-ui); font-size: var(--rm-support); line-height: 1.65; color: #454340; opacity: 1; margin: 0 0 8px; max-width: 62ch; }
/* no family of its own: the panel scope rule above gives it Klee, and Klee is
   the app's Japanese face. The serif is the letter's alone. */
.vpl-nb-p .jp { font-size: var(--rm-support); color: var(--color-jp-text); }

/* ------------------------------------------------------------
   THE 床の間 SHELL (rail + main)
   ------------------------------------------------------------ */
/* ⛔ ONE SCREEN, TWO COLUMNS (2026-07-27, owner ruling: "we do not need two
   screens"). This grid is now the ONLY thing that aligns the rail and the room.
   The room used to be a second body-level overlay copying --vp-rail-w into its
   own `left` to stay beside the rail; it is seated in .vp-stage below instead,
   so the second track holds it and nothing has to be kept in sync by hand. */
.vp-shell { min-height: 100%; background: var(--color-paper); display: grid; grid-template-columns: var(--vp-rail-w, 244px) minmax(0, 1fr); }
/* The stage is the second column and the room's containing block — it is what
   `position: absolute` in .chat-mode-overlay.in-portal measures against, which
   is the whole mechanism. It holds BOTH the room and the reference surfaces
   (.vp-main); which one is showing is the room's own business, not the grid's. */
.vp-stage { position: relative; min-width: 0; }
.vp-rail {
  position: sticky; top: 0; align-self: start; height: 100vh; width: 244px;
  display: flex; flex-direction: column; padding: 26px 18px 22px;
  border-right: 1px solid var(--vp-line);
  background: linear-gradient(180deg, var(--color-card) 0%, var(--color-paper) 100%);
  /* visibility is listed with no delay here so the rail becomes reachable the
     instant it starts coming back — the delayed half lives on the collapsed
     rule above (one complete list per rule, never a second shorthand) */
  transition: transform 0.34s var(--ease-settle, ease), visibility 0s;
}
.vp-rail-top { display: flex; align-items: flex-start; }
.vp-rail-brand { display: flex; align-items: center; gap: 11px; background: none; border: none; cursor: pointer; text-align: left; padding: 4px 10px 18px; flex: 1; }
.vp-brand-title { font-family: var(--font-ui); font-size: var(--rm-en); font-weight: 600; color: var(--color-ink); }
.vp-brand-sub { font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone); opacity: 0.92; }
/* ONE CORNER-KEY SPEC, AND IT IS THE ROOM'S MEASURED ONE (2026-07-28). This
   key and the handle below wore 32px/r8/0.42-stone with an ink-wash hover; the
   room's ✕ and letter key wore 30px/r9/0.68-stone and paper arriving under the
   pointer. Same act, same surface, two dresses — and the rail's was the one
   never measured: stone at 0.42 sits under WCAG 1.4.11's 3:1 for a control's
   own boundary, which is exactly why the room's was raised to 0.68 (3.20:1,
   recorded at css/chat-mode.css:381). The room's is the spec now.
   Reference block: css/chat-mode.css `.chat-letter-key, .chat-close-btn`.
   Pinned byte-for-byte against it by tests/css/control_room_test.js. */
.vp-rail-x {
  display: flex; align-items: center; justify-content: center; position: relative;
  background: rgba(253, 252, 249, 0); border: 1px solid rgba(44, 44, 44, 0);
  color: var(--color-stone); cursor: pointer;
  width: 30px; height: 30px; padding: 0; line-height: 1; border-radius: 9px;
  opacity: 0.68;
  transition: color 0.2s, background 0.24s var(--ease-settle, ease),
              border-color 0.24s var(--ease-settle, ease),
              box-shadow 0.24s var(--ease-settle, ease), transform 0.12s;
}
/* ⛔ NO box-shadow, and that IS the parity (measured 2026-07-28). The room's
   reference block declares one on hover — and it never paints: `.chat-header
   button:hover { box-shadow: none }` (chat-mode.css:2135) outranks it at
   (0,2,1) vs (0,2,0), so the room's keys hover flat. Copying the declaration
   here would have given the rail a lift the room does not have — the portal's
   own opt-out (:188-193) is a class weaker than the room's and would have let
   --shadow-rest's 8px/28px layer through. Same paper, same law: cards rest. */
#voicePortalOverlay .vp-rail-x:hover {
  color: var(--color-ink); opacity: 1;
  background: rgba(253, 252, 249, 0.97); border-color: rgba(44, 44, 44, 0.10);
}
#voicePortalOverlay .vp-rail-x:active { transform: scale(0.94); }
/* 30 → 44 tall, 44 wide (the touch floor; the same expander idiom as the room's
   keys, css/chat-mode.css). The left inset is 0: .vp-rail-brand is a button
   flush against it, and .vp-rail-top carries no gap between them. */
.vp-rail-x::after { content: ''; position: absolute; inset: -7px -7px -7px 0; }
.vp-nav { display: flex; flex-direction: column; gap: 1px; }
.vp-nav .vp-nav-item { width: 100%; justify-content: flex-start; }
.vp-nav .vp-nav-item.active { background: rgba(61,90,128,0.07); }
.vp-rail-foot { margin-top: auto; padding: 12px 10px 0; font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone); opacity: 0.92; border-top: 1px solid var(--vp-line); }
/* position: relative — the surface-swap ghost's containing block (.vp-swap-ghost
   above reads THIS padding for its offsets; the two move together). */
.vp-main { position: relative; padding: 40px 44px 90px; max-width: 960px; }

/* lobby */
/* one column since the shelf went (owner, 2026-07-27) — the second track was
   the shelf's, and an empty 292px track pulls the greeting off the room's axis */
/* ONE AXIS (owner pick, 2026-07-28 — S6): the landing centres on the column
   the lamp and the bar already share — lamp → words → bar, like a hanging
   scroll. The band of paper above the greeting is STRUCTURAL (the greeting
   rests on the bar by the 0-move press law; the lamp keeps its one seat at
   the top), so the shared axis is what makes the band read as sky instead of
   a missing panel. One alignment at every width: the old ≤1080px query that
   centred ONLY the narrow widths — against the seat's own left — is deleted,
   not repaired; it was the iPad's two-products bug. */
.vp-lobby { display: grid; grid-template-columns: minmax(0, 1fr); gap: 44px; align-items: center; min-height: calc(100vh - 150px); align-content: center; text-align: center; }
.vp-welcome { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 26px; align-items: center; padding: 8px 4px; }
/* The landing has no orb of its own since 2026-07-27 (the room's ensō is the
   one mark — see lobbyNode in voice-portal.js). With the orb cell gone the
   two-column track would size the greeting to `auto` and shrink-wrap it, so the
   orbless welcome says plainly that it is one column. */
.vp-welcome--noorb { grid-template-columns: minmax(0, 1fr); }
.vp-welcome-body { display: flex; flex-direction: column; gap: 16px; align-items: center; }
/* The greeting is the landing's one brush word (the Muji cut, owner pick
   2026-07-28): おかえり at a restrained display size in full ink — a warm
   word, not a headline. The romaji whispers beside it at the block's old
   0.75; the block itself no longer fades as a whole. */
.vp-greet { font-size: var(--rm-support); color: var(--color-stone); }
.vp-greet .jp { font-family: var(--font-jp); font-size: 24px; line-height: 1.2; color: var(--color-jp-text); }
.vp-greet .romaji { opacity: 0.75; }
/* (.vp-cta and .vp-shelf deleted 2026-07-28 — the landing's old call-to-action
   column and the shelf beside it; neither class is built anywhere in js/.) */

/* ------------------------------------------------------------
   THE STUDY PLAN — scroll & desk (vpj-*) + shared room bits (jy-*)
   ------------------------------------------------------------ */
.vpj { max-width: 1060px; }
.vpj-grid { display: grid; grid-template-columns: 264px minmax(0, 1fr); gap: 26px; align-items: start; margin-top: 6px; }
@media (max-width: 860px) { .vpj-grid { grid-template-columns: 1fr; } .vpj-rail { position: static; } }

/* the scroll: the whole road, always in sight.
   (.vpj-rod deleted 2026-07-29, S9's Muji cut — a 9px warm-grey bar across the
   rail's head, the darkest paint anywhere on the surface, drawn to make the
   rail read as a hanging scroll. Nobody was reading the metaphor, and it was
   spending the surface's heaviest ink on a decoration. The rail closes its own
   head with one radius all round, and the goal card takes the space back.) */
.vpj-rail { position: sticky; top: 18px; background: linear-gradient(rgba(255,255,255,0.55), rgba(250,248,243,0.4)); border: 1px solid var(--vp-line); border-radius: var(--radius-xl); padding: 0 0 16px; box-shadow: var(--shadow-rest); }
.vpj-goal { padding: 14px 16px 12px; border-bottom: 1px solid var(--vp-line); margin-bottom: 4px; }
.vpj-goal-lab { font-family: var(--font-ui); font-size: var(--rm-caption); letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-stone); opacity: 0.6; margin-bottom: 2px; }
.vpj-goal .jp { font-family: var(--font-jp); font-size: var(--rm-en); color: var(--color-jp-text); }
.vpj-goal-ro { font-family: var(--font-ui); font-style: italic; font-size: var(--rm-control); color: var(--color-romaji); margin-left: 7px; }
/* the destination in English, on its own line under the Japanese it names —
   the romaji law reaching the one place on the plan it never reached (S9) */
.vpj-goal-lead { font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone); line-height: 1.45; margin-top: 5px; }
.vpj-rail-stage { padding: 10px 16px 3px; font-family: var(--font-ui); font-size: var(--rm-caption); letter-spacing: 0.11em; text-transform: uppercase; color: var(--color-stone); opacity: 0.65; }
/* ⛔ THE TWO COLOURS THAT SURVIVED S7, and what each is allowed to mean:
   INDIGO = interactive · selection · WHERE YOU ARE. Every indigo below is one
   of those — .vpj-stone.on/.part/.here (+.mini), .vpj-dot-s.on/.part, .vpj-rung
   .hit/.cur, the .vpj-now border, .vpj-back, the .vpj-next-row hover title.
   It never dresses a static label, a stat, or a machine state again.
   GOLD = EARNED, and only earned — .vpj-stone.done, .vpj-dot-s.done,
   .vpj-pip.s-yours, .vpj-ms .st.yours. Plus the ONE ember (see .vp-srow-dot):
   a small amber-deep mark meaning "something waits for YOU". Text never
   carries the ember hue. (Owner pick B, 2026-07-28.) */
.vpj-stone { width: 100%; display: flex; align-items: center; gap: 9px; padding: 5px 16px; background: none; border: none; cursor: pointer; text-align: left; font-family: var(--font-ui); font-size: var(--rm-support); }
#voicePortalOverlay .vpj-stone:hover { background: rgba(255,255,255,0.7); }
.vpj-stone.sel { background: rgba(255,255,255,0.85); }
.vpj-stone .dot { flex: none; width: 10px; height: 10px; border-radius: 50%; border: 1.5px solid var(--vp-line-strong); background: var(--color-paper); }
.vpj-stone.on .dot { background: var(--color-accent); border-color: var(--color-accent); }
.vpj-stone.done .dot { background: var(--color-gold); border-color: var(--color-gold); }
.vpj-stone.part .dot { border-color: var(--color-accent); background: linear-gradient(90deg, var(--color-accent) 0 50%, transparent 50%); }
.vpj-stone .t { flex: 1; color: var(--color-stone); line-height: 1.35; }
.vpj-stone.on .t { color: var(--color-ink); font-weight: 600; }
.vpj-stone.done .t { color: var(--color-ink); }
.vpj-stone .mini { font-family: var(--font-ui); font-size: var(--rm-caption); color: var(--color-stone); opacity: 0.7; font-variant-numeric: tabular-nums; }
.vpj-stone.on .mini { color: var(--color-accent); opacity: 0.9; }
.vpj-stone.books .dot { border-style: dashed; }
/* ⛔ WHERE YOU ARE IS A RING, NOT A FILL (S9). The fill says what the room IS —
   gold for banked, indigo for a fresh chapter — and the ring says where the
   plan is pointing tonight. They are different questions, so they are
   different marks: a banked chapter you are standing in keeps every bit of
   the gold it earned and wears the indigo around it. This is the same halo
   .vpj-rung.cur uses, which is already the surface's you-are-here idiom. */
.vpj-stone.here .dot { box-shadow: 0 0 0 3px rgba(61,90,128,0.14); }
.vpj-stone.here .t { color: var(--color-ink); font-weight: 600; }
.vpj-stone.here .mini { color: var(--color-accent); opacity: 0.9; }
.vpj-dot-s { width: 10px; height: 10px; border-radius: 50%; border: 1.5px solid var(--vp-line-strong); background: var(--color-paper); flex-shrink: 0; }
.vpj-dot-s.on { background: var(--color-accent); border-color: var(--color-accent); }
/* the legend's you-are-here key: the RING, on the neutral paper dot — the one
   part of the marker that never changes with the fill underneath it (S9) */
.vpj-dot-s.here { box-shadow: 0 0 0 3px rgba(61,90,128,0.14); }
.vpj-dot-s.done { background: var(--color-gold); border-color: var(--color-gold); }
.vpj-dot-s.part { border-color: var(--color-accent); background: linear-gradient(90deg, var(--color-accent) 0 50%, transparent 50%); }
.vpj-dot-s.books { border-style: dashed; }
.vpj-dot-s.ahead { opacity: 0.5; }
/* the ember on the rail: full opacity since S7 — the hue now carries the
   meaning, so it must read at the same strength as .vp-srow-dot */
.vpj-dot-s.nudge { background: var(--color-amber-deep); border-color: var(--color-amber-deep); opacity: 1; }

/* the desk: this week's plan, or the opened chapter */
.vpj-desk { min-width: 0; }
.vpj-sum { display: flex; justify-content: space-between; gap: 18px; flex-wrap: wrap; font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone); margin-bottom: 16px; }
.vpj-pace { font-style: italic; opacity: 0.8; }
/* an eyebrow is a LABEL — "THIS WEEK", "YOUR GOAL" — and a label is not a
   control. Since S7 it wears the room's one eyebrow recipe (.vp-eyebrow,
   .jy-sec-label): stone at 0.6. (.g deleted with it — a gold eyebrow said
   "earned" over a heading nobody earned.) */
.vpj-eyebrow { font-family: var(--font-ui); font-size: var(--rm-caption); font-weight: 600; letter-spacing: 0.13em; text-transform: uppercase; color: var(--color-stone); opacity: 0.6; }
.vpj-now { background: var(--color-card-bright); border: 1px solid rgba(61,90,128,0.16); border-radius: var(--radius-lg); box-shadow: var(--shadow-rest); padding: 18px 20px; }
.vpj-aim { font-family: var(--font-ui); font-size: var(--rm-en); line-height: 1.6; color: var(--color-ink); margin-top: 7px; }
.vpj-aim .jp { font-family: var(--font-jp); color: var(--color-jp-text); }
.vpj-hunt { font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone); margin-top: 9px; }
/* the hunted phrase is emphasis inside a sentence — ink weight, not a link */
.vpj-hunt b { color: var(--color-ink); font-weight: 600; }
.vpj-next { margin-top: 14px; }
.vpj-next-row { width: 100%; display: flex; gap: 12px; align-items: flex-start; padding: 9px 2px; border: none; border-top: 1px solid var(--vp-line); background: none; cursor: pointer; text-align: left; font-family: var(--font-ui); font-size: var(--rm-support); }
.vpj-next .vpj-next-row:first-of-type { border-top: none; }
#voicePortalOverlay .vpj-next-row:hover .ttl { color: var(--color-accent); }
/* the THEN numbers are ordinals, not earnings (S7) */
.vpj-next-row .n { font-size: var(--rm-control); font-weight: 650; color: var(--color-stone); margin-top: 2px; font-variant-numeric: tabular-nums; }
.vpj-next-row .ttl { font-weight: 600; color: var(--color-ink); }
.vpj-next-row .why { font-size: var(--rm-support); color: var(--color-stone); margin-top: 1px; line-height: 1.5; }
.vpj-next-row .why .jp { font-family: var(--font-jp); color: var(--color-jp-text); }

/* the chapter card: milestones · ingredients · how solidly */
.vpj-chap { background: var(--color-card); border: 1px solid var(--vp-line); border-radius: var(--radius-xl); box-shadow: var(--shadow-rest); padding: 20px 22px; }
.vpj-back { display: block; background: none; border: none; cursor: pointer; padding: 0 0 12px; font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-accent); text-align: left; }
#voicePortalOverlay .vpj-back:hover { text-decoration: underline; }
.vpj-sec { margin-top: 18px; }
.vpj-pip { display: inline-block; flex: none; width: 9px; height: 9px; border-radius: 50%; }
.vpj-pip.s-no { box-shadow: inset 0 0 0 1.5px var(--vp-line-strong); }
/* half-filled = said once. Stone since S7: "once" is a count the machine kept,
   not a place you are standing (which is what indigo means on this rail). */
.vpj-pip.s-once { border: 1.5px solid var(--color-stone); background: linear-gradient(90deg, var(--color-stone) 0 50%, transparent 50%); }
.vpj-pip.s-yours { background: var(--color-gold); }
.vpj-ms { display: flex; gap: 10px; align-items: flex-start; padding: 7px 0; border-bottom: 1px solid var(--vp-line); font-family: var(--font-ui); font-size: var(--rm-support); }
.vpj-ms:last-child { border-bottom: none; }
.vpj-ms .vpj-pip { margin-top: 5px; }
.vpj-ms .t { flex: 1; color: var(--color-ink); line-height: 1.5; }
.vpj-ms .st { font-size: var(--rm-control); color: var(--color-stone); white-space: nowrap; margin-top: 2px; }
.vpj-ms .st.once { color: var(--color-stone); }
.vpj-ms .st.yours { color: var(--color-amber-deep); font-weight: 600; }
.vpj-ms-q { margin-top: 3px; }
.vpj-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.vpj-chip { display: inline-flex; align-items: baseline; gap: 6px; border: 1px solid var(--vp-line); border-radius: var(--radius); padding: 4px 10px; background: rgba(255,255,255,0.5); font-size: var(--rm-support); }
.vpj-chip .jp { font-family: var(--font-jp); color: var(--color-jp-text); }
.vpj-chip .ro { font-family: var(--font-ui); font-style: italic; font-size: var(--rm-control); color: var(--color-romaji); }
/* the JLPT tag is a measure the machine read off, not a thing you won (S7) */
.vpj-chip .lv { font-family: var(--font-ui); font-size: var(--rm-caption); font-weight: 600; letter-spacing: 0.06em; color: var(--color-stone); }
.vpj-words { font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone); margin-top: 8px; }
.vpj-ladder { display: flex; margin-top: 4px; }
.vpj-rung { flex: 1; position: relative; text-align: center; font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone); opacity: 0.7; padding-top: 16px; }
.vpj-rung::before { content: ''; position: absolute; top: 5px; left: 0; right: 0; height: 1.5px; background: var(--vp-line-strong); }
.vpj-rung:first-child::before { left: 50%; }
.vpj-rung:last-child::before { right: 50%; }
.vpj-rung::after { content: ''; position: absolute; top: 1px; left: 50%; margin-left: -4.5px; width: 9px; height: 9px; border-radius: 50%; background: var(--vp-line-strong); }
.vpj-rung.hit { opacity: 0.9; }
.vpj-rung.hit::after { background: var(--color-accent); }
.vpj-rung.cur { color: var(--color-accent); opacity: 1; font-weight: 600; }
.vpj-rung.cur::after { background: var(--color-accent); box-shadow: 0 0 0 3px rgba(61,90,128,0.14); }

/* the JLPT drawer — counts live here, never on the first screen */
.vpj-measures { margin-top: 22px; }
.vpj-measures summary { list-style: none; cursor: pointer; display: flex; align-items: baseline; gap: 8px; font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone); }
.vpj-measures summary::-webkit-details-marker { display: none; }
/* the fold mark is a 12px stroke chevron now (2026-07-28), so no font-size —
   and `align-self` because the summary aligns on the BASELINE and a span whose
   only child is a block-level svg has none worth aligning to */
.vpj-measures .tri { transition: transform 0.15s; align-self: center; }
.vpj-measures .tri svg { display: block; }
.vpj-measures[open] .tri { transform: rotate(90deg); }
.vpj-meas-body { margin-top: 10px; padding: 14px 16px; background: var(--color-card); border: 1px solid var(--vp-line); border-radius: var(--radius-lg); }
.vpj-meter { margin: 8px 0; }
.vpj-meter .lab { display: flex; justify-content: space-between; gap: 14px; font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone); margin-bottom: 4px; }
.vpj-meter .bar { height: 5px; border-radius: var(--radius-full); background: var(--vp-line); overflow: hidden; }
/* a filled meter is a READOUT in a drawer, not something to press: ink at the
   0.68 stone the room uses for graphics that must clear 3:1 (S7) */
.vpj-meter .bar i { display: block; height: 100%; background: rgba(87,83,78,0.68); border-radius: var(--radius-full); }
.vpj-meas-body .note { font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone); opacity: 0.65; margin-top: 6px; }

.jy-sec-label { font-family: var(--font-ui); font-size: var(--rm-caption); letter-spacing: 0.13em; text-transform: uppercase; color: var(--color-stone); opacity: 0.55; margin-bottom: 10px; }

/* "worth revisiting" — the ember, said properly (S7): the DOT carries the hue,
   the words stay ink. Terracotta leaves the portal with this rule; one room,
   one attention colour. (The token is app-wide and untouched.) */
.jy-nudge { display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0; font-family: var(--font-ui); font-size: var(--rm-caption); font-weight: 500; color: var(--color-stone); }
.jy-nudge .d { width: 6px; height: 6px; border-radius: 50%; background: var(--color-amber-deep); opacity: 1; }

.jy-legend { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; margin-top: 24px; }
.jy-key { display: inline-flex; align-items: center; gap: 7px; font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone); opacity: 0.75; }

.jy-det { padding: 4px 0 10px; }
.jy-det-line { font-family: var(--font-ui); font-size: var(--rm-support); line-height: 1.6; color: var(--color-stone); max-width: 58ch; }
.jy-det-line .jp { font-family: var(--font-jp); color: var(--color-jp-text); }
.jy-det-quote { display: flex; align-items: baseline; gap: 10px; margin-top: 8px; }
.jy-proof-mark { font-family: var(--font-ui); font-size: var(--rm-caption); letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-stone); opacity: 0.5; white-space: nowrap; }
.jy-det-sample { font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone); opacity: 0.75; margin-top: 6px; font-style: italic; }
.jy-det-warmth { margin-top: 8px; }

.jy-now-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.jy-now-eyebrow { font-family: var(--font-ui); font-size: var(--rm-caption); letter-spacing: 0.13em; text-transform: uppercase; color: var(--color-stone); opacity: 0.6; }
.jy-now-title { font-family: var(--font-ui); font-size: 20px; font-weight: 600; color: var(--color-ink); margin-top: 4px; }
.jy-now-anchor { margin-top: 6px; }
.jy-now-anchor .k { font-size: 17px; }
.jy-proof { margin-top: 18px; }
.jy-proof-item { display: flex; gap: 12px; align-items: flex-start; padding: 9px 0; border-top: 1px solid var(--vp-line); }
.jy-proof-item:first-of-type { border-top: 0; }
.jy-pchip { flex-shrink: 0; font-family: var(--font-ui); font-size: var(--rm-caption); font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; padding: 3px 8px; border-radius: var(--radius-full); margin-top: 2px; }
/* the same graded neutrals as .vp-chip above — one scale, said once (S7) */
.jy-pchip.b-solid   { color: var(--color-ink); background: rgba(44,44,44,0.05); }
.jy-pchip.b-forming { color: var(--color-stone); background: rgba(140,133,120,0.1); }
.jy-pchip.b-growing { color: var(--color-stone); background: rgba(140,133,120,0.1); }
.jy-pchip.b-watch { color: var(--vp-watching); background: rgba(140,133,120,0.1); }
.jy-proof-body { min-width: 0; }
.jy-proof-name { font-family: var(--font-ui); font-size: var(--rm-support); font-weight: 600; color: var(--color-ink); }
.jy-proof-read { font-family: var(--font-ui); font-size: var(--rm-support); line-height: 1.55; color: var(--color-stone); margin-top: 3px; max-width: 54ch; }
.jy-proof-q { margin-top: 5px; }
.jy-proof-q .k { font-size: var(--rm-en); }
/* (.jy-proof-said + its .g deleted 2026-07-28. Every other jy-proof-* class is
   written literally in voice-portal.js:1243-1262 and there is no
   `jy-proof-${…}` construction anywhere, so this one is simply unreachable.) */

/* sky */

/* ribbon + timeline + admin + honesty */
.jy-ribbon {
  position: relative; overflow: hidden; border: 1px solid var(--vp-line); border-radius: var(--radius-lg);
  background: var(--color-card); padding: 14px 18px; margin: 20px 0 4px; display: flex; gap: 14px; align-items: stretch;
}
/* MATERIAL, not status — this is the ribbon's woven edge, a textile the card is
   made of. It keeps its warm thread through the S7 sweep (allowlisted in
   tests/css/colour_law_test.js for exactly that reason). */
.jy-ribbon-weave { flex-shrink: 0; width: 8px; border-radius: 4px; background: rgba(160, 120, 80, 0.28); }
.jy-ribbon-body { min-width: 0; }
.jy-ribbon-top { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.jy-ribbon-name { font-family: var(--font-ui); font-size: var(--rm-support); font-weight: 600; color: var(--color-ink); }
.jy-ribbon-top .vp-anchor .k { font-size: var(--rm-support); }
.jy-gold-mark { font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-amber-deep); }
.jy-ribbon-sub { font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone); opacity: 0.7; margin-top: 3px; }
.jy-ribbon-read { font-family: var(--font-ui); font-size: var(--rm-support); color: var(--color-stone); margin-top: 7px; }
.jy-ribbon-read .jp { font-family: var(--font-jp); color: var(--color-jp-text); }
.jy-timeline { margin-top: 26px; }
.jy-tl-item { display: flex; align-items: baseline; gap: 11px; padding: 7px 0; }
/* EVERY row on this shelf is earned — a can-do carried to solid, or a chapter
   banked — so every dot is gold and the bank rows are marked by the ring, not
   by a second hue. The matcha that used to sit here said "solid" in the word
   graph's relation colour, which is a different sentence entirely (S9). */
.jy-tl-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--color-gold); position: relative; top: -1px; }
.jy-tl-item.bank .jy-tl-dot { box-shadow: 0 0 0 3px rgba(201,169,110,0.16); }
.jy-tl-what { font-family: var(--font-ui); font-size: var(--rm-support); line-height: 1.5; color: var(--color-stone); }
.jy-tl-item.bank .jy-tl-what { color: var(--color-ink); font-weight: 500; }
.jy-tl-what .jp { font-family: var(--font-jp); color: var(--color-jp-text); }
.jy-tl-date { margin-left: auto; font-family: var(--font-ui); font-size: var(--rm-caption); color: var(--color-stone); opacity: 0.55; white-space: nowrap; font-variant-numeric: tabular-nums; }
/* the fold, in the words surface's own quiet hand: no border, and indented
   past the dot so the shelf keeps one left edge whether it is open or shut */
.jy-tl-more { display: block; width: 100%; text-align: left; background: none; border: none; cursor: pointer;
  font-family: var(--font-ui); font-size: var(--rm-caption); color: var(--color-stone); opacity: 0.75; padding: 8px 0 0 19px; }
#voicePortalOverlay .jy-tl-more:hover { opacity: 1; }
.jy-admin { margin-top: 30px; padding: 16px 18px; border: 1px dashed var(--vp-line-strong); border-radius: var(--radius-lg); background: rgba(44,44,44,0.015); }
/* the teacher's register opens on a tap here too (S12) — one pattern on every
   surface it appears on, and the box is closed on every visit */
.jy-admin.closed > *:not(.jy-admin-head) { display: none; }
.jy-admin.closed .jy-admin-head { margin-bottom: 0; }
.jy-admin-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 4px; }
/* the head is a button when it guards a fold: browser chrome off, dress kept */
button.jy-admin-head { width: 100%; text-align: left; background: none; border: 0; padding: 0; cursor: pointer; }
button.jy-admin-head:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
#voicePortalOverlay button.jy-admin-head:hover .jy-admin-label { opacity: 0.75; }
.jy-admin-label { font-family: var(--font-ui); font-size: var(--rm-support); font-weight: 600; color: var(--color-ink); }
.jy-admin-sub { font-family: var(--font-ui); font-size: var(--rm-control); color: var(--color-stone); opacity: 0.6; }
.jy-admin-item { padding: 12px 0 2px; border-top: 1px solid var(--vp-line); margin-top: 10px; }
.jy-admin-item:first-of-type { border-top: 0; margin-top: 0; }
.jy-admin-top { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.jy-admin-name { font-family: var(--font-ui); font-size: var(--rm-support); font-weight: 600; color: var(--color-ink); }
.jy-admin-read { font-family: var(--font-ui); font-size: var(--rm-support); line-height: 1.6; color: var(--color-stone); margin-top: 6px; max-width: 64ch; }
.jy-admin-read .jp, .jy-admin-dir .jp { font-family: var(--font-jp); color: var(--color-jp-text); }
.jy-admin-dir { font-family: var(--font-ui); font-size: var(--rm-support); line-height: 1.55; color: var(--color-stone); opacity: 0.85; margin-top: 5px; }
.jy-admin-dir b { font-weight: 600; color: var(--color-ink); opacity: 0.75; }

/* ⛔ .vp-orb-fly RETIRED 2026-07-27 with the lantern it flew. It animated the
   landing's orb into the room's orb across the seam — a seam that stopped
   existing when the room BECAME the landing (S3R), leaving the rule with zero
   JS references for a month. Its removal is part of the same ruling: the portal
   does not draw room objects, so it has no orb to fly. */

/* ------------------------------------------------------------
   REDUCED MOTION + DESKTOP-ONLY
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .vp-surface { animation: none; }
  .vp-record-in { animation: none; }
  .vpl-sheet { animation: none; }
  #voicePortalOverlay { transition: none; transform: none; }
}
@media (max-width: 768px) {
  #voicePortalOverlay { display: none !important; }
}

/* ── THE ECHO — what just happened, and the way back (2026-07-27) ───────────
   The app's grammar for a reversible act, borrowed from the grid's grade echo:
   say it once, offer Undo, leave. ⛔ Deliberately NOT a confirm dialog. A modal
   makes the learner commit before he can see anything; an undo lets him watch
   the row go and take it back — kinder, and honest about how reversible this
   really is (the transcript never leaves the disk).
   It seats at the foot of the STAGE, left of the rail's edge. Whether that
   foot is CLEAR depends on whether the room is up — see the re-seat below. */
#voicePortalOverlay .vp-echo {
  position: fixed;
  left: calc(var(--vp-rail-w, 0px) + 26px);
  bottom: 26px;
  z-index: 9605;
  display: flex;
  align-items: baseline;
  gap: 14px;
  max-width: min(420px, calc(100vw - var(--vp-rail-w, 0px) - 52px));
  padding: 11px 16px;
  border-radius: var(--rm-pop-radius);
  background: var(--color-card, #FAFAF7);
  border: 1px solid var(--rm-pop-line);
  box-shadow: var(--rm-pop-shadow);   /* the popover spec, chat-mode.css head */
  font-size: var(--rm-support);
  line-height: 1.45;
  color: var(--color-stone);
  pointer-events: auto;
  animation: vpEchoIn 0.34s var(--ease-settle, ease) both;
  transition: opacity 0.55s ease, transform 0.55s ease, left 0.34s var(--ease-settle, ease);
}
/* While the room is up, the bar owns the bottom band — 16 to 104 from the
   viewport's foot (the dock's 88px floor over the container's 16px padding).
   The echo stood at 26, inside it: at 1280-1440 its 420px cap ran under the
   bar's left hands and, being pointer-events:auto by design, swallowed ＋ and
   ♪ for the whole 8s dwell. 118 = 104 + the file's 14px breathing gap.
   On Study plan / New words the room is CLOSED (navClick → closeRoomForNav →
   exitChatMode, so body.chat-mode is off) and the foot of the stage is honestly
   clear — the base seat stands there. ⛔ `bottom` stays OUT of the transition
   list above: the echo never re-seats inside one room state. */
body.chat-mode #voicePortalOverlay .vp-echo { bottom: 118px; }
#voicePortalOverlay .vp-echo.going { opacity: 0; transform: translateY(6px); }
#voicePortalOverlay .vp-echo-say { flex: 1; }
#voicePortalOverlay .vp-echo-undo {
  flex: 0 0 auto;
  font-family: var(--font-ui);
  font-size: var(--rm-support);
  color: var(--color-accent, #3D5A80);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  transition: opacity 0.2s ease;
}
#voicePortalOverlay .vp-echo-undo:hover { opacity: 0.7; }
@keyframes vpEchoIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* ⛔ the END STATE must land, or reduce-motion leaves an echo that never
   appears (reference_reduced_motion_must_land — this file has been struck by
   exactly that before) */
@media (prefers-reduced-motion: reduce) {
  #voicePortalOverlay .vp-echo { animation: none; opacity: 1; transform: none; }
  #voicePortalOverlay .vp-echo.going { opacity: 0; }
}

/* ── ⋯ AND ITS MENU: what he may do to a night of his own (2026-07-27) ──────
   Owner, holding up Claude's own sidebar: "perhaps we need something similar."
   Same shape, same reason — the row says what the night WAS; what you may DO to
   it is one mark away instead of printed on every row.

   It replaced two words (Rename · Set aside) spelled out inline on the OPEN
   row, and it is better on the point that matters: it reaches EVERY night in
   the list, not only the one already open, so naming a session no longer costs
   you the one you were reading.

   ⛔ THE ONE THING NOT COPIED IS HOVER-ONLY REVEAL. Claude's ⋯ appears on
   hover. Hover does not exist on the iPad — a primary device here since
   2026-07-16 — so on touch that mark would simply never appear and the whole
   feature would be invisible to half the product. It fades in on pointer
   devices and stands permanently visible on touch: one honest branch, same
   design. (The repo already draws this line for entity-peek, which stands down
   entirely without hover:hover + pointer:fine.) */
#voicePortalOverlay .vp-srow { position: relative; }
#voicePortalOverlay .vp-srow-more {
  position: absolute;
  top: 6px;
  right: 4px;
  width: 24px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-stone);
  background: none;
  border: 0;
  border-radius: 5px;
  padding: 0;
  cursor: pointer;
  opacity: 0;                     /* pointer devices only — see the media query */
  transition: opacity 0.18s ease, background 0.18s ease;
}
/* ⛔ NO font-size / line-height HERE ANY MORE (2026-07-28). The mark is a 16px
   stroke SVG, sized by its own viewBox — it answers to no font, which is the
   whole reason it left the type allowlist. A font-size on this rule would say
   nothing and would be the first thing a reader tried to tune. */
/* The 44px touch floor — the house idiom (css/vocab-panel.css:621), invisible,
   inheriting its host's pointer-events, costing the layout nothing.
   The mark is 24×22 at top:6 right:4 inside the row's own box, so:
   6 + 22 + 16 = 44 tall, 16 + 24 + 4 = 44 wide — the expander reaches DOWN and
   LEFT into the row it belongs to and stops exactly at that row's own top and
   right edges. ⛔ Never past the top: the row ABOVE is a full-width click
   target and the law protects a neighbour's visible box. Down-left is the
   ⋯'s own host, and a mis-press there now costs a dismissible menu rather
   than a session opening under him. */
.vp-srow-more::after {
  content: '';
  position: absolute;
  inset: -6px -4px -16px -16px;
}
#voicePortalOverlay .vp-srow:hover .vp-srow-more,
#voicePortalOverlay .vp-srow.on .vp-srow-more,
#voicePortalOverlay .vp-srow:focus-within .vp-srow-more { opacity: 0.68; }
#voicePortalOverlay .vp-srow-more:hover,
#voicePortalOverlay .vp-srow-more:focus-visible,
#voicePortalOverlay .vp-srow[data-menu-open] .vp-srow-more {
  opacity: 1;
  background: rgba(44, 44, 44, 0.06);
}
/* ⛔ TOUCH: there is no hover to reveal it with, so it is simply there. */
@media (hover: none), (pointer: coarse) {
  #voicePortalOverlay .vp-srow-more { opacity: 0.68; }
}

/* The menu is seated in the OVERLAY, not in the row: the rail scrolls and
   clips, and a popover inside a clipped column loses its bottom half. */
#voicePortalOverlay .vp-menu {
  position: fixed;
  z-index: 9610;
  min-width: 168px;
  padding: 5px;
  border-radius: var(--rm-pop-radius);
  background: var(--color-card, #FAFAF7);
  border: 1px solid var(--rm-pop-line);
  box-shadow: var(--rm-pop-shadow);   /* the popover spec, chat-mode.css head */
  animation: vpMenuIn 0.14s ease both;
}
#voicePortalOverlay .vp-menu-item {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 7px 10px;
  border: 0;
  border-radius: 7px;
  background: none;
  font-family: var(--font-ui);
  font-size: var(--rm-support);
  color: var(--color-ink);
  text-align: left;
  cursor: pointer;
  transition: background 0.14s ease;
}
#voicePortalOverlay .vp-menu-item:hover,
#voicePortalOverlay .vp-menu-item:focus-visible {
  background: rgba(44, 44, 44, 0.05);
}
#voicePortalOverlay .vp-menu-label { flex: 1; }
/* ⛔ THE RING IS NOT SUPPRESSED HERE, AND IT NEVER WAS. This rule carried an
   `outline: none` whose comment explained that it removed nothing — the app's
   global mark was a `box-shadow: 0 0 0 4px`, not an outline, so the line was a
   fix for the wrong property left standing beside a note saying so. It is gone
   (2026-07-28), and now it would actually do damage: the portal draws the
   ROOM's 2px outline (see the focus-ring block near the top of this file), and
   this rule would blank it on the one item the keyboard path lands on.
   WHO SEES A RING: a keyboard open moves focus to the first item
   (voice-portal.js, untouched) and that item wears it; a mouse open moves no
   focus, so nothing is marked. That is the whole behaviour, unchanged.
   ⭐ Read the computed style before "fixing" a ring: outline and box-shadow
   look identical and are not the same property. */
/* ⛔ NOT RED, even though the word is now "Remove". Claude's Delete is red
   because it deletes; this takes the night out of the list and leaves the
   transcript on disk, with an Undo on screen. Dressing a reversible act as a
   destructive one is a lie told in colour, and this palette spends red on
   nothing. It reads as the quieter of the two instead. */
#voicePortalOverlay .vp-menu-item.danger { color: var(--color-stone); }
/* ⛔ IT FADES, IT DOES NOT TRAVEL (owner, 2026-07-27: "make the menu not
   shake"). This slid 3px down as it appeared — measured 414 → 417 across the
   open — which puts the item you are aiming at somewhere else by the time you
   arrive. A popover opens where it will stay. */
@keyframes vpMenuIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* …and the same ruling on the way OUT. It used to be taken off the page in one
   frame, which reads as a flinch beside a menu that arrived by fading. 0.08s is
   under the sub-100ms band this file already uses for a state cross-fade — long
   enough to be a departure, short enough that the next press never waits for
   it. ⛔ OPACITY ONLY: a menu that slides away is still a menu that moved, and
   the owner's ruling covers both directions. This `animation` REPLACES vpMenuIn
   (later declaration, higher specificity), so there is no fill-mode fight.
   The JS side kills its pointer-events and takes the node out at 90ms. */
@keyframes vpMenuOut {
  to { opacity: 0; }
}
#voicePortalOverlay .vp-menu.vp-menu-out { animation: vpMenuOut 0.08s ease both; }
/* ⛔ the END STATE must land (reference_reduced_motion_must_land) */
@media (prefers-reduced-motion: reduce) {
  #voicePortalOverlay .vp-menu { animation: none; opacity: 1; transform: none; }
  #voicePortalOverlay .vp-menu.vp-menu-out { animation: none; opacity: 0; }
}

/* the name being typed sits exactly where the name is READ, so committing it
   moves nothing and there is no second surface to keep in agreement */
#voicePortalOverlay .vp-srow-rename {
  font-family: var(--font-ui);
  font-size: var(--rm-support);
  font-weight: 500;
  line-height: 1.35;
  color: var(--color-ink);
  width: 100%;
  background: transparent;
  border: 0;
  border-bottom: 1px solid rgba(61, 90, 128, 0.45);
  padding: 0 0 2px;
  outline: none;
}
