/* ───────────────────────────────────────────────────────────────
   soft syntax — the design kernel for rathan.me

   A warm, pastel, handmade take on code-as-structure. The metaphor is a
   living computational notebook (Quarto/tidyverse), never a dark IDE.

   Opt-in: any piece may use these tokens, override them, or ignore the
   file entirely. Readability is the veto rule — wobble and tilt belong on
   tiles and accents, never on running prose.
   ─────────────────────────────────────────────────────────────── */

/* ═══════════════════════════════════════════════════════════════════════
   RECURSIVE, SELF-HOSTED — 2026-08-08. Replaces a render-blocking
   third-party stylesheet from fonts.googleapis.com on every page.

   161.2 KB against the 302.5 KB Google actually serves for latin, and it
   ADDS seven characters the site renders that Google's latin subset does
   not contain: → ← − ≥ ✓ ▶ ◀. Those were silently falling back to a
   system font mid-sentence.

   Regenerate exactly (needs nothing in the repo — one-off commands):

     python3 -m venv /tmp/ft && /tmp/ft/bin/pip install fonttools brotli
     curl -L -o /tmp/Recursive.ttf \
       "https://raw.githubusercontent.com/google/fonts/main/ofl/recursive/Recursive%5BCASL%2CCRSV%2CMONO%2Cslnt%2Cwght%5D.ttf"
     /tmp/ft/bin/fonttools varLib.instancer /tmp/Recursive.ttf CRSV=0 wght=300:700 -o /tmp/Rec-trim.ttf
     /tmp/ft/bin/pyftsubset /tmp/Rec-trim.ttf \
       --unicodes="U+0020-007E,U+00A0-00FF,U+2000-206F,U+20AC,U+2122,U+2190-2193,U+2212,U+2215,U+221A,U+2248,U+2260,U+2264-2265,U+25B6,U+25C0,U+2713,U+FEFF,U+FFFD" \
       --flavor=woff2 --output-file=public/styles/fonts/recursive-v1.woff2

   CRSV is pinned to 0 because nothing here uses the cursive axis. wght is
   clamped to 300–700 because 700 is the heaviest weight in the whole site
   (`.pipe`). MONO, CASL and slnt are all live and must stay.

   Latin Extended-A was measured and left out: it costs 26 KB and no
   character on the site needs it. If a name ever does, that one character
   falls back and the rest of the word does not — an acceptable trade at
   26 KB, but a real one, so it is written down.

   The version is in the FILENAME, not a query string, because _headers
   caches this immutably for a year. To change the subset, bump to -v2.
   Licence: SIL OFL 1.1, alongside at OFL-Recursive.txt (OFL §2 requires
   the notice to travel with a redistributed copy, which self-hosting makes
   us). Credited on /debts/.
   ═══════════════════════════════════════════════════════════════════════ */
@font-face {
  font-family: "Recursive";
  src: url("/styles/fonts/recursive-v1.woff2") format("woff2");
  font-weight: 300 700;                /* must match the clamped wght range */
  font-style: oblique 0deg 15deg;      /* declares slnt, so font-style still selects it */
  font-display: swap;
}

/* ═══════════════════════════════════════════════════════════════════════
   VIDEO COORDINATION LAYER — added 2026-08-06.  ⟲ FULLY REVERSIBLE.

   Sampled from Rathan's test frame (see content/PALETTE-2026-08-06.md).
   The room is warm greige with one saturated element — a rust chair at
   #ad552f, 73% saturation against nothing else above 37%. The old palette
   was cool and green-cast, so the video would have read as pasted in.

   To roll back, restore these eight values and nothing else:
       --bg       #f5f7f0        --rule    #dfe4d6
       --bg-tint  #eef2e8        --ink     #1e2b26
       --card     #fffdf7        --pipe    #ca3722
       --coral    #ff6b5e        --num     #9a6410
   Everything else in this file is untouched. A copy of the original is at
   content/PALETTE-2026-08-06.md.
   ═══════════════════════════════════════════════════════════════════════ */

:root {
  /* surfaces — warmed off the green cast toward the room's greige walls
     (sampled #a59386 / #a79990, hue ~22°) */
  --bg:        #f4f1ea;   /* was #f5f7f0 */
  --bg-tint:   #ece7dd;   /* was #eef2e8 */
  --card:      #fffcf5;   /* was #fffdf7 */
  --rule:      #e2dbd0;   /* was #dfe4d6 */

  /* text — near-black warmed just off green so the dark rail sits beside
     the video without reading as a different room */
  --ink:       #232420;   /* was #1e2b26 */
  --ink-soft:  #55655c;
  /* was #74837a — measured 3.69:1 on --bg and 3.92:1 on --card, i.e. the one
     token in here that broke this file's own ≥4.5:1 contract. It gets used for
     real label text (units, field labels, section eyebrows), not just decoration,
     so it was darkened rather than quarantined. Now 4.89:1 / 5.20:1. */
  --ink-faint: #616f66;

  /* syntax roles — all checked for contrast on --bg */
  --fn:      #5f4bc4;  /* function names        */
  --pipe:    #ab4d27;  /* was #ca3722. The chair's rust, deepened until it
                          clears 4.5:1 on the new warmer --bg. Same hue family
                          as the sampled #ad552f, so the accent and the video's
                          only saturated object are the same colour. */
  /* was #0b7d70, which measured 4.45:1 on --bg and 4.08:1 on --bg-tint — the
     one remaining token breaking this file's own ≥4.5:1 contract, caught by
     re-measuring the whole token grid on 2026-08-08 rather than by looking.
     It carries real text (the lesson's marginal-revenue tag, the "ready when
     you are" note, `.str` in the archive dialect), so it gets darkened rather
     than quarantined. Now 5.01 / 5.52 / 4.59 on bg / card / bg-tint, and 5.52
     as a chart fill on --card against the 3:1 that SC 1.4.11 asks of graphics.
     Same shape of fix as --num on 2026-08-06: a palette shift moved a
     background and a colour that had nothing to do with it quietly fell below
     the line. */
  --str:     #0a7469;  /* "strings"             */
  --num:     #935e0e;  /* numbers — was #9a6410, which measured 4.62:1 on the old
                          cooler --bg but fell to 4.43:1 on the warmer one. */
  --cmt:     #5c6a60;  /* # comments — they carry real content, so they
                          clear 4.5:1 rather than sitting decoratively faint */
  --op:      #55655c;  /* == <- infix           */

  /* accents for tiles + highlights */
  --coral:   #e2703c;  /* was #ff6b5e — the pink lean fought the rust chair.
                          Now a warm orange; still clears 4.5:1 on --ink, which
                          it must for the lesson's "now" concept state. */
  --teal:    #12a594;
  --yellow:  #ffcf4a;
  --purple:  #8a7bff;
  --sky:     #5eb8e8;
  --rose:    #f48fb1;

  /* Two families, one typeface. Recursive's MONO axis lets the same font be
     gridded code or proportional prose — but font-variation-settings CANNOT
     make a FALLBACK font proportional (the spec: variation settings do not
     affect font selection where a fallback is used). So a page setting
     MONO 0 and falling back to `monospace` renders its prose 20.6% wider
     during every font swap, and each paragraph gains a line.
     Use --mono where the text is code, --text where it is prose. */
  --mono: "Recursive", ui-monospace, "SF Mono", Menlo, monospace;
  --text: "Recursive", "Helvetica Neue", Helvetica, Arial, sans-serif;

  /* --hand (Caveat) is deliberately NOT here. It is a second typeface used by
     exactly two headings, on /debts/ and /404 — and a `--hand` token sitting
     in the kernel that / and /lesson/ both load is a loaded gun: any future
     class="hand" on the storefront would render in Apple Chancery. Those two
     pages declare it locally, next to the only two words that use it. */

  --fold: cubic-bezier(.2, .8, .3, 1);
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--mono);
  /* MONO 1 = monospaced, CASL .6 = warm/casual letterforms. The whole
     concept in one declaration: machine grid, human hand. */
  font-variation-settings: "MONO" 1, "CASL" 0.6, "slnt" 0;
  font-size: clamp(0.95rem, 0.9rem + 0.35vw, 1.08rem);
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

::selection { background: var(--yellow); color: var(--ink); }

a { color: inherit; text-decoration-color: var(--coral); text-underline-offset: 3px; }
a:hover { color: var(--pipe); }

/* ── syntax tokens ─────────────────────────────────────────── */
.fn   { color: var(--fn); }
.pipe { color: var(--pipe); font-variation-settings: "MONO" 1, "CASL" 0.6, "wght" 700; }
.str  { color: var(--str); }
.num  { color: var(--num); }
.op   { color: var(--op); }
.var  { color: var(--ink); }
.kw   { color: var(--fn); font-variation-settings: "MONO" 1, "CASL" 0.6, "wght" 600; }

.cmt {
  color: var(--cmt);
  font-variation-settings: "MONO" 1, "CASL" 1, "slnt" -8;
}

/* console output, as R prints it */
.out {
  color: var(--ink-soft);
  font-variation-settings: "MONO" 1, "CASL" 0.3;
}
.out .prompt { color: var(--ink-faint); }

/* ── code blocks ───────────────────────────────────────────────
   .code  — a <pre>, for console output with real newlines
   .cells — a <div> of .line children, for authored source. Avoids
            <pre> whitespace traps and lets each line be a block
            (so it can animate and hold real elements).            */
.code {
  margin: 0;
  white-space: pre-wrap;
  tab-size: 2;
}
.cells { margin: 0; }
.cells .line { display: block; min-height: 1.7em; }
.cells .spacer { height: 1.1em; }
.indent { padding-left: 2ch; }

/* ── interactive tokens: the code IS the control ───────────── */
.poke {
  /* it's a <button>, but it must read as code — strip all native chrome */
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  font: inherit;
  font-variation-settings: inherit;
  color: inherit;
  line-height: inherit;
  cursor: pointer;
  border-radius: 5px;
  padding: 0 3px;
  margin: 0 -3px;
  border-bottom: 1.5px dotted currentColor;
  transition: background 140ms ease, transform 140ms var(--fold);
  position: relative;
}
.poke:hover, .poke:focus-visible {
  background: color-mix(in srgb, var(--yellow) 55%, transparent);
}
/* Keep the real focus ring. A yellow wash alone measured a 1.11:1 change
   against the unfocused state — under both the 3:1 that SC 1.4.11 wants for a
   focus indicator and the visibility SC 2.4.7 requires. --ink gives 13.8:1 on
   --bg, 15.2:1 on --card and 12.7:1 on --bg-tint, so one declaration covers
   every surface in the kernel. WCAG technique F78 names both criteria for
   exactly the `outline: none` construction this replaces. */
:where(.poke, .tile, .chip):focus-visible,
button:focus-visible, a:focus-visible, summary:focus-visible,
input:focus-visible, textarea:focus-visible, select:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}
.poke:active { transform: translateY(1px) scale(.97); }
.poke.just-changed { animation: pop 320ms var(--fold); }

@keyframes pop {
  0%   { background: color-mix(in srgb, var(--yellow) 90%, transparent); }
  100% { background: transparent; }
}

/* ── foldable block: "one legible surface, optional descent" ─ */
.fold > summary {
  cursor: pointer;
  list-style: none;
  color: var(--cmt);
  font-variation-settings: "MONO" 1, "CASL" 1, "slnt" -8;
  display: inline-flex;
  align-items: baseline;
  gap: .5ch;
  border-radius: 5px;
  padding: 0 4px;
  margin: 0 -4px;
}
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary:hover { background: color-mix(in srgb, var(--yellow) 45%, transparent); }
.fold > summary .caret { transition: transform 180ms var(--fold); display: inline-block; }
.fold[open] > summary .caret { transform: rotate(90deg); }
.fold .body {
  margin: .5rem 0 .5rem 2ch;
  padding-left: 1.2ch;
  border-left: 2px solid var(--rule);
  color: var(--ink-soft);
  animation: unfold 260ms var(--fold);
}
@keyframes unfold {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/* ── tiles: pokeable, hand-tilted ──────────────────────────── */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 1.15rem;
  margin: 1.2rem 0 0;
  padding: 0;
  list-style: none;
}

.tile {
  --accent: var(--coral);
  background: var(--card);
  border: 2px solid var(--ink);
  border-radius: 14px;
  padding: .95rem 1rem 1.05rem;
  box-shadow: 3px 3px 0 color-mix(in srgb, var(--ink) 14%, transparent);
  transform: rotate(var(--tilt, 0deg));
  transition: transform 220ms var(--fold), box-shadow 220ms var(--fold);
  position: relative;
  text-align: left;
  width: 100%;
  font: inherit;
  color: inherit;
  cursor: pointer;
  display: block;
  animation: dealIn 420ms var(--fold) backwards;
}
.tile:hover, .tile:focus-visible {
  transform: rotate(0deg) translateY(-4px);
  box-shadow: 6px 8px 0 var(--accent);
}
.tile[aria-expanded="true"] { transform: rotate(0deg); box-shadow: 5px 6px 0 var(--accent); }

@keyframes dealIn {
  from { opacity: 0; transform: rotate(var(--tilt, 0deg)) translateY(14px) scale(.96); }
  to   { opacity: 1; }
}

.tile .dot {
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--accent);
  display: inline-block;
  margin-bottom: .45rem;
}
.tile .topic {
  display: block;
  font-variation-settings: "MONO" 1, "CASL" 0.6, "wght" 650;
  line-height: 1.35;
}
.tile .meta {
  color: var(--ink-faint);
  font-size: .78em;
  margin-top: .35rem;
  display: flex;
  gap: .8ch;
  flex-wrap: wrap;
}
.tile .note {
  display: none;
  margin: .6rem 0 0;
  padding-top: .55rem;
  border-top: 1.5px dashed var(--rule);
  color: var(--cmt);
  font-variation-settings: "MONO" 1, "CASL" 1, "slnt" -8;
  font-size: .9em;
  line-height: 1.5;
}
.tile[aria-expanded="true"] .note { display: block; animation: unfold 240ms var(--fold); }

/* ── running prose ─────────────────────────────────────────────
   Recursive's MONO axis lets paragraphs go proportional while code stays
   on the mono grid — one typeface, two jobs. Monospace is charming for
   six words and punishing for six sentences, and readability wins. */
.prose {
  font-family: var(--text);          /* see the note on --mono / --text above */
  font-variation-settings: "MONO" 0, "CASL" 0.5, "slnt" 0;
  line-height: 1.65;
}
.fold .body .prose { margin: .2rem 0 .7rem; }
.fold .body .prose:last-child { margin-bottom: .2rem; }

/* ── misc ──────────────────────────────────────────────────── */
.cursor {
  display: inline-block;
  width: .55ch;
  height: 1.1em;
  background: var(--pipe);
  vertical-align: -.18em;
  animation: blink 1.15s steps(2, start) infinite;
}
@keyframes blink { 50% { opacity: 0; } }

.chip {
  display: inline-block;
  background: var(--card);
  border: 1.5px solid var(--ink);
  border-radius: 999px;
  padding: .12rem .7rem;
  font-size: .8em;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .tile { transform: none !important; }
}
