/* Words We Keep — main experience (layout + components; colors and type live in tokens.css)
   Grid: 6 columns (desktop/tablet), 4 columns (mobile), 20px margin + 20px gutter. */

:root {
  --m: 20px;
  --g: 20px;
  --cols: 6;
  --colw: calc((100vw - 2 * var(--m) - (var(--cols) - 1) * var(--g)) / var(--cols));
  --step: calc(var(--colw) + var(--g));
  --col2: calc(var(--m) + var(--step));
  --col3: calc(var(--m) + 2 * var(--step));
  --col4: calc(var(--m) + 3 * var(--step));
  --col6: calc(var(--m) + 5 * var(--step));

}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; /* no grey flash on tap (iOS) */ }

html, body { height: 100%; overflow: hidden; overscroll-behavior: none; touch-action: manipulation; /* no double-tap zoom; pinch and pan still work */ }
/* Nothing on the site selects — the quote in notes mode neither, since 2026-10-03 — except, on
   the form, the quote on the annotation sheet and the fields themselves (css/form.css). */
body { -webkit-user-select: none; user-select: none; }

body {
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
}

button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
[data-href] { cursor: pointer; } /* the site's links: buttons and spans with a data-href, not <a href> (no address strip on hover; js: follow) */
ul { list-style: none; }
[hidden] { display: none !important; }

.app {
  /* absolute, not fixed: iOS 26 Safari colours its toolbars from a position: fixed element that
     touches a viewport edge, re-sampled on its own schedule (mostly on scroll) — which left the
     bars in the wrong colour after leaving notes. With nothing fixed at the edges, Safari uses
     the body background, which js/app.js keeps in step with the page (syncChromeColor). */
  position: absolute;
  inset: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-text);
  font-size: var(--text-size);
  line-height: var(--text-leading);
  letter-spacing: var(--text-tracking);
  font-feature-settings: var(--text-features);
}

/* ---------- Shared bits ---------- */

/* Focus: no ring for mouse/touch; for Tab navigation a hairline box in the text's own colour —
   black on the page, the theme colour in notes, white on white text (Bill, 2026-10-10). A filled
   button takes its fill's colour instead (form.css: .btn--primary; .menu-apply below). */
:focus { outline: none; }
:focus-visible { outline: var(--hairline) solid currentColor; outline-offset: 3px; }

/* Text links: hairline underline that redraws left → right on hover. */
.link,
.n-source [data-href] {
  --line: var(--hairline);
  background: linear-gradient(currentColor, currentColor) left bottom / 100% var(--line) no-repeat;
  padding-bottom: var(--link-gap);
}
.link { display: inline-block; white-space: nowrap; }
@media (max-resolution: 1.5dppx) { .link, .n-source [data-href] { --line: 1px; } }
@media (hover: hover) {
  .link:hover,
  .n-source [data-href]:hover { animation: link-draw 0.7s var(--ease); }
}
@keyframes link-draw {
  0%      { background-position: right bottom; background-size: 100% var(--line); }
  45%     { background-position: right bottom; background-size: 0% var(--line); }
  45.01%  { background-position: left bottom;  background-size: 0% var(--line); }
  100%    { background-position: left bottom;  background-size: 100% var(--line); }
}

.icon {
  display: block;
  background: currentColor;
  -webkit-mask: var(--src) center / 100% 100% no-repeat;
  mask: var(--src) center / 100% 100% no-repeat;
}
/* The category symbols as still images (served by the fonts Worker, /s/: not in the public repo — workers/fonts). With no data-sym it is "All": the logo. */
.icon-mark  { --src: url(https://fonts.wordswekeep.org/s/All.svg); width: var(--mark); height: var(--mark); }
.icon-mark[data-sym="perspective"] { --src: url(https://fonts.wordswekeep.org/s/Perspective.svg); }
.icon-mark[data-sym="growth"]      { --src: url(https://fonts.wordswekeep.org/s/Growth.svg); }
.icon-mark[data-sym="drive"]       { --src: url(https://fonts.wordswekeep.org/s/Drive.svg); }
.icon-mark[data-sym="community"]   { --src: url(https://fonts.wordswekeep.org/s/Community.svg); }
.icon-mark[data-sym="romance"]     { --src: url(https://fonts.wordswekeep.org/s/Romance.svg); }
/* The symbols drawn live (js/app.js → "Symbols"): an <svg viewBox="0 0 60 60">, inked in the text colour. */
.sym { display: block; overflow: visible; }
.icon-arrow { --src: url(../assets/icons/arrow-up.svg); width: 12px; height: 12px; }
.icon-arrow--down { transform: rotate(180deg); }
.icon-asterisk   { --src: url(../assets/icons/asterisk.svg); width: 8px; height: 8px; }

/* ---------- Chrome ---------- */

.chrome { position: absolute; z-index: 20; }
.menu-btn .sym { width: var(--mark); height: var(--mark); }
.menu-btn.is-static::before { content: ""; position: absolute; inset: 0; background: currentColor; -webkit-mask: url(https://fonts.wordswekeep.org/s/All.svg) center / 100% 100% no-repeat; mask: url(https://fonts.wordswekeep.org/s/All.svg) center / 100% 100% no-repeat; } /* the still logo, until the drawn one is ready (back from the form; symbols' files missing) */
.menu-btn  { top: var(--mark-top); left: var(--mark-top); } /* the same distance from the top and the left edge */ /* centred on the caps of No. / Notes; same spot as the first category mark in notes mode */
.number    { top: calc(20px - var(--cap-offset)); left: var(--col2); white-space: nowrap; font-feature-settings: "ss01" 1, "ss07" 1, "lnum" 1, "pnum" 1; } /* caps on the mark's top edge; lining figures, proportional: tabular ones (--numeral-features) set a gap around the 1 of "16" */
/* The number rolls on a change (js/app.js → setNumber): a box one line high, the old and the next
   number stacked in it. Padding (and the same negative margin) keeps the crop off the digits' sides. */
.num { display: inline-block; vertical-align: top; height: calc(var(--text-size) * var(--text-leading)); overflow: hidden; padding: 0 2px; margin: 0 -2px; }
.num-roll { display: block; }
.num-roll > span { display: block; height: calc(var(--text-size) * var(--text-leading)); }
.notes-btn { top: calc(20px - var(--cap-offset)); right: var(--m); }
.add-btn   { left: var(--m); bottom: 20px; }
.arrows    { right: 16px; bottom: 16px; display: flex; }
/* The music player (Figma 555:563): under No. X, on the line of "Add words"; the whole of it is
   one button. Playing: the pause icon, the track's name rolling through its box with both edges
   faded (js/app.js → marquee). Paused: the play icon, the name still at its start, the right
   edge faded. */
@property --player-fade-l { syntax: '<length>'; inherits: false; initial-value: 0px; }
.player { left: var(--col2); bottom: 20px; display: flex; align-items: center; gap: 2px; padding-bottom: var(--link-gap); color: var(--fg); }
.player[hidden] { display: none; }
.player-icon { --src: url(../assets/icons/player-play.svg); width: 8px; height: 9.238px; margin: 0 5px; translate: 0 calc(2 * var(--u)); } /* centred in an 18px box, then lowered onto the middle of the name's lowercase letters (x-height; measured 2px below the line's centre, Bill 2026-10-12) */
.player.is-on .player-icon { --src: url(../assets/icons/player-pause.svg); }
.player-name {
  --edge: 8.583px;
  --player-fade-l: 0px;
  width: 60px;
  overflow: hidden;
  font-family: var(--font-text);
  font-style: italic;
  font-size: var(--text-size); /* the site's text size (Bill, 2026-10-12; Figma had 15) */
  line-height: 1.2;
  letter-spacing: -0.01em;
  font-feature-settings: "ss01" 1, "ss07" 1, "dlig" 1, "onum" 1, "pnum" 1;
  white-space: nowrap;
  -webkit-mask: linear-gradient(to right, transparent 0, #000 var(--player-fade-l), #000 calc(100% - var(--edge)), transparent 100%);
  mask: linear-gradient(to right, transparent 0, #000 var(--player-fade-l), #000 calc(100% - var(--edge)), transparent 100%);
  transition: --player-fade-l 0.6s var(--ease);
}
.player.is-on .player-name { --player-fade-l: var(--edge); }
.player-roll { display: flex; width: max-content; }
.player-roll > span { padding-right: 24px; } /* the gap before the name comes round again */
@media (max-width: 599px) {
  /* On a phone (Bill, 2026-10-12): No. X starts where notes' names do — the category names at the top
     and the names under "Words from" (20px + 2.667em, .n-indent--from). The player stays on the bottom
     row with "Add words", starting at the middle of the window (Bill, 2026-10-14; on the top bar
     with No. X until then). */
  .number { left: calc(var(--m) + 2.667em); }
  .player { left: 50%; }
}
@media (max-width: 359px) {
  .player-name { width: 40px; } /* 60 as desktop on a phone (Bill, 2026-10-14; 40 on all phones until then), but on the narrowest (320) 60 would reach into the ↑'s tap area; 40 keeps 16px clear, as 60 does at 360 */
}
.arrows button { width: 32px; height: 32px; display: grid; place-items: center; }
/* A pressed arrow rolls out of this box, and back in from the other side once the new quote has arrived (js/app.js → press). */
.arrow-crop { display: block; width: 12px; height: 12px; overflow: hidden; }

@media (max-width: 1023px) {
  .arrows { right: 12px; bottom: 12px; } /* same row as desktop; bigger touch targets, overlapping so the glyphs keep their spacing */
  .arrows button { width: 40px; height: 40px; margin-left: -8px; }
  .arrows button:first-child { margin-left: 0; }
}

/* Leaving for the form ("Add words"): everything but the logo fades out in 100ms, then the
   page changes; the form page fades its content in around the same logo (js/form.js). */
.app.is-leaving > :not(.menu-btn) { opacity: 0; transition: opacity 0.1s var(--ease); }
.app.is-arriving > .chrome:not(.menu-btn):not(.notes-btn):not(.arrows) { animation: fade-in var(--chrome-in) var(--ease) both; } /* back from the form (Notes and the arrows wait for the typing) */
.app.is-into-thoughts > .notes, .app.is-into-thoughts > .notes-btn { animation: fade-in var(--chrome-in) var(--ease) both; } /* back from the form's "Add thoughts instead": straight into notes, no typing (js/app.js → intoThoughts) */
/* While the quote types itself in (js/app.js → arrive), Notes and the arrows are held back; as
   the last letter lands they fade in like "Add words" does coming back from the form
   (js/app.js → showChrome sets .chrome-in for the length of the fade). */
.app.chrome-in .notes-btn, .app.chrome-in .arrows { animation: fade-in var(--chrome-in) var(--ease) both; transition: none; }
.app.is-typing .notes-btn, .app.is-typing .arrows { opacity: 0; pointer-events: none; }
.notes-btn, .arrows { transition: opacity 0.4s var(--ease); }

.app[data-mode="notes"]:not(.is-revealing) .menu-btn,
.app[data-mode="notes"]:not(.is-revealing) .number,
.app[data-mode="notes"]:not(.is-revealing) .add-btn,
.app[data-mode="notes"]:not(.is-revealing) .player,
.app[data-mode="notes"]:not(.is-revealing) .arrows { display: none; }

/* While the notes frame is being cropped in, the page under it is still the grey main page
   (palette held back on .app; the notes layer and Close carry their own), and the frame sits
   above the main controls so it covers them as it drops. */
.app.is-revealing { --bg: var(--paper); --fg: var(--ink); }
.app.is-revealing .notes { z-index: 25; }
.app.is-revealing .notes-btn { z-index: 26; }
.notes-btn { color: var(--fg); }
/* Leaving notes: the travelling quote, on a layer above the drop (cropped by the same edge). */
.quote-float-layer { position: absolute; inset: 0; z-index: 27; color: var(--fg); pointer-events: none; }
/* iOS crops a composited layer at its box, and an italic capital (J, f) hangs out of it to the
   left: the layer is --overhang wider on both sides (js/app.js puts the transform origin back on
   the text's corner). */
.quote-float { position: absolute; will-change: translate, scale; --overhang: 24px; --overhang-y: 0px; box-sizing: content-box; padding: var(--overhang-y) var(--overhang); margin: calc(-1 * var(--overhang-y)) calc(-1 * var(--overhang)); } /* js sets both from the quote's size (--ink-x, --ink-y) */

/* Scanner head: sits with its bottom on the dropping edge (moved by js with a transform).
   The "glow" is real, not painted on: a band above the edge blurs and brightens the content it
   has just passed over, so the text's own color smears into light. js sets the band's height
   through --band (a fraction of the 200px maximum, SCAN_BAND_MAX): it trails back to where the
   edge was a moment ago, and closes to nothing as the edge lands. A hairline marks the edge. */
.scan-edge {
  --band: 0;
  position: absolute;
  left: 0; right: 0; top: -200px;
  height: 200px;
  z-index: 28;
  pointer-events: none;
  border-bottom: 1px solid color-mix(in srgb, var(--fg) 70%, transparent);
  /* transform only — NOT opacity: hinting opacity here makes this element a "backdrop root",
     and the blur band inside it then has nothing behind it to blur (it silently does nothing). */
  will-change: transform;
}
.scan-edge::before,
.scan-edge::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 200px;
  transform: scaleY(var(--band));
  transform-origin: 50% 100%;
}
.scan-edge::before { background: linear-gradient(to bottom, transparent, color-mix(in srgb, var(--fg) var(--scan-tint), transparent)); } /* the tint */
.scan-edge::after {
  -webkit-backdrop-filter: blur(var(--scan-blur)) brightness(var(--scan-bright)) saturate(var(--scan-sat));
  backdrop-filter: blur(var(--scan-blur)) brightness(var(--scan-bright)) saturate(var(--scan-sat));
  /* Feathered on a curve down to --scan-full of the band, full strength from there to the edge (Bill,
     2026-10-12: on Romance's deep red the brightened band read as a block with a hard top; until then
     the top third, in a straight line). */
  --scan-feather: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1) calc(var(--scan-full) * 0.167), rgba(0, 0, 0, 0.35) calc(var(--scan-full) * 0.389), rgba(0, 0, 0, 0.7) calc(var(--scan-full) * 0.625), rgba(0, 0, 0, 0.92) calc(var(--scan-full) * 0.833), #000 var(--scan-full));
  -webkit-mask-image: var(--scan-feather);
  mask-image: var(--scan-feather);
}
/* The band's strength, per palette (Bill, 2026-10-12): the same brightening
   that makes Romance's red glow does little to the near-black navies and greens. The scan edge
   carries the quote's palette (data-theme, js/app.js → setMode). Until 2026-10-12, for all: blur 10px,
   brightness 1.35, saturate 1.5, tint 16%, the feather's full strength at 35% (straight). */
.scan-edge                         { --scan-full: 100%; --scan-blur: 2px; --scan-bright: 1.2; --scan-sat: 1.7; --scan-tint: 20%; }
.scan-edge[data-theme="perspective"] { --scan-full: 100%; --scan-blur: 3px; --scan-bright: 1.8; --scan-sat: 2; --scan-tint: 20%; }
.scan-edge[data-theme="growth"]      { --scan-full: 100%; --scan-blur: 7px; --scan-bright: 1.9; --scan-sat: 2.05; --scan-tint: 20%; }
.scan-edge[data-theme="drive"]       { --scan-full: 100%; --scan-blur: 5px; --scan-bright: 1.95; --scan-sat: 2.2; --scan-tint: 27%; }
.scan-edge[data-theme="community"]   { --scan-full: 100%; --scan-blur: 4px; --scan-bright: 1.8; --scan-sat: 2.15; --scan-tint: 27%; }
.scan-edge[data-theme="romance"]     { --scan-full: 100%; --scan-blur: 3px; --scan-bright: 1.2; --scan-sat: 2.1; --scan-tint: 22%; }

/* The quote's line-by-line stand-in during the scan (js/app.js → scanQuoteLines). */
.quote.quote-lines { position: absolute; margin: 0; max-width: none; pointer-events: none; white-space: nowrap; }
.quote-line { position: absolute; left: 0; top: 0; display: block; padding: var(--ink-y) var(--ink-x); margin: calc(-1 * var(--ink-y)) calc(-1 * var(--ink-x)); transform-origin: var(--ink-x) var(--ink-y); will-change: transform; } /* the padding: room for the italic overhang inside the layer (iOS crops a layer at its box); origin on the text's corner */

/* Afterglow patches (one per scanned item), behind the notes content. Opacity-only animation. */
.scan-glows { position: absolute; left: 0; top: 0; width: 100%; height: 0; z-index: 0; pointer-events: none; }
.scan-glow {
  position: absolute;
  display: block;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--fg) 55%, transparent), color-mix(in srgb, var(--fg) 18%, transparent) 60%, transparent);
  opacity: 0;
  will-change: opacity;
}


.app.is-dim .notes-btn { opacity: 0; pointer-events: none; }

/* How far a letter's ink may reach outside its box, in em of the quote's size: every copy of
   the quote that moves or fades on a layer of its own (scraps, typed letters, the notes
   stand-in's lines, the travelling quote) is padded by this much, or Safari crops the ink at
   the box. Measured on the faces (2026-09-27): Rose reaches 0.74 right, 0.50 left, 0.47 below;
   Author 0.36 below, 0.16 above; Story and Print 0.6 right. (Was 0.3 / 0.15, for an italic
   capital's overhang only.) */
:root { --ink-x: 0.8em; --ink-y: 0.5em; }

/* Mode change: the quote cut into scraps (built in js/app.js → dadaQuote). */
.dada { position: absolute; inset: 0; z-index: 15; overflow: hidden; pointer-events: none; font-feature-settings: normal; } /* js copies the quote's face onto it, features included */
.dada-scrap {
  position: absolute;
  white-space: nowrap;
  transform-origin: 0 0;
  /* Room for the letters' overhang: while a scrap fades it is a composited layer the size of
     its box, and Safari clips the glyphs at that box. The negative margin keeps the glyphs
     where they are. */
  padding: var(--ink-y) var(--ink-x);
  margin: calc(-1 * var(--ink-y)) calc(-1 * var(--ink-x));
}

/* ---------- Deck (main) ---------- */

.deck {
  position: absolute;
  inset: 0;
  overflow: hidden; /* also crops the thrown quote at the screen edge */
  touch-action: none;
}
.app[data-mode="notes"] .deck { pointer-events: none; }
.app[data-mode="notes"]:not(.is-revealing) .deck { opacity: 0; }

.track { position: absolute; inset: 0; pointer-events: none; } /* no will-change: on iOS it put the quote on its own layer, clipping the italic overhang at its left edge.
   pointer-events: a touch must land on the deck itself — iOS keeps sending a touch's events to the
   element it started on, and the quote's nodes are replaced mid-change (the swap), which stranded
   the finger (the scrub froze scattered). Only the language button takes pointer events. */
.track .lang { pointer-events: auto; }
.app.is-over-quote .deck { cursor: pointer; } /* tapping the quote opens notes (js/app.js → quoteTap) */
/* With a mouse, the cursor over the words is the magnifying glass instead, drawn by the page
   (js/app.js → makeGlass): the real one is hidden there. */
.has-glass .app.is-over-quote .deck { cursor: none; }
.glass { position: fixed; left: 0; top: 0; z-index: 2147483647; width: var(--glass-px); height: var(--glass-px); pointer-events: none; visibility: hidden; }
.glass.is-on { visibility: visible; }
.glass-in, .glass-in > div { position: absolute; inset: 0; }
/* Pressed (js: GLASS_PRESS): down quickly, back a little slower, round the hot spot. */
.glass-in { transform-origin: var(--glass-origin); transition: scale 200ms var(--ease); }
.glass.is-pressed .glass-in { scale: var(--glass-press); transition-duration: 100ms; }
@media (prefers-reduced-motion: reduce) { .glass-in { transition: none; } }
.glass svg { display: block; }
.glass-frost { -webkit-mask-size: 100% 100%; mask-size: 100% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; }
.slide { position: absolute; left: 0; width: 100%; height: 100%; }
.slide[data-pos="-1"] { top: -100%; }
.slide[data-pos="0"]  { top: 0; }
.slide[data-pos="1"]  { top: 100%; }

.slide .q-wrap {
  position: absolute;
  left: var(--col2);
  top: 50%;
  transform: translateY(-50%);
  padding-bottom: var(--main-quote-lift);
  max-width: calc(100vw - var(--col2) - 120px);
}

/* ---------- Quote ---------- */

.q-wrap { position: relative; }

/* The face (css/fonts.css, by data-font) may set --qf-*: family, style, leading, tracking,
   features and --qf-scale, its size against the tier's. Without one this is Instrument. */
.quote {
  font-family: var(--qf-family, var(--font-display));
  font-style: var(--qf-style, italic);
  font-weight: 400;
  line-height: var(--qf-leading, var(--quote-leading));
  letter-spacing: var(--qf-tracking, 0);
  font-feature-settings: var(--qf-features, "liga" 1, "clig" 1); /* ligatures asked for by name, in every face: a browser drops them as soon as letter-spacing is not 0 (css/fonts.css) */
  white-space: pre-line;
  overflow-wrap: break-word;
  text-wrap: auto; /* every line is filled as far as it goes, the same in every browser. Not `pretty`: Safari re-balances the whole paragraph with it and leaves lines short ("to live, each / has to do."), Chrome does not. No single-word last lines and no stranded openers: js/app.js → noOrphans */
  font-size: calc(var(--q-size) * var(--qf-scale, 1));
  width: var(--q-width);
  max-width: 100%;
}
/* --q-ratio: main size ÷ notes size. The notes quote is boxed by four columns; the main quote is
   boxed by the same four columns × this ratio, so its measure in em is never wider than the notes
   quote's, and the two always break into the same lines (at any window width).
   The box is measured in the tier's em, whatever the face's own scale — except a short quote's,
   which widens with its face's scale, so a face set larger still fits the same words on its two
   lines (Bill, 2026-10-06); the slide's four columns still cap it. */
.quote[data-tier="l"]  { --q-size: var(--quote-l);  --q-width: 10.94em; --q-ratio: 1.0667; }  /* 700 / 64 · 64/60 */
.quote[data-tier="m"]  { --q-size: var(--quote-m);  --q-width: calc(16.54em / var(--qf-scale, 1)); --q-ratio: 1.0834; }  /* 860 / 52 · 52/48 */
.quote[data-tier="s"]  { --q-size: var(--quote-s);  --q-width: calc(21.5em / var(--qf-scale, 1));  --q-ratio: 1.25; }    /* 860 / 40 · 40/32 */
.quote[data-tier="xs"] { --q-size: var(--quote-xs); --q-width: calc(26.88em / var(--qf-scale, 1)); --q-ratio: 1.1429; }  /* 860 / 32 · 32/28 */
.slide .quote { max-width: min(100%, calc((4 * var(--colw) + 3 * var(--g)) * var(--q-ratio))); }
/* Desktop (Figma: Type 310:3916): the main quote is boxed by the four columns from col2 with 40
   kept clear on the right (780 at 1280) — quotes of every length fill that, a short one too
   (Bill, 2026-10-06: a short quote held in its own narrower box broke onto two lines with the
   room for one beside it) — so its right edge follows the grid at every window width. The notes
   quote keeps the main quote's line breaks (js → lockLines); they are shorter than its four columns. */
@media (min-width: 1024px) {
  .quote { --q-measure: calc(4 * var(--colw) + 3 * var(--g) - 40 * var(--u)); }
  .slide .quote { --q-width: var(--q-measure); max-width: min(100%, var(--q-measure)); }
  /* The notes quote takes the same measure in em as the main one, at every length: the main
     one's box divided by the size ratio. Past the width where the type stops growing
     (--u's cap) the columns still widen, so a fixed em box in notes would hold less than the
     main line and break it again (Bill, 2026-10-06). */
  .notes .quote { --q-width: calc(var(--q-measure) / var(--q-ratio)); max-width: min(100%, var(--q-width)); }
}

/* The original words in Noto: each script's own font, size, leading, tracking, weight (css/fonts.css). */
.quote[data-native] {
  font-size: calc(var(--q-size) * var(--qf-native-scale, var(--qn-scale, var(--quote-native-scale))));
  font-family: var(--qf-native, var(--qn-serif, var(--font-native-quote)));
  font-weight: var(--qf-native-weight, var(--qn-weight, 500));
  font-style: normal;
  line-height: var(--qf-native-leading, var(--qn-leading, var(--quote-native-leading)));
  letter-spacing: var(--qf-native-tracking, var(--qn-tracking, 0.02em));
  font-feature-settings: normal;
}

.lang {
  position: absolute;
  z-index: 1; /* above the quote: the RGB lens paints the page colour in the margin round the quote's box, where the button sits (js/app.js → rgbLens) */
  left: -44px;
  top: -24px;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border: var(--hairline) solid currentColor;
  /* Each language's label, font, size, tracking and nudge (js/app.js → LANG_BUTTON, set inline). */
  font-family: var(--lb-font, var(--font-native-ui)), var(--font-native-ui);
  font-weight: var(--lb-weight, 500);
  font-size: var(--lb-size, var(--glyph-size));
  line-height: 1;
  letter-spacing: var(--lb-track, 0);
  font-feature-settings: normal;
  white-space: nowrap;
}
.lang > span { display: block; transform: translateY(var(--lb-nudge, 0px)); }
/* Pressed, it sets the change off: 10px towards the quote, fading, in 50ms (js/app.js →
   LANG_BTN_OUT_MS); once the new words are in it comes back from there, with a subtle bounce:
   15% past its place, then settling (Bill, 2026-10-06). It enters the same way after the quote
   has typed itself in (js/app.js → arrive: .is-in; until 2026-10-06 it faded up 8px). Towards
   the quote is down and to the right, where the button sits at the quote's top left; on a phone,
   where it sits above the quote, straight down (--lang-to). */
.lang { --lang-to-x: 10px; --lang-to-y: 10px; }
@keyframes lang-out { to { opacity: 0; translate: var(--lang-to-x) var(--lang-to-y); } }
.lang.is-back, .lang.is-in { animation: lang-back 0.25s both; }
@keyframes lang-back {
  0%   { opacity: 0; translate: var(--lang-to-x) var(--lang-to-y); animation-timing-function: cubic-bezier(0.2, 0.7, 0.3, 1); }
  60%  { opacity: 1; translate: calc(var(--lang-to-x) * -0.15) calc(var(--lang-to-y) * -0.15); animation-timing-function: cubic-bezier(0.4, 0, 0.4, 1); }
  100% { opacity: 1; translate: 0 0; }
}
.lang.is-out { animation: lang-out 0.05s var(--ease) forwards; pointer-events: none; } /* after .is-in: a button pressed while it is still coming in leaves at once */
/* With a mouse, hovering grows it to 1.1 (Bill, 2026-10-06). `scale` is separate from the
   animations' `translate`, so the two never fight. */
@media (hover: hover) and (pointer: fine) {
  .lang { transition: scale 0.2s var(--ease); }
  .lang:hover { scale: 1.1; }
}
/* No underlines while the paragraph is changing; they are drawn back in afterwards. */
.q-wrap.is-typing .quote:not(.quote-ghost) .ann,
.q-wrap.is-typing .quote-ghost .ann { background-size: 0 0 !important; }
/* The button steps aside while the quote is being retyped. */
.q-wrap.is-typing .lang,
.q-wrap.is-wiping .lang { visibility: hidden; }
/* A word of the quote wrapped to fade in by itself (js/app.js → wordsIn, buildScraps): its box takes
   in the room its ink may reach (the script faces' tails), with the same negative margin so nothing
   moves — iOS Safari redraws a fading word box by box, and wiped the tails of the neighbouring
   lines that lay inside it (js: drawWhole). */
.q-word { padding: var(--ink-y) var(--ink-x); margin: calc(-1 * var(--ink-y)) calc(-1 * var(--ink-x)); }
.quote-ghost { position: absolute; max-width: none; pointer-events: none; }

@media (max-width: 1023px) {
  .slide .q-wrap { max-width: none; right: calc(var(--m) + var(--step)); }
  .quote { --q-width: 100% !important; --q-ratio: 1; } /* same size in both modes */
}

@media (max-width: 599px) {
  .slide .q-wrap { left: var(--m); right: var(--m); }
  .slide .quote { max-width: 100%; } /* the notes quote is full width on a phone, not 4 columns */
  /* Every quote's box runs margin to margin; the rag is evened by js/app.js (evenLines), as at every width. */
  .lang { left: 0; top: -48px; --lang-to-x: 0px; } /* above the quote: it slips straight down towards it, and comes back up */
}

/* Room at a scroller's foot while the phone's keyboard is up, or while the thoughts box folds (js:
   setRoom, in app.js, form.js and admin.js): a spacer, never padding on the scroller — iOS Safari
   does not count a scroll container's own bottom padding as scrollable space. */
.kb-room { flex-shrink: 0; width: 100%; }

/* ---------- Notes mode ---------- */

.notes {
  position: absolute;
  inset: 0;
  z-index: 10;
  background: var(--bg); /* its own sheet of color, so it can be revealed over the page */
  color: var(--fg);
  overflow-x: hidden; /* the thrown quote is cropped by the screen edge */
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.notes::-webkit-scrollbar { display: none; }

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

.n-cats {
  position: sticky;
  top: var(--mark-top); /* the first category mark sits where the menu button was */
  height: 0;
  margin-left: var(--mark-top);
  z-index: 3;
  transition: filter 0.4s var(--ease);
}
.n-cats .sym { flex: none; width: var(--mark); height: var(--mark); } /* each category's symbol, drawn in by js/app.js (drawNoteSym) */
/* The name is centred on the symbol by its capitals, not by its line box (whose centre is 0.0575em higher): the capitals then sit on the top line, with Close. */
.n-cats li > span:last-child { position: relative; top: calc(var(--text-leading) * 0.5em - var(--cap-offset) - var(--cap-height) / 2); }
.n-cats li { display: flex; align-items: center; gap: 1.333em; height: var(--mark); margin-bottom: calc(4 * var(--u)); white-space: nowrap; }

.n-pin {
  position: sticky;
  top: 0;
  height: 0;
  z-index: 1;
  transition: filter 0.4s var(--ease);
}
/* Filtered only while scrolled: an always-on filter rasterizes the quote differently, which
   showed as a flicker when the travelling quote handed over to it. */
.n-pin.is-blurred { filter: blur(var(--pin-blur, 0px)); }
/* …and fades as the notes reach it — all of it, the thumbnail too, so the blurred background is
   evenly faint (Bill, 2026-10-10). Each palette fades only as far as AA needs: at --pin-dim-min the
   notes text keeps 4.5:1 even where the blurred quote is as dense as solid text (the more it
   shows, the more depth). Contrast there: perspective 4.50, growth 4.50, drive 4.62, community
   4.60, romance 4.56. js/app.js sets --pin-dim as the notes scroll. */
.n-pin { opacity: var(--pin-dim, 1); }
[data-theme="perspective"] { --pin-dim-min: 0.34; }
[data-theme="growth"]      { --pin-dim-min: 0.40; }
[data-theme="drive"]       { --pin-dim-min: 0.42; }
[data-theme="community"]   { --pin-dim-min: 0.26; }
[data-theme="romance"]     { --pin-dim-min: 0.38; }
.notes:not(.is-scrolling) .n-pin { transition: filter 0.4s var(--ease), opacity 0.4s var(--ease); }
.notes.is-scrolling .n-pin { transition: none; }

.n-meta p { white-space: nowrap; }
/* Indents line the names up under the label's second word. */
.n-indent--from { padding-left: 2.667em; } /* 40 / 15 */
.n-indent--kept { padding-left: 2em; }     /* 30 / 15 */
.n-native { font-family: var(--font-native); font-size: var(--text-native-size); letter-spacing: 0; font-style: normal; } /* any typed field's Chinese, Japanese, Korean (js: nativeRuns) — upright, even in an italic title */
.n-native.n-hant { font-family: var(--noto-sans-tc); }
/* Any other script Crimson Pro lacks (js: nativeRuns): its Noto, at --script-scale of the text so
   the x-heights meet, upright; a right-to-left run kept whole inside a left-to-right line. */
.n-script { font-size: calc(var(--script-scale) * 1em); letter-spacing: 0; font-style: normal; }
.n-script[dir="rtl"], .n-ltr { unicode-bidi: isolate; }
.n-cyrl, .n-grek { font-family: 'Noto Serif', var(--font-text); font-style: inherit; } /* they have real italics: a Russian or Greek book title stays italic */
.n-arab { font-family: 'Noto Sans Arabic', var(--font-native); }
.n-hebr { font-family: 'Noto Sans Hebrew', var(--font-native); }
.n-thai { font-family: 'Noto Sans Thai', var(--font-native); }
.n-deva { font-family: 'Noto Sans Devanagari', var(--font-native); }
.n-source {
  white-space: normal; display: block; text-wrap: balance;
  /* As wide as the top line allows: from the indent to the margin, keeping clear of Close (its
     width plus 40px). The old two-columns-minus-40 cap wrapped "Nike Air Jordan Commercial"
     at tablet and narrow-desktop widths for no reason. */
  max-width: calc(100vw - var(--col3) - 2.667em - var(--m) - 5em);
}

.n-row { display: block; }
.n-from { position: absolute; top: calc(20px - var(--cap-offset)); left: var(--col3); } /* its capitals on the top line, with Close and the category's name (it sat at 22px: 8 too low) */

.notes .q-wrap {
  position: absolute;
  top: var(--notes-quote-top);
  left: var(--col2);
  width: calc(4 * var(--colw) + 3 * var(--g));
}
/* Notes mode only changes the size. The measure stays the same in em as on the main screen,
   so the quote breaks into the same lines in both modes — and to make sure of it, the notes
   quote is rendered with the main quote's line breaks hard-coded (js/app.js → lockLines). */
.quote--locked { white-space: pre-line; text-wrap: auto; } /* plain greedy wrapping, not `pretty`: a
   line that fit on the main screen is left alone, and a line that no longer fits (the window
   changed size) wraps instead of running off the edge — the quote is never cut */
.notes .quote[data-tier="l"]  { --q-size: var(--quote-l-notes); }
.notes .quote[data-tier="m"]  { --q-size: var(--quote-m-notes); }
.notes .quote[data-tier="s"]  { --q-size: var(--quote-s-notes); }
.notes .quote[data-tier="xs"] { --q-size: var(--quote-xs-notes); }

/* Annotated words (and the link in a menu description, the privacy page's email address — Bill,
   2026-10-10): hairline dashed underline. */
.ann,
.cat-desc [data-href],
.pv-mail,
.send-note [data-href],
.dup-note [data-href] {
  cursor: pointer;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone; /* an underlined phrase may now wrap: each line gets its own full underline */
  background: repeating-linear-gradient(90deg, currentColor 0 2px, transparent 2px 4px) left bottom 0.08em / 100% 1px no-repeat;
}
@media (min-resolution: 2dppx) { .ann, .cat-desc [data-href], .pv-mail, .send-note [data-href], .dup-note [data-href] { background-size: 100% 0.5px; } }

.n-col {
  position: relative;
  z-index: 2;
  margin-left: var(--col3);
  width: 26.667em; /* 400 / 15 — follows the text size */
  max-width: calc(100vw - var(--col3) - var(--m));
  padding-top: var(--notes-quote-gap);
  padding-bottom: 160px;
  transition: filter 0.4s var(--ease);
}
.n-body { margin-top: 2em; display: flex; flex-direction: column; gap: 2em; } /* one gap (32 at 16px text) under "Kept by" and between the note and the context (Bill, 2026-10-09, from the spacing tool; 1.333em until then) */
.n-body p + p { margin-top: 1em; } /* between paragraphs (1.067em until 2026-10-09) */
.n-context { display: flex; flex-direction: column; gap: 1em; } /* "Context/" to its words (0.533em until 2026-10-09) */
.n-context p + p { margin-top: 1em; }

/* ---------- Thoughts (Figma 525:759, since 2026-10-09) ----------
   Under the context: the approved thoughts, titled "Thoughts/", oldest first — each a name (or
   "A fellow human") with its words indented as the keeper's name is — then "Add thoughts +" (the
   form's "In original language +", in the palette's colour). Pressed, it gives way to a box for
   the words (the form's field, on the dark ground as the annotation sheet's: white at 10%, white
   text) with a × to close it; once there are words, the name field and the "Add thoughts" button
   unfold under it. Sent, the whole thing is replaced by one line of thanks. */
.n-thoughts { display: flex; flex-direction: column; gap: 1em; } /* "Thoughts/" → the first, and thought → thought: 1em (Figma 15) */
.n-thought p { white-space: normal; }
.n-add { display: flex; flex-direction: column; width: calc(2 * var(--colw) + var(--g)); max-width: 100%; margin-top: 2em; } /* 4em (the column's 2em gap and 2em more) under whatever comes before — the note, the context, the thoughts — twice the gap between the blocks (Bill, 2026-10-09) */ /* on the grid, as the form's fields: two columns (four on a tablet, margin to margin on a phone — below); not the text's measure */
.n-add .more { align-items: flex-start; height: auto; } /* the words and the + on the block's top line (Figma: a 32 row, both at its top) */
.n-add .more { opacity: 0.6; transition: opacity 0.15s var(--ease); } /* at rest 60% of the palette's colour; full under the pointer and while pressed (Bill, 2026-10-09) */
/* …75% where 60% falls short of AA (4.5:1) on its background: Perspective 3.67 → 5.19, Community
   3.44 → 4.68; Growth, Drive and Romance pass at 60% (Bill, 2026-10-10). */
[data-theme="perspective"] .n-add .more, [data-theme="community"] .n-add .more { opacity: 0.75; }
@media (hover: hover) { .n-add .more:hover { opacity: 1; } }
.n-add .more:active { opacity: 1; }
.n-add {
  --field: rgba(255, 255, 255, 0.1);
  --field-hover: rgba(255, 255, 255, 0.16);
  --placeholder: rgba(255, 255, 255, 0.6);
  --btn-line: rgba(255, 255, 255, 0.5);
  --ink: #ffffff;
}
.n-add input, .n-add textarea { -webkit-user-select: text; user-select: text; } /* selection is off site-wide; a box one types in needs it */
.n-add textarea.field { height: var(--ta-m); } /* 160, as the form's large boxes */
.n-add .btn { color: var(--white); margin-top: 2.5em; } /* the name field 1px under the words; the button 2.5em under the name — 40 at 16, to look as the form's "Add words" (Bill, 2026-10-10; 1em until then) */
@media (hover: hover) { .n-add .btn:not(:disabled):hover { --bd: var(--white); --ring: var(--white); } }
.n-thanks { white-space: normal; }
.n-add .is-back { animation: fade-in 0.3s var(--ease) both; } /* the opener or the thanks coming back as the box folds (js/app.js → shutBox) */

.n-topfade { display: none; }
/* The notes' words laid out out of sight, so their fonts load early (js/app.js → warmNotesSoon):
   off the left edge, no height (nothing to scroll to), never seen or read. */
.n-warm { position: absolute; left: -10000px; top: 0; width: 320px; height: 0; overflow: hidden; visibility: hidden; pointer-events: none; }

/* Video thumbnail */
.thumb {
  position: relative;
  display: block;
  width: 160px;
  aspect-ratio: 160 / 284; /* always the Figma portrait crop, whatever the video's shape */
  overflow: hidden;
  /* no fill colour: a black box must never show while the image is (re)painting */
  /* Its own layer from the start: Safari otherwise builds one as the scan's colour split comes
     off (js: scanIn) and shows nothing in it for ~4 frames — the picture blinked out as notes
     arrived (Bill's recording, 2026-10-10; reproduced in WebKit, gone with this). */
  will-change: transform;
}
/* Desktop hover: a slight lift. (`scale` is separate from the pointer-lean's `translate`.) */
@media (hover: hover) and (pointer: fine) {
  .thumb { transition: scale 600ms var(--ease); }
  .thumb:hover { scale: 1.05; }
}
.thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.thumb::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,0.4)); }
.thumb-play {
  position: absolute;
  left: 8px;
  bottom: 8px;
  z-index: 1;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border: var(--hairline) solid var(--white);
  background: rgba(0, 0, 0, 0.05);
  -webkit-backdrop-filter: blur(25px);
  backdrop-filter: blur(25px);
}
.thumb-play img { position: static; width: 6px; height: 6.9282px; }

/* A video with no thumbnail of its own (Figma 421:1179): the platform's logo on black, a hairline
   frame in the palette's colour, square (160; the frames draw 160 × 174, Bill made it square 2026-10-04), and a smaller play mark in that colour with no frame
   (YouTube's logo is a play mark: it gets none). Sizes are shares of the card, so the 120-wide
   card on tablet and phone keeps the proportions. */
.thumb--logo { aspect-ratio: 1; background: #000; border: var(--hairline) solid var(--fg); }
.thumb--logo::after { content: none; } /* no shade over it */
.thumb--logo .thumb-logo { inset: auto; left: 50%; top: 50%; translate: -50% -50%; width: var(--logo-w); height: auto; object-fit: contain; }
.thumb--logo[data-platform="tiktok"]    { --logo-w: 38.05%; }  /* 60.9 of 160 */
.thumb--logo[data-platform="youtube"]   { --logo-w: 28.55%; }  /* 45.7 */
.thumb--logo[data-platform="vimeo"]     { --logo-w: 25.31%; }  /* 40.5 */
.thumb--logo[data-platform="instagram"] { --logo-w: 22.5%; }   /* 36 */
.thumb--logo[data-platform="apple"]     { --logo-w: 24.66%; }  /* 39.5 (music, Figma 430:2543) */
.thumb--logo[data-platform="spotify"]   { --logo-w: 26.25%; }  /* 42 (Figma 430:2603) */
/* Music (Figma 430:2508): the cover, square, with the video thumbnail's play box. */
.thumb[data-orientation="music"] { aspect-ratio: 1; }
/* A book's cover (picked in the library): as wide as a thumbnail, its own height, no shade, no
   play mark — a picture, not a button, so no hover lift either. */
.thumb--cover { aspect-ratio: auto; }
.thumb--cover::after { content: none; }
.thumb--cover img { position: static; height: auto; }
@media (hover: hover) and (pointer: fine) { .thumb--cover:hover { scale: none; } }
.thumb-play--sm { left: 7.5px; bottom: 7.5px; border: 0; }
.thumb-play--sm .icon { --src: url(../assets/icons/video-play.svg); width: 10.34px; height: 11.94px; color: var(--fg); }

/* A word from the page at the foot of the window (the library's toast: css/admin.css). Here it
   says a video cannot be played; in notes it is black with white text. */
.app > .toast {
  position: absolute; z-index: 50; left: 50%; bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  max-width: calc(100% - 2 * var(--m)); padding: calc(8 * var(--u)) calc(20 * var(--u));
  background: var(--white); color: var(--ink); box-shadow: 0 0 20px rgba(0, 0, 0, 0.10);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none;
  opacity: 0; transform: translate(-50%, 8px);
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}
.app > .toast.is-in { opacity: 1; transform: translate(-50%, 0); }
.app[data-mode="notes"] > .toast { background: #000; color: #fff; }

#nVideoPin .thumb { position: absolute; top: var(--notes-quote-top); left: var(--col6); }
#nVideoCol { display: none; }

/* Overlays dim the notes underneath. */
.app.is-dim .n-pin  { filter: blur(25px); }
.app.is-dim .n-col,
.app.is-dim .n-cats { filter: blur(10px); }
.app.is-dim .notes  { pointer-events: none; }

@media (max-width: 1023px) {
  .n-col { width: auto; margin-right: calc(var(--m) + var(--step)); max-width: none; }
  .n-add { width: calc(4 * var(--colw) + 3 * var(--g)); } /* the form's four columns */
  /* The thumbnail stays with the pinned quote, as on desktop, in the left margin where the
     column starts — not in the column, where it scrolled with the text (Bill, 2026-10-09).
     --quote-bottom: the quote's foot, set by js/app.js → layoutNotes. */
  #nVideoPin .thumb { top: calc(var(--quote-bottom, var(--notes-quote-top)) + var(--notes-quote-gap)); left: var(--m); width: 120px; }
  #nVideoCol { display: none; }
}

@media (max-width: 599px) {
  /* Mobile: everything flows and scrolls together under a fading top bar. */
  .n-cats { position: absolute; top: var(--mark-top); }
  .n-cats li { gap: calc(var(--m) + 2.667em - var(--mark-top) - var(--mark)); } /* the category names start where the indented names below do (.n-indent--from) */
  /* 124: "Words from" 31.4 under three categories' symbols. A fourth and a fifth push it down a row
     each (a symbol and its 4px), so that gap stays (Bill, 2026-10-12; js/app.js sets --n-cats). */
  .n-pin { position: static; height: auto; padding: calc(124px + max(0, var(--n-cats, 1) - 3) * (var(--mark) + 4 * var(--u))) var(--m) 0; filter: none !important; }
  .n-pin { opacity: 1 !important; } /* nothing scrolls over it here: no fading */
  .app.is-dim .n-pin { filter: blur(25px) !important; }
  .n-from { position: static; }
  .n-source { max-width: none; margin-right: 40px; } /* as wide as the notes body below */
  .notes .q-wrap { position: relative; top: auto; left: auto; width: auto; margin-top: 100px; }
  .notes .q-wrap.has-lang { margin-top: 128px; }
  #nVideoPin { display: block; }
  #nVideoPin .thumb { position: relative; top: auto; left: auto; width: 120px; margin: 40px 0 0 auto; } /* 40px under the quote */
  #nVideoCol { display: none; }
  .n-col { margin: 80px var(--m) 0 !important; padding-top: 0; padding-bottom: 120px; }
  .n-body { margin-right: 0; max-width: none; }
  .n-body > :not(.n-add) { max-width: 320px; } /* the text: the margins' full width, at most 320 (Bill, 2026-10-09; it stopped 40px short of the right margin) */
  .n-add { width: 100%; } /* the box: margin to margin, 20 each side, whatever the text's cap (Bill, 2026-10-09) */
  .n-topfade {
    display: block;
    position: absolute; /* a child of .app, so not fixed (see .app) and not inside the scrolling notes */
    top: 0; left: 0; right: 0;
    height: 58px;
    z-index: 15; /* over the notes (10), under the Close link (20) */
    pointer-events: none;
    background: linear-gradient(to bottom, var(--bg) 30%, transparent);
    opacity: 0; /* off at the top of the page, so it never veils the category label */
    transition: opacity 0.2s var(--ease);
  }
  .app.is-scrolled .n-topfade { opacity: 1; }
  .app:not([data-mode="notes"]) .n-topfade { display: none; } /* it belongs to the notes */
}

/* ---------- Overlays ---------- */

.overlay { position: absolute; inset: 0; z-index: 30; animation: fade-in 0.35s var(--ease) both; }

.ann-overlay { animation: none; } /* the word zooms; Back and the note fade (js/app.js) */
/* Close (X): drawn in CSS to match the 1px arrow icons — swap for an SVG when there is one. */
.x-btn { position: absolute; width: 32px; height: 32px; }
.x-btn::before,
.x-btn::after {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 15px; height: 1px;
  background: currentColor;
  transform: translate(-50%, -50%) rotate(45deg);
}
.x-btn::after { transform: translate(-50%, -50%) rotate(-45deg); }
.ann-word {
  position: absolute;
  transform-origin: 0 0;
  font-family: var(--qf-family, var(--font-display)); /* the quote's face, at its short-quote scale (js sets data-font) */
  font-style: var(--qf-style, italic);
  font-size: calc(var(--quote-annotation) * var(--qf-scale, 1));
  line-height: 1.2;
  letter-spacing: var(--qf-tracking, 0);
  font-feature-settings: var(--qf-features, "liga" 1, "clig" 1); /* ligatures asked for by name, in every face: a browser drops them as soon as letter-spacing is not 0 (css/fonts.css) */
  white-space: nowrap;
}
.ann-word[data-native] { /* a word of the original's: its script's font, size and spacing, as the quote (css/fonts.css) */
  font-family: var(--qf-native, var(--qn-serif, var(--font-native-quote)));
  font-style: normal; font-weight: var(--qf-native-weight, var(--qn-weight, 500));
  font-size: calc(var(--quote-annotation) * var(--qf-native-scale, var(--qn-scale, var(--quote-native-scale))));
  letter-spacing: var(--qf-native-tracking, var(--qn-tracking, 0.02em)); font-feature-settings: normal;
}
.ann-text { position: absolute; width: min(20.4em, calc(100vw - 2 * var(--m))); }

.video-overlay { display: grid; place-items: center; padding: 72px var(--m) 48px; }
.video-box { position: relative; max-width: 100%; }
.video-box[data-orientation="horizontal"] { width: min(1030px, 100% - 100px, calc((100vh - 140px) * 16 / 9)); aspect-ratio: 16 / 9; }
.video-box[data-orientation="vertical"]   { height: min(711px, calc(100vh - 120px)); aspect-ratio: 9 / 16; }
/* Music: the platform's own player (dark), untouched — its own rounded corners — in a straight
   container of the player's own grey (js: VIDEO[…].bg), --music-pad of it round the player, no
   outline. As wide as a video; the player at its own height (js: --player-h). */
.video-box[data-orientation="music"] { --music-pad: 8px; width: min(1030px, 100% - 100px, calc((100vh - 140px) * 16 / 9)); height: calc(var(--player-h, 152px) + 2 * var(--music-pad)); }
.video-box[data-orientation="music"] .video-frame iframe { inset: var(--music-pad); width: calc(100% - 2 * var(--music-pad)); height: var(--player-h, 152px); background: transparent; }
/* The X sits on the diagonal out of the video's top-right corner: its centre is the same
   distance (20px) to the right of the video as it is above it. 32px box → 16px to centre. */
.video-close { left: calc(100% + 4px); bottom: calc(100% + 4px); }
/* The player box stays invisible while the thumbnail morphs into its place. */
.video-box { opacity: 0; }
.video-box.is-ready { opacity: 1; }
.video-box .video-close { opacity: 0; transition: opacity 0.3s var(--ease); }
.video-box.is-ready .video-close { opacity: 1; }
.video-overlay { animation: none; }
.video-frame { background: #000 center / cover no-repeat; }
.video-morph { position: absolute; z-index: 35; /* inside .app, whose origin is the viewport's */ overflow: hidden; background: #000; pointer-events: none; }
.video-morph img { width: 100%; height: 100%; object-fit: cover; display: block; }
.video-frame, .video-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: #000; }

/* ---------- Menu ---------- */

/* Figma 412:522. One symbol for the whole menu (js/app.js → "Symbols"): it draws in as the menu
   opens and redraws itself into the symbol of the category under the pointer / the tapped row.
   The names never move. Desktop and tablet: the symbol sits on the left margin, centred on the
   previewed row, and glides to the next one while it redraws. */
.menu {
  position: absolute;
  inset: 0;
  z-index: 40;
  background: var(--paper);
  color: var(--ink);
  overflow: hidden; /* no overlay fade: same grey as the page, the rows animate in (js/app.js) */
  --lh: calc(var(--text-size) * var(--text-leading));
  --menu-sym: calc(200 * var(--u));                 /* the symbol's box */
  --row: max(48px, 8.9vh);                          /* from one name to the next: 74 in an 832 window */
  --list-top: max(80px, calc((100vh - 5 * var(--row) - var(--lh)) / 2)); /* the first name's top: the block (six names, the description beside them) centred in the window, clear of Back on a short one */
  --list-left: calc(var(--m) + var(--menu-sym) + 80 * var(--u));
  --desc-left: var(--col4);
}
.menu-back { position: absolute; top: calc(20px - var(--cap-offset)); left: var(--m); }

.menu-sym {
  position: absolute;
  left: var(--m);
  top: calc(var(--list-top) + var(--lh) / 2 - var(--menu-sym) / 2); /* centred on the first name… */
  width: var(--menu-sym);
  height: var(--menu-sym);
  translate: 0 calc(var(--at, 0) * var(--row));                     /* …then on row --at (js) */
  transition: translate var(--sym-ms, 0.8s) var(--ease);            /* --sym-ms: the redraw's own length (js, from the symbols' file) */
  pointer-events: none;
}

.cat-list { position: absolute; top: calc(var(--list-top) - (var(--row) - var(--lh)) / 2); left: var(--list-left); right: calc(100% - var(--desc-left) + var(--g)); } /* rows stop before the descriptor column, so reading it never re-triggers a row */
.cat-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 24px;
  width: 100%;
  height: var(--row);
  text-align: left;
  white-space: nowrap;
}
.cat-row .cat-name { display: inline-block; margin-top: var(--link-gap); padding-bottom: var(--link-gap); --line: var(--hairline); } /* the margin balances the underline's room: the text itself is centred in the row */
@media (max-resolution: 1.5dppx) { .cat-row .cat-name { --line: 1px; } }
.cat-row[data-empty] { opacity: 0.35; cursor: default; }
.cat-row .cat-ind { visibility: hidden; width: 8px; height: 8px; }
.cat-row.is-selected .cat-ind,
.cat-row.is-preview .cat-ind { visibility: visible; }
.cat-row.is-selected .cat-ind { --src: url(../assets/icons/asterisk.svg); }
.cat-row.is-preview:not(.is-selected) .cat-ind { --src: url(../assets/icons/arrow-left.svg); }
.cat-row[data-empty] .cat-ind { visibility: hidden; } /* nothing to go to: no arrow */

/* The descriptor column: its first line sits on "All words", its count on the last row. */
.cat-desc {
  position: absolute;
  top: var(--list-top);
  left: var(--desc-left);
  width: 25em;
  max-width: calc(100% - var(--desc-left) - var(--m));
  height: calc(5 * var(--row) + var(--lh));
  display: flex;
  flex-direction: column;
  gap: 1.333em;
}
.cat-desc-line { flex: 0 0 auto; width: 0; border-left: var(--hairline) solid currentColor; transform-origin: 50% 50%; } /* height set by js/app.js → sizeDescLine; between items it stretches from its centre (the count stays put) */

.menu-apply:focus-visible { outline-color: var(--ink); }
.menu-apply {
  display: none;
  position: absolute;
  left: var(--m); right: var(--m); bottom: 20px;
  height: 40px;
  background: var(--ink);
  color: var(--white);
  text-align: center;
}

/* Touch layouts (tablet, phone): a tap redraws the symbol and draws a hairline under the name —
   that is the preview, with the description; a tap on the underlined row applies it. */
@media (max-width: 1023px) {
  .menu {
    /* 160, and 60 to the names, in an 834 window; both give way on a narrower one, so the
       names never reach the descriptor (which also keeps clear of the longest name + its arrow). */
    --menu-sym: clamp(96px, 19.2vw, 160px);
    --row: max(48px, 10.2vh);   /* 85 in an 832 window */
    --list-left: calc(var(--m) + var(--menu-sym) + clamp(20px, calc(60px - (834px - 100vw) * 0.17), 60px)); /* 60 at 834 wide, 20 at 600 */
    --desc-left: max(50%, calc(var(--list-left) + 130px));
  }
  .cat-desc { width: 320px; }
  .cat-row.is-preview:not([data-empty]) .cat-name { /* an empty category can't be applied: no underline */
    background: linear-gradient(currentColor, currentColor) left bottom / 100% var(--line) no-repeat;
    animation: link-in 0.5s var(--ease);
  }
}
@keyframes link-in { from { background-size: 0% var(--line); } }

@media (max-width: 599px) {
  /* Phone (Figma 412:302): the symbol in the top right corner, redrawing in place; the list on
     the left margin under it, a rule between rows, the description under the previewed name.
     Symbol, rows and the space between them give way on a short window; past that the menu
     scrolls. */
  .menu {
    --rule: #b2b2b2;
    --menu-sym: clamp(96px, 18.8vh, 160px);   /* 160 in an 852 window */
    --row: clamp(50px, 7.75vh, 66px);         /* 66 */
    --list-top: calc(20px + var(--menu-sym) + min(40px, 4.7vh));
  }
  /* Only the list scrolls, under a top bar that holds Back and fades the rows out (as the notes'
     .n-topfade does); the symbol stays put over both. */
  .cat-list {
    top: 0; bottom: 0; left: 0; right: 0;
    padding: calc(var(--list-top) - (var(--row) - var(--lh)) / 2) var(--m) 20px;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
  }
  .cat-list::-webkit-scrollbar { display: none; }
  .menu::before {
    content: "";
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 58px;
    z-index: 1;
    pointer-events: none;
    background: linear-gradient(to bottom, var(--paper) 30%, transparent);
  }
  .menu-back, .menu-sym { z-index: 2; }
  .menu-sym { left: auto; right: var(--m); top: 20px; translate: none; }
  /* The hairline between rows belongs to the lower row. */
  li + li > .cat-row::before { content: ""; position: absolute; left: 0; right: 0; top: 0; border-top: var(--hairline) solid var(--rule); }
  .cat-row[data-empty] { opacity: 1; } /* the rule keeps its colour: only the name is dimmed */
  .cat-row[data-empty] > * { opacity: 0.35; }
  /* The description sits in the list, under the previewed row (js/app.js moves it into that row's li). */
  .cat-desc { position: static; margin: 0 0 calc((var(--row) - var(--lh)) / 2); padding-right: 4px; width: auto; max-width: none; height: auto; } /* folds open/shut by height (js/app.js → placeDesc); the text is never clipped: each line fades in once the space has opened for it */
  .cat-desc-line, #catCount { display: none; }
}

@media (max-width: 599px) {
  /* No room beside the video on a phone: Close goes above its right edge. */
  .video-box[data-orientation="horizontal"], .video-box[data-orientation="music"] { width: 100%; }
  .video-close { left: auto; right: -9px; bottom: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
