/* ARDHA — generated by build.mjs. Edit src/styles/*.css, not this file. */
/* ARDHA — self-hosted variable fonts. Generated by tools/fetch-fonts.mjs. */

@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("/assets/fonts/fraunces-latin.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Instrument Sans';
  font-style: normal;
  font-weight: 400 600;
  font-stretch: 100%;
  font-display: swap;
  src: url("/assets/fonts/instrument-latin.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/plexmono-latin-400.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Noto Serif Tamil';
  font-style: normal;
  font-weight: 400 600;
  font-stretch: 100%;
  font-display: swap;
  src: url("/assets/fonts/notoserif-ta-tamil.woff2") format('woff2');
  unicode-range: U+0964-0965, U+0B82-0BFA, U+200C-200D, U+20B9, U+25CC;
}

@font-face {
  font-family: 'Noto Serif Malayalam';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/assets/fonts/notoserif-ml-malayalam.woff2") format('woff2');
  unicode-range: U+0307, U+0323, U+0951-0952, U+0964-0965, U+0D00-0D7F, U+1CDA, U+1CF2, U+200C-200D, U+20B9, U+25CC, U+A830-A832;
}

/* ═══════════════════════════════════════════════════════════════════════════
   ARDHA · DESIGN TOKENS
   Every value in the system is declared once, here. Nothing downstream
   invents a colour, a duration, or an angle.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {

  /* ── TALA ─────────────────────────────────────────────────────────────────
     The rhythmic unit. Every duration on this site is a whole multiple of one
     beat, so the entire interface shares a tempo. There is no 300ms here. */

  --beat: 120ms;
  --b1:  var(--beat);                 /* a strike  — state change, hover      */
  --b2:  calc(var(--beat) * 2);       /* a step    — small entrance           */
  --b4:  calc(var(--beat) * 4);       /* a phrase  — element reveal           */
  --b8:  calc(var(--beat) * 8);       /* a cycle   — page / section transition */

  /* Two easing curves. Only two. */
  --strike:  cubic-bezier(.16, 1, .3, 1);   /* travels fast, settles long: a pose arriving */
  --sthiti:  cubic-bezier(.4, 0, .2, 1);    /* even and held: ambient only */
  /* Departures. A release is not a reversed strike — things should leave
     faster than they arrive, or the exit reads as reluctance. */
  --release: cubic-bezier(.7, 0, .84, 0);

  /* Parallax, in pixels over one viewport of scroll. Measured in pixels
     rather than screens on purpose: the moment a reader notices the
     mechanism they stop noticing the work, and ~40px is where that line is. */
  --drift-media: 24px;
  --drift-label: 12px;
  --magnet: 6px;

  /* ── ARAIMANDI ────────────────────────────────────────────────────────────
     The half-stance turns the body into a triangle; its thighs fall on a
     diagonal. That diagonal is the only non-orthogonal line in the system. */

  --angle: 24deg;
  --angle-run: 7%;   /* horizontal travel of a 24deg clip edge across a box */

  /* ── COLOUR ───────────────────────────────────────────────────────────────
     Granite at night, lit by one oil lamp. Never #000, never #fff. */

  --ink:        #0B0A0C;   /* ground */
  --granite:    #16151A;   /* primary surface */
  --granite-2:  #1F1E24;   /* raised surface */
  --granite-3:  #2A2830;   /* hairline on dark */
  --nila:       #2B3A46;   /* slate-teal — SHADOW TINT ONLY, never a fill */

  --plaster:    #EDE6DA;   /* limewash — the "light" of the site */
  --plaster-2:  #DCD3C4;
  --plaster-3:  #C9C0B4;
  --ash:        #8A8478;   /* secondary text on dark */
  --ash-d:      #6A6459;   /* secondary text on light */

  --brass:      #B08B4F;   /* antique brass — rules, tala ticks. Patina, not polish. */
  --brass-dim:  #6E5833;

  --agni:       #C2412A;   /* burnt vermilion — THE ONLY SATURATED COLOUR.
                              Never fills a shape larger than a fingertip. */
  --agni-lift:  #D4573D;   /* its hover state, nothing else */

  /* Semantic assignment — components reference these, not the raw hues. */
  --bg:         var(--ink);
  --surface:    var(--granite);
  --fg:         var(--plaster);
  --fg-muted:   var(--ash);
  --rule:       var(--granite-3);
  --accent:     var(--agni);

  /* ── TYPE ─────────────────────────────────────────────────────────────────
     Three voices. Display cuts stone, text speaks, mono catalogues. */

  --font-display: "Fraunces", "Iowan Old Style", Palatino, Georgia, serif;
  --font-text:    "Instrument Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono:    "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;
  --font-native:  "Noto Serif Tamil", "Noto Serif Malayalam", var(--font-display);
  --font-tamil:   var(--font-native);

  /* Fluid scale, 380px → 1600px viewport. Ratio ~1.28 with a hard jump at display. */
  --t-mono:  0.6875rem;                                    /* 11px, tracked  */
  --t-xs:    0.8125rem;                                    /* 13px           */
  --t-sm:    0.9375rem;                                    /* 15px           */
  --t-base:  clamp(1rem,     0.97rem + 0.15vw,  1.0625rem);
  --t-lg:    clamp(1.125rem, 1.06rem  + 0.32vw, 1.3125rem);
  --t-xl:    clamp(1.375rem, 1.21rem  + 0.80vw, 1.875rem);
  --t-2xl:   clamp(1.75rem,  1.40rem  + 1.75vw, 2.75rem);
  --t-3xl:   clamp(2.25rem,  1.55rem  + 3.50vw, 4.25rem);
  --t-4xl:   clamp(3rem,     1.60rem  + 7.00vw, 7rem);     /* her name */
  /* Threshold only. Capped against viewport HEIGHT as well as width, so a
     long two-line name can never push itself below the fold. */
  --t-5xl:   min(clamp(2.5rem, 8.6vw, 7.25rem), 13svh);

  --lh-tight: 0.94;
  --lh-snug:  1.12;
  --lh-body:  1.62;
  --measure:  62ch;   /* hard limit on body copy. Never exceeded. */

  /* ── SPACE ────────────────────────────────────────────────────────────────
     Vertical rhythm is locked to this scale so the page reads as engineered.
     Horizontal placement is deliberately irregular, by hand. */

  --s-1: 0.25rem;  --s-2: 0.5rem;   --s-3: 0.75rem;  --s-4: 1rem;
  --s-5: 1.5rem;   --s-6: 2rem;     --s-7: 3rem;     --s-8: 4rem;
  --s-9: 6rem;     --s-10: 8rem;    --s-11: 12rem;   --s-12: 16rem;

  --section-y: clamp(var(--s-9), 12vh, var(--s-11));

  /* ── STRUCTURE ────────────────────────────────────────────────────────────
     The rail owns the left margin. Content never crosses it. That enforced
     emptiness is what makes the layout read as expensive. */

  --rail-w:   clamp(2.75rem, 6vw, 5.5rem);
  --gutter:   clamp(1.25rem, 4vw, 3rem);
  --content:  76rem;
  --wide:     104rem;

  /* ── DEPTH ────────────────────────────────────────────────────────────────
     Depth comes from light and air. There is not one box-shadow in this file. */

  --grain: 0.042;          /* film grain opacity */
  --lightwell: 14% 0%;     /* the single directional source, upper-left */

  --z-grain: 90; --z-rail: 40; --z-nav: 50; --z-skip: 100;
}

/* Motion is a luxury, not a requirement. Honoured completely. */
@media (prefers-reduced-motion: reduce) {
  :root { --b1: 1ms; --b2: 1ms; --b4: 1ms; --b8: 1ms; --angle-run: 0%; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   ARDHA · BASE — reset, atmosphere, typography
   ═══════════════════════════════════════════════════════════════════════════ */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The rail's active tick is driven by this; see 03-components.css */
  scroll-timeline-name: --page;
  scroll-timeline-axis: block;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-text);
  font-size: var(--t-base);
  line-height: var(--lh-body);
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  min-height: 100vh;
}

img, video, svg { display: block; max-width: 100%; height: auto; }
button, input, textarea, select { font: inherit; color: inherit; }

/* ── ATMOSPHERE ─────────────────────────────────────────────────────────────
   Four global layers: grain, vignette, lightwell, and atmospheric perspective.
   Nothing here is decoration; this is how depth is built without shadows. */

/* 1 · GRAIN — fixed to the viewport, so it reads as film stock, not texture.
   It also kills banding in the dark gradients below. */
body::before {
  content: "";
  position: fixed; inset: -50%;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: var(--grain);
  background-image: url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'>\
<filter id='g'><feTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/>\
<feColorMatrix type='saturate' values='0'/></filter>\
<rect width='180' height='180' filter='url(%23g)'/></svg>");
  will-change: transform;
}

/* 2 · VIGNETTE — barely perceptible. Focuses the eye the way a centred
   layout normally would, which is why we can afford to be asymmetric. */
body::after {
  content: "";
  position: fixed; inset: 0;
  z-index: calc(var(--z-grain) - 1);
  pointer-events: none;
  background: radial-gradient(120% 100% at 50% 40%,
              transparent 42%, rgb(0 0 0 / .34) 100%);
}

/* 3 · LIGHTWELL — one warm source, fixed upper-left at a stage-wash angle.
   Because it never changes direction, the whole site is one physical room. */
.lightwell { position: relative; isolation: isolate; }
.lightwell::before {
  content: "";
  position: absolute; inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(70% 55% at var(--lightwell),
      rgb(176 139 79 / .085) 0%, rgb(176 139 79 / .028) 38%, transparent 72%),
    radial-gradient(90% 70% at 92% 108%,
      rgb(43 58 70 / .30) 0%, transparent 68%);
}

/* 4 · ATMOSPHERIC PERSPECTIVE — receding elements do not shrink, they
   desaturate and lift toward the granite tone, the way air behaves. */
.recede   { filter: saturate(.72) contrast(.94); opacity: .82; }
.recede-2 { filter: saturate(.52) contrast(.88); opacity: .62; }

/* ── TYPOGRAPHY ─────────────────────────────────────────────────────────────
   Fraunces' variable axes are used, not defaulted: WONK 0 / SOFT 0 at high
   optical size gives sharp spurs and high contrast — cut stone, not calligraphy. */

h1, h2, h3, h4, .display {
  font-family: var(--font-display);
  font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 144;
  font-weight: 400;
  line-height: var(--lh-tight);
  letter-spacing: -0.021em;
  text-wrap: balance;
  font-feature-settings: "ss01";
}

h1 { font-size: var(--t-3xl); }
h2 { font-size: var(--t-2xl); }
h3 { font-size: var(--t-xl); letter-spacing: -0.014em; }
h4 { font-size: var(--t-lg); letter-spacing: -0.008em; }

.display--hero {
  font-size: var(--t-5xl);
  font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 144;
  letter-spacing: -0.038em;
  line-height: 0.86;
}
.display--xl { font-size: var(--t-4xl); letter-spacing: -0.03em; }

/* The archival voice. This is the credibility engine: metadata set in tracked
   monospace makes the work read as catalogued rather than advertised. */
.mono, .meta {
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  font-weight: 450;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--fg-muted);
  font-feature-settings: "tnum";
}
.meta--brass  { color: var(--brass); }
.meta--accent { color: var(--agni); }

/* Native script appears as a secondary mark, never decoration — and only for
   the languages she actually works in. */
.tamil, .native {
  font-family: var(--font-native);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.5;
}

p { max-width: var(--measure); }
.lede {
  font-size: var(--t-lg);
  line-height: 1.52;
  color: var(--plaster-2);
  max-width: 34ch;
  letter-spacing: -0.006em;
}
.prose > * + * { margin-block-start: var(--s-5); }
.prose { color: var(--plaster-3); hyphens: none; }

/* Text never justified, never hyphenated, never wider than the measure. */

/* ── LINKS ──────────────────────────────────────────────────────────────────
   The underline is the interaction. Vermilion appears on one axis only. */

a { color: inherit; text-decoration: none; }

.link {
  position: relative;
  display: inline-block;
  padding-block-end: 1px;
  background-image: linear-gradient(var(--agni), var(--agni));
  background-size: 0% 1px;
  background-position: 0 100%;
  background-repeat: no-repeat;
  transition: background-size var(--b2) var(--strike), color var(--b1) linear;
}
.link:hover, .link:focus-visible { background-size: 100% 1px; color: var(--plaster); }

/* ── FOCUS ──────────────────────────────────────────────────────────────────
   Accessibility is part of the art direction. The focus ring is the one place
   vermilion is allowed to be loud, because it must be. */

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

.skip {
  position: absolute; top: var(--s-3); left: var(--s-3);
  z-index: var(--z-skip);
  padding: var(--s-3) var(--s-5);
  background: var(--agni); color: var(--plaster);
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: .14em; text-transform: uppercase;
  transform: translateY(-160%);
  transition: transform var(--b2) var(--strike);
}
.skip:focus { transform: translateY(0); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   ARDHA · STRUCTURE — the rail, the grid, navigation
   ═══════════════════════════════════════════════════════════════════════════ */

body { padding-inline-start: var(--rail-w); }

.wrap {
  width: 100%;
  max-width: var(--content);
  padding-inline: var(--gutter);
  margin-inline: 0;                 /* NOT auto. The page is left-anchored. */
}
.wrap--wide { max-width: var(--wide); }

/* A 12-column field. Templates place content off-axis by hand; there is no
   default centring anywhere in this system. */
.grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--s-6);
}
@media (max-width: 60rem) { .grid { grid-template-columns: repeat(6, minmax(0, 1fr)); } }

.section { padding-block: var(--section-y); position: relative; }
.section--tight { padding-block: var(--s-8); }

/* A sheared rule — the only divider in the system, cut on the araimandi angle. */
.rule {
  height: 1px; border: 0;
  background: linear-gradient(90deg, var(--brass-dim), transparent 62%);
  transform: skewY(calc(var(--angle) * -0.04));
  margin-block: var(--s-7);
}

/* ── THE TALA RAIL ──────────────────────────────────────────────────────────
   Eight ticks: one adi-tala cycle. The active tick advances with scroll.
   Non-interactive and nearly silent — its only job is to make the page feel
   counted. This is the site's signature element. */

.rail {
  position: fixed;
  inset-block: 0;
  inset-inline-start: 0;
  width: var(--rail-w);
  z-index: var(--z-rail);
  pointer-events: none;
  display: grid;
  grid-template-rows: 1fr auto 1fr;
  justify-items: center;
  padding-block: var(--s-6);
}

/* The continuous line the ticks sit on. */
.rail__line {
  grid-row: 1 / -1;
  grid-column: 1;
  width: 1px;
  justify-self: center;
  background: linear-gradient(180deg, transparent, var(--granite-3) 12%,
                              var(--granite-3) 88%, transparent);
  position: relative;
}

/* Completed portion of the cycle, driven by document scroll progress. */
.rail__fill {
  position: absolute; inset-inline: 0; inset-block-start: 0;
  height: 100%;
  background: linear-gradient(180deg, var(--brass-dim), var(--brass));
  transform-origin: top;
  scale: 1 var(--progress, 0);
}
@supports (animation-timeline: scroll()) {
  .rail__fill {
    scale: 1 0;
    animation: rail-fill linear both;
    animation-timeline: scroll(root block);
  }
}
@keyframes rail-fill { from { scale: 1 0; } to { scale: 1 1; } }

.rail__ticks {
  grid-row: 2; grid-column: 1;
  display: grid;
  gap: clamp(var(--s-3), 1.6vh, var(--s-5));
  justify-items: center;
  background: var(--bg);
  padding-block: var(--s-4);
}

.rail__tick {
  width: 7px; height: 1px;
  background: var(--granite-3);
  transition: width var(--b1) var(--strike),
              background-color var(--b1) linear;
}
.rail__tick.is-on { width: 13px; background: var(--brass); }
/* The downbeat. Vermilion, one pixel tall, the width of a fingernail. */
.rail__tick.is-now { width: 19px; background: var(--agni); height: 2px; }

.rail__count {
  grid-row: 3; grid-column: 1;
  align-self: end;
  writing-mode: vertical-rl;
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  letter-spacing: .32em;
  color: var(--brass-dim);
  text-transform: uppercase;
}

@media (max-width: 40rem) {
  .rail__ticks { gap: var(--s-2); }
  .rail__count { display: none; }
}

/* ── NAVIGATION ─────────────────────────────────────────────────────────────
   Set in the archival voice, small and tracked. It recedes until needed. */

.nav {
  position: fixed;
  inset-block-start: 0;
  inset-inline: var(--rail-w) 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-5);
  padding: var(--s-5) var(--gutter);
  mix-blend-mode: normal;
  transition: background-color var(--b4) var(--sthiti),
              backdrop-filter var(--b4) var(--sthiti);
}
.nav[data-scrolled="true"] {
  background: color-mix(in srgb, var(--ink) 82%, transparent);
  backdrop-filter: blur(14px) saturate(1.2);
}

.nav__mark {
  font-family: var(--font-display);
  font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 72;
  font-size: var(--t-lg);
  letter-spacing: -0.02em;
  line-height: 1;
}
.nav__list { display: flex; gap: clamp(var(--s-4), 2.2vw, var(--s-7)); list-style: none; padding: 0; }
.nav__link {
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--ash);
  padding-block-end: 3px;
  border-block-end: 1px solid transparent;
  transition: color var(--b1) linear, border-color var(--b1) linear;
}
.nav__link:hover, .nav__link:focus-visible { color: var(--plaster); }
.nav__link[aria-current="page"] { color: var(--plaster); border-block-end-color: var(--agni); }

/* Mobile: the list collapses to a single sheet, opened by a mono toggle. */
.nav__toggle { display: none; background: none; border: 0; cursor: pointer; }
@media (max-width: 48rem) {
  .nav__toggle {
    display: block;
    /* .nav is a flex row of three: mark, toggle, and the <nav> that wraps the
       list. The list is fixed-position so its wrapper is 0px wide — but with
       space-between it still claims the right-hand slot, which left the
       toggle stranded in the middle of the bar. */
    margin-inline-start: auto;
    font-family: var(--font-mono); font-size: var(--t-mono);
    letter-spacing: .15em; text-transform: uppercase; color: var(--ash);
  }
  /* The sheet is fixed over the whole screen, so the bar's own controls have
     to be lifted above it — otherwise opening the menu buries the Close
     button under it and a phone, with no Escape key, has no way out. */
  .nav__mark, .nav__toggle { position: relative; z-index: 2; }

  .nav__list {
    position: fixed;
    /* Full bleed, including over the rail. Stopping at the rail left a strip
       of the aperture clips showing down the left edge of an otherwise opaque
       sheet, which read as a rendering fault rather than a detail. */
    inset: 0;
    z-index: 1;
    flex-direction: column;
    /* Anchored to the floor, like the name on the threshold. Centred, the
       items floated in the middle of the screen with a third of the sheet
       empty beneath them and nothing holding them there. */
    justify-content: flex-end;
    align-items: flex-start;
    gap: 0;
    padding: var(--gutter) var(--gutter)
             calc(var(--s-9) + env(safe-area-inset-bottom, 0px));
    padding-inline-start: calc(var(--rail-w) + var(--gutter));
    /* Fully opaque. At 97% the aperture clips showed through as a bright
       sliver down the left edge — clip-path and backdrop-filter do not
       reliably combine, so the blur was not doing the work it looked like. */
    background: var(--ink);
    clip-path: polygon(0 0, 100% 0, 100% 0, 0 0);
    transition: clip-path var(--b4) var(--strike);
  }
  .nav__list[data-open="true"] { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); }

  /* Set in the display face, at a size worth crossing a room for. Tracked
     mono at 11px is the archival voice — right for a raga, wrong for the
     only navigation on a phone. */
  .nav__list .nav__link {
    font-family: var(--font-display);
    font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 96;
    font-size: var(--t-2xl);
    font-weight: 400;
    letter-spacing: -0.02em;
    text-transform: none;
    line-height: 1.12;
    color: var(--plaster);
    padding-block: var(--s-3);
    border: 0;
  }
  .nav__list .nav__link[aria-current="page"] { color: var(--brass); }
  .nav__list .nav__link[aria-current="page"]::after {
    content: "";
    display: inline-block;
    width: 6px; height: 6px;
    margin-inline-start: var(--s-3);
    background: var(--agni);
    rotate: 45deg;
    translate: 0 -0.35em;
  }

  /* Each arrives a beat after the last, so the sheet opens as a phrase. */
  .nav__list[data-open="true"] li {
    animation: arrive var(--b2) var(--strike) both;
  }
  .nav__list[data-open="true"] li:nth-child(1) { animation-delay: var(--b1); }
  .nav__list[data-open="true"] li:nth-child(2) { animation-delay: calc(var(--beat) * 1.5); }
  .nav__list[data-open="true"] li:nth-child(3) { animation-delay: var(--b2); }
  .nav__list[data-open="true"] li:nth-child(4) { animation-delay: calc(var(--beat) * 2.5); }
  .nav__list[data-open="true"] li:nth-child(5) { animation-delay: calc(var(--beat) * 3); }
  .nav__list[data-open="true"] li:nth-child(6) { animation-delay: calc(var(--beat) * 3.5); }
}

/* ── FOOTER ─────────────────────────────────────────────────────────────────
   Back to near-black. One email, one line. No form, no modal, no social wall. */

.foot {
  padding-block: var(--s-9) var(--s-6);
  border-block-start: 1px solid var(--granite-3);
}
.foot__grid { display: flex; flex-wrap: wrap; gap: var(--s-6) var(--s-9); align-items: end; justify-content: space-between; }
.foot__mail {
  font-family: var(--font-display);
  font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 96;
  font-size: var(--t-xl);
  letter-spacing: -0.02em;
}

/* ═══════════════════════════════════════════════════════════════════════════
   ARDHA · MOTION — "tala, not drift"
   Every duration is a whole multiple of one beat. Entrances arrive and hold:
   a short translation plus a clip-path wipe along the 24deg araimandi diagonal.
   Never a bare fade. Never a bounce. Never a spring.
   ═══════════════════════════════════════════════════════════════════════════ */

@keyframes arrive {
  from {
    opacity: 0;
    transform: translateY(14px);
    clip-path: polygon(0 100%, 100% calc(100% - var(--angle-run)), 100% 100%, 0 100%);
  }
  to {
    opacity: 1;
    transform: translateY(0);
    clip-path: polygon(0 0, 100% calc(0% - var(--angle-run)), 100% 100%, 0 100%);
  }
}

/* Without JavaScript and without scroll-driven animation support, content is
   simply, correctly visible. The reveal is an enhancement, never a gate. */
.reveal { opacity: 1; }

/* Preferred path: CSS-native scroll-driven animation. No library, no JS in the
   critical path, no scroll listener, no jank. */
@supports (animation-timeline: view()) {
  .reveal {
    animation: arrive both var(--strike);
    animation-timeline: view();
    animation-range: entry 4% cover 26%;
  }
}

/* Fallback path: a tiny IntersectionObserver adds .is-in. JS adds .js to <html>
   only when it will actually run the observer, so nothing is ever left hidden. */
.js-io .reveal { opacity: 0; }
.js-io .reveal.is-in {
  animation: arrive var(--b4) var(--strike) both;
}

/* Staggering: whole beats only, so a group of siblings reads as a phrase. */
.reveal[data-beat="1"] { animation-delay: var(--b1); }
.reveal[data-beat="2"] { animation-delay: var(--b2); }
.reveal[data-beat="3"] { animation-delay: calc(var(--beat) * 3); }
.reveal[data-beat="4"] { animation-delay: var(--b4); }

/* Scroll-driven animations are progress-based, so delays must not apply there. */
@supports (animation-timeline: view()) {
  .reveal[data-beat] { animation-delay: 0s; }
  .reveal[data-beat="1"] { animation-range: entry 6%  cover 30%; }
  .reveal[data-beat="2"] { animation-range: entry 9%  cover 34%; }
  .reveal[data-beat="3"] { animation-range: entry 12% cover 38%; }
  .reveal[data-beat="4"] { animation-range: entry 15% cover 42%; }
}

/* The site loses its rhythm and keeps all of its content. Non-negotiable. */
@media (prefers-reduced-motion: reduce) {
  .reveal, .js-io .reveal, .js-io .reveal.is-in {
    animation: none !important;
    opacity: 1 !important;
    clip-path: none !important;
    transform: none !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   ARDHA · MEDIA — three crops, one corner cut, one grade
   ═══════════════════════════════════════════════════════════════════════════ */

/* One corner cut, on the araimandi angle. Never all four, never a radius.
   run / rise = tan(24deg) = 0.4452  ->  rise = run x 2.246 */
.frame {
  --cut-x: clamp(1rem, 2.2vw, 2rem);
  --cut-y: calc(var(--cut-x) * 2.246);
  position: relative;
  overflow: hidden;
  background: var(--granite-2);
  clip-path: polygon(0 0, calc(100% - var(--cut-x)) 0, 100% var(--cut-y), 100% 100%, 0 100%);
}
.frame--flat { clip-path: none; }

/* THREE CROPS. ONLY THREE. NO EXCEPTIONS. */
.frame--body   { aspect-ratio: 2 / 3;  }  /* the pose: full figure, araimandi, mandi */
.frame--stage  { aspect-ratio: 16 / 9; }  /* the dancer in architecture: scale, light */
.frame--detail { aspect-ratio: 1 / 1;  }  /* mudra, ankle bells, eyes — the signature */

.frame img, .frame video {
  width: 100%; height: 100%;
  object-fit: cover;
  /* The single grade that makes fifteen shoots read as one body of work:
     55% duotone (shadows -> nila, highlights -> plaster) over 45% original,
     so skin, silk and jewellery stay legible. Defined in the page's SVG defs. */
  filter: url(#grade);
}

/* Hover stutters in three quantised steps rather than zooming smoothly —
   the rhythmic footwork of a jati, made into an interaction. */
.frame--live img {
  transition: scale var(--b4) steps(3, end), filter var(--b4) steps(3, end);
  will-change: scale;
}
.frame--live:hover img,
.frame--live:focus-within img { scale: 1.055; filter: url(#grade) brightness(1.08); }

/* Placeholder state, so the layout is correct before photography lands and
   nothing shifts when it does. */
.frame--empty {
  display: grid; place-items: center;
  background:
    linear-gradient(135deg, var(--granite-2), var(--granite));
  border: 1px solid var(--granite-3);
}
.frame--empty::after {
  content: attr(data-slot);
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  letter-spacing: .2em; text-transform: uppercase;
  color: var(--brass-dim);
}

/* Caption in the archival voice, hung under the frame on the left. */
.figure { margin: 0; }
.figure__cap {
  display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4);
  align-items: baseline;
  margin-block-start: var(--s-3);
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--ash);
}
.figure__cap .tamil { letter-spacing: 0; text-transform: none; color: var(--brass); font-size: var(--t-xs); }

/* Video: chrome stripped to nothing. The work is allowed to be longer than
   the design. */
.player {
  position: relative;
  background: #000;
  border: 1px solid var(--granite-3);
}
.player video, .player iframe {
  width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;
  filter: none;   /* moving image is never graded — only stills are */
}

/* ── THE HELD LOOP ──────────────────────────────────────────────────────────
   The threshold's moving image, inside the frame rather than behind the page. */
.frame__video {
  width: 100%; height: 100%;
  object-fit: cover;
  filter: url(#grade-soft);   /* a lighter hand — see the note in layout.mjs */
}

/* WCAG 2.2.2: moving content that autoplays for more than five seconds needs
   a way to stop it. Set in the archival voice and kept nearly invisible until
   wanted — it is a control, not a call to action. */
.loop__toggle {
  position: absolute;
  inset-block-end: var(--s-3); inset-inline-start: var(--s-3);
  z-index: 2;
  padding: var(--s-2) var(--s-3);
  background: color-mix(in srgb, var(--ink) 62%, transparent);
  backdrop-filter: blur(6px);
  border: 1px solid color-mix(in srgb, var(--plaster) 22%, transparent);
  color: var(--plaster);
  font-family: var(--font-mono); font-size: 0.5625rem;
  letter-spacing: .18em; text-transform: uppercase;
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--b2) var(--sthiti), border-color var(--b1) linear;
}
[data-loop]:hover .loop__toggle,
.loop__toggle:focus-visible { opacity: 1; }
.loop__toggle:hover { border-color: var(--agni); }
/* Never hide it from someone navigating by keyboard or touch. */
@media (hover: none) { .loop__toggle { opacity: .85; } }

/* ═══════════════════════════════════════════════════════════════════════════
   ARDHA · COMPONENTS
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── SECTION LABEL ──────────────────────────────────────────────────────────
   Movement number + name, in the archival voice, with a brass tick. */
.label {
  display: flex; align-items: baseline; gap: var(--s-4);
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: .22em; text-transform: uppercase;
  color: var(--brass);
  margin-block-end: var(--s-6);
}
.label::before {
  content: ""; width: clamp(1.5rem, 4vw, 3.5rem); height: 1px;
  background: var(--brass-dim); translate: 0 -0.3em;
}
.label__n { color: var(--agni); }

/* ── THRESHOLD ──────────────────────────────────────────────────────────────
   House lights down. One held image, her name cut in stone, almost no UI.
   Entry is earned by scrolling — there is no bouncing chevron begging for it. */
.threshold {
  min-height: 100svh;
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.8fr);
  /* invocation at the top, image breathing in the middle, name anchored to
     the floor. The eye falls the way a dancer settles into the stance. */
  grid-template-rows: auto minmax(var(--s-8), 1fr) auto;
  column-gap: clamp(var(--s-6), 5vw, var(--s-9));
  padding: calc(var(--s-10) + 2vh) var(--gutter) clamp(var(--s-7), 7vh, var(--s-9));
  position: relative;
}

.threshold__invocation {
  grid-column: 1; grid-row: 1;
  max-width: 28ch;
}

.threshold__media {
  grid-column: 2; grid-row: 1 / -1;
  display: grid;
  align-content: center;
  justify-items: end;
  /* Bleeds to the right edge and stops hard on the left. An image that stops
     politely inside the gutter on both sides is a template. */
  margin-inline-end: calc(var(--gutter) * -1);
}
.threshold__media .frame {
  --cut-x: clamp(1.4rem, 3vw, 3rem);
  /* The hero bleeds off the right edge, which takes the usual top-right cut
     off-screen with it. The motif moves to the inboard corner — the one
     facing the name — so the 24deg angle still reads on the most important
     image on the site. One corner, always the one you can see. */
  clip-path: polygon(var(--cut-x) 0, 100% 0, 100% 100%, 0 100%, 0 var(--cut-y));
  height: min(64svh, 34rem);
  width: auto;
  max-width: 100%;
}

.threshold__name {
  grid-column: 1; grid-row: 3;
  align-self: end;
  position: relative;
  z-index: 2;
}
.threshold__name .display--hero { display: block; }
/* Second line pulled in under the first — the name reads as one carved block. */
.threshold__name .display--hero + .display--hero { margin-block-start: -0.1em; margin-inline-start: 0.05em; }

@media (max-width: 62rem) {
  .threshold {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto 1fr auto;
    row-gap: var(--s-6);
  }
  .threshold__invocation { grid-column: 1; grid-row: 1; }
  .threshold__media { grid-column: 1; grid-row: 2; justify-items: start;
    margin-inline-end: 0; margin-inline-start: calc(var(--gutter) * -1); }
  .threshold__media .frame { height: min(44svh, 22rem); }
  .threshold__name { grid-column: 1; grid-row: 3; }
}

/* ── REPERTOIRE ─────────────────────────────────────────────────────────────
   An editorial list, never a card grid. Density signals seriousness: raga,
   tala, composer, duration, premiere — the catalogue, not the flyer. */
.rep { list-style: none; padding: 0; margin: 0; border-block-start: 1px solid var(--granite-3); }

.rep__item { border-block-end: 1px solid var(--granite-3); position: relative; }

.rep__row {
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 1.35fr) minmax(0, 1fr) 7rem;
  gap: var(--s-5);
  align-items: baseline;
  padding-block: var(--s-6);
  padding-inline-end: var(--s-4);
  transition: padding-inline-start var(--b2) var(--strike),
              background-color var(--b2) var(--sthiti);
  padding-inline-start: 0;
  cursor: default;
}
.rep__item:hover .rep__row,
.rep__item:focus-within .rep__row {
  padding-inline-start: var(--s-4);
  background: linear-gradient(90deg, color-mix(in srgb, var(--nila) 22%, transparent), transparent 46%);
}

.rep__n { font-family: var(--font-mono); font-size: var(--t-mono); letter-spacing: .16em; color: var(--brass-dim); }
.rep__item:hover .rep__n { color: var(--agni); }

.rep__title {
  font-family: var(--font-display);
  font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 96;
  font-size: var(--t-xl);
  letter-spacing: -0.018em;
  line-height: var(--lh-snug);
}
.rep__tamil { display: block; font-size: var(--t-sm); color: var(--brass); margin-block-start: var(--s-1); }
.rep__note { color: var(--ash); font-size: var(--t-sm); line-height: 1.55; max-width: 44ch; }
.rep__dur { font-family: var(--font-mono); font-size: var(--t-mono); letter-spacing: .14em; color: var(--ash); text-align: right; }

/* Metadata chips — the archival register. */
.specs { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-5); margin-block-start: var(--s-3); }
.spec { font-family: var(--font-mono); font-size: var(--t-mono); letter-spacing: .14em; text-transform: uppercase; color: var(--ash); }
.spec b { font-weight: 450; color: var(--brass-dim); margin-inline-end: var(--s-2); }

/* Mobile placement lives in 07-mobile.css, where the whole row is restated. */

/* ── LINEAGE ────────────────────────────────────────────────────────────────
   Parampara as one unbroken vertical line, because the tradition is
   continuous. Not cards. */
.lineage { position: relative; padding-inline-start: var(--s-7); list-style: none; margin: 0; }
.lineage::before {
  content: ""; position: absolute; inset-block: 0.6em 0; inset-inline-start: 3px; width: 1px;
  background: linear-gradient(180deg, var(--brass), var(--brass-dim) 70%, transparent);
}
.lineage__item { position: relative; padding-block-end: var(--s-8); }
.lineage__item::before {
  content: ""; position: absolute; inset-inline-start: calc(var(--s-7) * -1);
  inset-block-start: 0.62em; width: 7px; height: 7px;
  background: var(--brass); rotate: 45deg;
}
.lineage__item:first-child::before { background: var(--agni); }
.lineage__years { font-family: var(--font-mono); font-size: var(--t-mono); letter-spacing: .18em; color: var(--brass-dim); }
.lineage__who { font-family: var(--font-display); font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 72; font-size: var(--t-lg); margin-block: var(--s-2) var(--s-3); }

/* ── THE RECORD ─────────────────────────────────────────────────────────────
   Deliberately unglamorous. Monospace, plain rules, real file sizes. Curators
   and juries trust a page that is not trying to seduce them. This page converts. */
.record { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: var(--t-xs); }
.record caption { text-align: left; }
.record th {
  text-align: left; font-weight: 450; font-size: var(--t-mono);
  letter-spacing: .2em; text-transform: uppercase; color: var(--brass-dim);
  padding-block: var(--s-3); border-block-end: 1px solid var(--granite-3);
}
.record td { padding-block: var(--s-4); border-block-end: 1px solid var(--granite-3); vertical-align: baseline; color: var(--plaster-3); }
.record td:first-child { color: var(--plaster); padding-inline-end: var(--s-5); }
.record tr:hover td { background: color-mix(in srgb, var(--nila) 14%, transparent); }
.record .size { color: var(--ash); white-space: nowrap; }

.dl { display: inline-flex; align-items: center; gap: var(--s-3); color: var(--plaster); }
.dl::after {
  content: ""; width: 9px; height: 9px;
  border-inline-end: 1px solid var(--agni); border-block-end: 1px solid var(--agni);
  rotate: 45deg; translate: 0 -2px;
  transition: translate var(--b1) var(--strike);
}
.dl:hover::after, .dl:focus-visible::after { translate: 0 2px; }

/* A CV / credits list: year on the left in mono, entry on the right. */
.ledger { list-style: none; padding: 0; margin: 0; }
.ledger li {
  display: grid; grid-template-columns: 5.5rem minmax(0, 1fr); gap: var(--s-5);
  padding-block: var(--s-4); border-block-end: 1px solid var(--granite-3);
  align-items: baseline;
}
.ledger li > :first-child { font-family: var(--font-mono); font-size: var(--t-mono); letter-spacing: .16em; color: var(--brass-dim); }
.ledger b { font-weight: 400; display: block; }
.ledger small { color: var(--ash); font-size: var(--t-xs); }
@media (max-width: 40rem) { .ledger li { grid-template-columns: minmax(0, 1fr); gap: var(--s-1); } }

/* ── PLATE ──────────────────────────────────────────────────────────────────
   The light section. Limewash on granite — used once, for the invocation, so
   the dark -> light -> dark arc is felt physically. */
.plate { background: var(--plaster); color: var(--ink); }
.plate .lede, .plate .prose { color: color-mix(in srgb, var(--ink) 82%, var(--plaster)); }
.plate .mono, .plate .meta, .plate .spec { color: var(--ash-d); }
.plate .label { color: color-mix(in srgb, var(--brass) 72%, var(--ink)); }
.plate .label::before { background: color-mix(in srgb, var(--brass) 60%, var(--plaster)); }
.plate .rule { background: linear-gradient(90deg, color-mix(in srgb, var(--ink) 30%, var(--plaster)), transparent 62%); }
.plate .link:hover { color: var(--ink); }

/* ── ACTION ─────────────────────────────────────────────────────────────────
   Not a button. A ruled line that fills on the araimandi angle. */
.action {
  display: inline-flex; align-items: center; gap: var(--s-4);
  font-family: var(--font-mono); font-size: var(--t-mono);
  letter-spacing: .2em; text-transform: uppercase;
  padding: var(--s-4) var(--s-6);
  border: 1px solid var(--granite-3);
  position: relative; overflow: hidden;
  color: var(--plaster);
  transition: color var(--b2) linear, border-color var(--b2) linear;
}
.action::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: var(--plaster);
  clip-path: polygon(0 100%, 100% calc(100% - var(--angle-run)), 100% 100%, 0 100%);
  transition: clip-path var(--b4) var(--strike);
}
.action:hover, .action:focus-visible { color: var(--ink); border-color: var(--plaster); }
.action:hover::before, .action:focus-visible::before {
  clip-path: polygon(0 0, 100% calc(0% - var(--angle-run)), 100% 100%, 0 100%);
}
.plate .action { border-color: color-mix(in srgb, var(--ink) 22%, var(--plaster)); color: var(--ink); }
.plate .action::before { background: var(--ink); }
.plate .action:hover, .plate .action:focus-visible { color: var(--plaster); border-color: var(--ink); }

/* ── GALLERY ────────────────────────────────────────────────────────────────
   An editorial scatter on a strict baseline. No masonry, no lightbox, no
   uniform thumbnail grid, no carousel dots. */
.scatter { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--gutter); row-gap: var(--s-8); align-items: start; }
.scatter > * { grid-column: span 4; }
.scatter > .is-body   { grid-column: span 4; }
.scatter > .is-stage  { grid-column: span 7; }
.scatter > .is-detail { grid-column: span 3; }
.scatter > .drop-1 { margin-block-start: var(--s-8); }
.scatter > .drop-2 { margin-block-start: var(--s-10); }
@media (max-width: 62rem) {
  .scatter { grid-template-columns: repeat(6, minmax(0, 1fr)); row-gap: var(--s-7); }
  .scatter > * , .scatter > .is-stage { grid-column: span 6; }
  .scatter > .is-detail { grid-column: span 3; }
  .scatter > .drop-1, .scatter > .drop-2 { margin-block-start: 0; }
}

/* A shoot heading: photographer credit in the archival voice. */
.shoot__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-3) var(--s-6); margin-block-end: var(--s-6); }

/* ── DRAFT FLAG ─────────────────────────────────────────────────────────────
   Shown while any record still holds invented placeholder text. Invented
   credentials read as fact to a programmer or a jury, so the site refuses to
   present itself as finished until they are gone. Removed automatically. */
.draft-flag {
  position: fixed;
  inset-block-end: 0; inset-inline: var(--rail-w) 0;
  z-index: var(--z-nav);
  margin: 0;
  padding: var(--s-3) var(--gutter);
  background: var(--agni);
  color: var(--plaster);
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  letter-spacing: .16em;
  text-transform: uppercase;
}
.is-draft .foot { padding-block-end: var(--s-9); }
@media print { .draft-flag { display: none; } }

/* ── A CATALOGUE ROW AS A LINK ──────────────────────────────────────────────
   The whole row is the target, not just the title — on a phone especially,
   a heading-sized tap target is the difference between usable and not. */
.rep__row--link { text-decoration: none; color: inherit; cursor: pointer; }
.rep__row--link:focus-visible { outline-offset: -2px; }

/* An arrow that arrives on the araimandi angle, like everything else. */
.rep__go {
  display: inline-block;
  width: 9px; height: 9px;
  margin-inline-start: var(--s-3);
  border-block-start: 1px solid var(--brass-dim);
  border-inline-end: 1px solid var(--brass-dim);
  rotate: 45deg;
  translate: 0 1px;
  transition: translate var(--b2) var(--strike), border-color var(--b1) linear;
}
.rep__item:hover .rep__go,
.rep__row--link:focus-visible .rep__go { translate: 4px 1px; border-color: var(--agni); }

.form-pager {
  display: flex; justify-content: space-between; gap: var(--s-5);
  margin-block-start: var(--s-6);
}
@media (max-width: 40rem) {
  .form-pager { flex-direction: column; gap: var(--s-3); }
}

/* A quiet admission, not a warning banner. Shown while a gallery is still
   holding someone else's photographs, and removed by unticking one box. */
.stand-in {
  font-family: var(--font-mono);
  font-size: var(--t-mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--brass-dim);
  border-inline-start: 1px solid var(--agni);
  padding-inline-start: var(--s-4);
  margin-block-end: var(--s-7);
  max-width: 54ch;
}

/* ═══════════════════════════════════════════════════════════════════════════
   ARDHA · MOBILE
   The design is composed for a wide asymmetric field. A phone has no field,
   so rather than shrinking the desktop composition this restates it: one
   column, the same rhythm, the same restraint.
   Kept in one file so the mobile intent is reviewable in one place.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 60rem) {

  /* ── THE STRUCTURAL FIX ───────────────────────────────────────────────────
     Templates place content with inline `grid-column: 9 / span 4` and similar,
     written against the 12-column field. Below 60rem the grid is 6 columns, so
     a start line of 9 does not exist: the browser invents implicit columns off
     the side of the screen. That is what pushed the contact page 213px wide and
     let the aside land on top of the email.

     Everything becomes full width here, which is what the composition wants on
     a phone anyway. !important is needed to beat the inline styles, and this is
     the one place in the system that uses it. */
  .grid > * { grid-column: 1 / -1 !important; }
  .grid { row-gap: var(--s-7); }

  /* ── THE RAIL ─────────────────────────────────────────────────────────────
     At desktop the reserved margin is a luxury. At 390px it was eating 11% of
     the screen, which is width the type needs more. */
  :root { --rail-w: clamp(1.75rem, 7vw, 2.75rem); }
  .rail__ticks { gap: var(--s-2); padding-block: var(--s-3); }
  .rail__tick { width: 5px; }
  .rail__tick.is-on { width: 9px; }
  .rail__tick.is-now { width: 13px; }

  /* ── TYPE ─────────────────────────────────────────────────────────────────
     The name is the hero. Its clamp floor was 2.5rem, which on a phone left it
     at 40px — smaller than the sentence above it. */
  :root {
    --t-5xl: min(clamp(3.25rem, 15vw, 7.25rem), 17svh);
    --t-4xl: clamp(2.5rem, 11vw, 4.25rem);
    --section-y: clamp(var(--s-8), 9vh, var(--s-9));
  }
  .lede { max-width: none; font-size: var(--t-lg); }
  .prose, p { max-width: none; }

  /* ── LONG STRINGS ─────────────────────────────────────────────────────────
     An email address set in display type cannot wrap at a space because it has
     none. Left alone, medhamayurigs@gmail.com simply left the screen. */
  .display--xl, .foot__mail, h1, h2, h3 {
    overflow-wrap: anywhere;
    hyphens: none;
  }
  .foot__mail { font-size: var(--t-lg); }
  /* The contact page sets the address in display type; shrink it on a phone so
     it wraps at most once instead of four times. */
  .contact__mail { font-size: clamp(1.375rem, 7.4vw, 2.25rem); letter-spacing: -0.02em; }

  /* ── TAP TARGETS ──────────────────────────────────────────────────────────
     44px is the minimum comfortable touch size. The archival voice is small by
     design, so the padding grows rather than the type. */
  .nav__link, .link, .dl, .ledger a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  .nav__toggle {
    min-height: 44px;
    min-width: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: flex-end;
  }
  .action { padding: var(--s-4) var(--s-5); min-height: 48px; }

  /* ── THE CATALOGUE ROW ────────────────────────────────────────────────────
     Four cells, laid out in four columns on desktop. Narrowing the grid to two
     columns without saying where each cell goes left the programme note in the
     40px number column, 419px tall. Named cells, placed explicitly:

        01 │ Bharatanatyam            <- number, heading
           │ பரதநாட்டியம்
        ───┴──────────────────────
        the programme note, full width
        SOLO                                                                  */
  .rep__row {
    grid-template-columns: 2.5rem minmax(0, 1fr);
    column-gap: var(--s-4);
    row-gap: var(--s-3);
    padding-block: var(--s-5);
    align-items: start;
  }
  .rep__n    { grid-column: 1; grid-row: 1; }
  .rep__head { grid-column: 2; grid-row: 1; }
  .rep__body { grid-column: 1 / -1; grid-row: 2; }
  .rep__dur  { grid-column: 1 / -1; grid-row: 3; text-align: left; }

  .rep__title { font-size: var(--t-lg); }
  .rep__note { max-width: none; }
  /* The format already shows as a spec chip under the heading; the third-row
     repeat is desktop's right-hand column and is noise on a phone. */
  .rep__head .specs { display: none; }

  /* The wordmark is the route home and was an 18px tap target. */
  .nav__mark {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  /* The pause control matters more on touch, not less: a visitor who wants the
     motion stopped has no hover to reveal it with. */
  .loop__toggle {
    min-height: 44px;
    min-width: 64px;
    justify-content: center;
    display: inline-flex;
    align-items: center;
    inset-block-end: var(--s-4);
    inset-inline-start: var(--s-4);
  }
  .skip { min-height: 44px; display: inline-flex; align-items: center; }

  /* ── THRESHOLD ────────────────────────────────────────────────────────────
     One column: the line, the image bleeding the full width, the name on the
     floor. The image earns its size here — it is the only thing on screen. */
  /* Three rows, and the IMAGE is the flexible one. Giving the image a fixed
     share of the viewport pushed the name below the fold on every phone tested
     (by 4px on a 15 Pro Max, 105px on an SE). Letting it absorb whatever is
     left over means the name is always on the floor of the first screen,
     whatever the device. */
  .threshold {
    grid-template-rows: auto minmax(0, 1fr) auto;
    padding-block: calc(var(--s-8) + 1vh) var(--s-7);
    row-gap: var(--s-5);
  }
  .threshold__invocation { max-width: 22ch; }
  .threshold__media {
    justify-items: stretch;
    margin-inline: calc(var(--rail-w) * -1) calc(var(--gutter) * -1);
  }
  /* The frame is taken out of flow so it cannot size its own grid track.
     Left in flow, `height: 100%` on a grid item is circular — the browser
     falls back to the video's intrinsic 2:3, which is exactly what kept
     pushing the name off the bottom of the screen. Absolute inside a relative
     track, the row is sized by what is left and the image simply fills it. */
  .threshold__media {
    position: relative;
    min-height: 11rem;   /* never a letterbox strip on a short screen */
  }
  .threshold__media .frame {
    position: absolute;
    inset: 0;
    height: auto;
    width: auto;
    aspect-ratio: auto;
    /* Bleeding to both edges leaves no corner to cut, so square it off. */
    clip-path: none;
  }
  .threshold__name .display--hero + .display--hero { margin-block-start: -0.06em; }

  /* ── TABLES ───────────────────────────────────────────────────────────────
     The record page is a real table and does not survive 390px in three
     columns. Restacked as blocks, keeping the archival register. */
  .record, .record tbody, .record tr, .record td { display: block; width: 100%; }
  .record thead { display: none; }
  .record tr {
    padding-block: var(--s-4);
    border-block-end: 1px solid var(--granite-3);
  }
  .record td { border: 0; padding-block: var(--s-1); }
  .record td:first-child { font-size: var(--t-sm); padding-inline-end: 0; }
  .record .size { color: var(--ash); font-size: var(--t-mono); }
  .record tr:hover td { background: none; }

  /* ── LINEAGE & LISTS ──────────────────────────────────────────────────────*/
  .lineage { padding-inline-start: var(--s-6); }
  .lineage__item::before { inset-inline-start: calc(var(--s-6) * -1); }
  .specs { gap: var(--s-2) var(--s-4); }

  /* ── MEDIA ────────────────────────────────────────────────────────────────*/
  .scatter { row-gap: var(--s-7); }
  .player video, .player iframe { aspect-ratio: 16 / 9; }
  /* Hover cannot happen on a touch screen, so the stutter never fires; drop the
     transition rather than leave it waiting. */
  .frame--live img { transition: none; }

  /* ── FOOTER ───────────────────────────────────────────────────────────────*/
  .foot__grid { gap: var(--s-6); }
  .foot__grid > * { flex: 1 1 100%; }
}

/* Very small phones — 360px and below. */
@media (max-width: 23.5rem) {
  :root { --gutter: 1rem; --rail-w: 1.5rem; }
  .nav__mark { font-size: var(--t-base); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   ARDHA · THE APERTURE SEQUENCE
   The threshold's imagery, performing the adavu it depicts: strike open on the
   24deg diagonal, hold the pose, release, arrive elsewhere.
   ═══════════════════════════════════════════════════════════════════════════ */

/* THE FIELD IS THE WHOLE THRESHOLD.
   Confined to the right-hand column, the sequence used about 40% of the screen
   and the middle-left sat empty — the apertures read as an illustration beside
   the name rather than the space the name stands in. The field now spans the
   full width and the type sits on top of it, so a station can open left,
   centre or right. */
.apertures {
  position: relative;
  grid-column: 1 / -1;
  grid-row: 1 / -1;
  align-self: stretch;
  z-index: 0;
  margin-inline: calc(var(--rail-w) * -1) calc(var(--gutter) * -1);
  isolation: isolate;
}
/* The words own the layer above the imagery. fit-content matters: as a grid
   item the name box spanned the whole column while the letters ended well
   short of it, so collision checks against it were reserving space that was
   only ever empty. The box now describes the ink. */
.threshold__invocation,
.threshold__name { position: relative; z-index: 2; width: fit-content; }

/* ── TIMING ─────────────────────────────────────────────────────────────────
   Whole beats only, like everything else. One station:

     strike  2 beats   240ms   the aperture opens
     hold   14 beats  1680ms   the pose is held
     release 2 beats   240ms   it closes
     rest    2 beats   240ms   darkness before the next

   Four stations, the moving one holding double, gives a 12s cycle. Long
   enough that it never feels like a slideshow; short enough that a visitor
   who lingers sees the whole phrase. */
.apertures {
  --cycle: calc(var(--beat) * 80);    /* 9.6s at a 120ms beat */
}
/* Keyframe offsets must be literal percentages — they cannot read a custom
   property — so the timings above are written out in the keyframes below.
   Changing the phrase means editing both. */

.ap {
  position: absolute;
  inset: auto;
  /* Closed: a sliver lying on the araimandi diagonal. */
  clip-path: polygon(0 100%, 100% calc(100% - var(--angle-run)), 100% 100%, 0 100%);
  opacity: 0;
  will-change: clip-path, opacity;
}
.ap__frame { width: 100%; height: 100%; }
.ap__frame img, .ap__frame video { width: 100%; height: 100%; object-fit: cover; }
/* The poster still sits under the clip and is only revealed when motion is
   refused. It costs nothing otherwise — the browser has the file already,
   because it is the video's own poster. */
.ap__still { position: absolute; inset: 0; display: none; filter: url(#grade-soft); }
/* Stills keep the full grade; the moving clip takes the lighter one, because
   the sunset's own reds already sit on --agni. */
.ap__frame img { filter: url(#grade); }
.ap__frame video { filter: url(#grade-soft); }

/* ── THE STATIONS ───────────────────────────────────────────────────────────
   Each is placed by one edge pair and a WIDTH, with aspect-ratio supplying the
   height. Fixing all four insets instead let the leftover space decide the
   shape: the two meant to be 2:3 came out at 0.94, near square.

   Spread across the whole canvas — left, centre and right — and kept clear of
   the two blocks of type: the invocation upper-left, and the name on the
   floor. Overlaps are measured, not eyeballed. */
.ap--a { top:  6%;   right:  1%; width: 24%; aspect-ratio: 2 / 3;  }  /* tall, right */
.ap--b { top: 30%;   left:  36%; width: 28%; aspect-ratio: 16 / 9; }  /* wide, centre */
.ap--c { top: 17%;   right: 28%; width: 21%; aspect-ratio: 2 / 3;  }  /* tall, centre-right */
.ap--d { top: 26%;   left:   3%; width: 11%; aspect-ratio: 1;      }  /* small, far left */

/* ── THE PHRASE ─────────────────────────────────────────────────────────────
   Open along the diagonal, hold wide, close along it again. */
@keyframes aperture {
  0%   { clip-path: polygon(0 100%, 100% calc(100% - var(--angle-run)), 100% 100%, 0 100%); opacity: 0; }
  2%   { clip-path: polygon(0 0, 100% calc(0% - var(--angle-run)), 100% 100%, 0 100%); opacity: 1; }
  20%  { clip-path: polygon(0 0, 100% calc(0% - var(--angle-run)), 100% 100%, 0 100%); opacity: 1; }
  22%  { clip-path: polygon(0 0, 100% calc(0% - var(--angle-run)), 100% 0, 0 0); opacity: 0; }
  100% { clip-path: polygon(0 0, 100% calc(0% - var(--angle-run)), 100% 0, 0 0); opacity: 0; }
}

/* One station holds twice as long, so the phrase is not four equal beats. */
@keyframes aperture-long {
  0%   { clip-path: polygon(0 100%, 100% calc(100% - var(--angle-run)), 100% 100%, 0 100%); opacity: 0; }
  2%   { clip-path: polygon(0 0, 100% calc(0% - var(--angle-run)), 100% 100%, 0 100%); opacity: 1; }
  40%  { clip-path: polygon(0 0, 100% calc(0% - var(--angle-run)), 100% 100%, 0 100%); opacity: 1; }
  42%  { clip-path: polygon(0 0, 100% calc(0% - var(--angle-run)), 100% 0, 0 0); opacity: 0; }
  100% { clip-path: polygon(0 0, 100% calc(0% - var(--angle-run)), 100% 0, 0 0); opacity: 0; }
}

.ap {
  animation: aperture var(--cycle) var(--strike) infinite both;
}
.ap--long { animation-name: aperture-long; }

/* THE HANDOVER OVERLAPS. Each station spans 22% of the cycle (40% for the long
   one) but the next starts 2% early, so the one closing and the one opening
   share a beat. Previously the delays matched the spans exactly, which left
   roughly half a second of empty darkness between every window — the sequence
   read as four separate events rather than one continuous phrase.

   a  0 → 22%   ·  b  20 → 42%   ·  c  40 → 82%   ·  d  80 → 102% (wraps to a) */
.ap--a { animation-delay: 0s; }
.ap--b { animation-delay: calc(var(--cycle) * 0.20); }
.ap--c { animation-delay: calc(var(--cycle) * 0.40); }
.ap--d { animation-delay: calc(var(--cycle) * 0.80); }

/* ── WITHOUT MOTION ─────────────────────────────────────────────────────────
   One aperture, open, held. The composition survives intact; only the
   sequencing is lost. */
@media (prefers-reduced-motion: reduce) {
  .ap { animation: none; opacity: 0; clip-path: none; }
  .ap--a { opacity: 1; top: 6%; right: 0; width: 76%; aspect-ratio: 2 / 3; }
  /* Every station is a clip now, so hiding the video used to leave the
     threshold empty. The still that ships with each one is shown instead. */
  .ap--motion video { display: none; }
  .ap--motion .ap__still { display: block; }
}

/* ── MOBILE ─────────────────────────────────────────────────────────────────
   A phone has no field to move across, and four overlapping stations at 390px
   would be noise. The sequence collapses to the single bleeding frame that
   already works there. */
@media (max-width: 60rem) {
  .apertures {
    grid-column: 1;
    grid-row: 2;
    /* Bleeds into the rail on one side and the gutter on the other, landing
       exactly on both screen edges. */
    margin-inline: calc(var(--rail-w) * -1) calc(var(--gutter) * -1);
    min-height: 11rem;
    /* No aspect-ratio here. The grid row already gives this box a definite
       height, and an aspect-ratio would then compute the WIDTH from it
       (483 x 4/5 = 386) instead of taking the 322px column — which is how a
       stage that was meant to bleed to the edges ended up 13-23px past them. */
  }

  /* Each station carries a desktop width and aspect-ratio. `inset: 0` alone
     does not undo them: for an absolutely positioned box the explicit width
     wins over the left/right pair, so every clip stayed at 11-28% of the
     stage — a 78px postage stamp on a 390px phone. Reset all three. */
  .ap {
    inset: 0;
    width: auto;
    height: auto;
    aspect-ratio: auto;
  }
  .ap__frame { aspect-ratio: auto; }

  /* The phrase survives; the composition does not. A phone has no field to
     scatter four windows across, so the stations stop being places and become
     turns: each clip fills the same frame, opening and closing on the same
     24deg diagonal.

     Station b is dropped here. It is the one 16:9 export (640x360), and
     covering a portrait stage with it means a 2.6x upscale that throws away
     about 70% of its width — and width is the whole point of that shot, the
     wide shoreline behind her. Three stations retimed to close the gap it
     leaves: a 0-42%, c 40-82%, d 80-102%, each opening 2% before the last
     one finishes, exactly as on the desktop cycle. */
  .ap--b { display: none; }
  .ap--a, .ap--c { animation-name: aperture-long; }
  .ap--a { animation-delay: 0s; }
  .ap--c { animation-delay: calc(var(--cycle) * 0.40); }
  .ap--d { animation-name: aperture; animation-delay: calc(var(--cycle) * 0.80); }

  /* Reduced motion needs nothing extra here. That block runs earlier in this
     file and holds station a open; these `.ap` resets are later at equal
     specificity, so the held frame fills the stage too. An override that also
     set `right: auto` shrank it to zero width — `.ap__frame` is width:100% of
     an auto-width parent, which is circular. */
}


/* ═══════════════════════════════════════════════════════════════════════════
   ARDHA · MOTION SYSTEM
   The spec is MOTION.md. This is its implementation, and it is almost entirely
   CSS: JavaScript counts the rail, opens the menu, pauses off-screen video and
   applies magnetic pull. Nothing else. Remove it and the site still reveals,
   still transitions, still holds.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── SCROLLING ──────────────────────────────────────────────────────────────
   Native, deliberately. A hijacked scroll fights the OS — trackpad momentum,
   Page Down, spacebar, find-in-page and the screen-reader caret all stop
   behaving like every other window on the machine — and it moves scrolling
   off the compositor onto the main thread, which is exactly backwards. The
   smoothness comes from what happens AS you scroll, not from overriding how
   scrolling works. */
:where([id]) { scroll-margin-top: var(--s-9); }

/* ── TEXT REVEALS ───────────────────────────────────────────────────────────
   Display type reveals by line, along the 24deg diagonal. Body copy does not
   reveal at all: line-by-line is an act of emphasis, and applied to a
   paragraph it becomes an obstacle between the reader and the sentence. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* .display--xl only. The name on the threshold is .display--hero and
       arrives with the page, not with the scroll. */
    .display--xl {
      animation: line-strike var(--b4) var(--strike) both;
      animation-timeline: view();
      animation-range: entry 2% cover 24%;
    }

    /* Media drifts slower than the page; its caption stays welded to it, so
       the pair never separates. */
    .figure .frame,
    .player {
      animation: drift linear both;
      animation-timeline: view();
      animation-range: cover;
    }
    .label {
      animation: drift-label linear both;
      animation-timeline: view();
      animation-range: cover;
    }
  }
}

@keyframes line-strike {
  from {
    opacity: 0;
    transform: translateY(14px);
    clip-path: polygon(0 100%, 100% calc(100% - var(--angle-run)), 100% 100%, 0 100%);
  }
  to {
    opacity: 1;
    transform: none;
    clip-path: polygon(0 0, 100% calc(0% - var(--angle-run)), 100% 100%, 0 100%);
  }
}

@keyframes drift       { from { translate: 0 calc(var(--drift-media) * 0.5); } to { translate: 0 calc(var(--drift-media) * -0.5); } }
@keyframes drift-label { from { translate: 0 calc(var(--drift-label) * 0.5); } to { translate: 0 calc(var(--drift-label) * -0.5); } }

/* ── PINNING: REFUSED ───────────────────────────────────────────────────────
   The spec called for exactly one pinned section: a form's name held while its
   photographs pass. Built and measured, it failed twice. It painted behind the
   photographs (the scatter's items sit later in the DOM and win the stacking
   context), and the only clean fix — reserving a column the images may never
   enter — costs about 220px of a 1256px measure, shrinking every photograph on
   the page by roughly 17%. The page exists to show the photographs. A pinned
   label that repeats the h1 two hundred pixels above it does not earn that.

   So nothing on this site pins. The scroll stays the reader's. If a section
   ever arrives with something worth holding — a repertoire piece read against
   its notes — the budget is unspent and waiting. See MOTION.md section 5. */

/* ── MAGNETIC PULL ──────────────────────────────────────────────────────────
   Pointer only, capped at 6px, translate only so it stays on the compositor.
   Past about 6px it stops reading as weight and starts reading as the button
   avoiding the cursor. */
@media (hover: hover) and (pointer: fine) {
  .action[data-magnet] {
    transition: translate var(--b2) var(--strike),
                color var(--b2) linear,
                border-color var(--b2) linear;
    will-change: translate;
  }
}

/* ── SCENE TRANSITIONS ──────────────────────────────────────────────────────
   Cross-document View Transitions: no router, no library, no polyfill. Where
   it is unsupported the navigation is an ordinary page load. */
@view-transition { navigation: auto; }

@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) {
    animation: vt-release var(--b4) var(--release) both;
  }
  ::view-transition-new(root) {
    animation: vt-strike var(--b8) var(--strike) both;
  }
  /* A form's name is the same object either side of the navigation, so it
     morphs rather than cross-fading. */
  ::view-transition-group(form-title) { animation-duration: var(--b8); }
}

@keyframes vt-release { to   { opacity: 0; transform: translateY(-8px); } }
@keyframes vt-strike  { from { opacity: 0; transform: translateY(12px);
                               clip-path: polygon(0 100%, 100% calc(100% - var(--angle-run)), 100% 100%, 0 100%); } }

/* Reduced motion: no view transition at all, rather than a faster one. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) { animation: none; }
}
