/* ============================================================================
   Elzatian — profile site
   ----------------------------------------------------------------------------
   EDITORIAL LIGHT. The whole page is one light canvas: a warm off-white ground,
   black type at a scale that does the work, and exactly one accent — a signal
   red used for marks and the script flourish, never for decoration.

   The idea: a printed portfolio, not a web page. Huge uppercase display type,
   hard alignment, generous air, and one piece of theatre — the stacked deck in
   "What I do", where the cards deal themselves over giant background words as
   you scroll. Nothing moves that does not need to, and no video is loaded to
   deliver the first impression.
   ========================================================================= */

/* ---------------------------------------------------------------- fonts --- */
/* Variable Inter, self-hosted, weights 400-900. The heavier end is the whole
   point of this design: the display type is set at 900, which the old 300-700
   file could not reach. OFL, so it ships with the site rather than being pulled
   from a font CDN — no third-party request on the critical path.

   NOTE: a @font-face src cannot be cache-stamped from here, so if you ever
   replace one of these files, give it a NEW filename (…-v2.woff2) and update the
   src below. Replacing a file in place leaves the browser holding the old one. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url('../fonts/inter-var-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC,
                 U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url('../fonts/inter-var-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* The script face, used for exactly one word at a time — the flourish that signs
   a statement off. Never for body copy. */
@font-face {
  font-family: 'Pacifico';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/pacifico-latin.woff2') format('woff2');
}

/* --------------------------------------------------------------- tokens --- */
:root {
  /* the canvas: warm off-white, never pure white. Brightened at the owner's
     request so the black type stands further off the ground — still warm, still
     not #fff. --paper stays a touch lighter than the page so panels read as paper
     laid on it. */
  --page:       #F4F3EF;
  --paper:      #FCFBF9;
  --surface:    #FFFFFF;

  /* the deck cards: a pale blue-grey, the one cool note against the warm ground */
  --deck:       #CBD4E0;

  --ink:        #0A0A0A;
  --ink-soft:   #2A2A28;
  --body:       #2B2A27;
  --muted:      #4F4B46;

  --line:       rgba(10, 10, 10, .13);
  --line-2:     rgba(10, 10, 10, .07);
  --line-strong:rgba(10, 10, 10, .30);

  /* one accent, used sparingly enough that it still means something */
  --accent:     #C9301F;
  --accent-soft:rgba(201, 48, 31, .09);
  /* for type sitting on the ink panels */
  --accent-on-dark: #FF6A5C;

  /* The portal disc. Sampled from the owner's own portrait visual (rgb 244 197
     25), not invented: the opening composition is her image and the site has no
     business repainting it a different yellow. It is a one-off, not a second
     accent — nothing else on the site uses it. */
  --portal-gold: #F4C519;

  --shell:      1400px;
  --gut:        clamp(20px, 4.6vw, 84px);

  /* The tallest a work tile is allowed to get in a rail. A rail of nothing but
     portrait clips would otherwise be as wide as the page and twice as tall; at
     this cap the tile crops instead. */
  --tile-cap: 620px;

  /* editorial corners: near-square. Rounded cards read as app UI, not print. */
  --r-lg:       4px;
  --r-md:       3px;
  --r-pill:     999px;

  --f: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --f-script: 'Pacifico', cursive;

  --ease:       cubic-bezier(.22, .61, .36, 1);
  --ease-out:   cubic-bezier(.16, 1, .3, 1);

  --nav-h:      66px;

  /* the display scale. Uppercase at 900 needs negative tracking or it reads as
     a shout rather than a headline. */
  --track:      -.035em;
}

/* ================================================== THE DARK TITLE CARD ====
   The opening card runs dark; the rest of the About page does not. Her portrait is
   a cut-out and reads badly pasted onto white, so the card it sits in is black — but
   a whole page of light type on black is tiring to read, and the page is mostly text.
   So the dark lives on the CARD, not the page.

   This is a token flip and nothing else: every colour inside the card is written in
   terms of the same tokens as the rest of the site, so flipping them here re-colours
   the type, the hairlines, the chips and the buttons together — and the card's own
   gradient (which is built from --surface/--paper/--page) comes out dark for free.
   Two themes written as one theme cannot drift apart. */
.title-card {
  --page:        #0B0B0A;
  --paper:       #141413;
  --surface:     #171716;
  --ink:         #F3F1ED;
  --ink-soft:    #E4E1DA;
  --body:        #C6C2BA;
  --muted:       #98948B;
  --line:        rgba(243, 241, 237, .15);
  --line-2:      rgba(243, 241, 237, .08);
  --line-strong: rgba(243, 241, 237, .34);
  --accent:      #FF6A5C;
  --accent-soft: rgba(255, 106, 92, .14);
}

/* ---------------------------------------------------------------- reset --- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: var(--f);
  font-size: 20px;
  font-weight: 400;
  line-height: 1.65;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
body.is-locked { overflow: hidden; }

/* ---------------------------------------------------------- the tone layer --
   A fixed wash behind everything: three very soft radial tones that drift at
   different speeds as the page scrolls, so the ground under the type is never
   quite the same twice.

   z-index -1 puts it behind all the page content but above the body's own
   background. Any section that paints an opaque --page background would hide it,
   so those sections are transparent now — which looks identical, because --page
   is what the body already paints.

   The driver only ever writes transforms (see site.js): animating
   background-position would repaint these huge gradients on every scroll frame,
   where a transform is composited and costs almost nothing. */
.tone {
  position: fixed; inset: 0; z-index: -1;
  overflow: hidden; pointer-events: none;
  opacity: 0;
  transition: opacity 1.1s var(--ease-out);
}
.tone.is-on { opacity: 1; }
.tone__blob {
  position: absolute; display: block;
  width: 110vmax; height: 110vmax; border-radius: 50%;
  will-change: transform;
  transform: translate3d(var(--tx, 0), var(--ty, 0), 0) scale(var(--sc, 1));
}
.tone__blob--a {
  left: -34vmax; top: -40vmax;
  background: radial-gradient(closest-side, rgba(226, 58, 46, .13), rgba(226, 58, 46, 0) 72%);
}
.tone__blob--b {
  right: -40vmax; top: 4vmax;
  background: radial-gradient(closest-side, rgba(86, 122, 186, .15), rgba(86, 122, 186, 0) 72%);
}
.tone__blob--c {
  left: -8vmax; bottom: -46vmax;
  background: radial-gradient(closest-side, rgba(228, 176, 84, .16), rgba(228, 176, 84, 0) 72%);
}

img, video { max-width: 100%; height: auto; display: block; }
h1, h2, h3, h4, p, ul, ol, dl, dd, figure, blockquote { margin: 0; }
ul, ol { padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
input, select, textarea { font: inherit; color: inherit; }
hr { border: 0; border-top: 1px solid var(--line); margin: 0; }
:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
::selection { background: var(--ink); color: var(--page); }

.sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 999;
  background: var(--ink); color: var(--page); padding: 12px 18px; font-weight: 700;
}
.skip:focus { left: 12px; top: 12px; }

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

/* ----------------------------------------------------------- typography --- */
.display,
.display-xl, .display-l, .display-m {
  font-weight: 900;
  line-height: .95;
  letter-spacing: var(--track);
  /* --track is -0.035em, which tightens the SPACE glyph as well as the letters. At 58px
     that is a two-pixel bite out of every word gap, so "known as Elza" set at display size
     reads as "known asElza". Word-spacing puts the gap back without loosening the letters. */
  word-spacing: .09em;
  text-transform: uppercase;
  text-wrap: balance;
  color: var(--ink);
}
.display-xl { font-size: clamp(34px, 6.4vw, 96px); line-height: .94; }
.display-l  { font-size: clamp(28px, 4.6vw, 64px); }
.display-m  { font-size: clamp(23px, 3vw, 42px); }

h3 { font-weight: 800; letter-spacing: -.02em; line-height: 1.1; }
h4 { font-weight: 800; letter-spacing: -.01em; }

.eyebrow {
  font-size: 14.5px;
  font-weight: 800;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--accent);
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 18px;
}
.eyebrow::before {
  content: "";
  width: 26px; height: 2px; background: var(--accent); flex: none;
}

.caps {
  font-size: 11.5px; font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase; color: var(--muted);
}
.lead { font-size: clamp(19px, 1.5vw, 22px); line-height: 1.6; color: var(--body); }
.sub { color: var(--body); }
.muted { color: var(--muted); }
.fineprint { font-size: 17px; color: var(--muted); line-height: 1.65; }
.hint { font-size: 16px; color: var(--muted); }
.note {
  font-size: 17px; color: var(--body);
  border-left: 3px solid var(--accent); padding-left: 14px;
}
.hot { color: var(--accent); font-weight: 700; }
.kicker { font-size: 16px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
.meta { font-size: 15.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); font-weight: 700; }
.mt-l { margin-top: 44px; }
.mt-m { margin-top: 26px; }

/* -------------------------------------------------------------- buttons --- */
.btn {
  --bg: var(--ink);
  --fg: var(--page);
  display: inline-flex; align-items: center; justify-content: center;
  gap: 10px;
  min-height: 50px;
  padding: 0 26px;
  background: var(--bg);
  color: var(--fg);
  border: 2px solid var(--bg);
  border-radius: var(--r-md);
  font-weight: 800;
  font-size: 17px;
  letter-spacing: .04em;
  text-transform: uppercase;
  transition: background .2s var(--ease), color .2s var(--ease),
              border-color .2s var(--ease), transform .2s var(--ease);
}
.btn span { position: relative; }
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }

/* the primary button is the ink itself; on hover it gives way to the accent */
.btn--gold { --bg: var(--ink); --fg: var(--page); }
.btn--gold:hover { --bg: var(--accent); border-color: var(--accent); }

.btn--ghost { --bg: transparent; --fg: var(--ink); border-color: var(--ink); }
.btn--ghost:hover { --bg: var(--ink); --fg: var(--page); }

.btn--on-dark { --bg: var(--page); --fg: var(--ink); border-color: var(--page); }
.btn--on-dark:hover { --bg: var(--accent); --fg: #fff; border-color: var(--accent); }

.btn--sm { min-height: 40px; padding: 0 18px; font-size: 12.5px; }

.arrow-link {
  display: inline-flex; align-items: center; gap: 10px;
  font-weight: 800; font-size: 16.5px; letter-spacing: .06em;
  text-transform: uppercase;
  border-bottom: 2px solid var(--ink);
  padding-bottom: 3px;
  transition: color .2s var(--ease), border-color .2s var(--ease), gap .2s var(--ease);
}
.arrow-link::after { content: "→"; transition: transform .25s var(--ease-out); }
.arrow-link:hover { color: var(--accent); border-color: var(--accent); }
.arrow-link:hover::after { transform: translateX(5px); }

/* ------------------------------------------------- the word reveal -------- */
/* Every display heading is split into words, each in its own clipped box, and
   the words rise into place as the heading arrives. Editorial sites live or die
   on this move: it gives a page a sense of being set rather than loaded.
   The split happens in JS so the HTML a crawler or screen reader receives stays
   one plain heading — see site.js.
   NOTE the class is `split-words`, not `split`: `.split` is already the
   two-column LAYOUT class used by the hire and about pages, and reusing the name
   silently turned every heading into a grid. */
.split-words { display: inline; }
.split-words .w {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding: .06em 0 .10em;
}
.split-words .w i {
  display: inline-block;
  font-style: normal;
  transform: translateY(118%);
  transition: transform .95s var(--ease-out) var(--wd, 0ms);
  will-change: transform;
}
.split-words.is-in .w i { transform: none; }
/* the container keeps its fade but drops its slide: the words are doing the
   moving, and two transforms on the same axis read as a stutter */
.reveal--head { transform: none !important; }

/* ------------------------------------------------------ the read bar ------ */
/* Three pixels of accent showing how far through the page you are. Cheap, and
   it quietly signals that the page is a single considered thing. */
.readbar {
  position: fixed; top: 0; left: 0; z-index: 100;
  height: 3px; width: var(--read, 0%);
  background: var(--accent);
  pointer-events: none;
}

/* ------------------------------------------------------------ structure --- */
/* Section rhythm. This was clamp(70px, 9vw, 150px), which put 259px between two
   sections at desktop width — enough that the work rows read as separate pages rather
   than one run of work. At this the gap is ~156px: still generous, but the eye can
   hold two sections at once, which is the point of a scrolling page. */
/* Section rhythm. Two sections of work should read as one run of work, not as separate
   pages: at 88px top and bottom the gap between two sections was 176px. 62px puts 124px
   between them, which still separates them without the page feeling like a corridor. */
.band { padding: clamp(38px, 4.4vw, 62px) 0; position: relative; }
.band--tight { padding: clamp(30px, 3.4vw, 48px) 0; }
.band--sunk { background: var(--paper); }
/* Jumping to a section must not put its heading under the sticky bar. The section
   rhythm is tighter than the bar is tall on a narrow window, so this is not optional. */
main [id] { scroll-margin-top: calc(var(--nav-h) + 16px); }

.section-head {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: clamp(20px, 4vw, 70px);
  align-items: end;
  margin-bottom: clamp(36px, 4.6vw, 68px);
}
.section-head__l { min-width: 0; }
.section-head__l .display { margin-top: 0; }

/* reveal-on-scroll: transform+opacity only, so it never costs a layout pass */
.reveal { opacity: 0; transform: translateY(26px); }
.reveal.is-in {
  opacity: 1; transform: none;
  transition: opacity .7s var(--ease-out) var(--d, 0ms),
              transform .8s var(--ease-out) var(--d, 0ms);
}

/* ================================================================= TOPBAR ==
   Clean white, edge to edge, no rule under it. The nav is the quietest thing on
   the page so the type can be the loudest. */
.topbar {
  position: sticky; top: 0; z-index: 60;
  background: var(--surface);
  height: var(--nav-h);
  display: flex; align-items: center;
}
.topbar__in {
  display: flex; align-items: center; gap: clamp(16px, 3vw, 46px);
  width: 100%;
}
/* The wordmark is a link, and on a phone it measured 100x20 — under the 24x24 a target
   needs at all, and far under the 44px a thumb wants. The padding is invisible (it has no
   ground of its own) and rides inside the 66px bar. */
.wordmark { display: flex; flex-direction: column; justify-content: center; gap: 1px;
            min-width: 0; min-height: 44px; padding: 4px 0; }
.wordmark__name {
  font-weight: 900; font-size: 20px; letter-spacing: -.03em;
  text-transform: uppercase; line-height: 1;
}
.wordmark__role {
  /* Tightened from .14em: at .14em the line ran 13px wider than the space the
     topbar gives it, so the last word was cut to "AI TRA…" on a wide screen. */
  font-size: 9.5px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.nav { display: flex; align-items: center; gap: clamp(14px, 2.2vw, 34px); margin-left: auto; }
.nav a {
  white-space: nowrap;
  font-size: 13px; font-weight: 700; letter-spacing: .02em;
  color: var(--ink-soft);
  padding: 6px 0;
  position: relative;
}
.nav a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 2px; background: var(--accent);
  transform: scaleX(0); transform-origin: left;
  transition: transform .28s var(--ease-out);
}
.nav a:hover::after, .nav a.is-active::after { transform: scaleX(1); }
.nav a.is-active { color: var(--ink); }
.topbar__cta { display: flex; align-items: center; gap: 12px; }
/* "Book a course" fits the bar at a full desktop width but wrapped onto two lines from
   roughly 900px up to 1270px, where the bar is a 66px strip: a two-line label inside a
   pill that is taller than its own min-height. The label stays on one line, and the
   wordmark's role line — which already truncates with an ellipsis by design — takes the
   squeeze. */
.topbar__cta .btn { white-space: nowrap; }

.burger {
  display: none;
  width: 44px; height: 44px;
  border: 2px solid var(--ink); border-radius: var(--r-md);
  align-items: center; justify-content: center;
}
.burger i { display: block; width: 18px; height: 2px; background: var(--ink); position: relative; }
.burger i::before, .burger i::after {
  content: ""; position: absolute; left: 0; width: 18px; height: 2px; background: var(--ink);
}
.burger i::before { top: -6px; }
.burger i::after { top: 6px; }

.drawer {
  position: fixed; inset: var(--nav-h) 0 0 auto; z-index: 59;
  width: min(88vw, 400px);
  background: var(--surface);
  border-left: 1px solid var(--line);
  padding: 28px var(--gut) 40px;
  display: flex; flex-direction: column;
  transform: translateX(102%);
  transition: transform .42s var(--ease-out);
  overflow-y: auto;
}
.drawer.is-open { transform: none; }
/* Menu rows. The CTA in the drawer foot is a <button>, not a row: :not(.btn) keeps the
   row's 18px 0 padding off it — with that padding the label sat flush against the
   button's edge and read as cut off. */
.drawer a:not(.btn) {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  padding: 18px 0; border-bottom: 1px solid var(--line);
  font-weight: 900; font-size: 26px; letter-spacing: -.03em; text-transform: uppercase;
}
.drawer a small { font-size: 14px; letter-spacing: .14em; color: var(--muted); font-weight: 700; text-transform: uppercase; }
.drawer__foot { margin-top: auto; padding-top: 26px; }
.drawer__foot .btn { width: 100%; padding: 18px 24px; border-bottom: 0; white-space: nowrap; }

/* ============================================================== COVER BAND ==
   The opening film. The band's shape is the clip's own, and that is the only shape
   that shows the whole word — the measurement behind it is on `.cover` below. */
.cover-wrap {
  /* No explicit height. The band's shape comes from `aspect-ratio` on `.cover` itself,
     derived from the element's real width — see the note there. `offsetHeight` on this
     wrapper (used by the hand-over in site.js) still measures the band correctly.
     Deliberately not capped by the viewport height: a `vh` cap is what reintroduces
     letterboxing, on exactly the wide screens where it is most obvious. The spot band
     below already runs at 56.25vw, so the two read as a matched pair of full-bleed
     films. */
  position: relative;
  /* No runway. The band is simply one section and the hero follows it.
     An earlier version pinned the band here and pulled the hero up over it by a
     50vh runway. That only looked right while the hero was opaque. It is
     transparent now — it has to be, so the gradient tone layer shows through
     behind the words — so on the way back up the film and the words were visible
     at once, which reads as the second banner lying on top of the first. */
}
/* The black shoulder above the strip. Two things need it. The owner wants the whole
   background of the wordmark black, and on the arrival the black should reach the top of
   the screen rather than showing the page's own ground above a floating strip. And the
   strip is short — 333px across a laptop — so a visitor whose gesture carries a little past
   the end of the sequence used to push it off the top of the screen and the whole writing
   animation played where they could not see it: the owner's "look into the laptop version
   too, it look like it has some bug on the animation of meet elza". With the shoulder, the
   words land well down the screen and there is room to overshoot through. */
.cover-wrap {
  background: #000;
  /* The shoulder, halved again: "make the laptop version smaller by 10% and top and bottom
     padding reduce by half ... make the mobile version smaller by 20% and top and bottom
     padding reduce as well". It is here to give the words somewhere to land, not to be a
     screenful — and the band keeps its own height now, so this clamp IS the black above the
     film that she sees. */
  padding-top: clamp(8px, 2vh, 26px);
  /* Black below the strip as well as above it. Without it the page's own tint shows in the
     gap between the black block and the reel that follows, as a grey gradient running out of
     the black — the owner's "make sure the background is black, and not gradient into grey". */
  padding-bottom: clamp(14px, 3.5vh, 48px);
}
.cover {
  position: relative;
  /* The band IS the clip's shape, taken from the element's real width. The clip is drawn to
   this shape rather than the shape being chosen for it: "meet elza" in the script face is a
   wide, short word (about 4.7:1), so a 16:9 band could only hold it with a big black band
   above and below — the owner's "make sure also the padding on top and below is not too
   big". The strip is sized to the words, and the words fill it to about 92% of the width
   and 76% of the height. Nothing is cropped and nothing is letterboxed.

   `aspect-ratio` rather than a height in `vw`, because `100vw` includes the scrollbar
   while the band's own width does not: `calc(100vw * 370/1404)` comes out about 1% too
   tall at a desktop width, and that 1% is a real crop of the lettering. Taking the height
   from the element's actual width cannot drift. `min-height` only matters below about
   260px wide, where the shape is not exactly the clip's — no real device. */
aspect-ratio: 1404 / 370;
  min-height: 90px;
  /* The clip's own ground colour (pure black, as the wordmark's reference has it), as a
     backstop — it only covers the instant before the first frame decodes. */
  background: #000;
  display: grid; place-items: center;
  overflow: hidden;
}
/* The film fills the band, and because the band is the clip's exact shape, `cover`
   shows the identical frame to `contain` — it is simply the one that cannot leave a
   hairline bar when the height rounds down by a pixel. No `object-position`: with no
   crop to place, anchoring it either way would only invite a crop back in.

   It was `contain` historically, in bands of other shapes, and the bars were painted the
   film's own ground colour to hide them. That does not hide them — the film's edges are
   not that colour — so the band's shape does the work instead. */
.cover__video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
/* ------------------------------------------------------ how big the band is --
   THE WALK ENDS ON THIS BAND (see the settle in portal.js), and the band is the film plus a
   shoulder of black — not a full screen of black with the film somewhere in the middle. The
   owner, after the full-height version: "make the laptop version smaller by 10% and top and
   bottom padding reduce by half" — the shoulder clamps above are already halved, and with the
   band keeping its own height (the film's shape plus that shoulder) halving them is something
   she can actually see.

   The item carries its own width, and that is not decoration. In a centring GRID an item with
   no width is measured against its content, and this section's content is an absolutely
   positioned film plus a clipped `.sr` heading — so the strip collapsed to its `min-height`
   (a 287x90 card) and the lettering came out a fraction of its size: "the words is nice, but a
   bit small". Flex, not a grid, for the same reason — a percentage width on a grid item
   resolves against a track that is sized to that item, so it stays collapsed.

   76% is 84% less a tenth, the 10% she asked for on a laptop. These live here rather than beside
   `.cover-wrap` above because the band's own checks read the FIRST `.cover {` block in this file
   as the band's base rule. */
@media (min-width: 1100px) {
  /* The film at the top of the black section, and the section's bottom padding is a TENTH OF
     THE FILM'S OWN HEIGHT: "the black section meet elza, increase the bottom padding of the
     section by 10%".

     The top padding is zero — the film sits flush under the header — and the black that shows
     above the words is the margin the CLIP draws inside its own frame, about 12% of its height.
     Ten per cent of the film's height is the step she asked for past that: a visible strip
     under the words, not the screen of black she has twice rejected. Written in `vw` because
     the film's height follows the window's WIDTH, not its height — the film is 68% of the
     window and 740/2808 of its own width, so a tenth of it is 0.68 × 0.2635 × 0.1 ≈ 1.8vw
     (23px at 1280, 35px at 1920). A `padding-bottom: 10%` would be a tenth of the window's
     width instead — 128px at 1280 — which is four times this and not what she asked for. */
  .cover-wrap { display: flex; align-items: flex-start; justify-content: center; padding-top: 0; padding-bottom: 1.8vw; }
  .cover-wrap > .cover { width: 68%; }
}
/* A phone gets the stacked clip, which is nearly square, so at the full width of the phone the
   two rows read as a poster rather than a title: "for mobile version, also reduce the font size
   of the word meet elza". 61% is 64% less the 5% she asked for after that.

   The black section hugs the film: no padding above or below it, the pair equal — "mobile version:
   make the top and bottom padding same. follow the top padding size". The top had been zero since
   the film was pinned to the top of the section, so the bottom follows it to zero and the section
   ends where the film does; the words keep the black the clip itself draws around them. Before
   this the section carried a min-height of half the stage, which is what put the 148px of empty
   black under the words she asked to bring down — "i rather it shorten, the big black space under
   the word in mobile version". The same shape test as the door drawing and the portrait, because
   `.cover`'s height follows its own width. Same specificity as the rule above and written after
   it, so a portrait screen above 1100px takes this one. */
@media (max-width: 760px), (max-width: 1180px) and (orientation: portrait) {
  .cover-wrap { display: flex; align-items: flex-start; justify-content: center; padding-top: 0; padding-bottom: 0; }
  .cover-wrap > .cover { width: 61%; margin-inline: auto; }
}

/* ================================================================= THE SPOT ==
   A second film directly after the banner: a ten-second product spot, played once.
   When it ends it gives its place to the section below — the band closes up and the
   hero rises into the room it left, so a finished film never sits between a visitor
   and the words.

   The band's height is exactly 16:9 (56.25vw), which is the clip's own shape, so
   there are no bars to hide; the ground colour is sampled from the clip anyway
   (rgb 202 206 210) for the capped and phone cases, where a bar does appear. */
/* The band is exactly the clip's shape (56.25vw is 16:9), so there are no bars to
   hide on any ordinary window; the background is sampled from the clip (rgb 202 206
   210) for the short-window case, where a bar does appear. The film is absolutely
   positioned rather than stretched as a grid item — as a grid item its height came
   from its own aspect ratio and it overflowed the band. */
.spot {
  position: relative;
  overflow: hidden;
  background: #CACED2;
  height: clamp(200px, 56.25vw, 100vh);
}
.spot__video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;
  display: block;
}

/* ============================================================ BAND SOUND BUTTON ==
   Both films carry their own soundtrack, and both keep it muted in the markup: a muted
   clip is the only kind a browser will autoplay. So the picture is already moving and
   the clip simply has no voice, and this button gives it one — in the element it is
   already playing in, so nothing opens over the film.

   The state lives on the button, not on a class: `aria-pressed` is what the glyph and
   the ground colour are keyed off, so there is one source of truth and it is the one a
   screen reader reads. No backdrop-filter here — it is expensive over moving footage. */
.band-sound {
  position: absolute;
  right: clamp(14px, 3vw, 40px);
  bottom: clamp(14px, 3vw, 40px);
  z-index: 2;
  display: inline-flex; align-items: center; gap: 10px;
  padding: 12px 20px;
  font: inherit; font-size: 14.5px; font-weight: 800;
  letter-spacing: .16em; text-transform: uppercase;
  color: #fff;
  background: rgba(16, 16, 15, .64);
  border: 0; border-radius: var(--r-pill); cursor: pointer;
  transition: background .2s var(--ease-out), transform .2s var(--ease-out);
}
.band-sound:hover { background: var(--accent); transform: translateY(-2px); }
/* The banner's own pill is gone: the owner asked for the music button off the "Meet
   Elza" band, so the `.cover .band-sound` overrides that used to light it off black
   left with it. The band below still uses the base button, and the base button is
   shaded for a mid-tone ground rather than a black one. */
/* the glyph is the state: a play triangle while the band is silent, a stop square
   once it is speaking, so the button says what it will do next */
.band-sound i {
  width: 0; height: 0;
  border-left: 9px solid currentColor;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
}
.band-sound[aria-pressed="true"] i {
  width: 9px; height: 9px;
  border: 0; background: currentColor;
}
.band-sound[aria-pressed="true"] { background: var(--accent); }
/* On a phone the band is only about 219px tall, so a full-size pill takes half the
   width and reads louder than the film. Same button, quieter. */
@media (max-width: 700px) {
  .band-sound { padding: 10px 15px; font-size: 14px; gap: 8px; letter-spacing: .12em; }
  .band-sound i { border-left-width: 7px; border-top-width: 5px; border-bottom-width: 5px; }
}
/* ==================================================== WIDE VIEW (a phone) ==
   The same pill, on the other side of the film's own controls. A phone shows a 16:9
   film in a strip about a third of the screen's height, so the owner asked for a
   control to turn it: "add an icon where i can click and rotate to horizontal so that
   the view is bigger". This is that control's face — the turning is done by the
   platform (site.js), and where no platform will do it there is nothing this button
   could honestly promise, so it is shown only where a phone would use it.

   Hidden above the phone breakpoint: a desktop window is already the shape the film
   was drawn for, and an extra pill there would be a control that does nothing. On a
   phone it sits bottom-LEFT so it cannot crowd the sound pill on the right, and it is
   the same weight of ink and type — a control on a film has to stay readable against
   moving footage. */
.band-wide {
  position: absolute;
  left: clamp(14px, 3vw, 40px);
  bottom: clamp(14px, 3vw, 40px);
  z-index: 2;
  display: none;
  align-items: center; gap: 9px;
  padding: 11px 17px;
  font: inherit; font-size: 13.5px; font-weight: 800;
  letter-spacing: .13em; text-transform: uppercase;
  color: #fff;
  background: rgba(16, 16, 15, .64);
  border: 0; border-radius: var(--r-pill); cursor: pointer;
  transition: background .2s var(--ease-out), transform .2s var(--ease-out);
}
.band-wide:hover, .band-wide:focus-visible { background: var(--accent); transform: translateY(-2px); }
.band-wide__icon { width: 19px; height: 19px; flex: none; display: block; }
.band-wide[aria-pressed="true"] { background: var(--accent); }
@media (max-width: 760px) {
  .band-wide { display: inline-flex; }
}
/* Both pills, on a phone, at the bottom of their film: the wide-view pill on the LEFT, the
   sound pill on the RIGHT. A media query adds no specificity, so nothing may repeat this
   positioning further up the file, and the base rule above already puts the wide pill at
   `left: clamp(14px, 3vw, 40px)` — the phone block that used to move it to the right edge
   (stacked above the sound pill, because side by side the two pills left only 28px of film
   between them) is gone. The owner asked for it back on the left, on BOTH bands: "add the
   wide view button to my 3 me video too. but add at the left side of the screen. for the
   video below 3 me, shift the wide view button to the left side". At the phone sizes above
   the pills come down a step (see the block above), which is what buys the room. */
@media (max-width: 760px) {
  .band-sound, .band-wide { padding: 9px 13px; font-size: 12.5px; gap: 7px; letter-spacing: .1em; }
  .band-wide__icon { width: 17px; height: 17px; }
}
@media (max-width: 400px) {
  .band-wide span { font-size: 11.5px; }
}
/* ========================================================== BACK TO THE TOP ==
   It scrolls up. It inverts on the dark About page for free: it is built from the
   same two tokens as everything else. */
.totop {
  position: fixed;
  right: clamp(14px, 2.4vw, 30px);
  bottom: clamp(14px, 2.4vw, 30px);
  z-index: 70;
  display: inline-flex; align-items: center; gap: 9px;
  padding: 12px 18px;
  font: inherit; font-size: 14.5px; font-weight: 800;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--page); background: var(--ink);
  border: 0; border-radius: var(--r-pill); cursor: pointer;
  /* out of the way until it is wanted — and out of the tab order with it */
  opacity: 0; transform: translateY(14px); pointer-events: none;
  transition: opacity .35s var(--ease-out), transform .35s var(--ease-out),
              background .2s var(--ease-out);
}
.totop.is-on { opacity: 1; transform: none; pointer-events: auto; }
.totop:hover { background: var(--accent); }
.totop__arrow { font-size: 18px; line-height: 1; }

/* the band loops, so it never gives up its place and the section below never rises */
/* (There was a `.cover::after` here: a 96px gradient at the band's bottom edge fading to
   the page colour, so the old film melted into the page instead of ending on a hard line.
   It is gone because the band is black now and that fade read as a grey gradient INSIDE the
   black — the owner's "make sure the background is black, and not gradient into grey". The
   band ends on black, the reel below starts on its own footage, and the hard line between
   them is what the black block wants. */

/* =================================================================== HERO ==
   No video. The first impression is type: the positioning line set enormous,
   which costs one font file instead of nine megabytes of footage. */
/* The hero IS the deck: the section is three screens tall with the stage pinned
   inside it, so the section itself carries no padding — the words do. */
.hero {
  position: relative;
  padding: 0;
  /* transparent on purpose: the fixed tone layer has to show through behind the
     words. It looks identical, because --page is what the body already paints.
     Because it is transparent it must NOT overlap the cover band — see
     .cover-wrap. */
  background: transparent;
}
/* The words are the deck's section head. The driver measures THIS element's
   height to work out its scroll travel, so the padding has to live here rather
   than on .hero — otherwise the words scroll in before the stage pins. */
.hero .stack__head { padding: clamp(40px, 5.5vw, 88px) 0 clamp(30px, 4vw, 56px); }
.hero__inner { display: flex; flex-direction: column; gap: clamp(26px, 4vw, 52px); }
.hero__media { display: none; }

.hero__meta {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
  padding-bottom: 16px;
}
.hero__index {
  font-size: 14.5px; font-weight: 800; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink);
}
.hero__badge {
  margin-left: auto;
  font-size: 14.5px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent);
  border: 2px solid var(--accent); border-radius: var(--r-pill);
  padding: 5px 14px;
}

.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
  gap: clamp(22px, 4vw, 64px);
  align-items: end;
}
.hero__title {
  font-size: clamp(28px, 4.2vw, 58px);
  font-weight: 900;
  line-height: .95;
  letter-spacing: var(--track);
  text-transform: uppercase;
  text-wrap: balance;
  /* --tilt-x/--tilt-y are written by site.js from the pointer position, so the
     headline leans very slightly toward the cursor. Four degrees at most: enough
     to feel alive, not enough to make the type hard to read. */
  transform: perspective(1400px) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
  transform-style: preserve-3d;
  transition: transform .2s var(--ease-out);
  will-change: transform;
}
.hero__title .a, .hero__title .b { display: block; }
/* the second line steps in — the one asymmetric move in the composition */
.hero__title .b { padding-left: .09em; }
/* The three verbs ARE the three roles, and they are the only red in the
   headline — the thread that ties the words to the cards dealt below them. */
.hero__title em { font-style: normal; color: var(--accent); }

/* The hero timeline lived here — three tracks you could scrub, with a playhead
   and a filling bar. It was removed when the three-roles deck became the hero:
   the deck's cards state the same three roles, and the roles are stated once.
   If it is ever wanted back, it is in this project's history. */

.hero__aside { display: flex; flex-direction: column; gap: 24px; padding-bottom: 8px; }
.hero__sub { font-size: clamp(16px, 1.3vw, 19px); line-height: 1.6; color: var(--body); max-width: 44ch; }
.hero__actions { display: flex; gap: 12px; flex-wrap: wrap; }

.hero__foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 20px; flex-wrap: wrap;
  border-top: 1px solid var(--line);
  padding-top: 18px;
}
.hero__study { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.hero__study-kind { color: var(--ink); }
.hero__study-note { font-size: 16.5px; color: var(--muted); }
.hero__ask { font-size: 16.5px; color: var(--muted); max-width: 48ch; text-align: right; }
.hero__rail { display: flex; align-items: center; gap: 14px; }
.hero__scenename {
  font-size: 14.5px; font-weight: 800; letter-spacing: .14em;
  text-transform: uppercase;
}
.hero__loop { display: block; width: 130px; height: 3px; background: var(--line); overflow: hidden; }
.hero__loop i { display: block; height: 100%; width: 0; background: var(--accent); }

/* ================================================================ MARQUEE ==
   One line of capability, running. Ink on the paper ground. */
.marquee {
  overflow: hidden;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  padding: 20px 0;
  background: var(--paper);
}
.marquee__track {
  display: flex; gap: 0; width: max-content;
  animation: marquee-run 46s linear infinite;
}
.marquee__item {
  font-size: clamp(20px, 2.5vw, 38px);
  font-weight: 900;
  letter-spacing: -.02em;
  text-transform: uppercase;
  padding: 0 30px;
  display: inline-flex; align-items: center; gap: 30px;
  color: var(--ink);
  white-space: nowrap;
}
.marquee__item::after {
  content: ""; width: 10px; height: 10px; background: var(--accent); border-radius: 50%;
}
@keyframes marquee-run { to { transform: translateX(-50%); } }
/* an animation nobody can see should not be asking for frames. The band's film is
   paused in script instead, because a <video> ignores animation-play-state. */
.marquee.is-offscreen .marquee__track,
.tape.is-offscreen { animation-play-state: paused; }

/* ============================================================== THE DECK ==
   "What I do": three roles dealt as cards over the words WHAT I DO. The words
   start sharp and blow out of focus as the cards come in, so the background
   recedes exactly as the foreground arrives.
   The section is tall on purpose: that height IS the scroll the cards run on. */
.stack {
  position: relative;
  background: transparent;
  padding: 0;
  /* The extra height IS the scroll runway the cards are dealt along. Without it
     the deck would just scroll past instead of holding still while the roles
     change. Reset to auto wherever the deck unpins (phone / reduced motion). */
  min-height: 300vh;
}
/* The words are centred in the PINNED STAGE, not in the section: the section is
   three screens tall, so centring them there would park them a screen and a half
   away from the cards they are meant to sit behind. */
.stack__bg {
  position: absolute; inset: 0;
  display: grid; align-content: center; justify-items: center;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
  padding-inline: 2vw;
}
.stack__bg span {
  display: block;
  font-size: clamp(56px, 13vw, 200px);
  font-weight: 900;
  line-height: .80;
  letter-spacing: -.05em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: .13;
  filter: blur(var(--bg-blur, 0px));
  transform: translateY(var(--bg-shift, 0px));
  white-space: nowrap;
  will-change: filter, transform;
}
.stack__head { position: relative; z-index: 1; padding-top: clamp(60px, 8vw, 130px); }

.stack__stage {
  position: sticky; top: 0; z-index: 1;
  height: 100vh;
  display: grid; place-items: center;
  padding: 4vh var(--gut);
}
.stack__deck {
  position: relative;
  width: min(100%, 640px);
  aspect-ratio: 5 / 4;
  perspective: 1600px;
}

/* the red tab on the deck's edge — the one vertical mark in the layout */
.stack__tag {
  position: absolute; right: -14px; top: 18%;
  background: var(--accent);
  color: #fff;
  font-size: 13.5px; font-weight: 800; letter-spacing: .22em;
  text-transform: uppercase;
  writing-mode: vertical-rl;
  padding: 16px 7px;
  z-index: 5;
}

/* ============================================================= THE CARDS ==
   Used by the deck (services) and by the courses grid. In the deck the scroll
   driver writes --ty/--sc/--re per card; as a plain grid they just sit there. */
.card-service {
  background: var(--deck);
  color: var(--ink);
  border-radius: var(--r-lg);
  padding: clamp(22px, 3vw, 40px);
  display: flex; flex-direction: column;
  gap: 14px;
  position: relative;
  overflow: hidden;
}
.card-service__n {
  font-size: clamp(30px, 4.6vw, 62px);
  font-weight: 900;
  line-height: 1;
  letter-spacing: -.05em;
  color: var(--ink);
}
.card-service h3 {
  font-size: clamp(21px, 2.5vw, 34px);
  font-weight: 900; text-transform: uppercase; letter-spacing: -.03em;
}
.card-service > p { font-size: 18.5px; line-height: 1.55; color: #23262B; }
.card-service ul { display: flex; flex-direction: column; gap: 5px; }
.card-service ul li {
  font-size: 17px; font-weight: 600; color: #23262B;
  display: flex; gap: 9px; align-items: baseline;
}
.card-service ul li::before { content: "—"; color: rgba(10,10,10,.4); }
.card-service .arrow-link { margin-top: auto; align-self: flex-start; }

/* in the deck, one card per slot, driven by the scroll position */
.stack__deck .card-service {
  position: absolute; inset: 0;
  transform: translateY(var(--ty, 0%)) scale(var(--sc, 1)) rotateX(var(--rx, 0deg));
  transform-origin: 50% 100%;
  opacity: var(--re, 1);
  box-shadow: 0 26px 60px rgba(10, 10, 10, .13);
  will-change: transform;
  justify-content: flex-start;
}
/* the deck card's number sits bottom-right, as in a printed plate */
.stack__deck .card-service__n {
  position: absolute; right: clamp(22px, 3vw, 40px); bottom: clamp(18px, 2.4vw, 32px);
  opacity: .9;
}

/* ============================================================== SHOWCASE ==
   The work. Real clips, muted as previews, click to hear them. */
/* The column list IS the data. The template emits one proportional track per clip
   (--cols: 1.778fr 0.563fr 0.563fr), so the row fills edge to edge exactly and —
   because each track's width is proportional to its clip's ratio — every card in
   the row comes out the same height. That is what keeps a vertical reel in its
   place: it gets the narrow track its shape earns instead of a full-width one.
   Letting auto-fit/minmax guess the tracks is what left holes and giant tiles. */
.rail {
  display: grid;
  gap: clamp(14px, 1.6vw, 24px);
  grid-template-columns: var(--cols, 1fr);
  /* start, not stretch: stretching makes every card as tall as the tallest in the row,
     which fills the shorter ones with dead space under their media. */
  align-items: start;
}

/* More clips than fit one row comfortably: pair them up rather than let the tracks
   shrink into slivers. */
.rail--wrap { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* A rail made only of tall clips would otherwise be as wide as the page and twice
   as tall. Cap the tile's height so the frame crops rather than the layout blowing
   out: a tile's width is capped at (cap x its own ratio), which pins its height at
   the cap. Phones are exempt — a full-width vertical reel is exactly right there. */
@media (min-width: 641px) {
  .rail .card-work__media { max-width: calc(var(--tile-cap, 620px) * var(--ar, 1.7778)); }
}

.card-work { min-width: 0; }
.card-work__media {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--ink);
  aspect-ratio: var(--ar, 1.7778);
  cursor: pointer;
  /* the tile wipes up as the card arrives, and the frame inside settles back
     from a slight push — so the work is revealed rather than just present */
  clip-path: inset(0 0 100% 0);
  transition: clip-path 1.05s var(--ease-out);
}
.card-work.reveal.is-in .card-work__media { clip-path: inset(0 0 0 0); }
.card-work__media img,
.card-work__media video {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
  transform: scale(1.07);
  transition: transform 1.2s var(--ease-out), opacity .4s var(--ease);
}
.card-work.reveal.is-in .card-work__media img,
.card-work.reveal.is-in .card-work__media video { transform: scale(1); }
.card-work__media video { opacity: 0; }
.card-work:hover .card-work__media video { opacity: 1; }
.card-work__tag {
  position: absolute; top: 12px; left: 12px; z-index: 2;
  background: var(--page); color: var(--ink);
  font-size: 14px; font-weight: 800; letter-spacing: .14em;
  text-transform: uppercase;
  padding: 6px 11px; border-radius: var(--r-pill);
}
.card-work__play {
  position: absolute; left: 12px; bottom: 12px; z-index: 2;
  display: inline-flex; align-items: center; gap: 9px;
  background: var(--page); color: var(--ink);
  border-radius: var(--r-pill);
  padding: 9px 16px 9px 11px;
  font-size: 14.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  transition: background .2s var(--ease), color .2s var(--ease);
}
.card-work__play i {
  width: 22px; height: 22px; border-radius: 50%; background: var(--accent);
  display: grid; place-items: center; flex: none;
}
.card-work__play i::before {
  content: ""; border-left: 7px solid #fff; border-top: 5px solid transparent;
  border-bottom: 5px solid transparent; margin-left: 2px;
}
.card-work__play:hover { background: var(--ink); color: var(--page); }
.card-work__body { padding: 16px 2px 0; }
/* Two classes on purpose. The meta is a <p>, and the plain `.card-work__meta`
   selector loses to `.card-work__body p` further down — same element, later rule,
   higher specificity — so the label rendered at the body size instead of its own.
   Matching on two classes wins outright and does not depend on source order. */
.card-work .card-work__meta {
  font-size: 14px; font-weight: 800; letter-spacing: .14em;
  text-transform: uppercase; color: var(--accent); margin-bottom: 7px;
  /* Two lines are reserved whether the text needs them or not, so titles across a
     rail sit on one line. A wrapped meta is normal here — the card's shape and its
     duration both live in it — and letting that push the title down makes the row
     look ragged. */
  line-height: 1.45;
  min-height: 2.9em;
}
.card-work__body h3 { font-size: clamp(18px, 1.7vw, 24px); margin-bottom: 6px; }
.card-work__body p { font-size: 18px; color: var(--body); }

.empty-slot {
  border: 2px dashed var(--line-strong);
  border-radius: var(--r-lg);
  padding: clamp(30px, 5vw, 64px);
  text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
}
.empty-slot h3 { font-size: clamp(22px, 3vw, 36px); text-transform: uppercase; }
.empty-slot p { color: var(--body); max-width: 52ch; }
.empty-slot__k { color: var(--accent); }

/* ============================================================ STYLE RANGE == */
.range__head {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(20px, 4vw, 70px); align-items: end;
  margin-bottom: clamp(30px, 4vw, 56px);
}
.compare {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: #EAE1D8;
  cursor: ew-resize;
  touch-action: none;
  user-select: none;
}
.compare img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.compare__after { clip-path: inset(0 0 0 var(--split, 50%)); }
.compare__handle {
  position: absolute; top: 0; bottom: 0; left: var(--split, 50%);
  width: 2px; background: var(--ink); z-index: 3;
}
.compare__handle::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 40px; height: 40px; margin: -20px 0 0 -20px;
  border-radius: 50%; background: var(--ink);
  box-shadow: 0 0 0 4px rgba(244, 243, 239, .9);
}
.compare__label {
  position: absolute; top: 14px; z-index: 4;
  background: var(--ink); color: var(--page);
  font-size: 14px; font-weight: 800; letter-spacing: .14em;
  text-transform: uppercase; padding: 6px 12px; border-radius: var(--r-pill);
}
.compare__label--l { left: 14px; }
.compare__label--r { right: 14px; background: var(--accent); }
.compare__cap {
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  margin-top: 14px;
  font-size: 15.5px; color: var(--muted);
}
.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 1px;
  background: var(--line);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  margin-top: clamp(34px, 4.4vw, 64px);
}
.step { background: transparent; padding: 26px 22px 30px; }
.step__n {
  display: block;
  font-size: 16px; font-weight: 900; letter-spacing: .1em;
  color: var(--accent); margin-bottom: 14px;
}
.step h3 { font-size: 19px; text-transform: uppercase; margin-bottom: 9px; }
.step p { font-size: 17.5px; color: var(--body); line-height: 1.55; }

/* ================================================================ COURSES == */
.grid-services {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: clamp(14px, 1.8vw, 26px);
}
.course {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  gap: clamp(22px, 4vw, 60px);
  padding: clamp(26px, 3.4vw, 52px) 0;
  border-top: 1px solid var(--line);
}
.course__main { min-width: 0; }
.course__top { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; }
.course__top h3 { font-size: clamp(23px, 3vw, 42px); text-transform: uppercase; }
.course__level {
  font-size: 14px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase;
  color: var(--accent); border: 2px solid var(--accent);
  border-radius: var(--r-pill); padding: 5px 12px;
}
.course__summary { font-size: 19px; color: var(--body); max-width: 60ch; margin-bottom: 20px; }
.course__outline { display: flex; flex-direction: column; gap: 8px; margin-bottom: 20px; }
.course__outline li {
  font-size: 18px; color: var(--body);
  display: flex; gap: 12px; align-items: baseline;
  padding-bottom: 8px; border-bottom: 1px solid var(--line-2);
}
.course__outline li::before { content: "→"; color: var(--accent); font-weight: 800; flex: none; }
.course__outcome { font-weight: 700; font-size: 18.5px; border-left: 3px solid var(--ink); padding-left: 16px; }
.course__side { display: flex; flex-direction: column; gap: 20px; }
.specs { display: flex; flex-direction: column; gap: 0; }
.specs > div {
  display: flex; justify-content: space-between; gap: 16px;
  padding: 11px 0; border-bottom: 1px solid var(--line);
}
.specs dt { font-size: 14.5px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.specs dd { font-size: 18px; font-weight: 700; text-align: right; }
.course__side .btn { width: 100%; }

/* ============================================ COURSES — the product listing ===
   The courses page reads as a shelf of products, not a syllabus: a picture, the
   name, one line of what it is, everything you'll learn folded away behind a
   click, and one button that opens WhatsApp. The picture is a square, because the
   course posters the owner supplies are square — a wider frame would crop the
   title printed inside them.
   A course with no picture yet shows a designed tile (ink panel, big accent
   number) so the shelf never has a hole in it. */
.grid-courses {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: clamp(18px, 2.2vw, 30px);
}
.pcard {
  display: flex; flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line);
  overflow: hidden;
}
.pcard__media {
  position: relative; display: block;
  aspect-ratio: 1 / 1;
  background: var(--ink);
  overflow: hidden;
}
.pcard__media img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .5s var(--ease-out);
}
.pcard:hover .pcard__media img { transform: scale(1.03); }
.pcard__ph {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; justify-content: center; align-items: flex-start;
  gap: 10px;
  padding: clamp(16px, 2vw, 26px);
  background: var(--ink);
}
/* The number is set to fill the tile rather than sit in a corner of it: a big
   black panel with one small number in the bottom corner reads as a missing
   image, and a hole in a product shelf is worse than a plain tile. */
.pcard__ph b {
  font-size: clamp(104px, 14vw, 190px); font-weight: 900; line-height: .78;
  letter-spacing: -.06em; color: var(--accent-on-dark);
}
.pcard__ph i {
  font-style: normal; font-size: 15px; font-weight: 800;
  letter-spacing: .14em; text-transform: uppercase; color: var(--page);
  border-top: 2px solid var(--accent); padding-top: 10px; width: 100%;
}
.pcard__days {
  position: absolute; top: 0; right: 0; z-index: 2;
  background: var(--accent); color: #fff;
  font-size: 14.5px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  padding: 8px 13px;
}
.pcard__body {
  display: flex; flex-direction: column; gap: 14px; flex: 1;
  padding: clamp(18px, 2vw, 26px);
}
.pcard__series {
  font-size: 14px; font-weight: 800; letter-spacing: .14em;
  text-transform: uppercase; color: var(--accent);
}
.pcard__title { font-size: clamp(19px, 1.45vw, 23px); text-transform: uppercase; line-height: 1.12; }
.pcard__title a { color: inherit; transition: color .2s var(--ease); }
.pcard__title a:hover { color: var(--accent); }
.pcard__sum { font-size: 18px; line-height: 1.55; color: var(--body); }
.pcard__learn { border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.pcard__learn summary {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  cursor: pointer; padding: 13px 0; list-style: none;
  font-size: 15px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
}
.pcard__learn summary::-webkit-details-marker { display: none; }
/* the + that becomes a − when the list is open, so the control reads as a control */
.pcard__learn summary::after {
  content: "+"; font-size: 19px; font-weight: 800; color: var(--accent);
  line-height: 1; flex: none;
}
.pcard__learn[open] summary::after { content: "−"; }
.pcard__learn summary span { color: var(--muted); margin-left: auto; letter-spacing: .04em; }
.pcard__learn ul { display: flex; flex-direction: column; gap: 8px; padding: 4px 0 16px; }
.pcard__learn li {
  font-size: 16.5px; line-height: 1.45; color: var(--body);
  display: flex; gap: 9px; align-items: baseline;
}
.pcard__learn li::before { content: "→"; color: var(--accent); font-weight: 800; flex: none; }
.pcard__foot { margin-top: auto; display: flex; flex-direction: column; gap: 12px; align-items: flex-start; }
.pcard__cta { width: 100%; display: flex; justify-content: center; }

/* A WhatsApp button is the one place a second colour is allowed, because the
   green IS the signal — a reader recognises it before they read the label. The
   label is set in ink rather than white on purpose: white on WhatsApp green
   measures 1.98:1, which is a button nobody can read. Ink on it is 9.98:1. */
.btn--wa { --bg: #25D366; --fg: #0A0A0A; border-color: #25D366; }
.btn--wa:hover { --bg: #1EBE5A; border-color: #1EBE5A; --fg: #0A0A0A; }

/* the "ask me on WhatsApp" band under the shelf */
.wa-band {
  display: flex; flex-wrap: wrap; gap: clamp(18px, 3vw, 44px);
  align-items: center; justify-content: space-between;
  background: var(--ink); color: var(--page);
  padding: clamp(26px, 3.2vw, 48px);
}
.wa-band .eyebrow, .wa-band h2 { color: var(--page); }
.wa-band__note { font-size: 18px; line-height: 1.6; max-width: 56ch; margin-top: 10px; color: var(--page); }
.wa-band__btn { flex: none; }

/* the course poster on a course's own page */
.course-top {
  display: grid;
  /* The poster is capped rather than given a share of the row: at half the page it
     was 527px square, which is a billboard next to a four-line panel. It sits at
     up to 400px and the panel takes everything else. */
  grid-template-columns: minmax(0, 400px) minmax(0, 1fr);
  gap: clamp(22px, 3vw, 52px);
  align-items: stretch;
}
.course-top__media { margin: 0; }
.course-top__media img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 1 / 1; object-fit: cover;
  border: 1px solid var(--line);
}
.course-top__side { display: flex; min-width: 0; }
/* The panel matches the poster's height, so the row reads as two blocks of one
   composition instead of an image with a hole beside it. */
.course-panel {
  display: flex; flex-direction: column; justify-content: space-between;
  gap: clamp(16px, 2vw, 26px); flex: 1; min-width: 0;
  background: var(--surface);
  border: 1px solid var(--line);
  padding: clamp(18px, 2.2vw, 30px);
}
.course-panel .specs > div { padding: 11px 0; }
.course-panel .notice { margin-top: 0; }
.course-panel .notice strong { display: block; margin-bottom: 6px; }

/* the curriculum, as two readable columns rather than one long narrow ribbon */
/* It sits on the same surface as the duration panel above it, so the two blocks
   read as one system rather than as type floating on the page ground. */
.course-learn {
  background: var(--surface);
  border: 1px solid var(--line);
  padding: clamp(22px, 2.8vw, 44px);
}
.course-learn__title { margin-bottom: 22px; }
.course__outline--cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2px clamp(24px, 3.4vw, 56px);
  margin-bottom: 30px;
}

.faq { border-top: 1px solid var(--line); }
.faq details { border-bottom: 1px solid var(--line); }
.faq summary {
  cursor: pointer; list-style: none;
  padding: 22px 0;
  font-size: clamp(17px, 1.9vw, 24px);
  font-weight: 800; letter-spacing: -.02em;
  display: flex; justify-content: space-between; align-items: center; gap: 20px;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "+"; font-size: 26px; font-weight: 400; color: var(--accent);
  transition: transform .3s var(--ease-out);
}
.faq details[open] summary::after { transform: rotate(45deg); }
.faq p { padding: 0 0 24px; color: var(--body); max-width: 78ch; }

/* ============================================================== DOWNLOADS == */
.learner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(24px, 4vw, 64px);
  align-items: start;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: clamp(26px, 3.6vw, 56px);
}
.learner__main h3 { font-size: clamp(22px, 2.6vw, 36px); text-transform: uppercase; margin-bottom: 12px; }
.learner__main p { color: var(--body); margin-bottom: 24px; }
.learner__acts { display: flex; gap: 12px; flex-wrap: wrap; }
.learner__list { display: flex; flex-direction: column; }
.learner__list li {
  display: flex; justify-content: space-between; gap: 16px; align-items: baseline;
  padding: 13px 0; border-bottom: 1px solid var(--line);
  font-weight: 700; font-size: 18.5px;
}
.learner__list li small { font-size: 15px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .1em; }

/* ==================================================================== CTA ==
   The closing statement, with the script flourish signing it off. */
.band-cta { padding: clamp(48px, 5.2vw, 86px) 0; background: var(--ink); color: var(--page); }
.cta-card { display: flex; flex-direction: column; align-items: flex-start; gap: 22px; }
.cta-card .eyebrow { color: var(--accent-on-dark); }
.cta-card .eyebrow::before { background: var(--accent-on-dark); }
.band-cta__title { color: var(--page); }
.band-cta__title .a, .band-cta__title .b { display: block; }
.band-cta__title .b em, .cta-card em {
  font-family: var(--f-script);
  font-style: normal;
  text-transform: none;
  color: var(--accent-on-dark);
  font-weight: 400;
  letter-spacing: 0;
  font-size: 1.12em;
}
.cta-card p { color: rgba(244, 243, 239, .9); max-width: 58ch; font-size: 20px; }
.band-cta__acts { display: flex; gap: 12px; flex-wrap: wrap; }

/* ================================================================= FOOTER == */
.footer { background: transparent; padding: clamp(40px, 5vw, 68px) 0 28px; border-top: 1px solid var(--line); }
.footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 1fr));
  gap: clamp(24px, 3vw, 54px);
  padding-bottom: clamp(34px, 4vw, 60px);
}
.footer__wordmark {
  font-size: clamp(30px, 4vw, 54px); font-weight: 900;
  letter-spacing: -.04em; text-transform: uppercase; line-height: .9;
  margin-bottom: 12px;
}
.footer__wordmark em { font-style: normal; color: var(--accent); }
.footer__tag { color: var(--body); font-size: 18px; max-width: 40ch; margin-bottom: 22px; }
.footer h4 {
  font-size: 14.5px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 16px;
}
.footer__grid > div > a {
  display: block; padding: 6px 0; font-size: 18px; color: var(--ink-soft);
  transition: color .2s var(--ease), transform .2s var(--ease);
}
.footer__grid > div > a:hover { color: var(--accent); transform: translateX(3px); }
.newsletter { display: flex; gap: 8px; flex-wrap: wrap; max-width: 420px; }
.newsletter input {
  flex: 1 1 180px; min-height: 44px; padding: 0 14px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
}
.footer__bar {
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  border-top: 1px solid var(--line); padding-top: 20px;
}

/* ============================================================ SOUND IN PLACE == */
/* The clip is its own player: clicking it switches the sound on where it already
   sits, so nothing ever covers the page. The lit button is the whole feedback —
   it turns accent-filled and the label changes to "Mute sound". */
/* Keyed off aria-pressed, exactly like .band-sound, so both sound controls on the
   site read the same and there is one source of truth for the state. */
.card-work__play[aria-pressed="true"] { background: var(--accent); color: #fff; }
/* the play triangle becomes the same stop square the band control uses */
.card-work__play[aria-pressed="true"] i {
  width: 9px; height: 9px; border-radius: 0; background: currentColor;
}
.card-work__play[aria-pressed="true"] i::before { content: none; }

/* ================================================================== FORMS == */
.form { display: flex; flex-direction: column; gap: 18px; }
/* min-width: 0 on the field AND on the controls inside it. A <select> will not shrink
   below its longest option unless it is told to, and a grid item's default min-width
   is auto — so one long option name ("AI Film Foundations — Live cohort ...") pushed
   the whole sign-up form 170px past the right edge of a phone. */
.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }
.field--row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.field--row > * { min-width: 0; }
.field label { font-size: 14.5px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.field input, .field select, .field textarea {
  width: 100%; min-width: 0; max-width: 100%;
  min-height: 50px; padding: 12px 14px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  font-size: 18.5px;
}
.field textarea { min-height: 140px; resize: vertical; }
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--ink); }
.field select.is-flash { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

.flashbar { background: var(--accent); color: #fff; padding: 14px 0; }
.flashbar p { font-weight: 700; }

/* ============================================================ INNER PAGES == */
.page-hero { padding: clamp(36px, 4.6vw, 72px) 0 clamp(26px, 3.4vw, 46px); border-bottom: 1px solid var(--line); }
.page-hero__title {
  font-size: clamp(30px, 5vw, 72px);
  font-weight: 900; line-height: .94; letter-spacing: var(--track);
  text-transform: uppercase;
}
.page-hero__sub { margin-top: 20px; font-size: clamp(18px, 1.5vw, 22px); color: var(--body); max-width: 58ch; }

.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: clamp(24px, 4vw, 64px); align-items: start; }

/* ---- about: the animated title card, the timeline, the reach table ----
   The card takes its ground from the tokens, so it is a light panel on the light
   site and a dark one on the About page without a second rule. That matters most
   for her portrait: a cut-out shot against a dark ground needs a dark ground to
   sit on, and pasting it onto white left it looking cut out. */
.title-card {
  position: relative;
  background: linear-gradient(168deg, var(--surface) 0%, var(--paper) 56%, var(--page) 100%);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  /* The top air is now the side air. The card used to open with 135px of dead black
     before the first line on a 1280px window — 51px of card padding, then 30px of
     grid margin, 26px of grid padding and 27px of centring slack — against only 51px
     down each side. The top padding is cut and the grid's own top spacing with it, so
     the card opens the way it is framed. */
  padding: clamp(22px, 2.6vw, 38px) clamp(26px, 4vw, 60px) clamp(26px, 4vw, 60px);
  overflow: hidden;
}
.title-card__words { margin-bottom: 26px; }
.title-card__pre { font-size: 14.5px; font-weight: 800; letter-spacing: .18em; text-transform: uppercase; color: var(--accent); margin-bottom: 12px; }
.title-card__name {
  font-size: clamp(28px, 4.6vw, 64px); font-weight: 900; line-height: .94;
  letter-spacing: var(--track); text-transform: uppercase;
}
/* The name is already built out of one element per letter, so the letters can be
   moved individually. Hovering the name ripples it: each letter lifts, the next a
   beat after the last, because every letter carries its own --i from the template. */
.title-card__name .ltr {
  display: inline-block;
  transition: transform .38s var(--ease-out), color .38s var(--ease-out);
}
.title-card__name:hover .ltr {
  transform: translateY(-.07em);
  color: var(--accent);
  transition-delay: calc(var(--i, 0) * 45ms);
}
.title-card__roles { position: relative; min-height: 1.5em; margin-top: 14px; }
.title-card__roles [data-role] {
  position: absolute; inset: 0; opacity: 0;
  font-size: clamp(15px, 1.5vw, 20px); font-weight: 800;
  letter-spacing: .04em; text-transform: uppercase; color: var(--accent);
  transition: opacity .6s var(--ease);
}
.title-card__roles [data-role].is-on { opacity: 1; }
/* The words start at the top of the row instead of being centred against the portrait.
   Centring was the fix for a column 814px tall — the words were top-aligned in it and
   everything under the buttons was followed by ~600px of empty black. The portrait is
   capped now, so the two are within 80px of each other and the centring only bought
   slack above the name that read as a gap. */
.title-card__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 22px; margin-top: 0; padding-top: 18px; border-top: 1px solid var(--line); align-items: start; }
.title-card__official, .title-card__lead, .title-card__meta { font-size: 18.5px; color: var(--body); }
/* The credits line sits directly under the buttons with no rule and no space of its own
   — the two ran together. It is a footnote to the card, so it gets a hairline and air. */
.title-card__meta { margin-top: 34px; padding-top: 22px; border-top: 1px solid var(--line); }
.title-card__rule { border-top: 1px solid var(--line); margin: 24px 0; }
/* The toolkit is a tape that RUNS, not a wall of chips that sits there. The list is
   written twice in the markup, so translating the track by half its width loops it
   with no seam. */
.title-card__tape { overflow: hidden; margin-top: 22px; }
.title-card__tape .tape {
  display: flex; gap: 10px; flex-wrap: nowrap; width: max-content;
  margin-top: 0;
  animation: tape-run 38s linear infinite;
}
@keyframes tape-run { to { transform: translateX(-50%); } }
.title-card__tape span, .tape span {
  font-size: 14px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  padding: 6px 13px; color: var(--body); white-space: nowrap;
}
.title-card__acts { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 30px; }
/* The portrait is a cut-out cropped to the hip (543x600). Uncapped it made the card 1042px
   tall — taller than the viewport it opens in, so the bottom of the shot was always below
   the fold. The cap is a share of the viewport, so the shot is the same size on screen as
   it was at 543x744: 0.82 of the old cap, which is exactly the share the picture lost. */
.title-card__figure {
  display: block; border-radius: var(--r-lg); overflow: hidden;
  max-height: min(58vh, 580px);
  /* the portrait leans a degree or two toward the cursor — see site.js */
  transform: perspective(1200px) rotateX(var(--card-tilt-x, 0deg)) rotateY(var(--card-tilt-y, 0deg));
  transition: transform .25s var(--ease-out);
  will-change: transform;
}
.title-card__figure img { width: 100%; height: 100%; max-height: min(58vh, 580px); object-fit: contain; object-position: center bottom; }
/* The cut-out stops mid-shin, so the card was drawing a straight slice across her trousers
   with a band of dark under it — which reads as amputation, not as a frame. There is no
   fuller shot of her in the tree (the candid one is a headshot, the cover a title frame),
   so the picture is cropped to the hip (543x600) and the last sliver of leg is dissolved:
   the crop lands on the grey trousers, and a short mask under it means the shot ends in the
   card's own ground instead of on a flat line. A share of the height, not a pixel count, so
   it holds at every size; -webkit- is set alongside for Safari. */
.title-card__figure img {
  -webkit-mask-image: linear-gradient(to bottom, #000 84%, rgba(0, 0, 0, .85) 92%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 84%, rgba(0, 0, 0, .85) 92%, transparent 100%);
}
.title-card__full { width: 100%; }

/* The profile is the one page on the site that is mostly prose, so it is set for
   reading rather than for display. Two things were wrong: the body sat in the NARROW
   column (1fr against the aside's 1.7fr) so three paragraphs were squeezed into 448px
   and ran 8–10 lines each, and the facts opposite them took the wide column. The body
   takes the wide column now, capped at a 62-character measure — past roughly 70 the
   eye loses the start of the next line — and the facts sit in the narrow one. */
.profile-grid { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); gap: clamp(28px, 4vw, 72px); align-items: start; }
.profile-aside { position: sticky; top: calc(var(--nav-h) + 22px); }
.profile-body > * + * { margin-top: 22px; }
.profile-body h3 { font-size: clamp(21px, 2.4vw, 32px); text-transform: uppercase; margin-top: 34px; }
.profile-body p { color: var(--body); font-size: 19px; line-height: 1.75; max-width: 62ch; }

.timeline { display: flex; flex-direction: column; gap: 0; border-top: 1px solid var(--line); }
.timeline__top {
  /* The title takes the room and the dates hug their own width. This used to be
     minmax(0, 1fr) minmax(0, 2fr), which gave the TITLE the narrow column — 105px
     on a phone, so "Advanced Certificate in Learning and Performance (ACLP)" came
     out one word per line, and 195px on a desktop, where it still wrapped. */
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 20px; padding: 22px 0; border-bottom: 1px solid var(--line);
}
.timeline__years { font-size: 16px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--accent); }
/* A row answers the cursor: the rule warms behind it and the title steps in. Nothing
   moves far enough to shift the layout. */
.timeline li { transition: background .3s var(--ease-out); }
.timeline li:hover { background: linear-gradient(90deg, var(--accent-soft), transparent 68%); }
.timeline li h3 { transition: transform .3s var(--ease-out); }
.timeline li:hover h3 { transform: translateX(6px); }
/* The rows arrive one after another rather than all at once. */
.timeline li:nth-child(2) { transition-delay: .06s; }
.timeline li:nth-child(3) { transition-delay: .12s; }
.timeline li:nth-child(4) { transition-delay: .18s; }
.timeline li:nth-child(5) { transition-delay: .24s; }
.timeline__role { font-size: clamp(18px, 2vw, 26px); font-weight: 800; letter-spacing: -.02em; text-transform: uppercase; }
.timeline__note { font-size: 18.5px; line-height: 1.6; color: var(--body); margin-top: 6px; }

/* The head is a heading, not a table cell: it sits above the grid instead of inside
   it. It used to be a grid cell, which left the <ul> occupying one 180px column
   while the remaining six columns showed nothing but the hairline colour — a large
   empty panel beside a list squeezed into a sixth of the width. With the head out of
   the grid there are exactly as many columns as there are links. */
.reach { display: block; background: none; border: 0; }
.reach__head { margin-bottom: clamp(20px, 3vw, 34px); }
.reach__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  grid-template-rows: auto auto 1fr;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
}
/* Each cell is read as a little table: label, value, note. It used to be its own
   flex column, so "TikTok — training" wrapped to two lines and pushed ITS value 24px
   below everyone else's — the five values did not sit on one line. The cells share
   the row's three tracks now, so every label, value and note lines up whatever a
   label does. The parent's 1px row gutter is inherited (a subgrid cannot change it),
   so the 7px rhythm between the parts is carried by the margins below. */
.reach__list > li {
  display: grid; grid-template-columns: minmax(0, 1fr);
  grid-template-rows: subgrid; grid-row: span 3;
  gap: 7px;
  background: var(--page);
  padding: 20px 22px;
}
.reach__label { font-size: 14.5px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); margin-bottom: 6px; }
/* The link is a value too: it used to keep the plain 20px body size while the spans
   beside it took the clamp, and with no wrap allowed its address was cut off by the
   cell edge. */
.reach__value, .reach__list a {
  font-size: clamp(18px, 1.7vw, 24px); font-weight: 900; letter-spacing: -.03em;
  /* a long address breaks rather than pushing the cell wider than its column */
  overflow-wrap: anywhere;
  margin-bottom: 6px;
}
.reach__note { font-size: 16px; color: var(--muted); }

/* The teaching list is a plain 3-up grid with real breathing room. Its label was
   written as h4 in the sheet while the markup emits h3, so it rendered unstyled; the
   rule now matches the tag. The rows were 15px against a 17.5px body — smaller than
   the prose they belong to. */
.trains { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(22px, 3vw, 44px); }
.trains__group { border-top: 1px solid var(--line); padding-top: 20px; margin-top: 20px; }
.trains__group h3, .trains__group h4 { font-size: 14.5px; letter-spacing: .16em; text-transform: uppercase; color: var(--muted); margin-bottom: 12px; }
.trains__group li { padding: 8px 0; font-size: 19px; line-height: 1.55; color: var(--body); }
/* Without a rule the teaching list read as loose lines rather than a list of things. */
.trains__group li + li { border-top: 1px solid var(--line-2); }

/* The consulting card is two columns, not one. The points ran the full 1268px of the
   card — lines of ~160 characters, which is not a paragraph, it is a line of code. The
   head takes the left, the numbered points the right, and both stay readable. */
.consult { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); gap: clamp(26px, 4vw, 64px); align-items: start;
           background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: clamp(24px, 3vw, 44px); }
.consult__head { margin-bottom: 16px; }
.consult__points { display: flex; flex-direction: column; gap: 16px; margin-top: 16px; }
.consult__points li { display: flex; gap: 12px; align-items: baseline; font-size: 19px; line-height: 1.6; color: var(--body); }
.consult__points li p { max-width: 58ch; }
.consult__points li::before { content: "→"; color: var(--accent); font-weight: 800; }

/* The markup emits <span class="n"> and <span class="label">, but the sheet was styling
   dt/dd — so neither rule applied and the value and its caption ran together inline
   ("Reply in 2 daysUsually sooner"). Match the markup. */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 22px; }
.stats > div { display: flex; flex-direction: column; gap: 7px; }
.stats > div .n, .stats > div dd { font-size: clamp(21px, 2.2vw, 30px); font-weight: 900; letter-spacing: -.03em; line-height: 1.05; }
.stats > div .label, .stats > div dt { font-size: 14.5px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
/* Stacked, not a label beside its value. Two of these values run to 147 and 110
   characters; in a justify-content: space-between row inside a 448px column they were
   compressed into a dense block on the right, and the label competed with them. Label
   above value reads as a definition, which is what it is. */
.facts { display: flex; flex-direction: column; gap: 0; }
.facts > div { display: flex; flex-direction: column; gap: 7px; padding: 15px 0; border-bottom: 1px solid var(--line); }
.facts dd { font-size: 19px; line-height: 1.6; }
/* Skills and AI read as a set of separate items, so each one is its own
   round-cornered chip — the shape of a button, but a label: no link, no cursor
   change and no hover state. The chips wrap, so a list this long stays inside
   the narrow aside column instead of running past its edge. */
.facts dd.facts__chips {
  display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 0;
  font-size: 16px; line-height: 1.25;
}
.facts__chips .chip {
  display: inline-block;
  padding: 7px 15px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--body);
  font-size: 16px; font-weight: 700; letter-spacing: -.01em;
  cursor: default;
}

/* ---- hire: the sample clip before the enquiry form ---- */
.sample { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: clamp(26px, 4vw, 60px); align-items: center; }
.sample__media {
  position: relative; border-radius: var(--r-lg); overflow: hidden;
  background: var(--paper); aspect-ratio: 864 / 496;
}
.sample__media img, .sample__media video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* The clip runs in place, so it is visible without a hover. It used to be opacity 0
   until :hover — which on a touch screen meant it never appeared at all, and on a
   desktop meant it sat as a still until you happened to pass over it. The still
   underneath is the fallback: it shows before the first frame paints, and it is what
   a reduced-motion visitor sees instead. */
.sample__media video { opacity: 1; }
.sample__words h2 { margin: 12px 0 16px; }
.sample__words p { font-size: 19px; line-height: 1.7; color: var(--body); max-width: 46ch; }

/* ---- resources ---- */
.dl { display: flex; flex-direction: column; border-top: 1px solid var(--line); }
.dl__row {
  display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) auto;
  gap: 18px; align-items: center;
  padding: 20px 0; border-bottom: 1px solid var(--line);
}
.dl__body h3 { font-size: clamp(17px, 1.8vw, 23px); margin-bottom: 4px; }
.dl__body p { font-size: 17.5px; color: var(--body); }
.dl__cat { font-size: 14px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; color: var(--accent); }
.grid-pdfs { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: clamp(14px, 1.8vw, 24px); }
.pdf {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 24px; display: flex; flex-direction: column; gap: 12px;
}
.pdf__icon { width: 34px; height: 42px; border: 2px solid var(--ink); border-radius: 2px; position: relative; }
.pdf__icon::after { content: ""; position: absolute; top: -2px; right: -2px; border: 9px solid var(--page); border-left-color: transparent; border-bottom-color: transparent; }
.promptblock {
  background: var(--ink); color: #E9E7E3; border-radius: var(--r-lg);
  padding: 20px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 16px; line-height: 1.6; white-space: pre-wrap; overflow-x: auto;
}

/* ---- admin ---- */
.admin-list { display: flex; flex-direction: column; border-top: 1px solid var(--line); }
.admin-list__a {
  display: grid; grid-template-columns: 120px minmax(0, 1fr) auto;
  gap: 16px; align-items: center; padding: 14px 0; border-bottom: 1px solid var(--line);
}
.admin-list__t { font-weight: 700; }
.mini { font-size: 14.5px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; border: 2px solid var(--ink); border-radius: var(--r-pill); padding: 6px 13px; }
.mini--danger { border-color: var(--accent); color: var(--accent); }
.upload { border: 2px dashed var(--line-strong); border-radius: var(--r-lg); padding: 26px; display: flex; flex-direction: column; gap: 16px; }
.upload__drop { text-align: center; padding: 26px; border-radius: var(--r-md); background: var(--paper); }
.upload__fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; }
.showcase-admin { display: grid; gap: 26px; }

/* ============================================================ RESPONSIVE == */
@media (max-width: 1080px) {
  .hero__grid { grid-template-columns: 1fr; align-items: start; }
  .hero__aside { padding-bottom: 0; }
  .footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .profile-grid { grid-template-columns: 1fr; }
  .profile-aside { position: static; }
}
@media (max-width: 900px) {
  .section-head, .range__head { grid-template-columns: 1fr; align-items: start; }
  .learner { grid-template-columns: 1fr; }
  .course { grid-template-columns: 1fr; }
  .course-top { grid-template-columns: 1fr; }
  .course__outline--cols { grid-template-columns: 1fr; }
  .course-detail { grid-template-columns: 1fr !important; }
  .wa-band { flex-direction: column; align-items: flex-start; }
  .wa-band__btn { width: 100%; display: flex; justify-content: center; }
  .split { grid-template-columns: 1fr; }
  /* The teaching list and the consulting card are grids now, so they have to be told
     to stack — a three-column list on a phone is three narrow columns of wrapping. */
  .trains { grid-template-columns: 1fr; gap: 0; }
  .consult { grid-template-columns: 1fr; }
  .sample { grid-template-columns: 1fr; }
  .nav { display: none; }
  .burger { display: inline-flex; }
  /* The wordmark's role line has nowhere to go on a phone and was being cut to
     "AI TRA…". The three roles are stated in full in the hero below it, so the
     header shows the name alone rather than half a word. */
  .wordmark__role { display: none; }
  .topbar__cta .btn { display: none; }
  /* the deck becomes a plain stack: a pinned scroll-jack on a phone is a trap,
     and the cards read perfectly well as a list */
  .stack { min-height: 0; }
  .stack__stage { position: static; height: auto; min-height: 0; padding: 0 var(--gut) 10px; }
  .stack__deck { aspect-ratio: auto; width: 100%; display: grid; gap: 14px; }
  .stack__deck .card-service {
    position: static; inset: auto;
    transform: none; opacity: 1; box-shadow: none;
  }
  .stack__deck .card-service__n { position: static; }
  .stack__tag { display: none; }
}
@media (max-width: 640px) {
  body { font-size: 19px; }
  .footer__grid { grid-template-columns: 1fr; }
  .field--row { grid-template-columns: 1fr; }
  .dl__row { grid-template-columns: 1fr; gap: 10px; }
  .hero__foot { flex-direction: column; align-items: flex-start; }
  .hero__badge { margin-left: 0; }
  .title-card__grid { grid-template-columns: 1fr; }
  .learner__acts .btn, .hero__actions .btn, .band-cta__acts .btn { width: 100%; }
  .compare__cap { font-size: 14.5px; }
  /* One row of proportional tracks is unusable on a phone — a three-clip rail
     would hand each card a third of 390px. Pair them up, and let a wide clip
     take the full width. */
  .rail, .rail--wrap { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .rail > .card-work[data-shape="wide"] { grid-column: 1 / -1; }
  /* Half-width columns push the longest meta to three lines, which knocks the
     titles across a row out of line. Tighten it so it still fits the two lines
     the card reserves. */
  .card-work .card-work__meta { font-size: 13.5px; letter-spacing: .11em; }
  /* Certificates and employment: the dates go ABOVE the title. In the two-column
     grid the title was left 105px on a phone, so "Advanced Certificate in
     Learning and Performance (ACLP)" came out one word per line. */
  .timeline__top { display: flex; flex-direction: column-reverse; gap: 7px; }
  .timeline__note { margin-top: 10px; }
  /* The label/value rows stack too: side by side at 390px the label steals the
     room the value needs, and the value is the part being read. */
  .facts > div { flex-direction: column; gap: 5px; }
  .reach > * { padding: 16px 0; }
}

/* ======================================================= REDUCED MOTION ==== */
@media (prefers-reduced-motion: reduce) {
  .hero__title { transform: none; }
  .reveal { opacity: 1; transform: none; }
  .split-words .w { overflow: visible; }
  .split-words .w i { transform: none; transition: none; }
  .readbar { display: none; }
  .card-work__media { clip-path: none; }
  .card-work__media img, .card-work__media video { transform: none; }
  .marquee__track { animation: none; }
  .totop { transition: none; }
  /* the toolkit tape and the portrait's lean both stop; the card is simply still */
  .title-card__tape .tape { animation: none; }
  .title-card__figure { transform: none; }
  .timeline li, .timeline li h3 { transition: none; }
  /* the band's film never autoplays under reduced motion — see site.js. The poster
     frame stands in, so the banner still shows the words. */
  .hero__loop { display: none; }
  /* the hire page's sample clip is the same deal: the still stands in, and the clip
     is taken out of the render so it cannot loop silently behind it */
  .sample__video { display: none; }
  .stack { min-height: 0; }
  .stack__stage { position: static; height: auto; min-height: 0; }
  .stack__deck { aspect-ratio: auto; display: grid; gap: 14px; }
  .stack__deck .card-service { position: static; inset: auto; transform: none !important; opacity: 1 !important; box-shadow: none; }
  .stack__deck .card-service__n { position: static; }
}

/* ============================================================== PRINTING == */
@media print {
  .topbar, .drawer, .marquee, .footer, .skip, .band-cta { display: none !important; }
  body { background: #fff; }
}

/* ============================================================== THE PORTAL ==
   The opening move on the home page, and the only thing on the site that asks
   the visitor to scroll before it shows them anything else.

   The composition is the owner's own portrait: a mustard-yellow disc, her
   profile in front of it, everything else empty off-white. On scroll the disc
   becomes a doorway — it splits down the centre, two leaves swing apart with
   real perspective, the portrait separates from the ground and drifts past the
   viewer, and the camera pushes through the opening until the frame is empty
   and the site's own ground is what is left. The banner then ends. It does NOT
   hand over to the film band below it: the visitor keeps scrolling and the
   film arrives the way any other section does.

   GEOMETRY IS THE OWNER'S IMAGE, MEASURED. The source visual is 1672x940, the
   disc is 749px across with its centre at (869.5, 445.5), and the portrait's
   silhouette occupies x 659-1171, y 140-940. Everything below is those numbers
   as percentages of a 1672:940 scene box, so the poster is the original frame
   at every window size and the portrait is never stretched.

   THE DISC IS PAINTED, NOT CROPPED, and that is the whole trick. Two leaves,
   each half the disc's bounding square, each carrying a `--page`-coloured
   rectangle with a half-disc drawn on its inner edge. Slide a leaf and its
   half-disc rides out with it — and the off-white half of the leaf, which is
   exactly the colour of the ground behind it, moves invisibly. That gives a
   door that opens without a single pixel of the original PNG, which is why it
   can scale to any size and split cleanly down the centre.

   The ground is four off-white panels around the disc's bounding square, so
   the space the doors vacate is a hole, and through the hole is the real page.

   Only transform and opacity are ever written (the site's rule everywhere):
   the camera is a scale on the scene, the leaves are translate/rotate, and the
   layers dissolve by opacity. Nothing here repaints.

   Traps, all of them paid for once:
   * The stage must NOT have a `background`. It is the off-white ground panels
     that paint the poster, and a background on the stage would seal the hole
     the entire effect depends on.
   * The ground panels extend far past the disc. Sized to the disc alone they
     stop covering the window on a wide screen and the poster leaks the page
     behind it at the edges.
   * `z-index: 80` — above the film band's sound button (70) so nothing behind
     the poster is clickable, below the topbar (100) so the nav stays usable.
   * An odd frame shape is handled by the scene box: it keeps the 1672:940
     proportion and simply gets smaller, so the composition is never cropped.
     Phones get their own framing (see the 760px query) because a 16:9 frame on
     a 390px-wide screen would leave the portrait postage-stamp small. */
.portal {
  /* Zero height on purpose: the stage is taken out of the flow by the pin, and
     the runway below is the only thing that takes up scroll. The film band
     therefore sits exactly at the pin's end, not a stage-height past it.

     A zero-height box takes none of its children's height with it, which is why
     the runway is a SIBLING of this section and not inside it. */
  position: relative;
  height: 0;
}
/* The runway: empty, and the only part of the opening that takes up scroll.
   Height and the offset that cancels the sticky topbar both come from portal.js,
   so a visitor with no JavaScript gets no runway at all. */
.portal__runway { height: 0; }
.portal__stage {
  /* A drag that starts anywhere on the hero must always scroll the page vertically. Nothing in
     this block sets `touch-action`, but the stage sits over the whole viewport and the way-in
     button inside it covers it exactly, so the vertical pan is stated outright rather than left to
     the default: a visitor who drags down on the opening is scrolling, and must never be held. */
  touch-action: pan-y;
  /* FIXED, always. The poster must never scroll: while the visitor is inside the
     runway below, the scroll drives the animation and the picture stays put. This
     was previously handed to GSAP's pinning, which is the usual way and the way
     the brief asks for — but a pin is a thing that can fail to take (a late
     layout, a restored scroll position, a stale cached script) and when it fails
     the poster simply scrolls away and the visitor never sees the opening at all.
     A fixed stage cannot fail: it is the browser's own layout.

     portal.js hides it the moment the visitor is past the runway, so it does not
     sit over the site afterwards, and shows it again on the way back up.

     `top: 0` is the window's top edge, and it is painted under the topbar
     (z-index 50, the bar is 60) so the nav stays visible and usable over it. */
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  z-index: 50;
  overflow: hidden;             /* the ground panels and the draped portrait end here */
  display: grid;
  place-items: center;
  /* deliberately no background — see the note at the top of this block */
}
.portal__scene {
  position: relative;
  aspect-ratio: 1672 / 940;
  width: min(100%, calc(100vh * 1672 / 940));
  transform-origin: 52% 47.4%;  /* the disc's centre: the camera pushes into it */
  will-change: transform;
}

/* -- the disc's bounding square. Everything that must stay locked to the disc
      lives inside it, in percentages of a square, so the layout needs no unit
      juggling between the two axes. */
/* The colourful cloud behind the doors — the thing the camera flies through between the yellow
   circle opening and the words arriving (the owner: "zoom through to see the colourful cloud,
   than zoom through into the text animation"). It sits under the leaves in the DOM, so the shut
   disc hides it completely, and it is a little larger than the hole so the push never brings an
   edge of it into frame. Drawn by scripts/make-cloud.py, no generation credits. */
.portal__cloud {
  position: absolute;
  inset: -6%;
  width: 112%;
  height: 112%;
  object-fit: cover;
  display: block;
}

.portal__hole {
  position: absolute;
  left: 52%;
  top: 47.39%;
  width: 44.79%;                /* 749 / 1672 */
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
}
/* The disc's own group: the grounds, the leaves and the portrait, scaled as one
   so the disc and the edge of the ground stay welded together. Perspective is
   here rather than on the square above, so a leaf's translateZ reads as depth in
   the door and not as the whole poster tipping. */
.portal__zoomer {
  position: absolute;
  inset: 0;
  perspective: 1400px;
  perspective-origin: 50% 50%;
  transform-origin: 50% 50%;
  will-change: transform;
}

/* -- ground: four panels around the square, the hole the doors vacate -------- */
/* Sized to reach past the window on every ordinary shape, and no further. These
   are the biggest surfaces in the whole sequence, four of them are composited at
   once, and on a phone an oversized panel is the first thing that turns a smooth
   scroll into a stuttering one. `overflow: hidden` on the stage clips whatever
   hangs over, so the only requirement is that they still cover the window at the
   camera's largest scale — and the camera scales them FURTHER OUT, never back in. */
.portal__ground {
  position: absolute;
  background: var(--page);
  will-change: opacity;
}
.portal__ground--l { top: -80%; bottom: -80%; right: 100%; width: 130%; }
.portal__ground--r { top: -80%; bottom: -80%; left: 100%; width: 130%; }
.portal__ground--t { left: 0; right: 0; bottom: 100%; height: 90%; }
.portal__ground--b { left: 0; right: 0; top: 100%; height: 90%; }

/* -- the two leaves --------------------------------------------------------- */
.portal__door {
  position: absolute;
  top: 0;
  height: 100%;
  width: 50%;
  background: var(--page);      /* the corners of the square, off-white on off-white */
  overflow: hidden;
  will-change: transform, opacity;
  backface-visibility: hidden;
}
.portal__door--l { left: 0;  transform-origin: left center; }
.portal__door--r { right: 0; transform-origin: right center; }
/* The disc, at full diameter inside a half-width leaf, CENTRED ON THE LEAF'S
   INNER EDGE — so the half that shows is the outer half of the circle and the
   straight edge of it is the seam down the middle of the disc. Anchored the other
   way round (which is what the first build did) each leaf shows the *inner* half
   and the two leaves come out as a bow tie. */
.portal__disc {
  position: absolute;
  top: 0;
  width: 200%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--portal-gold);
}
.portal__door--l .portal__disc { left: 0; }
.portal__door--r .portal__disc { right: 0; }
/* The leaf's own face shading and the hairline down its leading edge are gone with
   the door swing: they existed to give the opening leaves depth, and nothing opens
   now. The disc above is the whole of it. */

/* -- the portrait, on its own layer above the doors ------------------------- */
/* The silhouette's position in the owner's visual is left 659px, top 140px of a
   1672x940 frame — offsets of -29.31% and -42.12% of the disc's diameter from
   the DISC'S CENTRE. This box is the disc's square, whose top-left corner is
   half a diameter up and left of that centre, so both numbers carry +50%: the
   portrait keeps its exact relationship to the disc at every window size, and
   never stretches (width is set, height follows the file). */
.portal__portrait {
  position: absolute;
  left: 20.69%;
  top: 7.88%;
  width: 70.76%;
  height: auto;
  z-index: 3;
  will-change: transform, opacity;
  pointer-events: none;
  user-select: none;
}

/* ============ THE DOOR IN THE CLOUDS, AND THE WAY IN ========================
   Two layers and a control, all three new with the owner's second half of the
   opening: the picture the poster becomes, the light behind that picture's door,
   and the button that starts the move.

   The drawing is fitted with `object-fit: cover`, so it fills the stage whatever
   its shape — 16:9 on a laptop, 9:16 on a phone, one file each. Cover means the
   picture is cropped to the frame, and the door is therefore NOT at a fixed
   place in the element: portal.js works out where the door has landed from the
   drawing's real pixel dimensions before it aims the camera at it. Nothing here
   tries to guess it.

   `transform-origin` below is only the resting value; the script overwrites it
   with the door's own centre as soon as it has measured the picture. */
.portal__dream {
  position: absolute;
  inset: 0;
  opacity: 0;                   /* the poster is what the visitor starts on */
  background: #fff;             /* the drawing's own ground, not the page's off-white */
  overflow: hidden;
  transform-origin: 50% 48%;    /* the disc's centre: the clouds bloom out of it */
  will-change: transform, opacity;
}
.portal__dream-art {
  position: absolute;
  inset: 0;
  margin: 0;
  display: block;
  transform-origin: 61% 48%;    /* rewritten by portal.js to the door's real centre */
  will-change: transform;
}
.portal__dream-art > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  display: block;
  /* No `user-select`, no drag: the poster is a picture, and a visitor who drags
     across it is scrolling the page, not taking the drawing away. */
  -webkit-user-drag: none;
  user-select: none;
  pointer-events: none;
}
/* The light inside the doorway. Drawn rather than lifted from the picture: it has
   to be able to fill the whole frame at the end of the push, and it has to stay
   sharp when it does — a scaled-up bitmap goes soft exactly when the visitor is
   looking most closely. It is white at the core and warms as it opens out, which
   is the sunset in the doorway rather than a lighthouse. */
.portal__light {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  /* The WASH: a warm white that can cover the frame on its own. The glow used to
     live here too, but the glow and the wash want different treatment — the wash
     comes up evenly while the glow has to come out of the doorway — so the glow is
     the child below and this layer is only what settles over everything. That is
     what makes the change from drawing to film happen inside the light instead of
     on top of the drawing's corners. */
  background: rgba(255, 249, 238, .88);
  will-change: opacity;
}
/* The BLOOM: the light coming out of the doorway. It starts the size of the
   doorway's opening (BEAT 05 scales it up from roughly 1) and swells to several
   times the frame, so the light arrives from the opening the visitor is stepping
   into rather than appearing everywhere at once. Centred, because the push has
   already brought the doorway's centre to the middle of the frame by the time it
   appears — see `aim` in portal.js. */
.portal__light-bloom {
  position: absolute;
  inset: 0;
  display: block;
  background: radial-gradient(circle at 50% 50%,
    #ffffff 0%,
    #fff9ec 26%,
    rgba(255, 241, 217, .9) 46%,
    rgba(255, 231, 200, .5) 66%,
    rgba(255, 226, 200, .14) 84%,
    rgba(255, 255, 255, 0) 100%);
  transform: scale(1);
  transform-origin: 50% 50%;
  will-change: transform;
}
/* ------------------------------------------------------------------ the way in
   The whole banner is the control. Invisible: the owner asked for the "scroll
   down" and its arrow to come off the poster, so there is deliberately not one
   word of instruction left on it — but a picture you are meant to step into has
   to be pressable, and this is what a pointer, a thumb and a keyboard all land
   on. `cursor: pointer` is the one visual sign it exists.

   It is fixed over the stage (z-index 50) and under the topbar (60), so the
   navigation is still clickable above it, and it takes down with the stage once
   the visitor is through — portal.js does that, so an invisible full-screen
   button can never be left sitting over the site. */
.portal__open {
  position: fixed;
  inset: 0;
  z-index: 55;
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: transparent;
  cursor: pointer;
}
/* Nothing to see until it has focus, and then a ring at the frame's own edge:
   the poster fills the window, so anything smaller would be a box drawn on her
   face. Visible only for a keyboard, and only while it is the focus. */
.portal__open:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: -6px;
}
.portal__open[aria-hidden="true"],
.portal__open.is-gone { display: none; }

/* -- phones: the frame is re-cut, never stretched --------------------------- */
/* A 16:9 frame on a 390px-wide screen leaves the disc smaller than the wordmark.
   The square keeps its shape and the portrait keeps its offsets (both are
   percentages of the disc), so this is a different crop of the same picture, not
   a re-layout of it. */
/* ============================================= THE STICKERS AROUND THE DISC
   Six drawn shapes (inline SVG in the template) that fly in once, settle behind
   her, and scatter when the visitor opens the door. The layer sits at z-index 2,
   between the disc and the portrait (3) — so a sticker may cross the disc's rim
   and can never cross her. Positions are percentages of the disc's square, and
   that is what makes the set scale with her: the same picture on a 328px phone
   disc and a 537px laptop disc.

   Motion is opt-in from the script side. With no GSAP, or with reduced motion,
   `is-armed` is never added, so the stickers simply sit where they are drawn —
   the same degradation the rest of the opening has. The resting tilt is `--rot`,
   which portal.js reads so the flight lands on the drawn angle rather than a
   second guess at it.

   ON THE POSTER: the ?stickers=1 gate is gone, at the owner's request, so this is
   live for every visitor. The disc-turning layer above keeps its own ?turn=1 gate. */
.portal__stickers {
  position: absolute; inset: 0;                 /* the shorthand, for engines that have it */
  top: 0; right: 0; bottom: 0; left: 0;         /* …and the longhands older ones actually read */
  z-index: 2; pointer-events: none;
}

.portal__st {
  position: absolute;
  display: block;
  width: var(--w, 18%);
  max-width: 104px;             /* the laptop disc would otherwise inflate them */
  /* SQUARE WITHOUT `aspect-ratio`. The shorthand is the neatest way to say this, but it is
     among the newest properties used here, and a badge whose height comes from an
     unsupported `aspect-ratio` has NO height at all: it is not drawn, with nothing in the
     markup or the console to show why. That is invisible from the server side and arrives
     as "the icons don't show on my phone". The containing block is the disc's own square,
     so a percentage height is the same number as the percentage width, and `aspect-ratio`
     stays behind it as the expression of intent for engines that do support it. */
  height: var(--w, 18%);
  max-height: 104px;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);   /* left/top are the sticker's CENTRE */
}
.portal__st-in {
  display: block;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  padding: 13%;
  overflow: hidden;
  transform-origin: 50% 50%;
  background: #fffaf0;                    /* yellow art never disappears on yellow */
  /* Plain declaration first, `clamp()` after: an engine that does not know `clamp()`
     drops that declaration whole, and a dropped border would take the badge's dark edge
     — the thing keeping yellow art off the yellow disc — with it. */
  border: 2px solid var(--ink);
  border-width: clamp(1.5px, .22vw, 2.5px);
  border-radius: 30%;
  box-shadow: 0 5px 0 rgba(15, 15, 15, .12), 0 10px 20px rgba(15, 15, 15, .13);
}
.portal__st:nth-child(3n) .portal__st-in { border-radius: 50%; }
.portal__st:nth-child(3n + 1) .portal__st-in { border-radius: 42% 58% 46% 54%; }
/* The resting tilt, in `transform` rather than the newer individual `rotate:` property.
   `transform` is what every engine has, and it is also the property GSAP writes — so the
   moment a badge is armed its inline transform replaces this rule, with no chance of the
   two stacking into a double tilt. One property, one behaviour, on every browser. */
.portal__stickers:not(.is-armed) .portal__st-in { transform: rotate(var(--rot, 0deg)); }
.portal__st svg { display: block; width: 100%; height: 100%; overflow: visible; }

/* ---------------------------------------------- the disc, reading as turning
   One flat colour has no angle to show, so the disc cannot simply be rotated: a
   plain circle looks identical at every degree. This is what makes it READ as a
   surface turning — a warm sweep of light travelling round, plus a very faint
   brushed texture, both on a ring clipped to the circle. It sits on the sticker
   plane (z-index 2), so it crosses the yellow and never her.

   Pure CSS, so it works with no script at all — which is also why a failed GSAP
   leaves it running: it is not part of the sequence. A reduced-motion visitor gets
   a still disc. LIVE ON THE POSTER: the ?turn=1 gate is gone, at the owner's request. */
.portal__disc-turn {
  position: absolute; inset: 0; z-index: 2;
  top: 0; right: 0; bottom: 0; left: 0;      /* the longhands an older engine reads */
  border-radius: 50%; overflow: hidden; pointer-events: none;
}
.portal__disc-turn i {
  position: absolute; inset: 0; display: block;
  top: 0; right: 0; bottom: 0; left: 0;
  /* A plain gradient first, the conic pair after. An engine without conic gradients drops
     the second declaration whole and still gets a sweep of light going round; with them it
     gets the richer one. Two declarations instead of a feature test, and no script. */
  background: linear-gradient(118deg,
    rgba(255,255,255,.26) 0%, rgba(255,255,255,0) 38%,
    rgba(255,255,255,.20) 62%, rgba(255,255,255,0) 100%);
  background:
    conic-gradient(from 0deg,
      rgba(255,255,255,.36) 0deg, rgba(255,255,255,.07) 44deg,
      rgba(255,255,255,0) 92deg, rgba(255,255,255,.27) 168deg,
      rgba(255,255,255,0) 214deg, rgba(255,255,255,.15) 286deg,
      rgba(255,255,255,0) 332deg, rgba(255,255,255,.36) 360deg),
    repeating-conic-gradient(from 0deg,
      rgba(255,255,255,.06) 0deg 1.6deg, rgba(96,64,0,.04) 1.6deg 3.2deg);
  animation: portal-disc-turn 18s linear infinite;
  will-change: transform;
}
@keyframes portal-disc-turn { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .portal__disc-turn i { animation: none; } }

/* Ten marks scattered irregularly through the white space at her SIDES, never
   above or over her head. Deliberately uneven x/y spacing avoids two rigid icon
   columns; the eye reads a loose constellation instead. The four AI marks are
   interleaved with the creative ones. `--arc` alternates the curve direction. */
.st--play   { left:  -4%; top:  8%; --w: 17%; --rot: -14deg; --dx: -1.00; --dy: -0.74; --arc:  .8; }
.st--chip   { left:   5%; top: 28%; --w: 16%; --rot:   9deg; --dx: -1.00; --dy: -0.40; --arc: -.8; }
.st--heart  { left: -10%; top: 49%; --w: 14%; --rot: -10deg; --dx: -1.00; --dy: -0.04; --arc:  .7; }
.st--neural { left:   4%; top: 71%; --w: 14%; --rot:  -7deg; --dx: -1.00; --dy:  0.38; --arc: -.7; }
.st--bubble { left:  -6%; top: 94%; --w: 14%; --rot:  -4deg; --dx: -1.00; --dy:  0.78; --arc:  .7; }
.st--video  { left: 105%; top: 14%; --w: 18%; --rot:  12deg; --dx:  1.00; --dy: -0.68; --arc: -.8; }
.st--prompt { left: 117%; top: 35%; --w: 16%; --rot:  -8deg; --dx:  1.00; --dy: -0.32; --arc:  .8; }
.st--pen    { left: 103%; top: 51%; --w: 16%; --rot:  16deg; --dx:  1.00; --dy: -0.02; --arc: -.7; }
.st--spark  { left: 114%; top: 74%; --w: 13%; --rot:   8deg; --dx:  1.00; --dy:  0.42; --arc:  .8; }
.st--gen    { left: 106%; top: 93%; --w: 15%; --rot:  10deg; --dx:  1.00; --dy:  0.76; --arc: -.7; }

@media (max-width: 760px) {
  .portal__scene { aspect-ratio: auto; width: 100%; height: 100%; }
  .portal__hole { left: 50%; top: 40%; width: 84%; }
  /* Her body, on the tall phone frame. The source picture is cut mid-torso, and at
     the desktop offsets that cut lands 190px short of the bottom of a 390x844
     screen — her body stops in the air with a hand's width of nothing under it, and
     the scroll cue floats below the gap. These offsets are the same picture scaled
     about her head (so the head keeps its place on the disc, and the composition
     stays the source's) which carries the crop's edge down to just above the cue.
     Percentages of the disc's square: it holds on every phone. */
  .portal__portrait { left: 13.92%; top: 0.55%; width: 94.1%; }
  /* Six on a phone — three creative, three AI. More than this turns the 74px band
     above a 390px poster into a toolbar; fewer makes the requested AI vocabulary
     disappear. portal.js skips what is hidden, so the stagger stays even. */
  .st--video, .st--heart, .st--pen, .st--chip { display: none; }
  /* An irregular side scatter — never a crown over her head. The ponytail side
     deliberately carries fewer; the clearer left white space takes four. Because
     the layer is behind her, a narrow crop can only hide a badge, never put one on
     her face. */
  .st--play   { left:  -2%; top: -2%; --w: 13%; }
  .st--neural { left:   2%; top: 27%; --w: 13%; }
  .st--bubble { left:  -4%; top: 58%; --w: 13%; }
  .st--spark  { left:   3%; top: 91%; --w: 12%; }
  .st--gen    { left: 101%; top:  1%; --w: 13%; }
  .st--prompt { left: 104%; top: 22%; --w: 13%; }
}
/* -- a TABLET held upright: the same re-cut, for the same reason ------------- */
/* 768x1024 is a size the responsive checks are run at, and it falls 8px above the phone
   query. The 16:9 frame fits that window's WIDTH, so the scene came out 753x423 inside a
   1024-tall window: a 337px disc with 600px of empty ground around it — the
   "postage-stamp small" the phone query exists to prevent, at a window twice as wide.
   The SHAPE is what decides, not the pixel width: a window taller than it is wide gets the
   phone's crop. Landscape tablets and every desktop are untouched, and the drawing's own
   <source> switches on the same condition so the crop and the picture always agree. */
@media (min-width: 761px) and (max-width: 1180px) and (orientation: portrait) {
  .portal__scene { aspect-ratio: auto; width: 100%; height: 100%; }
  .portal__hole { left: 50%; top: 40%; width: 84%; }
  .portal__portrait { left: 13.92%; top: 0.55%; width: 94.1%; }
}
/* An awkward window (short and wide) is the one case the 16:9 scene cannot
   serve: at 900x420 the disc would be smaller than the type in the topbar. */
@media (max-height: 560px) and (min-width: 761px) {
  .portal__hole { top: 46%; width: 62%; }
}

/* -- reduced motion: no pin, no runway, no camera --------------------------- */
/* The poster is a plain band and the page scrolls past it; the only thing left
   that moves is a crossfade, written by portal.js.

   The band shown here is the DRAWING rather than her portrait: the poster is a
   still either way, and the drawing is the picture the opening is about — the
   door she was going to walk into. portal.js holds the dream layer at full
   opacity in this mode and hides the portrait's scene, and the clouds' own zoom
   and the light are never run at all.

   The stage stays POSITIONED — `relative`, still in flow — and must never be `static`: the
   drawing is a SIBLING of the scene, anchored with `inset: 0`, and its only positioned
   ancestor is `.portal`, which is `position: relative; height: 0` on purpose because the
   stage is normally fixed and the runway is what takes scroll. A static stage therefore left
   the drawing, its picture and its <img> at height 0: the band painted as an empty pale field
   with the film band under it, which is what a reduced-motion visitor was being shown. */
@media (prefers-reduced-motion: reduce) {
  /* In flow, as a plain band, with the runway at zero height (portal.js never
     sizes it): the poster is simply the first thing on the page and the film
     band follows it. */
  .portal__stage { position: relative !important; top: auto !important; left: auto !important;
                   width: auto !important; height: 80vh; transform: none !important; }
  /* The dream — the clouds and the door — is the LAST child of the stage, so it paints over the
     scene. What normally keeps it behind is the sequence's own transform, pushing it back in 3D;
     `transform: none` above takes exactly that away, and the clouds then covered her portrait and
     the icons. A visitor with animations off saw clouds and empty space where the poster is. So
     the order is stated outright here instead of being left to a transform that is no longer there. */
  .portal__scene { transform: none !important; z-index: 2; }
  .portal__dream { position: absolute !important; transform: none !important; z-index: 1; }
  .portal__dream-art { transform: none !important; }
  .portal__light { display: none !important; }
  /* The control goes with the animation: with no sequence to start, a full-screen
     invisible button would be a trap rather than a way in — the poster is simply
     the first thing on the page and the film follows it. */
  .portal__open { display: none !important; }
}

/* -- no GSAP: the same plain band, carried by a class ------------------------ */
/* A vendored script that never arrives is not a preference, so a media query cannot see it:
   portal.js puts `portal--plain` on <html> when GSAP is missing, and these are the
   reduced-motion rules again under that class — the opening in flow, as a still band, with her
   portrait on it and the film following it. The duplication is deliberate and the two are not
   interchangeable: the media query has to work with NO JavaScript at all, and the class has to
   work without a media query. Both show the same poster, still, and both state their paint
   order: the clouds are the last child of the stage, so anything that cancels the transform
   holding them back has to say which layer is in front. */
.portal--plain .portal__stage { position: relative; top: auto; left: auto;
                                width: auto; height: 80vh; transform: none; }
.portal--plain .portal__scene { transform: none; z-index: 2; }
.portal--plain .portal__dream { z-index: 1; }
.portal--plain .portal__light { display: none; }
.portal--plain .portal__open { display: none; }

/* ================================================================= THE GUIDES ===
   The article pages — /guides (the index) and /guides/<slug> (the read).

   The reading page had three problems and this block is the fix for each: a masthead
   set at page-hero size (up to 132px) that shouted two lines before saying anything;
   a prose column pinned to the left of a 1440px page with about 700px of empty ground
   beside it; and section headings at display size, so a six-stage method had six equal
   shouts and no hierarchy at all. So the masthead steps down, that wasted margin
   becomes a sticky contents rail — six stages need wayfinding and the rail earns the
   space — and the section headings step down with the stage number moved into the
   eyebrow. The measure stays where the site reads: about 62 characters.
   ============================================================================= */

.art-hero__title { font-size: clamp(26px, 3.8vw, 52px); }
.art-hero__meta {
  margin-top: 18px;
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px;
  font-size: 15px; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted);
}

/* How far through the piece the reader is: a hairline under the bar. Decorative, so
   it is aria-hidden in the markup and it never gates content. */
.art-progress {
  position: fixed; left: 0; right: 0; top: var(--nav-h); height: 2px;
  z-index: 55; pointer-events: none; background: transparent;
}
.art-progress i {
  display: block; height: 100%; background: var(--accent);
  transform: scaleX(0); transform-origin: 0 50%;
}

/* The article is a two-column grid whose columns change job per section: the heading and
   its stage number take the left, the prose takes the right. No contents rail — the
   owner's call, and the right one: a second navigation on a six-section page, which still
   left the prose as the narrow half of the screen. Nothing is left dead either: the page
   is wide and both columns carry something. */
.art__sec {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 62ch);
  gap: clamp(22px, 3.6vw, 64px);
  align-items: start;
  padding-top: clamp(24px, 3vw, 44px);
  border-top: 1px solid var(--line);
  margin-bottom: clamp(26px, 3.4vw, 46px);
  scroll-margin-top: calc(var(--nav-h) + 26px);
}
.art__sec:first-of-type { padding-top: 0; border-top: 0; }
/* The measure is declared on the column that holds the prose, in an element that also
   declares its font-size — `ch` resolves against the element's own size, and inherited it
   came out at 78 characters a line. */
.art__text { font-size: 18.5px; max-width: 62ch; }
.art__h {
  font-size: clamp(23px, 2.4vw, 36px); font-weight: 900; line-height: 1.06;
  letter-spacing: -.02em; color: var(--ink); margin-bottom: 14px;
}
.art__h--list { margin-bottom: 20px; }
.art__p { font-size: 18.5px; line-height: 1.75; color: var(--body); margin-bottom: 14px; }

/* "Where this is taught" was a paragraph with two links loose inside it. It is a note
   under the piece, so it looks like one. */
.art__note {
  margin-top: clamp(26px, 3.2vw, 44px);
  padding: clamp(18px, 2.4vw, 28px);
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
}
.art__note p { font-size: 18.5px; line-height: 1.65; color: var(--body); }
.art__noteacts { margin-top: 16px; display: flex; flex-wrap: wrap; gap: 10px; }

/* One card shape, so a guide looks the same wherever it is listed — the index and the
   "keep reading" rail at the foot of a piece. */
.art-list {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: clamp(16px, 2vw, 28px);
}
.art-list--tight { margin-top: 4px; }
.art-card {
  display: flex; flex-direction: column; gap: 8px;
  padding: clamp(18px, 2.2vw, 26px);
  background: var(--surface); border: 1px solid var(--line);
  transition: transform .25s var(--ease-out), border-color .25s var(--ease-out);
}
.art-card:hover { transform: translateY(-3px); border-color: var(--line-strong); }
.art-card__title { font-size: clamp(18px, 1.5vw, 21px); font-weight: 800; line-height: 1.18; letter-spacing: -.015em; }
.art-card__sub { font-size: 18px; line-height: 1.6; color: var(--body); }
.art-card__go {
  margin-top: auto; padding-top: 10px;
  font-size: 15px; font-weight: 800; letter-spacing: .14em; text-transform: uppercase;
  color: var(--accent);
}
.art-card__go i { font-style: normal; display: inline-block; transition: transform .25s var(--ease-out); }
.art-card:hover .art-card__go i { transform: translateX(4px); }

/* The band on a phone. The strip takes the phone clip's own shape (900x510) so there is no
   letterboxing, and the words in that clip are stacked — "meet" over "elza". The class is
   put on by site.js at the same moment it switches clips, which is what keeps the two in
   step: a phone that never runs the script keeps the wide clip AND the wide strip, instead
   of a wide clip cropped by a phone-shaped box. */
.cover--phone {
  /* the phone clip's own shape (900x950): two stacked lines of big type are a near-square
     block, so the strip is re-shaped to the words rather than the words shrunk into a wide
     strip. That is what the owner's "increase the font of meet elza to another size" buys —
     the words went from filling 35% of the strip's width to about 82%. */
  aspect-ratio: 900 / 950;
}
/* The cover band's sound button is gone from the markup, so the phone rule that
   used to hide it has gone with it — the owner's "remove the music background for
   the meet elza banner, so remove the button for the music" now holds on every
   screen rather than only on a phone. */
/* Phones and narrow windows: one column, the heading over the prose. */
@media (max-width: 1000px) {
  .art__sec { grid-template-columns: minmax(0, 1fr); gap: clamp(14px, 3vw, 22px); }
  .art__text { max-width: none; }
}

/* The brief under the hero of an article: the answer, stated once, before the argument
   starts. It is the block an answer engine lifts and the block a skimming reader
   actually reads, so it is the loudest thing on the page after the headline — the
   claims sit at full ink weight rather than in muted small print, because anything
   that has to be read should not be faint. Same card family as the "where this is
   taught" note, with the rule moved to the top so the two are not mistaken for each
   other. */
.art__brief {
  padding: clamp(20px, 2.6vw, 32px);
  background: var(--surface);
  border: 1px solid var(--line);
  border-top: 3px solid var(--accent);
  margin-bottom: clamp(28px, 3.4vw, 48px);
}
.art__brief-list {
  margin-top: 14px;
  list-style: none;
  padding: 0;
  display: grid;
  gap: 12px;
}
.art__brief-list li {
  position: relative;
  padding-left: 26px;
  font-size: 18.5px;
  line-height: 1.6;
  font-weight: 600;
  color: var(--ink);
}
.art__brief-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: .66em;
  width: 12px;
  height: 2px;
  background: var(--accent);
}

/* The one line that hands a reader from an article to the guides shelf. */
.art__cross {
  margin-top: clamp(22px, 2.6vw, 34px);
  font-size: 18.5px;
  line-height: 1.65;
  color: var(--body);
  max-width: 62ch;
}


/* ------------------------------------------------------- sources ----------- */
/* A dated piece quotes a survey, a paper or an announcement, so it says which and links
   to it. Set at reading weight rather than as grey small print: an attribution nobody
   reads is the same as no attribution. */
.art__sources { margin-top: clamp(32px, 3.6vw, 52px); border-top: 1px solid var(--line); padding-top: 24px; }
.art__sources ul { margin-top: 14px; display: grid; gap: 10px; list-style: none; padding: 0; }
.art__sources li { font-size: 15.5px; font-weight: 600; line-height: 1.5; color: var(--ink); }
.art__sources a { color: inherit; text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 2px; text-decoration-color: var(--accent); }
.art__sources a:hover { color: var(--accent); }
