/* ============================================================================
   base.css — tokens, reset, chrome.

   Typography is the structure here. Fraunces carries every display size with
   its optical-size axis driven explicitly (opsz 144 at the top of the scale,
   down to 12 for reading sizes) and enough weight behind it to hold a page —
   a sturdy, warm serif rather than a hairline one. Geist sets running text,
   Geist Mono sets every label.

   Colour is warm ink, bone type and one vermilion. Dark is the default and
   light is the alternate, both expressed as the same six variables so the page
   can be flipped by a toggle or interpolated by the quote passage — which
   inverts to whichever theme is not currently on.
   ========================================================================= */

@font-face {
  font-family: 'Playfair';
  src: url('../fonts/playfair.woff2') format('woff2');
  font-weight: 800; font-style: normal; font-display: block;
}
@font-face {
  font-family: 'Rozha';
  src: url('../fonts/rozha-devanagari.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: block;
  unicode-range: U+0900-097F, U+200C-200D, U+20B9;
}
@font-face {
  font-family: 'Fraunces';
  src: url('../fonts/fraunces.woff2') format('woff2-variations');
  font-weight: 100 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Fraunces';
  src: url('../fonts/fraunces-italic.woff2') format('woff2-variations');
  font-weight: 100 900; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'Geist';
  src: url('../fonts/geist.woff2') format('woff2-variations');
  font-weight: 100 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'GeistMono';
  src: url('../fonts/geist-mono.woff2') format('woff2-variations');
  font-weight: 100 900; font-style: normal; font-display: swap;
}

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

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body, h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd, ul, ol { margin: 0; padding: 0; }
ul, ol { list-style: none; }
img, svg, video, canvas { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

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

:root {
  /* dark is the default look */
  --paper:   #0A0B0A;
  --paper-2: #131614;
  --ink:     #F4F6F2;
  --ink-2:   #C2C8BE;
  --mute:    #858C80;
  --line:    rgba(244, 246, 242, 0.15);
  --accent:  #C6F24E;
  --on-accent: #0A0B0A;

  color-scheme: dark;

  --serif: 'Fraunces', 'Iowan Old Style', Georgia, 'Times New Roman', serif;
  --sans:  'Geist', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --mono:  'GeistMono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* one scale, used everywhere */
  --t-mega: clamp(3.2rem, 15.5vw, 18rem);
  --t-hero: clamp(2.4rem, 8.4vw, 9rem);
  --t-h2:   clamp(1.9rem, 5vw, 5rem);
  --t-h3:   clamp(1.35rem, 2.5vw, 2.3rem);
  --t-lead: clamp(1.3rem, 2.2vw, 2rem);
  --t-body: 1.0625rem;
  --t-sm:   0.9375rem;
  --t-lbl:  0.6875rem;

  /* elevation: two shadows each, a tight contact one and a wide ambient one,
     so things sit on the page rather than float over it */
  --lift-1: 0 1px 2px rgba(0, 0, 0, 0.34), 0 3px 10px rgba(0, 0, 0, 0.26);
  --lift-2: 0 2px 6px rgba(0, 0, 0, 0.38), 0 14px 38px rgba(0, 0, 0, 0.36);
  --lift-3: 0 6px 18px rgba(0, 0, 0, 0.42), 0 34px 78px rgba(0, 0, 0, 0.46);
  --edge:   inset 0 0 0 1px rgba(244, 246, 242, 0.07);

  --pad: clamp(1.15rem, 4.5vw, 4.5rem);
  --max: 1600px;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
}

/* the alternate: bone paper, warm ink */
:root[data-theme='light'] {
  --paper:   #F4F4EF;
  --paper-2: #E6E7DF;
  --ink:     #0A0B0A;
  --ink-2:   #3A3E38;
  --mute:    #5E6359;
  --line:    rgba(10, 11, 10, 0.15);
  --accent:  #4A6606;
  --on-accent: #F7F8F3;

  /* bone paper takes a softer, warmer shadow than the graphite ground */
  --lift-1: 0 1px 2px rgba(10, 11, 10, 0.10), 0 3px 10px rgba(10, 11, 10, 0.07);
  --lift-2: 0 2px 6px rgba(10, 11, 10, 0.11), 0 14px 34px rgba(10, 11, 10, 0.11);
  --lift-3: 0 6px 18px rgba(10, 11, 10, 0.13), 0 34px 70px rgba(10, 11, 10, 0.16);
  --edge:   inset 0 0 0 1px rgba(10, 11, 10, 0.07);

  color-scheme: light;
}

/* The quote passage inverts to whichever theme is not on. `.night` is the
   no-JS / reduced-motion equivalent of that flip. */
.night {
  --paper:   #F4F4EF;
  --paper-2: #E6E7DF;
  --ink:     #0A0B0A;
  --ink-2:   #3A3E38;
  --mute:    #5E6359;
  --line:    rgba(10, 11, 10, 0.15);
  --accent:  #4A6606;
  --on-accent: #F7F8F3;
}

:root[data-theme='light'] .night {
  --paper:   #0A0B0A;
  --paper-2: #131614;
  --ink:     #F4F6F2;
  --ink-2:   #C2C8BE;
  --mute:    #858C80;
  --line:    rgba(244, 246, 242, 0.15);
  --accent:  #C6F24E;
  --on-accent: #0A0B0A;
}

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

html {
  background: var(--paper);
  /* the bar is fixed, so an anchored jump must stop below it */
  scroll-padding-top: clamp(4.5rem, 9vw, 6.5rem);
}

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 1.55;
  letter-spacing: -0.011em;
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-kerning: normal;
  font-variant-ligatures: common-ligatures contextual;
  text-rendering: optimizeLegibility;
}

::selection { background: var(--accent); color: var(--on-accent); }
::-webkit-scrollbar { width: 0; height: 0; }

html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-stopped { overflow: hidden; }

@media (hover: hover) and (pointer: fine) {
  body.cursor-on, body.cursor-on * { cursor: none !important; }
}

/* paper tooth */
body::after {
  content: '';
  position: fixed;
  inset: -50%;
  z-index: 9997;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.84' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.4'/%3E%3C/svg%3E");
}

/* screen on dark, multiply on light */
body::after { mix-blend-mode: screen; opacity: 0.13; }
:root[data-theme='light'] body::after { mix-blend-mode: multiply; opacity: 0.3; }
:root.is-inverted body::after { mix-blend-mode: multiply; opacity: 0.3; }
:root[data-theme='light'].is-inverted body::after { mix-blend-mode: screen; opacity: 0.13; }

/* ---------- type primitives --------------------------------------------- */

.wrap { width: 100%; max-width: var(--max); margin-inline: auto; padding-inline: var(--pad); }

/* Display: Fraunces with the optical-size axis at the top of its range and
   real weight behind it — the point is presence, not hairlines. */
.dsp {
  font-family: var(--serif);
  font-weight: 800;
  font-variation-settings: 'opsz' 144;
  line-height: 0.86;
  letter-spacing: -0.038em;
  font-variant-numeric: lining-nums;
}

.dsp--md { font-variation-settings: 'opsz' 72; font-weight: 750; letter-spacing: -0.032em; line-height: 0.94; }
.dsp--sm { font-variation-settings: 'opsz' 36; font-weight: 700; letter-spacing: -0.022em; line-height: 1.04; }
.dsp em, .it { font-style: italic; font-synthesis: none; }

/* The same face set for reading: optical size drops, weight comes back to
   book, and it behaves like a text serif. */
.serif-read {
  font-family: var(--serif);
  font-variation-settings: 'opsz' 12;
  font-weight: 400;
  letter-spacing: -0.002em;
}

.lbl {
  font-family: var(--mono);
  font-size: var(--t-lbl);
  font-weight: 400;
  line-height: 1.35;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--mute);
  font-variant-numeric: tabular-nums;
}

.lbl--ink { color: var(--ink); }
.lbl--acc { color: var(--accent); }

.lead {
  font-family: var(--serif);
  font-variation-settings: 'opsz' 20;
  font-weight: 400;
  font-size: var(--t-lead);
  line-height: 1.32;
  letter-spacing: -0.014em;
  text-wrap: pretty;
}

.copy { font-size: var(--t-body); line-height: 1.62; color: var(--ink-2); max-width: 64ch; }
.copy + .copy { margin-top: 1em; }
.copy strong { color: var(--ink); font-weight: 500; }
.copy em { font-family: var(--serif); font-variation-settings: 'opsz' 12; font-style: italic; font-weight: 500; color: var(--ink); }

.rule { height: 1px; background: var(--line); border: 0; transform-origin: 0 50%; }

/* line masks + word scrub, filled in by core.js */
.ln__i-mask { padding-bottom: 0.06em; }
.ln__i { display: block; will-change: transform; }
.wd { display: inline-block; will-change: opacity; }
.ch { display: inline-block; will-change: transform; }
.ch-mask { padding-bottom: 0.06em; }

.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  font-family: var(--mono);
  font-size: var(--t-lbl);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  border: 1px solid var(--line);
  border-radius: 100px;
  padding: 0.9rem 1.5rem;
  overflow: hidden;
  isolation: isolate;
  background: transparent;
  cursor: pointer;
}

.btn::before {
  content: '';
  position: absolute; inset: 0; z-index: -1;
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: 50% 100%;
  transition: transform 0.5s var(--ease);
}

.btn:hover { color: var(--on-accent); border-color: var(--accent); }
.btn:hover::before { transform: scaleY(1); }
.btn i { font-style: normal; transition: transform 0.45s var(--ease); }
.btn:hover i { transform: translateX(5px); }

/* ---------- chrome ------------------------------------------------------ */

.cur, .cur-dot {
  position: fixed; top: 0; left: 0; z-index: 10000;
  pointer-events: none; border-radius: 50%; opacity: 0; will-change: transform;
}

.cur {
  width: 44px; height: 44px; margin: -22px 0 0 -22px;
  border: 2px solid var(--ink);
  /* a hairline of the page colour behind the ring so it separates from a
     plate, the WebGL ground, or anything else it happens to be over */
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--paper) 60%, transparent);
  display: grid; place-items: center;
  transition: opacity 0.25s ease, background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}

.cur-dot {
  width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; background: var(--accent);
  box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--paper) 55%, transparent);
}

.cur__t {
  font-family: var(--mono); font-size: 0.5rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--on-accent);
  opacity: 0; transition: opacity 0.2s ease; white-space: nowrap;
}

.cur.is-tagged { border-color: var(--accent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 30%, transparent); }
/* Fill only once there is a word to carry — an empty accent disc reads as a
   glitch rather than a label. */
.cur.is-worded { background: var(--accent); }
.cur.is-worded .cur__t { opacity: 1; }

/* the ring flares for the length of the press */
.cur.is-lit { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 45%, transparent), 0 0 18px color-mix(in srgb, var(--accent) 40%, transparent); }

/* and a ring is left behind where the click landed */
.cur-flash {
  position: fixed; top: 0; left: 0; z-index: 9999;
  width: 42px; height: 42px; margin: -21px 0 0 -21px;
  border: 2px solid var(--accent); border-radius: 50%;
  pointer-events: none; opacity: 0; will-change: transform, opacity;
}

@media (hover: none), (pointer: coarse) { .cur, .cur-dot, .cur-flash { display: none !important; } }
@media (prefers-reduced-motion: reduce) { .cur-flash { display: none !important; } }

.nav {
  position: fixed; inset: 0 0 auto 0; z-index: 90;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: clamp(1rem, 2.2vw, 1.7rem) var(--pad);
  color: var(--ink);
}

/* The bar is fixed, so anything tall enough passes underneath it. A scrim keeps
   the wordmark and the links readable over whatever is behind. It is plain CSS
   and always on, so this holds with JavaScript off too. */
.nav::before {
  content: '';
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(to bottom,
              var(--paper) 0%,
              color-mix(in srgb, var(--paper) 82%, transparent) 55%,
              transparent 100%);
  /* the gradient reaches past the bar so the fade ends in open space */
  bottom: -1.6rem;
  transition: opacity 0.35s ease;
}

/* Once the page has actually moved, commit to a solid bar — a gradient alone
   cannot carry body text scrolling behind it. */
.nav::after {
  content: '';
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: color-mix(in srgb, var(--paper) 97%, transparent);
  border-bottom: 1px solid var(--line);
  opacity: 0;
  transition: opacity 0.35s ease;
}

@supports (backdrop-filter: blur(1px)) {
  .nav::after { background: color-mix(in srgb, var(--paper) 90%, transparent); backdrop-filter: blur(12px) saturate(1.35); }
}

.nav.is-stuck::after { opacity: 1; }
.nav.is-stuck::before { opacity: 0; }

.nav__id { display: inline-flex; align-items: baseline; }

/* Held covered from the very first paint, on the one page that animates it.

   The wordmark is real markup, so without this it renders normally for as long
   as it takes the runtime to arrive — measured at half a second on a local
   connection and longer on a real one — and then vanishes as the scene takes
   over. That is the flash. Doing it in CSS means there is no frame in which it
   was ever painted.

   Scoped to html.js.motion, both of which the document head sets before first
   paint, so no-JS and reduced motion show the wordmark plainly and nothing has
   to undo this for them. inline-block because a clip needs a box. */
html.js.motion .nav__mark--hold {
  display: inline-block;
  clip-path: inset(-20% 100% -20% 0%);
}

.nav__mark {
  font-family: var(--serif);
  font-variation-settings: 'opsz' 24;
  font-weight: 600;
  font-size: clamp(0.95rem, 2.4vw, 1.2rem);
  letter-spacing: -0.018em;
  white-space: nowrap;
}

.nav__links { display: flex; align-items: center; gap: clamp(0.85rem, 2vw, 2.1rem); }

.nav__links a {
  position: relative;
  font-family: var(--mono); font-size: var(--t-lbl);
  letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--mute); padding-block: 2px;
}

.nav__links a::after {
  content: ''; position: absolute; left: 0; bottom: 0;
  width: 100%; height: 1px; background: currentColor;
  transform: scaleX(0); transform-origin: 100% 50%;
  transition: transform 0.45s var(--ease);
}

.nav__links a:hover { color: var(--ink); }
.nav__links a:hover::after { transform: scaleX(1); transform-origin: 0 50%; }
.nav__links a[aria-current='page'] { color: var(--accent); }
.nav__sm { display: none; }
@media (min-width: 760px) { .nav__sm { display: inline-flex; } }

.theme-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0;
  border: 1px solid var(--line); border-radius: 50%;
  background: transparent; color: var(--mute); cursor: pointer;
  transition: color 0.3s ease, border-color 0.3s ease, transform 0.4s var(--ease);
}

.theme-btn:hover { color: var(--accent); border-color: var(--accent); transform: rotate(28deg); }
.theme-btn svg { width: 14px; height: 14px; }
.theme-btn .sun { display: none; }
:root[data-theme='light'] .theme-btn .sun { display: block; }
:root[data-theme='light'] .theme-btn .moon { display: none; }

/* the last link must not run off a 390px screen */
@media (max-width: 480px) {
  .nav { padding-inline: 1rem; }
  .nav__links { gap: 0.7rem; }
  .nav__mark { font-size: 0.8rem; }
  .nav__links a { font-size: 0.62rem; letter-spacing: 0.07em; }
}

.nav.is-inverted, .nav.is-inverted .nav__links a:hover { color: var(--paper); }
.nav.is-inverted .lbl, .nav.is-inverted .nav__links a { color: rgba(239, 234, 225, 0.62); }

.prog {
  position: fixed; top: 0; left: 0; height: 2px; width: 100%;
  background: var(--accent); transform: scaleX(0); transform-origin: 0 50%;
  z-index: 95; will-change: transform;
}

/* ---------- loading state ----------------------------------------------
   The archive, the reader and the home teaser all have to fetch posts.json
   before they have anything to say. What stood here was a sentence — "Reading
   the index…" — which tells a visitor the page is working but not what is
   coming, and then replaces itself with something a completely different
   height, so the page jumps.

   Bars in the shape of the thing being fetched fix both: the row that arrives
   lands where its placeholder was, and the shape itself says "a list of
   entries" before a single word of one exists. */

/* display: block because it is a <span> inside the article title and the
   prose — neither of which will take a <div> — and an inline box would give
   the bars no height and the sweep nothing to run across. */
.skel { display: none; --skel: 1.05rem; }
/* Only with a runtime. Nothing fills these in without one, and grey bars that
   never resolve read as a page that broke rather than one that is loading —
   the <noscript> beside them says what is actually going on. */
html.js .skel { display: block; }
.skel[hidden] { display: none; }

/* Off screen, still read aloud. The bars themselves are aria-hidden, so
   without this the heading is an empty element until the fetch lands. */
.vh {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

.skel__bar {
  display: block; height: var(--skel);
  border-radius: 2px;
  background: var(--line);
  /* Never the accent, and never a pulse of it. This is the page admitting it
     has nothing yet; it should sit under the reading colour, not compete with
     the entries about to replace it. */
  opacity: 0.85;
}

/* The sweep is one gradient moving across, on the container rather than on
   every bar, so a row of them reads as one surface catching light instead of
   four things blinking out of step. */
html.motion .skel {
  -webkit-mask-image: linear-gradient(100deg, #000 38%, rgba(0, 0, 0, 0.45) 50%, #000 62%);
          mask-image: linear-gradient(100deg, #000 38%, rgba(0, 0, 0, 0.45) 50%, #000 62%);
  -webkit-mask-size: 260% 100%;
          mask-size: 260% 100%;
  animation: skel-sweep 1.6s linear infinite;
}

@keyframes skel-sweep {
  from { -webkit-mask-position: 160% 0; mask-position: 160% 0; }
  to   { -webkit-mask-position: -60% 0; mask-position: -60% 0; }
}

/* An entry-shaped row: the number, the title, its summary, the meta. Same
   grid as .entry, so the real rows drop straight into these positions. */
.skel__row {
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.55rem;
  padding: clamp(1.2rem, 2.8vw, 2.1rem) 0;
  border-top: 1px solid var(--line);
}

.skel__row:last-child { border-bottom: 1px solid var(--line); }

@media (min-width: 860px) {
  .skel__row { grid-template-columns: 3.5rem minmax(0, 1fr) auto; gap: 2.5rem; align-items: start; }
}

.skel__no { width: 1.6rem; height: 0.7rem; }
.skel__title { height: clamp(1.35rem, 2.5vw, 2rem); width: min(100%, 26ch); }
.skel__sum { margin-top: 0.6rem; height: 0.8rem; width: min(100%, 48ch); }
.skel__meta { width: 7rem; height: 0.7rem; }

/* And an article: the title, its meta, then paragraphs. The widths taper the
   way a last line of a paragraph does, so it reads as prose rather than as a
   block of identical bars. */
.skel__head { height: clamp(2rem, 5vw, 3.4rem); width: min(100%, 18ch); margin-bottom: 1.1rem; }
.skel__line { height: 0.9rem; margin-bottom: 0.75rem; }
.skel__line--a { width: 96%; }
.skel__line--b { width: 88%; }
.skel__line--c { width: 61%; margin-bottom: 2rem; }

/* ---------- the steady-state scroll hint -------------------------------- */

/* Shown by core.js once the page has been still for a moment and there is
   still somewhere to go. Pinned to the bottom edge rather than to the hero,
   because it comes back further down the page as well. */
.hint {
  position: fixed; z-index: 90;
  left: 50%; bottom: clamp(1.1rem, 3vh, 2.2rem);
  display: flex; flex-direction: column; align-items: center; gap: 0.55rem;
  padding: 0.65rem 1.15rem 0.55rem;
  background: none; border: 0; color: var(--mute);
  cursor: pointer;
  opacity: 0; transform: translate(-50%, 0.75rem);
  pointer-events: none;
  transition: opacity 0.6s var(--ease), transform 0.6s var(--ease), color 0.3s ease;
}

.hint.is-on { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
.hint:hover, .hint:focus-visible { color: var(--accent); }

/* It floats over whatever happens to be at the bottom of the page, which on a
   phone is usually body text — the label was landing on a sentence and taking
   the sentence with it. So it carries its own ground.

   Nearly solid, and no backdrop blur, which is where this differs from the
   nav. The nav is a full-width bar and a hard edge across the screen would be
   heavy, so it blurs; a pill this size that blurs the words behind it reads as
   a smudge on the page rather than a chip sitting on it. */
.hint::before {
  content: '';
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  border-radius: 999px;
  background: color-mix(in srgb, var(--paper) 96%, transparent);
  border: 1px solid var(--line);
}

/* The rail is the hint. A dash falls down it and starts again, which says
   "this way" without an arrow, and reads at any size.

   Two pixels wide at 0.7, not one at 0.5. The hairline it started as measured
   **2.2:1** against the page — under the 3:1 a non-text cue needs, and the
   thing a visitor was being asked to notice. */
.hint__rail {
  position: relative; display: block;
  width: 2px; height: 2.1rem; border-radius: 1px;
  background: currentColor; opacity: 0.7;
  overflow: hidden;
}

.hint__rail i {
  position: absolute; left: 0; top: 0;
  width: 2px; height: 50%; border-radius: 1px;
  background: var(--accent);
}

html.motion .hint.is-on .hint__rail i { animation: hint-fall 1.7s linear infinite; }

/* It fades out at the bottom and in again at the top rather than parking off
   the end. The first version rested for 40% of every cycle, which left the
   rail alone on screen for three quarters of a second at a time — and the
   rail is the faint half. Now the dash is gone for a tenth of the cycle. */
@keyframes hint-fall {
  0%        { transform: translateY(-110%); opacity: 0; }
  18%       { opacity: 1; }
  88%, 100% { transform: translateY(215%); opacity: 0; }
}

.hint__lbl { letter-spacing: 0.2em; color: inherit; }

/* A phone gets more of everything. The label stays — it was dropped once for
   room, which left the hairline as the entire hint — and it is set in the
   reading colour rather than the muted one, because eleven muted pixels at
   the bottom edge of a small screen is exactly where this went unnoticed. */
@media (max-width: 560px) {
  .hint { padding: 0.6rem 1rem 0.5rem; gap: 0.5rem; color: var(--ink-2); }
  .hint__rail { height: 2rem; }
  .hint__lbl { font-size: 0.75rem; letter-spacing: 0.22em; }

  /* Out of the middle of the column and into the gutter. A phone's measure is
     the whole screen, so a chip in the centre of it lands square on the line
     someone is reading; the text is left-aligned, so the right edge is where
     the rag already is and where the eye is not returning to. */
  .hint { left: auto; right: var(--pad); transform: translate(0, 0.75rem); }
  .hint.is-on { transform: translate(0, 0); }
}

.loader, .curtain { display: none; }
html.js .curtain { display: grid; }

.curtain {
  position: fixed; inset: 0; z-index: 9990;
  grid-template-columns: repeat(5, 1fr); pointer-events: none;
}

.curtain__p { background: var(--ink); transform: scaleY(0); transform-origin: 50% 100%; will-change: transform; }

/* Held down on arrival, set by the document head before first paint, so a
   page reached through a transition does not flash into view under the
   curtain that is about to lift off it. core.js takes it from here. */
html.crossing .curtain__p { transform: scaleY(1); transform-origin: 50% 0%; }

/* The arrivals where the intro is not going to run — a second visit in the
   same tab, or a URL naming a section. Decided in the document head so the
   cover never paints; core.js removes the element a moment later. */
html.js.no-intro .loader { display: none; }

html.js .loader {
  position: fixed; inset: 0; z-index: 9995;
  background: var(--paper); display: grid; place-items: center;
  /* The footer sits at the bottom edge, so the name centres in the room above
     it rather than in the whole screen. That is what lets the type be set as
     large as it is without the last row running into the label. */
  padding-bottom: clamp(3.2rem, 10vh, 6.5rem);
  will-change: transform;
}

/* ── the typography loader ────────────────────────────────────────────────
   Devanagari at display scale, one cell per syllable group, each with its own
   displacement filter, so the change travels along the name one group at a
   time rather than arriving everywhere at once. */

.tl__defs { position: absolute; width: 0; height: 0; pointer-events: none; }

/* The rain sits behind the type, and paints its own paper ground — the trails
   are made by sheeting that colour over the canvas every frame. */
.tl__rain {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none; z-index: 0;
}

.tl__stage {
  position: relative; z-index: 1;
  /* One unit drives both axes, so the ratio between them — and therefore
     which of the two governs the glyph scale — cannot drift apart. */
  /* Both axes, because the binding constraint changes with the window: on a
     wide short one it is the height — two rows of this have to clear the
     footer — and on a narrow tall one it is the width. */
  --tl-u: clamp(1.5rem, min(9vw, 13.2vh), 8.6rem);
  --tl-gap: clamp(0.1rem, 0.6vw, 0.6rem);
  display: flex; flex-wrap: wrap; justify-content: center; align-items: baseline;
  gap: 0 var(--tl-gap);
  width: 100%; padding-inline: clamp(0.4rem, 2vw, 2rem);
  will-change: transform;
}

/* Where a line may break. There is one of these before every syllable but
   the first, and the loader turns on the ones it wants once it has worked out
   how many lines the name takes at this window size. */
.tl__break { flex-basis: 100%; height: 0; }
.tl__break--off { display: none; }

.tl__cell { will-change: transform, opacity; }

/* each cell is as wide as its own group needs, so a long one like MANI does
   not run into its neighbour */
.tl__cell { --tl-w: 2; }

.tl__svg {
  display: block;
  /* Cells are measured from the fonts rather than guessed, and the Latin ones
     are wide, so the four syllables of the first word only fit one line if
     the floor stays small. The name still fills the screen: the final swell
     is computed from what is actually left over, not fixed. */
  width: calc(var(--tl-w) * var(--tl-u));
  /* Taller than VBH/UNIT times the width unit, so width is what governs the
     glyph scale everywhere — otherwise height quietly caps it below the room
     available and the unit stops controlling the size. */
  height: calc(3 * var(--tl-u));   /* the 3 is ROW_H in typography-loader.js */
  overflow: visible;
}

/* The Devanagari is path data, baked out of AMS Chhatrapati — a legacy face
   that carries its shapes on ASCII codepoints, so it cannot be set as text
   without putting the keystrokes in the document. There is no font to name
   here and no size: the loader scales and places each path itself.

   The Latin is set as text, at a size the loader writes inline per group. */
.tl__dev { fill: var(--ink); }
.tl__lat {
  font-family: 'Playfair', var(--serif);
  font-weight: 800; letter-spacing: -0.03em; fill: var(--accent);
}

/* and the sheet it becomes, which is what wipes the page in */
.tl__sheet {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  background: var(--accent);
  will-change: transform;
}

.loader__foot {
  position: absolute; left: var(--pad); right: var(--pad);
  bottom: clamp(1.6rem, 5vw, 3rem);
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  color: var(--mute);
}

.loader__pct { font-variant-numeric: tabular-nums; color: var(--accent); }


/* ---------- entry row (home teaser + archive) --------------------------- */

.entry {
  position: relative;
  /* One column on a phone. With three children in a two-column grid the meta
     wrapped under the number and its width crushed the title. */
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.55rem; align-items: baseline;
  padding: clamp(1.2rem, 2.8vw, 2.1rem) 0;
  border-top: 1px solid var(--line);
  isolation: isolate; overflow: hidden;
  transition: padding-inline 0.55s var(--ease), color 0.4s ease;
}

.entry:last-child { border-bottom: 1px solid var(--line); }

.entry::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: var(--accent); transform: scaleY(0); transform-origin: 50% 100%;
  transition: transform 0.55s var(--ease);
}

@media (min-width: 860px) {
  .entry { grid-template-columns: 3.5rem minmax(0, 1fr) auto; gap: 2.5rem; }
  .entry:hover { padding-inline: 1.5rem; color: var(--on-accent); }
  .entry:hover::before { transform: scaleY(1); }
  .entry:hover .entry__no, .entry:hover .entry__sum, .entry:hover .entry__meta { color: var(--on-accent); }
}

.entry__no { font-family: var(--mono); font-size: var(--t-lbl); color: var(--mute); transition: color 0.4s ease; font-variant-numeric: tabular-nums; }
.entry__main { display: block; min-width: 0; }

.entry__title {
  display: block;
  font-family: var(--serif);
  font-variation-settings: 'opsz' 48;
  font-weight: 600;
  font-size: clamp(1.35rem, 2.5vw, 2rem);
  line-height: 1.12;
  letter-spacing: -0.022em;
  text-wrap: balance;
  overflow-wrap: break-word;   /* "authorization" must never run off a phone */
}

.entry__sum { display: block; margin-top: 0.6rem; max-width: 62ch; font-size: var(--t-sm); line-height: 1.5; color: var(--ink-2); transition: color 0.4s ease; }

.entry__meta {
  display: flex; flex-wrap: wrap; gap: 0.3rem 1.2rem; margin-top: 0.15rem;
  font-family: var(--mono); font-size: var(--t-lbl);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--mute);
  white-space: nowrap; transition: color 0.4s ease; font-variant-numeric: tabular-nums;
}

@media (min-width: 860px) { .entry__meta { flex-direction: column; align-items: flex-end; text-align: right; margin-top: 0; } }

.chip {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-family: var(--mono); font-size: var(--t-lbl); letter-spacing: 0.08em;
  color: var(--mute); background: transparent;
  border: 1px solid var(--line); border-radius: 100px;
  padding: 0.35rem 0.8rem; cursor: pointer;
  transition: color 0.3s ease, border-color 0.3s ease, background-color 0.3s ease;
}

.chip i { font-style: normal; opacity: 0.5; }
.chip:hover { color: var(--ink); border-color: var(--ink); }
.chip.is-on { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }

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

.foot { padding-block: clamp(4rem, 12vh, 8rem) clamp(1.5rem, 3vw, 2.5rem); }

.foot__mail {
  display: block;
  font-family: var(--serif);
  font-variation-settings: 'opsz' 144;
  font-weight: 600;
  font-size: clamp(1.7rem, 7.2vw, 7rem);
  line-height: 0.92;
  letter-spacing: -0.03em;
  word-break: break-word;
  will-change: transform;
}

.foot__mail .it { font-style: italic; }

.foot__grid {
  display: grid; grid-template-columns: 1fr; gap: 2rem;
  margin-top: clamp(3rem, 8vw, 6rem); padding-top: 1.6rem; border-top: 1px solid var(--line);
}

@media (min-width: 820px) { .foot__grid { grid-template-columns: repeat(4, 1fr); gap: 2.5rem; } }

.foot__col h4 { margin-bottom: 0.9rem; }
.foot__col li + li { margin-top: 0.4rem; }
.foot__col a, .foot__col span { font-size: var(--t-sm); color: var(--ink-2); transition: color 0.3s ease; }
.foot__col a:hover { color: var(--accent); }

.foot__stamp {
  display: flex; flex-wrap: wrap; gap: 0.8rem 2rem;
  justify-content: space-between; align-items: baseline;
  margin-top: clamp(2.5rem, 6vw, 4rem);
}

.foot__stamp b {
  font-family: var(--serif); font-variation-settings: 'opsz' 60;
  font-weight: 600; font-style: italic;
  font-size: clamp(1.05rem, 2.4vw, 1.8rem);
  letter-spacing: -0.02em; color: var(--accent);
}

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

html.js [data-r] { opacity: 0; }
html.js [data-r='up'] { transform: translateY(32px); }

@media (prefers-reduced-motion: reduce) {
  html.js [data-r] { opacity: 1 !important; transform: none !important; }
  .loader, .curtain { display: none !important; }
  * { animation: none !important; scroll-behavior: auto !important; }
}
