* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
html { scroll-behavior: smooth; }
/* Time-of-day palette. There is only ONE theme now — a soft slate blue that
   breathes with the UK day: script.js rewrites --bg-h/--bg-s/--bg-l/--bg-glow
   from KazuLib.skyTint every 10 seconds (lighter towards midday, a faint
   orange horizon glow around sunrise/sunset, true AMOLED black in deep
   night). They are registered <number> properties so they carry typed
   initial values, but they are deliberately NEVER transitioned: each step is
   under a fifth of one 8-bit colour level, so it already reads as a glide,
   whereas a CSS transition on an inherited custom property restyles the whole
   document every frame (it used to cost a full CPU core at idle; see
   applySkyTint in script.js). --bg-glow (the larger of the daylight and dusk
   factors) scales the decorative gradient layers, so the night sky can reach
   #000 while day and golden hour keep their depth. The fallbacks in each
   var() are the pre-registration night values; the critical inline first
   paint in index.html matches the deep-night result. */
@property --bg-h { syntax: '<number>'; inherits: true; initial-value: 215; }
@property --bg-s { syntax: '<number>'; inherits: true; initial-value: 33; }
@property --bg-l { syntax: '<number>'; inherits: true; initial-value: 0; }
@property --bg-glow { syntax: '<number>'; inherits: true; initial-value: 0; }
/* html owns the canvas colour (and the overscroll fringe); body carries the
   tinted gradient on top. */
html { background: hsl(var(--bg-h, 215) calc(var(--bg-s, 33) * 1%) calc(var(--bg-l, 9) * 1%)); }
body { background: hsl(var(--bg-h, 215) calc(var(--bg-s, 33) * 1%) calc(var(--bg-l, 9) * 1%)); }

body {
  position: relative;
  min-height: 100vh;
  width: 100%;
  overflow-x: hidden;
  font-family: 'Nunito', system-ui, sans-serif;
  color: var(--ink);
  background: var(--page-bg);
}
/* ---- Theme variables ---- */
body {
  color-scheme: dark;
  /* One soft slate-blue palette, easy on the eyes: moderate saturation,
     lighter towards midday, true AMOLED black in deep night. Every blue
     below is built from the live --bg-* tint, and every decorative layer is
     scaled by --bg-glow (the larger of the daylight/dusk factors), so the
     sky reaches #000 only when there is genuinely no light in it. The pink
     bloom low-left ties the sky to the blossom branches whenever there IS
     light. Around the horizon crossings the hue warms towards indigo and
     the .sky-glow element adds the faint orange sunrise/sunset wash. */
  --page-bg: radial-gradient(720px 520px at 72% 8%, hsl(var(--bg-h, 215) calc(var(--bg-s, 33) * 1%) calc((var(--bg-l, 9) + 16 * var(--bg-glow, 1)) * 1%) / .5) 0%, hsl(var(--bg-h, 215) calc(var(--bg-s, 33) * 1%) calc((var(--bg-l, 9) + 16 * var(--bg-glow, 1)) * 1%) / 0) 60%), radial-gradient(900px 620px at 6% 88%, rgba(244,154,193,calc(.07 * var(--bg-glow, 1))) 0%, rgba(244,154,193,0) 60%), radial-gradient(1200px 700px at 80% -10%, hsl(var(--bg-h, 215) calc(var(--bg-s, 33) * 1%) calc((var(--bg-l, 9) + 9 * var(--bg-glow, 1)) * 1%)) 0%, hsl(var(--bg-h, 215) calc(var(--bg-s, 33) * 1%) calc((var(--bg-l, 9) + 9 * var(--bg-glow, 1)) * 1%) / 0) 55%), radial-gradient(1000px 600px at 0% 5%, hsl(calc(var(--bg-h, 215) + 12) calc(var(--bg-s, 33) * 1%) calc((var(--bg-l, 9) + 7 * var(--bg-glow, 1)) * 1%)) 0%, hsl(calc(var(--bg-h, 215) + 12) calc(var(--bg-s, 33) * 1%) calc((var(--bg-l, 9) + 7 * var(--bg-glow, 1)) * 1%) / 0) 50%), linear-gradient(170deg, hsl(var(--bg-h, 215) calc(var(--bg-s, 33) * 1%) calc((var(--bg-l, 9) + 5 * var(--bg-glow, 1)) * 1%)) 0%, hsl(var(--bg-h, 215) calc(var(--bg-s, 33) * 1%) calc(var(--bg-l, 9) * 1%)) 45%, hsl(calc(var(--bg-h, 215) + 8) calc((var(--bg-s, 33) + 4) * 1%) calc((var(--bg-l, 9) + 2 * var(--bg-glow, 1)) * 1%)) 100%);
  --ink: #eaf4ff;
  --ink-soft: #9db8d4;
  --ink-faint: #6f8aac;
  --accent: #5cc6ff;
  --card-bg: rgba(46,70,108,.74);
  --card-blur: blur(10px);
  --card-bd: 1px solid rgba(92,150,220,.55);
  --card-shadow: 0 14px 34px rgba(3,10,24,.16);
}

.hidden { display: none !important; }

/* ---- Atmosphere: cherry-blossom petals ---- */
/* Anchored to the page (absolute, not fixed). Rain and aurora keep their
   original one-viewport stage; blossom modes add .atmosphere--page and script
   supplies the live document height so petals can cross the footer before
   the clipped overflow removes them. */
.atmosphere { position: absolute; top: 0; left: 0; right: 0; height: 100vh; overflow: hidden; pointer-events: none; z-index: 0; }
.atmosphere.atmosphere--page { height: var(--atmosphere-height, 100vh); }
/* Petal physics: three animations compose the drift — fallY on `transform`
   (linear = terminal velocity), petalSway on the `translate` property and
   petalRock on `rotate` (both ease-in-out alternate = smooth harmonic
   motion, never a zigzag). The individual transform properties compose WITH
   `transform`, which is what lets the three layers stack. Opacity comes
   from --po (inline per petal) so fallY can fade petals in as they detach
   from the branches. --spawn-y lets the transform end at the document's
   bottom regardless of its starting branch; --exit-pad carries the full
   glyph beyond the footer before overflow clips it. */
.petal::before { content: ''; display: block; width: .65em; height: .9em; background: var(--sakura-petal, #f5b9d2); border-radius: 70% 20% 65% 30%; }
.petal {
  position: absolute; filter: blur(2px);
  animation-name: fallY, petalSway, petalRock;
  animation-timing-function: linear, ease-in-out, ease-in-out;
  animation-iteration-count: infinite, infinite, infinite;
  animation-direction: normal, alternate, alternate;
}
/* Rain streaks: injected by script.js when live weather says it's raining.
   Same pattern as .petal: timing in the class, duration/delay inline per drop. */
.drop { position: absolute; width: 1.5px; height: clamp(60px, 12vh, 110px); border-radius: 1px; background: linear-gradient(to bottom, rgba(170,210,255,0), rgba(170,210,255,.6)); animation-name: rainfall; animation-timing-function: linear; animation-iteration-count: infinite; }
@media (prefers-reduced-motion: reduce) { .petal, .drop { animation: none; display: none; } }
@keyframes fallY {
  /* --y0 lets branch-spawned petals start exactly on their blossom (0vh);
     top-spawned ones keep the default above-screen lead-in. --dx is the
     total sideways wind drift across the whole fall (away from the branch),
     so a petal visibly TRAVELS down and away instead of hovering in place.
     --fall-y is the runway length frozen onto each petal at spawn (script.js):
     reading the shared --atmosphere-height here instead made every falling
     petal teleport whenever a live card grew the page. The calc() default
     keeps the no-JS fallback petals working. Petals dim out over the last
     stretch, so a cohort sized for a shorter page never pops at its loop
     point before the gentle re-seed lands. */
  0%   { transform: translate(0, var(--y0, -12vh)); opacity: 0; }
  7%   { opacity: var(--po, .7); }
  88%  { opacity: var(--po, .7); }
  100% { transform: translate(var(--dx, 0vw), var(--fall-y, calc(var(--atmosphere-height, 100vh) - var(--spawn-y, 0px) + var(--exit-pad, 48px)))); opacity: 0; }
}
/* Flutter: the only oscillation — kept tight so it reads as a wobble on top
   of the wind drift, never a shuttle back and forth. */
@keyframes petalSway {
  from { translate: -0.9vw 0; }
  to   { translate: 0.9vw 0; }
}
@keyframes petalRock {
  from { rotate: -14deg; }
  to   { rotate: 16deg; }
}
@keyframes rainfall { 0% { transform: translateY(-15vh) rotate(9deg); } 100% { transform: translateY(115vh) rotate(9deg); } }
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .55; transform: scale(.82); } }

/* ---- Sakura scenery: moon + overhanging branches ---- */
/* Sunset sky. An absolute, viewport-tall layer at the top of the page (it
   scrolls away with the hero, like the scenery) painted first, so the sun,
   moon and branches sit in front of it. Two stacked palettes crossfade on
   --sunset-late: ::before is golden-hour orange (the sun still up), ::after
   the rose/violet afterglow once it has gone. The whole layer's strength is
   --sunset (0 all day and all night, easing in over the ~110 minutes before
   sunset and out over the ~85 after; KazuLib.sunsetGlow, written by
   script.js each minute and by the inline boot script for the first paint).
   Each palette is a vertical sky (indigo up high, magenta, orange, gold at the
   horizon) plus a wide glow anchored where the sun leaves the screen (right
   edge, ~70% down: the end of the sky arc). Static gradients with an opacity
   that steps ~1% a minute: no animation, so it costs nothing and shows on
   every device, phones and low-power mode included. Christmas has its own
   pine palette and skips it. */
.sunset-sky {
  position: absolute; top: 0; left: 0; right: 0; height: 100vh;
  z-index: 0; pointer-events: none; overflow: hidden;
  opacity: var(--sunset, 0);
}
.sunset-sky::before, .sunset-sky::after { content: ''; position: absolute; inset: 0; }
.sunset-sky::before {
  opacity: calc(1 - var(--sunset-late, 0));
  background:
    radial-gradient(150% 76% at 100% 60%, rgba(255,150,45,.96) 0%, rgba(255,120,50,.7) 22%, rgba(255,92,72,.4) 46%, rgba(255,92,72,.14) 66%, rgba(255,92,72,0) 84%),
    linear-gradient(to bottom, rgba(30,28,88,0) 0%, rgba(84,58,128,.3) 20%, rgba(214,96,104,.42) 40%, rgba(255,152,60,.6) 57%, rgba(255,196,96,.4) 69%, rgba(255,196,96,.1) 80%, rgba(255,196,96,0) 92%);
}
.sunset-sky::after {
  opacity: var(--sunset-late, 0);
  background:
    radial-gradient(150% 76% at 100% 62%, rgba(238,92,124,.74) 0%, rgba(196,72,144,.5) 26%, rgba(112,62,152,.3) 52%, rgba(84,62,152,.1) 70%, rgba(84,62,152,0) 86%),
    linear-gradient(to bottom, rgba(24,22,84,0) 0%, rgba(58,44,132,.38) 20%, rgba(150,70,152,.42) 42%, rgba(240,112,112,.4) 58%, rgba(255,142,92,.26) 70%, rgba(255,142,92,.08) 80%, rgba(255,142,92,0) 92%);
}
body.season-christmas .sunset-sky { display: none; }
/* Anchored to the top of the page (absolute, not fixed): it scrolls away
   with the hero and comes back when you scroll back up. z-index 0, same as
   .atmosphere; it sits earlier in the DOM so petals fall in front of the
   branches. All motion is transform-only; the moon is a static gradient so
   it never repaints under the glass cards' backdrop-filter. Colours come
   from the --sakura-* custom properties so seasons can restyle the SVGs. */
.sakura-scene {
  position: absolute; top: 0; left: 0; right: 0; height: 100vh; overflow: hidden;
  z-index: 0; pointer-events: none;
  --sakura-branch: #3e2a3c;
  --sakura-branch-rim: rgba(244,180,205,.22);
  --sakura-petal: #f4a8c4;
  --sakura-petal-deep: #e2709c;
  --sakura-bud: #d95f8f;
  --sakura-stamen: #ffe9f2;
}
/* Dev-only trajectory overlay. The SVG uses the same percentage frame as
   skyArcPoint(), so it scales with the scenery without JS resize work. It is
   deliberately hidden by default; kazudev's Sky curve guide turns it on. */
.sky-curve-guide {
  position: absolute; inset: 0; width: 100%; height: 100%; display: none;
  overflow: visible; pointer-events: none;
}
.sky-curve-guide__glow {
  fill: none; stroke: rgba(255,214,118,.20); stroke-width: 12px;
  stroke-linecap: round;
}
.sky-curve-guide__line {
  fill: none; stroke: rgba(255,239,181,.94); stroke-width: 2px;
  stroke-linecap: round; stroke-dasharray: 7px 10px;
  filter: drop-shadow(0 0 4px rgba(255,211,96,.9));
}
/* Sky body: the sun by day, the moon by night, riding an arc across the
   scenery layer (script.js positions it from the UK clock once a minute;
   the transition glides each step). The no-JS pose is the moon upper-right. */
.sky-body {
  position: absolute; left: 78%; top: 16%; transform: translate(-50%, -50%);
  width: clamp(92px, 10vw, 148px); aspect-ratio: 1; border-radius: 50%;
  transition: left 1.8s ease, top 1.8s ease, opacity 1.2s ease;
}
/* Raised by the inline #boot-tint script in <head>, lifted by script.js once
   updateSkyBody has picked sun/moon and placed the body. Until then the
   markup's no-JS pose (a moon, upper right) would show at any time of day and
   then hop; instead the body fades in, already in position. Without JS the
   class never appears, so the no-JS moon still renders. */
html.sky-pending .sky-body { opacity: 0; }
.sky-body--moon {
  background: radial-gradient(circle at 38% 34%, #fffdf6 0%, #f8eed6 46%, #eedcae 74%, rgba(238,220,174,0) 78%);
  box-shadow: 0 0 46px 22px rgba(255,240,205,.12), 0 0 130px 64px rgba(186,212,255,.07);
  opacity: .92;
}
.sky-body--sun {
  background: radial-gradient(circle at 38% 34%, #fffbe8 0%, #ffeda8 42%, #ffcf52 74%, rgba(255,207,82,0) 80%);
  box-shadow: 0 0 60px 30px rgba(255,214,90,.32), 0 0 170px 84px rgba(255,190,60,.15);
  opacity: .95;
}
/* Near either horizon (rising/setting): golden-hour sun, dimmer amber moon. */
.sky-body--sun.sky-body--low {
  background: radial-gradient(circle at 38% 34%, #fff3d0 0%, #ffcf7a 45%, #ff9a3d 76%, rgba(255,154,61,0) 82%);
  box-shadow: 0 0 70px 34px rgba(255,160,60,.38), 0 0 180px 90px rgba(255,120,40,.16);
}
.sky-body--moon.sky-body--low { opacity: .7; }
/* Horizon glow: a broad, faint orange wash parked where the sun crosses the
   horizon. script.js slides its x along the horizon once a minute and fades
   it on the dusk bell (inline opacity), so it only ever shows around sunrise
   and sunset. Rendered before the sun/moon + branches, so it sits behind
   them. */
.sky-glow {
  position: absolute; left: 78%; top: 50%; transform: translate(-50%, -50%);
  width: clamp(340px, 52vw, 780px); aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,148,54,.30) 0%, rgba(255,122,44,.13) 45%, rgba(255,110,40,0) 70%);
  opacity: 0;
  transition: left 1.8s ease, top 1.8s ease, opacity 2.4s ease;
}
.sakura-branch { position: absolute; display: block; height: auto; }
.sakura-branch--a {
  top: 0; right: 0; width: clamp(270px, 30vw, 440px);
  transform-origin: 100% 0;
  /* No idle sway: the branches sit still so foreground animations never appear to move the background. */
}
.sakura-branch--b {
  left: 0; top: 46%; width: clamp(190px, 21vw, 300px);
  transform-origin: 0 80%;
  /* No idle sway: sits still alongside the cards. */
}
/* Live weather layers (rain / snow / wind / clouds), drawn by weather-fx.js on
   one fixed canvas behind the content. script.js only creates it for desktop
   visitors, and only loads that file for them; this block is the second,
   independent guard: on phones and tablets (coarse pointer, no hover, mobile
   width), with reduced motion, or in low-power mode it is never displayed
   even if something creates it. It sits after .sakura-scene in the DOM (so
   clouds drift in front of the sun/moon and rain falls in front of the
   branches) and before .atmosphere (petals stay on top). */
.weather-fx {
  position: fixed; inset: 0; width: 100vw; height: 100vh;
  z-index: 0; pointer-events: none;
}
/* Clouds live on their own canvas that scrolls WITH the page (like the sky
   scenery, viewport-tall at the top), so they stay put as you scroll instead
   of following you; rain, snow and wind stay on the fixed canvas. */
.weather-fx--clouds { position: absolute; inset: auto; top: 0; left: 0; right: 0; height: 100vh; }
@media (pointer: coarse), (hover: none), (max-width: 768px), (prefers-reduced-motion: reduce) {
  .weather-fx { display: none !important; }
}
body.low-power .weather-fx { display: none !important; }
/* Cherry-blossom season only (20 Mar - 10 May, UK date; lib.js sakuraInBloom):
   outside it the branches are not drawn. The sun/moon share this layer, so
   only the branches go. html.no-sakura is set by the inline #boot-tint script
   before first paint and kept in sync by script.js; the static fallback
   petals in index.html stay hidden out of season until script.js has built
   its own (data-built), which is also what lets an explicit ?atmosphere=
   preview show petals out of season. */
html.no-sakura .sakura-branch { display: none; }
html.no-sakura .atmosphere:not([data-built]) .petal { display: none; }
/* Christmas owns a pine/cranberry/gold palette + its own tree scene; the
   pink branches would clash, so they sit the season out. The scene itself stays:
   the sun and moon live in it and must still cross the Christmas sky. */
body.season-christmas .sakura-branch { display: none; }
/* Phones: no left branch (no horizontal room), smaller corner branch, and no
   sky body — it just reads as a smudge behind the hero avatar. Custom scrollbar
   is hidden on touch / compact screens so native inertial scrolling prevails. */
@media (max-width: 768px) {
  .sakura-branch--b { display: none; }
  .sakura-branch--a { width: clamp(180px, 50vw, 260px); }
  .sky-body { display: none; }
  .cscroll { display: none !important; }
}
@media (pointer: coarse) {
  .cscroll { display: none !important; }
}
@media (prefers-reduced-motion: reduce) {
  .sakura-scene, .sakura-branch--a, .sakura-branch--b { animation: none; }
}

.container {
  position: relative; z-index: 1; max-width: 1120px; margin: 0 auto;
  padding: clamp(28px,5vw,64px) clamp(18px,4vw,40px) 64px;
  padding-left: max(clamp(18px,4vw,40px), env(safe-area-inset-left));
  padding-right: max(clamp(18px,4vw,40px), env(safe-area-inset-right));
}

/* ---- Hero ---- */
.hero { position: relative; text-align: center; padding: 8px 0; display: flex; flex-direction: column; align-items: center; }
.pfp-ring {
  position: relative;            /* anchors .party-hat and the pride ::before halo */
  margin-bottom: 8px; padding: 5px; border-radius: 50%;
  /* No idle bob: the profile picture sits still (vetoed by the owner). */
}
.pfp {
  display: block; width: 181px; height: 181px; border-radius: 50%;
  background: var(--card-bg); object-fit: cover;
}
/* Editorial serif wordmark instead of the old rounded-sans + rainbow-shimmer
   combo (the saturated multi-stop gradient sliding forever was the thing that
   read "generated"). Fraunces at optical size with a quiet vertical ink
   sheen — from full ink into a whisper of the theme accent — keeps the colour
   tied to the palette without shouting. Seasonal themes still repaint it
   below. Size pulled down from billboard to masthead scale. The padding
   gives the italic cap-height ascenders and the final letter's exit stroke
   room: with background-clip:text any glyph pixel outside the element's box
   loses its gradient and turns invisible, which used to clip the dot and
   tail of the final "i". */
.title {
  position: relative; margin: 20px 0 6px; padding: .06em .14em;
  font-family: 'Fraunces', 'Fredoka', serif; font-weight: 560; font-style: italic;
  font-optical-sizing: auto;
  font-size: clamp(42px,7.4vw,80px); line-height: 1.04; letter-spacing: -.015em;
  color: var(--ink); /* fallback for browsers without color-mix/clip */
  background: linear-gradient(178deg, var(--ink) 55%, color-mix(in srgb, var(--ink) 62%, var(--accent)) 125%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: titleReveal 1s cubic-bezier(.2,.6,.35,1) .26s both;
}

/* ---- Quote box ---- */
.quote-box {
  display: block; width: 100%; max-width: 560px; margin: 0 auto;
  padding: 0 26px; font-family: inherit;
  border: none; background: none; cursor: pointer;
  max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height .5s ease, opacity .45s ease, padding .5s ease, margin .5s ease;
}
.quote-box.visible {
  max-height: 200px; opacity: 1; padding: 18px 26px; margin: 4px auto 0;
}
@media (prefers-reduced-motion: reduce) { .quote-box { transition: none; } }
.quote-text {
  font-style: italic; font-weight: 600; font-size: 16px; line-height: 1.45; color: var(--ink);
}
.quote-text::before { content: '”'; } .quote-text::after { content: '”'; }
.quote-author {
  margin-top: 8px; font-size: 12px; font-weight: 800; letter-spacing: .06em;
  text-transform: uppercase; color: var(--accent);
}

/* ---- Cards ---- */
.card {
  position: relative; background: var(--card-bg); backdrop-filter: var(--card-blur);
  border: var(--card-bd); border-radius: 22px; box-shadow: var(--card-shadow); overflow: hidden;
}

/* ---- Stat strip ---- */
/* Align text stacks at the top so the extra weather row cannot shift its
   label and value above the other cards. Values fill the narrowest tile,
   with tabular-nums keeping the clock steady. */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit,minmax(210px,1fr)); gap: 16px; margin-top: 34px; }
.stat-card { border-radius: 22px; padding: 18px 14px; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; }
.stat-label { font-size: 12px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--accent); }
.stat-value { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: clamp(30px, 8vw, 42px); line-height: 1.05; margin-top: 6px; color: var(--ink); font-variant-numeric: tabular-nums; white-space: nowrap; }
.stat-sub { font-size: 14px; font-weight: 700; color: var(--ink-soft); margin-top: 4px; }
.stat-faint { font-size: 12.5px; font-weight: 700; color: var(--ink-faint); margin-top: 2px; }
.stat-loading-text { font-size: 15px; font-weight: 700; color: var(--ink-faint); }
.stat-card--bday {
  background: linear-gradient(150deg,#0ea5e9,#7c5cff); border-radius: 22px; padding: 18px 14px;
  box-shadow: 0 16px 34px rgba(80,110,230,.3); color: #fff; backdrop-filter: none; border: 1px solid transparent;
}
.stat-label--onbrand { color: rgba(255,255,255,.85); }
.stat-sub--onbrand { color: rgba(255,255,255,.92); }
.stat-card--bday .stat-value { color: #fff; }

.section-title {
  font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 26px; margin: 46px 0 16px;
  color: var(--ink); display: flex; align-items: center; gap: 10px;
}

/* ---- Right now ---- */
.right-now-grid { display: grid; grid-template-columns: repeat(auto-fit,minmax(300px,1fr)); gap: 18px; }
.discord-card, .steam-card { border-radius: 24px; padding: 22px; position: relative; overflow: hidden; }
.discord-card { background: linear-gradient(150deg,#1e2147 0%,#404ee0 60%,#1e2147 100%); box-shadow: 0 16px 38px rgba(40,40,110,.32); backdrop-filter: none; border: none; }
.steam-card { background: linear-gradient(150deg,#1b2838 0%,#2a475e 60%,#1b2838 100%); box-shadow: 0 16px 38px rgba(20,40,70,.32); backdrop-filter: none; border: none; }
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; position: relative; }
.card-head-title { display: flex; align-items: center; gap: 9px; font-weight: 800; font-size: 14px; letter-spacing: .04em; text-transform: uppercase; }
.pill-link {
  position: relative; overflow: hidden; font-size: 12px; font-weight: 800; text-decoration: none;
  padding: 5px 11px; border-radius: 999px; display: inline-block;
  transition: transform .25s cubic-bezier(.34,1.5,.6,1), box-shadow .25s ease, filter .25s ease;
}
.pill-link:hover { transform: translateY(-2px) scale(1.05); box-shadow: 0 6px 16px rgba(0,0,0,.18); filter: brightness(1.12); }
.pill-link:active { transform: translateY(0) scale(.97); }
.pill-link::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: linear-gradient(100deg, transparent 30%, rgba(255,255,255,.45) 50%, transparent 70%);
  background-size: 220% 100%; background-position: 150% 0; opacity: 0;
  transition: opacity .5s ease;
}
.pill-link:hover::after { opacity: 1; transition: opacity .5s ease; animation: pillSheen 1.4s ease forwards; }
@keyframes pillSheen { from { background-position: 150% 0; } to { background-position: -50% 0; } }
.steam-card .pill-link { padding: 5px 12px; }
.steam-bg-icon, .discord-bg-icon, .mal-bg-icon { position: absolute; right: -20px; bottom: -26px; opacity: .12; }

.presence-row { display: flex; align-items: center; gap: 14px; }
.avatar-wrap { position: relative; flex: none; }
.avatar-img { width: 64px; height: 64px; border-radius: 18px; object-fit: cover; box-shadow: 0 6px 16px rgba(40,90,160,.22); background: #dceefb; }
.status-dot { position: absolute; right: -4px; bottom: -4px; width: 18px; height: 18px; border-radius: 50%; border: 3px solid #fff; }
.presence-text { min-width: 0; }
.presence-name { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 20px; color: var(--ink); line-height: 1.1; }
.presence-status { font-size: 13px; font-weight: 700; margin-top: 2px; display: flex; align-items: center; gap: 6px; }

.info-pill { margin-top: 16px; display: flex; gap: 12px; align-items: center; border-radius: 16px; padding: 13px 15px; }
.info-pill--game { background: linear-gradient(135deg,rgba(124,92,255,.12),rgba(14,165,233,.12)); border: 1px solid rgba(124,92,255,.25); }
.info-pill--spotify { margin-top: 12px; background: rgba(30,215,96,.1); border: 1px solid rgba(30,215,96,.28); }
.info-pill--steam-game { background: rgba(102,192,244,.1); border: 1px solid rgba(102,192,244,.28); margin-bottom: 14px; gap: 12px; padding: 12px 14px; }
.info-pill-emoji:empty { display: none; }
.info-pill-emoji { font-size: 26px; flex: none; }
.info-pill-icon { width: 30px; height: 30px; border-radius: 8px; flex: none; object-fit: cover; }
.info-pill--steam-game .info-pill-emoji { font-size: 24px; }
.info-pill-label { font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
.info-pill-value { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 17px; color: var(--ink); line-height: 1.15; }
.steam-game-name { color: #fff; font-size: 16px; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.info-pill-sub { font-size: 12px; font-weight: 700; color: var(--ink-soft); margin-top: 1px; }
.spotify-art { width: 42px; height: 42px; border-radius: 9px; flex: none; object-fit: cover; background: #cfeede; }
.spotify-song { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 16px; color: var(--ink); line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.idle-banner { margin-top: 16px; font-size: 14px; font-weight: 700; color: var(--ink-faint); background: rgba(170,200,225,.16); border-radius: 14px; padding: 13px 15px; text-align: center; }

.loading-row { display: flex; align-items: center; gap: 12px; padding: 14px 4px; color: var(--ink-faint); font-weight: 700; }
.pulse-dot { width: 12px; height: 12px; border-radius: 50%; animation: pulse 1.1s ease-in-out infinite; }

/* Small pill button used by the weather / Discord fetch-error states */
.retry-btn {
  appearance: none; border: 1px solid rgba(140,180,230,.4); background: rgba(120,170,230,.16);
  color: inherit; font: inherit; font-size: 12px; font-weight: 800; letter-spacing: .04em;
  border-radius: 999px; padding: 6px 14px; cursor: pointer;
  transition: transform .25s ease, background .25s ease;
}
.retry-btn:hover { transform: translateY(-1px); background: rgba(120,170,230,.3); }
.retry-btn:active { transform: translateY(0) scale(.97); }
@media (prefers-reduced-motion: reduce) { .retry-btn { transition: none; } .retry-btn:hover, .retry-btn:active { transform: none; } }

.steam-avatar { width: 64px; height: 64px; border-radius: 14px; object-fit: cover; box-shadow: 0 6px 18px rgba(0,0,0,.45); flex: none; }
.steam-card .presence-name { font-size: 20px; }
.steam-card .presence-status { color: #9fc0d8; }

.discord-card .card-head-title, .discord-card .presence-name { color: #fff !important; }
.discord-card .presence-status, .discord-card .idle-banner { color: #c7cdfb; }
.discord-card .pill-link { color: #fff !important; background: rgba(255,255,255,.16) !important; }
.discord-card .loading-row { color: #c7cdfb; }
.discord-card .info-pill--game { background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.22); }
.discord-card .info-pill--spotify { background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.22); }
.discord-card .idle-banner { background: rgba(255,255,255,.12); }

/* ---- Discord: extra Lanyard presence details ---- */
.discord-username {
  display: block; margin-top: 1px; padding: 0;
  font-family: inherit; font-size: 13px; font-weight: 700; line-height: 1.35;
  color: #c2c8f5; background: none; border: none; cursor: pointer; letter-spacing: .01em;
  transition: color .15s ease;
}
.discord-username:hover { color: #fff; text-decoration: underline; }
.discord-platform { display: flex; align-items: center; justify-content: center; flex: none; color: #c7cdfb; opacity: .85; width: 13px; height: 13px; }
.discord-platform svg { display: block; width: 100%; height: 100%; }
.discord-custom { display: flex; align-items: center; gap: 8px; margin-top: 13px; color: #dfe2fb; font-size: 14px; font-weight: 600; }
.discord-custom-emoji { flex: none; font-size: 16px; line-height: 1; display: inline-flex; align-items: center; }
.discord-custom-emoji-img { width: 18px; height: 18px; display: block; }
.discord-custom-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-style: italic; }
.info-pill-elapsed { color: #aeb6f5 !important; margin-top: 2px; font-variant-numeric: tabular-nums; }

/* Spotify mini-player progress bar */
.info-pill .presence-text { flex: 1; min-width: 0; }
.info-pill--spotify { align-items: flex-start; }
.spotify-art { width: 46px; height: 46px; }
.spotify-progress { margin-top: 9px; }
.spotify-bar { height: 5px; border-radius: 999px; background: rgba(255,255,255,.2); overflow: hidden; }
/* The fill is always full width and slides in from the left (translateX -100% =
   empty, 0 = full) inside the clipped track. script.js moves it in small
   discrete steps (paintSpotifyBar) and it is deliberately NOT transitioned:
   the old `transition: width` re-laid-out the whole card on every frame, and
   even a compositor-only transform transition keeps the GPU producing a frame
   per vsync for as long as a song plays (see SPOTIFY_BAR_MS). */
.spotify-bar-fill { display: block; height: 100%; width: 100%; border-radius: 999px; background: #1db954; transform: translateX(-100%); }
.spotify-times { display: flex; justify-content: space-between; margin-top: 5px; font-size: 11px; font-weight: 700; color: #b9c0f7; font-variant-numeric: tabular-nums; }

/* ---- Toast (copy-to-clipboard confirmation) ---- */
.toast {
  position: fixed; left: 50%; bottom: 28px; transform: translate(-50%, 24px); z-index: 90;
  max-width: min(90vw, 440px); padding: 13px 22px; border-radius: 999px;
  background: var(--card-bg); backdrop-filter: var(--card-blur); border: var(--card-bd);
  box-shadow: 0 14px 34px rgba(3,10,24,.3); color: var(--ink);
  font-size: 14px; font-weight: 700; text-align: center; white-space: nowrap;
  opacity: 0; pointer-events: none; transition: opacity .3s ease, transform .3s ease;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
@media (max-width: 480px) { .toast { white-space: normal; } }
@media (prefers-reduced-motion: reduce) { .toast { transition: opacity .3s ease; transform: translate(-50%, 0); } }

.recent-games { position: relative; border-top: 1px solid rgba(102,192,244,.16); padding-top: 14px; }
.recent-games-label { font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: #9fc0d8; margin-bottom: 10px; }
.recent-game-row { display: flex; align-items: center; gap: 12px; padding: 7px 0; border-bottom: 1px solid rgba(102,192,244,.08); }
.recent-game-logo { width: 40px; height: 40px; border-radius: 8px; object-fit: cover; flex: none; background: rgba(102,192,244,.14); }
.recent-game-name { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 15px; color: #fff; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.recent-game-hours { font-size: 12px; font-weight: 700; color: #9fc0d8; margin-top: 1px; }

.steam-fallback { padding: 4px 0 8px; }
.steam-fallback-title { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 22px; color: #fff; margin-bottom: 6px; }
.steam-fallback-text { font-size: 13px; font-weight: 600; color: #9fc0d8; line-height: 1.5; }

/* ---- MyAnimeList (currently watching, via Jikan) ---- */
.mal-card { border-radius: 24px; padding: 22px; position: relative; overflow: hidden; background: linear-gradient(150deg,#16213e 0%,#2e51a2 60%,#16213e 100%); box-shadow: 0 16px 38px rgba(20,40,100,.32); backdrop-filter: none; border: none; }

/* ---- Letterboxd (latest film watched) ---- */
/* Base rule sits with the other card bases, BEFORE the liquid-glass section,
   so the glass overrides win on source order (and this stays the fallback
   where color-mix is unsupported). It used to live after the glass section,
   which silently cancelled the LB card's glass. */
.lb-card { border-radius: 24px; padding: 22px; position: relative; overflow: hidden; background: linear-gradient(150deg,#14181c 0%,#2c343c 60%,#14181c 100%); box-shadow: 0 16px 38px rgba(10,16,24,.4); backdrop-filter: none; border: none; }
.mal-card .loading-row { color: #c7d4f7; }
.mal-card .pill-link { color: #fff !important; background: rgba(255,255,255,.16) !important; }
.mal-card .idle-banner { background: rgba(255,255,255,.12); color: #c7d4f7; }

.mal-row { display: flex; align-items: center; gap: 12px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.22); border-radius: 14px; padding: 10px 12px; margin-top: 10px; text-decoration: none; transition: transform .25s ease, background .25s ease; }
.mal-row:hover { transform: translateY(-2px); background: rgba(255,255,255,.17); }
.mal-row:active { transform: translateY(0) scale(.98); }
.mal-cover { width: 44px; height: 62px; border-radius: 8px; object-fit: cover; flex: none; background: rgba(143,176,255,.18); }
.mal-info { min-width: 0; flex: 1; }
.mal-title { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 15px; color: #fff; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mal-progress { display: flex; align-items: center; gap: 10px; margin-top: 6px; }
.mal-bar { flex: 1; height: 5px; border-radius: 999px; background: rgba(255,255,255,.2); overflow: hidden; }
.mal-bar span { display: block; height: 100%; border-radius: 999px; background: #8fb0ff; }
.mal-eps { font-size: 11px; font-weight: 700; color: #c7d4f7; white-space: nowrap; font-variant-numeric: tabular-nums; }
.mal-progress .mal-eps:only-child { margin-left: 0; }
@media (prefers-reduced-motion: reduce) { .mal-row { transition: none; } .mal-row:hover, .mal-row:active { transform: none; } }

/* ---- Clickable presence pills + recent games ---- */
/* These are <a> elements that only get an href when there's a real
   destination (Spotify track, Steam store page). No href = inert. */
a.info-pill { text-decoration: none; color: inherit; }
a.info-pill[href] { cursor: pointer; transition: transform .25s ease, filter .25s ease; }
a.info-pill[href]:hover { transform: translateY(-2px); filter: brightness(1.08); }
a.info-pill[href]:active { transform: translateY(0) scale(.98); }
a.info-pill:not([href]) { cursor: default; }
a.recent-game-row { text-decoration: none; color: inherit; border-radius: 10px; padding: 7px 6px; margin: 0 -6px; transition: background .2s ease; }
a.recent-game-row[href]:hover { background: rgba(102,192,244,.12); }
@media (prefers-reduced-motion: reduce) {
  a.info-pill[href], a.recent-game-row { transition: none; }
  a.info-pill[href]:hover, a.info-pill[href]:active { transform: none; }
}

/* ---- PC specifications ---- */
/* Four grouped cards keep the ten supplied components scannable without a
   table. The 7/5 then 5/7 spans give the section a bento rhythm, while every
   card becomes full-width before its copy gets cramped. As with the rest of
   the site, .card supplies the glass rim/refraction and all colour variation
   is derived from the active theme's single accent token. Type scales with
   each card's own width; the lists stretch into the shared row height so
   short specs don't leave a large unused pocket below them. */
.pc-specs-grid {
  display: grid; grid-template-columns: repeat(12,minmax(0,1fr)); gap: 18px;
}
.pc-spec-card {
  min-width: 0; border-radius: 24px; padding: clamp(22px,3vw,30px);
  container-type: inline-size; display: flex; flex-direction: column;
}
.pc-spec-card--graphics {
  grid-column: span 7; min-height: 270px; display: grid;
  grid-template-columns: minmax(0,1fr) auto; align-items: end; gap: 20px;
  background:
    radial-gradient(circle at 88% 18%, color-mix(in srgb, var(--accent) 30%, transparent), transparent 42%),
    linear-gradient(145deg, color-mix(in srgb, var(--accent) 14%, transparent), transparent 66%),
    color-mix(in srgb, var(--card-bg) 62%, transparent);
}
.pc-spec-card--platform { grid-column: span 5; }
.pc-spec-card--storage {
  grid-column: span 5;
  background:
    radial-gradient(circle at 8% 100%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 48%),
    color-mix(in srgb, var(--card-bg) 58%, transparent);
}
.pc-spec-card--build {
  grid-column: span 7;
  background:
    radial-gradient(circle at 100% 0%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 46%),
    color-mix(in srgb, var(--card-bg) 58%, transparent);
}
.pc-spec-card-title {
  margin: 0 0 20px; font-family: 'Fredoka', sans-serif; font-weight: 600;
  font-size: 20px; line-height: 1.2; color: var(--accent);
}
.pc-spec-feature-copy { min-width: 0; align-self: stretch; display: flex; flex-direction: column; }
.pc-spec-feature-copy .pc-spec-card-title { margin-bottom: auto; padding-bottom: 20px; }
.pc-spec-maker {
  margin-bottom: 7px; font-size: 15px; font-weight: 800; color: var(--ink-soft);
}
.pc-spec-feature-name {
  margin: 0; font-family: 'Fredoka', sans-serif; font-weight: 500;
  font-size: clamp(32px,8cqi,46px); line-height: 1.06; color: var(--ink);
}
.pc-spec-feature-name strong { display: block; color: var(--accent); font-weight: 700; }
.pc-gpu-mark {
  min-width: 0; padding-left: 20px;
  border-left: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  display: flex; flex-direction: column; align-items: flex-end; color: var(--ink); text-align: right;
}
.pc-gpu-mark span {
  font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-soft); white-space: nowrap;
}
.pc-gpu-mark strong {
  font-family: 'Fredoka', sans-serif; font-weight: 700;
  font-size: clamp(60px,18cqi,100px); line-height: .9; letter-spacing: -.04em;
}
.pc-spec-list, .pc-storage-list { margin: 0; flex: 1; }
.pc-spec-list { display: grid; gap: 18px; align-content: space-between; }
.pc-spec-list--build { grid-template-columns: repeat(3,minmax(0,1fr)); gap: 20px; }
.pc-spec-entry dt {
  margin-bottom: 6px; font-size: 14px; font-weight: 800; color: var(--ink-faint);
}
.pc-spec-entry dd {
  margin: 0; font-family: 'Fredoka', sans-serif; font-weight: 600;
  font-size: clamp(22px,6.5cqi,28px); line-height: 1.25; color: var(--ink);
}
.pc-spec-list--build dd { font-size: clamp(20px,5cqi,28px); }
.pc-storage-list { display: grid; gap: 18px; align-content: space-between; }
.pc-storage-entry {
  display: grid; grid-template-columns: 90px minmax(0,1fr); align-items: baseline; gap: 16px;
}
.pc-storage-entry dt {
  font-family: 'Fredoka', sans-serif; font-weight: 700; font-size: 28px;
  line-height: 1; color: var(--accent); white-space: nowrap;
}
.pc-storage-entry dd {
  margin: 0; font-weight: 700; font-size: clamp(17px,4.8cqi,19px); line-height: 1.35; color: var(--ink);
}
@media (max-width: 860px) {
  .pc-spec-card { grid-column: 1 / -1; }
  .pc-spec-card--graphics { min-height: 0; }
}
@media (max-width: 600px) {
  .pc-spec-card--graphics { grid-template-columns: minmax(0,1fr); align-items: start; }
  .pc-gpu-mark {
    min-width: 0; padding: 18px 0 0; border-left: 0;
    border-top: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
    align-items: flex-start; text-align: left;
  }
  .pc-gpu-mark strong { font-size: 62px; }
  .pc-spec-list--build { grid-template-columns: minmax(0,1fr); }
  .pc-spec-list--build dd { font-size: clamp(22px,6.5cqi,28px); }
}

/* ---- Music / YouTube Music playlist ---- */
.music-grid { display: grid; grid-template-columns: minmax(0,1fr); }
.music-card {
  position: relative; border-radius: 24px; padding: 22px; overflow: hidden;
  isolation: isolate;
  border: none;
  /* Liquid-glass base: frosted backdrop + translucent red-tinted fill so the
     card reads as glass over its blurred cover art. */
  background:
    linear-gradient(150deg, rgba(255,26,26,.18), rgba(124,29,43,.18)),
    color-mix(in srgb, var(--card-bg) 50%, transparent);
  -webkit-backdrop-filter: blur(6px) saturate(1.7);
          backdrop-filter: blur(6px) saturate(1.7);
}
.music-card::before {
  content: ""; position: absolute; inset: -10%; z-index: -1;
  background: url('assets/Sisu.jpg') center / cover no-repeat;
  filter: blur(28px) saturate(1.3) brightness(.65);
  transform: scale(1.15);
  opacity: .45;
}
.music-card::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(150deg, rgba(20,8,10,.35), rgba(10,6,14,.5));
}
.music-card .card-head-title { color: #ff6b6b !important; }
.music-card .pill-link { color: #ff6b6b !important; background: rgba(255,255,255,.12) !important; }
.music-card .music-playlist-name,
.music-card .music-playlist-sub,
.music-card .music-recent-label,
.music-card .music-track-name { color: #fff; }
.music-card-inner { display: flex; gap: 20px; align-items: stretch; }
/* Left zone column: spreads the cover + playlist info across the card's full
   height so no dead space sits under the text. */
.music-side { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: space-evenly; }
.music-cover-wrap {
  position: relative; width: 152px; height: 152px; border-radius: 18px; flex: none; overflow: hidden;
  text-decoration: none; display: flex; align-items: center; justify-content: center;
  background: linear-gradient(150deg,#ff1a1a,#7c1d2b);
  box-shadow: 0 10px 24px rgba(255,0,0,.22);
}
.music-cover { position: relative; z-index: 1; width: 100%; height: 100%; object-fit: cover; display: block; }
.music-body { min-width: 0; }
.music-playlist-label { font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: #ff4d4d; }
.music-playlist-name { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: clamp(22px,4vw,28px); color: var(--ink); line-height: 1.08; margin-top: 4px; }
.music-playlist-sub { font-size: 13px; font-weight: 700; color: var(--ink-soft); margin-top: 5px; }
.music-recent { margin-top: 16px; border-top: 1px solid rgba(255,90,90,.18); padding-top: 12px; }
/* Featured tracks: fills the card's empty right side on wide screens,
   split off by a hairline. Stacks below the playlist info on mobile. */
.music-featured { flex: 1.1; min-width: 0; border-left: 1px solid rgba(255,90,90,.18); padding-left: 20px; }
.music-recent-label { font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft); margin-bottom: 6px; }
.music-track-row { display: flex; align-items: center; gap: 13px; padding: 8px 0; border-bottom: 1px solid rgba(140,170,200,.1); }
.music-track-row:last-child { border-bottom: none; }
.music-track-text { min-width: 0; flex: 1; }
.music-track-num { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 15px; color: #ff4d4d; width: 16px; text-align: center; flex: none; }
.music-track-name { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 16px; color: var(--ink); line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.music-track-artist { font-size: 12px; font-weight: 700; color: var(--ink-soft); margin-top: 1px; }

/* ---- Music card hover: expand/bounce + equalizer (desktop only) ---- */
/* The whole effect is gated behind @media (hover: hover) and (pointer: fine)
   so touch devices never see it, and killed under prefers-reduced-motion at
   the bottom of this block. Bars sit invisible and paused until hover, so
   the animation only ever runs while the mouse is over the card. */
.music-viz {
  display: flex; align-items: flex-end; gap: 3px; height: 22px; margin-top: 12px;
  opacity: 0; transition: opacity .35s ease;
}
.music-viz span {
  width: 4px; height: 100%; border-radius: 2px;
  transform: scaleY(.22); transform-origin: bottom;
  background: linear-gradient(180deg,#ff8a8a,#ff1a1a);
  animation: music-viz-bounce 1.1s ease-in-out infinite;
  animation-play-state: paused;
}
/* Varied durations/delays so the bars move out of sync, like a real meter. */
.music-viz span:nth-child(2) { animation-duration: .85s; animation-delay: .14s; }
.music-viz span:nth-child(3) { animation-duration: 1.25s; animation-delay: .3s; }
.music-viz span:nth-child(4) { animation-duration: .75s; animation-delay: .06s; }
.music-viz span:nth-child(5) { animation-duration: 1.05s; animation-delay: .22s; }
.music-viz span:nth-child(6) { animation-duration: .9s; animation-delay: .38s; }
.music-viz span:nth-child(7) { animation-duration: 1.2s; animation-delay: .1s; }
@keyframes music-viz-bounce {
  0%, 100% { transform: scaleY(.22); }
  50% { transform: scaleY(1); }
}
@media (hover: hover) and (pointer: fine) {
  /* .music-card also carries .scroll-reveal — same fix as .social-card above:
     restate the transition at higher specificity so the reveal transition
     doesn't clobber the bouncier hover one. */
  .music-card.scroll-reveal {
    transition: opacity .7s cubic-bezier(.21,.6,.35,1), transform .45s cubic-bezier(.34,1.56,.64,1), box-shadow .45s ease;
  }
  /* .scroll-reveal.is-visible pins transform: translateY(0) after reveal and
     would win on source order, so restate the hover transform at higher
     specificity. The overshoot easing makes the expand bounce. */
  .music-card.scroll-reveal.is-visible:hover {
    transform: scale(1.03);
    box-shadow: 0 22px 46px rgba(255,26,26,.3), var(--glint);
  }
  .music-card:hover .music-viz { opacity: 1; }
  .music-card:hover .music-viz span { animation-play-state: running; }
}
@media (prefers-reduced-motion: reduce) {
  .music-viz { display: none; }
}
@media (max-width: 560px) {
  .music-card-inner { flex-direction: column; align-items: center; text-align: center; }
  .music-cover-wrap { width: 120px; height: 120px; }
  .music-body { width: 100%; }
  .music-recent { text-align: left; }
  .music-viz { justify-content: center; }
  .music-side { align-items: center; }
  .music-featured {
    width: 100%; text-align: left; margin-top: 18px;
    border-left: none; padding-left: 0;
    border-top: 1px solid rgba(255,90,90,.18); padding-top: 14px;
  }
}

/* ---- Socials ---- */
/* Solid brand tiles: full-bleed brand gradient, glossy bevelled edges, and
   the name + handle centred below a bare white logo -- the old boxed icon
   badge is gone, so the mark sits straight on the tile. The whole design
   hangs off two custom properties -- --brand-a (tile centre, lighter) and
   --brand-b (tile edge, darker) -- so a new network only needs a
   .social-card--<name> rule with its two brand stops; the bevel, gloss and
   hover are inherited. Deliberately NOT .card members: there is no backdrop
   to refract through a solid tile, so the glass layer and its displacement
   maps skip them entirely. */
.socials-grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(180px,1fr)); gap: 16px; }
.social-card {
  --brand-a: #7c5cff; /* fallback centre stop (violet) */
  --brand-b: #4a34b8; /* fallback edge stop */
  position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: center; text-align: center;
  padding: 26px 16px 20px; border-radius: 22px; overflow: hidden;
  text-decoration: none; color: #fff;
  background: radial-gradient(125% 135% at 50% 28%, var(--brand-a) 0%, var(--brand-b) 82%);
  box-shadow:
    inset 0 2px 1px rgba(255,255,255,.38),   /* top light-catch */
    inset 2px 0 5px rgba(255,255,255,.10),   /* left sheen */
    inset -2px -4px 8px rgba(0,0,0,.30),     /* lower-right depth */
    0 14px 28px rgba(0,0,0,.32);
  transition: transform .22s cubic-bezier(.25,.8,.35,1), box-shadow .22s ease, z-index .3s;
  -webkit-tap-highlight-color: transparent;
}
/* Broad gloss across the top half, like light hitting a lacquered tile. */
.social-card::before {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  background: linear-gradient(178deg, rgba(255,255,255,.20) 0%, rgba(255,255,255,.05) 34%, rgba(255,255,255,0) 54%);
  pointer-events: none;
}
/* Hover sheen: a soft band of light sweeps once across the face, like the
   tile catching a lamp as it tips towards you. Parked off the left edge and
   glides to off the right edge on hover. ONE-WAY on purpose: the transition
   lives only on the :hover rule, so when the pointer leaves, the band snaps
   straight back to its parking spot (both ends sit fully outside the tile,
   so the reset is invisible). Putting the transition on the base rule made
   the band sweep BACKWARDS on mouse-leave, so sliding along the row flashed
   the previous tile and the current one at the same time. The print rides
   above the band (z-index below) and the tile's overflow: hidden clips it
   to the rounded corners. */
.social-card::after {
  content: ''; position: absolute; top: -30%; bottom: -30%; left: 0; width: 55%;
  background: linear-gradient(100deg, rgba(255,255,255,0) 0%, rgba(255,255,255,.13) 40%, rgba(255,255,255,.30) 50%, rgba(255,255,255,.13) 60%, rgba(255,255,255,0) 100%);
  transform: translateX(-180%) skewX(-14deg);
  pointer-events: none;
}
.social-card:hover::after {
  transform: translateX(300%) skewX(-14deg);
  transition: transform .6s cubic-bezier(.45,0,.25,1);
}
/* Hover is a plain physical lift: straight up, no scale, no tilt. The top
   light-catch brightens and the drop shadow deepens, so the tile reads as
   rising towards the light rather than growing. */
.social-card:hover {
  transform: translateY(-3px); z-index: 2;
  box-shadow:
    inset 0 2px 1px rgba(255,255,255,.48),
    inset 2px 0 5px rgba(255,255,255,.12),
    inset -2px -4px 8px rgba(0,0,0,.26),
    0 18px 32px rgba(0,0,0,.38);
}
/* The press is what sells the button: a near-instant down-stroke, the outer
   shadow collapses, and depth pools at the top edge like the face sank into
   its bezel. Releasing glides back on the base .22s transition. */
.social-card:active {
  transform: translateY(1px);
  transition-duration: .07s;
  box-shadow:
    inset 0 1px 1px rgba(255,255,255,.20),
    inset 2px 0 5px rgba(255,255,255,.06),
    inset -2px -4px 8px rgba(0,0,0,.28),
    inset 0 10px 18px rgba(0,0,0,.32),
    0 5px 12px rgba(0,0,0,.30);
}
/* The mark is just a positioning wrapper now: no frosted box, no rounded
   background, no shadow -- the bare logo sits directly on the brand tile.
   Mark, name and handle are raised above the sheen sweep so the light
   passes across the lacquer, not the print.
   Do NOT call it .social-badge: EasyList / Fanboy's Social Blocking list ship
   a generic `##.social-badge` hide rule, so uBlock Origin and Adblock Plus
   users saw every logo vanish (the tiles collapsed to name + handle). Keep
   class names in this section off those lists (evals/adblock-filters.js
   checks the live lists; tests.js pins the known offenders). */
.social-mark, .social-name, .social-handle { position: relative; z-index: 1; }
.social-mark {
  width: 48px; height: 48px; margin-bottom: 12px;
  display: flex; align-items: center; justify-content: center;
  transition: transform .28s cubic-bezier(.34,1.3,.5,1);
}
.social-mark svg { width: 40px; height: 40px; display: block; filter: drop-shadow(0 2px 4px rgba(0,0,0,.28)); transition: filter .28s ease; }
/* No tilt: the mark lifts straight up with the tile and grows a touch, its
   shadow softening as if it floated a little off the face. */
.social-card:hover .social-mark { transform: translateY(-2px) scale(1.07); }
.social-card:hover .social-mark svg { filter: drop-shadow(0 5px 9px rgba(0,0,0,.38)); }
.social-mark--text { font-family: 'Fredoka', sans-serif; font-weight: 700; font-size: 17px; color: #fff; letter-spacing: .03em; text-shadow: 0 1px 2px rgba(0,0,0,.3); }
.social-mark--dots span { width: 24px; height: 24px; border-radius: 50%; display: block; box-shadow: inset 0 -2px 3px rgba(0,0,0,.25); }
.social-name { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 17px; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,.35); }
.social-handle { font-size: 13px; font-weight: 700; color: rgba(255,255,255,.74); transition: color .22s ease; }
.social-card:hover .social-handle { color: #fff; }
.social-card:focus { outline: none; }
.social-card:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }

/* Brand stops -- one modifier per network. Adding a social = copy a tile in
   index.html + one line here (lighter centre stop, darker edge stop). */
.social-card--x { --brand-a: #3d3d44; --brand-b: #0e0e11; }
.social-card--instagram {
  background:
    radial-gradient(125% 135% at 50% 28%, rgba(255,255,255,.14) 0%, rgba(0,0,0,.22) 82%),
    linear-gradient(45deg, #feda75, #fa7e1e, #d62976, #962fbf, #4f5bd5);
}
.social-card--youtube { --brand-a: #f23a30; --brand-b: #a80f14; }
.social-card--reddit { --brand-a: #ff6a30; --brand-b: #c22e00; }
.social-card--tiktok { --brand-a: #2c2c34; --brand-b: #0b0b0f; }
.social-card--mal { --brand-a: #4069c8; --brand-b: #1c3873; }
.social-card--letterboxd { --brand-a: #3b4552; --brand-b: #14181d; }

/* Reduced motion: the lift/press/sheen are decorative — the tile simply
   changes its shadows, with no movement at all. */
@media (prefers-reduced-motion: reduce) {
  .social-card, .social-card::after, .social-card:hover::after, .social-mark, .social-mark svg, .social-handle { transition: none; }
  .social-card:hover, .social-card:active, .social-card:hover .social-mark { transform: none; }
}

/* ---- Stories ---- */
.stories-grid { display: grid; grid-template-columns: repeat(auto-fit,minmax(300px,1fr)); gap: 18px; }
.story-link { text-decoration: none; color: inherit; display: block; }
.story-card { position: relative; border-radius: 24px; padding: 26px; overflow: hidden; height: 100%; transition: transform .2s ease, box-shadow .2s ease; }
.story-link:hover .story-card { transform: translateY(-4px); }
.story-title { position: relative; font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 30px; color: #fff; line-height: 1.05; margin: 16px 0 8px; }
.story-desc { position: relative; font-size: 14px; font-weight: 600; color: rgba(255,255,255,.85); line-height: 1.55; max-width: 300px; }
.story-cta {
  position: relative; display: inline-flex; align-items: center; gap: 7px; margin-top: 18px;
  background: #fff; font-weight: 800; font-size: 13px; padding: 9px 16px; border-radius: 999px;
  transition: transform .25s cubic-bezier(.34,1.5,.6,1), box-shadow .25s ease, gap .25s ease;
}
.story-link:hover .story-cta { transform: scale(1.06); box-shadow: 0 8px 20px rgba(0,0,0,.28); gap: 11px; }

/* ---- Back to top ---- */
/* Fades + scales in/out instead of popping via display:none, matching the
   .scroll-reveal easing used everywhere else. Stays display:flex at all times
   so the transition can actually run; .to-top.hidden overrides the generic
   .hidden{display:none!important} rule (two classes beats one at equal
   !important weight) and drives the hidden state with opacity/transform/
   pointer-events instead. right: 28px (not 20px) because the custom page
   scrollbar (.cscroll) ends flush at 20px from the screen edge (4px offset
   + 16px wide) — 28px leaves an 8px gap instead of touching it. No hover
   state by design; only :active gives press feedback. */
.to-top {
  position: fixed; right: 28px; bottom: 20px; z-index: 50; width: 52px; height: 52px;
  border-radius: 50%; border: none; cursor: pointer;
  background: linear-gradient(150deg,#0ea5e9,#7c5cff); color: #fff; font-size: 23px; line-height: 1;
  box-shadow: 0 12px 28px rgba(80,110,230,.42); display: flex; align-items: center; justify-content: center;
  opacity: 1; transform: translateY(0) scale(1); pointer-events: auto;
  transition: opacity .35s cubic-bezier(.21,.6,.35,1), transform .3s cubic-bezier(.34,1.6,.55,1);
}
.to-top:active { transform: translateY(-1px) scale(.96); }
.to-top.hidden {
  display: flex !important;
  opacity: 0;
  transform: translateY(14px) scale(.85);
  pointer-events: none;
}
@media (max-width: 480px) {
  .to-top {
    width: 44px; height: 44px; font-size: 18px; right: 14px;
    bottom: max(16px, env(safe-area-inset-bottom, 16px));
  }
}

@media (prefers-reduced-motion: reduce) {
  .to-top { transition: none; }
}

/* ============ MEADOW FOOTER (a grassy field with a house) ============ */
/* The last thing on the page: a full-bleed illustrated field with a cottage,
   trees, a picket fence, a path and wildflowers, the credit line resting on the
   grass. It is one decorative inline SVG (index.html: 1440x340 units, the hills
   running far past both edges so any width fills) painted entirely from the
   --mf-* tokens below, so it follows the same clock as the sky:
     - Day and night. --m-day is 0 in deep night and 1 by day, read from the live
       page tint (--bg-l: 0% black night .. ~36% at noon; full colour once the sky
       is ~22% light) plus a little of the sunset glow, so at dusk the field is
       still half lit by the low sun. Every colour is a colour-mix() between its
       --d-* (day) and --n-* (night) value.
     - The window lights. --m-lit is a proper "dark enough" switch, not a creep:
       0 while it is light (through the whole day and the sunset itself), easing
       on once the afterglow has faded (--m-day below 0.4, i.e. from about 20
       minutes after sunset, fully on by about 55) and easing off again about an
       hour after sunrise (tests.js pins the timing for every season). When it
       is on, the window glass turns amber, each pane gets a bright lit centre, a
       soft halo surrounds the windows, the attic window and the door lantern, and
       warm light pools on the grass and the path. Even without colour-mix() the
       panes and pools (plain gradients) still light up.
     - Sunset. --m-warm folds up to 24% orange (--sunset, from KazuLib.sunsetGlow)
       into everything except the glass and the blossom.
     - Cherry-blossom season. The pink blossom on the big tree is only there
       while html:not(.no-sakura), the same 20 Mar - 10 May window as the branches.
     - Christmas repaints it as a snowy field with the lights on all day.
   Static paint, no animation, so it costs nothing and shows on every device.
   Browsers without colour-mix() keep the plain day colours. It sits at
   z-index 0 (above the weather canvases, below .container) so the fixed
   back-to-top button, which lives inside .container, stays on top of it. */
.footer.meadow {
  position: relative; z-index: 0; margin-top: -34px; overflow: hidden; text-align: center;
  /* Skipped (no style, layout or paint) until the reader nears the bottom of the
     page. The illustration is ~535 SVG nodes at the very end of a long page, and
     on a phone every incremental layout pass while the document streams in
     used to re-lay-out all of it: with it skipped, first contentful paint is
     ~28% earlier (4x-throttled phone profile, median of 9). The placeholder is
     the art's real height (viewBox 1440x340, width clamp(860px, 100%, 1800px)),
     so nothing shifts when it renders; `auto` then remembers the measured one. */
  content-visibility: auto;
  contain-intrinsic-block-size: auto calc(clamp(860px, 100vw, 1800px) * 340 / 1440);
  --m-day: clamp(0, calc((var(--bg-l, 36) + var(--sunset, 0) * 9) / 22), 1);
  --m-dp: calc(var(--m-day) * 100%);
  --m-lit: clamp(0, calc(((1 - var(--m-day)) - 0.6) / 0.2), 1);
  --m-lp: calc(var(--m-lit) * 100%);
  --m-warm: calc(var(--sunset, 0) * 24%);
  /* October lanterns: paint shares the cottage's daylight and light switch. */
  --d-pumpkin: #ce7535; --n-pumpkin: #653421;
  --d-pumpkin-rib: #ad5228; --n-pumpkin-rib: #42241f;
  --d-pumpkin-face: #3c242b; --n-pumpkin-face: #ffd48b;
  --d-pumpkin-stem: #596b43; --n-pumpkin-stem: #283c30;
  --d-pumpkin-lit: #ffd48b; --n-pumpkin-lit: #ffd48b;
  --mf-pumpkin: var(--d-pumpkin);
  --mf-pumpkin-rib: var(--d-pumpkin-rib);
  --mf-pumpkin-face: var(--d-pumpkin-face);
  --mf-pumpkin-stem: var(--d-pumpkin-stem);
  --mf-pumpkin-lit: var(--d-pumpkin-lit);
  /* Day colours (--d-*) and night colours (--n-*). --mf-* below are what the scene paints with. */
  --d-far: #8fc3a0; --n-far: #0e1d2c;
  --d-far-hi: #b7dfbd; --n-far-hi: #1b3a49;
  --d-far-tree: #6fae86; --n-far-tree: #0b1826;
  --d-mid: #63b46e; --n-mid: #10261f;
  --d-mid-hi: #93d582; --n-mid-hi: #1d4a3a;
  --d-near: #48a259; --n-near: #12301f;
  --d-near-hi: #82ca6c; --n-near-hi: #1f5236;
  --d-fg: #2e7f45; --n-fg: #0e2a1b;
  --d-fg-hi: #4fa457; --n-fg-hi: #184129;
  --d-blade: #8fd67d; --n-blade: #24583a;
  --d-blade-fg: #56b060; --n-blade-fg: #1a4a2d;
  --d-path: #e8d6ae; --n-path: #3b3f4e;
  --d-stone: #f1e3c0; --n-stone: #4a5064;
  --d-wall: #f8edd9; --n-wall: #38425e;
  --d-wall-2: #e5d3b5; --n-wall-2: #2c3550;
  --d-base: #b8b1a5; --n-base: #2a3040;
  --d-roof: #c9583f; --n-roof: #33263f;
  --d-roof-2: #a8452f; --n-roof-2: #281d33;
  --d-trim: #fff4e0; --n-trim: #465074;
  --d-chimney: #a9513c; --n-chimney: #2d2233;
  --d-cap: #7d3b2c; --n-cap: #211a2b;
  --d-door: #7c4b2d; --n-door: #2c2130;
  --d-door-2: #5f3720; --n-door-2: #221925;
  --d-frame: #fffaf0; --n-frame: #4a5478;
  --d-glass: #c4e4f8; --n-glass: #2b3656;
  --d-box: #8b5b37; --n-box: #2a2230;
  --d-trunk: #7d5634; --n-trunk: #22201f;
  --d-leaf-1: #2f8a4a; --n-leaf-1: #10301f;
  --d-leaf-2: #47a857; --n-leaf-2: #163c27;
  --d-leaf-3: #7dcd6b; --n-leaf-3: #22503a;
  --d-bush: #3b9c50; --n-bush: #12331f;
  --d-fence: #faf6ee; --n-fence: #4d5771;
  --d-blossom: #f9b9d1; --n-blossom: #5b4360;
  --lit-glass: #ffd27a; /* the lit window colour: the lights, once they are on */
  --mf-far: var(--d-far);
  --mf-far-hi: var(--d-far-hi);
  --mf-far-tree: var(--d-far-tree);
  --mf-mid: var(--d-mid);
  --mf-mid-hi: var(--d-mid-hi);
  --mf-near: var(--d-near);
  --mf-near-hi: var(--d-near-hi);
  --mf-fg: var(--d-fg);
  --mf-fg-hi: var(--d-fg-hi);
  --mf-blade: var(--d-blade);
  --mf-blade-fg: var(--d-blade-fg);
  --mf-path: var(--d-path);
  --mf-stone: var(--d-stone);
  --mf-wall: var(--d-wall);
  --mf-wall-2: var(--d-wall-2);
  --mf-base: var(--d-base);
  --mf-roof: var(--d-roof);
  --mf-roof-2: var(--d-roof-2);
  --mf-trim: var(--d-trim);
  --mf-chimney: var(--d-chimney);
  --mf-cap: var(--d-cap);
  --mf-door: var(--d-door);
  --mf-door-2: var(--d-door-2);
  --mf-frame: var(--d-frame);
  --mf-glass: var(--d-glass);
  --mf-box: var(--d-box);
  --mf-trunk: var(--d-trunk);
  --mf-leaf-1: var(--d-leaf-1);
  --mf-leaf-2: var(--d-leaf-2);
  --mf-leaf-3: var(--d-leaf-3);
  --mf-bush: var(--d-bush);
  --mf-fence: var(--d-fence);
  --mf-blossom: var(--d-blossom);
}
@supports (color: color-mix(in oklab, red 50%, blue)) {
  .footer.meadow {
    --mf-far: color-mix(in oklab, color-mix(in oklab, var(--d-far) var(--m-dp), var(--n-far)), #ff9a4a var(--m-warm));
    --mf-far-hi: color-mix(in oklab, color-mix(in oklab, var(--d-far-hi) var(--m-dp), var(--n-far-hi)), #ff9a4a var(--m-warm));
    --mf-far-tree: color-mix(in oklab, color-mix(in oklab, var(--d-far-tree) var(--m-dp), var(--n-far-tree)), #ff9a4a var(--m-warm));
    --mf-mid: color-mix(in oklab, color-mix(in oklab, var(--d-mid) var(--m-dp), var(--n-mid)), #ff9a4a var(--m-warm));
    --mf-mid-hi: color-mix(in oklab, color-mix(in oklab, var(--d-mid-hi) var(--m-dp), var(--n-mid-hi)), #ff9a4a var(--m-warm));
    --mf-near: color-mix(in oklab, color-mix(in oklab, var(--d-near) var(--m-dp), var(--n-near)), #ff9a4a var(--m-warm));
    --mf-near-hi: color-mix(in oklab, color-mix(in oklab, var(--d-near-hi) var(--m-dp), var(--n-near-hi)), #ff9a4a var(--m-warm));
    --mf-fg: color-mix(in oklab, color-mix(in oklab, var(--d-fg) var(--m-dp), var(--n-fg)), #ff9a4a var(--m-warm));
    --mf-fg-hi: color-mix(in oklab, color-mix(in oklab, var(--d-fg-hi) var(--m-dp), var(--n-fg-hi)), #ff9a4a var(--m-warm));
    --mf-blade: color-mix(in oklab, color-mix(in oklab, var(--d-blade) var(--m-dp), var(--n-blade)), #ff9a4a var(--m-warm));
    --mf-blade-fg: color-mix(in oklab, color-mix(in oklab, var(--d-blade-fg) var(--m-dp), var(--n-blade-fg)), #ff9a4a var(--m-warm));
    --mf-path: color-mix(in oklab, color-mix(in oklab, var(--d-path) var(--m-dp), var(--n-path)), #ff9a4a var(--m-warm));
    --mf-stone: color-mix(in oklab, color-mix(in oklab, var(--d-stone) var(--m-dp), var(--n-stone)), #ff9a4a var(--m-warm));
    --mf-wall: color-mix(in oklab, color-mix(in oklab, var(--d-wall) var(--m-dp), var(--n-wall)), #ff9a4a var(--m-warm));
    --mf-wall-2: color-mix(in oklab, color-mix(in oklab, var(--d-wall-2) var(--m-dp), var(--n-wall-2)), #ff9a4a var(--m-warm));
    --mf-base: color-mix(in oklab, color-mix(in oklab, var(--d-base) var(--m-dp), var(--n-base)), #ff9a4a var(--m-warm));
    --mf-roof: color-mix(in oklab, color-mix(in oklab, var(--d-roof) var(--m-dp), var(--n-roof)), #ff9a4a var(--m-warm));
    --mf-roof-2: color-mix(in oklab, color-mix(in oklab, var(--d-roof-2) var(--m-dp), var(--n-roof-2)), #ff9a4a var(--m-warm));
    --mf-trim: color-mix(in oklab, color-mix(in oklab, var(--d-trim) var(--m-dp), var(--n-trim)), #ff9a4a var(--m-warm));
    --mf-chimney: color-mix(in oklab, color-mix(in oklab, var(--d-chimney) var(--m-dp), var(--n-chimney)), #ff9a4a var(--m-warm));
    --mf-cap: color-mix(in oklab, color-mix(in oklab, var(--d-cap) var(--m-dp), var(--n-cap)), #ff9a4a var(--m-warm));
    --mf-door: color-mix(in oklab, color-mix(in oklab, var(--d-door) var(--m-dp), var(--n-door)), #ff9a4a var(--m-warm));
    --mf-door-2: color-mix(in oklab, color-mix(in oklab, var(--d-door-2) var(--m-dp), var(--n-door-2)), #ff9a4a var(--m-warm));
    --mf-frame: color-mix(in oklab, color-mix(in oklab, var(--d-frame) var(--m-dp), var(--n-frame)), #ff9a4a var(--m-warm));
    --mf-glass: color-mix(in oklab, var(--lit-glass) var(--m-lp), color-mix(in oklab, var(--d-glass) var(--m-dp), var(--n-glass)));
    --mf-box: color-mix(in oklab, color-mix(in oklab, var(--d-box) var(--m-dp), var(--n-box)), #ff9a4a var(--m-warm));
    --mf-trunk: color-mix(in oklab, color-mix(in oklab, var(--d-trunk) var(--m-dp), var(--n-trunk)), #ff9a4a var(--m-warm));
    --mf-leaf-1: color-mix(in oklab, color-mix(in oklab, var(--d-leaf-1) var(--m-dp), var(--n-leaf-1)), #ff9a4a var(--m-warm));
    --mf-leaf-2: color-mix(in oklab, color-mix(in oklab, var(--d-leaf-2) var(--m-dp), var(--n-leaf-2)), #ff9a4a var(--m-warm));
    --mf-leaf-3: color-mix(in oklab, color-mix(in oklab, var(--d-leaf-3) var(--m-dp), var(--n-leaf-3)), #ff9a4a var(--m-warm));
    --mf-bush: color-mix(in oklab, color-mix(in oklab, var(--d-bush) var(--m-dp), var(--n-bush)), #ff9a4a var(--m-warm));
    --mf-fence: color-mix(in oklab, color-mix(in oklab, var(--d-fence) var(--m-dp), var(--n-fence)), #ff9a4a var(--m-warm));
    --mf-blossom: color-mix(in oklab, var(--d-blossom) var(--m-dp), var(--n-blossom));
  }
}
.meadow-art {
  display: block; position: relative; left: 50%; translate: -50% 0;
  width: clamp(860px, 100%, 1800px); height: auto; aspect-ratio: 1440 / 340; overflow: visible;
}
/* Phones only ever see the middle of the scene (the svg is never narrower than
   860px); nudge the window so the tree-and-house group sits centred. */
@media (max-width: 600px) { .meadow-art { translate: calc(-50% - 44px) 0; } }
.mf-c-far { fill: var(--mf-far); }
.mf-c-mid { fill: var(--mf-mid); }
.mf-c-near { fill: var(--mf-near); }
.mf-c-fg { fill: var(--mf-fg); }
.mf-c-path { fill: var(--mf-path); }
.mf-c-stone { fill: var(--mf-stone); }
.mf-c-wall { fill: var(--mf-wall); }
.mf-c-wall2 { fill: var(--mf-wall-2); }
.mf-c-base { fill: var(--mf-base); }
.mf-c-roof { fill: var(--mf-roof); }
.mf-c-roof2 { fill: var(--mf-roof-2); }
.mf-c-trim { fill: var(--mf-trim); }
.mf-c-chimney { fill: var(--mf-chimney); }
.mf-c-cap { fill: var(--mf-cap); }
.mf-c-door { fill: var(--mf-door); }
.mf-c-door2 { fill: var(--mf-door-2); }
.mf-c-frame { fill: var(--mf-frame); }
.mf-c-glass { fill: var(--mf-glass); }
.mf-c-box { fill: var(--mf-box); }
.mf-c-trunk { fill: var(--mf-trunk); }
.mf-c-leaf1 { fill: var(--mf-leaf-1); }
.mf-c-leaf2 { fill: var(--mf-leaf-2); }
.mf-c-leaf3 { fill: var(--mf-leaf-3); }
.mf-c-fence { fill: var(--mf-fence); }
.mf-s-farhi { stroke: var(--mf-far-hi); }
.mf-s-midhi { stroke: var(--mf-mid-hi); }
.mf-s-nearhi { stroke: var(--mf-near-hi); }
.mf-s-fghi { stroke: var(--mf-fg-hi); }
.mf-s-blade { stroke: var(--mf-blade); }
.mf-s-bladefg { stroke: var(--mf-blade-fg); }
.mf-far-tree { fill: var(--mf-far-tree); }
.mf-bush { fill: var(--mf-bush); }
.mf-blossom { fill: var(--mf-blossom); }
.mf-shadow { fill: #04140a; opacity: .22; }
.mf-crest { fill: none; stroke-width: 3; stroke-linecap: round; opacity: .55; }
.mf-blades { fill: none; stroke-width: 1.9; stroke-linecap: round; }
.mf-line { fill: none; stroke: var(--mf-frame); stroke-width: 2.4; }
.mf-courses { fill: none; stroke: var(--mf-roof-2); stroke-width: 1.6; opacity: .5; }
.mf-trimline { fill: none; stroke: var(--mf-trim); stroke-width: 4; stroke-linejoin: round; stroke-linecap: round; }
.mf-doorline { fill: none; stroke: var(--mf-door-2); stroke-width: 1.6; }
.mf-smoke { fill: none; stroke: #e8eef5; stroke-width: 4; stroke-linecap: round; opacity: calc(.18 + .3 * var(--m-day)); }
.mf-glow { opacity: var(--m-lit); }
.mf-flowers { opacity: calc(.45 + .55 * var(--m-day)); }
html.no-sakura .mf-blossom { display: none; }
/* A soft shade at the bottom keeps the credit line readable at any scale and
   time of day; the line itself rests on the foreground grass. */
.meadow::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 72px; pointer-events: none;
  background: linear-gradient(to top, rgba(4,26,12,.55), rgba(4,26,12,0));
}
.meadow-credit {
  position: absolute; left: 0; right: 0; bottom: max(14px, env(safe-area-inset-bottom, 0px)); z-index: 1;
  margin: 0; padding: 0 16px; font-size: 13px; font-weight: 800; letter-spacing: .02em;
  color: #f4f8e6; text-shadow: 0 1px 3px rgba(4,24,12,.75);
}
/* Fade in only: the generic .scroll-reveal also slides the element 28px, and a
   full-bleed element at the very bottom of the page would grow the document. */
.footer.meadow.scroll-reveal, .footer.meadow.scroll-reveal.is-visible { transform: none; transition: opacity .9s ease; }
/* Christmas: a snowy field, snowy roof, pines with snow-tipped boughs, lights on. */
body.season-christmas .footer.meadow {
  --d-far: #cfe0ea; --d-far-hi: #eef6fb; --d-far-tree: #9fbccb;
  --d-mid: #e3eff6; --d-mid-hi: #ffffff; --d-near: #eff6fa; --d-near-hi: #ffffff;
  --d-fg: #d3e4ee; --d-fg-hi: #f1f8fc; --d-blade: #ffffff; --d-blade-fg: #cfe2ed;
  --d-path: #c9b58e; --d-stone: #efe4c6; --d-roof: #f3f8fb; --d-roof-2: #d6e5ef; --d-trim: #ffffff;
  --d-leaf-1: #1d6b46; --d-leaf-2: #24825a; --d-leaf-3: #e9f5f0; --d-bush: #2a7a55;
  --d-fence: #ffffff; --d-wall: #f6ead4;
  --m-lit: max(0.75, clamp(0, calc(((1 - var(--m-day)) - 0.6) / 0.2), 1)); /* cosy lights on all day */
}
body.season-christmas .mf-flowers, body.season-christmas .mf-blossom { display: none; }
body.season-christmas .meadow-credit { color: #0f3b2a; text-shadow: 0 1px 0 rgba(255,255,255,.65); }
body.season-christmas .meadow::after { display: none; }

/* October: an autumn field; its existing day/night and sunset mixes still run. */
html.season-halloween .footer.meadow {
  --d-far: #a6a47d; --n-far: #231e30; --d-far-hi: #c6bd99; --n-far-hi: #372b42;
  --d-far-tree: #8d775b; --n-far-tree: #241d2b;
  --d-mid: #85734b; --n-mid: #29272b; --d-mid-hi: #b5a06a; --n-mid-hi: #41373b;
  --d-near: #746744; --n-near: #282d25; --d-near-hi: #a58a56; --n-near-hi: #3b4030;
  --d-fg: #514e35; --n-fg: #1e241f; --d-fg-hi: #85794e; --n-fg-hi: #323b29;
  --d-blade: #bfa46b; --n-blade: #454535; --d-blade-fg: #9c8753; --n-blade-fg: #3e4030;
  --d-leaf-1: #a86137; --n-leaf-1: #4c2d32; --d-leaf-2: #cd914c; --n-leaf-2: #704331;
  --d-leaf-3: #e3b86d; --n-leaf-3: #8c5b3e; --d-bush: #8d7148; --n-bush: #39362c;
  --d-roof: #634c62; --n-roof: #302536; --d-roof-2: #4c394e; --n-roof-2: #241c2b;
}
@supports (color: color-mix(in oklab, red 50%, blue)) {
  .footer.meadow {
    --mf-pumpkin: color-mix(in oklab, var(--d-pumpkin) var(--m-dp), var(--n-pumpkin));
    --mf-pumpkin-rib: color-mix(in oklab, var(--d-pumpkin-rib) var(--m-dp), var(--n-pumpkin-rib));
    --mf-pumpkin-face: color-mix(in oklab, var(--n-pumpkin-face) var(--m-lp), var(--d-pumpkin-face));
    --mf-pumpkin-stem: color-mix(in oklab, var(--d-pumpkin-stem) var(--m-dp), var(--n-pumpkin-stem));
    --mf-pumpkin-lit: color-mix(in oklab, var(--d-pumpkin-lit) var(--m-dp), var(--n-pumpkin-lit));
  }
}
.mf-halloween { display: none; pointer-events: none; }
html.season-halloween .mf-halloween { display: block; }
.mf-hw-pumpkin { fill: var(--mf-pumpkin); }
.mf-hw-rib { fill: none; stroke: var(--mf-pumpkin-rib); stroke-width: 1.3; }
.mf-hw-stem { fill: var(--mf-pumpkin-stem); }
.mf-hw-face { fill: var(--mf-pumpkin-face); }
.mf-hw-lit { fill: var(--mf-pumpkin-lit); opacity: var(--m-lit); }
.mf-hw-light { stop-color: var(--mf-pumpkin-lit); }
.mf-hw-pool { fill: url(#mf-hw-pool); opacity: var(--m-lit); }
.mf-hw-halo { fill: url(#mf-hw-halo); opacity: var(--m-lit); }
html.season-halloween .mf-flowers, html.season-halloween .mf-blossom { display: none; }
html.season-halloween .meadow-credit { color: #fff1d9; }

/* ============ SEASONAL EFFECTS ============ */
/* These rules intentionally sit AFTER the base palette block so the
   Christmas repaint (higher specificity) always wins while the season is live. */

/* ---- Birthday: party hat (revealed on body.season-birthday) ---- */
.party-hat {
  position: absolute; display: none;
  width: 74px; height: auto; top: -68px; left: 50%; margin-left: -30px;
  transform: rotate(14deg); transform-origin: bottom center;
  z-index: 3; pointer-events: none;
  filter: drop-shadow(0 4px 8px rgba(0,0,0,.35));
}
body.season-birthday .party-hat { display: block; }

/* ---- Birthday: the rest of the page joins the party ---- */
/* Same pink -> purple -> blue set as the hat + confetti, so the day reads as
   one theme instead of a lone hat on the default page. */
body.season-birthday .title {
  background: linear-gradient(100deg, #ff5ea8 0%, #a05cff 45%, #5cc6ff 100%);
  background-size: 200% auto; -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
body.season-birthday .stat-card--bday {
  background: linear-gradient(150deg, #ff5ea8 0%, #a05cff 100%);
  box-shadow: 0 16px 34px rgba(160,92,255,.35);
}
/* The birthday pop-up wears its tile's seasonal colours too (see script.js
   flyModal: the tile grows into the panel, so the two must match). */
body.season-birthday .modal-panel--bday {
  background: linear-gradient(150deg, #ff5ea8 0%, #a05cff 100%);
  box-shadow: 0 16px 34px rgba(160,92,255,.35);
}

/* ---- Birthday: balloon canvas (physics in lib.js, rendering in script.js) ---- */
#balloon-canvas { position: fixed; inset: 0; z-index: 0; pointer-events: none; }

/* ---- Birthday: confetti canvas (created/removed by JS) ---- */
#confetti-canvas { position: fixed; inset: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 70; }

/* ---- UK New Year: finite fireworks ---- */
#fireworks-canvas { position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 70; }
body.low-power #fireworks-canvas { display: none; }
@media (prefers-reduced-motion: reduce) { #fireworks-canvas { display: none; } }

/* ---- Pride (June): aro-ace halo around the profile picture ---- */
/* The spin lives on the season rule, not the base: with `animation` on the
   base it ran all year at opacity 0, and an infinite animation keeps the
   compositor producing a frame on every vsync (144 a second on a fast
   display) even though nothing is visible. Measured: ~0.3 CPU cores at idle
   for an invisible halo. Out of season the halo has no animation at all. */
@keyframes prideSpin { to { transform: rotate(360deg); } }
.pfp-ring::before {
  content: ""; position: absolute; inset: -18px; border-radius: 50%;
  z-index: -1; opacity: 0;
  background: conic-gradient(from 0deg,
    #E28C00 0deg, #ECCD00 72deg, #FFFFFF 144deg, #62AEDC 216deg, #203856 288deg, #E28C00 360deg);
  filter: blur(10px);
  -webkit-mask: radial-gradient(circle, #000 52%, rgba(0,0,0,0) 74%);
          mask: radial-gradient(circle, #000 52%, rgba(0,0,0,0) 74%);
  pointer-events: none;
}
/* steps(420) is the same uniform spin at 30 steps a second (0.86 degrees each,
   under the blur radius: indistinguishable), and it is a frame-rate limiter:
   the compositor skips drawing the frames where nothing changed, ~58% less GPU
   work than a smooth 144 Hz spin (measured on a bare page). Only valid
   because the rotation is linear: steps() would flatten an eased one. */
body.season-pride .pfp-ring::before { opacity: 1; animation: prideSpin 14s steps(420) infinite; }

/* ---- Christmas (Dec 25): dark festive palette override ---- */
body.season-christmas {
  --page-bg:
    radial-gradient(1000px 620px at 85% -10%, rgba(255,190,80,.10) 0%, rgba(255,190,80,0) 55%),
    radial-gradient(900px 620px at 4% 2%, rgba(158,27,50,.28) 0%, rgba(158,27,50,0) 55%),
    linear-gradient(165deg, #0c2e1f 0%, #0a3524 42%, #071f16 100%);
  --ink: #fff7e8; --ink-soft: #e8d3ae; --ink-faint: #b9a488;
  --accent: #ffd166;
  --card-bg: rgba(16,52,36,.66); --card-blur: blur(12px);
  --card-bd: 1px solid rgba(255,209,102,.38); --card-shadow: 0 14px 34px rgba(2,16,10,.45);
}

/* ---- Christmas: hero + cards follow the palette ---- */
/* The base card rules keep their brand colours the rest of the year (and are
   pinned by the glass-shadow gate tests); these scoped overrides repaint the
   lot into the pine/cranberry/gold set while the season is live. */
body.season-christmas .title {
  background: linear-gradient(100deg, #ffd166 0%, #fff3d6 45%, #ff8a8a 100%);
  background-size: 200% auto; -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
body.season-christmas .stat-card--bday {
  background: linear-gradient(150deg, #a4133c, #6d0f28);
  border: 1px solid rgba(255,209,102,.5);
  box-shadow: 0 16px 34px rgba(80,8,26,.5);
}
body.season-christmas .modal-panel--bday {
  background: linear-gradient(150deg, #a4133c, #6d0f28);
  border: 1px solid rgba(255,209,102,.5);
  box-shadow: 0 16px 34px rgba(80,8,26,.5);
}
body.season-christmas .discord-card,
body.season-christmas .steam-card,
body.season-christmas .mal-card {
  background: linear-gradient(150deg, #123c29 0%, #1b5a3a 60%, #123c29 100%);
  box-shadow: 0 16px 38px rgba(2,16,10,.5), var(--glint);
}
body.season-christmas .discord-card .card-head-title,
body.season-christmas .steam-card .card-head-title,
body.season-christmas .mal-card .card-head-title,
body.season-christmas .lb-card .card-head-title,
body.season-christmas .music-card .card-head-title { color: #ffd166 !important; }
body.season-christmas .discord-card .pill-link,
body.season-christmas .steam-card .pill-link,
body.season-christmas .mal-card .pill-link,
body.season-christmas .lb-card .pill-link {
  color: #ffe9b0 !important; background: rgba(255,209,102,.16) !important;
}
body.season-christmas .music-card .pill-link { color: #ffd166 !important; background: rgba(255,209,102,.16) !important; }
body.season-christmas .info-pill--game,
body.season-christmas .info-pill--spotify,
body.season-christmas .info-pill--steam-game {
  background: rgba(255,209,102,.1); border: 1px solid rgba(255,209,102,.3);
}
body.season-christmas .discord-card .presence-status,
body.season-christmas .discord-card .idle-banner,
body.season-christmas .steam-card .presence-status { color: #d9c9a8; }

/* ---- Halloween: cosy pumpkin/plum glass, with quiet illustrated scenery ---- */
/* The root class is set before first paint, and only when Christmas is not
   owning the palette. All artwork is static: no timers, rAF, or mobile costs. */
html.season-halloween body {
  --page-bg:
    radial-gradient(780px 580px at 78% 8%, rgba(225,151,85,calc(.13 * var(--bg-glow, 1))) 0%, transparent 65%),
    radial-gradient(900px 640px at 6% 84%, rgba(139,87,157,calc(.11 * var(--bg-glow, 1))) 0%, transparent 65%),
    linear-gradient(170deg, hsl(var(--bg-h) calc(var(--bg-s) * 1%) calc((var(--bg-l) + 5 * var(--bg-glow)) * 1%)) 0%, hsl(var(--bg-h) calc(var(--bg-s) * 1%) calc(var(--bg-l) * 1%)) 48%, hsl(calc(var(--bg-h) + 5) calc(var(--bg-s) * 1%) calc((var(--bg-l) + 2 * var(--bg-glow)) * 1%)) 100%);
  --ink: #fff2de; --ink-soft: #dfc6b9; --ink-faint: #c1aebf; --accent: #ffc078;
  --card-bg: rgba(61,40,66,.86);
  --card-bd: 1px solid rgba(223,159,103,.4);
  --card-shadow: 0 14px 34px rgba(24,12,28,.32);
  --hw-pumpkin: #dc8948; --hw-rib: #b76b39; --hw-face: #ffe2a0; --hw-stem: #82916b;
  --hw-bat: #332333;
}
html.season-halloween body:not(.season-birthday) .title {
  background: linear-gradient(105deg, #fff0d9 12%, #ffd09a 70%, #e2b6b3);
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
html.season-halloween .card:not(.stat-card--bday) {
  background: linear-gradient(145deg, rgba(108,68,105,.3), rgba(89,50,62,.16)), var(--card-bg);
  box-shadow: var(--card-shadow), var(--glint);
}
/* The existing stories have inline brand fills; only this seasonal repaint
   needs priority over those fills. Their layout/refraction is unchanged. */
html.season-halloween .story-card {
  background: linear-gradient(145deg, rgba(108,68,105,.3), rgba(89,50,62,.16)), var(--card-bg) !important;
  box-shadow: var(--card-shadow), var(--glint) !important;
}
html.season-halloween body:not(.season-birthday) .stat-card--bday {
  background: linear-gradient(145deg, #ad6038, #63384e); border-color: rgba(255,194,121,.45);
  box-shadow: 0 16px 34px rgba(62,26,32,.35);
}
/* Pop-ups match the tile they grew out of: the plum glass of the other stat
   tiles, and the pumpkin gradient for the birthday one. */
html.season-halloween .modal-panel {
  background: linear-gradient(145deg, rgba(108,68,105,.3), rgba(89,50,62,.16)), var(--card-bg);
}
html.season-halloween body:not(.season-birthday) .modal-panel--bday {
  background: linear-gradient(145deg, #ad6038, #63384e); border-color: rgba(255,194,121,.45);
  box-shadow: 0 16px 34px rgba(62,26,32,.35);
}
html.season-halloween .card-head-title,
html.season-halloween .story-title,
html.season-halloween .stat-label,
html.season-halloween .lb-label,
html.season-halloween .music-track-num,
html.season-halloween .music-kicker,
html.season-halloween .card .info-pill-label { color: var(--accent) !important; }
html.season-halloween .card .pill-link { color: var(--accent) !important; }
html.season-halloween .story-cta { color: #382137 !important; background: #ffd09a; }
html.season-halloween .card .pill-link { background: rgba(255,192,120,.12) !important; }
html.season-halloween .card .idle-banner,
html.season-halloween .card .loading-row { color: var(--ink-soft); }
html.season-halloween .copy-btn,
html.season-halloween .retry-btn,
html.season-halloween .dev-reset,
html.season-halloween .dev-seg,
html.season-halloween .dev-panel-close { border-color: rgba(223,159,103,.4); background: rgba(194,127,91,.12); }
html.season-halloween .dev-seg button.is-active { background: var(--accent); color: #382137; }
html.season-halloween .modal-panel { border-color: rgba(223,159,103,.4); }
html.season-halloween a:focus-visible,
html.season-halloween button:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
html.season-halloween .cscroll-thumb { background: linear-gradient(180deg, #ffe5ba, #dfa87d); }
html.season-halloween .music-viz span { background: linear-gradient(180deg, #ffe0a6, #d48760); }
html.season-halloween .music-card.scroll-reveal.is-visible:hover { box-shadow: 0 22px 46px rgba(149,84,51,.3), var(--glint); }
.halloween-decor { position: absolute; display: none; pointer-events: none; }
html.season-halloween .halloween-decor { display: block; }
.halloween-bats { width: min(560px, 100%); height: auto; top: 26px; left: 50%; transform: translateX(-50%); fill: var(--hw-bat); }
.halloween-pumpkins { width: 94px; height: auto; bottom: -12px; right: -35px; z-index: 3; filter: drop-shadow(0 4px 7px rgba(26,13,28,.3)); }
.hw-pumpkin { fill: var(--hw-pumpkin); }
.hw-rib { fill: none; stroke: var(--hw-rib); stroke-width: 1.3; }
.hw-face { fill: var(--hw-face); }
.hw-stem { fill: var(--hw-stem); }
@media (max-width: 480px) {
  .halloween-bats { width: 340px; top: 34px; }
  .halloween-pumpkins { width: 76px; right: -22px; bottom: -10px; }
}

/* ---- Christmas tree: background, desktop/tablet only (>=769px) ---- */
.xmas-scene { position: fixed; left: 0; bottom: 0; z-index: 0; pointer-events: none; display: none; }
.xmas-tree { display: block; width: clamp(170px,19vw,280px); height: auto; filter: drop-shadow(0 10px 22px rgba(0,0,0,.5)); }
.xmas-lights circle { animation: xmasTwinkle 2.6s ease-in-out infinite; }
.xmas-lights circle:nth-child(2n) { animation-delay: .9s; }
.xmas-lights circle:nth-child(3n) { animation-delay: 1.7s; }
@keyframes xmasTwinkle { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@media (min-width: 769px) { body.season-christmas .xmas-scene { display: block; } }
@media (prefers-reduced-motion: reduce) { .xmas-lights circle { animation: none; } }

/* ---- Respect reduced motion: keep the halo, stop the spin (confetti is skipped in JS) ---- */
@media (prefers-reduced-motion: reduce) { body.season-pride .pfp-ring::before { animation: none; } }

/* ============ DEV SETTINGS PANEL (secret code: kazudev) ============ */
/* Floating season-trigger overrides; the element is built and owned by
   script.js. Runs on the same glass tokens as the toast/modal, so it follows
   the active theme — including the Christmas palette the panel itself can
   force on. */
.dev-panel {
  position: fixed; left: 18px; bottom: 18px; z-index: 80;
  width: min(92vw, 304px); padding: 14px 16px 12px; border-radius: 18px;
  background: var(--card-bg); border: var(--card-bd);
  -webkit-backdrop-filter: var(--card-blur); backdrop-filter: var(--card-blur);
  box-shadow: 0 18px 44px rgba(3,10,24,.45); color: var(--ink);
  font-size: 13px;
}
.dev-panel[hidden] { display: none; }
.dev-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 2px; }
.dev-panel-title { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 16px; }
.dev-panel-close {
  width: 26px; height: 26px; border: none; cursor: pointer; border-radius: 50%;
  background: rgba(140,170,210,.18); color: var(--ink); font-size: 13px; line-height: 1;
  display: flex; align-items: center; justify-content: center; transition: background .2s ease;
}
.dev-panel-close:hover { background: rgba(140,170,210,.34); }
.dev-panel-sub { font-size: 11.5px; font-weight: 600; color: var(--ink-soft); margin-bottom: 10px; }
.dev-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 7px 0; border-top: 1px solid rgba(140,170,210,.16); }
.dev-row-label { font-weight: 700; white-space: nowrap; }
.dev-live {
  display: inline-block; margin-left: 4px; padding: 1px 7px; border-radius: 999px;
  background: #3ddc97; color: #06281a; font-size: 9.5px; font-weight: 800;
  letter-spacing: .06em; text-transform: uppercase; vertical-align: 2px;
}
.dev-live[hidden] { display: none; }
.dev-seg { display: flex; flex: none; border-radius: 999px; overflow: hidden; border: 1px solid rgba(140,170,210,.3); }
.dev-seg button {
  border: none; cursor: pointer; padding: 4px 9px; font-size: 11px; font-weight: 700;
  background: transparent; color: var(--ink-soft); transition: background .15s ease, color .15s ease;
}
.dev-seg button + button { border-left: 1px solid rgba(140,170,210,.3); }
.dev-seg button.is-active { background: var(--accent); color: #0d1b31; }
.dev-reset {
  margin-top: 10px; width: 100%; padding: 7px 10px; border-radius: 10px; cursor: pointer;
  border: 1px solid rgba(140,170,210,.3); background: rgba(140,170,210,.14);
  color: var(--ink); font-size: 12px; font-weight: 700; transition: background .2s ease;
}
.dev-reset:hover { background: rgba(140,170,210,.28); }
.dev-hint { margin-top: 9px; font-size: 10.5px; font-weight: 600; color: var(--ink-faint); text-align: center; }
.dev-hint kbd {
  padding: 1px 5px; border-radius: 5px; border: 1px solid rgba(140,170,210,.35);
  background: rgba(140,170,210,.14); font-family: inherit; font-size: 10px;
}

/* ============ PAGE-LOAD ENTRANCE ANIMATION ============ */
/* A staggered "settle into place" cascade that plays on every load / refresh.
   Pure CSS (animation-fill-mode: both) so content is hidden before first paint —
   no flash — and always resolves to the visible end state even if JS fails.
   The whole thing is gated off under prefers-reduced-motion at the bottom. */

@keyframes revealUp {
  from { opacity: 0; transform: translateY(30px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes revealPop {
  0%   { opacity: 0; transform: translateY(6px) scale(.7); }
  55%  { opacity: 1; }
  78%  { transform: translateY(0) scale(1.05); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes titleReveal {
  from { opacity: 0; transform: translateY(26px); filter: blur(12px); }
  to   { opacity: 1; transform: translateY(0);    filter: blur(0); }
}
/* Ambient petal layer and scenery stay steady from first paint */

/* Hero accents */
.hero .pfp { animation: revealPop .85s ease-out .14s both; }
/* (.title carries titleReveal in the .title rule above) */

/* The cascading blocks (above-the-fold hero stats only — everything further
   down the page is revealed on scroll instead, see SCROLL REVEAL below) */
.stat-card {
  animation: revealUp .75s cubic-bezier(.21,.6,.35,1) both;
}

/* Stagger, flowing down the page */
.stat-card:nth-child(1) { animation-delay: .34s; }
.stat-card:nth-child(2) { animation-delay: .40s; }
.stat-card:nth-child(3) { animation-delay: .46s; }
.stat-card:nth-child(4) { animation-delay: .52s; }

/* Reduced motion: skip the whole entrance — everything is simply present. */
@media (prefers-reduced-motion: reduce) {
  .atmosphere, .sakura-scene, .hero .pfp, .title, .stat-card {
    animation: none !important;
  }
}

/* ============ SCROLL REVEAL ============ */
/* Everything marked .scroll-reveal starts hidden and fades + slides into
   place the first time it scrolls into view: script.js watches the set with
   an IntersectionObserver and adds .is-visible (one-shot, with a small
   per-batch stagger so grids ripple in together). Elements already on screen
   on load intersect immediately, so a refresh still waves the top of the page
   in (the petal layer joins in with its own ambientFade, defined above).
   Skipped entirely under prefers-reduced-motion. */
.scroll-reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity .7s cubic-bezier(.21,.6,.35,1), transform .7s cubic-bezier(.21,.6,.35,1);
}
.scroll-reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* Cards used to stagger their entrance via transition-delay, but that delay lives
   on the whole element and also delayed the hover transform on mouse-leave (the
   icon's hover transition is on a separate child with no delay), so later cards
   like Letterboxd visibly lagged behind their icon while X (nth-child 1, 0s) looked
   in sync. Removed entirely so every card's hover motion is always delay-free. */
.story-link.scroll-reveal:nth-child(1)  { transition-delay: 0s; }
.story-link.scroll-reveal:nth-child(2)  { transition-delay: .08s; }

/* .social-card also carries .scroll-reveal — restate its own transition here
   (higher specificity: two classes) so the fade-in transition above doesn't
   clobber the snappier hover transform/box-shadow transition set on .social-card. */
.social-card.scroll-reveal {
  transition: opacity .7s cubic-bezier(.21,.6,.35,1), transform .22s cubic-bezier(.25,.8,.35,1), box-shadow .22s ease, border-color .3s ease;
}
/* .scroll-reveal.is-visible pins transform: translateY(0) at equal specificity to
   .social-card:hover and wins on source order, silently killing the hover lift
   once a card has revealed. Restate the hover transform here at higher specificity. */
.social-card.scroll-reveal.is-visible:hover {
  transform: translateY(-3px);
}
/* Same trap on the down-stroke: restate :active after :hover (equal specificity,
   source order decides) so pressing a revealed card still sinks instead of
   holding the hover lift, and keep the snappy press duration the plain
   .social-card:active rule would lose to the restated transition above. */
.social-card.scroll-reveal.is-visible:active {
  transform: translateY(1px);
  transition-duration: .07s;
}

@media (prefers-reduced-motion: reduce) {
  .scroll-reveal {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

/* ============ POPULATE ANIMATION ============ */
/* When a live card's data arrives, the "Connecting to…" row swaps out and
   the content pops in: a quick fade + rise on the content block, while
   script.js's popReveal() glides the card's height from the loading height
   to the content height in the same beat. The class is added by popReveal
   only on the hidden → visible transition, and removing display:none
   restarts the keyframes, so retries after an error animate again.
   Reduced-motion and low-power keep the old instant swap (popReveal skips
   both halves there; these rules are the belt to its braces). */
@keyframes populateIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}
.populated-in {
  animation: populateIn .38s cubic-bezier(.22,.9,.3,1) both;
}
@media (prefers-reduced-motion: reduce) {
  .populated-in { animation: none; }
}
body.low-power .populated-in { animation: none; }

/* ============ OFF-SCREEN POWER SAVE ============ */
/* script.js tags fx layers with .fx-paused when they scroll out of the
   viewport (IntersectionObserver, 100px early-wake margin). Freezing
   animation-play-state stops the compositor work for petals, rain, aurora,
   branch sway and the tree lights while nothing
   can see them; every animation resumes mid-cycle when scrolled back. */
.fx-paused, .fx-paused * { animation-play-state: paused !important; }

/* ============ LIQUID GLASS ============ */
/* An Apple-style "liquid glass" treatment, applied site-wide: the live-stat
   tiles, the social tiles, the copy toast, the "Right now" presence cards
   (Discord, Steam, MyAnimeList, Letterboxd), the music card and the story
   cards. Two parts —
     1. always-on (every browser): a translucent brand-tinted fill over a dark
        base so the page faintly shows through, plus a specular "glint" that
        defines the edge with LIGHT on glass (a crisp top light-catch, a soft
        top-left sheen, lower-right depth) — deliberately NOT a flat coloured
        border, which is what made the old edges look drawn-on.
     2. Chromium-only: an SVG feDisplacementMap wired up in script.js that refracts
        the page content at the rounded rim (set as an inline backdrop-filter that
        supersedes the blur below). Where that can't run, this base look remains.
   The fills stay dark enough that the forced-white card text keeps its
   contrast in both themes; only the birthday tile keeps its solid brand
   gradient. Placed after the .card / .toast rules so it wins on source order
   at equal specificity. */


/* Default: EVERY .card is liquid glass — the birthday tile is the one opt-out
   (it keeps its solid brand gradient). This is what makes the effect
   automatic for future sections: a new card with class="card your-card" gets
   the glint rim, frosted backdrop and glint shadow with zero extra CSS; add
   a translucent fill override further down only if it needs its own brand
   hue. Placed first in this section so every per-card rule below still wins.
   (.card keeps its themed translucent base as the default fill — a background
   here would clobber the music card's red tint via the :not() specificity.) */
.card:not(.stat-card--bday) {
  --glint:
    inset 0 1.5px 0.5px rgba(255, 255, 255, .55),   /* crisp top-edge light-catch */
    inset 2px 3px 4px rgba(255, 255, 255, .18),     /* soft top-left sheen */
    inset -2px -3px 8px rgba(0, 0, 0, .24);         /* lower-right depth */
  border: none;
  -webkit-backdrop-filter: blur(6px) saturate(1.7);
          backdrop-filter: blur(6px) saturate(1.7);
}
/* The glint shadow goes on plain .card (specificity 0-1-0, before the per-card
   shadows below) so brand shadows still win; the birthday tile is re-pinned
   right after since it would otherwise pick the glint up. */
.card { box-shadow: var(--card-shadow), var(--glint); }
.stat-card--bday { box-shadow: 0 16px 34px rgba(80,110,230,.3); }
.stat-card:not(.stat-card--bday),
.toast {
  --glint:
    inset 0 1.5px 0.5px rgba(255, 255, 255, .55),   /* crisp top-edge light-catch */
    inset 2px 3px 4px rgba(255, 255, 255, .18),     /* soft top-left sheen */
    inset -2px -3px 8px rgba(0, 0, 0, .24);         /* lower-right depth */
  /* fade the themed fill back so the refraction/blur behind actually reads as glass.
     Falls back to the solid var(--card-bg) where color-mix is unsupported. */
  background: color-mix(in srgb, var(--card-bg) 50%, transparent);
  border: none;
  -webkit-backdrop-filter: blur(6px) saturate(1.7);
          backdrop-filter: blur(6px) saturate(1.7);
}
.stat-card:not(.stat-card--bday) {
  /* sky -> violet brand tint (the same 150deg #0ea5e9 -> #7c5cff as the title +
     birthday tile), kept translucent over a fainter themed base so it stays glass
     and still adapts to light/dark. The base is the final background-color layer. */
  background:
    linear-gradient(150deg, rgba(14, 165, 233, .22), rgba(124, 92, 255, .22)),
    color-mix(in srgb, var(--card-bg) 40%, transparent);
  box-shadow: var(--card-shadow), var(--glint);
}
.toast {
  box-shadow: 0 14px 34px rgba(3, 10, 24, .3), var(--glint);
}

/* ---- The "Right now", music + story cards join the glass set ---- */
/* --glint first, so every card below (and the music card's hover glow) can
   reference it. */
.discord-card,
.steam-card,
.mal-card,
.lb-card,
.music-card,
.story-card {
  --glint:
    inset 0 1.5px 0.5px rgba(255, 255, 255, .55),   /* crisp top-edge light-catch */
    inset 2px 3px 4px rgba(255, 255, 255, .18),     /* soft top-left sheen */
    inset -2px -3px 8px rgba(0, 0, 0, .24);         /* lower-right depth */
}
/* The presence + story cards get the frosted backdrop. (The music card now
   carries its own backdrop-filter up in its base rule, so the page behind it
   blurs through the translucent cover art too.) */
.discord-card,
.steam-card,
.mal-card,
.lb-card,
.story-card {
  border: none;
  -webkit-backdrop-filter: blur(6px) saturate(1.7);
          backdrop-filter: blur(6px) saturate(1.7);
}
/* Brand hues as translucent fills over a mostly-dark base: the page leaks
   only ~10% through, so the cards keep their colour identity and the white
   text its contrast in both themes, while the petals/refraction still read at
   the rim. Where color-mix is unsupported the whole declaration is dropped
   and the original opaque gradient (further up) remains. */
.discord-card {
  background:
    linear-gradient(150deg, rgba(30,33,71,.66) 0%, rgba(64,78,224,.55) 60%, rgba(30,33,71,.66) 100%),
    color-mix(in srgb, #101c33 68%, transparent);
  box-shadow: 0 16px 38px rgba(40,40,110,.32), var(--glint);
}
.steam-card {
  background:
    linear-gradient(150deg, rgba(27,40,56,.68) 0%, rgba(42,71,94,.58) 60%, rgba(27,40,56,.68) 100%),
    color-mix(in srgb, #101c33 68%, transparent);
  box-shadow: 0 16px 38px rgba(20,40,70,.32), var(--glint);
}
.mal-card {
  background:
    linear-gradient(150deg, rgba(22,33,62,.66) 0%, rgba(46,81,162,.55) 60%, rgba(22,33,62,.66) 100%),
    color-mix(in srgb, #101c33 68%, transparent);
  box-shadow: 0 16px 38px rgba(20,40,100,.32), var(--glint);
}
.lb-card {
  background:
    linear-gradient(150deg, rgba(20,24,28,.66) 0%, rgba(44,52,60,.58) 60%, rgba(20,24,28,.66) 100%),
    color-mix(in srgb, #101c33 68%, transparent);
  box-shadow: 0 16px 38px rgba(10,16,24,.4), var(--glint);
}
/* The music card keeps its glass glint + drop shadow; its translucent fill
   and frosted backdrop live in the base .music-card rule further up. (The
   story cards carry their translucent gradients inline in index.html — inline
   styles win over anything written here.) */
.music-card { box-shadow: 0 16px 38px rgba(255,26,26,.16), var(--glint); }
.music-card::after {
  background: linear-gradient(150deg, rgba(20,8,10,.4), rgba(10,6,14,.55));
}

/* ============ CARD DETAIL MODALS ============ */
/* Each .stat-card opens a glass pop-up (markup in index.html, wired in
   script.js). The panel reuses the same brand-tinted glass + glint as the
   stat/social tiles so it sits in the existing visual language. */

/* clickable affordance on the four tiles */
.stat-card[data-modal] {
  cursor: pointer;
  transition: transform .25s cubic-bezier(.34,1.45,.55,1), box-shadow .25s ease;
}
.stat-card[data-modal]:hover { transform: translateY(-3px); }
.stat-card[data-modal]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.stat-card[data-modal]::after {
  content: "\2922"; /* ⤢ expand */
  position: absolute; right: 12px; bottom: 9px; z-index: 2;
  font-size: 14px; line-height: 1; color: var(--ink-soft);
  opacity: 0; transition: opacity .25s ease; pointer-events: none;
}
.stat-card[data-modal]:hover::after,
.stat-card[data-modal]:focus-visible::after { opacity: .65; }
.stat-card--bday[data-modal]::after { color: rgba(255,255,255,.9); }
@media (pointer: coarse), (max-width: 768px) {
  .stat-card[data-modal]::after { opacity: .45; }
}

body.modal-open { overflow: hidden; }

/* The dim + blur backdrop lives on ::before so only IT fades; the panel itself
   is moved by script.js (openModal/closeModal), which flies it from the card
   that was clicked to its resting place and back again (FLIP, Web Animations). */
.modal-overlay {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.modal-overlay::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: rgba(4,10,22,.55);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  opacity: 0; transition: opacity .34s ease;
}
.modal-overlay.is-open::before { opacity: 1; }
.modal-panel.is-flying { -webkit-backdrop-filter: none; backdrop-filter: none; }
/* Phones: a plain dim instead of a live blur behind the moving panel */
@media (pointer: coarse), (max-width: 768px) {
  .modal-overlay::before { background: rgba(4,10,22,.68); -webkit-backdrop-filter: none; backdrop-filter: none; }
}
.modal-overlay[hidden] { display: none; }

.modal-panel {
  position: relative; width: 100%; max-width: 560px; max-height: 88vh; overflow-y: auto;
  border-radius: 26px; padding: 26px 24px 28px; color: var(--ink);
  background:
    linear-gradient(150deg, rgba(14,165,233,.18), rgba(124,92,255,.18)),
    var(--card-bg);
  -webkit-backdrop-filter: blur(22px) saturate(1.6); backdrop-filter: blur(22px) saturate(1.6);
  /* identical edge light + drop shadow to the stat tiles, so the tile and the
     panel it grows into look like one object at both sizes */
  --glint:
    inset 0 1.5px 0.5px rgba(255, 255, 255, .55),
    inset 2px 3px 4px rgba(255, 255, 255, .18),
    inset -2px -3px 8px rgba(0, 0, 0, .24);
  box-shadow: var(--card-shadow), var(--glint);
  transform-origin: 0 0; will-change: transform;
}
.modal-panel--wide { max-width: 760px; }
/* focused only to anchor the focus-trap — no visible ring on the container itself */
.modal-panel:focus { outline: none; }

.modal-close {
  position: absolute; top: 14px; right: 14px; width: 38px; height: 38px;
  border: none; cursor: pointer; border-radius: 50%;
  background: rgba(140,170,210,.18); color: var(--ink);
  font-size: 24px; line-height: 1; display: flex; align-items: center; justify-content: center;
  transition: background .2s ease, transform .2s ease;
}
.modal-close:hover { background: rgba(140,170,210,.34); transform: rotate(90deg) scale(1.06); }

.modal-title {
  font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 23px;
  margin: 2px 44px 18px 2px; color: var(--ink);
  display: flex; align-items: center; gap: 9px;
}
.modal-body { font-size: 15px; }
.modal-lead { font-size: 14px; font-weight: 600; color: var(--ink-soft); line-height: 1.5; margin-bottom: 10px; }
.modal-lead-src { font-size: 12px; color: var(--ink-faint); }
.modal-credit { font-size: 12px; font-weight: 600; color: var(--ink-faint); margin-top: 10px; }
.modal-credit--center { text-align: center; }

.modal-note {
  margin-top: 12px; background: rgba(140,170,210,.12); border-radius: 16px;
  padding: 12px 16px; font-size: 14px; font-weight: 600; color: var(--ink-soft); line-height: 1.5;
}
.modal-note--center { text-align: center; color: var(--ink); }
.modal-note strong { color: var(--ink); font-weight: 800; }
.modal-note-hr { height: 1px; background: rgba(140,170,210,.22); margin: 10px 0; }
.modal-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.modal-row-sub { color: var(--ink-faint); font-weight: 700; }
.modal-row.modal-row-sub { margin-top: 6px; font-size: 13px; }

.modal-actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
.modal-actions--center { justify-content: center; }
.modal-btn {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer; border: none;
  text-decoration: none; font-family: inherit; font-size: 14px; font-weight: 800;
  padding: 11px 18px; border-radius: 999px; color: #fff;
  background: linear-gradient(150deg,#0ea5e9,#7c5cff);
  box-shadow: 0 8px 20px rgba(80,110,230,.35);
  transition: transform .25s cubic-bezier(.34,1.5,.6,1), box-shadow .25s ease, filter .25s ease, background .2s ease;
}
.modal-btn:hover { transform: translateY(-2px) scale(1.04); filter: brightness(1.08); box-shadow: 0 12px 26px rgba(80,110,230,.5); }
.modal-btn:active { transform: scale(.97); }
.modal-btn--ghost { background: rgba(140,170,210,.16); color: var(--ink); box-shadow: none; }
.modal-btn--ghost:hover { background: rgba(140,170,210,.3); filter: none; }

/* Time modal */
.tz-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.tz-cell { background: rgba(140,170,210,.12); border-radius: 16px; padding: 13px 16px; }
.tz-label { font-size: 11px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; color: var(--accent); }
.tz-time { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 28px; line-height: 1.05; margin-top: 6px; font-variant-numeric: tabular-nums; }
.tz-meta { font-size: 12px; font-weight: 700; color: var(--ink-soft); margin-top: 4px; }

/* Weather globe — clamp keeps the whole pop-up scroll-free on laptop-height screens */
.globe-frame {
  position: relative; width: 100%; height: clamp(200px, 34vh, 340px);
  border-radius: 18px; overflow: hidden; background: #05070d;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
}
.globe-frame iframe { width: 100%; height: 100%; border: 0; display: block; }

/* 5-day forecast strip (weather modal) */
.forecast-strip { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin-top: 12px; }
.forecast-day {
  background: rgba(140,170,210,.12); border-radius: 14px; padding: 9px 6px;
  text-align: center; box-shadow: inset 0 0 0 1px rgba(255,255,255,.06);
}
.forecast-label { font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--accent); }
.forecast-condition { font-size: 12px; line-height: 1.35; min-height: 2.7em; margin: 8px 0; }
.forecast-emoji { font-size: 24px; line-height: 1.2; margin-top: 5px; }
.forecast-temps { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 14px; margin-top: 4px; font-variant-numeric: tabular-nums; color: var(--ink); }
.forecast-min { color: var(--ink-soft); font-weight: 500; }
.forecast-precip { font-size: 11px; font-weight: 700; color: #7db8f0; margin-top: 3px; font-variant-numeric: tabular-nums; }
.forecast-empty { grid-column: 1 / -1; text-align: center; font-size: 13px; font-weight: 700; color: var(--ink-soft); padding: 10px 0; }
@media (max-width: 520px) {
  .forecast-strip { gap: 6px; }
  .forecast-day { padding: 8px 4px; }
  .forecast-emoji { font-size: 20px; }
  .forecast-temps { font-size: 13px; }
}

/* Age modal */
.age-head { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: clamp(21px,5vw,28px); color: var(--ink); margin-bottom: 12px; }
.age-head .age-sub { font-family: 'Nunito', sans-serif; font-weight: 700; font-size: 16px; color: var(--ink-soft); }
/* minmax(min(200px,100%),1fr): the heartbeats count is 12 chars at 23px (~160px),
   so 4-across 118px tiles clipped it against the panel edge. 200px minimum wraps
   the row to 2x2 in the 512px modal and 1-up on phones. */
.stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(200px,100%),1fr)); gap: 10px; }
.stat-tile { background: rgba(140,170,210,.12); border-radius: 16px; padding: 12px 16px; }
.stat-tile-num { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 23px; line-height: 1; font-variant-numeric: tabular-nums; color: var(--ink); }
.stat-tile-label { font-size: 12px; font-weight: 700; color: var(--ink-soft); margin-top: 6px; }
.sign-row { display: flex; align-items: center; gap: 14px; margin-top: 12px; background: rgba(140,170,210,.12); border-radius: 16px; padding: 12px 16px; }
.sign-glyph { font-size: 34px; line-height: 1; flex: none; }
.sign-name { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 20px; color: var(--ink); line-height: 1.1; }
/* Fun units (age modal): playful equivalents from ageBreakdown — replaced the
   old life-in-weeks canvas so the pop-up fits without scrolling. */
.fun-wrap { margin-top: 14px; }
.fun-title { font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--accent); }
.fun-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(200px,100%),1fr)); gap: 10px; margin-top: 9px; }
.fun-cell { display: flex; align-items: center; gap: 12px; background: rgba(140,170,210,.12); border-radius: 14px; padding: 10px 14px; }
.fun-emoji { font-size: 26px; line-height: 1; flex: none; }
.fun-num { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 18px; line-height: 1.05; font-variant-numeric: tabular-nums; color: var(--ink); }
.fun-label { font-size: 11px; font-weight: 700; color: var(--ink-soft); margin-top: 2px; }

/* Age modal layout: wider panel, tiles always 2-up, milestone + star sign side
   by side and fun units 4-up on desktop; everything stays 2-up and compact on
   phones so the whole pop-up fits one screen with no inner scroll. */
.modal-panel--age { max-width: 680px; }
.modal-panel--age .stat-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.modal-panel--age .fun-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.age-meta { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 10px; }
.age-meta .modal-note, .age-meta .sign-row { margin-top: 0; }
.age-meta .sign-row { align-items: center; }
@media (max-width: 640px) {
  .modal-panel--age .fun-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .age-meta { grid-template-columns: 1fr; gap: 8px; margin-top: 8px; }
  .modal-panel--age { padding: 16px 12px 16px; }
  .modal-panel--age .modal-title { margin-bottom: 8px; }
  .modal-panel--age .age-head { font-size: clamp(18px, 5.6vw, 22px); margin-bottom: 8px; }
  .modal-panel--age .age-head .age-sub { font-size: 14px; }
  .modal-panel--age .stat-tiles { gap: 8px; }
  .modal-panel--age .stat-tile { padding: 9px 11px; border-radius: 13px; }
  .modal-panel--age .stat-tile-num { font-size: clamp(15px, 4.9vw, 19px); }
  .modal-panel--age .stat-tile-label { font-size: 11px; margin-top: 4px; }
  .modal-panel--age .modal-note { padding: 9px 12px; border-radius: 13px; font-size: 13px; line-height: 1.35; }
  .modal-panel--age .modal-row.modal-row-sub { margin-top: 3px; font-size: 12px; }
  .modal-panel--age .sign-row { padding: 9px 12px; border-radius: 13px; }
  .modal-panel--age .sign-name { font-size: 17px; }
  .modal-panel--age .fun-wrap { margin-top: 10px; }
  .modal-panel--age .fun-grid { margin-top: 6px; gap: 8px; }
  .modal-panel--age .fun-cell { padding: 8px 11px; border-radius: 13px; }
  .modal-panel--age .fun-num { font-size: 16px; }
  .modal-panel--age .fun-label { font-size: 11px; }
}

/* Birthday countdown */
.countdown { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 12px; }
.cd-cell { background: rgba(140,170,210,.14); border-radius: 16px; padding: 12px 6px; text-align: center; }
.cd-num { font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: clamp(24px,7vw,32px); line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }
.cd-label { font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; color: var(--ink-soft); margin-top: 6px; }

@media (max-width: 520px) {
  .modal-panel { padding: 22px 16px 24px; border-radius: 22px; }
  .tz-grid { grid-template-columns: 1fr; }
  .modal-actions { flex-direction: column; }
  .modal-btn { justify-content: center; }
}

@media (prefers-reduced-motion: reduce) {
  .modal-overlay::before { transition: opacity .2s ease; }
  .modal-close:hover { transform: none; }
  .stat-card[data-modal] { transition: none; }
  .stat-card[data-modal]:hover { transform: none; }
  .modal-btn:hover { transform: none; }
}

/* ---- Konami dragon easter egg ----
   Fixed-position, pointer-events none, created on the fly by script.js.
   The  glyph faces left, so every keyframe keeps scaleX(-1) to fly right. */
.konami-dragon {
  position: fixed; top: 18%; left: 0; z-index: 200; font-size: 64px; line-height: 1;
  pointer-events: none; user-select: none;
  filter: drop-shadow(0 10px 20px rgba(3,10,24,.5));
  animation: konamiFly 3.2s cubic-bezier(.3,.55,.45,1) forwards;
}
@keyframes konamiFly {
  0%   { transform: translate(-14vw, 0)   scaleX(-1) rotate(-4deg); }
  25%  { transform: translate(21vw, -7vh) scaleX(-1) rotate(5deg); }
  50%  { transform: translate(53vw, 3vh)  scaleX(-1) rotate(-5deg); }
  75%  { transform: translate(81vw, -6vh) scaleX(-1) rotate(4deg); }
  100% { transform: translate(116vw, 0)   scaleX(-1) rotate(0deg); }
}
.konami-flake { width: 8px; height: 8px; border-radius: 50%; background: #d9ebff; }
.konami-flake {
  position: fixed; top: -5%; z-index: 199; pointer-events: none; user-select: none;
  animation: konamiFlakeFall linear forwards;
}
@keyframes konamiFlakeFall {
  to { transform: translateY(115vh) rotate(320deg); opacity: .15; }
}
@media (prefers-reduced-motion: reduce) {
  .konami-dragon, .konami-flake { display: none; }
}

/* ============ AURORA ATMOSPHERE (clear nights) ============ */
/* Injected by script.js setAtmosphere('aurora') when Open-Meteo says the sky
   is clear or mainly clear after dark (?atmosphere=aurora previews it). Two
   blurred gradient ribbons drift slowly over a static starfield. All motion
   is CSS; there is no per-frame JS. */
.aurora { position: absolute; inset: 0; overflow: hidden; }
.aurora-stars {
  position: absolute; left: 0; right: 0; top: 0; height: 55%;
  background-image:
    radial-gradient(1.6px 1.6px at 8% 30%, rgba(255,255,255,.95), rgba(255,255,255,0) 70%),
    radial-gradient(1.2px 1.2px at 18% 12%, rgba(255,255,255,.75), rgba(255,255,255,0) 70%),
    radial-gradient(1.8px 1.8px at 27% 44%, rgba(255,255,255,.9), rgba(255,255,255,0) 70%),
    radial-gradient(1.1px 1.1px at 36% 20%, rgba(255,255,255,.7), rgba(255,255,255,0) 70%),
    radial-gradient(1.5px 1.5px at 47% 8%, rgba(255,255,255,.85), rgba(255,255,255,0) 70%),
    radial-gradient(1.2px 1.2px at 58% 36%, rgba(255,255,255,.7), rgba(255,255,255,0) 70%),
    radial-gradient(1.7px 1.7px at 67% 14%, rgba(255,255,255,.9), rgba(255,255,255,0) 70%),
    radial-gradient(1.1px 1.1px at 76% 42%, rgba(255,255,255,.7), rgba(255,255,255,0) 70%),
    radial-gradient(1.5px 1.5px at 85% 22%, rgba(255,255,255,.85), rgba(255,255,255,0) 70%),
    radial-gradient(1.3px 1.3px at 93% 38%, rgba(255,255,255,.75), rgba(255,255,255,0) 70%),
    radial-gradient(1.1px 1.1px at 14% 58%, rgba(255,255,255,.65), rgba(255,255,255,0) 70%),
    radial-gradient(1.4px 1.4px at 52% 55%, rgba(255,255,255,.7), rgba(255,255,255,0) 70%),
    radial-gradient(1.1px 1.1px at 71% 62%, rgba(255,255,255,.6), rgba(255,255,255,0) 70%),
    radial-gradient(1.3px 1.3px at 89% 58%, rgba(255,255,255,.65), rgba(255,255,255,0) 70%);
}
.aurora-ribbon {
  position: absolute; left: -25%; width: 150%; border-radius: 50%;
  /* The blur that softened these bands used to force a full re-raster of two
     ~150%-wide elements every animation frame (and kept waking the glass
     cards' backdrop-filter). The softness is baked in instead: the gradient
     stops fade the long edges and this mask fades the short ones, so the
     drift animation is pure compositor transform work. */
  pointer-events: none; will-change: transform;
  -webkit-mask: linear-gradient(to bottom, rgba(0,0,0,0), #000 35%, #000 65%, rgba(0,0,0,0));
          mask: linear-gradient(to bottom, rgba(0,0,0,0), #000 35%, #000 65%, rgba(0,0,0,0));
}
.aurora-ribbon--a {
  top: 3vh; height: 36vh; opacity: .55; transform: rotate(-8deg);
  background: linear-gradient(100deg,
    rgba(62,220,160,0) 0%, rgba(62,220,160,.55) 28%, rgba(92,198,255,.45) 52%,
    rgba(160,92,255,.35) 74%, rgba(62,220,160,0) 100%);
  animation: auroraDriftA 26s ease-in-out infinite alternate;
}
.aurora-ribbon--b {
  top: 13vh; height: 28vh; opacity: .38; transform: rotate(-4deg);
  background: linear-gradient(95deg,
    rgba(92,198,255,0) 0%, rgba(92,198,255,.4) 32%, rgba(62,220,160,.5) 62%,
    rgba(92,198,255,0) 100%);
  animation: auroraDriftB 32s ease-in-out infinite alternate;
}
@keyframes auroraDriftA {
  from { transform: rotate(-11deg) translateX(-6%) skewX(-8deg); }
  to   { transform: rotate(-4deg)  translateX(6%)  skewX(6deg); }
}
@keyframes auroraDriftB {
  from { transform: rotate(-3deg)  translateX(5%)  skewX(5deg); }
  to   { transform: rotate(-10deg) translateX(-5%) skewX(-6deg); }
}
@media (prefers-reduced-motion: reduce) {
  .aurora-ribbon { animation: none; }
}

/* ============ LETTERBOXD CARD ============ */
/* The .lb-card base rule lives up with the other card bases (next to
   .mal-card). It must come BEFORE the liquid-glass section: down here it
   overrode the translucent fill, glint shadow and frosted backdrop on source
   order, which is why the LB card used to render solid instead of glass. */
.lb-bg-icon { position: absolute; right: -20px; bottom: -26px; opacity: .12; }
.lb-card .pill-link { color: #fff !important; background: rgba(255,255,255,.16) !important; }
.lb-card .loading-row { color: #b8c4cf; }
.lb-card .idle-banner { background: rgba(255,255,255,.12); color: #b8c4cf; }
.lb-entry {
  display: flex; align-items: center; gap: 14px; text-decoration: none;
  background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.22);
  border-radius: 14px; padding: 12px 14px;
  transition: transform .25s ease, background .25s ease;
}
.lb-entry:hover { transform: translateY(-2px); background: rgba(255,255,255,.17); }
.lb-entry:active { transform: translateY(0) scale(.98); }
.lb-poster {
  width: 52px; height: 74px; border-radius: 8px; object-fit: cover; flex: none;
  background: rgba(255,128,0,.15); box-shadow: 0 4px 12px rgba(0,0,0,.35);
}
.lb-poster--empty {
  display: flex; align-items: center; justify-content: center; font-size: 24px;
}
.lb-info { min-width: 0; flex: 1; }
.lb-label {
  font-size: 11px; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; color: #00e054;
}
.lb-title {
  font-family: 'Fredoka', sans-serif; font-weight: 600; font-size: 17px;
  color: #fff; line-height: 1.2; margin-top: 3px;
}
.lb-year { color: #b8c4cf; font-weight: 500; font-size: 14px; }
.lb-stars { color: #ff8000; font-size: 15px; letter-spacing: .08em; margin-top: 3px; }
.lb-watched { font-size: 12px; font-weight: 700; color: #b8c4cf; margin-top: 3px; }
@media (prefers-reduced-motion: reduce) {
  .lb-entry { transition: none; }
  .lb-entry:hover, .lb-entry:active { transform: none; }
}

/* ---- MAL card: "also reading" section label ---- */
.mal-section-label {
  font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: #c7d4f7; margin-top: 16px; padding-top: 12px;
  border-top: 1px solid rgba(255,255,255,.14);
}

/* ---- Music card: ListenBrainz recent tracks ---- */
a.music-track-row { text-decoration: none; color: inherit; border-radius: 10px; }
a.music-track-row:hover { background: rgba(255,255,255,.08); }

/* ---- Weather modal: "compare with your sky" output ---- */
.sky-compare { margin-top: 16px; }

/* ============ CUSTOM SCROLLBAR ============ */
/* JS-built page scroller (see the custom-scrollbar IIFE in script.js). The
   native bar is hidden only once script.js sets html.has-cscroll — which it
   does solely on fine-pointer devices — so touch and no-JS keep the real
   thing. Geometry lives in lib.js; this block is purely cosmetic. */
html.has-cscroll { scrollbar-width: none; }
html.has-cscroll::-webkit-scrollbar { display: none; }

.cscroll {
  position: fixed; top: 10px; bottom: 10px; right: 4px; width: 16px;
  display: flex; flex-direction: column; z-index: 80;
  user-select: none; -webkit-user-select: none; touch-action: none;
}
.cscroll--hidden { display: none; }

.cscroll-track {
  position: relative; flex: 1; margin: 2px 1px;
  border-radius: 999px;
  background: rgba(255,255,255,.07);
  border: 1px solid rgba(255,255,255,.09);
  cursor: pointer;
}
.cscroll-thumb {
  position: absolute; left: 0; right: 0; top: 0; min-height: 10px;
  border-radius: 999px;
  background: linear-gradient(180deg, #f5faff 0%, #d3e9fc 100%);
  box-shadow: 0 1px 6px rgba(3,10,24,.45), inset 0 0 0 1px rgba(255,255,255,.55);
  cursor: grab;
  transition: box-shadow .2s ease;
}
.cscroll-thumb:hover {
  box-shadow: 0 1px 8px rgba(3,10,24,.5), 0 0 10px rgba(92,198,255,.55), inset 0 0 0 1px rgba(255,255,255,.7);
}
.cscroll-dragging .cscroll-thumb,
.cscroll-thumb:active {
  cursor: grabbing;
  box-shadow: 0 1px 8px rgba(3,10,24,.5), 0 0 14px rgba(92,198,255,.75), inset 0 0 0 1px rgba(255,255,255,.8);
}
/* No text selection or cursor flicker while the thumb is dragged. */
html.cscroll-dragging, html.cscroll-dragging * {
  cursor: grabbing !important;
  user-select: none !important; -webkit-user-select: none !important;
}

.cscroll-btn {
  flex: none; height: 18px; display: flex; align-items: center; justify-content: center;
  padding: 0; border: 0; background: none; cursor: pointer;
  color: var(--ink-faint);
  transition: color .2s ease, transform .15s ease;
}
.cscroll-btn:hover { color: var(--accent); }
.cscroll-btn:active { transform: scale(.82); color: var(--accent); }
.cscroll-btn svg { width: 10px; height: 10px; display: block; fill: currentColor; }


/* Inner scrollers (card modal, any overflow container found by the DOM scan
   in script.js): a zero-height sticky wrapper is the host's first in-flow
   child and sticks to the scrollport ON THE COMPOSITOR, so the bar rides the
   host's own clipping and border radius WITHOUT lagging behind fast
   composited scrolls — the old absolute bar, re-pinned by a JS transform
   each frame, trailed a frame behind. The bar is absolute inside the
   wrapper; script.js sets its height (the host's content-box height, so the
   modal panel's own padding keeps the arrows clear of its 26px corner
   radius) and right offset (4px in from the host's inner edge) per measure.
   Slimmer than the window bar to fit tight panels. Assumes a block-flow
   host. */
.cscroll-stick { position: sticky; top: 0; height: 0; z-index: 5; }
.cscroll--inner { position: absolute; top: 0; right: 4px; bottom: auto; width: 12px; }
.cscroll--inner .cscroll-btn { height: 14px; }
.cscroll--inner .cscroll-btn svg { width: 8px; height: 8px; }

/* Native bars are hidden only where a custom bar is active. */
.cscroll-host { scrollbar-width: none; }
.cscroll-host::-webkit-scrollbar { display: none; }

/* Page scroll is locked while a modal is open (body.modal-open) — the window
   bar would be a dead control, so it goes away until the modal closes. */
body.modal-open > .cscroll { display: none; }

/* ============ SCROLL PERFORMANCE ============ */
/* Render containment: below-the-fold glass cards skip layout/paint entirely
   until they approach the viewport. Each of these carries a backdrop-filter,
   and every filtered card on screen re-filters its backdrop on every scroll
   frame — capping how many are "live" at once is what keeps scrolling smooth
   on mobile GPUs. contain-intrinsic-size holds an estimated size so the
   scrollbar/page height stays stable before first render; once rendered, the
   `auto` keyword remembers the real size. No visual change. */
.discord-card,
.steam-card,
.mal-card,
.lb-card {
  content-visibility: auto;
  contain-intrinsic-size: auto 420px;
}
.pc-spec-card {
  content-visibility: auto;
  contain-intrinsic-size: auto 280px;
}
.music-card {
  content-visibility: auto;
  contain-intrinsic-size: auto 340px;
}
/* The story cards are deliberately NOT contained: .story-link:hover lifts the
   inner .story-card by 4px, which overflows the link's border box — with
   content-visibility's paint containment that clipped the card's top edge on
   hover. Two small cards aren't worth the containment anyway. */

/* Scroll-idle refraction: while the page is actively scrolling, script.js
   sets html.glass-scrolling and the Chromium cards rest their inline SVG
   displacement-map filter, falling back to this frosted blur — the exact
   look Safari/Firefox render full-time. Re-running nine SVG filters against
   a moving backdrop was the desktop scroll stutter; the refractive rims
   return ~120ms after the scroll stops. (!important beats the inline
   backdrop-filter the refraction code sets.) Selector mirrors the refraction
   list in script.js: every refracting .card (the birthday tile stays solid),
   plus the toast. */
html.glass-scrolling .card:not(.stat-card--bday),
html.glass-scrolling .toast {
  -webkit-backdrop-filter: blur(6px) saturate(1.7) !important;
          backdrop-filter: blur(6px) saturate(1.7) !important;
}

/* ============ LOW-POWER MODE (body.low-power) ============ */
/* script.js sets body.low-power when KazuLib.lowPowerMode() says the device
   can't afford the ambient layer: the visitor asked for it (save-data,
   prefers-reduced-motion) or several weakness signals stacked up (few cores +
   coarse pointer + small screen + little RAM — see lib.js). This block mirrors
   what the per-feature prefers-reduced-motion queries already do, then goes
   where media queries can't:
     - the frosted-glass backdrop-filter behind every card (a live blur that
       re-runs for every frame while anything animates beneath it) is switched
       off, and the translucent fills go near-solid so text contrast survives
       the lost blur;
     - the liquid-glass SVG refraction, particle layers, canvas bursts and the
       wheel-lerp smooth scroll are skipped in script.js (nothing to override
       here — they never get built);
     - the remaining ambient loops (branch sway, halo spin, twinkle,
       aurora drift, equalizer, pulse dots) and all entrance motion stop. */
body.low-power { --card-blur: none; }
body.low-power { --card-bg: rgba(40,63,98,.95); }
body.low-power .card:not(.stat-card--bday),
body.low-power .stat-card:not(.stat-card--bday),
body.low-power .social-card,
body.low-power .music-card,
body.low-power .discord-card,
body.low-power .steam-card,
body.low-power .mal-card,
body.low-power .lb-card,
body.low-power .story-card,
body.low-power .toast,
body.low-power .dev-panel,
body.low-power .modal-overlay::before,
body.low-power .modal-panel {
  -webkit-backdrop-filter: none !important;
          backdrop-filter: none !important;
}
body.low-power .petal,
body.low-power .drop { animation: none; display: none; }
body.low-power .atmosphere,
body.low-power .sakura-scene,
body.low-power .sakura-branch--a,
body.low-power .sakura-branch--b,
body.low-power .aurora-ribbon,
body.low-power .xmas-lights circle,
body.low-power .pfp-ring,
body.low-power .pfp-ring::before,
body.low-power .title,
body.low-power .pulse-dot,
body.low-power .music-viz span,
body.low-power .hero .pfp,
body.low-power .stat-card {
  animation: none !important;
}
body.low-power .music-viz { display: none; }
/* Everything is simply present — the observer/stagger in script.js is skipped
   too, this is the belt-and-braces half (and covers a slow script.js). */
body.low-power .scroll-reveal {
  opacity: 1 !important;
  transform: none !important;
  transition: none !important;
}

/* ============ SMARTPHONE & MOBILE RESPONSIVE OPTIMIZATIONS (<= 560px) ============ */
@media (max-width: 560px) {
  .container {
    padding-top: clamp(20px, 4vh, 36px);
    padding-left: max(14px, env(safe-area-inset-left));
    padding-right: max(14px, env(safe-area-inset-right));
    padding-bottom: max(48px, env(safe-area-inset-bottom));
  }

  /* Hero: balanced avatar and typography */
  .hero { padding: 2px 0 6px; }
  .pfp { width: clamp(124px, 32vw, 154px); height: clamp(124px, 32vw, 154px); }
  .title {
    font-size: clamp(36px, 9.2vw, 54px);
    margin: 12px 0 4px;
    padding: .04em .1em;
  }
  .quote-box { padding: 0 16px; }
  .quote-box.visible { padding: 12px 16px; margin-top: 2px; }
  .quote-text { font-size: 14.5px; line-height: 1.4; }
  .quote-author { font-size: 11px; margin-top: 6px; }

  /* Stat strip: punchy 2x2 bento grid */
  .stat-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
    margin-top: 20px;
  }
  .stat-card {
    border-radius: 18px;
    padding: 14px 8px;
  }
  .stat-label { font-size: 11px; letter-spacing: .08em; }
  .stat-value {
    font-size: clamp(19px, 5.2vw, 25px);
    margin-top: 4px;
    line-height: 1.1;
  }
  .stat-sub { font-size: 12px; margin-top: 3px; }
  .stat-faint { font-size: 11px; }
  .stat-loading-text { font-size: 13px; }

  /* Section titles */
  .section-title {
    font-size: 22px;
    margin: 32px 0 12px;
    gap: 8px;
  }

  /* Right now cards */
  .right-now-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 12px;
  }
  .discord-card, .steam-card, .mal-card, .lb-card {
    border-radius: 20px;
    padding: 16px 14px;
  }
  .card-head { margin-bottom: 12px; }
  .avatar-img, .steam-avatar {
    width: 52px; height: 52px; border-radius: 15px;
  }
  .status-dot { width: 16px; height: 16px; border-width: 2.5px; }
  .presence-name { font-size: 18px; }
  .presence-status { font-size: 12px; }
  .info-pill {
    margin-top: 12px;
    padding: 10px 12px;
    border-radius: 14px;
    gap: 10px;
  }
  .info-pill-emoji { font-size: 22px; }
  .info-pill-icon { width: 26px; height: 26px; }
  .info-pill-value { font-size: 15px; }
  .info-pill-sub { font-size: 11.5px; }
  .spotify-art { width: 38px; height: 38px; }
  .spotify-song { font-size: 14.5px; }
  .mal-row { padding: 8px 6px; }
  .mal-thumb { width: 38px; height: 52px; border-radius: 8px; }
  .mal-title { font-size: 14px; }
  .mal-sub { font-size: 11.5px; }
  .lb-entry { gap: 12px; }
  .lb-poster { width: 48px; border-radius: 7px; }
  .lb-title { font-size: 15.5px; }

  /* PC Specs */
  .pc-spec-card {
    border-radius: 20px;
    padding: 16px 14px;
  }
  .pc-spec-card-title { font-size: 14px; margin-bottom: 10px; }
  .pc-spec-feature-name { font-size: clamp(20px, 5.2vw, 26px); }
  .pc-gpu-mark { padding-top: 12px; }
  .pc-gpu-mark strong { font-size: clamp(40px, 11vw, 54px); }
  .pc-gpu-mark span { font-size: 10px; }
  .pc-storage-entry { grid-template-columns: 68px minmax(0, 1fr); gap: 10px; }
  .pc-storage-entry dt { font-size: 20px; }
  .pc-storage-entry dd { font-size: 13px; }
  .pc-spec-entry dt { font-size: 11px; margin-bottom: 2px; }
  .pc-spec-entry dd, .pc-spec-list--build dd { font-size: 17px; }
  .pc-spec-list { gap: 12px; }

  /* YouTube Music Playlist */
  .music-card {
    border-radius: 20px;
    padding: 16px 14px;
  }
  .music-cover-wrap { width: 110px; height: 110px; border-radius: 16px; }
  .music-playlist-name { font-size: clamp(20px, 5.2vw, 24px); }
  .music-playlist-sub { font-size: 12px; }
  .music-track-row { padding: 9px 4px; gap: 10px; }
  .music-track-num { font-size: 13.5px; width: 14px; }
  .music-track-name { font-size: 14.5px; }
  .music-track-artist { font-size: 11.5px; }

  /* Socials: clean 2-column grid */
  .socials-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
  }
  .social-card {
    padding: 18px 10px 14px;
    border-radius: 18px;
  }
  .social-card:last-child:nth-child(odd) {
    grid-column: span 2;
    padding: 16px 14px 14px;
  }
  .social-mark { width: 36px; height: 36px; margin-bottom: 8px; }
  .social-mark svg { width: 30px; height: 30px; }
  .social-name { font-size: 15px; }
  .social-handle { font-size: 12px; }

  /* Stories */
  .stories-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 12px;
  }
  .story-card {
    border-radius: 20px;
    padding: 20px 16px;
  }
  .story-title { font-size: clamp(22px, 6vw, 26px); margin: 10px 0 6px; }
  .story-desc { font-size: 13.5px; max-width: 100%; }
  .story-cta { margin-top: 14px; padding: 8px 14px; font-size: 12.5px; }

  /* Modals: mobile dynamic viewport */
  .modal-overlay { padding: 10px; }
  .modal-panel {
    padding: 20px 14px 22px;
    border-radius: 20px;
    max-height: min(90vh, calc(100dvh - 20px));
  }
  .modal-title { font-size: 20px; margin: 2px 34px 14px 2px; }
  .modal-close { top: 10px; right: 10px; width: 36px; height: 36px; font-size: 22px; }
  .countdown { gap: 6px; }
  .cd-cell { padding: 8px 3px; border-radius: 12px; }
  .cd-num { font-size: clamp(19px, 5.2vw, 25px); }
  .cd-label { font-size: 10px; letter-spacing: .05em; }
  .forecast-strip { gap: 6px; }
  .forecast-day { padding: 8px 3px; }
  .forecast-emoji { font-size: 20px; }
  .forecast-temps { font-size: 13px; }
  .fun-grid { gap: 8px; }
  .fun-cell { padding: 8px 10px; border-radius: 12px; gap: 8px; }
  .fun-emoji { font-size: 22px; }
  .fun-num { font-size: 16px; }
  .toast {
    width: min(calc(100vw - 24px), 380px);
    bottom: max(16px, env(safe-area-inset-bottom, 16px));
    padding: 10px 16px;
    font-size: 13px;
  }
  .dev-panel {
    left: 10px;
    right: 10px;
    width: auto;
    max-width: 340px;
    margin: 0 auto;
    bottom: max(14px, env(safe-area-inset-bottom, 14px));
  }

  /* Tactile active feedback for touch */
  .card[role="button"]:active,
  .stat-card[data-modal]:active,
  .pill-link:active,
  .retry-btn:active,
  .modal-btn:active,
  .modal-close:active,
  .story-cta:active,
  .quote-box:active {
    transform: scale(0.97);
    transition-duration: .08s;
  }
}

/* Extra small devices (<= 360px) */
@media (max-width: 360px) {
  .container {
    padding-left: max(10px, env(safe-area-inset-left));
    padding-right: max(10px, env(safe-area-inset-right));
  }
  .stat-grid { gap: 8px; }
  .stat-card { padding: 12px 6px; }
  .stat-value { font-size: 18px; }
  .socials-grid { gap: 8px; }
  .social-card { padding: 14px 6px 12px; }
  .cd-num { font-size: 18px; }
  .cd-label { font-size: 9px; }
}



