/* ==========================================================================
   Huda Academy — site stylesheet
   Built 2026-07-28 from the settled lab work. Every value in the token block
   is a client decision; see docs/DESIGN-LIBRARY.md for what was approved and
   why. A second client changes the tokens and the logo, not the components.

   NOT the rejected first direction — that is parked in
   docs/archive/v1-rejected/ and must not be built on.
   ========================================================================== */

/* ---------- tokens -------------------------------------------------------- */
:root{
  /* palette — ground derived from bayyinah.com and shifted off the pink;
     the red is sampled from the client's own logo, NOT from Bayyinah */
  --paper:#F8F4EE;
  --paper-2:#EEE7DC;
  --paper-3:#E2D8C8;
  --terracotta:#A83226;
  --terracotta-deep:#86251B;
  --tan:#B09F88;
  --gold:#F2A33A;
  --ink:#34231A;
  --ink-soft:#655548;
  --cream:#F4EEE4;

  /* The nav is sized around the logo, not the other way round.
     ONE HEIGHT, ALWAYS — client 2026-08-02: "keep it the same size as when
     you're in the body sections". These ARE the old `.scrolled` values (88 /
     62 / 37) promoted to the default; the bar used to open at 126px and shrink
     to 88 past scrollY 4, which meant the first thing the page did was resize
     its own furniture. It no longer changes size at any scroll position. */
  --nav-h:88px; --mark-h:62px; --word-h:41.4px;
  /* banner height — the sticky stack's negative offset is derived from it,
     so the two can never disagree */
  --banner-h:44px;

  /* the hero scrim is almost off on purpose — the copy halo does the work */
  --scrim:.05;

  /* THE BAND RHYTHM. From .about down, sections strictly alternate light, dark,
     light, dark. Every section's background is one of these two and nothing
     else, so the rhythm is a single decision rather than nine independent ones
     — and so a section inserted or reordered later is a one-token fix.

     Order, and it must stay in step:
       about L · mission D · values L · events D · news L · stats D ·
       testimonials L · give D · loc L · mood (transition) · cta+footer dark

     Dark is a VEIL, not a fill: it is translucent, so the body's Khatim lattice
     reads through the dark steps and is covered on the light ones. That is the
     second rhythm running underneath this one, and it is why dark must not be
     swapped for an opaque tone without deciding what happens to the pattern. */
  --band-light:var(--paper);
  --band-dark:rgba(238,231,220,.78);

  /* How far the About ornament reaches past the clip on each side, as a share
     of the CLIP's width — the value tools/make-ornate-frame.py solves for.
     Desktop has room for it: it lands in the grid gap on one side and the page
     gutter on the other, both of which are wider than this at every width down
     to the 860px breakpoint. */
  --about-frame-pull:8.654%;

  /* The Experience film's CLOSED width. One token: the closed size, the height
     derived from it, the spacer that reserves its footprint in the column row,
     and the calc that opens it all reference this, so they cannot drift apart.
     (The About frame taught that lesson — its closed width was written twice
     and the two disagreed by 93px.) */
  --xp-mid-w:clamp(280px,34vw,620px);
}
/* The intermediate lockup step that lived here is GONE, and its reason went
   with it. It existed because five nav groups against a 90px lockup ran the
   row down to its 24px minimum gap around 1045px. Fixing the height at 62px
   gives that space back at every width, so the step was solving a problem the
   size change had created. Re-measured after removing it — see the nav block. */
/* FLUID BELOW 900, NOT A SECOND FIXED STEP. 2026-08-04, client: "on smaller
   screens the logo is getting cut off". It was: this block set the lockup to a
   FLAT 50/30px at every width under 900, so at 320px the brand still wanted
   145px, and the row — date + socials + lockup + burger — needed 434px in a
   305px viewport. The lockup ran off the right edge and took the burger with
   it, 73px past the edge and completely unreachable.
   The caps are the old values exactly, so nothing changes at 480 and up; below
   that the lockup scales with the viewport instead of standing still. Floors
   are set so the row still fits at 280px, which is narrower than any phone
   currently sold. */
@media (max-width:900px){
  :root{
    --nav-h:clamp(58px,15.5vw,74px);
    --mark-h:clamp(30px,10.5vw,50px);
    --word-h:clamp(20.1px,7.04vw,33.5px);
    --banner-h:56px;
  }
}

*{box-sizing:border-box}
html{scrollbar-gutter:stable}

/* THE PAGE NEVER SCROLLS SIDEWAYS. Added 2026-08-01 after the events arrows
   pushed the whole document 13px wide on every laptop-width screen (see the
   .events bleed cap for that root cause, which is fixed properly rather than
   hidden here). This is the backstop, not the fix: any future section that
   bleeds, rotates or drifts past the right edge now gets clipped instead of
   handing the reader a horizontal scrollbar for the entire site.

   `clip`, NEVER `hidden`. overflow:hidden on <body> makes it a scroll container,
   which re-parents every position:sticky on the page — .mood__sticky resolves
   against the viewport and would stop sticking. (.values__sticky was the other
   one; Core values is the film strip now and no longer sticky at all.)
   overflow-x:clip creates no scroll container, so sticky is untouched; it is
   also the only value that may legally pair with overflow-y:visible.

   Stated on both elements on purpose: <html> alone lets a viewport-propagated
   background reintroduce the scroll, and <body> alone is overridden when the
   html box is the propagation source. */
html,body{overflow-x:clip}
body{
  margin:0;color:var(--ink);
  font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
  line-height:1.6;
  /* Khatim lattice wallpaper. One continuous layer on <body> so the tile rhythm
     never breaks at a section boundary — light sections veil it rather than
     repaint it. Tile is a 4-way mirror of the client plate, so it is seamless
     by construction; 880px renders the star cell at ~220px, matching the plate. */
  background-color:var(--paper);
  background-image:url("../img/pattern-khatim.webp");
  background-size:880px auto;
  background-repeat:repeat;
}
img{display:block}
a{color:inherit}
/* Proportional, not a fixed cap. max-width:1180px was 68% of a 1568px viewport
   but only 57% of a 1920px one, so the page got narrower the bigger the screen.
   88% holds the proportion; the 1460px ceiling stops line length running away
   on very wide monitors. 1568 -> 1380px (88%). 1920 -> 1460px (76%).
   Used by .stats, .mission and .footer — the nav and hero have their own. */
.wrap{width:min(88%,1460px);margin-inline:auto}
.skip{position:absolute;left:-9999px;top:0;z-index:100;background:var(--terracotta);
  color:#FFFFFF;padding:.8rem 1.2rem;border-radius:0 0 4px 0}
.skip:focus{left:0}


/* ---------- announcement banner + the sticky stack ------------------------ */
/* THE WHOLE "NOT STICKY BANNER, STICKY NAV" BEHAVIOUR IS THIS ONE RULE. The
   stack is sticky at MINUS the banner's height, so it travels up until exactly
   the banner has left the viewport and then pins with the nav at top:0. The
   banner is gone for good; the nav never moves again. No scroll handler, so
   nothing here can be left in the wrong place by a failed script.

   The stack is in normal flow, so the banner and nav sit ABOVE the hero rather
   than over it — which is what the DarusSalam reference does and is why .hero
   no longer pads its copy down by the nav height. */
.topstack{position:sticky;top:calc(-1 * var(--banner-h));z-index:40}

.topbar{
  height:var(--banner-h);display:flex;align-items:center;justify-content:center;
  gap:clamp(.7rem,2vw,1.15rem);padding:0 clamp(1rem,3.5vw,2.75rem);
  /* THE TWO BARS TRADED GROUNDS, client 2026-08-02. The banner and the nav were
     both near-black (#170D05 against #2C1F17) and read as one slab with a seam
     in it. The ask was contrast between them, so the banner took the nav's brown
     and the nav took the red — see .nav::after. Which way round was chosen off a
     side-by-side of both.
     Gradient, not the flat #170D05 it replaces: a flat dark solid this wide
     dithers a diagonal hairline on this display pipeline. */
  background:linear-gradient(180deg,#2C1F17 0%,#1D1109 100%);
  box-shadow:inset 0 -1px 0 rgba(244,238,228,.10)}
/* Full cream, not the .86 it was. Partial-alpha cream is the thing the hero
   notes flag — the ratio math assumes a solid fill and hairline type never
   renders as one. Solid here measures 14.4:1 on the gradient's lighter stop,
   and the darker stop only improves it. */
.topbar__msg{margin:0;color:var(--cream);
  font-size:.8rem;line-height:1.2;letter-spacing:.01em;text-align:center}
/* Outlined, not filled — the filled button in this bar is Donate, and two solid
   buttons stacked vertically would compete. Matches the reference too. */
.topbar__btn{
  flex:0 0 auto;display:inline-flex;align-items:center;
  padding:.34rem .8rem;border:1px solid rgba(244,238,228,.45);border-radius:3px;
  color:var(--cream);text-decoration:none;white-space:nowrap;
  font-size:.72rem;font-weight:600;letter-spacing:.08em;
  transition:background-color 180ms ease,border-color 180ms ease}
.topbar__btn:hover{background:var(--terracotta);border-color:var(--terracotta);color:#FDF3E8}
.topbar__btn:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
@media (max-width:720px){
  .topbar{gap:.6rem;padding-inline:.8rem}
  .topbar__msg{font-size:.68rem}
  .topbar__btn{font-size:.64rem;padding:.3rem .6rem;letter-spacing:.04em}
}

/* ---------- nav ----------------------------------------------------------- */
/* No backdrop-filter. A blurred sticky nav over moving video stalled the
   renderer once already — do not reintroduce it. */
/* SOLID ONCE THE INTRO IS OVER, AND SOLID FOR GOOD. Client 2026-08-02, refined
   the same day: it originally went solid past scrollY 4, then briefly solid
   from first paint, and now lands here — transparent only while the logo intro
   is actually playing, solid from the moment that ends and permanently after.

   THE HOOK IS `intro-done`, and it is the right one because it is already set
   by all three ways the intro can end: fly() completing (site.js finish()), a
   wheel/touch/scroll cancelling it (site.js skip()), and the 6s failsafe in the
   inline <head> script. So "after the animation finishes OR after someone
   scrolls to cancel it" is one selector, not two, and it cannot desync from
   whatever the intro actually did.

   IT IS NO LONGER TIED TO SCROLL POSITION. The old rule keyed off .scrolled,
   so scrolling back to the top made the bar go transparent again mid-session,
   which is the thing that made it feel unstable. Once solid, it stays solid.
   `.scrolled` now only does the 126 -> 88 height change.

   TWO LAYERS, BECAUSE GRADIENTS DO NOT INTERPOLATE. A background-image cannot
   be transitioned, so the solid bar is its own layer whose OPACITY is animated
   — compositor-only, no paint. ::before is the scrim that keeps the links legible
   over video during the intro; ::after is the solid bar above it.

   NO-JS IS SOLID IMMEDIATELY, and that is deliberate: without scripting there
   is no intro to wait for (the overlay is display:none without .js), so the
   default state here is the solid one and `html.js` is what temporarily hides
   it. A dead site.js therefore gives a normal solid nav rather than a
   permanently transparent one. */
/* The gap is fluid for the same reason the lockup is: 1.5rem between three
   items is 48px of a 320px row spent on air, and it was part of what pushed the
   burger off the edge. Unchanged at 1000px and up. */
.nav{
  position:relative;z-index:2;height:var(--nav-h);
  display:flex;align-items:center;gap:clamp(.3rem,2.4vw,1.5rem);
  padding:0 clamp(.55rem,3.5vw,2.75rem);
  background:none;
  transition:height 300ms ease;
}
.nav::before,.nav::after{content:"";position:absolute;inset:0;pointer-events:none}
.nav::before{z-index:-2;
  background:linear-gradient(180deg,rgba(20,13,9,.55) 0%,rgba(20,13,9,0) 100%)}
/* RED, client 2026-08-02 — the other half of the swap described at .topbar.
   The brown that used to be here is now the banner's; this is the terracotta
   family already in the tokens, sampled from the school's own logo, so the bar
   is not introducing a new colour to the page.
   Cream links measure 6.4:1 on the lighter stop. The one thing this broke was
   the Donate CTA, which was red on brown and would have been red on red — see
   the note at .nav__cta. */
.nav::after{z-index:-1;
  background:linear-gradient(180deg,#962B20 0%,#86251B 100%);
  box-shadow:0 1px 0 rgba(244,238,228,.14),0 10px 30px -18px rgba(0,0,0,.9);
  opacity:1}
/* THE INTRO-TRANSPARENCY IS GONE, and it had to go with the banner.
   It existed so the hero video showed through the bar while the logo intro
   played, and it was keyed to `intro-done` so the bar went solid the moment the
   intro ended or a scroll cancelled it. That only worked while the nav OVERLAID
   the hero. The banner put the nav in normal flow above the hero, so there is
   no longer any video behind it — transparent would now reveal the page's paper
   ground as a pale strip under a dark banner, which is worse than either state
   it was choosing between. Solid always.
   The `intro-done` behaviour is therefore not a thing the client lost by being
   overruled; the layout removed what it was for. Flagged rather than silently
   dropped. */
/* `.scrolled` NO LONGER CHANGES ANYTHING VISUAL. site.js still toggles it past
   scrollY 4 and it is left in place deliberately — it is a useful hook and
   removing the toggle would mean touching the scroll handler for no gain — but
   the height it used to set is now the default, and the background it used to
   set is now owned by `intro-done`. If nothing claims it in the next round of
   work, delete the class and the toggle together rather than leaving a listener
   running for a selector no rule matches. */
/* LOCKUP CENTRED, LINKS SPLIT EITHER SIDE OF IT. Client 2026-08-02.

   THE FIRST ATTEMPT WAS ABSOLUTE-CENTRING THE LOGO over a right-aligned menu,
   and it failed at every desktop width — measured, the menu overlapped the
   lockup by 92px at 1585 and 279px at 1135. One row cannot hold a date, a
   centred logo, five dropdown groups and a CTA with the groups all on one side.
   So the groups split: About / Academics / Islamic life to the left, Admissions
   / Community / Enroll to the right.

   BOTH HALVES ARE `flex:1 1 0`, WHICH IS WHAT ACTUALLY CENTRES THE LOGO. Equal
   basis means they always take equal width whatever is inside them, so the
   brand between them lands on the true centre line and stays there as labels
   change. Each half then aligns its own contents toward the middle, so the two
   groups hug the lockup rather than drifting to the corners.

   THE DATE IS OUT OF FLOW. In flow it would eat from the left half and push the
   logo right by half its width. Absolutely placed, it costs the row nothing and
   the left half's right-alignment leaves the far-left corner free for it. */
/* EQUAL-BASIS HALVES ARE WHAT CENTRE THE LOGO. `flex:1 1 0` on both sides means
   they take identical width regardless of what is inside them, so the brand
   between them sits on the true centre line and stays there — verified as 0px
   off centre at every width the row is used at. */
.nav__side{display:flex;align-items:center;flex:1 1 0;min-width:0;
  gap:clamp(.7rem,1.5vw,1.5rem)}
/* space-between puts the date hard left and the links hard against the logo. */
.nav__side--l{justify-content:space-between}
.nav__side--r{justify-content:flex-start}
.nav__links{display:flex;align-items:center;gap:clamp(.7rem,1.5vw,1.5rem);min-width:0}
/* Both the inner gap and the outer margin are fluid. The margin is on top of
   the row's own gap, so at 320px the flat 1rem floor was spending another 32px
   of a row that was already 44px over. Unchanged at 1000px and up. */
.brand{display:flex;align-items:center;gap:clamp(.35rem,2.2vw,.9rem);
  text-decoration:none;
  flex:0 0 auto;margin-inline:clamp(.3rem,2.6vw,2.6rem)}

/* THE DATE IS A FLEX SIBLING OF THE LINKS, NOT AN OVERLAY, and that is the
   whole point. Absolutely positioned at the far left it cost the row nothing —
   but it also reserved nothing, so as the viewport narrowed the left group slid
   underneath it: measured, the first control overlapped the date below ~1300px
   and by 886px it was at x=-18, off the left edge entirely. As a flex item it
   simply takes its width and the links can never occupy it.

   Tabular figures so the row does not twitch when the day number goes from one
   digit to two; nowrap because it is a date. */
/* Lifted from .72 to .92 when the bar went red. Over the old near-black brown
   .72 was comfortable; over the red it composites to #D7B59D and measures
   4.11:1, under AA at this size. .92 brings it back to 5.7:1. */
.nav__hijri{margin:0;flex:0 0 auto;
  color:rgba(244,238,228,.92);font-size:.74rem;font-weight:600;
  letter-spacing:.11em;text-transform:uppercase;white-space:nowrap;
  font-variant-numeric:tabular-nums}
.nav__hijri[hidden]{display:none}
/* Tightened where the row is tightest — the date, the lockup and the burger all
   have to fit a phone. The size and tracking come down rather than the date
   being dropped, because dropping it is the one thing that was ruled out.
   FLUID, not two steps: the steps left the date at a flat .6rem everywhere
   below 430, which is the same failure the lockup had. The caps are the old
   values, so nothing changes above 640. */
.nav__hijri{font-size:clamp(.5rem,2.6vw,.74rem);
  letter-spacing:clamp(.01em,.55vw,.11em)}

/* THE ROW IS SIZED AGAINST THE WIDEST DATE, NOT TODAY'S. The Hijri month name
   is the widest variable on this row and it changes twelve times a year:
   "20 SAFAR 1448 AH" is 16 characters, "28 DHUʻL-QIʻDAH 1447 AH" is 23. Sizing
   against whatever month it happens to be today ships a nav that breaks in two
   months on screens that were fine — which is the same class of bug as the
   burger, arriving later. lab/_navfit.html sweeps the widths with the longest
   of the twelve substituted in; rerun it after touching anything on this row.
   Measured with that date: with the socials in the bar the row is cut at every
   width up to 412px. Without them it clears 280px, which is narrower than any
   phone sold. .dsocial carries them in the drawer at these widths. */
@media (max-width:640px){
  .nav .nav__social{display:none}
}
.brand__mark{height:var(--mark-h);width:auto;aspect-ratio:333/370;transition:height 300ms ease}
/* 507x199 since 2026-09-30: ACADEMY redrawn 1.8x so it reads in the nav (client,
   option C of lab/wordmark-01.html). The image kept its width and grew taller, so
   --word-h went up by 199/178 with it: HUDA and the row width are unchanged. */
.brand__word{height:var(--word-h);width:auto;aspect-ratio:507/199;transition:height 300ms ease}
.brand:focus-visible{outline:2px solid var(--cream);outline-offset:6px;border-radius:4px}



/* ---------- dropdowns ----------------------------------------------------- */
/* THE PANELS OPEN ON :hover AND :focus-within BEFORE ANY SCRIPT RUNS. site.js
   adds click/tap and Escape on top, but the CSS alone is a working menu for a
   mouse and for a keyboard, so a dead site.js costs the tap affordance and
   nothing else. Same principle as the reveals: navigation must not depend on a
   second file loading. */
.nav__item{position:relative;display:flex;align-items:center}
.nav__top{
  display:inline-flex;align-items:center;gap:.4rem;
  background:none;border:0;cursor:pointer;font:inherit;
  color:rgba(244,238,228,.86);font-size:.82rem;font-weight:600;
  letter-spacing:.14em;text-transform:uppercase;padding:.4rem 0;
  /* nowrap is not cosmetic. "Islamic life" is the only two-word label and it
     wrapped to two lines from ~1060px down, which pushed the nav to 55px of
     content inside a fixed-height bar — measured, not guessed. The breakpoint
     below is then set by where the row genuinely runs out of width. */
  white-space:nowrap;
  transition:color 180ms ease}
.nav__top i{
  width:7px;height:7px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;
  transform:rotate(45deg) translate(-1px,-1px);transition:transform 200ms ease;opacity:.7}
.nav__item:hover .nav__top,.nav__item:focus-within .nav__top,.nav__top[aria-expanded="true"]{color:#FFFFFF}
.nav__item:hover .nav__top i,.nav__item:focus-within .nav__top i,
.nav__top[aria-expanded="true"] i{transform:rotate(225deg) translate(-1px,-1px);opacity:1}

.nav__menu{
  position:absolute;top:100%;left:50%;transform:translate(-50%,6px);
  min-width:max-content;padding:.55rem 0;margin-top:.55rem;
  background:linear-gradient(180deg,#2C1F17 0%,#1D1109 100%);
  border:1px solid rgba(244,238,228,.12);border-radius:8px;
  box-shadow:0 18px 44px -16px rgba(0,0,0,.75);
  opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity 170ms ease,transform 200ms cubic-bezier(.22,1,.36,1),visibility 0s linear 170ms}
/* The gap between the button and the panel would drop the hover; this bridges
   it so the pointer can travel without the menu closing under it. */
.nav__menu::before{content:"";position:absolute;left:0;right:0;top:-.75rem;height:.75rem}
.nav__item:hover .nav__menu,.nav__item:focus-within .nav__menu,
.nav__top[aria-expanded="true"] + .nav__menu{
  opacity:1;visibility:visible;pointer-events:auto;transform:translate(-50%,0);
  transition:opacity 170ms ease,transform 200ms cubic-bezier(.22,1,.36,1),visibility 0s}
.nav__menu a{
  display:block;padding:.6rem 1.15rem;white-space:nowrap;
  color:rgba(244,238,228,.82);text-decoration:none;
  font-size:.86rem;font-weight:500;letter-spacing:.01em;text-transform:none;
  transition:background-color 150ms ease,color 150ms ease}
.nav__menu a:hover{background:rgba(244,238,228,.08);color:#FFFFFF}
.nav__menu a:focus-visible{outline:2px solid var(--gold);outline-offset:-2px;border-radius:3px}
.nav__top:focus-visible{outline:2px solid var(--gold);outline-offset:4px;border-radius:3px}
@media (prefers-reduced-motion:reduce){
  .nav__menu,.nav__top i{transition:none}
}

/* ---------- level 3: flyouts ---------------------------------------------- */
/* SAME CSS-FIRST CONTRACT AS THE LEVEL-2 PANELS. :hover and :focus-within open
   these with no script; site.js only adds click/tap and Escape on top. A
   submenu that needs JS to open is a dead end when site.js dies, and these
   carry sixteen of the nav's destinations.

   ONLY ANCHORS LIVE INSIDE .nav__flyout, AND THAT IS THE DEPTH CAP. Three
   levels is the ceiling; enforcing it in the markup rather than by convention
   means a fourth cannot be added without inventing a new class to do it. */
.nav__sub{position:relative;display:block}
.nav__subtop{
  width:100%;display:flex;align-items:center;justify-content:space-between;gap:1.75rem;
  background:none;border:0;cursor:pointer;font:inherit;text-align:left;
  padding:.6rem 1.15rem;white-space:nowrap;
  color:rgba(244,238,228,.82);
  font-size:.86rem;font-weight:500;letter-spacing:.01em;
  transition:background-color 150ms ease,color 150ms ease}
/* The chevron points SIDEWAYS, not down. The panel opens beside this row, and a
   down chevron here would promise an accordion that never arrives. */
.nav__subtop i{
  flex:0 0 auto;width:6px;height:6px;
  border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;
  transform:rotate(-45deg);opacity:.55;transition:opacity 180ms ease}
.nav__sub:hover .nav__subtop,.nav__sub:focus-within .nav__subtop,
.nav__subtop[aria-expanded="true"]{background:rgba(244,238,228,.08);color:#FFFFFF}
.nav__sub:hover .nav__subtop i,.nav__sub:focus-within .nav__subtop i,
.nav__subtop[aria-expanded="true"] i{opacity:1}
.nav__subtop:focus-visible{outline:2px solid var(--gold);outline-offset:-2px;border-radius:3px}

.nav__flyout{
  position:absolute;top:-.55rem;left:100%;
  min-width:max-content;padding:.55rem 0;margin-left:.4rem;
  background:linear-gradient(180deg,#2C1F17 0%,#1D1109 100%);
  border:1px solid rgba(244,238,228,.12);border-radius:8px;
  box-shadow:0 18px 44px -16px rgba(0,0,0,.75);
  opacity:0;visibility:hidden;pointer-events:none;transform:translateX(-4px);
  transition:opacity 150ms ease,transform 180ms cubic-bezier(.22,1,.36,1),visibility 0s linear 150ms}
/* Bridges the margin gap, same job as .nav__menu::before one level up: without
   it the pointer crosses dead space and the flyout closes under it. */
.nav__flyout::before{content:"";position:absolute;top:0;bottom:0;left:-.55rem;width:.55rem}
.nav__sub:hover .nav__flyout,.nav__sub:focus-within .nav__flyout,
.nav__subtop[aria-expanded="true"] + .nav__flyout{
  opacity:1;visibility:visible;pointer-events:auto;transform:translateX(0);
  transition:opacity 150ms ease,transform 180ms cubic-bezier(.22,1,.36,1),visibility 0s}

/* THE RIGHT-HAND GROUPS FLY LEFT. Admissions / Student life / Community sit in
   the right half and their panels are already close to the viewport edge, so a
   flyout opening further right lands off-screen. Mirrored rather than clamped —
   a clamp would slide the submenu on top of the panel that opened it. */
.nav__side--r .nav__flyout{
  left:auto;right:100%;margin-left:0;margin-right:.4rem;transform:translateX(4px)}
.nav__side--r .nav__flyout::before{left:auto;right:-.55rem}
.nav__side--r .nav__sub:hover .nav__flyout,
.nav__side--r .nav__sub:focus-within .nav__flyout,
.nav__side--r .nav__subtop[aria-expanded="true"] + .nav__flyout{transform:translateX(0)}
.nav__side--r .nav__subtop i{transform:rotate(135deg)}
@media (prefers-reduced-motion:reduce){
  .nav__flyout{transition:none}
}

.nav__links > a{
  color:var(--cream);text-decoration:none;font-size:.86rem;font-weight:500;
  letter-spacing:.06em;text-transform:uppercase;white-space:nowrap;
  text-shadow:0 1px 3px rgba(0,0,0,.55);
}
.nav__links > a:hover{color:#FFFFFF}
/* THE ONE PRIMARY ACTION IN THE BAR, client 2026-08-02: "make the CTA pop".
   It was a ghost outline — a 1px translucent cream border and cream text, i.e.
   exactly the weight of the five plain links beside it. Nothing about it said
   "this is the thing to press", and on the dark bar an outline reads as a
   disabled state as often as an active one.

   Solid now, which is the language the page already uses: .btn in the hero and
   the giving section is solid for the primary action and outlined for the
   secondary. The nav was the only place that got it backwards.

   The fill is a two-stop gradient rather than flat — same reason the dark bands
   are, and it also gives the inset top highlight something to sit on so the
   button reads as raised rather than as a coloured rectangle.

   INVERTED TO CREAM 2026-08-02, AND THIS IS NOT A STYLE PREFERENCE. It was
   #B33929 -> #96291C, i.e. the same terracotta family the bar itself now uses.
   Red on red is not a button, and "make the CTA pop" was the explicit note that
   put a fill here in the first place — so the fill had to move to the other end
   of the palette to keep doing its job. Cream is the only other ground the bar
   already contains (the links, the lockup), so this stays inside the palette.
   If the nav is ever taken back to brown, TAKE THIS BACK TO TERRACOTTA with it;
   cream-on-brown would make the CTA the brightest thing on the page.

   Contrast measured, not assumed: #34231A on the gradient's lighter stop is
   14.5:1, and the darker stop is still clear of 4.5:1. */
.nav__cta{
  display:inline-flex;align-items:center;min-height:42px;padding:.6rem 1.45rem;
  border:1px solid #F5EEE4;border-radius:4px;
  background:linear-gradient(180deg,#F5EEE4 0%,#E3D8BF 100%);
  color:#34231A!important;
  font-size:.8rem!important;font-weight:700;letter-spacing:.12em;
  /* The outer glow was terracotta, which was a lift against a brown bar and is
     invisible against a red one. Deep brown-black instead — it is the bar that
     has to darken under the button now, not tint. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.55),
    0 2px 4px rgba(0,0,0,.28),
    0 8px 18px -7px rgba(41,20,8,.75);
  transition:transform 200ms cubic-bezier(.22,1,.36,1),
             box-shadow 220ms ease, background-color 200ms ease, border-color 200ms ease;
}
/* Lifts on hover. transform only — no layout property animates, so this cannot
   reflow the row, and the neighbours do not move when it rises. */
.nav__cta:hover{
  background:linear-gradient(180deg,#FEFBF5 0%,#F4EEE4 100%);
  border-color:#FEFBF5;
  transform:translateY(-2px);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.7),
    0 3px 6px rgba(0,0,0,.30),
    0 14px 26px -8px rgba(41,20,8,.85);
}
.nav__cta:active{transform:translateY(0);
  box-shadow:inset 0 2px 5px rgba(0,0,0,.35),0 1px 2px rgba(0,0,0,.25)}
.nav__cta:focus-visible{outline:2px solid var(--gold);outline-offset:3px}
@media (prefers-reduced-motion:reduce){
  .nav__cta{transition:background-color 200ms ease}
  .nav__cta:hover{transform:none}
}
/* flex:0 0 auto IS LOAD-BEARING. Without it the burger is just another flex
   item on an over-full row, and it shrank from its declared 44px to 20px and
   still ended 3px past the viewport edge at 390px. It is the last item in the
   row and the only way into the menu, so it is the one thing that must never
   give up width. Everything else on the row yields first. */
.nav__burger{display:none;flex:0 0 auto;margin-left:auto;
  width:44px;height:44px;border:0;background:none;cursor:pointer;padding:10px}
.nav__burger span{display:block;height:2px;background:var(--cream);margin:5px 0;border-radius:2px;transition:transform 250ms ease,opacity 200ms ease}
/* Breakpoint set by where the nav runs out of room, not a notional phone width */
/* BREAKPOINT RAISED 900 -> 1330, and it is the centred layout that costs it.
   The rule on this project is that this number is set by when the row actually
   runs out of width, not by a notional device. Measured: equal halves mean the
   row needs 2x(date + three groups) + the lockup, which stops fitting at
   ~1330px. Below that the first control is pushed off the left edge.
   THIS IS A REAL TRADE: 1280 laptops now get the burger where they used to get
   the full menu. It is the price of a centred lockup plus an always-on date
   plus five groups in one 88px row. The alternative is a second thin tier for
   the date, which buys the desktop menu back down to ~900px at the cost of
   about 30px of height — offered to the client, not assumed.

   RAISED AGAIN 1330 -> 1460 when the nav went to six groups (2026-08-02). The
   right half went from two groups to three — Student life was added between
   Admissions and Community — and that half also carries the Donate button, so
   it is now the wider of the two and it is what runs out of room first.
   THIS NUMBER IS AN ESTIMATE, NOT A MEASUREMENT, and it is the one thing in
   this block that was not measured: it is scaled from the 1330 figure by the
   width of the added label. VERIFY IT BY DRAGGING THE WINDOW — the failure to
   look for is the leftmost control sliding under or past the left edge, which
   is what 1330 was set to prevent. Adjust here and at the min-width:1461 twin
   below; they must stay 1 apart or the drawer and the bar can both show.
   (1460 is also .wrap's max-width. COINCIDENCE, not a relationship — changing
   one does not imply changing the other.) */
@media (max-width:1460px){
  /* THE DATE SURVIVES THE BREAKPOINT. It lives inside the left half, so hiding
     the halves outright would take it with them — and "always displayed" was
     the whole request. Only the LINKS go; the left half stays as a shrink-wrap
     container for the date alone, and the brand centres in what is left. */
  .nav__links{display:none}
  .nav__side--r{display:none}
  .nav__side--l{flex:0 0 auto}
  /* THE RULE THAT USED TO BE HERE NEVER FIRED. It read `.nav__social{display:
     none}` and was overridden by `.soc-row{display:flex}` further down this
     file — identical specificity (0,1,0), later wins — so the socials have been
     riding along into the phone row this whole time, which is 69px of a row
     that could not spare it. Found 2026-08-04 while measuring the burger going
     off the edge. The working rule is at the foot of this section, where it is
     both scoped to .nav and set at the width the row actually needs it.
     Left at this breakpoint: nothing. Between 640 and 1460 the socials fit with
     36px+ to spare, so they stay, which is also the behaviour that has been
     live and unremarked-on. */
  .brand{margin-inline:auto}
  .nav__burger{display:block}
  /* Lockup back to the left edge on mobile: with both link halves gone there is
     nothing to centre it between, and a centred logo with a burger beside it
     reads as an unbalanced row rather than a deliberate one. */
  .brand{margin-inline:0}
}

/* mobile drawer */
.nav__drawer{
  position:absolute;top:100%;left:0;right:0;z-index:1;display:none;
  background:linear-gradient(180deg,#2C1F17 0%,#1D1109 100%);
  padding:1rem clamp(1rem,3.5vw,2.75rem) 1.6rem;
  box-shadow:0 18px 34px -26px rgba(0,0,0,.9);
}
/* Scrollable, because five open groups are taller than a phone. inset bottom 0
   plus overscroll containment so flicking the drawer does not scroll the page
   underneath it. */
.nav__drawer{max-height:calc(100vh - var(--nav-h));overflow-y:auto;overscroll-behavior:contain}

/* GROUPS ARE <details>. No script operates them — the burger is the only
   scripted part of the drawer, so a dead site.js leaves a menu that cannot be
   opened rather than one that opens onto an accordion that cannot. */
.dgroup{border-bottom:1px solid rgba(244,238,228,.14)}
.dgroup > summary{
  list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;
  padding:.95rem 0;color:var(--cream);
  font-size:.95rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
}
.dgroup > summary::-webkit-details-marker{display:none}
.dgroup > summary::after{
  content:"";width:8px;height:8px;flex:none;margin-left:1rem;
  border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;
  transform:rotate(45deg) translate(-2px,-2px);transition:transform 200ms ease;opacity:.7}
.dgroup[open] > summary::after{transform:rotate(225deg) translate(-2px,-2px);opacity:1}
.dgroup > summary:focus-visible{outline:2px solid var(--gold);outline-offset:2px;border-radius:3px}
.dgroup a{
  display:block;padding:.62rem 0 .62rem 1rem;color:rgba(244,238,228,.78);
  text-decoration:none;font-size:.92rem;letter-spacing:.01em;text-transform:none;border:0}
.dgroup a:last-child{padding-bottom:1rem}
.dgroup a:hover{color:#FFFFFF}

/* LEVEL 3 IN THE DRAWER IS JUST ANOTHER <details>, NESTED. Same contract as the
   groups above it — no script operates it, so the whole menu survives a dead
   site.js. Indent plus a hairline rail is the only thing that says "one level
   down"; a full border would read as a new group rather than a child of one.
   Nesting <details> is also what caps the depth here: .dsub is styled to
   contain anchors, and there is no .dsub-of-a-.dsub to reach for. */
.dsub{margin-left:1rem;border-left:1px solid rgba(244,238,228,.13)}
/* A .dsub can be the last thing in a group, in which case no <a> is :last-child
   and the group loses its bottom padding. Restored here. */
.dgroup > .dsub:last-child{margin-bottom:1rem}
.dsub > summary{
  list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;
  padding:.62rem 0 .62rem 1rem;color:rgba(244,238,228,.78);
  font-size:.92rem;font-weight:600;letter-spacing:.01em;text-transform:none}
.dsub > summary::-webkit-details-marker{display:none}
.dsub > summary::after{
  content:"";width:7px;height:7px;flex:none;margin-left:1rem;
  border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;
  transform:rotate(45deg) translate(-2px,-2px);transition:transform 200ms ease;opacity:.6}
.dsub[open] > summary{color:#FFFFFF}
.dsub[open] > summary::after{transform:rotate(225deg) translate(-2px,-2px);opacity:1}
.dsub > summary:focus-visible{outline:2px solid var(--gold);outline-offset:2px;border-radius:3px}
.dsub a{padding-left:2rem;font-size:.89rem}
.dsub a:last-child{padding-bottom:.62rem}
@media (prefers-reduced-motion:reduce){ .dsub > summary::after{transition:none} }

/* THIS AND THE DESKTOP CTA NOW DIVERGE, AND THE OLD NOTE HERE SAID THEY MUST
   NOT — so here is why it changed. Both were terracotta-on-brown and the note
   was right to keep them in step. The nav then went red, which forced .nav__cta
   to cream; the DRAWER is still brown (it hangs off the bar, it is not the bar),
   so terracotta is still the correct fill here and cream would make this the
   brightest thing on a phone screen. They are answering to different grounds
   now, not drifting. If the nav ever goes back to brown, both go terracotta and
   the original "keep them in step" rule applies again unchanged. */
.dcta{
  display:block;margin-top:1.2rem;text-align:center;
  padding:.95rem 1.2rem;border:1px solid #B33929;border-radius:4px;
  background:linear-gradient(180deg,#B33929 0%,#96291C 100%);
  color:#FDF3E8;text-decoration:none;
  font-size:.9rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.20),
    0 2px 4px rgba(0,0,0,.28),
    0 8px 18px -7px rgba(168,50,38,.85)}
.dcta:active{background:linear-gradient(180deg,#96291C 0%,#86251B 100%)}
@media (prefers-reduced-motion:reduce){ .dgroup > summary::after{transition:none} }
html.nav-open .nav__drawer{display:block}
/* Twin of the max-width:1460 above — keep them 1px apart. */
@media (min-width:1461px){ html.nav-open .nav__drawer{display:none} }

/* ---------- social links (nav bar / drawer / footer) ---------------------- */
/* ONE RULE SET, THREE GROUNDS. The same pair of links appears in the red bar,
   in the brown drawer and in the near-black footer, so nothing here names a
   colour: the glyph is `currentColor` and each instance inherits from the block
   it sits in. Only the sizes are per-home.

   The marks are <use> references into the sprite at the top of <body>. The
   sprite is positioned out of flow rather than display:none — see the note
   there — and this rule is what takes it out of the layout. */
.sprite{position:absolute;width:0;height:0;overflow:hidden}

.soc-row{display:flex;align-items:center;gap:.35rem}
/* The tap target is the padded box, not the 18px glyph inside it. Sized here
   rather than per-home so the nav's pair is never a 16px target. */
.soc{
  display:inline-flex;align-items:center;justify-content:center;
  padding:.5rem;border-radius:50%;
  color:rgba(244,238,228,.78);
  transition:color 200ms ease,background-color 200ms ease;
}
.soc svg{display:block;width:18px;height:18px;fill:currentColor}
.soc:hover{color:#FFFFFF;background:rgba(244,238,228,.12)}
.soc:focus-visible{outline:2px solid var(--cream);outline-offset:2px}
@media (prefers-reduced-motion:reduce){ .soc{transition:none} }

/* In the bar. Pulled in tight against the date and stepped down a size — the
   row is 88px of fixed height carrying a date, six groups, a lockup and a
   button, and these are the least important thing in it. */
.nav__util{display:flex;align-items:center;flex:0 0 auto;min-width:0;gap:.15rem}
.nav__social{margin-left:.35rem}
.nav__social .soc{padding:.4rem}
.nav__social .soc svg{width:16px;height:16px}
/* Cream at .78 over the red bar composites to roughly the same value the date
   was lifted to when the bar changed colour — see .nav__hijri. */
.nav__social .soc{color:rgba(244,238,228,.86)}

/* In the drawer. Full 44px targets, centred under the Donate button, with the
   same hairline above them that separates the groups. */
.dsocial{justify-content:center;gap:.6rem;margin-top:1.1rem;padding-top:1rem;
  border-top:1px solid rgba(244,238,228,.12)}
.dsocial .soc{padding:.7rem}
.dsocial .soc svg{width:22px;height:22px}

/* In the footer. Sits under the blurb in the brand column; the negative left
   margin cancels the button's own padding so the first glyph lines up with the
   text above it rather than sitting a few pixels in. */
.footer__social{margin:1.1rem 0 0 -.5rem}
.footer__social .soc svg{width:20px;height:20px}

/* ---------- hero ---------------------------------------------------------- */
.hero{position:relative;min-height:100vh;min-height:100svh;display:flex;overflow:hidden;isolation:isolate}
.hero__media{position:absolute;inset:0;z-index:0;background:#1B1209}
.hero__video{width:100%;height:100%;object-fit:cover;display:block}

.hero__scrim{
  position:absolute;inset:0;z-index:1;pointer-events:none;opacity:var(--scrim);
  background:
    radial-gradient(125% 88% at 50% 40%, rgba(12,16,22,.10) 0%, rgba(9,12,17,.58) 58%, rgba(6,8,12,.88) 100%),
    linear-gradient(180deg, rgba(14,18,25,.66) 0%, rgba(31,20,10,.40) 44%, rgba(10,13,19,.84) 100%);
}
/* Grounding only — top for the nav, bottom for the hero's edge into the stat
   band. The middle is deliberately clear so the building is not buried. */
.hero__plate{
  position:absolute;inset:0;z-index:2;pointer-events:none;
  background:linear-gradient(180deg,
    rgba(7,9,13,.30) 0%, rgba(7,9,13,.06) 22%, rgba(7,9,13,0) 45%,
    rgba(7,9,13,.10) 74%, rgba(7,9,13,.46) 100%);
}
/* Every large dark area is a real gradient plus noise — never a flat fill, or
   the display pipeline dithers a visible diagonal hairline across it. */
.hero__noise{
  position:absolute;inset:0;z-index:3;pointer-events:none;opacity:.035;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
}
.hero__inner{
  position:relative;z-index:4;width:100%;max-width:1240px;margin-inline:auto;
  display:flex;align-items:center;justify-content:center;text-align:center;
  /* Top padding is no longer clearing the nav — the nav sits ABOVE the hero
     in flow now, not over it. This is just breathing room. */
  padding:clamp(2rem,7vh,4.5rem) clamp(1rem,3.5vw,2.75rem) 4rem;
}
.hero__copy{position:relative;color:var(--cream);max-width:52rem;text-align:center}
/* The copy halo, not the scrim, is what makes a 0.05 scrim survivable:
   it darkens the words and leaves the rest of the frame alone. */
.hero__copy::before{
  content:"";position:absolute;inset:-40%;z-index:-1;pointer-events:none;
  background:radial-gradient(closest-side ellipse at 50% 50%,
    rgba(6,8,12,.65) 0%, rgba(6,8,12,.585) 58%, rgba(6,8,12,.195) 81%, rgba(6,8,12,0) 100%);
}
.hero__eyebrow{
  position:relative;padding-top:1.6rem;font-size:.78rem;font-weight:700;letter-spacing:.19em;
  text-transform:uppercase;color:var(--cream);margin:0 auto 1.15rem;
  text-shadow:0 1px 2px rgba(0,0,0,.85),0 0 10px rgba(0,0,0,.55);
}
.hero__eyebrow::before{
  content:"";position:absolute;top:0;left:50%;transform:translateX(-50%);
  width:64px;height:3px;background:var(--gold);border-radius:2px;
}
.hero h1{
  font-weight:600;font-size:clamp(2.05rem,3.95vw,3.25rem);line-height:1.1;
  letter-spacing:-.025em;margin:0 0 1.25rem;
  text-shadow:0 1px 2px rgba(0,0,0,.8),0 0 16px rgba(0,0,0,.6);
}
.hero h1 .ln{display:block}
.hero h1 .ln:first-child{color:var(--gold);font-style:italic}
.hero .lede{
  font-size:clamp(1.02rem,1.35vw,1.16rem);color:var(--cream);max-width:38rem;
  margin:0 auto 2.25rem;text-shadow:0 1px 2px rgba(0,0,0,.85),0 0 10px rgba(0,0,0,.5);
}
.hero .cta-row{justify-content:center}

/* ---------- buttons ------------------------------------------------------- */
.cta-row{display:flex;flex-wrap:wrap;gap:.8rem}
.btn{
  display:inline-flex;align-items:center;gap:.6em;min-height:52px;padding:.95rem 1.9rem;
  border-radius:4px;background:var(--terracotta);color:#FFFFFF;font-size:.95rem;font-weight:600;
  letter-spacing:.03em;text-decoration:none;
  transition:background-color 200ms ease,transform 200ms ease;
}
.btn:hover{background:var(--terracotta-deep);transform:translateY(-1px)}
.btn--ghost{background:transparent;border:1px solid rgba(244,238,228,.55);color:var(--cream)}
.btn--ghost:hover{background:rgba(244,238,228,.12)}
.btn:focus-visible{outline:3px solid var(--cream);outline-offset:3px}

/* ---- the hero on small and SHORT screens ---------------------------------
   2026-08-04, client: "the hero section is getting cut off on smaller
   devices". Measured with lab/_herofit.html, which sweeps real viewports with
   BOTH dimensions — a width-only sweep cannot see this, because the failure is
   vertical. The hero is min-height:100svh, so it never clipped; it GREW past
   the screen and pushed its own call to action under the fold:

     280x653  Fold cover      CTA 86px below the fold
     320x568  iPhone SE 1     CTA 165px below
     360x640  Android small   CTA 55px below
     375x667  iPhone SE 2/3   CTA 24px below
     568x320 / 667x375 / 844x390  every phone in LANDSCAPE, 113-183px below

   You landed on the site and the two things the page exists to make you do —
   enrol, or call the office — were off the screen.

   The cause is a stack of FIXED vertical spacing: 4rem of bottom padding,
   1.6rem above the eyebrow, 1.25rem under the headline, 2.25rem under the
   lede, 52px buttons. None of it knew how tall the screen was.

   THE DESKTOP HERO IS LOCKED (H1-H5, DESIGN-LIBRARY) AND IS NOT TOUCHED. Both
   blocks below are gated on the viewport being small AND short, so a 1440x900
   desktop, a 768x1024 iPad and a 390x844 phone — all of which already fit —
   match to nothing here and render exactly as before. */
@media (max-width:900px) and (max-height:760px){
  /* THE STRUCTURAL HALF OF THE BUG, and the larger half. The hero asks for a
     WHOLE screen of height, then sits underneath a 56px banner and a 58-74px
     nav — so the first screen was only ever showing 100svh MINUS 114px of the
     hero, and the bottom 114px, which is where the buttons live, was below the
     fold by construction. No amount of tightening the copy fixes that; the
     hero has to ask for the screen it will actually get.
     vh first as the fallback, then svh: on a phone the two differ by the URL
     bar, and svh is the one that does not jump when it hides. */
  .hero{min-height:calc(100vh - var(--banner-h) - var(--nav-h));
        min-height:calc(100svh - var(--banner-h) - var(--nav-h))}
  .hero__inner{padding-top:clamp(1rem,4vh,2rem);
    padding-bottom:clamp(1.1rem,4.5vh,2.5rem)}
  .hero__eyebrow{padding-top:clamp(.55rem,2.2vh,1.6rem);
    margin-bottom:clamp(.45rem,1.6vh,1.15rem);
    font-size:clamp(.6rem,1.9vh,.78rem)}
  /* min() of a width term and a height term: whichever dimension is the binding
     constraint wins, so this reads on a 320-wide portrait AND on a 390-tall
     landscape without a separate rule for each. */
  .hero h1{font-size:clamp(1.55rem,min(8.2vw,5.6vh),3.25rem);
    margin-bottom:clamp(.45rem,1.8vh,1.25rem)}
  .hero .lede{font-size:clamp(.9rem,min(4vw,2.5vh),1.16rem);
    margin-bottom:clamp(.75rem,3vh,2.25rem)}
  .hero .btn{min-height:44px;padding:.6rem 1.25rem;font-size:.88rem}
}
/* Narrow screens whatever their height: the headline's floor was 2.05rem, so
   it stopped scaling at ~519px wide and sat at a flat 32.8px on a 280px phone.
   Capped at exactly the old floor, so 400px and up is unchanged. */
@media (max-width:400px){
  .hero h1{font-size:clamp(1.62rem,8.4vw,2.05rem)}
}
/* A PHONE HELD SIDEWAYS is the tightest case on the site: ~320px of screen, of
   which the banner and nav were taking 114 — a third of it — before the hero
   got any. The chrome has to shrink too or nothing below it can fit. These are
   the same tokens the rest of the page measures itself against, so the drawer's
   max-height and every sticky offset follow automatically.
   Gated on height alone being tiny, so no portrait phone sees it. */
@media (max-width:900px) and (max-height:480px){
  :root{--nav-h:clamp(48px,14vh,58px);--banner-h:clamp(36px,11vh,44px)}
  .hero__inner{padding-top:.75rem;padding-bottom:.85rem}
  .hero .lede{margin-bottom:.6rem}
  .hero__eyebrow{margin-bottom:.35rem}
}

/* ---------- shared section furniture -------------------------------------- */
.kicker{font-size:.74rem;font-weight:700;letter-spacing:.19em;text-transform:uppercase;
  color:var(--terracotta);margin:0 0 .9rem}
.rule{width:64px;height:3px;background:var(--terracotta);border-radius:2px}

/* Reveal is additive only: content is visible until .js proves scripting is
   alive, and the <head> failsafe puts it back regardless. */
html.js .rv{opacity:0;transform:translateY(16px)}
html.js.in .rv{opacity:1;transform:none;transition:opacity .7s ease,transform .8s cubic-bezier(.22,1,.36,1)}
html.js.in .rv:nth-child(2){transition-delay:.07s}
html.js.in .rv:nth-child(3){transition-delay:.14s}
html.in .rv{opacity:1!important;transform:none!important}

/* ---------- stat band ----------------------------------------------------- */
/* Slot 6 — DARK, and the first section to take artwork of its own. Client pick
   2026-07-30 from lab/bg-01.html, option D: the flourish band, full strength,
   keeping its own ground.


   IT IS NOT A TILE and must not be treated as one. It is a single bottom-
   anchored ornament on a peach ground with its top two-thirds empty, so it is
   placed exactly the way bg-01 placed it — 100% width, no repeat, pinned to the
   bottom edge. Set background-repeat and you get a row of severed flourishes.

   THE GROUND COMES WITH IT, per the client: #E4D9C8 at L=.714. That is a real
   third tone — the light bands are L=.895 and the veil dark is L=.827 — so this
   band sits well below the rhythm rather than inside it. It reads as a feature
   band, which is the point, and being much darker than the veil it cannot be
   mistaken for a light slot, so the alternation still holds either side of it.

   Legibility on that deeper ground was measured, not assumed: the terracotta
   numerals land 5.03:1 and the labels 6.06:1, both clear of 4.5:1.

   Lossy WebP is safe here where it was not for a tile: there is no seam to
   protect, so q92 at 37KB rather than a lossless file.

   Being opaque it covers the body's Khatim lattice in this band. One ornament
   at a time here, by design. */
/* ORNAMENT REMOVED 2026-08-01, client call: "keep that same color but remove
   those squiggly line designs". The tone stays exactly #E4D9C8 — it is what
   separates this band from the paper above and below it, and the accessibility
   figures quoted above were measured against it, so it must not drift. Only
   bg-flourish.webp is gone. The asset stays in assets/img/ in case the band
   ever wants it back. Being opaque this still covers the body lattice. */
.stats{
  background-color:#E4D9C8;
  padding:clamp(3.5rem,9vh,6.5rem) 0;
}
/* The numbers take the space they need; the seal takes only its own width. */
.stats__row{display:flex;align-items:center;gap:clamp(1.2rem,3vw,2.5rem)}
/* TWO, NOT THREE, since 2026-09-19: the "13 levels, infant to Grade 8"
   cell was removed on the client’s instruction (see the note in
   index.html). Left at repeat(3,1fr) the two survivors would hold two
   thirds of the band and leave an empty column against the seal. */
.stats__grid{display:grid;grid-template-columns:repeat(2,1fr);flex:1;min-width:0}
/* DIVIDERS REMOVED 2026-07-30, client call. The band used a hairline between
   every cell and one before the seal. Spacing does the separating now — the
   figures are set large enough that they group on their own, and the rules were
   adding furniture the band did not need. Padding is kept so the columns keep
   their rhythm without them. */
.stats__accred{flex:0 0 auto;padding-left:clamp(1.2rem,3vw,2.5rem)}
.stats__accred img{width:clamp(82px,7.5vw,116px);height:auto}
.stat{padding:0 clamp(1rem,3vw,2.5rem)}
.stat:first-child{padding-left:0}
.stat__num{display:block;font-size:clamp(2.6rem,6.2vw,4.4rem);font-weight:600;line-height:1;
  letter-spacing:-.035em;color:var(--terracotta);font-variant-numeric:tabular-nums}
.stat__body{display:block}
.stat__label{display:block;margin-top:.7rem;font-size:.82rem;font-weight:600;
  letter-spacing:.13em;text-transform:uppercase;color:var(--ink)}
.stat__src{display:block;margin-top:.5rem;font-size:.72rem;color:var(--ink-soft);opacity:.75}
/* A missing feed must never render as 0 — see site.js */
.stat.is-empty .stat__num{color:var(--ink-soft);opacity:.45;letter-spacing:0}
.stat.is-empty .stat__src{color:var(--terracotta);opacity:1;font-weight:600}
/* MOBILE, REWRITTEN 2026-08-03. The old rule stacked each cell as a full-width
   poster — number on its own line, label under it, seal on a row of its own —
   and the band came out 743px tall on a 390px phone. Three numbers should not
   cost a screen and a half of scroll; the client's word for it was "bloated",
   and rightly, because at that height it stops reading as a band at all.

   The fix needs no markup: .stat__num and .stat__body are already siblings, so
   turning .stat into a flex row puts the figure left and its label right. Each
   cell becomes a ROW OF A BAND rather than a poster, and the band lands ~330px.

   Still dividerless (client call, 2026-07-30) and still #E4D9C8 (client-locked)
   — this changes density only. */
@media (max-width:820px){
  .stats{padding:clamp(2.4rem,6vh,3.4rem) 0}
  .stats .wrap{position:relative}
  .stats__row{display:block}
  .stats__grid{grid-template-columns:1fr;gap:1.05rem}
  .stat{display:flex;align-items:center;gap:.85rem;padding:0}
  /* Right-aligned and tabular so the labels all start at the same x, whatever
     digit count a future figure has. */
  .stat__num{flex:0 0 auto;min-width:2.4ch;text-align:right;
    font-size:clamp(1.95rem,8.5vw,2.45rem);letter-spacing:-.03em}
  .stat__body{flex:1 1 auto;min-width:0}
  .stat__label{margin-top:0;font-size:.75rem;letter-spacing:.1em;line-height:1.45}
  .stat__src{margin-top:.2rem;font-size:.68rem}
  /* The seal takes the dead space beside the kicker instead of a row of its own,
     which is where the old layout spent ~150px. The first cell reserves that
     width so a long label can never run under it. */
  .stats__accred{position:absolute;top:-.3rem;right:0;padding:0;margin:0}
  .stats__accred img{width:clamp(54px,15vw,72px)}
  .stats__grid>.stat:first-child{padding-right:clamp(66px,18vw,86px)}
}
/* Tablet portrait keeps the rows but not the phone's header-line seal: there is
   enough width here that the rows would sit in a lake of empty ground with the
   seal marooned above it. Capping the rows and centring the seal against them
   restores the desktop reading — figures, then the mark that vouches for them. */
@media (min-width:641px) and (max-width:820px){
  .stats__grid{max-width:33rem}
  .stats__grid>.stat:first-child{padding-right:0}
  .stats__accred{top:50%;transform:translateY(-50%);right:clamp(0px,4vw,3rem)}
  .stat__num{font-size:2.7rem}
}

/* ---------- mission ------------------------------------------------------- */
/* Slot 2 — DARK. A veil, not a fill, so the body lattice reads through it;
   opaque here would break the pattern. This is the section the veil tone was
   originally chosen for, and --band-dark is that value promoted to a token. */
.mission{background:var(--band-dark);padding:clamp(3.5rem,10vh,7rem) 0}
/* "Our mission" is scaled up into a real heading rather than an eyebrow. Scoped
   to .mission on purpose — .kicker is shared with the stat band's "By the
   numbers", which stays small. Tracking drops from .19em to .07em because the
   wide letterspacing that makes 12px uppercase legible reads as sprawl at 30px.
   Held below the statement's size so the hierarchy still runs label → statement. */
.mission .kicker{font-size:clamp(1.25rem,2.1vw,1.9rem);letter-spacing:.07em;
  margin:0 0 .2rem}
.mission .rule{margin:1rem 0 1.6rem}
/* The client's official mission statement. Left-aligned, not centred: 60 words
   centred and ragged on both sides read as a poster rather than a statement.
   58ch, not a pixel cap — at this type size that lands near 1080px, so it still
   fills ~74% of the wrap while keeping line length inside the readable band. */
.mission__lead{font-weight:500;letter-spacing:-.016em;line-height:1.3;margin:0;
  font-size:clamp(1.45rem,3.05vw,2.6rem);max-width:58ch}
/* "God conscious lives" is the hinge into the ayah — the only emphasis. */
.mission__lead em{font-style:italic;color:var(--terracotta)}
/* The diagonal: dateline holds the bottom-left, ayah anchors the bottom-right,
   both hanging off one full-width rule. */
.mission__foot{margin-top:clamp(2rem,4vw,3rem);padding-top:1.8rem;
  border-top:1px solid rgba(168,50,38,.20);
  display:flex;justify-content:space-between;align-items:flex-end;gap:2rem;flex-wrap:wrap}
.mission__sig{margin:0;font-size:.8rem;letter-spacing:.12em;
  text-transform:uppercase;color:var(--ink-soft)}

/* ---------- ayah ---------------------------------------------------------- */
/* No box and no rule of its own — it sits in the bottom-right of the mission's
   diagonal and the section rule above it does the dividing. A framed pull-quote
   reads as decoration; this should read as a source. */
.ayah{margin:0 0 0 auto;text-align:right;max-width:1050px}

/* The verse is painted from SVG outlines, not set in a font. That is a
   deliberate trade, made 2026-07-28: the stroke-draw reveal animates pen paths,
   and you cannot stroke a glyph the browser is rendering from a font file, so
   Lateef's exact letterform was given up to get the handwriting motion. The
   outlines are Amiri Bold — a slightly wider, slightly heavier Naskh than
   Lateef; see DESIGN-LIBRARY A1 for the side-by-side.
   The two-Arabic-faces cost A1 recorded is CLOSED as of 2026-07-29: the giving
   ayah was traced in the same face and now uses the same draw, so Amiri Bold
   outlines are the only Arabic the page paints.
   The verse is STILL REAL TEXT in the DOM, in .u-sr — so it is still read as
   Arabic, still indexed and still copyable. Only the painting is outlines. */
.ayah__ar{margin:0 0 0 auto;width:fit-content;color:var(--ink)}
/* Sized by width, not by a font-size, because there is no font. The old Lateef
   line measured 503px at its 83.2px cap — a 6.05 ratio — so these three stops
   are exactly the old clamp(2.5rem,5.6vw,5.2rem) multiplied through. The
   section was laid out around that measure and still is. */
.ayah__svg{display:block;margin-left:auto;overflow:visible;
  width:clamp(242px,33.9vw,503px);max-width:100%;height:auto;
  fill:currentColor}
/* Mask internals. Inert until site.js applies the mask — see section 4 there. */
.ayah__svg .sk{fill:none;stroke:#FFFFFF;stroke-linecap:round;stroke-linejoin:round;opacity:0}
.ayah__svg .ayah__fill{fill:#FFFFFF;opacity:0}
.ayah__en{margin:.6rem 0 0;padding:0;border:0;font-style:italic;
  color:var(--ink-soft);line-height:1.6;font-size:clamp(.96rem,1.2vw,1.06rem)}
.ayah__en p{margin:0}
.ayah__ref{margin-top:.55rem;font-size:.74rem;letter-spacing:.14em;font-weight:600;
  text-transform:uppercase;color:var(--terracotta)}

/* The write-on. Strokes are drawn right → left, the direction the script is
   actually written, so the line reads as being set down by a pen rather than
   uncovered by a moving edge. The earlier version wiped a rectangle across the
   line, which sliced harakat in half as it passed; a stroke draw brings each
   mark in with the letter it belongs to.

   Nothing here hides the ayah. The mask ships ATTACHED in the markup with a
   WHITE backdrop, i.e. revealing everything, so the no-JS state and the shipped
   state are the same file. site.js blackens that rect on load — long before the
   mission can be scrolled to — so the first thing a reader sees of the verse is
   the pen drawing it. If site.js 404s or throws, the rect stays white and the
   verse is simply on the page.

   `mask` is set as a presentation attribute precisely so this CSS can beat it:
   a reduced-motion user who changes the setting mid-draw gets the whole verse
   on the next frame. It targets .ayah__ink — the <g> — and NOT the outermost
   <svg>: masking the root routes through CSS masking on a replaced element,
   where maskUnits="userSpaceOnUse" resolves against the CSS box (503x102)
   rather than SVG user space (x 317-1095, y 51-205), so nearly every stroke
   falls outside the box and the draw shows a fragment and then snaps. */
@media (prefers-reduced-motion:reduce){
  .ayah__ink{mask:none!important}
}

/* Below the flex wrap point the dateline and ayah stack, so the ayah loses its
   right edge and hangs off the section's left edge with everything else. */
@media (max-width:860px){
  .mission__lead{max-width:none}
  .ayah{margin-left:0;text-align:left}
  .ayah__ar{margin-left:0}
  .ayah__svg{margin-left:0}
}

/* ---------- mission, layout M7 — LOCKED 2026-08-01 ------------------------ */
/* Replaced M3, the editorial diagonal live since 2026-07-28. It was compared
   against M3 on the real page behind a localStorage switcher for a day; the
   switcher, the losing section and the inverted-default machinery are all gone
   and this is simply the mission section now. See DESIGN-LIBRARY for the
   measurements that settled it.

   Full-bleed, so it overrides .mission's padding and carries no .wrap — each
   half pads itself to the same gutter .wrap would have used. */
.mission--m7{padding:0;background:none}
/* min-height lowered 2026-08-01 so it stops padding the band out beyond what
   either column needs. The band's height should be set by the LEFT column — six
   lines of statement plus the kicker and dateline, ~360px — and the verse group
   sized to meet that, rather than both columns floating in a taller box. */
.mission7{display:grid;grid-template-columns:1fr 1fr;align-items:stretch;
  min-height:clamp(380px,50vh,560px)}
.mission7__half{display:flex;align-items:center}
/* THE OUTER PADDING REPRODUCES .wrap's GUTTER EXACTLY, in percentages.
   A half is a `1fr` grid item, so its percentage padding resolves against the
   grid area — half the section — and the section is a full-bleed block, i.e.
   the true content width with no scrollbar in it. So `100%` here is W/2:
     · 12%              = 6% of W  — .wrap's gutter while .wrap is 88% wide
     · calc(100% - 730px) = W/2 - 730 = (W - 1460)/2 — its gutter once .wrap has
                          hit its 1460px cap at 1659px and stopped growing
   max() picks whichever is currently larger, which is exactly the crossover.
   Deliberately NOT `max(6vw, calc((100vw - 1460px)/2))`, the obvious version:
   100vw includes the classic scrollbar and .wrap's 88% does not, so that lands
   ~7px right of every other section on the page — a small misalignment, but
   this section sits between two that do line up and it would read as sloppy. */
.mission7__half--l{background:var(--band-dark);
  padding:clamp(3rem,7vh,5rem) clamp(1.6rem,4vw,4rem)
          clamp(3rem,7vh,5rem) max(12%,calc(100% - 730px))}
/* Hard against that gutter, NOT margin-left:auto. The lab pushed this block
   right so it hugged the split and let the lattice fill the outer space, which
   works in isolation; on the real page it starts the statement ~120px right of
   the About section above and the Experience below, and reads as a section that
   has slipped. Aligned, the spare room falls in the middle where the split
   already is. */
.mission7__half--l .mission7__inner{max-width:34rem;margin-right:auto}
/* THE DARK HALF: two real stops, never a flat fill. A flat dark solid at this
   size gets spatially dithered by the display pipeline into a diagonal hairline
   spanning the band — the standing bug in CLAUDE.md. Gradient plus the noise
   layer below gives the dither nothing to lock onto. */
.mission7__half--r{position:relative;isolation:isolate;color:var(--cream);
  background:linear-gradient(158deg,#932B1C 0%,#86251B 54%,#662013 100%);
  justify-content:center;
  padding:clamp(3rem,7vh,5rem) max(12%,calc(100% - 730px))
          clamp(3rem,7vh,5rem) clamp(1.6rem,4vw,4rem)}
/* 42rem is past the space that actually exists between the two paddings (~666px
   at 1920), so the flex container is what bounds this — i.e. the verse takes
   every pixel available and the cap only bites on very wide monitors. */
.mission7__half--r .mission7__inner{max-width:42rem;position:relative}
.mission7__noise{position:absolute;inset:0;z-index:-1;opacity:.035;
  mix-blend-mode:overlay;pointer-events:none;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>")}
.mission7 .rule{margin:1rem 0 1.5rem}
/* 1.36rem/1.65 against M3's 2.6rem/1.30 — the whole point of the comparison.
   544px and 48 characters a line, versus M3's 1460px and 73. */
.mission7__lead{margin:0;font-weight:400;letter-spacing:0;
  font-size:clamp(1.12rem,1.55vw,1.36rem);line-height:1.65}
.mission7__lead em{font-style:italic;color:var(--terracotta)}
.mission7 .mission__sig{margin-top:2rem}
/* fill:currentColor on .ayah__svg is what puts the verse in cream here without
   the markup differing from M3's. */
.ayah--m7{margin:0;text-align:left;max-width:none}
/* width:auto RESETS .ayah__ar's shared `width:fit-content`, and it is load
   bearing. The stacked rule below sizes the verse with `min(100%, …)`, and a
   percentage against a shrink-to-fit parent is circular — the browser drops
   back to the 300px default width for a replaced element, so the verse rendered
   at a fixed 300px at every mobile width instead of filling the column. Made
   the parent a normal block and the percentage resolves against the column. */
.ayah--m7 .ayah__ar{margin:0;color:var(--cream);width:auto}
/* SCALED UP 2026-08-01, client: the verse was small and the red half was mostly
   empty. 620px against the previous 480 — the ceiling is set by the inner's
   40rem (640px), which itself is bounded by the half minus its two paddings
   (666px at 1920), so the verse can grow to 620 and still clear both edges.
   33vw rather than 27vw so it keeps that proportion down the range instead of
   only being bigger on wide screens. */
.ayah--m7 .ayah__svg{margin-left:0;width:clamp(240px,36vw,660px)}
/* Pushed well clear of the verse and set larger. The old 1.1rem gap tucked the
   translation under the Arabic as a caption; at this size the verse is the
   headline and the translation is the line you actually read, so it gets air
   above it and a size that admits it is body copy.
   Contrast checked on the gradient, not assumed: the cream at .82 measures
   6.3:1 over the mid stop and 5.3:1 over the lightest one, and the gold
   reference 4.7:1 — all clear of 4.5:1. */
.ayah--m7 .ayah__en{color:rgba(244,238,228,.82);max-width:30ch;
  margin-top:clamp(2rem,6vh,3.6rem);
  font-size:clamp(1.06rem,1.5vw,1.32rem);line-height:1.55}
.ayah--m7 .ayah__ref{color:var(--gold);margin-top:1.1rem;font-size:.82rem}
@media (max-width:900px){
  .mission7{grid-template-columns:1fr;min-height:0}
  /* 6%, not 6vw. Stacked, each half is a full-width grid item, so 100% is the
     section width and 6% is .wrap's gutter exactly — where 6vw is ~1px wide
     because it counts the scrollbar. Same reason as the desktop rule above. */
  .mission7__half--l,.mission7__half--r{padding-inline:6%}
  /* width:100% matters as much as max-width:none. These are flex items, so
     with max-width dropped they fall to shrink-to-fit — and the widest child is
     then the translation at its 30ch cap, which sized the whole group (verse
     included) to ~333px on a 768px tablet. Filling the half puts the `min(100%,
     560px)` above back on a real width. */
  .mission7__half--l .mission7__inner,
  .mission7__half--r .mission7__inner{margin-inline:0;max-width:none;width:100%}
  .ayah--m7 .ayah__svg{width:min(100%,560px)}
}


/* ---------- utilities ----------------------------------------------------- */
/* Screen-reader-only. Carries the ayah as real Arabic text alongside the SVG
   painting, so losing the live type costs the look of the glyphs and nothing
   else — the verse is still announced, indexed and selectable. */
.u-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);
  white-space:nowrap;border:0}

/* ---------- The Huda Experience ------------------------------------------- */
/* From lab/experience-05.html, approved 2026-07-30. Four columns of the
   school's own photographs drifting continuously, with the film between them;
   scrolling opens the film to fill the screen and pushes the columns aside.

   OUTSIDE THE BAND RHYTHM, like .mood — a full-bleed moment, not a band, so it
   sits between .mission and .values without flipping the alternation. Ground is
   the dark veil so the mission runs into it seamlessly; only slivers of it show.

   ── THE PERPETUAL-ANIMATION BAN ───────────────────────────────────────────
   The drift is a perpetual animation, which this project prohibits. It is built
   to the one sanctioned exception, whose three conditions it meets in full:
     1. GATED ON VISIBILITY — site.js block 12 adds .is-live only while the
        section is on screen and strips it on the way out.
     2. TRANSFORM ONLY — translate3d on the track. Compositor work; no layout,
        no paint, no filter.
     3. THE backdrop-filter NAV IS GONE, which was the other half of the stall.
   It is inert without JS: nothing moves until .is-live lands.

   The loop is seamless by construction — each track holds its photographs twice
   and travels exactly -50%, so the end frame is identical to the start. */
/* SOLID ground, no lattice. #F2E9DF is exactly what the translucent veil used
   to composite to over --paper, so the band's colour is unchanged and only the
   texture has gone. The lattice survives on the thresholds below, which makes
   it read as the edge material rather than as wallpaper. */
/* Slot 3 — LIGHT. It is in the rhythm now, not outside it: when this section
   was added it took the dark veil so the mission would flow into it seamlessly,
   which put two dark bands back to back. Light here restores the alternation,
   and the wall of photographs covers nearly all of the ground anyway, so the
   tone only shows in the slivers between columns. */
/* One token for the gap because the DRIFT DISTANCE DEPENDS ON IT — see the
   keyframe. Three places used to carry the same clamp() literal, and the
   keyframe silently assumed it. */
.xp{position:relative;background:var(--band-light);overflow:hidden;
  --xp-gap:clamp(.4rem,.7vw,.8rem)}
/* One band, not a runway. The earlier version put an expanding film in the
   middle, which needed 240vh of scroll and a sticky stage to open in. With copy
   there instead there is nothing to open, so the section is simply 0.9 of a
   screen — about a quarter of the page cost. */
.xp__stage{position:relative;display:flex;align-items:center;gap:var(--xp-gap);
  height:min(92vh,860px)}
/* Nothing on desktop: the columns stay direct flex children of the stage and
   this element does not generate a box. It earns its keep below 820px, where it
   becomes the wall layer — see the mobile block at the foot of this section. */
.xp__wall{display:contents}

/* flex:1 so the columns share whatever the film is not using and the outermost
   pair sits flush to the viewport edge. A fixed width left a band of paper down
   each side. Flex gaps apply only BETWEEN items, so there is no outer padding. */
/* flex:1 so the columns share the width the copy is not using and the outermost
   pair sits flush to the viewport edge. Flex gaps apply only BETWEEN items, so
   there is no outer padding to strip. */
.xp .col{flex:1 1 0;min-width:64px;align-self:stretch;overflow:hidden}
.xp .col__track{display:flex;flex-direction:column;gap:var(--xp-gap);
  will-change:transform}
.xp .col__track figure{margin:0;overflow:hidden;border-radius:2px;background:var(--paper-3)}
/* height:auto is load-bearing: the <img> carries width/height attributes so the
   column never reflows as photographs arrive, but those become presentational
   hints — width:100% overrides the width hint while NOTHING overrode the height
   one, so height:560px applied, both axes were then set, aspect-ratio was
   ignored and every tile rendered at its full source height. */
.xp .col__track img{width:100%;height:auto;aspect-ratio:1/1;object-fit:cover}

/* Still until .is-live lands — no JS, or off screen, means no motion. */
.xp.is-live .col__track{animation:xp-drift var(--dur) linear infinite}
.xp.is-live .col--up   .col__track{animation-direction:normal}
.xp.is-live .col--down .col__track{animation-direction:reverse}
/* -50% IS HALF A GAP SHORT OF THE SEAM, and was, silently, all along.
   A track of M tiles of height t with gap g is M*t + (M-1)*g tall, but the
   distance that maps every tile onto the position of the one a set later is
   (M/2)*(t+g). The difference is exactly g/2 — about 4px — so the loop landed
   4px off every cycle and jumped. It is independent of how many sets the track
   holds, so it does not go away now that site.js can add more than one. */
@keyframes xp-drift{
  from{transform:translate3d(0,0,0)}
  to  {transform:translate3d(0,calc(-50% - var(--xp-gap) / 2),0)}
}
/* Five speeds, none a multiple of another, or the columns fall into step and
   the wall reads as one sliding sheet. Keyed to explicit classes, never
   nth-of-type — index-based selectors here have already silently left a column
   with no --dur and therefore an invalid animation-duration once. */
.xp .col--1{--dur:58s} .xp .col--2{--dur:71s} .xp .col--3{--dur:64s}
.xp .col--4{--dur:83s} .xp .col--5{--dur:76s}

/* THE HEAD THRESHOLD IS GONE, client 2026-08-02: "remove the pattern thing and
   just make it flat against the divide, like the stat section above."

   What was here: an opaque band of the site's Khatim lattice across the top of
   the section, with the columns inset by exactly its height so the photographs
   clipped on the band's inner edge and read as passing behind it.

   Why it went, beyond being asked: the band was designed when this section sat
   after the MISSION, whose translucent veil let the lattice read as continuous
   page furniture closing over the photographs. Since the 2026-08-01 reorder it
   sits under the STAT BAND, which is opaque #E4D9C8 and carries no lattice at
   all — so the band had become a strip of pattern appearing from nowhere
   between two flat grounds, which is the opposite of the connective tissue it
   was there to be. The photographs now start at the section's own top edge and
   the stat band's lower boundary is the divide, doing the same job with one
   fewer device.

   `--xp-edge` went with it: the token existed only so the band height and the
   column inset could not drift apart, and there is now neither. */

/* ---- the frosted plate ---------------------------------------------------
   Absolute and centred, never a flex item: its size then cannot push the
   columns around, and centred stays centred at every width. Column 3 runs
   underneath it — which is where the extra column width came from, since the
   previous version reserved this space and let nothing into it.

   ── backdrop-filter, and the risk it carries ──────────────────────────────
   CLAUDE.md records this project's renderer being stalled by an infinite
   transform PLUS a backdrop-filter sticky nav, and "the backdrop-filter nav is
   gone" is one of the three conditions that permit any perpetual animation
   here. This pairs a blur with the drifting columns again, deliberately. Two
   things make it a different bet from the nav that caused the stall: the blur
   is on ONE small element rather than a full-width bar that repaints on every
   scroll frame, and the drift is transform-only and visibility-gated.

   THE LEGIBILITY FLOOR IS THE ALPHA, NOT THE BLUR. Blur smears the colours
   under the plate but does not lighten them — a dark photograph behind a purely
   blurred plate is still dark and the ink still fails on it. .82 keeps body copy
   comfortable over the darkest photographs in this set.
   Because the alpha does the work, THE SOLID FALLBACK LOOKS ALMOST IDENTICAL,
   which is the escape hatch: delete the two backdrop-filter lines and the
   design barely moves. */
.xp .plate{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:4;
  width:var(--xp-mid-w);max-width:92vw;text-align:center;
  padding:clamp(1.6rem,3.2vw,2.6rem) clamp(1.4rem,2.8vw,2.4rem);
  border-radius:6px;border:1px solid rgba(168,50,38,.16);
  box-shadow:0 30px 70px -46px rgba(52,35,26,.5);
  background:rgba(242,233,223,.82);
  -webkit-backdrop-filter:blur(16px) saturate(1.12);
          backdrop-filter:blur(16px) saturate(1.12)}
/* Browsers without backdrop-filter get the opaque plate — the same thing the
   escape hatch would produce. */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .xp .plate{background:#F3EBE1}
}
.xp .kicker{margin:0 0 .6rem}
.xp__h{margin:0;font-weight:600;letter-spacing:-.03em;line-height:1.03;
  font-size:clamp(1.9rem,3.6vw,3.1rem)}
.xp__h em{font-style:normal;color:var(--terracotta)}
.xp__rule{margin:1.1rem auto 0}
.xp__lead{margin:1.3rem 0 0;color:var(--ink-soft);font-size:clamp(1rem,1.2vw,1.12rem)}
.xp .btn{margin-top:1.4rem}

@media (prefers-reduced-motion:reduce){
  .xp.is-live .col__track{animation:none}
}
/* Columns drop out as the viewport narrows — five, four, three, two — so the
   tiles never shrink into thumbnails. The drift is the idea, so it is always
   the column count that gives way, never the motion. */
@media (max-width:1100px){ .xp .col--5{display:none} }

/* ---- mobile, REBUILT 2026-08-03 ------------------------------------------
   WHAT WAS BROKEN. The rule here was `height:auto;min-height:min(96vh,760px)`.
   The columns are flex items with align-self:stretch, so on a container of
   height:auto THEY set the height — and a track is deliberately twice its own
   content so the loop can close, so the stage grew to the full doubled track:
   1519px, 1.8 screens, at 390px wide. Three things broke at once.

     1. The band stopped being a band. It was the tallest section on the page.
     2. The plate is centred on the stage, so the heading sat ~760px down — a
        phone reader met a wall of photographs with no title on it, scrolled
        past the title, then met more wall.
     3. THE WALL EMPTIED. The drift travels -50% of the track, which reads as
        continuous only while the column CLIPS it — the window has to be the
        band, with the rest of the track waiting above and below. With the
        whole track visible there was nothing in reserve, so the photographs
        slid up and left blank ground behind them. That is the "broken
        background" the client reported, and it is this line, not the images.

   THE FIX is to stop the columns setting the height. .xp__wall becomes an
   absolutely positioned layer (inset:0) so the wall fills whatever the stage
   is and clips again, and the PLATE becomes the stage's only in-flow child —
   so the height is now driven by the copy, which is the thing that actually
   has a minimum on a phone. min-height keeps it a band when the copy is short;
   it can still grow if the copy is tall, and because the wall is inset:0 it
   grows with it and can never run out.

   INVARIANT WORTH KNOWING: the loop only stays seamless while half a track is
   at least as tall as the stage — i.e. 4 tiles per column >= stage height. It
   holds at two columns (tiles ~184px at 390px wide, 736 >= 620) and it is why
   the column count, not the tile size, is what gives way as the viewport
   narrows. Three columns on a phone would put 124px tiles against a 620px
   stage and the tails would open up again. site.js block 12 now enforces this
   by repeating each track until it is satisfied, so it is belt and braces. */
@media (max-width:820px){
  .xp{--xp-mid-w:min(86vw,460px)}
  .xp .col--4{display:none}
  .xp__stage{display:flex;align-items:center;justify-content:center;
    height:auto;min-height:min(84vh,620px);
    padding:clamp(2.2rem,6vh,3.6rem) 0}
  .xp__wall{display:flex;position:absolute;inset:0;gap:var(--xp-gap)}
  /* In flow now, so it sets the height instead of floating over nothing. */
  .xp .plate{position:relative;left:auto;top:auto;transform:none;
    flex:0 1 auto;width:var(--xp-mid-w)}
}
@media (max-width:560px){ .xp .col--1{display:none} }

/* ---------- core values: film strip + copy on the photograph --------------- */
/* lab/values-01.html S5 x V2, chosen by the client 2026-08-02. It replaced the
   sticky-scroll collage, which was ~540 lines here plus a scroll controller in
   site.js; both are gone rather than parked. The nine unchosen implementations
   live in the lab if any of this is ever revisited.

   THE SECTION IS NORMAL HEIGHT AGAIN. The collage was `height:150vh` with a
   sticky child, which is why it needed a scroll controller at all. This is an
   ordinary two-column section, so it costs the page no extra scroll runway and
   nothing here is tied to scroll position. */
.vx{background:var(--band-dark);padding:clamp(3.5rem,10vh,7rem) 0;
  position:relative;overflow-x:clip}
.vx__head{margin-bottom:clamp(2rem,4.5vw,3.4rem)}
.vx .kicker{font-size:clamp(1.25rem,2.1vw,1.9rem);letter-spacing:.07em;margin:0 0 .2rem}
.vx .rule{margin:1rem 0 0}

/* View LEFT, strip RIGHT — the client's instruction. The strip column is capped
   at 31% rather than left as a bare fraction: its frames are meant to be legible
   previews, and width is half of what makes a wide photograph readable.
   CENTRED, not top-aligned, so neither column leaves a wedge of bare tan. */
.vx__grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(280px,31%);
  gap:clamp(1.8rem,4vw,4rem);align-items:center}

/* Panels stacked in ONE grid cell — the container is always as tall as the
   tallest of the four, so switching values never reflows the page below. */
html.js .vx__view{display:grid}
html.js .vp{grid-area:1/1;opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity 240ms ease}
html.js .vp.is-active{opacity:1;visibility:visible;pointer-events:auto}
/* No-JS: the selector cannot work, so it goes and all four read in flow. */
html:not(.js) .vx__sel{display:none}
html:not(.js) .vp + .vp{margin-top:clamp(2.4rem,5vw,4rem)}

/* ---- the panel: the copy is written on the photograph -------------------- */
/* THE SCRIM IS THE WHOLE RISK HERE and it is measured against the picture, not
   eyeballed. An earlier gradient on this page faded to zero over the band where
   the tallest strokes sit and measured 2.35:1 while looking fine, because most
   of the text sat low where the scrim was dense. This one holds >=.72 alpha
   through the ENTIRE copy band and only releases above it.
   Cream is set at FULL alpha — a cream at .72 measured 2.7:1 on the hero and
   that lesson is not worth re-learning. */
.vp{position:relative;border-radius:14px;overflow:hidden;isolation:isolate;
  box-shadow:0 1px 2px rgba(56,36,19,.10),0 26px 60px -30px rgba(56,36,19,.45)}
.vp__media{margin:0}
.vp__media img{display:block;width:100%;height:auto;aspect-ratio:5/4;object-fit:cover}
/* 2026-08-02: the first three stops are UNTOUCHED — they are the measured copy
   band and the 2.35:1 lesson above is exactly what re-tuning them would repeat.
   What changed is only what happens ABOVE the copy: it used to still be at .40
   two-thirds up and .12 near the top, which is what made the photograph read as
   tinted rather than lit. It now releases to nothing by 70%. No text lives up
   there, so no measured ratio moves. */
.vp::before{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(0deg,
    rgba(28,13,4,.90) 0%, rgba(28,13,4,.86) 26%, rgba(28,13,4,.72) 46%,
    rgba(28,13,4,.22) 58%, rgba(28,13,4,0) 70%)}
/* Every large dark area on this page is a gradient PLUS noise, never a flat
   fill, or the display pipeline dithers a visible diagonal hairline across it.
   Scoped to .vp — a bare .noise here would resolve `inset:0` against .vx and
   paint the whole section. */
.vp__noise{position:absolute;inset:0;z-index:2;pointer-events:none;
  opacity:.04;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>")}
.vp__copy{position:absolute;inset:auto 0 0;z-index:3;
  padding:clamp(1.3rem,3vw,2.6rem);color:var(--cream);max-width:44rem}
.vp__t{font-family:"Cormorant Garamond",Georgia,serif;font-weight:700;line-height:1.02;
  letter-spacing:-.015em;margin:0 0 .6rem;font-size:clamp(2rem,4vw,3.2rem);
  color:var(--cream)}
.vp__lead,.vp__rest{margin:0 0 .8rem}
.vp__act{margin:1.2rem 0 0}

/* ---- the strip: the open frame vacates ------------------------------------ */
/* lab/values-02.html fix D + selected-state r1, chosen by the client 2026-08-02
   to answer two faults in the version that shipped hours earlier:

     1. "it doesn't make sense to have the image twice — once on the selector
        and another time in the actual screen"
     2. "when you choose one it gives it like a red border, idk if I like that"

   THE DUPLICATION WAS STRUCTURAL, NOT A STYLING SLIP. A selector made of the
   photographs, beside a view made of the same photograph, shows the chosen
   picture twice by construction — no amount of tuning fixes that. So the OPEN
   FRAME GIVES UP ITS PICTURE: the live frame drops to a terracotta plate
   carrying only the name, because its photograph is already showing at size in
   the panel next to it. At no moment is any image on screen twice.

   AND THAT SWAP IS ALSO THE SELECTED-STATE, which is why the ring is gone
   rather than restyled. Changing a frame's entire ground is the largest state
   change available; a 3px marker on top of it would be labelling something
   already unmissable. The other three cues — size, brightness, the label going
   full cream — all still hold.

   THE COST, STATED: the strip is at its least photographic exactly where you
   are looking. If that ever reads as a hole rather than as a deliberate step
   aside, lab/values-02.html has three other fixes and the ring options with it.

   (The old note about the ring not being expressible as an inset shadow or an
   `outline` has gone with the ring. It is preserved in values-02 if a marker is
   ever wanted back — the constraint is real and non-obvious.) */
.vsel{display:flex;flex-direction:column;margin:0;padding:0;
  gap:.5rem;height:clamp(440px,66vh,680px)}
.vsel__i{appearance:none;-webkit-appearance:none;background:none;border:0;margin:0;
  font:inherit;color:inherit;text-align:left;cursor:pointer;width:100%;
  position:relative;display:flex;align-items:flex-end;
  flex:1 1 0;min-height:0;padding:0;
  border-radius:12px;overflow:hidden;isolation:isolate;
  transition:flex-grow 420ms cubic-bezier(.22,1,.36,1),background-color 360ms ease}
.vsel__i:focus-visible{outline:3px solid var(--terracotta);outline-offset:3px}
/* 1.35, DOWN FROM 2. The open frame no longer has a photograph to show, so the
   height it used to need is height spent on a flat plate. It still has to read
   as the largest frame — it is the selected one — but at 2 the plate was 40%
   of the strip and the section's biggest single area of flat colour. 1.35 keeps
   the rank and gives the three photographs their space back. */
.vsel__i.is-active{flex-grow:1.35;
  background:linear-gradient(160deg,var(--terracotta) 0%,var(--terracotta-deep) 100%)}
/* Gradient, not a flat fill — the standing rule for every large coloured area
   on this page, and this plate is large enough to dither if left flat. */
.vsel__thumb{display:block;position:absolute;inset:0;z-index:-2;
  transition:opacity 300ms ease}
.vsel__i.is-active .vsel__thumb{opacity:0}
/* NO DUOTONE on the closed frames. A terracotta duotone was tried and made them
   both unreadable and yellow. A duotone suits a card you are meant to
   RECOGNISE and ruins a thumbnail you are meant to READ. Plain
   dim-and-desaturate instead — no hue shift, so a closed frame is the same
   photograph, only quieter. */
.vsel__thumb img{width:100%;height:100%;object-fit:cover;
  filter:saturate(.72) brightness(.68);transition:filter 360ms ease}
.vsel__i:hover .vsel__thumb img{filter:saturate(.9) brightness(.86)}
/* The foot scrim exists to hold cream type over a photograph. The open frame
   has no photograph, so it would be darkening the terracotta plate for nothing
   — and muddying it, since the plate is already dark enough for cream. */
.vsel__i::after{content:"";position:absolute;inset:auto 0 0;height:72%;z-index:-1;
  background:linear-gradient(0deg,rgba(28,13,4,.88) 0%,rgba(28,13,4,.58) 44%,rgba(28,13,4,0) 100%);
  transition:opacity 300ms ease}
.vsel__i.is-active::after{opacity:0}
.vsel__t{position:relative;display:inline-block;color:var(--cream);
  font-family:"Cormorant Garamond",Georgia,serif;font-weight:700;line-height:1.05;
  letter-spacing:-.012em;font-size:clamp(1.1rem,1.8vw,1.5rem);
  padding:.7rem .9rem;transition:color 240ms ease}

/* ---- responsive ---------------------------------------------------------- */
/* Strip ABOVE the panel on small screens: you tap the control, then read the
   result. A control below its own output means every tap scrolls. */
@media (max-width:900px){
  .vx__grid{grid-template-columns:1fr;gap:clamp(1.6rem,4vw,2.4rem)}
  .vx__sel{order:-1}
  /* ON A PHONE THE STRIP STOPS BEING AN ACCORDION, and that is structural
     rather than a preference. Turned horizontal, a closed frame is ~43px wide
     at 390 and the label on it clipped — "Excellence" and "Grounding" both lost
     their tails. Opening it less bought 64px, still short of the ~68px the word
     needs, and breaking mid-word gave "Excellenc / e". The only way to keep the
     label ON the frame was ~11px type, below the floor for readable text.
     So: four EQUAL frames, labels underneath on the paper where they have the
     full frame width. At 390 each frame is 81px and "Excellence" sets on one
     line at .8rem. Dropping the closed labels was the other option and is
     exactly the note already given about the newsletter tiles — on a phone,
     unlabelled photographs "just look like an image".
     Selection then rests on three cues: full colour, terracotta label, and a
     rule under it. The rule matters because saturation and hue are both colour,
     and colour alone must not be the only signal. The border is on every label,
     transparent until active, so nothing moves when the selection changes. */
  /* THE STRIP DROPS ITS PICTURES AND BECOMES FOUR SPINES — client 2026-08-03,
     "rather than putting the image, write the word vertically in bold, the B at
     the top and then downwards, and you click it and it shows the image below."

     It is also the answer to the problem the note above was working around. The
     old horizontal strip showed each photograph at 77x228 with the label under
     it — and the SAME photograph again, at full size, in the panel directly
     below. Every value was therefore on screen twice, which is the exact fault
     the desktop layout spends its open-frame vacate avoiding, reintroduced by
     the stacking. Dropping the thumbnails removes the duplicate outright, and
     it removes the reason the label had to shrink to .8rem and sit off the
     frame: set vertically, the word has the frame's whole height to run down,
     so it can be larger here than it was when it sat horizontally.

     writing-mode + text-orientation:upright, NOT a rotation — the client asked
     for the letters stacked reading downwards, which a 90-degree rotate does
     not give. letter-spacing works along the inline axis, and the inline axis
     is now vertical, so it is what opens the letters up. Cormorant to echo
     .vp__t, which is the same word set large in the panel it opens. */
  /* Centred and capped: a spine is a spine. Left to flex:1 1 0 alone each frame
     is 163px wide at 768 with one column of letters down the middle of it, which
     reads as four empty cards rather than four labels. 120px is about the width
     the word wants. Phones are well under the cap and are unaffected. */
  .vsel{height:auto;flex-direction:row;gap:.5rem;align-items:stretch;
    justify-content:center}
  /* justify-content START, not centre: the client asked for the first letter at
     the top of the frame and the word running down from it, so all four begin
     on the same line and the short ones (Taqwa) simply end sooner. */
  /* overflow VISIBLE here, not hidden. The frame has nothing left to clip — the
     thumbnail it was written for is display:none in this block — and while it
     was on, a word that did not fit was silently CUT instead of spilling
     somewhere you could see. Visible means this class of fault can only ever be
     obvious, never quiet. */
  .vsel__i{flex:1 1 0;max-width:120px;height:auto;overflow:visible;padding:0;gap:0;
    flex-direction:column;justify-content:flex-start;align-items:center;
    border-radius:12px;border:1px solid rgba(168,50,38,.14);
    background:linear-gradient(160deg,var(--paper) 0%,var(--paper-2) 100%);
    box-shadow:0 1px 2px rgba(56,36,19,.08)}
  .vsel__i::after{display:none}
  /* The picture is the panel's job now. Kept in the markup so nothing else has
     to change and the desktop rail still has it. */
  .vsel__thumb{display:none}
  .vsel__i.is-active{flex-grow:1;
    background:linear-gradient(160deg,var(--terracotta) 0%,var(--terracotta-deep) 100%);
    border-color:transparent}
  /* SIZE AND FACE ARE TREATMENT D from lab/values-spine-01.html, client pick
     2026-08-03. The first attempt set this at 1.12rem and read as jitter: a
     stacked column centres every glyph on its own width, so Cormorant's
     hairline I landed under its heavy O, and at ~18px those hairlines are near
     enough a single pixel to shimmer. The fix is not a different face — it is
     the size. At ~26px the thin strokes resolve, the column steadies, and the
     serif still echoes .vp__t, which is this same word set large in the panel
     the spine opens. B and C in that file are the Inter answers if this is ever
     reopened; the cost of D, stated, is that it is the tallest of the four. */
  /* THE PADDING IS THE ONLY CLEARANCE THERE IS, which is why it is generous.
     The frame is as tall as the longest word and no taller — it auto-sizes to
     it — so the longest word's breathing room is exactly this padding and
     nothing else. At .9rem it was 15px against a 12px corner radius, which put
     the final E of EXCELLENCE and the G of BELONGING inside the curve of the
     frame: not clipped, but reading as clipped, which is the same complaint.
     Client 2026-08-04, "they get cut off in the box". Raised so the last letter
     clears the corner. Every frame grows by the same amount, so they stay level.
     -webkit- prefix for Safari before 14, which otherwise ignores the property
     and lays the word on its side instead of stacking it. */
  .vsel__t{writing-mode:vertical-rl;
    -webkit-text-orientation:upright;text-orientation:upright;
    width:auto;text-align:center;text-transform:uppercase;
    color:var(--ink);font-family:"Cormorant Garamond",Georgia,serif;
    font-size:clamp(1.5rem,6.6vw,1.62rem);
    letter-spacing:.02em;line-height:1.04;
    padding:clamp(1.4rem,5.5vw,1.9rem) .25rem;border-bottom:0}
  .vsel__i.is-active .vsel__t{color:var(--cream)}
}
@media (max-width:560px){
  .vp__media img{aspect-ratio:3/4}
}
/* Every state change survives; only the travel goes. */
@media (prefers-reduced-motion:reduce){
  html.js .vp,.vsel__i,.vsel__thumb img,.vsel__t{transition:none}
}

/* ---------- shared section head ------------------------------------------- */
/* Heading left, "see all" right. Same rail for events and newsletters so two
   adjacent list sections read as a pair rather than two inventions. */
.sec-head{display:flex;align-items:flex-start;justify-content:space-between;
  gap:1.5rem;flex-wrap:wrap;margin-bottom:clamp(1.8rem,3.5vw,2.6rem)}
.sec-head .kicker{font-size:clamp(1.25rem,2.1vw,1.9rem);letter-spacing:.07em;margin:0 0 .2rem}
.sec-head .rule{margin:1rem 0 0}
.sec-head__link{align-self:flex-end;text-decoration:none;font-weight:600;
  font-size:.92rem;color:var(--terracotta);white-space:nowrap;
  border-bottom:1px solid rgba(168,50,38,.3);padding-bottom:2px;
  transition:color 180ms ease,border-color 180ms ease}
.sec-head__link:hover{color:var(--terracotta-deep);border-color:var(--terracotta-deep)}

/* ---------- quick-nav rail ------------------------------------------------ */
/* From lab/qnav-02.html, hover F ("curtain"), chosen by the client 2026-08-02.
   Six photographs of the school, each a shortcut to a page that does not exist
   yet. Sits directly after the mission because the client placed it there.

   Slot: LIGHT, and it is not a free choice. The mission above ends on a split
   of tan veil and deep terracotta; .events below is the tan veil. A dark rail
   between them puts three tan bands back to back and the alternation collapses
   — the same failure .events and .xp each already had to be corrected for. On
   paper the rail also does its own job better: these are dark cards, and dark
   cards on paper read as objects sitting on the page.

   THE HOVER IS A CURTAIN. A terracotta panel wipes up over the lower 31% of the
   photograph and the copy lands on it. Three things about it are deliberate:

     - It is scaleY from a bottom origin, NOT height. A compositor property, so
       the panel costs nothing to animate and cannot reflow anything. Every
       card in this rail keeps its exact box for the whole gesture, which is
       what stops the row twitching under a trackpad mid-swipe and what stops
       scroll-snap re-evaluating mid-gesture.
     - The panel sits BEHIND the text (z-index 1 against the body's 2), so the
       title is legible against the photograph before the wipe and against
       terracotta after it, and never caught halfway between the two.
     - The scrim RETREATS as the curtain arrives. Two dark layers stacked would
       turn the terracotta to mud, and once the panel is up it is the panel
       doing the legibility work.

   WHAT THE CLIENT CORRECTED, so nobody re-adds it: there is no eyebrow, no
   grade-band line, and no circle-arrow badge in the corner. The title is the
   page name, at Cormorant 700 and roughly double the newsletter tiles' size,
   and it is the only thing on the card at rest. */
.qnav{background:var(--band-light);padding:clamp(3.5rem,10vh,7rem) 0;
  --ease:cubic-bezier(.22,1,.36,1);
  /* The card width, and the whole point of the second lab round: the client's
     note on the first was "these are just really small", twice. 460px is 2.7x
     the area of that version and puts ~2.9 cards across .wrap where five fit
     before. The rail deliberately shows fewer destinations, larger. */
  --qn-w:clamp(260px,32vw,460px)}

/* The shared .hscroll is a three-up track by default — right for the flyers and
   the newsletter tiles, wrong here. Two classes deep so these beat the shared
   single-class rules at every width; the two media blocks below have to repeat
   the specificity or the desktop width would win on a phone as well. */
.qnav .hscroll__track > *{width:var(--qn-w)}
.qnav .hscroll__track{gap:clamp(1rem,1.8vw,1.7rem);
  /* Deeper than the shared padding because these shadows are much larger, and
     the track is their scroll parent, so it is the thing that would clip them. */
  padding:1rem .4rem 2rem;margin-inline:-.4rem}
@media (max-width:900px){ .qnav .hscroll__track > *{width:74%} }
@media (max-width:560px){ .qnav .hscroll__track > *{width:86%} }

/* THE RAIL RUNS WIDER THAN THE TEXT COLUMN, and it has to, because of an
   arithmetic accident rather than a taste call. Measured at 1905: .wrap is
   1460, three 460px cards plus two 27px gaps are 1434, and the fourth card is
   therefore left with **-1px**. It read as a hairline sliver against the right
   edge — the client's note was that it "gets cut off very early".

   Anything that only shows a few pixels of the next card is worse than showing
   none: it looks like a rendering fault rather than like an affordance. The
   fourth card has to show enough to be recognisably a card.

   THE BLEED IS THE ONE .events ALREADY USES, cap and all, and the cap is not
   arbitrary. The arrows hang 22px outside .hscroll and carry a 3px ring, so
   26px of each gutter is already spoken for; a bleed that spends the whole
   gutter puts the arrows OUTSIDE THE VIEWPORT and the entire document scrolls
   sideways. That exact bug shipped once on this site (see the .events note) and
   this is the formula that fixed it: 6.8182% is the gutter expressed against
   .wrap rather than the viewport — gutter is 6vw, .wrap is 88vw, 6/88 — which
   is what makes it scrollbar-safe, since 100vw INCLUDES the classic scrollbar
   and 88% does not.

   Result at 1905: the track goes 1460 -> 1607 and the fourth card shows 146px,
   about a third of itself. The cards also all shift left by 73px, which is the
   other half of what was asked for.

   THE HEADING DOES NOT MOVE. It stays on .wrap's spine with every other section
   heading on the page, and .events does exactly the same thing directly below —
   so a bleeding track under a heading on the spine is already this page's
   established shape, not a new one. */
@media (min-width:901px){
  .qnav .hscroll{margin-inline:calc(-1 * clamp(0px,6.8182% - 26px,110px))}
}
/* HEIGHT GUARD, and it is the one thing the lab file did not need. The card is
   4:5, so its height is always its width x 1.25 — and below 900px the width is
   a PERCENTAGE OF THE VIEWPORT, which means the taller the card is allowed to
   get the wider the screen. Measured at 900px: 576 wide and **720 tall**, on a
   laptop whose viewport is about 800. The card was taller than the fold, and it
   got there by being on a bigger screen, which is backwards.
   Capping the WIDTH at 57.6vh caps the HEIGHT at 72vh (57.6 x 1.25) at every
   width at once — continuously, with no extra breakpoint to keep in step, and
   without touching the three width rules above. It only ever binds on short
   wide viewports, which is exactly the case that was broken. */
.qnav .hscroll__track > *{max-width:57.6vh}

.qn{position:relative;display:block;aspect-ratio:4/5;border-radius:14px;
  overflow:hidden;text-decoration:none;color:inherit;background:#1E150F;
  box-shadow:
    0 2px 4px rgba(56,36,19,.14),
    0 12px 24px -8px rgba(56,36,19,.18),
    0 34px 56px -18px rgba(56,36,19,.22);
  transition:transform 520ms var(--ease),box-shadow 520ms ease}
.qn:hover,.qn:focus-within{transform:translateY(-8px);
  box-shadow:
    0 2px 4px rgba(56,36,19,.14),
    0 20px 36px -8px rgba(56,36,19,.24),
    0 52px 84px -24px rgba(56,36,19,.30)}
.qn__img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  transition:transform 900ms var(--ease)}
.qn:hover .qn__img,.qn:focus-within .qn__img{transform:scale(1.07)}
/* Bottom-weighted, and stronger than the newsletter tiles' because the title
   here is nearly twice the size and sits over whatever the photograph happens
   to be doing — a sunlit car line on one card, a dim prayer hall on the next. */
/* 2026-08-02: the scrim used to darken again at the top (.30 at 100%) and never
   fully released in the middle (.14 at 62%), which read as a tint over the whole
   photograph — the client's words, twice. Nothing sits above the title, so that
   upper half was paying a legibility cost for text that isn't there. The band
   the title actually occupies is unchanged (.92/.60 across the bottom third);
   above it the gradient now clears completely. Measured after the change, not
   eyeballed — see the note on .vp::before for why that matters here. */
.qn__scrim{position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(0deg,rgba(18,10,6,.92) 0%,rgba(18,10,6,.60) 30%,
    rgba(18,10,6,.10) 56%,rgba(18,10,6,0) 72%);
  transition:opacity 520ms ease}
.qn:hover .qn__scrim,.qn:focus-within .qn__scrim{opacity:.35}
.qn__curtain{position:absolute;inset:auto 0 0 0;height:31%;z-index:1;
  pointer-events:none;
  background:linear-gradient(0deg,var(--terracotta-deep) 0%,var(--terracotta) 62%,
    rgba(168,50,38,.86) 100%);
  transform:scaleY(0);transform-origin:bottom center;
  transition:transform 560ms var(--ease)}
.qn:hover .qn__curtain,.qn:focus-within .qn__curtain{transform:scaleY(1)}
.qn__body{position:absolute;inset:auto 0 0 0;z-index:2;display:block;
  padding:clamp(1.1rem,2vw,1.7rem)}
/* Weight 700 at full alpha, and the two together are not decoration:
   contrast-ratio maths assumes a solid fill and a light serif's hairlines never
   render as one — the hero measured a comfortable 4.3:1 and still read washy
   until the weight went up. Cream on terracotta once the curtain is up. */
.qn__t{display:block;margin:0;font-family:"Cormorant Garamond",Georgia,serif;
  font-weight:700;font-size:clamp(1.75rem,2.9vw,2.9rem);line-height:1.02;
  letter-spacing:-.012em;color:#FDF8F1;text-wrap:balance}
/* 0fr -> 1fr animates a genuinely unknown height without measuring it in JS and
   without max-height's guess-a-number problem. It is inside a fixed-ratio card,
   so nothing outside this box moves. */
.qn__reveal{display:grid;grid-template-rows:0fr;opacity:0;
  transition:grid-template-rows 520ms var(--ease),opacity 380ms ease}
.qn__reveal > span{overflow:hidden;min-height:0;display:block}
.qn:hover .qn__reveal,.qn:focus-within .qn__reveal{grid-template-rows:1fr;opacity:1}
.qn__x{display:block;padding-top:.55rem;font-size:clamp(.9rem,1.05vw,1rem);
  line-height:1.45;color:#F1EBDF;font-weight:500;max-width:34ch}
.qn:focus-visible{outline:3px solid var(--terracotta);outline-offset:4px;border-radius:8px}

/* NO HOVER MEANS NO REVEAL. On a phone every part of this is dead, so the
   revealed state IS the resting state: curtain up, description on. Without it
   the rail on mobile is six photographs with a title and nothing else — which
   is the exact note the client gave about the newsletter tiles on 2026-07-30,
   and the reason .issue-tile__go is always visible rather than a hover state. */
@media (hover:none){
  .qn__curtain{transform:scaleY(1)}
  .qn__scrim{opacity:.35}
  .qn__reveal{grid-template-rows:1fr;opacity:1}
}
@media (prefers-reduced-motion:reduce){
  /* The state still arrives; only the travel is dropped. Removing the state as
     well would hide the description from the readers least able to go hunting
     for it. */
  .qn,.qn__img,.qn__scrim,.qn__curtain,.qn__reveal{transition-duration:1ms}
  .qn:hover,.qn:focus-within{transform:none}
  .qn:hover .qn__img,.qn:focus-within .qn__img{transform:none}
}

/* ---------- upcoming events ----------------------------------------------- */
/* Slot 4 — DARK. Was light, which put it against .values' light and started the
   run of same-tone sections that threw the rest of the page out of step. */
.events{background:var(--band-dark);padding:clamp(3.5rem,10vh,7rem) 0}
/* Now a .hscroll__track (see the shared block above) rather than a 3-up grid —
   the grid capped the section at three flyers.
   align-items:flex-start is load-bearing because the flyers set their own
   heights: flex would otherwise stretch every card to the tallest and leave the
   short one with a band of empty card under its own image, the exact dead space
   the natural-sizing decision exists to avoid. */
.events__grid{align-items:flex-start}
.event{margin:0}
.event__card{display:block;text-decoration:none;color:inherit;
  border-radius:12px;overflow:hidden;background:var(--paper-2);
  box-shadow:0 20px 46px -30px rgba(52,35,26,.5);
  transition:transform 300ms cubic-bezier(.22,1,.36,1),box-shadow 300ms ease}
.event__card:hover{transform:translateY(-4px);
  box-shadow:0 30px 60px -30px rgba(52,35,26,.6)}
.event__card:focus-visible{outline:3px solid var(--terracotta);outline-offset:3px}
/* ---------- the flyer: shown at its own size, whatever that is ----------
   NO FIXED RATIO, NO CROP, NO PAD. Client decision 2026-07-30 after seeing the
   alternatives side by side. Each poster is displayed exactly as uploaded and
   the card takes its height from it, so the row ends up uneven — which is the
   accepted cost, and the reason .events__grid top-aligns.

   Worth recording, because this is also the answer to the CMS question rather
   than a retreat from it: of everything tried, THIS is the only option that
   needs nothing from whoever uploads the image. No cropping to a spec, no
   focal-point picker, no processing step, and nothing can be silently cut off,
   because there is no box to fit. Square, 4:5, portrait, a phone screenshot —
   all just work. What was rejected and why:
     - object-fit:cover      crops to fill. On a poster that slices the text off
     - object-fit:contain    whole flyer, dead grey bands
     - blurred backdrop fill bands filled with a blown-up copy of the poster;
                             uniform and automatic, but the client did not like
                             the look of it
   If uneven card bottoms ever become the objection, the honest fix is to ask
   for portrait artwork in the CMS upload hint — guidance, not work. */
.event__flyer{display:block;width:100%;height:auto;background:var(--paper-3)}
.event__meta{display:flex;gap:.9rem;align-items:flex-start;padding:1rem 1.1rem 1.15rem}
/* Date block: the one thing a parent scans for, so it gets its own column
   rather than being buried at the front of a sentence. */
.event__date{flex:0 0 auto;text-align:center;line-height:1.05;color:var(--terracotta);
  font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;font-weight:600;
  padding-top:.15rem}
.event__date b{display:block;font-size:1.5rem;letter-spacing:-.02em;
  font-variant-numeric:tabular-nums}
/* NOT EVERYTHING HERE IS A DATED EVENT. The school also posts announcements and
   job ads — real examples of both are in this grid — and those have no day and
   month to put in the column. A word set at the 1.5rem the number uses looks
   like a mistake, so they get a small tag instead, which keeps the three cards'
   text columns aligned without pretending there is a date. */
.event__date--tag{padding:.28rem .5rem;border-radius:3px;
  background:rgba(168,50,38,.10);font-size:.62rem;letter-spacing:.13em}
.event__body{display:block;min-width:0}
.event__t{display:block;font-weight:600;font-size:clamp(1rem,1.25vw,1.1rem);
  letter-spacing:-.01em;line-height:1.25}
.event__time{display:block;margin-top:.2rem;font-size:.85rem;color:var(--ink-soft)}

@media (max-width:900px){
  .events__grid{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:600px){
  .events__grid{grid-template-columns:1fr;gap:1.6rem}
}

/* ---------- newsletter archive -------------------------------------------- */
/* Slot 5 — LIGHT. Client pick 2026-07-30 from lab/news-02.html variant A, after
   a day on variant B (dark throughout). Structure is unchanged between the two —
   lead issue over the back catalogue — only the GROUND moved back to paper.

   WHY THE DARK VERSION CAME OUT. It worked on its own and the photographs looked
   better on it, but it cost two things the paper version does not:
     · the band rhythm. Every other band here alternates two shades of warm paper
       (--band-light / --band-dark, the latter a tan veil). A near-black slot 5
       made slots 4, 5 and 6 all non-light and put the page's hardest tonal edge
       immediately above .stats, which then had to be the recovery from it with
       nothing in between. On paper the light → dark → light alternation is
       simply back, and the slot 4 and slot 6 notes about it are correct again.
     · scarcity. .footer, .mood and .cta are the page's dark moments. A fourth,
       much higher up, spent something that was not this section's to spend.

   WHAT SURVIVED THE MOVE, and this is the point of the variant: THE TILES STAY
   DARK. A photograph is a dark object, so the back catalogue still reads as a
   row of dark cards — it just sits on paper instead of in a dark room. The
   section gains contrast rather than losing it.

   If the dark ground is ever wanted back, lab/news-02.html variant B is intact
   and variant C is the halfway house (paper lead, dark strip for the tiles). */
/* 0.75x, client 2026-08-02: "the section is just too big". Applied to the
   SPACING and the lead's own type, not to everything — scaling the body copy
   and the flag with it would have taken 16px text to 12px and the uppercase
   flag under 10px, which is the section getting less readable rather than less
   tall. Every value below is the old one times .75, kept as a clamp so the
   ratio holds at every width instead of only at the cap. */
.news{background:var(--band-light);padding:clamp(2.6rem,7.5vh,5.25rem) 0}
/* (.news__intro, the standfirst, is gone with its markup — it restated what a
   newsletter is above a heading that already says "The newsletter".) */

/* ---- lead + back catalogue, side by side ---------------------------------
   The lead keeps the bulk of the row; the previous issues are a column beside
   it, not a band under it. That is where most of the height went: the section
   was a full-width card PLUS a full-width scroller PLUS the gap between them,
   stacked. Side by side, the tallest thing in the section is just the lead.

   3.17fr : 1fr, AND THIS NUMBER IS SOLVED, NOT PICKED. The back issues are
   squares whose covers fill them, so the column's WIDTH is also each square's
   HEIGHT — the ratio and the number of issues stop being independent choices.
   Setting two stacked squares plus their gap equal to the lead (16:9 cover plus
   its ~90px body row) gives a 342px square against a 1087px lead at the 1460px
   cap, and both columns end level. Change the issue count, the lead's aspect
   ratio or the body height and this has to be re-solved or one column dangles.
   It holds proportionally at narrower widths, so it is one value, not a set.
   align-items:start so nothing stretches to paper over a mismatch. */
.news__grid{display:grid;grid-template-columns:minmax(0,3.17fr) minmax(0,1fr);
  gap:clamp(1.1rem,2.2vw,1.9rem);align-items:start}
/* One column below 900. The lead's own media/body split has already stacked by
   then (see .issue-lead__in), so this is the second and last reflow. */
@media (max-width:900px){
  .news__grid{grid-template-columns:minmax(0,1fr);gap:clamp(1.6rem,4vw,2.2rem)}
}

/* ---- the lead issue: the current one, at size ----------------------------
   The card is back. On the dark ground it was dropped, because a cream card
   there was the brightest object on the page and read as a dialog dropped onto
   the section. On paper that argument is gone and the opposite one applies: with
   nothing but a tonal step to separate the lead from its ground, it needs an
   edge to be an object at all.

   ELEVATION IS BORROWED WHOLESALE FROM .tst__card, not re-invented. That section
   owns the page's only radius-and-shadow language and says so in its own note.
   It also already solved this exact problem: on --band-light a near-white card
   is about 1.07:1 against its ground, so the tonal-separation leg of "raised" is
   gone and only the shadow is left — which is why the hairline and the tightened
   contact layer below are not decoration, they are the replacement for it. Warm
   brown at low alpha, never grey: grey on a cream ground desaturates the paper
   under it and reads as a smudge rather than an edge. */
.issue-lead{position:relative;display:block;text-decoration:none;color:inherit;
  background:#FFFDFA;border-radius:14px;
  border:1px solid rgba(168,50,38,.13);
  box-shadow:
    0 1px 2px rgba(56,36,19,.16),
    0 4px 8px rgba(56,36,19,.12),
    0 14px 24px -6px rgba(56,36,19,.18),
    0 32px 52px -12px rgba(56,36,19,.20);
  transition:transform .22s cubic-bezier(.22,1,.36,1),box-shadow .22s ease}
.issue-lead:hover{transform:translateY(-6px);
  box-shadow:
    0 1px 2px rgba(56,36,19,.14),
    0 6px 12px rgba(56,36,19,.12),
    0 20px 34px -6px rgba(56,36,19,.22),
    0 44px 70px -12px rgba(56,36,19,.26)}
/* The lit top edge of a raised object — invisible on its own, and the thing that
   stops the card looking pasted on. A pseudo-element rather than an inset shadow
   on .issue-lead itself, because the media inside is clipped to the card and an
   inset would be painted under it. */
.issue-lead::after{content:"";position:absolute;inset:0;border-radius:14px;
  pointer-events:none;box-shadow:inset 0 1px 0 rgba(255,255,255,.75)}
/* STACKED, NOT SIDE BY SIDE — the card lost about 40% of its width when the
   back catalogue moved beside it, and a two-column split inside what is left
   gives the body ~300px of text column under a 3.4rem title. Picture over copy
   instead, which is also the shape the small entries beside it use, so the two
   columns read as the same component at two sizes. */
.issue-lead__in{display:block}
/* The media is flush to the card edge now rather than a free-standing framed
   picture, so the radius is inherited from the card's TOP corners (it was the
   left pair while this was a side-by-side split) and the body's padding is the
   only gap. */
/* ASPECT-RATIO, NOT MIN-HEIGHT. min-height:clamp(300px,30vw,440px) was sized
   for a half-width column; across the full card it made the picture the whole
   viewport on a narrow screen. The ratio is the asset's own 900x502, so the
   cover crop is doing nothing at any width and the reserved box always matches
   what loads — no shift when the image arrives. */
/* display:block IS LOAD-BEARING AND MUST STAY. This element is a <span>. It
   only ever rendered because .issue-lead__in was a grid and grid items get
   blockified; the moment that became display:block the span went back to
   display:inline, where aspect-ratio, overflow and height are all ignored — so
   the box collapsed to nothing and the lead lost its picture entirely. Any
   future change to .issue-lead__in's display must not assume this is a block. */
/* 16:9, not 3:2. The lead column is now over three times the width of the side
   column, so at 3:2 the cover alone was 725px tall and the section grew instead
   of shrinking. 16:9 takes ~120px back and is what the .news__grid ratio was
   solved against — the two values are a pair. */
.issue-lead__media{display:block;position:relative;overflow:hidden;aspect-ratio:16/9;
  background:var(--paper-3);
  border-radius:13px 13px 0 0}
.issue-lead__media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
/* NOTE: the media no longer lifts on its own. On the dark ground it was a
   free-standing framed picture and had its own hover transform; inside a card
   that would slide the picture within its own clip while the card lifts around
   it. The card is the object that moves now. */
/* A SINGLE ROW UNDER THE COVER, not a stacked block: identity and title on the
   left, button on the right. It was a column of four stacked children (flag,
   title, excerpt, button) with the section's full 2.55rem padding around it —
   about 240px of card below the picture. As a row with the excerpt gone it is
   ~90px, and that difference is most of what makes the section shorter. */
.issue-lead__body{padding:clamp(.95rem,1.7vw,1.3rem) clamp(1.1rem,2vw,1.5rem);
  display:flex;align-items:center;justify-content:space-between;gap:1rem}
.issue-lead__head{display:block;min-width:0}
.issue-lead__flag{display:inline-flex;align-items:center;gap:.5rem;align-self:flex-start;
  background:rgba(168,50,38,.10);color:var(--terracotta);border-radius:3px;
  padding:.3rem .55rem;font-size:.62rem;letter-spacing:.14em;text-transform:uppercase;
  font-weight:700}
/* The measure goes on the element carrying the font-size — `ch` resolves against
   the element it is written on, so 22ch on a wrapper inheriting 16px would be
   352px, not the ~600px it is here. Two lab files shipped a two-words-wide
   block from exactly this. */
/* 0.75x. Still comfortably the largest type in the section and still the
   biggest thing in its own card, which is all the "lead" status needs now that
   there is no full-width scroller under it competing for the same rank. */
.issue-lead__t{display:block;margin:.2rem 0 0;font-size:clamp(1.35rem,2.2vw,1.85rem);
  line-height:1.08;max-width:18ch;font-family:"Cormorant Garamond",Georgia,serif;
  font-weight:600;letter-spacing:-.01em;color:var(--ink)}
/* (.issue-lead__x, the lead's excerpt, is gone with its markup — see the note
   in index.html at .issue-lead__body. Restore both together if it comes back.) */
/* A REAL button, not a caption. This was plain gold text sitting directly under
   the excerpt, at body weight and body size — so on the lead, the one item on
   the page most meant to be opened, nothing said "click this". It now speaks the
   site's existing primary-action language (.btn: 4px radius, terracotta, white,
   .03em) rather than inventing a second button style for one section.
   The whole .issue-lead is the anchor, so this is a span that LOOKS like a
   button — hover and focus are therefore driven from the anchor, not from here,
   or the affordance would only light up when the pointer is over the pill. */
/* justify-self/align-self/fit-content together: .issue-lead__body stretches its
   children, so inline-flex alone still came out the full width of the column and
   read as a banner rather than a button. */
.issue-lead__cta{display:inline-flex;align-items:center;gap:.6em;
  justify-self:start;align-self:start;width:fit-content;
  margin-top:0;flex:0 0 auto;min-height:44px;padding:.68rem 1.3rem;
  border-radius:4px;background:var(--terracotta);color:#FFFFFF;
  font-size:.95rem;font-weight:600;letter-spacing:.03em;
  transition:background-color 200ms ease,transform 200ms ease}
.issue-lead__cta i{font-style:normal;display:inline-block;
  transition:transform 240ms cubic-bezier(.22,1,.36,1)}
.issue-lead:hover .issue-lead__cta{background:var(--terracotta-deep);transform:translateY(-1px)}
.issue-lead:hover .issue-lead__cta i{transform:translateX(5px)}
.issue-lead:focus-visible{outline:none}
/* Terracotta, not cream. Cream was correct against the dark ground and is
   invisible against a near-white card — a focus ring that disappears on the
   page's most important link is worse than no styling at all. */
.issue-lead:focus-visible .issue-lead__cta{outline:3px solid var(--terracotta-deep);
  outline-offset:3px}

/* ---- the back issues, as a column beside the lead -------------------------
   WAS a horizontal scroller of square photo tiles across the full width, with
   the archive button pinned to its right. All of that is gone: the scroller,
   its two arrow buttons, the shadow-clipping padding it needed, the negative
   margin that compensated for the padding, and the four-tiles-fill-the-wrap
   sizing. It was ~90 lines of CSS holding up the second-tallest thing in the
   section, and the column below replaces it in about 30.

   PHOTO-BEHIND-TEXT DOES NOT SURVIVE THE SHRINK, which is why these are not
   just smaller .issue-tiles. That treatment needs enough area for a scrim to
   fade across; at 84px it is a thumbnail with a dark smear on it and the title
   is unreadable. So the picture becomes a thumbnail BESIDE the text on paper,
   which is the same move the lead makes (picture, then copy) at a smaller size.

   .issue-tile and its --tile-ar token are now UNREFERENCED BY THE PAGE. Left in
   place rather than deleted: the archive page these all link to is the obvious
   place for them, and lab/news-02.html is still built on them. If the archive
   ships without them, delete both and this note. */
/* (.news__sideh, the "Previous issues" label, is gone with its markup — the
   entries carry their own numbers and dates beside a lead flagged "Current
   issue", so it was labelling a distinction the content already made.) */
.news__side{display:flex;flex-direction:column}
.news__past{list-style:none;margin:0;padding:0;
  display:flex;flex-direction:column;gap:clamp(.55rem,1vw,.8rem)}

/* THE BACK ISSUES ARE SQUARE COVERS WITH THE TITLE ACROSS THE FOOT — client
   2026-08-02, replacing the thumbnail-beside-text card. The picture fills the
   whole tile edge to edge and the text sits on it, so the cover gets 100% of
   the card instead of the 42% the split gave it.

   THIS BRINGS BACK `cover` CROPPING, WHICH WAS REMOVED ONE ROUND AGO to answer
   "if they put in an image that's kinda zoomed out you won't be able to see
   anything". The concern has not gone away, it has been outgrown: the failure
   was a 198x148 box where the surviving middle of a crop carried nothing. At
   342 square the crop keeps roughly four times the area, and "the image takes
   up the entire square" cannot be done with `contain` — letterboxing is the
   opposite of filling the tile. If a real cover still reads badly here, the fix
   is the tile's RATIO matching the artwork, not going back to a mount.

   The text needs its own ground on a photograph: two-stop gradient, never a
   flat wash, per the standing rule for every dark area on this page. */
.issue-mini{position:relative;display:block;overflow:hidden;
  aspect-ratio:1/1;border-radius:12px;background:var(--paper-3);
  text-decoration:none;color:inherit;
  box-shadow:0 1px 2px rgba(56,36,19,.14),
             0 10px 20px -8px rgba(56,36,19,.22);
  transition:transform .2s cubic-bezier(.22,1,.36,1),box-shadow .2s ease}
.issue-mini:hover{transform:translateY(-3px);
  box-shadow:0 1px 2px rgba(56,36,19,.14),0 18px 32px -10px rgba(56,36,19,.30)}
.issue-mini:focus-visible{outline:3px solid var(--terracotta-deep);outline-offset:3px}
.issue-mini img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
/* Starts its fade at 42% rather than at the text, so the title never lands on
   the seam where the gradient begins — that edge is visible on a light cover. */
.issue-mini__scrim{position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(180deg,
    rgba(20,12,8,0) 42%,rgba(20,12,8,.58) 72%,rgba(20,12,8,.90) 100%)}
.issue-mini__body{position:absolute;inset:auto 0 0 0;
  padding:clamp(.9rem,1.5vw,1.25rem)}
/* Gold, not terracotta: terracotta is a mid tone that was chosen against paper
   and does not hold up over a photograph. Gold is the page's on-dark accent. */
.issue-mini__meta{display:block;font-size:.64rem;letter-spacing:.14em;
  text-transform:uppercase;font-weight:700;color:var(--gold)}
.issue-mini__t{display:block;margin-top:.3rem;
  font-family:"Cormorant Garamond",Georgia,serif;font-weight:600;
  font-size:clamp(1.1rem,1.5vw,1.42rem);line-height:1.14;letter-spacing:-.005em;
  color:var(--cream)}

/* THE ARCHIVE CTA IS THE END OF THE COLUMN NOW, not a button pinned beside a
   scroller — so it is full width of the column and centred, and it drops the
   flex/auto-margin positioning that placement needed. Same button language as
   the lead's own CTA, one step quieter. It sits in the SECTION HEADER now, not
   at the foot of the back-issue column — .sec-head is already a
   space-between row, so it needs no positioning of its own, and it keeps the
   right column from ending on two calls to action stacked on each other. */
.news__all{
  display:inline-flex;align-items:center;gap:.6em;flex:0 0 auto;
  padding:.7rem 1.2rem;border-radius:4px;
  text-decoration:none;white-space:nowrap;
  background:var(--terracotta);color:#FCF6F0;
  font-weight:600;font-size:clamp(.88rem,1vw,.96rem);
  box-shadow:0 1px 2px rgba(56,36,19,.18),0 10px 18px -8px rgba(56,36,19,.28);
  transition:background-color .18s ease,transform .18s ease}
.news__all:hover{background:var(--terracotta-deep);transform:translateY(-1px)}
.news__all i{font-style:normal;display:inline-block;transition:transform 240ms ease}
.news__all:hover i{transform:translateX(4px)}
.news__all:focus-visible{outline:3px solid var(--terracotta-deep);outline-offset:3px}
@media (prefers-reduced-motion:reduce){
  .issue-mini,.news__all,.news__all i{transition:none}
  .issue-mini:hover,.news__all:hover{transform:none}
}

/* Shadow is WARM BROWN now, not black. Black was right in a dark room; on cream
   it desaturates the paper under the card and reads as a smudge rather than as
   height — the same rule .tst__card states for its own hairline. The tiles
   themselves stay dark, which is the whole point of this variant: a photograph
   is a dark object, so the back catalogue is a row of dark cards ON paper. */
.issue-tile{position:relative;display:block;aspect-ratio:var(--tile-ar);
  border-radius:10px;overflow:hidden;text-decoration:none;color:inherit;
  background:#1E150F;
  box-shadow:
    0 1px 2px rgba(56,36,19,.16),
    0 4px 8px rgba(56,36,19,.12),
    0 14px 24px -6px rgba(56,36,19,.18),
    0 30px 48px -12px rgba(56,36,19,.20);
  transition:transform 320ms cubic-bezier(.22,1,.36,1),box-shadow 320ms ease}
.issue-tile:hover{transform:translateY(-5px);
  box-shadow:
    0 1px 2px rgba(56,36,19,.14),
    0 6px 12px rgba(56,36,19,.12),
    0 20px 34px -6px rgba(56,36,19,.22),
    0 42px 66px -12px rgba(56,36,19,.26)}
.issue-tile img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
/* TWO SCRIMS, deliberately. The wide one tones the whole photograph; the short
   one is a directional PLATE under the text block itself. One gradient cannot
   serve a bright sky and a dim prayer hall at the same time, and a CMS will
   supply both — the hero's own hard-won rule applied to an unknown upload. */
.issue-tile__scrim{position:absolute;inset:0;
  background:
    linear-gradient(180deg,rgba(20,12,8,.50) 0%,rgba(20,12,8,.12) 32%,rgba(20,12,8,.24) 52%,rgba(20,12,8,.88) 100%),
    linear-gradient(0deg,rgba(20,12,8,.78) 0%,rgba(20,12,8,0) 48%)}
/* The one place gold is allowed to touch a photograph. */
.issue-tile__edge{position:absolute;inset:9px;border:1px solid rgba(242,163,58,.32);
  border-radius:4px;pointer-events:none}
.issue-tile__body{position:absolute;inset:auto 0 0 0;padding:clamp(.95rem,1.7vw,1.35rem)}
/* Number and date share one line. Stacked they were most of the picture once
   the tile went landscape. */
.issue-tile__meta{display:block;font-size:.62rem;letter-spacing:.15em;
  text-transform:uppercase;font-weight:700;color:var(--gold);margin-bottom:.35rem;
  font-variant-numeric:tabular-nums}
/* Weight 600 and full alpha: contrast-ratio maths assumes a solid fill, and a
   light serif's hairlines never render as one — the hero measured a comfortable
   4.3:1 and still read washy until the weight went up.
   Clamped at two lines because this block is bottom-anchored over a photograph.
   Everywhere else a long title just makes its box taller; here it climbs out of
   the scrim and ends up on the raw picture with nothing behind it. */
.issue-tile__t{font-family:"Cormorant Garamond",Georgia,serif;font-weight:600;
  font-size:clamp(1.15rem,1.65vw,1.5rem);line-height:1.1;color:#F8F3EC;
  letter-spacing:-.005em;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.issue-tile__x{margin-top:.3rem;font-size:.84rem;line-height:1.45;color:#DAC8AA;
  font-weight:500;
  display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}
/* THE AFFORDANCE, added 2026-07-30. Client: the tiles "just look like an image",
   with nothing saying they are clickable — worst on mobile, where there is no
   hover to reveal anything and the lift on :hover never happens at all.
   So it is ALWAYS VISIBLE, not a hover reveal. A touch device gets no second
   state, and an affordance that only exists on desktop is not an affordance.
   Gold and underlined because on a photograph the underline is what survives —
   colour alone is not a reliable signal for a link, and this sits on an
   arbitrary CMS upload. */
/* A bordered pill rather than an underlined word. The underline read as a link
   in running text; ON A PHOTOGRAPH, at 12px, it did not read as a target at all.
   A closed outline with its own ground does — and it has to, because these are
   the mobile case: there is no hover there, so whatever says "tap me" has to be
   visible in the resting state. The plate also lifts the text off whatever the
   photo happens to be doing underneath it. */
.issue-tile__go{display:inline-flex;align-items:center;gap:.5em;margin-top:.75rem;
  padding:.48rem .85rem;border-radius:4px;
  border:1px solid rgba(242,163,58,.55);background:rgba(18,11,7,.5);
  font-size:.73rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
  color:var(--gold);
  transition:background-color 200ms ease,border-color 200ms ease,color 200ms ease}
.issue-tile__go i{font-style:normal;transition:transform 240ms ease}
.issue-tile:hover .issue-tile__go{background:var(--gold);border-color:var(--gold);color:#2C1F17}
.issue-tile:hover .issue-tile__go i{transform:translateX(3px)}
.issue-tile:focus-visible{outline:none}
.issue-tile:focus-visible .issue-tile__go{outline:3px solid var(--cream);outline-offset:3px}
/* The excerpt is the first thing to go when space is tight — the title and the
   "read" cue are what make the card legible AND obviously clickable, and on a
   phone the tile is too short to carry all three without crowding. */
@media (max-width:560px){
  .issue-tile__x{display:none}
  /* The ratio itself is retuned further down, where the existing ladder lives —
     see the --tile-ar rule under the 560px note. Putting it here as well just
     created two rules fighting, and the later one won. */
  .issue-tile__body{padding:1.05rem 1rem 1.1rem}
  .issue-tile__t{font-size:1.12rem;line-height:1.22}
}
/* NO-IMAGE STATE — a washed lattice on terracotta, keeping the SAME two scrims a
   photo tile gets. An earlier pass gave the fallback a weaker scrim and a
   full-strength pattern and the title became the one unreadable thing in the
   row. THE TEMPLATE MUST EMIT THIS CLASS FROM THE ABSENCE OF AN IMAGE — set by
   hand it will be forgotten and the entry becomes a blank slot, which is the
   exact failure the fallback exists to prevent. */
.issue-tile--type{background:
  linear-gradient(165deg,rgba(146,41,28,.84) 0%,rgba(85,29,15,.93) 100%),
  url("../img/pattern-khatim.webp") center/460px auto repeat,
  #602011}

/* (The 1000px squaring rule that used to sit here is gone — the tiles are
   square at every width above 560px now, so it was restating the default.) */
@media (max-width:860px){
  /* (The grid-template-columns:1fr that used to sit here is gone with the
     side-by-side split — .issue-lead__in is display:block at every width now,
     so there was no grid left for it to restate. Same for the radius override:
     the media rounds its TOP corners as standard now, not only when stacked.) */
  /* 16:9 gives a 196px letterbox strip on top of a 480px card — too thin to
     carry the issue, and the main reason the card looked squashed on a phone.
     4:3 is ~262px at 414, which reads as the lead image it is. */
  .issue-lead__media{aspect-ratio:4/3}
  .issue-lead__t{max-width:none}
  /* The horizontal padding was ZERO — a leftover from when this was not a card
     and the copy sat on the page ground. As a card it meant every line of the
     lead ran flush into its own edge on every phone. */
  .issue-lead__body{padding:clamp(1.3rem,4.5vw,1.9rem)}
}
/* At 78% of a phone the tile is nearly full width, so it stays landscape — a
   square there is almost as tall as the lead and the hierarchy inverts.
   RETUNED 16/10 -> 5/4 (2026-07-30) because the body gained a button. The tile
   is a fixed-ratio box with .issue-tile__body pinned to its foot, so every line
   added to the body eats the photograph instead of growing the card: at 16/10
   that is a ~273x171 box carrying meta, a two-line title and a 34px pill —
   about 84% of the tile height was text jammed against the bottom edge.
   5/4 buys back ~47px, which is the pill plus its margin, and lands the content
   at ~66%. It is still wider than tall, so the lead stays the biggest thing on
   screen and the hierarchy the comment above protects still holds. */
@media (max-width:560px){ .news{--tile-ar:5/4} }
@media (prefers-reduced-motion:reduce){
  .issue-lead:hover .issue-lead__media,.issue-tile:hover{transform:none}
}

/* ---------- mood shift ---------------------------------------------------- */
/* Scroll-driven tonal descent, sitting between .loc and .cta. Behaviour is
   block 7 of site.js; everything here is the settled state it animates FROM and
   TO, never the animation itself.

   DEFAULT IS THE END STATE, at content height, with no pin. With site.js dead,
   404'd or still parsing this renders as a finished burgundy statement at
   7.8:1 — a real section, just a static one, and crucially no dead scrolling.
   The 220vh runway and the pin are added back only under html.js. */
.mood{position:relative;height:auto;
  background-color:var(--terracotta-deep);color:var(--cream)}
.mood__sticky{position:static;height:auto;
  display:flex;align-items:center;justify-content:center;
  padding:clamp(4rem,14vh,9rem) 7vw;text-align:center;overflow:hidden}

/* THE ONE TUNING KNOB. Section height minus the pinned child is the runway the
   whole transition happens over: 220vh -> 120vh of scrolling. Not 260vh, which
   is what the reference used and what .values already is — at 220 the gesture
   reads as related to the collage rather than a repeat of it. */
html.js .mood{height:220vh}
html.js .mood__sticky{position:sticky;top:0;
  height:100vh;
  height:100svh;   /* URL-bar safe. site.js measures the runway off THIS element
                      rather than innerHeight, so the units cannot desync. */
  padding:7vw}

/* NO LATTICE HERE, unlike every other light section. This is the one place the
   Khatim wallpaper drops out entirely, and that is the point rather than a gap.
   The anti-dither cover does not depend on it — the two layers below were
   always doing that independently, which is why removing it was safe on a
   full-viewport flat dark end state. */
.mood__sheen{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:radial-gradient(120% 84% at 50% 34%,
    rgba(255,255,255,.055) 0%, rgba(255,255,255,.015) 46%, rgba(0,0,0,.075) 100%)}
/* The closing scene — the CTA's own plate, arriving over the last third.
   DEFAULT IS VISIBLE, matching the rule that the CSS ships the END state: with
   site.js dead this is a photographic section with cream type on it, which is a
   real section rather than a broken one. Block 7 drives it from 0. */
.mood__scene{position:absolute;inset:0;z-index:2;pointer-events:none;opacity:1}
.mood__photo{width:100%;height:100%;object-fit:cover;object-position:50% 42%}
/* The footage sits on the still and is cropped identically, so the reveal is a
   dissolve in place rather than a reframe. Starts invisible and block 9 reveals
   it on the first decoded frame — otherwise it paints as a black rectangle over
   the still while it buffers. */
.mood__video{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:50% 42%;opacity:0;transition:opacity .7s ease}
.mood__video.is-ready{opacity:1}
@media (prefers-reduced-motion:reduce){ .mood__video{display:none} }
/* object-position matched to the video above it, so the still-to-footage
   dissolve is in place rather than a reframe. */

/* The halo. Two stacked layers rather than one: the radial does the real work
   under the centred copy, and the linear underneath it guarantees a floor of
   darkening right to the edges — without it, a headline that runs wide on a
   narrow screen reaches past the radial's falloff and the last word sits on
   bare sky. Measured against the actual pixels: centre lands ~10.8:1, the worst
   corner of the type ~3.1:1. */
.mood__plate{position:absolute;inset:0;
  background:
    radial-gradient(96% 80% at 50% 46%,
      rgba(8,5,4,.74) 0%, rgba(8,5,4,.66) 48%, rgba(8,5,4,.46) 80%, rgba(8,5,4,.26) 100%),
    linear-gradient(180deg,
      rgba(8,5,4,.34) 0%, rgba(8,5,4,.26) 45%,
      rgba(8,5,4,.46) 74%, rgba(8,5,4,.92) 100%)}
/* THE BOTTOM STOP IS STILL DOING WORK, but a different job since 2026-07-30.
   It was written to manage the seam with the separate .cta section that used to
   follow: same photograph at two different crops, and .cta's scrim started
   almost clear at its top, so its brightest sky sat directly beneath this
   section's darkened lawn — a bright/dark inversion that read as a rendering
   fault. Falling to .92 made that an intentional close instead.

   That section is gone and .mood now runs straight into the FOOTER, which is
   itself dark (#2C1F17 -> #1E150F). The .92 close is therefore still right, and
   for a better reason than before: the page now darkens continuously from the
   footage into the footer with no seam to manage at all.
   It also sits under the CTA buttons, so lightening it costs contrast there —
   see .mood__actions. Do not raise the last stop without re-measuring both. */

.mood__noise{position:absolute;inset:0;z-index:3;pointer-events:none;
  opacity:.035;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>")}

/* 4, above sheen(1) / scene(2) / noise(3). Explicit rather than relying on DOM
   order, because the copy must never end up under the plate. */
.mood__inner{position:relative;z-index:4;max-width:980px}
.mood__eyebrow{margin:0 0 1.6rem;font-size:.75rem;font-weight:700;
  letter-spacing:.19em;text-transform:uppercase}
/* Floor is 2rem, far under the size ramp elsewhere, because the lines are
   hard-authored and a wrap is not harmless: .ln is a block, so an over-long
   line does not clip, it wraps into a fourth visual line that the per-line
   colour stagger then only half-covers. Measured on the longest current line,
   "Every child leaves here": 2.3rem wraps at 320px, 2.1rem fits by 8px, 2rem
   fits by ~21px. The floor only binds below ~432px — above that 7.4vw has
   already overtaken it — so it costs nothing on anything bigger than a phone.
   RE-MEASURE IF THE COPY CHANGES. It is sized to a specific string. */
.mood__h{margin:0;font-family:"Cormorant Garamond",Georgia,serif;font-weight:600;
  font-size:clamp(2rem,7.4vw,7rem);line-height:.92;letter-spacing:-.03em}
.mood__h .ln{display:block}
.mood__h em{font-style:italic}
.mood__cite{max-width:620px;margin:2rem auto 0;font-size:.8rem;font-weight:600;
  letter-spacing:.12em;text-transform:uppercase}

/* ---- the ask, revealed at the end of the ramp (site.js block 7) ----
   DEFAULT IS VISIBLE. Same rule as the rest of this section: the stylesheet
   ships the finished state, and the script only supplies the travel. With JS
   dead these are simply a CTA on a photographic section.
   `transition` is here only so the reveal is smooth if the script writes
   opacity in coarse steps; the script is the one driving it. */
.mood__actions{display:flex;flex-wrap:wrap;gap:.9rem;justify-content:center;
  margin:2.2rem 0 0;transition:opacity .35s ease}
.mood__office{margin:1.1rem 0 0;font-size:.92rem;color:rgba(244,238,228,.85);
  transition:opacity .35s ease}
.mood__office a{color:var(--gold);text-underline-offset:3px}
.mood__office a:hover{color:#FFFFFF}
/* NO `transition` on colour anywhere in this section. A transition is a
   crossfade, and a crossfade is the exact thing block 7 exists to avoid. */

@media (max-width:600px){
  html.js .mood__sticky{padding:6vw}
  .mood__sticky{padding:clamp(3rem,10vh,5rem) 6vw}

  /* ---- phone type, MEASURED 2026-07-30. Do not eyeball this. ----------------
     The section read as tiny text stranded in a huge empty band. The ceiling is
     set by the headline, and the headline is sized to one exact string: "Every
     child leaves here" costs 8.15px of width per 1px of font-size, and the 6vw
     padding leaves 0.88 x viewport. So the largest size that still fits is
     34.6px at 320w, 42.1px at 390w, 46.4px at 430w — i.e. only ~x1.3 of head-
     room on a phone, NOT more.

     9.9vw sits at 92% of that wrapping limit at EVERY phone width (320 -> 31.7,
     390 -> 38.6, 430 -> 42.6), and the 2.78rem cap is exactly where the desktop
     7.4vw rule takes over, so the two meet with no step at the breakpoint.

     A wrap here is not cosmetic: .ln is display:block, so an over-long line does
     not clip, it becomes a FOURTH visual line that site.js's per-line colour
     stagger (data-snap) only half-covers.
     RE-MEASURE IF THE COPY CHANGES — harness recipe is in DESIGN-LIBRARY. */
  .mood__h{font-size:clamp(1.95rem,9.9vw,2.78rem)}

  /* The supporting text was the worse offender: 12px, 12.8px and 14.7px on a
     390px screen. Raised toward comfortable reading sizes rather than scaled by
     the same ratio as the headline, and tracking eased off because letter-
     spacing tuned for 12px is too loose once the text is half again as large. */
  .mood__eyebrow{font-size:1rem;letter-spacing:.15em;margin-bottom:1.15rem}
  /* The cite has to be a clamp, not a flat size. "THE HUDA ACADEMY · EST. 2000"
     is uppercase and tracked, costing ~22px of width per 1px of font-size, so at
     320w (282px available) its ceiling is ~12.8px — it was ALREADY at the limit
     as shipped. A flat .98rem broke it onto two lines there. 3.7vw sits safely
     under the limit on wider phones and the .8rem floor keeps 320w exactly as it
     was rather than making it worse. */
  .mood__cite{font-size:clamp(.8rem,3.7vw,1rem);letter-spacing:.1em;margin-top:1.7rem}
  .mood__office{font-size:1.06rem;margin-top:1.25rem}
  .mood__actions{margin-top:2rem;gap:.85rem}

  /* ---- the white space, which was the real complaint ----------------------
     220vh with a 100svh pinned child leaves 120vh of RUNWAY — on a 844px phone
     that is ~1000px of scrolling spent looking at a ~220px text block alone in a
     full-height viewport. It is worst early: the ask is gated to the last 7% of
     the ramp (ACT_A 0.93 in site.js block 7, deliberately after the photo
     settles because buttons rising through a cross-fading ground read as a
     glitch), so for most of the descent there is genuinely nothing else there.

     Bringing the ask forward would break that choreography, so the runway gets
     shortened instead: 150vh leaves 50vh of travel, ~58% less dead scroll, and
     every phase still lands in the same order because block 7 normalises
     everything to p = scrolled / runway. Desktop keeps the full 220vh gesture. */
  html.js .mood{height:150vh}
}

/* Same escape hatch .values uses: content height, no pin, settled on the end
   state — which is the colour the page needs for the handoff into the footer.
   site.js bails on its own, so nothing overwrites this. */
@media (prefers-reduced-motion:reduce){
  html.js .mood{height:auto}
  html.js .mood__sticky{position:static;height:auto;padding:clamp(4rem,14vh,9rem) 7vw}
}

/* ---------- flyer lightbox ------------------------------------------------
   A flyer is an A4-shaped poster full of small print, shown in a ~460px card.
   It is legible as an object there but not as a document, so the card has to
   open it. `hidden` on the container does the hiding; these rules only lay it
   out once it is open. */
.lb[hidden]{display:none}
.lb{position:fixed;inset:0;z-index:120;display:grid;place-items:center;
  padding:clamp(1rem,4vw,3rem)}
.lb__backdrop{position:absolute;inset:0;background:rgba(12,8,5,.86);
  animation:lbFade 220ms ease}
.lb__panel{position:relative;z-index:1;max-width:min(92vw,760px);
  animation:lbRise 260ms cubic-bezier(.22,1,.36,1)}
/* max-height on the IMAGE, not the panel: a tall flyer must shrink to fit the
   viewport rather than overflow it, and object-fit:contain keeps it undistorted
   whatever ratio the CMS produced. */
.lb__img{display:block;width:auto;max-width:100%;max-height:86vh;
  object-fit:contain;border-radius:8px;
  box-shadow:0 40px 90px -30px rgba(0,0,0,.8)}
.lb__close{position:absolute;top:-14px;right:-14px;z-index:2;
  width:44px;height:44px;border:0;border-radius:50%;cursor:pointer;
  display:grid;place-items:center;font-size:1.5rem;line-height:1;
  background:var(--paper);color:var(--ink);
  box-shadow:0 8px 24px -6px rgba(0,0,0,.6)}
.lb__close:hover{background:#FFFFFF}
.lb__close:focus-visible{outline:3px solid var(--gold);outline-offset:3px}
@media (max-width:600px){ .lb__close{top:-10px;right:-10px;width:40px;height:40px} }
@keyframes lbFade{ from{opacity:0} to{opacity:1} }
@keyframes lbRise{ from{opacity:0;transform:translateY(12px) scale(.98)} to{opacity:1;transform:none} }
@media (prefers-reduced-motion:reduce){
  .lb__backdrop,.lb__panel{animation:none}
}
/* Scroll lock while the dialog is open. Set on <html> by block 11, matching the
   .intro-lock convention already in this file. */
html.lb-open,html.lb-open body{overflow:hidden}

/* ---------- horizontal scroller (shared: events + newsletter tiles) --------
   ONE mechanism used by two sections, because they had the same problem: a
   fixed 3-column grid that silently caps the section at three items. The moment
   the school posts a fourth flyer or a fourth issue, a grid either drops it or
   reflows into a second row nobody scrolls to. A track scrolls.

   IT IS A REAL SCROLL CONTAINER, not a JS carousel. Touch swipe, trackpad
   two-finger, shift+wheel and keyboard all work natively; scroll-snap does the
   alignment. site.js block 10 only adds the ARROWS on top, so with the script
   dead this is still a working, swipeable list — the arrows just never appear.

   The arrows are not decoration either: on a desktop with a mouse and no
   trackpad there is otherwise no affordance at all for horizontal content. */
.hscroll{position:relative}
.hscroll__track{list-style:none;margin:0;padding:0;
  display:flex;gap:clamp(1.2rem,2.6vw,2.2rem);
  overflow-x:auto;overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;scroll-behavior:smooth;
  /* The track is the scroll parent, so its own edges must not clip the cards'
     shadows. Padding gives them room and is compensated by the negative margin
     so the first card still lines up with the section's left edge. */
  padding:.5rem .25rem 1.25rem;margin-inline:-.25rem;
  scrollbar-width:none;-ms-overflow-style:none}
.hscroll__track::-webkit-scrollbar{display:none}
.hscroll__track > *{scroll-snap-align:start;flex:0 0 auto;margin:0}
@media (prefers-reduced-motion:reduce){ .hscroll__track{scroll-behavior:auto} }

/* Three across on desktop; the calc subtracts the two gaps so three cards land
   exactly edge to edge with no fourth peeking. */
.hscroll__track > *{width:calc((100% - 2 * clamp(1.2rem,2.6vw,2.2rem)) / 3)}

/* EVENTS POSTERS RUN WIDER THAN THE TEXT COLUMN. Client 2026-07-30: keep three
   visible but make each box bigger. Those two only reconcile if the track is
   wider than .wrap, so it bleeds symmetrically past it, with three still
   landing edge to edge.

   THE BLEED IS CAPPED BY WHAT THE GUTTER CAN AFFORD. Fixed 2026-08-01 — this
   was the page-wide horizontal scrollbar the client's friend hit.

   The old value was a flat `min(5vw,110px)`, justified as "5vw < 6vw at every
   width, so it always fits". The arithmetic was right and the conclusion was
   wrong, because the bleed is not the only thing spending the gutter: the
   arrows hang a further 22px outside .hscroll and carry a 3px ring. .wrap is
   88% wide, so the gutter is 6vw; a 5vw bleed leaves 1vw for a 25px overhang,
   and 1vw only reaches 25px at 2500px. Below that the arrows sat OUTSIDE THE
   VIEWPORT and the whole document scrolled sideways by 9-13px — at every width
   from 901px to ~1670px, i.e. every common laptop, and equally on a 1920 panel
   at 125% or 150% Windows scaling, which is 1536 / 1280 CSS px. It only looked
   fine on this machine because at 1905px .wrap has hit its 1460px cap and the
   gutter is 222px rather than 6%.

   6.8182% is the gutter expressed against .wrap rather than the viewport:
   gutter = 6vw and .wrap = 88vw, so 6/88. Percentages resolve against .wrap's
   content box, which is what makes this scrollbar-safe — 100vw INCLUDES the
   classic scrollbar and 88% does not, so any vw-based version of this sum is
   already ~15px out before it starts. 26px is the 22px overhang plus the ring.
   Above 1659px .wrap caps and the real gutter grows past 6.8182%, so the cap
   simply stops binding and the bleed rides the 110px ceiling.

   Cost, measured in-browser rather than estimated: posters go 389 -> 382 at
   1280, 440 -> 434 at 1440 and 524 -> 512 at 1920. About 2% — the cap only ever
   takes back the slice the arrows needed, and it takes least where the gutter
   is tightest. The client's "make each box bigger" survives intact.

   Scoped to .events on purpose. The newsletter tiles are text cards, not
   artwork, and they do not gain anything from the extra width. */
@media (min-width:901px){
  .events .hscroll{margin-inline:calc(-1 * clamp(0px,6.8182% - 26px,110px))}
}
@media (max-width:900px){
  /* 1.6 cards visible. The PART-VISIBLE second card is the affordance — it is
     what tells a thumb there is more to the right without needing an arrow. */
  .hscroll__track > *{width:62%}
}
@media (max-width:560px){ .hscroll__track > *{width:78%} }

/* Arrows. Hidden until block 10 proves the track actually overflows, so a
   section with three items and nothing to scroll never shows a dead control. */
/* SOLID TERRACOTTA, NOT A PALE DISC. These were --paper (#F8F4EE) with a
   terracotta glyph, sitting on a --band-dark (#EEE7DC-ish) section — a cream
   circle on cream paper, measured at 1.1:1 against its own background. The
   control was invisible until you knew where to look. Inverting it puts the
   button at 5.7:1 on the band and matches .btn, which is the language the rest
   of the page already uses for "this is a thing you press".
   The cream ring is doing real work: the arrows overhang the poster cards, and
   without it a terracotta disc over a dark flyer edge loses its silhouette. */
.hscroll__btn{position:absolute;top:calc(50% - 25px);z-index:4;
  width:50px;height:50px;border:0;border-radius:50%;cursor:pointer;
  display:none;place-items:center;
  background:var(--terracotta);color:#FDF3E8;
  box-shadow:
    0 0 0 3px rgba(248,244,238,.92),
    0 6px 18px -4px rgba(52,35,26,.5),
    inset 0 1px 0 rgba(255,255,255,.22);
  font-size:1.35rem;font-weight:700;line-height:1;
  transition:transform 180ms ease,background-color 180ms ease,opacity 180ms ease}
.hscroll.is-scrollable .hscroll__btn{display:grid}
.hscroll__btn:hover{background:var(--terracotta-deep);transform:scale(1.08)}
.hscroll__btn:focus-visible{outline:3px solid var(--terracotta);outline-offset:5px}
/* Disabled rather than removed at the ends: a control that vanishes mid-gesture
   makes the row feel broken. .55 not .32 — at a third opacity the point of this
   whole change was undone for the arrow that is dimmed at rest, which on first
   load is the one the eye lands on. */
.hscroll__btn[disabled]{opacity:.55;cursor:default;transform:none}
.hscroll__btn--prev{left:-22px}
.hscroll__btn--next{right:-22px}
@media (max-width:900px){
  /* No room to hang them outside the wrap, and a thumb does not need them. */
  .hscroll__btn{display:none!important}
}

/* ---------- mid-page CTA -------------------------------------------------- */
/* The one loud block on the page, and the only place terracotta is used at
   full-bleed rather than as an accent. It sits between two cream sections
   specifically so it interrupts — a reader convinced by "values are lived" had
   nothing to act on for another six sections.

   NOT A FLAT FILL, for the reason every dark block on this site is not: a large
   even area of one colour gets spatially dithered by the display pipeline into a
   visible diagonal hairline. Real two-stop gradient plus the same noise overlay
   the hero, the CTA and the footer all carry. */
/* DEEPER RED, 2026-07-30. The first pass ran #B13822 -> #A83226 -> #92281B,
   anchored on the brand terracotta. Full-bleed at that value it read cheap —
   a bright orange-red is fine as a 12px accent on cream, which is the only way
   the rest of the site uses it, and quite another thing across the whole
   viewport. This is the same hue family pulled down to --terracotta-deep, which
   is where .mood already lands, so the page now has one deep red it returns to
   rather than two competing ones. Still the loudest block on the page; just no
   longer the brightest. */
.midcta{position:relative;isolation:isolate;overflow:hidden;
  background:linear-gradient(160deg,#92281B 0%,#86251B 52%,#652011 100%);
  color:var(--cream);padding:clamp(2.8rem,7vh,4.5rem) 0}
.midcta__noise{position:absolute;inset:0;z-index:0;pointer-events:none;
  opacity:.05;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>")}
/* Copy left, actions right, and they drop to stacked well before the text gets
   tight — a 2-line headline next to two buttons is the failure case. */
.midcta__inner{position:relative;z-index:1;display:flex;flex-wrap:wrap;
  align-items:center;justify-content:space-between;gap:clamp(1.5rem,4vw,3rem)}
.midcta__copy{max-width:34rem}
/* Cream, not --gold, and that is a measured decision rather than a taste one:
   gold on this terracotta is 4.06:1, and an 11px eyebrow is small text needing
   4.5 however bold it is set. Cream gets 5.65:1. Everywhere else on the site
   gold sits on near-black, where it has all the contrast it needs — it just
   does not survive being put on the brand red. */
.midcta__eyebrow{margin:0 0 .5rem;font-size:.7rem;font-weight:700;
  letter-spacing:.2em;text-transform:uppercase;color:var(--cream);opacity:.9}
.midcta__h{margin:0;font-family:"Cormorant Garamond",Georgia,serif;font-weight:600;
  font-size:clamp(1.9rem,4vw,3rem);line-height:1.04;letter-spacing:-.025em}
/* .92 not .88 — at .88 the lede lands on 4.74:1, which clears its 4.5 floor but
   with nothing spare. .92 puts it at 5.03:1. */
.midcta__lede{margin:.8rem 0 0;font-size:clamp(.95rem,1.2vw,1.06rem);
  color:rgba(244,238,228,.92);max-width:38ch}
.midcta__actions{display:flex;flex-wrap:wrap;gap:.9rem;flex:0 0 auto}
/* Inverted on purpose. The page's normal .btn is terracotta on cream, which
   would vanish into this ground; cream-on-terracotta is the loudest thing
   available and measures 5.6:1 the other way up. */
.btn--onbright{background:var(--cream);color:var(--terracotta-deep)}
.btn--onbright:hover{background:#FFFFFF;color:var(--terracotta-deep)}
.btn--ghostbright{background:transparent;color:var(--cream);
  box-shadow:inset 0 0 0 1px rgba(244,238,228,.55)}
.btn--ghostbright:hover{background:rgba(244,238,228,.10);color:var(--cream)}
@media (max-width:860px){
  .midcta__inner{flex-direction:column;align-items:flex-start}
}

/* ---------- closing CTA — RETIRED 2026-07-30 ------------------------------
   The .cta section was folded into .mood, which now closes the page: same
   footage, same two buttons, same front-office line, one section instead of
   two doing it back to back. Roughly 60 lines of .cta__* rules were deleted
   with it — .cta, __media, __video, __plate, __scrim, __noise, __inner,
   __copy, __h, __lede, __office and the quote card.
   Anything worth keeping moved: the directional-plate reasoning now lives on
   .mood__plate, and the measured contrast numbers are in DESIGN-LIBRARY A3.
   .cta-row survives because the HERO uses it. */

/* ---------- giving -------------------------------------------------------- */
/* Slot 8 — DARK. */
/* ---------- giving: the overlapping cards ---------------------------------- */
/* lab/give-02.html option 1 + lab/give-03.html option 2, approved 2026-07-30.
   Structure: .give__shell (outer card) > .give__grid > .give__copy + .give__stage,
   where .give__stage holds the photo card and the ayah card that laps over it. */
.give{background:var(--band-dark);padding:clamp(3.5rem,10vh,7rem) 0}

/* THREE DEPTHS, AND ONLY THREE. Borrowed from .tst__card, which owns the site's
   only radius-and-shadow language. Each tier is the one below it with the wide
   layers pushed further, so the cards read as a STACK rather than as three
   unrelated boxes that happen to touch. */
.give__shell{
  /* Lighter than the band it sits on, or the shell is invisible: .give paints
     --band-dark and an equal-tone shell would have nothing to separate it. */
  background:rgba(248,244,238,.86);
  border:1px solid rgba(168,50,38,.10);border-radius:18px;
  box-shadow:0 1px 2px rgba(56,36,19,.10),0 10px 20px -10px rgba(56,36,19,.14);
  padding:clamp(1.6rem,3.4vw,3rem)}

/* G6 proportions: the picture column is widened because the verse now lives on
   the photograph and needs the width to be worth reading. */
.give__grid{display:grid;grid-template-columns:minmax(0,.86fr) minmax(0,1.25fr);
  gap:clamp(1.4rem,3vw,2.6rem);align-items:center}

/* NO RESERVED GUTTER ANY MORE. G1 pulled the ayah card LEFT out of the picture
   column and over this one, so this column had to reserve ~128px of clear space
   for it or the heading ran underneath. G6 has no card to intrude — the verse
   is on the photograph — so the reserve is gone and the copy gets the width
   back. If a card-based direction ever returns, so does the reserve, and it has
   to be expressed on the same vw basis as that card's offset. */
.give__copy{position:relative;z-index:1;align-self:center}

.give__eyebrow{margin:0 0 .8rem;font-size:.74rem;font-weight:700;
  letter-spacing:.19em;text-transform:uppercase;color:var(--terracotta)}
/* Section-heading scale, the same clamp .tst__h uses. What stood here before
   was a 0.74rem uppercase kicker marked up as the <h2> — the smallest text in
   its own section. */
.give__h{margin:0;font-size:clamp(1.9rem,3.6vw,3rem);line-height:1.12;
  font-weight:600;letter-spacing:-.028em;color:var(--ink)}
/* The hero's device: an italic coloured first line. Terracotta, not the hero's
   gold — gold is for dark grounds and measures under 2:1 on this cream. */
.give__accent{display:block;font-style:italic;color:var(--terracotta);
  font-weight:600;letter-spacing:-.02em}
.give .rule{margin:1.2rem 0 1.5rem}

.give__hadith{margin:0 0 1.3rem;padding:0 0 0 1.1rem;
  border-left:2px solid rgba(168,50,38,.25)}
.give__hadith p{margin:0;color:var(--ink);line-height:1.55;
  font-size:clamp(1rem,1.15vw,1.1rem)}
.give__hadith cite{display:block;margin-top:.5rem;font-style:normal;
  font-size:.7rem;letter-spacing:.14em;font-weight:600;text-transform:uppercase;
  color:var(--terracotta)}
.give__body{margin:0 0 1.7rem;color:var(--ink-soft);max-width:44ch;
  font-size:clamp(1rem,1.15vw,1.1rem);line-height:1.62}
.give__act{margin:0}

/* THE PICTURE BREAKS TWO EDGES OF THE SHELL — right and bottom. Client
   2026-08-01, after M7: they wanted the verse "in your face", and asked for the
   image box to lap over the side AND the bottom of the card.

   align-self:end is what makes the bottom one work. Under the grid's default
   centring, a negative bottom margin only shrinks the item's margin box and the
   item re-centres, so the picture moves down by HALF the value and the number
   written here is not the overhang you get. Pinned to the row's end, the margin
   box bottom sits on the row bottom and the margin moves the real edge exactly.

   The bottom value is THE SHELL'S OWN PADDING PLUS THE OVERHANG, because the
   row's bottom sits one padding inside the shell's outer edge — the first term
   only buys that back. Written as the same clamp .give__shell uses, so the two
   cannot drift apart if that padding is ever changed.

   Measured: 87px past the right edge and 55px past the bottom at 1920, landing
   inside .give's own bottom padding with 35-64px to spare at every width, so
   nothing below moves and nothing is clipped. */
.give__stage{position:relative;align-self:end;
  margin-right:calc(-1 * clamp(4.5rem,8.5vw,8.5rem));
  margin-bottom:calc(-1 * (clamp(1.6rem,3.4vw,3rem) + clamp(1.5rem,3vw,3.5rem)))}
/* Two depths now, not three. G1 stacked shell + photo + ayah card; the ayah
   card is gone, so the photograph is the only thing lifted off the shell. */
.give__media{position:relative;margin:0;border-radius:14px;overflow:hidden;
  background:#FFFCF7;
  box-shadow:
    0 1px 2px rgba(56,36,19,.16),
    0 4px 8px rgba(56,36,19,.12),
    0 14px 24px -6px rgba(56,36,19,.18),
    0 32px 52px -12px rgba(56,36,19,.20)}
.give__media img{display:block;width:100%;height:auto;aspect-ratio:5/4;
  object-fit:cover}

/* THE VERSE PLATE — the verse is written ON the photograph, no card.
   overflow:hidden on .give__media is doing real work here: it clips this to the
   picture's 14px radius, so the scrim follows the rounded bottom corners.

   THE SCRIM IS GRADED AND ITS STOPS WERE MEASURED, NOT EYEBALLED. The first
   version faded to zero alpha across the top ~26% of the plate, which is
   exactly the band the verse's tallest strokes occupy, and on this photograph
   that band is sky. Compositing the gradient over the real pixels inside the
   verse's own bounding box and taking the WORST pixel gave 2.35:1 against a
   4.5:1 requirement — and it looked completely fine, because most of the verse
   sits low where the scrim is dense and only three or four letter-tops fail.
   These stops hold ~.82 alpha through the whole verse band and fade only above
   it. Re-measured the same way: verse 7.5:1, translation 11.0:1, reference
   8.3:1. The headroom over 4.5 is deliberate — give.webp is a placeholder and a
   brighter replacement must not quietly push this back under. RE-MEASURE IF THE
   PHOTOGRAPH CHANGES. */
.give__plate{position:absolute;inset:auto 0 0 0;z-index:2;isolation:isolate;
  color:var(--cream);
  padding:clamp(2.4rem,5vw,4rem) clamp(1.4rem,2.8vw,2.4rem) clamp(1.3rem,2.4vw,2rem);
  background:linear-gradient(to top,
    rgba(48,23,8,.94) 0%, rgba(56,26,9,.90) 55%, rgba(68,28,10,.82) 82%,
    rgba(83,30,13,.45) 94%, rgba(95,33,17,0) 100%)}
/* Same noise the dark sections carry: this is a large darkened area and a flat
   one invites the display pipeline's diagonal dither. */
.give__noise{position:absolute;inset:0;z-index:-1;opacity:.05;
  mix-blend-mode:overlay;pointer-events:none;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>")}

.give__ar{position:relative;margin:0;width:auto;color:var(--cream)}
/* Fills the plate, which is most of the picture column — so the verse renders
   ~840px at 1920 against G1's 368px. That is the whole point of the change:
   3:92 is a LONGER verse than the mission's 49:13 (951 viewBox units against
   783), so at G1's size its letterforms were drawn at 46% of M7's and the
   write-on read as a detail rather than an event. At 840px it lands at 0.836
   px/unit — parity with M7's 0.843. */
.give__svg{width:100%;margin-left:0}
.give__en{position:relative;margin:.7rem 0 0;padding:0;border:0;font-style:italic;
  color:rgba(244,238,228,.9);line-height:1.5;font-size:clamp(.98rem,1.2vw,1.14rem)}
.give__en p{margin:0}
.give__ref{position:relative;margin:.4rem 0 0;font-size:.72rem;letter-spacing:.14em;
  font-weight:600;text-transform:uppercase;color:var(--gold)}

/* REWRITTEN 2026-08-03. What was here turned BOTH break-outs off when the grid
   stacked, on the reasoning that a full-width picture has no side left to lap
   over. The measurement says otherwise: at 390px the picture came out 290x181
   inside a 343px shell, and the verse plate measured 202px — TALLER THAN THE
   PICTURE IT SITS ON. The scrim covered the photograph completely and then
   overflowed it, so the school building was a dark sliver above the verse and
   the "picture breaking the card" that the whole G6 direction is about had
   quietly become a small inset thumbnail. Client: "the ayah covers the whole
   image and the image is really small, and it doesn't hang off the card."

   Three moves, and they compound:
     - the sides break out to the shell's own edge (its padding, cancelled), so
       the picture goes 290 -> 343 wide;
     - the picture is PORTRAIT here rather than landscape, so the plate covers
       about half of it instead of all of it;
     - the bottom overhang comes back, so it laps over the card as it does on
       desktop. Same arithmetic as the desktop rule: the pull is the shell's own
       padding PLUS the overhang, because the row's bottom already sits one
       padding inside the shell's outer edge.
   Picture area goes from ~52,000px2 to ~147,000px2 — 2.8x — and the verse is
   still on the photograph, which is the point of G6.

   HEADROOM CHECKED: 32px of overhang against .give's own bottom padding, which
   is 84px at a 844px-tall phone. Nothing below moves. */
@media (max-width:860px){
  .give__grid{grid-template-columns:1fr;gap:2rem}
  .give__body{max-width:none}
  .give__stage{order:2;align-self:end;
    margin-inline:calc(-1 * clamp(1.6rem,3.4vw,3rem));
    margin-bottom:calc(-1 * (clamp(1.6rem,3.4vw,3rem) + 2rem))}
  .give__media img{aspect-ratio:16/10}
  /* The plate stays ON the picture — it is the whole idea, and it survives the
     narrow column because the verse is sized as a share of the plate rather
     than at a fixed width. Padding tightens so the scrim does not swallow the
     picture whole at phone height. */
  .give__plate{padding:clamp(1.9rem,9vw,2.8rem) clamp(1.1rem,4vw,1.6rem) clamp(1rem,3.4vw,1.4rem)}
}
/* Portrait only where the plate would otherwise own the frame. Above 560 the
   picture is wide enough that 16/10 already leaves the verse at ~40% of it. */
@media (max-width:560px){
  .give__media img{aspect-ratio:4/5}
}

/* ---------- about --------------------------------------------------------- */
/* Slot 1 — LIGHT. It sets the phase for everything below it: About is the first
   band after the hero, so making this dark would invert all nine. */
.about{background:var(--band-light);padding:clamp(3.5rem,10vh,7rem) 0}
/* Image column is capped rather than 1fr: at 1460px an even split gives the
   photo ~700px and the copy a line length nothing wants to read. */
.about__grid{display:grid;grid-template-columns:minmax(0,.85fr) minmax(0,1fr);
  gap:clamp(1.8rem,5vw,4.5rem);align-items:center}
.about__media{margin:0}

/* ---- ornament frame around the clip, approved 2026-07-30 ------------------
   Client's own artwork (docs/DESIGN-LIBRARY.md). Three things decide this rule:

   1. THE VIDEO KEEPS ITS SIZE. It stays the layout box at the full column width
      and the frame is drawn around it by an overflowing ::before, so the
      ornament costs the clip nothing. An earlier pass sized the frame first and
      fitted the video inside it, which shrank the clip by a third.
   2. ONLY THE HORIZONTAL OFFSETS COME FROM --about-frame-pull. A percentage
      `top` resolves against HEIGHT while left/right resolve against WIDTH, so
      the same number on all four edges sits the frame closer above than below.
      Width is pinned, height follows from aspect-ratio, and centring makes the
      overhang equal on all four sides at every viewport.
   3. THE PNG IS A MASK, the colour is a token. The artwork's own colour is a
      pale cream that would be invisible on --paper; masking means the tint is
      --terracotta at 8%, changeable here rather than by re-exporting a file.

   The source is square and the clip is 4:3. tools/make-ornate-frame.py fits one
   to the other by lengthening only the straight runs — corners and the mid-side
   diamonds keep their original size — inserting whole repeats of the beading's
   own 4px period so its rhythm never breaks. --ar is that file's output size and
   must be changed with it. */
.about__frame{position:relative}
.about__frame::before{
  content:"";position:absolute;z-index:0;pointer-events:none;
  left:calc(var(--about-frame-pull) * -1); right:calc(var(--about-frame-pull) * -1);
  top:50%; transform:translateY(-50%);
  aspect-ratio:1098/862;
  background-color:var(--terracotta);opacity:.08;
  -webkit-mask-image:url("../img/about-frame.webp");
          mask-image:url("../img/about-frame.webp");
  -webkit-mask-size:100% 100%;   mask-size:100% 100%;
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
}
.about__frame .about__video{position:relative;z-index:1}
/* Radius/shadow live on the inner frame, not the <figure>, so the caption sits
   below the card instead of inside it. Backdrop is a two-stop gradient rather
   than a flat charcoal — same reason as every other dark area on this page.
   It is only ever seen in the sliver a non-4:3 replacement clip would letterbox. */
.about__video{border-radius:10px;overflow:hidden;
  background:linear-gradient(180deg,#22180E,#1B1209);
  box-shadow:0 26px 60px -30px rgba(52,35,26,.5)}
.about__media img,
.about__video video{display:block;width:100%;height:auto;aspect-ratio:4/3;object-fit:cover}
.about__cap{margin:.75rem 0 0;text-align:center;color:var(--ink-soft);
  font-size:.82rem;letter-spacing:.03em}
/* Matches .mission .kicker — the client asked for the same treatment as
   "Our mission", and the shared .kicker rule is still the small stat-band one. */
.about .kicker{font-size:clamp(1.25rem,2.1vw,1.9rem);letter-spacing:.07em;margin:0 0 .2rem}
.about .rule{margin:1rem 0 1.5rem}
.about__lead{margin:0;font-weight:600;letter-spacing:-.022em;line-height:1.18;
  font-size:clamp(1.5rem,2.9vw,2.35rem)}
.about__body{margin:1.1rem 0 0;color:var(--ink-soft);max-width:44ch;
  font-size:clamp(1rem,1.25vw,1.1rem)}
/* The "and" is the whole argument — both halves, not one. Same emphasis device
   the mission uses on "God conscious lives", so the two sections rhyme. */
.about__body em{font-style:italic;color:var(--terracotta);font-weight:600}
.about__copy .btn{margin-top:1.7rem}

@media (max-width:860px){
  /* Stacked order REVERSED 2026-07-30 at the client's call: copy first, clip
     under it. The old note here read "image first when stacked: it is the thing
     that earns the read" — on a phone that turned out to push the heading and
     the argument a full screen down, so you scrolled past a silent video to
     find out what the section was even about. Grid `order` only, so the DOM
     order is untouched and the video still reads first to a screen reader. */
  .about__grid{grid-template-columns:1fr;gap:1.8rem}
  .about__copy{order:1}
  .about__media{order:2}
  .about__body{max-width:none}
  /* Stacked, the media column IS the wrap, so there is no grid gap left for the
     ornament to overflow into — only the 6% page gutter, and the frame wants
     8.654%. Overflowing anyway puts a horizontal scrollbar on the page. So
     below this width the frame stops overflowing and the clip gives up the room
     instead: 1 / (1 + 2 * .08654) = 85.24%, which lands the ornament's outer
     edge exactly on the column edge. This is the one place the frame costs the
     video any size, and it is the only place there is nowhere else to take it
     from. */
  .about__frame{width:85.24%;margin-inline:auto}
}

/* ---------- testimonials -------------------------------------------------- */
/* From lab/testimonial-03.html, built off the client's bayyinah reference and
   approved 2026-07-28. The named feature was "the cards are like raised off of
   the background", and TWO things make that read — only one of them is shadow:

     1. Tonal separation. A near-white card on a solid, deeper ground. Earlier
        passes used --paper on a translucent --paper-3 with the lattice showing
        through: about 1.16:1 and busy underneath, and no amount of shadow
        rescues a card that is nearly the tone of its ground. This is 1.42:1.
     2. Spread. Negative spread shrinks a shadow before it is drawn, so the
        wide soft layers that actually sell height vanish. Only the contact
        layer is tightened here; the rest are allowed to spread.

   Shadows are warm brown, never neutral grey — grey on a tan ground
   desaturates the paper under it and reads as a smudge rather than depth.

   This section owns the page's ONLY radius-and-shadow language. Anything boxed
   later reuses these values rather than inventing a third style. */
/* Slot 7 — LIGHT, and this is the one section the band rhythm cost something.
   The approved raised-card treatment leaned on TONAL SEPARATION: a near-white
   card on a solid #DFD5C2 ground measured 1.42:1, and the note above says in so
   many words that no amount of shadow rescues a card that is nearly the tone of
   its ground. On the light band the card is 1.07:1 against its ground, so that
   leg is simply gone and only the shadow is left to carry the elevation.
   What replaces it, below: a warm hairline that gives the card a real edge, and
   a contact shadow tightened so the card still reads as sitting ON the paper
   rather than floating over it. The wide soft layers are untouched.
   If this ever looks flat next to the original, the fix that keeps BOTH is to
   swap this section with .give in the markup — testimonials would land on a dark
   slot, get their deep ground back, and the alternation still holds. That is an
   IA change, so it is not made here. */
/* overflow-x:clip contains the DEAL RIGHT exit. The leaving card translates
   118% of its own width to the right — measured at 2231px in a 1905px viewport
   — which grows document scrollWidth and flashes a horizontal scrollbar on
   every single advance. `clip` and not `hidden`: hidden on one axis forces the
   other to `auto`, which would turn this section into a scroll container and
   crop the cards' drop shadows and the upward fan. `clip` crops the runaway
   card and leaves the vertical axis genuinely visible. */
.tst{background:var(--band-light);padding:clamp(3.5rem,10vh,6.5rem) 0;
  overflow-x:clip}
.tst__head{text-align:center;margin-bottom:clamp(3rem,5.5vw,4.2rem)}
.tst__h{margin:0;font-size:clamp(1.9rem,3.6vw,3rem);line-height:1.14;
  font-weight:600;letter-spacing:-.028em;color:var(--ink)}
.tst__rule{width:56px;height:3px;background:var(--terracotta);border-radius:2px;
  margin:1.3rem auto 0}

/* Flex, not grid: five never divides evenly into a column count, and wrap +
   centre puts the orphans on the last row in the middle instead of hanging one
   off the left edge looking like a mistake.
   Gutters are wide on purpose — at a ~22px gap the falloff from one card runs
   into the next and the row reads as one smudged strip. */
   .tst__stack is the same element in both modes: this flex grid is what it is
   until site.js adds .is-deck and the block at the end of this section turns it
   into the pile. */
.tst__stack{display:flex;flex-wrap:wrap;justify-content:center;
  gap:2.6rem clamp(1.4rem,2.1vw,2rem);align-items:stretch}

/* margin:0 is load-bearing. <figure>'s UA default is `1em 40px`, and the lab
   file had a blanket figure reset that this stylesheet does not — so each card
   carried 80px of horizontal margin, which pushed five cards from 1368px to
   1768px and wrapped the fifth onto its own row. */
.tst__card{flex:1 1 248px;max-width:292px;position:relative;margin:0;
  display:flex;flex-direction:column;text-align:center;
  background:#FFFDFA;border-radius:12px;padding:2.1rem 1.5rem 1.7rem;
  /* The hairline is doing the job the ground used to — see the .tst note. Warm
     brown at low alpha, never grey: grey on a cream ground desaturates the
     paper under it and reads as a smudge rather than as an edge. */
  border:1px solid rgba(168,50,38,.13);
  box-shadow:
    0 1px 2px rgba(56,36,19,.16),
    0 4px 8px rgba(56,36,19,.12),
    0 14px 24px -6px rgba(56,36,19,.18),
    0 32px 52px -12px rgba(56,36,19,.20);
  transition:transform .22s cubic-bezier(.22,1,.36,1),box-shadow .22s ease}
/* The lit top edge of a raised object. Invisible on its own, and the thing
   that stops the card looking pasted on. */
.tst__card::after{content:"";position:absolute;inset:0;border-radius:12px;
  pointer-events:none;box-shadow:inset 0 1px 0 rgba(255,255,255,.75)}
.tst__card:hover{transform:translateY(-6px);
  box-shadow:
    0 1px 2px rgba(56,36,19,.14),
    0 6px 12px rgba(56,36,19,.12),
    0 20px 34px -6px rgba(56,36,19,.22),
    0 44px 70px -12px rgba(56,36,19,.26)}

.tst__badge{width:56px;height:56px;border-radius:50%;margin:0 auto 1.15rem;
  display:flex;align-items:center;justify-content:center;overflow:hidden;
  background:linear-gradient(160deg,#B23728 0%,#A83226 42%,#86251B 100%);
  box-shadow:0 3px 8px -2px rgba(56,36,19,.34),
             inset 0 1px 0 rgba(255,255,255,.22);
  color:#FDF3E8;user-select:none}
/* Georgia for this one glyph, deliberately — the only non-Inter type on the
   site. Inter's quotation mark is two flat slabs and reads as a dash at badge
   size; a serif gives the round "66" the reference actually shows, and Georgia
   needs no webfont. */
.tst__badge i{font-family:Georgia,"Times New Roman",serif;font-style:normal;
  font-size:2.9rem;line-height:1;transform:translateY(.30em)}

.tst__q{margin:0 0 1.4rem;font-size:.97rem;line-height:1.62;color:var(--ink)}
.tst__by{margin-top:auto;padding-top:1.1rem;
  border-top:1px solid rgba(168,50,38,.20);
  font-size:.82rem;font-weight:600;color:var(--ink);line-height:1.45}
.tst__by em{display:block;font-style:normal;font-weight:500;font-size:.74rem;
  letter-spacing:.12em;text-transform:uppercase;color:var(--terracotta);
  margin-top:.3rem}

@media (max-width:620px){
  .tst__card{flex:1 1 100%;max-width:420px}
}
@media (prefers-reduced-motion:reduce){
  .tst__card{transition:none}
  .tst__card:hover{transform:none}
}

/* ---------- testimonials: the deck ---------------------------------------- */
/* Approved as lab/testimonial-04.html "Design A"; exit is DEAL RIGHT, chosen
   from lab/testimonial-05.html.

   EVERYTHING BELOW IS GATED ON .is-deck, WHICH ONLY site.js ADDS.
   Above this comment is the five-card grid, and that is what the section is
   until JS proves it ran. The cards are absolutely positioned in deck mode, so
   if this were unconditional a dead site.js would pile five quotes on the same
   40 square pixels with no arrows to page them. The grid fallback is the point;
   do not "simplify" it away. */
/* Fallback state: the portrait and the arrows are simply not there, and
   .tst__deck / .tst__col are inert blocks wrapping the flex grid above. */
.tst__media{display:none}
.tst__ctl{display:none}

/* Sized to fill the wrap, not to a comfortable-looking cap. An earlier 1010px
   max-width left ~450px of empty band either side on a 1920 screen and the
   whole section read as bland — the fix was not more padding but more content.
   .95fr keeps the photo nearly the width of the card: these are group shots of
   six or seven children and small faces are just texture.
   Everything here is vw-clamped so the ≤760px block below still wins on
   phones, where the compact version is already right. */
.tst.is-deck .tst__deck{display:grid;
  grid-template-columns:minmax(0,.95fr) minmax(0,1fr);
  gap:clamp(1.2rem,3.2vw,2.9rem);align-items:center;
  max-width:1300px;margin:0 auto}
/* Undo the flex grid — in deck mode the cards are absolutely positioned. */
.tst.is-deck .tst__stack{display:block}

.tst.is-deck .tst__media{display:block;position:relative;margin:0;
  border-radius:12px;overflow:hidden;
  /* Same elevation language as .tst__card — see the note at the top of .tst.
     A second shadow recipe here would read as a different surface. */
  box-shadow:
    0 1px 2px rgba(56,36,19,.16),
    0 14px 24px -6px rgba(56,36,19,.18),
    0 32px 52px -12px rgba(56,36,19,.20)}
/* 4:3 because these are GROUP photographs — see the note in index.html. The
   files are already cropped to 4:3, so object-fit has nothing to trim and
   nobody gets clipped; it is here only to stop an odd-sized replacement from
   distorting. Change this ratio and you re-crop all five in the browser. */
.tst.is-deck .tst__media img{display:block;width:100%;height:auto;
  aspect-ratio:4/3;object-fit:cover;
  transition:opacity 220ms ease}
/* Set by the script for one 220ms beat while the src is exchanged. */
.tst.is-deck .tst__media img.is-swapping{opacity:0}

/* padding-right is the room the fanned corners live in. Without it the back
   cards are clipped by the column and the pile reads as one card. */
.tst.is-deck .tst__stack{position:relative;padding-right:clamp(52px,4.4vw,68px);
  min-height:clamp(238px,26vw,384px)}

.tst.is-deck .tst__card{position:absolute;left:0;top:0;bottom:0;
  width:calc(100% - clamp(52px,4.4vw,68px));max-width:none;flex:none;
  justify-content:center;text-align:left;
  padding:clamp(1.6rem,2.6vw,2.9rem);
  /* The fan. --i is depth, set by the script: 0 is the front card.
     These numbers were raised once already — 14px/-10px was arithmetically a
     fan and visually a single card. Do not reduce them without looking. */
  transform:
    translate(calc(var(--i) * 17px), calc(var(--i) * -11px))
    rotate(calc(var(--i) * 1.5deg))
    scale(calc(1 - var(--i) * .035));
  opacity:calc(1 - var(--i) * .04);
  z-index:calc(10 - var(--i));
  /* NO TRANSITION HERE ON PURPOSE — see .is-ready below. */
  transition:none;
  will-change:transform}

/* The fan is a RESTING STATE, not an entrance.
   --i is set by JS, so on first paint every card would transition from the
   unset (identity) transform out to its slot — five cards visibly fanning open
   from a single pile every time the page loads, which is not what was approved
   and reads as a glitch. So the cards are transition-less until the script has
   painted the initial depths and forced a reflow; it then adds .is-ready and
   the transition exists for the arrows only.
   Must stay BEFORE .is-dealing: both are (0,4,0) and source order decides. */
.tst.is-deck.is-ready .tst__card{
  transition:transform 440ms cubic-bezier(.22,1,.36,1),opacity 340ms ease}
/* The grid's lift-on-hover would fight the fan transform and snap the card out
   of the pile. The deck's motion is the arrows, not the pointer. */
.tst.is-deck .tst__card:hover{transform:
    translate(calc(var(--i) * 17px), calc(var(--i) * -11px))
    rotate(calc(var(--i) * 1.5deg))
    scale(calc(1 - var(--i) * .035))}

.tst.is-deck .tst__badge{width:clamp(44px,3.6vw,62px);height:clamp(44px,3.6vw,62px);
  margin:0 0 clamp(.95rem,1.4vw,1.5rem)}
.tst.is-deck .tst__badge i{font-size:clamp(2.2rem,3vw,3.2rem)}
.tst.is-deck .tst__q{margin:0;
  font-family:"Cormorant Garamond",Georgia,serif;font-weight:600;
  font-size:clamp(1.12rem,2.25vw,1.95rem);line-height:1.26;letter-spacing:-.014em}
.tst.is-deck .tst__by{margin-top:clamp(1.1rem,1.6vw,1.7rem);padding-top:0;border-top:0;
  font-size:clamp(.66rem,.72vw,.78rem);font-weight:700;letter-spacing:.16em;
  text-transform:uppercase;color:var(--terracotta)}

/* ---- DEAL RIGHT — the exit -----------------------------------------------
   One class on the leaving card: out to the right and gone, while the script
   re-indexes it to the back of the pile. 420ms here MUST match EXIT_MS in
   site.js block 12, or the card is re-indexed mid-flight and snaps.
   Source order matters: this has the same specificity as .tst.is-deck
   .tst__card above, so it wins only by sitting after it. Keep it here. */
.tst.is-deck .tst__card.is-dealing{
  transform:translate(118%,-6%) rotate(11deg) scale(.96);opacity:0;
  transition:transform 420ms cubic-bezier(.4,0,.25,1),opacity 340ms ease 80ms}

.tst.is-deck .tst__ctl{display:flex;align-items:center;gap:.7rem;
  margin-top:1.4rem}
.tst__btn{width:42px;height:42px;border:0;border-radius:50%;cursor:pointer;
  display:grid;place-items:center;background:var(--terracotta);color:#FDF3E8;
  box-shadow:0 3px 8px -2px rgba(56,36,19,.34),inset 0 1px 0 rgba(255,255,255,.22);
  transition:background-color 160ms ease,transform 160ms ease}
.tst__btn:hover{background:var(--terracotta-deep);transform:translateY(-1px)}
.tst__btn:focus-visible{outline:3px solid var(--terracotta);outline-offset:3px}
.tst__count{margin:0 0 0 .2rem;font-size:.72rem;font-weight:700;
  letter-spacing:.13em;color:var(--ink-soft);font-variant-numeric:tabular-nums}

@media (max-width:760px){
  /* Portrait beside a card gives each about 40% of a phone's width and the
     quote sets three words to the line. Stacked, both get the full measure. */
  .tst.is-deck .tst__deck{grid-template-columns:1fr;gap:1.5rem;max-width:460px}
  .tst.is-deck .tst__stack{min-height:250px}
  /* The desktop sizes above are vw-clamped and already bottom out at these
     values on a phone; this is here so a future change to a clamp MAXIMUM
     cannot quietly move the mobile layout, which is signed off as-is. */
  .tst.is-deck .tst__card{padding:1.6rem 1.7rem}
}
@media (prefers-reduced-motion:reduce){
  /* The script also drops EXIT_MS to 0, so the swap is a cut rather than a
     440ms transition that has been switched off half way. */
  .tst.is-deck .tst__card,
  .tst.is-deck .tst__card.is-dealing,
  .tst.is-deck .tst__media img{transition:none}
  .tst__btn{transition:none}
  .tst__btn:hover{transform:none}
}

/* ---------- location ------------------------------------------------------ */
/* From lab/map-03.html — the client's reference file, kept to its own values
   rather than restyled: its 16:9 plate, its ring geometry, its city dots, its
   copy. Two substitutions only, both to avoid adding a font the site does not
   already carry:
     - the headline is Cormorant Garamond, which the reference used AND which
       .vp__t and .vsel__t already use, so it costs nothing;
     - the 12–14px labels are Inter rather than DM Sans. Both are geometric
       grotesques and at label size the difference is not visible; a third
       family for seven words of map furniture is not worth the request.

   ================= THE MAP CONTENT IS THE REFERENCE'S OWN =================
   The city dots are hand-placed and two are wrong: JACKSONVILLE is drawn
   south-east of the marker and is really NORTH-east, and BENTON is drawn
   nearly due south and is really south-west. The 20/30/45 MIN rings cannot be
   substantiated — there is no campus address in the repo and no routing
   source, and a straight-line ring is not an isochrone regardless.
   `lab/map-02.html` variant E is the same composition drawn from OSM with the
   towns in their real places and the rings in miles. Correct this before the
   site leaves the noindex preview. Tracked in CLIENT-BRIEF §3.
   ========================================================================== */
/* The reference shipped this as a self-contained 1600px panel with its OWN
   opaque gradient background. Dropped onto the page that read exactly like a
   screenshot pasted on top — a hard rectangle edge, a second shade of cream,
   and a left edge that lined up with nothing else on the site.

   Integrating it is mostly subtraction:
     - the panel background is GONE. The SVG is transparent everywhere it is
       not drawing, so the section's own ground shows through and the map sits
       on the page instead of on a card. This also kills the letterbox: the
       empty area above and below a `meet`-scaled SVG is just the page now.
     - the copy moved into .wrap, so its left edge lines up with the mission,
       the stat band and every other section instead of floating at 3.8%.
     - the reference palette (#32261C / #C05B41 / #625B55 / #F3EFEC) is
       replaced by the site tokens. They were close enough to look like a
       mistake rather than a choice — a slightly different cream and a
       slightly different terracotta read as "foreign", not "accent".
   The composition, the ring geometry and the city coordinates are untouched. */
/* Slot 9 — LIGHT. It is also the last band before .mood, and light is the right
   note to hand over on: mood opens at cream and descends to burgundy, so a dark
   step here would make the descent start by climbing back up. */
.loc{position:relative;background:var(--band-light);
  padding:clamp(3rem,8vh,5rem) 0 clamp(2rem,5vh,3rem)}
.loc__stage{position:relative;width:100%;aspect-ratio:16/9;
  max-height:min(76vh,860px);margin-inline:auto}
.loc__river{position:absolute;z-index:1;inset:0;pointer-events:none;opacity:.9;
  background-image:url("../img/ref-river.png");
  background-repeat:no-repeat;background-position:center top;
  background-size:100% auto}
.loc__map{position:absolute;inset:0;z-index:2}
.loc__map svg{width:100%;height:100%;display:block}

/* Copy rides in .wrap so it shares the page's left edge. */
.loc__copy{position:relative;z-index:5;width:min(38%,34rem);
  padding-block:clamp(1rem,4vw,3rem)}
.loc__inner{position:absolute;inset:0;z-index:5;display:flex;align-items:center;
  pointer-events:none}
.loc__inner .wrap{width:min(88%,1460px);margin-inline:auto}
.loc__copy > *{pointer-events:auto}
/* A wash anchored to the words, not to the layout — the hero's own trick. It
   keeps the type legible where the rings run behind it without flattening the
   rest of the map. */
.loc__copy::before{content:"";position:absolute;inset:-14% -40% -14% -30%;
  z-index:-1;pointer-events:none;
  background:radial-gradient(68% 58% at 34% 50%,
    rgba(238,231,220,.96) 0%,rgba(238,231,220,.86) 44%,rgba(238,231,220,0) 78%)}

.loc__eyebrow{margin:0 0 1.1rem;color:var(--terracotta);font-size:.78rem;
  font-weight:700;letter-spacing:.19em;text-transform:uppercase}
.loc__h{margin:0;color:var(--ink);font-family:"Cormorant Garamond",Georgia,serif;
  font-size:clamp(2.3rem,4.6vw,4.5rem);line-height:.96;font-weight:600;
  letter-spacing:-.035em}
.loc__h span{color:var(--terracotta)}
.loc__p{margin:1.9rem 0 0;color:var(--ink-soft);
  font-size:clamp(1rem,1.2vw,1.15rem);line-height:1.72;max-width:34ch}

/* One line and a link, not a directory. The four-row contact block that lived
   here moved to the footer 2026-07-29 — see the note in index.html. Reference
   information belongs where people look for it, and at the map all a reader
   wants is where the building is and how to get there. */
.loc__addr{margin:1.5rem 0 0;padding-top:1.3rem;
  border-top:1px solid rgba(168,50,38,.20);
  color:var(--ink-soft);font-size:.95rem;line-height:1.6}
/* width:fit-content with display:block so the underline hugs the words instead
   of running the full column, and so the link drops onto its own line rather
   than trailing the city name. */
.loc__dir{display:block;width:fit-content;margin-top:.45rem;color:var(--terracotta);
  font-weight:600;font-size:.86rem;text-decoration:none;
  border-bottom:1px solid rgba(168,50,38,.32);padding-bottom:1px}
.loc__dir:hover{border-bottom-color:var(--terracotta)}
/* .loc__todo lived here and is gone: the only unconfirmed value left in this
   section moved to the footer with the rest of the contact block, so the marker
   moved too. It is .footer__todo now — same job, gold instead of terracotta
   because terracotta disappears on the footer's dark ground. */

.loc .ring{fill:none;stroke:rgba(168,50,38,.42);stroke-width:1.2;
  stroke-dasharray:1.6 3.7}
.loc .city-dot{fill:var(--terracotta)}
.loc .city-label{fill:var(--ink);font-size:12.5px;font-weight:700;
  letter-spacing:.08em}
.loc .time-label{fill:var(--terracotta);font-size:12px;font-weight:700;
  letter-spacing:.08em}
.loc .marker-halo{fill:rgba(168,50,38,.13)}
.loc .marker{fill:var(--terracotta)}
.loc .little-rock{fill:var(--ink);font-size:23px;font-weight:700;
  letter-spacing:.055em}

/* ---------- location: radar sweep ----------------------------------------
   THE ONE PERPETUAL ANIMATION ON THE SITE, and it is a deliberate exception to
   the standing rule — see the note in CLAUDE.md. Two things make it safe here
   where the hero medallion was not:
     1. it is GATED ON VISIBILITY. site.js block 8 adds .is-scanning only while
        the section is actually on screen and strips it the moment it leaves, so
        it is never spinning under the rest of the page;
     2. the backdrop-filter nav that was the other half of that stall is gone
        (see the note at .nav), and this only ever animates `transform` and
        `opacity` — both compositor properties, no layout, no paint.
   If the renderer ever stutters again, this is the first thing to switch off.

   Everything below is INERT until .is-scanning lands. With site.js dead the map
   renders exactly as it did before the radar existed: the sweep is invisible,
   the blips are invisible, the dots sit at full strength. */
.loc .loc__radar{opacity:0}
/* Fills come from the two gradients in the markup, not from a flat colour — see
   the note beside them. A flat wedge showed a hard cut where it left the frame. */
.loc .radar__wedge{fill:url(#radarFade)}
.loc .radar__edge{stroke:url(#radarEdge);stroke-width:1.6}
/* Blips are the "found it" flash under each dot. Sized once and animated on
   OPACITY ONLY — animating `r` would relayout the geometry every frame. */
.loc .blip{fill:rgba(168,50,38,.30);opacity:0}

.loc.is-scanning .loc__radar{
  opacity:1;
  /* view-box, not fill-box: the origin below is in viewBox units, and fill-box
     would resolve it against the group's own bounding box instead. */
  transform-box:view-box;
  transform-origin:1030px 494px;
  animation:locSweep 7s linear infinite;
}
/* The 7s here, the 7s above and every --d in the markup are ONE number. Change
   it in one place only and the blips fall out of step with the sweep. */
.loc.is-scanning .blip{animation:locBlip 7s linear infinite;animation-delay:var(--d)}
.loc.is-scanning .city-dot{animation:locFound 7s linear infinite;animation-delay:var(--d)}
.loc.is-scanning .city-label{animation:locLabel 7s linear infinite;animation-delay:var(--d)}

@keyframes locSweep{ from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
/* Fast attack, slow decay — a blip that fades out symmetrically reads as a
   pulsing dot rather than as something the sweep just hit. */
@keyframes locBlip{
  0%{opacity:0} 1%{opacity:1} 18%{opacity:0} 100%{opacity:0}
}
@keyframes locFound{
  0%{opacity:.55} 1%{opacity:1} 26%{opacity:.55} 100%{opacity:.55}
}
@keyframes locLabel{
  0%{opacity:.62} 1%{opacity:1} 26%{opacity:.62} 100%{opacity:.62}
}
/* Between sweeps the dots sit BELOW full strength so the flash has somewhere to
   travel to. Only applied while scanning — with JS off they stay at 1. */
.loc.is-scanning .city-dot{opacity:.55}
.loc.is-scanning .city-label{opacity:.62}

/* The global reduce rule at the end of this file kills `animation` outright,
   which would leave the wedge parked at 12 o'clock as a static triangle. Hide
   it instead, and hand the dots back at full strength. */
@media (prefers-reduced-motion:reduce){
  .loc .loc__radar{display:none}
  .loc.is-scanning .city-dot,
  .loc.is-scanning .city-label{opacity:1}
  .loc .blip{display:none}
}

@media (max-width:900px){
  .loc__stage{aspect-ratio:auto;max-height:none;min-height:0}
  .loc__inner{position:relative;inset:auto;display:block}
  .loc__copy{width:auto;padding-block:0 clamp(1.5rem,5vw,2.5rem)}
  .loc__copy::before{inset:-8% -12%}
  .loc__map{position:relative;inset:auto;height:min(78vw,460px)}
  .loc__river{display:none}
}

/* ---------- footer -------------------------------------------------------- */
/* Dark section: real two-stop gradient + noise overlay, never a flat fill. */
.footer{position:relative;background:linear-gradient(180deg,#2C1F17 0%,#1E150F 100%);
  color:var(--cream);padding:clamp(3rem,7vh,4.5rem) 0 2rem;overflow:hidden}
.footer__noise{position:absolute;inset:0;pointer-events:none;opacity:.035;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>")}
.footer .wrap{position:relative}
.footer__top{display:flex;flex-wrap:wrap;gap:2rem;align-items:flex-start;justify-content:space-between}
.footer__brand img{height:80.5px;width:auto}  /* 72 x 199/178: HUDA unchanged */
.footer__note{max-width:26rem;color:rgba(244,238,228,.72);font-size:.9rem;margin:1rem 0 0}
.footer__cols{display:flex;flex-wrap:wrap;gap:clamp(2rem,6vw,4.5rem)}
.footer h3{font-size:.74rem;letter-spacing:.18em;text-transform:uppercase;
  color:var(--gold);margin:0 0 .8rem;font-weight:700}
.footer ul{list-style:none;margin:0;padding:0}
.footer li{margin-bottom:.5rem}

/* Contact column. Wider than the link columns because an address and an email
   are real strings, not one-word nav labels — at the shared column width
   "principal@thehudaacademy.org" wrapped mid-word. */
.footer__contact{min-width:16rem}
.footer__contact address{font-style:normal;margin:0 0 .8rem;
  color:rgba(244,238,228,.82);font-size:.9rem;line-height:1.6}
.footer__contact ul{font-size:.9rem}
.footer__hours{margin:.9rem 0 0;font-size:.82rem;line-height:1.6;
  color:rgba(244,238,228,.6)}
/* Same job as .loc__todo did before the block moved: an unconfirmed value has
   to look unconfirmed, or it ships. Gold rather than terracotta because
   terracotta is nearly invisible on the footer's dark ground. */
.footer__todo{color:var(--gold);opacity:.75;font-style:italic;
  text-decoration:underline dashed rgba(242,163,58,.45);text-underline-offset:3px}
.footer li a{color:rgba(244,238,228,.82);text-decoration:none;font-size:.9rem}
.footer li a:hover{color:#FFFFFF}
.footer__bottom{margin-top:clamp(2rem,5vh,3rem);padding-top:1.4rem;
  border-top:1px solid rgba(244,238,228,.14);display:flex;flex-wrap:wrap;gap:1rem;
  justify-content:space-between;font-size:.78rem;color:rgba(244,238,228,.55)}

@media (prefers-reduced-motion:reduce){
  *{animation:none!important;transition:none!important}
}

/* ==========================================================================
   THE CREST LAYER — appended to site.css ONCE by tools/crest-migrate.py
   (2026-09-30). From then on it is ordinary site CSS: edit it here like any
   other rule. (tools/crest-layer.css is the record of what was appended.)

   Client, 2026-09-30: the crest colour scheme (proved in lab/crest/) goes live
   on every page, with the new mark (mihrab over an open book, a flame inside)
   replacing the old one. Everything above this block is the old site.css run
   through tools/crest_palette.py; this block is what a colour swap alone
   cannot do — the logo and the signature moves the crest palette brought.

   Palette:
     glow   #F8F4EE / #EEE7DC     shield taupe  #C4B59D / #B09F88 / #9D877A
     ink    #34231A               banner red    #A83226 / #86251B
     flame  #FBB718 -> #F49830 -> #E0602F -> #A83226
   ========================================================================== */

:root{
  --taupe:#C4B59D;
  --taupe-deep:#9D877A;
  --espresso:#2C1F17;
  --espresso-deep:#1E150F;
  --flame-y:#FBB718;
  --flame-tip:#FFD27A;   /* the flame's pale tip — small text on red */
  --flame-o:#E0602F;
  --flame:linear-gradient(90deg,var(--flame-y) 0%,#F49830 30%,var(--flame-o) 62%,var(--terracotta) 100%);
}

/* ---------- 1. THE NEW MARK --------------------------------------------------
   huda-logo-dark.webp is the mark's colourway for dark grounds: its dark-brown
   strokes turned to the glow cream so they read on the espresso bar, the taupe
   band and the flame exactly as drawn (lab/logo-2026-09-30/make_logo.py). It
   is near-square (631x640) where the old mark was taller (333x370), so only
   the aspect ratio changes; the height tokens (--mark-h) and the wordmark
   beside it stay exactly as they were. */
.brand__mark{aspect-ratio:631/640}

/* The footer shows the mark above the wordmark. More specific than the base
   `.footer__brand img` rule, which sizes the wordmark. */
.footer__brand .footer__mark{display:block;width:88px;height:auto;margin:0 0 1rem}
.footer__brand .footer__note::before{
  content:"Where guidance promotes excellence";display:block;margin:0 0 .55rem;
  font-size:.72rem;font-weight:700;letter-spacing:.2em;text-transform:uppercase;
  color:var(--gold)}

/* ---------- 2. THE TWO BARS: RED RIBBON OVER A DARK BAR ---------------------
   The banner takes the crest's red ribbon and the nav goes to the espresso of
   the lettering, so the mark and wordmark are the lightest things on it. The
   two bars still contrast (client, 2026-08-02). Both stay gradients — a flat
   dark solid this wide dithers a diagonal hairline. */
.topbar{
  background:linear-gradient(180deg,#A83226 0%,#8E281D 100%);
  box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}
.topbar__btn{border-color:rgba(244,238,228,.7)}
.topbar__btn:hover{background:var(--espresso);border-color:var(--espresso)}
.nav::after{
  background:linear-gradient(180deg,#34251C 0%,#241912 100%);
  box-shadow:0 1px 0 rgba(196,181,157,.28),0 10px 30px -18px rgba(0,0,0,.9)}
/* Donate becomes the flame — the one hot object on the bar, as the flame is
   the one hot object in the mark. Cream on it measures 4.93:1. */
.nav__cta{
  border-color:#C23F2A;
  background:linear-gradient(180deg,#C23F2A 0%,#A83226 100%);
  color:#FFF8EE!important;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.22),0 2px 4px rgba(0,0,0,.3),
    0 8px 18px -7px rgba(224,96,47,.55)}
.nav__cta:hover{
  border-color:#D24A30;
  background:linear-gradient(180deg,#D24A30 0%,#B5382A 100%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 3px 6px rgba(0,0,0,.3),
    0 14px 26px -8px rgba(224,96,47,.7)}

/* ---------- 3. THE FLAME RULE ----------------------------------------------
   Every accent rule becomes a short flame: yellow through orange to the
   banner red, the mark's own flame read left to right. */
.rule,.tst__rule,.hero__eyebrow::before{background:var(--flame)}

/* Small text on red: the remapped amber misses AA there (3.89:1 on the ayah
   panel, 4.39:1 for the email on the closing red), and the saturated flame
   yellow only reaches 3.78:1. The flame's pale tip clears every red stop
   (4.68:1 at worst). The .ayah--m7 selector is needed: site.css sets the
   mission ayah's colour with two classes. */
.ayah__ref,.ayah--m7 .ayah__ref{color:var(--flame-tip)}
.mood__office a{color:var(--flame-tip)}

/* ---------- 4. THE SHIELD BAND ----------------------------------------------
   The stat band is the page's one solid light band: pale shield taupe with
   the darker rim as hairlines. Its red numbers measure 4.78:1 on it. */
.stats{background-color:#E4D9C8;
  box-shadow:inset 0 1px 0 rgba(157,135,122,.45),inset 0 -1px 0 rgba(157,135,122,.45)}

/* ---------- 5. ONE CAPTION THE NEW GROUND PUSHED UNDER AA -----------------
   /about/faculty/'s role captions (11px, #896D5E, a neutral the mapping leaves
   alone) measured 4.58:1 on the old paper #FFFAF1 and 4.34:1 on the crest
   glow #F8F4EE. Same hue, two points darker in L*: 4.67:1. `html` lifts it
   over about-faculty.css (loaded later); the dark-band variant
   `.abf-faith-wrap .abf-person-role` still outranks it. */
html .abf-person-role{color:#846859}
/* Same case on /about/leadership/: the eyebrow on the red structure band is
   small text on red, so it takes the flame tip like .ayah__ref (amber measured
   4.06:1 there). `html` lifts it over about-leadership.css's two-class rule. */
html .abl-structure-head .abl-eyebrow{color:var(--flame-tip)}
