/* centina.app — design system
   Single 594px column on the app's own canvas ground. The site is built
   from the app's literal materials — styles/app.css in the centina repo
   is the source of truth for every value here. Light is the default;
   [data-theme=dark] maps the same anatomy onto the night palette. */

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

/* Light is the app itself: ink on the warm greige canvas, white chrome
   cards with hairline rings, the one steel-blue accent. */
:root {
  --bg: #f1f1ef; /* the canvas ground */
  --surface: #ffffff; /* chrome — cards, buttons, key chips */
  --panel: #f7f6f4; /* quiet wells — code, diagrams, glyph strips */
  --border: #e4e3e1; /* the chrome hairline */
  --divider: #eae8e5; /* row rules inside surfaces */
  --field-border: rgba(27, 27, 26, 0.08);
  --hover: rgba(27, 27, 26, 0.05); /* the neutral hover wash */
  --well: #eceae7; /* recessed tracks — the film bar */
  --thumb: #c9c6c1;

  --text1: #373530; /* ink — headings, names, working bold */
  --text2: #494743; /* prose, half a register off the ink */
  --text3: #787774; /* secondary — meta, labels, captions */

  --accent: #337ea9; /* the app's selection blue */
  --amber: #d9730d;
  --steel: #787774;
  --err: #e03e3e;

  --code-bg: #f7f6f4;
  --code-border: #e4e3e1;
  --code-fg: #373530;
  --code-dim: #9c9a96; /* gutter, comments, resting copy */
  --code-str: #337ea9; /* strings take the accent */

  --selection-bg: rgba(51, 126, 169, 0.25);
  --focus: rgba(51, 126, 169, 0.55);

  --shadow-card: 0 1px 2px rgba(27, 27, 26, 0.04);
  --shadow-card-hover: 0 1px 2px rgba(27, 27, 26, 0.05),
    0 8px 22px rgba(27, 27, 26, 0.09);
  --shadow-float: 0 16px 48px -12px rgba(27, 27, 26, 0.25);

  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica,
    Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, monospace;

  /* The app's easings: expo-out for everything that moves, the spring
     only for the mark's easter egg. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-snappy: cubic-bezier(0.175, 0.885, 0.32, 1.1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.5, 1);
}

/* Dark maps the anatomy onto the night palette: same greige warmth,
   inverted — surfaces lift by lightness instead of shadow. */
[data-theme="dark"] {
  --bg: #2f3437;
  --surface: #373c3f;
  --panel: #33383b;
  --border: rgba(255, 255, 255, 0.09);
  --divider: rgba(255, 255, 255, 0.06);
  --field-border: rgba(255, 255, 255, 0.09);
  --hover: rgba(255, 255, 255, 0.055);
  --well: #3a4043;
  --thumb: #52585c;

  --text1: rgba(255, 255, 255, 0.92);
  --text2: rgba(255, 255, 255, 0.76);
  --text3: rgba(151, 154, 155, 0.95);

  --accent: #529cca;
  --amber: #ffa344;
  --steel: #979a9b;
  --err: #ff7369;

  --code-bg: #33383b;
  --code-border: rgba(255, 255, 255, 0.08);
  --code-fg: rgba(255, 255, 255, 0.88);
  --code-dim: rgba(151, 154, 155, 0.8);
  --code-str: #529cca;

  --selection-bg: rgba(82, 156, 202, 0.35);
  --focus: rgba(82, 156, 202, 0.6);

  /* Shadow barely carries on a dark ground — the border does the work. */
  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.2);
  --shadow-card-hover: 0 1px 2px rgba(0, 0, 0, 0.25),
    0 8px 22px rgba(0, 0, 0, 0.35);
  --shadow-float: 0 16px 48px -12px rgba(0, 0, 0, 0.5);
}

/* ---------- 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;
  /* iOS Safari font boosting would inflate single lines inside wide
     scrollable code blocks; type is already sized for small screens. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text1);
  font-family: var(--font-sans);
  font-size: 15px;
  /* 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;
  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 {
  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,
    #f1f1ef,
    rgba(241, 241, 239, 0.738) 19%,
    rgba(241, 241, 239, 0.541) 34%,
    rgba(241, 241, 239, 0.382) 47%,
    rgba(241, 241, 239, 0.278) 56.5%,
    rgba(241, 241, 239, 0.194) 65%,
    rgba(241, 241, 239, 0.126) 73%,
    rgba(241, 241, 239, 0.075) 80.2%,
    rgba(241, 241, 239, 0.042) 86.1%,
    rgba(241, 241, 239, 0.021) 91%,
    rgba(241, 241, 239, 0.008) 95.2%,
    rgba(241, 241, 239, 0.002) 98.2%,
    rgba(241, 241, 239, 0)
  );
  opacity: 1;
}

.veil::after {
  background: linear-gradient(
    180deg,
    #2f3437,
    rgba(47, 52, 55, 0.738) 19%,
    rgba(47, 52, 55, 0.541) 34%,
    rgba(47, 52, 55, 0.382) 47%,
    rgba(47, 52, 55, 0.278) 56.5%,
    rgba(47, 52, 55, 0.194) 65%,
    rgba(47, 52, 55, 0.126) 73%,
    rgba(47, 52, 55, 0.075) 80.2%,
    rgba(47, 52, 55, 0.042) 86.1%,
    rgba(47, 52, 55, 0.021) 91%,
    rgba(47, 52, 55, 0.008) 95.2%,
    rgba(47, 52, 55, 0.002) 98.2%,
    rgba(47, 52, 55, 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(--text3);
  transition: color 150ms var(--ease-out);
}

@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 — the app masthead's 24 — so navigation
   never changes it. Always in the accent: the one point of brand color
   on the sheet, exactly as the app's shelf carries it. */
.logo-mark {
  width: 24px;
  height: 24px;
  display: inline-block;
  flex: none;
  color: var(--accent);
}

.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: 13px;
  color: var(--text3);
  text-decoration: none;
  transition: color 150ms var(--ease-out);
  /* 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);
  }
}

/* Site-level lists (the docs nav, the landing's site links) sit one
   register up from the on-page spy lists — a touch of weight now that
   the system face can afford it, so the rail's two levels never blend. */
.aside :is(.docs-nav, .site) a {
  color: var(--text2);
  font-weight: 500;
}

/* 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 ---------- */

/* The app's selection bar for the rails: a 2×14 accent 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(--accent);
}

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

/* 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;
}

/* The masthead is the app's home lockup, verbatim: the 24px arch in
   accent, the wordmark at 15/650 beside it, 9px apart. */
.article h1 {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 15px;
  font-weight: 650;
  line-height: 24px;
  letter-spacing: 0.01em;
  color: var(--text1);
}

/* The shelf's secret, kept: pointing at the mark turns the arch its
   counterclockwise quarter. Not a control — just an answer. */
.article h1 .logo-mark {
  transition: transform 380ms var(--ease-spring);
}

@media (hover: hover) {
  .article h1:hover .logo-mark {
    transform: rotate(-90deg);
  }
}

/* The hocket byline shares the masthead row in the crumbs' quiet voice,
   a beat wider than the lockup's own 9px gap; it sits beside the h1 —
   not inside — so hovering it doesn't turn the arch. */
.masthead {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.byline {
  font-size: 13px;
  line-height: 24px;
  color: var(--text3);
  text-decoration: none;
  transition: color 150ms var(--ease-out);
}

/* The mark carries its own ink in both themes; img is block site-wide. */
.byline .hocket-mark {
  display: inline-block;
  width: 15px;
  height: auto;
  vertical-align: -2.5px;
  margin: 0 1px;
}

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

/* Doc titles speak over the wordmark register — the one place type
   grows past the row. Back to block: the landing h1's flex is for the
   logo lockup, but here the branded .name span would become a flex item
   and the 9px gap would swallow the word space. */
.doc h1 {
  display: block;
  font-size: 20px;
  letter-spacing: -0.01em;
}

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

/* A headerless section carries its anchor id itself; same landing
   offset as the h2s. */
.section[id] {
  scroll-margin-top: 6rem;
}

/* Hierarchy by weight now, the app's way: a 400/550/650 ladder in the
   same ink, secondary matter in the gray. */
.section h2 {
  font-size: 15px;
  font-weight: 650;
  line-height: 24px;
  color: var(--text1);
  scroll-margin-top: 6rem;
  text-wrap: balance;
}

.section h3 {
  font-size: 15px;
  font-weight: 550;
  line-height: 24px;
  color: var(--text1);
  text-wrap: balance;
}

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

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

.article em {
  font-style: italic;
  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 — key/value at the app's field size */

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

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

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

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

.article code {
  font-family: var(--font-mono);
  font-size: 13px;
  background: var(--hover);
  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 working-bold weight in
   the primary ink — a register shift, not a chip. Name.tsx applies it
   on the landing page, markdown.ts in the docs. */
.name {
  font-weight: 550;
  color: var(--text1);
}

/* Inside headings the name rides the heading's own register — the
   550 working-bold only means emphasis against 400 prose; against a
   650 title it reads as a demotion. */
h1 .name,
h2 .name,
h3 .name {
  font-weight: inherit;
}

/* Key chips are the app's control idiom in miniature: a white chrome
   well with the field's hairline ring. */
kbd {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1;
  color: var(--text2);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--field-border), 0 1px 0 var(--field-border);
  border-radius: 4px;
  /* A 19px chip: 11px chord + 4/4 split, the extra shadow px below
     seating the key on its surface. */
  padding: 4px 6px;
  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);
  font-size: 13px;
}

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

.chip-link {
  color: var(--accent);
  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-out);
}

@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 ---------- */

/* The app's raised white well — home-action at button scale: hairline
   ring, soft shadow, the hover lift. */
.cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border-radius: 8px;
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--border), var(--shadow-card);
  color: var(--text1);
  font-size: 13px;
  font-weight: 550;
  line-height: 16px; /* 8 + 16 + 8 — a 32px button */
  text-decoration: none;
  width: fit-content;
  transition: box-shadow 150ms var(--ease-out), transform 150ms var(--ease-out);
}

@media (hover: hover) {
  .cta:hover {
    box-shadow: inset 0 0 0 1px var(--border), var(--shadow-card-hover);
    transform: translateY(-1px);
  }
}

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

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

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

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

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

/* A quiet panel well, the app's own: greige ground, hairline ring, ink
   text with strings in the accent. */
.codeblock {
  position: relative;
  background: var(--code-bg);
  border-radius: 8px;
  box-shadow: inset 0 0 0 1px var(--code-border);
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 20px;
  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(--code-dim);
  text-align: right;
  padding-right: 0.75rem;
  user-select: none;
  font-variant-numeric: tabular-nums;
}

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

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

/* An opaque cap over the first code line's tail: a long line scrolls
   under the copy button instead of through it, fading as it goes. Caps
   only the first line's band — lines below pass to the edge untouched. */
.codeblock .copy-well {
  position: absolute;
  top: 0;
  right: 0;
  height: 32px; /* pre's top padding + one line — flush with the button */
  padding: 8px 8px 0 16px;
  background: linear-gradient(to right, transparent, var(--code-bg) 16px);
  pointer-events: none;
}

.codeblock .copy {
  position: relative;
  pointer-events: auto;
  width: 24px; /* the theme toggle's square */
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--code-dim);
  border-radius: 4px;
  transition: background-color 150ms var(--ease-out), color 150ms var(--ease-out),
    transform 0.15s var(--ease-snappy);
}

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

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

.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(--accent);
}

.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(--panel);
  box-shadow: inset 0 0 0 1px var(--divider);
  border-radius: 8px;
  font-family: var(--font-mono);
  font-size: 12.5px;
  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);
}

/* The MCP tool strip wears the glyph vocabulary one tool per row —
   a legend read top to bottom, like the terminal it summarizes. */
.glyphs.tools {
  flex-direction: column;
  gap: 4px;
}

.glyphs.tools .l {
  font-family: var(--font-sans);
  font-size: 15px;
}

/* ---------- 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;
}

/* Each shot is a home-card: white chrome, hairline ring, the soft
   shadow, and the app's hover lift as the zoom affordance. */
.slide .shot {
  aspect-ratio: 1512 / 823; /* the shots' window ratio — others mount on the plate */
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface);
  box-shadow: var(--shadow-card);
  cursor: zoom-in;
  transition: box-shadow 150ms var(--ease-out), transform 150ms var(--ease-out);
}

@media (hover: hover) {
  .slide .shot:hover {
    box-shadow: var(--shadow-card-hover);
    transform: translateY(-1px);
  }
}

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

.slide .shot img {
  display: block;
  width: 100%;
  height: 100%;
  /* The shots vary a little in ratio; contain mounts each whole on the
     white plate, rather than cropping its chrome away. */
  object-fit: contain;
}

.slide figcaption {
  font-size: 12px;
  line-height: 20px;
  color: var(--text3);
  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-out);
}

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

.lb-shot {
  width: min(1280px, 100vw - 48px, (100dvh - 176px) * (1512 / 823));
  aspect-ratio: 1512 / 823;
  position: relative;
  border-radius: 12px;
  overflow: hidden;
  background-color: var(--surface);
  background-size: contain; /* inline thumb holds the frame while -full loads */
  background-position: center;
  background-repeat: no-repeat;
  box-shadow: var(--shadow-float);
  transform: translateY(0);
  filter: blur(0);
  transition: transform 0.22s var(--ease-out), filter 0.22s var(--ease-out);
}

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

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

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

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

.lb-cap {
  color: var(--text2);
  font-size: 13px;
  line-height: 20px;
}

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

@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(--well);
  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;
}

/* ---------- feature shots ---------- */

/* Editor clips mounted as plates: the canvas ground arrives inside the
   image, so the card ring and shadow do the framing. */
.feature-shot {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
}

.feature-shot img {
  height: auto;
  border-radius: 10px;
  box-shadow: 0 0 0 1px var(--border), var(--shadow-card);
}

.feature-shot figcaption {
  font-size: 12px;
  line-height: 20px;
  color: var(--text3);
  text-align: center;
}

/* ---------- 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: 10px;
  overflow: hidden;
  background: var(--panel);
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}

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

.figure figcaption {
  text-align: center;
  color: var(--text3);
  font-size: 13px;
}

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

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

.article > footer p {
  color: var(--text3);
  font-size: 13px;
}

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

@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;
  font-size: 13px;
  color: var(--text3);
}

.article .crumbs a {
  color: var(--text3);
  text-decoration: none;
  transition: color 150ms var(--ease-out);
}

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

/* The wordmark keeps the masthead lockup in the crumbs, same as the h1.
   Three classes deep so the ink survives `.article .crumbs a`'s gray. */
.article .crumbs .crumb-home {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-size: 15px;
  font-weight: 650;
  letter-spacing: 0.01em;
  color: var(--text1);
}

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

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

/* prose links — the app's accent, with a quiet underline that lifts.
   Unclassed anchors only: .cta and .chip-link carry their own idioms. */
.article .section a:not([class]) {
  color: var(--accent);
  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 150ms var(--ease-out);
}

@media (hover: hover) {
  .article .section a:not([class]):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 — the app's panel-header voice on the columns,
   row rules only, nothing framed */
.table-scroll {
  overflow-x: auto;
}

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

.doc-table th {
  color: var(--text3);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: left;
}

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

.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: 550;
  color: var(--text1);
}

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

/* doc screenshots — plates mounted on the sheet. markdown.ts emits
   .doc-shot figures, wrapped in .doc-shots when two sit side by side;
   the source width attribute is the intended display width (@2x files),
   capped by the column via the img reset's max-width. */
.doc-shot {
  margin: 0;
}

.doc-shot img {
  height: auto;
  border-radius: 8px;
  background: var(--panel);
  box-shadow: 0 0 0 1px var(--border), var(--shadow-card);
}

.doc-shots {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 8px;
}

/* ASCII screens and diagram sources — a quiet panel well, no gutter */
.diagram {
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--divider);
  border-radius: 8px;
  padding: 12px 16px;
  overflow-x: auto;
}

.diagram pre {
  font-family: var(--font-mono);
  font-size: 12px;
  /* The mono's natural cell (~1.25em) — tight, terminal-true, and the
     box-drawing strokes meet across rows instead of dashing. */
  line-height: 15px;
  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 150ms var(--ease-out);
}

.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-out);
}

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

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

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

[data-theme="dark"] .theme-toggle .ic-moon {
  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-out) 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-out) 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-out) 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-out);
}

/* 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-out),
    transform var(--dur-rise) var(--ease-out),
    filter var(--dur-rise) var(--ease-out);
}

/* spacing when media sits between prose */

.section > :is(.codeblock, .figure, .feature-shot, .filmstrip-container, .glyphs, .keys, .diagram, .table-scroll) + :is(p, ul, ol),
.section > :is(p, ul, ol) + :is(.codeblock, .figure, .feature-shot, .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;
  }

  .article h1 .logo-mark {
    transition: none;
    transform: none;
  }

  .slide .shot,
  .cta {
    transition: box-shadow 150ms ease;
  }

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

  .theme-toggle .ic-sun,
  .theme-toggle .ic-moon {
    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;
    }
  }
}
