/* diary/live.css — 即時開講 styles, owned by fe-live. Adapted from ../machi.css (mic, status
 * strip, transcript, toast) onto the 台灣風土 tokens diary.css already defines on :root. Round
 * 2026-08-16: the transcript is ONE paper-like page (.lvPage, below), not per-turn cards.
 * Every selector is scoped under #viewLive and every class/keyframe is lv-prefixed, so this
 * file can never collide with the shell or with journal.css / ask.css. */

/* ---- round-8 bottom stack -----------------------------------------------------------------------
 * diary.css :root owns the SHARED geometry — --safeB (home indicator, floored at 12px),
 * --navH (island height), --navLift, --navSafe (= safeB + navLift + navH: NOTHING may render
 * below this line) and --dockGap. This file only CONSUMES them; every token it defines is
 * --lv*, lives on #viewLive, and is keyed off the two attributes live.js publishes there:
 *   data-mic   = the verbatim micMode ('boot'|'idle'|'rec'|'reconnect'|'wait')
 *   data-phase = 'idle' | 'active' | 'stopped' | 'saved'
 * --lvNavSafe re-reads the shared token behind a fallback so this surface still clears the
 * island if it is ever served against a diary.css that predates the token block.
 * There is no viewport unit and no :has() left in this file: the flex chain
 * body -> .diaryMain -> #viewLive -> .lvPage -> .lvPageBody owns the height budget, so the
 * page is exactly (viewport - hero - status - dock reserve) and every state re-solves itself
 * (the review row lives INSIDE the page's pinned footer now, so it costs the page body, not
 * the column). If live.js fails to boot, both attributes are absent and the surface renders in
 * the BIG-dock default: over-reserved, nothing occluded. Never invert that by making the
 * compact form the default. */
#viewLive {
  --lvNavSafe: var(--navSafe, calc(max(env(safe-area-inset-bottom, 0px), 12px) + 70px));
  --lvGap: var(--dockGap, 14px);
  --lvMicSize: 116px;
  --lvMicGlyph: 54px;
  --lvDock: 150px;                 /* 116 mic + 6 gap + ~28 label pill */
  --lvDockDir: column;
  --lvDockGap: 6px;
  --lvDockReserve: calc(var(--lvNavSafe) + var(--lvGap) + var(--lvDock) + 12px);
  display: flex;
  flex-direction: column;
  /* flex 1 1 auto + min-height:min-content, and .lvPage below carries flex-basis 0. That
     trio is what makes the budget real, and all three were measured on 2026-08-15 at 390x{844,
     750,660,620,568}: with flex-shrink 0 (or with a basis-auto transcript) this column simply
     grows the DOCUMENT — the old saved state (a 330-400px card above the transcript) overflowed
     844 by 42px and, at scrollTop 0, the newest line sat under the dock. Shrinking is what fixes
     that, and min-content is the floor that keeps it honest: the page is a flex item with
     min-height:auto, so CSS refuses to squash it below head + footer + its body's own
     min-height (96px, 64px on short screens). Past that floor the column is taller than the
     viewport, #viewLive stays min-content tall, and the DOCUMENT scrolls by exactly the deficit
     with the reserve intact — verified at 660/620/568. (An engine without `min-content` drops
     the line and falls back to min-height:auto, which for a column flex item IS its min-content
     — same behaviour.) */
  flex: 1 1 auto;
  min-height: min-content;
  padding-bottom: var(--lvDockReserve);
}
/* COMPACT dock: recording (a small stop puck under the thumb) and BOTH review states (the same
   puck, now 繼續開講). One rule, four tokens — every future compact state joins this selector
   list and nothing else changes. Connection trouble (boot / wait / reconnect) deliberately does
   NOT resize the dock, it only recolours and relabels it, so mic-state and phase never fight. */
#viewLive[data-phase="stopped"],
#viewLive[data-phase="saved"] {
  --lvMicSize: 56px;
  --lvMicGlyph: 26px;
  --lvDock: 56px;
  --lvDockDir: row;
  --lvDockGap: 10px;
}
/* RECORDING occupies the island nav's vacated 56px band.  The stop puck stays dead-centre while
   its label sits in the right grid track, so lowering the control never moves the thumb target
   sideways.  --safeB + --navLift is exactly the static tabbar's bottom edge on both notched and
   non-notched phones; the 56px reserve therefore gains 28px of ruled notes without overlap. */
#viewLive[data-mic="rec"] {
  --lvGap: var(--navLift, 14px);
  --lvMicSize: 56px;
  --lvMicGlyph: 26px;
  --lvDock: 56px;
  --lvDockDir: row;
  --lvDockGap: 0px;
}
#viewLive[data-mic="rec"] .lvMicRow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--lvMicSize) minmax(0, 1fr);
}
#viewLive[data-mic="rec"] .lvMic { grid-column: 2; }
#viewLive[data-mic="rec"] .lvMicLabel {
  grid-column: 3;
  justify-self: start;
  margin-left: 8px;
  max-width: calc(50vw - 48px);
}

/* ---- mic row — 紅燈停、綠燈行: 電鍋綠 when it is your turn, seal-red while recording ----------
 * Pinned above the island tab bar (owner request 2026-08-15): thumb-reachable on phones,
 * always visible regardless of transcript scroll. z-index sits under the tabbar (20). */
#viewLive .lvMicRow {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(var(--lvNavSafe) + var(--lvGap));   /* --dockGap above the island's top edge */
  display: flex;
  flex-direction: var(--lvDockDir);    /* column when big, row when compact */
  align-items: center;
  justify-content: center;
  gap: var(--lvDockGap);
  z-index: 18;
  pointer-events: none;                /* the bar is a layout ghost … */
}
#viewLive .lvMicRow > * { pointer-events: auto; }     /* … its children stay tappable */
#viewLive .lvMic {
  position: relative;
  width: var(--lvMicSize);
  height: var(--lvMicSize);
  border: none;
  border-radius: 50%;
  background: var(--brand);
  color: var(--brand-ink);
  display: grid;
  place-items: center;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  box-shadow: 0 3px 12px rgba(43, 42, 38, 0.28), inset 0 -3px 0 rgba(0, 0, 0, 0.14);
  /* width/height/border-radius animate the big-circle → small-puck morph; the transcript's
     height is NOT animated (animating the ruled window mid-turn would drift the grid) —
     setPhase's re-pagination is what keeps the newest line visible instead */
  transition: transform 0.15s ease, background-color 0.2s ease, color 0.2s ease,
              width 0.2s ease, height 0.2s ease, border-radius 0.2s ease;
}
#viewLive .lvMic svg { width: var(--lvMicGlyph); height: var(--lvMicGlyph); }
#viewLive .lvMic:active { transform: scale(0.94); }
#viewLive .lvMic:disabled {
  background: var(--line);
  color: var(--muted);
  cursor: default;
  box-shadow: none;
}
#viewLive .lvMic.rec { background: var(--rec); color: #fffdf8; }
#viewLive .lvMic.rec::before,
#viewLive .lvMic.rec::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid var(--rec);
  animation: lvRing 1.8s ease-out infinite;
  pointer-events: none;
}
#viewLive .lvMic.rec::after { animation-delay: 0.9s; }
@keyframes lvRing {
  from { transform: scale(1);   opacity: 0.8; }
  to   { transform: scale(1.9); opacity: 0; }
}
#viewLive .lvMic.reconnect { background: var(--warn); color: var(--warn-ink); }
#viewLive .lvMicLabel {                /* floats over content: give it a legible pill */
  /* one line, always: --lvDock is a hardcoded number describing this row, so a label that
     wrapped (Dynamic Type, iOS text zoom, a longer future string) would grow the dock past
     its reserve and re-occlude the last card. This clamp is mandatory, not cosmetic. */
  white-space: nowrap;
  max-width: calc(100vw - 40px);
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 0.88rem;
  color: var(--muted);
  padding: 3px 12px;
  border-radius: 999px;
  background: rgba(255, 253, 248, 0.82);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  border: 1px solid var(--line);
}

/* ---- status strip ------------------------------------------------------------------------------ */
#viewLive .lvStatus {
  display: flex;
  flex-wrap: wrap;               /* §C.1: copy + retry pill + report link wrap on narrow phones */
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 6px 16px 2px;
  font-size: 0.84rem;
  color: var(--muted);
  text-align: center;
}
/* §C.1 waiting-state affordances: manual [再試一次]/[重新連線] pill + quiet 回報連線問題 link */
#viewLive .lvStatusBtn {
  min-height: 34px;
  padding: 0 16px;
  border: none;
  border-radius: 999px;
  background: var(--brand);
  color: var(--brand-ink);
  font-family: var(--sans);
  font-size: 0.86rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  box-shadow: 0 2px 8px rgba(43, 42, 38, 0.18);
  transition: transform 0.15s ease;
}
#viewLive .lvStatusBtn:active { transform: scale(0.96); }
#viewLive .lvStatusLink {
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: 0.78rem;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  padding: 4px 6px;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
#viewLive .lvStatusLink:hover { color: var(--brand); }
#viewLive .lvStatus .dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--faint);
}
#viewLive .lvStatus.ok .dot   { background: var(--brand); }
#viewLive .lvStatus.live .dot { background: var(--rec); animation: lvDotpulse 1.4s ease-in-out infinite; }
#viewLive .lvStatus.warn .dot { background: var(--warn); }
#viewLive .lvStatus.err .dot  { background: var(--rec); }
#viewLive .lvStatus.warn { color: var(--warn); }
#viewLive .lvStatus.err  { color: var(--rec); }
@keyframes lvDotpulse { 50% { opacity: 0.35; } }

/* ---- THE PAGE — one recording session = one paper-like page written live ------------------------
 * Owner brief 2026-08-16: 「你一邊講，這一頁一邊被寫下來」. One large paper container; finalized
 * text is dark ink, the ONE forming partial is lighter brand-green with a blinking caret, finals
 * append INLINE (no cards, no repeated timestamps, no per-sentence pencils), a paragraph opens only
 * after a real pause. "Modern journal", not "school notebook": the paper cues are a faint LEFT
 * MARGIN LINE (--lvMarginX) and, since round 9, FAINT HORIZONTAL RULES on the ink only.
 * How the rules stay honest (owner mockup 2026-08-16, "make it look like a diary"): the ruled
 * grid is painted on .lvInk ALONE — never on .lvPageBody, whose children above the ink (the kai
 * daily question, which wraps to 1 or 2 lines, and the idle hint, which comes and goes) have
 * content-dependent heights and would slide the grid origin. Because the ruled box is .lvInk,
 * the daily question, the edit bar's chrome and the closing lines all sit OUTSIDE the grid and
 * keep the typography the owner asked to leave alone; only the transcript is quantised, and it
 * is quantised structurally: .lvInk's line-height is a LENGTH (--lvLine) and the background tile
 * is the SAME length, so one rule == one visual line at every width and no descendant font-size
 * can drift the two apart. One rule per VISUAL line, never per ASR segment (segments stay inline
 * spans); a paragraph break costs exactly one blank ruled line. The typography of the ink is the
 * old card text, byte for byte
 * (clamp(1.25rem,4.5vw,1.6rem) / 1.6 / 500); the only typographic change is that the partial
 * now inherits it (an inline partial cannot be a different size without the line box jumping
 * on every finalize) and the daily question reads in brand green (owner mockup: green kai).
 * Height budget: body -> .diaryMain -> #viewLive -> .lvPage (flex 1 1 0px) -> .lvPageBody
 * (flex 1 1 0px) -> .lvPaper (flex 1 1 0px) — same trio the old transcript pane used, so the
 * paper is exactly (viewport - hero - status - dock reserve) and shrinks to head + 96px body +
 * footer before the page-scroll fallback kicks in. No viewport unit, no :has().
 * Round 13 (翻頁): the body STOPPED being a scroll region — it clips, and the sheet inside it
 * turns one page at a time. Nothing about the height budget changed; what changed is that the
 * leftover height is now a WINDOW onto the ink instead of a viewport into a scroller. */
#viewLive {
  --lvMarginX: 22px;                 /* the margin line's axis, from the paper's left border */
  --lvTextX: 34px;                   /* text inset from the paper's left border */
  --lvInkLive: rgba(28, 107, 84, 0.72);   /* forming ink = brand with alpha (no new hue) */
  /* ruled paper. --lvFs is VERBATIM the clamp .lvPageBody already shipped; it exists as a token
     only so the rule pitch can be the SAME expression as the line box. --lvLine is therefore
     both the ruled area's line-height and its background tile height: 32px on every phone
     (the clamp MIN binds below 444px), fluid to 40.96px at ≥569px. NEVER hardcode 28/32/40 —
     a fixed period drifts a full line off the text within 8 lines on a tablet.
     --lvRule is --faint (#736a5a) at 9% alpha ≈ 1/2.7 the contrast of a --line hairline, i.e.
     deliberately weaker than the vertical margin line. --lvRule0 is the same hue at zero alpha
     (not the keyword `transparent`) so no engine composites a grey tinge at the hard stop.
     Both hardcode --faint's hex: re-tone --faint and you must re-tone these by hand. */
  --lvFs: clamp(1.25rem, 4.5vw, 1.6rem);
  --lvLine: calc(var(--lvFs) * 1.6);
  --lvRule: rgba(115, 106, 90, 0.09);
  --lvRule0: rgba(115, 106, 90, 0);
  /* 翻頁 (round 13). The page no longer scrolls — it TURNS. Duration/easing are the owner's
     spec: 260 ms inside the 200–300 band, and a NAMED decelerating curve (the page is thrown
     and settles) rather than the `ease` keyword, which accelerates out of the gate. */
  --lvFlipMs: 260ms;
  --lvFlipEase: cubic-bezier(0.20, 0.70, 0.20, 1);
  /* The ruled sheet hangs this far LEFT of the text column so its rules reach the vertical margin
     line. .lvInk used to carry the whole trick alone (negative margin + equal padding) because
     nothing between it and the page clipped; now .lvPaper and .lvSheetClip both clip, so each box
     between the text column and the ink repeats the pull. Every one of them is width-neutral —
     the text column is byte-for-byte where it always was. */
  --lvPull: calc(var(--lvTextX) - var(--lvMarginX));
}
#viewLive .lvPage {
  position: relative;
  flex: 1 1 0px;
  display: flex;
  flex-direction: column;
  margin-top: 10px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 1px 3px rgba(43, 42, 38, 0.05);
}
#viewLive .lvPage::before {          /* the one paper cue: a faint left margin line */
  content: "";
  position: absolute;
  top: 14px;
  bottom: 14px;
  left: var(--lvMarginX);
  width: 1px;
  background: var(--line);
  pointer-events: none;
  /* the margin line paints OVER the ruled sheet, exactly as it did when the ink was in flow and
     this positioned pseudo outranked it. Without this, the 10 crossings each shift ~11 levels. */
  z-index: 2;
}

/* header: ONE timestamp (date always, ・上午/下午 h:mm from session start) + ONE 修改 control */
#viewLive .lvPageHead {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 40px;
  padding: 6px 8px 0 var(--lvTextX);
}
#viewLive .lvPageDate {
  font-family: var(--sans);
  font-size: 0.84rem;
  letter-spacing: 0.06em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
#viewLive .lvPageEdit {
  min-height: 36px;
  padding: 0 12px;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--brand);
  font-family: var(--sans);
  font-size: 0.9rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
#viewLive .lvPage.editing .lvPageEdit { background: var(--brand); color: var(--brand-ink); }

/* body: the CLIP HOST of the paged sheet (it does not scroll — see 翻頁 below). The ink
   typography lives HERE (was .lvUtt .text) — every segment inherits it, so the forming partial
   and the finalized text share one line box. It is a flex COLUMN so .lvPaper takes the leftover
   height and the ink carries its rules to the bottom of the page (an empty ruled sheet is what
   makes it read as a diary you are writing into).
   line-height stays a RATIO here on purpose: the daily question, the hint and the closing lines
   live outside the ruled area and must keep the type they ship. NOTE: a flex container does not
   collapse its children's margins. Rendered spacing is identical today only because every
   adjacent pair already has a 0 on one side (daily 14/hint 0, hint 10/ink 0, ink 0/saveDone 18);
   any margin added here in future ADDS where it used to collapse. */
#viewLive .lvPageBody {
  position: relative;                /* the ghost's containing block during a 翻頁 */
  flex: 1 1 0px;
  min-height: 96px;
  overflow: hidden;                  /* round 13: NOT a scroll region any more — pages turn */
  display: flex;
  flex-direction: column;
  padding: 4px 16px 14px var(--lvTextX);
  font-size: var(--lvFs);
  line-height: 1.6;
  font-weight: 500;
  color: var(--text);
  overflow-wrap: anywhere;
  text-align: left;
  /* the 3D depth the departing page turns in. It lives HERE, not on .lvPaper, because the
     ghost is a child of the body (see .lvPaper below): .lvPaper stays permanently clipped so
     the INCOMING sheet can never hang its ink outside the card while it slides in from +16%. */
  perspective: 1100px;
  perspective-origin: 50% 50%;
}
/* the sheet's non-ruled children never flex: only .lvSheetClip grows */
#viewLive .lvDaily,
#viewLive .lvHint,
#viewLive .lvSaveDone { flex: none; }

/* ---- 翻頁: the paged view over the ONE ink ---------------------------------------------------
   Pagination is a VIEW, never a re-parenting: the DOM stays one .lvInk with every p.lvPara as
   its children (turns, finals, offsets, tier-2 re-layout, editing and save all key on that), and
   a page is simply a translateY of the ink inside .lvSheetClip. Page boundaries are paragraph-only;
   an oversized paragraph stays intact and gets the narrow vertical-review exception below.
   .lvPaper is the clip host and never unclips: it is what keeps the incoming sheet's overhang
   (its from-state is translateX(±16%)) inside the card. Only the GHOST escapes, and it escapes
   through .lvPageBody / .lvPage, which go overflow:visible for the 260 ms of the turn. */
#viewLive .lvPaper {
  position: relative;
  flex: 1 1 0px;
  min-height: calc(3 * var(--lvLine));
  overflow: hidden;                  /* clips the incoming sheet's ±16% overhang, always */
  margin-left: calc(-1 * var(--lvPull));
  padding-left: var(--lvPull);
}
#viewLive .lvSheet,
#viewLive .lvGhost {
  position: absolute;
  display: flex;
  flex-direction: column;
  padding-left: var(--lvPull);       /* … and gives the text column back to the sheet's own rows */
  transform-origin: 0 50%;
  /* even the tiny edge lift can flash a backface on some WebKit builds */
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
#viewLive .lvSheet { inset: 0; z-index: 1; }
/* THE ruled window: one page's worth of the ink, clipped to the current page (see --lvPull for
   why this box repeats the margin-line offset). */
#viewLive .lvSheetClip {
  position: relative;
  flex: 1 1 0px;
  overflow: hidden;
  margin-left: calc(-1 * var(--lvPull));
  padding-left: var(--lvPull);
}
/* A paragraph taller than the usable sheet is the sole vertical-scroll case. It remains one
   semantic page, so the next horizontal turn still begins at a paragraph boundary. Scrollbars are
   visually suppressed to keep the paper treatment; touch/wheel/keyboard scrolling still works. */
#viewLive .lvSheetClip.lvOverflowPage {
  overflow-y: auto;
  overscroll-behavior-y: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
#viewLive .lvSheetClip.lvOverflowPage::-webkit-scrollbar { display: none; }
/* Once a finalized paragraph has opened a page during speech, its first sentence is a semantic
   fence. display:block makes that fence survive a wider rewrap instead of collapsing onto the
   previous sentence's last visual line. */
#viewLive .lvSeg.lvPageStart { display: block; }
#viewLive .lvSeg.lvPageStart.gap::before { content: none; }
/* the departing page: a clone of .lvSheet, inert, positioned over the paper by JS. It MUST be
   opaque — .lvSheet itself is transparent (the card carries the surface), so without this the
   live page reads straight through the page that is leaving. One narrow, one-sided shadow along
   the spine edge; no curl, no gradient overlay, no second layer. */
#viewLive .lvGhost {
  z-index: 2;
  background: var(--surface);
  box-shadow: 10px 0 20px -8px rgba(43, 42, 38, 0.22);
  pointer-events: none;              /* belt and braces: the clone also carries [inert] */
}
#viewLive .lvGhost.back { box-shadow: -10px 0 20px -8px rgba(43, 42, 38, 0.22); }
#viewLive .lvGhost.back { transform-origin: 100% 50%; }
/* will-change rides on .lvAnim ALONE and is removed with it: a permanently promoted layer is
   what leaves iOS CJK text soft after the motion ends. */
#viewLive .lvSheet.lvAnim,
#viewLive .lvGhost.lvAnim {
  transition: transform var(--lvFlipMs) var(--lvFlipEase),
              opacity var(--lvFlipMs) var(--lvFlipEase);
  will-change: transform, opacity;
}
#viewLive .lvPage.lvFlipping,
#viewLive .lvPage.lvFlipping .lvPageBody { overflow: visible; }
#viewLive .lvPage.lvFlipping::before { opacity: 0; }   /* no margin line beside the flying page */
#viewLive .lvPaper.lvFlipping { pointer-events: none; }  /* no editor can open mid-flight */
/* Active hypothesis: a dedicated two-rule window, never a child of .lvInk. Finalized text pays
   for these two lines in JS while recording, so the partial cannot clip or repaginate the diary. */
#viewLive .lvPartialSlot {
  position: absolute;
  z-index: 3;
  left: 0;
  right: 0;
  bottom: 0;
  height: calc(2 * var(--lvLine));
  overflow: hidden;
  display: flex;
  align-items: flex-end;
  padding-left: var(--lvPull);
  line-height: var(--lvLine);
  color: var(--lvInkLive);
  background-color: var(--surface);
  background-image: linear-gradient(to bottom,
    var(--lvRule) 0, var(--lvRule) 1px, var(--lvRule0) 1px, var(--lvRule0) 100%);
  background-size: 100% var(--lvLine);
  pointer-events: none;
}
#viewLive .lvPartialSlot[hidden] { display: none; }
#viewLive .lvPartialText {
  display: block;
  width: 100%;
  max-height: calc(2 * var(--lvLine));
  overflow: hidden;
}
#viewLive .lvPartialText::after {
  content: "";
  display: inline-block;
  width: 0.09em;
  height: 1.05em;
  margin-left: 0.12em;
  vertical-align: -0.15em;
  background: var(--brand);
  animation: lvCaret 1s steps(1) infinite;
}

/* ---- /mvp continuous-scroll experiment ---------------------------------------------------------
   The owner retired page turns on the MVP after small phones exposed brittle, over-frequent cuts.
   live.js adds .lvScrollMode only for the /mvp document path.  The paper keeps one height budget,
   while its ruled transcript becomes the sole natural vertical scroller; the mic dock and page
   footer (timer / stop-state review) remain outside it and therefore fixed.  The provisional text
   stays a sibling overlay on the final two rules, never part of .lvInk. */
#viewLive.lvScrollMode .lvPageBody { perspective: none; }
#viewLive.lvScrollMode .lvSheetClip {
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
#viewLive.lvScrollMode .lvSheetClip::-webkit-scrollbar { display: none; }
#viewLive.lvScrollMode .lvInk {
  position: relative;
  top: auto;
  left: auto;
  right: auto;
  width: 100%;
  min-height: 100%;
  transform: none !important;
}
/* One normal pitch already closes the ruled sheet.  While recording, two additional pitches keep
   the newest finalized line above the fixed provisional slot instead of underneath it. */
#viewLive.lvScrollMode[data-phase="active"] .lvInk {
  padding-bottom: calc(3 * var(--lvLine));
}
#viewLive.lvScrollMode .lvSeg.lvPageStart { display: inline; }
#viewLive.lvScrollMode .lvSeg.lvPageStart.gap::before { content: " "; }
#viewLive.lvScrollMode .lvPager,
#viewLive.lvScrollMode .lvGhost { display: none !important; }

/* Manual review is sticky: reaching the same pixel by layout does not silently re-arm following.
   This compact, explicit return sits above the two-line provisional slot and never enters ink. */
#viewLive.lvScrollMode .lvTailBtn {
  position: absolute;
  right: 12px;
  bottom: calc(2 * var(--lvLine) + 10px);
  z-index: 8;
  min-height: 36px;
  padding: 0 13px;
  border: 1px solid rgba(28, 107, 84, 0.28);
  border-radius: 999px;
  background: rgba(255, 253, 248, 0.94);
  color: var(--brand);
  font-family: var(--sans);
  font-size: 0.82rem;
  font-weight: 700;
  box-shadow: 0 2px 10px rgba(43, 42, 38, 0.14);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
#viewLive.lvScrollMode .lvTailBtn:active { transform: scale(0.97); }

/* Machi lives in the margin. This layer is absolute/non-flow: adding or opening a cue cannot
   change any ink measurement or page fence. */
#viewLive .lvMachiLayer {
  position: absolute;
  z-index: 5;
  inset: 0;
  pointer-events: none;
}
#viewLive .lvMachiPin {
  position: absolute;
  left: 0;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid rgba(28, 107, 84, 0.24);
  border-radius: 50%;
  background: rgba(255, 253, 248, 0.96);
  color: var(--brand);
  font-family: var(--sans);
  font-size: 0.88rem;
  line-height: 1;
  cursor: pointer;
  pointer-events: auto;
  box-shadow: 0 2px 8px rgba(43, 42, 38, 0.12);
  animation: lvMachiIn 180ms ease-out both;
}
#viewLive .lvMachiPin[hidden] { display: none; }
#viewLive .lvMachiPin.memory { font-size: 0.82rem; }
#viewLive .lvMachiPin.answer { font-weight: 700; }
#viewLive .lvMachiPin[aria-expanded="true"] { background: var(--surface-2); }
@keyframes lvMachiIn { from { opacity: 0; transform: translateY(3px); } }
#viewLive .lvMachiNote {
  position: absolute;
  left: 34px;
  right: 8px;
  max-width: 430px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: rgba(255, 253, 248, 0.98);
  color: var(--text);
  font-family: var(--sans);
  font-size: 0.9rem;
  line-height: 1.6;
  pointer-events: auto;
  box-shadow: 0 10px 28px rgba(43, 42, 38, 0.16);
}
#viewLive .lvMachiNote[hidden] { display: none; }
#viewLive .lvMachiNoteHead { display: block; color: var(--brand); margin-bottom: 3px; }
#viewLive .lvMachiNoteBody { margin: 0; }
#viewLive .lvMachiNote.answer .lvMachiNoteBody { color: var(--muted); }
/* In the continuous spoken diary, Machi writes under the sentence it heard. This is deliberately
   closer to a pencilled annotation than a chat bubble: no card, shadow, dismiss button or margin
   glyph, and the user's ruled ink remains visually dominant. */
#viewLive.lvScrollMode .lvMachiInline {
  display: block;
  box-sizing: border-box;
  max-width: min(38rem, calc(100% - 1.15rem));
  margin: 2px 0 10px 1.15rem;
  padding: 3px 0 4px 0.75rem;
  border-left: 2px solid color-mix(in srgb, var(--brand) 28%, transparent);
  color: color-mix(in srgb, var(--text) 72%, var(--brand));
  font-family: var(--sans);
  font-size: 0.86rem;
  line-height: 1.55;
  letter-spacing: 0;
  animation: lvMachiIn 180ms ease-out both;
}
#viewLive.lvScrollMode .lvMachiInline .lvMachiNoteHead {
  color: color-mix(in srgb, var(--brand) 78%, var(--text));
  font-size: 0.78rem;
  font-weight: 700;
  margin-bottom: 1px;
}
#viewLive.lvScrollMode .lvMachiInline .lvMachiNoteBody { display: block; }
#viewLive.lvScrollMode .lvMachiInline.answer .lvMachiNoteBody {
  color: color-mix(in srgb, var(--muted) 78%, var(--brand));
}
#viewLive .lvMachiNews {
  display: grid;
  gap: 2px;
  margin-top: 8px;
  padding: 8px 10px;
  border-left: 2px solid color-mix(in srgb, var(--brand) 48%, transparent);
  border-radius: 0 8px 8px 0;
  background: color-mix(in srgb, var(--brand) 5%, transparent);
  color: inherit;
  text-decoration: none;
}
#viewLive .lvMachiNews:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
#viewLive .lvMachiNewsTitle { line-height: 1.45; }
#viewLive .lvMachiNewsMeta { color: var(--muted); font-size: 0.75rem; }
#viewLive .lvMachiQuote { display: block; margin-top: 7px; color: var(--muted); }
#viewLive .lvMachiGo {
  min-height: 40px;
  margin-top: 8px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--brand);
  font: inherit;
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
#viewLive .lvSeg.lvMachiHighlight {
  background: linear-gradient(to top, rgba(222, 196, 87, 0.18) 0 36%, transparent 36% 100%);
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
#viewLive .lvSeg.lvMachiTarget {
  background: linear-gradient(to top, rgba(73, 151, 126, 0.14) 0 34%, transparent 34% 100%);
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* §C.2 daily prompt — the page's first lines, in every state (never hidden by turns) */
#viewLive .lvDaily {
  font-size: 1rem;
  font-weight: 400;
  margin: 2px 0 14px;
  border-radius: 7px;
}
/* Opening silence does not manufacture a Machi message. It softly returns the eye to the
   personalized seed already on the paper, without changing its box size or moving the scroller. */
@keyframes lvWelcomeInvite {
  from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--brand) 18%, transparent); }
  to { box-shadow: 0 0 0 7px transparent; }
}
#viewLive .lvDaily.lvSilenceInvite {
  background: color-mix(in srgb, var(--brand) 5%, transparent);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--brand) 5%, transparent);
  animation: lvWelcomeInvite 850ms ease-out 1;
}
#viewLive .lvNews {
  display: grid;
  gap: 3px;
  margin: 0 0 11px;
  padding: 8px 10px;
  border-left: 2px solid color-mix(in srgb, var(--brand) 55%, transparent);
  border-radius: 0 8px 8px 0;
  background: color-mix(in srgb, var(--brand) 5%, transparent);
  color: inherit;
  font-family: var(--sans);
}
#viewLive .lvNews[hidden],
#viewLive .lvNewsMore[hidden] { display: none; }
#viewLive .lvNewsCap {
  color: var(--brand);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
}
#viewLive .lvNewsPrimary,
#viewLive .lvNewsLink {
  display: grid;
  gap: 1px;
  border-radius: 5px;
  color: inherit;
  text-decoration: none;
}
#viewLive .lvNewsPrimary:focus-visible,
#viewLive .lvNewsLink:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
#viewLive .lvNewsTitle,
#viewLive .lvNewsLinkTitle {
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  font-size: 0.86rem;
  line-height: 1.45;
}
#viewLive .lvNewsMeta,
#viewLive .lvNewsLinkSource { color: var(--muted); font-size: 0.7rem; }
#viewLive .lvNewsMore { margin-top: 3px; border-top: 1px solid var(--line); }
#viewLive .lvNewsMoreLabel {
  min-height: 32px;
  padding-top: 6px;
  color: var(--brand);
  font-size: 0.76rem;
  font-weight: 700;
  cursor: pointer;
}
#viewLive .lvNewsLinks { display: grid; gap: 8px; padding: 2px 0 5px; }
#viewLive .lvNewsLink { padding: 2px 0 2px 8px; border-left: 1px solid var(--line); }
#viewLive .lvQuote {
  display: grid;
  gap: 4px;
  margin: 0 0 11px;
  padding: 7px 10px 8px;
  border-left: 2px solid color-mix(in srgb, var(--brand) 48%, transparent);
  border-radius: 0 8px 8px 0;
  background: color-mix(in srgb, var(--brand) 4%, transparent);
}
#viewLive .lvQuote[hidden] { display: none; }
#viewLive .lvQuoteText {
  margin: 0;
  color: inherit;
  font-family: var(--kai);
  font-size: 1rem;
  line-height: 1.6;
}
#viewLive .lvQuoteCitation {
  justify-self: start;
  color: var(--muted);
  font-family: var(--sans);
  font-size: 0.72rem;
  line-height: 1.45;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
#viewLive .lvQuoteCitation:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
#viewLive .lvQuoteCitation[hidden] { display: none; }   /* uncited spark prompt (audit #63) */
#viewLive .lvQuote[data-kind="prompt"] { border-left-style: dashed; }
#viewLive .lvDailyLabel {
  display: block;
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  color: var(--brand);               /* owner mockup: the label is green like its question */
  margin-bottom: 4px;
}
#viewLive .lvDailyQ {
  display: block;
  font-family: var(--kai);
  font-size: 1.18rem;
  line-height: 1.75;
  color: var(--brand);               /* owner mockup: green kai */
}
/* idle hint — left-aligned page text, gone the moment the page has ink */
#viewLive .lvHint {
  font-size: 1.02rem;
  font-weight: 400;
  line-height: 1.8;
  color: var(--faint);
  margin: 0 0 10px;
}

/* ---- the ruled sheet ------------------------------------------------------------------------------
   THE ruled box. Its padding box starts on the margin line's axis (margin-left pulls it left by
   exactly --lvTextX − --lvMarginX and the padding puts the text back), so the rules run from the
   vertical line to the page's right inset, exactly as in the owner's mockup.
   The tile carries a 1px rule at its TOP, so rules land at 0, L, 2L … from the padding box:
   offset 0 is the ruled sheet's top edge (the rule under the daily question), the padding-top of
   one L is the mockup's blank ruled line, and every rule below is the BOTTOM EDGE of a line box —
   which is where CJK ink naturally rests (descent + half-leading leave ~5px of air). No optical
   lift, no background-position nudge, and background-attachment stays scroll so the rules move
   with the text.
   Round 13: the ink is ABSOLUTE inside .lvSheetClip and JS owns two of its properties — the
   transform (translateY to the current page's ink-local offset, always an exact multiple of
   --lvLine so the gradient's phase is preserved) and min-height (clip height + that offset, so
   an empty page still paints its rules to the bottom edge — what flex:1 0 auto used to do).
   left/right:0 inside a clip that already carries --lvPull reproduces the old flex-stretch
   geometry exactly: same box, same width, same rules, to the pixel (A/B-verified). */
#viewLive .lvInk {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  min-height: 100%;
  /* the negative margin moved UP to .lvSheetClip (which is what clips now); left/right:0 pin the
     ink to that box, so its padding box still starts on the margin line and the padding still
     puts the text back at --lvTextX. Same two numbers, one box higher. */
  padding-left: var(--lvPull);
  padding-top: var(--lvLine);
  /* the invariant: line-height as a LENGTH is inherited as a length, so EVERY line box in the
     ruled area is exactly one pitch tall whatever font-size a descendant carries. For the ink's
     own 20px text the computed value is unchanged (1.6 × 20 = 32px), so nothing moves a pixel. */
  line-height: var(--lvLine);
  background-image: linear-gradient(to bottom,
    var(--lvRule) 0, var(--lvRule) 1px, var(--lvRule0) 1px, var(--lvRule0) 100%);
  background-size: 100% var(--lvLine);
  background-position: 0 0;
  background-repeat: repeat-y;
  background-origin: padding-box;
  background-clip: padding-box;
}
/* once the closing lines are on the page the entry is finished: the rules stop after the last
   sentence instead of pushing 收尾 to the bottom of the sheet */
#viewLive[data-phase="saved"] .lvInk {
  min-height: 0;                 /* JS drops its inline min-height in this phase (see applyPage) */
  /* one more pitch so the rule UNDER the final sentence is inside the padding box
     (background-clip would otherwise clip it) — and the page ends on a ruled line,
     the way it looks while you are still writing. */
  padding-bottom: var(--lvLine);
}

/* paragraphs + segments. Segments are inline siblings joined with NO text separator (the ASR
   emits 。，？); .gap adds a word space when the previous one did not close with punctuation.
   A paragraph break costs exactly ONE blank ruled line (was 0.55em). */
#viewLive .lvPara { margin: 0 0 var(--lvLine); }
#viewLive .lvPara:last-child { margin-bottom: 0; }
#viewLive .lvSeg { color: var(--text); transition: color 0.4s ease; }   /* the ink "dries" on finalize */
/* The inter-sentence gap must behave like a WORD SPACE, not like an indent. A margin on the
   segment's start (what this used to be) renders as a stray leading indent whenever that
   sentence happens to open a new visual line — round-8 verify: 「 我一點都不著急」. A GENERATED
   COLLAPSIBLE SPACE is removed by white-space processing at both ends of a line, exactly as a
   typed space is, so the gap can only ever show BETWEEN two sentences on the SAME line.
   It carries NO font-size of its own on purpose: inside the ruled area line-height is a LENGTH,
   so a SMALLER font in an inline box does not shrink its box — it re-centres a shorter content
   box in the same 32px leading and hangs ~2px lower than the strut, which grew the line box by
   2px and drifted every rule below it (measured, Chrome 390px). Same font = same strut = no
   drift; the space renders at the font's own word space, which is what it is. */
#viewLive .lvSeg.gap::before { content: " "; }
#viewLive .lvSeg[data-fidx] {
  cursor: pointer;
  -webkit-tap-highlight-color: rgba(28, 107, 84, 0.10);
}
#viewLive .lvSeg[data-fidx]:active { background: var(--surface-2); border-radius: 6px; }
#viewLive .lvSeg.live { color: var(--lvInkLive); }
#viewLive .lvSeg.live::after {       /* the caret: still forming */
  content: "";
  display: inline-block;
  width: 0.09em;
  height: 1.05em;
  margin-left: 0.12em;
  vertical-align: -0.15em;
  background: var(--brand);
  animation: lvCaret 1s steps(1) infinite;
}
@keyframes lvCaret { 50% { opacity: 0; } }

/* edit mode (header 修改): every correctable sentence shows a dotted underline; the one being
   edited becomes the editor itself (contenteditable) on a celadon slab */
#viewLive .lvPage.editing .lvSeg[data-fidx] {
  text-decoration: underline dotted;
  text-decoration-color: rgba(28, 107, 84, 0.5);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}
#viewLive .lvPage .lvSeg.lvEditing[data-fidx] {   /* (1,4,0), later: beats the edit-mode underline */
  background: var(--surface-2);
  border-radius: 6px;
  /* NO SPREAD. A spread grows the slab vertically too, and on ruled paper that is not a free
     choice: the rule sits half-a-leading below the line's content box, and half-leading is a FONT
     METRIC — measured 2.00px with PingFang TC / Noto Sans TC / Songti TC / the missing-font
     fallback at --lvLine 32, i.e. even a 2px spread lands exactly ON the rule and erases it under
     the sentence being edited. Two horizontal-only copies give the same padded look sideways with
     ZERO vertical growth, so the rules run through the editor on every font. (journal.css's
     .jrTurn.jrNow wash is the same fix, for the same reason — keep the two in step.) */
  box-shadow: 3px 0 0 var(--surface-2), -3px 0 0 var(--surface-2);
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  outline: none;
  text-decoration: none;
  color: var(--text);
  caret-color: var(--brand);
  cursor: text;
}
/* the edit bar is a sibling of the paragraphs INSIDE the ruled ink, so it has to pay for its own
   space in whole ruled lines or everything below it drifts off the grid: 2 lines of box + 1 line
   of margin = 3 × --lvLine. It also opts back out of the inherited line-height LENGTH — 1.6 on
   0.72rem restores its shipped 18.4px line box. (min-height, not height: if the bar's content
   ever outgrows 2 lines it grows by content and the grid drifts below it, but that is edit mode
   only and transient — clipping the buttons would be worse.) */
#viewLive .lvEditBar {               /* the ONLY place a segment timestamp is shown */
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  margin: 0 0 var(--lvLine);
  min-height: calc(2 * var(--lvLine));
  line-height: 1.6;
  font-size: 0.72rem;
  font-weight: 400;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.05em;
}
#viewLive .lvEditTime { margin-right: auto; }
#viewLive .lvEditBtn {
  min-width: 52px;                           /* generous tap targets, quiet chrome */
  min-height: 38px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
#viewLive .lvEditBtn:active { transform: scale(0.96); }
#viewLive .lvEditBtn.ok {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-ink);
}

/* footer: pinned at the paper's bottom — 麻吉咧聽 while listening, the review row after stop.
   z-index keeps it (and its fade band) ABOVE the page that is flying out of the card. */
#viewLive .lvPageFoot {
  position: relative;
  z-index: 3;
  flex: none;
  padding: 8px 16px 10px var(--lvTextX);
}
/* ---- 「1 / 2」 — the page counter, and the read-back control ---------------------------------
   It sits in the 14px fade band .lvPageFoot::before already occupies, which is exactly where the
   owner's mockup puts it. Hidden outright on a one-page entry, so a short session is unchanged. */
#viewLive .lvPager {
  position: absolute;
  left: 50%;
  top: -19px;                        /* the pill floats in the fade band, CLEAR of 麻吉咧聽 */
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 0 3px;
  border-radius: 999px;
  background: rgba(255, 253, 248, 0.92);
  border: 1px solid var(--line);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}
#viewLive .lvPagerNum {
  font-family: var(--sans);
  font-size: 0.76rem;
  line-height: 22px;
  letter-spacing: 0.06em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  min-width: 36px;
  text-align: center;
}
/* the pill is 22px of CHROME (the mockup's proportions — it must not shove the listening row
   down or sit on the last ruled line) with a 40×40 HIT AREA hung off it, so the tap target is
   full size while the ornament stays small. */
#viewLive .lvPagerBtn {
  position: relative;
  min-width: 34px;
  height: 22px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--brand);
  font-size: 0.95rem;
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
#viewLive .lvPagerBtn::after { content: ""; position: absolute; inset: -9px -3px; }
#viewLive .lvPagerBtn:disabled { color: var(--faint); opacity: 0.45; cursor: default; }
#viewLive .lvPageFoot::before {      /* soft fade instead of a hairline */
  content: "";
  position: absolute;
  left: var(--lvMarginX);            /* spans the whole ruled band, gutter included */
  right: 16px;
  top: -14px;
  height: 14px;
  background: linear-gradient(to bottom, rgba(255, 253, 248, 0), var(--surface));
  pointer-events: none;
}
#viewLive .lvListen {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 26px;
  font-family: var(--kai);
  font-size: 0.92rem;
  letter-spacing: 0.03em;
  color: var(--muted);
}
#viewLive .lvListenCap { white-space: nowrap; }
#viewLive .lvClock {
  font-family: var(--sans);
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
}
#viewLive .lvMeter {                 /* 24 bars, newest on the right; live.js writes --v (0.08..1) */
  flex: 1 1 auto;
  margin-left: auto;                 /* capped by max-width, so the auto margin pushes it right */
  max-width: 180px;
  height: 18px;
  display: flex;
  align-items: center;
  gap: 3px;
}
#viewLive .lvMeter i {
  display: block;
  flex: 1 1 0;
  height: 100%;
  border-radius: 2px;
  background: var(--brand);
  opacity: 0.5;
  transform: scaleY(var(--v, 0.12));
  transform-origin: 50% 50%;
  transition: transform 90ms linear;
}

/* ---- review row (in the footer) — the ephemeral session's one decision point ---------------------- */
#viewLive .lvSave {                  /* was a card; now plain footer content */
  margin-top: 0;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
  text-align: left;
}
/* §A.2 EOS confirm row — 麻吉聽做這 N 句，有對嗎？ [有聽錯] [聽對了]. The sentences are on the
   page right above, so nothing is re-quoted and there is no scroll box. */
#viewLive .lvConfirm {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin: 0 0 6px;
  padding: 0;
  background: transparent;
  border: none;
}
#viewLive .lvConfirmText {
  flex: 1 1 160px;
  font-size: 0.98rem;
  line-height: 1.5;
  color: var(--text);
  overflow-wrap: anywhere;
}
#viewLive .lvConfirmBtns {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 0;
  margin-left: auto;
}
#viewLive .lvConfirmBtn {
  min-height: 40px;
  padding: 0 18px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  font-family: var(--sans);
  font-size: 0.92rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.15s ease;
}
#viewLive .lvConfirmBtn:active { transform: scale(0.97); }
#viewLive .lvConfirmBtn.ok {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-ink);
}
#viewLive .lvSaveMeta {
  font-family: var(--kai);
  color: var(--muted);
  font-size: 0.92rem;
  letter-spacing: 0.03em;
  text-align: left;
}
#viewLive .lvSaveBtn {
  margin-top: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;              /* generous tap target */
  padding: 0 22px;
  border: none;
  border-radius: 999px;
  background: var(--brand);
  color: var(--brand-ink);
  font-family: var(--sans);
  font-size: 0.98rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  box-shadow: 0 2px 8px rgba(43, 42, 38, 0.18);
  transition: transform 0.15s ease, opacity 0.2s ease;
}
#viewLive .lvSaveBtn:active { transform: scale(0.97); }
#viewLive .lvSaveBtn:disabled { opacity: 0.55; cursor: default; }
#viewLive .lvSaveErr {
  margin-top: 8px;
  color: var(--rec);
  font-size: 0.84rem;
  line-height: 1.6;
  overflow-wrap: anywhere;
}

/* ---- the page's closing lines (in-body, after the ink): title, summary, 去看日記 → --------------------
   No 暫定 chip / dashed spine / standing note any more (owner 2026-08-16): the pair simply
   appears; a correction that actually changes it says so with a toast. */
#viewLive .lvSaveDone {
  margin-top: 18px;
  /* .lvOffPage (round 13): once the entry is saved the closing lines RESERVE their slot on every
     page and are merely made invisible on the pages that are not the last one. Never display —
     hiding them would shrink the ruled window, which adds a page, which makes this page not the
     last one, which hides them again. visibility keeps cap(k) stable in the saved phase. */
  padding-top: 12px;
  border-top: 1px solid var(--line);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.6;
  text-align: left;
}
#viewLive .lvSaveDone.lvOffPage { visibility: hidden; }
#viewLive .lvSavedTitle {
  font-family: var(--serif);
  font-weight: 900;
  font-size: 1.15rem;
  letter-spacing: 0.02em;
}
#viewLive .lvSavedSummary {
  margin-top: 6px;
  color: var(--muted);
  font-size: 0.92rem;
  line-height: 1.75;
  text-align: left;
  overflow-wrap: anywhere;
}
#viewLive .lvGotoBtn {
  margin-top: 10px;
  border: none;
  background: transparent;
  color: var(--brand);
  font-family: var(--sans);
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: rgba(28, 107, 84, 0.45);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  padding: 6px 10px;             /* comfortable tap target without looking like a pill */
}
#viewLive .lvGotoBtn:hover { color: var(--rec); text-decoration-color: var(--rec); }

/* (the per-tab .lvToast is gone — diary.css owns the one shared .mToast and lifts it clear of
   this tab's mic dock; live.js's showToast is a thin guarded call to MachiDiary.toast.) */

/* ---- small/short screens --------------------------------------------------------------------------- */
@media (max-height: 640px) {
  /* specificity guarantees the compact rule (1,1,0) still wins over this (1,0,0), whatever
     the source order — a short screen shrinks the BIG dock only */
  #viewLive { --lvMicSize: 72px; --lvMicGlyph: 34px; --lvDock: 106px; }
  #viewLive .lvPageBody { min-height: 64px; }
}

@media (prefers-reduced-motion: reduce) {
  /* caret stays visible but static; the ink does not fade; the meter is simply not shown */
  #viewLive .lvSeg.live::after,
  #viewLive .lvPartialText::after { animation: none; }
  #viewLive .lvSeg { transition: none; }
  #viewLive .lvMeter { display: none; }
  #viewLive .lvMeter i { transition: none; }
  #viewLive .lvStatus.live .dot { animation: none; }
  #viewLive .lvMic.rec::before, #viewLive .lvMic.rec::after { animation: none; opacity: 0; }
  #viewLive .lvMic { transition: none; }
  #viewLive .lvMic:active { transform: none; }
  #viewLive .lvTailBtn:active { transform: none; }
  #viewLive .lvSaveBtn:active { transform: none; }
  #viewLive .lvEditBtn:active { transform: none; }
  #viewLive .lvStatusBtn, #viewLive .lvConfirmBtn { transition: none; }
  #viewLive .lvStatusBtn:active,
  #viewLive .lvConfirmBtn:active { transform: none; }
  /* 翻頁 swaps instantly (goToPage takes the no-ghost path); this is only the backstop for
     someone who flips the OS setting while a turn is already in flight. */
  #viewLive .lvSheet.lvAnim,
  #viewLive .lvGhost.lvAnim { transition: none; }
  #viewLive .lvMachiPin { animation: none; }
  #viewLive .lvDaily.lvSilenceInvite { animation: none; }
}
