/* Afters Cards base site styles.
   Base layer (overflow backstop, page ground plus texture, the skip
   link, selection colour, glow utility, print rule set) plus, from T1,
   the loader (M1) and the hero (S1), and from T2 the shared section
   text primitives, the M2 entrance system and S2 (`#what`, glow one,
   the M4 torch). Later sections land ticket by ticket per
   IMPLEMENTATION-PLAN.md §6. */

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

html,
body {
  /* Overflow backstop (lesson carried over from the pitch's AC-40). */
  overflow-x: clip;
}

body {
  margin: 0;
  min-height: 100vh;
  min-height: 100svh;
  background-color: var(--void);
  background-image: var(--weave);
  color: var(--paper);
  font-family: var(--font-text);
  font-size: var(--body);
  line-height: 1.55;
}

::selection {
  background: var(--ember);
  color: var(--void);
}

/* Grain overlay: sits above every layer, including the loader, per the
   stacking table in IMPLEMENTATION-PLAN.md §1. Texture only, never
   animated. */
#grain {
  position: fixed;
  inset: 0;
  z-index: 70;
  pointer-events: none;
  opacity: 0.14;
  background-image: var(--grain-svg);
  background-size: 240px 240px;
  background-repeat: repeat;
}

/* Skip link: visually hidden until it receives focus. */
.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 100;
  transform: translateY(-120%);
  padding: 0.75em 1.25em;
  background: var(--void);
  color: var(--paper);
  border: 1px solid var(--hair);
  font-family: var(--font-data);
  font-size: 0.875rem;
  text-decoration: none;
}

.skip-link:focus {
  transform: translateY(0);
}

/* The glow utility (IMPLEMENTATION-PLAN.md §1). S2 no longer uses it (its
   torch is the light now, see .agent/DECISIONS.md 2026-08-09); it is kept for
   S7's footer glow in T7, which sets its own bottom/left position and wraps it
   in a `.glow-wrap` (base rule further down). The containing section needs
   position: relative; overflow: clip. `.glow` is position: relative so the
   wrapper takes its size; the wrapper can then carry a drift transform without
   fighting this element's own breathe keyframe transform (the §4 M3
   wrapper/inner rule). */
.glow {
  position: relative;
  z-index: 0;
  width: min(48vw, 700px);
  aspect-ratio: 1;
  background: radial-gradient(
    closest-side,
    rgba(255, 244, 232, 0.95) 0%,
    var(--ember) 26%,
    var(--amber) 50%,
    rgba(255, 226, 200, 0.3) 68%,
    transparent 80%
  );
  filter: blur(58px);
  opacity: 0.6;
  pointer-events: none;
}

/* A slow breathe gives the glow life without motion that reads as an
   effect. Gated to non-reduced-motion. Drift (S2) is added per-section,
   on the wrapper, not this element (see the comment above). */
@media (prefers-reduced-motion: no-preference) {
  .glow {
    animation: glow-breathe 8s var(--ease-inout) infinite alternate;
  }
}

@keyframes glow-breathe {
  from {
    opacity: 0.5;
    transform: scale(0.97);
  }
  to {
    opacity: 0.68;
    transform: scale(1.03);
  }
}

@media (max-width: 900px) {
  .glow {
    width: min(64vw, 460px);
    opacity: 0.5;
  }
}

/* -----------------------------------------------------------------------
   M1, the slit loader (IMPLEMENTATION-PLAN.md §4).

   Full mode only, once per session. Hidden by default so JS-off can
   never see it (a stray opaque overlay with no script to remove it would
   blank the page forever). js/main.js decides, per load, whether to run
   it at all (sessionStorage) and drives the class changes below; the
   panel and hairline transitions are plain CSS so their timing matches
   the durations named in the plan exactly.
   ----------------------------------------------------------------------- */
#loader {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 60;
}

html.js:not(.rm) #loader {
  display: block;
}

.loader-panel {
  position: absolute;
  left: 0;
  width: 100%;
  height: 50%;
  background: var(--void);
  transition: transform 860ms var(--ease-inout);
}

.loader-panel--top {
  top: 0;
}

.loader-panel--bottom {
  bottom: 0;
}

#loader.is-open .loader-panel--top {
  transform: translateY(-101%);
}

#loader.is-open .loader-panel--bottom {
  transform: translateY(101%);
}

.loader-hairline {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 62vw;
  height: 1px;
  background: var(--ember);
  transform: translate(-50%, -50%) scaleX(0);
  transition: transform 420ms var(--ease-inout), opacity 220ms var(--ease-out);
}

#loader.is-grown .loader-hairline {
  transform: translate(-50%, -50%) scaleX(1);
}

/* Fade the slit line out the instant the panels open, so it doesn't linger
   as an orange line across the revealed hero video (founder, 2026-08-23). */
#loader.is-open .loader-hairline {
  opacity: 0;
}

/* -----------------------------------------------------------------------
   S1, the hero (IMPLEMENTATION-PLAN.md §5 S1).

   Fully composed the moment it is revealed (by the loader, or instantly
   when the loader is skipped): no entrance animation on any hero
   content, no m2 classes here.
   ----------------------------------------------------------------------- */
#hero {
  position: relative;
  overflow: clip;
  height: 100vh;
  height: 100svh;
  color: var(--paper);
}

.hero-video {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

.hero-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0.18) 100%);
  pointer-events: none;
}

.hero-wordmark {
  position: absolute;
  z-index: 2;
  top: var(--content-pad);
  left: var(--content-pad);
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-stretch: 125%;
  font-size: 0.9375rem;
  letter-spacing: 0.08em;
}

.hero-title {
  position: absolute;
  z-index: 2;
  left: var(--content-pad);
  bottom: clamp(6rem, 18vh, 9rem);
  max-width: 10ch;
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-stretch: 125%;
  font-size: var(--h1);
  line-height: 0.95;
  letter-spacing: -0.01em;
  text-wrap: balance;
}

.hero-arrow {
  position: absolute;
  z-index: 2;
  left: 50%;
  bottom: clamp(1.5rem, 4vh, 2.5rem);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  transform: translateX(-50%);
  color: var(--paper);
  transition: color var(--dur-s) var(--ease-out);
}

.hero-arrow:hover,
.hero-arrow:focus-visible {
  color: var(--ember);
}

html.js:not(.rm) .hero-arrow {
  animation: hero-arrow-bounce 1.6s var(--ease-inout) infinite alternate;
}

@keyframes hero-arrow-bounce {
  from {
    transform: translateX(-50%) translateY(0);
  }
  to {
    transform: translateX(-50%) translateY(8px);
  }
}

.hero-pause {
  display: none;
  position: absolute;
  z-index: 2;
  right: var(--content-pad);
  bottom: clamp(1.5rem, 4vh, 2.5rem);
  padding: 0.5em 0.75em;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-data);
  font-size: 0.75rem;
  color: var(--dim);
  transition: color var(--dur-s) var(--ease-out);
}

.hero-pause:hover,
.hero-pause:focus-visible {
  color: var(--paper);
}

html.js:not(.rm) .hero-pause {
  display: inline-block;
}

/* Top-right contact CTA. Courier kicker register (mirrors the wordmark
   top-left), quiet over the video until reached; hover/focus goes ember and
   nudges the chevron down to signal it scrolls to the footer. */
.hero-contact {
  position: absolute;
  z-index: 2;
  top: var(--content-pad);
  right: var(--content-pad);
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  font-family: var(--font-data);
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--paper);
  transition: color var(--dur-s) var(--ease-out);
}

.hero-contact svg {
  transition: transform var(--dur-s) var(--ease-out);
}

.hero-contact:hover,
.hero-contact:focus-visible {
  color: var(--ember);
}

.hero-contact:hover svg,
.hero-contact:focus-visible svg {
  transform: translateY(2px);
}

/* Narrow screens: the top-left wordmark and top-right CTA share one line and
   would collide below ~360px (they overlap and interleave at 320px). Shrink
   both a notch, tighten tracking and drop the CTA chevron so they clear each
   other down to 320px. */
@media (max-width: 400px) {
  .hero-wordmark,
  .hero-contact {
    font-size: 0.8125rem;
    letter-spacing: 0.04em;
  }

  .hero-contact svg {
    display: none;
  }
}

/* -----------------------------------------------------------------------
   Shared section text primitives (IMPLEMENTATION-PLAN.md §1 Type).

   The kicker, section heading, body copy and caption register are the
   same from S2 on; defined once here and reused ticket by ticket rather
   than redeclared per section. `.section-inner` is the content column
   (max-width var(--content-max), centred, side padding) that every
   section's text and media sit inside; it is `position: relative` with
   `z-index: 1` so it always paints above a section's `.glow-wrap`
   (`z-index: 0`) regardless of DOM order.
   ----------------------------------------------------------------------- */
.section-inner {
  position: relative;
  z-index: 1;
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--content-pad);
}

.kicker {
  margin: 0;
  font-family: var(--font-data);
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--dim);
}

.section-title {
  margin: 0.75em 0 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-stretch: 125%;
  font-size: var(--h2);
  line-height: 0.95;
  letter-spacing: -0.01em;
  text-wrap: balance;
  color: var(--paper);
}

.body-text {
  max-width: 34em;
  margin: 1.25em 0 0;
  font-family: var(--font-text);
  font-size: var(--body);
  line-height: 1.55;
  color: var(--paper);
}

.caption {
  margin: 1.5em 0 0;
  font-family: var(--font-data);
  font-size: 0.8125rem;
  color: var(--dim);
}

/* Visually hidden, still read by assistive tech (standard clip pattern).
   Used for the static full sentence behind S2's cycling heading word. */
.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;
}

/* S2's cycling heading word (founder request; the pitch had a scroll-driven
   version at ../pitch/js/main.js:781). js/main.js swaps `.cycle__word` on a
   timer in full mode only; reduced motion and JS-off leave it on its initial
   word ("rave"). The hidden `.cycle__sizer` reserves the widest word's width
   so a swap never reflows the heading or the copy beneath it; the animated
   word is absolutely positioned over that reserved box. This word cycle is a
   motion the plan's §0.5 does not list; it ships at founder request. */
.cycle {
  position: relative;
  display: inline-block;
  text-align: left;
}

.cycle__sizer {
  visibility: hidden;
}

/* The glyph fill stays --paper (so contrast is never photo-dependent); the
   only colour is a soft glow, ember at the core and amber in the bloom. This
   uses ember in its sanctioned "glow" role, not as a fill (§1), and ties the
   cycling word to the torch and S7 glow language. em-relative blur so it
   tracks the display font size. */
.cycle__word {
  position: absolute;
  left: 0;
  top: 0;
  color: var(--paper);
  text-shadow:
    0 0 0.28em rgba(255, 90, 54, 0.6),
    0 0 0.85em rgba(255, 178, 90, 0.45),
    0 0 1.8em rgba(255, 178, 90, 0.25);
  transition: opacity 240ms var(--ease-out);
}

/* Bloom on change: when a new word lands, js/main.js adds .is-blooming and
   this one-shot keyframe flares the glow bright and wide, then settles to the
   resting text-shadow above (the 100% stop matches it exactly, so there is no
   jump when the animation ends). Full mode only: the class is never added in
   reduced motion or JS-off, where the word does not cycle. */
@keyframes cycle-bloom {
  0% {
    text-shadow:
      0 0 0.22em rgba(255, 90, 54, 1),
      0 0 0.7em rgba(255, 178, 90, 0.85),
      0 0 1.9em rgba(255, 178, 90, 0.6);
  }
  100% {
    text-shadow:
      0 0 0.28em rgba(255, 90, 54, 0.6),
      0 0 0.85em rgba(255, 178, 90, 0.45),
      0 0 1.8em rgba(255, 178, 90, 0.25);
  }
}

.cycle__word.is-blooming {
  animation: cycle-bloom 780ms var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .cycle__word {
    transition: none;
  }
}

/* -----------------------------------------------------------------------
   M2, entrances (IMPLEMENTATION-PLAN.md §4).

   Hidden initial state, scoped so it is never applied outside full mode
   (reduced motion and JS-off both show every `.m2` element already in
   place). The `transform` property, not the standalone CSS `translate`
   property: GSAP's `y` reads and writes `transform`, and the standalone
   property would never be cleared, leaving every element sitting 24px
   low forever. This CSS runs before first paint (the head inline
   script), so there is no flash of hidden content.
   ----------------------------------------------------------------------- */
html.js:not(.rm) .m2 {
  opacity: 0;
  transform: translateY(24px);
}

/* M3, the media reveal (IMPLEMENTATION-PLAN.md §4): elements carrying
   this class scale from 0.84 to 1 as they cross the scroll trigger band
   (js/main.js's setUpReveal(), full mode only). S3 below is the first
   consumer. will-change lives here so any future `.reveal` element
   picks it up without redeclaring it per section. */
.reveal {
  will-change: transform;
}

/* Base `.glow-wrap` rule, kept for T7's S7 footer glow (S2 no longer uses a
   glow; the torch is its light now). The generic parts live here so each
   usage overrides only its own top/right or bottom/left offset. */
.glow-wrap {
  position: absolute;
  z-index: 0;
  pointer-events: none;
}

/* -----------------------------------------------------------------------
   S2, `#what`: full-bleed secondary hero with the torch as its background
   (see .agent/DECISIONS.md 2026-08-09; deviates from the plan's original
   §5 S2 "text on solid ground" and §4 M4 "reduced motion fully lit").

   Layer stack, bottom to top:
     #torch-stage .torch-base  z0  dark club image, the resting state
     #torch-stage .torch-top   z0  lit club image, revealed by the torch mask
                                    (full mode only; hidden otherwise)
     .torch-scrim              z1  legibility gradient, always on
     .section-inner            z2  the text
   The dark base + always-on scrim guarantee text contrast in every mode,
   including reduced motion and JS-off where .torch-top is display:none and
   only the dark image shows.
   ----------------------------------------------------------------------- */
#what {
  position: relative;
  overflow: clip;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  align-items: center;
  background-color: var(--void);
  /* The torch reveal is pointer-tracked on the whole section; claiming only
     pan-y keeps vertical page scroll free through a touch drag over it. */
  touch-action: pan-y;
}

#what .section-inner {
  /* Above the scrim (z1); the shared primitive is z1, made explicit here so
     the layering does not depend on DOM order. width:100% so the centred
     content column resolves against the flex parent. padding-block gives the
     vertically-centred text room against the top and bottom of the hero. */
  z-index: 2;
  width: 100%;
  padding-block: var(--section-pad-block);
}

#torch-stage {
  position: absolute;
  inset: 0;
  z-index: 0;
}

#torch-stage img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.torch-base {
  filter: brightness(0.12);
}

.torch-top {
  --tx: 50%;
  --ty: 50%;
  /* Larger than the framed-block version: the pool now plays across a whole
     viewport-height hero, so it needs more reach to read as a torch. */
  --torch-r: clamp(9rem, 28vw, 22rem);
  /* Default hidden: in reduced motion and JS-off only the dark base shows,
     which keeps the hero readable (a fully-lit club image behind near-white
     text would fail contrast). Full mode turns it on and masks it below. */
  display: none;
}

html.js:not(.rm) .torch-top {
  display: block;
  mask-image: radial-gradient(circle var(--torch-r) at var(--tx) var(--ty), black 0%, black 55%, transparent 100%);
  -webkit-mask-image: radial-gradient(circle var(--torch-r) at var(--tx) var(--ty), black 0%, black 55%, transparent 100%);
}

/* Always-on legibility scrim. Darkest on the left, where the text column
   sits, and fading to clear on the right so the torch reveal reads in the
   open space. A second bottom-up wash lifts contrast under the lower lines.
   Values are --void (14, 14, 17) at varying alpha. */
.torch-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(
      to right,
      rgba(14, 14, 17, 0.92) 0%,
      rgba(14, 14, 17, 0.78) 32%,
      rgba(14, 14, 17, 0.24) 66%,
      rgba(14, 14, 17, 0) 100%
    ),
    linear-gradient(
      to top,
      rgba(14, 14, 17, 0.55) 0%,
      rgba(14, 14, 17, 0) 45%
    );
}

@media (max-width: 768px) {
  /* Torch effect OFF on mobile (founder, 2026-08-10): there is no hover to
     drive it and a touch only lit a small circle, so the club image barely
     showed. Instead show the image as a plain, brighter background (the dark
     0.12 base was the torch's unlit state) with the torch reveal hidden. */
  .torch-base {
    filter: brightness(0.55);
  }

  html.js:not(.rm) .torch-top {
    display: none;
  }

  /* Lighter, more even scrim now the image is a full background (not a dark
     base with a lit reveal): enough to keep near-white text legible over the
     club photo, no more. */
  .torch-scrim {
    background:
      linear-gradient(
        to top,
        rgba(14, 14, 17, 0.72) 0%,
        rgba(14, 14, 17, 0.4) 45%,
        rgba(14, 14, 17, 0.32) 100%
      );
  }
}

/* -----------------------------------------------------------------------
   S3, `#object`: the card as a physical object (IMPLEMENTATION-PLAN.md §5
   S3). Denim ground, a void -> denim boundary hence the hairline above
   (§1). Adapted from ../pitch/index.html:139-211 and
   ../pitch/css/site.css's "THE CARD" / "THE FLIP" sections; not a literal
   copy - see the mode-routing and token-scoping notes below.

   Three render modes for the flip:
     - html.js:not(.rm): interactive 3D flip. .card-face becomes
       absolutely positioned inside .card-flip-inner, which carries the
       rotateY. The M3 scale (`.reveal`, above) lives on the ancestor
       .card-flip, a different node, so nothing ever gets two transforms
       (§4 M3).
     - html.rm and html:not(.js): neither is claimed by the
       `html.js:not(.rm)` override block below, so both fall through to
       the default rules beneath - front then back, stacked, fully
       visible, flip button inert (no script runs to answer its click in
       either mode).

   Card faces (front and back) are now both fixed images: the back became a
   raster on 2026-08-19 (see product/DECISIONS.md), the front always was, so
   the card no longer nests the live print-register palette it once did. .card
   keeps only --paper (its ground, shown behind an image while it loads) and
   --ink; --void inherits from the site as the card's shadow colour. The
   .flip-hint button sits outside .card and keeps this site's own tokens.
   ----------------------------------------------------------------------- */
#object {
  background-color: var(--denim);
  background-image: var(--weave);
  padding-block: var(--section-pad-block);
  border-top: 1px solid var(--hair);
}

/* Founder redesign (.agent/DECISIONS.md 2026-08-09), reworked 2026-08-23 into
   three grid siblings — heading, card, copy — so the heading can sit top-left
   while the card and the body copy centre against EACH OTHER, not the heading.
   Mobile is a single stacked column in source order (heading, card, then body),
   spaced by the 3rem gap. Scoped to #object so the shared .section-inner
   primitive is untouched elsewhere. */
#object .section-inner {
  display: grid;
  gap: 3rem;
}

@media (min-width: 768px) {
  #object .section-inner {
    /* Card column takes the larger share (up to 34rem), the copy the rest.
       The copy track carries an 18rem floor: without it, in the 768-950px band
       the fixed 34rem card + gap eat almost the whole row and the paragraph
       ladders to a few words a line. With the floor the card yields instead. */
    grid-template-columns: minmax(0, 34rem) minmax(18rem, 1fr);
    grid-template-areas:
      "head ."
      "card copy";
    column-gap: clamp(2.5rem, 6vw, 7rem);
    /* B2: clear vertical space between the section title and the card beneath
       it (the two used to touch). This is the heading-row -> card-row gap. */
    row-gap: 2.5rem;
    /* B3: card and copy centre against each other. The heading is its own row
       above, so the copy no longer centres against (heading + card) — it tracks
       the card's own height and reads as centred on the card. */
    align-items: center;
  }

  .object-head { grid-area: head; }
  .card-object { grid-area: card; }
  .object-copy { grid-area: copy; }

  /* Left-aligned on desktop to hold the consistent left position; the base
     rule below keeps the card centred in the single-column mobile stack. */
  #object .card-flip {
    margin-inline: 0;
  }
}

/* The stage holds the card and, behind it, a soft light pool (::before).
   On a near-black section a dark drop shadow is almost invisible, so the
   lift only reads once there is something lighter behind the card for the
   shadow and the card's edges to sit against. Warm-neutral, very low chroma
   (NOT an ember/amber glow, so it stays clear of the two-glow rule, §0.4);
   it is environment lighting, not a coloured glow. Static: it does not tilt
   or scale with the card, so the card moves over a fixed pool of light. */
.card-stage {
  position: relative;
}

.card-stage::before {
  content: "";
  position: absolute;
  z-index: 0;
  left: 50%;
  top: 50%;
  width: 128%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  /* Warm neutral hue (~55) rather than the old cool violet-grey (280): this
     is the one value behind the section reading "flat", nudged warm at the
     same low chroma so it's still environment light, not a coloured glow, and
     doesn't spend the two-glow budget (founder/impeccable, 2026-08-23). */
  background: radial-gradient(
    closest-side,
    oklch(0.42 0.012 55 / 0.55) 0%,
    oklch(0.32 0.01 55 / 0.32) 42%,
    transparent 72%
  );
  filter: blur(12px);
  pointer-events: none;
}

.card-flip {
  position: relative;
  z-index: 1;
  /* 34rem: the card is the one novel object in this section and needs real
     presence on desktop (CHANGES.md §5). It was temporarily dropped to 25rem
     on 2026-08-10 because the live back's address area read as an empty void
     at that size. Freezing the back as a fixed image (2026-08-19) removed that
     constraint, so the card returns to 34rem. The back raster is 1480px native,
     so at this size it still downscales and stays crisp. */
  max-width: 34rem;
  margin-inline: auto;
  perspective: 1400px;
}

/* Default / fallback layout - stacked, both faces visible. This is what
   html.rm and html:not(.js) both see, since neither is claimed by the
   html.js:not(.rm) override block further down. */
.card-flip-inner {
  display: flex;
  flex-direction: column;
  gap: 2.5rem;
}

.card {
  position: relative;
  width: 100%;
  aspect-ratio: 148 / 105;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-text);
  line-height: 1.6;
  /* Both faces are now fixed images (the back became a raster on 2026-08-19,
     see product/DECISIONS.md; the front always was), so the card no longer
     carries the back's live print palette. --paper is the card ground shown
     behind an image while it loads; --ink is the fallback text colour.
     container-type is kept so the box-shadow's cqw units below resolve as
     they did before (founder, 2026-08-10). */
  container-type: inline-size;
  --paper: oklch(0.957 0.008 88);
  --ink: oklch(0.235 0.014 62);
  /* The card's own drop shadow, cast in --void (inherited from :root, not
     redeclared) so it reads as this object's shadow on the dark ground it
     floats on. Deepened and pushed down-right to match the resting tilt's
     implied light (from the upper left), which sells the lift. cqw units so
     it scales with the card. */
  box-shadow:
    0 0.7cqw 1.4cqw oklch(from var(--void) calc(l * 0.55) c h / 0.6),
    2.2cqw 5cqw 10cqw oklch(from var(--void) calc(l * 0.5) c h / 0.62),
    4.5cqw 10cqw 22cqw oklch(from var(--void) calc(l * 0.5) c h / 0.52);
}

.card.front,
.card.back {
  overflow: hidden;
}

.card.front img,
.card.back img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
}

/* Cursor-follow tilt (founder override of §0.5's no-cursor-followers rule,
   recorded in .agent/DECISIONS.md). Its own node so the fast pointer-driven
   tilt never shares a transform with the slow flip (on .card-flip-inner) or
   the M3 scale (on .card-flip) - §4 M3. --tilt-x/--tilt-y default to a slight
   resting angle so the card still reads as an object before the pointer
   arrives; setUpCardTilt() overrides them toward the pointer on hover and
   clears them (easing back to rest) on leave. Interactive mode only: the
   reduced-motion / JS-off stacked fallback never gets these rules, so it
   stays flat. */
html.js:not(.rm) .card-tilt {
  --tilt-x: 2deg;
  --tilt-y: -7deg;
  transform-style: preserve-3d;
  transform: rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
  transition: transform 220ms var(--ease-out);
}

/* The flip. rotateY only, on its own node, so it never shares a transform
   with the tilt above or the M3 scale on .card-flip (§4 M3). */
html.js:not(.rm) .card-flip-inner {
  position: relative;
  display: block;
  aspect-ratio: 148 / 105;
  transform-style: preserve-3d;
  transition: transform var(--dur-m) var(--ease-inout);
}

html.js:not(.rm) .card-flip.is-flipped .card-flip-inner {
  transform: rotateY(180deg);
}

html.js:not(.rm) .card-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
}

html.js:not(.rm) .card-face-back {
  transform: rotateY(180deg);
}

html.js:not(.rm) .card {
  height: 100%;
}

/* The visible flip control. Site UI, not part of the card object, so it
   keeps this site's own tokens throughout (never the print palette
   scoped to .card, never --on-dark - that token does not exist here). */
/* Shared pill button (the card flip control and the contact submit are the
   same object; layout differs, the look does not). */
.flip-hint,
.contact-submit {
  padding: 0.6em 1.4em;
  border: 1px solid var(--hair);
  border-radius: 999px;
  background: transparent;
  color: var(--paper);
  font-family: var(--font-text);
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color var(--dur-s) var(--ease-out),
    color var(--dur-s) var(--ease-out),
    box-shadow var(--dur-s) var(--ease-out);
}

/* Inline submit status (role="status", aria-live). Empty by default so it
   takes no space until contact-form.js writes to it. States tint the text:
   pending/success stay quiet (dim), error warms to ember. */
.contact-status {
  margin: 1rem 0 0;
  min-height: 1.4em;
  font-family: var(--font-text);
  font-size: 0.9rem;
  color: var(--dim);
}

.contact-status[data-state="error"] {
  color: var(--ember);
}

.flip-hint {
  display: block;
  margin: 2rem auto 0;
}

/* Ember on hover/focus (warm micro-accent). Ember in its sanctioned
   interactive-hover role (§1 permits it for hover), not as a fill: the border
   and label take the ember, plus a faint ember ring so the control warms up
   when reached. */
.flip-hint:hover,
.flip-hint:focus-visible,
.contact-submit:hover,
.contact-submit:focus-visible {
  border-color: var(--ember);
  color: var(--ember);
  box-shadow: 0 0 0 1px oklch(from var(--ember) l c h / 0.35),
    0 0 1.5rem oklch(from var(--ember) l c h / 0.25);
}

/* -----------------------------------------------------------------------
   S7 #contact: the conversion + footer (IMPLEMENTATION-PLAN.md §5 S7).
   Void ground, the second sanctioned glow (bottom-left, dimmed a notch below
   S2 and static, per §1). The interest form is underline-only fields so it
   reads as ink on the page, not a shopfront input; focus lights the underline
   ember. The submit posts to Web3Forms via js/contact-form.js (no backend of
   our own); with JS off, the mailto link below is the working path.
   ----------------------------------------------------------------------- */
#contact {
  position: relative;
  overflow: clip;
  background-color: var(--void);
  background-image: var(--weave);
  padding-block: var(--section-pad-block);
}

#contact .glow-wrap {
  bottom: 6%;
  left: 4%;
}

/* Dimmed a notch below S2 and static (no breathe), per §5 S7 "static". */
#contact .glow {
  opacity: 0.42;
  animation: none;
}

@media (max-width: 768px) {
  /* On mobile the section is a tall single column, so the small bottom-left
     glow read as a stray smudge low on the page. Enlarge it and centre it
     behind the headline + form so it warms the whole block. */
  #contact .glow-wrap {
    bottom: auto;
    top: 30%;
    left: 50%;
    transform: translateX(-50%);
  }

  #contact .glow {
    width: min(125vw, 560px);
    opacity: 0.5;
  }
}

/* A single clean vertical stack (role, name, email, button), not a wrapping
   flex row — in the right column the row form laddered into a cramped 2x2
   grid. Each field is full width of the stack; the stack tops out at 26rem so
   the fields don't run uncomfortably long (founder, 2026-08-23). */
.contact-form {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 1.5rem;
  margin-top: 2.75rem;
  max-width: 26rem;
}

.contact-form .field {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.contact-form .field label {
  font-family: var(--font-data);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--dim);
}

.contact-form .field input,
.contact-form .field select {
  appearance: none;
  width: 100%;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--hair);
  border-radius: 0;
  padding: 0.5rem 0;
  color: var(--paper);
  font-family: var(--font-text);
  font-size: var(--body);
  transition: border-color var(--dur-s) var(--ease-out),
    box-shadow var(--dur-s) var(--ease-out);
}

/* The select mirrors the underline inputs; appearance:none removes the native
   caret, so a chevron is drawn in as a background image and room is left for
   it. The options themselves fall back to the OS menu (dark tokens can't style
   the open list cross-browser), so their colours are set for legibility. */
.contact-form .field select {
  padding-right: 1.4rem;
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23f2efe9' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.1rem center;
}

.contact-form .field select option {
  background: var(--void);
  color: var(--paper);
}

/* The ember underline is the visible focus indicator (native outline removed
   only because this replaces it with a clearly-visible one). A faint ember
   glow below the line matches the pill buttons' "warms up when reached"
   treatment so every interactive element focuses the same way. */
.contact-form .field input:focus,
.contact-form .field select:focus {
  outline: none;
  border-color: var(--ember);
  box-shadow: 0 1px 0.9rem -0.2rem oklch(from var(--ember) l c h / 0.4);
}

/* Start-aligned in the vertical stack: a natural-width pill under the
   full-width underline fields reads cleaner than a full-width submit bar. */
.contact-submit {
  align-self: start;
  margin-top: 0.5rem;
}

/* The intro leads with the title (no kicker above it), so the shared
   .section-title top margin is dead leading here — zero it so the title's cap
   height tops-aligns with the form's first field in the two-column layout. */
.contact-intro .section-title {
  margin-top: 0;
}

/* #contact as two columns on wider screens: the intro (title + copy) on the
   left, the form on the right; stacks below 820px (founder, 2026-08-23). The
   form's own top margin is zeroed here so it aligns with the intro's top. */
@media (min-width: 820px) {
  .contact-cols {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: clamp(2rem, 5vw, 4rem);
    align-items: start;
  }
  .contact-cols .contact-form {
    margin-top: 0;
  }
}

.contact-direct {
  margin-top: 2rem;
  font-family: var(--font-data);
  font-size: 0.875rem;
  color: var(--dim);
}

.contact-direct a {
  color: var(--paper);
  text-decoration: underline;
  transition: color var(--dur-s) var(--ease-out);
}

.contact-direct a:hover,
.contact-direct a:focus-visible {
  color: var(--ember);
}

.footer-fine {
  margin-top: 4rem;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.5rem 2rem;
  font-family: var(--font-data);
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--dim);
}

.footer-fine p {
  margin: 0;
}

/* Small print stays quiet: the Rainfall link hovers to --paper, not ember,
   and keeps its underline so colour is never the only signal (§5 S7). */
.footer-fine a {
  color: var(--dim);
  text-decoration: underline;
  transition: color var(--dur-s) var(--ease-out);
}

.footer-fine a:hover,
.footer-fine a:focus-visible {
  color: var(--paper);
}

/* Legal links + the (currently inert) Instagram mark, clustered in the middle
   of the fine-print row. */
.footer-legal {
  display: inline-flex;
  align-items: center;
  gap: 1rem;
}
.footer-ig {
  display: inline-flex;
  color: var(--dim);
}

/* -----------------------------------------------------------------------
   #audience: the two supply-side photo cards (artist / venue). Each card is
   the link; the scene is the ground, a bottom-weighted scrim carries the
   overlaid text, and a soft ember glow + deep shadow lift it off the page.
   The ember here is a deliberate, restrained spend beyond the two-glow
   budget (founder, 2026-08-23) — resting rim is faint, hover intensifies.
   ----------------------------------------------------------------------- */
#audience {
  position: relative;
  background-color: var(--void);
}

#audience .section-inner {
  padding-block: var(--section-pad-block);
}

.audience-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(1rem, 3vw, 1.75rem);
  margin-top: clamp(2rem, 5vw, 3rem);
}

@media (min-width: 720px) {
  .audience-grid {
    grid-template-columns: 1fr 1fr;
  }
}

.audience-card {
  position: relative;
  display: block;
  overflow: hidden;
  aspect-ratio: 4 / 5;
  border-radius: 16px;
  border: 1px solid var(--hair);
  text-decoration: none;
  color: var(--paper);
  box-shadow: 0 18px 48px -12px rgba(0, 0, 0, 0.6),
    0 0 26px -4px oklch(from var(--ember) l c h / 0.14);
  transition: transform var(--dur-s) var(--ease-out),
    box-shadow var(--dur-s) var(--ease-out);
}

.audience-photo {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Bottom-weighted legibility scrim, in --void so it reads as the room going
   dark rather than a grey wash. */
.audience-card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    to top,
    rgba(14, 14, 17, 0.92) 0%,
    rgba(14, 14, 17, 0.55) 42%,
    rgba(14, 14, 17, 0.1) 100%
  );
}

.audience-body {
  position: absolute;
  z-index: 2;
  inset: auto 0 0 0;
  padding: clamp(1.25rem, 3vw, 2rem);
}

.audience-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-stretch: 125%;
  font-size: clamp(1.4rem, 1rem + 1.8vw, 2rem);
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--paper);
}

.audience-copy {
  margin: 0.85em 0 0;
  max-width: 30em;
  font-family: var(--font-text);
  font-size: var(--body);
  line-height: 1.5;
  color: var(--paper);
}

.audience-cta {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  margin-top: 1.1em;
  font-family: var(--font-data);
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--amber);
}

.audience-cta svg {
  transition: transform var(--dur-s) var(--ease-out);
}

.audience-card:hover,
.audience-card:focus-visible {
  transform: translateY(-4px);
  outline: none;
  box-shadow: 0 26px 60px -12px rgba(0, 0, 0, 0.65),
    0 0 40px -4px oklch(from var(--ember) l c h / 0.3);
}

.audience-card:focus-visible {
  box-shadow: 0 0 0 2px var(--amber),
    0 26px 60px -12px rgba(0, 0, 0, 0.65),
    0 0 40px -4px oklch(from var(--ember) l c h / 0.3);
}

.audience-card:hover .audience-cta svg,
.audience-card:focus-visible .audience-cta svg {
  transform: translateX(3px);
}

@media (prefers-reduced-motion: reduce) {
  .audience-card {
    transition: none;
  }
  .audience-card:hover,
  .audience-card:focus-visible {
    transform: none;
  }
}

/* Mobile hero video reads darker than desktop: a 16:9 crowd shot cover-cropped
   into a narrow portrait frame shows a tighter, darker slice under the same
   scrim. Lift the scrim's floor and nudge video brightness on small screens
   (mirrors the torch's own mobile brightness bump). Founder/impeccable
   2026-08-23. */
@media (max-width: 768px) {
  .hero-scrim {
    background: linear-gradient(to top, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.12) 100%);
  }
  .hero-video {
    filter: brightness(1.1);
  }
}

@media print {
  #grain,
  #loader,
  video,
  .glow,
  #torch-stage,
  .torch-scrim {
    display: none !important;
  }

  /* The hero collapses to plain text on white when the background images are
     hidden; drop its full-viewport min-height so it does not print as a page
     of near-empty space. */
  #what {
    min-height: 0;
  }

  body {
    background: #fff;
    color: #000;
  }
}
