/* ============================================================================
   PJSK Viewer — UI styled after Project Sekai: Colorful Stage
   ----------------------------------------------------------------------------
   Design DNA (derived from the official app + colorfulstage.com + pjsekai.sega.jp):
     · Bright, light background — soft sky/cream gradients with confetti motifs
     · Pop-flat surfaces: white cards with very soft shadows + rounded corners
     · Pill-shaped buttons, white with a colored leading icon
     · Triangle (▲) confetti background pattern in pink/cyan/mint
     · Rounded gothic typography (M PLUS Rounded 1c / Zen Maru Gothic)
     · Pink accent (#ff5fb1-ish) used sparingly for emphasis
     · Unit identity via colored "seal" chips on the side of each card
   ============================================================================ */

/* Google Fonts are loaded non-blocking from index.html via the
   rel="preload" + media="print" onload swap pattern. Do NOT add an
   @import here — it would re-introduce render blocking. */

:root {
  /* base palette — light, like the official app */
  --bg-1: #eaf5ff;          /* sky top */
  --bg-2: #fbeaf5;          /* pink bottom */
  --surface: #ffffff;
  --surface-2: #f7f9ff;
  --ink: #1a2240;
  --ink-soft: #58608a;
  --muted: #8b94bb;
  --line: #e2e8f5;
  --line-strong: #cfd6ef;

  --accent: #ff5fb1;        /* PJSK pink */
  --accent-2: #33d1e6;      /* miku cyan */
  --accent-3: #88dd44;      /* fresh green */
  --accent-4: #ffd166;      /* highlight yellow */
  --accent-violet: #884cc2;

  /* official-leaning unit colors */
  --u-light_sound:    #4455dd;  /* Leo/need cobalt */
  --u-idol:           #88dd44;  /* MMJ leaf green */
  --u-street:         #ee1166;  /* VBS hot red */
  --u-theme_park:     #ff9900;  /* WxS goldenrod */
  --u-school_refusal: #884cc2;  /* 25-ji violet */
  --u-piapro:         #33d1e6;  /* VS Miku cyan */
  --u-none:           #8b94bb;

  /* shadows */
  --shadow-sm: 0 1px 3px rgba(34, 44, 90, 0.08), 0 1px 2px rgba(34, 44, 90, 0.04);
  --shadow-md: 0 6px 18px rgba(34, 44, 90, 0.10), 0 2px 5px rgba(34, 44, 90, 0.06);
  --shadow-lg: 0 18px 38px rgba(34, 44, 90, 0.14), 0 6px 12px rgba(34, 44, 90, 0.08);
  --shadow-pop: 0 8px 0 rgba(34, 44, 90, 0.10); /* slight chunky-shadow for pill buttons */

  --radius: 14px;
  --radius-lg: 22px;
  --radius-pill: 999px;

  --font-ui: "M PLUS Rounded 1c", "Zen Maru Gothic", "Hiragino Maru Gothic ProN",
             -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --font-display: "Zen Maru Gothic", "M PLUS Rounded 1c", "Hiragino Maru Gothic ProN", sans-serif;
}

/* ---------- reset & globals ---------- */
* { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0;
  color: var(--ink);
  font-family: var(--font-ui);
  font-weight: 500;
  font-size: 15px;
  line-height: 1.55;
  letter-spacing: 0.01em;
  min-height: 100vh;
  background:
    radial-gradient(1100px 700px at 80% -10%, #d8efff 0%, transparent 60%),
    radial-gradient(900px 700px at -10% 110%, #ffe0f0 0%, transparent 55%),
    linear-gradient(180deg, var(--bg-1) 0%, var(--bg-2) 100%);
  background-attachment: fixed;
}

/* Triangle confetti motif drawn with CSS — multiple layered SVG data URIs */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  background-image:
    /* pink triangle small */
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'><polygon points='14,4 25,24 3,24' fill='%23ff8fc4' opacity='0.32'/></svg>"),
    /* cyan triangle medium */
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='44' height='44' viewBox='0 0 44 44'><polygon points='22,6 40,38 4,38' fill='%2333d1e6' opacity='0.22'/></svg>"),
    /* mint triangle large */
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='60' height='60' viewBox='0 0 60 60'><polygon points='30,8 54,52 6,52' fill='%2388dd44' opacity='0.18'/></svg>");
  background-position: 6% 12%, 84% 28%, 22% 78%;
  background-repeat: no-repeat;
  background-size: 28px, 44px, 60px;
}
/* second sprinkle layer for richness */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  background-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'><polygon points='10,3 18,17 2,17' fill='%23ffb74d' opacity='0.25'/></svg>"),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='36' height='36' viewBox='0 0 36 36'><polygon points='18,5 33,31 3,31' fill='%23ff5fb1' opacity='0.18'/></svg>"),
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'><polygon points='7,2 13,12 1,12' fill='%23884cc2' opacity='0.22'/></svg>");
  background-position: 92% 8%, 14% 42%, 68% 88%;
  background-repeat: no-repeat;
  background-size: 20px, 36px, 14px;
}

a { color: var(--accent); text-decoration: none; font-weight: 600; }
a:hover { color: #e23f96; }
img { display: block; max-width: 100%; }

code {
  font-family: ui-monospace, "SF Mono", "JetBrains Mono", monospace;
  background: #eef2fb; color: #36406b;
  padding: 1px 6px; border-radius: 6px;
  font-size: 0.88em; font-weight: 500;
}

/* ---------- layout ---------- */
body {
  display: grid;
  grid-template-columns: 270px 1fr;
  min-height: 100vh;
  position: relative;
}

/* ---------- top header pills (rank/coins style) ---------- */
.top-bar {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 22px; flex-wrap: wrap;
}
.header-pill {
  background: var(--surface);
  border-radius: var(--radius-pill);
  padding: 6px 16px 6px 8px;
  box-shadow: var(--shadow-sm);
  display: inline-flex; align-items: center; gap: 10px;
  font-weight: 700;
  color: var(--ink);
  font-size: 13px;
}
.header-pill .ico {
  width: 28px; height: 28px;
  border-radius: 50%;
  display: grid; place-items: center;
  background: linear-gradient(135deg, var(--accent-2), var(--accent));
  color: white; font-size: 14px;
}
.header-pill .label { color: var(--muted); font-size: 11px; font-weight: 600; margin-right: 4px; text-transform: uppercase; letter-spacing: 0.08em; }
.header-pill .val { color: var(--ink); font-weight: 800; }

/* ---------- sidebar ---------- */
#nav {
  background: var(--surface);
  border-right: 1px solid var(--line);
  padding: 22px 16px;
  position: sticky; top: 0;
  height: 100vh;
  overflow-y: auto;
  display: flex; flex-direction: column;
  box-shadow: 2px 0 18px rgba(34, 44, 90, 0.04);
  z-index: 10;
}
.brand {
  display: flex; align-items: center; gap: 12px;
  margin-bottom: 22px;
  padding: 6px 4px;
}
.brand .brand-mark {
  width: 44px; height: 44px;
  border-radius: 14px;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: white;
  display: grid; place-items: center;
  font-family: var(--font-display);
  font-weight: 900; font-size: 18px;
  box-shadow: 0 4px 14px rgba(255, 95, 177, 0.32);
  letter-spacing: -1px;
}
.brand .brand-text { line-height: 1.1; }
.brand .logo {
  font-family: var(--font-display);
  font-weight: 900; font-size: 18px;
  color: var(--ink);
  letter-spacing: -0.3px;
}
.brand .brand-sub {
  font-size: 10px; color: var(--muted);
  margin-top: 3px;
  letter-spacing: 0.18em; text-transform: uppercase;
  font-weight: 700;
}
/* When the brand-sub doubles as the build-version badge, the contents are
   a long SHA + UTC timestamp — the uppercase + 0.18em letter-spacing
   default would overflow the nav. Soften the typography and let it wrap
   onto two lines if the sidebar is narrow. Tabular numerals keep digits
   monospaced so version strings line up nicely between deploys. */
.brand .brand-sub#build-version {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 500;
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  word-break: break-word;
  line-height: 1.25;
  max-width: 160px;
}

#nav nav { display: flex; flex-direction: column; gap: 4px; }
#nav nav a {
  display: flex; align-items: center; gap: 10px;
  color: var(--ink-soft);
  padding: 11px 14px;
  border-radius: 12px;
  font-weight: 700;
  font-size: 14px;
  transition: background .12s ease, color .12s ease, transform .12s ease;
}
#nav nav a:hover { background: var(--surface-2); color: var(--ink); }
#nav nav a.active {
  background: linear-gradient(95deg, rgba(255,95,177,0.12), rgba(51,209,230,0.10));
  color: var(--ink);
  box-shadow: inset 0 0 0 1px rgba(255,95,177,0.18);
}
#nav nav a .nico {
  width: 26px; height: 26px;
  border-radius: 8px;
  background: var(--surface-2);
  display: grid; place-items: center;
  font-size: 13px;
  color: var(--accent);
  flex-shrink: 0;
}
#nav nav a.active .nico { background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: white; }

.region {
  margin-top: 18px;
  padding: 12px;
  background: var(--surface-2);
  border-radius: 14px;
  border: 1px solid var(--line);
}
.region label {
  font-size: 10px; color: var(--muted);
  display: block; margin-bottom: 6px;
  text-transform: uppercase; letter-spacing: 0.14em;
  font-weight: 700;
}
.region select {
  width: 100%;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 8px 10px;
  font-size: 13px; font-weight: 600;
  font-family: var(--font-ui);
  cursor: pointer;
}
.region select:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

#nav footer {
  margin-top: auto;
  color: var(--muted);
  font-size: 11px;
  padding-top: 18px;
  font-weight: 500;
}
#nav footer a { color: var(--ink-soft); }

/* ---------- main ---------- */
#app {
  padding: 28px 36px 80px;
  max-width: 1320px;
  position: relative;
  z-index: 1;
}
#view.loading::after {
  content: ""; display: block;
  width: 36px; height: 36px;
  border: 3px solid var(--line);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .9s linear infinite;
  margin: 100px auto;
}
@keyframes spin { to { transform: rotate(360deg); } }

h1, h2, h3 {
  font-family: var(--font-display);
  color: var(--ink);
  letter-spacing: -0.01em;
}
h1 { margin: 0 0 6px; font-size: 30px; font-weight: 900; }
h2 { margin: 30px 0 14px; font-size: 20px; font-weight: 800; }
h2 .accent-bar {
  display: inline-block;
  width: 6px; height: 22px; border-radius: 3px;
  background: linear-gradient(180deg, var(--accent), var(--accent-2));
  vertical-align: -4px;
  margin-right: 10px;
}
h3 { margin: 0 0 4px; font-size: 15px; font-weight: 800; }
p.lead { color: var(--ink-soft); margin: 0 0 22px; max-width: 760px; font-weight: 500; }

/* ---------- hero ---------- */
.hero {
  position: relative;
  background:
    linear-gradient(135deg, #ffffff 0%, #f4f8ff 100%);
  border-radius: var(--radius-lg);
  padding: 36px 36px 32px;
  margin-bottom: 26px;
  box-shadow: var(--shadow-md);
  overflow: hidden;
}
.hero::before {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(500px 280px at 90% 10%, rgba(51,209,230,0.18), transparent 60%),
    radial-gradient(420px 280px at 10% 110%, rgba(255,95,177,0.20), transparent 60%);
  pointer-events: none;
}
.hero::after {
  content: ""; position: absolute; top: -10px; right: -10px;
  width: 200px; height: 200px;
  background:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'><polygon points='30,30 60,80 0,80' fill='%23ff5fb1' opacity='0.18'/><polygon points='130,20 170,90 90,90' fill='%2333d1e6' opacity='0.18'/><polygon points='60,120 90,170 30,170' fill='%2388dd44' opacity='0.18'/><polygon points='140,130 180,180 100,180' fill='%23ffd166' opacity='0.18'/></svg>") center/contain no-repeat;
  pointer-events: none;
}
.hero h1 {
  position: relative;
  font-size: 34px;
  background: linear-gradient(95deg, var(--accent), var(--accent-2) 80%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero p { position: relative; color: var(--ink-soft); margin: 10px 0 0; max-width: 720px; font-weight: 500; }

/* ---------- grid + cards ---------- */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 16px;
}
.card {
  position: relative;
  background: var(--surface);
  border-radius: var(--radius);
  padding: 16px 16px 14px;
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--line);
  overflow: hidden;
  cursor: pointer;
  transition: transform .14s ease, box-shadow .14s ease, border-color .14s ease;
  display: block;
  color: var(--ink);
}
.card:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-md);
  border-color: var(--line-strong);
  color: var(--ink);
}
.card h3 { font-size: 15px; font-weight: 800; color: var(--ink); margin: 6px 0 4px; line-height: 1.3; }
.card .tag {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; padding: 3px 10px;
  border-radius: var(--radius-pill);
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: white;
  font-weight: 800;
  letter-spacing: 0.04em;
  margin-bottom: 4px;
  text-transform: uppercase;
  box-shadow: 0 2px 6px rgba(255,95,177,0.30);
}
.card .meta {
  color: var(--ink-soft);
  font-size: 12px;
  margin-top: 6px;
  font-weight: 500;
}
.card.unit-stripe::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
  background: var(--stripe, var(--u-none));
}
.card.unit-stripe {
  padding-left: 20px;
}

/* unit color helpers */
.u-light_sound { --stripe: var(--u-light_sound); }
.u-idol { --stripe: var(--u-idol); }
.u-street { --stripe: var(--u-street); }
.u-theme_park { --stripe: var(--u-theme_park); }
.u-school_refusal { --stripe: var(--u-school_refusal); }
.u-piapro { --stripe: var(--u-piapro); }
.u-none { --stripe: var(--u-none); }

/* unit "seal" — like the in-game round chip */
.unit-seal {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--surface);
  border-radius: var(--radius-pill);
  padding: 4px 14px 4px 4px;
  box-shadow: var(--shadow-sm);
  font-size: 13px; font-weight: 800;
  color: var(--ink);
}
.unit-seal .dot {
  width: 26px; height: 26px;
  border-radius: 50%;
  background: var(--stripe);
  box-shadow: inset 0 -2px 0 rgba(0,0,0,0.12);
  display: grid; place-items: center;
  color: white; font-size: 12px; font-weight: 900;
}

/* breadcrumbs */
.crumbs { color: var(--ink-soft); margin-bottom: 18px; font-size: 13px; font-weight: 600; }
.crumbs a { color: var(--ink-soft); }
.crumbs a:hover { color: var(--accent); }

/* ---------- inputs / search ---------- */
.search-row { display: flex; gap: 10px; margin: 8px 0 22px; flex-wrap: wrap; }
.search-row input, .search-row select {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: 10px 18px;
  font-size: 14px; font-weight: 600;
  font-family: var(--font-ui);
  min-width: 180px;
  box-shadow: var(--shadow-sm);
  transition: border-color .14s ease, box-shadow .14s ease;
}
.search-row input:focus, .search-row select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 4px rgba(255,95,177,0.18);
}
.search-row input::placeholder { color: var(--muted); }

/* ---------- empty / messages ---------- */
.empty {
  color: var(--ink-soft);
  padding: 50px;
  text-align: center;
  background: var(--surface);
  border: 2px dashed var(--line-strong);
  border-radius: var(--radius-lg);
  font-weight: 600;
}

/* ---------- buttons ---------- */
.btn {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: 10px 20px;
  font-size: 14px;
  font-weight: 800;
  font-family: var(--font-ui);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: transform .1s ease, box-shadow .14s ease, border-color .14s ease;
  display: inline-flex; align-items: center; gap: 8px;
}
.btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); border-color: var(--line-strong); }
.btn:active { transform: translateY(0); }
.btn.primary {
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: white;
  border-color: transparent;
  box-shadow: 0 6px 14px rgba(255,95,177,0.30);
}
.btn.primary:hover { box-shadow: 0 10px 22px rgba(255,95,177,0.40); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }

/* The "big action" pill button used on the home for the four hero CTAs,
   inspired by the in-game bottom action bar (Gacha/Characters/Story/Show). */
.action-pills {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 14px;
  margin: 0 0 28px;
}
.action-pill {
  background: var(--surface);
  border-radius: var(--radius-pill);
  padding: 10px 18px 10px 10px;
  display: flex; align-items: center; gap: 12px;
  box-shadow: var(--shadow-md);
  border: 1px solid var(--line);
  cursor: pointer;
  transition: transform .12s ease, box-shadow .14s ease;
  color: var(--ink);
  text-decoration: none;
}
.action-pill:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); color: var(--ink); }
.action-pill .ap-ico {
  width: 44px; height: 44px;
  border-radius: 14px;
  display: grid; place-items: center;
  font-size: 20px;
  color: white;
  background: linear-gradient(135deg, var(--ap-c1, var(--accent)), var(--ap-c2, var(--accent-2)));
  flex-shrink: 0;
  box-shadow: 0 4px 10px rgba(34,44,90,0.18);
}
.action-pill .ap-text { line-height: 1.15; }
.action-pill .ap-title { font-weight: 800; font-size: 15px; color: var(--ink); }
.action-pill .ap-sub { font-size: 12px; color: var(--ink-soft); margin-top: 2px; font-weight: 500; }

.action-pill.c-stories  { --ap-c1: #ff7eb6; --ap-c2: #ff5fb1; }
.action-pill.c-events   { --ap-c1: #ffb74d; --ap-c2: #ff9900; }
.action-pill.c-chars    { --ap-c1: #88dd44; --ap-c2: #4caf50; }
.action-pill.c-music    { --ap-c1: #33d1e6; --ap-c2: #4455dd; }
.action-pill.c-assets   { --ap-c1: #b388e0; --ap-c2: #884cc2; }

/* ---------- character grid ---------- */
.char-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 14px;
}
.char-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 18px 14px 14px;
  text-align: center;
  box-shadow: var(--shadow-sm);
  transition: transform .14s ease, box-shadow .14s ease;
  position: relative;
  overflow: hidden;
}
.char-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
.char-card::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 60px;
  background: linear-gradient(180deg, var(--stripe, var(--u-none)) 0%, transparent 100%);
  opacity: 0.18;
  pointer-events: none;
}
.char-avatar {
  width: 84px; height: 84px;
  margin: 4px auto 12px;
  border-radius: 50%;
  background: var(--stripe, #cfd6ef);
  display: flex; align-items: center; justify-content: center;
  font-size: 32px;
  font-family: var(--font-display);
  font-weight: 900;
  color: white;
  box-shadow:
    inset 0 -4px 0 rgba(0,0,0,0.12),
    0 6px 16px rgba(34,44,90,0.16);
  border: 3px solid white;
  position: relative;
  z-index: 1;
}
.char-name { font-weight: 800; font-size: 14px; color: var(--ink); }
.char-name-en {
  color: var(--muted);
  font-size: 11px;
  letter-spacing: 0.08em;
  font-weight: 700;
  margin-top: 2px;
  text-transform: uppercase;
}
.char-unit-label {
  color: var(--ink-soft);
  font-size: 11px;
  margin-top: 8px;
  font-weight: 700;
  padding: 4px 10px;
  background: var(--surface-2);
  border-radius: var(--radius-pill);
  display: inline-block;
}

/* ---------- reader ---------- */
.reader {
  display: grid; grid-template-columns: 1fr; gap: 18px;
  max-width: 940px; margin: 0 auto;
}
.reader-stage {
  position: relative;
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: 0;
  overflow: hidden;
  aspect-ratio: 16/9;
  background-image: var(--bg-img, none);
  background-size: cover;
  background-position: center;
  display: flex; align-items: flex-end;
  box-shadow: var(--shadow-md);
  border: 1px solid var(--line);
  /* Stage is click-to-advance / click-to-skip-reveal (Phase 12). The stage
     click handler explicitly bails when the event target is inside the
     dialogue text (.speaker / .line / .jp-popup / [data-jp-lookable]) so
     selecting text, hovering JP tokens, and clicking popup buttons never
     advance the player. See reader/index.js stage.addEventListener('click'). */
  cursor: pointer;
  /* Suppress the mobile browser default tap-highlight (a translucent blue
     wash Chrome/Android paints over the tap target before the click fires).
     In fullscreen this reads as a brief blue tint over the whole viewport
     just before the dialogue advances. We have our own ring + confetti
     feedback; the OS overlay is redundant and out-of-style. */
  -webkit-tap-highlight-color: transparent;
}
/* Same suppression for the app-wide tap-fx-layer when it's reparented
   into #stage on fullscreen — it inherits hit-testing from the stage,
   and some browsers paint the highlight on the layer itself. Belt and
   braces. */
.tap-fx-layer,
.reader-stage * {
  -webkit-tap-highlight-color: transparent;
}
/* Subtle bottom vignette so white speaker text always has a darker substrate
   behind it, no matter what the scene background looks like. This is gentler
   than the previous white wash and matches the in-game story player. */
.reader-stage::before {
  content: ""; position: absolute; inset: 0;
  background:
    linear-gradient(180deg, transparent 0%, transparent 55%, rgba(0,0,0,0.40) 100%);
  pointer-events: none;
  z-index: 2;
}
/* If no background image we want a soft fallback gradient */
.reader-stage:not([style*="--bg-img"])::after {
  content: ""; position: absolute; inset: 0;
  background:
    linear-gradient(135deg, #d8efff 0%, #ffe0f0 100%);
  pointer-events: none;
}
/* Default stacking for direct children. Specific overlays (dialogue, scene
   banner) opt out by setting position:absolute themselves — see those rules. */
.reader-stage > *:not(.dialogue-overlay):not(.scene-banner):not(.stage-portrait):not(.stage-live2d):not(.info-pills):not(.stage-bg):not(.stage-fullcolor):not(.stage-flashback):not(.stage-blackwipe):not(.stage-fulltext):not(.stage-fs-btn):not(.stage-settings-btn):not(.stage-settings-panel):not(.jp-popup):not(.tap-fx-layer) {
  position: relative; z-index: 2;
}
/* .tap-fx-layer is reparented into #stage when fullscreen activates so the
   tap effect renders above the FS host. The allowlist above used to capture
   it and apply position:relative; z-index:2, which collapsed the layer to
   0×0 and reset its inset:0 — making child .tap-fx absolutes position
   relative to a stub at its natural-flow corner, so ring scales rendered
   as a teal viewport tint ("blue tint, no rings" bug). The :not exemption
   above + the rule at line 2321 below keep position:fixed; inset:0 active. */

/* Background cross-fade layers (Phase 9). Two stacked absolute divs that
   ping-pong as the active BG changes — the incoming one fades to opacity:1
   while the outgoing one fades to 0. The 300ms timing is roughly halfway
   between sekai-viewer's 200ms layer fade and the perceived feel of a
   scene transition in-game. */
.reader-stage .stage-bg {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  transition: opacity 300ms ease;
  z-index: 1;
  pointer-events: none;
}
.reader-stage .stage-bg.active { opacity: 1; }

/* While the BG is swapping, dim the dialogue overlay so text doesn't fight
   the transition. Mirrors sekai-viewer's dialog.hide(200) -> draw bg -> show. */
.dialogue-overlay.fading { opacity: 0; transition: opacity 200ms ease; }

/* Dialogue-hidden state (Commit E of mobile touch overhaul).
   When the reader has `.dialogue-hidden`, the dialogue bubble + speaker +
   any JP-lookup popups fade out and stop intercepting pointer events so
   the user sees the artwork unobstructed. A subsequent tap or swipe-up
   removes the class (handled in JS) and the overlay fades back in.

   We intentionally do NOT hide .scene-banner or .info-pills here — those
   are scene metadata, not dialogue. The user can still see which scene
   they're in while the dialogue is dismissed. */
.reader.dialogue-hidden .dialogue-overlay,
.reader.dialogue-hidden .jp-popup {
  opacity: 0;
  pointer-events: none;
  transition: opacity 220ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .reader.dialogue-hidden .dialogue-overlay,
  .reader.dialogue-hidden .jp-popup {
    transition: none;
  }
}

/* Speaker portrait — lightweight "character on stage" using member_cutout.
   Full Live2D rendering would require shipping the Cubism SDK (~1MB JS)
   plus loading .moc3 models from sekai-live2d-assets, so we use this
   static-but-real game-asset approach instead. */
.reader-stage .stage-portrait {
  position: absolute;
  right: 4%;
  bottom: 0;
  max-height: 92%;
  max-width: 48%;
  object-fit: contain;
  object-position: bottom right;
  z-index: 2;
  filter: drop-shadow(0 8px 24px rgba(34,44,90,0.18));
  transition: opacity 0.25s ease, transform 0.35s ease;
  opacity: 1;
  pointer-events: none;
}
.reader-stage .stage-portrait.hidden { opacity: 0; }

/* Live2D canvas overlay — lives in the same stacking slot as the cutout but
   spans the full stage so the model's bottom-anchor positioning matches the
   in-game framing (feet near the bottom edge, head 95% up). The canvas is
   `position: absolute` so it doesn't claim layout space when hidden. */
.reader-stage .stage-live2d {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
  pointer-events: none;
  /* Slight drop shadow under the character to anchor them visually on the
     scene background, similar to the cutout's filter. */
  filter: drop-shadow(0 8px 24px rgba(34,44,90,0.22));
}
.reader-stage .stage-live2d[hidden] { display: none; }

.reader-stage .info-pills {
  position: absolute; top: 16px; left: 16px; right: 16px;
  display: flex; gap: 8px; flex-wrap: wrap; z-index: 4;
}
.reader-stage .info-pills[hidden] { display: none; }
.pill {
  background: rgba(255,255,255,0.92);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.6);
  border-radius: var(--radius-pill);
  padding: 5px 12px;
  font-size: 12px;
  color: var(--ink-soft);
  font-weight: 700;
  box-shadow: var(--shadow-sm);
}
.pill strong { color: var(--ink); font-weight: 800; margin-left: 4px; }

/* In-game style dialogue overlay (Phase 8). No panel, no border — just white
   text with a strong black stroke so it stays legible on any background.
   Speaker name sits above the line with a thin gradient underline. */
.dialogue-overlay {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 0 7% 5%;
  z-index: 3;
  /* Soft dark scrim behind the dialogue area so white text stays legible
     on bright scene backgrounds without requiring heavy per-glyph shadows.
     Starts fully transparent at the top, ramps to 33% black by 50px, then
     holds that level to the bottom edge. */
  background: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0) 0,
    rgba(0, 0, 0, 0.33) 50px,
    rgba(0, 0, 0, 0.33) 100%
  );
  /* Overlay itself ignores pointer events so it doesn't block stage clicks,
     but individual text nodes opt back in (see .speaker / .line below) so
     the dialogue is selectable + hoverable for future enhancements. */
  pointer-events: none;
  /* Per-step text stroke so the in-game look survives over any background. */
  --txt-stroke: 1.5px;
}
.reader.has-secondary .dialogue-overlay {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 28px;
}
/* `[hidden]` must beat the .has-secondary display:grid rule above so effect-step
   scene banners don't show stale dialogue text underneath them. !important is
   the simplest cross-browser fix — the `hidden` attribute should always win. */
.dialogue-overlay[hidden] { display: none !important; }
@media (max-width: 720px) {
  .reader.has-secondary .dialogue-overlay {
    grid-template-columns: 1fr;
    gap: 6px;
  }
  .dialogue-overlay { padding: 0 5% 4%; }
}

/* Speaker name + its sibling gradient underline. Splitting them lets the
   underline sit at a fixed y regardless of font metrics, and lets us shift
   the name independently to overlap the underline like the in-game label. */
.speaker-row {
  position: relative;
  display: inline-block;        /* shrink-wraps to the speaker text */
  padding-bottom: 6px;          /* reserves room for the underline track */
}
.speaker {
  /* Roboto 900 for Latin, Zen Kaku Gothic New 900 for JP. The softer Roboto
     paired with a wide, semi-transparent black halo (no drop shadow) gives
     the in-game label that fuzzy outline rather than a hard stroke. */
  font-family: "Roboto", "Zen Kaku Gothic New", "Hiragino Kaku Gothic ProN", "Yu Gothic", var(--font-display);
  font-weight: 900;
  font-size: clamp(15px, 1.7vw, 21px);
  color: #fff;
  display: block;
  padding-right: 26px;          /* reserve right-side space for the fade */
  /* Fat translucent black halo around each glyph — reads as a soft outline
     against scene art without the brittle look of a hard 1.4px stroke. */
  -webkit-text-stroke: 4px rgba(0, 0, 0, 0.3);
  paint-order: stroke fill;
  letter-spacing: 0.02em;
  position: relative;
  z-index: 1;
  /* Pull the name down + slightly left so the bottom of the glyphs lands
     on the sibling underline and the halo extends past the gradient's
     left edge (matches the in-game label overlap). */
  margin-bottom: -4px;
  margin-left: -10px;
  transform: translateY(4px);
  /* Selectable + hoverable for future enhancements. */
  pointer-events: auto;
  user-select: text;
}
.speaker[hidden] { display: none; }
/* If both the name and its underline are hidden, collapse the row entirely so
   narrator/unknown lines float at the bottom with no reserved label height. */
.speaker-row:has(> .speaker[hidden]) { display: none; }

/* Sibling gradient underline. Extends past the speaker-row's content box
   (left:-30, right:-200) so the streak runs in from the left margin and
   trails further to the right than the name itself. */
.speaker-underline {
  position: absolute;
  left: -20px; right: -200px; bottom: 0;
  height: 8px;
  border-radius: 4px;
  background: linear-gradient(90deg,
    rgba(255,255,255,0.75) 0%,
    rgba(255,255,255,0.5) 35%,
    rgba(255,255,255,0) 100%);
  pointer-events: none;
  z-index: 0;
}
.speaker-underline[hidden] { display: none; }

.line {
  /* Phase 9: ~2/3 of previous size so more text fits on one line. The
     speaker label keeps its original 15-21px range so name/line read with
     a clear size contrast (matches in-game proportions).

     Reader font-size slider: --reader-line-scale (set on .reader by
     PREF.fontSize, default 1) multiplies the clamped base size. The
     clamp() still bounds the responsive sizing; the multiplier shifts
     the whole curve up or down within its declared rails. */
  font-size: calc(clamp(11px, 1.2vw, 16px) * var(--reader-line-scale, 1));
  line-height: 1.55;
  /* Phase 10: reserve at least three lines of vertical space so the speaker
     name's y-position doesn't shift between short and long dialogue. */
  min-height: calc(1.55em * 3);
  margin-top: 8px;        /* breathing room below the thicker underline */
  color: #fff;
  font-weight: 600;
  white-space: pre-wrap;
  -webkit-text-stroke: 1.1px #000;
  paint-order: stroke fill;
  text-shadow: 0 2px 8px rgba(0,0,0,0.6);
  letter-spacing: 0.01em;
  /* Selectable + hoverable for future enhancements (mouseover/touch). */
  pointer-events: auto;
  user-select: text;
  cursor: text;
}

/* ---- Scene banner (Phase 8): dark pill centered on stage, used for the
   'place / time-of-day' effect steps. The horizontal gradient gives it a
   fade on both ends, matching the in-game stinger banner. */
.scene-banner {
  position: absolute;
  left: 0; right: 0; top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  z-index: 3;
  pointer-events: none;
  /* Background lives on the banner row itself so the fade can extend wider
     than the text. */
  background:
    linear-gradient(90deg, rgba(33,38,52,0) 0%, rgba(33,38,52,0.78) 22%, rgba(33,38,52,0.85) 50%, rgba(33,38,52,0.78) 78%, rgba(33,38,52,0) 100%);
  padding: 18px 4%;
}
.scene-banner[hidden] { display: none; }
.scene-banner-text {
  color: #fff;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(16px, 2.1vw, 26px);
  letter-spacing: 0.04em;
  text-shadow: 0 2px 8px rgba(0,0,0,0.6);
}
/* Phase 13: secondary-language ruby line beneath the primary banner text.
   Smaller, slightly translucent, sits directly under the primary line. */
.scene-banner-text-secondary {
  color: rgba(255,255,255,0.82);
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(11px, 1.35vw, 16px);
  letter-spacing: 0.02em;
  line-height: 1.25;
  text-shadow: 0 1px 4px rgba(0,0,0,0.55);
  max-width: 80%;
  text-align: center;
}
.scene-banner-text-secondary[hidden] { display: none; }

/* ---- Phase 13 stage overlay layers ---------------------------------
   All four sit absolutely over the stage. They start hidden and are
   driven by inline styles (opacity / clip-path / transform) set by the
   effect helpers in reader.js. */
.stage-fullcolor,
.stage-flashback,
.stage-blackwipe,
.stage-fulltext {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
/* Full-screen color (BlackIn/Out, WhiteIn/Out) — z above bg + chars + bubble,
   below the controls/header so we don't block menu interactions. */
.stage-fullcolor {
  background: #000;
  opacity: 0;
  z-index: 9;
}
.stage-fullcolor[hidden] { display: none; }

/* Flashback tint — lighter than fullcolor (30% black), sits under it. */
.stage-flashback {
  background: rgba(0,0,0,0.30);
  opacity: 0;
  z-index: 4;
}
.stage-flashback[hidden] { display: none; }

/* BlackWipe — solid black layer animated by clip-path. Sits at the same
   stack level as fullcolor so it covers everything but UI chrome. */
.stage-blackwipe {
  background: #000;
  clip-path: inset(0 0 0 100%);
  z-index: 10;
}
.stage-blackwipe[hidden] { display: none; }

/* FullScreenText — centered narration card with optional black fade BG.
   Sits above flashback but below blackwipe so blackwipes still cover it. */
.stage-fulltext {
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 6;
}
.stage-fulltext[hidden] { display: none; }
.stage-fulltext-bg {
  position: absolute;
  inset: 0;
  background: rgba(0,0,0,0.85);
  opacity: 0;
  transition: opacity 200ms ease-in-out;
}
.stage-fulltext-bg[hidden] { display: none; }
.stage-fulltext-body {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 24px 32px;
  text-align: center;
  z-index: 1;
}
.stage-fulltext-line {
  color: #fff;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(20px, 2.6vw, 30px);
  line-height: 1.45;
  letter-spacing: 0.04em;
  text-shadow: 0 2px 12px rgba(0,0,0,0.75);
  max-width: 80vw;
  white-space: pre-wrap;
}
.stage-fulltext-line-secondary {
  color: rgba(255,255,255,0.82);
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(13px, 1.55vw, 18px);
  line-height: 1.4;
  letter-spacing: 0.02em;
  text-shadow: 0 1px 6px rgba(0,0,0,0.7);
  max-width: 78vw;
  white-space: pre-wrap;
}
.stage-fulltext-line-secondary[hidden] { display: none; }

.reader-controls {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 0 4px;
}

/* Audio control strip below the main playback row */
.reader-audio {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  padding: 10px 14px;
  margin: -4px 0 0;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  font-size: 13px;
  color: var(--ink-soft);
}
.reader-audio .audio-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  cursor: pointer; user-select: none;
}
.reader-audio .audio-toggle input[type="checkbox"] {
  accent-color: var(--accent);
  width: 16px; height: 16px;
}
.reader-audio .audio-toggle.vol { gap: 10px; flex: 1; min-width: 160px; max-width: 280px; }
.reader-audio .audio-toggle.vol input[type="range"] {
  flex: 1; accent-color: var(--accent);
}
.reader-audio .now-playing {
  margin-left: auto;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 12px;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
/* The progress bar is an <input type="range"> so users can drag to jump to
   any line. We strip the OS-default appearance and rebuild the track + thumb
   to match the prior gradient-pill look. Each vendor prefix needs its own
   rule because browsers won't apply shared selectors across them. */
.progress {
  flex: 1; min-width: 140px;
  height: 8px;
  background: var(--line);
  border-radius: 999px;
  box-shadow: inset 0 1px 2px rgba(34,44,90,0.06);
  /* Reset range-input chrome (border, focus ring, font) */
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
  padding: 0;
  outline: none;
  cursor: pointer;
  /* The browser-default thumb extends outside the 8px track on some
     engines; allow it to visually overhang without clipping the gradient. */
  overflow: visible;
}
.progress:focus-visible {
  box-shadow: inset 0 1px 2px rgba(34,44,90,0.06), 0 0 0 3px rgba(99, 102, 241, 0.28);
}
/* WebKit / Blink: track + thumb are separate pseudo-elements. */
.progress::-webkit-slider-runnable-track {
  height: 8px;
  background: linear-gradient(
    to right,
    var(--accent) 0%,
    var(--accent-2) calc((var(--progress-pct, 0)) * 1%),
    transparent calc((var(--progress-pct, 0)) * 1%),
    transparent 100%
  );
  border-radius: 999px;
}
.progress::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px; height: 16px;
  margin-top: -4px;          /* center on 8px track */
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid #fff;
  box-shadow: 0 1px 3px rgba(34,44,90,0.25);
}
/* Firefox: --moz pseudo-elements; track + progress are separate primitives. */
.progress::-moz-range-track {
  height: 8px;
  background: transparent;   /* the filled portion paints via -progress */
  border-radius: 999px;
}
.progress::-moz-range-progress {
  height: 8px;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  border-radius: 999px;
}
.progress::-moz-range-thumb {
  width: 16px; height: 16px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid #fff;
  box-shadow: 0 1px 3px rgba(34,44,90,0.25);
}
.counter {
  color: var(--ink-soft);
  font-size: 13px;
  min-width: 80px;
  text-align: right;
  font-weight: 700;
}

.appear { display: flex; gap: 8px; flex-wrap: wrap; margin: 4px 0 0; padding: 0 4px; }
.appear .chip {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: 5px 14px;
  font-size: 12px;
  color: var(--ink-soft);
  font-weight: 700;
  box-shadow: var(--shadow-sm);
}

.script-trail {
  margin-top: 6px;
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--line);
  padding: 10px;
  max-height: 420px;
  overflow-y: auto;
}
.script-trail .entry {
  padding: 10px 14px;
  border-radius: 12px;
  margin-bottom: 4px;
  cursor: pointer;
  opacity: 0.6;
  transition: background .12s ease, opacity .12s ease;
}
.script-trail .entry:hover { background: var(--surface-2); opacity: 1; }
.script-trail .entry.current {
  opacity: 1;
  background: linear-gradient(90deg, rgba(255,95,177,0.10), rgba(51,209,230,0.06));
}
.script-trail .entry .who {
  color: var(--accent);
  font-size: 12px;
  font-weight: 800;
  margin-bottom: 2px;
}
.script-trail .entry .what {
  font-size: 13px;
  color: var(--ink-soft);
  font-weight: 500;
}
.script-trail .entry.current .what { color: var(--ink); }

/* Hide the secondary entry column by default so single-language layout is
   unaffected. When .has-secondary is on, both columns render side-by-side. */
.script-trail .entry .entry-col.secondary { display: none; }
.reader.has-secondary .script-trail .entry {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}
.reader.has-secondary .script-trail .entry .entry-col.secondary {
  display: block;
  border-left: 1px solid rgba(34,44,90,0.08);
  padding-left: 14px;
}
.reader.has-secondary .script-trail .entry .entry-col.secondary .who {
  color: var(--accent-2, #33d1e6);
}
@media (max-width: 720px) {
  .reader.has-secondary .script-trail .entry {
    grid-template-columns: 1fr;
    gap: 6px;
  }
  .reader.has-secondary .script-trail .entry .entry-col.secondary {
    border-left: 0;
    border-top: 1px solid rgba(34,44,90,0.08);
    padding-left: 0;
    padding-top: 6px;
  }
}

/* Secondary-language dropdown in the audio strip */
.reader-audio .secondary-lang select {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 4px 8px;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
  max-width: 200px;
}

/* ---------- asset viewer ---------- */
.asset-viewer {
  display: grid;
  grid-template-columns: 320px 1fr;
  gap: 18px;
  align-items: start;
}
.asset-viewer .panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 18px;
  box-shadow: var(--shadow-sm);
}
.asset-viewer .panel label {
  display: block;
  font-size: 11px;
  color: var(--muted);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  margin-bottom: 6px;
}
.asset-viewer input {
  width: 100%;
  background: var(--surface-2);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 9px 12px;
  margin-bottom: 12px;
  font-size: 13px;
  font-family: ui-monospace, "SF Mono", monospace;
  font-weight: 500;
}
.asset-viewer input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(255,95,177,0.18);
}
.asset-viewer .preset-list { display: grid; gap: 6px; }
.asset-viewer .preset-list .btn {
  text-align: left;
  justify-content: flex-start;
  font-size: 13px;
  padding: 9px 14px;
  font-weight: 600;
}
.asset-viewer .preview {
  min-height: 460px;
  display: flex; align-items: center; justify-content: center;
  background:
    linear-gradient(45deg, #f0f4ff 25%, transparent 25%),
    linear-gradient(-45deg, #f0f4ff 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, #f0f4ff 75%),
    linear-gradient(-45deg, transparent 75%, #f0f4ff 75%);
  background-size: 24px 24px;
  background-position: 0 0, 0 12px, 12px -12px, -12px 0;
  background-color: #fbfcff;
  border-radius: var(--radius-lg);
  padding: 28px;
  border: 1px solid var(--line);
}
.asset-viewer .preview img {
  max-height: 60vh;
  border-radius: 12px;
  box-shadow: var(--shadow-lg);
  background: white;
}

/* ---------- song cards ---------- */
.song-card-art {
  aspect-ratio: 1;
  background-color: var(--surface-2);
  background-size: cover;
  background-position: center;
  border-radius: 10px;
  margin-bottom: 12px;
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--line);
}
.card .tag.tag-soft {
  background: var(--surface-2);
  color: var(--ink-soft);
  margin-right: 4px;
  margin-bottom: 4px;
  box-shadow: none;
  border: 1px solid var(--line);
  font-size: 10px;
  padding: 2px 8px;
}

/* ================================================================
   ASSET-DRIVEN COMPONENTS
   ================================================================ */

/* When a CDN image 404s, hide it cleanly so the layout still flows. */
img.img-broken {
  visibility: hidden;
}

/* ---------- Featured banner (home page hero) ---------- */
.featured-banner {
  position: relative;
  display: block;
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-bottom: 28px;
  aspect-ratio: 21 / 9;
  background: var(--surface-2);
  box-shadow: var(--shadow-lg);
  border: 1px solid var(--line);
  transition: transform .18s ease, box-shadow .18s ease;
  color: white;
  isolation: isolate;
}
.featured-banner:hover { transform: translateY(-2px); box-shadow: 0 18px 40px rgba(20,30,80,0.18); }
.featured-banner-art {
  position: absolute; inset: 0;
  background-size: cover;
  background-position: center;
  z-index: 1;
}
.featured-banner-overlay {
  position: absolute; inset: 0;
  background: linear-gradient(95deg,
    rgba(20, 26, 60, 0.78) 0%,
    rgba(20, 26, 60, 0.55) 36%,
    rgba(20, 26, 60, 0.05) 70%,
    rgba(20, 26, 60, 0)    100%);
  z-index: 2;
}
.featured-banner-content {
  position: absolute;
  left: 36px;
  bottom: 30px;
  z-index: 4;
  max-width: 55%;
}
.featured-banner-eyebrow {
  display: inline-flex; align-items: center; gap: 8px;
  background: rgba(255, 255, 255, 0.16);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.28);
  color: white;
  font-weight: 700; font-size: 11px;
  padding: 6px 12px;
  border-radius: 999px;
  text-transform: uppercase; letter-spacing: 0.12em;
  margin-bottom: 12px;
}
.featured-banner-eyebrow .dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px rgba(255,95,177,0.35);
  animation: pulse 1.8s ease-in-out infinite;
}
@keyframes pulse { 50% { box-shadow: 0 0 0 8px rgba(255,95,177,0); } }
.featured-banner-title {
  font-family: var(--font-display);
  font-size: clamp(22px, 3vw, 36px);
  font-weight: 900;
  margin: 0 0 8px;
  color: white;
  text-shadow: 0 2px 12px rgba(0,0,0,0.4);
  line-height: 1.1;
}
.featured-banner-outline {
  color: rgba(255,255,255,0.92);
  font-size: 14px;
  margin: 0 0 14px;
  text-shadow: 0 1px 6px rgba(0,0,0,0.45);
  font-weight: 500;
  max-width: 540px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.featured-banner-cta { display: flex; align-items: center; gap: 14px; }
.featured-banner-cta .meta { color: rgba(255,255,255,0.85); text-shadow: 0 1px 4px rgba(0,0,0,0.4); }
.featured-banner-logo {
  position: absolute;
  right: 32px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  width: 28%;
  max-width: 320px;
  filter: drop-shadow(0 6px 18px rgba(0,0,0,0.32));
}
.featured-banner-logo img {
  width: 100%; height: auto;
  display: block;
}
@media (max-width: 720px) {
  .featured-banner { aspect-ratio: 16/10; }
  .featured-banner-content { left: 18px; bottom: 16px; max-width: 80%; }
  .featured-banner-logo { display: none; }
}

/* ---------- Song strip (latest songs scroller) ---------- */
.song-strip {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 14px;
}
.song-strip-item {
  display: block;
  transition: transform .14s ease;
}
.song-strip-item:hover { transform: translateY(-3px); }
.song-strip-art {
  aspect-ratio: 1;
  border-radius: 12px;
  overflow: hidden;
  background: var(--surface-2);
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--line);
  margin-bottom: 8px;
}
.song-strip-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.song-strip-title {
  font-weight: 800; color: var(--ink); font-size: 13px;
  line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.song-strip-sub { font-size: 11px; color: var(--muted); margin-top: 2px; }

/* ---------- Cast strip (circular character bubbles) ---------- */
.cast-strip {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(86px, 1fr));
  gap: 14px;
}
.cast-bubble {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  transition: transform .14s ease;
}
.cast-bubble:hover { transform: translateY(-3px); }
.cast-bubble-ring {
  width: 72px; height: 72px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--stripe, var(--accent)), var(--surface));
  padding: 3px;
  display: grid; place-items: center;
  box-shadow: var(--shadow-sm);
  position: relative;
}
.cast-bubble-ring::before {
  content: '';
  position: absolute; inset: 3px;
  border-radius: 50%;
  background: white;
}
.cast-bubble-ring img,
.cast-bubble-letter {
  position: relative;
  width: 64px; height: 64px;
  border-radius: 50%;
  object-fit: cover;
  display: grid; place-items: center;
  font-weight: 900; color: var(--ink); font-size: 22px;
}
.cast-bubble-name {
  font-size: 11px; color: var(--ink-soft); font-weight: 700;
  text-align: center;
  line-height: 1.2;
  max-width: 90px;
}

/* ---------- Event strip (smaller event cards in home) ---------- */
.event-strip {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 14px;
}
.event-strip-item {
  display: block;
  background: var(--surface);
  border-radius: var(--radius-md);
  overflow: hidden;
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  transition: transform .14s ease, box-shadow .14s ease;
}
.event-strip-item:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
.event-strip-art {
  aspect-ratio: 16/9;
  background: var(--surface-2);
  overflow: hidden;
}
.event-strip-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.event-strip-meta { padding: 10px 12px; }
.event-strip-meta h3 { font-size: 13px; margin: 0 0 2px; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.event-strip-meta .meta { font-size: 11px; }

/* ---------- Event grid (event-stories page) ---------- */
.event-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 16px;
}
.event-card {
  display: flex; flex-direction: column;
  background: var(--surface);
  border-radius: var(--radius-md);
  overflow: hidden;
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  transition: transform .14s ease, box-shadow .14s ease;
}
.event-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); border-color: var(--line-strong); }
.event-card-art {
  position: relative;
  aspect-ratio: 16/9;
  background: var(--surface-2);
  overflow: hidden;
}
.event-card-art > img { width: 100%; height: 100%; object-fit: cover; display: block; }
.event-card-logo {
  position: absolute;
  left: 12px; bottom: 10px;
  width: 40%; max-width: 130px;
  filter: drop-shadow(0 2px 6px rgba(0,0,0,0.45));
}
.event-card-logo img { width: 100%; display: block; }
.event-card-body { padding: 12px 14px 14px; }
.event-card-body h3 { font-size: 14px; margin: 6px 0 2px; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* ---------- Event detail hero ---------- */
.event-detail-hero {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) 1fr;
  gap: 28px;
  align-items: center;
  margin-bottom: 18px;
}
.event-detail-banner {
  position: relative;
  aspect-ratio: 16/9;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-md);
}
.event-detail-banner > img { width: 100%; height: 100%; object-fit: cover; display: block; }
.event-detail-logo {
  position: absolute;
  left: 20px; bottom: 18px;
  width: 40%; max-width: 200px;
  filter: drop-shadow(0 4px 10px rgba(0,0,0,0.5));
}
.event-detail-logo img { width: 100%; }
.event-detail-info h1 { font-size: clamp(22px, 2.6vw, 32px); margin-top: 8px; }
@media (max-width: 880px) {
  .event-detail-hero { grid-template-columns: 1fr; }
}

.pill-row {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin-top: 12px;
}
.pill-row .pill {
  background: var(--surface-2);
  border-radius: 999px;
  padding: 8px 14px;
  font-size: 12px;
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  display: inline-flex; align-items: center;
}

/* ---------- Character tile (with portrait) ---------- */
.char-tile {
  position: relative;
  background: var(--surface);
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  overflow: hidden;
  aspect-ratio: 3 / 4;
  box-shadow: var(--shadow-sm);
  transition: transform .15s ease, box-shadow .15s ease;
  isolation: isolate;
}
.char-tile:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
.char-tile-bg {
  position: absolute; inset: 0;
  background:
    radial-gradient(ellipse at 50% 0%, color-mix(in srgb, var(--stripe) 18%, transparent), transparent 65%),
    linear-gradient(180deg, var(--surface) 0%, var(--surface-2) 100%);
  z-index: 1;
}
.char-tile::before {
  content: '';
  position: absolute; top: 0; left: 0; right: 0;
  height: 5px;
  background: linear-gradient(90deg, var(--stripe), color-mix(in srgb, var(--stripe) 50%, white));
  z-index: 5;
}
.char-tile-portrait {
  position: absolute;
  inset: 0 0 65px 0;
  z-index: 2;
  display: flex; align-items: flex-end; justify-content: center;
  overflow: hidden;
}
.char-tile-portrait img {
  max-height: 110%;
  max-width: 130%;
  object-fit: contain;
  filter: drop-shadow(0 6px 14px rgba(20, 30, 80, 0.18));
  transform: translateY(8%);
}
.char-tile-icon {
  position: absolute;
  left: 12px; top: 12px;
  z-index: 4;
  width: 44px; height: 44px;
  border-radius: 12px;
  background: white;
  border: 2px solid var(--stripe);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  display: grid; place-items: center;
}
.char-tile-icon img { width: 100%; height: 100%; object-fit: cover; }
.char-tile-icon span { font-weight: 900; color: var(--ink); font-size: 18px; }
.char-tile-info {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 3;
  padding: 12px 14px 14px;
  background: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.96) 32%);
  text-align: left;
}
.char-tile-name-jp { font-weight: 800; color: var(--ink); font-size: 14px; line-height: 1.15; }
.char-tile-name-en {
  color: var(--muted); font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 700;
  margin-top: 2px;
}

/* Override the old char-grid to be a bit bigger now that we have portraits */
.char-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 16px;
}

/* Unit-coloured accent bar (per-unit gradient) */
.accent-bar-unit {
  background: linear-gradient(180deg, var(--stripe), color-mix(in srgb, var(--stripe) 50%, white)) !important;
}

/* ---------- Music card (with real jacket) ---------- */
.music-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 18px;
}
.music-card {
  display: flex; flex-direction: column;
  background: var(--surface);
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: transform .15s ease, box-shadow .15s ease;
}
.music-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-md); border-color: var(--line-strong); }
.music-card-art {
  aspect-ratio: 1;
  background: var(--surface-2);
  overflow: hidden;
}
.music-card-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.music-card-body { padding: 12px 14px 14px; }
.music-card-body h3 { font-size: 14px; line-height: 1.3; margin: 0 0 4px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.music-card-tags { margin-top: 6px; }

/* ---------- Music detail ---------- */
.music-detail {
  display: grid;
  grid-template-columns: 280px 1fr;
  gap: 32px;
  align-items: start;
}
.music-detail-art {
  aspect-ratio: 1;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface-2);
  box-shadow: var(--shadow-lg);
  border: 1px solid var(--line);
}
.music-detail-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
@media (max-width: 720px) {
  .music-detail { grid-template-columns: 1fr; }
  .music-detail-art { width: 100%; max-width: 320px; }
}

/* ---------- Difficulty chips (PJSK-style coloured badges) ---------- */
.diff-row { display: flex; flex-wrap: wrap; gap: 10px; }
.diff-chip {
  border-radius: 14px;
  padding: 12px 18px;
  color: white;
  min-width: 110px;
  box-shadow: var(--shadow-sm);
  font-weight: 700;
  background: linear-gradient(135deg, var(--diff-c1, #888), var(--diff-c2, #555));
}
.diff-chip-label { font-size: 11px; letter-spacing: 0.12em; opacity: 0.85; }
.diff-chip-level { font-family: var(--font-display); font-size: 28px; font-weight: 900; line-height: 1; margin: 4px 0; }
.diff-chip-notes { font-size: 11px; opacity: 0.85; font-weight: 600; }
.diff-chip.diff-easy    { --diff-c1: #74d6c0; --diff-c2: #34b8a0; }
.diff-chip.diff-normal  { --diff-c1: #6dc5ff; --diff-c2: #3a8fe6; }
.diff-chip.diff-hard    { --diff-c1: #ffd55a; --diff-c2: #ff9a23; }
.diff-chip.diff-expert  { --diff-c1: #ff7790; --diff-c2: #ee2255; }
.diff-chip.diff-master  { --diff-c1: #c280ff; --diff-c2: #7a3ed1; }
.diff-chip.diff-append  { --diff-c1: #ffb6e6; --diff-c2: #ff5fb1; }

/* ---------- responsive ---------- */

/* ---------- responsive ---------- */
@media (max-width: 980px) {
  body { grid-template-columns: 1fr; }
  #nav {
    position: relative;
    height: auto;
    flex-direction: row;
    flex-wrap: wrap;
    padding: 14px;
    gap: 10px;
  }
  .brand { margin-bottom: 0; }
  #nav nav { flex-direction: row; flex-wrap: wrap; flex: 1 1 100%; gap: 4px; }
  #nav nav a { padding: 8px 12px; font-size: 13px; }
  #nav nav a .nico { width: 22px; height: 22px; font-size: 11px; }
  #nav .region { width: 100%; margin: 0; }
  #nav footer { display: none; }
  #app { padding: 18px; }
  .asset-viewer { grid-template-columns: 1fr; }
  .reader-stage { aspect-ratio: 4/3; }
  .hero { padding: 24px 22px; }
  .hero h1 { font-size: 26px; }
}

/* ============================================================================
   JP Lookup popup (Phase: jp-lookup step 6). Anchored hover/tap dictionary
   surface — small card with headword, reading, POS, glosses, paginator, and
   Jisho/Weblio external-search buttons. Positioning is set inline by popup.js
   (left/top via getBoundingClientRect); CSS controls only look + feel.
   ============================================================================ */
.jp-popup {
  /* `position: fixed` is set inline; we keep z-index high so the popup
     floats above the dialogue overlay (z:3) and any stage portrait (z:2). */
  z-index: 50;
  min-width: 220px;
  max-width: 360px;
  background: var(--surface, #fff);
  color: var(--ink, #1a2240);
  border: 1px solid var(--line-strong, #cfd6ef);
  border-radius: var(--radius, 14px);
  box-shadow: var(--shadow-lg, 0 18px 38px rgba(34,44,90,.14), 0 6px 12px rgba(34,44,90,.08));
  padding: 12px 14px;
  font-family: var(--font-ui, system-ui, sans-serif);
  font-size: 14px;
  line-height: 1.45;
  /* Override the dialogue-overlay's pointer-events: none so popup buttons
     are actually clickable when popup is appended into the stage tree. */
  pointer-events: auto;
  /* Flex column so head + foot stay sized to content and body can take the
     remaining space and become scrollable when content overflows. The actual
     max-height is set inline by position() based on viewport room. The gap
     replaces the old body margin-bottom so head/body/foot stay visually
     separated whether or not the body is scrolling. */
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* Hard fallback so a runaway entry can never paint outside the viewport
     even before position() runs (initial measure pass uses default size). */
  max-height: calc(100vh - 24px);
  /* When the body overflows, the rounded corners of the body's scrollbar
     should clip cleanly to the popup's border-radius. */
  overflow: hidden;
}
.jp-popup-body {
  /* In a popup-overflow scenario, the body scrolls independently while head
     and foot remain visible — the standard flex-scroll trick (min-height:0
     lets a flex item shrink below its content size). */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* Smooth touch scrolling on iOS. */
  -webkit-overflow-scrolling: touch;
  /* Slim scrollbar that doesn't fight the popup chrome. */
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong, #cfd6ef) transparent;
}
.jp-popup-body::-webkit-scrollbar { width: 6px; }
.jp-popup-body::-webkit-scrollbar-thumb {
  background: var(--line-strong, #cfd6ef);
  border-radius: 3px;
}
.jp-popup-head, .jp-popup-foot {
  /* Pin the chrome — don't shrink when the body needs room. */
  flex: 0 0 auto;
}
.jp-popup-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: 6px;
  border-bottom: 1px solid var(--line, #e2e8f5);
  padding-bottom: 6px;
}
.jp-popup-headword {
  font-family: "Zen Maru Gothic", var(--font-display, sans-serif);
  font-weight: 700;
  font-size: 20px;
  color: var(--ink, #1a2240);
}
.jp-popup-reading {
  font-size: 13px;
  color: var(--ink-soft, #58608a);
}
.jp-popup-reasons:empty { display: none; }
.jp-popup-reasons {
  font-size: 11px;
  color: var(--muted, #8b94bb);
  font-style: italic;
}
.jp-popup-body {
  /* Stack senses vertically with a small gap. (Scroll/flex sizing for
     overflow is set on the earlier .jp-popup-body block.) */
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* No margin-bottom — the popup root's flex layout handles spacing now;
     a bottom margin here would create dead space between scrollable body
     and the pinned foot, making the foot appear detached. */
}
.jp-popup-sense {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.jp-popup-pos {
  font-size: 11px;
  color: var(--accent-violet, #884cc2);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.jp-popup-gloss { color: var(--ink, #1a2240); }
.jp-popup-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  border-top: 1px solid var(--line, #e2e8f5);
  padding-top: 6px;
}
.jp-popup-pager { display: inline-flex; align-items: center; gap: 6px; }
.jp-popup-pager button {
  width: 24px; height: 24px;
  border-radius: var(--radius-pill, 999px);
  border: 1px solid var(--line-strong, #cfd6ef);
  background: var(--surface-2, #f7f9ff);
  color: var(--ink, #1a2240);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
.jp-popup-pager button:disabled {
  opacity: 0.4;
  cursor: default;
}
.jp-popup-page-label {
  font-size: 11px;
  color: var(--muted, #8b94bb);
  min-width: 32px;
  text-align: center;
}
.jp-popup-ext { display: inline-flex; gap: 6px; }
.jp-popup-ext button {
  border-radius: var(--radius-pill, 999px);
  border: 1px solid var(--line-strong, #cfd6ef);
  background: #fff;
  padding: 3px 10px;
  font-size: 11px;
  font-weight: 700;
  color: var(--ink-soft, #58608a);
  cursor: pointer;
}
.jp-popup-ext button:hover { background: var(--surface-2, #f7f9ff); }

/* JP-lookup EDRDG attribution. Small italic credit under the reader-audio
   bar; CC BY-SA 4.0 license requires visible attribution while the data
   is in use. Anchors inherit the page link color but keep a subtle hover. */
.jp-attribution {
  margin-top: 8px;
  font-size: 11px;
  color: var(--muted, #8b94bb);
  font-style: italic;
  line-height: 1.4;
}
.jp-attribution a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
.jp-attribution a:hover { color: var(--ink-soft, #58608a); }

/* Visual affordance for lookable JP tokens. We use a dotted underline that
   shows up only on hover (so the line of text doesn't look like a sea of
   underlines while reading). On touch devices `:hover` is sticky, so the
   hint only appears mid-long-press — acceptable. */
[data-jp-lookable] {
  cursor: help;
  /* Use text-decoration-* triplet so it doesn't clash with the inherited
     text-decoration on the parent .line. */
  text-decoration: underline dotted transparent;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  transition: text-decoration-color 120ms ease;
}
[data-jp-lookable]:hover {
  text-decoration-color: rgba(255, 255, 255, 0.85);
}

/* Furigana display modes. The reader root carries .furigana-off /
   .furigana-hover / .furigana-always; <rt> elements (kana readings above
   kanji) follow the chosen mode. We use display:none for off so the line
   keeps its native height instead of reserving ruby space. For hover mode
   we keep the rt in the layout but transparent — so it doesn't reflow when
   it appears — and fade it in on hover or popup-open. */
.furigana-off rt { display: none; }
.furigana-always rt {
  visibility: visible;
  font-size: 0.55em;
  color: rgba(255, 255, 255, 0.85);
  font-weight: 400;
  letter-spacing: 0;
  user-select: none;
}
.furigana-hover rt {
  visibility: hidden;
  font-size: 0.55em;
  color: rgba(255, 255, 255, 0.85);
  font-weight: 400;
  letter-spacing: 0;
  user-select: none;
  transition: visibility 0s linear 80ms;
}
.furigana-hover [data-jp-token]:hover rt {
  visibility: visible;
  transition: visibility 0s linear 0s;
}

/* Furigana toggle UI: 3-button segmented control matching the muted/auto
   row visually but compact. Active button uses the accent gradient. */
.furigana-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--ink-soft, #58608a);
}
.furigana-toggle .label { user-select: none; }
.furigana-toggle .seg {
  display: inline-flex;
  border: 1px solid var(--line, #d6dcee);
  border-radius: 6px;
  overflow: hidden;
}
.furigana-toggle .seg button {
  background: transparent;
  border: 0;
  padding: 4px 10px;
  font: inherit;
  color: var(--ink-soft, #58608a);
  cursor: pointer;
  border-right: 1px solid var(--line, #d6dcee);
  min-width: 52px;
}
.furigana-toggle .seg button:last-child { border-right: 0; }
.furigana-toggle .seg button:hover { background: var(--surface-2, #f7f9ff); }
.furigana-toggle .seg button.is-active {
  background: linear-gradient(135deg, #b8caff, #d6b8ff);
  color: #1a1a2e;
  font-weight: 600;
}

/* ============================================================================
   Reader landscape height cap (Phase: mobile QoL).

   When a phone is rotated to landscape, the default 16:9 aspect-ratio on
   .reader-stage drives height from width — and on a typical Android phone
   (e.g. 915 × 412) that yields a stage ~514px tall in a 412px-tall viewport,
   so the bottom of the dialogue overlay is clipped and the user has to
   scroll. We cap height to the dynamic viewport and let width derive from
   the 16:9 aspect-ratio (auto), which keeps the stage centered with
   letterbox padding on the sides instead of vertical clipping.

   Targets short *landscape* viewports only — desktop "landscape" windows
   are always tall enough that the 16:9 stage fits naturally. The 600px
   threshold matches common rotated-phone heights without catching laptops.
   ============================================================================ */
@media (orientation: landscape) and (max-height: 600px) {
  .reader-stage:not(:fullscreen):not(:-webkit-full-screen):not(.is-fake-fullscreen) {
    /* Switch height-driven sizing: derive width from height × 16/9 instead
       of height from width. */
    height: 100dvh;
    max-height: 100dvh;
    width: auto;
    max-width: 100%;
    margin-left: auto;
    margin-right: auto;
  }
  /* Let the reader container shed its 940px cap so the height-driven stage
     can centre itself across the full landscape viewport. */
  .reader { max-width: none; }
}

/* ============================================================================
   Reader fullscreen mode (Phase: mobile QoL).

   Two paths:
     1. Native Fullscreen API     → .reader-stage:fullscreen / :-webkit-full-screen
     2. CSS-only fallback         → .reader-stage.is-fake-fullscreen
                                    (paired with body.reader-fake-fullscreen)

   Both paths drop the 16:9 aspect-ratio so the stage fills the device's
   real aspect (portrait phone, tablet, etc.). Background art uses `cover`
   so it crops gracefully; the dialogue overlay stays anchored to the
   bottom because it was always positioned that way. The fake-fullscreen
   path also locks body scroll + raises z-index above #nav so the stage
   truly covers the viewport.
   ============================================================================ */
.reader-stage:fullscreen,
.reader-stage:-webkit-full-screen,
.reader-stage.is-fake-fullscreen {
  aspect-ratio: auto;
  width: 100vw;
  height: 100vh;
  /* Use dynamic viewport units when available so iOS Safari's URL bar
     doesn't clip the bottom of the dialogue overlay. */
  height: 100dvh;
  max-width: none;
  max-height: none;
  border-radius: 0;
  border: 0;
  box-shadow: none;
}

/* Native Fullscreen API: the browser creates a top-layer host, so we only
   need to remove the aspect-ratio constraint. The CSS-only fallback needs
   position:fixed to overlay the viewport. */
.reader-stage.is-fake-fullscreen {
  position: fixed;
  inset: 0;
  z-index: 1000;
}
body.reader-fake-fullscreen {
  overflow: hidden;
}

/* Fullscreen toggle button. Lives inside #stage as a child element so the
   Fullscreen API's top-layer host (which only displays descendants of the
   element it was called on) keeps it visible. Floats in the top-right
   corner as a discreet "chip" so it doesn't fight with the dialogue or
   the portrait. The same styling applies in normal + fullscreen states
   for consistency; only the safe-area insets are stronger in fullscreen. */
.reader-stage .stage-fs-btn {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 4;
  padding: 6px 12px;
  font-size: 12px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.3);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  cursor: pointer;
  font-weight: 600;
  letter-spacing: 0.01em;
  /* Make sure taps register even if a transparent overlay is on top. */
  pointer-events: auto;
}
.reader-stage .stage-fs-btn:hover {
  background: rgba(0, 0, 0, 0.75);
}
/* In real or fake fullscreen mode, honour the device's safe-area insets
   so notch / status-bar regions don't cover the button. */
:fullscreen .stage-fs-btn,
:-webkit-full-screen .stage-fs-btn,
.reader-stage.is-fake-fullscreen .stage-fs-btn {
  top: max(10px, env(safe-area-inset-top));
  right: max(10px, env(safe-area-inset-right));
}

/* ============================================================================
 * Stage settings drawer (Phase: mobile-fullscreen QoL).
 *
 * A gear chip that floats next to the fullscreen toggle, and a drop-down
 * panel that mirrors the most-used controls from the audio bar below the
 * reader (mute, auto-voice, volume, font size, furigana, secondary language).
 *
 * Why duplicate the controls instead of moving them? In native fullscreen
 * mode the OS top-layer only displays descendants of the element passed to
 * requestFullscreen — that's #stage. The audio bar lives *outside* #stage
 * (it sits in .reader-audio below the controls), so it's hidden whenever the
 * stage is fullscreened. Reparenting the whole bar is brittle (it tangles
 * with grid layout, accessibility focus order, and the existing wiring),
 * so we mount a second copy of the inputs *inside* #stage and keep both
 * sides in sync — see reader/index.js. The fs-* inputs have the same
 * controls; just different ids prefixed `fs-`.
 *
 * Visual style mirrors .stage-fs-btn (translucent-black chip, white text,
 * backdrop blur) so the two stage chips feel like a pair.
 * ========================================================================= */
.reader-stage .stage-settings-btn {
  position: absolute;
  top: 10px;
  right: 130px;
  z-index: 4;
  padding: 6px 12px;
  font-size: 12px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.3);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  cursor: pointer;
  font-weight: 600;
  letter-spacing: 0.01em;
  pointer-events: auto;
}
.reader-stage .stage-settings-btn:hover {
  background: rgba(0, 0, 0, 0.75);
}
/* In real/fake fullscreen, respect safe-area insets so the chip pair
   isn't covered by a notch. The settings chip sits to the LEFT of the
   fullscreen chip; their gap is calculated from the fs chip's known
   width (~120px including padding) to keep them visually paired. */
:fullscreen .stage-settings-btn,
:-webkit-full-screen .stage-settings-btn,
.reader-stage.is-fake-fullscreen .stage-settings-btn {
  top: max(10px, env(safe-area-inset-top));
  right: calc(max(10px, env(safe-area-inset-right)) + 120px);
}

/* The drawer itself — anchored under the gear chip, top-right of the stage.
   White-on-translucent-black is the user's stated preference. Hidden by
   default; toggled via the [hidden] attribute from JS. Max-height keeps
   it from running off short viewports; the body scrolls instead. */
.reader-stage .stage-settings-panel {
  position: absolute;
  top: 44px;
  right: 10px;
  z-index: 5;
  min-width: 260px;
  max-width: min(340px, calc(100vw - 20px));
  max-height: calc(100% - 60px);
  display: flex;
  flex-direction: column;
  background: rgba(0, 0, 0, 0.72);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 12px;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
  pointer-events: auto;
  overflow: hidden;   /* round outer corners; body handles scroll */
}
.reader-stage .stage-settings-panel[hidden] { display: none; }
:fullscreen .stage-settings-panel,
:-webkit-full-screen .stage-settings-panel,
.reader-stage.is-fake-fullscreen .stage-settings-panel {
  top: calc(max(10px, env(safe-area-inset-top)) + 34px);
  right: max(10px, env(safe-area-inset-right));
}

.stage-settings-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 12px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.15);
  flex: 0 0 auto;
}
.stage-settings-title {
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.02em;
}
.stage-settings-close {
  background: transparent;
  color: #fff;
  border: none;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
  opacity: 0.8;
}
.stage-settings-close:hover { opacity: 1; }

.stage-settings-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 10px 12px 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-size: 13px;
}
.stage-settings-row {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #fff;
  cursor: pointer;
}
.stage-settings-row.stage-settings-slider,
.stage-settings-row.stage-settings-group {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  cursor: default;
}
.stage-settings-label {
  font-size: 12px;
  opacity: 0.85;
  display: flex;
  justify-content: space-between;
}
.stage-settings-value {
  opacity: 0.7;
  font-variant-numeric: tabular-nums;
}
.stage-settings-row input[type="range"] {
  width: 100%;
  accent-color: #4dd1ff;
}
.stage-settings-row input[type="checkbox"] {
  accent-color: #4dd1ff;
}
.stage-settings-row select {
  width: 100%;
  padding: 6px 8px;
  background: rgba(255, 255, 255, 0.08);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 6px;
  font-size: 13px;
}
.stage-settings-row select option {
  /* Native dropdown menu defaults to system colors; force readable text
     in case the browser keeps the dark background. */
  background: #1a1a1a;
  color: #fff;
}
.stage-settings-row .seg {
  display: inline-flex;
  gap: 0;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 8px;
  overflow: hidden;
}
.stage-settings-row .seg button {
  flex: 1 1 0;
  padding: 6px 10px;
  background: transparent;
  color: #fff;
  border: none;
  border-left: 1px solid rgba(255, 255, 255, 0.18);
  cursor: pointer;
  font-size: 12px;
}
.stage-settings-row .seg button:first-child { border-left: none; }
.stage-settings-row .seg button.is-active {
  background: rgba(77, 209, 255, 0.28);
  font-weight: 700;
}

/* ============================================================================
 * Skeleton placeholders -- shown while home/page data is loading.
 *
 * Each .skel-<name> matches the rough shape and size of the real content it
 * replaces, so the layout doesn't shift when the real DOM is swapped in. The
 * shimmer is a single keyframe applied via CSS variables on a linear-gradient
 * background-image, kept cheap so it doesn't cost mobile CPU.
 * ========================================================================= */
@keyframes pjsk-skel-shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position:  200% 0; }
}
.skel {
  position: relative;
  display: inline-block;
  border-radius: 12px;
  background: linear-gradient(90deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.10) 50%, rgba(0,0,0,0.05) 100%);
  background-size: 200% 100%;
  animation: pjsk-skel-shimmer 1.4s ease-in-out infinite;
  flex: 0 0 auto;
}
@media (prefers-reduced-motion: reduce) {
  .skel { animation: none; }
}

/* Featured-banner skeleton: full-width hero block matching the real banner. */
.skel-featured-banner {
  display: block;
  width: 100%;
  height: 220px;
  margin-bottom: 18px;
}
@media (max-width: 700px) {
  .skel-featured-banner { height: 160px; }
}

/* One song-strip card skeleton (the parent .song-strip already lays them out
 * horizontally; we just size the placeholder to match a real item). */
.skel-song-strip {
  width: 140px;
  height: 180px;
  margin-right: 10px;
}
@media (max-width: 700px) {
  .skel-song-strip { width: 110px; height: 150px; }
}

/* One cast-bubble skeleton (circle + name line). */
.skel-cast-strip {
  width: 72px;
  height: 96px;
  margin-right: 8px;
  border-radius: 12px;
}

/* When a skel sits inside the real container, ensure container layout still
 * works (.song-strip and .cast-strip are flex/grid -- the .skel honours it
 * because we kept display: inline-block + flex: 0 0 auto). */

/* ───────────────────────── Reader tap effect ────────────────────────────
 * Visual feedback for stage taps. JS spawns a <div.tap-fx> containing
 * 3 rings + ~12 triangle confetti pieces at the click point; CSS does
 * all the actual animation. See public/js/reader/tap-effect.js for the
 * spawn logic and rationale.
 *
 * Layout: .tap-fx is a 0×0 anchor pinned at the click point with
 * absolute positioning. All children (.tap-fx-ring, .tap-fx-tri) center
 * on it via translate(-50%, -50%) and animate as transforms from there.
 *
 * No JS animation loop — once .is-active is added, CSS transitions run
 * to completion and the JS timer removes the node ~1.1s later.
 *
 * Reduced motion: respects prefers-reduced-motion by skipping the
 * confetti and shrinking the ring pulse to a single small fade.
 */

.tap-fx {
  position: absolute;
  width: 0;
  height: 0;
  pointer-events: none;
  /* Pin the anchor exactly at the click point. JS sets left/top in px. */
  z-index: 30;
  /* Sit above the stage backgrounds but below the dialogue bubble.
     .dialogue-overlay sits at z-index ~50 in app.css. */

  /* 3D context for confetti tumble. Each .tap-fx-tri child rotates
     around X/Y/Z; without perspective those rotations collapse to flat
     2D shear. 600px feels right for a ~70px throw radius — enough
     foreshortening to read as paper-in-3D-space without making the
     pieces look like they're inside a fishbowl. */
  perspective: 600px;
  transform-style: preserve-3d;
}

/* App-wide tap-effect overlay. A single fixed-position layer pinned to the
   viewport so the tap effect can fire on ANY click in the app — not just
   inside the reader stage — without interfering with the click target.
   `pointer-events: none` makes it transparent to hit-testing; the very
   high z-index keeps the effect visible even above modals/drawers. JS
   spawns .tap-fx children using viewport-relative (clientX, clientY)
   coordinates, which line up directly with the layer's content box. */
.tap-fx-layer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 99999;
  overflow: hidden;
  /* Prevent any inherited filter/transform from creating a containing
     block that would shift our pinned children. */
  contain: layout style;
}

.tap-fx-ring {
  position: absolute;
  left: 0;
  top: 0;
  width: 18px;
  height: 18px;
  /* Center the ring on the anchor. */
  transform: translate(-50%, -50%) scale(0.2);
  border-radius: 50%;
  /* Stroke that matches the keyframe's start width. The teal hue is
     painted with a moderate alpha so the ring reads as a soft accent
     rather than a solid painted shape (user feedback: "more
     transparent"). No mix-blend-mode — blend modes desaturate edges and
     create a soft halo by design, which is the "blurry" look we want to
     avoid here. */
  border: 4px solid var(--tap-fx-ring-color, rgb(80, 200, 200));
  background: transparent;
  opacity: 0;
  /* Composite each ring on its own layer during the brief animation so
     the GPU doesn't repaint the stage on every frame. */
  will-change: transform, opacity;
}

/* Rings: a ripple that expands outward from the tap point. Each ring
   starts as a small dot and snaps out to a wider radius, fading as it
   travels. At any instant during the animation the stack reads as the
   reference screenshot — three concentric circles of different sizes,
   because each successive ring starts a beat later and so is "behind"
   the one before it.

   We use @keyframes so we can shape the opacity curve independently of
   the scale curve: ring is at full opacity through ~70% of the run
   (the readable "painted shape" beat from the reference image), then
   fades to 0 over the final 30%. The scale curve eases out fast in
   the first half and decelerates so the ring decelerates as it
   reaches its outer radius (matches the in-game ripple feel).

   Per-ring start-delay is tight (0 / 35 / 70 ms) — enough that the
   rings read as three distinct trailing ripples, not enough that they
   feel like three separate taps. Total run time per ring: 200 ms; the
   last ring finishes at 70 + 200 = 270 ms. Border-width animates from
   5px → 1px so the ring visibly thins at the edge instead of clipping
   off at full thickness.

   If you retune the animation duration / delay, also bump LIFETIME_MS
   in tap-effect.js (it must outlast the latest ring's animation end). */
@keyframes tap-fx-ring-ripple {
  /* Ring grows from a dot, holds peak opacity briefly, then fades out while
     still expanding — and the border thins from 4px to 1px so the ring
     visibly tapers as it reaches its outer radius (rather than popping off
     at full thickness).

     Tuning (vs. the earlier 200ms / 0.95 peak): rings are now lighter and
     quicker to match user feedback ("more transparent, smaller, faster").
     Peak alpha drops to 0.65 so the rings read as a soft accent rather
     than a painted shape; total run shortens to 140ms so the burst
     resolves before the user's next tap.

     Confetti is intentionally NOT retuned here — user feedback called it
     out as "fine". The hard-remove timer LIFETIME_MS in tap-effect.js is
     the confetti's wall clock (≥ 430ms), so shrinking rings cannot leak
     nodes. */
  0%   { opacity: 0;    border-width: 4px; transform: translate(-50%, -50%) scale(0.2); }
  18%  { opacity: 0.65; border-width: 2px; }
  60%  { opacity: 0.50; border-width: 1.5px; }
  100% { opacity: 0;    border-width: 1px; transform: translate(-50%, -50%) scale(var(--tap-fx-ring-scale, 2.2)); }
}

/* Per-ring final scales — shrunk from the earlier 1.7/2.6/3.6 stack so
   the burst fits visually inside the confetti spread (which itself
   tops out at distance ≤ 70px). Stagger tightened from 35/70 to
   20/40ms so the three rings still read as a rolling ripple while
   keeping the full sequence inside the 140ms keyframe budget. */
.tap-fx.is-active .tap-fx-ring--0 {
  --tap-fx-ring-scale: 1.3;
  animation: tap-fx-ring-ripple 140ms cubic-bezier(0.2, 0.85, 0.25, 1) 0ms forwards;
}
.tap-fx.is-active .tap-fx-ring--1 {
  --tap-fx-ring-scale: 2.0;
  animation: tap-fx-ring-ripple 140ms cubic-bezier(0.2, 0.85, 0.25, 1) 20ms forwards;
}
.tap-fx.is-active .tap-fx-ring--2 {
  --tap-fx-ring-scale: 2.7;
  animation: tap-fx-ring-ripple 140ms cubic-bezier(0.2, 0.85, 0.25, 1) 40ms forwards;
}

/* Triangle confetti. Drawn with the classic CSS border trick: a 0×0
   element with three transparent borders + one colored border becomes
   an upward-pointing triangle. The element is sized by --tap-fx-size
   (the triangle's bounding box edge length).

   Final position is encoded as translate(--tap-fx-dx, --tap-fx-dy) +
   rotation; CSS transitions from the origin (no transform) to that
   final transform when .is-active is added. */
.tap-fx-tri {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
  border-style: solid;
  /* Triangle dimensions picked INDEPENDENTLY by JS so each piece of
     paper has its own aspect ratio (tall-skinny, short-fat, near-
     isosceles). The border-triangle trick:
        bottom border  = triangle height  (= --tap-fx-h)
        left/right     = triangle width/2 (= --tap-fx-w / 2)
     Falls back to the legacy --tap-fx-size on either axis when an
     older spawnTapEffect happens to be on the page (defensive only).
   */
  border-width: 0
                calc(var(--tap-fx-w, var(--tap-fx-size, 12px)) / 2)
                var(--tap-fx-h, var(--tap-fx-size, 12px))
                calc(var(--tap-fx-w, var(--tap-fx-size, 12px)) / 2);
  border-color: transparent transparent var(--tap-fx-color, #fff) transparent;
  /* 3D plate behavior: keep our own 3D space so child transforms tumble
     instead of flattening, and draw the back face too — when a piece
     tumbles past 90° it would otherwise vanish (the border-triangle
     trick has no painted back side, but with backface-visibility:visible
     the browser mirrors the front face for us). */
  transform-style: preserve-3d;
  backface-visibility: visible;
  /* Origin: centered on the anchor, no drift yet. Per-piece initial
     orientation in 3D (--tap-fx-rx0, --tap-fx-ry0) so every triangle
     starts facing a different direction — even before the throw, the
     burst reads as a cloud of independent paper plates, not a flat
     stencil. */
  transform: translate(-50%, -50%)
             rotateX(var(--tap-fx-rx0, 0deg))
             rotateY(var(--tap-fx-ry0, 0deg));
  opacity: 1;
  will-change: transform, opacity;
  /* Soft glow matches the in-game paper-confetti material (`mat_common`).
     Tints the surrounding pixels with the confetti color so each piece
     reads as glowing paper, not a flat shape. */
  filter: drop-shadow(0 0 4px var(--tap-fx-color, #fff));
}

.tap-fx.is-active .tap-fx-tri {
  /* Per-piece duration (--tap-fx-dur, set on each .tap-fx-tri by JS) so
     pieces settle at different moments instead of all freezing at the
     same instant. Default kept at 390ms for back-compat with any
     consumer that doesn't set the variable. */
  transition: transform var(--tap-fx-dur, 390ms) cubic-bezier(0.18, 0.7, 0.36, 1) var(--tap-fx-delay, 0ms),
              opacity var(--tap-fx-dur, 390ms) ease-out var(--tap-fx-delay, 0ms);
  /* Final transform encodes the full throw: drift to (dx, dy), tumble
     around X and Y (so the piece flips like real paper falling), and
     Z spin. The translate(-50%, -50%) centering survives because we
     re-apply it here. Transform order matters: translate first so the
     piece lands in the right place, then rotations tumble it around
     its own center. */
  transform: translate(calc(-50% + var(--tap-fx-dx, 0px)),
                       calc(-50% + var(--tap-fx-dy, 0px)))
             rotateX(var(--tap-fx-rx, 0deg))
             rotateY(var(--tap-fx-ry, 0deg))
             rotateZ(var(--tap-fx-rot, 0deg));
  opacity: 0;
}

/* Reduced-motion: small fade, no flying confetti. Some users get
   migraines from a flurry of moving particles; this keeps the visual
   feedback while removing the motion. */
@media (prefers-reduced-motion: reduce) {
  .tap-fx-tri { display: none; }
  .tap-fx.is-active .tap-fx-ring--0,
  .tap-fx.is-active .tap-fx-ring--1,
  .tap-fx.is-active .tap-fx-ring--2 {
    transition: opacity 200ms ease-out;
    transform: translate(-50%, -50%) scale(2);
    opacity: 0;
  }
  /* Swipe-trail glow dots: skip the radial expansion + glow on
     reduced-motion, just hide them. The companion confetti bursts
     are already hidden by the .tap-fx-tri rule above. */
  .swipe-trail-dot { display: none; }
}

/* Swipe-trail glow dot. One spawned at each "anchor" along a finger
   drag (see public/js/reader/swipe-trail.js). The CSS animation does
   all the work — JS only spawns + hard-removes after the matching
   TRAIL_FADE_MS (720ms) wall-clock.

   Why a radial-gradient background instead of box-shadow or a single
   solid + filter: drop-shadow? A radial gradient renders entirely on
   the GPU compositor when paired with `will-change`, no per-frame
   paint. Single-element fade + bloom in one shot, very cheap. */
.swipe-trail-dot {
  position: absolute;
  left: 0;
  top: 0;
  width: 22px;
  height: 22px;
  margin-left: -11px;   /* center on the (left, top) anchor */
  margin-top: -11px;
  pointer-events: none;
  border-radius: 50%;
  /* Teal-into-transparent matches the ring tint so the trail reads
     as the same visual language as the tap burst. */
  background: radial-gradient(circle at 50% 50%,
              rgba(140, 230, 230, 0.85) 0%,
              rgba(140, 230, 230, 0.55) 35%,
              rgba(140, 230, 230, 0)    72%);
  filter: blur(0.5px);
  opacity: 0;
  will-change: transform, opacity;
  /* 720 ms matches swipe-trail.js TRAIL_FADE_MS. The keyframe spikes
     opacity at 8% so the dot pops into view (mirroring a tap-fx
     ring's initial flash), then fades out while growing slightly
     so the trail looks like glowing breath rather than a static
     bead. forwards keeps the end state (opacity: 0) until the JS
     timeout hard-removes the node. */
  animation: swipe-trail-dot-fade 720ms ease-out forwards;
}

@keyframes swipe-trail-dot-fade {
  0%   { opacity: 0;    transform: scale(0.6); }
  8%   { opacity: 0.95; transform: scale(1.0); }
  60%  { opacity: 0.40; transform: scale(1.3); }
  100% { opacity: 0;    transform: scale(1.6); }
}

/* ============================================================================
   GLOBAL SEARCH — sidebar input + dedicated #/search page
   ----------------------------------------------------------------------------
   The input lives inside #nav (right under .brand). The results page renders
   inside #view like any other route, so the input persists across navigation.
   ============================================================================ */

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0);
  white-space: nowrap; border: 0;
}

.topbar-search {
  position: relative;
  margin: 0 0 16px;
}
.topbar-search input {
  width: 100%;
  padding: 10px 12px 10px 34px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  outline: none;
  transition: border-color .12s ease, background .12s ease, box-shadow .12s ease;
}
.topbar-search input::placeholder { color: var(--muted); }
.topbar-search input:focus {
  background: var(--surface);
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(255, 95, 177, 0.18);
}
.topbar-search-icon {
  position: absolute;
  left: 12px; top: 50%; transform: translateY(-50%);
  color: var(--muted);
  font-size: 16px;
  pointer-events: none;
}
/* Hide the native clear button in WebKit so the icon doesn't fight it. */
.topbar-search input::-webkit-search-cancel-button { -webkit-appearance: none; }

/* ---------- search results page ---------- */

.search-empty {
  max-width: 720px;
}
.search-examples {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin-top: 18px;
}
.search-example-chip {
  display: inline-flex; align-items: center;
  padding: 8px 14px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  font-weight: 700;
  font-size: 14px;
  transition: background .12s ease, border-color .12s ease, color .12s ease, transform .12s ease;
}
.search-example-chip:hover {
  background: var(--surface-2);
  border-color: var(--accent);
  color: var(--ink);
  transform: translateY(-1px);
}

.search-status {
  color: var(--ink-soft);
  font-size: 14px;
  margin: 4px 0 14px;
}
.search-status strong { color: var(--ink); }
.search-status a { color: var(--accent); font-weight: 700; }
.search-status .search-meta-ms,
.search-status .search-meta-cap {
  color: var(--muted);
  font-size: 12px;
  margin-left: 4px;
}
.search-status-error { color: var(--accent); }

.search-results {
  display: flex; flex-direction: column;
  gap: 10px;
}
.search-result-card {
  display: block;
  padding: 14px 16px;
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease;
  text-decoration: none;
  color: var(--ink);
}
/* When a result has a per-kind icon (event logo / card thumb / chibi), the
   card becomes a horizontal flex with the icon on the left and the existing
   meta+row stack on the right. Unit/special results render without an icon
   and keep the original block layout. */
.search-result-card.has-icon {
  display: flex;
  align-items: center;
  gap: 12px;
}
.search-result-icon {
  flex-shrink: 0;
  width: 56px;
  height: 56px;
  /* Slight bg so the chibi (transparent webp) sits on something on light
     and dark themes alike. Same surface-2 var that other thumbnails use. */
  background: var(--surface-2, rgba(0, 0, 0, 0.04));
  border-radius: 8px;
  overflow: hidden;
  /* The icon wrapper holds either a single <img> (single-character / event
     logo / card thumb) or a 2x2 grid of chibi sprites for multi-character
     area conversations. */
}
.search-result-icon > img {
  width: 100%;
  height: 100%;
  /* Event logos are wide rectangles; chibi/card thumbs are squarer. The
     fixed square box + object-fit:contain handles both gracefully. */
  object-fit: contain;
  display: block;
}
/* Multi-character actionset hits: pack up to 4 chibis into a 2x2 grid.
   1 char  -> single <img> (handled above)
   2 chars -> top row only (1fr 1fr / 1fr), images fill their half
   3 chars -> three cells of a 2x2 (TL, TR, BL); last cell stays empty
   4 chars -> all four corners filled */
.search-result-icon-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 1px;
  /* No padding — the cells are already small (28px nominal). The bg shows
     through the 1px gap to visually separate adjacent chibis. */
}
.search-result-icon-grid.n-2 {
  grid-template-rows: 1fr;
}
.search-result-icon-grid > img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  min-width: 0;
  min-height: 0;
}
.search-result-card.has-icon .search-result-main {
  flex: 1;
  min-width: 0; /* allow crumb ellipsis inside the flex item */
}
.search-result-card:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
  border-color: var(--line-strong);
}
.search-result-meta {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 8px;
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 6px;
}
.search-result-crumb {
  font-weight: 700;
  color: var(--ink-soft);
  text-transform: none;
  letter-spacing: 0;
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-result-line {
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}
.search-result-row {
  display: flex;
  gap: 12px;
  align-items: baseline;
  line-height: 1.5;
}
.search-result-speaker {
  flex-shrink: 0;
  font-weight: 700;
  color: var(--accent);
  font-size: 14px;
  min-width: 80px;
  max-width: 140px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-no-speaker {
  color: var(--muted);
  font-weight: 500;
  font-style: italic;
}
.search-result-body {
  color: var(--ink);
  font-size: 14px;
  flex: 1;
  word-break: break-word;
}
.search-result-card mark {
  background: rgba(255, 209, 102, 0.5); /* --accent-4 with alpha */
  color: var(--ink);
  padding: 0 2px;
  border-radius: 3px;
  font-weight: 700;
}

/* Unit color stripe — uses the existing --u-* vars to tint the left edge of
   unit-story results. Event results have no unit so they fall through to
   the default no-stripe look. */
.search-result-card.u-light_sound    { border-left: 3px solid var(--u-light_sound); }
.search-result-card.u-idol           { border-left: 3px solid var(--u-idol); }
.search-result-card.u-street         { border-left: 3px solid var(--u-street); }
.search-result-card.u-theme_park     { border-left: 3px solid var(--u-theme_park); }
.search-result-card.u-school_refusal { border-left: 3px solid var(--u-school_refusal); }
.search-result-card.u-piapro         { border-left: 3px solid var(--u-piapro); }

/* Skeleton row matching final result-card dimensions. */
.skel-search-result {
  height: 78px;
  border-radius: var(--radius);
}

/* On narrow viewports (<= 700px) the speaker no longer needs a fixed column
   so the body line gets to use the full row. */
@media (max-width: 700px) {
  .search-result-row { flex-direction: column; gap: 4px; }
  .search-result-speaker { min-width: 0; max-width: none; }
}

/* ============================================================
   C6.4 — Advanced search panel + kind-filter chips
   ============================================================ */

.search-empty-hint {
  margin-top: 22px;
  color: var(--muted);
  font-size: 13px;
}
.search-empty-hint code {
  background: var(--surface-2);
  padding: 1px 6px;
  border-radius: 4px;
  font-size: 12px;
  color: var(--ink-soft);
}
.search-empty-hint a { color: var(--accent); font-weight: 700; }

/* Collapsible advanced panel — sits between the status row and the kind
   chips. Uses native <details> so keyboard + a11y come for free. */
.search-advanced {
  margin: 4px 0 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.search-advanced-summary {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 12px;
  cursor: pointer;
  list-style: none;
  font-size: 13px;
  color: var(--ink-soft);
  user-select: none;
}
.search-advanced-summary::-webkit-details-marker { display: none; }
.search-advanced-summary::before {
  content: "▸";
  display: inline-block;
  width: 12px;
  color: var(--muted);
  transition: transform .12s ease;
}
.search-advanced[open] > .search-advanced-summary::before {
  transform: rotate(90deg);
}
.search-advanced-label { font-weight: 700; color: var(--ink); }
.search-advanced-active {
  font-size: 11px;
  font-weight: 700;
  padding: 1px 8px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: #fff;
  text-transform: uppercase;
  letter-spacing: .05em;
}
.search-advanced-body {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  padding: 4px 12px 14px;
}
.search-advanced-field {
  display: flex; flex-direction: column; gap: 4px;
  font-size: 12px;
  color: var(--muted);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
}
.search-advanced-field input {
  font: inherit;
  font-size: 14px;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  color: var(--ink);
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--surface-2);
  border: 1px solid var(--line);
}
.search-advanced-field input:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--bg);
}
.search-advanced-field input::placeholder { color: var(--muted); }
@media (max-width: 700px) {
  .search-advanced-body { grid-template-columns: 1fr; }
}

/* Kind-filter chips. Each is a toggle button with a count badge. The 'All'
   pill clears the kinds filter. Empty (zero-count) chips are dimmed so the
   active corpora are visually obvious. */
.search-kind-chips {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin: 0 0 14px;
}
.search-kind-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 13px;
  font-weight: 700;
  font-family: inherit;
  cursor: pointer;
  transition:
    background .12s ease,
    border-color .12s ease,
    color .12s ease,
    transform .12s ease;
}
.search-kind-chip:hover {
  background: var(--surface-2);
  border-color: var(--accent);
  color: var(--ink);
  transform: translateY(-1px);
}
.search-kind-chip.is-active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.search-kind-chip.is-active:hover { color: #fff; }
.search-kind-chip.is-empty {
  opacity: .45;
  cursor: default;
}
.search-kind-chip.is-empty:hover {
  background: var(--surface);
  border-color: var(--line);
  color: var(--ink-soft);
  transform: none;
}
.search-kind-chip-count {
  font-size: 11px;
  font-weight: 700;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  color: var(--muted);
  min-width: 24px;
  text-align: center;
}
.search-kind-chip.is-active .search-kind-chip-count {
  background: rgba(255,255,255,.18);
  color: #fff;
}
.search-kind-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ============================================================
   C6.5 — Infinite-scroll sentinel
   ============================================================ */
.search-infinite-sentinel {
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
}
.search-infinite-sentinel::before {
  content: "";
  width: 18px; height: 18px;
  border-radius: 50%;
  border: 2px solid var(--surface-2);
  border-top-color: var(--accent);
  animation: pjsk-spin 0.8s linear infinite;
  opacity: 0;
  transition: opacity .12s ease;
}
.search-infinite-sentinel.is-loading::before { opacity: 1; }
@keyframes pjsk-spin {
  to { transform: rotate(360deg); }
}
