/*!
 *      ██          ██
 *        ██      ██
 *      ██████████████
 *    ████  ██████  ████
 *  ██████████████████████
 *  ██  ██████████████  ██
 *  ██  ██          ██  ██
 *        ██████████
 *            ██
 *
 *  ahoy, stowaway. — sereja
 *//* Montserrat — self-hosted, split by script so a page only downloads
   the subsets it renders. 400 for reading, 400 italic for emphasis, 500
   for UI labels, 600 for display and chrome. */

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/montserrat-400-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/montserrat-400-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/montserrat-400-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/montserrat-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/montserrat-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Montserrat';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/montserrat-400italic-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}

@font-face {
  font-family: 'Montserrat';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/montserrat-400italic-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'Montserrat';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/montserrat-400italic-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Montserrat';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/montserrat-400italic-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/montserrat-500-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/montserrat-500-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/montserrat-500-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/montserrat-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/montserrat-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/assets/fonts/montserrat-600-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/assets/fonts/montserrat-600-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/assets/fonts/montserrat-600-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/assets/fonts/montserrat-600-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/assets/fonts/montserrat-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ── Design tokens ───────────────────────────────────────────────────
   One dark palette, one typeface, one radius family, one glass recipe.
   Every colour, size and distance on the site is one of these. Brand
   identity on the social links (YouTube red, the Instagram gradient,
   the TikTok split) is data, not theme, and only ever shows on hover. */
:root {
  /* Neutrals — a hair off pure black / white so long reads aren't
     clinical, strictly monochrome. The home rides a punchier pair. */
  --bg: #121212;
  --fg: #e8e8e8;
  --surface: #1c1c1c;

  /* Ink — four rungs. Every text picks one. */
  --ink:        var(--fg);
  --ink-body:   color-mix(in oklab, var(--fg) 86%, transparent);
  --ink-muted:  color-mix(in oklab, var(--fg) 62%, transparent);
  --ink-subtle: color-mix(in oklab, var(--fg) 45%, transparent);

  /* Strokes — rest / hover / pressed. */
  --border:          color-mix(in oklab, var(--fg) 9%, transparent);
  --border-strong:   color-mix(in oklab, var(--fg) 38%, transparent);
  --border-stronger: color-mix(in oklab, var(--fg) 52%, transparent);

  --surface-hover:   color-mix(in oklab, var(--fg) 7%, transparent);
  --surface-pressed: color-mix(in oklab, var(--fg) 14%, transparent);

  /* Glass — the floating chrome. Scrim — the veil under overlays. */
  --glass:       color-mix(in oklab, var(--bg) 50%, transparent);
  --glass-solid: color-mix(in oklab, var(--bg) 95%, transparent);
  --scrim:       color-mix(in oklab, var(--bg) 22%, transparent);
  --scrim-deep:  color-mix(in oklab, var(--bg) 88%, transparent);
  --blur: 8px;
  --lift: 0 10px 28px -10px rgb(0 0 0 / 0.75);

  /* Signal tints — feedback states only, never chrome. */
  --hud-on:  #7df9c8;
  --hud-off: #ff8a8a;
  --env-accent: #ff4d57;

  --radius-sm:   8px;
  --radius:      14px;
  --radius-pill: 999px;

  /* Type. Chrome speaks in 600 uppercase with tracking; reading copy in
     400. Never introduce a fourth weight. */
  --font: 'Montserrat', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Courier New', monospace;
  --weight:      600;
  --weight-ui:   500;
  --weight-body: 400;
  --tracking:    0.03em;

  --size-ui:    clamp(13px, 1.05vw, 16px);   /* chrome */
  --text-micro: 0.6875rem;                   /* 11px — counters, kbd */
  --text-meta:  0.8125rem;                   /* 13px — captions, meta, notes */
  --text-body:  0.9375rem;                   /* 15px — reading copy */
  --text-lede:  1.0625rem;                   /* 17px — lede, blockquote */
  --text-h4:    0.9375rem;
  --text-h3:    1.0625rem;                   /* 17px */
  --text-h2:    1.25rem;                     /* 20px */
  --text-title: 1.5rem;                      /* 24px — page title */
  --leading:    1.6;

  /* Motion — one duration, one easing. */
  --ease: cubic-bezier(.22,.8,.24,1);
  --dur:  220ms;

  /* Layout. The frame is the canvas width (per body[data-width]);
     header, canvas and toolkit share it. */
  --gutter:   clamp(1.25rem, 3vw, 3rem);
  --gutter-y: clamp(1.1rem, 2vw, 1.6rem);
  --header-clearance:  clamp(4.5rem, 9vh, 6rem);
  --toolkit-bottom:    clamp(1rem, 3vh, 1.75rem);
  --toolkit-clearance: clamp(4rem, 9vh, 5.5rem);
  --toc-width:         13rem;
  --toc-gap:           clamp(2.5rem, 4vw, 4rem);
  --frame-max: 680px;

  /* Home grid — set by the stage script; tokens keep the fallbacks. */
  --cell-w: 137px;
  --cell-h: 41px;
  --size: clamp(9px, calc(var(--cell-w) * 0.115), 52px);
}

body[data-width="ultra"]    { --frame-max: 1800px; }
body[data-width="wide"]     { --frame-max: 1400px; }
body[data-width="standard"] { --frame-max: 1000px; }
body[data-width="compact"]  { --frame-max: 680px;  }
body[data-width="showcase"] { --frame-max: 680px;  }
body[data-width="full"]     { --frame-max: none;   }

/* The home and the loader ride the punchier pair so the wall of glyphs
   reads crisp. */
body.page--home { --bg: #0f0f0f; --fg: #f2f2f2; }
/* ── Reset + document ────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  background: var(--bg);
  color: var(--fg);
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  scrollbar-width: thin;
  scrollbar-color: var(--border-stronger) transparent;
}

/* Chrome is the default voice; reading surfaces switch to the body
   voice themselves (.page-head, .prose, captions). */
body {
  min-height: 100dvh;
  font-family: var(--font);
  font-weight: var(--weight);
  font-size: var(--size-ui);
  letter-spacing: var(--tracking);
  line-height: normal;
  overscroll-behavior: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

a { color: inherit; }
button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
img, svg, video, canvas { display: block; max-width: 100%; }
[hidden] { display: none !important; }

:focus-visible {
  outline: 2px solid var(--ink-muted);
  outline-offset: 3px;
  border-radius: 4px;
}

::selection { background: var(--fg); color: var(--bg); }

/* Screen-reader only — for names the layout draws another way. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Button ──────────────────────────────────────────────────────────
   A soft filled pill: no outline, the fill deepens on hover and press. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.72em 1.35em;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--surface-hover);
  color: var(--fg);
  font-family: var(--font);
  font-weight: var(--weight);
  font-size: var(--size-ui);
  letter-spacing: var(--tracking);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition:
    background var(--dur) var(--ease),
    color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}

.btn:hover,
.btn:focus-visible {
  background: var(--surface-pressed);
  outline: none;
}

.btn[aria-pressed="true"] { background: var(--surface-pressed); }

.btn:disabled { cursor: default; }

.btn[data-state="ok"],
.btn[data-state="ok"]:hover {
  color: var(--hud-on);
  box-shadow: 0 0 18px -6px color-mix(in oklab, var(--hud-on) 50%, transparent);
}

.btn[data-state="fail"],
.btn[data-state="fail"]:hover {
  color: var(--hud-off);
  box-shadow: 0 0 18px -6px color-mix(in oklab, var(--hud-off) 50%, transparent);
}

/* ── Motion ──────────────────────────────────────────────────────────
   Cross-document view transitions for every same-origin navigation and
   the same-document hand-off of the x-default loader. The home grid and
   the socials carry named groups so they morph as persistent elements
   instead of crossfading with the root. */
@view-transition {
  navigation: auto;
}

::view-transition-group(root),
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 460ms;
  animation-timing-function: var(--ease);
}

::view-transition-group(sj-pattern),
::view-transition-old(sj-pattern),
::view-transition-new(sj-pattern),
::view-transition-group(sj-channels),
::view-transition-new(sj-channels) {
  animation-duration: 460ms !important;
  animation-timing-function: var(--ease) !important;
}

/* ── Header ──────────────────────────────────────────────────────────
   Fixed and transparent itself, sharing the canvas frame (--frame-max
   mirrored on body[data-width]) so the wordmark and the socials sit on
   the same axis as the content; the brand and the socials each rest on
   a glass pill — the toolkit's recipe — so they read over whatever
   scrolls under them. */
.site-header {
  position: fixed;
  inset: 0 0 auto;
  z-index: 10;
  padding-block: var(--gutter-y);
  text-transform: uppercase;
  pointer-events: none;
}

.site-header__inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1.5rem;
  margin-inline: auto;
  padding-inline: var(--gutter);
  max-width: calc(var(--frame-max, 1000px) + var(--gutter) * 2);
}

.site-header__inner > * { pointer-events: auto; }

body[data-width="full"] .site-header__inner { max-width: none; }

/* The glass bleeds outward by its own padding, so the text inside the
   pills still sits on the frame's edges. */
.site-header__brand,
.channels--header {
  padding: 0.5em 1em;
  background: var(--glass);
  backdrop-filter: blur(var(--blur));
  -webkit-backdrop-filter: blur(var(--blur));
  border-radius: var(--radius-pill);
}

.site-header__brand { margin-inline-start: -1em; }
.channels--header { margin-inline-end: -1em; }

.site-header__brand {
  display: flex;
  align-items: center;
  gap: clamp(0.65rem, 1.4vw, 0.9rem);
  flex-shrink: 0;
}

.site-header__brand .env-tag {
  padding-inline: 0.2em 0;
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* ── Logo ────────────────────────────────────────────────────────── */
.logo {
  display: inline-flex;
  align-items: center;
  color: var(--fg);
  text-decoration: none;
}

a.logo { outline: none; }

span.logo {
  cursor: default;
  user-select: none;
  -webkit-user-select: none;
}

.logo__mark {
  display: inline-flex;
  flex-shrink: 0;
  height: 1em;
  margin-inline-end: 0.5em;
}

.logo__mark svg {
  height: 100%;
  width: auto;
  display: block;
}

.logo__text { white-space: nowrap; }

/* ── Project crumb ───────────────────────────────────────────────────
   `<logo> / project #id` beside the wordmark on project pages. Much
   smaller and dimmer than the wordmark: the logo is the brand, the crumb
   is the position in the sequence. Click copies the numeric short-link. */
.logo-crumb {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  color: var(--ink-subtle);
  text-decoration: none;
  font-size: var(--text-micro);
  letter-spacing: 0.14em;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--dur) var(--ease);
}

.logo-crumb:hover,
.logo-crumb:focus-visible {
  color: var(--ink);
  outline: none;
}

.logo-crumb__sep {
  color: var(--ink-subtle);
  font-weight: var(--weight-body);
  opacity: 0.7;
}

.logo-crumb__hash { opacity: 0.7; }
.logo-crumb__id { font-variant-numeric: tabular-nums; }

.logo-crumb[data-state="ok"] > *,
.logo-crumb[data-state="fail"] > * { display: none; }

.logo-crumb[data-state="ok"]::after,
.logo-crumb[data-state="fail"]::after { white-space: nowrap; }

.logo-crumb[data-state="ok"] { color: var(--hud-on); }
.logo-crumb[data-state="ok"]::after { content: "✓ " attr(data-label-copied); }
.logo-crumb[data-state="fail"] { color: var(--hud-off); }
.logo-crumb[data-state="fail"]::after { content: "✗ " attr(data-label-failed); }

/* ── Socials ─────────────────────────────────────────────────────── */
.channels {
  display: flex;
  list-style: none;
}

.channels--header { gap: clamp(0.9rem, 1.4vw, 1.4rem); }

.channels a {
  color: var(--fg);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur) var(--ease), text-shadow var(--dur) var(--ease);
}

.channels a:hover,
.channels a:focus-visible {
  color: var(--channel-accent, var(--fg));
  outline: none;
}

/* Instagram — the official gradient painted into the label. */
.channels a[data-channel="instagram"]:hover,
.channels a[data-channel="instagram"]:focus-visible {
  background: linear-gradient(45deg, #F77737 0%, #FD1D1D 25%, #E1306C 50%, #C13584 75%, #833AB4 100%);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}

/* TikTok — the cyan / magenta split of the logo. */
.channels a[data-channel="tiktok"]:hover,
.channels a[data-channel="tiktok"]:focus-visible {
  color: var(--fg);
  text-shadow: -1.5px 0 0 #FE2C55, 1.5px 0 0 #25F4EE;
}

.channels__short { display: none; }
.channels__full  { display: inline; }

/* The header measures its own frame: when it is too tight for the full
   labels next to the brand, the socials switch to their short ones. */
.site-header__inner { container: header / inline-size; }

@container header (max-width: 640px) {
  .site-header__brand,
  .channels--header { padding: 0.45em 0.8em; }
  .site-header__brand { margin-inline-start: -0.8em; gap: 0.6rem; }
  .channels--header { gap: 0.75rem; margin-inline-end: -0.8em; }
  .channels--header .channels__full  { display: none; }
  .channels--header .channels__short { display: inline; }
  .logo-crumb { display: none; }
  .env-tag__label { display: none; }
  .env-tag { gap: 0; padding-inline: 0.1em 0; }
}

/* Per-character wrapper used by the glitch script. The 1em box keeps
   a swapped block glyph from pushing its neighbours; the family chain
   names the symbol fonts that ship the block characters on each OS. */
.gl-ch {
  display: inline-block;
  line-height: 1;
  height: 1em;
  vertical-align: baseline;
  overflow: visible;
  font-family:
    'Montserrat',
    'Segoe UI Symbol', 'Cascadia Mono',
    'Apple Symbols', 'Menlo',
    'DejaVu Sans Mono', 'Noto Mono',
    system-ui, sans-serif;
}

/* ── Build label ─────────────────────────────────────────────────────
   Glass pill with a pulsing dot on non-production builds, no outline:
   inline next to the wordmark on inner pages, floating top-centre on
   the home. */
.env-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  padding: 0.38em 0.85em 0.38em 0.75em;
  background: var(--glass);
  backdrop-filter: blur(var(--blur));
  -webkit-backdrop-filter: blur(var(--blur));
  border-radius: var(--radius-pill);
  color: var(--ink);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}

.stage .env-tag {
  position: fixed;
  top: var(--gutter-y);
  left: 50%;
  transform: translateX(-50%);
  z-index: 11;
  font-size: 0.72rem;
}

.env-tag__dot {
  position: relative;
  width: 0.55em;
  height: 0.55em;
  border-radius: 50%;
  background: var(--env-accent);
  animation: env-tag-pulse 1.6s var(--ease) infinite;
}

.env-tag__dot::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--env-accent);
  animation: env-tag-halo 1.6s var(--ease) infinite;
}

@keyframes env-tag-pulse {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 1; }
}

@keyframes env-tag-halo {
  0%   { transform: scale(1);   opacity: 0.55; }
  70%  { transform: scale(2.6); opacity: 0; }
  100% { transform: scale(2.6); opacity: 0; }
}

/* ── Chrome over a cover ─────────────────────────────────────────────
   A fullscreen piece can be any colour. The chrome script samples the
   picture under the top and the bottom band and flips the ink per band
   (data-chrome-top / data-chrome-bottom); `chrome: light | dark` in
   frontmatter pins both. The dark ink is the same recipe on a light
   ground — the same glass at the same opacity, the same blur — so both
   looks are one design code. Derived tokens are re-declared because
   custom properties resolve where they are defined, not where they are
   used. */
body[data-canvas="cover"][data-chrome-top="dark"] .site-header,
body[data-canvas="cover"][data-chrome-bottom="dark"] :is(.toolkit, .canvas-widths, .piece__caption) {
  --bg: #f2f2f2;
  --fg: #101010;
  --ink: var(--fg);
  --ink-body: color-mix(in oklab, var(--fg) 86%, transparent);
  --ink-muted: color-mix(in oklab, var(--fg) 62%, transparent);
  --ink-subtle: color-mix(in oklab, var(--fg) 45%, transparent);
  --border: color-mix(in oklab, var(--fg) 9%, transparent);
  --border-strong: color-mix(in oklab, var(--fg) 38%, transparent);
  --border-stronger: color-mix(in oklab, var(--fg) 52%, transparent);
  --surface-hover: color-mix(in oklab, var(--fg) 7%, transparent);
  --surface-pressed: color-mix(in oklab, var(--fg) 14%, transparent);
  --glass: color-mix(in oklab, var(--bg) 50%, transparent);
  --glass-solid: color-mix(in oklab, var(--bg) 95%, transparent);
  color: var(--fg);
}
/* ── Toolkit ─────────────────────────────────────────────────────────
   The floating glass pill at the bottom of every route: copy link,
   native share, language popover. On the x-default loader it stays
   hidden until the locale swap. */
.toolkit {
  position: fixed;
  bottom: var(--toolkit-bottom);
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.55rem;
  width: max-content;
  max-width: calc(100vw - 2rem);
  background: var(--glass);
  backdrop-filter: blur(var(--blur));
  -webkit-backdrop-filter: blur(var(--blur));
  border-radius: var(--radius);
}

body.page--loader .toolkit { display: none; }

.toolkit__group {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.toolkit__btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.85rem;
  height: 1.85rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--fg);
  transition:
    background var(--dur) var(--ease),
    color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}

.toolkit__btn:hover,
.toolkit__btn:focus-visible {
  background: var(--surface-hover);
  outline: none;
}

/* Text variant — the language toggle and the width switcher. */
.toolkit__btn--text {
  width: auto;
  padding-inline: 0.7rem;
  font-weight: var(--weight);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.toolkit__btn[aria-pressed="true"] { background: var(--surface-pressed); }

/* Feedback: mint = done, coral = failed; the icon swaps to a check. */
.toolkit__btn[data-state="ok"],
.toolkit__btn[data-state="ok"]:hover {
  color: var(--hud-on);
  background: transparent;
  box-shadow: 0 0 14px -6px color-mix(in oklab, var(--hud-on) 50%, transparent);
}

.toolkit__btn[data-state="fail"],
.toolkit__btn[data-state="fail"]:hover {
  color: var(--hud-off);
  background: transparent;
  box-shadow: 0 0 14px -6px color-mix(in oklab, var(--hud-off) 50%, transparent);
}

.toolkit__icon {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 0.95rem;
  height: 0.95rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: opacity var(--dur) var(--ease);
}

.toolkit__icon--ok { opacity: 0; }
.toolkit__btn[data-state="ok"] .toolkit__icon:not(.toolkit__icon--ok) { opacity: 0; }
.toolkit__btn[data-state="ok"] .toolkit__icon--ok { opacity: 1; }

/* Language group — a hairline separates it from the share group. */
.toolkit__lang {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding-left: 0.65rem;
}

.toolkit__lang::before {
  content: '';
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 1px;
  height: 60%;
  background: var(--border-strong);
}

.toolkit__lang[data-open="true"] [data-toolkit-lang-toggle] { background: var(--surface-hover); }

/* Popover — lifts upward from the toggle, near-opaque so it reads over
   the home grid. */
.toolkit__lang-list {
  position: absolute;
  bottom: calc(100% + 0.5rem);
  right: 0;
  z-index: 11;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 9.5rem;
  padding: 0.3rem;
  list-style: none;
  background: var(--glass-solid);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-radius: var(--radius);
  box-shadow: var(--lift);
  opacity: 0;
  transform: translateY(4px);
  pointer-events: none;
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.toolkit__lang[data-open="true"] .toolkit__lang-list {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

.toolkit__lang-option {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.45rem 0.65rem;
  border-radius: var(--radius-sm);
  color: var(--ink-body);
  font-weight: var(--weight-body);
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  text-decoration: none;
  white-space: nowrap;
  outline: none;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}

.toolkit__lang-option:hover,
.toolkit__lang-option:focus-visible {
  background: var(--surface-hover);
  color: var(--fg);
}

.toolkit__lang-option[aria-checked="true"] {
  color: var(--fg);
  background: var(--surface-hover);
  cursor: default;
  pointer-events: none;
}

.toolkit__lang-code {
  flex-shrink: 0;
  min-width: 1.6em;
  font-weight: var(--weight);
  letter-spacing: 0.06em;
  color: var(--fg);
}

.toolkit__lang-name { line-height: 1; }

/* ── Contents ────────────────────────────────────────────────────────
   On screens without room for the rail the table of contents lives
   here, the leftmost control: the button opens a glass panel above the
   pill with the same list (scripts/modules/toc.ts moves it over). */
.toolkit__contents { display: inline-flex; }

.toolkit__toc {
  position: absolute;
  bottom: calc(100% + 0.5rem);
  left: 50%;
  z-index: 11;
  width: max-content;
  min-width: 14rem;
  max-width: calc(100vw - 2rem);
  max-height: min(60vh, 28rem);
  overflow: auto;
  padding: 0.85rem 1.1rem 1rem;
  background: var(--glass-solid);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-radius: var(--radius);
  box-shadow: var(--lift);
  font-weight: var(--weight-body);
  font-size: var(--text-meta);
  line-height: 1.55;
  letter-spacing: 0;
  text-transform: none;
  text-align: left;
  scrollbar-width: thin;
  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, 4px);
  pointer-events: none;
  transition:
    opacity var(--dur) var(--ease),
    transform var(--dur) var(--ease),
    visibility 0s linear var(--dur);
}

.toolkit__contents[data-open] .toolkit__toc {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
  pointer-events: auto;
  transition-delay: 0s;
}

.toolkit__contents[data-open] [data-toolkit-toc-toggle] { background: var(--surface-hover); }

.toolkit__toc-label {
  display: block;
  margin-bottom: 0.45rem;
  font-family: var(--font);
  font-weight: var(--weight);
  font-size: var(--text-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-subtle);
}

/* ── Canvas width switcher ───────────────────────────────────────────
   A debug control that reuses the toolkit's pill and text buttons,
   floating just above it. Rendered hidden on every inner page; the
   canvas script reveals it only when the URL carries `?width=`. */
.canvas-widths {
  position: fixed;
  bottom: calc(var(--toolkit-bottom) + 3.1rem);
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.35rem;
  padding: 0.35rem 0.55rem;
  width: max-content;
  max-width: calc(100vw - 2rem);
  background: var(--glass);
  backdrop-filter: blur(var(--blur));
  -webkit-backdrop-filter: blur(var(--blur));
  border-radius: var(--radius);
}
/* ── Canvas ──────────────────────────────────────────────────────────
   The frame is the page's canvas width (body[data-width]) — compact,
   standard, wide, ultra, full, showcase — and it applies to everything
   inside: text, media, grids. The mode (body[data-canvas]) says how the
   frame behaves: `page` scrolls, `showcase` centres, `cover` pins a
   media piece fullscreen (media.css). */
.canvas {
  min-height: 100vh;
  padding: var(--header-clearance) var(--gutter) var(--toolkit-clearance);
}

.canvas__inner {
  width: 100%;
  max-width: var(--frame-max);
  margin-inline: auto;
}

body[data-width="full"] .canvas__inner { max-width: none; }

/* Showcase — formal vertical centring. --stage-h is the JS-measured
   visual viewport (mobile WebKit resolves 100dvh past the visible area
   and would drift the piece); dvh is the no-JS fallback. Content taller
   than the stage simply scrolls. */
body[data-canvas="showcase"] .canvas {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--stage-h, 100dvh);
}

/* Cover — the piece is pinned under the chrome; the page never scrolls. */
body[data-canvas="cover"] {
  height: 100dvh;
  overflow: hidden;
}

body[data-canvas="cover"] .canvas { min-height: 0; }

/* ── Page title block ────────────────────────────────────────────── */
.page-head {
  margin-bottom: clamp(1.5rem, 4vh, 2.5rem);
  font-weight: var(--weight-body);
  letter-spacing: 0;
  text-transform: none;
}

.page-head__title {
  font-weight: var(--weight);
  font-size: var(--text-title);
  line-height: 1.2;
  letter-spacing: -0.005em;
  color: var(--ink);
  text-wrap: balance;
}

.page-head__lede {
  margin-top: 0.65rem;
  font-size: var(--text-lede);
  line-height: 1.5;
  color: var(--ink);
  text-wrap: pretty;
}

/* ── Table of contents ───────────────────────────────────────────────
   `toc: true`. Where the screen leaves room beside the column the list
   is a rail to the right of it that follows the scroll, the current
   section marked with a dot; elsewhere the script moves the same list
   into the toolkit's contents panel (toolkit.css) and hides this nav. */
.page { position: relative; }

.toc {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(100% + var(--toc-gap));
  width: var(--toc-width);
  font-weight: var(--weight-body);
  font-size: var(--text-meta);
  line-height: 1.55;
  letter-spacing: 0;
  text-transform: none;
}

.toc[data-mode="toolkit"] { display: none; }

.toc__sticky {
  position: sticky;
  top: var(--header-clearance);
}

.toc__label {
  display: block;
  margin-bottom: 0.75rem;
  font-family: var(--font);
  font-weight: var(--weight);
  font-size: var(--text-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-subtle);
}

.toc__list { list-style: none; }
.toc__list ol { padding-left: 1em; list-style: none; }
.toc__list li + li,
.toc__list ol { margin-top: 0.3em; }

.toc__list a {
  position: relative;
  display: block;
  color: var(--ink-subtle);
  text-decoration: none;
  transition: color var(--dur) var(--ease);
}

.toc__list a:hover,
.toc__list a:focus-visible { color: var(--ink); outline: none; }
.toc__list a[aria-current="true"] { color: var(--ink); }

.toc .toc__list a[aria-current="true"]::before {
  content: '';
  position: absolute;
  left: -1em;
  top: 0.62em;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
}

/* ── Prose ───────────────────────────────────────────────────────────
   Reading copy. Every Markdown element, plus the raw-HTML blocks the
   content may carry. Vertical rhythm is one body line between blocks;
   headings carry their own air above. */
.prose {
  color: var(--ink-body);
  font-weight: var(--weight-body);
  font-size: var(--text-body);
  line-height: var(--leading);
  letter-spacing: 0;
  text-transform: none;
  text-wrap: pretty;
}

.prose > * + * { margin-top: 1em; }

/* Headings — anchored below the fixed header when linked to. */
.prose :is(h2, h3, h4)[id] { scroll-margin-top: calc(var(--header-clearance) + 0.5rem); }

.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
  position: relative;
  font-weight: var(--weight);
  color: var(--ink);
  line-height: 1.3;
  text-wrap: balance;
}

.prose h1 { font-size: var(--text-title); line-height: 1.2; letter-spacing: -0.005em; margin-top: 2.2em; }
.prose h2 { font-size: var(--text-h2); letter-spacing: -0.005em; margin-top: 2.4em; }
.prose h3 { font-size: var(--text-h3); margin-top: 2em; }
.prose h4 { font-size: var(--text-h4); margin-top: 1.75em; }
.prose h5,
.prose h6 {
  font-size: var(--text-meta);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-top: 1.75em;
}

.prose > :is(h1, h2, h3, h4, h5, h6) + * { margin-top: 0.6em; }
.prose > :is(h1, h2, h3, h4, h5, h6):first-child { margin-top: 0; }

/* Anchor — a quiet `#` that appears on hover / focus. */
.prose .anchor {
  position: absolute;
  left: -1.25em;
  width: 1.25em;
  color: var(--ink-subtle);
  text-decoration: none;
  opacity: 0;
  transition: opacity var(--dur) var(--ease), color var(--dur) var(--ease);
}

.prose .anchor::before { content: '#'; }

.prose :is(h2, h3, h4):hover .anchor,
.prose .anchor:focus-visible { opacity: 1; outline: none; }
.prose .anchor:hover { color: var(--ink); }

@media (max-width: 48em) {
  .prose .anchor { position: static; margin-left: 0.4em; width: auto; }
}

/* Inline */
.prose a {
  color: var(--fg);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 0.2em;
  transition: text-decoration-color var(--dur) var(--ease);
}

.prose a:hover,
.prose a:focus-visible {
  text-decoration-color: var(--fg);
  outline: none;
}

.prose strong { color: var(--fg); font-weight: var(--weight); }
.prose em { font-style: italic; }
.prose mark { background: color-mix(in oklab, var(--fg) 16%, transparent); color: var(--fg); border-radius: 3px; padding: 0.05em 0.25em; }
.prose del { color: var(--ink-subtle); text-decoration-color: var(--ink-subtle); }
.prose ins { text-decoration: none; border-bottom: 1px solid var(--border-stronger); }
.prose abbr[title] { text-decoration: underline dotted; text-underline-offset: 0.2em; cursor: help; }
.prose small { font-size: var(--text-meta); color: var(--ink-muted); }
.prose sub, .prose sup { font-size: 0.75em; line-height: 0; }

.prose code,
.prose kbd,
.prose samp,
.prose var {
  font-family: var(--mono);
  font-style: normal;
  font-size: 0.875em;
  padding: 0.1em 0.4em;
  color: var(--ink);
  border-radius: var(--radius-sm);
  background: color-mix(in oklab, var(--fg) 7%, transparent);
}

.prose kbd {
  padding: 0.15em 0.5em;
  font-size: var(--text-micro);
  letter-spacing: 0.04em;
  background: color-mix(in oklab, var(--fg) 11%, transparent);
}

/* Lists */
.prose ul,
.prose ol { padding-left: 1.3em; }
.prose li + li,
.prose li > ul,
.prose li > ol { margin-top: 0.35em; }
.prose li::marker { color: var(--ink-subtle); }
.prose ol { list-style: decimal; }
.prose ol li::marker { font-variant-numeric: tabular-nums; }

/* Task lists (GFM) */
.prose li:has(> input[type="checkbox"]) { list-style: none; margin-left: -1.3em; padding-left: 1.6em; position: relative; }
.prose li > input[type="checkbox"] {
  position: absolute;
  left: 0;
  top: 0.35em;
  appearance: none;
  width: 0.95em;
  height: 0.95em;
  border: 1px solid var(--border-stronger);
  border-radius: 4px;
  background: transparent;
}
.prose li > input[type="checkbox"]:checked { background: var(--fg); border-color: var(--fg); }
.prose li > input[type="checkbox"]:checked::after {
  content: '';
  position: absolute;
  left: 0.28em;
  top: 0.08em;
  width: 0.28em;
  height: 0.5em;
  border: solid var(--bg);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

/* Definition lists */
.prose dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.45rem 1.25rem; }
.prose dt { color: var(--ink); font-weight: var(--weight); font-size: var(--text-meta); }
.prose dd { margin: 0; }
@media (max-width: 40em) {
  .prose dl { grid-template-columns: 1fr; gap: 0.15rem; }
  .prose dd + dt { margin-top: 0.75rem; }
}

/* Quotes */
.prose blockquote {
  padding: 0.15rem 0 0.15rem 1.15rem;
  border-left: 2px solid var(--border-strong);
  font-size: var(--text-lede);
  line-height: 1.5;
  color: var(--ink);
}

.prose blockquote p + p { margin-top: 0.75rem; }

.prose blockquote cite,
.prose blockquote footer {
  display: block;
  margin-top: 0.6rem;
  font-size: var(--text-meta);
  font-style: normal;
  color: var(--ink-muted);
}

/* Code blocks — Shiki paints the tokens; the frame is ours. */
.prose pre {
  padding: 1rem 1.15rem;
  overflow-x: auto;
  border-radius: var(--radius);
  background: var(--surface) !important;
  font-size: 0.875em;
  line-height: 1.55;
  color: var(--fg);
  scrollbar-width: thin;
}

.prose pre code {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  font-size: inherit;
  white-space: pre;
}

/* Rules */
.prose hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin-block: clamp(2rem, 4vh, 3rem);
}

/* Tables — scroll inside their box when wider than the column. */
.prose .table-scroll {
  overflow-x: auto;
  scrollbar-width: thin;
  -webkit-overflow-scrolling: touch;
}

.prose table {
  width: 100%;
  min-width: 28rem;
  border-collapse: collapse;
  font-size: var(--text-meta);
  line-height: 1.5;
}

.prose th,
.prose td {
  padding: 0.6rem 1rem 0.6rem 0;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--border);
}

.prose th { font-weight: var(--weight); color: var(--ink-muted); }
.prose td { color: var(--ink-body); }
.prose :is(th, td)[align="right"] { text-align: right; padding-right: 0; padding-left: 1rem; font-variant-numeric: tabular-nums; }
.prose :is(th, td)[align="center"] { text-align: center; }

/* Details */
.prose details {
  padding: 0.85rem 1.1rem;
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--fg) 4%, transparent);
}

.prose summary {
  cursor: pointer;
  font-weight: var(--weight);
  color: var(--ink);
  list-style: none;
}

.prose summary::-webkit-details-marker { display: none; }

.prose summary::before {
  content: '+';
  display: inline-block;
  width: 1.2em;
  color: var(--ink-subtle);
  transition: transform var(--dur) var(--ease);
}

.prose details[open] summary::before { content: '−'; }
.prose details[open] summary { margin-bottom: 0.6rem; }
.prose details > :not(summary) + * { margin-top: 0.75em; }

/* Footnotes (GFM) */
.prose .footnotes {
  margin-top: clamp(2rem, 4vh, 3rem);
  font-size: var(--text-meta);
  color: var(--ink-muted);
}

.prose .footnotes h2 { display: none; }
.prose .footnotes ol { padding-left: 1.4em; }
.prose .footnotes li + li { margin-top: 0.3em; }
.prose .footnotes a[data-footnote-backref] { text-decoration: none; }
.prose sup a[data-footnote-ref] { text-decoration: none; padding: 0 0.15em; }

/* ── Cross-links to the rest of the gallery ──────────────────────────
   The last paragraph of a write-up (inside .prose) or the last line of
   a caption: the same voice as the text around it, a little air above. */
.prose .more { margin-top: 2.2em; }
.more__label { color: var(--ink-muted); }
/* ── Media in prose ──────────────────────────────────────────────────
   A paragraph of images becomes a figure (rehypeGallery): one image at
   frame width, or a grid of two to four. Raw HTML with the same classes
   works too. Media never leaves the frame; the width preset is what
   makes it bigger. */
.prose img,
.prose video {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  background: var(--surface);
}

.prose figure,
.prose .media { margin-block: clamp(1.5rem, 3vh, 2rem); }
.prose figure > :where(img, video, picture) { margin: 0; }

.prose figure img { cursor: zoom-in; }
.prose video { cursor: pointer; }

.media__grid {
  display: grid;
  gap: 0.75rem;
}

.media--2 .media__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.media--3 .media__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.media--4 .media__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }

@media (max-width: 40em) {
  .media__grid { gap: 0.5rem; }
  .media--4 .media__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.prose figcaption,
.caption__text {
  margin-top: 0.7rem;
  font-family: var(--font);
  font-weight: var(--weight-body);
  font-size: var(--text-meta);
  line-height: 1.5;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-muted);
}

/* Embeds — `<div class="embed"><iframe …></iframe></div>` in raw HTML. */
.prose .embed {
  position: relative;
  aspect-ratio: var(--ratio, 16 / 9);
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--surface);
}

.prose .embed > iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* ── Media projects — one piece, like a painting ─────────────────────
   The piece fills the frame width at its own ratio (--ratio from the
   file or frontmatter), scaled down to fit the visible stage, whole and
   never cropped; caption under it. The canvas centres it (showcase). */
.piece {
  --cap: calc(var(--stage-h, 100dvh) - var(--header-clearance) - var(--toolkit-clearance) - 9rem);
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  margin: 0;
}

.piece__media {
  display: block;
  width: min(100%, calc(var(--cap) * var(--ratio, 16 / 9)));
  height: auto;
  aspect-ratio: var(--ratio, 16 / 9);
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--surface);
}

img.piece__media { cursor: zoom-in; }
video.piece__media { cursor: pointer; }

.piece__caption {
  margin-top: 1.1rem;
  text-align: center;
}

.piece__caption .caption__text {
  max-width: 36rem;
  margin-inline: auto;
}

.caption__title {
  font-size: var(--size-ui);
  font-weight: var(--weight);
  letter-spacing: var(--tracking);
  text-transform: uppercase;
  color: var(--ink);
}

.piece__caption .more { margin-top: 0.5rem; }

/* ── Cover — `width: full` on a media piece ──────────────────────────
   The same markup pinned fullscreen under the chrome, object-fit cover;
   the caption rests bottom-left above the toolkit on a glass panel, so
   it reads over any picture. The ink follows chrome.css. */
body[data-canvas="cover"] .piece__media {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  border-radius: 0;
  cursor: default;
  background: var(--bg);
}

body[data-canvas="cover"] .piece__caption {
  position: fixed;
  left: var(--gutter);
  bottom: var(--toolkit-clearance);
  z-index: 1;
  max-width: 34rem;
  margin: 0;
  padding: 0.95rem 1.15rem 1rem;
  text-align: left;
  background: var(--glass);
  backdrop-filter: blur(var(--blur));
  -webkit-backdrop-filter: blur(var(--blur));
  border-radius: var(--radius);
}

body[data-canvas="cover"] .piece__caption .caption__text { margin-inline: 0; }

/* Pixel art keeps its pixels whatever the frame does. */
.piece__media--pixelated { image-rendering: pixelated; }
/* ── Lightbox ────────────────────────────────────────────────────────
   Native <dialog>: any image in the prose or a media piece opens at
   full size over a deep scrim, with the caption under it and prev /
   next between the page's images. Esc, the close control, the backdrop
   and a swipe all close it. */
.lightbox {
  width: 100vw;
  height: 100dvh;
  max-width: none;
  max-height: none;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--fg);
  overflow: hidden;
}

.lightbox::backdrop {
  background: var(--scrim-deep);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

.lightbox[open] { animation: lightbox-in var(--dur) var(--ease) both; }

@keyframes lightbox-in {
  from { opacity: 0; }
}

.lightbox__figure {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.9rem;
  margin: 0;
  padding: var(--header-clearance) var(--gutter) var(--toolkit-clearance);
}

.lightbox__img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  border-radius: var(--radius);
  animation: lightbox-img var(--dur) var(--ease) both;
}

@keyframes lightbox-img {
  from { opacity: 0; transform: scale(0.985); }
}

.lightbox__caption {
  max-width: 40rem;
  text-align: center;
  font-weight: var(--weight-body);
  font-size: var(--text-meta);
  line-height: 1.5;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-muted);
}

.lightbox__caption:empty { display: none; }

.lightbox__close,
.lightbox__count {
  position: absolute;
  top: var(--gutter-y);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.lightbox__close {
  right: var(--gutter);
  padding: 0.5em 0.9em;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--glass);
  backdrop-filter: blur(var(--blur));
  -webkit-backdrop-filter: blur(var(--blur));
  color: var(--ink-muted);
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.lightbox__close:hover,
.lightbox__close:focus-visible { color: var(--ink); background: var(--surface-pressed); outline: none; }

.lightbox__count {
  left: var(--gutter);
  padding: 0.5em 0;
  color: var(--ink-subtle);
  font-variant-numeric: tabular-nums;
}

.lightbox__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 2.6rem;
  height: 2.6rem;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--glass);
  backdrop-filter: blur(var(--blur));
  -webkit-backdrop-filter: blur(var(--blur));
  color: var(--ink);
  transition: background var(--dur) var(--ease), opacity var(--dur) var(--ease);
}

.lightbox__nav:hover,
.lightbox__nav:focus-visible { background: var(--surface-pressed); outline: none; }
.lightbox__nav--prev { left: var(--gutter); }
.lightbox__nav--next { right: var(--gutter); }
.lightbox__nav[hidden] { display: none; }

.lightbox__nav svg {
  width: 1rem;
  height: 1rem;
  margin: auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

@media (max-width: 40em) {
  .lightbox__nav { display: none; }
  .lightbox__figure { padding-inline: 0.75rem; }
}

body[data-lightbox] { overflow: hidden; }

/* ── 404 + Offline ───────────────────────────────────────────────────
   One scaffold for both: a large code, a passively glitching label in
   the chrome voice and one pill button, centred in the frame. Selectors
   are grouped so the two never drift. */
.not-found,
.offline {
  min-height: calc(100dvh - var(--header-clearance) - var(--toolkit-clearance));
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--gutter);
}

.not-found__code,
.offline__code {
  font-size: clamp(3rem, 8vw, 5rem);
  line-height: 1;
  letter-spacing: 0.04em;
}

.not-found__label,
.offline__label {
  margin-top: 0.9rem;
  font-size: var(--text-meta);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-subtle);
}

.not-found__back,
.offline__retry { margin-top: clamp(1.5rem, 4vh, 2.5rem); }

/* Site-wide offline overlay — the same chrome painted over any route
   when the browser goes offline. Pinned to the inner-page neutrals so
   it reads the same over the home. */
.offline-overlay {
  --bg: #121212;
  --fg: #e8e8e8;
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  background: var(--bg);
  color: var(--fg);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease);
}

body[data-offline] .offline-overlay {
  opacity: 1;
  pointer-events: auto;
}

body[data-offline] { overflow: hidden; }

/* ── Hidden console ──────────────────────────────────────────────────
   Backquote / triple-tap. A glass panel in the toolkit's vocabulary:
   near-opaque glass, the site radius and lift, no outline and no rules
   inside. The buffer is monospace; the chrome is a title and "esc". */
.console {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: grid;
  align-items: start;
  justify-items: center;
  padding: clamp(2rem, 9vh, 6rem) 1rem 1rem;
  background: var(--scrim);
  backdrop-filter: blur(var(--blur));
  -webkit-backdrop-filter: blur(var(--blur));
  animation: console-fade var(--dur) var(--ease) both;
}

@keyframes console-fade {
  from { opacity: 0; }
}

.console__window {
  display: flex;
  flex-direction: column;
  width: min(640px, 100%);
  height: min(400px, 64vh);
  overflow: hidden;
  background: var(--glass-solid);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-radius: var(--radius);
  box-shadow: var(--lift);
}

.console__head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 14px 0;
}

.console__title {
  font-family: var(--font);
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-subtle);
  user-select: none;
  -webkit-user-select: none;
}

.console__close {
  padding: 4px 6px;
  font-family: var(--font);
  font-size: 10.5px;
  letter-spacing: var(--tracking);
  text-transform: uppercase;
  color: var(--ink-subtle);
  transition: color var(--dur) var(--ease);
}

.console__close:hover,
.console__close:focus-visible { color: var(--ink); outline: none; }

.console__body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.55;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
}

.console__output {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 6px 14px 14px;
  scrollbar-width: none;
}

.console__output::-webkit-scrollbar { display: none; }

.console__line { white-space: pre; }

/* Boot logo — tight leading so the block glyphs read as one figure. */
.console__line--art {
  text-align: center;
  color: var(--ink-subtle);
  line-height: 1;
  font-size: 9px;
}

.console__line--art:last-of-type { margin-bottom: 0.7em; }
.console__line--hint  { color: var(--ink-subtle); margin-bottom: 0.7em; white-space: normal; }
.console__line--echo  { color: var(--ink-muted); }
.console__line--error { color: var(--hud-off); }

.console__form {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.55em;
  align-items: baseline;
}

.console__prompt {
  color: var(--ink-muted);
  user-select: none;
  -webkit-user-select: none;
}

.console__input {
  appearance: none;
  min-width: 0;
  border: 0;
  outline: 0;
  background: transparent;
  color: var(--ink);
  font: inherit;
  caret-color: var(--ink);
}

@media (max-width: 45em) {
  .console { padding: 0; }
  .console__window {
    width: 100%;
    height: 100dvh;
    max-height: 100dvh;
    border-radius: 0;
    box-shadow: none;
  }
  .console__head { padding-top: max(8px, env(safe-area-inset-top)); }
}

/* ── Home stage ──────────────────────────────────────────────────────
   A wall of the brand phrase drawn on a canvas (one element, any
   screen), the socials in a hole at the centre, the toolkit and the
   build label floating over it. The stage script sets --cell-w,
   --cell-h and --size on :root and places the socials nav on the grid. */
.stage {
  position: fixed;
  inset: 0;
  overflow: hidden;
  isolation: isolate;
  text-transform: uppercase;
}

canvas.pattern {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.stage canvas.pattern { view-transition-name: sj-pattern; }

.channels--center {
  position: absolute;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: var(--cell-w);
  grid-auto-rows: var(--cell-h);
  font-size: var(--size);
}

/* Only the target's real socials get the named slot — the loader's nav
   is an empty spacer. */
body:not(.page--loader) .channels--center { view-transition-name: sj-channels; }

.channels--center > a,
.channels--center > span {
  display: flex;
  align-items: center;
  justify-content: center;
}

body.page--home {
  height: 100%;
  overflow: hidden;
}

/* Loader label, dead-centre in the hole. */
.loader-status {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  margin: 0;
  font-size: var(--size);
  letter-spacing: var(--tracking);
  color: var(--fg);
  white-space: nowrap;
  pointer-events: none;
}

/* Typed-in dots after the label; the loader swaps them on a fixed
   tick. The box reserves three-dot width so the label never reflows. */
.loader-status__dots {
  display: inline-block;
  margin-left: 0.35em;
  letter-spacing: 0.25em;
  min-width: 2.4em;
  text-align: left;
}
