/* ==========================================================================
   Saor Sagashi — /guides/saor/
   SELF-CONTAINED. This file does not load, extend or import css/style.css.

   That is deliberate. style.css's :root is Lumenæ's locked brand palette;
   redefining --cream to hold a near-black would be brand-blending through the
   back door and would leave every Lumenæ token lying about what it contains.
   The cost is ~200 duplicated lines of reset / shell / nav / reveal STRUCTURE.
   That is the cheaper mistake.

   Palette provenance, the full contrast table and the re-derivation script:
   docs/saor-brand.md. Band architecture and art direction:
   docs/saor-subpage-brief.md.

   In one line: Lumenæ is candlelit paper. Saor is moonlit stone.
   ========================================================================== */

/* ---------- Reset (mirrors style.css) ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; }
/* `height: auto` is load-bearing, not boilerplate. HTML width/height attributes
   are presentational hints that map to CSS width/height. A rule setting only
   `width` overrides half the pair and the element keeps the attribute height.
   The cover mark (`width: 54px`, attributes 1024x1024) rendered 54x1024 and
   stretched the journal cover to 320x1196 instead of 320x427.
   Measured 2026-08-31. Every other image on the site sets both dimensions, which
   is why this stayed latent. */
img, svg, video { display: block; max-width: 100%; height: auto; }
picture { display: contents; }   /* keep <img> in the parent's flex/grid flow */
/* ...but ONLY the <img>. `display: contents` promotes EVERY child of <picture>,
   and <source> is a child. It then becomes a real flex or grid item: measured
   2026-08-31 on /guides/saor/, a <source> took a 490x392 box inside .sa-who__moon
   and pushed the journal cover into the wrong grid column entirely. It stayed
   latent for a long time because in .brand and .footer__brand the stray item
   measures 0x0 and nothing moves. <source> is never rendered; say so. */
source { display: none; }
button, input { font: inherit; color: inherit; }
ul[role="list"], ol[role="list"] { list-style: none; padding: 0; }

/* ---------- Tokens ---------- */
:root {
  /* Surfaces — neutral, from the mask logo. LOCKED value is --sa-shadow, which
     is the vector's own shadow colour; the rest of the ramp is derived from it. */
  --sa-void:       #0F0F10;
  --sa-night:      #1A1A1B;
  --sa-shadow:     #2E2D2D;
  --sa-stone:      #55524C;   /* 2.23 on night. #3B3A38 measured 1.53 and was
                                   invisible — this token carries every divider
                                   on the page, so it has to actually read. */

  /* Type on dark — the mask's own two light values, verbatim. */
  --sa-bone:       #E7E8E8;   /* 14.17 on night — AAA */
  --sa-bone-2:     #C9CBCD;   /* 10.69 on night — AAA */

  /* Materials — warm, from the journal PDF. Wood, paper and cloth ONLY.
     A brown never colours a border, a background or a piece of chrome. */
  --sa-bark:       #2B211D;
  --sa-umber:      #3A2F28;   /* journal ink — the paper band's text colour */
  --sa-earth:      #4A3D33;
  --sa-drift:      #7A6A56;   /* MARK ONLY — fails on every ground here */
  --sa-wood:       #B5946A;   /* 6.14 on night — AA */
  --sa-sand:       #C9AD82;   /* 8.10 on night — AAA */
  --sa-dune:       #D8C4A8;
  --sa-linen:      #E0CFAE;
  --sa-paper:      #FAF1E0;

  /* The accent. ONE hue, three stops. See the warning below. */
  --sa-ember:      #B5651D;   /* 4.01 on night — MARKS + large display only */
  --sa-ember-lit:  #D18A46;   /* 6.16 on night — text-safe on dark */
  --sa-ember-deep: #8C4712;   /* 6.19 on paper — text-safe on paper */

  /* The candle, and nothing else. Two points from Lumenæ's --gold #D9A441, so
     it reads as the door light if used anywhere but the Flame Method band. */
  --sa-candle:     #D9A35F;

  /* ⚠ --sa-ember IS NOT A TEXT COLOUR. Measured: 4.01 on --sa-night, 3.17 on
     --sa-shadow, 3.87 on --sa-paper, 2.83 on --sa-linen. It clears 3:1, so it
     is valid for hairlines, icons, focus rings and large display type (>=24px).
     It fails as body copy on every surface in this file. Body-weight accent
     text takes --sa-ember-lit on dark and --sa-ember-deep on paper. Same shape
     as Lumenæ's --terracotta-lit rule. Forget it and the page fails AA while
     looking completely intentional. */

  /* Type. Three families, and the split between them is the whole system.

     Quicksand is Seb's pick for display (2026-08-31) and REPLACES Bodoni Moda in
     that role. It has no italic, which is not a gap to work around: it is what
     hands the entire italic register to Nunito, so a slanted word on this page is
     always the text face and never the display face.

     Neither Quicksand nor Nunito carries an `opsz` axis, so every
     `font-variation-settings: "opsz" N` that used to drive Bodoni per context is
     gone. Optical compensation now lives in size, weight and tracking — which is
     why the tracking values below are not decoration and should not be rounded off.

     Bodoni survives in exactly one place: the "Lumenæ" wordmark, which is Lumenæ's
     and is locked to that face wherever it appears. It is requested SUBSET to those
     glyphs, the same request the homepage makes. That inverts this page's old rule
     (it used to need the full family and had to avoid the subset); loading both
     under one family name is still a silent-fallback bug, so there is still only
     ever one Bodoni request per page. */
  --sa-display: "Quicksand", ui-rounded, "Segoe UI", system-ui, sans-serif;
  --sa-text:    "Nunito", ui-sans-serif, system-ui, "Segoe UI", sans-serif;
  --sa-mark:    "Bodoni Moda", Georgia, serif;

  /* Rhythm — same 4/8 scale as Lumenæ. This is ecosystem-level, not brand. */
  --s-1: .25rem;  --s-2: .5rem;   --s-3: .75rem;  --s-4: 1rem;
  --s-5: 1.5rem;  --s-6: 2rem;    --s-7: 3rem;    --s-8: 4rem;
  --s-9: 6rem;    --s-10: 8rem;

  --shell: 1140px;
  --radius: 14px;
  --radius-sm: 9px;

  /* Motion — slower and fewer than Lumenæ, and nothing scales. */
  --ease-out:    cubic-bezier(.22, 1, .36, 1);
  --ease-soft:   cubic-bezier(.4, 0, .2, 1);
  --ease-reveal: cubic-bezier(.16, 1, .3, 1);
  --t-fast: 180ms;
  --t-mid: 300ms;
  --t-slow: 700ms;
  --t-reveal: 1100ms;         /* Lumenæ is 900ms */

  /* Same generator as style.css:60, carried by value rather than imported. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.42'/%3E%3C/svg%3E");
}

/* Anchor targets clear the sticky bar.

   Every cross-page link in the nav is <page>#section, so arriving at one lands
   the section top flush with the viewport top -- and .nav is position: sticky,
   so the first line of the section arrives underneath it. scroll-margin-top is
   the property that fixes it without moving anything on the page: it applies
   only while scrolling TO the element.

   The value is the stuck bar (56px) plus a little air, not the resting bar
   (68px). By the time a jump of any length settles, .nav.is-stuck has already
   engaged. */
:target, .section, .hero, .sa-hero { scroll-margin-top: 72px; }

/* ---------- Base ---------- */
html { scroll-behavior: smooth; }
body {
  background: var(--sa-night);
  color: var(--sa-bone);
  /* Nunito carries body, quotes and UI; Quicksand carries display. 19px was a
     Bodoni number — that face sets small for its point size. Nunito's x-height is
     considerably larger, so the same measure reads a size bigger and 17px is the
     equivalent. The 15px floor further down still holds. */
  font-family: var(--sa-text);
  font-size: 1.0625rem;       /* 17px */
  line-height: 1.72;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: max(clamp(1.25rem, 5vw, 3rem), env(safe-area-inset-left, 0px));
  padding-inline-end: max(clamp(1.25rem, 5vw, 3rem), env(safe-area-inset-right, 0px));
}

.section { position: relative; padding-block: clamp(4rem, 11vw, 8rem); }
@media (max-width: 699px) {
  .section { padding-block: clamp(3rem, 12vw, 4.5rem); }
}

/* ---------- Typography ----------
   Quicksand above, Nunito below, and the boundary is deliberate: headings are the
   geometric face, everything a reader spends time inside is the text face. Both
   are rounded sans with related skeletons, so the hierarchy is carried by weight,
   size and tracking rather than by letterform contrast. Flatten any of the three
   and the page reads as one undifferentiated block. */
.sa-display, .sa-h2 { font-family: var(--sa-display); }
.sa-display, .sa-h2, .sa-h3 { text-wrap: balance; margin: 0; }

/* Quicksand's circular bowls open up at display size and the line falls apart
   without negative tracking. 400 rather than 300: a hairline weight on a dark
   ground loses more contrast than the token value suggests. */
.sa-display {
  font-weight: 400;
  font-size: clamp(2.4rem, 7vw, 4.2rem);
  line-height: 1.06; letter-spacing: -.018em;
}
.sa-h2 {
  font-weight: 500;
  font-size: clamp(1.7rem, 3.8vw, 2.375rem);
  line-height: 1.2; letter-spacing: -.012em;
  margin-bottom: var(--s-5);
}
/* Nunito 700, not Quicksand — a pillar heading sits inside its own paragraph, and
   separating it by FAMILY rather than by another weight step is what keeps five
   of them from reading as five more lines of body copy. */
.sa-h3 {
  font-family: var(--sa-text);
  font-weight: 700; font-size: 1.0625rem; line-height: 1.4;
  letter-spacing: .004em;
}

/* Uppercase, tracked out, never more than a few words — the one place the type
   goes below 15px, and only because those three things together keep it legible
   without a utility face. Verify on a real phone before changing it. */
.sa-eyebrow {
  font-family: var(--sa-display);
  font-size: .8125rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--sa-wood);
  margin-bottom: var(--s-4);
}

.sa-lede {
  font-size: clamp(1.125rem, 1.5vw, 1.25rem);
  line-height: 1.7;
  color: var(--sa-bone-2);
  max-width: 60ch; text-wrap: pretty;
}
.sa-body { color: var(--sa-bone-2); max-width: 62ch; text-wrap: pretty; }
/* Nunito's italic is a real italic, not an oblique, and this is the register it
   exists for on this page — Quicksand has none at all. */
.sa-quote {
  font-style: italic; font-weight: 400;
  font-size: clamp(1.3rem, 2.4vw, 1.625rem);
  line-height: 1.46; color: var(--sa-bone); max-width: 30ch;
}
em { font-style: italic; }
strong { font-weight: 700; color: var(--sa-bone); }

/* ---------- Accessibility ---------- */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip-link {
  position: absolute; left: var(--s-4); top: var(--s-4);
  transform: translateY(-160%);
  background: var(--sa-bone); color: var(--sa-night);
  padding: var(--s-3) var(--s-5); border-radius: var(--radius-sm);
  text-decoration: none; z-index: 1000;
  transition: transform var(--t-mid) var(--ease-out);
}
.skip-link:focus-visible { transform: translateY(0); }

/* Ember-lit, never gold. Gold is Lumenæ's. */
:where(a, button, input):focus-visible {
  outline: 2.5px solid var(--sa-ember-lit);
  outline-offset: 3px;
  border-radius: 4px;
}
/* On the paper band the light accent would vanish; the deep one is the pair. */
.sa-journal :where(a, button, input):focus-visible { outline-color: var(--sa-ember-deep); }

/* ---------- Reveal system ----------
   Same one-way contract as Lumenæ (main.js adds .is-in and unobserves), with
   Saor's slower timing. .reveal--bloom is deliberately NOT carried over: it
   scales, and nothing on this page scales. Panels use the base .reveal. */
.reveal {
  transition: opacity var(--t-reveal) var(--ease-reveal),
              transform var(--t-reveal) var(--ease-reveal);
  transition-delay: calc(var(--d, 0) * 110ms);
}
.js .reveal { opacity: 0; transform: translate3d(0, 12px, 0); }
.js .reveal.is-in { opacity: 1; transform: translate3d(0, 0, 0); }

.reveal--rise {
  transition: opacity var(--t-reveal) var(--ease-reveal),
              transform var(--t-reveal) var(--ease-reveal),
              clip-path var(--t-reveal) var(--ease-reveal);
  transition-delay: calc(var(--d, 0) * 110ms);
}
.js .reveal--rise { opacity: 0; transform: translate3d(0, 24px, 0); clip-path: inset(0 0 42% 0); }
.js .reveal--rise.is-in { opacity: 1; transform: translate3d(0, 0, 0); clip-path: inset(0 0 -18% 0); }

.reveal--settle {
  transition: opacity var(--t-reveal) var(--ease-reveal),
              letter-spacing var(--t-reveal) var(--ease-reveal);
  transition-delay: calc(var(--d, 0) * 110ms);
}
.js .reveal--settle { opacity: 0; letter-spacing: .32em; }
.js .reveal--settle.is-in { opacity: 1; letter-spacing: .16em; }

/* ---------- Buttons ---------- */
.sa-btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s-2);
  min-height: 50px;
  padding: var(--s-3) var(--s-6);
  font-family: var(--sa-display);
  font-size: .875rem; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase;
  text-decoration: none; cursor: pointer;
  border: 1px solid var(--sa-stone);
  border-radius: var(--radius-sm);
  background: transparent; color: var(--sa-bone);
  transition: border-color var(--t-mid) var(--ease-soft),
              color var(--t-mid) var(--ease-soft),
              background-color var(--t-mid) var(--ease-soft);
}
.sa-btn:hover { border-color: var(--sa-ember); color: var(--sa-ember-lit); }
@media (hover: none) { .sa-btn:active { border-color: var(--sa-ember); color: var(--sa-ember-lit); } }

/* The fill is --sa-ember-DEEP, not --sa-ember. --sa-paper on --sa-ember measures
   3.87 and fails AA for a button label; on --sa-ember-deep it is 6.19. This is
   the same rule as everywhere else in the file — ember is a mark colour, and the
   moment it has to carry text it hands off to one of its two siblings. */
.sa-btn--solid {
  background: var(--sa-ember-deep); border-color: var(--sa-ember-deep);
  color: var(--sa-paper);                /* 6.19 — AA */
}
.sa-btn--solid:hover { background: #7E400F; border-color: #7E400F; color: var(--sa-paper); }

/* Paper-band variant. --sa-ember-deep because the light stop disappears here. */
.sa-btn--paper { border-color: rgba(58,47,40,.28); color: var(--sa-umber); }
.sa-btn--paper:hover { border-color: var(--sa-ember-deep); color: var(--sa-ember-deep); }

/* ---------- Nav ----------
   Lumenæ's bar, inverted. Structure and behaviour are identical (js/main.js
   drives both), including the 68 -> 56px shrink and its PAIRED 12px margin:
   .nav is position:sticky and therefore still in flow, so shrinking the height
   alone pulls every following band up by the delta and the page jumps at ~1px
   of scroll. The margin returns exactly what the height gives up, on the same
   curve. Change one number and you must change the other. */
.nav {
  position: sticky; top: 0; z-index: 100;
  background: color-mix(in srgb, var(--sa-night) 82%, transparent);
  border-bottom: 1px solid transparent;
  padding-top: env(safe-area-inset-top, 0px);
  transition: border-color var(--t-mid) var(--ease-soft),
              background-color var(--t-mid) var(--ease-soft),
              margin-bottom var(--t-mid) var(--ease-soft),
              translate var(--t-mid) var(--ease-soft);
}
.nav.is-hidden { translate: 0 -100%; }
@supports (backdrop-filter: blur(1px)) { .nav { backdrop-filter: blur(14px) saturate(1.2); } }
.nav.is-stuck { border-bottom-color: var(--sa-stone); background: color-mix(in srgb, var(--sa-night) 94%, transparent); }
.nav.is-stuck { margin-bottom: 12px; }
.nav.is-stuck .nav__inner { min-height: 56px; }

.nav__inner {
  display: flex; align-items: center; gap: var(--s-5);
  min-height: 68px;
  transition: min-height var(--t-mid) var(--ease-soft);
}
.brand {
  display: inline-flex; align-items: center; gap: var(--s-3);
  min-height: 44px; text-decoration: none; color: var(--sa-bone);
}
.brand__mark { width: 34px; height: 34px; }
/* Lumenæ's wordmark, on Saor's page. Locked to Bodoni Moda wherever it appears,
   which is why the family is named here rather than inherited — body is Nunito
   now, and an inherited wordmark would silently stop being the wordmark. */
.brand__word {
  font-family: var(--sa-mark);
  font-style: italic; font-weight: 500;
  letter-spacing: -.01em; font-size: 1.3rem; line-height: 1;
}
/* Breadcrumb rather than a link row: this page has one parent and one identity,
   and a five-item primary nav here would compete with the mask. */
.nav__crumb {
  display: none; margin-inline-start: auto;
  align-items: center; gap: var(--s-3);
  font-family: var(--sa-display); font-weight: 600;
  font-size: .8125rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--sa-wood);
}
.nav__crumb a { color: var(--sa-wood); text-decoration: none; transition: color var(--t-fast) var(--ease-soft); }
.nav__crumb a:hover { color: var(--sa-bone); }
.nav__crumb b { font-weight: 500; color: var(--sa-bone-2); }
.nav__crumb i { font-style: normal; opacity: .5; }
.nav__cta { margin-inline-start: auto; }
.nav .sa-btn { min-height: 40px; padding: var(--s-2) var(--s-4); font-size: .8125rem; }

.nav__toggle {
  display: grid; place-items: center; flex: none;
  width: 44px; height: 44px; margin-inline-start: var(--s-2);
  background: none; border: 0; cursor: pointer; color: var(--sa-bone);
}
.nav__toggle-rules { display: grid; gap: 5px; width: 21px; }
.nav__toggle-rules span {
  display: block; height: 1.5px; width: 100%;
  background: currentColor; border-radius: 2px;
  transition: transform var(--t-mid) var(--ease-out);
}
.nav__toggle[aria-expanded="true"] .nav__toggle-rules span:first-child { transform: translateY(3.25px) rotate(13deg); }
.nav__toggle[aria-expanded="true"] .nav__toggle-rules span:last-child  { transform: translateY(-3.25px) rotate(-13deg); }

/* Scroll lock is CSS-driven off a class on <html>, deliberately not JS-event
   driven — a matchMedia listener that failed to fire once left the page
   unscrollable with the menu hidden. */
.nav-locked body { position: fixed; top: var(--lock-top, 0px); left: 0; right: 0; width: 100%; }

.nav__panel {
  position: fixed; inset: 0; z-index: 200;
  background: var(--sa-night);
  display: grid; align-content: center;
  overflow-x: hidden; overflow-y: auto;
  padding: var(--s-9) clamp(1.5rem, 8vw, 3rem) calc(var(--s-8) + env(safe-area-inset-bottom, 0px));
  opacity: 0; visibility: hidden;
  transition: opacity var(--t-mid) var(--ease-soft), visibility 0s linear var(--t-mid);
}
@media (max-height: 500px) { .nav__panel { align-content: start; padding-block: var(--s-7) var(--s-6); } }
.nav__panel.is-open { opacity: 1; visibility: visible; transition-delay: 0s, 0s; }
.nav__panel-inner { position: relative; z-index: 1; }
.nav__index { display: grid; gap: var(--s-2); margin-bottom: var(--s-8); }
.nav__index li {
  opacity: 0; transform: translateY(12px);
  transition: opacity var(--t-slow) var(--ease-reveal), transform var(--t-slow) var(--ease-reveal);
}
.nav__panel.is-open .nav__index li { opacity: 1; transform: none; }
.nav__panel.is-open .nav__index li:nth-child(1) { transition-delay: 60ms; }
.nav__panel.is-open .nav__index li:nth-child(2) { transition-delay: 120ms; }
.nav__panel.is-open .nav__index li:nth-child(3) { transition-delay: 180ms; }
.nav__panel.is-open .nav__index li:nth-child(4) { transition-delay: 240ms; }
.nav__panel.is-open .nav__index li:nth-child(5) { transition-delay: 300ms; }
.nav__index a {
  display: flex; align-items: baseline; gap: var(--s-4);
  min-height: 48px; padding-block: var(--s-2);
  font-family: var(--sa-display); font-weight: 500;
  font-size: clamp(1.55rem, 6.6vw, 2rem); line-height: 1.24;
  letter-spacing: -.01em;
  color: var(--sa-bone); text-decoration: none;
}
.nav__index a span {
  font-style: normal; font-weight: 600;
  font-size: .8125rem; letter-spacing: .12em; color: var(--sa-ember-lit);
}
.nav__panel-cta { width: 100%; }

@media (max-width: 379px) { .nav__cta { display: none; } }
/* Matches the homepage's 900px switch so the two pages change shape together. */
@media (min-width: 900px) {
  .nav__crumb { display: flex; }
  .nav__cta { margin-inline-start: 0; }
  .nav__toggle { display: none; }
  .nav__panel { display: none; }
  .nav-locked body { position: static; top: auto; width: auto; }
}

/* ---------- Atmosphere ----------
   Where Lumenæ blooms, Saor rims. Depth comes from contrast and vignette, and
   the gradients run edge-to-centre rather than centre-out. A centred radial
   bloom here would read as borrowed from the other brand. */
.sa-dark { position: relative; isolation: isolate; overflow: hidden; background: var(--sa-night); }
.sa-dark > * { position: relative; z-index: 1; }
.sa-dark::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    linear-gradient(to right, var(--sa-void), transparent 26%),
    linear-gradient(to left,  var(--sa-void), transparent 26%);
  opacity: .55;
}
.sa-grain {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image: var(--grain);
  opacity: .07;                 /* Lumenæ uses .05 — his world is photographic */
  mix-blend-mode: overlay;
}

/* ---------- Artwork ----------
   ⚠ THE ONE RULE THAT GOVERNS EVERY IMAGE ON THIS PAGE: no text sits over
   artwork. Ever. Each picture gets its own column or its own band, and copy sits
   beside it or below it on a flat --sa-night ground.

   That is a contrast decision, not a taste one. tools/og-saor.js already works
   this way -- the share card's headline ends at x=584 and the spiral's box does
   not start until x=630 -- and it is the only arrangement where a contrast figure
   can be trusted. Measured against a token colour, text over a photograph reads
   as passing while looking wrong, because the probe walks up to the first opaque
   background and finds --sa-night rather than the gold it is actually sitting on.
   Introduce a scrim and the numbers become a negotiation. Keep them apart and
   every run on the page measures against a flat, known ground.

   The art is GOLD and ships ungraded -- Seb's call, 2026-08-31, overriding the
   "saor" skill. Chrome stays on the ember/neutral ramp; see the palette warning
   at the top of this file. */

/* Every source is a PORTRAIT column (736x1318, 720x1284, 1200x2133), so they are
   framed as columns. Feathered top and bottom rather than boxed: the picture
   should read as light emerging out of the dark, and the bottom stop is what
   lets the copy underneath sit on plain --sa-night with no visible seam. The
   left and right edges of the artwork are already near-black, so they need no
   feather of their own. */
.sa-column {
  position: relative;
  display: grid;
  overflow: hidden;
}
.sa-column > * { grid-area: 1 / 1; }
/* The artwork is taken OUT OF FLOW, and both halves of that matter.

   `picture` is `display: contents` in the reset, so the <img> is the grid item and
   the <picture> is not -- selecting only direct children misses the artwork and
   auto-places it into a second row, which is how the first build rendered every
   picture BELOW the thing it was meant to sit behind.

   Making it a grid item instead is not enough either. A column carries its own
   `aspect-ratio` and `max-height`, and an in-flow item cannot be clamped by them:
   the row grows to the image's automatic minimum size and the ratio is ignored.
   Measured on the second build -- .sa-who__moon asked for 16/11 and rendered
   490x871, the image's own 1000/1778. Absolute positioning removes it from the
   sizing entirely, so the column keeps the shape the design gave it. */
.sa-column__art {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  /* TWO gradients composited, not one. The vertical fade alone left hard
     vertical edges: the note further up about the artwork being near-black at
     the sides is true of the divider but not of these, whose starfields run
     right to the frame. `intersect` multiplies the two so the picture is
     feathered on all four sides and reads as light coming out of the dark
     rather than as a photograph in a box. -webkit-mask-composite carries the
     old WebKit keyword for the same operation. */
  -webkit-mask-image:
    linear-gradient(to bottom, transparent 0%, #000 15%, #000 70%, transparent 100%),
    linear-gradient(to right,  transparent 0%, #000 14%, #000 86%, transparent 100%);
          mask-image:
    linear-gradient(to bottom, transparent 0%, #000 15%, #000 70%, transparent 100%),
    linear-gradient(to right,  transparent 0%, #000 14%, #000 86%, transparent 100%);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}

/* ---------- Hero ---------- */
.sa-hero {
  position: relative; overflow: hidden;
  background: var(--sa-night);
  padding-block: clamp(2rem, 6vw, 4rem) clamp(4rem, 11vw, 7rem);
  isolation: isolate;
}
.sa-hero > * { position: relative; z-index: 1; }
/* One vignette, corners in --sa-void. Static. */
.sa-hero::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(78% 62% at 50% 34%, transparent 30%, var(--sa-void) 100%);
}
.sa-hero__inner { display: grid; justify-items: center; text-align: center; gap: var(--s-6); }

/* The stage holds the clip and nothing else. It used to share its grid cell with
   the mark; Seb separated them on 2026-08-31 because the near-white mask was
   reading as the subject and the picture as its backdrop.

   SQUARE, because the source is: DeepShadowwork.mp4 is 720x720. Every other
   .sa-column on this page is a portrait crop of a portrait source, and giving a
   square clip a portrait frame would throw away a third of a composition that is
   already tightly framed on one figure.

   max-height is keyed to the VIEWPORT, not to a breakpoint — this is still the
   one element tall enough to push the headline below the fold on a short screen,
   and the mark underneath now sits below it, so the clearance matters more than
   it did. */
.sa-hero__stage {
  width: min(78vw, 420px);
  aspect-ratio: 1;
  max-height: min(48vh, 420px);
}
/* Centred, unlike the stills. The old crop was weighted to 76% to keep a figure
   at the FOOT of a tall column of light; this clip is already framed on its
   figure and any offset just cuts a limb off. */
.sa-hero__stage .sa-column__art { object-position: 50% 50%; }

/* Pulled up into the clip's own bottom feather, so the mark rises out of where
   the figure is dissolving rather than sitting in a separate box below it. The
   negative margin cancels the grid gap and then some; it is written against
   --s-6 so the two move together if the rhythm changes. */
.sa-hero__mask {
  position: relative;
  width: min(38vw, 156px);
  aspect-ratio: 1;
  margin-top: calc(var(--s-6) * -1.35);
  display: grid; place-items: center;
}
/* One directional source, upper left, the way the thumbnail briefs light him.
   The dark pool that used to be the first stop in this list is gone with the
   stacked layout: it existed to hold the #E7E8E8 mark apart from artwork running
   to near-white directly behind it, and there is no longer anything behind it
   but --sa-night. */
.sa-hero__mask::before {
  content: ""; position: absolute; inset: -26%;
  background:
    radial-gradient(54% 54% at 38% 30%, rgba(231,232,232,.10), transparent 68%),
    radial-gradient(62% 62% at 50% 56%, rgba(181,101,29,.09), transparent 72%);
  pointer-events: none;
}
.sa-hero__mark { position: relative; z-index: 1; width: 100%; height: auto; }

/* The mote field. js/saor.js appends this canvas and sizes its backing store;
   the box is bigger than the mark on every side because the particles have to
   have somewhere to drift to.

   `screen` is doing real work and is not a flourish: it makes one particle
   colour correct on both grounds. Over --sa-night a bone mote glows; where one
   crosses the near-white mark it saturates to white and disappears into it,
   which is exactly what a mote coming off a lit edge should do. Without it the
   field has to be drawn UNDER the mark, and then nothing ever appears to leave.

   No `animation` here — every frame is drawn in JS, so the reduced-motion block
   below cannot reach it and saor.js has to make that decision itself. */
.sa-hero__motes {
  position: absolute; z-index: 2;
  left: -52%; top: -46%; width: 204%; height: 186%;
  pointer-events: none;
  mix-blend-mode: screen;
}

/* Word-by-word entrance. The spans only exist when js/saor.js creates them, so
   with JS off the line renders whole and unstyled — no blank hero. */
.sa-hero__line { max-width: 20ch; }
.sa-hero__line .w {
  display: inline-block;
  opacity: 0; filter: blur(7px);
  transition: opacity var(--t-slow) var(--ease-reveal),
              filter var(--t-slow) var(--ease-reveal);
  transition-delay: calc(var(--wd, 0) * 85ms);
}
.sa-hero__line.is-in .w { opacity: 1; filter: blur(0); }
.sa-hero__sub { color: var(--sa-bone-2); max-width: 46ch; }

/* ---------- The seam ----------
   The page's signature device, and it RECURS: every dark<->paper transition
   plus a reduced-scale echo under each pillar heading. A treatment that appears
   exactly once reads as an ornament dropped onto the page. */
/* The drawn path is still the seam. The artwork sits BEHIND it as a widened
   band -- a particle ribbon rotated out of its vertical source -- so the stroke
   reads as one filament picked out of a field of them. The band is taller than
   the stroke and feathered on every edge, because a hard-edged photograph
   between two sections would read as a divider dropped onto the page rather
   than as a seam in it. */
.sa-seam { position: relative; display: block; width: 100%; height: 34px; }
.sa-seam svg { position: relative; z-index: 1; width: 100%; height: 100%; display: block; }
.sa-seam__field {
  position: absolute; left: 0; right: 0; top: 50%; z-index: 0;
  translate: 0 -50%;
  width: 100%; height: 104px;
  object-fit: cover;
  /* .34 was a hedge. Seb asked for this artwork to actually be the divider, so
     the band is taller and reads at .48 -- still well under the copy either side
     of it, and still feathered to nothing at every edge. Raised 2026-08-31. */
  opacity: .48;
  pointer-events: none;
  -webkit-mask-image: radial-gradient(96% 54% at 50% 50%, #000 6%, transparent 74%);
          mask-image: radial-gradient(96% 54% at 50% 50%, #000 6%, transparent 74%);
}
/* pathLength="1" on the path, so the draw is a plain 0..1 fraction and nothing
   has to measure the geometry. --draw is written by js/saor.js on scroll.

   The undrawn state is scoped to .js for the same reason .reveal is: with no
   JavaScript the seam ships fully drawn. Without the scope there is a visible
   flash — the seam paints complete on first render, then saor.js sets --draw to
   0 and it vanishes before drawing back in. */
.sa-seam path {
  fill: none; stroke: var(--sa-ember); stroke-width: 1.5;
  stroke-dasharray: 1; stroke-dashoffset: 0;
}
.js .sa-seam path { stroke-dashoffset: calc(1 - var(--draw, 0)); }

/* ---------- Who he is ---------- */
.sa-who__grid { display: grid; gap: var(--s-6); margin-top: var(--s-6); }
@media (min-width: 760px) {
  .sa-who__grid { grid-template-columns: 1fr 1fr; gap: var(--s-8); align-items: start; }
}
/* A crescent half in light and half not, beside a heading that says one eye
   reveals and one eye hides. It sits ABOVE the pull-quote in the same column
   rather than behind it -- see the artwork rule: nothing on this page puts text
   over a picture, and stacking is what keeps the quote on a flat ground. */
.sa-who__side { display: grid; gap: var(--s-6); }
/* PORTRAIT, where the crescent that used to sit here was 5/4 landscape. The
   ribbon runs top to bottom of a 480x854 frame and a landscape box would crop it
   to a smear across the middle; 4/5 keeps enough of the twist to read as one
   continuous thing. Centred for the same reason -- the old 7% offset existed to
   catch a crescent in the top fifth of a different picture. */
.sa-who__veil { width: 100%; aspect-ratio: 4 / 5; max-height: min(58vh, 480px); }
.sa-who__veil .sa-column__art { object-position: 50% 50%; }
/* No blend mode here, and that is a decision rather than an omission.

   The source renders on a purple near-black, and the four-sided feather only
   ever softened the EDGES of that rectangle -- the tint inside them still read
   as a violet panel behind the ribbon. The obvious fix, crush the clip to true
   black and composite it with mix-blend-mode: screen, was measured and is
   wrong: blending happens inside the nearest stacking context, .shell is
   position:relative z-index:1 with no background of its own, so screen had a
   transparent backdrop and painted the black straight through -- rgb(0,0,0)
   against a page of rgb(26,26,27).

   tools/assets.js grades the clip's black point to --sa-night instead, which
   needs no compositing trick and cannot be undone by a stacking context
   appearing somewhere up the tree. */

/* ---------- The work: five pillars ----------
   Hairline rows, not a card grid. Lumenæ's .card component is its signature and
   reusing it here is the fastest way to make the two pages read as one brand. */
.sa-pillars { margin-top: var(--s-6); border-top: 1px solid var(--sa-stone); }
.sa-pillar { padding-block: var(--s-5); border-bottom: 1px solid var(--sa-stone); }
.sa-pillar__rule {
  display: block; width: 34px; height: 1px;
  background: var(--sa-ember); margin-bottom: var(--s-3);
  transform-origin: left;
  transition: transform var(--t-reveal) var(--ease-reveal);
  transition-delay: calc(var(--d, 0) * 110ms);
}
.js .sa-pillar .sa-pillar__rule { transform: scaleX(0); }
.js .sa-pillar.is-in .sa-pillar__rule { transform: scaleX(1); }
.sa-pillar p { color: var(--sa-bone-2); font-size: 1rem; max-width: 52ch; margin-top: var(--s-2); }
@media (min-width: 760px) {
  .sa-pillars { display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--s-8); }
  .sa-pillar:last-child { grid-column: 1 / -1; }
}

/* ---------- The journal: the one paper band ---------- */
.sa-journal {
  position: relative; overflow: hidden;
  background: var(--sa-paper); color: var(--sa-umber);
}
.sa-journal .sa-eyebrow { color: var(--sa-ember-deep); }
.sa-journal .sa-h2, .sa-journal .sa-display { color: var(--sa-umber); }
.sa-journal .sa-lede, .sa-journal .sa-body, .sa-journal p { color: var(--sa-earth); }
.sa-journal strong { color: var(--sa-umber); }
.sa-journal .sa-grain { mix-blend-mode: multiply; opacity: .035; }

.sa-journal__grid { display: grid; gap: var(--s-7); margin-top: var(--s-6); }
@media (min-width: 760px) {
  .sa-journal__grid { grid-template-columns: minmax(0, 320px) 1fr; gap: var(--s-8); align-items: start; }
}

/* The journal cover. This is a rendered image of page 1 of the print master, not
   a drawing of one -- tools/journal/cover.js produces it. The rules that used to
   draw a fake cover here (gradient, mark, title, meta) are gone with it. */
.sa-cover {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border: 1px solid rgba(58,47,40,.22);
  box-shadow: 0 18px 40px -28px rgba(43,33,29,.7);
}
.sa-parts { margin-top: var(--s-5); }
.sa-parts li {
  display: flex; gap: var(--s-4); align-items: baseline;
  padding-block: var(--s-3);
  border-bottom: 1px solid rgba(58,47,40,.16);
}
.sa-parts li:first-child { border-top: 1px solid rgba(58,47,40,.16); }
.sa-parts .n {
  font-family: var(--sa-display); font-weight: 600;
  font-size: .8125rem; letter-spacing: .1em; color: var(--sa-ember-deep); flex: none; min-width: 2.2em;
}
.sa-parts .t { font-style: italic; font-weight: 600; }

.sa-price {
  display: flex; align-items: baseline; gap: var(--s-3);
  margin-top: var(--s-6);
}
.sa-price__amount {
  font-family: var(--sa-display); font-weight: 600;
  font-size: 2.25rem; line-height: 1; letter-spacing: -.02em; color: var(--sa-umber);
  font-variant-numeric: tabular-nums;
}
.sa-price__unit { font-size: .9375rem; color: var(--sa-drift); }

.sa-buy { margin-top: var(--s-5); display: grid; gap: var(--s-4); justify-items: start; }
#buySlot:empty { display: none; }
/* Mirrors #formNotice on the homepage: same material, different accent, and it
   exists for the same reason — a control that pretends to work is worse than an
   obviously unfinished one. */
.sa-buy__notice {
  display: flex; gap: var(--s-3); align-items: flex-start;
  padding: var(--s-4) 0 0;
  border-top: 1px solid rgba(140,71,18,.34);
  font-size: .9375rem; color: var(--sa-earth); max-width: 52ch;
}
.sa-buy__notice svg { flex: none; margin-top: 4px; color: var(--sa-ember-deep); }
.sa-fine { margin-top: var(--s-4); font-size: .9375rem; color: var(--sa-drift); max-width: 52ch; }

/* ---------- Flame Method ----------
   The one warm moment, and the ONLY --sa-candle on the page. */
.sa-flame {
  position: relative; overflow: hidden;
  background: linear-gradient(to bottom, var(--sa-shadow), var(--sa-night) 72%);
  color: var(--sa-bone);
}
.sa-flame__inner { position: relative; z-index: 1; display: grid; justify-items: center; text-align: center; gap: var(--s-5); }
.sa-flame .sa-eyebrow { color: var(--sa-wood); }   /* drift is 2.63 on shadow */
/* The artwork REPLACED the bare 190px square: a figure seated at the foot of a
   tree of light, with the picture's own ground-glow sitting where the candle
   already was. The glow and the flame below are kept and composited on top, so
   the ritual still has a flame rather than only a photograph of one. */
.sa-flame__candle {
  position: relative;
  width: min(62vw, 260px);
  aspect-ratio: 720 / 1284;
  max-height: min(46vh, 380px);
  isolation: isolate;
}
/* Weighted to the foot of the tree: the seated figure and the ground-glow are
   what the Flame Method band is about, and the canopy above them is not. This
   also puts the picture's own light under the CSS candle rather than beside it. */
.sa-flame__candle .sa-column__art { object-position: 50% 96%; opacity: .92; }
.sa-flame__candle::before {
  content: ""; position: absolute; inset: 0; z-index: 2;
  background: radial-gradient(30% 22% at 50% 82%,
    rgba(217,163,95,.62), rgba(217,163,95,.16) 46%, transparent 74%);
  /* Opacity only. No scale (nothing on this page scales) and no flicker —
     stillness is the whole point of the ritual. 9s is a breath, not a flame. */
  animation: sa-breathe 9s var(--ease-soft) infinite;
}
.sa-flame__candle::after {
  content: ""; position: absolute; left: 50%; top: 80%; z-index: 3;
  width: 5px; height: 12px; translate: -50% 0;
  border-radius: 50% 50% 42% 42%;
  background: var(--sa-candle);
  box-shadow: 0 0 18px 6px rgba(217,163,95,.42);
}
@keyframes sa-breathe { 0%, 100% { opacity: .72; } 50% { opacity: 1; } }
.sa-flame__steps { display: grid; gap: var(--s-3); margin-top: var(--s-4); max-width: 46ch; }
.sa-flame__steps p { color: var(--sa-bone-2); }

/* ---------- Follow ---------- */
.sa-follow__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-6); }
.sa-follow__note { margin-top: var(--s-5); font-size: .9375rem; color: var(--sa-wood); max-width: 54ch; }

/* ---------- Footer ---------- */
.sa-footer {
  background: var(--sa-void);
  border-top: 1px solid var(--sa-shadow);
  padding-block: var(--s-8) calc(var(--s-7) + env(safe-area-inset-bottom, 0px));
}
.sa-footer__inner { display: grid; gap: var(--s-3); justify-items: center; text-align: center; }
.sa-footer__brand {
  display: inline-flex; align-items: center; gap: var(--s-3);
  min-height: 44px;
  color: var(--sa-bone-2); text-decoration: none;
  transition: color var(--t-fast) var(--ease-soft);
}
.sa-footer__brand:hover { color: var(--sa-bone); }
.sa-footer__brand img { width: 28px; height: 28px; }
.sa-footer__brand .brand__word { font-size: inherit; }
.sa-footer__meta { font-size: .875rem; color: var(--sa-wood); }   /* drift is 3.67 on void */
.sa-footer a:not(.sa-footer__brand) { color: var(--sa-wood); }

/* ---------- Phone ---------- */
@media (max-width: 759px) {
  .sa-cover { max-width: 240px; justify-self: center; }
}
@media (max-width: 699px) {
  .sa-hero { padding-block: clamp(2rem, 8vw, 3rem) clamp(3rem, 11vw, 4.5rem); }
  .sa-journal__grid { gap: var(--s-6); }
  .sa-price { margin-top: var(--s-5); }
  .sa-hero__inner { gap: var(--s-5); }
  .sa-seam { height: 24px; }
  .sa-buy { justify-items: stretch; }
  .sa-btn { width: 100%; }
  .sa-follow__actions .sa-btn { width: 100%; }
}
/* Short landscape: the mask alone would fill the viewport and push every word
   below the fold. Keyed on HEIGHT deliberately — real landscape phone widths
   (812, 844) sit above the phone breakpoint and a width query never sees them. */
@media (max-height: 560px) and (orientation: landscape) {
  .sa-hero__stage { width: min(46vw, 300px); max-height: 62vh; }
  .sa-hero__mask { width: min(22vw, 104px); margin-top: calc(var(--s-6) * -1); }
  .sa-hero { padding-block: var(--s-6) var(--s-7); }
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    /* The reveals and the word split set real per-element delays; without this
       they survive the reset and hold content back for up to a second. */
    transition-delay: 0s !important;
    animation-delay: 0s !important;
    scroll-behavior: auto !important;
  }
  .reveal { opacity: 1; transform: none; }
  .js .reveal--rise, .js .reveal--rise.is-in { opacity: 1; transform: none; clip-path: none; }
  .js .reveal--settle, .js .reveal--settle.is-in { opacity: 1; letter-spacing: .16em; }
  .js .sa-pillar .sa-pillar__rule { transform: scaleX(1); }
  .sa-hero__line .w { opacity: 1 !important; filter: none !important; }
  .sa-seam path { stroke-dashoffset: 0; }
  /* Nothing here can stop a <video> or a <canvas>: both are paused by
     js/saor.js instead, which reads the same media query. This block only
     covers the part CSS owns -- holding the poster frame steady underneath. */
  .sa-column__art { transition: none !important; }
  .sa-flame__candle::before { animation: none; opacity: .88; }
}
