/* ============================================================
   console/console.css — 00 · Console「夜と灯」
   ------------------------------------------------------------
   One device, no scroll. A molded-plastic monitor set into a
   matte panel, five keycaps, a timeline rail, a lamp — that lamp
   IS the theme switch. The chassis stays put across navigations;
   only the .screen carries view-transition-name (see shell.css)
   so the monitor grows glass-first into whichever world you enter.

   Tokens are the yoru-and-akari Console DS, copied from
   D:/DESIGN/yoru-and-akari Console Design System2/colors_and_type.css
   (2026-08-19). Fonts are self-hosted subsets in console/fonts/.
   Rules honoured from the DS README: lowercase UI, no emoji, no
   decorative gradients, cards by shadow not stroke, glass only
   where something shows through (the monitor OSD), no fade-from-
   nothing, spring only on the lamp.

   The Y2K device feel is done inside the DS — deeper layered
   neumorph shadows for a molded shell, a chin with the brand
   debossed and a real power LED, keycaps with sidewall shadow
   and a small colour inlay per world. No scanlines (that belongs
   to Mayonaka) and no 1px hard bevel (that belongs to core).
   ============================================================ */

/* ---------- fonts (self-hosted, subset) --------------------- */
@font-face {
  font-family: "Geist";
  src: url("fonts/Geist-Variable-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Geist Mono";
  src: url("fonts/GeistMono-Variable-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Zen Kaku Gothic New";
  src: url("fonts/ZenKakuGothicNew-400-console.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Zen Kaku Gothic New";
  src: url("fonts/ZenKakuGothicNew-500-console.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Zen Kaku Gothic New";
  src: url("fonts/ZenKakuGothicNew-700-console.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}
/* the CN classification labels on the keycaps. one SC font is enough here — 12 glyphs,
   subsetted from Noto Sans SC via @fontsource, ~2.9KB total. covers 生活影像项目工作流创作实验. */
@font-face {
  font-family: "Noto SC Console";
  src: url("fonts/NotoSansSC-console-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
  unicode-range: U+751F, U+6D3B, U+5F71, U+50CF, U+9879, U+76EE, U+5DE5, U+4F5C, U+6D41, U+521B, U+5B9E, U+9A8C;
}
/* per-world Latin sub-labels on keycaps. each face holds only the letters of that world's
   name (a·o·z·r for aozora, m·a·y·o·n·k for mayonaka …) so the total added weight is < 5KB.
   these are the DS display fonts for their respective worlds; here they read as a whisper of
   that world's voice, one word big enough to notice at 11px. */
@font-face {
  font-family: "Aozora Latin";  /* Inter, from worlds/aozora/fonts */
  src: url("fonts/Inter-console-aozora.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
  unicode-range: U+61, U+6F, U+72, U+7A;
}
@font-face {
  font-family: "Mayonaka Latin";  /* VT323, pixel; on the Mayonaka DS shortlist */
  src: url("fonts/VT323-console-mayonaka.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
  unicode-range: U+61, U+6B, U+6D, U+6E, U+6F, U+79;
}
@font-face {
  font-family: "Rift Latin";  /* Space Grotesk, the RIFT --font-display */
  src: url("fonts/SpaceGrotesk-console-rift.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
  unicode-range: U+66, U+69, U+72, U+74;
}
@font-face {
  font-family: "Kunlun Latin";  /* Orbitron, the KUNLUN display/military-spec face */
  src: url("fonts/Orbitron-console-kunlun.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
  unicode-range: U+6B, U+6C, U+6E, U+75;
}
@font-face {
  font-family: "Core Latin";  /* Pixelify Sans, the core display face */
  src: url("fonts/PixelifySans-console-core.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
  unicode-range: U+63, U+65, U+6F, U+72;
}

/* ---------- tokens ------------------------------------------ */
:root {
  --font-ui:   "Geist", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --font-mono: "Geist Mono", "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --font-jp:   "Zen Kaku Gothic New", "Hiragino Kaku Gothic ProN", "Yu Gothic UI", system-ui, sans-serif;
  --font-cn:   "Noto SC Console", "PingFang SC", "Microsoft YaHei", "Zen Kaku Gothic New", "Hiragino Sans GB", system-ui, sans-serif;

  --text-2xs: 10px; --text-xs: 11px; --text-sm: 12px; --text-base: 13px; --text-md: 14px;
  --text-lg: 16px; --text-xl: 19px; --text-2xl: 23px; --text-3xl: 28px; --text-4xl: 34px; --text-5xl: 44px;
  --tracking-tight: -0.012em; --tracking-wide: 0.04em; --tracking-caps: 0.08em;

  --radius-xs: 6px; --radius-sm: 9px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 22px; --radius-2xl: 28px;
  --hit-min: 44px;

  --ease-out:    cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.42, 1);
  --dur-fast: 140ms; --dur-base: 220ms; --dur-slow: 420ms;
}

/* akari (灯) — light, default */
:root, [data-theme="akari"] {
  --bg-base: #E8ECF3; --bg-surface: #EFF2F8; --bg-elevated: #F6F8FC; --bg-sunken: #DEE3EC; --bg-tint: #F1F4FB;
  --glass-bg: hsla(220, 30%, 98%, 0.62); --glass-border: hsla(225, 30%, 55%, 0.18); --glass-blur: 18px; --glass-sat: 140%;
  --ink-1: #0E1525; --ink-2: #394560; --ink-3: #6B7793; --ink-4: #A1ABBF; --ink-on-primary: #FFFFFF;
  --line-1: rgba(14, 21, 37, 0.06); --line-2: rgba(14, 21, 37, 0.10); --line-strong: rgba(14, 21, 37, 0.18);
  --primary-300: #92A8FB; --primary-500: #4F6CE8; --primary-700: #2E44B0;
  --ember-300: #FF9456; --ember-500: #F06A20; --ember-700: #C05015;
  --frost-500: #1EA8A0;
  --neo-hi: rgba(255, 255, 255, 0.95); --neo-lo: rgba(143, 158, 191, 0.45); --neo-lo-soft: rgba(143, 158, 191, 0.28);
  --shadow-raised:  -4px -4px 12px var(--neo-hi), 4px 4px 14px var(--neo-lo-soft);
  --shadow-lifted:  -6px -6px 18px var(--neo-hi), 8px 8px 22px var(--neo-lo);
  --shadow-inset:   inset 3px 3px 6px var(--neo-lo-soft), inset -3px -3px 6px var(--neo-hi);
  --shadow-inset-deep: inset 4px 4px 8px var(--neo-lo), inset -3px -3px 6px var(--neo-hi);
  --shadow-focus:   0 0 0 3px rgba(79, 108, 232, 0.28);
  --primary-glow:   rgba(79, 108, 232, 0.5);
  --lamp-glow-a: rgba(240, 106, 32, 0.50);
  --lamp-glow-b: rgba(240, 106, 32, 0.14);
  --lamp-pool:   rgba(240, 106, 32, 0.10);
  /* akari: the room is lit, so the panel is a lit pane sunk into the plastic —
     a cool paper tone, not a hole of night. rim / vignette / glare follow it. */
  --screen: #DDE3EF;
  --screen-rim: rgba(14, 21, 37, 0.07);
  --screen-vignette: rgba(143, 158, 191, 0.38);
  --screen-glare-a: rgba(255, 255, 255, 0.50);
  --screen-glare-b: rgba(255, 255, 255, 0.14);
  --screen-bezel: #1C2438;
  --screen-bloom: radial-gradient(120% 92% at 50% 38%, rgba(255, 255, 255, 0.55) 0%, rgba(255, 255, 255, 0) 72%);
  /* the full-moon accent in the masthead mark. ember in the light theme. */
  --wm-accent: var(--ember-500);
  /* page wash: the lamp's light falls from the upper right; a cool wash lower left */
  --page-wash:
    radial-gradient(90% 70% at 100% 0%, rgba(240, 106, 32, 0.07), transparent 60%),
    radial-gradient(100% 80% at 0% 100%, rgba(79, 108, 232, 0.08), transparent 60%),
    var(--bg-base);
}

/* yoru (夜) — dark */
[data-theme="yoru"] {
  --bg-base: #0B1020; --bg-surface: #131A2E; --bg-elevated: #1B2340; --bg-sunken: #080D1B; --bg-tint: #182146;
  --glass-bg: hsla(225, 35%, 14%, 0.62); --glass-border: hsla(220, 40%, 70%, 0.10); --glass-blur: 20px; --glass-sat: 130%;
  --ink-1: #E6EAF6; --ink-2: #A8B2CC; --ink-3: #6B7691; --ink-4: #434D67; --ink-on-primary: #0B1020;
  --line-1: rgba(255, 255, 255, 0.06); --line-2: rgba(255, 255, 255, 0.10); --line-strong: rgba(255, 255, 255, 0.18);
  --primary-300: #3D52AE; --primary-500: #7A95FA; --primary-700: #B4C3FC;
  --ember-300: #804418; --ember-500: #F07830; --ember-700: #FF9456;
  --frost-500: #38C8C0;
  --neo-hi: rgba(255, 255, 255, 0.05); --neo-lo: rgba(0, 0, 0, 0.55); --neo-lo-soft: rgba(0, 0, 0, 0.35);
  --shadow-raised:  -3px -3px 10px var(--neo-hi), 5px 5px 16px var(--neo-lo-soft);
  --shadow-lifted:  -4px -4px 14px var(--neo-hi), 8px 8px 24px var(--neo-lo);
  --shadow-inset:   inset 3px 3px 7px var(--neo-lo-soft), inset -2px -2px 5px var(--neo-hi);
  --shadow-inset-deep: inset 4px 4px 9px var(--neo-lo), inset -2px -2px 6px var(--neo-hi);
  --shadow-focus:   0 0 0 3px rgba(122, 149, 250, 0.35);
  --primary-glow:   rgba(122, 149, 250, 0.55);
  --lamp-glow-a: rgba(240, 120, 48, 0.60);
  --lamp-glow-b: rgba(240, 120, 48, 0.18);
  --lamp-pool:   rgba(240, 120, 48, 0.14);
  --screen: #05070E;
  --screen-rim: rgba(255, 255, 255, 0.035);
  --screen-vignette: rgba(0, 0, 0, 0.55);
  --screen-glare-a: rgba(255, 255, 255, 0.045);
  --screen-glare-b: rgba(255, 255, 255, 0.012);
  --screen-bezel: #141C33;
  --screen-bloom: radial-gradient(120% 92% at 50% 38%, rgba(122, 149, 250, 0.10) 0%, rgba(122, 149, 250, 0) 72%);
  /* the full-moon accent in the masthead mark. primary in the dark theme so it doesn't burn. */
  --wm-accent: var(--primary-500);
  --page-wash:
    radial-gradient(90% 70% at 100% 0%, rgba(240, 120, 48, 0.08), transparent 60%),
    radial-gradient(100% 80% at 0% 100%, rgba(122, 149, 250, 0.09), transparent 60%),
    var(--bg-base);
}

/* ---------- the device -------------------------------------- */
/* large-desktop scale: at 100% browser zoom on a big monitor the device reads
   small and the sub-10px mono goes blurry. Yoru's approved reference is her
   browser at 125%, so replicate exactly that: zoom the device and divide the
   two viewport-unit calcs (stagewrap width/height cap) by the same factor so
   the layout solves in the zoomed coordinate space. */
:root { --ui-zoom: 1; }
@media (min-width: 1600px) and (min-height: 880px) {
  :root { --ui-zoom: 1.25; }
}
html { background: var(--bg-base); color-scheme: light; }
[data-theme="yoru"] { color-scheme: dark; }
body {
  overflow: hidden;
  font: 400 var(--text-base)/1.5 var(--font-ui);
  color: var(--ink-1);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* a gentle theme-swap for the whole page (colour tokens cascade in); the switch itself is instant otherwise. */
  transition: background var(--dur-base) linear, color var(--dur-base) linear;
}
::selection { background: var(--primary-300); color: var(--ink-1); }

.device {
  position: fixed;
  inset: 0;
  overflow: hidden;
  zoom: var(--ui-zoom);
  background: var(--page-wash);
  /* leak: set by console.js to the hovered world's colour */
  --leak: transparent;
  --leak-soft: color-mix(in srgb, var(--leak) 32%, transparent);
  --leak-line: color-mix(in srgb, var(--leak) 50%, transparent);
}

/* ---------- top: masthead + lamp ---------------------------- */
.device-top {
  position: absolute;
  inset: 0 0 auto 0;
  height: 72px;
  /* the header itself doesn't intercept clicks; its children do (wordmark <a> and lamp <button>) */
  pointer-events: none;
}
.wordmark {
  position: absolute;
  left: 40px;
  top: 24px;
  display: block;
  color: var(--ink-1);
  pointer-events: auto;
  text-decoration: none;
  border-radius: 4px;
  /* hit area sits around the mark; svg keeps its own inline size */
  padding: 4px;
}
.wordmark:focus-visible { outline: 0; box-shadow: var(--shadow-focus); }
.wordmark-svg {
  display: block;
  height: 20px;
  width: auto;
}

/* the lamp: a small warm point, a switch, and the pool of light it throws on the panel */
.lamp {
  position: absolute;
  top: 22px;
  right: 32px;
  width: var(--hit-min);
  height: var(--hit-min);
  padding: 0;
  margin: 0;
  border: 0;
  background: transparent;
  border-radius: 50%;
  cursor: pointer;
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
  display: grid;
  place-items: center;
}
.lamp-dot {
  display: block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #FFD4B0 0 18%, #FF9456 52%, var(--ember-500) 100%);
  box-shadow:
    0 0 0 3px var(--bg-surface),
    0 0 18px 4px var(--lamp-glow-a),
    inset -1px -2px 3px rgba(192, 80, 21, 0.35);
  transform-origin: 50% 50%;
  animation: lamp-idle 4.2s var(--ease-spring) infinite;
  transition: box-shadow var(--dur-base) var(--ease-out), background var(--dur-base) linear;
}
.lamp:hover .lamp-dot {
  box-shadow:
    0 0 0 3px var(--bg-surface),
    0 0 28px 8px var(--lamp-glow-a),
    inset -1px -2px 3px rgba(192, 80, 21, 0.35);
}
.lamp:focus-visible { outline: 0; }
.lamp:focus-visible .lamp-dot { box-shadow:
    0 0 0 3px var(--bg-surface),
    0 0 22px 6px var(--lamp-glow-a),
    inset -1px -2px 3px rgba(192, 80, 21, 0.35),
    var(--shadow-focus);
}
.lamp:active .lamp-dot { transform: scale(0.92); }
/* in yoru (夜) the lamp reads as ON: a bright warm point on a dark panel.
   in akari (灯) the room itself is lit, so the physical bulb is cooler / smaller —
   the switch is in the "up" position, the light is elsewhere. */
[data-theme="akari"] .lamp-dot {
  background: radial-gradient(circle at 35% 30%, #FFF0E0 0 22%, #F1BFA2 60%, #C08063 100%);
  box-shadow:
    0 0 0 3px var(--bg-surface),
    0 0 10px 2px rgba(240, 106, 32, 0.20),
    inset -1px -2px 3px rgba(150, 90, 60, 0.45);
}
.lamp-light {
  position: absolute;
  top: -260px;
  right: -260px;
  width: 640px;
  height: 640px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--lamp-glow-b) 0%, var(--lamp-pool) 22%, transparent 62%);
  transform: translate(var(--lx, 0px), var(--ly, 0px));
  transition: transform 900ms var(--ease-out), opacity var(--dur-base) linear;
  will-change: transform;
  pointer-events: none;
}
/* in akari the room is diffusely lit — the lamp's pool doesn't punch through */
[data-theme="akari"] .lamp-light { opacity: 0.5; }
@keyframes lamp-idle {
  0%, 100% { transform: scale(1);    box-shadow: 0 0 0 3px var(--bg-surface), 0 0 18px 4px var(--lamp-glow-a), inset -1px -2px 3px rgba(192,80,21,.35); }
  50%      { transform: scale(1.12); box-shadow: 0 0 0 3px var(--bg-surface), 0 0 26px 7px var(--lamp-glow-a), inset -1px -2px 3px rgba(192,80,21,.35); }
}

/* ---------- monitor: molded plastic chassis around a glass screen ---------- */
.stagewrap {
  position: absolute;
  left: -28px;                 /* the chassis bleeds off the panel edge */
  top: calc(50% - 60px);
  transform: translateY(-50%);
  /* viewport units are computed against the REAL viewport, so inside the zoomed
     device they must be divided by --ui-zoom to stay true in this coordinate
     space (this is what makes zoomed-100% match the browser-zoom reference). */
  width: calc(58vw / var(--ui-zoom));
  min-width: 560px;
  /* also capped by viewport height: chassis height is (w - 68px) * 9/16 + 126px
     (side bezels 22+22 + frame 12+12; top 30 + chin 72 + frame 24), and its top
     edge (vertical centre − 60px − height/2) must stay below the 84px masthead
     line. without this, wide-but-short viewports (fullscreen on a scaled
     laptop) push the chassis top over the wordmark. */
  max-width: min(940px, calc((100vh / var(--ui-zoom) - 414px) * 16 / 9 + 68px));
}
/* the chassis: a chunky molded shell. its neumorph shadow is layered so it reads as a raised
   piece of hardware sitting on the panel, not a rectangle. an outer ground shadow lifts the
   whole device off the panel; a top-lit gradient across the front bezel fakes the way molded
   plastic catches ambient light; the chin under the glass carries the debossed brand mark, a
   speaker-slot pattern, and a real power LED. mold seams run down the sides (::before) and
   the leak halo lives on ::after so both are free for their own job. */
.console-monitor {
  position: relative;
  border-radius: var(--radius-2xl);
  padding: 30px 22px 72px 22px;                /* top bezel > side bezel, chin deeper still */
  background:
    /* very gentle top-lit sheen: warmer at the top edge, cooler at the bottom.
       kept under 3% so it never reads as a "decorative gradient" — it's the plastic. */
    linear-gradient(to bottom, rgba(255, 255, 255, 0.04), transparent 30%, rgba(0, 0, 0, 0.03) 100%),
    var(--bg-surface);
  box-shadow:
    /* raised body */
    -6px -6px 18px var(--neo-hi),
    10px 14px 30px var(--neo-lo),
    /* a soft ground cast under the whole device — so it looks like it sits on the panel */
    0 26px 40px -14px rgba(0, 0, 0, 0.28),
    /* rim hairline that reads as a bezel edge */
    inset 0 1px 0 var(--neo-hi),
    inset 0 -1px 0 var(--neo-lo-soft);
}
/* side mold seams: two hairlines running the full height, 8px inside each side of the shell.
   subtle enough to disappear at a glance, present enough to make the shell read as a molded
   object with a front half and a back half joined down the middle of the side. */
.console-monitor::before {
  content: "";
  position: absolute;
  inset: 12px 8px 12px 8px;
  border-radius: calc(var(--radius-2xl) - 6px);
  pointer-events: none;
  border-left: 1px solid transparent;
  border-right: 1px solid transparent;
  box-shadow:
    inset 1px 0 0 var(--line-1),
    inset -1px 0 0 var(--line-1);
}
/* the leak halo behind the screen edge — only shows when a key is hovered (device[data-leak]). */
.console-monitor::after {
  content: "";
  position: absolute;
  inset: 22px 16px 64px 16px;                  /* rings the sunken frame */
  border-radius: calc(var(--radius-md) + 6px);
  pointer-events: none;
  box-shadow: 0 0 0 0 transparent;
  transition: box-shadow var(--dur-slow) var(--ease-out);
}
.device[data-leak] .console-monitor::after { box-shadow: 0 0 90px 16px var(--leak-soft); }

/* the recessed well: a step of chassis plastic sunk between the shell and the
   bezel. monitor anatomy, outside in: chassis → this sunken frame → dark bezel
   → lit pane. gently elliptical corners so the assembly reads tube, not card. */
.screen-frame {
  position: relative;
  padding: 12px;
  border-radius: 22px / 18px;
  background: var(--bg-sunken);
  box-shadow: var(--shadow-inset-deep);
}
.screen {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: 15px / 12px;
  /* the display bezel: a dark frame between the sunken well and the pane —
     the one part of a display that stays dark whatever the room light does. */
  border: 10px solid var(--screen-bezel);
  /* pane fill + a centered backlight bloom, so the pane reads as lit from within */
  background: var(--screen-bloom), var(--screen);
  overflow: hidden;
  /* pane depth: a hairline at the glass edge + a soft vignette into the bezel */
  box-shadow:
    inset 0 0 0 1px var(--screen-rim),
    inset 0 0 90px var(--screen-vignette);
  transition: box-shadow var(--dur-slow) var(--ease-out), background var(--dur-fast) linear, transform 180ms var(--ease-out);
}
/* a little glare on the glass, so the pane reads as a screen and not a hole */
.screen::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(112deg, var(--screen-glare-a) 0%, var(--screen-glare-b) 28%, transparent 46%);
}
.device[data-leak] .screen {
  box-shadow:
    inset 0 0 0 1px var(--leak-line),
    inset 0 0 180px var(--leak-soft);
}
.screen video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
.screen video.is-live { opacity: 1; }

/* the chin: the bottom bezel of the chassis. debossed moon-mark on the left, speaker slots
   in the centre, a real power LED with tiny model-number engraving on the right. this is
   where the "molded product" reads loudest — a real consumer-electronics product face. */
.chin {
  position: absolute;
  /* the chassis bleeds 28px off the viewport's left edge; pad past it so the
     debossed mark is never clipped by the screen edge */
  left: 44px;
  right: 24px;
  bottom: 14px;
  height: 44px;
  display: grid;
  /* four columns: mark | speaker (fluid) | i/o ports | led-well.
     the ports and led-well take auto; the speaker absorbs the rest, so it stays wide on
     big viewports and shrinks toward its minimum on narrow ones. */
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: 18px;
  /* the chin itself is inert; interactive children (real io-port <a>s) opt back in below. */
  pointer-events: none;
}
/* debossed brand: the mark sits INTO the plastic. two-tone drop-shadow on the svg fill
   fakes the sunk lip (highlight below, shadow above). colour is a dim ink to keep it quiet. */
.chin-mark {
  /* grid-column 1 is load-bearing: the ::before grille claims column 2 first,
     and without this the mark auto-places into column 3, evicting the LED well
     to an overflow row. */
  grid-column: 1;
  display: block;
  height: 20px;
  width: auto;
  color: var(--ink-2);
  opacity: 0.8;
  filter:
    drop-shadow(0 -1px 0 var(--neo-lo-soft))
    drop-shadow(0 1.5px 0 var(--neo-hi));
}
[data-theme="yoru"] .chin-mark { color: var(--ink-2); opacity: 0.65; }
/* the speaker slot pattern: nine 1px vertical slits every 6px, sunken into the plastic.
   two mold lines run above and below. it fills the middle of the chin. */
.chin::before {
  content: "";
  grid-column: 2;
  justify-self: center;
  width: 240px;
  height: 11px;
  background:
    /* the slits themselves, in a repeating pattern */
    repeating-linear-gradient(
      to right,
      transparent 0 5px,
      rgba(14, 21, 37, 0.34) 5px 6.5px
    );
  border-radius: 1px;
  box-shadow:
    /* top edge = dark shadow (light comes from above → the slit's upper lip is in shadow)
       bottom edge = highlight (the lower lip catches the ambient light) */
    inset 0 1px 0 var(--neo-lo-soft),
    inset 0 -1px 0 var(--neo-hi);
  opacity: 0.9;
}
[data-theme="yoru"] .chin::before {
  background:
    repeating-linear-gradient(
      to right,
      transparent 0 5px,
      rgba(0, 0, 0, 0.8) 5px 6.5px
    );
  opacity: 0.8;
}
/* power LED area: LED + a tiny embossed model number below it. the number is diegetic — it's
   what you'd see stamped on the plastic of a real device, not decorative text. */
.chin-led-well {
  grid-column: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
}
.power-led {
  position: relative;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--bg-sunken) 92%, #000);
  box-shadow:
    inset 0 1px 1px rgba(0, 0, 0, 0.55),
    inset 0 -1px 0 var(--neo-hi);
  transition: background var(--dur-base) linear, box-shadow var(--dur-base) linear;
}
.power-led.is-on {
  background: radial-gradient(circle at 35% 30%, #E6FFC7 0 20%, #7BD46A 60%, #2A9432 100%);
  box-shadow:
    0 0 6px 1px rgba(120, 210, 100, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.45);
}
.power-led.is-fetching { animation: led-breathe 1.8s var(--ease-in-out) infinite; }
/* something failed to reach: the LED goes amber. y2k warning-light convention — matches
   the "no signal" line the screen puts up. amber overrides the green tint on is-on. */
.power-led.is-failed {
  background: radial-gradient(circle at 35% 30%, #FFE7C3 0 20%, #F0A048 55%, var(--ember-700) 100%);
  box-shadow:
    0 0 6px 1px rgba(240, 120, 48, 0.6),
    inset 0 1px 0 rgba(255, 225, 195, 0.55);
  animation: none;
}
@keyframes led-breathe {
  0%, 100% { box-shadow: 0 0 4px 1px rgba(120, 210, 100, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.35); }
  50%      { box-shadow: 0 0 10px 2px rgba(120, 210, 100, 0.75), inset 0 1px 0 rgba(255, 255, 255, 0.55); }
}

/* ---------- chin i/o ports: y2k a/v-panel row ------------------
   the chassis has one live port (github) and three empty sockets. each socket type has
   a slightly different opening — usb-a wide, usb-c oblong, trs round, din larger round —
   so the row reads as "the machine has several kinds of jacks" instead of "four buttons".
   labels appear only on live ports (real, or nothing). the chin itself is aria-labeled;
   live ports are real <a>s and are pointer-active while the rest of the chin isn't. */
.io-ports {
  grid-column: 3;
  display: flex;
  align-items: flex-end;
  gap: 8px;
  pointer-events: none;
}
.io-port {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 0 1px;
  color: var(--ink-3);
  text-decoration: none;
  border-radius: 2px;
  -webkit-tap-highlight-color: transparent;
}
.io-port.is-live { pointer-events: auto; cursor: pointer; }
/* the wechat port is a <button> (it acts on the screen instead of navigating);
   strip the UA chrome so it sits in the row like its <a> siblings. */
button.io-port {
  appearance: none;
  border: 0;
  margin: 0;
  background: transparent;
  font: inherit;
  -webkit-tap-highlight-color: transparent;
}
.io-socket {
  display: block;
  /* the socket well: recessed into the plastic. inset shadow makes the mouth read dark,
     and a hairline of highlight along the bottom lip catches the ambient light. */
  background: color-mix(in srgb, var(--bg-sunken) 88%, #000);
  box-shadow:
    inset 0 1.5px 2px rgba(0, 0, 0, 0.6),
    inset 0 -1px 0 var(--neo-hi);
}
/* the four port shapes. all are small; only the live one carries an inlay of the world
   colour whose feed this port ultimately belongs to (github → rift-blue for now). */
/* one uniform socket shape across the row (Yoru's call, 08-24) */
.io-socket { width: 14px; height: 7px; border-radius: 3.5px; }
/* live socket: a faint inner tint + a whisper of colour glow so the eye finds it. */
.io-port.is-live .io-socket {
  background:
    radial-gradient(circle at 50% 55%, color-mix(in srgb, var(--primary-500) 30%, transparent) 0 40%, transparent 80%),
    color-mix(in srgb, var(--bg-sunken) 82%, #000);
  box-shadow:
    inset 0 1.5px 2px rgba(0, 0, 0, 0.6),
    inset 0 -1px 0 var(--neo-hi),
    0 0 4px color-mix(in srgb, var(--primary-500) 30%, transparent);
}
.io-port.is-live:hover .io-socket,
.io-port.is-live:focus-visible .io-socket {
  box-shadow:
    inset 0 1.5px 2px rgba(0, 0, 0, 0.6),
    inset 0 -1px 0 var(--neo-hi),
    0 0 8px color-mix(in srgb, var(--primary-500) 50%, transparent);
}
.io-port.is-live:focus-visible { outline: 0; box-shadow: var(--shadow-focus); }
.io-port.is-live:active .io-socket { transform: translateY(1px); }
.io-label {
  font: 500 8px/1 var(--font-mono);
  letter-spacing: 0.14em;
  color: var(--ink-3);
  text-transform: lowercase;
  opacity: 0.85;
  filter: drop-shadow(0 1px 0 var(--neo-hi));
}
[data-theme="yoru"] .io-label { opacity: 0.6; }
.chin-model {
  font: 500 9px/1 var(--font-mono);
  letter-spacing: 0.16em;
  text-transform: lowercase;
  color: var(--ink-3);
  opacity: 0.7;
  white-space: nowrap;      /* an engraved nameplate never wraps */
  filter: drop-shadow(0 1px 0 var(--neo-hi));
}
[data-theme="yoru"] .chin-model { opacity: 0.55; }

/* what the screen shows while no loop plays: the day the rail points at.
   real events with their real times; nothing on an empty day. */
.daylog {
  position: absolute;
  left: 36px;
  right: 22px;
  top: 20px;
  bottom: 52px;             /* leave the OSD its room; overflow fades out */
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 78%, transparent);
          mask-image: linear-gradient(to bottom, #000 78%, transparent);
  margin: 0;
  padding: 0;
  list-style: none;
  font: 400 var(--text-sm)/1.75 var(--font-mono);
  letter-spacing: 0.01em;
  color: var(--ink-2);
  transition: opacity var(--dur-base) var(--ease-out);
}
.daylog li {
  display: flex;
  gap: 14px;
  min-width: 0;
  white-space: nowrap;
}
.daylog time { flex: 0 0 auto; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.daylog .t { overflow: hidden; text-overflow: ellipsis; }
.daylog .more { color: var(--ink-3); }
.device[data-leak] .daylog,
.screen video.is-live ~ .daylog { opacity: 0.22; }

/* honest failure state on the screen. one row per source that failed; each row is a
   short line + a real retry button that just re-runs that source's fetch. no fabricated
   events ever populate the rail, so this row is the only thing that admits the miss.
   sits centered on the screen so it reads as "the broadcast itself has no signal",
   not as a chrome-level toast. */
.no-signal {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-width: min(88%, 460px);
  padding: 14px 18px 12px;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.55);
  box-shadow:
    inset 0 0 0 1px rgba(240, 120, 48, 0.32),
    0 0 24px rgba(240, 120, 48, 0.10);
  color: var(--ember-500);
  font: 500 var(--text-sm)/1.35 var(--font-mono);
  letter-spacing: 0.02em;
  text-align: center;
  pointer-events: auto;
}
.no-signal[hidden] { display: none; }
.ns-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.ns-item + .ns-item { padding-top: 8px; border-top: 1px dashed rgba(240, 120, 48, 0.22); }
.ns-head {
  font: 700 var(--text-lg)/1 var(--font-mono);
  letter-spacing: 0.10em;
  text-transform: lowercase;
  color: var(--ember-500);
  filter: drop-shadow(0 0 6px rgba(240, 120, 48, 0.35));
}
.ns-sub {
  font: 400 var(--text-xs)/1.3 var(--font-mono);
  letter-spacing: 0.04em;
  color: color-mix(in srgb, var(--ember-500) 65%, #FFEEDA);
  opacity: 0.9;
}
.ns-retry {
  margin-top: 2px;
  padding: 6px 14px;
  border: 0;
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--ember-500) 22%, transparent);
  color: #FFEBD3;
  font: 500 var(--text-xs)/1 var(--font-mono);
  letter-spacing: 0.10em;
  text-transform: lowercase;
  cursor: pointer;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.16),
    inset 0 -1px 0 rgba(0, 0, 0, 0.35),
    0 0 6px color-mix(in srgb, var(--ember-500) 30%, transparent);
  transition: background var(--dur-fast) linear, transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) linear;
  -webkit-tap-highlight-color: transparent;
}
.ns-retry:hover { background: color-mix(in srgb, var(--ember-500) 32%, transparent); }
.ns-retry:active { transform: translateY(1px); }
.ns-retry:focus-visible { outline: 0; box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.16),
    inset 0 -1px 0 rgba(0, 0, 0, 0.35),
    0 0 0 3px rgba(240, 120, 48, 0.35);
}
/* while fetching, the retry disables itself visually (the LED breath is the wait cue). */
.no-signal.is-fetching .ns-retry { opacity: 0.55; pointer-events: none; }
/* akari: the panel is lit, so the failure plate goes light glass and the ember
   ink deepens for contrast. same geometry, same voice. */
[data-theme="akari"] .no-signal {
  background: rgba(255, 255, 255, 0.55);
  box-shadow:
    inset 0 0 0 1px rgba(192, 80, 21, 0.28),
    0 0 24px rgba(240, 106, 32, 0.10);
  color: var(--ember-700);
}
[data-theme="akari"] .ns-head { color: var(--ember-700); filter: none; }
[data-theme="akari"] .ns-sub { color: var(--ember-700); }
[data-theme="akari"] .ns-retry {
  color: var(--ember-700);
  background: color-mix(in srgb, var(--ember-500) 16%, transparent);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    inset 0 -1px 0 rgba(192, 80, 21, 0.20);
}

/* the one glass element on the console: OSD text sitting on the screen */
.osd {
  position: absolute;
  left: 36px;
  bottom: 12px;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: calc(100% - 28px);
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
          backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border: 1px solid var(--glass-border);
  font: 500 var(--text-xs)/1.2 var(--font-mono);
  letter-spacing: 0.02em;
  color: var(--ink-1);
  white-space: nowrap;
  transition: opacity var(--dur-fast) linear;
}
.osd[hidden] { display: none; }   /* display:flex would beat the UA hidden rule */
.osd-world { overflow: hidden; text-overflow: ellipsis; }
.osd-world .jp { font-family: var(--font-jp); margin-left: 4px; color: var(--ink-2); }
.osd-world .ev { color: var(--ink-2); }

/* the wechat QR card: the screen puts up a pairing code. white plate in both
   themes — a scannable code needs its quiet zone. click it (or Esc) to put it
   away. */
.qr-plate {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 14px 14px 10px;
  border-radius: var(--radius-sm);
  background: #FFFFFF;
  box-shadow:
    0 8px 28px rgba(0, 0, 0, 0.28),
    inset 0 0 0 1px rgba(14, 21, 37, 0.06);
  cursor: pointer;
}
.qr-plate img { display: block; width: 168px; height: 168px; }
.qr-cap {
  font: 500 var(--text-xs)/1 var(--font-mono);
  letter-spacing: 0.06em;
  color: #394560;
  text-transform: lowercase;
}

/* ---------- mail plate: the address reveal ----------------
   The mail port puts its own card on the screen. Three states share the
   layout language of the no-signal plate (position, size, mono voice), but
   split by voice: checking is a quiet primary tint, fail borrows the ember
   voice of no-signal, and the successful reveal shows a bare white plate
   holding a selectable address + a click-to-copy affordance. Clicking the
   plate background closes it (parity with .qr-plate); the interactive
   children (address link, copy, retry) stop propagation so they act instead
   of dismissing. */
.mail-plate {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-width: 240px;
  max-width: min(88%, 460px);
  padding: 14px 20px 12px;
  border-radius: var(--radius-sm);
  text-align: center;
  font: 500 var(--text-sm)/1.35 var(--font-mono);
  letter-spacing: 0.02em;
  pointer-events: auto;
  cursor: default;
}
.mail-head {
  font: 700 var(--text-lg)/1 var(--font-mono);
  letter-spacing: 0.10em;
  text-transform: lowercase;
}
.mail-sub {
  font: 400 var(--text-xs)/1.3 var(--font-mono);
  letter-spacing: 0.04em;
  opacity: 0.9;
}
/* turnstile seat: interaction-only keeps it unseen for most visitors; when
   Cloudflare wants a click the box materialises here on the plate. width
   grows the plate to the widget's 300px minimum; line-height 0 keeps the
   empty seat from adding phantom height while hidden. */
#mail-turnstile-host { width: 300px; max-width: 100%; line-height: 0; }
#mail-turnstile-host iframe { max-width: 100%; }
/* checking: quiet, cool. same body as no-signal but the ink is primary blue
   so it doesn't read as an alarm — this state is normal, waiting. */
.mail-plate[data-state="checking"] {
  background: rgba(0, 0, 0, 0.55);
  color: var(--primary-500);
  box-shadow:
    inset 0 0 0 1px rgba(122, 149, 250, 0.32),
    0 0 24px rgba(122, 149, 250, 0.10);
}
.mail-plate[data-state="checking"] .mail-head {
  color: var(--primary-500);
  filter: drop-shadow(0 0 6px rgba(122, 149, 250, 0.35));
}
[data-theme="akari"] .mail-plate[data-state="checking"] {
  background: rgba(255, 255, 255, 0.55);
  color: var(--primary-700);
  box-shadow:
    inset 0 0 0 1px rgba(46, 68, 176, 0.28),
    0 0 24px rgba(79, 108, 232, 0.10);
}
[data-theme="akari"] .mail-plate[data-state="checking"] .mail-head { color: var(--primary-700); filter: none; }
[data-theme="akari"] .mail-plate[data-state="checking"] .mail-sub { color: var(--primary-700); }

/* fail: the no-signal language. same wording rules, same retry cadence. */
.mail-plate[data-state="fail"] {
  background: rgba(0, 0, 0, 0.55);
  color: var(--ember-500);
  box-shadow:
    inset 0 0 0 1px rgba(240, 120, 48, 0.32),
    0 0 24px rgba(240, 120, 48, 0.10);
}
.mail-plate[data-state="fail"] .mail-head {
  color: var(--ember-500);
  filter: drop-shadow(0 0 6px rgba(240, 120, 48, 0.35));
}
.mail-plate[data-state="fail"] .mail-sub {
  color: color-mix(in srgb, var(--ember-500) 65%, #FFEEDA);
}
[data-theme="akari"] .mail-plate[data-state="fail"] {
  background: rgba(255, 255, 255, 0.55);
  color: var(--ember-700);
  box-shadow:
    inset 0 0 0 1px rgba(192, 80, 21, 0.28),
    0 0 24px rgba(240, 106, 32, 0.10);
}
[data-theme="akari"] .mail-plate[data-state="fail"] .mail-head { color: var(--ember-700); filter: none; }
[data-theme="akari"] .mail-plate[data-state="fail"] .mail-sub { color: var(--ember-700); }
.mail-retry {
  margin-top: 2px;
  padding: 6px 14px;
  border: 0;
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--ember-500) 22%, transparent);
  color: #FFEBD3;
  font: 500 var(--text-xs)/1 var(--font-mono);
  letter-spacing: 0.10em;
  text-transform: lowercase;
  cursor: pointer;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.16),
    inset 0 -1px 0 rgba(0, 0, 0, 0.35),
    0 0 6px color-mix(in srgb, var(--ember-500) 30%, transparent);
  transition: background var(--dur-fast) linear, transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) linear;
  -webkit-tap-highlight-color: transparent;
}
.mail-retry:hover { background: color-mix(in srgb, var(--ember-500) 32%, transparent); }
.mail-retry:active { transform: translateY(1px); }
.mail-retry:focus-visible {
  outline: 0;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.16),
    inset 0 -1px 0 rgba(0, 0, 0, 0.35),
    0 0 0 3px rgba(240, 120, 48, 0.35);
}
[data-theme="akari"] .mail-retry {
  color: var(--ember-700);
  background: color-mix(in srgb, var(--ember-500) 16%, transparent);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    inset 0 -1px 0 rgba(192, 80, 21, 0.20);
}

/* ok: a plain white plate. the address is selectable text + a mailto link;
   the copy button gives a click-to-copy affordance. Y2K device manners — a
   real product would print the address on the plate, not hide it. */
.mail-plate[data-state="ok"] {
  background: #FFFFFF;
  color: #394560;
  box-shadow:
    0 8px 28px rgba(0, 0, 0, 0.28),
    inset 0 0 0 1px rgba(14, 21, 37, 0.06);
}
.mail-plate[data-state="ok"] .mail-head {
  color: #6B7793;
  font-weight: 500;
  font-size: var(--text-xs);
  letter-spacing: 0.14em;
  opacity: 0.85;
}
.mail-addr {
  display: inline-block;
  padding: 4px 6px;
  font: 500 var(--text-md)/1.25 var(--font-mono);
  letter-spacing: 0.02em;
  color: #0E1525;
  text-decoration: none;
  border-radius: 4px;
  -webkit-user-select: text;
  user-select: text;
  cursor: text;
  word-break: break-all;
  max-width: 100%;
}
.mail-addr:hover { text-decoration: underline; }
.mail-addr:focus-visible { outline: 0; box-shadow: 0 0 0 3px rgba(79, 108, 232, 0.28); }
.mail-copy {
  margin-top: 2px;
  padding: 6px 14px;
  border: 0;
  border-radius: var(--radius-xs);
  background: #EFF2F8;
  color: #394560;
  font: 500 var(--text-xs)/1 var(--font-mono);
  letter-spacing: 0.10em;
  text-transform: lowercase;
  cursor: pointer;
  box-shadow: inset 0 -1px 0 rgba(14, 21, 37, 0.08);
  transition: background var(--dur-fast) linear, color var(--dur-fast) linear, transform var(--dur-fast) var(--ease-out);
  -webkit-tap-highlight-color: transparent;
}
.mail-copy:hover { background: #DEE3EC; }
.mail-copy:active { transform: translateY(1px); }
.mail-copy:focus-visible { outline: 0; box-shadow: inset 0 -1px 0 rgba(14, 21, 37, 0.08), 0 0 0 3px rgba(79, 108, 232, 0.28); }
.mail-copy.is-copied { background: #CFEBD4; color: #146629; }

/* key pressed: the screen goes black for one beat, the OSD blinks off */
.device.is-entering .screen { background: #000; transition: background 60ms linear; }
.device.is-entering .screen::after,
.device.is-entering .osd,
.device.is-entering .screen video { opacity: 0; }
/* scrubbed past today: the screen nudges, like a stop at the end of a track */
.device.is-bump .screen { transform: translateX(4px); }

/* ---------- crt power-off ritual ---------------------------
   The one beat before a world opens. On key press, the screen dies like an
   old set losing power: the pane goes to black, a bright horizontal line
   flashes across the middle as the beam collapses, then converges to a dot
   and extinguishes. Held frames can be captured with ?ritual=hold (see JS).
   Runs on top of every existing screen child, then the shell's view
   transition takes over from black. Reduced-motion skips the whole ritual.

   Two tracks share the 240 ms:
     .screen-crt     the black backdrop that fills the pane
     .screen-crt::before  the bright line — first widens, then narrows to a
                          dot, then fades

   The line's peak brightness is a controlled ~85% white with a soft glow —
   never a full-screen white flash (photobiology, and it just looks cheap). */
.screen-crt {
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  background: #000;
  opacity: 0;
}
.screen-crt::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100%;
  height: 2px;
  margin-top: -1px;
  transform: translateX(-50%) scaleX(0);
  transform-origin: 50% 50%;
  background: rgba(255, 255, 255, 0.85);
  box-shadow: 0 0 6px rgba(255, 255, 255, 0.35);
  opacity: 0;
  border-radius: 1px;
}
.device.is-collapsing .screen-crt {
  animation: crt-veil 240ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.device.is-collapsing .screen-crt::before {
  animation: crt-beam 240ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
/* the screen's existing children fade fast while the veil rolls over them */
.device.is-collapsing .screen > *:not(.screen-crt) {
  animation: crt-content-fade 70ms linear forwards;
}
/* guard: bump/entering nudges shouldn't run during the ritual */
.device.is-collapsing .screen { transform: none; transition: none; }

@keyframes crt-veil {
  0%   { opacity: 0; }
  25%  { opacity: 1; }
  100% { opacity: 1; }
}
@keyframes crt-beam {
  0%   { transform: translateX(-50%) scaleX(0);    opacity: 0;   height: 2px; margin-top: -1px; }
  18%  { transform: translateX(-50%) scaleX(0.06); opacity: 0.9; height: 2px; margin-top: -1px; }
  38%  { transform: translateX(-50%) scaleX(1);    opacity: 1;   height: 2px; margin-top: -1px; }
  60%  { transform: translateX(-50%) scaleX(1);    opacity: 1;   height: 1.4px; margin-top: -0.7px; }
  82%  { transform: translateX(-50%) scaleX(0.06); opacity: 1;   height: 1.4px; margin-top: -0.7px; }
  100% { transform: translateX(-50%) scaleX(0);    opacity: 0;   height: 1.4px; margin-top: -0.7px; }
}
@keyframes crt-content-fade {
  from { opacity: 1; }
  to   { opacity: 0; }
}
/* debug hold: JS pauses the ritual mid-flight and freezes it at ?t=<ms>
   (defaults 110 = the bright-line moment). The animations still exist,
   they just don't tick — Web Animations pause via JS. */
html[data-ritual="hold"] .device.is-collapsing .screen { cursor: default; }

.intro {
  margin: 24px 0 0 68px;
  max-width: 52ch;
  font: 400 var(--text-base)/1.6 var(--font-ui);
  color: var(--ink-2);
}

/* ---------- keys: keycaps with a colour inlay ---------- */
.keys {
  position: absolute;
  right: 100px;
  top: calc(50% - 60px);
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: 236px;
}
/* a real keycap: raised, with a small colour chip inlaid on its left edge. the chip is set
   per key by console.js reading worlds.json (--k on the .key element). the shadow layers give
   it sidewall / travel; press flips them to inset so the cap sinks half a millimetre. */
.key {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  height: 56px;
  padding: 0 16px 0 26px;                     /* room for the inlay stripe */
  border-radius: var(--radius-md);
  background: var(--bg-surface);
  color: var(--ink-1);
  text-decoration: none;
  font: 500 var(--text-base)/1 var(--font-ui);
  /* layered keycap shadow: top edge highlight + bottom sidewall + soft ground shadow */
  box-shadow:
    inset 0 1px 0 var(--neo-hi),
    inset 0 -2px 0 var(--neo-lo-soft),
    -3px -3px 8px var(--neo-hi),
    4px 6px 14px var(--neo-lo);
  transition:
    box-shadow var(--dur-fast) var(--ease-out),
    background var(--dur-fast) linear,
    transform var(--dur-fast) var(--ease-out);
  -webkit-tap-highlight-color: transparent;
}
.key-inlay {
  position: absolute;
  left: 10px;
  top: 12px;
  bottom: 12px;
  width: 4px;
  border-radius: 2px;
  background: var(--k, var(--ink-4));
  box-shadow:
    inset 0 0 0 1px rgba(0, 0, 0, 0.18),
    0 0 8px color-mix(in srgb, var(--k, transparent) 40%, transparent);
  opacity: 0.9;
}
/* the CN classification (what lives inside): the primary read on a key. */
.key-name {
  font: 500 var(--text-md)/1.05 var(--font-cn);
  letter-spacing: 0.02em;
  color: var(--ink-1);
  flex: 1 1 auto;
  min-width: 0;
}
/* the world's own name, small, in that world's DS voice. each face is per-key via a family swap. */
.key-latin {
  font: 500 var(--text-xs)/1 var(--font-ui);
  letter-spacing: 0.04em;
  color: var(--ink-3);
  text-transform: lowercase;
  flex: 0 0 auto;
}
.key[data-world="aozora"]   .key-latin { font-family: "Aozora Latin",   var(--font-ui);   font-weight: 500; letter-spacing: 0.06em; }
.key[data-world="mayonaka"] .key-latin { font-family: "Mayonaka Latin", var(--font-mono); font-weight: 400; letter-spacing: 0.02em; font-size: 13px; }
.key[data-world="rift"]     .key-latin { font-family: "Rift Latin",     var(--font-ui);   font-weight: 500; letter-spacing: 0.08em; }
.key[data-world="kunlun"]   .key-latin { font-family: "Kunlun Latin",   var(--font-ui);   font-weight: 700; letter-spacing: 0.10em; text-transform: uppercase; font-size: 10px; }
.key[data-world="core"]     .key-latin { font-family: "Core Latin",     var(--font-ui);   font-weight: 500; letter-spacing: 0.02em; font-size: 13px; }
.key:hover {
  background: var(--bg-tint);
  box-shadow:
    inset 0 1px 0 var(--neo-hi),
    inset 0 -2px 0 var(--neo-lo-soft),
    -4px -4px 12px var(--neo-hi),
    6px 8px 18px var(--neo-lo);
}
.key:focus-visible { outline: 0; box-shadow:
    inset 0 1px 0 var(--neo-hi),
    inset 0 -2px 0 var(--neo-lo-soft),
    -3px -3px 8px var(--neo-hi),
    4px 6px 14px var(--neo-lo),
    var(--shadow-focus);
}
.key:active, .key.is-pressed {
  background: var(--bg-sunken);
  box-shadow:
    inset 3px 3px 7px var(--neo-lo),
    inset -2px -2px 5px var(--neo-hi);
  transform: translateY(1px);
}

/* ---------- timeline rail ----------------------------------- */
.rail {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 104px;
  cursor: ew-resize;
  touch-action: none;
  outline: 0;
  --rail-y: 46px;
  --pad: 64px;
}
.rail-line {
  position: absolute;
  left: 0; right: 0;
  top: var(--rail-y);
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(to right, transparent, var(--ink-1) 5%, var(--ink-1) 95%, transparent);
  opacity: 0.12;
}
.rail-marks i {
  position: absolute;
  top: calc(var(--rail-y) - 3px);
  width: 1px;
  height: 8px;
  background: var(--ink-4);
  transform: translateX(-0.5px);
}
.rail-marks span {
  position: absolute;
  top: calc(var(--rail-y) + 14px);
  transform: translateX(-50%);
  font: 500 var(--text-2xs)/1 var(--font-mono);
  letter-spacing: 0.04em;
  color: var(--ink-3);
  white-space: nowrap;
}
.rail-marks span.year { color: var(--ink-4); }
.rail-events b {
  position: absolute;
  top: calc(var(--rail-y) - 3px);
  width: 8px;
  height: 8px;
  margin-left: -4px;
  border-radius: 50%;
  background: var(--bg-surface);
  box-shadow: 0 0 0 2px var(--bg-base), inset 0 0 0 1.5px var(--primary-500);
}
.rail-events b.warm { box-shadow: 0 0 0 2px var(--bg-base), inset 0 0 0 1.5px var(--ember-500); }
.rail-head {
  position: absolute;
  top: calc(var(--rail-y) - 5px);
  width: 12px;
  height: 12px;
  margin-left: -6px;
  border-radius: 50%;
  background: var(--primary-500);
  box-shadow: 0 0 0 2px var(--bg-base), 0 0 14px var(--primary-glow);
  transition: transform var(--dur-fast) var(--ease-out);
}
.rail:focus-visible .rail-head { box-shadow: 0 0 0 2px var(--bg-base), 0 0 14px var(--primary-glow), var(--shadow-focus); }
.rail-readout {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: 12px;
  font: 500 var(--text-xs)/1 var(--font-mono);
  letter-spacing: 0.02em;
  color: var(--ink-2);
  white-space: nowrap;
}
.rail-readout .today { color: var(--ink-3); }
.rail.at-end .rail-readout { color: var(--ink-1); }

/* one quiet line beside the rail: what this thing is. fades on first real scrub (js sets .is-dismissed),
   and localStorage remembers you saw it so it never comes back. */
.rail-hint {
  position: absolute;
  right: var(--pad);
  bottom: 12px;
  margin: 0;
  font: 400 var(--text-2xs)/1 var(--font-mono);
  letter-spacing: 0.06em;
  color: var(--ink-3);
  opacity: 0.65;
  pointer-events: none;
  transition: opacity var(--dur-slow) var(--ease-out);
  animation: rail-hint-pulse 3.6s var(--ease-in-out) infinite;
}
.rail-hint.is-dismissed { opacity: 0; animation: none; }
@keyframes rail-hint-pulse {
  0%, 100% { opacity: 0.65; }
  50%      { opacity: 0.30; }
}

/* ---------- phone: keys become a bottom row, the rail stands up ---------- */
@media (max-width: 979px) {
  .wordmark { left: 16px; top: 20px; padding: 4px; }
  .wordmark-svg { height: 18px; }
  .lamp { right: 12px; top: 14px; }
  .lamp-light { top: -300px; right: -300px; }

  .stagewrap {
    left: -10px;
    right: -10px;
    top: 74px;
    transform: none;
    width: auto;
    min-width: 0;
    max-width: none;
  }
  .console-monitor { padding: 16px 14px 44px 14px; border-radius: var(--radius-xl); }
  .screen-frame { padding: 8px; border-radius: 14px / 11px; }
  .screen { border-radius: 10px / 8px; border-width: 6px; }
  /* on 390 the chin drops the port row (not enough room to keep the sockets legible
     next to the speaker); the moon + speaker + LED still read. ports return on desktop. */
  .chin { left: 14px; right: 14px; bottom: 10px; height: 24px; gap: 12px; grid-template-columns: auto 1fr auto; }
  .chin-mark { height: 12px; }
  .chin::before { width: 60px; }
  .io-ports { display: none; }
  .chin-led-well { grid-column: 3; }
  .chin-model { display: none; }         /* not enough room on 390 */
  .no-signal { max-width: 84%; padding: 12px 14px 10px; }
  .ns-head { font-size: var(--text-md); }
  .mail-plate { max-width: 84%; min-width: 0; padding: 12px 14px 10px; }
  .mail-head { font-size: var(--text-md); }
  .mail-addr { font-size: var(--text-sm); }
  .osd { left: 22px; bottom: 10px; }
  .daylog { left: 22px; right: 14px; top: 14px; bottom: 46px; font-size: var(--text-xs); line-height: 1.7; }
  .intro { margin: 18px 0 0 30px; padding-right: 72px; font-size: var(--text-sm); }

  .keys {
    left: 12px;
    right: 12px;
    bottom: 16px;
    top: auto;
    transform: none;
    width: auto;
    flex-direction: row;
    gap: 6px;
  }
  .key { flex: 1 1 0; height: 52px; padding: 4px 6px 4px 12px; flex-direction: column; align-items: flex-start; justify-content: center; gap: 2px; }
  .key-inlay { left: 4px; top: 8px; bottom: 8px; width: 3px; }
  /* on 390 the two labels stack: CN classification above, latin whisper below.
     smaller sizes; the DS font faces still swap per-key. */
  .key-name { font-size: var(--text-sm); letter-spacing: 0; line-height: 1.1; }
  .key-latin { font-size: 9px; letter-spacing: 0.04em; }
  .key[data-world="mayonaka"] .key-latin,
  .key[data-world="core"]     .key-latin { font-size: 10px; }
  .key[data-world="kunlun"]   .key-latin { font-size: 8px; letter-spacing: 0.08em; }

  .rail {
    left: auto;
    right: 14px;
    top: 320px;
    bottom: 84px;
    height: auto;
    width: 64px;
    cursor: ns-resize;
    --rail-x: 46px;
    --pad: 20px;
  }
  .rail-line {
    left: var(--rail-x); right: auto; top: 0; bottom: 0;
    width: 2px; height: auto;
    background: linear-gradient(to bottom, transparent, var(--ink-1) 5%, var(--ink-1) 95%, transparent);
  }
  .rail-marks i { top: auto; left: calc(var(--rail-x) - 3px); width: 8px; height: 1px; transform: translateY(-0.5px); }
  .rail-marks span { top: auto; left: calc(var(--rail-x) - 14px); transform: translate(-100%, -50%); }
  .rail-events b { top: auto; left: calc(var(--rail-x) - 3px); margin: -4px 0 0 0; }
  .rail-head { top: auto; left: calc(var(--rail-x) - 5px); margin: -6px 0 0 0; }
  .rail-readout {
    bottom: auto; left: auto; right: 100%; top: 50%;
    transform: translateY(-50%);
    margin: 0 12px 0 0;
  }
  /* on mobile the rail stands up on the right; the hint runs into events and there's no
     room to place it well — the vertical rail is already legible as an interactive object.
     the localStorage flag is set on the first scrub either way. */
  .rail-hint { display: none; }
}

/* ---------- reduced motion: the still device ---------------- */
@media (prefers-reduced-motion: reduce) {
  body { transition: none; }
  .lamp-dot { animation: none; }
  .lamp-light { transition: none; transform: none !important; }
  .console-monitor, .console-monitor::after, .screen, .key, .rail-head, .osd, .power-led { transition: none; animation: none; }
  .rail-hint { animation: none; }
  .device.is-bump .screen { transform: none; }
  /* the crt power-off ritual is a motion effect — skip it entirely, hand
     off to the shell's view transition immediately (JS also short-circuits
     the 240 ms hold). */
  .device.is-collapsing .screen-crt,
  .device.is-collapsing .screen-crt::before,
  .device.is-collapsing .screen > *:not(.screen-crt) { animation: none; opacity: 0; }
}
