/* Warm paper palette, and the app's only theme.
   These were once a near-black "chrome" set (#0f1319 and friends) that the
   reader switched away from whenever a paper background was active. That made
   the desktop build open dark while the Android build opened warm, because the
   switch only fired once a texture had been applied — and the empty state, with
   no document and no texture, showed the raw dark stage. The warm values are now
   the defaults so both platforms look identical from the very first frame, and
   `body.on-paper` below only has to restate them for clarity rather than to
   change them. */
:root{
  --bg:#f2e3bc; --bg2:#f6ead0; --bg3:#efe0bd; --line:#d8c9a4;
  --fg:#1b1b1b; --fg2:#5c5346; --accent:#d9a24b; --accent2:#3f8fb8;
  --doc-badge:#3d5a94; --doc-badge-hover:#48699f;
  --danger:#d1615c; --page:#fffdf7;
  --tb:44px; --toolh:56px; --status:28px;
  /* One word-panel tile column. `.dict-cards` lays the tiles out with it and
   *  `dictColWidth` reads it back, so the panel's growth and the grid's tracks
   *  can never disagree about how wide a tile is. */
  --dict-col:190px;
}
/* Inter, latin subset, straight from the browser edition's Google Fonts
   stylesheet. Bundled locally because the desktop WebView is offline-first and
   must not depend on fonts.gstatic.com. This is a variable file, so one face
   covers every weight the reading pane uses (400/500/600/700). */
@font-face{
  font-family:'Inter';
  font-style:normal;
  font-weight:100 900;
  font-display:swap;
  src:url("fonts/inter-latin.woff2?v=40") format('woff2');
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
/* The reading pane mirrors the browser edition's stack, which puts Inter (a
   bundled webfont, see index.html) ahead of the system faces so Latin text
   matches worddiy.com. Han has no webfont on either side, so it falls through
   to the same OS CJK face the site uses; listing the CJK families explicitly
   just makes that fallback deterministic instead of leaving it to the UA. */
:root{
  --font-latin:"Inter","Segoe UI",system-ui,-apple-system,BlinkMacSystemFont,sans-serif;
  --font-han:"Inter","Segoe UI","Microsoft YaHei","微软雅黑","PingFang SC",
    "Hiragino Sans GB","Source Han Sans SC","Noto Sans CJK SC",
    "Malgun Gothic",system-ui,sans-serif;
}
html,body{margin:0;height:100%;overflow:hidden;background:var(--bg);color:var(--fg);
  font:14px/1.4 var(--font-han)}
body{display:flex;flex-direction:column}
/* pdf.js creates this measuring canvas and appends it to `document.body`. Its
   own stylesheet (`pdf_viewer.css`) defines it as hidden, but we only load
   `app.css` — so it arrived as a plain `300x150`, `position:static` block and
   joined `body`'s flex column as a real item, stealing ~590px off the bottom
   of `#stage` (the pane was squeezed to 114px tall and the rest read as a black
   band). It is only ever drawn into, never displayed, so hide it outright. */
canvas.hiddenCanvasElement,#hiddenCanvasElement{
  position:absolute!important;left:0!important;top:0!important;
  width:0!important;height:0!important;display:none!important}
.hidden{display:none!important}
.dim{color:var(--fg2)}
.tb-spacer{flex:1}

/* ---------- tab row (tab strip + tool buttons, one line) ----------
   The file name lives on its tab, so the old title bar is gone and the tab
   strip shares a single row with the buttons. That keeps the chrome to two
   rows total: this one and #toolbar. */
#tabrow{flex:0 0 auto;display:flex;align-items:center;
  background:var(--bg2);border-bottom:1px solid var(--line);
  padding-top:env(safe-area-inset-top)}
#tabbar{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:4px;
  padding:4px 0 4px 6px;overflow-x:auto;scrollbar-width:none}
#tabbar::-webkit-scrollbar{display:none}
#topbar{flex:0 0 auto;display:flex;align-items:center;gap:6px;
  padding:0 8px 0 4px}
#topbar .tb-btn{flex:0 0 auto;width:36px;height:36px;border:0;border-radius:8px;
  background:var(--bg3);color:var(--fg);font-size:16px;line-height:1;cursor:pointer}
#topbar .tb-btn:active{background:var(--accent);color:#111}
#topbar .tb-btn:disabled{opacity:.35}
/* Autosave state: quiet by default so it only draws the eye while pending. */
.tb-save{flex:0 0 auto;font-size:11px;color:var(--fg2);padding:0 4px;
  white-space:nowrap;transition:color .15s}
.tb-save[data-mode="pending"]{color:var(--accent)}
.tb-save[data-mode="saved"]{color:#3f9142}
.tb-save:empty{display:none}
#page-ind{flex:0 0 auto;display:flex;align-items:center;gap:2px;font-size:13px;
  background:var(--bg3);border-radius:8px;padding:0 6px;height:36px}
#page-input{width:34px;border:0;background:transparent;color:var(--fg);
  font:inherit;text-align:center;outline:none}

/* ---------- toolbar ---------- */
#toolbar{flex:0 0 auto;display:flex;align-items:stretch;gap:2px;padding:4px 6px;
  background:var(--bg2);border-bottom:1px solid var(--line);overflow-x:auto;
  scrollbar-width:none}
#toolbar::-webkit-scrollbar{display:none}
/* 蓝牙笔 tab on the 手写 button. A pencil reader needs the switch close at hand
   but must not be able to trip it by accident — a plain tap has to keep meaning
   "arm the pen". So the switch hides behind a long press on this small corner
   dot, which is small enough that no ordinary tap lands on it and marked enough
   that it is findable once you know it is there. */
.tool .tool-dot{position:absolute;right:5px;bottom:5px;width:9px;height:9px;
  border-radius:50%;border:1.5px solid var(--fg2);background:transparent;
  opacity:.5;pointer-events:none}
.tool .tool-dot.on{background:var(--accent);border-color:var(--accent);opacity:1}
/* The press target is the dot's whole corner, not the 9px mark itself — a dot
   that small is not hittable with a fingertip, and the long press is what
   distinguishes this from the button's own tap. */
.tool[data-tool="pen"]:after{content:'';position:absolute;right:0;bottom:0;
  width:26px;height:26px}
/* Long-press popover: one row, one switch, gone on any tap outside. It is fixed
   rather than anchored so it cannot be clipped by the toolbar's own scroll box. */
#bt-pop{position:fixed;z-index:80;display:none;align-items:center;gap:8px;
  padding:8px 12px;border-radius:12px;background:var(--bg2);
  border:1px solid var(--line);box-shadow:0 6px 22px rgba(0,0,0,.28);
  font-size:12px;color:var(--fg);white-space:nowrap}
#bt-pop.show{display:flex}
#bt-pop .bt-sw{display:flex;align-items:center;gap:5px;height:26px;border-radius:999px;
  border:0;padding:0 8px 0 4px;background:#f1ece0;color:#6b6250;cursor:pointer;
  font-size:12px;font-weight:600;font-family:inherit;line-height:1}
#bt-pop .bt-sw .knob{display:block;width:18px;height:18px;border-radius:50%;
  background:#fffdf7;box-shadow:0 1px 3px rgba(90,70,20,.3);
  transition:transform .16s cubic-bezier(.4,1.4,.5,1),background .13s}
#bt-pop .bt-sw[on]{background:#f0e2c2;color:#8a5a12}
#bt-pop .bt-sw[on] .knob{transform:translateX(4px);background:#d9a24b}
#bt-pop .bt-state{min-width:1.2em;text-align:center}
/* Inline SVG tool glyphs. They inherit currentColor so the [active] and
   :disabled states keep working, and a fixed box keeps every button the same
   height whether the glyph is text or SVG. */
.tool .ticon{width:19px;height:19px;flex:0 0 auto;fill:none;stroke:currentColor;
   stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
#pen-bar .pb-btn svg{width:24px;height:24px;fill:none;stroke:currentColor;
   stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
/* The lasso is new and easy to miss, so it gets a permanent accent outline. */
#pen-bar .pb-btn[data-ptool="lasso"]{box-shadow:inset 0 0 0 1.5px #e2c99a}
#pen-bar .pb-btn[data-ptool="lasso"][active]{box-shadow:inset 0 0 0 1.5px #d9a24b}
.tool{flex:0 0 auto;min-width:52px;height:var(--toolh);border:1px solid transparent;
  border-radius:10px;background:transparent;color:var(--fg2);font-size:17px;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:2px;cursor:pointer;padding:2px 6px;position:relative}
.tool span{font-size:10px;font-weight:600;letter-spacing:.02em}
/* 关闭单词 — the panel's ONLY exit, so it is pinned to the toolbar's far left
   (first child, before 撤销) and given more weight than a normal tool: a plain
   four-character label with no glyph, a filled plate and an accent border. The
   reader has to be able to hit it while holding the pen, and it must never be
   confused with the small glyph tools it sits beside. */
#btn-close-dict{min-width:96px;padding:0 12px;font-size:15px;
  flex-direction:row;background:#3a2f1c;border-color:var(--accent);color:#f6e6c8}
#btn-close-dict span{font-size:15px;font-weight:700;letter-spacing:.08em;white-space:nowrap}
#btn-close-dict:active{background:#2a2314;color:#fff}
#btn-close-dict.hidden{display:none!important}
.tool:active{background:var(--bg3)}
.tool[active],.tool.on{background:var(--bg3);border-color:var(--accent);color:var(--accent)}
/* Undo/redo grey out when the stack is empty so the reader can see at a glance
   whether there is anything to walk back. */
.tool:disabled{opacity:.35;cursor:default}
.tool:disabled:active{background:transparent}
/* A <select> cannot use the buttons' column flex, and native chrome would
   ignore the toolbar palette, so it gets its own flat appearance. */
#toolbar select.tool{display:inline-block;flex-direction:row;min-width:64px;
  padding:0 4px;font-size:12px;font-weight:600;font-family:inherit;
  background:transparent;color:var(--fg2);border:1px solid var(--line);
  border-radius:10px;cursor:pointer;outline:none;-webkit-appearance:none;
  appearance:none;text-align:center;text-align-last:center}
#toolbar select.tool:hover{background:var(--bg3)}
#toolbar select.tool option{background:var(--bg2);color:var(--fg)}
/* Accent picker: one segmented control instead of a status button plus a
   swap button, so the current accent is visible without a legend. Mirrors the
   website's 英音 / 美音 group. */
#toolbar .accent-group{flex:0 0 auto;display:inline-flex;align-items:center;
  gap:2px;height:var(--toolh);padding:3px;border-radius:10px;
  background:var(--bg3);align-self:center}
#toolbar .accent-btn{border:0;background:transparent;color:var(--fg2);
  font:600 12px/1 inherit;font-family:inherit;padding:5px 10px;border-radius:8px;
  cursor:pointer;transition:background .15s,color .15s,box-shadow .15s}
#toolbar .accent-btn:hover{color:var(--fg)}
#toolbar .accent-btn.active{background:var(--bg2);color:var(--accent);
  box-shadow:0 1px 3px rgba(0,0,0,.28)}
/* Group spacing only. This used to draw a 1px rule, but the toolbar already
   separates its groups by colour and rounding, and the white bars read as
   stray artefacts on a dark strip. Keep the flex item so the groups still
   breathe, just stop painting it. */
/* The old 1px separator buttons are gone; toolbar groups are spaced with a
   left margin instead, so no visible bar is drawn. */
.tb-gap{margin-left:7px}

/* ---------- stage ---------- */
#stage{flex:1 1 auto;position:relative;overflow:hidden;background:var(--bg)}
/* Paper background. It is the bottom-most layer of the stage and is stretched
   to the whole stage, so a chosen texture reads as the paper the pages sit on
   (GoodNotes style) rather than as a colour on the pages themselves. */
#bg-layer{position:absolute;top:0;right:0;bottom:0;left:0;z-index:0;pointer-events:none;
  background-repeat:repeat;background-position:0 0}
/* #bg-layer carries z-index:0, which does place it at level 0 in the stage's
  stacking context — but a positioned sibling with z-index:auto (#viewer, which
  is absolute and fills the stage) is painted in a later step and therefore covers it
  with #stage's own --bg fill. The paper texture was being laid down and then
  completely hidden whenever a document was open or the viewer was mounted,
  which is why the stage sampled as flat near-black. Pulling the two content
  panes above the paper and keeping #bg-layer at level 0 leaves the texture
  visible underneath both. */
#viewer,#study{z-index:1}
/* With a custom background active, the page sheet's own paper white is removed
   by knockOutPaper() (see app.js): the blank areas of the PDF become fully
   transparent, so the texture shows through them exactly where the page has no
   ink. The sheet itself is dropped too so its white fill and shadow cannot sit
   behind the transparent areas.
   Composites normally — the knockout already handles the blank paper, so
   `multiply` is no longer needed and would only muddy the PDF's own dark art.
   NOTE: #viewer and #pages must NOT create a stacking context, or they would
   isolate the transparent canvases from #bg-layer and the texture would vanish. */
body.has-bg .page{background:transparent;box-shadow:none}
body.has-bg #viewer,body.has-bg #pages,body.has-bg .page{isolation:auto}
/* #study is opaque (--bg2) and fills the stage, so with a paper background it
   would hide #bg-layer completely. Make its *fill* transparent so the texture
   reads as the paper the text is written on.
   🔴 This must be `background-color`, NOT the `background` shorthand. The
   shorthand also clears `background-image`, and `#study` now carries the two
   margin-bar gradients — so writing `background:transparent` here silently
   deleted them the moment any wallpaper was selected. `background-color` clears
   only the fill, which is the whole of what this rule was ever for. */
body.has-bg #study{position:relative;z-index:2;background-color:transparent}
/* The stage's own backdrop is the near-black --bg. With a paper texture chosen
   that backdrop only ever shows through the gaps of the tile (never, since the
   tile is opaque) or during the first frame before app.js runs, and it is what
   made the empty state — no document open — open on a wall of dark brown. Give
   the stage the paper's own colour so the very first paint is already warm. */
body.has-bg #stage{background:#f2e3bc}
/* Light papers (ivory/kraft) are pale, but the app's ink is near-white for the
   dark chrome. On these backgrounds the reading pane must flip to dark ink or
   the text is illegible. `.on-paper` is toggled by applyBackground(). */
body.on-paper{--fg:#1b1b1b;--fg2:#5c5346;--line:#c8bda8}
body.on-paper #study .st-tx,body.on-paper #study .st-heading,
body.on-paper #study .st-parnum,body.on-paper #study .st-block,
body.on-paper #study .st-para-body{color:#1b1b1b}
body.on-paper #study .st-trabs-item{color:#06386b}
body.on-paper #study .st-tg{background:#eef2f7;border-color:#c9d2d6;color:#334155}
/* The answer box kept its dark `--bg3` fill, which read as a hole punched in
   the cream paper. Give it a translucent wash of the ink colour instead, so it
   looks like a ruled writing area on the page. Only the textarea is touched —
   --bg3 --line are still needed for the toolbar, picker, menu and modal, which
   must stay dark because they sit outside the reading pane. */
body.on-paper #study .st-answerbox textarea{
  background:rgba(27,27,27,.045);border-color:#b9ab90;color:#1b1b1b}
body.on-paper #study .st-answerbox textarea:focus{
  background:rgba(27,27,27,.03);border-color:var(--accent)}
body.on-paper #study .st-answerbox textarea::placeholder{color:#5c5346;opacity:1}
/* The toolbar, second tool row and status bar carried the dark chrome palette
   into an otherwise light app: the paper stage was warm yellow while the frame
   around it stayed near-black, which reads as a dark app with a bright hole in
   the middle and is exactly the "一打开太难受" impression. Restate the chrome
   tokens while a paper background is active so the whole surface — pickers,
   menus and modals included, since they inherit these variables — turns light
   with the paper. Text and hairlines flip to dark ink to stay legible. */
body.on-paper{
  --bg:#f2e3bc; --bg2:#f6ead0; --bg3:#efe0bd; --line:#d8c9a4;
  --fg:#1b1b1b; --fg2:#5c5346; --page:#fffdf7;
}
#viewer{position:absolute;top:0;right:0;bottom:0;left:0;overflow:auto;-webkit-overflow-scrolling:touch;
  overscroll-behavior:contain;touch-action:pan-x pan-y}
/* Safari only delivers Apple Pencil as a `pen` pointer while the *viewport*
   forbids its own scroll handling, so the armed tool's paper has to opt out of
   panning — but only for the duration of a pen contact.
   🔴 It used to say `none` for as long as the tool was armed, which froze the
   finger over the paper completely: 「点手写笔后屏幕依然应该能拖动」 was impossible,
   because the armed tool claimed the paper even with the pen on the table. The
   claim now follows the *contact* (`#stage.pen-hold`, below), so a finger scrolls
   the paper normally until the nib actually goes down. The pen still gets its
   `none` while writing, which is the only thing Safari needs to keep reporting
   `pointerType: 'pen'`. */
#pages .page.tool-pen,#pages .page.tool-hl,#pages .page.tool-eraser,
#pages .page.tool-lasso,#pages .page.tool-shape{touch-action:pan-x pan-y}
/* `auto`, not `none`: 平移 must stay reachable while a writing tool is armed,
   and a scroller that forbids its own panning cannot be dragged back. The pen is
   still intercepted — `inkAccepts` runs in JS before any default is suppressed,
   and `#stage.pen-hold` restores `none` for the duration of the contact. */
#viewer.no-pan{touch-action:auto}
/* Safari picks the gesture's owner by walking up from the target, and with
   `overscroll-behavior` still `auto` on the scroller a stylus drag can rubber-
   band the whole document instead of drawing. `#stage.writing` is the class the
   tool switch actually sets (see `setTool`), so this is the hook that exists. */
#stage.writing #viewer{overscroll-behavior:none}
#pages{display:flex;flex-direction:column;align-items:center;gap:14px;
  padding:14px 0 calc(40px + env(safe-area-inset-bottom));min-height:100%}
.page{position:relative;background:var(--page);border-radius:3px;
  box-shadow:0 2px 14px rgba(0,0,0,.45);flex:0 0 auto;overflow:hidden}
.page canvas{display:block;position:absolute;left:0;top:0}
.page canvas.ink{pointer-events:none}
.page.tool-pen canvas.ink,.page.tool-hl canvas.ink,.page.tool-eraser canvas.ink,.page.tool-lasso canvas.ink,.page.tool-shape canvas.ink{pointer-events:auto}
.page canvas.ink{cursor:crosshair}
/* The canvas is the topmost layer over the paper and the element a pen resolves
   against, so it must not forbid the finger's scroll while the pen is away.
   🔴 The pen's `none` has to be in place *before* the nib lands: Safari decides
   once per gesture, at classification time, so a `none` that arrives on
   `pointerdown` is a gesture too late and the stroke is claimed as a scroll.
   The hover listener in `wireInk` therefore opens the palm window while the
   pencil is still in the air, and `#stage.pen-hold` — not this rule — is what
   carries the `none` from there. This selector keeps only the `pointer-events`
   half (`pointer-events` cannot express "stylus only"), so the canvas still
   resolves the pen for the hit test without ever outliving a contact: with the
   pen away the window is shut, the paper is `pan-y`, and the finger scrolls
   across it normally. */
.page.tool-pen canvas.ink,.page.tool-hl canvas.ink,.page.tool-eraser canvas.ink,
.page.tool-lasso canvas.ink,.page.tool-shape canvas.ink{touch-action:pan-x pan-y}
/* Writing mode lifts the ink canvas above the word-hit boxes so a stroke that
   starts over a word is not stolen as a tap. Leaving the mode drops it back.
   `overflow:visible` is required because the ink canvas is wider than the page:
   it extends into the gutters so the margins are writable too. */
#stage.writing .page{overflow:visible;
  box-shadow:0 0 0 2px var(--accent),0 2px 14px rgba(0,0,0,.45)}
#stage.writing .page canvas.ink{z-index:5}
#stage.writing .page .pnum{z-index:6}
.page .pnum{position:absolute;right:6px;bottom:4px;font-size:10px;color:#9aa4b2;
  background:rgba(255,255,255,.75);border-radius:4px;padding:0 4px;pointer-events:none}
.hit{position:absolute;border-radius:3px;cursor:pointer;
  background:transparent;transition:background .12s}
.hit:hover{background:rgba(217,162,75,.20)}
.hit.playing{background:rgba(217,162,75,.45);box-shadow:0 0 0 1px var(--accent) inset}
.hit.uk{background:transparent}
.hit.uk:hover{background:rgba(63,143,184,.20)}
.hit.uk.playing{background:rgba(63,143,184,.45);box-shadow:0 0 0 1px var(--accent2) inset}

/* ---------- empty / busy ---------- */
#empty-hint{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:10px;text-align:center;padding:24px}
.eh-icon{font-size:54px;opacity:.85}
#empty-hint h2{margin:0;font-size:19px;font-weight:700}
#empty-hint p{margin:0;color:var(--fg2);max-width:380px;font-size:13px}
#empty-hint code{background:var(--bg3);padding:1px 5px;border-radius:4px;font-size:12px}
#empty-hint .hint{font-size:12px;opacity:.7}
.primary{margin-top:8px;border:0;border-radius:10px;
  background:var(--accent,#d9a24b);background:#d9a24b;
  color:#151a20;font:600 14px/1 inherit;padding:11px 20px;cursor:pointer}
.primary:active{filter:brightness(.92)}
#busy{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:12px;background:rgba(15,19,25,.82);z-index:40}
.spinner{width:34px;height:34px;border-radius:50%;border:3px solid var(--line);
  border-top-color:var(--accent);animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
#busy-text{font-size:13px;color:var(--fg2)}
#busy-bar{width:220px;height:5px;border-radius:3px;background:var(--line);overflow:hidden;display:none}
#busy-bar.on{display:block}
#busy-fill{height:100%;width:0;background:var(--accent);transition:width .15s linear}

/* ---------- status bar ---------- */
#statusbar{flex:0 0 auto;height:var(--status);display:flex;align-items:center;gap:6px;
  padding:0 10px calc(0px + env(safe-area-inset-bottom));background:var(--bg2);
  border-top:1px solid var(--line);font-size:11px;white-space:nowrap;overflow:hidden}
.chip{background:var(--bg3);border-radius:5px;padding:2px 7px;color:var(--fg2)}
.chip.hot{color:var(--accent)}
button.chip{border:0;font:inherit;line-height:1.5;cursor:pointer}
button.chip.on{color:var(--accent);background:var(--bg2)}

/* ---------- menu ---------- */
#menu{position:absolute;top:calc(var(--tb) + env(safe-area-inset-top) + 4px);right:8px;
  z-index:60;background:var(--bg3);border:1px solid var(--line);border-radius:12px;
  padding:6px;min-width:216px;box-shadow:0 10px 34px rgba(0,0,0,.55)}
#menu button{display:block;width:100%;text-align:left;border:0;background:transparent;
  color:var(--fg);font:14px/1 inherit;padding:11px 12px;border-radius:8px;cursor:pointer}
#menu button:active{background:var(--accent);color:#151a20}

/* ---------- background picker ---------- */
#bg-picker{position:absolute;top:calc(var(--tb) + var(--toolh) + 8px);left:8px;
  z-index:65;background:var(--bg3);border:1px solid var(--line);border-radius:12px;
  padding:10px;box-shadow:0 10px 34px rgba(0,0,0,.55);max-height:calc(100vh - 200px);
  overflow:auto}
#bg-picker-head{font-size:11px;font-weight:700;letter-spacing:.05em;color:var(--fg2);
  text-transform:uppercase;margin:0 2px 8px}
#bg-picker-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:8px}
.bg-swatch{width:58px;height:58px;border-radius:10px;border:2px solid var(--line);
  cursor:pointer;padding:0;background-repeat:repeat;background-position:0 0;
  background-color:#fff;transition:border-color .15s,transform .1s;overflow:hidden}
.bg-swatch:hover{transform:translateY(-1px)}
.bg-swatch.active{border-color:var(--accent);box-shadow:0 0 0 2px rgba(217,162,75,.3)}
.bg-swatch-label{grid-column:1/-1;font-size:11px;color:var(--fg2);text-align:center;
  margin-top:2px}

/* ---------- toast / modal ---------- */
#toast{position:absolute;left:50%;bottom:calc(var(--status) + 20px);transform:translateX(-50%);
  background:rgba(28,36,46,.97);border:1px solid var(--line);color:var(--fg);
  padding:9px 16px;border-radius:10px;font-size:13px;z-index:80;max-width:86vw;text-align:center}
/* 🔴 `fixed`, not `absolute`. With `absolute` the overlay is laid out against
   the *document*, so on a page taller than the screen — which is the normal
   state of a scrolled reader — the centred box is placed at the document's
   midpoint and can land entirely below the fold. The scrim is full-bleed so it
   still painted, and the reader saw a featureless dark band at the bottom of
   the screen with no text in it: 「移动到文件夹报下部黑块，看不清报什么」.
   `fixed` anchors to the viewport, so the box is always on screen whatever the
   scroll position. The reader runs in standalone mode on iPad, where the
   document routinely overflows the visual viewport, so this is not an edge case.
   `100dvh` with a `100vh` fallback covers the same ground for the scrim. */
#modal{position:fixed;top:0;right:0;bottom:0;left:0;height:100vh;height:100dvh;
  background:rgba(8,11,15,.72);z-index:90;
  display:flex;align-items:center;justify-content:center;padding:20px}
/* 🔴 The colours are written out, not read from variables.
 *
 * The reader reported 「黑底黑字，什么也看不到」: the dialog appeared, was laid
 * out, and could not be read. With `var(--bg2)` an unresolved variable makes the
 * declaration invalid at computed-value time, so `background` falls back to its
 * initial `transparent` — and the dark scrim behind the box (`#modal` above)
 * then shows through as black, while `color` falls back to the initial `black`
 * for the text. Black on black is exactly that failure, and it was reproduced
 * here: stripping the variables from the stylesheet makes this box compute
 * `background:rgba(0,0,0,0)` and `color:rgb(0,0,0)`.
 *
 * The variables resolve correctly in the desktop browser, so the tablet is
 * resolving them differently and the box must not depend on them. A dialog is
 * the one surface that has to be legible when something else has already gone
 * wrong, so it carries its own colours.
 *
 * The order matters. `background:var(--bg2,#f6ead0);background:#f6ead0` puts the
 * variable first and the literal second, so the literal always wins. The
 * obvious-looking `background:#f6ead0;background:var(--bg2,#f6ead0)` does NOT
 * work: a variable that is *defined but empty* is a valid substitution, so the
 * second declaration still overrides — and `var(--x, fallback)` does not rescue
 * an empty value. That ordering was tested and it left the box transparent. A
 * theme that is half-applied produces exactly this empty-variable state. */
#modal-box{background:var(--bg2,#f6ead0);background:#f6ead0;color:#1b1b1b;
  border:1px solid var(--line,#d8c9a4);border:1px solid #d8c9a4;border-radius:14px;
  padding:18px;max-width:420px;width:100%;box-shadow:0 18px 50px rgba(0,0,0,.6);
  max-height:80vh;max-height:80dvh;overflow:auto}
#modal-title{font-weight:700;font-size:15px;margin-bottom:8px;color:#1b1b1b}
/* The error text has to be selectable: a screenshot the reader cannot read is
   the same as no report at all, and the whole point of this box is to get the
   message off the device. */
#modal-body{font-size:13px;color:#5c5346;white-space:pre-wrap;
  max-height:50vh;overflow:auto;user-select:text;-webkit-user-select:text}
#modal-ok{margin-top:14px}

/* ---- membership gate ----
   Opaque on purpose: while the verdict is unknown the reader underneath must
   not be partly usable, and a translucent scrim would still show the page the
   user has not been cleared to read. z-index sits above every other layer,
   including #modal, so a dialog raised during boot cannot cover the gate.

   Edges are spelled out rather than using the `inset` shorthand: this app
   targets Chrome 74 on the tablet, which predates it, and an ignored shorthand
   would collapse the panel to nothing at exactly the moment it must cover
   everything. */
#gate{position:fixed;top:0;right:0;bottom:0;left:0;background:#0b0f14;z-index:200;
  display:flex;align-items:center;justify-content:center;padding:24px}
#gate-box{max-width:360px;width:100%;text-align:center;color:#e8eef5;
  font-family:system-ui,-apple-system,"Segoe UI",sans-serif}
#gate-logo{font-weight:800;font-size:22px;letter-spacing:.5px;margin-bottom:14px}
#gate-msg{font-size:14px;line-height:1.7;color:#a9b6c4}
#gate-btn{margin-top:20px;padding:12px 26px;font-size:15px;font-weight:600;
  color:#0b0f14;background:#f2c14e;border:0;border-radius:10px;cursor:pointer}
#gate-btn:active{background:#d9ab3c}
#gate-sub{margin-top:16px;font-size:12px;color:#6f7c8a;line-height:1.6}
#gate-sub a{color:#8fb4e0}

/* ---------- document tab bar ---------- */
/* Room for up to 20 simultaneously open documents, GoodNotes-style: one tab per
   open file, click to switch. It stays visible even for a single document now,
   because the tab is the only place the file name appears. */
#tabbar::-webkit-scrollbar{height:6px}
#tabbar::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px}
.tab{flex:0 0 auto;display:flex;align-items:center;gap:6px;max-width:220px;
  height:32px;padding:0 4px 0 12px;border:1px solid var(--line);
  border-radius:8px;background:var(--bg3);color:var(--fg2);font-size:12px;
  cursor:pointer;user-select:none;transition:background .12s,color .12s}
.tab:hover{background:var(--bg2);color:var(--fg)}
.tab.active{background:var(--doc-badge);color:#fff;border-color:var(--doc-badge);
  font-weight:600}
.tab-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;
  color:var(--fg);font-weight:600}
.tab.active .tab-name{color:#fff}
/* The language edition has to survive truncation: it is the only thing telling
   two same-titled articles apart in the strip. */
.tab-lang{flex:0 0 auto;font-size:9px;font-weight:700;letter-spacing:.04em;
  padding:1px 4px;border-radius:4px;background:var(--line);color:var(--fg2)}
.tab-lang-ko{background:#2f6fd0;color:#fff}
.tab-lang-zh{background:#c0392b;color:#fff}
.tab.active .tab-lang{background:#ffffff40;color:#fff}
.tab-dot{flex:0 0 auto;width:6px;height:6px;border-radius:50%;background:var(--accent)}
.tab-dot.saved{background:#3f9142}
.tab-close{flex:0 0 auto;width:18px;height:18px;border:0;border-radius:4px;
  background:transparent;color:inherit;font-size:13px;line-height:1;cursor:pointer;
  opacity:.6;padding:0}
.tab-close:hover{opacity:1;background:#fff2}
.tab.active .tab-close:hover{background:#00000033}
.tabbar-full{flex:0 0 auto;font-size:11px;color:var(--danger);padding:0 8px;
  align-self:center;white-space:nowrap}
.tabbar-close-all{flex:0 0 auto;align-self:center;margin-left:6px;width:26px;height:26px;
  display:flex;align-items:center;justify-content:center;border:1px solid var(--line);
  border-radius:7px;background:var(--bg3);color:var(--fg2);font:inherit;font-size:14px;
  line-height:1;cursor:pointer;padding:0}
.tabbar-close-all:hover{background:var(--danger);border-color:var(--danger);color:#fff}

/* ---------- prompt dialog (rename / new folder) ---------- */
/* `fixed`, not `absolute`, for the same reason `#modal` is: the reader runs on
  iPad where the document overflows the visual viewport, and an `absolute`
  scrim is anchored to the document, not the screen. The dialog then opened
  partly (or wholly) below the fold with `align-items:center` clipping it
  symmetrically, and because the scrim itself does not scroll there was no way
  to reach 「确定」 — the reader could open 移动到文件夹 and never finish it.
  `100dvh` keeps the scrim matched to the *visual* viewport while the software
  keyboard is up; a `100vh` fallback covers engines without `dvh`. */
#prompt{position:fixed;top:0;right:0;bottom:0;left:0;height:100vh;height:100dvh;
  background:rgba(8,11,15,.72);z-index:95;
  display:flex;align-items:center;justify-content:center;padding:20px;
  overflow:auto;-webkit-overflow-scrolling:touch}
/* The box must be able to shrink. Without a `max-height` the folder list
  (`#prompt-list`, up to 34vh) plus the input and the action row made the box
  taller than the viewport, and a flex item centred in a `fixed` scrim that is
  larger than the screen spills equally off both ends with nothing to scroll. */
/* Same hardening as `#modal-box`: this is the box behind 「移动到文件夹」 and
 * 「新建文件夹」, and it had the identical `var(--bg2)` / `var(--fg2)` structure,
 * so an unresolved variable turns it into the 「长黑框，什么也看不到」 the reader
 * photographed. Colours are literal-first, variable-second. */
#prompt-box{background:var(--bg2,#f6ead0);background:#f6ead0;color:#1b1b1b;
  border:1px solid var(--line,#d8c9a4);border:1px solid #d8c9a4;border-radius:14px;
  padding:18px;max-width:420px;width:100%;box-shadow:0 18px 50px rgba(0,0,0,.6);
  max-height:80vh;max-height:80dvh;overflow:auto;
  display:flex;flex-direction:column}
#prompt-title{font-weight:700;font-size:15px;margin-bottom:8px;color:#1b1b1b}
#prompt-body{font-size:13px;color:#5c5346;margin-bottom:10px;white-space:pre-wrap}
#prompt-input{width:100%;height:38px;border:1px solid var(--line,#d8c9a4);border:1px solid #d8c9a4;
  border-radius:8px;
  background:var(--bg3,#efe0bd);background:#efe0bd;color:#1b1b1b;
  color:var(--fg,#1b1b1b);font:inherit;padding:0 10px;outline:none}
#prompt-input:focus{border-color:var(--accent)}
/* The list is the only variable-height part of the box, so it takes the
  leftover room and scrolls; the input and the action row stay put. `34vh`
  stays as the ceiling but is no longer load-bearing — flexing to the box's
  remaining space is what stops 确定 from being pushed off the bottom. */
#prompt-list{margin-top:10px;max-height:34vh;overflow:auto;
  border:1px solid var(--line,#d8c9a4);border:1px solid #d8c9a4;
  border-radius:8px;flex:1 1 auto;min-height:0;-webkit-overflow-scrolling:touch}
#prompt-list button{display:block;width:100%;text-align:left;border:0;border-bottom:1px
  solid #d8c9a4;border-bottom:1px solid var(--line,#d8c9a4);
  background:transparent;color:var(--fg,#1b1b1b);color:#1b1b1b;
  font:inherit;font-size:13px;
  padding:9px 12px;cursor:pointer}
#prompt-list button:last-child{border-bottom:0}
#prompt-list button:hover{background:var(--bg3)}
#prompt-list button.sel{background:var(--doc-badge);color:#fff}
.prompt-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px;
   flex:0 0 auto}
.prompt-actions button{height:36px;padding:0 16px;
  border:1px solid var(--line,#d8c9a4);border:1px solid #d8c9a4;
  border-radius:8px;background:var(--bg3,#efe0bd);background:#efe0bd;
  color:var(--fg,#1b1b1b);color:#1b1b1b;font:inherit;cursor:pointer}
.prompt-actions button.primary{background:var(--accent);border-color:var(--accent);
  color:#111;font-weight:600}

/* ---------- desktop chrome: two compact rows ---------- */
/* Tabs + window actions live on row 1 (#tabrow), the tool strip on row 2.
   Icon-only buttons keep both rows slim, so the reading area gets the space
   back. The threshold is low on purpose: an ordinary window is ~800px wide. */
@media (min-width:640px){
  #toolbar{justify-content:center;padding:4px 4px;gap:1px}
  .tool span{display:none}
  /* 关闭单词 is the only control whose label must survive the icon-only
     toolbar: it has no glyph, and the reader has to read it to know it exists.
     The universal `.tool span{display:none}` above would blank it out, leaving
     an anonymous dark plate. */
  #btn-close-dict span{display:inline}
  #btn-close-dict{min-width:86px;padding:0 12px}
  .tool{height:38px;min-width:34px;font-size:16px;padding:2px 3px}
  #toolbar select.tool{height:38px;min-width:52px;padding:0 2px}
  #toolbar .accent-group{height:38px;padding:2px}
  #toolbar .accent-btn{padding:4px 7px;font-size:11px}
  .tb-gap{margin-left:5px}
  :root{--toolh:38px}
  #status-hint{display:none}
}

/* --------------------------------------------------------- pdf.js text layer */
/* Transparent, selectable text sitting exactly over the canvas so a tap can
   be resolved back to the word underneath. The ink canvas keeps pointer
   events while a pen tool is active (see .page.tool-* rules above). */
.page .textLayer{position:absolute;left:0;top:0;overflow:hidden;opacity:1;
  line-height:1;pointer-events:none;transform-origin:0 0;z-index:1}
.page .textLayer span{position:absolute;white-space:pre;transform-origin:0 0;
  color:transparent;cursor:text}
.page .textLayer ::selection{background:rgba(217,162,75,.35)}

/* The study pane is selectable too (`user-select:text` is what lets the
   highlight gesture read `window.getSelection()`), so a press there paints a
   selection as well. Without a colour of our own WebKit falls back to its
   default wash, which on iPadOS is a grey-blue that reads as a smudge over the
   article — the reader taps mid-page, the article goes murky, and only a tap in
   the right margin (where nothing is selectable) clears it. Same tint as the
   PDF text layer above, so a selection looks like the highlighter either way.

   The tint has to be declared on EVERY ancestor up to `html`, not just on the
   pane and its leaves. A `::selection` pseudo-element resolves against the
   *common ancestor* of both range endpoints, so a range that spans more than one
   block is painted with the colour of whatever contains them all. A stroke that
   sweeps across the article is exactly that — WebKit reads the drag as a text
   selection, the range covers dozens of blocks, and the colour it lands on is
   `#study`'s. While that one resolved to transparent the UA default (grey-blue
   on iPadOS) took over and the whole article went murky the moment the pen
   touched down: 「写字的时候，那个蓝色整屏遮罩又出来了」. Declaring the colour at
   every level means the lookup can never fall through to the UA default. */
html ::selection,html::selection,body ::selection,body::selection,
#stage ::selection,#stage::selection,#viewer ::selection,#viewer::selection,
#study ::selection,#study::selection,#pages ::selection,#pages::selection,
.page .textLayer ::selection{background:rgba(217,162,75,.35)}

/* ------------------------------------------------------------- word popover */
/* Tapping words adds one card each, so a sentence's vocabulary can be
   collected in a single panel. Three to a row, wrapping to further rows.
   Each tile is a translucent, self-contained card so the sentence underneath
   stays readable; the panel is only a transparent layout box for them. */
/* `max-height` must leave room for the grip (22px) plus its 8px margin plus the
 * status bar at the bottom of the window; 70vh let a single long entry push the
 * grip down into the status bar, where it was effectively invisible. */
/* `width` here is only the fallback for the very first paint: `dictSizePanel`
 * overwrites it inline with a width derived from the tile count, so a one-word
 * lookup gets a one-column panel instead of a full-width band. */
.dict-panel{position:fixed;z-index:60;width:auto;max-width:min(1600px,calc(100vw - 16px));
  max-height:calc(100vh - 96px);
  display:flex;flex-direction:column;
  background:transparent;border:0;box-shadow:none;pointer-events:none;
  /* The panel holds live text, and the pen sweeps across it whenever the reader
   * writes near an open card. WebKit turns that sweep into a selection whose
   * endpoints straddle `body`'s two main branches (`#study` and `.dict-cards`),
   * and a selection that large is exactly the case where the browser stops
   * honouring the `::selection` wash declared on `body` and paints its own
   * grey-blue over the whole viewport instead — the 「整屏蓝遮罩」. The panel has
   * nothing to select anyway: the tiles exist to be read and copied by button. */
  user-select:none;-webkit-user-select:none;
  /* The panel's own box still must not swallow touches — only the tiles do. */
  font-size:13px;line-height:1.5;animation:dict-in .12s ease-out}@keyframes dict-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
/* The visible window onto the tiles. This is the element that clips: the panel
 * hands it whatever height is left after the grip strip, and anything taller
 * than that is cut off at this edge. Keeping the clip here rather than on the
 * panel means the grip keeps its own strip and never covers the last row's
 * buttons — an earlier version overlaid the grip on the tiles and hid them. */
.dict-view{flex:0 1 auto;min-height:0;overflow:hidden;
  /* Two grips sit in the panel now (one above, one below), so the tile window
   * has to give up their height plus the gaps. Without this the flex box shrank
   * the window by only one strip and the lower grip was pushed past the panel's
   * `max-height`, landing under the status bar. */
  max-height:calc(100vh - 96px - 34px - 34px - 8px - 6px)}
/** Tiles share one fixed-width column grid, so the row holds exactly as many as
 *  the window fits: one or two on a narrow reader, five on a 1180px one.
 *  `scrollbar-gutter:stable` reserves the vertical scrollbar's width from the
 *  start, so a sixth tile appearing (which makes the grid taller than the panel)
 *  no longer steals a column and drops the row from five tiles to four.
 *  210px is the largest column that still fits five across a 1180px window once
 *  that gutter is reserved: 5*210 + 4*10 = 1090 <= 1094.
 *  The grid is not height-capped: capping it made the tiles overflow their own
 *  container, which left the ↕ handle stranded in the middle of the second row
 *  instead of below the last tile. Growth is bounded by the panel instead, so
 *  the handle always sits directly under the last visible row. */
.dict-cards{padding:0;
  flex:0 0 auto;
  position:relative;
  z-index:1;
  pointer-events:auto;
  /* `auto-fill`, not `auto-fit`: the panel's width is now chosen from the tile
   * count (see `dictSizePanel`), so the grid must lay out exactly the columns
   * that fit and leave the rest of the track list empty. `auto-fit` collapses
   * the empty tracks and stretches the remaining tiles to fill the panel, which
   * would hide the one-column-per-tile growth the reader asked for.
   * `--dict-col` is the column width; `dictColWidth` reads it back, so the
   * number lives here only. */
  display:grid;grid-template-columns:repeat(auto-fill,var(--dict-col,190px));gap:10px;
  /* `grid-auto-rows:max-content` + `align-content:start` keeps every row at its
   * intrinsic height. Without them the grid compresses its rows to whatever
   * height the shrunken window offers, so dragging the handle down squashed the
   * tiles (measured: 5 cards at 38px tall) instead of cutting their lower half
   * away. `.dict-view` then does the clipping, which is the point of the handle. */
  grid-auto-rows:max-content;
  align-content:start}
/* The panel's writing surface. It lies *over* the whole panel so a stroke can
 * cross a tile as well as the gap between two, and it is `pointer-events:none`
 * on purpose: the panel must stay transparent to the finger so the reader can
 * still scroll the sentence behind it, and to the page's own tap layer so a
 * finger on a gap does nothing. The pen reaches this canvas through the panel's
 * capture-phase listeners instead of through hit-testing — see `dictInkLayer`.
 *
 * `touch-action` is `none` because the canvas has no gesture to defer to; a
 * stroke on it must never be read as a scroll of the page underneath.
 *
 * It is `position:fixed` in *viewport* coordinates, not a child of the panel,
 * because the reader asked for the ink to act as a screen overlay: dragging the
 * panel down to uncover a sentence must not drag the ink off what it underlines.
 * The strokes are stored in the panel's own px as before — only the canvas's
 * placement is viewport-anchored — so a stroke keeps the screen position it was
 * written at, and it survives the panel being closed entirely. */
/* The word panel's ink. It is a *page* canvas — a sibling of the page's own ink
   inside `.page` — so a stroke written over a word card scrolls and zooms with the
   sentence under it. Sizing and `padX` come from `applyScale`; see `dictInkCanvas`
   for why neither the panel nor a viewport overlay is right.
   `z-index:4` sits above the text layer (1) and the hit boxes (3) but below the
   page's own ink (5) while writing, so the reader's page strokes still read as the
   topmost layer; cards live in `.dict-panel` (z-index 60, on `body`) and stay above
   everything either way. */
.page canvas.dict-ink{position:absolute;left:0;top:0;z-index:4;
   pointer-events:none;touch-action:none}
/* 🔴 While the nib is down, the pen's hit target must be the *canvas*, not a card.
 *
 *  `pointer-events:none` above makes the canvas transparent to every pointer, so
 *  the pen's real target is whatever card it happens to be over — and the reader
 *  saw exactly that: `st-para-body`, `EM`, never the canvas. A card as the target
 *  is not merely cosmetic. A pen drag across live text is what builds a native
 *  selection range, which is the 「整屏蓝遮罩」 path, and a long-press over text is
 *  what raises WebKit's callout — both land on the card precisely because it is
 *  the target.
 *
 *  `body.pen-hold` is set the moment the nib touches down (see `setPenHeld` for
 *  why it follows the *contact* and not the armed tool), so gating on it hands the
 *  pen to the canvas for exactly the duration of a stroke and leaves the cards
 *  tappable by finger the rest of the time. `pointer-events:none` on a descendant
 *  cannot be undone by an ancestor's `auto`, but the reverse trap is subtler: an
 *  ancestor that is still `auto` catches the hit itself once the descendant goes
 *  transparent, so hiding only `.dict-cards` does not expose the canvas — it
 *  exposes `.dict-view`/`.dict-panel`. 🔴 The whole panel body has to go
 *  transparent, not just the cards.
 *
 *  🔴 The scope must be `body`, not `#stage`. `.dict-panel` is appended to `body`
 *  as a *sibling* of `#stage`, so a `#stage.pen-hold …` selector never matches it
 *  — the rule looked correct and did nothing, and the pen kept landing on
 *  `st-para-body`/`EM`.
 *
 *  The drag grip is exempt: it is not text, it has no stroke to receive, and the
 *  reader must keep being able to move the panel with the pencil in hand. A
 *  child's `auto` does override an ancestor's `none`, so pinning the grip back is
 *  enough and nothing else has to be re-enabled by hand.
 *
 *  The finger is unaffected: a touch and a pen are different pointers to the
 *  engine, and the panel stays tappable for the finger because this whole rule
 *  only exists while a *pen* contact is down. */
body.pen-hold .dict-panel,body.pen-hold .dict-panel *{pointer-events:none}
body.pen-hold .dict-panel .dict-grip,body.pen-hold .dict-panel .dict-grip *{pointer-events:auto}
/* With the cards transparent the pen falls through to the paper, where it would
 * land on the *page's* ink (`z-index:5` while writing) rather than on
 * `.dict-ink` (`z-index:4`) — the wrong layer, and the stroke would be filed as
 * page ink instead of word-panel ink. Raising `.dict-ink` above it for the
 * duration of the contact is what actually makes it the pen's target; the layer
 * order is restored the moment the nib lifts, so the reader's own page strokes
 * still paint over the panel's while both are at rest. */
body.pen-hold .page canvas.dict-ink{z-index:6;pointer-events:auto}
/* The word panel's fallback surface, used while the page tree has no box — i.e.
 * in study mode, where `#viewer{display:none}` collapses every `.dict-ink` rect
 * to 0x0 and the panel's ink would otherwise be sized to 2px and silently lose
 * every stroke (「单词区写不了字」). It is anchored to the viewport, in the same
 * coordinate space as `.dict-panel`, and is a *sibling* of the panel so dragging
 * the grip never slides the strokes with it. Sizing is done in JS
 * (`dictSyncInkSize`), which is the only place that knows the usable height. */
canvas.dict-ink.dict-own-ink{position:fixed;left:0;top:0;z-index:4;
    pointer-events:none;touch-action:none}
body.pen-hold canvas.dict-ink.dict-own-ink{z-index:6;pointer-events:auto}

/** The ↕ handle centred under the tiles. It is the only part of the panel that
 *  is not a tile, so the reader can drag the whole block down the screen and
 *  uncover the sentences the cards were covering. Deliberately high-contrast:
 *  a light pill on light paper disappears against the page. `margin-top:auto`
 *  keeps it on the panel's bottom edge even when the panel is taller than the
 *  tiles (after dragging up), so it never floats in the middle of the grid. */
.dict-grip{pointer-events:auto;cursor:ns-resize;touch-action:none;user-select:none;
  -webkit-user-select:none;-webkit-touch-callout:none;
  /* Normal flow, after the clipped tile window, so it always has its own strip
   * and never covers the last row of cards. `flex:0 0 auto` keeps it from being
   * squeezed when the panel is dragged down to its floor. */
  position:relative;align-self:center;flex:0 0 auto;
  margin:0 auto;
  display:flex;align-items:center;justify-content:center;
  width:96px;height:34px;border-radius:17px;
  background:#4a5462;border:1px solid #39424e;
  box-shadow:0 3px 10px rgba(0,0,0,.28)}
/* Two grips, one above and one below the tiles, both driving the same drag. A
 * single bottom grip was the first thing a tall entry pushed off the screen, so
 * the block could not be moved to uncover the sentence it was covering. */
.dict-grip-top{margin-bottom:8px}
.dict-grip-bottom{margin-top:6px}
/* The visual pill is only 76x22, but a fingertip needs ~44px of slop. The
   pseudo-element enlarges the touch target without changing the look or
   pushing the tiles, and it stays inside the panel so it never steals a
   tap meant for a word. */
.dict-grip::before{content:"";position:absolute;
  left:50%;top:50%;transform:translate(-50%,-50%);
  width:96px;height:44px}
.dict-grip:hover{background:#2f3742;border-color:#d9a24b}
.dict-grip:active{background:#242b34}
/* Two stacked chevrons, drawn with borders so no icon font or SVG is needed. */
.dict-grip-arrow{width:13px;height:15px;position:relative;opacity:1;
  touch-action:none;pointer-events:none;-webkit-user-select:none;user-select:none}
.dict-grip-arrow::before,.dict-grip-arrow::after{content:"";position:absolute;
  left:2px;width:8px;height:8px;border-right:2.5px solid #fff;border-bottom:2.5px solid #fff}
.dict-grip-arrow::before{top:0;transform:rotate(45deg)}
.dict-grip-arrow::after{bottom:0;transform:rotate(-135deg)}
.dict-dragging .dict-grip{cursor:ns-resize;background:#2f3742;border-color:#d9a24b}
.dict-dragging{cursor:ns-resize;user-select:none}
.dict-card{border:1px solid rgba(226,214,189,.6);border-radius:9px;padding:9px 10px 8px;
  background:rgba(255,253,247,.8);backdrop-filter:blur(3px);
  box-shadow:0 6px 18px rgba(0,0,0,.16);
  color:#1d2430;display:flex;flex-direction:column;min-width:0;
  /* Height must come from the content, never from the panel. `min-height:0`
   * here let the flex/grid squeeze a card down to 38px, so dragging the handle
   * down visibly crushed the tiles rather than hiding their lower half. */
  height:max-content;align-self:start}
/* Long entries (e.g. `close`) are taller than the panel. Their lower half is
 * meant to be clipped away by the panel while the reader drags the handle down,
 * so the body must not scroll — only the head and the actions stay pinned. */
.dict-body{flex:0 0 auto;min-height:0;overflow:hidden}
.dict-head,.dict-actions{flex:0 0 auto}
.dict-card-flash{animation:dict-card-flash .5s ease-out}
@keyframes dict-card-flash{from{background:#fdf0cf;border-color:#d9a24b}to{}}
.dict-head{display:flex;align-items:baseline;gap:8px;margin-bottom:4px}
.dict-word{font-size:16px;font-weight:700;letter-spacing:.2px;color:#8a5a12;
  font-family:Georgia,"Times New Roman",serif;word-break:break-word;min-width:0}
.dict-inflect{font-size:11px;color:#8a7a5a;font-style:italic}
.dict-close{margin-left:auto;border:0;background:transparent;cursor:pointer;
  font-size:18px;line-height:1;color:#9aa4b2;padding:0 2px;border-radius:6px;flex:0 0 auto}
.dict-close:hover{background:#f1ece0;color:#4a5462}
.dict-phon{display:flex;flex-wrap:wrap;gap:8px;margin:2px 0 7px;
  font-size:11.5px;color:#5d6b7e;font-family:Georgia,serif}
.dict-phon b{color:#b07d1e;font-size:10px;letter-spacing:.5px;margin-right:3px;
  font-family:system-ui,sans-serif}
.dict-row{display:flex;gap:7px;margin:5px 0;align-items:flex-start}
.dict-row .tag{flex:0 0 auto;width:19px;height:19px;border-radius:5px;
  display:inline-flex;align-items:center;justify-content:center;
  font-size:11px;font-weight:700;background:#e8f0fb;color:#2f6bb5;margin-top:2px}
.dict-row .tag-ko{background:#fceef0;color:#c0395a}
.dict-row p{margin:0;flex:1;min-width:0;word-break:break-word}
.dict-note{margin:6px 0 0;font-size:11px;color:#a08a6a;font-style:italic}
.dict-ex{margin-top:8px;border-top:1px dashed #e6dcc7;padding-top:7px}
.dict-ex h4{margin:0 0 4px;font-size:10.5px;font-weight:600;letter-spacing:.6px;
  color:#9aa4b2;text-transform:uppercase}
.dict-ex ul{margin:0;padding-left:15px;display:flex;flex-direction:column;gap:5px}
.dict-ex li::marker{color:#d9a24b}
.dict-ex p{margin:0;font-size:12px}
.dict-ex .ex-en{color:#2b3441}
.dict-ex .ex-ko{color:#7a6570;margin-top:1px}
.dict-actions{display:flex;gap:5px;margin-top:auto;padding-top:8px;flex-wrap:wrap;
  border-top:1px solid #f0e8d8}
.dict-actions button{flex:1 1 auto;border:1px solid #e4d8c0;background:#fff;
  border-radius:7px;padding:4px 7px;font-size:11.5px;cursor:pointer;color:#4a5462;
  white-space:nowrap}
.dict-actions button:hover{background:#fdf6e8;border-color:#d9a24b;color:#8a5a12}

/* ----------------------------------------------------- text highlight bar */
/* A single strip that rides just above the selected text: ✓, the ten everyday
   colours, then ＋ 更多颜色, 🔍 搜索, and finally ✕ 删除 / ⧉ 复制. It is kept to
   one row and allowed to scroll, because a strip that wrapped would grow taller
   than the line it describes. */
#hl-bar{position:fixed;z-index:70;display:none;align-items:center;
  padding:6px;border-radius:999px;background:#fffdf7;
  border:1px solid #e2d6bd;box-shadow:0 10px 30px rgba(0,0,0,.28);
  animation:hl-in .11s ease-out;max-width:calc(100vw - 12px)}
#hl-bar.on{display:flex}
@keyframes hl-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
/* The row holds the whole palette, so it is wider than any phone. `min-width:0`
   is what lets it shrink inside the rounded pill and scroll instead of pushing
   the pill past the screen edge — a flex child defaults to its content width. */
#hl-bar .hl-scroll{display:flex;align-items:center;gap:6px;
  flex:1 1 auto;min-width:0;max-width:100%;
  overflow-x:auto;overflow-y:hidden;scrollbar-width:none;
  -webkit-overflow-scrolling:touch;overscroll-behavior-x:contain;
  touch-action:pan-x}
#hl-bar .hl-scroll::-webkit-scrollbar{display:none}
#hl-bar .hl-sw{flex:0 0 auto;width:32px;height:32px;border-radius:50%;
  border:2px solid rgba(0,0,0,.10);cursor:pointer;padding:0;
  transition:transform .12s,box-shadow .12s;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent}
#hl-bar .hl-sw:hover{transform:scale(1.14)}
/* The dot the selection already wears, so the strip reports as well as paints. */
#hl-bar .hl-sw.hl-dot.sel{box-shadow:0 0 0 2px #fffdf7,0 0 0 4px #8a5a12}
/* ✓ = the colour the dragged words already carry. A hollow ring (`.empty`) means
   the sweep covers un-highlighted text, so the tick is reporting "no colour yet"
   rather than claiming a tint that is not there. */
#hl-bar .hl-sw.hl-cur{position:relative;border-color:rgba(0,0,0,.16)}
#hl-bar .hl-sw.hl-cur::after{content:'✓';position:absolute;inset:0;
  display:flex;align-items:center;justify-content:center;
  font:700 16px/1 inherit;color:#3d3222;text-shadow:0 1px 2px rgba(255,255,255,.85)}
#hl-bar .hl-sw.hl-cur.empty{background:repeating-linear-gradient(135deg,
  #fff 0 5px,#f0ece2 5px 10px)}
#hl-bar .hl-sw.hl-cur.empty::after{content:'';width:14px;height:14px;
  border:2px solid #b9ae98;border-radius:50%;position:static;margin:auto}
#hl-bar .hl-sw.hl-plus{background:#fff;color:#8a5a12;display:flex;
  align-items:center;justify-content:center;font:400 18px/1 inherit;
  border-color:#dcd0b6}
#hl-bar .hl-sw.hl-plus:hover{background:#fdf6e8;border-color:#d9a24b}
/* 🔍 = 搜索文章. Grouped with the colours but separated by a rule, because it
   navigates rather than paints and should not read as an eleventh colour. */
#hl-bar .hl-sw.hl-find{background:#fff;display:flex;align-items:center;
  justify-content:center;font-size:15px;line-height:1;border-color:#dcd0b6;
  margin-left:6px;position:relative}
#hl-bar .hl-sw.hl-find::before{content:'';position:absolute;left:-7px;top:4px;
  bottom:4px;width:1px;background:#e2d6bd}
#hl-bar .hl-sw.hl-find:hover{background:#fdf6e8;border-color:#d9a24b}
/* ✕ 删除 and ⧉ 复制, trailing the strip. They act on the highlight the reader
   tapped rather than on a colour, so they are ruled off from the swatches — and
   a tap cancels itself out if no highlight is being edited, which is why the
   tint is muted (a colour the finger can slide past must never look like a
   swatch it should try). */
#hl-bar .hl-sw.hl-act{background:#fff;display:flex;align-items:center;
  justify-content:center;font:600 15px/1 inherit;border-color:#dcd0b6}
#hl-bar .hl-sw.hl-del{color:#b4443a;margin-left:6px;position:relative}
#hl-bar .hl-sw.hl-del::before{content:'';position:absolute;left:-7px;top:4px;
  bottom:4px;width:1px;background:#e2d6bd}
#hl-bar .hl-sw.hl-del:hover{background:#fdeeeb;border-color:#e0554d;color:#a5372e}
#hl-bar .hl-sw.hl-cpy{color:#8a5a12}
#hl-bar .hl-sw.hl-cpy:hover{background:#fdf6e8;border-color:#d9a24b}

/* The search panel: one field, a hit counter, ▲▼ stepping, and the hit list.
   Results are listed in reading order so a tap is a jump, not a guess. */
#hl-find{position:fixed;z-index:76;top:8px;left:50%;transform:translateX(-50%);
  display:none;flex-direction:column;width:min(560px,calc(100vw - 16px));
  max-height:70vh;border-radius:14px;background:#fffdf7;border:1px solid #e2d6bd;
  box-shadow:0 14px 40px rgba(0,0,0,.30);overflow:hidden;
  animation:hl-in .11s ease-out}
#hl-find.on{display:flex}
#hl-find-top{display:flex;align-items:center;gap:6px;padding:8px}
#hl-find-input{flex:1 1 auto;min-width:0;height:34px;padding:0 10px;
  border:1px solid #e2d6bd;border-radius:8px;background:#fff;color:#3a4351;
  font:14px/1 inherit;font-family:inherit;outline:none}
#hl-find-input:focus{border-color:#d9a24b}
#hl-find-count{flex:0 0 auto;min-width:38px;text-align:center;color:#8a5a12;
  font:600 12px/1 inherit;-webkit-user-select:none;user-select:none}
#hl-find-top button{flex:0 0 auto;width:32px;height:32px;border-radius:8px;
  border:1px solid #e2d6bd;background:#fff;color:#4a5462;cursor:pointer;
  font-size:12px;line-height:1;padding:0;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent}
#hl-find-top button:hover{background:#fdf6e8;border-color:#d9a24b;color:#8a5a12}
#hl-find-list{overflow-y:auto;-webkit-overflow-scrolling:touch;
  border-top:1px solid #efe7d5}
#hl-find-list:empty{display:none}
#hl-find-list .hl-find-row{display:block;width:100%;text-align:left;
  padding:9px 12px;border:0;border-bottom:1px solid #f4eddd;background:transparent;
  color:#3a4351;font:13.5px/1.5 inherit;font-family:inherit;cursor:pointer;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent}
#hl-find-list .hl-find-row:hover{background:#fdf6e8}
#hl-find-list .hl-find-row.sel{background:#fdf1d6;color:#5a4415}
#hl-find-list .hl-find-empty{padding:12px;color:#9a8f7c;font-size:13px;
  text-align:center}
/* The current hit, flashed so the eye lands on the right word in a long
   sentence. `position:relative` keeps the ring from nudging the line box. */
#study mark.st-find{background:#ffe9a8;border-radius:3px;color:inherit;padding:0}
#study mark.st-find.hit{background:#ffd24d;
  box-shadow:0 0 0 2px rgba(217,162,75,.85)}

/* The palette popover. The strip carries the ten everyday colours; this holds
   the extra vivid set, which is far too long for one row, so it wraps into a
   grid. The panel stays compact because it is anchored above the strip and
   would otherwise cover the very line being coloured. */
#hl-panel{position:fixed;z-index:71;display:none;
  padding:8px;border-radius:14px;background:#fffdf7;border:1px solid #e2d6bd;
  box-shadow:0 10px 30px rgba(0,0,0,.28);animation:hl-in .11s ease-out;
  max-width:calc(100vw - 12px)}
#hl-panel.on{display:block}
#hl-panel .hl-grid{display:grid;grid-template-columns:repeat(8,34px);
  gap:6px;justify-content:center;padding:2px 0}
#hl-panel .hl-wrap{position:relative}
#hl-panel .hl-sw{width:34px;height:34px;border-radius:50%;
  border:2px solid rgba(0,0,0,.10);cursor:pointer;padding:0;
  transition:transform .12s,box-shadow .12s;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent}
#hl-panel .hl-sw:hover{transform:scale(1.12)}
#hl-panel .hl-sw.sel{box-shadow:0 0 0 2px #fffdf7,0 0 0 4px #8a5a12}
/* Slash swatch = remove the highlight. */
#hl-panel .hl-sw.none{background:linear-gradient(135deg,#fff 46%,#e0554d 46%,#e0554d 54%,#fff 54%)}
/* Diamond swatch = the reader's own colour. */
#hl-panel .hl-sw.custom{background:#fff;color:#8a5a12;display:flex;
  align-items:center;justify-content:center;font-size:14px;line-height:1}
/* 删除 + 复制 along the foot of the popover, so ＋ is a complete action set
   rather than a colour list the reader has to close again. */
#hl-panel .hl-acts{display:flex;gap:6px;margin-top:8px;
  padding-top:8px;border-top:1px solid #ece2cc}
#hl-panel .hl-pbtn{flex:1 1 0;height:32px;border-radius:9px;background:#fff;
  border:1px solid #dcd0b6;cursor:pointer;font:600 13px/1 inherit;
  display:flex;align-items:center;justify-content:center;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent}
#hl-panel .hl-pdel{color:#b4443a;border-color:#eccfc9}
#hl-panel .hl-pdel:hover{background:#fdeeeb;border-color:#e0554d;color:#a5372e}
#hl-panel .hl-pcpy{color:#8a5a12}
#hl-panel .hl-pcpy:hover{background:#fdf6e8;border-color:#d9a24b}
/* 高亮涂色完成后，再轻触那段涂色文字: a small 删除 + 复制 pill. 删除 leads and 复制
   sits to its right, so the destructive action is the one the finger reaches
   first and the common one stays a short slide away. Both carry a glyph only —
   labels would make the pill wide enough to cover the words it describes. */
#hl-copy-bar{position:fixed;z-index:70;display:none;align-items:center;gap:4px;padding:4px;
  border-radius:999px;background:#fffdf7;border:1px solid #e2d6bd;
  box-shadow:0 10px 30px rgba(0,0,0,.28);animation:hl-in .11s ease-out}
#hl-copy-bar.on{display:flex}
#hl-copy-bar .hl-copy-btn{width:36px;height:36px;border:1px solid #e4d8c0;
  background:#fff;border-radius:50%;padding:0;
  font:600 16px/1 inherit;font-family:inherit;cursor:pointer;color:#4a5462;
  display:flex;align-items:center;justify-content:center;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent}
#hl-copy-bar .hl-copy-btn:hover{background:#fdf6e8;border-color:#d9a24b;color:#8a5a12}
#hl-copy-bar .hl-del-btn{color:#b4443a;border-color:#eccfc9}
#hl-copy-bar .hl-del-btn:hover{background:#fdeeeb;border-color:#e0554d;color:#a5372e}
/* A hairline between 删除 and 复制 so the two never read as one wide target. */
#hl-copy-bar .hl-cpy-btn{margin-left:4px}
#hl-copy-bar .hl-cpy-btn::before{content:'';position:absolute;left:-4px;top:6px;
  bottom:6px;width:1px;background:#e6dcc6}
#hl-copy-bar .hl-cpy-btn{position:relative}
/* The picked colour is written with `<mark>` wrappers so it survives a
   re-layout and lands in the PDF export. */
#study mark.hl-mark{padding:0;border-radius:3px;color:inherit;
  /* `<mark>` carries a UA default of `background-color:yellow` that fills the
     whole box. Leaving it in place paints two stacked colours per highlight —
     the yellow slab plus our band on top — which is the reader's 「高亮颜色上下
     分成了两个颜色」. It has to be cleared before the band means anything. */
  background-color:transparent;
  /* The sweep is a background band rather than a full-box fill, because a
     full-height fill on an inline mark reads as a bar instead of a highlighter. */
  background-image:linear-gradient(var(--hl-fill,transparent),var(--hl-fill,transparent));
  background-repeat:no-repeat;
  /* The band is sized and positioned against the mark's own box, which for an
     inline element is the *font content box* (ascent + descent, e.g. 23px at
     19px Inter) and NOT the line box — measured identical at `line-height`
     30.4px, 40px and 64px. Each mark holds exactly one line because
     `hlSplitMarkByLine` cuts wrapped highlights apart, so the band needs no
     `box-decoration-break` and no line-box compensation.
     `0.10em` off the bottom puts the band's lower edge just under the baseline
     (the baseline sits `descent` above the box bottom) and `0.96em` of height
     carries it up to just past the cap height, so the sweep covers the glyphs
     without spilling into the leading above or the descender well below. */
  background-position:left 0 bottom .10em;background-size:100% .96em;
  /* Redundant now that a wrapped highlight is cut into one `<mark>` per line
     (see `hlSplitMarkByLine`), but it keeps the band right on engines that do
     implement it while a mark is briefly wrapped between layouts. */
  -webkit-box-decoration-break:clone;box-decoration-break:clone}
/* The band is our own layer, so the browser's selection wash would only muddy
   it; the reader can still see the selection through the tint. */
#study mark.hl-mark::selection{background:transparent}

/* ------------------------------------------------------ 涂写工具栏 (pen bar) */
/* A GoodNotes-style floating pill that appears only while a writing tool is
   armed. It is bottom-centred and sized for a stylus: every target is at least
   38px on its short side, because a pen tip is far less precise than a mouse
   and misses are destructive (they set down ink instead of pressing a button).
   The strip scrolls horizontally rather than wrapping so it never grows tall
   enough to sit on top of the words being copied. */
#pen-bar{position:fixed;z-index:75;left:8px;top:0;display:flex;align-items:center;gap:4px;
  padding:6px 8px;border-radius:999px;background:#fffdf7;
  border:1px solid #e2d6bd;box-shadow:0 12px 34px rgba(0,0,0,.26);
  max-width:96vw;overflow-x:auto;overflow-y:hidden;
  scrollbar-width:none;animation:pbar-in .13s ease-out;
  /* The bar floats over the page, so it must not hand its own drags to the
     ink canvas underneath; only its buttons take pointer events. */
  touch-action:none}
#pen-bar::-webkit-scrollbar{display:none}
#pen-bar.hidden{display:none}
@keyframes pbar-in{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
#pen-bar .pb-btn{flex:0 0 auto;width:44px;height:44px;border:0;border-radius:12px;
  background:transparent;color:#4a5462;font:400 21px/1 inherit;display:flex;
  align-items:center;justify-content:center;cursor:pointer;padding:0;
  transition:background .13s,color .13s,transform .1s}
#pen-bar .pb-btn:hover{background:#f3ead6}
#pen-bar .pb-btn:active{transform:scale(.93)}
#pen-bar .pb-btn[active]{background:#f0e2c2;color:#8a5a12;
  box-shadow:inset 0 0 0 1.5px #d9a24b}
#pen-bar .pb-btn[disabled]{opacity:.32;cursor:default}
#pen-bar .pb-btn[disabled]:hover{background:transparent}
#pen-bar .pb-btn[disabled]:active{transform:none}
#pen-bar .pb-sep{flex:0 0 auto;width:1px;height:28px;background:#e6dcc7;margin:0 3px}
/* Pen width: a GoodNotes-style slider whose thumb is a dot that grows with the
   width. It runs in one of two scales — see the 字号/毫米 toggle below — and the
   bar never shows a bare number without a unit. */
#pen-bar .pb-widthwrap{flex:0 0 auto;position:relative;height:44px;display:flex;
  align-items:center;padding:0 2px;gap:6px}
#pen-bar .pb-wlabel{flex:0 0 auto;min-width:74px;text-align:right;
  font:700 11.5px/1.2 inherit;color:#6b5a3a;font-variant-numeric:tabular-nums;
  white-space:nowrap}
#pen-bar .pb-width{-webkit-appearance:none;appearance:none;width:120px;height:44px;
  background:transparent;margin:0;padding:0;cursor:pointer;touch-action:none}
#pen-bar .pb-width::-webkit-slider-runnable-track{height:4px;border-radius:2px;
  background:linear-gradient(90deg,#cfc3a8,#8a7a5c)}
#pen-bar .pb-width::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
  width:16px;height:16px;border-radius:50%;background:#fffdf7;
  border:2px solid #8a5a12;box-shadow:0 1px 4px rgba(0,0,0,.28);margin-top:-6px}
#pen-bar .pb-width::-moz-range-track{height:4px;border-radius:2px;
  background:linear-gradient(90deg,#cfc3a8,#8a7a5c)}
#pen-bar .pb-width::-moz-range-thumb{width:16px;height:16px;border-radius:50%;
  background:#fffdf7;border:2px solid #8a5a12;box-shadow:0 1px 4px rgba(0,0,0,.28)}
/* The dot that reports the current width. It sits under the thumb and is
   pointer-transparent, so only the slider itself takes the drag. */
#pen-bar .pb-thumb{position:absolute;left:8px;top:50%;transform:translate(-50%,-50%);
  border-radius:50%;background:rgba(138,90,18,.22);pointer-events:none}
/* Nib picker (钢笔 / 圆珠笔 / 画笔). Three small chips; each carries a CSS-drawn
   nib silhouette so the choices read apart at a glance. */
#pen-bar .pb-nibs{flex:0 0 auto;display:flex;align-items:center;gap:3px}
#pen-bar .pb-nibs.hidden{display:none}
#pen-bar .pb-nib{flex:0 0 auto;width:40px;height:40px;border-radius:11px;border:0;
  padding:0;background:transparent;cursor:pointer;display:flex;align-items:center;
  justify-content:center;transition:background .13s,transform .1s}
#pen-bar .pb-nib:hover{background:#f3ead6}
#pen-bar .pb-nib:active{transform:scale(.93)}
#pen-bar .pb-nib[active]{background:#f0e2c2;box-shadow:inset 0 0 0 1.5px #d9a24b}
#pen-bar .pb-nib i{display:block;background:#4a5462}
#pen-bar .pb-nib[active] i{background:#8a5a12}
/* 钢笔 — a slim tapered nib: sharp point on top, narrow shoulders. */
#pen-bar .pb-nib i.pb-nib-fountain{width:13px;height:22px;
  clip-path:polygon(50% 0,78% 26%,78% 68%,50% 100%,22% 68%,22% 26%)}
/* 圆珠笔 — a plain even barrel with a flat blunt end: solid, no narrow point. */
#pen-bar .pb-nib i.pb-nib-ballpoint{width:13px;height:22px;
  clip-path:polygon(0 0,100% 0,100% 62%,72% 62%,72% 100%,28% 100%,28% 62%,0 62%)}
/* 画笔 — a wide splayed brush head, clearly the broadest of the three. */
#pen-bar .pb-nib i.pb-nib-brush{width:18px;height:22px;
  clip-path:polygon(36% 0,64% 0,72% 42%,96% 100%,4% 100%,28% 42%)}
/* 画笔稳定性 — four discrete stops rather than a track: a stylus mid-sentence
   cannot hit a hairline thumb, and "off / a bit / a lot / very" is the whole
   decision anyway. */
#pen-bar .pb-stab{flex:0 0 auto;display:flex;align-items:center;gap:2px}
#pen-bar .pb-stab.hidden{display:none}
#pen-bar .pb-stab-label{font-size:11px;color:#8a7b5e;margin-right:3px}
#pen-bar .pb-stab-b{flex:0 0 auto;min-width:30px;height:34px;border-radius:9px;
  border:0;padding:0 5px;background:transparent;cursor:pointer;font-size:11px;
  line-height:1;color:#6b6250;letter-spacing:1px;
  transition:background .13s,transform .1s}
#pen-bar .pb-stab-b:hover{background:#f3ead6}
#pen-bar .pb-stab-b:active{transform:scale(.93)}
#pen-bar .pb-stab-b[active]{background:#f0e2c2;color:#8a5a12;font-weight:600;
  box-shadow:inset 0 0 0 1.5px #d9a24b}
/* 蓝牙笔 switch. Mirrors the 稳定 row so the "how the stroke is captured"
   controls read as one family, but the control is a single two-position switch
   rather than two buttons: the question is whether the pencil is on the table,
   and a switch answers that at a glance where a pair of chips does not. */
#pen-bar .pb-src{flex:0 0 auto;display:flex;align-items:center;gap:2px}
#pen-bar .pb-src.hidden{display:none}
#pen-bar .pb-src-sw{flex:0 0 auto;display:flex;align-items:center;gap:5px;
  height:34px;border-radius:999px;border:0;padding:0 9px 0 5px;
  background:#f1ece0;cursor:pointer;font-size:12px;line-height:1;color:#6b6250;
  transition:background .13s}
#pen-bar .pb-src-sw:hover{background:#f3ead6}
#pen-bar .pb-src-sw:active{transform:scale(.96)}
#pen-bar .pb-src-sw .knob{display:block;width:22px;height:22px;border-radius:50%;
  background:#fffdf7;box-shadow:0 1px 3px rgba(90,70,20,.3);
  transition:transform .16s cubic-bezier(.4,1.4,.5,1),background .13s}
#pen-bar .pb-src-sw[on]{background:#f0e2c2;color:#8a5a12;font-weight:600}
#pen-bar .pb-src-sw[on] .knob{transform:translateX(4px);background:#d9a24b}
#pen-bar .pb-src-sw .pb-src-state{min-width:1.2em;text-align:center}
/* 自动识别 toggle: lights up while recognition is on, because a silent
   "sometimes my strokes change" is the kind of setting a reader forgets. The
   shape *picker* that used to sit beside it is gone — the shapes are chosen from
   the top toolbar, so the panel only carries the on/off. */
#pen-bar .pb-recog[active],
#pen-bar .pb-scrub[active]{background:#f0e2c2;box-shadow:inset 0 0 0 1.5px #d9a24b}
/* 涂鸦擦除 is destructive, so its armed state is tinted warm rather than the
   neutral gold the other toggles use — the reader should be able to tell at a
   glance that the pen will start rubbing ink out. */
#pen-bar .pb-scrub[active]{background:#f6e0d6;box-shadow:inset 0 0 0 1.5px #c9704a;
  color:#a8442a}
/* Colour swatches. `.pb-more` is the rainbow chip that opens the native picker
   for a colour the six presets do not cover. */
#pen-bar .pb-sw{flex:0 0 auto;width:38px;height:38px;border-radius:50%;
  border:0;padding:0;background:none;cursor:pointer;display:flex;
  align-items:center;justify-content:center;transition:transform .12s}
#pen-bar .pb-sw:hover{transform:scale(1.12)}
#pen-bar .pb-sw i{display:block;width:26px;height:26px;border-radius:50%;
  border:2px solid rgba(0,0,0,.12);box-shadow:inset 0 -2px 4px rgba(0,0,0,.10)}
#pen-bar .pb-sw[active] i{box-shadow:0 0 0 2px #fffdf7,0 0 0 4px #8a5a12;
  border-color:transparent}
#pen-bar .pb-sw.pb-more i{background:conic-gradient(#c0392b,#a86a12,#0d6b35,#1f4f9e,#b45ad0,#c0392b)}
#pen-bar .pb-note{flex:0 0 auto;padding:0 10px 0 4px;font:600 12px/1.25 inherit;
  color:#8a7a5c;white-space:nowrap}
/* On a short window the bar would eat the reading area, so trim it to the
   essentials there rather than letting it crowd the page. */
@media (max-height:560px){
  #pen-bar .pb-note{display:none}
}

/* ---------- study pane ---------- */
/* The pane *is* the reader. It reproduces the browser edition's article view
   (justified English on a 40px line grid with translations lifted into an
   absolutely positioned overlay) as live controls — typeable answer boxes,
   clickable citations, per-sentence audio — instead of the PDF's painted text. */
/* The pane replaces the PDF viewer outright: the reading column is the only
   content, exactly like the browser edition, where a page is HTML text and a
   white page canvas never appears beside it. `#viewer` keeps its markup and
   its scroll position for the paths that still print to it, but it is taken
   out of flow so it can neither claim width nor push the pane around.
   `:has()` rather than a class set by the toolbar handler, so every path that
   reveals the pane (state restore, programmatic open, a resize) lands on the
   same layout and the two can never desync.
   Chrome 74 (the Android WebView) has no `:has()`, so the same two rules are
   also written against `body.study-on`, which `syncStudyLayout()` keeps in
   step with `#study`'s `hidden` class on every path that changes it. */
#stage:has(> #study:not(.hidden)){display:flex;align-items:stretch}
#stage:has(> #study:not(.hidden))>#viewer{display:none}
body.study-on #stage{display:flex;align-items:stretch}
body.study-on #viewer{display:none}
/* The pane is the whole stage now. The reading column is produced by the
   pane's own symmetric padding rather than by `margin:auto` on each block:
   every per-block rule written as the `margin:0 0 Npx` shorthand (`.st-title`,
   `.st-scripture`, `.st-answerbox`, `.st-heading`, `.st-reviewhead`, …) resets
   `margin-left`/`margin-right` to 0, so auto-margins on the blocks were
   silently dropped for some kinds and kept for others — which is exactly the
   misalignment where the title started at the left edge while the questions
   and paragraphs sat centred. `align-items:center` on the pane and a
   `width:100%` on the blocks cannot be clobbered by any shorthand.
   `min-height:0` is load-bearing: a flex item defaults to `min-height:auto`,
   which would let the pane grow to its content height and get clipped by
   `#stage`'s `overflow:hidden` instead of scrolling internally.
   `touch-action` must be `manipulation`, not `pan-x pan-y`. The pan-only value
   declares the pane as a pure scroller, so the browser cancels the pointer
   gesture with a `pointercancel` as soon as a finger travels — and a drag then
   never starts a selection, leaving the reader with no colours at all.
   `manipulation` still pans and pinch-zooms while letting the drag build a
   range. `#study.no-pan` below restores `none` while a pen tool is armed. */
#study{flex:1 1 auto;max-width:none;min-height:0;overflow:auto;-webkit-overflow-scrolling:touch;
  position:relative;
  scrollbar-gutter:stable both-edges;
  overscroll-behavior:contain;touch-action:manipulation;
  background:var(--bg2);
  padding:14px 16px calc(40px + env(safe-area-inset-bottom));
  font-family:var(--font-han);
  font-size:19px;
  line-height:1.75}
/* 🔴 THE MARGIN BARS — 「左右各 16px 留白了，可以保持不变，把留白的区变颜色，
   不随背景改变而改变，可以比顶栏工具栏更粉一些，肉眼可变就行」.
 *
 * The reader wanted the writing area to *look* bounded. It already is: the column
 * is 1334px inside a 1366px pane, so there are 16px of margin on each side
 * (measured — `colLeft 16, colRight 16, colX 16, colW 1334, paneLeft 0,
 * paneRight 0`). But that margin was transparent `padding`, and 16px at a 19px
 * font reads as nothing at all, so the column looked flush against the left edge.
 *
 * 🔴 ONLY A COLOUR CHANGES. `layoutW` MUST STAY 1334.
 *
 * The tempting fix — widening the padding — would move `studyZoomLayoutWidth()`,
 * and that number is load-bearing in four places at once: `studyCenterShift`'s
 * horizontal centring, `snapStudyZoom`'s device-pixel quantisation, the ink
 * layer's `pageW`/`contentW`, and the canvas back-store size. Changing it would
 * silently re-derive all four, which is exactly the class of change that has cost
 * this project whole sessions. Colouring the existing padding touches none of
 * them: the padding box is unchanged, so every measurement above stays valid.
 *
 * 🔴 IT MUST OUTRANK `body.has-bg #study`, AND THIS IS NOT OPTIONAL.
 *
 * A first attempt put the gradients on the bare `#study` rule. `applyBackground()`
 * (app.js:753) toggles `body.has-bg` for every non-"none" wallpaper, and
 * app.css:212 then applies
 *
 *     body.has-bg #study{position:relative;z-index:2;background:transparent}
 *
 * `background` is the SHORTHAND, so it resets `background-image` to `none` and
 * silently deleted both gradients — the reader saw 「看不到左右的颜色改变，左侧
 * 还是文字紧贴边框」 the moment a wallpaper was on, which is the default. The
 * selector below is `body.has-bg #study` **with the class repeated** to reach
 * specificity (0,3,1) against that rule's (0,2,1), so it wins regardless of order,
 * and it re-states `position`/`z-index` so nothing else that rule was doing is lost.
 *
 * The bars are painted with `background-image` gradients on `#study` itself rather
 * than with pseudo-elements, because `#study` is the *scroller* — a pseudo-element
 * anchored to its padding box would scroll away with the content, whereas the
 * background of a scroll container is fixed to the padding box and stays put.
 * `background-clip: padding-box` keeps the colour out of the scrollbar gutter.
 *
 * `--margin-bar` is declared with a literal, NOT via a theme token, because the
 * reader asked for it to be 「不随背景改变而改变」 — it must survive
 * `body.on-paper` and every paper texture. It is deliberately pinker than the
 * chrome (`--bg2:#f6ead0` is a flat cream); this is `#f3d9d3`, the same family
 * with red pulled up and blue pulled down — clearly visible side by side, still
 * quiet enough not to compete with the text.
 *
 * 🔴 THE BARS ARE THE PAPER'S EDGE, SO THEY SCALE *INVERSELY* — `16px / z`.
 *
 * The reader's model, stated twice and unambiguous:
 *
 *     「放大时被文字挤掉（变窄甚至消失），缩小时变宽」
 *     「这样看起来我们的文章是有边界的」
 *
 * That is a sheet of paper, not a screen frame. Zooming *out* is leaning back:
 * you see more of the sheet, so the blank margin beside the text reads WIDER.
 * Zooming *in* is putting your eye against the page: the edge leaves the field
 * of view, so the margin narrows and eventually vanishes. The bar width is
 * therefore `16px / z`, not `16px`.
 *
 * The first build had the plain `16px`. On device that was wrong in both
 * directions at once — the reader saw no change when zooming out ("缩小的时侯
 * 左右的空白没被放大") and no change when zooming in ("放大时左右空白没被挤走").
 *
 * 🔴 `var(--study-zoom)`, NOT `--study-zoom-live`. `syncStudyZoom()` (app.js:2245)
 * writes `--study-zoom` on `#study` for every commit — the pinch's `finish()`,
 * 放大/缩小, 适应宽度, a restored state — and it is inheritable, so this rule reads
 * it directly with no JS involvement. `--study-zoom-live` tracks the fingers
 * *during* a pinch, but writing it is deliberately suppressed on the hot path
 * (measured 85.1ms/frame against 20.4ms; see app.js:2414). Using it here would
 * either reintroduce that write or leave the bars a frame stale, so the committed
 * value is the right one: the bars step at the same instant the text does.
 *
 * `16px / z` is only legal because the divisor is a bare NUMBER, which is exactly
 * how `String(state.studyZoom || 1)` writes it ("1.2", not "1.2px"). Verified in
 * the engine at z ∈ {0.25, 0.5, 1, 1.2, 2, 4}: every one resolved to the exact
 * 16/z. A unit on the divisor would make the whole `calc()` invalid and the bars
 * would silently disappear, so the number form is load-bearing.
 *
 * 🔴 AND IT MUST NOT BE TRIMMED BY THE CENTRING SHIFT. `--study-zx` slides the
 * column to centre it while it is narrower than the pane. The bars are painted on
 * `#study`'s own background, which does not move, so they stay pinned to the
 * padding box's left and right edges — the sheet's edges — while the column
 * slides inside them. That is the correct behaviour for a paper edge and it is
 * why the bars are on the scroller's background rather than on the column. */
#study,
body.has-bg.has-bg #study{
  --margin-bar:#f3d9d3;
  --margin-bar-w:calc(16px / var(--study-zoom, 1));
  background-image:
    linear-gradient(var(--margin-bar), var(--margin-bar)),
    linear-gradient(var(--margin-bar), var(--margin-bar));
  background-size:var(--margin-bar-w) 100%, var(--margin-bar-w) 100%;
  background-position:left top, right top;
  background-repeat:no-repeat;
  background-clip:padding-box, padding-box;
}
/* The reader makes its own highlight gesture and its own colour strip, so the
   native text-selection UI must never appear over the reading column. Three
   things summon it and all three are switched off here:
   `-webkit-touch-callout` is the long-press menu itself (复制/分享/全选/搜索);
   `user-select:none` stops the selection that would show the drag handles;
   `-webkit-touch-callout` on descendants is inherited, but `user-select` is
   not, so the column's children opt out explicitly further down.
   Text must stay *selectable* for `window.getSelection()` to report the range
   the highlight gesture depends on — hence `-webkit-user-select:text` rather
   than the `none` that would break highlighting altogether. */
#study,#study *{-webkit-touch-callout:none}
#study{-webkit-user-select:text;user-select:text}
#study .st-block,#study .st-para,#study .st-para-body,#study .st-title{user-select:text}
#study .st-play,#study .st-tg,#study .st-parnum,#study .st-figure{-webkit-user-select:none;user-select:none}
/* `zoom` is used for the PDF reader, but it MUST NOT be used here: Chrome 74's
   hit-testing ignores `zoom`, so every control inside this wrapper (the answer
   box 粘贴/清空 buttons, the review section) is unclickable — `elementFromPoint`
   never returns them and taps fall through to the container. `transform:scale`
   hit-tests correctly.
   `transform` leaves layout alone, so the wrapper still occupies its unscaled
   box. Three corrections follow: widen it by `1/zoom` and pull it left so the
   scaled result fills the pane and stays centred, and pull its bottom up by the
   fraction the scale removed so `#study` keeps scrolling to the true end of the
   article instead of leaving a `(1 - zoom)`-tall blank tail.
   Centring lives on the wrapper, not the pane, because `zoom` on the pane
   itself would also scale its scrollbar and padding. `position:relative` makes
   the wrapper the containing block for the ink canvas, which is a child of it
   so that the scale carries the ink along with the words it was written on.
   The column is centred here, not by `margin-inline:auto` on the blocks: rules
   such as `#study .st-title{margin:0 0 14px}` reset the whole `margin` shorthand
   and would wipe that out. `align-items:center` ignores `margin` entirely, so
   every block type centres no matter what the shorthand rules say. */
/* `--study-zoom-live` is the scale actually painted; `--study-zoom` is the
   committed one. They are equal at rest.
   The zoom is applied as the `scale()` alone. There is deliberately no
   `width: calc(100% / var(--study-zoom))` here: shrinking the *layout* column as
   the zoom grew meant magnifying the article also re-wrapped all 47 translation
   bodies to a much narrower column, and the re-wrapped picture could never match
   the transform-only preview the fingers were dragging. Measured on the tablet at
   zoom 5.049: the column went 1066px -> 211px, `#study.scrollHeight` read 89081
   mid-gesture but 248251 after the lift (2.79x), and the reader saw the page
   replaced under their hands. With a plain `width:100%` the column stays 1066px
   and 15523px tall at every zoom from 1 to 8, `scrollHeight` is exactly linear,
   the preview and the commit apply the same transform to the same DOM — so they
   agree by construction — and the zoom costs no layout at all: the 51.6ms
   re-wrap and the 86ms commit are gone. This is PDF/image-reader semantics:
   magnify, then pan.
   `top left` / `flex-start` rather than `top center` / `center`: a transform's
   overflow does not feed `scrollWidth`, so a *centred* scaled column overflows
   symmetrically and the left half is unreachable — the browser cannot scroll to
   negative offsets, and at 526% the pane could only reach 1671px of a 4160px
   column with 1653px clipped off each edge. Anchoring the origin at the left
   puts all the overflow on the positive side, so `scrollWidth` (4178px) covers
   the whole column (4160px) and every word is reachable.

   🔴 `width:100%` IS LOAD-BEARING — DO NOT NARROW IT TO CREATE A MARGIN.
   Tried and reverted: `width:calc(100% - 2 * var(--margin-bar-w))` (1302px at
   zoom 1) was introduced to manufacture centring room, but it re-wraps all 47
   translation bodies to a 30px-narrower column. The article's line breaks are
   reader-visible content and must not shift as a side effect of a margin change.
   Centring room is not needed anyway: at zoom 1 the sheet is 1334 in a 1334
   content box, the two 16px bars are painted on the pane's padding box
   (`background-clip:padding-box`), and the picture is `16 | 1334 | 16` — the bars
   touch the sheet on both sides and the layout is already symmetric. Measured
   `colLeft 16 / colRight 16`. */
#study-zoom{transform:translateX(var(--study-zx,0px)) scale(var(--study-zoom-live,var(--study-zoom,1)));
  transform-origin:top left;
  display:flex;flex-direction:column;align-items:center;
  position:relative;width:100%;min-width:0;
  /* Own stacking context, so the `scale()` above is what drives `#study`'s
     scrollable overflow.
     Without this the wrapper stays a plain positioned box with `z-index:auto`
     inside a flex parent, i.e. *not* a stacking context, and then its
     transformed overflow is not propagated to the scrolling ancestor. The
     browser instead blends the unscaled content height with the visual scaled
     height, so `#study.scrollHeight` came out at 265038 on a 48387px column at
     scale 3.77 — a number that changes as the reader scrolls and that the
     browser then clamps `scrollTop` down to. That is the whole "the page keeps
     moving for a second or two after the fingers lift" complaint: the clamp
     fires on the next frame and yanks the offset the pinch just re-anchored.
     `z-index:1` is what creates the context. (The rule below used to name
     `isolation:isolate` here; that declaration is not present, and measured on
     the tablet `getComputedStyle().isolation` is `auto`. Setting `z-index` back
     to `auto` was what reproduced the bad `scrollHeight`, so this property — not
     isolation — has always been the one doing the work. Keeping the note honest
     matters because deleting `z-index` as "redundant with isolation" would
     silently bring the clamp back.)

     The context is only half of it. `position:relative` with `z-index:auto`
     also fails to make this the containing block for its *absolutely
     positioned* descendants, and the ink canvas is one of them. During a live
     pinch the whole wrapper is transformed, and a transformed subtree is still
     composited for its own layer — so a `translate` that reached only some of
     the children would slide the ink out of the words it was written on: the
     strokes stay put while the text moves. Naming a `z-index` makes this a
     positioned containing block, so the canvas rides
     the same transform the text does. It must be a      `z-index` and not another `isolation`, because isolation creates the
     stacking context but leaves position resolution alone. `1` is enough: the canvas already carries
     `z-index:3` and the holder `z-index:3`, so
     everything keeps the order it had. */
  z-index:1}
/* Live pinch preview. `syncStudyZoomLive()` adds this class for the duration of
   the gesture and writes the scale and origin straight to the inline `transform`
   / `transform-origin`. Promoting the subtree to its own compositor layer lets
   the browser re-composite the scale instead of re-laying-out and re-painting
   the whole 1083-element column every frame (measured 160ms -> 100ms per frame
   on the tablet). `contain:paint` keeps the re-paint inside this box, and
   `backface-visibility` suppresses the extra flattened-copy pass a promoted
   layer would otherwise get. */
#study-zoom.zooming{will-change:transform;contain:paint;backface-visibility:hidden}
#study .st-block{margin:0 0 16px}
/* Ink for the study pane. The canvas is scaled by `#study-zoom`, so it must
   stay a descendant of that wrapper for `scale()` to reach it — but it cannot be
   positioned against the wrapper itself: the wrapper is as tall as the whole
   article, so `top:0` would pin the canvas to the article's first line and the
   drawing surface would scroll off screen after one screenful.

   So it sits in this zero-height holder, which is the zoom wrapper's first
   child, and the holder is *offset* down the article by the current scroll:
   `syncStudyInkScroll()` writes `top: scrollTop / zoom` in content px, which is
   measured in exactly the same space this holder is laid out in. The canvas
   therefore always covers the visible slice, while `InkLayer.scrollTop`
   records which content row that slice is showing. `height:0` plus
   `flex:0 0 auto` keeps the holder from adding a gap above the first block or
   being stretched by the column flex layout.

   Why not `position:sticky`: `#study-zoom` carries `transform:scale(...)`, and
   a transformed box becomes the containing block for its `sticky` descendants,
   so the holder would stick to the wrapper's own box — which is as tall as the
   whole article and never scrolls — instead of to `#study`'s scrollport. Measured
   on the tablet: the canvas stayed at the article's first line (`top:1210`
   against a pane top of `106`) after one screenful of scrolling, so the pen had
   nothing to draw on where the reader was looking. Absolute positioning against
   the same containing block with an explicit scroll-derived offset is what makes
   the holder follow the pane regardless of the transform.

   🔴 `z-index` must stay 0 — and must not be raised "so the canvas is on top".
   The PDF works that way (`#stage.writing .page canvas.ink{z-index:5}`), but
   there the canvas sits inside `.page`, whose own `background:var(--page)` is
   opaque and already conceals everything behind it — so the ink layer can be
   lifted freely without hiding any article text, because the text is *below*
   the page background rather than in front of it.
   The study pane has no such backing. `body.has-bg #study` is
   `position:relative;z-index:2`, so the pane is one stacking context, and the
   holder is its very first child (`zoom.insertBefore(holder, zoom.firstChild)`),
   with every article paragraph after it at `z-index:auto`. A positive `z-index`
   therefore paints above all of them, and hit-testing — which walks the
   stacking order backwards — reaches the holder before the text. Measured on
   the tablet at `z-index:3`: four consecutive pen contacts landed on
   `DIV#study-zoom` instead of `CANVAS.study-ink`, so no stroke reached the ink
   layer. `z-index:0` keeps the stacking context but sits below the article,
   which is where a drawing surface belongs. */
/* 🔴 `sticky`, NOT `absolute` — and this is the whole 「笔迹飘动」 bug.
 *
 * An absolutely positioned child is laid out against its nearest positioned
 * ancestor, which here is `#study-zoom` (`position:relative`). `#study-zoom` is
 * as tall as the article and *travels with the scroll*, so the holder was
 * pinned to the article's row 0 and the canvas's `top` was an article
 * coordinate. `syncStudyInkScroll()` then wrote `insetY = -scrollTop` to move
 * the canvas down to the content row the reader was looking at — but that is
 * only a *within-the-article* relocation, and `#study`'s scrollport already
 * subtracts `scrollTop` once on its own. The two cancelled, and the canvas sat
 * at a fixed viewport y for every scroll position.
 *
 * Measured at 1366x1004 on `JW_Article_281_KO.gys`, `zoom:1`, `scrollTop:6380`:
 * `layerScrollTop` 6380 (correct), `insetY` -6380 (correct), `canvasCssTop`
 * `6380px` (correct) — and yet `canvasRect.top` **102**, identical to its value
 * at `scrollTop:0`, while the article's first block had moved to `-6278`. The
 * canvas did not move for *any* scroll; `d(ink)/d(scroll) = 0` against the
 * text's `-1`. That is the reader's 「跟版面文字没有焊死」, and no arrangement of
 * `insetY` could ever fix it because the element was not in the coordinate
 * space the arithmetic assumed.
 *
 * `z-index` must stay 0 — and must not be raised "so the canvas is on top". See
 * the note above: the article's paragraphs paint at `z-index:auto` after this
 * element, and a positive `z-index` would put the holder in front of the text,
 * where hit-testing would reach it first and the pen would stop drawing.
 *
 * 🔴 `position:absolute`, and it MUST STAY absolute — `sticky` cannot work here.
 *
 * `#study-zoom` carries a `transform` (`scale(var(--study-zoom-live))`), and a
 * transformed element becomes the containing block for its `sticky` descendants
 * AND destroys their stickiness: inside a transformed ancestor `sticky` has
 * nothing to stick against and silently behaves as `relative`. Measured on
 * `JW_Article_281_KO.gys`, 1366x1004, `#study` scrolled 400px:
 *
 *     zoom 1.0, transform: scale(1):    holder moved    0px  ✅ sticks
 *     zoom 1.5, transform: scale(1.5):  holder moved  200px  🔴 travels
 *     zoom 1.5, transform: none:        holder moved    0px  ✅ sticks
 *
 * With `sticky` the canvas box top ran `102 -> 1385 -> 3529` as the reader
 * zoomed in and the ink slid off the words. `absolute` is correct precisely
 * because the wrapper's `scale()` then applies to the holder and the canvas
 * together, so the layer scales with the text; the scroll compensation is done
 * in `syncStudyInkScroll()` as `insetY = -scrollTop / zoom`. The holder is
 * therefore zero-height and needs no stamping. */
#study .study-ink-holder{position:absolute;top:0;left:0;height:0;width:100%;
  z-index:0;pointer-events:none}
#study canvas.study-ink{position:absolute;top:0;left:0;display:block;
  pointer-events:none;touch-action:pan-x pan-y}
/* 🔴 蓝牙笔在任何时候都不允许滚动屏幕 — the reader's rule, stated exactly.
 *
 * The `none` here is NOT conditional on `pen-hover`/`pen-hold`. Those windows
 * were an attempt to have the canvas forbid scrolling only while a pen was near
 * or down, on the theory that Apple Pencil would always announce itself with a
 * hover first. Measured on the tablet it does not: `#stage class = writing` with
 * no `pen-hover`, and the reader's report is unambiguous —
 * 「笔能滚屏，不能写」 with 「手指能滚」. Since Safari settles `touch-action`
 * once, at gesture classification, and the hover never arrives, the nib's very
 * first contact was always classified against `pan-y` and became a scroll: the
 * ink layer never saw a `pointerdown` at all. No amount of JS inside
 * `pointerdown` can repair that, because by then the gesture is already a pan.
 *
 * So the canvas forbids its own scrolling unconditionally. Only the *canvas* —
 * the panes keep their `pan-y`, which is what keeps the reader scrollable:
 *   · 手指 — `#study`/`#viewer` are still `pan-y`, and a one-finger drag anywhere
 *     the canvas is not the hit target scrolls as before. Where the canvas *is*
 *     the hit target (it covers the article while writing is armed) a finger drag
 *     no longer scrolls; that is the deliberate cost, and 两指 or the scrollbar
 *     still work because pinch/two-finger pan is a gesture-level decision that
 *     `touch-action` on a single contact does not govern.
 *   · 蓝牙笔 — a contact the pencil produces is classified against `none`, so the
 *     stroke reaches `pointerdown` on the ink layer and is drawn. It can never be
 *     turned into a scroll, at any moment, which is exactly what was asked for.
 *
 * `pen-hover`/`pen-hold` are kept for palm rejection (`user-select`, the red dot
 * diagnostic) but they are no longer load-bearing for capture. */

/* The canvas must take pen input but must NOT be visible to the caret API.
   `pointer-events:auto` made the canvas a hit-test target for
   `document.caretRangeFromPoint` as well as for `pointerdown`, so while a pen
   was armed the caret always answered `DIV.study-ink-holder` / `CANVAS.study-ink`
   instead of the character under the finger. `begin()` could then only fall back
   to the enclosing paragraph wrapper, whose top edge is a whole paragraph above
   the line being read — measured as a −220px jump on release. Hiding only the
   *caret* hit-test (`pointer-events` cannot do this selectively, so the drawing
   surface is marked with `aria-hidden` and excluded in `begin()`) keeps the
   pointer path intact: `pointerdown` still lands on the canvas, which is what
   `mountStudyInk` listens for. Verified: with the caret probe allowed past the
   canvas the same press resolves to `SPAN.st-tx` "For example, Satan incited Job"
   at row 198, one line tall, which is the invariant the commit restores. */
#stage.writing #study canvas.study-ink{pointer-events:auto;cursor:crosshair;
  touch-action:none}
/* Safari's stylus path checks `touch-action` on the *hit-tested* element before
   it checks any ancestor, and it only reports `pointerType: 'pen'` when that
   element forbids scrolling itself. The canvas is the element a pen actually
   lands on, so the canvas must say `none` — but **only while the nib is in
   contact**, which is what the reader asked for in as many words:
   「手能滚，笔能写，但是笔在写的时候不能滚，笔抬起来才可以滚」.
   🔴 This rule used to say `none` unconditionally, keyed to `#stage.writing` —
   i.e. to the *armed tool*. That froze the article for the finger the moment
   手写 was picked, with the pen still on the desk, which is the same mistake the
   page path had (see the `.page.tool-* canvas.ink` rule) and it survived one
   round of fixes because it was never the element measured on the tablet: the
   iPad reader writes in the study pane, where this canvas — not the paper —
   is what the finger lands on, and it covers the article edge to edge.
   The `none` now comes from `#stage.pen-hold` (contact) and `#stage.pen-hover`
   (the nib within range, armed by hover so Safari has it *before* the gesture is
   classified). With the pen away this is `pan-x pan-y` and the article scrolls
   in both directions.
   🔴 It said `pan-y`, justified by 「the pane scrolls vertically only」 — which is
   false. The article is wider than the pane whenever a long unbroken run, a wide
   figure, or the zoom wrapper is on screen, and `#study` is `overflow:auto`, so
   the reader can and does scroll sideways. `pan-y` removed that axis and left
   「蓝牙笔模式下不能左右滚屏」. Both axes must be named here for the finger to keep
   its pan, exactly as on the default rule.
   The pen is unaffected: while the nib is down, `#stage.pen-hold` narrows this
   to `none`, and a pen drag that is not a stroke is stopped by the
   `preventDefault()` in `wireInk`. */
#stage.writing #study,#stage.writing #viewer{touch-action:pan-x pan-y}
/* 🔴 蓝牙笔在任何时候都不允许滚动屏幕 — enforced on the element the browser asks.
 *
 * The canvas rule above (`#stage.writing #study canvas.study-ink{…none}`) is
 * necessary but NOT sufficient, and treating it as sufficient is why 「笔还是
 * 滚屏」 survived every previous attempt. Safari checks `touch-action` on the
 * *hit-tested* element, and the canvas is almost never that element: the
 * article's text lives in `#study .st-trabs{z-index:1}` while the holder sits at
 * `z-index:0`, so a nib that lands on a word resolves to `SPAN.st-en` /
 * `.st-para-body` and the classification consults `#study`'s own `pan-y` — the
 * canvas is never asked. The canvas is also only ~one screenful tall
 * (`syncStudyInkScroll` keeps it on the visible slice) against a 30k-px article,
 * so the reader is usually scrolled clear of it anyway.
 *
 * So the *pane* has to forbid its own scrolling while the pencil is present.
 * `pen-hold` is the nib down; `pen-hover` is the nib in range, which is the
 * signal that lets Safari have the answer before it classifies the gesture.
 * Both drop the instant the pencil leaves, so 手指能滚 is untouched — a finger
 * scroll is classified against `pan-y` because neither class is set. */
#stage.pen-hover #study,
#stage.pen-hover #viewer{touch-action:none!important}
/* 手指模式. When a finger is allowed to lay down ink the browser must stop
   treating its touch as a scroll, otherwise the stroke is swallowed as a pan and
   never reaches `pointermove` — the same reason the rule above exists. This is
   the *only* difference between the two modes, and it is why the switch lives
   next to 稳定 rather than on the tool row: it changes capture, not the tool.
   The price is explicit and unavoidable: with a finger inking, one finger can no
   longer scroll the page at all. */
#stage.finger-inking #study,#stage.finger-inking #viewer{touch-action:none}
/* Palm rejection. Between a pen's `pointerdown` and its `pointerup` the writing
   hand's heel is resting on the glass, and the system reports it exactly like an
   intentional finger — same `pointerType`, same pointer space, nothing in the
   event that tells them apart. The only usable discriminator is time, so for the
   duration of the contact the finger loses every gesture it normally has.
   Without this the palm scrolled the page out from under a live stroke: the line
   drifted as it was drawn.

   `none` on both panes, and it has to be here rather than on the canvas: the
   canvas already says `none` while 手写 is armed, but a palm *beside* the nib
   lands on the pane, not on the canvas, and it is the pane's `pan-y` that was
   letting it scroll.

   `#pages .page` is on the list for the same reason one level down: the paper
   carries `touch-action:pan-x pan-y` (see the rule near `#viewer.no-pan`) so the finger
   can scroll across it whenever the nib is up, and a palm resting on the paper
   would otherwise drag the page out from under the stroke. The window is the only
   thing that says `none` over the paper, which is exactly the point: `none` follows
   the *contact*, not the armed tool.

   `!important` because `#stage.writing #study` (two ids + a class) outranks this
   rule and would otherwise win the moment a writing tool is armed — which is
   precisely when the window opens, so without it the guard would never apply.

   Released the instant the nib lifts, with no cooldown: the reader asked that
   the other hand be able to drag 「抬笔后」, and a short tail would make the page
   feel stuck. See `setPenHeld` in app.js. */
#stage.pen-hold #study,#stage.pen-hold #viewer,
#stage.pen-hold #pages .page,#stage.pen-hold #study canvas.study-ink,
#stage.pen-hold #pages .page canvas.ink{touch-action:none!important}
/* `pen-hover` — the pen is *in range* but not yet touching.
   ➤ It exists for exactly one reason: `touch-action` is decided once, when the
   gesture is classified, so the paper has to forbid its own scrolling *before*
   the nib lands. The hover listener in `wireInk` sets this the moment Apple
   Pencil reports a `pointermove` with `buttons === 0` (about a centimetre above
   the glass) and clears it on `pointerleave` or the failsafe.
   ➤ Scope is deliberately narrower than `pen-hold`: **canvases only**. The panes
   keep their `pan-y` until the nib is actually down, because a hovering pen must
   not take the reader's scroll away — that would be the original complaint
   again, just triggered by the pen being near the screen instead of armed.
   Without this rule the pen's first stroke is eaten: Safari reads the paper's
   `pan-y` at classification time, claims the gesture as a scroll, and the ink
   layer never sees a `pointerdown` at all. */
#stage.pen-hover #pages .page canvas.ink,
#stage.pen-hover #study canvas.study-ink{touch-action:none!important}
/* Diagnostic: while the palm window is open the screen refuses finger input by
   design, and when something goes wrong that refusal is indistinguishable from
   a frozen reader. The dot is the visible form of `penHeld`, so 「拖不动」 can be
   attributed in one glance instead of by inference. Remove once the pen paths
   are trusted. */
#stage.pen-hold::after{content:"";position:fixed;right:8px;bottom:8px;
  width:10px;height:10px;border-radius:50%;background:#c0392b;z-index:90;
  pointer-events:none;box-shadow:0 0 0 2px rgba(255,255,255,.8)}
/* Belt and braces for the 「整屏蓝遮罩」. The `selectstart` gate in app.js stops
 * a *deliberate* selection, but it cannot stop what WebKit does to a pen drag
 * that has already been classified as a scroll: the sweep lands as a native
 * selection with no `selectstart` of its own. `user-select:none` while the nib
 * is down removes the target entirely, and with nothing selectable a large
 * cross-branch range cannot form — so the UA wash never has a reason to paint.
 * Nothing is lost: while the pen is down the reader is writing, and the
 * highlighter tool opens its own window (see `penOwnsScreen`).
 *
 * 🔴 The ` * ` descendant is load-bearing, not decoration. `user-select` is an
 * inherited property, so a rule on the *pane* only reaches a child that has no
 * declaration of its own — and this stylesheet hands out `user-select:text`
 * deliberately, at `#study .st-para-body` and friends, because the highlighter
 * reads `window.getSelection()` out of them. A child's own declaration always
 * beats an inherited value, so declaring `none` on `#study` alone left all 785
 * text nodes still `text` (measured: `#study` → `none`, `.st-para-body` →
 * `text` in the same pen-down state) and the sweep built its range exactly as
 * before. Matching the descendants directly is what actually revokes the
 * selection, and it is why the earlier attempt looked correct and changed
 * nothing. */
#stage.pen-hold #study,#stage.pen-hold #viewer,
#stage.pen-hold #study *,#stage.pen-hold #viewer *,
#stage.pen-hold .dict-panel,#stage.pen-hold .dict-panel *{
  user-select:none;-webkit-user-select:none}
/* The dictionary panel lives directly under `body`, so the `#stage.pen-hold`
 * rules cannot reach it through the panes — it is covered by the list above,
 * and is the piece of live text a pen sweep is most likely to cross. The tiles
 * have copy buttons, so nothing usable is lost. */
/* Writing mode must also stop the pane itself from panning with the *stylus*. The
   canvas sets `touch-action:none`, but Chrome picks the scroll container for a
   touch at touch-start by walking the whole ancestor chain, so a `pan-x pan-y`
   here would win for the pen and the stroke would scroll the pane instead of
   drawing on it. The pen's own `preventDefault` (see `wireInk`) is what stops
   that scroll now, so the finger keeps its pan — that is the whole point of the
   two-handed session. Mirrors `#viewer.no-pan`, which the same `setTool`
   applies to the PDF reader.

   🔴 `pan-y` was WRONG, and it caused two separate reader-visible faults:
   `#study` is `overflow:auto`, so it scrolls on both axes whenever the article
   is wider than the pane, but `pan-y` tells the engine that only VERTICAL
   panning is ours to handle. A horizontal drag then produced no pan at all —
   the reader reported 「手只能上下滚屏不能左右滚屏」. Worse, because a finger tap
   is indistinguishable from the start of a pan under `pan-y`, the engine
   claimed the gesture and CANCELled the pointer, so `pointerup` never reached
   `wireStudyTap` and the tap opened no word popup. `pan-x pan-y` restores both
   axes and lets a stationary tap resolve as a tap, exactly like the
   `manipulation` default it replaced (`manipulation` == `pan-x pan-y
   pinch-zoom`). The pen stays safe because its scroll is stopped by the
   `preventDefault()` in `wireInk`, not by this value — and `#stage.pen-hold`
   narrows it to `none` outright while the nib is down, which is the case that
   actually mattered. */
#study.no-pan{touch-action:pan-x pan-y}
/* Paragraph spacing follows the browser's `.jw-para` (margin-bottom:22px). */
#study .st-para{display:flex;gap:0;align-items:flex-start;margin:0 0 22px}
/* Mirrors the browser's `.jw-parnum`: 15px/700 in muted gold, inline at the
   head of the paragraph with a 6px gap to the text. */
#study .st-parnum{flex:0 0 auto;min-width:0;font-weight:700;color:#8a7a4f;
  font-size:15px;line-height:40px;padding-top:0;margin-right:6px}
/* The English alone is the in-flow content. Translations are lifted into an
   absolutely positioned overlay (see `.st-trabs` below), which is exactly how
   the browser edition renders a paragraph: `.article-paragraph` holds the
   English run at 20px/40px justified, and the visible Chinese lives in sibling
   `.trans-absolute-line` divs positioned at measured pixel offsets.
   The 40px line box matters: the overlay places each translation 0.775 of a
   line down, which lands it centred on the boundary between two English lines
   because 0.775 x 40 = 31 and the 17.55px Chinese then spans 22-40. */
#study .st-para-body{flex:1 1 auto;min-width:0;
  font-size:20px;line-height:40px;text-align:justify}
/* A heading, question, scripture, focus or review item reuses the paragraph's
   body/overlay machinery for its single sentence, but keeps the block's own
   typography: only a paragraph reads as a 20px/40px justified run. */
#study .st-line-body{font-size:inherit;line-height:1.6;text-align:inherit}
/* One measure for every kind of block — title, scripture, question, answer
   box, heading, paragraph, review item. `width:100%` resolves against the
   pane's content box and is then capped at the reading column, while the
   pane's `align-items:center` centres the lot. Using `width` rather than
   `max-width` + auto margins keeps all four edges aligned even though the
   per-block `margin` shorthands below reset the horizontal margins to 0.
   The column is the browser edition's 637px measure widened by 100px on each
   side at the user's request, giving a longer line and a smaller gutter. */
#study .st-block,#study .st-para,#study .st-title{width:100%;max-width:837px;box-sizing:border-box;
  /* 🔴 THE TEXT COLUMN IS CENTRED BY `align-items:center` ON `#study-zoom`,
     NOT BY ANYTHING IN THIS RULE. See the comment above `#study-zoom`.
   *
   * This is the reader's 「文字显示区还是整块贴紧左边」 and 「我把平板竖起来也是
   * 偏左啊，不居中啊」. The two rules were in tension and nobody had reconciled
   * them: `#study-zoom` filled the pane's content box (1334px on the tablet)
   * while every block here is capped at `max-width:837px`. A 837px block in a
   * 1334px column therefore sat hard against the left edge with **497px of
   * unused column to its right**. Measured on the tablet at zoom 1: `colW 1334`,
   * `colLeft 0`, `colRight 0` — the *box* was perfectly symmetric, which is why
   * every probe kept insisting the layout was centred while the reader's eyes
   * said otherwise. The probe was measuring the column; the reader was looking
   * at the text.
   *
   * 🔴 `margin-inline:auto` DOES NOT WORK HERE — DO NOT RE-ADD IT.
   * Rectifying that was tried and it silently failed on two of the three block
   * types. Auto inline margins lose to any later rule that writes the `margin`
   * *shorthand*, and both of these are later AND derive their own 0 for the
   * horizontal edges:
   *   `#study .st-block{margin:0 0 16px}`   — source index ~87049
   *   `#study .st-title{margin:0 0 14px}`   — source index ~114255
   * Naming `st-title`/`st-block` in this selector does not rescue it either:
   * specificity is scored *per selector*, so each of those single-selector
   * shorthands is (0,2,1) and this rule is (0,2,1) too — the tie is broken by
   * *source order*, and both shorthands come later. Measured in Edge with the
   * real rules: `.st-para` centred at 233.5/233.5 while `.st-title` and
   * `.st-block` stayed flush at 0/467.
   * `align-items:center` on the flex container ignores `margin` entirely, so it
   * centres every block type regardless of what the shorthands say.
   *
   * `width:100%` (rather than `max-width` alone) plus `box-sizing:border-box`
   * keeps all four edges of every block aligned, since the shorthands above
   * still reset the horizontal margins to 0. */
  }
/* Figures reuse the same 837px measure as a paragraph so the image column and
   the text column share both edges — the browser edition does the same with
   `.jw-figure` inside its 760px article. */
#study .st-figure{margin:6px 0 22px}
#study .st-figure img{display:block;width:100%;height:auto;border-radius:6px;
  background:rgba(0,0,0,.06)}
#study .st-figure.st-figure-missing{display:none}
#study .st-figcap{margin-top:8px;font-size:13.5px;line-height:1.55;
  color:var(--fg2);font-style:italic}
body.on-paper #study .st-figcap{color:#5c5346}
/* Sentences flow inline as one continuous justified run — the browser's
   `.article-paragraph`. Nothing here is a block, so the English forms a clean
   line grid for the translation overlay to be positioned against. */
#study .st-sent{display:inline}
#study .st-parnum+.st-para-body>.st-sent:first-child{margin-top:0}
#study .st-tx{display:inline}
/* The in-flow translation is a data carrier only; layoutTranslations() reads
   its text and paints the visible copy into the `.st-trabs` overlay. */
#study .st-tr{color:#0066cc;font-size:13.5px;line-height:17.55px;
  font-style:normal;font-weight:400;text-align:left;
  display:inline;padding-left:0;margin:0;white-space:nowrap}
/* The 🌐 toggle leads every sentence, exactly as on the website: a 16px box
   carrying the same globe emoji as the language switcher, inline-flex,
   `vertical-align:middle`, 6px gap to the text. The translation overlay indents
   to this box's right edge, so its metrics are load-bearing for the layout,
   not cosmetic. */
#study .st-tg{display:inline-flex;align-items:center;justify-content:center;
  width:16px;height:16px;box-sizing:border-box;padding:0;margin:0 6px 0 0;
  background:#eef2f7;color:#334155;border:1px solid #c9d2d6;border-radius:4px;
  font-size:11px;font-weight:400;line-height:16px;cursor:pointer;
  user-select:none;vertical-align:middle;transition:background-color .15s}
#study .st-tg:hover{background:#dbe3ec}
/* Sentence audio, matching the browser's 12px `.sentence-play-btn`: inline
   after the sentence text, baseline-aligned.
   A fixed 16x16 box with `vertical-align:middle` keeps the control from
   inflating the paragraph's 40px line box, which would shift the English line
   grid away from the offsets the translation overlay was computed against. */
#study .st-play{border:0;background:transparent;color:inherit;padding:0;
  width:16px;height:16px;box-sizing:content-box;font-size:12px;line-height:16px;
  text-align:center;cursor:pointer;vertical-align:middle;opacity:.6}
#study .st-play:hover{opacity:1}
/* Translations are lifted into an absolutely positioned overlay, exactly like
   the browser edition's `.trans-absolute-line` divs (measured: direct children
   of `.article-paragraph`, `position:absolute`, `white-space:nowrap`). The
   in-flow `.st-tr` inside each sentence is only a data carrier — the visible
   copy is the overlay item painted by layoutTranslations(). Keeping it out of
   flow is what lets the English alone form a clean justified line grid. */
#study .st-para-body .st-sent>.st-tr:not(.st-trabs-item){display:none}
#study .st-para-body .st-sent{margin-right:0}
#study .st-para-body>.st-sent+.st-sent::before{content:" "}
/* The overlay itself. It is pinned to all four edges so it shares the body's
   coordinate space: item `left`/`top` are then measured from the body's own
   top-left, matching how the browser positions `.trans-absolute-line` against
   `.article-paragraph`. */
#study .st-trabs{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none;z-index:1}
/* Hidden measurer. It must carry the same font metrics as a real fragment —
   including `display:block` with the overlay's full width — because
   `layoutTranslations()` wraps each translation by appending one span per
   character and reading back which ones landed on a new line. */
#study .st-trabs-probe{position:absolute;visibility:hidden;white-space:normal;
  display:block;font-size:13.5px;line-height:17.55px;font-style:normal;
  font-weight:400;color:#0066cc;box-sizing:border-box;margin:0;padding:0}
/* The items sit directly over the source sentence, so `pointer-events:none` is
   mandatory: with `auto` every press landed on the translation span instead of
   the text underneath, and the browser never began a selection at all — no drag
   could highlight anything, on tablet or desktop. Drag-select is the reader's
   primary gesture and the overlay must stay purely decorative. */
#study .st-trabs-item{position:absolute;white-space:nowrap;display:block;
  font-size:13.5px;line-height:17.55px;color:#0066cc;font-style:normal;
  font-weight:400;margin:0;padding:0;pointer-events:none;overflow:hidden;
  text-overflow:ellipsis}
/* A single switch hides the translation everywhere, exactly like the browser
   edition's `body.hidetr .tr`. It must follow `.st-trabs-item`: the overlay
   items also carry `.st-tr`, so the two selectors tie on specificity and the
   later one wins. The per-paragraph 「汉」 toggle hides its own layer instead. */
#study.hidetr .st-tr{display:none}
/* A per-sentence toggle silences only its own sentence's overlay items. The
   per-sentence rule must beat the global `hidetr` above in both directions:
   `.tr-sent-off` hides one sentence while others stay as `hidetr` left them,
   and `.tr-sent-on` re-show one sentence while `hidetr` still hides the rest.
   `#study.hidetr` (0,2,0) + class = (0,3,1) outranks the global (0,2,1). */
#study .st-trabs-item.tr-sent-off{display:none}
#study.hidetr .st-trabs-item.tr-sent-on,
#study .st-trabs-item.tr-sent-on{display:block}
#study .st-tg.is-off{background:#f1f5f9;border-color:#cbd5e1;color:#94a3b8}
#study .st-en{font-weight:600}
#study .st-heading .st-en,#study .st-reviewhead .st-en{font-size:17px;font-weight:700;
  color:var(--accent);text-transform:uppercase;letter-spacing:.03em}
#study .st-reviewhead{border-top:2px solid var(--accent);padding-top:14px;margin-top:26px}
#study .st-bullet{position:relative;padding-left:18px}
#study .st-bullet::before{content:"\2022";position:absolute;left:2px;color:var(--accent);font-weight:700}
#study .st-question .st-en{font-weight:700}
#study .st-title{margin:0 0 14px}
#study .st-title .st-en{font-size:20px;font-weight:700;line-height:1.35}
/* Song line and date band, mirroring `.jw-song` / `.jw-dateband` in the web
   edition: bold, letter-spaced, and set off from the article body so the
   opening 「SONG 55」 reads as page furniture rather than a heading. */
#study .st-song .st-en,#study .st-songbottom .st-en{font-size:13px;font-weight:700;
  letter-spacing:.06em;color:var(--accent)}
#study .st-song{margin:0 0 16px}
#study .st-songbottom{border-top:1px solid var(--line);margin:30px 0 0;padding-top:14px}
#study .st-dateband .st-en{font-size:12px;font-weight:700;letter-spacing:.10em;
  text-transform:uppercase;color:var(--accent)}
#study .st-dateband{background:var(--bg3);border-left:3px solid var(--accent);
  border-radius:2px;margin:0 0 20px;padding:7px 14px}
#study .st-dateband .st-tr{color:var(--fg2)}
#study .st-scripture,#study .st-focus{border-left:3px solid var(--accent);
  padding:8px 0 8px 12px;margin:0 0 16px;font-style:italic;color:var(--fg2)}
#study .st-answerbox{margin:0 0 18px}
#study .st-answerbox textarea{width:100%;min-height:64px;resize:vertical;font:inherit;
  font-size:14px;padding:9px 11px;border:1px solid var(--line);border-radius:7px;
  background:var(--bg3);color:var(--fg);outline:none}
#study .st-answerbox textarea:focus{border-color:var(--accent)}
#study .st-answerbox textarea::placeholder{color:var(--fg2);opacity:.7}
#study .st-answer-tools{display:flex;gap:8px;justify-content:flex-end;margin-top:6px}
#study .st-answer-tools button{font:inherit;font-size:13px;line-height:1;cursor:pointer;
  padding:11px 18px;border:1px solid var(--line);border-radius:8px;
  background:var(--bg3);color:var(--fg2);touch-action:manipulation;
  -webkit-tap-highlight-color:transparent}
#study .st-answer-tools button:active{background:var(--accent);color:#fff;border-color:var(--accent)}
#study a.jw-biblink{color:var(--accent2);cursor:pointer;border-bottom:1px dotted currentColor}
#study a.jw-biblink:hover{color:var(--accent)}
@media (max-width:820px){
  #study{flex:1 1 auto;max-width:none;border-left:0}
}
/* ---------- folder browser ---------- */
/* The library is a real folder tree on both platforms, so the reader offers a
   way to walk it. Deliberately confined to GoodYess and its children: there is
   no path entry, only "up" as far as the root, so the user cannot wander into
   the rest of the device from here. */
#browser{position:absolute;top:0;right:0;bottom:0;left:0;background:rgba(8,11,15,.72);
  z-index:88;display:flex;align-items:center;justify-content:center;padding:18px}
#browser-box{background:var(--bg2);border:1px solid var(--line);border-radius:14px;
  width:100%;max-width:560px;height:100%;max-height:640px;display:flex;flex-direction:column;
  box-shadow:0 18px 50px rgba(0,0,0,.6);overflow:hidden}
#browser-head{display:flex;align-items:center;gap:8px;padding:10px 12px;
  border-bottom:1px solid var(--line);background:var(--bg3);flex:0 0 auto}
#browser-up,#browser-close{width:38px;height:38px;flex:0 0 auto;border-radius:10px;
  border:1px solid var(--line);background:var(--bg2);color:var(--fg);font-size:16px;
  cursor:pointer;line-height:1}
#browser-up:disabled{opacity:.32;cursor:default}
#browser-path{flex:1 1 auto;min-width:0;font-size:12px;color:var(--fg2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}
#browser-list{flex:1 1 auto;overflow:auto;padding:6px;
  -webkit-overflow-scrolling:touch}
.browser-row{display:flex;align-items:center;gap:10px;width:100%;padding:11px 10px;
  border:0;border-radius:10px;background:transparent;color:var(--fg);font-size:14px;
  text-align:left;cursor:pointer;font-family:inherit}
.browser-row:hover,.browser-row:active{background:var(--bg3)}
.browser-ico{flex:0 0 auto;font-size:17px;width:22px;text-align:center}
.browser-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.browser-meta{flex:0 0 auto;font-size:11px;color:var(--fg2)}
.browser-empty{padding:26px 14px;text-align:center;font-size:13px;color:var(--fg2)}
#browser-actions{flex:0 0 auto;padding:10px 12px;border-top:1px solid var(--line);
  background:var(--bg3);display:flex;gap:8px}
#browser-new{flex:1 1 auto;padding:11px;border-radius:10px;border:1px solid var(--line);
  background:var(--bg2);color:var(--fg);font-size:14px;cursor:pointer;font-family:inherit}
#browser-new:hover{background:var(--bg)}

/* ------------------------------------------------------------- self check */
/* Five taps on the top bar. Deliberately does NOT cover the screen: the thing
   being diagnosed is usually a full-screen overlay, so the report has to sit in
   a corner where it cannot hide the evidence or swallow the gesture. Tap the box
   to dismiss. */
#selfcheck{position:fixed;left:6px;bottom:6px;max-width:min(96vw,560px);
  max-height:56vh;z-index:2147483000;margin:0;padding:0;
  display:flex;flex-direction:column;
  background:rgba(11,15,20,.94);color:#cfe0f0;
  border:1px solid #2a3a4a;border-radius:8px;
  font:10px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  box-shadow:0 6px 22px rgba(0,0,0,.45)}
/* 🔴 The overlay must not eat touches aimed at the article underneath.
 *
 * This panel is a child of <body>, fixed to the lower-left, and — with
 * `max-height:56vh` and `z-index:2147483000` — it covered the very words the
 * reader was trying to tap. A touch on it never reaches `#study`, so
 * `wireStudyTap` never ran and no popup appeared: every capture read
 * `tgt=selfcheck`, i.e. the fingertip landed on this panel, not on the text.
 *
 * The first attempt at a fix made the body `pointer-events:none` so taps fell
 * through. That is correct for the *article* but wrong for the *report*: it also
 * killed the panel's own scrolling, so the reader could no longer read the very
 * evidence being collected. A diagnostic that cannot be read is worse than
 * useless.
 *
 * So the panel is made **small and collapsible** instead, and it stays fully
 * interactive. It opens as a one-line header in the corner; the reader taps the
 * header to expand it to read, and taps again to collapse it out of the way
 * before tapping a word. Nothing is made click-through, so scrolling, selecting
 * and copying all keep working. */
#selfcheck{position:fixed;right:6px;bottom:6px;max-width:min(92vw,420px);
  max-height:34vh;z-index:2147483000;margin:0;padding:0;
  display:flex;flex-direction:column;
  background:rgba(11,15,20,.94);color:#cfe0f0;
  border:1px solid #2a3a4a;border-radius:8px;
  font:10px/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  box-shadow:0 6px 22px rgba(0,0,0,.45)}
#selfcheck>.sc-bar{flex:0 0 auto;cursor:pointer;
  padding:6px 10px;
  background:rgba(30,42,56,.96);color:#9fd0ff;border-radius:7px 7px 0 0;
  font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#selfcheck>.sc-body{flex:1 1 auto;min-height:0;
  padding:8px 10px;overflow:auto;
  white-space:pre-wrap;word-break:break-all;
  -webkit-overflow-scrolling:touch;
  -webkit-user-select:text;user-select:text}
/* Collapsed: only the header shows, so the panel occupies one line of the
   screen and cannot sit over the sentence the reader wants to tap. */
#selfcheck.sc-min>.sc-body{display:none}
/* The verdict row stays visible in BOTH states — it is the only line that
   answers 「为什么不弹窗」, and it must not depend on the body's scroll. */
#selfcheck>.sc-verdict{flex:0 0 auto;padding:4px 10px;
  background:rgba(16,26,36,.96);color:#ffd479;
  border-top:1px solid #2a3a4a;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* The copy row. The reader has to get a 2000-line report off an iPad and into
   the chat, and 「长按选中 → 拷贝」 is hopeless at that length (the selection
   handles fight the body's own scrolling). One 44px button does it in a tap.
   Kept OUTSIDE `.sc-body` so a long report can never scroll it off-screen, and
   visible in the collapsed state so it is reachable without expanding first. */
#selfcheck>.sc-acts{flex:0 0 auto;display:flex;gap:6px;
  padding:6px 8px;background:rgba(16,26,36,.96);
  border-top:1px solid #2a3a4a;border-radius:0 0 7px 7px}
#selfcheck>.sc-acts>button{flex:1 1 0;min-height:44px;
  font:600 13px/1 -apple-system,system-ui,sans-serif;
  color:#0b0f14;background:#9fd0ff;border:0;border-radius:6px;
  cursor:pointer;-webkit-appearance:none}
#selfcheck>.sc-acts>button.sc-alt{background:#4a5c70;color:#e8f1fa}
#selfcheck>.sc-acts>button:active{filter:brightness(.88)}
