/* ─────────────────────────────────────────────────────────────
   Components v2 — 方格稿紙. Draft. Reads tokens-v2.css only:
   no hex, no radius number, no font name in this file.

   Every component works on both surfaces. Put data-surface="app" on any
   ancestor to get the iOS shapes; the markup stays the same.
   Shown in design/styleguide.html (design/index.html has all five layers).
   ───────────────────────────────────────────────────────────── */

/* ── grounds ──────────────────────────────────────────────────── */
.gp-paper { background-color: var(--gp-bg); color: var(--gp-text); font-family: var(--gp-font-text); }
.gp-ruled {
  background-image: var(--gp-ruled);
  background-size: var(--gp-pitch) var(--gp-pitch);
  background-position: -1px -1px;
}
.gp-tint { background-color: var(--gp-bg-tint); }
.gp-night { background: var(--gp-night); color: var(--gp-on-night); }

/* ── labels ───────────────────────────────────────────────────── */
/* section eyebrow: red, the one place a label is red */
.gp-eyebrow {
  display: block; font-size: var(--gp-fs-eyebrow); font-weight: 700;
  letter-spacing: var(--gp-track-eyebrow); text-transform: uppercase; color: var(--gp-brand);
}
/* every other label: blue */
.gp-label {
  font-size: var(--gp-fs-eyebrow); font-weight: 700;
  letter-spacing: var(--gp-track-eyebrow); text-transform: uppercase; color: var(--gp-label);
}
/* kicker beside 一二三: plain ink, title case */
.gp-kicker { font-size: var(--gp-fs-lead); font-weight: 600; color: var(--gp-text); }
/* 一二三 and its kicker, side by side */
.gp-numhead { display: inline-flex; align-items: center; gap: 11px; }
/* 一二三: brush, red, followed by 、 */
.gp-num { font-family: var(--gp-font-brush); font-size: 22px; line-height: 1; color: var(--gp-brand); }
.gp-num::after { content: "\3001"; font-family: var(--gp-font-text); font-size: .66em; margin-right: -.4em; }

[data-surface="app"] .gp-chip { font-size: var(--gp-fs-caption); }
[data-surface="app"] .gp-label { font-size: var(--gp-fs-caption); font-weight: 600; letter-spacing: 0; text-transform: none; }
/* a line of plain information on a page, not a heading and not a warning (㉒ 不會自動續訂…、購買失敗的原因) */
.gp-note { margin: 0; font-size: var(--gp-fs-small); line-height: 1.6; color: var(--gp-text-2); }
.gp-note.is-c { text-align: center; }
/* a question's stem: the sentence the options answer (W2, and the 作答頁 C) */
.gp-stem { margin: 0; font-size: var(--gp-fs-lead); font-weight: 400; line-height: var(--gp-lh-reading); color: var(--gp-text); }

/* ── buttons ──────────────────────────────────────────────────── */
.gp-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--gp-s2);
  height: var(--gp-btn-h); padding: var(--gp-btn-pad);
  border: var(--gp-w-hair) solid transparent; border-radius: var(--gp-radius-btn);
  background: var(--gp-brand); color: var(--gp-on-brand);
  font: inherit; font-size: var(--gp-btn-fs); font-weight: 600; white-space: nowrap;
  cursor: pointer; text-decoration: none;
}
.gp-btn:hover { background: var(--gp-brand-deep); }
.gp-btn:focus-visible { outline: 2px solid var(--gp-label); outline-offset: 2px; }
.gp-btn:disabled { background: var(--gp-rule); color: var(--gp-text-muted); cursor: default; }
.gp-btn--sm { height: var(--gp-btn-h-sm); padding: 0 14px; font-size: var(--gp-fs-small); font-weight: 500; }
.gp-btn--block { display: flex; width: 100%; }
/* secondary: outline in blue */
.gp-btn--ghost { background: var(--gp-surface); color: var(--gp-label); border-color: var(--gp-label); }
.gp-btn--ghost:hover { background: var(--gp-select-tint); }
/* the app's main action is blue; red stays an identity colour there */
[data-surface="app"] .gp-btn:not(.gp-btn--ghost, .gp-btn--plain, .gp-btn--invert, .gp-btn--premium, .gp-btn--premium-ghost, .gp-btn--ink, .gp-btn--ghost-ink, .gp-btn--danger):not(:disabled) { background: var(--gp-label); }
[data-surface="app"] .gp-btn:not(.gp-btn--ghost, .gp-btn--plain, .gp-btn--invert, .gp-btn--premium, .gp-btn--premium-ghost, .gp-btn--ink, .gp-btn--ghost-ink, .gp-btn--danger):not(:disabled):hover { background: var(--gp-label-deep); }
/* ink: night fill — Sign in with Apple (the HIG wants it black) */
.gp-btn--ink { background: var(--gp-night); color: var(--gp-on-night); }
.gp-btn--ink:hover { background: var(--gp-text); }
/* ghost in ink: the other sign-in, and the quiet side of a sheet that asks (登出／先不要) */
.gp-btn--ghost-ink { background: var(--gp-surface); color: var(--gp-text); border-color: var(--gp-text); }
.gp-btn--ghost-ink:hover { background: var(--gp-bg-tint); }
/* danger: the one button that can't be undone (刪除帳號 G3). Red pen, not 朱印紅 —
   red here is a state, and the brand red never is. Disabled until the page says it may */
.gp-btn--danger { background: var(--gp-wrong); color: var(--gp-on-fill); }
.gp-btn--danger:hover { background: var(--gp-wrong); }
.gp-btn--danger:disabled { background: var(--gp-wrong-tint); color: var(--gp-wrong-edge); }
/* Premium 購買鈕：只用在買 Premium 的地方。燙金：gold-300 → gold-500 上亮下暗，跟學生證的
   金徽章同一個漸層；金底白字看不清，字用墨。沒有 icon、沒有光、不會動，只靠材質跟其他鈕分開；
   按下去往下沉一點。2026-10-15 定（10-14 的朱紅不用了；夜藍底也不用；掃光拿掉，見 proposals #plainbuy Y）。 */
.gp-btn--premium { position: relative; overflow: hidden; isolation: isolate;
  background: linear-gradient(var(--gp-premium-hi), var(--gp-premium)); color: var(--gp-ink-900); border-color: var(--gp-premium-ink);
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--gp-white) 55%, transparent); transition: transform .12s ease-out; }
.gp-btn--premium:hover { background: linear-gradient(var(--gp-premium-hi), color-mix(in srgb, var(--gp-premium) 80%, var(--gp-premium-hi))); }
.gp-btn--premium:active { transform: translateY(1px) scale(.99); }
/* 次要的 Premium 鈕（已經是 Premium 再加時間，PM1-2）：白底、gold-700 框和字，沒有光 */
.gp-btn--premium-ghost { background: var(--gp-surface); color: var(--gp-premium-ink); border-color: var(--gp-premium-ink); }
.gp-btn--premium-ghost:hover { background: color-mix(in srgb, var(--gp-premium-hi) 25%, var(--gp-surface)); }
.gp-btn--premium .gp-i { width: 20px; height: 20px; }
/* quiet: white with a rule */
.gp-btn--plain { background: var(--gp-surface); color: var(--gp-text); border-color: var(--gp-rule-strong); font-weight: 400; }
.gp-btn--plain:hover, .gp-btn--plain.is-on { background: var(--gp-select-tint); border-color: var(--gp-label); }
/* the two answers under a word card: 不太會 in the red pen, 我懂了 in green.
   Each carries its own mark (cross / tick) as well as its colour */
.gp-btn--no { background: var(--gp-wrong-tint); border-color: var(--gp-wrong-edge); }
.gp-btn--no .gp-mark { color: var(--gp-wrong); }
.gp-btn--no:hover { background: var(--gp-wrong-tint); border-color: var(--gp-wrong); }
.gp-btn--ok { background: var(--gp-correct-tint); border-color: color-mix(in srgb, var(--gp-correct) 45%, var(--gp-rule-strong)); }
.gp-btn--ok .gp-mark { color: var(--gp-correct); }
.gp-btn--ok:hover { background: var(--gp-correct-tint); border-color: var(--gp-correct); }
/* on the blue-black band */
.gp-btn--invert { background: var(--gp-on-fill); color: var(--gp-night); }
.gp-btn--invert:hover { background: var(--gp-bg); }
.gp-btn svg { flex: none; }

/* round icon button (app) */
.gp-icon-btn {
  width: 40px; height: 40px; display: grid; place-items: center; padding: 0;
  border: var(--gp-w-hair) solid var(--gp-rule-soft); border-radius: 50%;
  background: var(--gp-surface); color: var(--gp-text); font: inherit; cursor: pointer;
}

/* ── chips, pills, badges ─────────────────────────────────────── */
/* chip: a printed tag — Band A, 名詞. Square, a faint grey ground, ink letters, no line, not uppercase.
   A Band chip is <small>Band</small><b>A</b>: 「Band」 small and grey, the letter big — the letter is the point. */
.gp-chip {
  display: inline-flex; align-items: baseline; gap: 3px; padding: 3px 7px; line-height: 1.2;
  font-size: 11px; font-weight: 600; color: var(--gp-text); background: var(--gp-chip-ground);
}
.gp-chip small { font-size: 11px; font-weight: 500; color: var(--gp-text-muted); }
.gp-chip b { font-family: var(--gp-font-display); font-size: 15px; font-weight: 700; line-height: 1; }
/* pill: a filter you can press */
.gp-pill {
  display: inline-flex; align-items: center; gap: 6px; height: var(--gp-pill-h); padding: 0 14px;
  border: var(--gp-w-hair) solid var(--gp-rule-strong); border-radius: var(--gp-radius-pill);
  background: var(--gp-surface); color: var(--gp-text-body);
  font: inherit; font-size: var(--gp-fs-small); cursor: pointer; white-space: nowrap;
}
.gp-pill small { font-size: var(--gp-fs-caption); opacity: .7; font-variant-numeric: tabular-nums; }
.gp-pill:hover { border-color: var(--gp-label); }
.gp-pill.is-on { background: var(--gp-label); border-color: var(--gp-label); color: var(--gp-on-fill); }
[data-surface="app"] .gp-pill { border-color: var(--gp-rule-soft); }
/* badge: Premium, Band A in the app header */
.gp-badge {
  display: inline-flex; align-items: center; gap: 4px; padding: 3px 9px;
  border-radius: var(--gp-radius-pill); background: var(--gp-select-tint); color: var(--gp-label);
  font-size: var(--gp-fs-caption); font-weight: 600;
}

/* ── tabs ─────────────────────────────────────────────────────── */
/* segmented control */
.gp-seg {
  display: flex; padding: 3px; gap: 0;
  background: var(--gp-select-tint); border-radius: var(--gp-radius-pill);
}
.gp-seg button {
  flex: 1; height: var(--gp-pill-h); border: var(--gp-w-hair) solid transparent; border-radius: var(--gp-radius-pill);
  background: none; color: var(--gp-text-muted); font: inherit; font-size: 15px; cursor: pointer;
}
.gp-seg button.is-on { background: var(--gp-surface); color: var(--gp-text); font-weight: 600; border-color: var(--gp-rule-strong); }
[data-surface="app"] .gp-seg button.is-on { border-color: transparent; box-shadow: var(--gp-shadow-seg); }
/* --sm: at the right end of a settings row (讀音標註 關／注音／拼音). Hugs its words, doesn't stretch */
.gp-seg--sm { flex: none; padding: 2px; }
.gp-seg--sm button { flex: none; height: 28px; padding: 0 10px; font-size: var(--gp-fs-small); }

/* icon tabs (app, 練習 A2): four tiles over one long scroll. Tapping a tile scrolls to its
   section; scrolling lights the tile of the section on top. 10/10: one strip, the four cells
   split by hairlines, each with its coloured object (.gp-obj, o-* in icons-v2.js) — the four
   sections read as four small apps. The lit cell gets the select tint and a blue rule under it;
   the objects stay coloured either way. .is-compact is the same tabs once the page scrolls:
   objects only, smaller, stuck under the app bar, the lit one underlined. A count rides on the
   cell corner (.gp-icontabs__n) instead of being written out. */
.gp-icontabs {
  display: grid; grid-template-columns: repeat(var(--n, 4), 1fr); overflow: hidden;
  background: var(--gp-surface); border: var(--gp-w-hair) solid var(--gp-rule-strong); border-bottom-width: var(--gp-w-head);
  border-radius: var(--gp-radius-opt);
}
.gp-icontabs button {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: var(--gp-s1);
  padding: 10px 0 8px; background: none; color: var(--gp-text-muted); border: 0;
  font: inherit; font-size: var(--gp-fs-caption); cursor: pointer;
}
.gp-icontabs button + button { box-shadow: inset var(--gp-w-hair) 0 0 var(--gp-rule-soft); }
.gp-icontabs button.is-on { background: var(--gp-select-tint); color: var(--gp-label); font-weight: 600; box-shadow: inset 0 calc(-1 * var(--gp-w-head)) 0 var(--gp-label); }
.gp-icontabs button + button.is-on { box-shadow: inset var(--gp-w-hair) 0 0 var(--gp-rule-soft), inset 0 calc(-1 * var(--gp-w-head)) 0 var(--gp-label); }
.gp-icontabs .gp-obj { width: 40px; height: 40px; }
.gp-icontabs__n {
  position: absolute; top: 4px; right: calc(50% - 30px); min-width: 20px; padding: 0 5px; box-sizing: border-box;
  border-radius: var(--gp-radius-pill); background: var(--gp-near); color: var(--gp-on-fill);
  font-size: var(--gp-fs-caption); font-weight: 600; line-height: 20px; text-align: center; font-variant-numeric: tabular-nums;
}
.gp-icontabs.is-compact { border: 0; border-bottom: var(--gp-w-hair) solid var(--gp-rule-soft); border-radius: 0; background: none; }
.gp-icontabs.is-compact button { flex-direction: row; justify-content: center; padding: 6px 0; }
.gp-icontabs.is-compact button, .gp-icontabs.is-compact button + button { box-shadow: none; }
.gp-icontabs.is-compact button.is-on { background: none; box-shadow: inset 0 calc(-1 * var(--gp-w-head)) 0 var(--gp-label); }
.gp-icontabs.is-compact .gp-obj { width: 28px; height: 28px; }
.gp-icontabs.is-compact button > span:not(.gp-icontabs__n) { display: none; }
.gp-icontabs.is-compact .gp-icontabs__n { top: 0; right: calc(50% - 28px); }

/* big letter tabs (landing, Coverage A / B / C): unpicked letters are only a rule colour */
.gp-bigtabs { display: flex; gap: var(--gp-s8); }
.gp-bigtab {
  display: flex; align-items: baseline; gap: 8px; padding: 0; border: 0; background: none; cursor: pointer;
  font-family: var(--gp-font-display); font-size: 44px; font-weight: 600; line-height: .82; letter-spacing: -.02em;
  color: var(--gp-rule-strong);
}
.gp-bigtab span { font-family: var(--gp-font-text); font-size: 19px; font-weight: 500; letter-spacing: 0; }
.gp-bigtab:hover { color: var(--gp-text-muted); }
.gp-bigtab.is-on { color: var(--gp-label); }
.gp-bigtab.is-on span { color: var(--gp-text-muted); }

/* underline tabs */
.gp-tabs { display: flex; gap: var(--gp-s6); border-bottom: var(--gp-w-hair) solid var(--gp-rule-strong); }
.gp-tabs button {
  padding: 10px 0; margin-bottom: -1px; border: 0; border-bottom: var(--gp-w-head) solid transparent;
  background: none; color: var(--gp-text-muted); font: inherit; font-size: 15px; cursor: pointer;
}
.gp-tabs button.is-on { color: var(--gp-text); font-weight: 600; border-bottom-color: var(--gp-label); }

/* app tab bar */
.gp-tabbar { display: flex; border-top: var(--gp-w-hair) solid var(--gp-rule-soft); background: color-mix(in srgb, var(--gp-bg) 94%, transparent); }
.gp-tabbar button {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 8px 0 6px;
  border: 0; background: none; font: inherit; font-size: 10px; color: var(--gp-text-muted); cursor: pointer;
}
.gp-tabbar button.is-on { color: var(--gp-label); font-weight: 600; }
/* a tab that just opened (HM1-17): one blue dot beside its icon until it's tapped once */
.gp-tabbar button { position: relative; }
.gp-tabbar button.is-new::after { content: ""; position: absolute; top: 7px; left: calc(50% + 9px); width: 6px; height: 6px; border-radius: 50%; background: var(--gp-label); }

/* ── progress ─────────────────────────────────────────────────── */
.gp-steps { display: flex; gap: 4px; }
.gp-steps i { flex: 1; height: var(--gp-step-h); background: var(--gp-step-todo); }
[data-surface="app"] .gp-steps i { border-radius: 3px; }
.gp-steps i.is-done { background: var(--gp-step-done); }
.gp-steps i.is-cur { background: var(--gp-step-cur); }
.gp-steps--fine { gap: 2px; }

.gp-meter { height: 4px; background: var(--gp-rule); }
.gp-meter i { display: block; height: 100%; background: var(--gp-label); }

/* ── option rows (answer choices) ─────────────────────────────── */
/* 鍵帽: a 3px bottom edge, like a key you can push down. Nothing else on the
   page has one — cards and read-only rows keep a 1px frame — so the edge alone
   says "press". Rounded (16, round key) on both surfaces, even on square paper. */
.gp-opts { display: flex; flex-direction: column; gap: 6px; }
.gp-opt {
  display: flex; align-items: center; gap: 10px; width: 100%; min-height: var(--gp-opt-min-h);
  padding: 6px 12px 6px 9px; box-sizing: border-box; text-align: left;
  background: var(--gp-surface); border: var(--gp-w-hair) solid var(--gp-rule-strong); border-bottom-width: var(--gp-w-head);
  border-radius: var(--gp-radius-opt); box-shadow: var(--gp-shadow-opt);
  font: inherit; font-size: var(--gp-fs-body); line-height: 1.5; color: var(--gp-text); cursor: pointer;
}
.gp-opt__k {
  width: var(--gp-opt-key); height: var(--gp-opt-key); flex: none; display: grid; place-items: center;
  border-radius: var(--gp-radius-key); background: var(--gp-bg-tint);
  font-family: var(--gp-font-display); font-size: 14px; font-weight: 600; line-height: 1; color: var(--gp-text-2);
}
.gp-opt__t { flex: 1; }
.gp-opt__m { flex: none; width: 24px; display: grid; place-items: center; }
.gp-opt__m .gp-mark { width: 22px; height: 22px; }
@media (hover: hover) { .gp-opt:not(:disabled, .is-right, .is-wrong, .is-near, .is-dim):hover { border-color: var(--gp-label); background: var(--gp-select-tint); } }
/* pressed: the key goes down by the edge it loses */
.gp-opt:not(:disabled, .is-picked, .is-right, .is-wrong, .is-near, .is-dim):active { transform: translateY(1px); border-bottom-width: 2px; margin-bottom: 1px; }
/* is-picked stays down: the edge thins to 1.5 and the top sinks by what it lost, so the bottom
   line and the rows around it don't move */
.gp-opt.is-picked { border: var(--gp-w-cell) solid var(--gp-label); background: var(--gp-select-tint); box-shadow: none;
  margin-top: calc(var(--gp-w-head) - var(--gp-w-cell)); }
.gp-opt.is-picked .gp-opt__k { background: var(--gp-label); color: var(--gp-on-fill); }
.gp-opt.is-right { border: var(--gp-w-cell) solid var(--gp-correct); border-bottom-width: var(--gp-w-head); background: var(--gp-correct-tint); box-shadow: none; }
.gp-opt.is-wrong { border: var(--gp-w-cell) solid var(--gp-wrong-edge); border-bottom-width: var(--gp-w-head); background: var(--gp-wrong-tint); box-shadow: none; }
.gp-opt.is-near { border: var(--gp-w-cell) solid var(--gp-near); border-bottom-width: var(--gp-w-head); background: var(--gp-near-tint); box-shadow: none; }
.gp-opt.is-dim { color: var(--gp-text-muted); box-shadow: none; cursor: default; }
/* once marked, the key fills: green for right, red for wrong (same on both surfaces) */
.gp-opt.is-right .gp-opt__k { background: var(--gp-correct); color: var(--gp-on-fill); }
.gp-opt.is-wrong .gp-opt__k { background: var(--gp-wrong); color: var(--gp-on-fill); }
/* is-near (1-7): practice's wrong pick, said as 差一點 — apricot and a cross, never red.
   The answer is shown beside it at once (is-right); there is no second try */
.gp-opt.is-near .gp-opt__k { background: var(--gp-near); color: var(--gp-on-fill); }
.gp-opt.is-near .gp-mark { color: var(--gp-near); }
/* is-playing: the option the recording is reading now (C3, C4, C8). A blue key, not a pick */
.gp-opt.is-playing:not(.is-picked) .gp-opt__k { background: var(--gp-select-tint); color: var(--gp-label); box-shadow: 0 0 0 var(--gp-w-cell) var(--gp-label); }
/* --tight: several questions on one screen (C7, C9, C13, C15–C18): lower keys, closer rows */
.gp-opts--tight { gap: 4px; }
.gp-opts--tight .gp-opt { min-height: 44px; padding-block: 3px; font-size: var(--gp-fs-small); }
/* --keys: options you only hear (C3, C4). Three keys in a row, a letter on each */
.gp-opts--keys { flex-direction: row; gap: 10px; }
.gp-opts--keys .gp-opt { flex: 1; justify-content: center; min-height: 72px; padding: 0; }
.gp-opts--keys .gp-opt__k { width: 40px; height: 40px; font-size: var(--gp-fs-lead); }
/* the number of the gap a sentence went into (C15) sits where the mark goes, circled like the gap's own number (2-26) */
.gp-opt__m b { width: 22px; height: 22px; display: grid; place-items: center; border: var(--gp-w-hair) solid currentColor; border-radius: 50%;
  font: 600 var(--gp-fs-caption)/1 var(--gp-font-display); color: var(--gp-label); }
.gp-opt.is-right .gp-opt__m b { color: var(--gp-correct); }

[data-surface="app"] .gp-opt.is-wrong { border-color: var(--gp-wrong); }

/* explanation under the options */
.gp-expl { padding: 2px 0 2px var(--gp-s4); border-left: var(--gp-w-head) solid var(--gp-correct); font-size: 15px; line-height: var(--gp-lh-body); color: var(--gp-text-body); }
.gp-expl b { display: block; font-size: var(--gp-fs-meta); color: var(--gp-correct); }
.gp-expl.is-wrong { border-left-color: var(--gp-wrong); }
.gp-expl.is-wrong b { color: var(--gp-wrong); }
.gp-expl.is-near { border-left-color: var(--gp-near); }
.gp-expl.is-near b { color: var(--gp-near); }
/* an option nobody picked: a plain rule, the heading in ink */
.gp-expl.is-other { border-left-color: var(--gp-rule-strong); }
.gp-expl.is-other b { color: var(--gp-text-2); }
.gp-expl + .gp-expl { margin-top: var(--gp-s3); }
.gp-verdict { display: flex; align-items: center; gap: 8px; font-size: var(--gp-fs-lead); font-weight: 700; color: var(--gp-correct); }
.gp-verdict.is-wrong { color: var(--gp-wrong); }

/* ── 2-13 bottom panel ────────────────────────────────────────────
   Rises from the bottom once something is decided; the screen above stays
   where it was. Its tone is all it knows — ok (green) or near (apricot) —
   not whether an answer was right: the composite (3-8) picks the tone and
   writes the words. A mark and a title on the left, one small side button
   on the right, one main button under them. */
.gp-panel {
  display: flex; flex-direction: column; gap: var(--gp-s3); padding: var(--gp-s4) var(--gp-s5) var(--gp-s6);
  border-top: var(--gp-w-cell) solid; background: var(--gp-surface);
}
.gp-panel__top { display: flex; align-items: center; gap: var(--gp-s2); min-height: 34px; }
.gp-panel__v { display: flex; align-items: center; gap: 6px; font-size: var(--gp-fs-lead); font-weight: 700; }
.gp-panel__v .gp-mark { width: 26px; height: 26px; }
.gp-panel__side {
  margin-left: auto; display: inline-flex; align-items: center; height: var(--gp-btn-h-sm); padding: 0 14px; text-decoration: none; border: var(--gp-w-hair) solid; border-radius: var(--gp-radius-pill);
  background: var(--gp-surface); font: inherit; font-size: var(--gp-fs-small); font-weight: 500; cursor: pointer;
}
.gp-panel .gp-btn { width: 100%; }
/* 隨機練習（不限題數，QZ27）：主鈕左邊一顆窄的「結算」，白底、框線和字跟面板同色 */
.gp-panel__row { display: flex; gap: var(--gp-s2); }
.gp-panel__row .gp-btn { flex: 1; min-width: 0; }
.gp-panel__sub { flex: none; height: var(--gp-btn-h); padding: 0 var(--gp-s5); border: var(--gp-w-hair) solid; border-radius: var(--gp-radius-btn);
  background: var(--gp-surface); font: inherit; font-weight: 600; cursor: pointer; }
.gp-panel.is-ok { border-color: var(--gp-correct); background: var(--gp-correct-tint); }
.gp-panel.is-ok :is(.gp-panel__v, .gp-panel__side, .gp-panel__sub) { color: var(--gp-correct); border-color: var(--gp-correct); }
.gp-panel.is-ok .gp-btn, [data-surface="app"] .gp-panel.is-ok .gp-btn { background: var(--gp-correct); }
.gp-panel.is-near { border-color: var(--gp-near); background: var(--gp-near-tint); }
.gp-panel.is-near :is(.gp-panel__v, .gp-panel__side, .gp-panel__sub) { color: var(--gp-near); border-color: var(--gp-near); }
.gp-panel.is-near .gp-btn, [data-surface="app"] .gp-panel.is-near .gp-btn { background: var(--gp-near); }
.gp-panel.is-rising { animation: gp-rise .22s ease-out; }
@keyframes gp-rise { from { transform: translateY(100%); } }
@media (prefers-reduced-motion: reduce) { .gp-panel.is-rising { animation: none; } }

/* ── 2-14 highlighter ─────────────────────────────────────────────
   Inline, behind the lower part of the words, like a marker pen. The same
   pen marks the source and the explanation's quote of it, so the eye can
   match them. is-lit is the one being talked about: a ring flashes round it
   once (still, under Reduce Motion). Markup: <mark class="gp-hl">…</mark> */
.gp-hl {
  padding: 0 1px; border-radius: 2px; color: inherit;
  background: linear-gradient(transparent 42%, var(--gp-highlight) 42% 94%, transparent 94%);
  -webkit-box-decoration-break: clone; box-decoration-break: clone;
}
button.gp-hl { border: 0; font: inherit; cursor: pointer; }
.gp-hl.is-lit { background: var(--gp-highlight); box-shadow: 0 0 0 2px var(--gp-near); }
@media (prefers-reduced-motion: no-preference) { .gp-hl.is-lit { animation: gp-lit .9s ease-out 2; } }
@keyframes gp-lit { 0% { box-shadow: 0 0 0 0 var(--gp-near); } 60% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--gp-near) 0%, transparent); } 100% { box-shadow: 0 0 0 2px var(--gp-near); } }

/* ── 2-15 material that doesn't fit ───────────────────────────────
   A passage or picture in a box of fixed height (the page sets max-height).
   Only when it overflows does the page add .is-cut, and two keys appear:
   bottom right, full screen (.gp-fold__fs); bottom centre, open down
   (.gp-fold__more). Both sit on the passage, which keeps its full width;
   closed, what they cover is the faded edge. .is-open lets the box grow to the whole passage and
   pushes the question down with it, so the page scrolls once instead of the
   box scrolling inside it; the key flips to close. A picture (--pic) only
   gets full screen. If it fits, neither key is there. */
.gp-fold { position: relative; overflow: hidden; }
.gp-fold.is-open { max-height: none !important; padding-bottom: 48px; }
.gp-fold.is-cut:not(.is-open, .gp-fold--pic)::after {
  content: ""; position: absolute; inset: auto 0 0; height: 64px; pointer-events: none;
  background: linear-gradient(transparent, var(--gp-surface) 75%);
}
.gp-fold__fs, .gp-fold__more {
  display: none; position: absolute; z-index: 1; place-items: center; padding: 0; cursor: pointer;
  color: var(--gp-text-2); background: var(--gp-surface); border: var(--gp-w-hair) solid var(--gp-rule-strong); box-shadow: var(--gp-shadow-seg);
}
.gp-fold.is-cut .gp-fold__fs, .gp-fold.is-cut:not(.gp-fold--pic) .gp-fold__more { display: grid; }
.gp-fold__fs { bottom: 8px; right: 8px; width: 36px; height: 36px; border-radius: calc(var(--gp-radius-btn) * .75); }
.gp-fold__more { bottom: 8px; left: 50%; width: 56px; height: 32px; margin-left: -28px; border-radius: var(--gp-radius-pill); }
.gp-fold__fs .gp-i { width: 18px; height: 18px; }
.gp-fold__more .gp-i { width: 20px; height: 20px; transition: transform .2s; }
.gp-fold.is-open .gp-fold__more .gp-i { transform: rotate(180deg); }

/* ── 2-16 search field ────────────────────────────────────────────
   At rest it is a button (練習 ▸ 字卡 pushes the search page K1): a tinted capsule, the
   magnifier, the prompt in grey. Live (.is-live) it is the top of the search page: white,
   a blue cell frame, 取消 beside it. An empty field is an empty page. */
.gp-search { display: flex; align-items: center; gap: var(--gp-s3); }
.gp-search__f { flex: 1; display: flex; align-items: center; gap: var(--gp-s2); height: 40px; padding: 0 14px; box-sizing: border-box;
  border: 0; border-radius: var(--gp-radius-pill); background: var(--gp-bg-tint); color: var(--gp-text-muted);
  font: inherit; font-size: var(--gp-fs-body); text-align: left; cursor: pointer; }
/* on a tint band the tint field would vanish: it turns white with a hairline */
.gp-tint .gp-search__f { background: var(--gp-surface); box-shadow: inset 0 0 0 var(--gp-w-hair) var(--gp-rule-soft); }
.gp-search__f .gp-i { width: 18px; height: 18px; flex: none; }
.gp-search__f input { flex: 1; min-width: 0; border: 0; outline: 0; background: none; font: inherit; color: var(--gp-text); }
.gp-search.is-live .gp-search__f { background: var(--gp-surface); box-shadow: inset 0 0 0 var(--gp-w-cell) var(--gp-label); cursor: text; }
.gp-search__x { flex: none; border: 0; background: none; padding: 0; min-height: 44px; font: inherit; font-weight: 600; color: var(--gp-label); cursor: pointer; }

/* ── 2-17 empty state ─────────────────────────────────────────────
   A title naming the state plainly, one grey line of what will be here or why, then the way
   forward (a main button, sometimes a quieter one). No picture. Only a search with no result
   has no button — the keyboard is still up. */
.gp-empty { display: flex; flex-direction: column; align-items: center; gap: var(--gp-s6); padding: var(--gp-s8) 0; text-align: center; }
.gp-empty__t { margin: 0; font-size: var(--gp-fs-lead); font-weight: 600; color: var(--gp-text); }
.gp-empty__m { margin: 6px 0 0; font-size: var(--gp-fs-small); line-height: 1.6; color: var(--gp-text-muted); }
.gp-empty__a { display: flex; flex-direction: column; gap: var(--gp-s2); align-self: stretch; }
/* --done: a whole screen that ends something for good (G4 帳號刪掉了). A grey ring and a tick over the title —
   grey, not green: nothing was won */
.gp-empty__i { width: 64px; height: 64px; display: grid; place-items: center; border: var(--gp-w-cell) solid var(--gp-rule-strong); border-radius: 50%; color: var(--gp-text-muted); }
.gp-empty__i .gp-i { width: 28px; height: 28px; stroke-width: 2.2; }

/* ── 2-18 toast ───────────────────────────────────────────────────
   One line saying something just happened (換到 Band B 了、已加 3 個月). A night capsule at the
   top of the page; drops in, stays about 2s, goes. Not a button, asks nothing. */
.gp-toast { display: inline-flex; padding: var(--gp-s2) var(--gp-s4); border-radius: var(--gp-radius-pill);
  background: var(--gp-night); color: var(--gp-on-night); font-size: var(--gp-fs-small); font-weight: 600;
  box-shadow: 0 3px 8px color-mix(in srgb, var(--gp-night) 18%, transparent); }
.gp-toast.is-in { animation: gp-toast 2.6s ease-out both; }
@keyframes gp-toast { 0% { opacity: 0; transform: translateY(-12px); } 12%, 85% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .gp-toast.is-in { animation-name: gp-toast-fade; } @keyframes gp-toast-fade { 0%, 100% { opacity: 0; } 12%, 85% { opacity: 1; } } }

/* ── 2-19 fine print ──────────────────────────────────────────────
   Links at the foot of a page, grey and centred, 「・」 between: 恢復購買・使用條款・隱私政策. */
.gp-fine { display: flex; justify-content: center; flex-wrap: wrap; font-size: var(--gp-fs-caption); color: var(--gp-text-muted); }
.gp-fine a, .gp-fine button { min-height: 44px; display: inline-flex; align-items: center; border: 0; padding: 0; background: none; font: inherit; color: inherit; text-decoration: none; cursor: pointer; }
.gp-fine > * + *::before { content: "・"; }

/* ── 2-20 switch ──────────────────────────────────────────────────
   On／off at the right end of a settings row. Only three in the app (答題音效、每天提醒、分享使用分析);
   everything with more than two values is a 2-4 gp-seg--sm. Flipping it is saving it. */
.gp-switch { position: relative; flex: none; width: 51px; height: 31px; padding: 0; border: 0; border-radius: var(--gp-radius-pill);
  background: var(--gp-rule); cursor: pointer; transition: background .2s; }
.gp-switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 27px; height: 27px; border-radius: 50%;
  background: var(--gp-surface); box-shadow: var(--gp-shadow-seg); transition: transform .2s; }
.gp-switch.is-on { background: var(--gp-label); }
.gp-switch.is-on::after { transform: translateX(20px); }

/* ── 2-21 sheet ───────────────────────────────────────────────────
   A half-height card from the bottom (登入 ㊾′、登出 G2、字卡背面 K2). The page under it is covered by
   .gp-scrim, so only the sheet can be pressed; a tap on the scrim or a pull down closes it.
   Top: the 2-10 gp-grab. The screen sets where it sits; this only draws it. */
.gp-scrim { position: absolute; inset: 0; z-index: 4; background: color-mix(in srgb, var(--gp-night) 28%, transparent); }
.gp-sheet { position: absolute; left: 0; right: 0; bottom: 0; z-index: 5; display: flex; flex-direction: column; gap: var(--gp-s4);
  padding: 0 var(--gp-s5) var(--gp-s8); background: var(--gp-bg);
  border-radius: var(--gp-radius-card) var(--gp-radius-card) 0 0; box-shadow: 0 -8px 24px color-mix(in srgb, var(--gp-night) 14%, transparent); }
.gp-sheet > .gp-grab { margin-bottom: 4px; }
.gp-sheet__t { margin: 0; font-size: var(--gp-fs-lead); font-weight: 600; color: var(--gp-text); }
.gp-sheet__m { margin: -8px 0 0; font-size: var(--gp-fs-small); line-height: 1.6; color: var(--gp-text-2); }

/* ── 2-22 field ───────────────────────────────────────────────────
   A small label, then the box. Grey words in an empty box are the example, not a label.
   .gp-field__box can be an <input>, a <textarea> (--tall) or a row of 2-3 pills to pick one. */
.gp-field { display: flex; flex-direction: column; gap: 6px; }
.gp-field > b { font-size: var(--gp-fs-caption); font-weight: 600; color: var(--gp-text-2); }
.gp-field__box { width: 100%; box-sizing: border-box; min-height: 44px; padding: 11px 14px; border: var(--gp-w-hair) solid var(--gp-rule-strong);
  border-radius: calc(var(--gp-radius-card) * .75); background: var(--gp-surface); font: inherit; font-size: var(--gp-fs-body); color: var(--gp-text); resize: none; }
.gp-field__box::placeholder { color: var(--gp-text-muted); }
.gp-field__box:focus { outline: 0; border-color: var(--gp-label); box-shadow: inset 0 0 0 1px var(--gp-label); }
.gp-field__box--tall { min-height: 120px; line-height: 1.6; }
.gp-field__pills { display: flex; flex-wrap: wrap; gap: 6px; }

/* ── 2-31 select ──────────────────────────────────────────────────
   Pick one of a list too long for a segmented control (OB9 App 語言). Not the system menu: the box opens
   downward, the options inside it under a hairline, the picked one with a check. Open, the frame turns blue. */
.gp-select { border: var(--gp-w-hair) solid var(--gp-rule-strong); border-radius: var(--gp-radius-card); background: var(--gp-surface); overflow: hidden; }
.gp-select.is-open { border-color: var(--gp-label); box-shadow: inset 0 0 0 .5px var(--gp-label); }
.gp-select__v { display: flex; align-items: center; width: 100%; min-height: 56px; padding: 0 var(--gp-s4); border: 0; background: none;
  font: inherit; font-size: var(--gp-fs-lead); font-weight: 600; color: var(--gp-text); text-align: left; cursor: pointer; }
.gp-select__v .gp-i { margin-left: auto; color: var(--gp-label); transform: rotate(90deg); transition: transform .18s ease-out; }
.gp-select.is-open .gp-select__v { border-bottom: var(--gp-w-hair) solid var(--gp-rule); }
.gp-select.is-open .gp-select__v .gp-i { transform: rotate(-90deg); }
.gp-select__list { display: none; padding: var(--gp-s1) 0; }
.gp-select.is-open .gp-select__list { display: block; }
.gp-select__list button { display: flex; align-items: center; width: 100%; min-height: 48px; padding: 0 var(--gp-s4); border: 0; background: none;
  font: inherit; font-size: var(--gp-fs-body); color: var(--gp-text); text-align: left; cursor: pointer; }
.gp-select__list button.is-on { font-weight: 600; }
.gp-select__list button .gp-i { display: none; margin-left: auto; color: var(--gp-label); }
.gp-select__list button.is-on .gp-i { display: inline-block; }
@media (prefers-reduced-motion: reduce) { .gp-select__v .gp-i { transition: none; } }

/* ── 2-23 checkbox ────────────────────────────────────────────────
   A square and a sentence the user agrees to (G3：我知道剩下的 Premium 也會一起不見). The whole line is the button. */
.gp-check { display: flex; align-items: flex-start; gap: var(--gp-s3); width: 100%; padding: 6px 0; border: 0; background: none;
  font: inherit; font-size: var(--gp-fs-small); line-height: 1.5; text-align: left; color: var(--gp-text); cursor: pointer; }
.gp-check i { flex: none; width: 22px; height: 22px; display: grid; place-items: center; margin-top: -1px; box-sizing: border-box;
  border: var(--gp-w-cell) solid var(--gp-rule-strong); border-radius: 6px; background: var(--gp-surface); color: var(--gp-on-fill); }
.gp-check i .gp-i { width: 14px; height: 14px; stroke-width: 3; visibility: hidden; }
.gp-check.is-on i { border-color: var(--gp-label); background: var(--gp-label); }
.gp-check.is-on i .gp-i { visibility: visible; }

/* ── 2-24 heads-up ────────────────────────────────────────────────
   Something isn't as the user set it, or will be lost: apricot (1-7), never red — it is not an error.
   A line (.gp-warn) under the row it is about (㊻ 通知被關掉了), or boxed (--box) with a bold first
   line (G3 Premium 還剩 91 天). */
.gp-warn { margin: 0; font-size: var(--gp-fs-small); line-height: 1.6; font-weight: 400; color: var(--gp-near); }
.gp-warn--box { padding: var(--gp-s3) var(--gp-s4); border: var(--gp-w-hair) solid var(--gp-near); border-radius: calc(var(--gp-radius-card) * .75);
  background: var(--gp-near-tint); font-size: var(--gp-fs-small); color: var(--gp-text-body); }
.gp-warn--box b { display: block; color: var(--gp-near); font-weight: 600; }
/* the one thing to do about it (L3 登入) goes under the words */
.gp-warn--box > .gp-btn { display: flex; width: max-content; margin-top: var(--gp-s3); }
/* a full-width sign-in button (HM-ER-2: OB1's own Apple／Google button, only the one used last time) */
.gp-warn--box > .gp-btn--block { width: 100%; }

/* ── 2-25 question tabs ───────────────────────────────────────────
   Several questions on one passage or picture (C13, C14, C16): one dot per question, joined by a line, one
   question at a time, so you can jump back. Not answered: a frame. Answered: a blue fill, and the line between
   two answered dots fills too. Now: a ring around the dot. After a practice answer (C17): green with a tick,
   or apricot (1-7) with a cross — never red. Markup: buttons with an <i class="gp-qtabs__line"> between each pair. */
.gp-qtabs { display: flex; align-items: center; padding: 4px; }
.gp-qtabs button { flex: none; width: 36px; height: 36px; padding: 0; display: grid; place-items: center;
  border: var(--gp-w-cell) solid var(--gp-rule-strong); border-radius: 50%; background: var(--gp-surface);
  font: 500 var(--gp-fs-small) var(--gp-font-display); color: var(--gp-text-2); cursor: pointer; }
.gp-qtabs__line { flex: 1; min-width: 8px; height: 2px; background: var(--gp-rule); }
.gp-qtabs button:is(.is-done, .is-ok, .is-near) + .gp-qtabs__line:has(+ :is(.is-done, .is-ok, .is-near)) { background: var(--gp-label); }
.gp-qtabs button.is-done { border-color: var(--gp-label); background: var(--gp-label); color: var(--gp-on-fill); }
.gp-qtabs button.is-ok { border-color: var(--gp-correct); background: var(--gp-correct); color: var(--gp-on-fill); }
.gp-qtabs button.is-near { border-color: var(--gp-near); background: var(--gp-near); color: var(--gp-on-fill); }
.gp-qtabs button.is-on { box-shadow: 0 0 0 3px var(--gp-bg), 0 0 0 calc(3px + var(--gp-w-cell) + 0.5px) var(--gp-label); font-weight: 700; }
.gp-qtabs button.is-on:not(.is-done, .is-ok, .is-near) { border-color: var(--gp-label); color: var(--gp-label); }
.gp-qtabs .gp-i { width: 16px; height: 16px; stroke-width: 2.8; }

/* ── 2-26 blank ───────────────────────────────────────────────────
   A gap in a passage (C14–C18): its number on a line, then the words that went in, right in the text, so you
   read whether it fits. Now (.is-on): blue line on a blue ground. Marked: green, or apricot with the answer put in. */
.gp-blank { display: inline; padding: 0 4px; border: 0; border-bottom: var(--gp-w-cell) solid var(--gp-text-muted); background: none;
  font: inherit; line-height: inherit; color: var(--gp-text); cursor: pointer; }
/* the number is circled like an option's letter (2-6), so the gap and the sentence that went in read as one pair */
.gp-blank::before { content: attr(data-n); display: inline-grid; place-items: center; width: 1.5em; height: 1.5em; margin-right: 4px; vertical-align: 0.1em;
  border: var(--gp-w-hair) solid currentColor; border-radius: 50%; font: 600 var(--gp-fs-caption)/1 var(--gp-font-display); color: var(--gp-text-muted); }
.gp-blank[data-n=""]::before { display: none; }
.gp-blank:empty { padding: 0 1.4em 0 6px; }
.gp-blank.is-on { border-bottom-color: var(--gp-label); background: var(--gp-select-tint); }
.gp-blank.is-on::before { border-color: var(--gp-label); background: var(--gp-label); color: var(--gp-on-fill); }
.gp-blank.is-ok { border-bottom-color: var(--gp-correct); color: var(--gp-correct); }
.gp-blank.is-ok::before { color: var(--gp-correct); }
.gp-blank.is-near { border-bottom-color: var(--gp-near); background: var(--gp-near-tint); color: var(--gp-near); }
.gp-blank.is-near::before { color: var(--gp-near); }

/* ── 2-27 picture ─────────────────────────────────────────────────
   A question's picture (C3, C11, C14), framed like the passage; full screen comes from 2-15 (gp-fold--pic).
   In the design sheets a caption stands where the picture goes. As options (C1, C2, C10) the pictures sit two to
   a row in .gp-pics, each one a key (2-6) with its letter top left, and take the same states. */
.gp-pic { display: grid; place-items: center; min-height: 160px; padding: var(--gp-s4); box-sizing: border-box; text-align: center;
  border: var(--gp-w-hair) solid var(--gp-rule-soft); border-radius: var(--gp-radius-card); background: var(--gp-bg-tint);
  font-size: var(--gp-fs-small); line-height: 1.5; color: var(--gp-text-muted); }
.gp-pics { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.gp-pics .gp-opt { flex-direction: column; align-items: stretch; gap: 8px; padding: 8px; }
.gp-pics .gp-opt__k { align-self: flex-start; }
.gp-pics .gp-pic { min-height: 112px; border: 0; }

/* ── 2-28 calendar ────────────────────────────────────────────────
   Picking a day you have to see the weekday of (考試日 ㉙′): a month, not a wheel. Before today is grey;
   the picked day is a blue disc. Header: the month, then ‹ › */
.gp-cal__h { display: flex; align-items: center; justify-content: space-between; font-size: var(--gp-fs-lead); }
.gp-cal__h span { display: flex; }
.gp-cal { display: grid; grid-template-columns: repeat(7, 1fr); row-gap: 2px; text-align: center; font: 400 var(--gp-fs-lead) var(--gp-font-display); font-variant-numeric: tabular-nums; }
.gp-cal b { padding: 4px 0; font: 400 var(--gp-fs-caption) var(--gp-font-text); color: var(--gp-text-muted); }
.gp-cal button { justify-self: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%; background: none; font: inherit; color: var(--gp-text); cursor: pointer; }
.gp-cal button:disabled { color: var(--gp-rule-strong); cursor: default; }
.gp-cal button.is-on { background: var(--gp-label); color: var(--gp-on-fill); font-weight: 600; }

/* ── 2-29 status line ─────────────────────────────────────────────
   One grey line at the top of a tab when the server can't be reached (L1, L5): an icon, what's wrong, a grey
   summary on the right. Gone as soon as it connects. Grey, not apricot (2-24): there is nothing to do about it. */
.gp-status { display: flex; align-items: center; gap: var(--gp-s2); padding: 8px 12px; border-radius: calc(var(--gp-radius-card) * .75);
  background: var(--gp-bg-tint); font-size: var(--gp-fs-small); color: var(--gp-text-2); }
.gp-status .gp-i { width: 18px; height: 18px; color: var(--gp-text-muted); }
.gp-status small { margin-left: auto; font-size: var(--gp-fs-caption); color: var(--gp-text-muted); }

/* ── 2-30 reading aid ─────────────────────────────────────────────
   拼音或注音標在題目的字上（作答輔助 3-19 的讀音標註）。外層 .gp-ruby 加 --py 或 --zy，
   一個字一個 .gp-r。拼音用 <ruby>，在字的正上方；注音直排在字的右邊、聲調在倒數兩個注音中間的右邊（只有一個注音就在它右上）、輕聲的點在上面。
   讀音小一號灰字，字跟著讀音拉開、讀音不縮。
   .gp-ruby 一開就每一行都留讀音的高度（只標難字時沒標的行也一樣），切換時題目不跳。
   .gp-look：長按查字時亮起的那個詞（3-21），淡藍底、藍字。 */
.gp-ruby--py, .gp-ruby--zy { line-height: calc(1em + var(--gp-fs-ruby) * 2.6); }
.gp-ruby ruby { ruby-align: center; }
.gp-ruby rt { padding: 0 1px; font: 500 var(--gp-fs-ruby)/1.2 var(--gp-font-display); color: var(--gp-text-2); letter-spacing: 0; }
.gp-r { display: inline-flex; align-items: baseline; margin-right: 1px; }
.gp-r__zy { align-self: center; display: inline-flex; flex-direction: column; align-items: center; margin-left: 1px;
  font-size: .44em; line-height: .9; color: var(--gp-text-2); }
/* 注音照字的大小縮放：三個注音疊起來約 1.2 個字高 */
/* 聲調在最後一個注音的右邊、跟前一個注音的交界；只有一個注音時在它的右上 */
.gp-r__zy { padding-right: .65em; }
.gp-r__zy > span { position: relative; }
.gp-r__t { position: absolute; left: calc(100% + .15em); top: 0; transform: translateY(-50%); font-style: normal; line-height: 1; }
.gp-r__zy > span:only-child .gp-r__t { top: 15%; }
/* 有橫線的短文（.gp-reading）行高跟著讀音加大，橫線才對得上 */
.gp-reading:has(.gp-ruby--py) { --lh: 46px; }
.gp-reading:has(.gp-ruby--zy) { --lh: 46px; }
.gp-reading .gp-ruby { line-height: inherit; }
.gp-look { background: var(--gp-select-tint); box-shadow: 0 0 0 2px var(--gp-select-tint); color: var(--gp-label); }

/* ── cells: one character to a 田字格 ─────────────────────────── */
.gp-cells { display: flex; font-family: var(--gp-font-kai); color: var(--gp-text); line-height: 1; }
.gp-cells > * {
  width: var(--gp-cell); height: var(--gp-cell); display: grid; place-items: center; font-style: normal;
  background: var(--gp-cross), var(--gp-surface);
  border: var(--gp-w-hair) solid var(--gp-rule-strong); margin-left: calc(-1 * var(--gp-w-hair));
}
.gp-cells > :first-child { margin-left: 0; }
/* punctuation sits top-left in its cell, no cross */
.gp-cells > .p { background: var(--gp-surface); place-items: start; padding: .3em 0 0 .25em; box-sizing: border-box; }

/* word card unit: 拼音 over a 田字格, 注音格 to its right */
.gp-zi { display: inline-flex; flex-direction: column; gap: 6px; font-size: 66px; }
.gp-zi__py { height: 20px; font-size: 15px; color: var(--gp-text-2); text-align: center; width: 96px; }
.gp-zi__box { display: flex; border: var(--gp-w-hair) solid var(--gp-rule-strong); background: var(--gp-surface); }
.gp-zi__han {
  width: 96px; height: 96px; display: grid; place-items: center;
  font-family: var(--gp-font-kai); font-size: 66px; line-height: 1; background: var(--gp-cross), var(--gp-surface);
}
.gp-zi__zy {
  position: relative; width: 30px; display: flex; flex-direction: column; justify-content: center; align-items: center;
  border-left: var(--gp-w-hair) solid var(--gp-rule-strong);
  font-family: var(--gp-font-kai); font-size: 17px; line-height: 1.15;
}
.gp-zi__zy:empty { background: repeating-linear-gradient(to bottom, transparent 0 22px, var(--gp-rule-soft) 22px 23px); }
.gp-zi__zy em { position: absolute; right: 1px; top: 58%; font-style: normal; font-size: 15px; }

/* reading text on ruled lines */
.gp-reading {
  --lh: 36px;
  padding: 8px 16px 10px; font-size: var(--gp-fs-body); line-height: var(--lh); color: var(--gp-text-body);
  background-color: var(--gp-surface);
  background-image: linear-gradient(to bottom, transparent calc(var(--lh) - 1px), var(--gp-rule) calc(var(--lh) - 1px));
  background-size: 100% var(--lh); background-position: 0 8px;
  border: var(--gp-w-hair) solid var(--gp-rule-soft); border-radius: var(--gp-radius-card);
}

/* ── marks ────────────────────────────────────────────────────── */
/* brush tick / cross / circle: filled outlines from brush.py, use with <svg><use href="#gp-check"></svg> */
.gp-mark { display: inline-block; width: 28px; height: 28px; fill: currentColor; flex: none; }
.gp-mark--ok { color: var(--gp-correct); }
.gp-mark--no { color: var(--gp-wrong); }
/* the 會了 stamp: red pen, one rule */
.gp-stamp {
  display: inline-block; padding: 4px 12px; transform: rotate(-12deg);
  font-family: var(--gp-font-kai); font-size: 24px; font-weight: 700; letter-spacing: .1em; color: var(--gp-wrong);
  border: 2px solid var(--gp-wrong); border-radius: 2px;
  background: color-mix(in srgb, var(--gp-surface) 60%, transparent);
}
/* the done seal (1-6): a green ring, a tick, the date. Stamped on a row that is finished for
   today (今日任務) and on a mock exam that's been sat. .is-new stamps it once (.6s), nothing else moves */
.gp-seal {
  width: 50px; height: 50px; flex: none; box-sizing: border-box;
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  border: 2px solid var(--gp-correct); border-radius: 50%; color: var(--gp-correct); transform: rotate(-12deg);
  background: color-mix(in srgb, var(--gp-correct-tint) 60%, transparent);
}
.gp-seal .gp-i { width: 16px; height: 16px; margin-top: -2px; stroke-width: 2.6; }
.gp-seal small { font: 400 var(--gp-fs-caption)/1 var(--gp-font-display); letter-spacing: -.04em; font-variant-numeric: tabular-nums; }
.gp-seal.is-new { animation: gp-seal-in .6s cubic-bezier(.3, 1.3, .5, 1) both; }
@keyframes gp-seal-in { from { opacity: 0; transform: rotate(-12deg) scale(1.6); } }
@media (prefers-reduced-motion: reduce) { .gp-seal.is-new { animation: none; } }
/* the done tick (1-6, 10/20, was the square 完成 seal): the brush tick in correct, on the corner of what's
   done for today on the 主頁 desk. A tick says "done" to anyone; a seal needed reading. --sm is the one on a desk tile */
.gp-done { width: 32px; height: 32px; flex: none; fill: currentColor; color: var(--gp-correct); }
.gp-done--sm { width: 18px; height: 18px; }
.gp-done.is-new { animation: gp-done-in .6s cubic-bezier(.3, 1.3, .5, 1) both; }
@keyframes gp-done-in { from { opacity: 0; transform: scale(1.6); } }
@media (prefers-reduced-motion: reduce) { .gp-done.is-new { animation: none; } }

/* ── cards and lists ──────────────────────────────────────────── */
/* index card: a blue rule on top */
.gp-card {
  padding: var(--gp-s6); background: var(--gp-surface);
  border: var(--gp-w-hair) solid var(--gp-rule-strong); border-top: var(--gp-w-head) solid var(--gp-rule-ink);
  border-radius: var(--gp-radius-card);
}
a.gp-card:hover, button.gp-card:hover { border-color: var(--gp-label); }
[data-surface="app"] .gp-card { border: var(--gp-w-hair) solid var(--gp-rule-soft); }

/* list: a heading rule, then rows with rules between */
.gp-list { border-top: var(--gp-w-head) solid var(--gp-rule-ink); }
.gp-row {
  display: flex; align-items: center; gap: var(--gp-s3); width: 100%; padding: 10px 6px; box-sizing: border-box;
  border: 0; border-bottom: var(--gp-w-hair) solid var(--gp-rule-strong); background: none;
  font: inherit; text-align: left; color: var(--gp-text); cursor: pointer;
}
.gp-row__t { flex: 1; display: flex; flex-direction: column; gap: 3px; font-size: var(--gp-fs-body); }
.gp-row__t small { font-size: var(--gp-fs-caption); color: var(--gp-text-muted); }
.gp-row__v { font-size: var(--gp-fs-small); color: var(--gp-text-muted); font-variant-numeric: tabular-nums; }
/* app: the same list as a grouped iOS card */
[data-surface="app"] .gp-list { border: var(--gp-w-hair) solid var(--gp-rule-soft); border-radius: var(--gp-radius-card); background: var(--gp-surface); overflow: hidden; }
[data-surface="app"] .gp-row { padding: 12px 14px 12px 12px; border-bottom: 0; }
[data-surface="app"] .gp-row + .gp-row { border-top: var(--gp-w-hair) solid var(--gp-rule-soft); }
/* a group of lists (設定, 測驗紀錄): a small grey heading over each, an optional grey line under it */
.gp-list__h { margin: 0 0 6px 14px; font-size: var(--gp-fs-caption); font-weight: 400; color: var(--gp-text-muted); }
.gp-list__f { margin: 6px 14px 0; font-size: var(--gp-fs-caption); line-height: 1.5; color: var(--gp-text-muted); }
/* a row that holds a control (開關、分段) is not itself a button */
.gp-row.is-ctl { cursor: default; min-height: 52px; }
/* is-fixed: a setting this screen won't let you change (模擬考 M2). Grey, a lock after the words, the control can't be pressed */
.gp-row.is-fixed .gp-row__t { flex: none; color: var(--gp-text-muted); }
.gp-row.is-fixed > .gp-i { width: 16px; height: 16px; margin-right: auto; color: var(--gp-text-muted); }
.gp-row.is-fixed > :last-child { opacity: .5; pointer-events: none; }
.gp-row .chev { width: 16px; height: 16px; color: var(--gp-rule-strong); }
/* 不能點的一列（PM4「還沒公告」）：字灰、沒有箭頭 */
.gp-row.is-off { color: var(--gp-text-muted); cursor: default; }
/* 已選的那一列（PM4-2 場次）：字藍、右邊一個勾 */
.gp-row.is-on .gp-row__t { color: var(--gp-label); font-weight: 600; }
.gp-row.is-on > .gp-i:last-child { width: 18px; height: 18px; color: var(--gp-label); }
/* 搜尋對到的字（PM4-1） */
.gp-row__t mark { background: var(--gp-select-tint); color: var(--gp-label); border-radius: 2px; }
.gp-row__a { flex: none; border: 0; padding: 0 2px; background: none; font: inherit; font-size: var(--gp-fs-small); font-weight: 600; color: var(--gp-label); cursor: pointer; min-height: 44px; }
/* the one row that destroys something: 刪除帳號 */
.gp-row.is-danger .gp-row__t { color: var(--gp-wrong); }

/* ── word card deck (app) ─────────────────────────────────────────
   One line per card, on plain paper: the 田字格 is the only grid on screen.
   The next card sits under the top one, a little lower and narrower, so a
   sliver shows at rest and the whole card shows as the top one is swiped away. */
.gp-deck { position: relative; }
.gp-deck__card {
  position: absolute; inset: 0; display: flex; flex-direction: column; padding: var(--gp-s5);
  background: var(--gp-surface); border: var(--gp-w-hair) solid var(--gp-rule-soft); border-radius: var(--gp-radius-card);
  touch-action: none; user-select: none;
}
.gp-deck__card.is-next { transform: translateY(10px) scale(.95); transform-origin: 50% 100%; }
.gp-deck__card.is-top { cursor: grab; box-shadow: var(--gp-shadow-opt); }
.gp-deck__card.is-flying { transition: transform 280ms ease-in, opacity 280ms ease-in; }
.gp-deck__card.is-rising { transition: transform 200ms ease-out; }
.gp-deck__word { flex: 1; display: grid; place-items: center; }
.gp-deck__word > div { display: flex; gap: 14px; }
.gp-deck__card .gp-stamp { position: absolute; top: 28px; right: 24px; opacity: 0; }
/* held mid-drag (F3): the card takes the colour of the side it's going to — green for 我懂了, apricot for 不太會
   (the same 差一點 colour as practice, 10/14: not a wrong answer, it comes round again; grey is kept for locked)
   — and the word for that side floats beside it */
.gp-deck__card.is-lean-r { transform: translateX(44px) rotate(6deg); background: var(--gp-correct-tint); border-color: var(--gp-correct); }
.gp-deck__card.is-lean-l { transform: translateX(-44px) rotate(-6deg); background: var(--gp-near-tint); border-color: var(--gp-near); }
.gp-deck__side { position: absolute; top: 42%; z-index: 3; padding: 6px 14px; border-radius: var(--gp-radius-pill);
  background: color-mix(in srgb, var(--gp-surface) 82%, transparent); backdrop-filter: blur(6px); font-size: var(--gp-fs-small); font-weight: 600; }
.gp-deck__side.is-r { right: -6px; color: var(--gp-correct); }
.gp-deck__side.is-l { left: -6px; color: var(--gp-near); }
/* the top of a front: Band and 詞性 chips; a sentence card (F4) reads left to right, not centred */
.gp-deck__chips { display: flex; gap: 6px; }
/* ☆ in the card's top-right corner: 星號 is kept apart from 熟不熟 — tapping it doesn't flip the card, and 我懂了 doesn't clear it */
.gp-deck__star { position: absolute; top: 10px; right: 10px; z-index: 2; width: 44px; height: 44px; display: grid; place-items: center;
  border: 0; padding: 0; background: none; font-size: 20px; line-height: 1; color: var(--gp-rule-strong); cursor: pointer; }
.gp-deck__star.is-on { color: var(--gp-near); }
.gp-deck__sent { flex: 1; display: flex; align-items: center; font-size: var(--gp-fs-title); line-height: var(--gp-lh-reading); color: var(--gp-text); }
/* the front of a card: no reading yet */
.gp-deck__card:not(.is-back) .gp-zi__py { visibility: hidden; }

/* smaller 田字格 for the head of the back */
.gp-zi--sm { gap: 4px; }
.gp-zi--sm .gp-zi__py { width: 56px; height: 16px; font-size: var(--gp-fs-meta); }
.gp-zi--sm .gp-zi__han { width: 56px; height: 56px; font-size: 38px; }
.gp-zi--sm .gp-zi__zy { width: 22px; font-size: 12px; line-height: 1.1; }
.gp-zi--sm .gp-zi__zy em { font-size: 11px; right: 0; }

/* back of a word card: the word with its reading, then 釋義 and 例句.
   Spacing and two small labels separate the parts; no rules */
.gp-cb { flex: 1; display: flex; flex-direction: column; gap: var(--gp-s6); overflow-y: auto; }
.gp-cb__head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--gp-s3); }
.gp-cb__head > div { display: flex; gap: 8px; }
.gp-cb__pos { font-size: var(--gp-fs-meta); color: var(--gp-text-muted); padding-bottom: 2px; }
.gp-cb__sec .gp-label { display: block; margin-bottom: 6px; }
.gp-cb__zh { margin: 0; font-size: var(--gp-fs-body); line-height: var(--gp-lh-body); color: var(--gp-text-body); }
.gp-cb__ex { margin: 0; font-size: var(--gp-fs-lead); line-height: var(--gp-lh-reading); color: var(--gp-text); }
.gp-cb__ex mark { background: none; color: inherit; text-decoration: underline 2px var(--gp-label); text-underline-offset: 6px; }
.gp-cb__tr { margin: 4px 0 0; font-size: 15px; line-height: 1.55; color: var(--gp-text-muted); }
/* Noto Sans TC sets ’ and other Latin punctuation full-width */
.gp-cb__tr:is(:lang(en), :lang(vi), :lang(id)) { font-family: var(--gp-font-latin); }

/* ── screen skeleton (app) ────────────────────────────────────────
   2-9…2-12 in brand/design-layers.md. Every phone screen starts with a
   status bar and one of the top bars; a block on it starts with a section
   header; icons are line icons from icons-v2.js (<svg class="gp-i"><use href="#i-x"/></svg>). */

/* 2-9 status bar: the phone's own, drawn so screens read as screens */
.gp-sb { flex: none; display: flex; justify-content: space-between; align-items: center; height: 47px; padding: 4px 30px 0 34px; font-size: 16px; font-weight: 600; color: var(--gp-text); }
.gp-sb span:last-child { display: flex; gap: 6px; align-items: center; }
.gp-sb i { display: block; width: 25px; height: 12px; border: var(--gp-w-hair) solid currentColor; border-radius: 4px; position: relative; }
.gp-sb i::after { content: ""; position: absolute; inset: 2px 5px 2px 2px; background: currentColor; border-radius: 2px; }
/* the system keyboard, drawn as a block of its height so a screen with the keyboard up reads right (K1) */
.gp-kbd { flex: none; height: 291px; display: grid; place-items: center; background: var(--gp-rule-soft); color: var(--gp-text-muted); font-size: var(--gp-fs-small); }

/* 2-10 top bars. A bare 44pt icon button sits in all of them */
.gp-navbtn { width: 44px; height: 44px; display: grid; place-items: center; flex: none; padding: 0; border: 0; background: none; color: var(--gp-text); cursor: pointer; }
.gp-navbtn.is-link { color: var(--gp-label); }
/* a word instead of an icon: 完成 */
.gp-navbtn--text { width: auto; font-size: var(--gp-fs-lead); font-weight: 600; }
/* main tabs: wordmark, band badge, avatar */
.gp-appbar { flex: none; display: flex; align-items: center; gap: var(--gp-s2); height: 48px; padding: 0 var(--gp-s5); }
.gp-appbar__wm { font-family: var(--gp-font-wordmark); font-size: 17px; font-weight: 600; font-variation-settings: "opsz" 17; }
.gp-appbar__wm + .gp-badge { margin-left: auto; }
.gp-appbar.is-scrolled { border-bottom: var(--gp-w-hair) solid var(--gp-rule-soft); }
.gp-avatar { width: 30px; height: 30px; flex: none; border-radius: 50%; display: grid; place-items: center; background: var(--gp-label); color: var(--gp-on-fill); font-size: var(--gp-fs-meta); font-weight: 600; }
/* pushed or modal page: button | centred title | button (an empty <span> keeps a side) */
.gp-bar { flex: none; display: grid; grid-template-columns: 44px 1fr 44px; align-items: center; height: 44px; padding: 0 8px; }
.gp-bar > b { text-align: center; font-size: var(--gp-fs-lead); font-weight: 600; }
/* in a set: leave, progress, 作答輔助 */
.gp-qbar { flex: none; display: flex; align-items: center; gap: 10px; height: 52px; padding: 0 var(--gp-s5) 0 6px; }
.gp-qbar .gp-steps { flex: 1; }
/* onboarding (10/16): one unbroken bar instead of steps, so adding a page doesn't add a segment */
.gp-qbar > .gp-meter { flex: 1; }
/* 作答頁只寫「第幾／幾題」，不放進度條，免得跟播放列搞混（C）；模擬考右邊是倒數（__r，時間到變杏） */
.gp-qbar__n { flex: 1; font-size: var(--gp-fs-small); font-weight: 600; color: var(--gp-text-2); font-variant-numeric: tabular-nums; }
.gp-qbar__r { flex: none; margin-left: auto; display: inline-flex; align-items: center; gap: 4px; font: 600 var(--gp-fs-small) var(--gp-font-display); color: var(--gp-text-2); font-variant-numeric: tabular-nums; }
.gp-qbar__r.is-near { color: var(--gp-near); }
/* top of a sheet: grabber, then close and one text link */
.gp-grab { flex: none; width: 36px; height: 5px; margin: 6px auto 0; border-radius: 3px; background: var(--gp-rule-strong); }
.gp-sheetbar { flex: none; display: flex; justify-content: space-between; align-items: center; height: 40px; padding: 0 12px 0 6px; }
.gp-sheetbar .gp-navbtn { color: var(--gp-text-2); }
.gp-sheetbar a { padding: 0 8px; font-size: var(--gp-fs-small); color: var(--gp-label); text-decoration: none; }

/* 2-11 section header: blue icon, 20/600 title, a summary on the right */
.gp-sh { display: flex; align-items: center; gap: var(--gp-s2); min-height: 32px; }
/* 區塊標題負責分區，不負責搶眼：跟內文同大、只多一階粗，icon 退成灰 */
.gp-sh .gp-i { color: var(--gp-text-muted); width: 18px; height: 18px; }
.gp-sh h2 { margin: 0; font-size: var(--gp-fs-lead); font-weight: 600; }
.gp-sh__r { margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: var(--gp-fs-small); color: var(--gp-text-muted); font-variant-numeric: tabular-nums; }
.gp-sh__r .gp-i { color: var(--gp-text-muted); width: 16px; height: 16px; }

/* 2-12 line icon: 24-unit box, round 1.8 stroke, colour from the text */
.gp-i { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex: none; }
.gp-i--sm { width: 16px; height: 16px; }
.gp-i--fill { fill: currentColor; stroke: none; }
