/* JOVANA — Reader's Passport 讀者護照 · the stamp layer.
   Ported from the approved design prototype (Readers Passport.dc.html).

   THIS FILE LOADS ON EVERY PAGE, including art-*.html and arcade-*.html, which load NEITHER
   site-skins.css NOR the theme blocks. That is why every declaration goes through
   var(--pp-x, <editorial literal>): on those pages the fallback IS the palette. It also
   satisfies tools/page-chrome.test.mjs, which forbids bare hex in injected chrome.

   THE PASSPORT DOES NOT RE-SKIN. A passport is the one document that does not change when you
   cross a border: its paper, its inks, its faces and its rough edge are the design prototype's,
   verbatim, on every skin and every theme. The skins may restyle the site around it; the marks
   themselves are the Bureau's. (An earlier revision let each skin adapt the stamps — doodle
   switched the ink texture off, swapped the faces for its own — and the owner rightly rejected
   it: the handoff's art style is the product.) */

.jv-passport {
  /* the design's two voices of type, pinned — never the skin's */
  --pp-serif: "Cormorant Garamond", "Noto Serif TC", "Noto Serif SC", Georgia, serif;
  --pp-sans:  "Hanken Grotesk", "Noto Sans TC", "Noto Sans SC", system-ui, sans-serif;

  /* paper + ink — the passport's own, on every skin */
  --pp-paper:      #FBF9F4;
  --pp-paper-2:    #F4F0E8;
  --pp-edge:       rgba(27, 24, 19, .16);
  --pp-perf:       rgba(27, 24, 19, .30);
  --pp-ink:        #1B1813;
  --pp-ink-soft:   #4C463E;
  --pp-ink-faint:  #6E675C;   /* styles.css:43 certifies this at ≥4.6:1 on the cream surfaces */
  --pp-lock:       #8C8478;   /* the unstamped mark. 3.51:1 on cream — WCAG 1.4.11 wants 3:1 */

  /* the design's accents, pinned */
  --pp-accent:     #4F5BE3;
  --pp-accent-2:   #8A5CEC;
  --pp-accent-ink: #4a40c9;
  --pp-grad: linear-gradient(118deg, #4F5BE3 0%, #6a59e8 45%, #8A5CEC 100%);

  /* Flat colours ONLY. An SVG paint attribute takes a <color>, never a CSS gradient — passing
     a linear-gradient() renders BLACK and swallows what is behind it. public/spread.css:31
     records the same scar. These four are the stops of the holographic seal. */
  --pp-holo-1: #4a40c9;
  --pp-holo-2: #1f8a7a;
  --pp-holo-3: #b06a2d;
  --pp-holo-4: #8A5CEC;

  /* confetti, recoloured per skin — the arcade's candy palette is wrong on editorial cream */
  --pp-conf-1: #4F5BE3; --pp-conf-2: #8A5CEC; --pp-conf-3: #1f8a7a;
  --pp-conf-4: #E8B84B; --pp-conf-5: #B03434; --pp-conf-6: #1B1813;

  --pp-tilt:   8deg;          /* the resting angle of a mark that was pressed by a hand */
  --pp-radius: 16px;
  --pp-shadow: 0 6px 24px rgba(27, 24, 19, .08);
  --pp-rough:  1;             /* 0 disables the ink filter (prefers-contrast asks for that) */
}

/* .sr-only lives in reader.css and spread.css but NOT styles.css, and neither of those loads
   site-wide. So this file declares its own. */
.jv-passport .sr-only, .jv-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* The overlays — ceremony, toast, issuance — are pieces of the passport that land on OTHER
   pages, so they carry the document's faces and buttons with them instead of inheriting
   whatever skin the host page is wearing. */
.jv-fx { font-family: var(--pp-sans, "Hanken Grotesk", system-ui, sans-serif); }
.jv-fx .btn-primary {
  background: var(--pp-grad, linear-gradient(118deg, #4F5BE3 0%, #6a59e8 45%, #8A5CEC 100%));
  color: #fff; border: 0; border-radius: 999px;
  box-shadow: 0 8px 24px -8px rgba(110, 89, 232, .6);
  font-family: inherit; font-weight: 700;
}
.jv-fx .btn-ghost {
  background: var(--pp-paper, #FBF9F4); color: var(--pp-ink, #1B1813);
  border: 1px solid var(--pp-edge, rgba(27, 24, 19, .16)); border-radius: 999px;
  font-family: inherit;
}

/* ══ THE STAMP ═══════════════════════════════════════════════════════════ */

.jv-stamp {
  display: inline-grid; place-items: center; gap: 4px;
  background: none; border: 0; padding: 6px 4px 10px; margin: 0;
  font: inherit; color: inherit; cursor: pointer;
  border-radius: 50%;
  perspective: 620px;
}
.jv-stamp[disabled] { cursor: default; }

/* the rotating/tilting inner element — transform lives here so the button keeps a stable
   hit area and a stable focus ring while the art moves */
.jv-stamp-art {
  display: block;
  transform: rotate(var(--rot, 0deg));
  transition: transform .42s cubic-bezier(.22, .68, .32, 1);
  will-change: auto;
}
.jv-stamp-art svg { display: block; overflow: visible; }

/* THE FILTER GOES ON THE <g>, NOT ON THE <svg>.
   This is the difference between a stamp and a slightly wobbly diagram. On the SVG element the
   displacement is measured in CSS pixels, so a 2.4 scale is 2.4 screen pixels however large the
   mark is drawn — a faint shimmer. On the inner <g> it is measured in USER units of the 200-wide
   viewBox, so at a 260px render the same 2.4 becomes ~3.1px of travel and the noise grain scales
   with the artwork: the ring genuinely breaks up, the way ink does when a rubber die lifts.

   Still driven by a class, so the IntersectionObserver can switch it off for everything not
   being painted — the single largest performance lever on a page that can hold sixty stamps. */
.jv-stamp--rough .jv-stamp-art .jv-s > g { filter: url(#jvInk); }
.jv-stamp--dry   .jv-stamp-art .jv-s > g { filter: url(#jvInkLite); }
/* A visa is an HTML card, not an SVG, and a CSS filter roughens its border just as well —
   which is exactly how the prototype gets that hand-pressed rectangle. */
.jv-stamp--rough .jv-stamp-art .jv-visa { filter: url(#jvInk); }

.jv-stamp:hover .jv-stamp-art,
.jv-stamp:focus-visible .jv-stamp-art {
  transition-duration: .28s;
}
.jv-stamp.is-tracking .jv-stamp-art { transition: none; }

/* Round target, round ring, and offset enough to clear the seal's own outer circle so it does
   not read as a third concentric line. Extends the site's own idiom at styles.css:263. */
.jv-stamp:focus-visible {
  outline: 2px solid var(--pp-accent, #4F5BE3);
  outline-offset: 3px;
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--pp-accent, #4F5BE3) 22%, transparent);
}
/* Keyboard focus never tilts. Movement on focus is disorienting; focus gets the ring. */

/* ══ THE INK COLOURS ═════════════════════════════════════════════════════
   The prototype gives every domain its own ink — cinnabar for Mathematics, steel for AI,
   indigo for Physics, jade for Neuroscience — and that variety is most of why a page of
   stamps reads as a passport rather than a spreadsheet. The colour is chosen by JS (which
   knows the artefact's page) and applied as a data attribute rather than an inline hex, so
   the whole palette stays in this one place. */
[data-hue="ink"]     { --pp-ink: #1B1813; }
[data-hue="indigo"]  { --pp-ink: #4a40c9; }
[data-hue="steel"]   { --pp-ink: #1f6f8a; }
[data-hue="jade"]    { --pp-ink: #1F7A6D; }
[data-hue="amber"]   { --pp-ink: #b06a2d; }
[data-hue="sienna"]  { --pp-ink: #8a5a2b; }
[data-hue="magenta"] { --pp-ink: #A03470; }
[data-hue="crimson"] { --pp-ink: #B03434; }
[data-hue="rose"]    { --pp-ink: #9B2C4E; }
[data-hue="olive"]   { --pp-ink: #5C6B3A; }
[data-hue="plum"]    { --pp-ink: #6B4C8A; }

/* ── SVG paint and type, in CSS where var() actually resolves ─────────────
   A presentation attribute (fill="var(--pp-ink)") is NOT a CSS declaration: the function is
   never substituted, the paint falls back to black and the type to the default serif. Geometry
   stays in attributes; everything paintable is a class. */
.jv-s-ring  { fill: none; stroke: var(--pp-ink, #1B1813); }
.jv-s-core  { fill: none; }
.jv-s-dot   { fill: var(--pp-ink, #1B1813); }
.jv-s-glyph {
  fill: var(--pp-ink, #1B1813);
  font-family: var(--pp-serif, "Cormorant Garamond", Georgia, serif);
  font-size: 46px; font-style: italic; font-weight: 600;
}
.jv-s-date {
  fill: var(--pp-ink, #1B1813);
  font-family: var(--pp-sans, "Hanken Grotesk", system-ui, sans-serif);
  font-size: 10px; font-weight: 700; letter-spacing: 1.5px;
}
/* One rim, all the way round: English · 中文 · JOVANA ·. The lower half reads inverted, which
   is how a real rubber die is cut and how the design draws it. The sans face carries both
   scripts here — Noto Sans TC/SC is in the same request as Hanken Grotesk, so the mixed string
   stays on one baseline instead of hopping between a sans and a serif mid-word. */
.jv-s-rim {
  font-weight: 700; fill: var(--pp-ink, #1B1813);
  font-family: var(--pp-sans, "Hanken Grotesk", "Noto Sans TC", system-ui, sans-serif);
  font-size: 12.5px; letter-spacing: 3px;
}
/* a Grand Seal names its subject in Chinese where a stamp puts its date */
.jv-s-zh {
  fill: var(--pp-ink, #1B1813);
  font-family: var(--pp-sans, "Noto Sans TC", system-ui, sans-serif);
  font-size: 12px; font-weight: 700; letter-spacing: 2px;
}
.jv-s-glyph--seal { font-size: 42px; }

/* A Grand Seal is struck in foil. url(#…) in a CSS fill/stroke IS a real paint server and does
   resolve — it is only the gradient FUNCTION that cannot live in a paint attribute. */
.jv-s--seal .jv-s-dot,
.jv-s--seal .jv-s-glyph,
.jv-s--seal .jv-s-date,
.jv-s--seal .jv-s-rim  { fill: url(#jvHolo); }
.jv-s--seal .jv-s-ring { stroke: url(#jvHolo); fill: none; }
.jv-s--seal .jv-s-core { fill: url(#jvHolo); fill-opacity: .08; }

/* ── the milestone visa ──────────────────────────────────────────────────
   A dashed passport endorsement: serif glyph, both names, a date and a serial. */
.jv-visa {
  width: var(--vw, 214px); box-sizing: border-box;
  border: 2.5px solid var(--pp-ink, #1B1813);
  opacity: .95;
  border-radius: 10px; padding: 16px 18px 13px;
  color: var(--pp-ink, #1B1813);
  background: color-mix(in srgb, var(--pp-paper, #FBF9F4) 55%, transparent);
  text-align: left;
}
.jv-visa-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
/* One line, as printed. Wrapping "JOVANA PASSPORT · 里程碑" onto three lines is what a form
   does, not what a rubber plate does. */
.jv-visa-auth { font-size: 9.5px; letter-spacing: .22em; font-weight: 800; white-space: nowrap; }
.jv-visa-glyph {
  font-family: var(--pp-serif, "Cormorant Garamond", Georgia, serif);
  font-size: 21px; font-style: italic; font-weight: 600; line-height: 1;
}
.jv-visa-name {
  font-family: var(--pp-serif, "Cormorant Garamond", Georgia, serif);
  font-size: 25px; font-weight: 600; line-height: 1.1; margin-top: 3px;
}
.jv-visa-zh { font-size: 12px; opacity: .85; margin-top: 1px; }
/* An unissued visa is the same card, unfilled — the design's `op: e ? .95 : .4`. */
.jv-visa--locked { opacity: .4; }
.jv-visa-rule { border-top: 1.5px dashed currentColor; margin: 10px 0 7px; opacity: .7; }
.jv-visa-foot {
  display: flex; justify-content: space-between; gap: 8px;
  font-size: 10px; letter-spacing: .16em; font-weight: 700;
  /* The design sets the foot in the same tracked sans caps as the head — the whole endorsement
     is one voice of type, and .16em of tracking already keeps a serial legible. */
  font-family: var(--pp-sans, "Hanken Grotesk", system-ui, sans-serif);
  white-space: nowrap;
}

.jv-stamp-name {
  font-family: var(--pp-serif, "Cormorant Garamond", Georgia, serif);
  font-weight: 600; font-size: 18px; line-height: 1.15;
  color: var(--pp-ink, #1B1813);
  text-align: center; max-width: 15ch;
}
.jv-stamp-zh {
  font-size: 11.5px; line-height: 1.45; color: var(--pp-ink-faint, #6E675C);
  text-align: center; max-width: 22ch;
  /* Clamped so one long earning condition cannot make its whole grid row taller than the
     others. The full text is still in the accessible name and in the detail card. */
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.jv-stamp--locked .jv-stamp-name { color: var(--pp-ink-faint, #6E675C); }

/* THE LOCKED STATE IS A DIFFERENT DRAWING, NOT A FADED ONE.
   The prototype's opacity:.13 measures ≈1.05:1 on cream — not "failing", invisible. And it
   costs a full filtered SVG to render something nobody can see. This is six nodes instead of
   twenty, it passes 1.4.11, and it reads as an empty space on the page WAITING for a stamp,
   which is what a passport page actually looks like. */
.jv-lockmark {
  position: relative; display: grid; place-items: center;
  width: var(--s, 132px); height: var(--s, 132px);
  border: 2.5px dashed var(--pp-lock, #8C8478);
  border-radius: 50%;
  color: var(--pp-lock, #8C8478);
}
.jv-lockmark::before {
  content: ""; position: absolute; inset: calc(var(--s, 132px) * .055);
  border: 1px solid var(--pp-lock, #8C8478); border-radius: 50%; opacity: .7;
}
.jv-lockmark span {
  font-family: var(--pp-serif, "Cormorant Garamond", Georgia, serif);
  font-style: italic; font-weight: 600;
  font-size: calc(var(--s, 132px) * .3); line-height: 1;
}
/* Locked-ness is never colour alone: dashed ring + the padlock + the word. */
.jv-lockmark::after {
  content: "⌧"; position: absolute; bottom: calc(var(--s, 132px) * .1);
  font-size: calc(var(--s, 132px) * .1); opacity: .8;
}

/* A locked GRAND SEAL is blind-embossed: the ring pressed into the paper with no ink at all.
   That is precisely what an unstamped seal looks like in the real world, and it is beautiful
   rather than sad. */
.jv-lockmark--blind {
  border-style: solid;
  border-color: color-mix(in srgb, var(--pp-ink, #1B1813) 10%, transparent);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 70%, transparent),
    0 1px 0 color-mix(in srgb, #fff 55%, transparent);
  color: color-mix(in srgb, var(--pp-ink, #1B1813) 22%, transparent);
}
.jv-lockmark--blind::after { content: none; }

/* the animated foil sweep on an earned seal */
.jv-holo {
  position: absolute; inset: 8%; border-radius: 50%; pointer-events: none;
  mix-blend-mode: overlay;
  background: linear-gradient(115deg, transparent 30%, rgba(255,255,255,.65) 45%,
              rgba(120,220,255,.40) 50%, rgba(255,170,255,.40) 55%, transparent 70%);
  background-size: 200% 200%;
  animation: jvHolo 3.2s linear infinite;
}
.jv-stamp-wrap { position: relative; display: inline-block; }

/* ══ KEYFRAMES — the prototype's, verbatim where the values were already right ═══ */

@keyframes jvSlam {
  0%   { opacity: 0; transform: scale(2.9) rotate(-34deg) translateY(-14px); }
  55%  { opacity: 1; }
  78%  { transform: scale(.94) rotate(10deg); }
  100% { opacity: 1; transform: scale(1) rotate(var(--rot, 8deg)); }
}
@keyframes jvShake {
  10%, 90% { transform: translate(-1px, 1px); }
  20%, 80% { transform: translate(2px, -1px); }
  30%, 50%, 70% { transform: translate(-3px, 2px); }
  40%, 60% { transform: translate(3px, -2px); }
}
@keyframes jvRing  { 0% { transform: scale(.42); opacity: .9 } 100% { transform: scale(1.9); opacity: 0 } }
@keyframes jvSplat { 0% { transform: scale(0) rotate(0deg); opacity: 0 } 60% { opacity: 1 } 100% { transform: scale(1) rotate(24deg); opacity: .85 } }
@keyframes jvMarq  { from { transform: translateX(0) } to { transform: translateX(-50%) } }
@keyframes jvHolo  { from { background-position: 0% 50% } to { background-position: 200% 50% } }
@keyframes jvFloat { 0%, 100% { transform: translateY(0) rotate(var(--rot, -7deg)) } 50% { transform: translateY(-12px) rotate(calc(var(--rot, -7deg) + 2deg)) } }
@keyframes jvRise  { from { opacity: 0; transform: translateY(24px) } to { opacity: 1; transform: none } }
@keyframes jvPulse { 0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--pp-accent, #4F5BE3) 35%, transparent) } 50% { box-shadow: 0 0 0 9px transparent } }

/* ══ REDUCED MOTION ══════════════════════════════════════════════════════
   Rule by rule, following styles.css:612 — NOT the prototype's blanket
   *{animation:none!important}, which would also kill the site's own .reveal entrance and would
   make the static ISSUED substitution below impossible to draw. */
@media (prefers-reduced-motion: reduce) {
  .jv-holo { animation: none; background-position: 40% 50%; }
  .jv-stamp-art { transition: none; }
  .jv-marquee-track { animation: none; transform: none; }
  .jv-stamp-art, .jv-cer-art { animation: none !important; }
}

/* prefers-contrast: a rough edge lowers effective contrast, so drop the filter and go solid. */
@media (prefers-contrast: more) {
  .jv-stamp--rough .jv-stamp-art svg, .jv-stamp--dry .jv-stamp-art svg { filter: none; }
  .jv-lockmark { border-style: solid; border-width: 3px; }
}

/* the shared <defs> host — present, zero-sized, never painted */
.jv-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ══ THE CELEBRATION ═════════════════════════════════════════════════════ */

.jv-conf {
  position: fixed; inset: 0; z-index: 96; pointer-events: none;
  width: 100vw; height: 100vh;
}

/* ── the toast ───────────────────────────────────────────────────────────
   z-index 95 clears .nav (80) and .lang-pop / .jv-menu (90). pointer-events live on the card
   only — the page keeps scrolling and focus is never stolen, because this is a notification and
   not a dialog. */
.jv-toast {
  position: fixed; right: 22px; bottom: 22px; z-index: 95;
  width: min(360px, calc(100vw - 32px));
  display: grid; grid-template-columns: 96px 1fr auto; gap: 12px; align-items: start;
  padding: 14px 14px 16px;
  background: var(--pp-paper, #FBF9F4);
  border: 1px solid var(--pp-edge, rgba(27,24,19,.16));
  border-radius: var(--pp-radius, 16px);
  box-shadow: var(--shadow-lg, 0 24px 70px -24px rgba(40,30,70,.35));
  color: var(--pp-ink, #1B1813);
  animation: jvToastIn .22s cubic-bezier(.2, .7, .2, 1) both;
}
@media (max-width: 560px) { .jv-toast { right: 16px; left: 16px; bottom: 16px; } }
.jv-toast--out { animation: jvToastOut .26s ease-in both; }
@keyframes jvToastIn  { from { opacity: 0; transform: translateY(20px) } to { opacity: 1; transform: none } }
@keyframes jvToastOut { to { opacity: 0; transform: translateY(8px) } }

.jv-toast-art { display: grid; place-items: center; }
.jv-toast-kicker {
  font-size: 10.5px; letter-spacing: .22em; text-transform: uppercase; font-weight: 800;
  color: var(--pp-accent-ink, #4a40c9);
}
.jv-toast-name {
  font-family: var(--pp-serif, Georgia, serif); font-weight: 600;
  font-size: 21px; line-height: 1.15; margin-top: 3px;
}
.jv-toast-zh { font-size: 12px; color: var(--pp-ink-faint, #6E675C); margin-top: 2px; }
.jv-toast-go {
  display: inline-block; margin-top: 9px; font-size: 13px; font-weight: 600;
  color: var(--pp-accent-ink, #4a40c9); text-decoration: none;
}
.jv-toast-go:hover { text-decoration: underline; }
.jv-toast-count {
  float: right; font-family: var(--pp-serif, Georgia, serif); font-style: italic;
  font-size: 13px; color: var(--pp-ink-faint, #6E675C);
}
.jv-toast-x {
  width: 44px; height: 44px; margin: -8px -8px 0 0;   /* 44px hit area, visually smaller */
  border: 0; background: none; cursor: pointer;
  font-size: 20px; line-height: 1; color: var(--pp-ink-faint, #6E675C);
}
.jv-toast-x:hover { color: var(--pp-ink, #1B1813); }

/* the toast's mark lands, but quietly — no shake, no confetti, no backdrop */
.jv-slam-lite .jv-stamp-art { animation: jvSlamLite .34s cubic-bezier(.18, 1.4, .36, 1) both; }
@keyframes jvSlamLite {
  0%   { opacity: 0; transform: scale(1.9) rotate(-22deg) }
  100% { opacity: 1; transform: rotate(var(--rot, 6deg)) }
}

/* ── the ceremony ────────────────────────────────────────────────────────
   The only thing on this site allowed to interrupt, and it is done in about two seconds. */
.jv-cer-back {
  position: fixed; inset: 0; z-index: 100;
  display: grid; place-items: center; padding: 20px;
  background: color-mix(in srgb, var(--pp-ink, #1B1813) 55%, transparent);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  animation: jvFade .18s cubic-bezier(.2, .7, .2, 1) both;
}
@keyframes jvFade { from { opacity: 0 } to { opacity: 1 } }

.jv-cer {
  position: relative; width: min(560px, 94vw); text-align: center;
  padding: 30px clamp(20px, 4vw, 40px) 28px;
  background: var(--pp-paper, #FBF9F4);
  border: 1px solid var(--pp-edge, rgba(27,24,19,.16));
  border-radius: 24px;
  box-shadow: 0 24px 70px -24px rgba(40, 30, 70, .45);
  transform: rotate(-.6deg);   /* the prototype's — a card set down by a hand, not a browser */
  color: var(--pp-ink, #1B1813);
  /* A short viewport must never clip the actions. The card scrolls rather than the page, so
     Escape and Close stay reachable at any height. */
  max-height: calc(100vh - 40px); overflow-y: auto;
}
@media (max-width: 560px) {
  /* a centred modal on a 360px screen has no margin to breathe in, so it becomes a sheet */
  .jv-cer-back { place-items: end center; padding: 0; }
  .jv-cer { width: 100%; border-radius: 24px 24px 0 0; transform: none; }
}
.jv-shake { animation: jvShake .32s cubic-bezier(.36, .07, .19, .97) both; }

.jv-cer-kicker {
  font-size: 11.5px; letter-spacing: .3em; text-transform: uppercase; font-weight: 800;
  color: var(--pp-accent-ink, #4a40c9);
}
.jv-cer-stage {
  position: relative; display: grid; place-items: center; margin-top: 6px;
  min-height: clamp(190px, 36vh, 310px);
}
/* the empty slot the mark lands into — 200ms of nothing is what makes the slam read as a slam */
.jv-cer-slot {
  position: absolute; width: 62%; aspect-ratio: 1; border-radius: 50%;
  border: 2px dashed var(--pp-lock, #8C8478); opacity: .5;
}
.jv-cer-art { position: relative; }
.jv-slam .jv-stamp-art { animation: jvSlam .42s cubic-bezier(.16, 1.2, .3, 1) both; }

/* the impact: an expanding ink shockwave and nine droplets, on one frame */
.jv-impact::before {
  content: ""; position: absolute; width: 62%; aspect-ratio: 1; border-radius: 50%;
  border: 3px solid var(--pp-accent, #4F5BE3);
  animation: jvRing .62s cubic-bezier(.16, .8, .28, 1) both;
  pointer-events: none;
}
.jv-impact::after {
  content: ""; position: absolute; inset: 12%;
  background:
    radial-gradient(circle at 14% 22%, var(--pp-accent, #4F5BE3) 0 4px, transparent 5px),
    radial-gradient(circle at 86% 18%, var(--pp-accent, #4F5BE3) 0 3px, transparent 4px),
    radial-gradient(circle at 22% 84%, var(--pp-accent, #4F5BE3) 0 3px, transparent 4px),
    radial-gradient(circle at 78% 88%, var(--pp-accent, #4F5BE3) 0 5px, transparent 6px),
    radial-gradient(circle at 50% 96%, var(--pp-accent, #4F5BE3) 0 3px, transparent 4px);
  animation: jvSplat .52s ease-out both;
  pointer-events: none;
}

.jv-cer-title {
  font-family: var(--pp-serif, Georgia, serif); font-weight: 600;
  font-size: clamp(26px, 4vw, 34px); line-height: 1.1; margin: 2px 0 0;
  outline: none;
}
.jv-cer-zh { font-size: 15px; color: var(--pp-ink-soft, #4C463E); margin-top: 4px; }
.jv-cer-sub { font-size: 14px; color: var(--pp-ink-faint, #6E675C); margin-top: 8px; }
.jv-cer-row { display: flex; justify-content: center; gap: 8px; margin-top: 14px; min-height: 0; }
.jv-cer-row .jv-stamp { animation: jvRise .38s cubic-bezier(.2, .7, .2, 1) both; }
.jv-cer-more {
  align-self: center; font-family: var(--pp-serif, Georgia, serif); font-style: italic;
  font-size: 16px; color: var(--pp-ink-faint, #6E675C);
}
.jv-cer-actions {
  display: flex; justify-content: center; gap: 12px; margin-top: 22px; flex-wrap: wrap;
  opacity: 0; pointer-events: none; transition: opacity .3s;
}
/* Actions stay inert until the moment has landed, so an eager Return cannot skip it. Escape
   still works from the very first frame — never trap somebody in a party. */
.jv-cer-ready .jv-cer-actions { opacity: 1; pointer-events: auto; }
.jv-cer-title, .jv-cer-zh, .jv-cer-sub { animation: jvRise .42s cubic-bezier(.2, .7, .2, 1) both; }

/* a locked mark's card — the same dialog with none of the celebration */
.jv-det .jv-cer-stage { min-height: 240px; }
.jv-det-hint {
  margin: 14px auto 0; max-width: 40ch;
  font-family: var(--pp-serif, Georgia, serif); font-style: italic;
  font-size: 17px; line-height: 1.5; color: var(--pp-accent-ink, #4a40c9);
}

/* the reduced-motion substitution: a static over-stamp that says the one thing the animation
   was there to say */
.jv-cer-issued {
  position: absolute; bottom: 8%; left: 50%;
  transform: translateX(-50%) rotate(-7deg);
  border: 2.5px solid var(--pp-accent-ink, #4a40c9);
  color: var(--pp-accent-ink, #4a40c9);
  border-radius: 6px; padding: 5px 14px;
  font-size: 12px; font-weight: 800; letter-spacing: .22em; text-transform: uppercase;
}

/* ══ 開卷 · THE ISSUANCE ═════════════════════════════════════════════════
   Once, ever. A cover that writes itself, opens, and takes the first stamp. */
.jv-cer-back.jv-open { display: grid; place-items: center; align-content: center; gap: 22px; }

.jv-open-skip {
  position: absolute; top: 20px; right: 20px;
  border: 1px solid color-mix(in srgb, var(--pp-paper, #FBF9F4) 40%, transparent);
  background: none; color: var(--pp-paper, #FBF9F4);
  border-radius: 999px; padding: 7px 16px; font: inherit; font-size: 13px; cursor: pointer;
}
.jv-open-skip:hover { background: color-mix(in srgb, var(--pp-paper, #FBF9F4) 14%, transparent); }

.jv-open-book { position: relative; width: min(420px, 88vw); aspect-ratio: 1 / 1.24; perspective: 1400px; }

/* page one, underneath */
.jv-open-page {
  position: absolute; inset: 0; border-radius: 14px;
  background: var(--pp-paper, #FBF9F4);
  border: 1px solid var(--pp-edge, rgba(27,24,19,.16));
  display: grid; grid-template-rows: 1fr auto; place-items: center;
  padding: 26px 24px 30px; gap: 18px;
}
.jv-open-slot { position: relative; display: grid; place-items: center; }
.jv-open-sign { width: 100%; text-align: center; }
.jv-open-line { display: block; border-top: 1px dashed var(--pp-perf, rgba(27,24,19,.3)); margin-bottom: 8px; }
.jv-open-name {
  font-family: var(--pp-serif, Georgia, serif); font-style: italic; font-size: 22px;
  color: var(--pp-ink, #1B1813); min-height: 1.2em; display: inline-block;
}

/* the cover, on top, hinged at its left edge */
.jv-open-cover {
  position: absolute; inset: 0; border-radius: 14px; z-index: 2;
  background: var(--pp-cover, #1B1813);
  color: var(--pp-paper, #FBF9F4);
  display: grid; align-content: center; justify-items: center; gap: 14px;
  padding: 40px 30px;
  transform-origin: left center; backface-visibility: hidden;
  box-shadow: 0 30px 80px -30px rgba(0, 0, 0, .7);
  transition: transform .72s cubic-bezier(.42, 0, .28, 1);
}

.jv-open-rule { width: 60%; height: 1px; background: currentColor; opacity: .45; }
.jv-open-mark {
  font-family: var(--pp-serif, Georgia, serif); font-weight: 600;
  font-size: 30px; letter-spacing: .16em;
}
.jv-open-title {
  font-family: var(--pp-serif, Georgia, serif); font-weight: 600; margin: 0;
  font-size: 15px; letter-spacing: .34em; text-transform: uppercase; text-align: center;
  outline: none;
  /* the title writes itself: a wipe rather than a fade, so it reads as being inscribed */
  clip-path: inset(0 100% 0 0);
  transition: clip-path .9s cubic-bezier(.3, .7, .2, 1);
}
.jv-open-mark, .jv-open-rule { opacity: 0; transition: opacity .45s ease .1s; }
.jv-open-written .jv-open-mark, .jv-open-written .jv-open-rule { opacity: 1; }
.jv-open-written .jv-open-title { clip-path: inset(0 0 0 0); }
.jv-open-turned .jv-open-cover { transform: rotateY(-146deg); }

.jv-open-word, .jv-open-go {
  opacity: 0; transform: translateY(8px);
  transition: opacity .4s, transform .4s;
}
.jv-open-word {
  font-family: var(--pp-serif, Georgia, serif); font-style: italic;
  font-size: clamp(16px, 2.2vw, 20px); line-height: 1.5; text-align: center;
  color: var(--pp-paper, #FBF9F4); max-width: 34ch; margin: 0;
}
.jv-open-done .jv-open-word, .jv-open-done .jv-open-go { opacity: 1; transform: none; }
.jv-open-go { pointer-events: none; }
.jv-open-done .jv-open-go { pointer-events: auto; }

/* reduced motion: the book is already open, already signed, already stamped */
.jv-open-instant .jv-open-cover { display: none; }
.jv-open-instant .jv-open-title { clip-path: none; }

@media (prefers-reduced-motion: reduce) {
  .jv-open-cover { transition: none; display: none; }
  .jv-open-title { clip-path: none; transition: none; }
  .jv-open-mark, .jv-open-rule, .jv-open-word, .jv-open-go { opacity: 1; transform: none; transition: none; }
  .jv-open-go { pointer-events: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .jv-toast, .jv-toast--out, .jv-cer-back,
  .jv-cer-title, .jv-cer-zh, .jv-cer-sub, .jv-cer-row .jv-stamp { animation: none; }
  .jv-cer-actions { opacity: 1; pointer-events: auto; transition: none; }
  .jv-cer-slot { display: none; }
}
