/* =================================================================
   rishabh0111.github.io: style.css  v3
   Palette: Sanzo Wada "Indigo Lacquer"
   
   Design language: the feel of a 1960s Italian cultural magazine
   (Domus, Casabella) printed on uncoated stock. Strong typographic
   architecture, generous white field, moments of unexpected colour.
   Not a museum. Not a brutalist blog. Something with opinions.

   Dark Reader fix:
   · color-scheme: light dark tells DR the site handles both
   · @media (prefers-color-scheme: dark) provides our own palette
     so DR sees a finished dark theme and leaves it alone
================================================================= */

/* ════════════════════════════════════════════════════════════════
   TOKENS
════════════════════════════════════════════════════════════════ */
:root {
  /* Paper ground: a pale mint-tinted stock. --paper is --color-5
     lifted toward white so long-form text keeps its contrast; the mid
     and deep steps walk back down through --color-5 to --color-4, so
     the surface hierarchy is the palette itself rather than grey. */
  --paper:         #ECEFF4;
  --paper-mid:     #E5E9F0;   /* = brand --color-5, Gainsboro */
  --paper-deep:    #D8DEE9;
  --ink:           #2E3440;   /* = brand --color-1, Black */
  --ink-mid:       #485265;
  --ink-faint:     #5E6A83;

  /* Deep teal, the structural colour (--color-2 verbatim; 7.0:1 on
     --paper, so it can carry links and table headers as-is) */
  --indigo:        #3B4252;   /* = brand --color-2, Teal */
  --indigo-light:  #4C566A;   /* = brand --color-3, Cadet Blue */
  --indigo-ghost:  #ECEFF4;

  /* Lacquer: the accent, appears sparingly. Cadet-blue family:
     --lacquer is --color-3 darkened to clear AA on --paper (4.6:1),
     --lacquer-light is --color-4 verbatim for decorative, non-text use.
     Kept a brighter, greener teal than --indigo so the accent and the
     structural colour stay tellable apart. */
  --lacquer:       #1E768F;
  --lacquer-light: #88C0D0;   /* = brand --color-4 */

  /* Near-blacks for the header plane and code fields */
  --night:         #2E3440;
  --night-mid:     #3B4252;

  /* Fonts */
  --f-serif:   'Source Serif 4', Georgia, serif;
  --f-display: 'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;
  --w-display:    600;   /* Bricolage has no italic; weight, not slant, carries a heading */
  --w-display-xl: 700;   /* hero name, page and post titles */
  --f-mono:    'Martian Mono', 'Fira Mono', monospace;

  /* Layout. The gutters are deliberately tight: whatever the rails and
     the gap don't take, the article gets. */
  --article-w: 760px;
  --sidebar-w: 220px;
  --col-gap:   var(--s3);
  /* Caps the home and listing pages. The post page ignores it and runs
     the full width inside --page-pad; its columns are sized by the grid
     rather than by --article-w, which is kept for the listing layouts. */
  --max-w:     1280px;
  /* Gutter between the page edge and the content spine: the same
     --s5 that sits above and below the content, so the post page has
     one even frame on all four sides. One variable, because
     .post-wrap's padding and the bands' widths below it have to agree
     exactly or their edges stop lining up. */
  --page-pad:  var(--s3);
  --pill-w:    38px;      /* a collapsed rail */
  --h-header:  54px;
  --float-radius: 999px;  /* the phone's floating tab bar is a capsule, like the iOS 26 bar */

  /* Spacing: every unit is 8px */
  --s1:  0.5rem;   /*  8px */
  --s2:  1rem;     /* 16px */
  --s3:  1.5rem;   /* 24px */
  --s4:  2rem;     /* 32px */
  --s5:  2.5rem;   /* 40px */
  --s6:  3rem;     /* 48px */
  --s8:  4rem;     /* 64px */
  --s10: 5rem;     /* 80px */
  --s12: 6rem;     /* 96px */

  /* Easing */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  /* Semantic surface aliases (light mode) */
  --bg:           var(--paper);
  --bg-raised:    var(--paper-mid);
  --text:         var(--ink);
  --text-2:       #485265;   /* 7.4:1 on --paper, carries lead paragraphs */
  --text-3:       #5E6A83;   /* 4.6:1 on --paper; small mono labels need AA too */
  --accent:       var(--lacquer);
  --accent-dim:   var(--lacquer-light);
  --border:       var(--paper-deep);
  --header-bg:    var(--night);
  --header-text:  var(--ink);          /* light theme = dark text on light glass */
  --header-sub:   #556077;             /* darker so the mono nav reads clearly */

  /* Component-level semantic tokens: light.
     Everything that used to need a per-rule dark override now lives
     here, so light/dark is decided in exactly ONE place (below). */
  --link:         #1D6F87;
  --link-uline:   rgba(29,111,135,0.32);
  --code-fg:      #1D6F87;
  --code-bg:      var(--paper-mid);
  --quote-bg:     var(--paper-mid);
  --quote-fg:     var(--text-2);
  --hover-fg:     var(--text);          /* post-row title on hover */
  --navtitle-fg:  var(--text-2);
  --th-bg:        var(--indigo);
  --th-fg:        var(--indigo-ghost);
  --th-border:    transparent;
  --stripe:       rgba(0,0,0,0.022);
  --select-bg:    rgba(136,192,208,0.38);
  /* Blog leans to the structural teal, news to the sage green, so the
     two pill families stay tellable apart inside one teal palette. */
  --pill-blog-bg: rgba(59,66,82,0.12);
  --pill-blog-fg: #3B4252;
  --pill-news-bg: rgba(136,192,208,0.30);
  --pill-news-fg: #1D6F87;

  /* ── Glass: one material, three deliberate weights ──────────────
     Calibrated against iOS 26 "Liquid Glass". The navigation plane
     (floating header + tab bar) gets the heavy blur; overlays that
     briefly sit above content get the 12px "readable sweet spot";
     the scrim weight is a faint frost behind a surface that is
     already almost opaque. --glass-fx is the saturate/brightness lift
     that puts back the contrast the blur eats, kept far below the
     180% that smears a near-grey palette and drags the hero's teal
     glow unevenly across the bar. Glass is for the navigation layer:
     the content cards below carry a plain surface, not a blur. */
  --glass-blur:       12px;   /* small overlays: the figure button    */
  --glass-blur-nav:   22px;   /* header, tab bar, rails, back-to-top  */
  --glass-blur-scrim:  3px;   /* whisper behind a near-opaque panel   */
  --glass-fx:         saturate(1.25);
  --glass-bg:     color-mix(in srgb, var(--bg-raised) 72%, transparent);
  --glass-border: color-mix(in srgb, var(--border) 70%, transparent);
  --glass-hi:     rgba(255,255,255,0.55);   /* top inner-light edge */
  /* A light frost, not a slab. The readability wash sits on the fixed
     scene (.px::after) behind BOTH the header and the hero, so the tint
     here only has to unify the two; the wash already supplies the
     value. Kept transparent enough that the scene reads through. */
  --header-glass: color-mix(in srgb, var(--paper) 64%, transparent);
  --header-glass-solid: #ECEFF4;   /* reduced-transparency fallback   */
  /* Mobile nav panel: near-opaque; it covers body text, unlike the header bar */
  --nav-panel:    color-mix(in srgb, var(--paper) 97%, transparent);

  /* Plane = header / hero / footer surfaces. Now transparent so the
     parallax shows through; text colours follow the theme. */
  --plane-ink:    var(--ink);          /* hero/page titles            */
  --plane-ink-2:  var(--text-2);       /* crumbs, meta, excerpt       */
  --plane-faint:  #5E6A83;             /* separators, counts          */
  --plane-rule:   rgba(46,52,64,0.12); /* hairline on header/footer   */
  /* The readability wash over the parallax. It now lives on the fixed
     scene itself (.px::after), not on each plane's ::before, so the
     floating header frosts exactly the same backdrop the hero sits on,
     at every scroll position. Weighted to the top and bottom of the
     viewport (where the planes are) and clear through the middle so the
     contour texture still reads behind the content column. */
  --plane-scrim:  linear-gradient(180deg,
                    rgba(236,239,244,0.82) 0%,
                    rgba(236,239,244,0.52) 15%,
                    rgba(236,239,244,0)    34%,
                    rgba(236,239,244,0)    66%,
                    rgba(236,239,244,0.50) 86%,
                    rgba(236,239,244,0.80) 100%);

  /* Reading panel: "paper sheet" floating over the scene (content-first) */
  --panel:        color-mix(in srgb, var(--bg) 88%, transparent);
  --panel-border: color-mix(in srgb, var(--border) 55%, transparent);
  --panel-shadow: 0 10px 44px rgba(46,52,64,0.12);
  /* Cards carry text directly, so they sit more opaque than --panel */
  --card-bg:      #F6F8FA;

  /* Stat band: the one place the palette runs at full strength */
  --stat-bg:      #3B4252;
  --stat-fg:      #ECEFF4;   /* 8.7:1 on --stat-bg */
  --stat-dim:     #B4C0D4;   /* 5.5:1 on --stat-bg */

  /* ── Parallax scene: EDITORIAL (light) ───────────────────────────
     Brand-toned atmosphere behind the reading column: soft glows in
     the accent hues (--px-far), crisp geometric marks echoing a
     magazine masthead (--px-mid/--px-near, see the SVGs), a fine dot
     grain (--px-tex). Low chroma throughout so it never competes
     with text. */
  --px-base: linear-gradient(180deg, #F4F6FA 0%, #ECEFF4 46%, #DFE4EC 100%);
  --px-far:
    radial-gradient(circle 620px at 82% 6%, rgba(136,192,208,0.26), rgba(136,192,208,0) 70%),
    radial-gradient(circle 520px at 8% 88%, rgba(76,86,106,0.14), rgba(76,86,106,0) 74%);
  --px-mid:  url("../img/topo-light.svg");   /* contour survey map (still) */
  /* The living map (home, fine pointer): line colours low→high. */
  --topo-low:  #B8C4D6;
  --topo-mid:  #6E9CB8;
  --topo-high: #1E768F;
  --topo-opacity: 0.5;
  --px-near:
    radial-gradient(150% 60% at 50% 118%, rgba(216,222,233,0.85), rgba(216,222,233,0) 62%);
  --px-tex: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cg fill='%234C566A'%3E%3Ccircle cx='25' cy='35' r='2.2' opacity='.30'/%3E%3Ccircle cx='95' cy='70' r='1.6' opacity='.24'/%3E%3Ccircle cx='148' cy='30' r='2.4' opacity='.26'/%3E%3Ccircle cx='60' cy='135' r='1.9' opacity='.25'/%3E%3Ccircle cx='125' cy='120' r='2.1' opacity='.23'/%3E%3Ccircle cx='32' cy='98' r='1.5' opacity='.20'/%3E%3C/g%3E%3C/svg%3E");
  --px-tex-op: 0.22;
  /* The contours are the light theme's character; run them high enough
     to actually read, where the old corner arcs vanished at 0.22. */
  --px-mid-op:  0.85;
  --px-near-op: 1;
}

/* ────────────────────────────────────────────────────────────────
   DARK TOKENS: the single source of truth for the dark palette.

   Two triggers, one palette (defined once via a CSS variable list
   would require duplication, so we keep the block tight):
     1. [data-theme="dark"]  → explicit user choice (the toggle)
     2. prefers-color-scheme: dark, when the user hasn't chosen light
        → the no-JS / first-visit default
──────────────────────────────────────────────────────────────── */
:root[data-theme="dark"] {
  --bg:           #2E3440;
  --bg-raised:    #3B4252;
  --text:         #ECEFF4;
  --text-2:       #D8DEE9;   /* 6.6:1 on --bg, carries lead paragraphs */
  --text-3:       #A6B2CA;   /* 4.8:1 on --bg; small mono labels need AA too */
  --accent:       #88C0D0;   /* brightened --color-5 (Dark Orange), 7.6:1 on --bg */
  --accent-dim:   #4C566A;
  --border:       #434C5E;
  --header-bg:    #242933;
  --header-text:  #D8DEE9;
  --header-sub:   #6D7C99;
  --indigo:       #4C566A;
  --indigo-light: #88C0D0;   /* = brand --color-1, Sky Blue, 10:1 on --bg */
  --indigo-ghost: #2E3440;
  --paper:        #2E3440;
  --paper-mid:    #3B4252;
  --paper-deep:   #434C5E;
  --ink:          #ECEFF4;
  --ink-mid:      #D8DEE9;
  --ink-faint:    #A6B2CA;
  --night:        #242933;

  --link:         var(--indigo-light);
  --link-uline:   rgba(136,192,208,0.28);
  --code-fg:      var(--indigo-light);
  --code-bg:      var(--bg-raised);
  --quote-bg:     var(--bg-raised);
  --quote-fg:     var(--indigo-light);
  --hover-fg:     var(--indigo-light);
  --navtitle-fg:  var(--indigo-light);
  --th-bg:        var(--bg-raised);
  --th-fg:        var(--text-2);
  --th-border:    var(--border);
  --stripe:       rgba(255,255,255,0.03);
  --select-bg:    rgba(136,192,208,0.32);
  --pill-blog-bg: rgba(136,192,208,0.18);
  --pill-blog-fg: #A3CFE0;
  --pill-news-bg: rgba(76,86,106,0.45);
  --pill-news-fg: #88C0D0;

  --glass-bg:     color-mix(in srgb, var(--bg-raised) 58%, transparent);
  --glass-border: color-mix(in srgb, #ffffff 8%, transparent);
  --glass-hi:     rgba(255,255,255,0.06);
  --glass-fx:     saturate(1.2) brightness(1.06);
  --header-glass: color-mix(in srgb, #1B212B 52%, transparent);
  --header-glass-solid: #1C222C;
  --nav-panel:    color-mix(in srgb, #242933 97%, transparent);

  --plane-ink:    #ECEFF4;
  --plane-ink-2:  #A6B2CA;
  --plane-faint:  rgba(255,255,255,0.40);
  --plane-rule:   rgba(255,255,255,0.10);
  --plane-scrim:  linear-gradient(180deg,
                    rgba(6,10,16,0.62) 0%,
                    rgba(6,10,16,0.34) 15%,
                    rgba(6,10,16,0)    34%,
                    rgba(6,10,16,0)    66%,
                    rgba(6,10,16,0.34) 86%,
                    rgba(6,10,16,0.55) 100%);

  --panel:        color-mix(in srgb, var(--bg) 80%, transparent);
  --panel-border: color-mix(in srgb, #ffffff 6%, transparent);
  --panel-shadow: 0 10px 44px rgba(0,0,0,0.38);
  --card-bg:      #39404F;

  --stat-bg:      #3B4252;
  --stat-fg:      #ECEFF4;
  --stat-dim:     #B4C0D4;

  /* ── Parallax scene: SPACE (dark) ────────────────────────────── */
  --px-base: linear-gradient(180deg, #262B36 0%, #2E3440 55%, #22262F 100%);
  --px-far:
    radial-gradient(circle 620px at 78% 14%, rgba(136,192,208,0.20), rgba(136,192,208,0) 70%),
    radial-gradient(circle 460px at 14% 84%, rgba(94,129,172,0.22), rgba(94,129,172,0) 72%);
  --px-mid:  url("../img/topo-dark.svg");   /* the same contours, in frost (still) */
  --topo-low:  #3B4252;
  --topo-mid:  #5E81AC;
  --topo-high: #88C0D0;
  --topo-opacity: 0.6;
  --px-near:
    radial-gradient(150% 62% at 50% 122%, rgba(26,30,38,0.75), rgba(26,30,38,0) 60%);
  --px-tex: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cg fill='%23ECEFF4'%3E%3Ccircle cx='12' cy='20' r='1.1' opacity='.9'/%3E%3Ccircle cx='48' cy='60' r='.7' opacity='.6'/%3E%3Ccircle cx='96' cy='28' r='.9' opacity='.75'/%3E%3Ccircle cx='150' cy='80' r='1.2' opacity='.85'/%3E%3Ccircle cx='30' cy='120' r='.6' opacity='.5'/%3E%3Ccircle cx='74' cy='140' r='1' opacity='.7'/%3E%3Ccircle cx='165' cy='150' r='.7' opacity='.6'/%3E%3Ccircle cx='120' cy='115' r='.5' opacity='.45'/%3E%3Ccircle cx='20' cy='80' r='.5' opacity='.5'/%3E%3Ccircle cx='140' cy='42' r='.6' opacity='.55'/%3E%3C/g%3E%3C/svg%3E");
  --px-tex-op: 0.55;
  --px-mid-op: 0.9;
  --px-near-op: 1;
}

/* No explicit choice yet → follow the OS. Mirror of the block above. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:           #2E3440;
    --bg-raised:    #3B4252;
    --text:         #ECEFF4;
    --text-2:       #D8DEE9;   /* 6.6:1 on --bg, carries lead paragraphs */
    --text-3:       #A6B2CA;   /* 4.8:1 on --bg; small mono labels need AA too */
    --accent:       #88C0D0;   /* brightened --color-5 (Dark Orange), 7.6:1 on --bg */
    --accent-dim:   #4C566A;
    --border:       #434C5E;
    --header-bg:    #242933;
    --header-text:  #D8DEE9;
    --header-sub:   #6D7C99;
    --indigo:       #4C566A;
    --indigo-light: #88C0D0;   /* = brand --color-1, Sky Blue, 10:1 on --bg */
    --indigo-ghost: #2E3440;
    --paper:        #2E3440;
    --paper-mid:    #3B4252;
    --paper-deep:   #434C5E;
    --ink:          #ECEFF4;
    --ink-mid:      #D8DEE9;
    --ink-faint:    #A6B2CA;
    --night:        #242933;

    --link:         var(--indigo-light);
    --link-uline:   rgba(136,192,208,0.28);
    --code-fg:      var(--indigo-light);
    --code-bg:      var(--bg-raised);
    --quote-bg:     var(--bg-raised);
    --quote-fg:     var(--indigo-light);
    --hover-fg:     var(--indigo-light);
    --navtitle-fg:  var(--indigo-light);
    --th-bg:        var(--bg-raised);
    --th-fg:        var(--text-2);
    --th-border:    var(--border);
    --stripe:       rgba(255,255,255,0.03);
    --select-bg:    rgba(136,192,208,0.32);
    --pill-blog-bg: rgba(136,192,208,0.18);
    --pill-blog-fg: #A3CFE0;
    --pill-news-bg: rgba(76,86,106,0.45);
    --pill-news-fg: #88C0D0;

    --glass-bg:     color-mix(in srgb, var(--bg-raised) 58%, transparent);
    --glass-border: color-mix(in srgb, #ffffff 8%, transparent);
    --glass-hi:     rgba(255,255,255,0.06);
    --glass-fx:     saturate(1.2) brightness(1.06);
    --header-glass: color-mix(in srgb, #1B212B 52%, transparent);
    --header-glass-solid: #1C222C;
    --nav-panel:    color-mix(in srgb, #242933 97%, transparent);

    --plane-ink:    #ECEFF4;
    --plane-ink-2:  #A6B2CA;
    --plane-faint:  rgba(255,255,255,0.40);
    --plane-rule:   rgba(255,255,255,0.10);
    --plane-scrim:  linear-gradient(180deg,
                      rgba(6,10,16,0.62) 0%,
                      rgba(6,10,16,0.34) 15%,
                      rgba(6,10,16,0)    34%,
                      rgba(6,10,16,0)    66%,
                      rgba(6,10,16,0.34) 86%,
                      rgba(6,10,16,0.55) 100%);

    --panel:        color-mix(in srgb, var(--bg) 80%, transparent);
    --panel-border: color-mix(in srgb, #ffffff 6%, transparent);
    --panel-shadow: 0 10px 44px rgba(0,0,0,0.38);
    --card-bg:      #39404F;

    --stat-bg:      #3B4252;
    --stat-fg:      #ECEFF4;
    --stat-dim:     #B4C0D4;

    --px-base: linear-gradient(180deg, #262B36 0%, #2E3440 55%, #22262F 100%);
    --px-far:
      radial-gradient(circle 620px at 78% 14%, rgba(136,192,208,0.20), rgba(136,192,208,0) 70%),
      radial-gradient(circle 460px at 14% 84%, rgba(94,129,172,0.22), rgba(94,129,172,0) 72%);
    --px-mid:  url("../img/topo-dark.svg");   /* the same contours, in frost (still) */
  --topo-low:  #3B4252;
  --topo-mid:  #5E81AC;
  --topo-high: #88C0D0;
  --topo-opacity: 0.6;
    --px-near:
      radial-gradient(150% 62% at 50% 122%, rgba(26,30,38,0.75), rgba(26,30,38,0) 60%);
    --px-tex: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cg fill='%23ECEFF4'%3E%3Ccircle cx='12' cy='20' r='1.1' opacity='.9'/%3E%3Ccircle cx='48' cy='60' r='.7' opacity='.6'/%3E%3Ccircle cx='96' cy='28' r='.9' opacity='.75'/%3E%3Ccircle cx='150' cy='80' r='1.2' opacity='.85'/%3E%3Ccircle cx='30' cy='120' r='.6' opacity='.5'/%3E%3Ccircle cx='74' cy='140' r='1' opacity='.7'/%3E%3Ccircle cx='165' cy='150' r='.7' opacity='.6'/%3E%3Ccircle cx='120' cy='115' r='.5' opacity='.45'/%3E%3Ccircle cx='20' cy='80' r='.5' opacity='.5'/%3E%3Ccircle cx='140' cy='42' r='.6' opacity='.55'/%3E%3C/g%3E%3C/svg%3E");
    --px-tex-op: 0.55;
    --px-mid-op: 0.9;
    --px-near-op: 1;
  }
}

/* Tell browsers the active scheme so form controls / scrollbars match */
:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

/* Refined text selection: token-driven, no separate dark rule needed */
::selection {
  background: var(--select-bg);
  color: var(--text);
}

/* Theme transition: smooth the swap, but never animate on first paint
   or for users who prefer reduced motion */
body, .site-header, .site-footer, .sidebar-card,
.post-row, .post-content pre, .post-content blockquote,
.post-content code, .post-content th {
  transition: background-color 0.4s var(--ease-out),
              border-color 0.4s var(--ease-out),
              color 0.4s var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
  /* A ribbon that cannot animate must still be reachable, so the
     track stops and the row becomes a plain horizontal scroller. */
  .ribbon-track { animation: none !important; width: auto; }
  /* The hero's pointer effects are decorative; JS also checks this
     query and never binds, so these only cover the CSS-driven parts. */
  .hero-inner .hero-rise { opacity: 1; transform: none; animation: none; }
  /* The clamp deliberately survives here. Reduced motion asks for no
     ANIMATION, not for less functionality; the height transition is
     already killed by the rule above, and hiding the toggle instead
     would strand the rest of the essay behind a fade with no way in. */
}


/* ════════════════════════════════════════════════════════════════
   RESET + BASE
════════════════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--h-header) + 1rem);
}

/* Visible to screen readers, to nobody else */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* Accessible focus ring that respects the palette */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

body {
  /* The .px scene (fixed, z-index -1) is what the page actually sits on;
     this gradient only shows in the overscroll rubber-band. It used to be
     background-attachment: fixed, which forces the browser to repaint a
     full-viewport image on every scroll frame (the main cause of the
     laggy scroll on phones) for a surface that is almost never visible. */
  background: var(--px-base);
  color: var(--text);
  font-family: var(--f-serif);
  font-size: 17px;
  line-height: 1.78;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  /* Push footer to bottom even on short pages */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

main { flex: 1; }

/* Subtle noise on the page surface: low opacity, doesn't invert badly */
body::before {
  content: '';
  position: fixed; inset: 0;
  pointer-events: none; z-index: 200;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.88' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.022'/%3E%3C/svg%3E");
  opacity: 0.45;
}


/* ════════════════════════════════════════════════════════════════
   PARALLAX SCENE
   Garden (light) / Space (dark). Fixed behind everything, driven by
   token-supplied gradients + a vector texture. Layers translate via
   JS (scroll depth + pointer drift); reduced-motion leaves them still.
════════════════════════════════════════════════════════════════ */
.px {
  position: fixed; inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  contain: layout paint;
}
/* Readability wash, painted over the parallax layers as part of the
   fixed scene. Because it is viewport-fixed, anything floating in front
   of the scene (the sticky header above all) sits on exactly the same
   wash as the hero does, so the two never read as different surfaces. */
.px::after {
  content: '';
  position: absolute; inset: 0;
  background: var(--plane-scrim);
  pointer-events: none;
}
.px-layer {
  position: absolute; inset: -14%;
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
  will-change: transform;
  transition: opacity 0.6s var(--ease-out);
}
/* The layers only ever move on pointer drift, which JS binds on a fine
   pointer only. On a touch device nothing animates them, so promoting
   four near-full-viewport layers to their own GPU surfaces just burns
   memory and compositor time while scrolling. */
@media (pointer: coarse) {
  .px-layer { will-change: auto; }
}
.px-far  { background-image: var(--px-far); }
.px-mid  { background-image: var(--px-mid);  background-position: top center;    opacity: var(--px-mid-op, 1); }
.px-near { background-image: var(--px-near); background-position: bottom center; opacity: var(--px-near-op, 1); }
.px-tex  {
  background-image: var(--px-tex);
  background-repeat: repeat;
  background-size: 180px 180px;
  opacity: var(--px-tex-op);
}
/* The living contour map (React Bits Topography, drawn by home-fx.js
   on WebGL2). It takes the still map's place in the stack; the still
   SVG stays for touch screens, reduced motion and no WebGL. */
.px-topo {
  position: absolute; inset: 0;
  width: 100%; height: 100%; display: block;
}
.px.has-topo .px-mid { display: none; }

/* ── Click Spark: a burst of short lines at every click ────────── */
.click-spark {
  position: fixed; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 95;
}

/* ════════════════════════════════════════════════════════════════
   READING PROGRESS: single hairline, lacquer
════════════════════════════════════════════════════════════════ */
/* ════════════════════════════════════════════════════════════════
   IMAGE / DIAGRAM VIEWER

   A figure in an article is sized for the column, which is right for
   reading and useless for a dense diagram. Clicking one opens it here
   over a scrim at whatever size it wants, with zoom and pan.

   The scrim follows the theme rather than being dark in both. A mermaid
   diagram is rendered in the page's own palette, so in the light theme
   it draws dark ink, which a dark scrim swallowed. Mixing it from --bg
   means it tracks the theme with no second set of values to maintain.
════════════════════════════════════════════════════════════════ */
.lightbox {
  position: fixed; inset: 0; z-index: 200;
  display: flex; flex-direction: column;
  background: color-mix(in srgb, var(--bg) 93%, transparent);
  backdrop-filter: blur(var(--glass-blur-scrim)) var(--glass-fx);
  -webkit-backdrop-filter: blur(var(--glass-blur-scrim)) var(--glass-fx);
  touch-action: none;   /* nothing here scrolls or zooms the page under it */
}
.lightbox[hidden] { display: none; }

/* Scrolling the page underneath while zooming would be a mess */
body.lb-open { overflow: hidden; }

.lightbox-bar {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: flex-end;
  gap: var(--s1);
  padding: var(--s2) var(--s3);
}
.lightbox-zoom {
  font-family: var(--f-mono); font-size: 10px;
  letter-spacing: 0.12em;
  color: var(--text-3);
  min-width: 46px; text-align: center;
  font-variant-numeric: tabular-nums;   /* the number must not jitter */
}
/* Same tokens as every other control on the site, so the toolbar reads
   correctly on a pale scrim as well as a dark one. */
.lb-btn {
  width: 32px; height: 32px; padding: 0;
  display: grid; place-items: center;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 7px;
  color: var(--text-2);
  cursor: pointer;
  transition: background 0.14s, color 0.14s, border-color 0.14s;
}
.lb-btn:hover {
  background: var(--bg-raised);
  border-color: var(--accent);
  color: var(--accent);
}
.lb-btn svg { width: 15px; height: 15px; display: block; }

/* Centring is done by the figure's own transform, not by the stage.
   Grid/flex centring of a box bigger than its track is not dependable:
   the overflow direction is up to the browser, and a tall diagram ended
   up hanging off the bottom instead of sitting in the middle. Anchoring
   the box's centre to the stage's centre is exact at any size. */
.lightbox-stage {
  flex: 1 1 auto;
  position: relative;
  overflow: hidden;                 /* panning moves the figure, not this */
  touch-action: none;               /* the pointer handlers own the gesture */
}
/* No size caps here on purpose: the figure is laid out at its natural
   size and the script scales it to fill the stage. A max-width would
   quietly cap that measurement and the fit would come out short. */
.lightbox-content {
  position: absolute; top: 50%; left: 50%;
  /* Sized to the figure rather than to any track, so the scaling is
     centred on the figure itself and not on a clamped box. */
  width: max-content;
  height: max-content;
  transform-origin: center center;
  cursor: grab;
}
.lightbox-content.dragging { cursor: grabbing; }
.lightbox-content img,
.lightbox-content svg {
  display: block;
  max-width: none;
  max-height: none;
  user-select: none; -webkit-user-drag: none;
  -webkit-touch-callout: none;      /* no save-image sheet on a long press */
}
/* Mermaid flowchart labels are HTML inside <foreignObject>, so they
   inherit line-height from here; a zeroed one collapsed every label.
   Sequence diagrams use plain <text> and were unaffected, which is why
   only the flowcharts came out wrong. */
.lightbox-content { line-height: normal; }

/* Marks where a borrowed figure came from; never takes any space */
.lb-placeholder { display: none; }

/* The affordance: figures that open say so on hover */
.post-content img,
.post-content .mermaid-diagram svg { cursor: zoom-in; }
.post-content a img { cursor: pointer; }

/* Each figure gets a control in its corner. The artwork itself still
   opens on click, but a diagram is mostly whitespace between boxes;
   this is the part you can actually aim at. */
.fig-wrap { position: relative; display: block; }
span.fig-wrap { display: inline-block; max-width: 100%; }

.fig-zoom {
  position: absolute; top: 10px; right: 10px;
  width: 28px; height: 28px; padding: 0;
  display: grid; place-items: center;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 7px;
  color: var(--text-2);
  cursor: zoom-in;
  backdrop-filter: blur(var(--glass-blur)) var(--glass-fx);
  -webkit-backdrop-filter: blur(var(--glass-blur)) var(--glass-fx);
  box-shadow: 0 2px 8px rgba(0,0,0,0.14);
  opacity: 0; transform: translateY(-3px);
  transition: opacity 0.16s, transform 0.16s var(--ease-out),
              color 0.14s, border-color 0.14s;
}
.fig-zoom svg { width: 14px; height: 14px; display: block; }
.fig-wrap:hover .fig-zoom,
.fig-zoom:focus-visible { opacity: 1; transform: none; }
.fig-zoom:hover { color: var(--accent); border-color: var(--accent); }

/* Nothing hovers on a touch screen, so it simply stays put there */
@media (hover: none) {
  .fig-zoom { opacity: 1; transform: none; }
}


/* ════════════════════════════════════════════════════════════════
   BACK TO TOP + READING PROGRESS

   One control doing two jobs: the ring around it is how far you've
   read, the arrow inside takes you back. It replaces the loading-bar
   under the header, which read as "page is still loading" rather than
   "you are two thirds through".

   Nothing here is announced to screen readers except the button's own
   label: progress is decoration, and the ring is inert SVG.
════════════════════════════════════════════════════════════════ */
.scroll-top {
  position: fixed;
  right: var(--fab-right, var(--s4)); bottom: var(--s4);
  width: 60px; height: 60px;
  z-index: 98;
  display: grid; place-items: center;
  padding: 0; cursor: pointer;
  border-radius: 50%;
  /* The header's glass (same tint, same blur), so every floating
     control reads as one material with the bars. The border and the
     two-part shadow (tight contact, wide soft) separate it from the
     article panel it floats over. */
  background: var(--header-glass);
  backdrop-filter: blur(var(--glass-blur-nav)) var(--glass-fx);
  -webkit-backdrop-filter: blur(var(--glass-blur-nav)) var(--glass-fx);
  border: 1px solid var(--glass-border);
  box-shadow:
    0 12px 28px rgba(0,0,0,0.26),
    0 4px 8px rgba(0,0,0,0.18),
    inset 0 1px 0 var(--glass-hi);
  color: var(--text-2);
  /* Out of the way until there is progress worth showing */
  opacity: 0; visibility: hidden;
  transform: translateY(10px) scale(0.94);
  transition: opacity 0.2s linear,
              transform 0.28s var(--ease-out),
              right 0.26s var(--ease-out),
              box-shadow 0.2s var(--ease-out),
              border-color 0.15s,
              color 0.15s,
              visibility 0s linear 0.28s;
}
.scroll-top.visible {
  opacity: 1; visibility: visible;
  transform: none;
  transition: opacity 0.2s linear,
              transform 0.28s var(--ease-out),
              box-shadow 0.2s var(--ease-out),
              border-color 0.15s,
              color 0.15s,
              visibility 0s linear 0s;
}
/* Hover lifts it further off the page; press puts it back down */
.scroll-top:hover {
  color: var(--accent);
  border-color: var(--accent);
}
.scroll-top.visible:hover {
  transform: translateY(-2px);
  box-shadow:
    0 16px 34px rgba(0,0,0,0.30),
    0 5px 10px rgba(0,0,0,0.20),
    inset 0 1px 0 var(--glass-hi);
}
.scroll-top.visible:active {
  transform: translateY(0) scale(0.95);
  box-shadow:
    0 4px 10px rgba(0,0,0,0.24),
    inset 0 1px 0 var(--glass-hi);
}

/* Placement. The convention is the bottom-right corner, and that is
   where it goes, except on a post page, where the bottom-right corner
   belongs to the TOC rail and a button there covers the last entries.
   There it shifts left to straddle the article panel's right edge: it
   lands in the column gutter and over the panel's own padding, so it
   overlaps neither the rail nor a line of text. */
@media (min-width: 861px) {
  /* The strip between the TOC rail and the article's text is the column
     gap plus the panel's own inner padding: 64px with nothing in it.
     The button is sized and placed to fill that, clearing the rail by
     2px on one side and stopping short of the prose on the other. The
     open rail sits against the window edge (no page gutter), so the
     offset is the rail's width alone. */
  body:has(.post-sidebar) {
    --fab-right: calc(var(--sidebar-w) + 2px);
  }
  /* Rail docked at the edge: the article now runs to the gutter, so the
     button returns to the plain corner. The pill sits at the top of the
     window, the button at the bottom; they never meet. The state is
     read off body because a fixed element cannot see a variable set on
     .post-wrap. */
  body:has(.post-sidebar.collapsed) { --fab-right: var(--s4); }
}

/* The ring sits on the button's edge, arrow centred inside it */
.scroll-top-ring {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  transform: rotate(-90deg);          /* progress starts at 12 o'clock */
  overflow: visible;
}
/* Geometry (cx/cy/r) lives on the elements; as CSS properties it is
   too new to rely on. */
.scroll-top-ring circle { fill: none; stroke-width: 3.5; }
.ring-track { stroke: var(--border); stroke-width: 3; }
.ring-bar {
  stroke: var(--accent);
  stroke-linecap: round;
  /* dasharray/offset are set from JS: 2πr for r=21 */
  stroke-dasharray: 131.95;
  stroke-dashoffset: 131.95;
  transition: stroke-dashoffset 0.08s linear;
}

.scroll-top-arrow {
  width: 21px; height: 21px;
  fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  position: relative;
}


/* ════════════════════════════════════════════════════════════════
   HEADER
   A flat glass bar across the full width, flush to the top of the
   window, with the page sliding under it. Only the phone's tab bar
   floats as a pill; the header is a plane, not an object.
   Three things happen here: wordmark, nav, nothing else.
════════════════════════════════════════════════════════════════ */
.site-header {
  background: var(--header-glass);
  /* The blur lives on ::before, not here. An element with a
     backdrop-filter becomes the containing block for its fixed-position
     descendants, and .site-nav (which docks to the bottom of the
     window on phones) is one of them. A pseudo-element has no
     descendants to capture, so the glass survives without the trap. */
  height: var(--h-header);
  /* Edge to edge, but the wordmark and nav line up with the content:
     past --max-w the padding grows to centre them on it. */
  padding: 0 max(var(--s4), calc((100% - var(--max-w)) / 2 + var(--s4)));
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3);
  /* Sticky, not fixed, so it still reserves its own height in normal
     flow and nothing below needs a manual offset. */
  position: sticky;
  top: 0; z-index: 100;
  width: 100%;
  margin: 0;
  border-bottom: 1px solid var(--glass-border);
}
.site-header::before {
  content: '';
  position: absolute; inset: 0; z-index: -1;
  border-radius: inherit;
  backdrop-filter: blur(var(--glass-blur-nav)) var(--glass-fx);
  -webkit-backdrop-filter: blur(var(--glass-blur-nav)) var(--glass-fx);
}

/* OS "Reduce Transparency" / "Increase Contrast" (on Windows, turning
   off Transparency effects): the navigation plane, and the rails and
   back-to-top button that share its glass, go to a solid material, the
   way iOS drops Liquid Glass for an opaque fill under the same settings. The mobile tab bar is handled in its own
   ≤860 block. */
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .site-header, .site-footer, .scroll-top { background: var(--header-glass-solid); }
  .site-header::before,
  .site-footer, .scroll-top { backdrop-filter: none; -webkit-backdrop-filter: none; }
  /* The rails get the same treatment at the end of the file, after the
     .sidebar-card rules it has to override. */
}

/* Wordmark: mono, uppercase, with slash in lacquer.
   Tracking is 0.16em rather than 0.2em: the name runs 14 characters against
   the 5 of the placeholder wordmark this replaced, and the wider tracking
   left too little room beside the theme toggle on a 320px screen. */
.site-logo {
  font-family: var(--f-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--header-text);
  text-decoration: none;
  white-space: nowrap;
  display: flex; align-items: baseline; gap: 0;
}
.site-logo span {
  color: var(--accent);
  /* The slash leans; gives the wordmark life */
  display: inline-block;
  transform: rotate(10deg);
  margin-left: 1px;
  /* Eat the trailing letter-spacing after the slash so the cursor can
     sit right under it, the way it does in the favicon. */
  margin-right: -0.16em;
  font-style: normal;
}
/* Terminal cursor block after the slash: the same mark as the favicon
   ("RISHABH SHARMA/" + a prompt cursor). Static, like the favicon: the
   block tucks up close under the slash, not spaced off it. */
.logo-cur {
  display: inline-block;
  width: 0.5em;
  height: 0.16em;
  margin-left: 1px;
  border-radius: 1px;
  background: var(--accent);
}

/* Nav: generous gap, no box, just text. On the flat header bar the
   active tab is marked by an accent line along the bar's bottom edge
   (the >=861 block below); on phones the same links become the floating
   tab bar, which keeps its glass pill and sliding lens. */
.site-nav { display: flex; align-items: center; gap: 2px; }
.site-nav a {
  font-family: var(--f-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--header-sub);
  text-decoration: none;
  height: var(--h-header);
  display: flex; align-items: center;
  padding: 0 var(--s2);
  position: relative;
  transition: color 0.2s;
}
/* Nav icons: same monochrome set as everywhere else, so they take the
   link's colour through every theme and hover state for free. */
.site-nav a { gap: 7px; }
.nav-ic { display: grid; place-items: center; opacity: 0.75; transition: opacity 0.2s; }
.nav-ic svg { width: 13px; height: 13px; display: block; }
.site-nav a:hover .nav-ic,
.site-nav a.active .nav-ic { opacity: 1; }

/* The selection pill: sits behind the label, brightens in on hover,
   fills on active. z-index -1 drops it below the text but keeps it
   above the header's own glass (the sticky header is a stacking
   context, so -1 lands between its blur and its content). */
.site-nav a::after {
  content: '';
  position: absolute; inset: 8px 2px;
  z-index: -1;
  border-radius: 999px;
  background: color-mix(in srgb, var(--header-text) 13%, transparent);
  box-shadow:
    inset 0 1px 0 var(--glass-hi),
    inset 0 0 0 1px color-mix(in srgb, var(--header-text) 10%, transparent);
  opacity: 0;
  transform: scale(0.8);
  transition: opacity 0.2s var(--ease-out), transform 0.2s var(--ease-out);
}
.site-nav a:hover { color: var(--header-text); }
.site-nav a:hover::after  { opacity: 0.5; transform: scale(1); }
.site-nav a.active::after { opacity: 1; transform: scale(1); }
.site-nav a.active { color: var(--accent); }

/* Desktop: the header is a flat bar, so its tabs are flat too. The
   ::after pill above (still the phone tab bar's no-JS fallback) turns
   into a 2px line sitting on the bar's bottom edge: faint on hover,
   accent and full width on the current page. */
@media (min-width: 861px) {
  .site-nav { align-self: stretch; }
  .site-nav a::after {
    inset: auto var(--s2) -1px;          /* over the header's hairline */
    height: 2px;
    border-radius: 2px 2px 0 0;
    background: var(--accent);
    box-shadow: none;
    opacity: 0;
    transform: scaleX(0.4);
    transform-origin: center;
  }
  .site-nav a:hover::after  { opacity: 0.45; transform: scaleX(1); }
  .site-nav a.active::after { opacity: 1; transform: scaleX(1); }
  .site-nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: -6px; border-radius: 4px; }
}
/* The tab bar's sliding lens (see the ≤860 block); inert up here */
.nav-lens { display: none; }

/* Header right cluster: nav + theme switch + hamburger */
.header-right { display: flex; align-items: center; gap: var(--s1); }

/* Hamburger: desktop hides it; the ≤860px block turns it on.
   Three bars that morph into an X when expanded. */
.nav-toggle {
  display: none;
  background: none; border: 0; cursor: pointer;
  padding: 0; margin-left: var(--s1);
  width: 34px; height: 34px;
  align-items: center; justify-content: center;
  color: var(--header-text);
  border-radius: 3px;
}
.nav-toggle-box {
  position: relative;
  width: 18px; height: 12px;
  display: block;
}
.nav-toggle-bar {
  position: absolute; left: 0;
  width: 100%; height: 1.5px;
  background: currentColor;
  transition: transform 0.24s var(--ease-out), opacity 0.16s linear;
}
.nav-toggle-bar:nth-child(1) { top: 0; }
.nav-toggle-bar:nth-child(2) { top: 50%; transform: translateY(-50%); }
.nav-toggle-bar:nth-child(3) { bottom: 0; }

.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(1) {
  transform: translateY(5.25px) rotate(45deg);
}
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(3) {
  transform: translateY(-5.25px) rotate(-45deg);
}


/* ════════════════════════════════════════════════════════════════
   THEME TOGGLE
   A segmented radiogroup, not a cycling switch: light / system /
   dark all sit on the track at once, so every option is visible
   and one click away. A sliding thumb marks the current choice.
   Position is driven purely by data-theme-choice on <html> (set
   before first paint), so the control never flashes or lags.
════════════════════════════════════════════════════════════════ */
.theme-toggle {
  --tt-seg: 26px;                 /* one segment */
  --tt-pad: 3px;
  flex-shrink: 0;
  position: relative;
  display: inline-flex; align-items: center;
  padding: var(--tt-pad);
  height: calc(var(--tt-seg) + var(--tt-pad) * 2);
  border: 1px solid var(--plane-rule);
  border-radius: 999px;
  /* No blur of its own. It sits on the header's glass, and glass on
     glass muddies both. A plain tint, concentric with the capsule. */
  background: color-mix(in srgb, var(--header-text) 10%, transparent);
  transition: border-color 0.25s var(--ease-out);
}
.theme-toggle:hover { border-color: color-mix(in srgb, var(--header-text) 35%, transparent); }

/* The three options */
.tt-opt {
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 0; margin: 0;
  position: relative; z-index: 1;
  width: var(--tt-seg); height: var(--tt-seg);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  border-radius: 50%;
  color: var(--header-text);
  opacity: 0.45;
  transition: opacity 0.2s var(--ease-out), color 0.2s var(--ease-out);
}
.tt-opt svg { width: 13px; height: 13px; display: block; }
.tt-opt:hover { opacity: 0.8; }
.tt-opt[aria-checked="true"] { opacity: 1; }
.tt-opt:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The sliding thumb, parked under the selected segment */
.tt-thumb {
  position: absolute; z-index: 0;
  top: var(--tt-pad); left: var(--tt-pad);
  width: var(--tt-seg); height: var(--tt-seg);
  border-radius: 50%;
  background: color-mix(in srgb, var(--header-text) 20%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--header-text) 22%, transparent);
  transition: transform 0.28s var(--ease-out), background 0.25s;
}
/* Light is the first segment, so it needs no shift. The other two
   move by whole segments. Defaults to system for a no-JS load. */
:root[data-theme-choice="light"]  .tt-thumb { transform: translateX(0); }
.tt-thumb,
:root[data-theme-choice="system"] .tt-thumb { transform: translateX(var(--tt-seg)); }
:root[data-theme-choice="dark"]   .tt-thumb { transform: translateX(calc(var(--tt-seg) * 2)); }

/* The checked option gets the accent so the choice reads at a glance,
   not just from the thumb behind it. */
:root[data-theme-choice] .tt-opt[aria-checked="true"] { color: var(--accent); }


/* ════════════════════════════════════════════════════════════════
   FOOTER
   Mirrors header: same dark plane, same weight.
   The two dark bands bracket the page like a book cover.
   margin-top: auto is handled by flex on body.
════════════════════════════════════════════════════════════════ */
.site-footer {
  background: var(--header-glass);
  backdrop-filter: blur(var(--glass-blur-nav)) var(--glass-fx);
  -webkit-backdrop-filter: blur(var(--glass-blur-nav)) var(--glass-fx);
  padding: var(--s4) var(--s5);
  margin-top: var(--s5);        /* the last band never sits on the footer */
  /* Name | socials | palette. Equal side tracks keep the socials on the
     page's centre line whatever the two ends measure. */
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  gap: var(--s3);
  border-top: 1px solid var(--plane-rule);
  /* Never floats mid-page: body is flex column, main has flex:1 */
}
.site-footer p {
  font-family: var(--f-mono);
  font-size: 9px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--header-sub);
}
.footer-palette { display: flex; gap: 5px; align-items: center; justify-self: end; }

/* Social links: icon-only, named by aria-label and a tooltip. Quiet
   like the rest of the footer until pointed at. */
.footer-social { display: flex; gap: 4px; align-items: center; }
.footer-social a {
  display: grid; place-items: center;
  width: 34px; height: 34px;
  color: var(--header-sub);
  border: 1px solid transparent; border-radius: 8px;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.footer-social a:hover,
.footer-social a:focus-visible {
  color: var(--accent);
  border-color: var(--glass-border);
  background: var(--bg-raised);
}
.footer-social a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.footer-social svg { width: 16px; height: 16px; display: block; }
.footer-dot { width: 7px; height: 7px; border-radius: 50%; opacity: 0.7; }


/* ════════════════════════════════════════════════════════════════
   PAGE HEADER (listing pages)
   
   Design break from the old version: 
   · No decorative circle. No gradient blob.  
   · Instead: the title is huge and sits against the dark field.
     A single horizontal rule in lacquer breaks below it.
   · The page label floats at a rotated angle in the far right,
     a marginal note, like a hand-stamped category mark.
════════════════════════════════════════════════════════════════ */
.page-header {
  background: transparent;          /* parallax scene shows through */
  padding: var(--s10) var(--s5) var(--s8);   /* listing pages keep their own frame */
  position: relative;
  overflow: hidden;
}

/* The readability wash now lives on the fixed scene (.px::after), so the
   header and this plane share one surface, no per-plane ::before. */

/* Bottom rule: a measured lacquer line, not full-width */
.page-header::after {
  content: '';
  position: absolute; bottom: 0; left: var(--s5); right: var(--s5);
  height: 1px;
  background: linear-gradient(
    to right,
    var(--lacquer) 0%,
    var(--lacquer) 40px,
    transparent 40px
  );
}

/* News gets a slightly longer lacquer rule */
.page-header.news::after {
  background: linear-gradient(
    to right,
    var(--lacquer) 0%,
    var(--lacquer) 72px,
    transparent 72px
  );
}

.page-header-inner {
  max-width: 900px; margin: 0 auto;
  position: relative; z-index: 1;
}

.page-label {
  font-family: var(--f-mono);
  font-size: 9px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent);
  display: inline-flex; align-items: center; gap: var(--s1);
  margin-bottom: var(--s3);
}
.page-label::before {
  content: ''; width: 20px; height: 1px;
  background: var(--accent); display: inline-block; flex-shrink: 0;
}

/* Headline: very large, heavy and tight. This is the architecture. */
.page-header h1 {
  font-family: var(--f-display);
  font-size: clamp(3rem, 7vw, 5.5rem);
  font-weight: var(--w-display-xl);
  color: var(--plane-ink);
  letter-spacing: -0.03em;
  line-height: 0.95;
}

.page-header p {
  margin-top: var(--s3);
  font-size: 14.5px;
  color: var(--plane-ink-2);
  font-style: italic;
  max-width: 480px;
}

.post-count {
  font-family: var(--f-mono); font-size: 9px;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--plane-faint);
  margin-top: var(--s4); display: block;
}


/* ════════════════════════════════════════════════════════════════
   LISTING: the post rows
   
   These are not cards. They are entries in a ledger.
   The date is a monospaced timestamp in the left gutter.
   The title is large display type; it carries all the visual weight.
   On hover: the background warms, and a lacquer bar slides in on the left.
════════════════════════════════════════════════════════════════ */
.listing-wrap {
  max-width: 900px; margin: var(--s5) auto var(--s8);
  padding: var(--s2) var(--s6) var(--s6);
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 14px;
  box-shadow: var(--panel-shadow);
  backdrop-filter: blur(var(--glass-blur-scrim));
  -webkit-backdrop-filter: blur(var(--glass-blur-scrim));
}

.group-heading {
  font-family: var(--f-mono); font-size: 9px; letter-spacing: 0.24em;
  text-transform: uppercase; color: var(--text-3);
  display: flex; align-items: center; gap: var(--s2);
  margin: var(--s8) 0 0;
}
.group-heading::after {
  content: ''; flex: 1; height: 1px; background: var(--border);
}

/* The row */
.post-row {
  display: grid;
  grid-template-columns: 72px 1fr auto;
  gap: 0 var(--s4);
  padding: var(--s2) var(--s2);
  margin: 0 calc(var(--s2) * -1);
  border-bottom: 1px solid var(--border);
  align-items: baseline;
  text-decoration: none; color: inherit;
  position: relative;
  transition: background 0.18s;
}

/* The sliding lacquer bar on the left */
.post-row::before {
  content: '';
  position: absolute; left: 0; top: 0; bottom: 0;
  width: 2px;
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 0.2s var(--ease-out);
}

.post-row:hover {
  background: var(--bg-raised);
  box-shadow: 0 2px 12px rgba(0,0,0,0.06);
  border-radius: 2px;
}
.post-row:hover::before { transform: scaleY(1); }
.post-row:last-child { border-bottom: none; }

.post-row-date {
  font-family: var(--f-mono); font-size: 10px;
  color: var(--text-3); letter-spacing: 0.04em;
  white-space: nowrap; padding-top: 4px;
}

.post-row-title {
  font-family: var(--f-display);
  font-size: 1.35rem;
  font-weight: var(--w-display);
  color: var(--text);
  line-height: 1.25;
  display: block;
  transition: color 0.15s;
}
.post-row:hover .post-row-title { color: var(--hover-fg); }

/* News: lacquer title on hover */
.post-row.news-item:hover .post-row-title { color: var(--accent); }

/* Tags: smaller, tucked under title */
.post-row-tags { display: flex; gap: 4px; margin-top: 4px; flex-wrap: wrap; }
.post-row-tag {
  font-family: var(--f-mono); font-size: 8px; letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 2px 7px;
  background: var(--bg-raised);
  color: var(--text-3); border-radius: 1px;
  border: 1px solid var(--border);
}

.post-row-readtime {
  font-family: var(--f-mono); font-size: 9px; letter-spacing: 0.08em;
  color: var(--text-3); white-space: nowrap; align-self: center;
}

.post-row-excerpt {
  font-size: 13px; color: var(--text-2); font-style: italic;
  margin-top: 3px;
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
  overflow: hidden;
}

.empty-state {
  padding: var(--s12) 0; text-align: center;
  font-family: var(--f-display); font-weight: var(--w-display); font-size: 1.4rem; font-style: normal;
  color: var(--text-3);
}

/* Blog listing: one timeline, newest first, a <section> per year.
   Standalone posts are a single card on the line; a series is the same
   card drawn as a stack of three (a <details>, so it folds without JS)
   and lists its parts when opened. One card against a stack is the
   whole distinction; nothing else about the two differs in weight. */
.bt-year {
  --bt-gutter: 32px;
  --bt-rail: 7px;                  /* x of the line, from the gutter's left */
  position: relative;
  padding: var(--s4) 0 var(--s1) var(--bt-gutter);
  scroll-margin-top: calc(var(--h-header) + var(--s4));
}
.bt-year::before {                 /* the line; years abut, so it reads as one */
  content: '';
  position: absolute; left: var(--bt-rail); top: 0; bottom: 0;
  width: 1px; background: var(--border);
}
.bt-year:first-of-type::before { top: calc(var(--s4) + 8px); }
.bt-year:last-of-type::before  { bottom: var(--s5); }

.bt-year-label {
  position: relative;
  font-family: var(--f-mono); font-size: 10px; font-weight: 400;
  letter-spacing: 0.24em; color: var(--text-3);
  line-height: 16px;
  margin: 0 0 var(--s1);
}
.bt-year-label::before {           /* the year's knot on the line */
  content: '';
  position: absolute; top: 2px;
  left: calc(var(--bt-rail) - var(--bt-gutter) - 5px);
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--bg); border: 1.5px solid var(--text-3);
  box-sizing: border-box;
}

.bt-list { list-style: none; margin: 0; padding: 0; }
.bt-item { position: relative; margin: 0 0 var(--s2); }
.bt-item::before {                 /* this entry's node on the line */
  content: '';
  position: absolute; top: 22px;
  left: calc(var(--bt-rail) - var(--bt-gutter) - 3px);
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--text-3);
}
.bt-series::before {
  background: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
/* the stack's two back cards hang below the front one */
.bt-series { margin-bottom: calc(var(--s2) + 22px); }

/* The card, shared by both kinds */
.bt-card, .series-card {
  display: block;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-raised);
  color: inherit; text-decoration: none;
  transition: border-color 0.18s, box-shadow 0.25s var(--ease-out), transform 0.25s var(--ease-out);
}
.bt-card, .series-card > summary {
  display: grid; gap: 6px;
  padding: var(--s2) var(--s3) calc(var(--s2) + 2px);
}
.bt-card:hover, .series-card:hover { border-color: var(--text-3); }
.bt-card:hover { transform: translateY(-2px); }
.bt-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.bt-meta {
  display: flex; flex-wrap: wrap; gap: 2px var(--s2);
  font-family: var(--f-mono); font-size: 9px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--text-3);
}
.bt-kind { color: var(--text-2); }
.bt-series .bt-kind { color: var(--accent); }
.bt-title {
  font-family: var(--f-display); font-weight: var(--w-display);
  font-size: 1.3rem; line-height: 1.25; color: var(--text);
  transition: color 0.15s;
}
.bt-card:hover .bt-title, .series-card > summary:hover .bt-title { color: var(--accent); }

/* The stack: two back cards peeking out below the front one, each a
   little narrower and a shade deeper. On hover the deck fans out;
   opened, it tucks back in under the list of parts. */
.bt-stack { position: relative; isolation: isolate; }
.bt-stack::before, .bt-stack::after {
  content: '';
  position: absolute; inset: 0; z-index: -1;
  border: 1px solid var(--border); border-radius: 10px;
  transition: transform 0.35s var(--ease-out), border-color 0.18s;
}
.bt-stack::before {
  background: color-mix(in srgb, var(--bg-raised) 72%, var(--border));
  transform: translateY(9px) scaleX(0.955);
}
.bt-stack::after {
  z-index: -2;
  background: color-mix(in srgb, var(--bg-raised) 45%, var(--border));
  transform: translateY(18px) scaleX(0.91);
}
.bt-stack:hover::before { transform: translateY(11px) scaleX(0.955) rotate(1deg); }
.bt-stack:hover::after  { transform: translateY(22px) scaleX(0.91) rotate(-1.4deg); }
.bt-stack:hover::before, .bt-stack:hover::after { border-color: var(--text-3); }
.bt-stack:has(.series-card[open])::before { transform: translateY(6px) scaleX(0.97); }
.bt-stack:has(.series-card[open])::after  { transform: translateY(12px) scaleX(0.94); }
.series-card[open] { border-color: var(--accent); }
.series-card > summary {
  list-style: none; cursor: pointer;
  padding-right: calc(var(--s3) + 24px);
  position: relative;
}
.series-card > summary::-webkit-details-marker { display: none; }
.series-card > summary::after {          /* the same chevron as details.more */
  content: '';
  position: absolute; right: var(--s3); top: calc(var(--s2) + 5px);
  width: 7px; height: 7px;
  border-right: 1.5px solid var(--text-3);
  border-bottom: 1.5px solid var(--text-3);
  transform: rotate(45deg);
  transition: transform 0.2s var(--ease-out);
}
.series-card[open] > summary::after { transform: rotate(-135deg); top: calc(var(--s2) + 9px); }
.series-card > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 10px; }
.series-card-parts {
  padding: 0 var(--s3) var(--s1);
  border-top: 1px solid var(--border);
}
.series-card-parts .post-row {
  grid-template-columns: 56px 1fr auto;
  margin: 0 calc(var(--s1) * -1); padding: 12px var(--s1);
}
.series-card-parts .post-row-title { font-size: 1.05rem; }
.series-card-parts .post-row-date { padding-top: 3px; }

.group-count { letter-spacing: inherit; }

/* Blog index toolbar: one hairline row (search, status, jump links)
   and a line of tag filters under it, set in the same mono labels as
   the rest of the listing, so it reads as part of the page, not a form.
   Rendered hidden and switched on by assets/js/blog-index.js, which
   hides non-matching entries in place; the order never changes.
   .post-row sets display:grid, so [hidden] needs saying explicitly. */
.listing-wrap [hidden] { display: none !important; }
.post-row.is-last-shown { border-bottom: none; }

.listing-tools {
  padding: var(--s3) 0 0;
  display: grid; gap: 12px;
}
.listing-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0 var(--s3);
  border-bottom: 1px solid var(--border);
  transition: border-color 0.15s;
}
.listing-bar:focus-within { border-color: var(--accent); }
.listing-search {
  flex: 1 1 200px; min-width: 0;
  display: flex; align-items: center; gap: 10px;
  cursor: text;
}
.listing-search input {
  flex: 1; min-width: 0;
  font-family: var(--f-mono); font-size: 12px; letter-spacing: 0.04em;
  color: var(--text);
  background: transparent; border: none; border-radius: 0;
  padding: 12px 0;
  -webkit-appearance: none; appearance: none;
}
@media (pointer: coarse) { .listing-search input { font-size: 16px; letter-spacing: 0; } }  /* under 16px, iOS zooms on focus */
.listing-search input::placeholder { color: var(--text-3); }
.listing-search input:focus { outline: none; }
.listing-search input::-webkit-search-cancel-button { -webkit-appearance: none; }
.listing-search-ic {
  flex-shrink: 0; width: 14px; height: 14px;
  fill: none; stroke: var(--text-3); stroke-width: 2; stroke-linecap: round;
  transition: stroke 0.15s;
}
.listing-bar:focus-within .listing-search-ic { stroke: var(--accent); }
.listing-search-key {
  font-family: var(--f-mono); font-size: 10px; line-height: 1;
  color: var(--text-3);
  border: 1px solid var(--border); border-radius: 4px;
  padding: 3px 6px;
}
.listing-search input:focus ~ .listing-search-key,
.listing-search input:not(:placeholder-shown) ~ .listing-search-key { display: none; }
@media (hover: none) { .listing-search-key { display: none; } }   /* no keyboard to press "/" on */

.listing-status {
  font-family: var(--f-mono); font-size: 9px; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--text-3);
}
.listing-status:empty { display: none; }
.listing-jump { display: flex; gap: var(--s2); }
.listing-jump a {
  font-family: var(--f-mono); font-size: 9px; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--text-3); text-decoration: none;
  padding: 12px 0;
}
.listing-jump a:hover { color: var(--accent); }

.listing-tags { display: flex; flex-wrap: wrap; gap: 4px 14px; }
.listing-tag, .listing-tag-more {
  font-family: var(--f-mono); font-size: 9px; letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 3px 0;
  color: var(--text-3);
  background: none; border: none;
  border-bottom: 1px solid transparent;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}
.listing-tag span { margin-left: 5px; opacity: 0.6; }
.listing-tag:hover, .listing-tag-more:hover { color: var(--text); }
.listing-tag[aria-pressed="true"] { color: var(--accent); border-bottom-color: var(--accent); }
.listing-tag-more { color: var(--text-2); }
.listing-tag.is-extra { display: none; }
.listing-tools.tags-open .listing-tag.is-extra,
.listing-tag.is-extra.is-pinned { display: inline-block; }

.listing-none { padding: var(--s8) 0 var(--s4); }
.listing-clear {
  display: block; margin: var(--s2) auto 0;
  font-family: var(--f-mono); font-size: 10px; letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent); background: none; border: none; cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px;
}

@media (max-width: 640px) {
  .bt-year { --bt-gutter: 24px; --bt-rail: 5px; }
  .bt-card, .series-card > summary { padding-left: var(--s2); padding-right: var(--s2); }
  .series-card > summary { padding-right: calc(var(--s2) + 22px); }
  .series-card > summary::after { right: var(--s2); }
  .series-card-parts { padding: 0 var(--s2) var(--s1); }
  .bt-title { font-size: 1.15rem; }
  .listing-jump { display: none; }   /* the timeline is short enough to scroll */
}


/* ════════════════════════════════════════════════════════════════
   POST HERO
   
   The single post view opens with a dark full-bleed field.
   The title is very large: the page announces itself.
   Below the fold, the body returns to warm paper.
   
   Unusual choice: the title is set in the display font at an
   outsized scale, but the meta row directly below is tiny mono.
   The gap in scale is deliberate; it creates vertical rhythm.
════════════════════════════════════════════════════════════════ */
.post-hero {
  background: transparent;          /* parallax scene shows through */
  /* The hero is allowed the room: it is the one place on the page
     that isn't competing with the rails, and it sits on a wider inset
     than --page-pad so the title has air on both sides. */
  padding: var(--s8) var(--s8) var(--s6);
  position: relative; overflow: hidden;
}

/* Readability wash is on the fixed scene now (.px::after); see .page-header. */

/* A thin lacquer line across the hero foot, marks the plane change */
.post-hero::after {
  content: '';
  position: absolute; bottom: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(
    to right,
    transparent,
    var(--lacquer) 15%,
    var(--lacquer) 85%,
    transparent
  );
  opacity: 0.35;
}

.post-hero.news::after { opacity: 0.55; }

.post-hero-inner {
  /* No cap: .post-hero's own padding is the frame. One column, read top
     to bottom: crumb, title, meta, excerpt. */
  max-width: none; margin: 0 auto;
  position: relative; z-index: 1;
}

/* Breadcrumb: stays out of the way */
.post-crumb {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--f-mono); font-size: 9px;
  letter-spacing: 0.18em; text-transform: uppercase;
  margin-bottom: var(--s3);
}
.post-crumb a { color: var(--plane-ink-2); text-decoration: none; transition: color 0.14s; }
.post-crumb a:hover { color: var(--plane-ink); }
.post-crumb .sep { color: var(--plane-faint); }

.cat-pill {
  font-family: var(--f-mono); font-size: 8px;
  letter-spacing: 0.16em; text-transform: uppercase;
  padding: 2px 9px; border-radius: 2px;
}
.cat-pill.blog { background: var(--pill-blog-bg); color: var(--pill-blog-fg); }
.cat-pill.news { background: var(--pill-news-bg); color: var(--pill-news-fg); }

/* The post title: display serif at full stretch */
.post-title {
  font-family: var(--f-display);
  font-size: clamp(2.2rem, 4vw, 3.5rem);
  font-weight: var(--w-display-xl);
  line-height: 1.08;
  color: var(--plane-ink);
  letter-spacing: -0.02em;
  margin-bottom: var(--s4);
  /* In em, so the line keeps the same character count as the clamp
     resizes the type rather than getting longer on big screens. */
  max-width: 16em;
}
.post-title em { font-style: normal; color: var(--lacquer-light); }

/* Meta row: tiny, far below the title, like a caption */
.post-meta {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 0.15rem 0.8rem;
  font-family: var(--f-mono); font-size: 10px;
  color: var(--plane-ink-2); letter-spacing: 0.07em;
}
.post-meta .sep { color: var(--plane-faint); }
.meta-item { display: inline-flex; align-items: center; gap: 5px; }
.meta-item svg { width: 11px; height: 11px; opacity: 0.7; }
.post-source { color: var(--accent); }

/* Excerpt: italic, indented with lacquer hairline */
.post-excerpt {
  margin-top: var(--s4);
  font-size: 15px; line-height: 1.6;
  color: var(--plane-ink-2);
  font-style: italic; max-width: 60ch;
  padding-left: var(--s3);
  border-left: 1.5px solid var(--lacquer);
}


/* ════════════════════════════════════════════════════════════════
   POST BODY GRID
════════════════════════════════════════════════════════════════ */
/* Two rails by default: article + TOC on the right. A post that belongs
   to a series also gets a left rail (.has-rail) holding the parts list,
   but only once the viewport can afford a third column, see RESPONSIVE.
   Below that width the parts card becomes a strip above the article. */
/* The top offset lives here, on the container, not on the columns. A
   margin on a sticky column is added to its sticky `top`, so the rails
   would hang that far below the header for the whole scroll. */
.post-wrap {
  max-width: none;
  margin: var(--s5) auto 0; padding: 0 var(--page-pad);
  display: grid;
  grid-template-columns: 1fr var(--sidebar-w);
  column-gap: var(--col-gap);
  row-gap: 0;               /* the strip above the article sets its own */
  align-items: start;
}
/* A collapsed rail keeps its track, at zero width: the pill sits in it
   and overhangs into the page gutter, so the article gets the column
   back without the rail leaving the grid. */
.post-wrap:has(> .post-sidebar.collapsed) { grid-template-columns: 1fr 0px; }
/* Fallback placement: a full-width strip above the article. It only
   becomes a true left rail once there is room for a third column.
   As a strip the parts list lies flat: no scroll box, it's short and
   the page can absorb it. */
.post-rail {
  grid-column: 1 / -1; grid-row: 1;
  margin-bottom: var(--s4);   /* strip mode only; the rail resets it */
  min-width: 0;
}
.post-rail .sidebar-card { margin-bottom: 0; }
/* Wide and short: the parts flow into as many columns as the strip can
   hold, which usually means no scrolling at all. */
.post-rail .series-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  column-gap: var(--s5);
  overflow: visible;
}


/* ════════════════════════════════════════════════════════════════
   ARTICLE PROSE
   
   The goal: a book page that happens to be on screen.
   The §-marker lives in the negative space: margin reading.
   First paragraph gets a slightly larger lead, like a magazine opener.
   No bullet icons, no decorative flourishes. Typography alone.
════════════════════════════════════════════════════════════════ */
.post-content {
  padding: var(--s6) var(--s5) var(--s8);
  min-width: 0;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 14px;
  box-shadow: var(--panel-shadow);
  backdrop-filter: blur(var(--glass-blur-scrim));
  -webkit-backdrop-filter: blur(var(--glass-blur-scrim));
}

/* First paragraph: lead-in size, a gentle entry */
.post-content > p:first-of-type {
  font-size: 1.06em;
  color: var(--text-2);
  line-height: 1.8;
}

.post-content p { margin-bottom: 1.5rem; }

/* H2: display serif, regular weight. The § in the margin is the design. */
.post-content h2 {
  font-family: var(--f-display);
  font-size: clamp(1.6rem, 3vw, 2rem);
  font-weight: var(--w-display);
  color: var(--text);
  margin: var(--s8) 0 var(--s2);
  letter-spacing: -0.015em;
  line-height: 1.15;
  position: relative;
}
.post-content h2::before {
  content: '§';
  position: absolute; left: -1.8rem;
  font-family: var(--f-mono); font-size: 0.78rem;
  color: var(--accent); opacity: 0.5; top: 0.35rem;
  letter-spacing: 0;
}

/* H3: mono uppercase, very small: a classification mark */
.post-content h3 {
  font-family: var(--f-mono); font-size: 9.5px;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--text-2); margin: var(--s6) 0 var(--s2);
}

.post-content h4 {
  font-family: var(--f-serif); font-size: 1rem; font-weight: 700;
  color: var(--text); margin: var(--s4) 0 var(--s1);
}

/* Links: always underlined, underline colour changes on hover */
.post-content a {
  color: var(--link);
  text-decoration: underline;
  text-decoration-color: var(--link-uline);
  text-underline-offset: 3px;
  transition: text-decoration-color 0.15s, color 0.15s;
}
.post-content a:hover {
  color: var(--text);
  text-decoration-color: var(--accent);
}

.post-content ul, .post-content ol {
  margin: var(--s1) 0 var(--s4) var(--s4);
}
.post-content li { margin-bottom: 0.35rem; }
.post-content ul li::marker { color: var(--accent); }
.post-content ol li::marker {
  font-family: var(--f-mono); font-size: 0.82em; color: var(--text-3);
}

/* Inline code */
.post-content code {
  font-family: var(--f-mono); font-size: 0.85em;
  background: var(--code-bg); color: var(--code-fg);
  padding: 1px 5px; border-radius: 2px;
}

/* Code blocks: dark field, lacquer left edge */
.post-content pre {
  background: var(--night);
  border-left: 2px solid var(--lacquer);
  padding: var(--s4) var(--s4);
  overflow-x: auto; margin: var(--s5) 0;
  border-radius: 1px;
}
.post-content pre code {
  background: none; color: #D8DEE9;
  padding: 0; font-size: 12.5px; line-height: 1.7;
}

/* Syntax highlighting: Rouge tags the tokens at build time (see
   _config.yml); these colour them. The field is --night in both
   themes, so one palette serves both: Nord's own syntax colours, with
   comments lifted from Nord's #616E88 so they clear 4.5:1 on it. */
.post-content .highlight .c, .post-content .highlight .c1,
.post-content .highlight .cm, .post-content .highlight .cs,
.post-content .highlight .ch, .post-content .highlight .cd { color: #939FB5; font-style: italic; }
.post-content .highlight .cp, .post-content .highlight .cpf { color: #81A1C1; }
.post-content .highlight .k,  .post-content .highlight .kd,
.post-content .highlight .kn, .post-content .highlight .kr,
.post-content .highlight .kp, .post-content .highlight .kv,
.post-content .highlight .kc, .post-content .highlight .o,
.post-content .highlight .ow, .post-content .highlight .nt,
.post-content .highlight .nb, .post-content .highlight .bp { color: #81A1C1; }
.post-content .highlight .kt, .post-content .highlight .nc,
.post-content .highlight .nn, .post-content .highlight .na,
.post-content .highlight .no, .post-content .highlight .nl { color: #8FBCBB; }
.post-content .highlight .nf, .post-content .highlight .fm,
.post-content .highlight .ne { color: #88C0D0; }
.post-content .highlight .nd { color: #D08770; }
.post-content .highlight .s,  .post-content .highlight .s1,
.post-content .highlight .s2, .post-content .highlight .sh,
.post-content .highlight .sb, .post-content .highlight .sc,
.post-content .highlight .sd, .post-content .highlight .sx,
.post-content .highlight .sr, .post-content .highlight .ss,
.post-content .highlight .sa, .post-content .highlight .dl { color: #A3BE8C; }
.post-content .highlight .se, .post-content .highlight .si { color: #EBCB8B; }
.post-content .highlight .m,  .post-content .highlight .mi,
.post-content .highlight .mf, .post-content .highlight .mh,
.post-content .highlight .mo, .post-content .highlight .mb,
.post-content .highlight .il { color: #B48EAD; }
.post-content .highlight .gd { color: #BF616A; }
.post-content .highlight .gi { color: #A3BE8C; }
.post-content .highlight .gh, .post-content .highlight .gu { color: #88C0D0; font-weight: 600; }
/* Rouge flags what its lexer can't parse; in prose-adjacent snippets
   that is usually fine code, so no alarm colour. */
.post-content .highlight .err { color: inherit; background: none; }

/* Mermaid diagrams: rendered client-side, see default.html.
   Own field (not the code-block "night" background) since the SVG paints its
   own node colors and needs a neutral, theme-matched backdrop to sit on. */
.post-content .mermaid-diagram {
  margin: var(--s5) 0; padding: var(--s4);
  background: var(--surface, var(--code-bg));
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow-x: auto;
  text-align: center;
}
.post-content .mermaid-diagram svg { max-width: 100%; height: auto; }
.post-content .mermaid-diagram:empty { display: none; }

/* Blockquote: a warm field, no decorative quote mark */
.post-content blockquote {
  margin: var(--s6) 0;
  padding: var(--s3) var(--s4);
  background: var(--quote-bg);
  border-left: 2px solid var(--lacquer);
}
.post-content blockquote p {
  font-family: var(--f-display);
  font-size: 1.28rem; font-weight: 500; font-style: normal;
  line-height: 1.5; color: var(--quote-fg); margin-bottom: 0;
}

/* HR: three dots, sparse */
.post-content hr {
  border: none; height: 1px; background: var(--border);
  margin: var(--s8) 0; position: relative;
}
.post-content hr::after {
  content: '· · ·';
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  background: var(--bg); padding: 0 var(--s2);
  font-family: var(--f-mono); font-size: 10px;
  color: var(--text-3); letter-spacing: 0.24em;
}

/* Images: centred in the measure, caption tracks the image */
.post-content img {
  max-width: 100%; display: block;
  border: 1px solid var(--border); margin: var(--s5) auto;
  border-radius: 3px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.post-content figure { margin: var(--s6) 0; text-align: center; }
.post-content figcaption {
  font-family: var(--f-mono); font-size: 9px;
  letter-spacing: 0.1em; color: var(--text-3);
  margin-top: var(--s1);
}

/* Tables */
.post-content table {
  width: 100%; border-collapse: collapse; font-size: 14px; margin: var(--s5) 0;
}
.post-content th {
  background: var(--th-bg); color: var(--th-fg);
  font-family: var(--f-mono); font-size: 9px;
  letter-spacing: 0.14em; text-transform: uppercase;
  padding: 9px 14px; text-align: left; font-weight: 400;
  border-bottom: 1px solid var(--th-border);
}
.post-content td { padding: 9px 14px; border-bottom: 1px solid var(--border); vertical-align: top; }

/* A table wider than the column scrolls inside its own box, never the
   page. The wrapper is added by the post layout's script; without JS,
   narrow screens fall back to making the table itself the scroller. */
.post-content .table-wrap {
  max-width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  margin: var(--s5) 0;
}
.post-content .table-wrap > table { margin: 0; }
.post-content .table-wrap:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 720px) {
  .post-content table { display: block; max-width: 100%; overflow-x: auto; }
  .post-content .table-wrap > table { display: table; max-width: none; overflow: visible; }
  .post-content th, .post-content td { padding: 8px 10px; }
}

/* Long unbroken inline code (hashes, binary strings, URLs) wraps rather
   than pushing the column wider. Code blocks keep their own scroller. */
.post-content :not(pre) > code { overflow-wrap: anywhere; }
.post-content tr:nth-child(even) td { background: var(--stripe); }

/* Post tags */
.post-tags {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin-top: var(--s8); padding-top: var(--s4);
  border-top: 1px solid var(--border);
}
.post-tag {
  font-family: var(--f-mono); font-size: 9px; letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 3px 10px; border: 1px solid var(--border);
  color: var(--text-3); border-radius: 1px;
  transition: border-color 0.15s, color 0.15s;
}
.post-tag {
  display: inline-flex; align-items: center; gap: 5px;
}
.post-tag svg { width: 10px; height: 10px; opacity: 0.6; }
.post-tag:hover { border-color: var(--accent); color: var(--text); }
.post-tag:hover svg { opacity: 1; }


/* ════════════════════════════════════════════════════════════════
   POST NAVIGATION
   Two columns. Each is a flat plane. No fuss.

   Held to the same measure as the article and the "more posts" band;
   it used to run full-bleed, which read as part of the footer chrome
   and left the article sitting directly on top of it.
════════════════════════════════════════════════════════════════ */
.post-nav {
  /* width, not padding: the border-top has to stop where the content
     stops, so the gutters live outside the box rather than inside it */
  width: calc(100% - var(--page-pad) * 2);
  max-width: none;
  margin: var(--s5) auto;
  display: grid; grid-template-columns: 1fr 1fr;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.post-nav-item {
  background: var(--bg); padding: var(--s4) var(--s5);
  text-decoration: none; color: inherit;
  transition: background 0.15s, box-shadow 0.15s;
  border-right: 1px solid var(--border);
  min-width: 0;
}
.post-nav-item:last-child { border-right: none; }
.post-nav-item:hover {
  background: var(--bg-raised);
  box-shadow: inset 0 0 0 1px rgba(54,76,110,0.15);
}
.post-nav-item.next { text-align: right; }

.post-nav-dir {
  font-family: var(--f-mono); font-size: 8px;
  letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--text-3);
  display: flex; align-items: center; gap: 6px;
  margin-bottom: 5px;
}
.post-nav-item.next .post-nav-dir { justify-content: flex-end; }
.post-nav-dir svg { width: 12px; height: 12px; }
/* The arrow leans into the direction of travel on hover */
.post-nav-item.prev:hover .post-nav-dir svg { transform: translateX(-3px); }
.post-nav-item.next:hover .post-nav-dir svg { transform: translateX(3px); }
.post-nav-dir svg { transition: transform 0.18s var(--ease-out); }
.post-nav-title {
  font-family: var(--f-display); font-size: 1.1rem;
  font-weight: var(--w-display); color: var(--navtitle-fg); line-height: 1.3;
}


/* ════════════════════════════════════════════════════════════════
   SIDEBAR
════════════════════════════════════════════════════════════════ */
/* The rail is one viewport-tall column: cards are laid out top-down and
   the TOC absorbs every pixel the other cards don't claim, so a long
   article's contents stay visible instead of running off the fold.
   The padding is deliberately thin: every rem here is a rem the TOC
   doesn't get. `overflow: hidden` is the guard that matters: without it
   a card that refuses to shrink paints straight over the post nav and
   the footer below, because the sticky box's height is capped but its
   overflowing content is not clipped. */
.post-sidebar {
  /* No margin and no padding of its own: it starts at the row's top edge
     and runs to within --s2 of the viewport floor, which is as much room
     as the TOC can be given. .post-rail is set up identically so the two
     always agree. */
  padding: 0;
  position: sticky; top: calc(var(--h-header) + var(--s1));
  /* max-, not a fixed height: on a short post the rail must stay short
     too, or the grid row grows to a full viewport of empty column. */
  max-height: calc(100vh - var(--h-header) - var(--s1) * 2);
  display: flex; flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

.sidebar-card {
  /* The header's glass: same tint and blur as the bar above it. */
  background: var(--header-glass);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  /* Tight: the rail is narrow, so padding here is width taken from the
     entries themselves, and vertically it is TOC rows lost. */
  padding: var(--s2) var(--s2);
  margin-bottom: var(--s2);
  backdrop-filter: blur(var(--glass-blur-nav)) var(--glass-fx);
  -webkit-backdrop-filter: blur(var(--glass-blur-nav)) var(--glass-fx);
  box-shadow:
    0 1px 3px rgba(0,0,0,0.06),
    inset 0 1px 0 var(--glass-hi);   /* hairline top highlight = the "glass" tell */
}

/* Card head: icon, label, collapse control. The head is the one part
   that survives collapsing; it becomes the pill. */
.sidebar-head {
  /* Never shrinks. The card is a flex column with overflow:hidden, so a
     shrinkable head gets squeezed by the list below it: the label
     clips against the card border and the toggle disappears with it. */
  flex: 0 0 auto;
  display: flex; align-items: flex-start; gap: 7px;
  margin-bottom: var(--s1); padding-bottom: var(--s1);
  border-bottom: 1px solid var(--border);
}
.sidebar-icon {
  flex: 0 0 auto; display: grid; place-items: center;
  height: 14px;                 /* aligns with the label's first line */
  color: var(--text-3);
}
.sidebar-icon svg { width: 12px; height: 12px; display: block; }

.sidebar-label {
  font-family: var(--f-mono); font-size: 8px;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--text-3); display: block;
  flex: 1 1 auto; min-width: 0;
  line-height: 1.45;
  overflow-wrap: anywhere;      /* a long series name must not overflow */
}
/* "Part 8 of 10" breaks as one unit, never leaving the total alone on
   a line of its own. */
.sidebar-part { white-space: nowrap; }

.rail-toggle {
  flex: 0 0 auto;
  width: 22px; height: 22px; padding: 0;
  margin-top: -2px;             /* optically level with the label */
  display: grid; place-items: center;
  background: none; border: 1px solid transparent; border-radius: 6px;
  color: var(--text-3); cursor: pointer;
  transition: color 0.14s, background 0.14s, border-color 0.14s;
}
.rail-toggle:hover {
  color: var(--accent);
  background: var(--bg-raised);
  border-color: var(--border);
}
.rail-toggle svg { width: 13px; height: 13px; display: block; }
.rail-toggle .ic-expand { display: none; }

/* ── Collapsed: a tab docked to the page edge ────────────────────
   The list is dropped, the toggle swaps its chevron for the section's
   own icon, and the heading turns on its side beneath it, so a docked
   rail still says what it is, not just which icon it wears. Only the
   inner corners are rounded, so the pill reads as docked to the page
   edge rather than floating near it. It stays inside the article's
   row (see below for why). */
.post-rail.collapsed,
.post-sidebar.collapsed {
  /* Still a grid item, not fixed. Fixed positioning pinned the pill to
     the window, so it floated up over the hero and stayed there for the
     whole page; as a grid item it lives in the article's row and can
     only travel within it. Its track is collapsed to 0 and the pill is
     pulled out through the page gutter by a negative margin, so it
     reaches the window edge without holding a column open. */
  display: block;
  width: var(--pill-w);
  max-height: none; min-height: 0;
  padding: 0;
  overflow: visible;
  z-index: 40;
}
.post-rail.collapsed {
  /* Strip mode: a tab above the article, flush left. */
  grid-column: 1 / -1; grid-row: 1;
  justify-self: start;
  margin: 0 0 var(--s3) calc(-1 * var(--page-pad));
}
.post-sidebar.collapsed {
  position: sticky;
  top: calc(var(--h-header) + var(--s2));
  justify-self: end;
  margin: 0 calc(-1 * var(--page-pad)) 0 0;
}

.post-rail.collapsed .sidebar-card,
.post-sidebar.collapsed .sidebar-card {
  width: var(--pill-w);
  min-height: 0;
  padding: 5px 4px 9px;
  margin: 0;
  cursor: pointer;              /* the whole tab expands, not just the button */
  box-shadow: 0 6px 18px rgba(0,0,0,0.18), inset 0 1px 0 var(--glass-hi);
}
.post-rail.collapsed .sidebar-card {
  border-radius: 0 9px 9px 0;
  border-left-color: transparent;
}
.post-sidebar.collapsed .sidebar-card {
  border-radius: 9px 0 0 9px;
  border-right-color: transparent;
}

/* ── Open: docked to the same edge ───────────────────────────────
   An open rail keeps the tab's footing: it sits against the window
   edge, square and borderless on that side, rather than floating a
   gutter in from it. The page gutter on that side is dropped (see
   .post-wrap in RESPONSIVE), so the rail keeps its width and the
   article takes the gutter it gave up. Only where the rail really is
   a side column: the TOC is hidden at 860px and under, and the parts
   rail docks from 1024px. */
.post-sidebar:not(.collapsed) .sidebar-card {
  border-radius: 9px 0 0 9px;
  border-right-color: transparent;
}

/* The whole tab is the control, so the whole tab answers the pointer:
   icon and words together, not the 30px button alone. The docked edge
   keeps its transparent border either way, so the tab still reads as
   part of the page edge rather than as a floating outline. */
.post-rail.collapsed .sidebar-card,
.post-sidebar.collapsed .sidebar-card {
  transition: background 0.16s, border-color 0.16s, color 0.16s;
}
.post-rail.collapsed .sidebar-card:hover,
.post-sidebar.collapsed .sidebar-card:hover {
  background: var(--bg-raised);
  color: var(--accent);
  border-top-color: var(--accent);
  border-bottom-color: var(--accent);
}
.post-rail.collapsed .sidebar-card:hover { border-right-color: var(--accent); }
.post-sidebar.collapsed .sidebar-card:hover { border-left-color: var(--accent); }

/* Both children follow the card rather than lighting on their own */
.post-rail.collapsed .sidebar-card:hover .sidebar-label,
.post-sidebar.collapsed .sidebar-card:hover .sidebar-label,
.post-rail.collapsed .sidebar-card:hover .rail-toggle,
.post-sidebar.collapsed .sidebar-card:hover .rail-toggle { color: inherit; }

/* The button's own hover chrome would be a second box inside the tab */
.post-rail.collapsed .rail-toggle:hover,
.post-sidebar.collapsed .rail-toggle:hover {
  background: none;
  border-color: transparent;
}
.post-rail.collapsed .sidebar-head,
.post-sidebar.collapsed .sidebar-head {
  margin: 0; padding: 0; border-bottom: none;
  flex-direction: column;
  align-items: center;
  gap: 7px;
}
/* The button carries the section icon while collapsed, so the separate
   icon would be the same glyph twice. */
.post-rail.collapsed .sidebar-icon,
.post-rail.collapsed .series-list,
.post-sidebar.collapsed .sidebar-icon,
.post-sidebar.collapsed #toc-list { display: none; }

/* The heading, turned on its side. vertical-rl reads top-to-bottom,
   which is the direction a docked tab is scanned in. It is capped and
   clipped rather than allowed to run: a long series name would
   otherwise make the tab taller than the screen. */
.post-rail.collapsed .sidebar-label,
.post-sidebar.collapsed .sidebar-label {
  flex: 0 1 auto;
  writing-mode: vertical-rl;
  text-orientation: mixed;
  white-space: nowrap;
  overflow: hidden;
  max-height: 46vh;
  min-width: 0;
  letter-spacing: 0.18em;
  line-height: 1;
}
/* Each tab's words lean away from the edge they are docked to: the
   right tab reads downward, the left tab upward. vertical-rl only gives
   the downward direction, so the left one is turned through 180°, a
   paint-only change, the box it occupies is unaffected. */
.post-rail.collapsed .sidebar-label { transform: rotate(180deg); }

/* Icon above the words, whatever the source order says */
.post-rail.collapsed .rail-toggle,
.post-sidebar.collapsed .rail-toggle { order: -1; }

/* Collapsed, the card must not stretch down the rail; it is a pill.
   min-height has to go too: #toc-card carries a 4rem floor so the open
   TOC never shrinks to nothing, and that floor was showing up as empty
   space under the pill's icon. */
.post-sidebar.collapsed #toc-card {
  flex: 0 0 auto;
  min-height: 0;
  overflow: visible;
}

.post-rail.collapsed .rail-toggle,
.post-sidebar.collapsed .rail-toggle { width: 30px; height: 30px; }
.post-rail.collapsed .ic-collapse,
.post-sidebar.collapsed .ic-collapse { display: none; }
.post-rail.collapsed .ic-expand,
.post-sidebar.collapsed .ic-expand { display: block; }
.post-rail.collapsed .rail-toggle svg,
.post-sidebar.collapsed .rail-toggle svg { width: 15px; height: 15px; }

/* TOC card: the rail's only tenant, so it simply takes the whole rail
   and scrolls its list internally. */
#toc-card {
  display: flex; flex-direction: column;
  flex: 1 1 auto; min-height: 4rem;
  margin-bottom: 0;
  overflow: hidden;
}
#toc-card .sidebar-head { flex: 0 0 auto; }

#toc-list {
  list-style: none;
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  /* Thin, unobtrusive scrollbar: the rail is a quiet surface */
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
  padding-right: 2px;
}
#toc-list::-webkit-scrollbar { width: 5px; }
#toc-list::-webkit-scrollbar-track { background: transparent; }
#toc-list::-webkit-scrollbar-thumb {
  background: var(--border); border-radius: 3px;
}
#toc-list::-webkit-scrollbar-thumb:hover { background: var(--text-3); }

#toc-list li { padding: 2px 0; }
#toc-list a {
  font-family: var(--f-mono); font-size: 10.5px;
  color: var(--text-2); text-decoration: none;
  display: block; line-height: 1.45;
  padding-left: 0;
  border-left: 1.5px solid transparent;
  transition: color 0.14s, padding-left 0.14s, border-color 0.14s;
}
#toc-list a:hover { color: var(--text); }
#toc-list a.active {
  color: var(--accent);
  padding-left: 7px;
  border-left-color: var(--accent);
}
#toc-list .toc-h3 { padding-left: 10px; opacity: 0.65; }

/* ════════════════════════════════════════════════════════════════
   MORE POSTS: the band under the article

   Sits on the same measure as the article so the page keeps one spine.
   Columns are separated by the same hairline rules the listing uses,
   not by boxes: this is a footnote to the article, not a card deck.
════════════════════════════════════════════════════════════════ */
/* Same content box as .post-wrap (which is max-w with an s5 gutter
   inside it), so every edge on the page lines up. */
.post-more {
  width: calc(100% - var(--page-pad) * 2);
  max-width: none;
  margin: 0 auto;             /* spacing comes from .post-nav's margin */
}
.post-more-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s3);
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--border);
}
.post-more-label {
  font-family: var(--f-mono); font-size: 9px;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--text-3);
}
.post-more-all {
  font-family: var(--f-mono); font-size: 9px;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-3); text-decoration: none;
  transition: color 0.15s;
  white-space: nowrap;
}
.post-more-all {
  display: inline-flex; align-items: center; gap: 6px;
}
.post-more-all svg { width: 12px; height: 12px; transition: transform 0.18s var(--ease-out); }
.post-more-all:hover { color: var(--accent); }
.post-more-all:hover svg { transform: translateX(3px); }

/* One row, always: auto-flow column puts every item on the same track
   whatever the count, and equal auto-columns keep them even. It only
   stacks at the phone breakpoint below. */
.post-more-grid {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 0;
}
.post-more-item {
  min-width: 0;
  display: flex; flex-direction: column; gap: 5px;
  padding: var(--s3) var(--s2) var(--s3);
  text-decoration: none; color: inherit;
  border-right: 1px solid var(--border);
  transition: background 0.15s;
}
/* Five across needs real width. Drop the tail rather than let the row
   squeeze into unreadable columns; the "All posts" link covers the rest. */
@media (max-width: 1100px) {
  .post-more-item:nth-child(n+4) { display: none; }
  /* item 3 becomes the last visible one, so it closes the row */
  .post-more-item:nth-child(3) { border-right: none; padding-right: 0; }
}
.post-more-item:first-child { padding-left: 0; }
.post-more-item:last-child { border-right: none; padding-right: 0; }
.post-more-item:hover { background: var(--bg-raised); }
.post-more-item time {
  font-family: var(--f-mono); font-size: 9px;
  letter-spacing: 0.1em; color: var(--text-3);
}
.post-more-title {
  font-family: var(--f-display); font-size: 1.02rem;
  font-weight: var(--w-display); line-height: 1.32; color: var(--navtitle-fg);
}
.post-more-item:hover .post-more-title { color: var(--accent); }
.post-more-series {
  font-family: var(--f-mono); font-size: 8px;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--text-3); margin-top: auto; padding-top: var(--s1);
}

/* Series ("parts") card. Wherever the card ends up (left rail, strip
   above the article), the list itself is always the scroll container, so
   the "centre the current part" script has one element to talk to. */
#series-card {
  display: flex; flex-direction: column;
  min-height: 0; overflow: hidden;
}
#series-card .sidebar-head { flex: 0 0 auto; }

.series-list {
  list-style: none;
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
.series-list::-webkit-scrollbar { width: 5px; }
.series-list::-webkit-scrollbar-track { background: transparent; }
.series-list::-webkit-scrollbar-thumb {
  background: var(--border); border-radius: 3px;
}
.series-list::-webkit-scrollbar-thumb:hover { background: var(--text-3); }
.series-list li { padding: var(--s1) 0; border-bottom: 1px solid var(--border); }
.series-list li:last-child { border-bottom: none; }
.series-list a {
  color: var(--text-2); text-decoration: none; font-size: 13px;
  line-height: 1.4; display: block; transition: color 0.14s;
}
.series-list a:hover { color: var(--text); }
.series-list li.current a { color: var(--accent); }


/* ════════════════════════════════════════════════════════════════
   404
════════════════════════════════════════════════════════════════ */
.error-page {
  max-width: 600px; margin: var(--s10) auto var(--s12);
  padding: 0 var(--s3); text-align: center;
}
/* The 404 itself: two digits and an orbit where the zero goes. Sized
   off the viewport so it fills a phone without overrunning it. */
.error-code {
  display: flex; align-items: center; justify-content: center;
  gap: 0.06em;
  font-family: var(--f-display); font-weight: var(--w-display-xl);
  font-size: clamp(5.5rem, 30vw, 10rem); line-height: 1;
  letter-spacing: -0.04em;
  color: var(--text);
  animation: error-float 6s ease-in-out infinite;
}
.error-digit {
  display: inline-block;
  animation: error-in 0.7s var(--ease-out) both, error-glitch 5s steps(1) 1.2s infinite;
}
.error-digit:last-child { animation-delay: 0.14s, 1.35s; }

/* The zero: a solid ring, a dashed track turning the other way, and a
   dot riding the track. */
.error-orbit {
  position: relative; flex-shrink: 0;
  width: 0.62em; height: 0.62em; margin: 0 0.12em;
  border: 0.085em solid currentColor; border-radius: 50%;
  animation: error-in 0.7s var(--ease-out) 0.07s both;
}
.error-orbit::before {
  content: ''; position: absolute; inset: -0.16em;
  border: 1.5px dashed color-mix(in srgb, var(--accent) 70%, transparent);
  border-radius: 50%;
  animation: error-spin 24s linear infinite reverse;
}
.error-sat {
  position: absolute; inset: -0.16em; border-radius: 50%;
  animation: error-spin 3.2s linear infinite;
}
.error-sat::after {
  content: ''; position: absolute; top: 0; left: 50%;
  width: 0.09em; height: 0.09em; margin: -0.045em 0 0 -0.045em;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0.08em 0.02em var(--accent), 0 0 0.3em color-mix(in srgb, var(--accent) 60%, transparent);
}

.error-msg {
  font-family: var(--f-display); font-size: clamp(1.5rem, 6vw, 2rem);
  font-weight: var(--w-display); letter-spacing: -0.01em;
  color: var(--text); margin: var(--s4) 0 var(--s1);
}
.error-text { color: var(--text-2); font-size: 15px; }
.error-actions {
  display: flex; flex-wrap: wrap; justify-content: center; gap: var(--s1) var(--s3);
  margin-top: var(--s4);
}
.error-actions a {
  font-family: var(--f-mono); font-size: 10px; letter-spacing: 0.18em;
  text-transform: uppercase; text-decoration: none;
  color: var(--link);
  padding: 10px 16px;
  border: 1px solid var(--border); border-radius: 999px;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.error-actions a:hover, .error-actions a:focus-visible {
  color: var(--accent); border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

@keyframes error-in {
  from { opacity: 0; transform: translateY(-0.25em) scale(0.92); }
}
@keyframes error-float {
  50% { transform: translateY(-6px); }
}
@keyframes error-spin {
  to { transform: rotate(360deg); }
}
/* Quiet for most of the cycle, then three frames of a split signal. */
@keyframes error-glitch {
  0%, 90%, 100% { transform: none; text-shadow: none; clip-path: none; }
  92% { transform: translateX(-0.03em); clip-path: inset(10% 0 45% 0);
        text-shadow: 0.05em 0 var(--accent), -0.04em 0 var(--accent-dim); }
  94% { transform: translateX(0.04em); clip-path: inset(55% 0 5% 0);
        text-shadow: -0.05em 0 var(--accent), 0.03em 0 var(--accent-dim); }
  96% { transform: translateX(-0.01em); clip-path: none;
        text-shadow: 0.02em 0 var(--accent); }
}
@media (prefers-reduced-motion: reduce) {
  .error-code, .error-digit, .error-orbit, .error-orbit::before,
  .error-sat { animation: none; }
}


/* ════════════════════════════════════════════════════════════════
   HOME: the portfolio

   Same grammar as the rest of the site: a hero plane floating on the
   parallax scene, then one continuous paper sheet holding every
   section, closed by a second plane. Sections are separated by the
   same mono rules the blog listing uses. Nothing here is a "card".
════════════════════════════════════════════════════════════════ */

/* Shared: the interpunct that joins two facts on one line */
.hero .sep, .home .sep { margin: 0 0.5em; opacity: 0.45; }

/* Sections fade up as they arrive. JS adds .in; print + reduced-motion
   force them visible (see the bottom of this block). */
/* ── Motion: everything arrives as you reach it ──────────────────
   [data-anim] elements get .anim from the home script and .in once
   they enter the viewport (after React Bits AnimatedContent and
   FadeContent). --anim-i, set on the children of a [data-stagger]
   parent, spaces them out. Without JS nothing gets .anim, so the
   page is simply there. */
.anim {
  opacity: 0;
  transition: opacity 0.7s var(--ease-out), transform 0.8s var(--ease-out);
  transition-delay: calc(var(--anim-i, 0) * 80ms);
  will-change: opacity, transform;
}
.anim[data-anim="up"]    { transform: translateY(28px); }
.anim[data-anim="left"]  { transform: translateX(-32px); }
.anim[data-anim="right"] { transform: translateX(32px); }
.anim[data-anim="fade"]  { transform: none; }
.anim.in { opacity: 1; transform: none; will-change: auto; }
/* A tile's own hover lift must not fight the entrance. */
.anim.in.glow:hover { transform: translateY(-2px); }

/* The lede, word by word (after React Bits ScrollReveal): --p is
   the paragraph's progress up the viewport, --w each word's place
   in it. A word is faint and soft until the progress passes it. */
.ds-lede.is-split .w {
  display: inline-block;
  opacity: calc(0.18 + 0.82 * clamp(0, (var(--p, 1) - var(--w) * 0.85) * 6, 1));
  filter: blur(calc((1 - clamp(0, (var(--p, 1) - var(--w) * 0.85) * 6, 1)) * 3px));
  transition: opacity 0.12s linear, filter 0.12s linear;
}

/* A sheen that slides across the address every few seconds (after
   React Bits ShinyText): the text is filled with a gradient that is
   mostly its own colour, with one bright band moving through it. */
.contact-email.shiny {
  background-image: linear-gradient(120deg,
    var(--plane-ink) 0%, var(--plane-ink) 40%,
    var(--accent) 50%,
    var(--plane-ink) 60%, var(--plane-ink) 100%);
  background-size: 250% auto; background-position: 150% center;
  background-repeat: repeat;   /* the ends match, so the tiles join unseen */
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: var(--plane-ink);
  animation: shine 5s linear infinite;
}
@keyframes shine {
  0%   { background-position: 150% center; }
  60%  { background-position: -50% center; }
  100% { background-position: -50% center; }
}
.contact-email.shiny:hover { -webkit-text-fill-color: var(--accent); animation-play-state: paused; background-size: 250% auto; }


/* ── Hero ──────────────────────────────────────────────────────
   Two columns on desktop: the name block, and the portrait rising
   out of its disc. The figure runs taller than the tagline block and
   is centred on it, so the two read as one composition.
──────────────────────────────────────────────────────────────── */
.hero {
  background: transparent;
  padding: var(--s6) var(--s5);
  position: relative; overflow: hidden;
}
/* Readability wash is on the fixed scene now (.px::after). */
.hero::after {
  content: ''; position: absolute; bottom: 0; left: var(--s5); right: var(--s5);
  height: 1px;
  background: linear-gradient(to right,
    var(--lacquer) 0%, var(--lacquer) 56px, transparent 56px);
}

/* Name block on the left, portrait plate on the right. The name runs
   big and carries the text column alone, so the portrait bottom-aligns
   with the link row, so the two sit on one baseline instead of the photo
   floating past the text top and bottom. */
.hero-inner {
  max-width: var(--max-w); margin: 0 auto;
  position: relative; z-index: 1;
  display: grid;
  /* The text column hugs the name (max-content) rather than taking all
     the spare width as 1fr; a 1fr column left a huge dead band between
     the name and the photo. The pair is centred as one unit. */
  grid-template-columns: max-content clamp(230px, 20vw, 300px);
  justify-content: center;
  column-gap: clamp(var(--s5), 4vw, var(--s6));
  align-items: center;
}
.hero-text { min-width: 0; }

/* Greeting between the eyebrow and the name, leading into it ("Hi 👋,
   I'm" → Rishabh Sharma). Small, in the display face; the hand waves on
   a loop with a long rest between waves. */
.hero-text .page-label { margin-bottom: var(--s2); }
.hero-hi {
  margin: 0 0 10px;
  font-family: var(--f-display);
  font-size: clamp(1.2rem, 2.2vw, 1.6rem);
  font-weight: var(--w-display);
  color: var(--plane-ink-2);
  letter-spacing: -0.01em;
}
.hero-wave {
  display: inline-block;
  margin-left: 2px;
  transform-origin: 72% 78%;
  animation: hero-wave 3s ease-in-out 0.6s infinite;
}
@keyframes hero-wave {
  0%   { transform: rotate(0deg); }
  6%   { transform: rotate(14deg); }
  12%  { transform: rotate(-8deg); }
  18%  { transform: rotate(14deg); }
  24%  { transform: rotate(-4deg); }
  30%  { transform: rotate(10deg); }
  36%, 100% { transform: rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-wave { animation: none; }
}

/* The name carries the page, and it carries it alone now: the
   tagline and status moved to About. So it runs large. Surname in
   italic accent. home-fx.js rasterises it into particles that spring
   into place and scatter under the pointer; the <h1> underneath stays
   for SEO / screen readers / no-JS. */
.hero-name-wrap {
  position: relative;
  display: block;
  width: max-content;
  max-width: 100%;
}
.hero-name {
  margin: 0;
  font-family: var(--f-display);
  font-size: clamp(3.2rem, 9vw, 6.6rem);
  font-weight: var(--w-display-xl);
  color: var(--plane-ink);
  letter-spacing: -0.03em;
  line-height: 0.92;
}
.hero-name em { font-style: normal; color: var(--accent); }
:root[data-theme="dark"] .hero-name { --fx-ink: #ECEFF4; --fx-accent: #88C0D0; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .hero-name { --fx-ink: #ECEFF4; --fx-accent: #88C0D0; }
}


.status-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent);
  margin-right: 9px; flex-shrink: 0;
  animation: pulse-dot 2.4s var(--ease-out) infinite;
}
@keyframes pulse-dot {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }
  50%      { box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 0%, transparent); }
}

/* Link pills: mono, hairline, accent on hover. They also drift a few
   px toward the pointer (see the magnetic handler in default.html). */
.hero-links { display: flex; flex-wrap: wrap; gap: var(--s1); margin-top: var(--s4); }
.hero-links a {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--f-mono); font-size: 9.5px;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--plane-ink-2); text-decoration: none;
  padding: 8px 15px;
  border: 1px solid var(--plane-rule);
  border-radius: 999px;
  position: relative; overflow: hidden;
  will-change: transform;
  transition: color 0.18s, border-color 0.18s,
              transform 0.35s var(--ease-out), background 0.18s;
}
/* A tint that wipes in from the left rather than just switching on */
.hero-links a::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  transform: scaleX(0); transform-origin: left;
  transition: transform 0.4s var(--ease-out);
}
.hero-links a:hover {
  color: var(--plane-ink);
  border-color: var(--accent);
}
.hero-links a:hover::before { transform: scaleX(1); }
.hero-link-ic { display: inline-flex; }
.hero-link-ic svg { width: 12px; height: 12px; display: block; }

/* ── Portrait: a cut-out rising out of a disc ────────────────
   The photo has no background of its own, so it stands in a circle
   drawn in the site's palette. The figure is the disc plus the room
   above it (1 : 1.22); .pf-cut clips to a shape that is square on top
   and round on the bottom, so the shoulders end at the circle's edge
   while the head rises past its rim. The image is sized from the
   width, so any cut-out of a similar shape lands the same way. */
.hero-portrait {
  margin: 0; position: relative; isolation: isolate;
  aspect-ratio: 1 / 1.22;
}
/* The disc is glass, not paint: the animated contour scene runs on
   behind it, softened by a light blur rather than covered, so the
   portrait sits IN the hero's world instead of on a sticker. The
   site's glass material (tint, edge, top light) plus a sheen across
   the face of it and a faint accent glow behind the head. */
.pf-disc {
  position: absolute; left: 0; right: 0; bottom: 0;
  aspect-ratio: 1; border-radius: 50%;
  background:
    linear-gradient(140deg,
      rgba(255,255,255,0.22) 0%, rgba(255,255,255,0.05) 38%, transparent 60%),
    radial-gradient(58% 52% at 50% 40%,
      color-mix(in srgb, var(--accent) 26%, transparent) 0%, transparent 75%),
    color-mix(in srgb, color-mix(in srgb, var(--accent) 14%, var(--bg-raised)) 40%, transparent);
  backdrop-filter: blur(2px) var(--glass-fx);
  -webkit-backdrop-filter: blur(2px) var(--glass-fx);
  border: 1px solid color-mix(in srgb, var(--accent) 38%, transparent);
  box-shadow:
    inset 0 1px 0 var(--glass-hi),
    inset 0 -18px 36px -18px color-mix(in srgb, var(--accent) 22%, transparent),
    0 24px 48px -26px rgba(46,52,64,0.4);
  transition: transform 0.6s var(--ease-out);
}
/* Bottom radii are half the width across and half the DISC's height
   down: 0.5 / 1.22 of the figure = 41%. */
.pf-cut {
  position: absolute; inset: 0; z-index: 1;
  overflow: hidden;
  border-radius: 0 0 50% 50% / 0 0 41% 41%;
}
.pf-cut img {
  position: absolute; bottom: 0; left: 50%;
  width: 116%; height: auto; max-width: none;
  transform: translateX(-50%);
  transform-origin: 50% 100%;
  /* A soft shadow to seat it, and a faint accent halo so dark hair
     above the rim still separates from the dark theme's ground. */
  filter: saturate(0.92)
    drop-shadow(0 8px 16px rgba(20,24,31,0.22))
    drop-shadow(0 0 14px color-mix(in srgb, var(--accent) 22%, transparent));
  transition: transform 0.6s var(--ease-out);
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .hero-portrait:hover .pf-cut img { transform: translateX(-50%) translateY(-4px) scale(1.02); }
  .hero-portrait:hover .pf-disc { transform: scale(1.015); }
}

/* Fallback when the photo is absent: the monogram in the disc. */
.portrait-monogram { display: none; }
.hero-portrait.no-img .pf-cut { display: none; }
.hero-portrait.no-img .portrait-monogram {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  aspect-ratio: 1;
  display: grid; place-items: center;
  font-family: var(--f-display); font-weight: var(--w-display);
  font-size: 4.5rem; letter-spacing: 0.06em;
  color: var(--plane-ink-2);
}

/* ── Hero entrance ────────────────────────────────────────────
   A short stagger down the column on load. Each child declares its
   own delay so the order is readable here rather than in JS. */
.hero-inner .hero-rise {
  opacity: 0; transform: translateY(16px);
  animation: hero-rise 0.7s var(--ease-out) forwards;
  animation-delay: var(--rise-d, 0s);
}
@keyframes hero-rise { to { opacity: 1; transform: none; } }


/* ── Bands + cards: the home page's structural system ────────
   The page is a stack of full-bleed BANDS. Each band centres a
   .band-inner column; scannable content sits in .card surfaces
   floating on the parallax scene, while prose keeps a bare
   reading column (.band-narrow) because a card is the wrong
   shape for an essay. This replaces the old single .home-wrap
   sheet, which gave eight very different sections one identical
   surface and no boundaries. */
.home { padding-bottom: var(--s8); }

.band { padding: var(--s10) 0; }
.band + .band { padding-top: 0; }
.band-inner {
  max-width: var(--max-w);
  margin: 0 auto;
  padding: 0 var(--s5);
}
/* Prose measure: long-form text wants ~70ch, not the full grid.
   These two bands are centred as whole units, heading and all, so the
   inset reads as a deliberate interlude rather than a stray indent. */
.band-narrow { max-width: 820px; }
/* Centred bands get a symmetric heading: a rule on BOTH sides, so the
   label sits in the middle instead of hanging off the left edge. */
.band-narrow .group-heading,
.band-mid    .group-heading { justify-content: center; }
.band-narrow .group-heading::before,
.band-mid    .group-heading::before {
  content: ''; flex: 1; height: 1px; background: var(--border);
}
.band-inner > .group-heading { margin-top: 0; margin-bottom: var(--s4); }

/* The card surface. Slightly more opaque than --panel: text sits
   directly on it, so the scene behind must not read through. */
.card {
  background: var(--card-bg);
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  padding: var(--s4);
  backdrop-filter: blur(var(--glass-blur-scrim)) var(--glass-fx);
  -webkit-backdrop-filter: blur(var(--glass-blur-scrim)) var(--glass-fx);
  box-shadow:
    0 1px 3px rgba(0,0,0,0.05),
    inset 0 1px 0 var(--glass-hi);
  transition: border-color 0.2s var(--ease-out),
              transform 0.2s var(--ease-out),
              box-shadow 0.2s var(--ease-out);
}

/* The reading sheet for prose: same family as .card but no lift,
   because nothing here is a target. */
.home-prose.sheet {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  padding: var(--s5) var(--s5);
  backdrop-filter: blur(var(--glass-blur-scrim));
  -webkit-backdrop-filter: blur(var(--glass-blur-scrim));
}


/* ── Dossier rail: navigation and section labels in one control ──
   A column of small monochrome icons fixed in the left margin, each
   ringed like a bead on a thread. The section you are reading lights
   up, names itself in a pill that sits against its card, and a fill
   line tracks how far down the page you are. This replaces two things
   that used to duplicate each other: a chip bar pinned under the
   header, and a label column inside every section.

   Shown only from 1200px up, where the margin has room for it. Below
   that it is `display:none` and each section carries its own compact
   heading (.ds-heading). Without JS it is still a plain vertical nav;
   JS adds `.is-ready` (past the hero) and `.is-active` (current dot). */
.drail { display: none; }

@media (min-width: 1200px) {
  .drail {
    display: flex; flex-direction: column;
    position: fixed;
    /* Hug the left edge of the viewport rather than tracking the
       content margin, so the dots stay in the deep gutter and the
       active label pill laps the card by as little as possible. */
    left: clamp(12px, calc((100vw - 1180px) / 2 - 116px), 40px);
    top: 50%;
    transform: translateY(-50%);
    z-index: 80;
    padding: var(--s2) 0;
    opacity: 0; visibility: hidden;
    transition: opacity .4s var(--ease-out), visibility .4s;
  }
  .drail.is-ready { opacity: 1; visibility: visible; }

  /* The spine: a hairline through the icon centres, filled from the
     top down to wherever you have read to. */
  .drail-line {
    position: absolute; left: 9px; top: var(--s2); bottom: var(--s2);
    width: 1px; background: var(--border);
  }
  .drail-line i {
    position: absolute; inset: 0 0 auto 0; height: 0;
    background: var(--accent);
    transition: height .15s linear;
  }

  .drail a {
    position: relative;
    display: flex; align-items: center;
    padding: 7px 0;
    text-decoration: none;
    z-index: 1;
  }
  /* The bead: a ringed icon that reads like a dot on the thread, and
     masks the spine behind it. */
  .drail-dot {
    flex: none;
    display: grid; place-items: center;
    width: 19px; height: 19px;
    border-radius: 50%;
    background: var(--card-bg);
    border: 1px solid var(--border);
    color: var(--text-3);
    transition: border-color .18s, background .18s, color .18s, transform .18s;
  }
  .drail-dot svg { width: 11px; height: 11px; display: block; }
  /* The section name. Hidden until the dot is active or hovered;
     an opaque pill so it stays readable where it laps the card. */
  .drail a > span {
    margin-left: 11px;
    font-family: var(--f-mono); font-size: 12px; letter-spacing: 0.12em;
    text-transform: uppercase; white-space: nowrap;
    color: var(--text-3);
    background: var(--card-bg);
    border: 1px solid var(--panel-border);
    border-radius: 999px;
    padding: 4px 11px;
    opacity: 0; transform: translateX(-6px);
    transition: opacity .18s var(--ease-out), transform .18s var(--ease-out), color .18s;
    pointer-events: none;
  }
  .drail a:hover > span,
  .drail a:focus-visible > span { opacity: 1; transform: none; color: var(--text); }
  .drail a:hover .drail-dot { border-color: var(--accent); color: var(--accent); }

  .drail a.is-active .drail-dot {
    border-color: var(--accent);
    color: var(--accent);
    background: color-mix(in srgb, var(--accent) 14%, var(--card-bg));
    transform: scale(1.12);
  }
  /* A short bar from the lit icon to its label: the near end of the
     line that runs on to the card's accented edge. */
  .drail a.is-active::after {
    content: ''; position: absolute; left: 20px;
    top: 50%; margin-top: -0.5px;
    width: 8px; height: 1px; background: var(--accent);
  }
  .drail a.is-active > span {
    opacity: 1; transform: none;
    color: var(--accent);
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
    box-shadow: 0 4px 14px rgba(0,0,0,0.10);
  }

}


/* ── Pillars: the triage grid: 2×2 cards, each a claim ─────── */
.pillars {
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: var(--s3);
}


/* ── Metrics: four stat tiles in the first band ────────────────
   The same tile every other section is cut from (shadcn's dashboard
   section-cards: label, then the figure), so the numbers belong to
   the page rather than sitting on it in their own colour. */
.metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s3); }
.metric { padding: var(--s3); }
.metric-label {
  display: block; margin-bottom: var(--s2);
  font-family: var(--f-mono); font-size: 9px;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--text-3); line-height: 1.6;
}
.metric-figure {
  display: block;
  font-family: var(--f-display);
  font-size: clamp(2rem, 3.6vw, 2.8rem);
  font-weight: var(--w-display-xl); line-height: 1;
  color: var(--text); letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
#metrics { padding-top: var(--s6); padding-bottom: var(--s6); }
@media (max-width: 900px) { .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s2); } }


/* ── Warp Text: the name as glass (after React Bits WarpText) ────
   home-fx.js draws the name into a canvas laid over the h1, grown by
   a margin so bent glyphs have room, and the h1 goes transparent
   underneath, still there for layout, readers and search. */
.hero-name-wrap { position: relative; }
.hero-name-warp {
  position: absolute; display: block; pointer-events: none;
  opacity: 0; transition: opacity 0.9s var(--ease-out);
}
.hero-name-wrap.is-warp .hero-name-warp { opacity: 1; }
.hero-name-wrap.is-warp .hero-name,
.hero-name-wrap.is-warp .hero-name em { color: transparent; }
@media print { .hero-name-warp { display: none; } .hero-name-wrap.is-warp .hero-name { color: inherit; } }

/* ── Split Text: the hero name, letter by letter ─────────────────
   home-fx.js wraps each letter; each rises out of a blur with its own
   delay. With no split (reduced motion, no JS) the h1 is simply set. */
.hero-name.is-split .st-ch {
  display: inline-block;
  animation: st-in 0.9s var(--ease-out) both;
  animation-delay: calc(0.15s + var(--i, 0) * 45ms);
}
@keyframes st-in {
  from { opacity: 0; transform: translateY(0.35em); filter: blur(10px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}
/* Blur Text: "Hi, I'm", word by word, from above */
.hero-hi.is-blur .bt-w {
  display: inline-block;
  animation: bt-in 0.7s var(--ease-out) both;
  animation-delay: calc(0.05s + var(--i, 0) * 120ms);
}
@keyframes bt-in {
  0%   { opacity: 0; transform: translateY(-0.6em); filter: blur(10px); }
  50%  { opacity: 0.6; transform: translateY(0.1em); filter: blur(4px); }
  100% { opacity: 1; transform: none; filter: blur(0); }
}
/* Rotating Text: the role eyebrow, one word at a time. All the words
   sit in one grid cell so the stage is as wide as the widest of them;
   the current one slides up in, the last one slides up out. */
.page-label.has-rt .rt {
  display: inline-grid; vertical-align: bottom;
  overflow: hidden; line-height: 1.5;
}
.page-label.has-rt .rt-w {
  grid-area: 1 / 1; white-space: nowrap;
  transform: translateY(110%); opacity: 0;
  transition: transform 0.45s var(--ease-out), opacity 0.3s;
}
.page-label.has-rt .rt-w.is-in  { transform: none; opacity: 1; }
.page-label.has-rt .rt-w.is-out { transform: translateY(-110%); opacity: 0; }

/* Magnet: a pill pulls toward a pointer that comes within reach */
.magnet {
  transform: translate3d(var(--mx, 0px), var(--my, 0px), 0) scale(var(--dock-s, 1));
  transform-origin: 50% 100%;
  transition: transform 0.5s ease-in-out, color 0.2s, border-color 0.2s, background-color 0.2s;
}
.magnet.is-near { transition: transform 0.3s ease-out, color 0.2s, border-color 0.2s, background-color 0.2s; }
/* Dock: the pills grow toward the pointer (--dock-s from home-bits.js). */
.hero-links.is-dock a { transition: transform 0.18s ease-out, color 0.2s, border-color 0.2s, background-color 0.2s; }
.hero-links.is-dock a.is-near { transition: transform 0.18s ease-out, color 0.2s, border-color 0.2s, background-color 0.2s; }

/* ── Line Sidebar, on the rail's own beads ────────────────────────
   --effect (0..1, from home-bits.js) is how near the pointer is,
   or 1 for the active section. The bead swells and takes the accent
   with it, and the label pill slides out in proportion, so the rail
   answers the pointer before it arrives. */
@media (min-width: 1200px) {
  .drail.is-near a { --effect: 0; }
  .drail.is-near .drail-dot {
    transform: scale(calc(1 + var(--effect) * 0.14));
    border-color: color-mix(in srgb, var(--accent) calc(var(--effect) * 100%), var(--border));
    color: color-mix(in srgb, var(--accent) calc(var(--effect) * 100%), var(--text-3));
  }
  .drail.is-near a > span {
    opacity: var(--effect);
    transform: translateX(calc(-6px + var(--effect) * 6px));
    transition: color .18s;
  }
  .drail.is-near a.is-active > span { opacity: 1; transform: none; }
}

/* ── Target Cursor ────────────────────────────────────────────────
   (after React Bits TargetCursor) The native pointer gives way to a
   ring in the theme's own ink: a dot on the pointer, a ring that
   trails it a touch, and over a target four corner brackets that fly
   to its corners. Painted in --text / --accent, not a blend mode, so
   it always reads on the ground it's over. Fine pointers only. */
body.has-tc, body.has-tc * { cursor: none !important; }
.tc {
  position: fixed; left: 0; top: 0; width: 0; height: 0;
  pointer-events: none; z-index: 2147483000;
  transition: opacity 0.18s;
}
.tc.is-away { opacity: 0; }
.tc-dot {
  position: absolute; left: -3px; top: -3px; width: 6px; height: 6px;
  border-radius: 50%; background: var(--text);
  box-shadow: 0 0 0 1.5px var(--bg);   /* halo: still reads over ink-coloured text */
}
.tc-ring {
  position: absolute; left: -14px; top: -14px; width: 28px; height: 28px;
  border-radius: 50%; border: 1.5px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  box-shadow: 0 0 0 1px var(--bg);     /* halo: still reads over accent-coloured text */
  transition: transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.22s, border-color 0.22s;
}
.tc.is-on .tc-ring { transform: scale(0.55); opacity: 0.6; }
.tc.is-down .tc-ring { transform: scale(0.8); }
.tc.is-on.is-down .tc-ring { transform: scale(0.45); }
.tc-c {
  position: absolute; left: 0; top: 0; width: 12px; height: 12px;
  border: 2px solid var(--accent);
  opacity: 0;
  transition: transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.18s;
  will-change: transform;
}
.tc.is-on .tc-c { opacity: 1; }
.tc-tl { border-right: none; border-bottom: none; }
.tc-tr { border-left: none; border-bottom: none; }
.tc-br { border-left: none; border-top: none; }
.tc-bl { border-right: none; border-top: none; }

/* ── Counter: rolling digits for the metrics ─────────────────────*/
.metric-figure.odo { display: inline-flex; align-items: flex-start; line-height: 1.12; }
.odo-col {
  display: inline-block; position: relative; overflow: hidden;
  width: 0.6em; height: 1.12em;
}
.odo-strip {
  position: absolute; left: 0; top: 0; width: 100%;
  display: flex; flex-direction: column;
  transform: translateY(0);
  transition: transform 1.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.odo-strip i { display: grid; place-items: center; height: 1.12em; line-height: 1; font-style: normal; overflow: hidden; }
.odo-sep { display: inline-block; height: 1.12em; line-height: 1.12; }

/* ── Text Type: the now line ─────────────────────────────────────*/
.tt-cursor { display: inline-block; margin-left: 2px; color: var(--accent); animation: tt-blink 0.9s steps(2, start) infinite; }
.tt-cursor.is-done { animation: none; opacity: 0; transition: opacity 0.6s; }
@keyframes tt-blink { to { visibility: hidden; } }

/* ── Accordion Gallery: project covers ───────────────────────────
   Panels share the cover's width; the active one takes most of it.
   Inactive panels are greyed and dimmed; the caption rides on the
   active panel. Each panel shows its image full-height, centred. */
.proj .proj-carousel.ag {
  display: flex; gap: 3px; overflow: hidden;
  aspect-ratio: 16 / 9; padding: 0;
  scroll-snap-type: none;
}
.proj .proj-carousel.ag .shot {
  position: relative; flex: 1 1 0; min-width: 0;
  transition: flex-grow 0.55s cubic-bezier(0.2, 0.8, 0.2, 1);
  cursor: pointer; overflow: hidden;
}
.proj .proj-carousel.ag .shot.is-active { flex-grow: 3.4; }
.proj .proj-carousel.ag .shot img {
  position: absolute; left: 50%; top: 0; height: 100%; width: auto; max-width: none;
  min-width: 100%; aspect-ratio: auto; object-fit: cover; object-position: left top;
  transform: translateX(-50%);
  border: none; border-radius: 0; box-shadow: none;
  filter: grayscale(1) brightness(0.72);
  transition: filter 0.55s;
}
.proj .proj-carousel.ag .shot.is-active img { filter: none; }
.proj .proj-carousel.ag .shot::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, transparent 55%, rgba(20, 24, 31, 0.6) 100%);
  opacity: 1; transition: opacity 0.4s;
}
.proj .proj-carousel.ag .shot figcaption {
  opacity: 0; transform: translateX(-10px);
  transition: opacity 0.4s 0.15s, transform 0.4s 0.15s;
}
.proj .proj-carousel.ag .shot.is-active figcaption { opacity: 1; transform: none; }
.proj .proj-carousel.ag .shot:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.proj .proj-carousel.ag-single .shot { flex: 1; }
@media (max-width: 520px) {
  .proj .proj-carousel.ag .shot:not(.is-active) { flex: 0 0 20px; }
}

/* ── Pixel Card: the cover plates ────────────────────────────────*/
.proj-plate { isolation: isolate; }
.proj-plate .pixel-canvas { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; }
.proj-plate > *:not(.pixel-canvas) { position: relative; z-index: 1; }
.proj-plate .proj-plate-lang { position: absolute; }

/* ── Dot Grid: the projects band ─────────────────────────────────*/
#projects > .dot-grid { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }

/* ── Magnet Lines: behind the closing plane ──────────────────────*/
.contact { position: relative; }
.magnet-lines {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  display: grid;
  grid-template-columns: repeat(var(--columns, 22), 1fr);
  grid-template-rows: repeat(var(--rows, 8), 1fr);
  justify-items: center; align-items: center;
  opacity: 0.5;
  -webkit-mask-image: radial-gradient(ellipse at center, #000 30%, transparent 78%);
  mask-image: radial-gradient(ellipse at center, #000 30%, transparent 78%);
}
.magnet-lines span {
  display: block; width: 1px; height: 18px; border-radius: 1px;
  background: var(--plane-ink-2);
  transform-origin: center; will-change: transform;
  transform: rotate(var(--rotate, 0deg));
  transition: transform 0.12s linear;
}
.contact-inner { position: relative; z-index: 1; }
@media (hover: none) { .magnet-lines { display: none; } }

/* ── Specular Button ──────────────────────────────────────────────
   A radial highlight at the pointer and a rim that catches it. */
.specular { position: relative; overflow: hidden; isolation: isolate; --sx: 50%; --sy: 0%; }
.specular::before {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: -1;
  background: radial-gradient(120px circle at var(--sx) var(--sy), rgba(255,255,255,0.28), transparent 60%);
  opacity: 0; transition: opacity 0.25s;
}
.specular::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: -1;
  border-radius: inherit; padding: 1px;
  background: radial-gradient(90px circle at var(--sx) var(--sy), rgba(255,255,255,0.9), transparent 70%);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor; mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite: exclude;
  opacity: 0; transition: opacity 0.25s;
}
.specular:hover::before, .specular:hover::after { opacity: 1; }
.specular > span { position: relative; z-index: 1; }

/* ── True Focus: the off-hours words ─────────────────────────────*/
.interests.is-focus { position: relative; }
.focus-word { display: inline-block; transition: filter 0.3s ease, color 0.3s ease; filter: blur(1.5px); color: var(--text-3); }
.focus-word.is-active { filter: blur(0); color: var(--text); }
.focus-frame {
  position: absolute; left: 0; top: 0; pointer-events: none;
  transition: transform 0.45s cubic-bezier(0.2, 0.8, 0.2, 1), width 0.45s cubic-bezier(0.2, 0.8, 0.2, 1), height 0.45s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.focus-frame .fc {
  position: absolute; width: 9px; height: 9px;
  border: 2px solid var(--accent); border-radius: 2px;
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--accent) 60%, transparent));
}
.fc-tl { top: -7px; left: -8px; border-right: none; border-bottom: none; }
.fc-tr { top: -7px; right: -8px; border-left: none; border-bottom: none; }
.fc-bl { bottom: -7px; left: -8px; border-right: none; border-top: none; }
.fc-br { bottom: -7px; right: -8px; border-left: none; border-top: none; }

/* ── Scroll Velocity: the loops are moved by script ──────────────*/
.ribbon.is-jsloop .ribbon-track { animation: none; will-change: transform; }

/* ── Tiles: the one card surface the home's components share ───
   Pillars, project posters, timeline cards, credential panels: each section
   has its own shape, but they are cut from the same material so
   the page still reads as one thing. */
.tile {
  background: var(--card-bg);
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.05), inset 0 1px 0 var(--glass-hi);
  min-width: 0;
}
/* Spotlight: a radial light that follows the pointer across the
   tile (after React Bits SpotlightCard). --mouse-x/--mouse-y are set
   by the home script; the light is drawn on ::before so the tile's
   content sits above it. */
.spot {
  position: relative; overflow: hidden;
  --mouse-x: 50%; --mouse-y: 50%;
}
.spot::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(320px circle at var(--mouse-x) var(--mouse-y),
              color-mix(in srgb, var(--accent) 16%, transparent), transparent 70%);
  opacity: 0; transition: opacity 0.5s var(--ease-out);
}
.spot:hover::before, .spot:focus-within::before { opacity: 1; }
.spot > * { position: relative; }

/* Glow: the border lights up around the pointer (after React Bits
   MagicBento's border glow). The ring is a radial gradient on ::after,
   masked to the border by two stacked masks composited with xor, so
   only the frame is painted. --glow-* are set by the home script. */
.glow {
  position: relative;
  --glow-x: 50%; --glow-y: 50%; --glow-intensity: 0; --glow-radius: 220px;
}
.glow::after {
  content: ''; position: absolute; inset: 0; padding: 2px;
  border-radius: inherit; pointer-events: none; z-index: 2;
  background: radial-gradient(var(--glow-radius) circle at var(--glow-x) var(--glow-y),
    color-mix(in srgb, var(--accent) calc(var(--glow-intensity) * 90%), transparent) 0%,
    color-mix(in srgb, var(--accent) calc(var(--glow-intensity) * 40%), transparent) 35%,
    transparent 65%);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite: exclude;
  transition: opacity 0.3s var(--ease-out);
}
.glow:hover {
  box-shadow: 0 8px 28px rgba(0,0,0,0.14),
              0 0 32px color-mix(in srgb, var(--accent) 18%, transparent);
}

/* The icon plate a tile can carry: a tinted square, accent glyph. */
.tile-ic {
  display: grid; place-items: center; flex-shrink: 0;
  width: 38px; height: 38px; border-radius: 10px;
  background: color-mix(in srgb, var(--accent) 13%, transparent);
  color: var(--accent);
}
.tile-ic svg { width: 18px; height: 18px; }
/* Glass Icons (React Bits): a tinted back plate turned a few degrees
   behind a frosted front; on hover the back turns further and the
   front lifts. The plain plate above is what no-hover devices keep. */
.glass-ic {
  background: none; perspective: 24em; transform-style: preserve-3d;
  position: relative;
}
.glass-ic .gi-back, .glass-ic .gi-front {
  position: absolute; inset: 0; border-radius: 10px;
  transition: transform 0.3s cubic-bezier(0.83, 0, 0.17, 1);
}
.glass-ic .gi-back {
  background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 45%, var(--bg)));
  transform: rotate(12deg); transform-origin: 100% 100%;
  box-shadow: 0.35em -0.35em 0.6em rgba(0,0,0,0.12);
}
.glass-ic .gi-front {
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--card-bg) 55%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, #fff 28%, transparent) inset;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  transform-origin: 80% 50%; color: var(--text);
}
.glass-ic .gi-front svg { width: 17px; height: 17px; }
@media (hover: hover) {
  .tile:hover .glass-ic .gi-back  { transform: rotate(22deg) translate3d(-0.3em, -0.3em, 0.3em); }
  .tile:hover .glass-ic .gi-front { transform: translate3d(0, 0, 1.6em); }
}
@media (pointer: coarse) {
  .glass-ic .gi-front { backdrop-filter: none; -webkit-backdrop-filter: none; background: color-mix(in srgb, var(--card-bg) 80%, transparent); }
}

/* ── Experience: a timeline ─────────────────────────────────────
   Three tracks: the period in a gutter, a spine with a dot per
   job, and the job as a tile. The current job's dot is lit. */
.tl { list-style: none; margin: 0; padding: 0; }
.tl-item {
  display: grid;
  grid-template-columns: 160px 28px minmax(0, 1fr);
  column-gap: var(--s2);
}
.tl-item + .tl-item { margin-top: var(--s3); }
.tl-when {
  padding-top: 26px; text-align: right; line-height: 1.5; white-space: nowrap;
  font-family: var(--f-mono); font-size: 10px;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-3);
}
.tl-spine { position: relative; }
.tl-spine::before {
  content: ''; position: absolute; left: 50%;
  top: calc(var(--s3) * -1); bottom: calc(var(--s3) * -1);
  width: 1px; background: var(--border);
  transform: translateX(-50%);
}
.tl-item:first-child .tl-spine::before { top: 30px; }
.tl-item:last-child  .tl-spine::before { bottom: calc(100% - 30px); }
.tl-dot {
  position: absolute; left: 50%; top: 30px;
  width: 11px; height: 11px; border-radius: 50%;
  transform: translate(-50%, -50%);
  background: var(--bg); border: 2px solid var(--accent);
}
.tl-item.is-current .tl-dot {
  background: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 22%, transparent);
}
.tl-card { padding: var(--s3); }
/* Electric Border (React Bits) on the current job: a canvas past the
   card's edge traces its outline with drifting noise; two blurred
   rings and a wash behind it are the glow. */
.tl-card.electric { position: relative; overflow: visible; isolation: isolate; }
.tl-card.electric > .eb-layers { position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: 0; }
.tl-card.electric > *:not(.eb-layers) { position: relative; z-index: 1; }
.eb-canvas { position: absolute; left: -40px; top: -40px; display: block; z-index: 2; }
.eb-glow-1, .eb-glow-2, .eb-bg-glow { position: absolute; inset: 0; border-radius: inherit; pointer-events: none; }
.eb-glow-1 { border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); filter: blur(1px); }
.eb-glow-2 { border: 2px solid var(--accent); filter: blur(4px); opacity: 0.25; }
.eb-bg-glow { z-index: -1; transform: scale(1.03); filter: blur(28px); opacity: 0.08;
  background: linear-gradient(-30deg, var(--accent), transparent, var(--accent)); }
@media (prefers-reduced-motion: reduce) { .eb-canvas, .eb-glow-2 { display: none; } }
.tl-head { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.tl-head .co-logo { height: 46px; min-width: 46px; max-width: 132px; padding: 0 10px; }
.tl-head .co-logo img { max-height: 30px; max-width: 108px; }
.tl-title { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.tl-co { margin: 0; font-family: var(--f-display); font-size: 1.3rem; font-weight: var(--w-display); color: var(--text); line-height: 1.15; }
.tl-role { font-size: 13px; color: var(--text-2); }
.tl-head .tl-when-inline { display: none; }
.tl-card > .project-body { margin-top: var(--s2); font-size: 14px; }
.tl-card > .chips { margin-top: var(--s3); }
/* Ventures under one employer, as a strip of small tiles: three
   products read as three things before the fold is opened. Once it
   is, the full venture blocks repeat the names, so the strip steps
   aside. */
.venture-strip {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--s2); margin-top: var(--s3);
}
.venture-tile {
  padding: 10px 12px; border-radius: 9px;
  border: 1px solid var(--border); border-left: 2px solid var(--accent);
  background: color-mix(in srgb, var(--bg-raised) 55%, transparent);
}
.venture-tile b {
  display: block; margin-bottom: 3px;
  font-family: var(--f-mono); font-size: 10px; font-weight: 400;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent);
}
.venture-tile span { font-size: 12.5px; line-height: 1.45; color: var(--text-2); }
.tl-card:has(.xp-more[open]) .venture-strip { display: none; }
.xp-more { margin-top: var(--s3); }
.xp-more > .points { margin-top: var(--s2); }
.xp-more[open] > summary { margin-bottom: var(--s1); }
@media (max-width: 720px) {
  .tl-item { grid-template-columns: 20px minmax(0, 1fr); column-gap: var(--s1); }
  .tl-when { display: none; }
  .tl-head .tl-when-inline { display: inline; margin-left: auto; }
  .tl-item:first-child .tl-spine::before { top: 26px; }
  .tl-item:last-child  .tl-spine::before { bottom: calc(100% - 26px); }
  .tl-dot { top: 26px; }
}


/* ── Bullets (shared by jobs + projects) ─────────────────────── */
.points { list-style: none; margin: var(--s2) 0 0; }
.points li {
  position: relative; padding-left: var(--s3);
  margin-bottom: 0.65rem;
  font-size: 14.5px; line-height: 1.7; color: var(--text-2);
}
.points li:last-child { margin-bottom: 0; }
.points li::before {
  content: ''; position: absolute; left: 0; top: 0.75em;
  width: 8px; height: 1px; background: var(--accent); opacity: 0.75;
}
.points strong { color: var(--text); font-weight: 700; }
.points em { color: var(--text-3); }
.points code {
  font-family: var(--f-mono); font-size: 0.85em;
  background: var(--code-bg); color: var(--code-fg);
  padding: 1px 5px; border-radius: 2px;
}


/* ── Chips (stack / skills) ──────────────────────────────────── */
.chips {
  list-style: none; display: flex; flex-wrap: wrap; gap: 5px;
  margin: var(--s2) 0 0;
}
.chips li {
  font-family: var(--f-mono); font-size: 8.5px;
  letter-spacing: 0.1em; text-transform: uppercase;
  padding: 3px 8px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  color: var(--text-3); border-radius: 1px;
  transition: border-color 0.15s, color 0.15s;
}
.chips li:hover { border-color: var(--accent); color: var(--text); }


/* ── Projects: laid out by the carousel above; this is the card
   itself. Sizing lives with .carousel-track > .project. ─────── */
.project:hover {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  transform: translateY(-2px);
  box-shadow:
    0 6px 20px rgba(0,0,0,0.07),
    inset 0 1px 0 var(--glass-hi);
}
.project-body { margin-top: var(--s2); font-size: 14.5px; color: var(--text-2); }
.project-body strong { color: var(--text); }
.project-foot .chips { margin: 0; }
.project-links { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s3); }
.project-links a, .section-more a, .section-more button {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--f-mono); font-size: 9.5px;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--link); text-decoration: none;
  transition: color 0.15s;
}
.project-links a:hover, .section-more a:hover, .section-more button:hover { color: var(--accent); }

/* The arrow nudge belongs to "go somewhere else" links. Project links now
   carry a type icon instead, which must not slide on hover. */
.section-more a span, .section-more button span { transition: transform 0.2s var(--ease-out); }
.section-more a:hover span, .section-more button:hover span { transform: translateX(3px); }

/* Inline link-type icon: github / external / pen */
.link-ic { display: inline-flex; flex-shrink: 0; }
.link-ic svg { width: 12px; height: 12px; display: block; }
.writing-card .post-row:first-child { border-top: none; }
.section-more { display: flex; gap: var(--s5); margin-top: var(--s4); }
.resource-list { list-style: none; }
.resource-list li { padding: var(--s1) 0; border-bottom: 1px solid var(--border); }
.resource-list li:last-child { border-bottom: none; }
.resource-list a {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
  font-size: 13px; color: var(--text-2); text-decoration: none;
  transition: color 0.15s;
}
.resource-list a span { transition: transform 0.2s var(--ease-out); }
.resource-list a:hover { color: var(--accent); }
.resource-list a:hover span { transform: translateX(3px); }
.bg-title + .bg-item { margin-top: 0; }

/* Verify links for certs that bundle several credentials */
.cert-links { display: flex; flex-wrap: wrap; gap: 6px; margin-top: var(--s1); }
.cert-links a {
  display: inline-flex; align-items: center; gap: 5px;
  font-family: var(--f-mono); font-size: 8.5px;
  letter-spacing: 0.1em; text-transform: uppercase;
  padding: 3px 9px;
  border: 1px solid var(--border); border-radius: 999px;
  color: var(--text-3); text-decoration: none;
  transition: border-color 0.15s, color 0.15s;
}
.cert-links a span { opacity: 0.6; }
.cert-links a:hover { border-color: var(--accent); color: var(--accent); }
.cert-links a:hover span { opacity: 1; }

.interests {
  margin-top: var(--s6); padding-top: var(--s3);
  border-top: 1px solid var(--border);
  font-size: 13.5px; color: var(--text-2);
}
.interests-label {
  font-family: var(--f-mono); font-size: 8.5px;
  letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--accent); margin-right: var(--s2);
}


/* ── Progressive disclosure ───────────────────────────────────
   The page carried ~3,600px of prose and bullets. None of it is
   deleted: the default view shows the top of each block and the
   rest is one click away, so the page can be skimmed in a screen
   or two and still read in full. */
.prose-clamp {
  max-height: 17rem;
  overflow: hidden;
  position: relative;
  transition: max-height 0.45s var(--ease-out);
}
.prose-clamp.open { max-height: 200rem; }
.prose-clamp.open::after { opacity: 0; }

/* <details> disclosure for bullet overflow, no JS needed */
details.more { margin-top: var(--s2); }
details.more > summary,
.venture > summary {
  cursor: pointer; list-style: none;
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--f-mono); font-size: 9px;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--text-3);
  transition: color 0.15s;
}
details.more > summary::-webkit-details-marker,
.venture > summary::-webkit-details-marker { display: none; }
details.more > summary::before,
.venture > summary::before {
  content: ''; flex-shrink: 0;
  width: 6px; height: 6px;
  border-right: 1.4px solid currentColor;
  border-bottom: 1.4px solid currentColor;
  transform: rotate(45deg) translate(-1px,-1px);
  transition: transform 0.2s var(--ease-out);
}
details.more[open] > summary::before,
.venture[open] > summary::before {
  transform: rotate(-135deg) translate(-2px,-2px);
}
details.more > summary:hover,
.venture > summary:hover { color: var(--accent); }
.band-head > .group-heading { margin: 0; flex: 1; }

.carousel-track > .project {
  flex: 0 0 clamp(280px, 42%, 460px);
  scroll-snap-align: start;
  /* Equal height regardless of content; the footer is pinned to the
     bottom so the stack rows line up across cards. */
  min-height: 400px;
  display: flex; flex-direction: column;
  padding: var(--s4);
}
.carousel-track > .project .project-foot { margin-top: auto; }
.car-dot.active { background: var(--accent); transform: scale(1.5); }


/* ── Toolkit ribbons ──────────────────────────────────────────
   Each row scrolls its own items. See the .ribbon block further
   down for the live rules. */


/* ── Contact plane: mirrors the hero ────────────────────────── */
.contact {
  background: transparent;
  padding: var(--s10) var(--s5) var(--s12);
  position: relative; overflow: hidden;
}
/* Readability wash is on the fixed scene now (.px::after). */
.contact::after {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px;
  background: linear-gradient(to right,
    transparent, var(--lacquer) 15%, var(--lacquer) 85%, transparent);
  opacity: 0.35;
}
.contact-inner {
  max-width: var(--max-w); margin: 0 auto;
  position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: center;
  text-align: center;
}
.contact-email {
  font-family: var(--f-display);
  font-size: clamp(1.6rem, 4.6vw, 3.1rem);
  font-weight: var(--w-display); letter-spacing: -0.02em;
  color: var(--plane-ink); text-decoration: none;
  background-image: linear-gradient(var(--accent), var(--accent));
  background-size: 0% 1px; background-repeat: no-repeat;
  background-position: 0 100%;
  transition: background-size 0.4s var(--ease-out), color 0.2s;
}
.contact-email:hover { color: var(--accent); background-size: 100% 1px; }
.contact .hero-links { justify-content: center; margin-top: var(--s5); }


/* ── Home responsive ─────────────────────────────────────────── */

@media (max-width: 1000px) {
  .ribbon { grid-template-columns: 150px 1fr; gap: var(--s2); }
}

@media (max-width: 860px) {
  /* One column, portrait first: the same disc as the desktop, small
     enough above the eyebrow to pair with the name rather than
     overpower it. */
  .hero-inner {
    grid-template-columns: 1fr;
    gap: var(--s4);
    justify-items: start;
  }
  /* Capped at the column so a name wider than the screen wraps rather
     than dragging the greeting and links past the right gutter. */
  .hero-text { width: 100%; }
  .hero-portrait {
    order: -1;
    width: clamp(150px, 42vw, 190px);
    margin-top: var(--s1);
  }
  .hero-portrait.no-img .portrait-monogram { font-size: 2.4rem; }
}

@media (max-width: 700px) {
  .band { padding: var(--s8) 0; }
  .band-inner { padding: 0 var(--s3); }
  .metrics { grid-template-columns: 1fr 1fr; gap: var(--s2); }
  /* Label on top, figure on the floor: tiles in a row share a
     height, so the figures line up however the labels wrap. */
  .metric { display: flex; flex-direction: column; justify-content: space-between;
            gap: var(--s2); padding: var(--s2) var(--s2) var(--s3); }
  .metric-label { margin-bottom: 0; }
  .carousel-track > .project { flex-basis: 84%; min-height: 0; }

  /* The timeline folds to a single column: the period becomes a
     header line above its card, and the spine/node come off since
     there is no gutter left for them to sit in. */
  .timeline::before { display: none; }
}

@media (max-width: 640px) {
  .hero { padding: var(--s5) var(--s3) var(--s6); }
  .hero-name { font-size: clamp(2.4rem, 11.5vw, 4rem); }   /* Bricolage 700 runs ~7.2em for the full name: fits one line from ~330px */
  .hero-links { margin-top: var(--s4); }
  .contact { padding: var(--s8) var(--s5) var(--s10); }
  .section-more { flex-direction: column; gap: var(--s2); }
  .card { padding: var(--s3); border-radius: 10px; }
  .job-card, .project { padding: var(--s3); }
}

@media (prefers-reduced-motion: reduce) {
  .status-dot { animation: none; }
  .anim { opacity: 1; transform: none; transition: none; }
  .hero-name.is-split .st-ch, .hero-hi.is-blur .bt-w { animation: none; }
  .ds-lede.is-split .w { opacity: 1; filter: none; }
  .contact-email.shiny { animation: none; }
  /* The ring still tracks progress; it just stops easing there */
  .scroll-top { transition: opacity 0.2s linear, visibility 0s linear; transform: none; }
  .scroll-top.visible { transform: none; }
  .ring-bar { transition: none; }
}

@media print {
  .anim { opacity: 1 !important; transform: none !important; }
  .hero, .contact { padding: 0 0 var(--s3); }
  .hero::before, .hero::after,
  .contact::before, .contact::after { display: none; }
  .home { padding: 0; }
  .band { padding: var(--s4) 0; }
  .band-inner { padding: 0; max-width: none; }
  /* Cards are a screen affordance; on paper they become plain blocks */
  .card, .home-prose.sheet, .tile {
    background: none !important; box-shadow: none !important;
    border: none !important; backdrop-filter: none !important;
    padding: 0;
  }
  .card { border-bottom: 1px solid #ddd !important; padding-bottom: var(--s2); }
  .metric-figure, .metric-label { color: #000 !important; }
  .drail, .hero-portrait, .hero-links { display: none; }
  .hero-inner .hero-rise { opacity: 1 !important; transform: none !important; animation: none !important; }
}


/* ════════════════════════════════════════════════════════════════
   RESPONSIVE
════════════════════════════════════════════════════════════════ */

/* Wide desktops: let the measure and the sidebar grow rather than
   leaving the field empty. --max-w recomputes from these. */
@media (min-width: 1180px) {
  :root { --article-w: 840px; --sidebar-w: 240px; }
}
@media (min-width: 1440px) {
  :root { --article-w: 900px; --sidebar-w: 260px; --max-w: 1400px; }
}
/* Very wide displays. --max-w still caps the home and listing pages;
   the post page is uncapped and runs to the --page-pad frame. */
@media (min-width: 1760px) {
  :root { --sidebar-w: 280px; --max-w: 1520px; }
}

/* Three columns: parts rail | article | TOC. The parts rail should be up
   on any laptop that shows the TOC rail: a series card that is a strip
   above the article on one screen and a rail on the next reads as a bug,
   not a layout choice. The old 1360px gate missed most laptops (1920px
   at 125–150% Windows scaling is 1280–1536 CSS px). The rail starts
   narrow and widens with the viewport so the measure pays as little as
   possible: ~530px of article at 1024px, ~750px at 1280px. Below 1024px
   a third column can't be paid for at all (the article would drop under
   400px), so the strip stays. */
/* An open TOC rail docks to the right edge (see "Open: docked to the
   same edge"), so the right gutter goes to the article. A collapsed
   one keeps the gutter: its pill reaches the edge through it. */
@media (min-width: 861px) {
  .post-wrap:has(> .post-sidebar:not(.collapsed)) { padding-right: 0; }
}
/* Below three columns the parts list is a strip spanning the whole row;
   it keeps the gutter the TOC gave up so it stays centred on the page. */
@media (min-width: 861px) and (max-width: 1023px) {
  .post-wrap:has(> .post-sidebar:not(.collapsed)) > .post-rail:not(.collapsed) {
    margin-right: var(--page-pad);
  }
}

/* ── The collapse handle ─────────────────────────────────────────
   An open side rail folds by a round handle on its inner edge, the
   edge that moves, straddling the border like a drawer pull, with the
   chevron pointing at the side it folds into. Out of the head, so the
   heading gets the card's full width. The handle overhangs into the
   column gap, so the rail and card stop clipping: their lists are the
   scroll containers and every flex child above them has min-height:0,
   so nothing else needs the clip. Strip mode and phones keep the
   in-head toggle. The parts rail's half is in the 1024px block. */
@media (min-width: 861px) {
  .post-sidebar:not(.collapsed),
  .post-sidebar:not(.collapsed) #toc-card { overflow: visible; }
  .post-sidebar:not(.collapsed) .sidebar-card { position: relative; }
  .post-sidebar:not(.collapsed) .rail-toggle {
    position: absolute; top: 9px; z-index: 2;
    width: 24px; height: 24px; margin: 0;
    border-radius: 50%;
    background: var(--bg-raised);
    border-color: var(--glass-border);
    box-shadow: 0 2px 8px rgba(0,0,0,0.18);
  }
  .post-sidebar:not(.collapsed) .rail-toggle:hover,
  .post-sidebar:not(.collapsed) .rail-toggle:focus-visible {
    color: var(--accent); border-color: var(--accent);
    background: var(--bg-raised);
  }
  .post-sidebar:not(.collapsed) .rail-toggle svg { width: 12px; height: 12px; }
  .post-sidebar:not(.collapsed) .rail-toggle { left: -12px; }
}

@media (min-width: 1024px) {
  :root { --rail-w: 180px; --rail-gap: var(--col-gap); }

  .post-wrap.has-rail {
    grid-template-columns: var(--rail-w) 1fr var(--sidebar-w);
    column-gap: var(--rail-gap);
  }
  /* One track per rail still in flow. Placement is left to source order
     (rail, article, sidebar) so these stay declarative. */
  .post-wrap.has-rail:has(> .post-rail.collapsed) {
    grid-template-columns: 0px 1fr var(--sidebar-w);
  }
  .post-wrap.has-rail:has(> .post-sidebar.collapsed) {
    grid-template-columns: var(--rail-w) 1fr 0px;
  }
  .post-wrap.has-rail:has(> .post-rail.collapsed):has(> .post-sidebar.collapsed) {
    grid-template-columns: 0px 1fr 0px;
  }

  /* Three columns: the parts pill gets its own sticky track on the left
     instead of riding above the article as it does in strip mode. */
  .post-wrap.has-rail > .post-rail.collapsed {
    grid-column: 1; grid-row: 1;
    position: sticky; top: calc(var(--h-header) + var(--s2));
    margin: 0 0 0 calc(-1 * var(--page-pad));
  }
  /* :not(.collapsed) again: this selector outranks the collapsed block,
     so without the guard the rail stayed a sticky grid item while the
     grid had already dropped to two tracks, which pushed the article
     into the sidebar's 220px column. */
  .post-wrap.has-rail > .post-rail:not(.collapsed) {
    grid-column: 1; grid-row: 1;
    margin: 0;                      /* identical box to .post-sidebar */
    position: sticky; top: calc(var(--h-header) + var(--s1));
    max-height: calc(100vh - var(--h-header) - var(--s1) * 2);
    display: flex; flex-direction: column;
    min-height: 0;
    overflow: hidden;               /* same guard as the right rail */
  }
  /* One narrow column again; the list itself does the scrolling.
     Scoped to :not(.collapsed): this selector is more specific than the
     collapsed rules, so without the guard it would switch the list back
     on inside a collapsed pill. */
  .post-wrap.has-rail > .post-rail:not(.collapsed) .series-list {
    display: block;
    overflow-y: auto;
  }
  /* Docked to the left edge like its tab, mirroring the TOC rail: the
     left gutter goes to the article. */
  .post-wrap.has-rail:has(> .post-rail:not(.collapsed)) { padding-left: 0; }
  .post-wrap.has-rail > .post-rail:not(.collapsed) .sidebar-card {
    border-radius: 0 9px 9px 0;
    border-left-color: transparent;
  }
  /* The collapse handle, on the rail's right (inner) edge; the TOC's
     half and the reasoning are under "The collapse handle". */
  .post-wrap.has-rail > .post-rail:not(.collapsed),
  .post-wrap.has-rail > .post-rail:not(.collapsed) #series-card { overflow: visible; }
  .post-wrap.has-rail > .post-rail:not(.collapsed) .sidebar-card { position: relative; }
  .post-wrap.has-rail > .post-rail:not(.collapsed) .rail-toggle {
    position: absolute; top: 9px; right: -12px; z-index: 2;
    width: 24px; height: 24px; margin: 0;
    border-radius: 50%;
    background: var(--bg-raised);
    border-color: var(--glass-border);
    box-shadow: 0 2px 8px rgba(0,0,0,0.18);
  }
  .post-wrap.has-rail > .post-rail:not(.collapsed) .rail-toggle:hover,
  .post-wrap.has-rail > .post-rail:not(.collapsed) .rail-toggle:focus-visible {
    color: var(--accent); border-color: var(--accent);
    background: var(--bg-raised);
  }
  .post-wrap.has-rail > .post-rail:not(.collapsed) .rail-toggle svg { width: 12px; height: 12px; }
}
@media (min-width: 1360px) {
  :root { --rail-w: 210px; }
}
@media (min-width: 1440px) {
  :root { --rail-w: 240px; }
}

@media (max-width: 860px) {
  /* Each of these has to restate the collapsed selector to match its
     specificity. `.post-sidebar.collapsed { display: block }` and the
     :has() track rules both outrank a bare `.post-sidebar` / `.post-wrap`,
     so a rail collapsed on a desktop (the choice is remembered) came
     back as a floating tab here, in a layout that has no rails at all. */
  .post-wrap,
  .post-wrap:has(> .post-sidebar.collapsed) { grid-template-columns: 1fr; }
  .post-sidebar,
  .post-sidebar.collapsed { display: none; }

  .post-hero { padding: var(--s6) var(--s5) var(--s5); }
  /* The parts list, on a phone. Laid open it stood a whole screen of
     links between the title and the first paragraph, so here it starts
     folded (default.html) into one full-width bar (series name, part
     number, a chevron) and opens in place. Open, it lies flat: a
     nested scroller under a thumb is a trap (overscroll-behavior:
     contain, right for a sticky rail, swallowed the page swipe). The
     docked side-tab the desktop uses makes no sense without a rail. */
  .post-rail:not(.collapsed) #series-card { max-height: none; overflow: visible; }
  .post-rail:not(.collapsed) .series-list { display: block; overflow: visible; }

  .post-rail.collapsed {
    width: auto;
    justify-self: stretch;
    margin: 0 0 var(--s3);
  }
  .post-rail.collapsed .sidebar-card {
    width: auto;
    padding: 10px 12px 10px 14px;
    border-radius: 10px;
    border-left-color: var(--border);
    box-shadow: none;
  }
  .post-rail.collapsed .sidebar-card:hover { border-color: var(--accent); }
  .post-rail.collapsed .sidebar-head {
    flex-direction: row;
    align-items: center;
    gap: var(--s1);
  }
  .post-rail.collapsed .sidebar-icon { display: block; }
  .post-rail.collapsed .sidebar-label {
    writing-mode: horizontal-tb;
    transform: none;
    white-space: normal;
    max-height: none;
    letter-spacing: 0.1em;
    line-height: 1.45;
  }
  .post-rail.collapsed .rail-toggle { order: 0; width: 28px; height: 28px; }
  /* One chevron for both states: down to open, up to fold */
  .post-rail .ic-expand,
  .post-rail.collapsed .ic-expand { display: none; }
  .post-rail .ic-collapse,
  .post-rail.collapsed .ic-collapse { display: block; }
  .post-rail .ic-collapse svg { transform: rotate(90deg); transition: transform 0.2s var(--ease-out); }
  .post-rail.collapsed .ic-collapse svg { transform: rotate(-90deg); }
  .post-nav { grid-template-columns: 1fr; }
  .post-content h2::before { display: none; }

  /* Two across, not three: below this the columns stop being readable */
  .post-more-item:nth-child(n+3) { display: none; }
  .post-more-item:nth-child(2) { border-right: none; padding-right: 0; }

  /* Navigation docks to the bottom of the screen: every destination
     always visible, within thumb reach. That is one tap instead of two
     (open the menu, then choose), and it puts the controls where a hand
     holding a phone already is. The hamburger has nothing left to open,
     so it goes; the theme switch stays in the header, where it is one
     tap and not navigation.

     Modelled on the iOS 26 tab bar, which does two things ours now
     does too:
       · the capsule hugs its tabs and sits centred; with two or three
         destinations a bar stretched edge to edge read as empty space
       · the selected tab is a coloured glyph AND a glass "lens" behind
         it; the lens slides between tabs and follows a scrubbing finger
         (the JS moves it; the ::after pill below is the no-JS fallback)
     Two more were tried and cut: minimising the bar to a circle on
     scroll (the bar should just stay a pill) and a blurred scroll-edge
     under it (a smear over the text, and one more per-frame
     backdrop-filter on a phone). */
  .nav-toggle { display: none; }

  :root {
    --tab-h:   54px;   /* the capsule                              */
    --tab-pad: 4px;    /* lens inset from the capsule edge         */
    --tab-w:   78px;   /* one tab; the JS reads this to place lens */
    /* One spring for the lens and the tabs, a touch of overshoot, the
       way iOS's interactive springs settle. */
    --spring: cubic-bezier(0.34, 1.28, 0.64, 1);
  }

  .site-nav {
    position: fixed;
    /* Centred and only as wide as its tabs: a capsule, not a strip.
       The bottom offset stacks the home-indicator inset on its gap. */
    left: 50%; right: auto; top: auto;
    bottom: calc(var(--s2) + env(safe-area-inset-bottom, 0px));
    transform: translateX(-50%);
    height: var(--tab-h);
    max-width: calc(100% - var(--s3) * 2);
    padding: var(--tab-pad);
    z-index: 95;
    flex-direction: row; align-items: stretch;
    gap: 0;
    /* No material on the nav itself; the glass is .nav-shell below,
       a leaf of its own; the lens and tabs move by transform and
       opacity, which stay on the compositor. */
    background: none; border: 0; box-shadow: none;
    /* The bar is not a scroll surface: a finger sliding along it is
       scrubbing the lens, and the browser must not claim it for a pan. */
    touch-action: none;
    /* Always on: there is no open/closed state left to animate */
    opacity: 1; visibility: visible;
    transition: none;
  }

  /* The glass. Same material as the header; the two floating bars are
     one navigation plane, just in two places. */
  .nav-shell {
    position: absolute; inset: 0;
    background: var(--header-glass);
    backdrop-filter: blur(var(--glass-blur-nav)) var(--glass-fx);
    -webkit-backdrop-filter: blur(var(--glass-blur-nav)) var(--glass-fx);
    border: 1px solid var(--glass-border);
    border-radius: var(--float-radius);
    overflow: hidden;
    /* The glass tell: a specular edge along the top inside, a faint
       shade along the bottom, a soft cast outside. */
    box-shadow:
      inset 0 1px 0 var(--glass-hi),
      inset 0 -1px 0 rgba(46,52,64,0.06),
      0 8px 28px rgba(46,52,64,0.22),
      0 2px 8px rgba(46,52,64,0.14);
  }
  /* A sheen down from the top edge, the light catching the glass. */
  .nav-shell::before {
    content: '';
    position: absolute; inset: 0;
    border-radius: inherit;
    background: linear-gradient(180deg, var(--glass-hi), transparent 48%);
    opacity: 0.45;
    pointer-events: none;
  }
  @media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
    .nav-shell {
      background: var(--header-glass-solid);
      backdrop-filter: none; -webkit-backdrop-filter: none;
    }
    .nav-shell::before { display: none; }
  }

  /* Icon over label, each tab a fixed width, so the lens can be placed
     by index. Tabs never change size; folding away is a transform. */
  .site-nav a {
    position: relative; z-index: 1;       /* above the lens and shell */
    flex: 0 0 auto; width: var(--tab-w); min-width: 0;
    height: auto; padding: 0;
    flex-direction: column; justify-content: center; align-items: center;
    gap: 3px;
    font-size: 9px; letter-spacing: 0.1em;
    -webkit-tap-highlight-color: transparent;
    transition: transform 0.45s var(--spring), opacity 0.2s, color 0.2s;
  }
  .site-nav a .nav-ic {
    opacity: 0.85;
    transition: transform 0.3s var(--spring), opacity 0.2s;
  }
  .site-nav a .nav-ic svg { width: 21px; height: 21px; }
  .site-nav a.active .nav-ic { opacity: 1; }
  /* Press: the glyph gives a little under the finger */
  .site-nav a:active .nav-ic { transform: scale(0.86); }
  .site-nav a .nav-lb {
    transition: opacity 0.15s, transform 0.3s var(--ease-out);
  }

  /* No-JS fallback: the header's selection pill, filling the tab. The
     JS replaces it with the sliding lens and hides it. */
  .site-nav a::after { inset: 0; }
  .site-nav.has-lens a::after { display: none; }
  /* Without the script there is no shell either; the nav carries the
     material itself so the bar is never a set of bare links. */
  .site-nav:not(.has-lens) {
    background: var(--header-glass);
    border: 1px solid var(--glass-border);
    border-radius: var(--float-radius);
  }

  /* The lens. One element, positioned by --lens-x/--lens-w from the
     script; transform-only movement so the slide stays on the
     compositor. --lens-s swells it slightly while a finger is down. */
  .nav-lens {
    display: block;
    position: absolute;
    top: var(--tab-pad); bottom: var(--tab-pad); left: var(--tab-pad);
    width: var(--lens-w, var(--tab-w));
    transform: translateX(var(--lens-x, 0px)) scale(var(--lens-s, 1));
    transform-origin: center;
    border-radius: 999px;
    background:
      linear-gradient(180deg, var(--glass-hi), transparent 58%),
      color-mix(in srgb, var(--header-text) 11%, transparent);
    box-shadow:
      inset 0 0 0 1px color-mix(in srgb, var(--header-text) 8%, transparent),
      0 1px 3px rgba(46,52,64,0.10);
    opacity: 1;
    transition:
      transform 0.45s var(--spring),
      width 0.45s var(--spring),
      opacity 0.25s 0.15s;     /* fades back in once the shell has room */
    will-change: transform;
    pointer-events: none;
  }
  .site-nav.no-active .nav-lens { opacity: 0; }

  /* The tab bar's footprint from the window floor: home-indicator inset
     + its 16px gap + its height = --nav-floor. Content clears that
     plus a breathing gap; the FAB parks one --s2 above its top edge.
     The clearance is the footer's own bottom padding, not the body's:
     the footer runs to the bottom of the page and the bar floats over
     its lower edge, instead of a band of empty page under the footer. */
  :root { --nav-floor: calc(env(safe-area-inset-bottom, 0px) + var(--s2) + var(--tab-h)); }
  .site-footer { padding-bottom: calc(var(--nav-floor) + var(--s4)); }
  .scroll-top { bottom: calc(var(--nav-floor) + var(--s2)); }

  /* The bar is a capsule now, not a strip, so where the screen has
     room the button comes down to sit beside it on the same line: two
     floating controls, the way iOS parks its search button next to the
     tab bar. The script sets the class only when the gap beside the bar
     can take the button; on a narrow screen, or with more tabs, it
     stays parked above the bar as before. Centred on the bar's line,
     since the two are not the same height at every width. */
  :root { --fab-size: 60px; }
  body.fab-beside .scroll-top {
    bottom: calc(var(--s2) + env(safe-area-inset-bottom, 0px)
                 + (var(--tab-h) - var(--fab-size)) / 2);
  }
}

@media (max-width: 640px) {
  :root { --s5: 1.25rem; }
  /* Phones: the page frame, like everything else */
  .post-hero { padding: var(--s5) var(--page-pad) var(--s4); }
  .page-header { padding: var(--s8) var(--s5) var(--s6); }
  .post-title { font-size: clamp(2rem, 8vw, 2.8rem); }
  .page-header h1 { font-size: clamp(2.4rem, 10vw, 3.6rem); }

  /* Hide decorative shapes: too large relative to narrow viewport */
  .page-header-inner::before,
  .post-hero-inner::before { display: none; }

  /* Slimmer reading panels on phones */
  .listing-wrap { padding-left: var(--s3); padding-right: var(--s3); border-radius: 10px; }
  .post-content { padding-left: var(--s3); padding-right: var(--s3); border-radius: 10px; }

  .post-row {
    grid-template-columns: 1fr;
    gap: 2px;
    padding: var(--s2) var(--s1);
  }
  .post-row-date { font-size: 9px; }
  .post-row-readtime { display: none; }

  .post-nav { grid-template-columns: 1fr; }
  .post-nav-item { border-right: none; border-bottom: 1px solid var(--border); }
  .post-nav-item:last-child { border-bottom: none; }

  /* Phones give up more of the gutter; every band narrows with it */
  :root { --page-pad: var(--s2); }

  /* Closer to the corner, and a touch smaller. The bottom offset is
     inherited from the ≤860 rule (--nav-floor + --s2), so it keeps its
     gap above the tab bar without restating the calc. */
  :root { --fab-size: 54px; }
  .scroll-top {
    right: var(--s2);
    width: var(--fab-size); height: var(--fab-size);
  }
  .scroll-top-arrow { width: 19px; height: 19px; }
  .post-more-grid { grid-auto-flow: row; grid-auto-columns: auto; }
  .post-more-item {
    border-right: none; border-bottom: 1px solid var(--border);
    padding-left: 0; padding-right: 0;
  }
  /* item 2 is the last *visible* one here; :last-child is hidden */
  .post-more-item:nth-child(2) { border-bottom: none; }

  /* Phones: one column, name then socials then palette, all flush left.
     The icon row steps out by its own inset so the glyphs line up with
     the text above rather than their hit areas. */
  .site-footer { grid-template-columns: 1fr; gap: var(--s2); justify-items: start; }
  .footer-social { margin-left: -9px; }
  .footer-palette { justify-self: start; }
}

/* ════════════════════════════════════════════════════════════════
   PRINT
════════════════════════════════════════════════════════════════ */
@media print {
  .site-header, .site-footer, .post-sidebar, .post-rail,
  .post-nav, .scroll-top, .theme-toggle, .px, .lightbox,
  body::before { display: none !important; }
  body { background: #fff !important; }
  .post-hero { background: none; padding: 0 0 var(--s3); }
  .post-hero * { color: black !important; }
  .post-title { font-size: 2.2rem; }
  .post-content, .listing-wrap {
    background: none !important; box-shadow: none !important;
    border: none !important; backdrop-filter: none !important;
  }
  .page-header-inner::before,
  .post-hero-inner::before { display: none; }
}

/* ════════════════════════════════════════════════════════════════
   HOME v4: the dossier layout.

   A label column and a content column on one grid, top to bottom.
   The left edge never moves, so the page can be scanned by its
   labels and read only where it earns it.

   Added in the redesign: screenshots, "project at a glance" tables,
   the one-sentence promise, side-by-side components, and brand marks
   painted as CSS masks. See _templates/logos/README.md for where the
   marks come from and why some names deliberately have none.
════════════════════════════════════════════════════════════════ */
/* ── Screenshots ──────────────────────────────────────────────────
   Light/dark pairs. The site has no <picture> convention, and
   data-theme is an attribute rather than a media query, so the swap
   is done by showing one of two images. */
.shot { position: relative; display: block; margin: 0; }
.shot img {
  display: block; width: 100%; height: auto;
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  box-shadow: 0 8px 30px rgba(46,52,64,0.13);
  background: var(--card-bg);
}
/* One theme swap, used by screenshots and by company wordmarks that
   only read on one ground each. The `.shot img` selector sets
   `display: block`, so the hiding rules are scoped `.shot img.…` to
   match its specificity and still win. */
.shot img.shot-dark,
.co-logo .logo-dark { display: none; }
:root[data-theme="dark"] .shot img.shot-light,
:root[data-theme="dark"] .co-logo .logo-light { display: none; }
:root[data-theme="dark"] .shot img.shot-dark,
:root[data-theme="dark"] .co-logo .logo-dark  { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .shot img.shot-light,
  :root:not([data-theme="light"]) .co-logo .logo-light { display: none; }
  :root:not([data-theme="light"]) .shot img.shot-dark,
  :root:not([data-theme="light"]) .co-logo .logo-dark  { display: block; }
}
.shot figcaption {
  margin-top: 10px;
  font-family: var(--f-mono); font-size: 10px; letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--text-3);
}
/* An image that does not exist yet says so, rather than 404ing. */
.shot-missing {
  display: grid; place-items: center; text-align: center;
  aspect-ratio: 16 / 10;
  border: 1px dashed color-mix(in srgb, var(--accent) 55%, transparent);
  border-radius: 10px;
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  padding: var(--s4);
}

/* ── The promise: one sentence, given the weight of a pull quote ─ */
.promise {
  margin: 0;
  font-family: var(--f-display);
  font-size: clamp(1.35rem, 2.3vw, 1.95rem);
  font-weight: var(--w-display); line-height: 1.32; color: var(--text);
  letter-spacing: -0.01em;
  border-left: 2px solid var(--accent);
  padding-left: var(--s3);
}
.promise em { font-style: normal; color: var(--accent); }
.promise code {
  font-family: var(--f-mono); font-size: 0.72em;
  background: var(--code-bg); color: var(--code-fg);
  padding: 1px 6px; border-radius: 4px;
}

/* ── Project at a glance ─────────────────────────────────────────
   A table, not prose. Two columns, mono key, hairline rows. */
.glance { width: 100%; border-collapse: collapse; }
.glance th, .glance td {
  text-align: left; vertical-align: baseline;
  padding: 9px 0; border-bottom: 1px solid var(--border);
}
.glance tr:last-child th, .glance tr:last-child td { border-bottom: none; }
.glance th {
  width: 92px; padding-right: var(--s2);
  font-family: var(--f-mono); font-size: 9.5px; font-weight: 400;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--text-3);
  white-space: nowrap;
}
.glance td { font-size: 13px; line-height: 1.6; color: var(--text-2); }
.glance code {
  font-family: var(--f-mono); font-size: 11.5px;
  background: var(--code-bg); color: var(--code-fg);
  padding: 1px 5px; border-radius: 4px;
}

/* ── The parts of a bigger project, as cards ─────────────────────
   Nivara Desk is one product with three separable pieces. Cards say
   that better than bare columns: each is a thing you can pick up on
   its own. Stacked one per row, full width, so every project reads
   the same way down the page and each card gets room for its points. */
.parts-stack {
  display: flex; flex-direction: column;
  gap: var(--s3);
  margin-top: var(--s3);
}
.comp {
  display: flex; flex-direction: column; gap: var(--s2);
  padding: var(--s3);
  border-radius: 12px;
  border: 1px solid var(--panel-border);
  border-top: 2px solid var(--accent);
  background: var(--card-bg);
  box-shadow: 0 1px 3px rgba(0,0,0,0.05), inset 0 1px 0 var(--glass-hi);
  transition: border-color .2s var(--ease-out),
              transform .2s var(--ease-out),
              box-shadow .2s var(--ease-out);
}
.comp:hover {
  transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  border-top-color: var(--accent);
  box-shadow: 0 6px 20px rgba(0,0,0,0.07), inset 0 1px 0 var(--glass-hi);
}
.comp-head { display: flex; align-items: baseline; gap: var(--s1); flex-wrap: wrap; }
.comp-label {
  font-family: var(--f-display); font-size: 1.15rem; color: var(--text);
  margin: 0; font-weight: var(--w-display);
}
.comp-lang {
  font-family: var(--f-mono); font-size: 9.5px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--accent);
}
.comp-promise {
  font-size: 13.5px; line-height: 1.55; color: var(--text);
  font-weight: 700; margin: 0;
}
.comp-promise code {
  font-family: var(--f-mono); font-size: 11.5px; font-weight: 400;
  background: var(--code-bg); color: var(--code-fg);
  padding: 1px 5px; border-radius: 4px;
}
.comp .points { margin-top: 0; }
.comp .points li { font-size: 13px; line-height: 1.65; }
.comp .chips { margin-top: var(--s1); }
.comp .chips li { font-size: 9.5px; }
.comp-more {
  font-family: var(--f-mono); font-size: 10px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--accent); text-decoration: none;
}
.comp-more:hover { text-decoration: underline; }

/* ── Tech marks ──────────────────────────────────────────────────
   Real <img> elements, not CSS masks. A mask paints one colour by
   definition, which is what made these monochrome; devicon's marks
   carry their own gradients and fills, so they are rendered as
   images and left alone. */
.tech-ic {
  width: 14px; height: 14px; flex-shrink: 0;
  object-fit: contain; vertical-align: -2px;
}
.chips li { display: inline-flex; align-items: center; gap: 6px; }

/* ── Toolkit ribbons: one row per group, always one row ─────────
   The group's items are emitted twice inside the track. The track
   slides left by exactly half its own width and restarts, so the
   second run is under the cursor at the moment the first would run
   out: no jump, no gap. */
.ribbon { padding: 10px 0; }
.ribbon + .ribbon { border-top: 1px solid var(--band-line); }
.ribbon-label {
  margin: 0 0 6px;
  font-family: var(--f-mono); font-size: 9.5px; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--accent);
}
/* The loop runs the full width of the band and fades at both ends
   (after React Bits LogoLoop), so items enter and leave rather than
   being cut off against a hard edge. */
.ribbon-view {
  overflow: hidden;
  --fade-0: 0px; --fade: clamp(24px, 8%, 120px);
  -webkit-mask-image: linear-gradient(90deg, transparent var(--fade-0), #000 var(--fade), #000 calc(100% - var(--fade)), transparent calc(100% - var(--fade-0)));
  mask-image: linear-gradient(90deg, transparent var(--fade-0), #000 var(--fade), #000 calc(100% - var(--fade)), transparent calc(100% - var(--fade-0)));
}
/* Where the rail is shown, the loop is clear under the rail and fades
   in past it, and the same on the right, so the band stays centred. */
@media (min-width: 1200px) { .ribbon-view { --fade-0: 170px; --fade: 290px; } }
.ribbon-track {
  display: flex; width: max-content;
  animation: ribbon-slide 46s linear infinite;
}
.ribbon-rev .ribbon-track { animation-direction: reverse; }
.ribbon:hover .ribbon-track,
.ribbon:focus-within .ribbon-track { animation-play-state: paused; }
/* JS parks this on rows that are scrolled off screen: a stopped
   animation is not composited every frame, so the ~11 marquee rows
   cost nothing while you are reading any other section. */
.ribbon.is-offscreen .ribbon-track { animation-play-state: paused; }
@keyframes ribbon-slide {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
/* The right padding must equal the internal gap, or the join between
   the two runs is narrower than every other gap and the loop shows a
   visible stutter once per cycle. */
.ribbon-run {
  display: flex; align-items: center; gap: var(--s4);
  margin: 0; padding: 0 var(--s4) 0 0; list-style: none;
  flex-shrink: 0;
}
.tool {
  display: flex; align-items: center; gap: 8px;
  white-space: nowrap; flex-shrink: 0;
}
.tool img { width: 22px; height: 22px; object-fit: contain; display: block; }
.tool span {
  font-family: var(--f-mono); font-size: 10.5px; letter-spacing: 0.04em;
  color: var(--text-2);
}
/* No brand mark in either icon set: initials, in the same footprint. */
.tool-textual i {
  width: 22px; height: 22px; flex-shrink: 0;
  display: grid; place-items: center; font-style: normal;
  font-family: var(--f-mono); font-size: 9px; letter-spacing: 0.03em;
  color: var(--text-3);
  border: 1px solid var(--border); border-radius: 5px;
}

/* Motion is decoration here. Switched off, the row becomes an
   ordinary horizontal scroller the reader drives. */
@media (prefers-reduced-motion: reduce) {
  .ribbon-track { animation: none; width: auto; }
  .ribbon-view { overflow-x: auto; -webkit-mask-image: none; mask-image: none; }
  .ribbon-run:nth-child(2) { display: none; }
}

.co-logo {
  height: 58px; min-width: 58px; max-width: 158px;
  flex-shrink: 0; padding: 0 12px;
  display: grid; place-items: center;
  border: 1px solid var(--panel-border); border-radius: 10px;
  background: var(--card-bg); overflow: hidden;
}
/* Sized so a wordmark with a second line ("zeonix / Global") is still
   readable; at 28px that line rendered about four pixels tall. */
.co-logo img {
  display: block;
  max-height: 38px; max-width: 134px;
  width: auto; height: auto; object-fit: contain;
}
.co-logo b {
  font-family: var(--f-display); font-size: 1.25rem; font-weight: var(--w-display);
  color: var(--accent); letter-spacing: 0.02em; line-height: 1;
}

.xp-when {
  margin-left: auto; font-family: var(--f-mono); font-size: 10px;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-3);
  white-space: nowrap;
}
.xp-venture { margin-top: var(--s3); padding-left: var(--s3); border-left: 1px solid var(--border); }
.xp-venture h4 {
  margin: 0 0 2px; font-family: var(--f-mono); font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); font-weight: 400;
}
.xp-venture p.note { margin: 0 0 var(--s1); font-size: 12px; color: var(--text-3); }

/* ═════════════════════════════════════════════════════════════════
   LAYOUT B: DOSSIER
   A stack of section cards, one content column each. The section
   name is not printed on the card: from 1200px up the left-margin
   rail (.drail) names whichever section you are reading; below that
   each card shows a compact .ds-heading instead.
═════════════════════════════════════════════════════════════════ */
/* ONE content width, everywhere. The old page ran bands at 1280px,
   then 820px for About, then 1000px for Writing: three spines as you
   scrolled. band-narrow and band-mid are kept as aliases so any page
   still using them lands on the same measure instead of drifting. */
.home .band-inner,
.home .band-narrow,
.home .band-mid { max-width: 1180px; }
.home .band { padding: var(--s8) 0; }
/* From 1200px the rail sits in the left margin and its label pill
   reaches ~170px in. Between 1200px and ~1450px the margin is thinner
   than that, so the column pulls in (the same amount on both sides,
   to stay centred) until the margin alone clears the pill. */
@media (min-width: 1200px) {
  .home .band-inner {
    padding-inline: clamp(var(--s5), calc(176px - (100vw - 1180px) / 2), 176px);
  }
}

/* Each section is a full-width row. They used to be a stack of
   identical glass cards, which read as one long slab in a different
   coat; the same box around a grid, a timeline and a list said
   nothing about what was inside. Now the rows alternate: open on the
   scene, then a tinted band, then open again. The band is the only
   thing drawn; the components inside each row do the rest. */
.ds-row {
  padding: var(--s8) 0;
  scroll-margin-top: var(--h-header);
  /* The Electric Border canvas runs 40px past its card on every side;
     on a phone that reached past the screen and widened the whole page.
     clip (not hidden) makes no scroll box, so the sticky rail still works. */
  overflow-x: clip;
}
.ds-band {
  background: var(--band-bg);
  border-top: 1px solid var(--band-line);
  border-bottom: 1px solid var(--band-line);
}
.ds-band + .ds-band { border-top: none; }
:root {
  --band-bg:   color-mix(in srgb, var(--bg-raised) 78%, transparent);
  --band-line: color-mix(in srgb, var(--border) 70%, transparent);
}
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .ds-band { background: var(--bg-raised); }
}

/* Compact section heading, shown only where the rail is not (below
   1200px). One mono line, the note trailing in serif on the same
   line so it costs a single row of height, not three. */
.ds-heading {
  margin: 0 0 var(--s4);
  font-family: var(--f-mono); font-size: 12.5px; font-weight: 400;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent);
}
.ds-heading small {
  font-family: var(--f-serif); font-size: 12.5px; letter-spacing: 0;
  text-transform: none; color: var(--text-3); margin-left: var(--s1);
}
@media (min-width: 1200px) { .ds-heading { display: none; } }

.ds-val { min-width: 0; }
.proj-blurb {
  margin: 0 0 var(--s2);
  font-size: 14.5px; line-height: 1.6; color: var(--text-2);
}
.proj-parts-label {
  margin: var(--s5) 0 var(--s2);
  font-family: var(--f-mono); font-size: 9.5px;
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-3);
}
.proj > .chips { margin-top: var(--s4); }
.proj > .project-links { margin-top: var(--s4); }

/* ── Common project layout ──────────────────────────────────────
   heading → carousel → write-up → part cards → stack → links.
   Every project runs this order, so the page reads the same way
   whichever one you land on. */

/* Screenshots as a horizontal strip you swipe or scroll through,
   instead of a 2-up grid. Any number of shots fits without changing
   the layout below it. Dots underneath jump between them. */
.proj-shots { margin-top: var(--s4); }
.proj-carousel {
  display: flex;
  gap: var(--s3);
  padding: var(--s1) 2px var(--s2);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--accent) 45%, transparent) transparent;
}
.proj-carousel .shot {
  flex: 0 0 min(78%, 440px);
  scroll-snap-align: start;
}
.proj-carousel .shot figcaption { font-size: 9px; }
.proj-carousel:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 4px;
}

/* Dots: one per screenshot, the current one drawn as a pill. Built
   by JS (so they only appear where they work); hidden if a project
   has a single shot. */
.proj-dots {
  display: flex; justify-content: center; align-items: center;
  gap: 7px;
  margin-top: var(--s2);
}
.proj-dots:empty { display: none; }
.proj-dot {
  width: 7px; height: 7px; padding: 0;
  border: none; border-radius: 999px;
  background: color-mix(in srgb, var(--text-3) 55%, transparent);
  cursor: pointer;
  transition: width .2s var(--ease-out), background .2s var(--ease-out);
}
.proj-dot:hover { background: var(--text-3); }
.proj-dot.is-on {
  width: 20px;
  background: var(--accent);
}
.proj-dot:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
  .proj-dot,
  .drail, .drail-dot, .drail a > span, .drail-line i { transition: none; }
}

/* The write-up: the promise in view, the rest behind a fold. */
.proj-copy { margin-top: var(--s4); }
.proj-copy > .project-body { margin-top: var(--s3); }
.proj-copy > .points { margin-top: var(--s3); }
.proj-copy > .glance {
  margin-top: var(--s4); padding-top: var(--s3);
  border-top: 1px solid var(--border);
}

/* Magic Bento's second half: one large soft light that follows the
   pointer across the whole projects band. */
#projects { position: relative; overflow: hidden; }
.grid-spotlight {
  position: absolute; left: var(--x, 50%); top: var(--y, 50%);
  width: 720px; height: 720px; border-radius: 50%;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle,
    color-mix(in srgb, var(--accent) 16%, transparent) 0%,
    color-mix(in srgb, var(--accent) 7%, transparent) 30%,
    transparent 68%);
  pointer-events: none; opacity: 0; z-index: 0;
  transition: opacity 0.4s var(--ease-out);
}
.grid-spotlight.is-on { opacity: 1; }
#projects > .band-inner { position: relative; z-index: 1; }

/* ── Projects: a grid of poster cards ──────────────────────────
   One tile per project: the screenshots as a cover that bleeds to
   the card's edges, then the name, one line, chips, links. The
   write-up is a fold at the foot of the card. Two across; the grid
   makes room for the next project on its own. */
.proj-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s3); align-items: start;
}
.proj { padding: var(--s3); overflow: hidden; }
.proj .proj-shots { margin: calc(var(--s3) * -1) calc(var(--s3) * -1) 0; }
.proj .proj-carousel { gap: 0; padding: 0; scrollbar-width: none; }
.proj .proj-carousel::-webkit-scrollbar { display: none; }
.proj .proj-carousel .shot { flex: 0 0 100%; }
/* No screenshots: a plate in the cover's place, the name's initial in
   the display face on an accent wash, so the card keeps the same
   silhouette as the ones with shots. */
.proj-plate {
  position: relative; aspect-ratio: 16 / 9; overflow: hidden;
  display: grid; place-items: center;
  border-bottom: 1px solid var(--panel-border);
  background:
    radial-gradient(120% 90% at 18% 0%, color-mix(in srgb, var(--accent) 26%, transparent), transparent 60%),
    radial-gradient(90% 80% at 100% 100%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 55%),
    var(--bg-raised);
}
.proj-plate-initial {
  font-family: var(--f-display); font-weight: var(--w-display-xl); font-size: clamp(5rem, 12vw, 8rem);
  line-height: 1; color: color-mix(in srgb, var(--accent) 55%, var(--text));
  letter-spacing: -0.03em;
}
/* On a white tile, like an app icon, so a dark mark (Solidity) reads
   on the dark theme as well as a bright one (JavaScript) does. */
.proj-plate-mark {
  width: clamp(88px, 15vw, 128px); height: auto; aspect-ratio: 1;
  object-fit: contain; padding: 18px; border-radius: 26px;
  background: #fff; box-shadow: 0 12px 30px rgba(0,0,0,0.28);
}
.proj-plate-lang {
  position: absolute; left: var(--s2); bottom: var(--s2);
  padding: 4px 9px; border-radius: 6px;
  font-family: var(--f-mono); font-size: 10px; letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--text-2);
  background: color-mix(in srgb, var(--card-bg) 88%, transparent);
}
/* Every cover the same height, cropped from the top, so the two
   cards line up whatever each screenshot's proportions are. */
.proj .shot img {
  aspect-ratio: 16 / 9; object-fit: cover; object-position: left top;
  border: none; border-bottom: 1px solid var(--panel-border);
  border-radius: 0; box-shadow: none;
}
.proj .shot figcaption {
  position: absolute; left: var(--s2); bottom: var(--s2); margin: 0;
  padding: 4px 9px; border-radius: 6px;
  background: color-mix(in srgb, var(--card-bg) 88%, transparent);
  color: var(--text-2);
}
.proj .proj-dots { margin-top: var(--s2); }
.proj .ds-fhead { margin-top: var(--s3); }
.proj .ds-fhead h2 { font-size: 1.45rem; }
.proj .proj-blurb { margin-bottom: 0; }
.proj > .chips { margin-top: var(--s3); }
.proj > .project-links { margin-top: var(--s2); }
/* A piece named as a chip: label, then its language in the accent. */
.chips .chip-sub { margin-left: 6px; color: var(--accent); }
/* Four show; the rest are hidden while the grid is collapsed and
   rise in when the button opens it. Without JS the class is never
   set, so every card shows. */
.proj-grid.is-collapsed > .proj:nth-child(n + 5) { display: none; }
.proj-grid-more { margin-top: var(--s4); }
.proj-grid-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 0; border: none; background: none; cursor: pointer;
  font: inherit;
}
.proj-grid-btn[aria-expanded="true"] span { transform: rotate(90deg); }

/* ── The fold: one disclosure for projects, jobs and the essay ──
   The page reads at a glance closed; open, the full write-up drops
   in beneath the summary. The summary keeps the same mono label as
   the other section furniture; its chevron comes from details.more. */
.proj-more { margin-top: var(--s3); padding-top: var(--s2); border-top: 1px solid var(--border); }
.proj-more > .promise { margin-top: var(--s2); font-size: 1.15rem; line-height: 1.4; }
.proj-more > .project-body,
.proj-more > .points,
.proj-more > .glance { margin-top: var(--s3); }
.proj-more > .glance { padding-top: var(--s3); border-top: 1px solid var(--border); }
.proj-more > .parts-stack { margin-top: 0; }
.proj-more[open] > summary { margin-bottom: var(--s1); }

.ds-fhead { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; margin-bottom: var(--s2); }
.ds-fhead h2 { font-family: var(--f-display); font-size: 1.8rem; font-weight: var(--w-display); margin: 0; color: var(--text); }
.ds-fhead .xp-when { margin-left: auto; }

/* The pill that names what kind of thing a project is. */
.kind-pill {
  font-family: var(--f-mono); font-size: 9.5px; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  border-radius: 999px; padding: 3px 9px;
}

/* ── About: the lede, then the three claims as icon tiles ───────
   Both used to be their own dossier rows (Background, Position).
   The tiles carry the pillars' marks (spark, shield, server), so
   the three claims register before they are read. Below 900px they
   stack one per row (see the responsive block). */
#about .ds-prose { max-width: 40rem; }
.pillar-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s3); margin-top: var(--s4);
}
.pillar { padding: var(--s3); }
.pillar .tile-ic { margin-bottom: var(--s2); }
.pillar-title {
  margin: 0 0 6px; line-height: 1.2;
  font-family: var(--f-display); font-size: 1.15rem; font-weight: var(--w-display); color: var(--text);
}
.pillar-body { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--text-2); }

/* ── About prose: the reading measure is set by the grid column ── */
.ds-prose p {
  font-size: 14px; line-height: 1.8; color: var(--text-2);
  margin: 0 0 var(--s2);
}
.ds-prose p:last-child { margin-bottom: 0; }
.ds-prose em { color: var(--text-3); }

/* The positioning line (from the hero) opens the essay as its first
   paragraph: one passage, not a separate standfirst. Ink rather than
   the body grey, so it still leads. */
#about .ds-lede { color: var(--text); }
#about .ds-lede strong { font-weight: 700; color: var(--accent); }

/* The "now" line, also from the hero: a mono footnote under the essay. */
#about .ds-now {
  display: flex; align-items: center;
  margin: var(--s3) 0 0;
  font-family: var(--f-mono); font-size: 10.5px;
  letter-spacing: 0.04em; line-height: 1.5;
  color: var(--text-3);
}
#about .ds-now .status-dot { margin-right: 9px; }
/* The essay, folded under the tiles. */
.about-more { margin-top: var(--s3); }
.about-more[open] > summary { margin-bottom: var(--s2); }
.about-more > .ds-prose { max-width: 40rem; }

/* ── Writing: each post a row tile, the date as a block ─────── */
.post-list .post-row {
  margin: 0 0 var(--s2); padding: var(--s2) var(--s3);
  border: 1px solid var(--panel-border); border-radius: 12px;
  background: var(--card-bg);
  box-shadow: 0 1px 3px rgba(0,0,0,0.05), inset 0 1px 0 var(--glass-hi);
  transition: border-color 0.2s var(--ease-out), transform 0.2s var(--ease-out);
}
.post-list .post-row:last-of-type { margin-bottom: 0; }
.post-list .post-row::before { display: none; }
.post-list .post-row:hover { border-color: var(--accent); transform: translateX(4px); }
.post-list .section-more { margin-top: var(--s3); }

/* ── Toolkit groups ──────────────────────────────────────────── */
.tk-group + .tk-group { margin-top: var(--s5); }
.tk-group > .group-heading { margin: 0 0 var(--s2); }

/* ── Credentials: two panels, one card geometry ────────────────
   Education, then certifications and awards, each a full-width panel.
   Inside, every entry is a card of the same shape (mark, name,
   source, and a foot for the score or verify chips), laid in one
   grid, so the two subsections read as the same thing. */
.cred-stack { display: grid; gap: var(--s3); }
.cred-panel { padding: var(--s2) var(--s3) var(--s3); }
.cred-panel-title {
  margin: 0 0 var(--s2);
  font-family: var(--f-mono); font-size: 9.5px; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--accent);
}
.cred-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--s2);
}
.cred-card--w2 { grid-column: span 2; }
.cred-card--w3 { grid-column: span 3; }
@media (max-width: 900px) {
  .cred-grid { grid-template-columns: 1fr; }
  .cred-card--w2, .cred-card--w3 { grid-column: auto; }
}
.cred-card {
  display: flex; align-items: flex-start; gap: var(--s2);
  padding: var(--s2) var(--s2) var(--s2) var(--s2);
  border: 1px solid var(--border); border-radius: 10px;
  background: color-mix(in srgb, var(--bg-raised) 55%, transparent);
  transition: border-color 0.2s var(--ease-out);
}
.cred-card:hover { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.cred-card .tile-ic { width: 34px; height: 34px; border-radius: 9px; }
.cred-card .tile-ic svg { width: 16px; height: 16px; }
.cred-body { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 100%; }
.cred-head {
  margin: 0; line-height: 1.3;
  font-family: var(--f-display); font-weight: var(--w-display); font-size: 1.05rem; color: var(--text);
}
.cred-sub { margin: 2px 0 0; font-size: 12.5px; font-style: italic; color: var(--text-2); }
.cred-meta {
  margin: 3px 0 0; font-family: var(--f-mono); font-size: 9.5px;
  letter-spacing: 0.05em; line-height: 1.6; color: var(--text-3);
}
/* The foot sits at the bottom of the card whatever the text above did. */
.cred-foot { margin: auto 0 0; padding-top: var(--s1); }
.cred-foot.cert-links { margin-top: auto; }
.cred-score { font-family: var(--f-mono); font-size: 10px; color: var(--accent); white-space: nowrap; }

/* ── Responsive ───────────────────────────────────────────────────
   Every multi-column grid on the page collapses to one track. */
/* Below 900px the poster and pillar grids go to one track. */
@media (max-width: 900px) {
  .proj-grid, .pillar-grid { grid-template-columns: 1fr; }
}

@media (max-width: 900px) {
  .ds-row { padding: var(--s5) 0; }
  .proj-carousel .shot { flex-basis: 86%; }
}


/* ════════════════════════════════════════════════════════════════
   TOUCH DEVICES: no blur on content
   Last in the file on purpose: these undo backdrop-filters declared
   all through the sheet at the same specificity, so they must come
   after every one of them.
════════════════════════════════════════════════════════════════ */
@media (pointer: coarse) {
  /* Every backdrop-filter is a
     texture the compositor re-blurs on each scroll frame, and a phone
     GPU can't keep several of them at 60fps; the article panel alone
     is a viewport-wide one that never leaves the screen. The panels
     are 80–88% opaque already, so their 3px scrim was nearly invisible
     and goes for free; the glass cards trade their blur for a heavier
     tint so the scene's texture doesn't print through the text. The
     two navigation bars and the back-to-top button beside the tab bar
     keep their glass (it's the material of the design and they are
     small) at a lighter radius. */
  :root { --glass-blur-nav: 14px; }
  .post-content, .listing-wrap, .card, .home-prose.sheet,
  .hero-portrait.no-img .portrait-monogram,
  .sidebar-card, .site-footer {
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  .sidebar-card { background: color-mix(in srgb, var(--bg-raised) 92%, transparent); }
  .site-footer  { background: var(--header-glass-solid); }
}


/* Reduced transparency, part two: the rails share the header's glass, so
   they share its solid fallback too. Here rather than beside the header's
   rule because .sidebar-card is declared further down than that. */
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .sidebar-card {
    background: var(--header-glass-solid);
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
}

/* ── SKETCHES ──────────────────────────────────────────────────────
   Hand-drawn figures (_templates/sketches → _includes/sketches) are
   inline SVG whose paths carry tone classes, not colours, so they
   follow the toggle like everything else. s- outline, h- hatching,
   f- solid fill, t- text. Same eight hues as the Mermaid roles.
──────────────────────────────────────────────────────────────── */
:root {
  --sk-ink: var(--text);         --sk-soft: var(--text-3, #5E6A83);
  --sk-blue: #2563EB;   --sk-blue-fill: #DBEAFE;
  --sk-violet: #7C3AED; --sk-violet-fill: #EDE9FE;
  --sk-green: #059669;  --sk-green-fill: #D1FAE5;
  --sk-cyan: #0891B2;   --sk-cyan-fill: #CFFAFE;
  --sk-amber: #D97706;  --sk-amber-fill: #FEF3C7;
  --sk-red: #DC2626;    --sk-red-fill: #FEE2E2;
  --sk-ink-fill: #E5E9F0; --sk-soft-fill: #E5E9F0;
}
:root[data-theme="dark"] {
  --sk-blue: #60A5FA;   --sk-blue-fill: #1B3050;
  --sk-violet: #A78BFA; --sk-violet-fill: #2B1D4A;
  --sk-green: #34D399;  --sk-green-fill: #123A2E;
  --sk-cyan: #22D3EE;   --sk-cyan-fill: #0E3540;
  --sk-amber: #FBBF24;  --sk-amber-fill: #42310D;
  --sk-red: #F87171;    --sk-red-fill: #431A1B;
  --sk-ink-fill: #3B4252; --sk-soft-fill: #3B4252;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --sk-blue: #60A5FA;   --sk-blue-fill: #1B3050;
    --sk-violet: #A78BFA; --sk-violet-fill: #2B1D4A;
    --sk-green: #34D399;  --sk-green-fill: #123A2E;
    --sk-cyan: #22D3EE;   --sk-cyan-fill: #0E3540;
    --sk-amber: #FBBF24;  --sk-amber-fill: #42310D;
    --sk-red: #F87171;    --sk-red-fill: #431A1B;
    --sk-ink-fill: #3B4252; --sk-soft-fill: #3B4252;
  }
}
figure.sketch { margin: 1.75rem 0; }
figure.sketch svg { display: block; width: 100%; height: auto; margin: 0 auto; overflow: visible; }
/* The house caption is a 9px mono label; a sketch's caption is a sentence that says
   what the colours mean, so it keeps the mono face at a size you can read. */
.post-content figure.sketch figcaption { margin: .6rem auto 0; max-width: 62ch; font-size: 12px; line-height: 1.55; letter-spacing: .02em; color: var(--text-3); }
.sketch-svg path { fill: none; stroke-linecap: round; stroke-linejoin: round; }
.sketch-svg text { font-family: 'Caveat', 'Segoe Print', 'Comic Sans MS', cursive; }
.sketch-svg .s-ink, .sketch-svg .h-ink { stroke: var(--sk-ink); }       .sketch-svg .f-ink { fill: var(--sk-ink-fill); }     .sketch-svg .t-ink { fill: var(--sk-ink); }
.sketch-svg .s-soft, .sketch-svg .h-soft { stroke: var(--sk-soft); }    .sketch-svg .f-soft { fill: var(--sk-soft-fill); }   .sketch-svg .t-soft { fill: var(--sk-soft); }
.sketch-svg .s-blue, .sketch-svg .h-blue { stroke: var(--sk-blue); }    .sketch-svg .f-blue { fill: var(--sk-blue-fill); }   .sketch-svg .t-blue { fill: var(--sk-blue); }
.sketch-svg .s-violet, .sketch-svg .h-violet { stroke: var(--sk-violet); } .sketch-svg .f-violet { fill: var(--sk-violet-fill); } .sketch-svg .t-violet { fill: var(--sk-violet); }
.sketch-svg .s-green, .sketch-svg .h-green { stroke: var(--sk-green); } .sketch-svg .f-green { fill: var(--sk-green-fill); } .sketch-svg .t-green { fill: var(--sk-green); }
.sketch-svg .s-cyan, .sketch-svg .h-cyan { stroke: var(--sk-cyan); }    .sketch-svg .f-cyan { fill: var(--sk-cyan-fill); }   .sketch-svg .t-cyan { fill: var(--sk-cyan); }
.sketch-svg .s-amber, .sketch-svg .h-amber { stroke: var(--sk-amber); } .sketch-svg .f-amber { fill: var(--sk-amber-fill); } .sketch-svg .t-amber { fill: var(--sk-amber); }
.sketch-svg .s-red, .sketch-svg .h-red { stroke: var(--sk-red); }       .sketch-svg .f-red { fill: var(--sk-red-fill); }     .sketch-svg .t-red { fill: var(--sk-red); }
.sketch-svg [class^="h-"] { opacity: .5; }
.sketch-svg [class^="f-"] { stroke: none; }

/* "Check it with a chatbot": a checkpoint opens ChatGPT or Claude with
   the coaching prompt already typed (see _includes/coach.html). */
.coach { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .75rem; margin: 1.1rem 0 1.6rem;
  padding: .7rem .9rem; border: 1px dashed var(--border); border-radius: 10px; font-size: .92em; }
.coach-label { color: var(--text-3, inherit); }
.coach a.coach-btn { display: inline-flex; align-items: center; gap: .35em; padding: .3em .85em; border-radius: 999px;
  border: 1px solid var(--accent); color: var(--accent); text-decoration: none; font-family: var(--f-mono); font-size: .82em; }
.coach a.coach-btn:hover, .coach a.coach-btn:focus-visible { background: var(--accent); color: var(--bg); }
/* DSA problem tables (_includes/dsa-problems.html): difficulty as a pill, in the
   same three hues the list uses everywhere else: easy green, medium amber, hard red. */
.post-content .dsa-problems-count { font-family: var(--f-mono); font-size: 12px; letter-spacing: .02em; color: var(--text-3); }
.dsa-problems td:first-child { color: var(--text-3); font-family: var(--f-mono); font-size: 12px; }
.dsa-problems .diff { display: inline-block; padding: .1em .6em; border-radius: 999px; font-family: var(--f-mono); font-size: 11px; letter-spacing: .03em; border: 1px solid; }
.dsa-problems .diff-easy   { color: var(--sk-green); background: var(--sk-green-fill); border-color: var(--sk-green); }
.dsa-problems .diff-medium { color: var(--sk-amber); background: var(--sk-amber-fill); border-color: var(--sk-amber); }
.dsa-problems .diff-hard   { color: var(--sk-red);   background: var(--sk-red-fill);   border-color: var(--sk-red); }
.dsa-problems td.dsa-coach { white-space: nowrap; font-family: var(--f-mono); font-size: 12px; }
/* ── /SKETCHES ── */
