/* hocket.dev — design system
   Single 594px column, one type size, hierarchy by color alone.
   Light is the default; [data-theme=dark] flips the registers. */

/* ---------- fonts ---------- */

@font-face {
  font-family: "Geist";
  src: url("/fonts/Geist-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Geist Mono";
  src: url("/fonts/GeistMono-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* ---------- tokens ---------- */

/* Light is hocket's paper dress (the Notion egg): white canvas, warm paper
   surfaces, Notion's published light-mode inks. */
:root {
  --bg: #ffffff;
  --text1: #37352f;
  --text2: #696762;
  --text3: #9b9a97;
  --wash1: #ebeced; /* overlay/selection surface — chips, hover fills */
  --wash2: #f7f6f3; /* warm paper — frames, strips */
  --tborder: rgba(55, 53, 47, 0.1);
  --thumb: #c6c4bf;
  --code-bg: #f1f0ec; /* the agent-screen surface */
  --code-fg: #37352f;
  --selection-fg: #37352f;
  --selection-bg: rgba(206, 205, 202, 0.5);
  --focus: rgba(55, 53, 47, 0.4);

  /* hocket's own grammar — used only where the product uses it */
  --amber: #d9730d;
  --steel: #9b9a97;
  --err: #e03e3e;
  --accent: #0b6e99;

  --logo-body: #487ca5;
  --logo-note: #e9f3f7;
  --logo-note-warm: #f8ecdf;
  --logo-wave: #cb912f;

  --font-sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI",
    system-ui, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo,
    Consolas, monospace;

  --ease-snappy: cubic-bezier(0.175, 0.885, 0.32, 1.1);
  --ease-smooth: cubic-bezier(0.19, 1, 0.22, 1);

  --track: -0.00563rem;
}

/* Dark is hocket's vivid dress: the bright blue of DOS-era IDEs, white
   foreground, teal accent — the easter-egg theme, worn openly. */
[data-theme="dark"] {
  --bg: #235fb8;
  --text1: #f7f9fd;
  --text2: #d5ddea;
  --text3: #b3c2d8;
  --wash1: #094277; /* overlay blue — chips, hover fills */
  --wash2: #1a4e9e; /* the recessed agent surface — frames, strips */
  --tborder: rgba(247, 249, 253, 0.16);
  --thumb: #6d89a9;
  --code-bg: #07345f; /* raised panel — Turbo Pascal territory */
  --code-fg: #f7f9fd;
  --selection-fg: #07345f;
  --selection-bg: #3abbb7;
  --focus: rgba(247, 249, 253, 0.55);

  --amber: #ffc75f;
  --steel: #63809c;
  --err: #e37b78;
  --accent: #3abbb7;

  /* The mark in vivid ink: white body like the text, all three notes cut
     out in the screen blue, the wave in attention gold. */
  --logo-body: #f7f9fd;
  --logo-note: #235fb8;
  --logo-note-warm: #235fb8;
  --logo-wave: #ffc75f;
}

/* ---------- reset ---------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

h1,
h2,
h3,
p,
dl,
dd,
figure,
blockquote,
pre {
  margin: 0;
}

img,
svg {
  display: block;
  max-width: 100%;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}

/* ---------- base ---------- */

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text1);
  font-family: var(--font-sans);
  font-size: 15.5px;
  /* The one leading, fixed: every text row is 24px, so stacks land on
     the 4px grid wherever they end. */
  line-height: 24px;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track);
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-synthesis: none;
  font-feature-settings: "kern" 1, "liga" 1, "calt" 1;
  min-height: 100%;
  overflow-y: scroll;
  transition: background-color 0.2s ease;
}

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

a {
  color: inherit;
}

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

/* top fade — a fixed veil the content scrolls beneath. A real element
   (not a body pseudo) so page transitions can name it and hold it
   perfectly still while the pages change hands underneath it. */

.veil {
  pointer-events: none;
  z-index: 9;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 8rem;
}

.veil::before,
.veil::after {
  content: "";
  position: absolute;
  inset: 0;
  transition: opacity 0.2s ease;
}

.veil::before {
  background: linear-gradient(
    180deg,
    #fff,
    hsla(0, 0%, 100%, 0.738) 19%,
    hsla(0, 0%, 100%, 0.541) 34%,
    hsla(0, 0%, 100%, 0.382) 47%,
    hsla(0, 0%, 100%, 0.278) 56.5%,
    hsla(0, 0%, 100%, 0.194) 65%,
    hsla(0, 0%, 100%, 0.126) 73%,
    hsla(0, 0%, 100%, 0.075) 80.2%,
    hsla(0, 0%, 100%, 0.042) 86.1%,
    hsla(0, 0%, 100%, 0.021) 91%,
    hsla(0, 0%, 100%, 0.008) 95.2%,
    hsla(0, 0%, 100%, 0.002) 98.2%,
    hsla(0, 0%, 100%, 0)
  );
  opacity: 1;
}

.veil::after {
  background: linear-gradient(
    180deg,
    #235fb8,
    rgba(35, 95, 184, 0.738) 19%,
    rgba(35, 95, 184, 0.541) 34%,
    rgba(35, 95, 184, 0.382) 47%,
    rgba(35, 95, 184, 0.278) 56.5%,
    rgba(35, 95, 184, 0.194) 65%,
    rgba(35, 95, 184, 0.126) 73%,
    rgba(35, 95, 184, 0.075) 80.2%,
    rgba(35, 95, 184, 0.042) 86.1%,
    rgba(35, 95, 184, 0.021) 91%,
    rgba(35, 95, 184, 0.008) 95.2%,
    rgba(35, 95, 184, 0.002) 98.2%,
    rgba(35, 95, 184, 0)
  );
  opacity: 0;
}

[data-theme="dark"] .veil::before {
  opacity: 0;
}

[data-theme="dark"] .veil::after {
  opacity: 1;
}

/* ---------- shell ---------- */

.shell {
  max-width: 594px;
  margin: 0 auto;
  padding: 80px 16px;
}

/* ---------- aside (toc) ---------- */

.aside {
  /* Every masthead row (crumbs, h1, rail rows) is a 24px line box, so
     with 80px to the first row's text (76px top + the 4px ring pad) it
     lands on the crumb/h1 line with no compensation. The 16px padding is
     the caret's gutter — the rail clips horizontally, so the caret lives
     inside the box while the text keeps its 72px edge. The 4px on the
     other three sides keeps focus rings (2px outline, 2px offset) clear
     of that clip; max-height gives it back so the bottom edge stays
     16px off the viewport's. */
  position: fixed;
  top: 76px;
  left: 56px;
  padding: 4px 4px 4px 16px;
  z-index: 10;
  max-height: calc(100vh - 92px);
  overflow: hidden auto;
  scrollbar-width: none;
}

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

/* The way home: a lone arrow on the masthead row, answering the crumbs
   across the gutter at the same y. Its 24px line box plus the rail's 8px
   beat step the docs list down exactly one 32px row, so both rails keep
   sharing the lattice. Sized to its glyph — a block link would stretch
   the whole rail width into an invisible target. */
.rail-home {
  display: flex;
  align-items: center;
  width: 16px;
  height: 24px;
  margin-bottom: 8px;
  color: var(--text2);
  transition: color 0.14s ease;
}

@media (hover: hover) {
  .rail-home:hover {
    color: var(--text1);
  }
}

/* A full section-breath between the rail's two lists — the article's own
   32px rhythm, four times the 8px item beat, so the lists can never read
   as one stack. */
.aside nav + nav {
  margin-top: 32px;
}

/* The mark, one size everywhere — landing h1 and docs breadcrumb — so
   navigation never changes it. */
.logo-mark {
  height: 18px;
  width: auto;
  display: inline-block;
  flex: none;
}

.aside nav ul {
  display: flex;
  flex-direction: column;
  gap: 8px;
  list-style: none;
  margin: 0;
  padding: 0;
  /* 24px rows + the 8px gap: the rail steps in 32s, all on the 4px grid. */
  line-height: 24px;
}

.aside nav a {
  font-size: 15.5px;
  color: var(--text3);
  letter-spacing: -0.0025rem;
  text-decoration: none;
  transition: color 0.14s ease;
  /* Long doc-section labels ellipsize before touching the column; the
     24px line box keeps descenders clear of the clip. Block, not
     inline-block: with overflow hidden an inline-block baselines on its
     bottom edge and the row would grow past its line box. fit-content,
     not the row: a focus ring should wrap the label. */
  display: block;
  width: fit-content;
  max-width: min(240px, calc((100vw - 594px) / 2 - 88px));
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.aside nav a[data-active="true"] {
  color: var(--text1);
}

@media (hover: hover) {
  .aside nav a:hover {
    color: var(--text1);
  }
}

/* Hierarchy by color, as everywhere: site-level lists (the docs nav, the
   landing's site links) sit one register up from the on-page spy lists,
   so the rail's two levels never blend. */
.aside :is(.docs-nav, .site) a {
  color: var(--text2);
}

/* This page's sections mirror the docs list on the right — the classic
   docs arrangement, both rails pinned. Fixed width (not content width)
   so the list's left edge sits at the same x on every page. */
.aside.toc {
  left: auto;
  /* 72px minus the ring pad, so the label column still ends at 72px. */
  right: 68px;
  /* The label width plus the caret and ring gutters. */
  width: calc(min(240px, (100vw - 594px) / 2 - 88px) + 20px);
}

.aside.toc a {
  max-width: 100%;
}

/* ---------- rail caret ---------- */

/* A terminal's caret for the rails: a 2×14 bar beside the active row
   that slides — never jumps — when the selection moves. docs.js and the
   landing spy drive it 32px per row; the transition retargets mid-glide
   as you scroll. */
.aside nav {
  position: relative;
}

.rail-caret {
  position: absolute;
  top: 5px; /* centers the bar in the first 24px row */
  left: -14px; /* 12px gap + the 2px bar — inside the rail's padding box, so the clip spares it */
  width: 2px;
  height: 14px;
  border-radius: 1px;
  background: var(--text1);
}

.rail-caret.is-live {
  transition: transform var(--caret-slide) var(--ease-smooth);
}

/* The nav carets (left rail, landing rail) are named shared elements:
   between pages the caret glides to the clicked doc while the lists
   dissolve around it. */
.aside:not(.toc) .rail-caret {
  view-transition-name: nav-caret;
}

/* ---------- article ---------- */

.article {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.article > header {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.article h1 {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 15.5px;
  font-weight: 400;
  /* One 24px masthead row on every page — h1 here, crumbs on the docs —
     so the mark and wordmark sit at the same y wherever you land. */
  line-height: 24px;
  letter-spacing: var(--track);
  color: var(--text1);
}

.section {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.section h2 {
  font-size: 15.5px;
  font-weight: 400;
  line-height: 24px;
  letter-spacing: var(--track);
  color: var(--text1);
  scroll-margin-top: 6rem;
  text-wrap: balance;
}

.section h3 {
  font-size: 15.5px;
  font-weight: 400;
  line-height: 24px;
  letter-spacing: var(--track);
  color: var(--text1);
  text-wrap: balance;
}

.article p,
.article li,
.article dl {
  font-size: 15.5px;
  font-weight: 400;
  line-height: 24px;
  letter-spacing: var(--track);
  color: var(--text2);
  text-wrap: pretty;
}

.article p strong,
.article li strong {
  font-weight: 500;
  color: var(--text1);
}

/* Geist has no italic; emphasis is a register lift, like everything else here */
.article em {
  font-style: normal;
  color: var(--text1);
}

.am {
  color: var(--amber);
}

.article ul,
.article ol {
  display: flex;
  flex-direction: column;
  gap: 8px;
  list-style: none;
  margin: 0;
  padding: 0;
}

.article ul li,
.article ol li {
  position: relative;
  padding: 0 0 0 12px;
}

.article ul li::before {
  content: "·";
  position: absolute;
  left: 0;
  top: 0;
  color: var(--text3);
}

/* numbered subsections — the counter does the counting */

.subsections {
  display: flex;
  flex-direction: column;
  gap: 16px;
  counter-reset: subsection;
}

.subsection {
  display: flex;
  flex-direction: column;
  gap: 8px;
  counter-increment: subsection;
}

.subsection h3::before {
  content: counter(subsection) ".";
  color: var(--text3);
  margin-right: 4px;
}

/* meta list under the h1 */

.meta {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 32px;
}

.meta dt {
  color: var(--text3);
  white-space: nowrap;
  font-size: 15.5px;
  font-weight: 400;
}

.meta dd {
  color: var(--text2);
  font-size: 15.5px;
  font-weight: 400;
}

/* ---------- inline code & kbd ---------- */

.article code {
  font-family: var(--font-mono);
  font-size: 14px;
  background: var(--wash1);
  border-radius: 4px;
  padding: 0.1em 0.35em;
  color: var(--text1);
  letter-spacing: 0;
  white-space: nowrap;
}

/* The app's name, wherever prose speaks it: the product's own mono in
   the primary ink — a register shift, not a chip. Name.tsx applies it
   on the landing page, markdown.ts in the docs. 14px is the site's one
   inline-mono size (.article code). */
.name {
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--text1);
  letter-spacing: 0;
}

kbd {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1;
  color: var(--text2);
  background: var(--wash1);
  box-shadow: inset 0 0 0 1px var(--tborder);
  border-radius: 4px;
  /* A 20px chip: 13px chord + 3/4 split — the extra bottom px optically
     centers mono ink that sits low in its box. */
  padding: 3px 6px 4px;
  letter-spacing: 0;
  white-space: nowrap;
}

/* keys table */

.keys {
  display: grid;
  grid-template-columns: auto 1fr;
  /* 24px rows + the 8px gap — the table steps in 32s, like the rails. */
  gap: 8px 24px;
  align-items: baseline;
  margin-top: 4px;
}

.keys .action {
  color: var(--text2);
}

/* ---------- links ---------- */

.chip-link {
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}

.chip-link .chip-text {
  position: relative;
}

.chip-link .chip-text::after {
  content: "";
  position: absolute;
  bottom: -1px;
  left: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 0.2s var(--ease-smooth);
}

@media (hover: hover) {
  .chip-link:hover .chip-text::after {
    transform: scaleX(1);
  }
}

.chip-link:focus-visible .chip-text::after {
  transform: scaleX(1);
}

.chip-link .chip-icon {
  display: inline-block;
  width: 12px;
  height: 12px;
  margin-left: 2px;
  vertical-align: middle;
  position: relative;
  top: -1px;
}

/* ---------- cta ---------- */

.cta-row {
  display: flex;
  gap: 8px;
}

.cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  border-radius: 4px;
  box-shadow: inset 0 0 0 1px var(--wash1);
  color: var(--text1);
  font-size: 15.5px;
  font-weight: 400;
  line-height: 16px; /* 8 + 16 + 8 — a 32px button */
  letter-spacing: var(--track);
  text-decoration: none;
  width: fit-content;
  transition: background-color 80ms ease, box-shadow 80ms ease,
    transform 0.15s var(--ease-snappy);
}

@media (hover: hover) {
  .cta:hover {
    background: var(--wash2);
  }
}

.cta:active {
  transform: scale(0.96);
}

.cta svg {
  color: var(--text3);
  transition: transform 0.2s var(--ease-smooth);
}

@media (hover: hover) {
  .cta:hover svg {
    transform: translateY(1px);
  }
}

/* ---------- code blocks ---------- */

.codeblock {
  position: relative;
  background: var(--code-bg);
  border-radius: 6px;
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 24px; /* code keeps the prose rhythm */
  color: var(--code-fg);
  letter-spacing: 0;
}

.codeblock pre {
  overflow-x: auto;
  padding: 12px 0;
}

.codeblock .line {
  display: grid;
  grid-template-columns: 2.5rem 1fr;
  white-space: pre;
  padding-right: 44px;
}

.codeblock .gutter {
  color: var(--text3);
  text-align: right;
  padding-right: 0.75rem;
  user-select: none;
  font-variant-numeric: tabular-nums;
}

.codeblock .tok-com {
  color: var(--text3);
}

.codeblock .tok-str {
  color: var(--accent);
}

.codeblock .copy {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 24px; /* the theme toggle's square */
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text3);
  border-radius: 4px;
  transition: background-color 80ms ease, color 80ms ease,
    transform 0.15s var(--ease-snappy);
}

.codeblock .copy::after {
  content: "";
  position: absolute;
  inset: -8px;
}

@media (hover: hover) {
  .codeblock .copy:hover {
    background: rgba(0, 0, 0, 0.05);
    color: var(--text1);
  }

  [data-theme="dark"] .codeblock .copy:hover {
    background: rgba(255, 255, 255, 0.08);
  }
}

.codeblock .copy:active {
  transform: scale(0.92);
}

.codeblock .copy svg {
  position: absolute;
  transition: opacity 0.15s ease, transform 0.15s var(--ease-snappy);
}

.codeblock .copy .ic-check {
  opacity: 0;
  transform: scale(0.5);
  color: var(--text1);
}

.codeblock .copy[data-copied="true"] .ic-copy {
  opacity: 0;
  transform: scale(0.5);
}

.codeblock .copy[data-copied="true"] .ic-check {
  opacity: 1;
  transform: scale(1);
}

/* ---------- status glyph strip ---------- */

.glyphs {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  padding: 12px 16px;
  background: var(--wash2);
  border-radius: 6px;
  font-family: var(--font-mono);
  font-size: 14px;
  letter-spacing: 0;
}

.glyphs .pair {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  white-space: nowrap;
}

.glyphs .g {
  color: var(--text1);
}

.glyphs .g[data-tone="amber"] {
  color: var(--amber);
}

.glyphs .g[data-tone="steel"] {
  color: var(--steel);
}

.glyphs .g[data-tone="err"] {
  color: var(--err);
}

.glyphs .l {
  color: var(--text2);
}

/* ---------- filmstrip ---------- */

.filmstrip-container {
  user-select: none;
}

.filmstrip {
  display: flex;
  overflow-x: auto;
  gap: 8px;
  scrollbar-width: none;
  cursor: grab;
}

.filmstrip.dragging {
  cursor: grabbing;
}

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

.slide {
  flex: 0 0 auto;
  width: min(calc((100vw - 32px) * 0.8), 448px);
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
}

.slide .shot {
  aspect-ratio: 3248 / 2112; /* the screenshots' native ratio — no crop */
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: 8px;
  overflow: hidden;
  background: var(--wash2);
  cursor: zoom-in;
}

.slide .shot::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 0.5px solid var(--tborder);
  border-radius: inherit;
  z-index: 10;
  pointer-events: none;
}

.slide .shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.slide figcaption {
  font-size: 14px;
  line-height: 20px;
  color: var(--text3);
  letter-spacing: var(--track);
  text-align: center;
}

/* The lightbox — the page recedes behind a tinted blur; the shot rises
   in on the site's entrance grammar. Exit is a plain, faster fade.
   Clicking anywhere or esc closes; ← → page between shots instantly. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 24px;
  cursor: zoom-out;
  background: color-mix(in srgb, var(--bg) 84%, transparent);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  opacity: 1;
  transition: opacity 0.22s var(--ease-smooth);
}

.lightbox.leaving {
  opacity: 0;
  transition-duration: 0.14s;
}

.lb-shot {
  width: min(1280px, 100vw - 48px, (100dvh - 176px) * (3248 / 2112));
  aspect-ratio: 3248 / 2112;
  position: relative;
  border-radius: 10px;
  overflow: hidden;
  background-color: var(--wash2);
  background-size: cover; /* inline thumb holds the frame while -full loads */
  box-shadow: 0 16px 48px -12px rgba(0, 0, 0, 0.25);
  transform: translateY(0);
  filter: blur(0);
  transition: transform 0.22s var(--ease-smooth), filter 0.22s var(--ease-smooth);
}

.lb-shot::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 0.5px solid var(--tborder);
  border-radius: inherit;
  pointer-events: none;
}

.lb-shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@starting-style {
  .lightbox {
    opacity: 0;
  }

  .lb-shot {
    transform: translateY(8px);
    filter: blur(3px);
  }
}

.lb-cap {
  color: var(--text2);
  font-size: 14px;
  line-height: 20px;
  letter-spacing: var(--track);
}

.lb-hint {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text3);
  font-size: 13px;
}

@media (hover: none) {
  .lb-hint {
    display: none; /* no keyboard, no keyboard hints */
  }
}

.film-bar {
  position: relative;
  width: 100%;
  height: 2px;
  margin-top: 8px;
  border-radius: 2px;
  background: var(--wash2);
  overflow: hidden;
}

.film-bar .bar {
  position: absolute;
  top: 0;
  height: 100%;
  min-width: 48px;
  border-radius: 2px;
  background: var(--thumb);
  transition: left 60ms linear, width 60ms linear;
}

/* ---------- figure ----------
   Currently unused — kept deliberately for src/components/Figure.tsx. */

.figure {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.figure .frame {
  aspect-ratio: 16 / 10;
  border-radius: 8px;
  overflow: hidden;
  background: var(--wash2);
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}

.figure .frame::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 0.5px solid var(--tborder);
  border-radius: inherit;
  pointer-events: none;
}

.figure figcaption {
  text-align: center;
  color: var(--text3);
  font-size: 15.5px;
  letter-spacing: 0.00063rem;
}

/* ---------- footer ---------- */

.article > footer {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.article > footer p {
  color: var(--text3);
  font-size: 15.5px;
  letter-spacing: 0.00063rem;
}

.article > footer a {
  color: var(--text2);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 30%, transparent);
  transition: color 0.14s ease, text-decoration-color 0.14s ease;
}

@media (hover: hover) {
  .article > footer a:hover {
    color: var(--text1);
    text-decoration-color: currentColor;
  }
}

/* ---------- documentation ---------- */

/* Scoped under .article to outrank the `.article p` prose rule — the
   crumbs are a <p>, and the prose color/leading must not reach them.
   The inherited 24px line box makes this the same masthead row as the
   landing h1: mark and wordmark at the same y on every page. */
.article .crumbs {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text3);
}

.article .crumbs a {
  color: var(--text2);
  text-decoration: none;
  transition: color 0.14s ease;
}

@media (hover: hover) {
  .article .crumbs a:hover {
    color: var(--text1);
  }
}

.crumb-home {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.crumb-here {
  color: var(--text1);
}

.doc-body {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

/* prose links — a quiet underline that lifts on hover */
.doc .section a {
  color: var(--text1);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 30%, transparent);
  transition: text-decoration-color 0.14s ease;
}

@media (hover: hover) {
  .doc .section a:hover {
    text-decoration-color: currentColor;
  }
}

/* ordered lists — the counter does the counting, like .subsections */
.article ol {
  counter-reset: item;
}

.article ol > li {
  counter-increment: item;
  padding-left: 20px;
}

.article ol > li::before {
  content: counter(item) ".";
  position: absolute;
  left: 0;
  top: 0;
  color: var(--text3);
}

.article li > ul,
.article li > ol {
  margin-top: 8px;
  padding-left: 4px;
}

/* keymap tables whose keys sit on the right: action left, chord right */
.keys-flip {
  grid-template-columns: 1fr auto;
}

.keys-flip .chord {
  text-align: right;
}

/* reference tables — row rules only, nothing framed */
.table-scroll {
  overflow-x: auto;
}

.doc-table {
  width: 100%;
  border-collapse: collapse;
}

.doc-table th {
  color: var(--text3);
  font-weight: 400;
  text-align: left;
}

.doc-table th,
.doc-table td {
  padding: 8px 12px 8px 0;
  border-bottom: 1px solid var(--tborder);
  vertical-align: top;
  font-size: 15.5px;
  letter-spacing: var(--track);
}

.doc-table th:last-child,
.doc-table td:last-child {
  padding-right: 0;
}

.doc-table td {
  color: var(--text2);
}

/* Inside a table, commands may wrap — hiding the Guards column behind a
   scroll would cost more than a broken chip costs. */
.doc-table code {
  white-space: normal;
}

.doc-table td strong {
  font-weight: 500;
  color: var(--text1);
}

.doc-table tbody tr:last-child td {
  border-bottom: none;
}

/* ASCII screens and diagram sources — a quiet paper frame, no gutter */
.diagram {
  background: var(--wash2);
  border-radius: 6px;
  padding: 12px 16px;
  overflow-x: auto;
}

.diagram pre {
  font-family: var(--font-mono);
  font-size: 13px;
  /* The mono's natural cell (~1.25em) — tight, terminal-true, and the
     box-drawing strokes meet across rows instead of dashing. */
  line-height: 16px;
  color: var(--text2);
  letter-spacing: 0;
}

/* ---------- theme toggle ---------- */

.theme-toggle {
  position: fixed;
  top: 16px;
  right: 16px;
  z-index: 100;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text3);
  transition: color 0.12s ease;
}

.theme-toggle::after {
  content: "";
  position: absolute;
  inset: -8px;
}

@media (hover: hover) {
  .theme-toggle:hover {
    color: var(--text2);
  }
}

.theme-toggle:active {
  color: var(--text1);
}

.theme-toggle svg {
  position: absolute;
  transition: opacity 0.2s ease, transform 0.3s var(--ease-smooth);
}

.theme-toggle .ic-waves {
  opacity: 1;
  transform: translateY(0) scale(1);
}

.theme-toggle .ic-sun {
  opacity: 0;
  transform: rotate(-90deg) scale(0.6);
}

[data-theme="dark"] .theme-toggle .ic-waves {
  opacity: 0;
  transform: translateY(4px) scale(0.6);
}

[data-theme="dark"] .theme-toggle .ic-sun {
  opacity: 1;
  transform: rotate(0deg) scale(1);
}

/* ---------- motion ---------- */

/* One signature, one set of dials. Every entrance on the site is the
   same move — rise var(--rise), unblur var(--rise-blur) — and every
   sequence walks the same var(--step) rhythm. Tune it all here. */

:root {
  --rise: 12px; /* the signature entrance distance */
  --rise-blur: 3px; /* the unblur that rides along */
  --dur-rise: 0.6s; /* one entrance duration, everywhere */
  --step: 70ms; /* the cascade rhythm */

  --vt-exit: 0.12s; /* old page steps aside, briskly */
  --vt-lag: 40ms; /* the beat before the new page speaks */
  --vt-fade: 0.18s; /* new page turns opaque */

  --caret-slide: 0.3s; /* the rail caret's glide between rows */
}

/* ─────────────────────────────────────────────────────────
 * ENTRANCE CASCADE (each page, on load — pure CSS, no JS)
 *
 *     0ms   crumbs / h1 fade up, 12px → 0, unblur
 *    70ms   title / meta
 *   140ms   lead section
 *   210ms   first doc section (then 70ms apart, capped at 350ms)
 *   420ms   doc footer
 *
 * Below-fold landing sections instead hide once JS arrives and
 * rise on scroll (.rise). Arriving from inside the site, the
 * cascade stands down — the transition is the entrance.
 * ───────────────────────────────────────────────────────── */

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(var(--rise));
    filter: blur(var(--rise-blur));
  }
  to {
    opacity: 1;
    transform: translateY(0);
    filter: blur(0);
  }
}

.intro {
  animation: fade-up var(--dur-rise) var(--ease-smooth) both;
}

.intro-2 {
  animation-delay: var(--step);
}

.intro-3 {
  animation-delay: calc(2 * var(--step));
}

/* Doc sections continue the header's rhythm one by one — never as a
   single slab — and the stagger caps once the fold is surely past, so
   nothing keeps animating for a reader who can't see it. */
.doc-body > .section {
  animation: fade-up var(--dur-rise) var(--ease-smooth) both;
  animation-delay: calc(3 * var(--step));
}

.doc-body > .section:nth-child(2) {
  animation-delay: calc(4 * var(--step));
}

.doc-body > .section:nth-child(n + 3) {
  animation-delay: calc(5 * var(--step));
}

.doc footer {
  animation: fade-up var(--dur-rise) var(--ease-smooth) both;
  animation-delay: calc(6 * var(--step));
}

/* The cascade greets first arrivals only. Coming from inside the site —
   through a view transition (.vt-in) or a plain same-origin navigation
   (.nav-internal) — content simply takes its place. */
.vt-in :is(.intro, .doc-body > .section, .doc footer),
.nav-internal :is(.intro, .doc-body > .section, .doc footer) {
  animation: none;
}

/* ─────────────────────────────────────────────────────────
 * PAGE TRANSITION (cross-document view transitions)
 *
 *     0ms   old page fades out (120ms)
 *    40ms   new page fades in (180ms)
 *   220ms   settled
 *
 * One quiet move: a staged cross-fade. Nothing translates,
 * nothing morphs, nothing changes size — navigation reads as
 * a change of content, not a change of place. The stage (out,
 * then in) keeps misaligned prose from double-exposing, and
 * pinned chrome — the veil, the theme toggle, both rails —
 * holds perfectly still while the pages change hands.
 * Browsers without support simply navigate.
 * ───────────────────────────────────────────────────────── */

@view-transition {
  navigation: auto;
}

/* Pinned chrome — fixed at identical positions on both sides, so their
   snapshot groups hold perfectly still while the pages change hands. */
.veil {
  view-transition-name: veil;
}

.theme-toggle {
  view-transition-name: theme-toggle;
}

.aside {
  view-transition-name: aside;
}

.aside.toc {
  view-transition-name: toc;
}

@keyframes vt-fade-in {
  from {
    opacity: 0;
  }
}

@keyframes vt-fade-out {
  to {
    opacity: 0;
  }
}

::view-transition-old(root) {
  animation: vt-fade-out var(--vt-exit) ease both;
}

::view-transition-new(root) {
  animation: vt-fade-in var(--vt-fade) ease-out var(--vt-lag) both;
}

/* The rails never box-morph — the default would stretch two
   different-height lists across each other. Snapshots keep their natural
   size, anchor top-left, and cross-fade in place: entries the two pages
   share sit perfectly still, changed ones dissolve. Aligned rows can't
   double-expose, so this fade overlaps rather than stages. */
::view-transition-group(aside),
::view-transition-group(toc) {
  animation: none;
}

::view-transition-old(aside),
::view-transition-new(aside),
::view-transition-old(toc),
::view-transition-new(toc) {
  height: auto;
  object-fit: none;
  object-position: top left;
}

::view-transition-old(aside),
::view-transition-old(toc) {
  animation: vt-fade-out var(--vt-fade) ease both;
}

::view-transition-new(aside),
::view-transition-new(toc) {
  animation: vt-fade-in var(--vt-fade) ease both;
}

/* The caret is the one element that morphs: the same 2×14 bar on both
   sides, so its group animation is a pure vertical glide to the clicked
   doc, on the site's expo-out. */
::view-transition-group(nav-caret) {
  animation-duration: var(--caret-slide);
  animation-timing-function: var(--ease-smooth);
}

/* sections — hidden by JS only after load, revealed on view */

.rise {
  opacity: 0;
  transform: translateY(var(--rise));
  filter: blur(var(--rise-blur));
}

.rise.in {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
  transition: opacity var(--dur-rise) var(--ease-smooth),
    transform var(--dur-rise) var(--ease-smooth),
    filter var(--dur-rise) var(--ease-smooth);
}

/* spacing when media sits between prose */

.section > :is(.codeblock, .figure, .filmstrip-container, .glyphs, .keys, .diagram, .table-scroll) + :is(p, ul, ol),
.section > :is(p, ul, ol) + :is(.codeblock, .figure, .filmstrip-container, .glyphs, .keys, .diagram, .table-scroll) {
  margin-top: 0;
}

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

@media (max-width: 900px) {
  .aside {
    display: none;
  }
}

@media (max-width: 768px) {
  .veil {
    display: none;
  }

  .filmstrip {
    flex-direction: column;
    overflow-x: visible;
    cursor: default;
  }

  .slide {
    width: 100%;
  }

  .film-bar {
    display: none;
  }
}

@media (max-width: 520px) {
  .article p,
  .article li {
    hyphens: auto;
  }

  .keys {
    gap: 8px 16px;
  }
}

/* ---------- reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .intro,
  .doc-body > .section,
  .doc footer {
    animation: none;
  }

  /* The page transition already carries no motion — pure opacity — so
     it plays as designed. The caret still marks the row but stops
     gliding: it snaps in place and rides the rail's cross-fade. */
  .rail-caret.is-live {
    transition: none;
  }

  .aside:not(.toc) .rail-caret {
    view-transition-name: none;
  }

  .rise {
    transform: none;
    filter: none;
  }

  .rise.in {
    transition: opacity 0.3s ease;
  }

  .theme-toggle svg {
    transition: opacity 0.2s ease;
  }

  .theme-toggle .ic-waves,
  .theme-toggle .ic-sun {
    transform: none;
  }

  /* The lightbox keeps its fade but loses the rise and unblur. */
  .lb-shot {
    transition: none;
  }

  @starting-style {
    .lb-shot {
      transform: none;
      filter: none;
    }
  }
}
