/* NO @import HERE. There used to be `@import url("../fonts/fonts.css")` on
   this line, and it cost a serialised round trip: the browser cannot discover
   fonts.css until it has fetched and begun parsing this file. Measured on
   localhost, fonts.css started 3ms AFTER site.css finished - on a real
   connection that is a whole RTT. Because the faces are `font-display: swap`,
   the hero rendered in fallback and then swapped to Bebas Neue partway through
   its own intro animation, which is what read as the headline "glitching" on
   load. fonts.css is now a plain <link> in the head, fetched in parallel.
   Do not put it back. */

/* ============================================================
   Vanguard Barbell - v1
   Structure: Barbell Brigade's order (pricing early), Westside's
   hero anatomy, PWRBLD's callout + card components.
   Measured in reference/reports/build-spec.md.
   Headline face locked to Bebas Neue 2026-08-03.
   ============================================================ */

:root {
  /* Sampled from assets/gym-photos/01 - 32,461 saturated-red pixels
     off the plates and platform; modal value #B50007.             */
  --red-deep: #b50007;   /* the literal gym red - non-text only (2.80:1) */
  --red:      #dc1420;   /* CTA fill - white on it 5.04:1 */
  --red-text: #ee2b37;   /* red TEXT - 4.75:1 on --ink */

  --ink:   #0a0a0a;
  --coal:  #131210;      /* warm-biased panel, not neutral grey */
  --paper: #e8e4dc;
  --dim:   rgba(232, 228, 220, 0.66);
  --faint: rgba(232, 228, 220, 0.42);   /* 3.43:1 - GRAPHICS ONLY, or text 24px+ */
  /* --faint at 0.42 measures 3.43:1 on --ink. That clears WCAG AA for large
     text and for non-text graphics (both 3:1) but FAILS the 4.5:1 required for
     normal-size text, and it was carrying five small-text styles: the price
     tier names, the pricing note, the gallery counter, the "coming soon" spec
     labels and the footer line. 0.60 measures 5.96:1, which passes with room
     while still reading clearly quieter than --dim (7.04:1).
     Use --faint for bars, rules and 24px+ display numerals; --faint-text for
     anything a person actually reads at body or label size. */
  --faint-text: rgba(232, 228, 220, 0.60);
  --rule:  rgba(232, 228, 220, 0.14);

  /* The competition plate colour code, straight off his own floor:
     25kg red · 20kg blue · 15kg yellow · 10kg green. Every lifter reads
     these as weights without being told; everyone else just sees colour.
     Used for the marquee markers - available for more. */
  --kg25: #d8232a;
  --kg20: #2664c4;
  --kg15: #e4c229;
  --kg10: #2f9e57;

  --f-display: "Bebas Neue", "Barlow Condensed", sans-serif;
  --f-label:   "Barlow Condensed", "Inter", sans-serif;
  --f-body:    "Inter", system-ui, sans-serif;

  --gut: clamp(1.25rem, 5vw, 4.5rem);
  --maxw: 1240px;
  --hdr: 74px;
}

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

body {
  margin: 0;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--f-body);
  font-size: 17px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
img { display: block; max-width: 100%; }
a { color: inherit; }
:where(a, button, summary):focus-visible { outline: 2px solid var(--red-text); outline-offset: 3px; }

/* `.shell` owns inline padding. Section classes must set padding-BLOCK
   only - the `padding` shorthand would silently zero it. */
.shell { max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gut); }

.skip { position: absolute; left: -9999px; background: var(--red); color: #fff; padding: .75rem 1rem; z-index: 200; }
.skip:focus { left: 1rem; top: 1rem; }

/* ---------- shared type ---------- */

.eyebrow {
  font-family: var(--f-label);
  font-size: clamp(14px, 1vw, 15px); font-weight: 700;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--red-text);
  margin: 0 0 .9rem;
  display: flex; align-items: center; gap: .75rem;
}
.eyebrow::after { content: ""; height: 1px; flex: 1; max-width: 4.5rem; background: var(--red-deep); }
.eyebrow--plain::after { display: none; }

.h2 {
  font-family: var(--f-display); font-weight: 400;
  font-size: clamp(2.1rem, 4.6vw, 3.9rem);
  line-height: 1; letter-spacing: .01em; text-transform: uppercase;
  margin: 0; text-wrap: balance;
}
.lede { max-width: 54ch; color: var(--dim); margin: 1rem 0 0; font-size: clamp(15.5px, 1.05vw, 17px); }
.lede b { color: var(--paper); font-weight: 500; }

.btn {
  display: inline-block;
  /* `z-index: 0` opens a stacking context so the ::before fill can sit at
     -1 - behind the label, but still inside the button. */
  position: relative; z-index: 0; overflow: hidden;
  font-family: var(--f-label);
  font-size: 15px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; text-decoration: none;
  padding: .95rem 1.6rem; border: 2px solid transparent;
  transition: border-color .18s ease, color .18s ease, transform .18s ease;
}
.btn::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  transform: scaleX(0); transform-origin: left;
  transition: transform .3s cubic-bezier(.2,.7,.3,1);
}
.btn:hover, .btn:focus-visible { transform: translateY(-2px); }
.btn:hover::before, .btn:focus-visible::before { transform: scaleX(1); }

/* Red is reserved for the PRIMARY action. A ghost button that fills red on
   hover reads as the main CTA the moment you touch it, which is exactly
   backwards - so secondary fills warm grey and only primary moves in red. */
.btn--primary { background: var(--red); color: #fff; border-color: var(--red); }
.btn--primary::before { background: #9e0006; }
.btn--primary:hover, .btn--primary:focus-visible { border-color: #9e0006; }

.btn--ghost { color: var(--paper); border-color: var(--rule); }
.btn--ghost::before { background: #242220; }
.btn--ghost:hover, .btn--ghost:focus-visible { border-color: var(--paper); color: var(--paper); }

/* ============================================================
   Header + mobile nav
   ============================================================ */

.masthead {
  position: fixed; inset: 0 0 auto 0; z-index: 100;
  height: var(--hdr);
  display: flex; align-items: center;
  transition: background-color .25s ease, border-color .25s ease;
  border-bottom: 1px solid transparent;
}
.masthead[data-stuck] {
  background: rgba(10, 10, 10, .92);
  backdrop-filter: blur(10px);
  border-bottom-color: var(--rule);
}
.masthead__in {
  width: 100%; max-width: var(--maxw); margin-inline: auto;
  padding-inline: var(--gut);
  display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;
}

.brand { display: flex; align-items: center; gap: .8rem; text-decoration: none; min-height: 44px; }
.brand__emblem {
  width: clamp(34px, 4vw, 44px); height: auto;
  transition: transform .3s cubic-bezier(.2,.7,.3,1);
}
.brand:hover .brand__emblem { transform: scale(1.09); }
/* Wordmark is set in the site face, never the logo's blackletter -
   that register belongs to the room and arrives via photography. */
.brand__word {
  font-family: var(--f-display); font-weight: 400;
  font-size: clamp(1.05rem, 1.4vw, 1.3rem); line-height: .92;
  letter-spacing: .04em; text-transform: uppercase;
}
.brand__word span { display: block; }
.brand__word span + span { color: var(--dim); }

.nav { display: none; align-items: center; gap: 2rem; }
@media (min-width: 900px) { .nav { display: flex; } }
.nav a {
  font-family: var(--f-label); font-size: 15px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; text-decoration: none;
  padding-block: .3rem;
}
/* Underline wipes in from the left and out to the right, so leaving a link
   reads as a different gesture from arriving on it. */
.nav a:not(.btn) { position: relative; }
.nav a:not(.btn)::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: var(--red); transform: scaleX(0); transform-origin: right;
  transition: transform .28s cubic-bezier(.2,.7,.3,1);
}
.nav a:not(.btn):hover::after,
.nav a:not(.btn):focus-visible::after { transform: scaleX(1); transform-origin: left; }
.nav .btn { padding: .6rem 1.15rem; font-size: 13.5px; }
/* Ghost until the header sticks, so the hero holds a single red CTA */
.nav .btn--primary { background: transparent; color: var(--paper); border-color: var(--rule); }
.nav .btn--primary:hover, .nav .btn--primary:focus-visible { background: var(--red); border-color: var(--red); color: #fff; }
.masthead[data-stuck] .nav .btn--primary { background: var(--red); border-color: var(--red); color: #fff; }
.masthead[data-stuck] .nav .btn--primary:hover { background: var(--red-deep); border-color: var(--red-deep); }

/* burger */
.burger {
  display: flex; flex-direction: column; justify-content: center; gap: 5px;
  width: 44px; height: 44px; padding: 0 10px;
  background: none; border: 0; cursor: pointer;
}
@media (min-width: 900px) { .burger { display: none; } }
.burger span { display: block; height: 2px; background: var(--paper); transition: transform .22s ease, opacity .18s ease; }
.burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.drawer {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(10, 10, 10, .985);
  padding: calc(var(--hdr) + 2rem) var(--gut) 2rem;
  display: flex; flex-direction: column; gap: .35rem;
  transform: translateY(-100%);
  transition: transform .3s cubic-bezier(.2,.7,.3,1);
  overflow-y: auto;
}
.drawer[data-open] { transform: none; }
@media (prefers-reduced-motion: reduce) { .drawer { transition: none; } }
.drawer a {
  font-family: var(--f-display); font-weight: 400;
  font-size: clamp(2rem, 9vw, 2.9rem); line-height: 1.12;
  text-transform: uppercase; text-decoration: none; letter-spacing: .015em;
  padding-block: .3rem; border-bottom: 1px solid var(--rule);
}
.drawer a:last-of-type { border-bottom: 0; }
.drawer .btn { margin-top: 1.5rem; align-self: flex-start; }
body[data-locked] { overflow: hidden; }

/* ============================================================
   Hero
   ============================================================ */

.hero { position: relative; min-height: 100svh; display: flex; align-items: flex-end; overflow: hidden; }
.hero__media { position: absolute; inset: 0; }
.hero__media img {
  width: 100%; height: 100%; object-fit: cover; object-position: center;
  filter: brightness(1.14) contrast(1.06) saturate(1.08);
}
.hero__media::after {
  content: ""; position: absolute; inset: 0;
  background:
    linear-gradient(to right, rgba(10,10,10,.94) 0%, rgba(10,10,10,.80) 32%, rgba(10,10,10,.30) 60%, rgba(10,10,10,.16) 100%),
    linear-gradient(to top, rgba(10,10,10,.78) 0%, rgba(10,10,10,.08) 46%, rgba(10,10,10,.42) 100%);
}
@media (max-width: 799px) {
  .hero__media::after {
    background: linear-gradient(to top,
      rgba(10,10,10,.97) 0%, rgba(10,10,10,.93) 40%, rgba(10,10,10,.62) 62%,
      rgba(10,10,10,.18) 80%, rgba(10,10,10,.55) 100%);
  }
}
.hero__inner {
  position: relative; z-index: 10; width: 100%;
  padding-block: clamp(7rem, 14vh, 11rem) clamp(2rem, 5vw, 3.5rem);
}
@media (max-width: 799px) {
  /* Backing tied to the TEXT BLOCK, not the crop - a new photo drops in
     without re-tuning. The kicker sits ~83% up this box, so the gradient
     must span the full height, not a bottom-anchored slice. */
  .hero__inner::before {
    content: ""; position: absolute; inset: 0; z-index: -1;
    background: linear-gradient(to top,
      rgba(10,10,10,.97) 0%, rgba(10,10,10,.95) 72%, rgba(10,10,10,.88) 88%, rgba(10,10,10,0) 100%);
  }
}
/* rem, never ch - `ch` measures the BODY font, not the display face */
.hero__copy { max-width: 34rem; }
@media (min-width: 800px) { .hero__copy { max-width: min(52rem, 60%); } }

.hero h1 {
  font-family: var(--f-display); font-weight: 400;
  font-size: clamp(2.55rem, 5.6vw, 5.25rem);
  line-height: .98; letter-spacing: .01em; text-transform: uppercase;
  margin: 0; text-wrap: balance;
}
.hero__sub {
  max-width: 46ch; margin: clamp(1.1rem, 2.2vw, 1.6rem) 0 0;
  font-size: clamp(15px, 1.05vw, 17px); font-weight: 300; line-height: 1.5; color: var(--dim);
}
.hero__sub b { font-weight: 500; color: var(--paper); }
@media (max-width: 799px) { .hero__sub { font-weight: 400; color: rgba(232,228,220,.82); } }

.cta-row { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: clamp(1.5rem, 3vw, 2.25rem); }
@media (max-width: 599px) {
  .cta-row { display: grid; grid-template-columns: 1fr; gap: .6rem; }
  .cta-row .btn { text-align: center; }
}

.stats {
  display: flex; flex-wrap: wrap; gap: clamp(1rem, 2.5vw, 2.5rem);
  margin: clamp(2rem, 4.5vw, 3.25rem) 0 0;
  padding-top: clamp(1.1rem, 2vw, 1.5rem);
  border-top: 1px solid var(--rule); list-style: none;
}
.stats li {
  font-family: var(--f-label); font-size: 14px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--dim);
}
.stats b { color: var(--red-text); font-weight: 700; }

/* ============================================================
   Sections
   ============================================================ */

.section { padding-block: clamp(3.75rem, 9vw, 7rem); }
.section--rule { border-top: 1px solid var(--rule); }
.section__head { margin-bottom: clamp(2rem, 4.5vw, 3.25rem); }

/* --- 2. four callouts (PWRBLD grid) --- */
.callouts { display: grid; gap: 1px; background: var(--rule); border-block: 1px solid var(--rule); }
@media (min-width: 620px) { .callouts { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1000px) { .callouts { grid-template-columns: repeat(4, 1fr); } }
/* These are the four things JJ asked to lead with, so they're built as panels
   rather than text in a grid: raised off the page background, a red rule along
   the top edge, and an oversized ghost numeral for weight. */
.callout {
  background: var(--coal);
  padding: clamp(1.7rem, 2.8vw, 2.1rem) clamp(1.35rem, 2.4vw, 1.85rem) clamp(1.9rem, 3vw, 2.2rem);
  min-height: 240px; position: relative; overflow: hidden;
  display: flex; flex-direction: column;   /* top-aligned: see the h3 min-height */
  transition: background-color .3s ease, opacity .3s ease, filter .35s ease,
              transform .34s cubic-bezier(.2,.7,.3,1), box-shadow .34s ease;
}
.callout h2, .callout p { transition: color .3s ease; }
/* Always-on top rule; it deepens and the card lifts on hover. */
.callout::after {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
  background: var(--red-deep);
  transition: background-color .3s ease;
}
/* Dock effect. The hovered card lifts out of the row: scales up, raises above
   its neighbours, throws a shadow, and the siblings recede.

   Two honest limits, both from the 1px-gap grid this sits in.
   1. A real Mac dock also MAGNIFIES the neighbours, tapering outward. Here that
      would pull each neighbour away from its own cell and open gaps along the
      hairline seams, so the siblings recede instead of swelling.
   2. Scaling UP is safe where scaling down would not be: the card grows over
      the seams and covers them rather than exposing background.
   The earlier note on this file said "a transform on the card would break the
   seams" - true for a translate or a shrink, not for a centred scale-up.

   Gated on hover:hover so a tap on a phone cannot leave a card stuck lifted
   with its siblings dimmed. The dim uses :not(:hover) rather than letting a
   later rule win a specificity TIE - relying on source order for that is
   exactly how the disciplines hover got silently killed. */
.callout { transform-origin: center; z-index: 0; }
.callout::after { transition: background-color .3s ease, height .3s ease; }
@media (hover: hover) {
  /* NOTHING happens to the siblings. They used to drop to .32 and blur(1.5px),
     which did read as depth but made three of the four cards unreadable the
     moment you moved the mouse - Darian cut it 2026-08-03. The hovered card
     now has to earn attention by itself, so its own treatment is stronger to
     compensate: lighter panel, deeper shadow, thicker rule. Do not reinstate
     a sibling dim here or on .prices. */
  /* `.callouts:hover .callout:hover` and NOT the shorter `.callout:hover`.
     .callouts is a .reveal--stagger, and `.js .reveal--stagger[data-seen] > *`
     sets `transform: none` at (0,3,0). A bare `.callout:hover` is (0,2,0) and
     loses, so the scale silently did nothing while the shadow, z-index and
     background all applied - which is the worst kind of broken, because it
     looks like it half works. This selector is (0,4,0) and wins outright.
     Same trap that killed the disciplines slide. Do not shorten it. */
  .callouts:hover .callout:hover {
    background: #262220;
    z-index: 2;
    transform: scale(1.07);
    box-shadow: 0 30px 64px -12px rgba(0, 0, 0, .95),
                0 0 0 1px rgba(232, 228, 220, .22);
  }
  .callouts:hover .callout:hover::after { background: var(--red); height: 6px; }
  .callouts:hover .callout:hover p { color: rgba(232, 228, 220, .94); }
}

/* The index sits IN FLOW as the first flex item, not absolutely in a corner,
   so nothing can crowd it. Still from data-no, so it is never selectable or
   read aloud. */
.callout::before {
  content: attr(data-no);
  font-family: var(--f-label); font-size: 12px; font-weight: 700;
  letter-spacing: .2em; color: var(--red-text);
  margin-bottom: 1.15rem;
  pointer-events: none; user-select: none;
  transition: color .3s ease;
}
.callout:hover::before { color: var(--paper); }

/* Two lines of heading are RESERVED whether a heading needs them or not, so
   all four body paragraphs start on the same line. Without this, "Open 24/7,
   365" sets one line while "Competition equipment" sets two, and the four
   cards read as four different layouts. */
.callout h2 {
  font-family: var(--f-display); font-weight: 400;
  font-size: clamp(1.8rem, 2.4vw, 2.2rem);
  line-height: 1; letter-spacing: .02em; text-transform: uppercase;
  margin: 0 0 .85rem; min-height: 2em;
}
.callout p {
  margin: 0; font-size: 15.5px; line-height: 1.55;
  color: rgba(232, 228, 220, .74);
}
/* One column below 620px: every heading fits one line there, so the reserved
   second line would just be a hole. */
@media (max-width: 619px) {
  .callout { min-height: 0; }
  .callout h2 { min-height: 0; }
}

/* --- 2b. ad-phrase marquee ------------------------------------------------
   This phrase set originates in a live Meta creative (see context/ADS.md),
   with JJ's approved website terminology overriding the older key wording.
   It is the first thing below the fold precisely because
   that is where someone arriving from an ad checks they're in the right
   place. Website-only wording overrides belong in the source comment and
   durable record; context/ADS.md remains an unchanged historical source.

   The track holds TWO identical sets and travels exactly -50%, so the loop
   seam lands on the start of set two. Both sets must stay identical in width
   or the loop will visibly jump.                                          */
.marquee { overflow: hidden; border-block: 1px solid var(--rule); background: var(--coal); }
.marquee__track { display: flex; width: max-content; padding-block: .95rem; }
.marquee__set {
  display: flex; align-items: center; gap: clamp(1.5rem, 3vw, 2.75rem);
  margin: 0; padding: 0 clamp(1.5rem, 3vw, 2.75rem) 0 0; list-style: none;
}
.marquee__set li {
  display: flex; align-items: center; gap: clamp(1.5rem, 3vw, 2.75rem);
  font-family: var(--f-display); font-weight: 400;
  font-size: clamp(1.1rem, 2.1vw, 1.55rem); line-height: 1;
  letter-spacing: .045em; text-transform: uppercase;
  color: var(--dim); white-space: nowrap;
}
/* Markers are ONE colour. These used to run the plate colour code - red 25 /
   blue 20 / yellow 15 / green 10 off his own floor - which was a nice idea and
   read as circus at a glance: four saturated hues in a band whose whole job is
   to be scanned in half a second. Darian cut it 2026-08-03, red and greyscale
   only. Don't reintroduce the four colours here; the tokens still exist in
   :root if a future component wants them. */
.marquee__set li::after {
  content: ""; flex: none; width: 7px; height: 7px;
  background: var(--red-deep); transform: rotate(45deg);
}

/* The marquee is visually dense, so the following section needs less of the
   generic desktop top gap than a normal section-to-section transition. */
@media (min-width: 900px) {
  .marquee + .section { padding-top: clamp(4rem, 6vw, 5rem); }
}
/* Every phrase reads at the same weight. There was a
   `li:first-child { color: var(--paper) }` here lighting "Mt. Pleasant's
   Strongest Gym" to 100% against the others at 66%. Because it keyed off
   :first-child of each set, the SAME phrase was always the lit one with no
   visible reason, which reads as a rendering fault rather than emphasis.
   Darian spotted it 2026-08-03. If a phrase ever needs to anchor the band,
   give it its own class - don't key off position. */

/* --- 3. memberships (PWRBLD cards) --- */
.prices { display: grid; gap: 1px; background: var(--rule); border: 1px solid var(--rule); }
@media (min-width: 700px) { .prices { grid-template-columns: repeat(3, 1fr); } }
.price {
  background: var(--ink); padding: clamp(1.9rem, 3vw, 2.5rem);
  display: flex; flex-direction: column; gap: .5rem; min-height: 300px;
  position: relative;
  transition: background-color .3s ease, opacity .3s ease, filter .35s ease,
              transform .34s cubic-bezier(.2,.7,.3,1), box-shadow .34s ease;
}
.price::after {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: var(--red); transform: scaleX(0); transform-origin: left;
  transition: transform .38s cubic-bezier(.2,.7,.3,1);
}
.price:hover { background: var(--coal); }
.price:hover::after { transform: scaleX(1); }

/* Same dock as the callouts. Two traps here, both live:
   1. .prices is a .reveal--stagger too, so a bare `.price:hover` at (0,2,0)
      would lose its transform to `.js .reveal--stagger[data-seen] > *` at
      (0,3,0) and silently do nothing. `.prices:hover .price:hover` is (0,4,0).
   2. .price--feature's red outline IS a box-shadow (inset), not a border - a
      plain outer shadow on hover would erase it. It has to be re-declared with
      both shadows in one value.
   The feature card also recedes LESS than the others when a sibling is
   hovered: dimming it to .32 takes its red outline with it, and the
   recommended plan should not disappear because you moused over Day Pass. */
.price { transform-origin: center; z-index: 0; }
@media (hover: hover) {
  /* Siblings are left alone here too - see the note on .callouts. */
  .prices:hover .price:hover {
    z-index: 2;
    transform: scale(1.07);
    background: #1b1917;
    box-shadow: 0 30px 64px -12px rgba(0, 0, 0, .95),
                0 0 0 1px rgba(232, 228, 220, .22);
  }
  .prices:hover .price--feature:hover {
    box-shadow: inset 0 0 0 2px var(--red), 0 30px 64px -12px rgba(0, 0, 0, .95);
  }
  .prices:hover .price:hover::after { height: 4px; }
}
.price__name {
  font-family: var(--f-label); font-size: 13px; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: var(--faint-text); margin: 0;
}
.price__fig {
  font-family: var(--f-display); font-weight: 400;
  font-size: clamp(3.2rem, 6vw, 4.4rem); line-height: .88; letter-spacing: .01em;
  color: var(--red-text); margin: .45rem 0 0;
  font-variant-numeric: tabular-nums;
}
.price__fig small { font-size: .34em; letter-spacing: .1em; color: var(--dim); margin-left: .25em; }
.price ul { margin: .55rem 0 0; padding: 0; list-style: none; display: grid; gap: .3rem; }
.price li { font-size: 14.5px; color: var(--dim); }
.price .btn { margin-top: auto; align-self: flex-start; }
/* The feature card already sits on --coal, so the shared :hover would be a
   no-op on it - it needs its own, one step lighter. The red edge is an inset
   shadow, not a border: a border would push this cell out of the 1px grid. */
.price--feature { background: var(--coal); box-shadow: inset 0 0 0 2px var(--red); }
.price--feature:hover { background: #1b1917; }
.price--feature .price__fig { color: var(--red); }
.price__flag {
  font-family: var(--f-label); font-size: 11px; font-weight: 700; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink); background: var(--paper);
  align-self: flex-start; padding: .12rem .45rem; margin-bottom: .3rem;
}
.prices__note { margin: 1.1rem 0 0; font-size: 14.5px; color: var(--faint-text); }

/* --- 3b. the other tiers, behind a toggle --------------------------------
   Rendered as a compact list rather than three more cards on purpose: if the
   secondary options open as equals, the toggle has bought nothing.

   The toggle is a FULL-WIDTH BAR welded to the bottom edge of .prices
   (margin-top:-1px against that grid's own 1px border), so it reads as a
   fourth row of the same table. As an inline 14px line under the grid it had
   no edge to catch the eye and was being missed. Fill on hover is #1b1917,
   not red: red is reserved for primary actions on this site. */
.more {
  margin-top: -1px; width: 100%;
  position: relative; overflow: hidden;
  display: flex; align-items: center; justify-content: center; gap: .75rem;
  background: none; cursor: pointer;
  border: 1px solid var(--rule);
  padding: clamp(1rem, 1.8vw, 1.25rem) 1.25rem;
  font-family: var(--f-label); font-size: 13.5px; font-weight: 700;
  letter-spacing: .18em; text-transform: uppercase; color: var(--paper);
  transition: border-color .25s ease;
}
.more::before {
  content: ""; position: absolute; inset: 0; background: #1b1917;
  transform: scaleX(0); transform-origin: left;
  transition: transform .4s cubic-bezier(.2,.7,.3,1);
}
.more > * { position: relative; z-index: 1; }
.more:hover, .more:focus-visible { border-color: var(--paper); }
.more:hover::before, .more:focus-visible::before { transform: scaleX(1); }
.more__n {
  font-size: 11px; color: var(--red-text);
  border: 1px solid currentColor; border-radius: 999px;
  min-width: 1.5em; padding: .1em .35em; text-align: center; letter-spacing: 0;
}
.more__chev {
  width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .3s cubic-bezier(.2,.7,.3,1);
}
.more[aria-expanded="true"] .more__chev { transform: rotate(-135deg) translate(-2px, -2px); }

/* --- attention sweeps ---
   Two things on this page are easy to walk straight past: the month-to-month
   CTA (the action we actually want) and the "Other options" bar (three whole
   tiers hidden behind it). Both get a light band that crosses them on a long
   cycle.

   Wrapped in `no-preference` rather than given a `reduce` override, because
   the fail-safe direction here is nothing at all: a shimmer that never runs
   costs a user nothing, and neither element starts hidden or depends on the
   animation completing.

   All four run on the SAME 5s period but at four different delays (0.8 / 1.2 /
   2.0 / 2.6s). Sweeping together reads as a page-wide flicker rather than as
   separate invitations - offsetting them is what makes each one land. */
@media (prefers-reduced-motion: no-preference) {
  /* The three PRIMARY calls to action - hero, most-popular tier, closing CTA -
     plus the drawer bar. Deliberately NOT the nav/drawer "Join": it is on
     screen the entire time you scroll, and a permanent shimmer in a sticky
     header is a distraction rather than a cue. Every host already has
     overflow:hidden, which is what clips the band to the button shape. */
  .hero .btn--primary::after,
  .price--feature .btn--primary::after,
  .final .btn--primary::after,
  .more::after {
    content: ""; position: absolute; top: -25%; bottom: -25%; left: 0;
    pointer-events: none;
    animation: sweep 5s ease-in-out infinite;
  }
  /* Sits above the label (the ::before fill is at -1, the label at auto) so it
     reads as light passing OVER the button, not behind it. */
  .hero .btn--primary::after,
  .price--feature .btn--primary::after,
  .final .btn--primary::after {
    width: 42%;
    background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .45), transparent);
  }
  .hero .btn--primary::after          { animation-delay: .8s; }
  .price--feature .btn--primary::after { animation-delay: 1.2s; }
  .final .btn--primary::after         { animation-delay: 2s; }
  /* z-index 2 because .more > * is already at 1. */
  .more::after {
    width: 34%; z-index: 2;
    background: linear-gradient(100deg, transparent, rgba(232, 228, 220, .14), transparent);
    animation-delay: 2.6s;
  }
  /* Once it has been opened the bar has done its job; a drawer that keeps
     advertising itself after you have opened it is just noise. */
  .more[aria-expanded="true"]::after { animation: none; opacity: 0; }

  /* A second, quieter cue on the bar - the chevron dips as if pulling down.
     Only while closed: the moment aria-expanded flips this selector stops
     matching, the animation is dropped, and the existing transition carries
     the chevron round to its open position. */
  .more[aria-expanded="false"] .more__chev { animation: chev-nudge 3.4s ease-in-out 2s infinite; }
}

/* The drawer expands rather than appearing. grid-template-rows 0fr -> 1fr is
   the only way to transition to an element's INTRINSIC height - max-height
   needs a guessed number, and guessing high makes the close look like it
   stalls before anything moves.

   Collapsed state is scoped to .js on purpose: with no script the wrapper
   keeps its natural height and every tier is visible and reachable, which is
   the same fail-safe rule the reveals follow. */
.plans-wrap > .plans { min-height: 0; }
.js .plans-wrap {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows .55s cubic-bezier(.22,.72,.26,1);
}
/* clip-path, NOT overflow:hidden. The drawer only needs to clip VERTICALLY -
   that is the whole job of the 0fr->1fr collapse. overflow:hidden also clipped
   horizontally, which would have sliced the hover panel's gutter bleed and its
   whole box-shadow off flush at both ends of every row. inset(0 -100vw) clips
   exactly the two edges the collapse needs and leaves the sides open. */
.js .plans-wrap > .plans { clip-path: inset(0 -100vw); }
.js .plans-wrap[data-open] { grid-template-rows: 1fr; }

/* -1px so the drawer's own top rule sits ON the bar's bottom rule, not below
   it - otherwise opening the drawer opens a 1px double line. */
.plans { margin-top: -1px; border-top: 1px solid var(--rule); }

/* The rows introduce themselves in sequence as the box opens, so it reads as
   three things arriving rather than one block appearing at full height.
   Closing runs with no delay, otherwise the last row hangs behind while the
   box is already collapsing over it. */
.js .plans-wrap .plan {
  opacity: 0; transform: translateY(12px);
  transition: opacity .35s ease, transform .4s cubic-bezier(.2,.7,.3,1);
}
.js .plans-wrap[data-open] .plan {
  opacity: 1; transform: none;
  transition-delay: calc(var(--i, 0) * 90ms + 130ms);
}
.plan {
  display: grid; gap: .15rem .5rem; padding: 1.15rem 0;
  border-bottom: 1px solid var(--rule); align-items: baseline;
  position: relative; z-index: 0;
}
/* The dock lift lives on a PSEUDO-ELEMENT, not on .plan itself, and that is
   deliberate. .plan already owns `transform` for the drawer's open stagger, and
   `.js .plans-wrap[data-open] .plan` sets a transition-delay of up to 310ms on
   it - so a hover written onto .plan would inherit that delay and the card
   would lift a third of a second after the cursor arrived, then sink late on
   the way out. The pseudo has its own transition and nothing else touches it.

   Bleeds 1.25rem into the gutter so the panel reads as a slab lifting out of
   the table rather than a tinted row; the clip-path change above is what lets
   that bleed and the shadow actually show. */
.plan::before {
  content: ""; position: absolute; z-index: -1;
  inset: -1px -1.25rem;
  /* Same panel value the callouts use. #1b1917 was tried first and measured
     correct while reading as nothing - against a near-black section it is not
     enough separation to say "this row lifted". Contrast, not just geometry. */
  background: #262220;
  box-shadow: 0 20px 46px -14px rgba(0, 0, 0, .95),
              0 0 0 1px rgba(232, 228, 220, .22);
  opacity: 0; transform: scale(.985);
  transition: opacity .26s ease, transform .32s cubic-bezier(.2, .7, .3, 1);
  pointer-events: none;
}
@media (hover: hover) {
  /* Same rule as the callouts and the price cards: the hovered row comes
     forward and NOTHING happens to its siblings. Do not add a dim here. */
  .plans:hover .plan:not(.plan--paused):hover { z-index: 2; }
  .plans:hover .plan:not(.plan--paused):hover::before { opacity: 1; transform: none; }
  .plans:hover .plan:not(.plan--paused):hover .plan__go { border-bottom-color: var(--red); }
  .plans:hover .plan:not(.plan--paused):hover .plan__note { color: rgba(232, 228, 220, .82); }
}
@media (min-width: 700px) {
  .plan { grid-template-columns: 1fr auto 1.4fr auto; gap: 1.5rem; }
}
.plan__name {
  margin: 0; font-family: var(--f-label); font-size: 13px; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: var(--paper);
}
.plan__fig {
  margin: 0; font-family: var(--f-display); font-weight: 400;
  font-size: 1.9rem; line-height: 1; color: var(--red-text);
  font-variant-numeric: tabular-nums;
}
.plan__fig small { font-size: .4em; letter-spacing: .1em; color: var(--dim); margin-left: .25em; }
.plan__note { margin: 0; font-size: 14.5px; color: var(--dim); transition: color .26s ease; }
.plan__go {
  font-family: var(--f-label); font-size: 13px; font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase; text-decoration: none;
  color: var(--paper); white-space: nowrap;
  border-bottom: 2px solid var(--rule); padding-bottom: 2px;
  transition: border-color .25s ease;
}
.plan__go:hover { border-bottom-color: var(--red); }

/* Keep an expired seasonal offer discoverable without making it look buyable. */
.js .plans-wrap[data-open] .plan.plan--paused,
.plan--paused { opacity: .42; }
.plan--paused::before { display: none; }
.plan--paused .plan__go {
  color: var(--dim); border-bottom-color: var(--rule); cursor: default;
}

/* --- 4. equipment --- */
.split { display: grid; gap: clamp(2rem, 5vw, 3.5rem); align-items: start; }
@media (min-width: 900px) { .split { grid-template-columns: 1.05fr .95fr; } }
.spec { margin: 0; display: grid; gap: 0; border-top: 1px solid var(--rule); }
.spec > div {
  position: relative;
  display: grid; gap: .15rem .5rem;
  padding: 1rem 0 1rem .9rem;
  border-bottom: 1px solid var(--rule);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  transition: background-color .25s ease;
}
/* Red spine marks the row driving the stage. Absolute, not a border, so
   the row's text never shifts between states. */
.spec > div::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--red); transform: scaleY(0); transform-origin: top;
  transition: transform .3s cubic-bezier(.2,.7,.3,1);
}
.spec > div:hover { background: rgba(232, 228, 220, .05); }
.spec > div:hover::before { transform: scaleY(1); }
.spec dd { transition: color .25s ease; }
.spec > div:hover dd { color: var(--paper); }
@media (min-width: 520px) { .spec > div { grid-template-columns: 8.5rem 1fr; gap: 1.25rem; align-items: baseline; } }

/* --- 4b. photo stage ------------------------------------------------------
   Eleven stacked photos as ONE continuous gallery, deliberately decoupled from
   the spec list beside it: with only eleven shots the per-category sets
   overlapped badly and implied each photo evidenced the row you were on.
   Images carry data-src and are hydrated one frame ahead, so landing on the
   page costs one photo, not eleven. */
.stage {
  position: relative; margin: 0; overflow: hidden;
  background: var(--coal); border: 1px solid var(--rule);
  aspect-ratio: 3 / 4;
}
@media (min-width: 900px) {
  .stage { aspect-ratio: 4 / 5; position: sticky; top: calc(var(--hdr) + 2rem); }
}
/* THREE EXPLICIT TIERS, and they matter more than they look.
   A shot that is neither incoming nor outgoing can still be on screen: click
   again before a transition settles and the previous outgoing frame loses
   data-out, which strips its animation, so it snaps from ±58% back to dead
   centre at ~96% opacity and fades for .9s. Measured: two such stale frames
   visible during rapid clicking.

   With the stale frame and the real outgoing BOTH at z-index 0/auto the tie
   broke on DOM order, and the images are indexed 0..10 ascending:
     forward  from k: outgoing k, stale k-1 -> stale is earlier, loses  ✓
     backward from k: outgoing k, stale k+1 -> stale is LATER,  WINS    ✗
   So going backward you saw a stale photograph painted over the correct one,
   and forward never showed it. That, not the animation, was the "backward is
   glitchy" report. Since incoming and outgoing always cover the full frame
   between them, a tier-0 stale frame is now not merely lower but invisible. */
.stage__img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0; z-index: 0;
}
.stage__img[data-on] { opacity: 1; z-index: 2; }

/* The shot being pushed OUT by an automatic advance. Held opaque, ABOVE any stale frame
   and BELOW the incoming one: the two overlap by up to 42% at the end of the
   push, and the incoming has to be the one painting over that overlap or the
   outgoing shot would reappear across the frame just before it snaps away. */
.stage__img[data-out] { opacity: 1; z-index: 1; }

/* Covers the image so a click anywhere advances, but sits BELOW the caption
   so the dot buttons stay clickable. */
.stage__advance {
  position: absolute; inset: 0; z-index: 2;
  background: none; border: 0; padding: 0; cursor: pointer;
}

/* Explicit forward/back. Above .stage__advance (z-index 2) so the back arrow
   is not swallowed by the advance-anywhere layer underneath it. Each carries
   its own edge scrim because these sit over photography that runs from black
   floor to blown-out fluorescent tube - a bare chevron survives neither. */
.stage__arrow {
  position: absolute; top: 0; bottom: 0; z-index: 3;
  width: clamp(52px, 12%, 78px);
  display: flex; align-items: center;
  background: none; border: 0; padding: 0; cursor: pointer;
  opacity: .72; transition: opacity .3s ease;
}
.stage__arrow::before {
  content: ""; position: absolute; inset: 0;
  transition: opacity .3s ease; opacity: .55;
}
.stage__arrow--prev { left: 0; justify-content: flex-start; padding-left: clamp(.7rem, 2%, 1.1rem); }
.stage__arrow--next { right: 0; justify-content: flex-end; padding-right: clamp(.7rem, 2%, 1.1rem); }
.stage__arrow--prev::before { background: linear-gradient(90deg, rgba(10,10,10,.82), rgba(10,10,10,0)); }
.stage__arrow--next::before { background: linear-gradient(270deg, rgba(10,10,10,.82), rgba(10,10,10,0)); }
.stage__arrow i {
  position: relative; display: block; width: 13px; height: 13px;
  border-top: 2px solid var(--paper); border-right: 2px solid var(--paper);
  transition: transform .3s cubic-bezier(.2,.7,.3,1);
}
.stage__arrow--next i { transform: rotate(45deg); }
.stage__arrow--prev i { transform: rotate(-135deg); }
.stage__arrow:hover, .stage__arrow:focus-visible { opacity: 1; }
.stage__arrow:hover::before, .stage__arrow:focus-visible::before { opacity: 1; }
.stage__arrow--next:hover i, .stage__arrow--next:focus-visible i { transform: rotate(45deg) translate(2px, -2px); }
.stage__arrow--prev:hover i, .stage__arrow--prev:focus-visible i { transform: rotate(-135deg) translate(2px, -2px); }
.stage__cap {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 4;
  display: flex; align-items: center; justify-content: space-between; gap: .75rem;
  padding: 1.6rem .9rem .8rem;
  background: linear-gradient(to top, rgba(10,10,10,.9), rgba(10,10,10,0));
  pointer-events: none;                 /* only the dots inside take clicks */
}
.stage__count {
  font-family: var(--f-label); font-size: 12px; font-weight: 700;
  letter-spacing: .16em; color: var(--faint-text); flex: none;
}
.stage__count b { color: var(--paper); font-weight: 700; }
.stage__dots { display: flex; gap: .3rem; pointer-events: auto; }
/* 24x30 hit area around a 14x2 bar. The button was 14px wide, which fails
   WCAG 2.2 AA target size (2.5.8, 24x24 minimum) - and it could not fall back
   to the spacing exception either, because at a 4.8px gap the 24px circles
   around adjacent dots overlap. The BAR stays 14px so nothing looks different;
   only the button around it grew. Eleven of them now measure
   11*24 + 10*4.8 = 312px, which still fits the 335px stage. */
.stage__dots button {
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 30px; padding: 0;
  background: none; border: 0; cursor: pointer;
}
.stage__dots button::before {
  content: ""; display: block; width: 14px; height: 2px; background: var(--faint);
  transition: background-color .3s ease, transform .3s ease;
}
.stage__dots button:hover::before { background: var(--paper); }
.stage__dots button[aria-current]::before { background: var(--red); transform: scaleY(2); }
.spec dt {
  font-family: var(--f-label); font-size: 13px; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: var(--red-text);
}
.spec dd { margin: 0; font-size: 15.5px; color: var(--dim); }
.spec__soon dt { color: var(--faint-text); }

/* --- 5. who trains here --- */
.who { position: relative; overflow: hidden; }
.who__media { position: absolute; inset: 0; }
.who__media img { width: 100%; height: 100%; object-fit: cover; filter: brightness(.9); }
.who__media::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to right, rgba(10,10,10,.95) 0%, rgba(10,10,10,.86) 45%, rgba(10,10,10,.55) 100%);
}
/* A left-to-right scrim does nothing on a narrow screen - the copy spans the
   full width there, so it needs a flat wash instead. */
@media (max-width: 899px) {
  .who__media::after {
    background: linear-gradient(to bottom, rgba(10,10,10,.90) 0%, rgba(10,10,10,.93) 100%);
  }
}
.who .shell { position: relative; z-index: 2; }
.who__copy { max-width: 48rem; }

/* The four disciplines, set as display type. Hovering one hollows the other
   three out to outlined type and draws a red rule under the live row, so the
   list reads as a set of choices rather than four labels. */
.disc { margin: clamp(2rem, 4vw, 3rem) 0 0; padding: 0; list-style: none; }
.disc li {
  position: relative;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: clamp(.9rem, 2vw, 1.6rem);
  padding: clamp(.5rem, 1.2vw, .8rem) 0;
  border-top: 1px solid var(--rule);
}
.disc li:last-child { border-bottom: 1px solid var(--rule); }
/* Sits ON the next row's border-top (bottom:-1px), so the red replaces the
   hairline rather than stacking a second line beneath it. */
.disc li::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
  background: var(--red);
  transform: scaleX(0); transform-origin: left;
  transition: transform .45s cubic-bezier(.2,.7,.3,1);
}
/* The hover slide lives on the INNER elements, never on the <li>.
   .disc is a .reveal--stagger, and the stagger's reset rule
   `.js .reveal--stagger[data-seen] > *  { transform: none }` is (0,3,0) -
   it outranks `.disc li:hover` at (0,2,1) and silently killed the slide.
   The stagger only ever touches direct children, so i/span are safe. */
/* Set in the DISPLAY face, not the label face, and large enough to read as an
   index rather than as a caption. At 12px label type these were a footnote
   beside a 57px word; on the display face at roughly half the word's size they
   become part of the same typographic system. Still --faint so they order the
   list without competing with it, and still red on hover. */
.disc i {
  font-family: var(--f-display); font-style: normal; font-weight: 400;
  font-size: clamp(1.45rem, 2.7vw, 2.05rem); line-height: 1;
  letter-spacing: .04em;
  color: var(--faint); flex: none;
  font-variant-numeric: tabular-nums;
  transition: color .3s ease, transform .35s cubic-bezier(.2,.7,.3,1);
}
.disc span {
  font-family: var(--f-display); font-weight: 400;
  font-size: clamp(2.1rem, 5.2vw, 3.6rem); line-height: 1;
  letter-spacing: .015em; text-transform: uppercase;
}
/* What is actually in the building for each discipline. Below the hover
   breakpoint this is a plain line under the word and always readable - a
   touch user gets the information, not a hidden state. */
.disc em {
  font-style: normal; flex: 1 0 100%;
  margin: .3rem 0 .15rem; font-size: 14px; line-height: 1.5; color: var(--dim);
}

@media (hover: hover) and (min-width: 760px) {
  /* Reserved on the same line and merely invisible, never display:none - the
     row height must not change when you hover it. */
  .disc em {
    flex: 0 1 auto; margin: 0 0 0 auto;
    text-align: right; max-width: 17em;
    /* Two lines RESERVED whether a line needs them or not - same reason the
       callout headings reserve two. Without it the two detail lines that wrap
       make rows 01 and 04 15px taller than 02 and 03, and four rows of three
       different heights reads as a mistake rather than a rhythm. */
    min-height: 3em;
    opacity: 0; transform: translateX(-10px);
    transition: opacity .35s ease, transform .4s cubic-bezier(.2,.7,.3,1);
  }
  .disc li:hover em { opacity: 1; transform: none; }
}
@media (hover: hover) {
  .disc li:hover i, .disc li:hover span { transform: translateX(clamp(.35rem, 1.2vw, .9rem)); }
  .disc li:hover::after { transform: scaleX(1); }
  .disc li:hover i { color: var(--red-text); }
  /* The index dim on non-hovered rows is gone for the same reason as the card
     dims. The hollow-out below is KEPT: it is a different treatment, not a
     fade, and it is the section's signature. Say the word and it goes too. */
}
/* Hollowing the other three is the whole gesture, so it is gated on the
   feature: without -webkit-text-stroke, color:transparent would simply erase
   three of the four words. Falls back to the plain dim instead. */
@supports (-webkit-text-stroke: 1px red) {
  @media (hover: hover) {
    .disc span {
      -webkit-text-stroke: 1px transparent;
      transition: color .35s ease, -webkit-text-stroke-color .35s ease,
                  transform .35s cubic-bezier(.2,.7,.3,1);
    }
    .disc:hover li:not(:hover) span {
      color: transparent;
      -webkit-text-stroke-color: rgba(232, 228, 220, .3);
    }
    .disc:hover li:not(:hover) em { color: var(--faint); }
  }
}
@supports not (-webkit-text-stroke: 1px red) {
  @media (hover: hover) {
    .disc span { transition: opacity .3s ease, transform .35s cubic-bezier(.2,.7,.3,1); }
    .disc:hover li:not(:hover) span { opacity: .32; }
  }
}

/* --- 6. location ---
   Two graphics, both pure CSS, both deliberately non-literal. We have no
   verified coordinates for the unit and JJ knows this geography cold, so a
   drawn map would be a claim we cannot back; a real map tile would be the
   first third-party request on a page that currently makes zero. */
/* The two columns' first headings have to start on the same line, and the left
   one is pushed down by the survey tape sitting above it. So the tape's height
   and its gap are NAMED here and the right column reuses them, rather than a
   second hardcoded number guessing at the sum. Measured before this: FIND US
   at 4325.34, HOURS at 4314.15 - 11.19px apart, which is exactly
   (16 + 27.2) - 32. Change the tape and the hours follow it. */
.loc {
  display: grid; gap: clamp(2rem, 5vw, 3.5rem);
  --tape-h: 16px;
  --tape-gap: clamp(1.1rem, 2.2vw, 1.7rem);
  /* Declared HERE, not on .neon, because the paragraph below the sign needs the
     same two numbers to know how far to get out of its way, and a custom
     property set on .neon is invisible to its siblings. */
  --neon-w: clamp(9rem, 19vw, 16rem);
  --neon-h: calc(var(--neon-w) / 2.009);   /* viewBox 316.2 / 157.41 */
  --hours-pair-h: 66.5px;                  /* HOURS 20.1 + .9rem 14.4 + ALWAYS OPEN 32 */
  /* Glow radii, as variables so they can SHRINK WITH THE SIGN. On mobile the
     sign is ~117px wide and the outer radius was a fixed 130px - a blur bigger
     than the element it surrounds. Chromium coped; WebKit refused to rasterise
     it and painted nothing at all, so the 24/7 was invisible on every iOS
     browser while desktop looked perfect. Keep these proportional. */
  --g1: 4px; --g2: 13px; --g3: 34px; --g4: 70px; --g5: 130px;
}
@media (min-width: 900px) { .loc { grid-template-columns: 1fr 1fr; align-items: start; } }

/* Survey tape above the address: minor ticks every 11px, majors every 55px.
   Reads as "a measured position" without drawing one. index.html drives
   background-position-x from the scroll; the two layers share one offset so
   they stay a single ruler. Speed lives there, not here. */
.tape {
  display: block; height: var(--tape-h); margin-bottom: var(--tape-gap);
  background:
    repeating-linear-gradient(90deg, var(--rule) 0 1px, transparent 1px 11px) left bottom / 100% 6px repeat-x,
    repeating-linear-gradient(90deg, var(--rule) 0 1px, transparent 1px 55px) left bottom / 100% 16px repeat-x;
}

/* Neon 24/7 behind the hours. Outlined, never filled - a solid slab of red at
   this size would out-shout "Always open" sitting on top of it, and the whole
   point is that the hours stay the thing you read. */
.loc__when { position: relative; }
/* Sized off the space that actually exists, not off the viewport. Two things
   constrain it and both were measured: horizontally the right column is
   barely wider than "ALWAYS OPEN" itself just past the 900px two-column
   breakpoint (a 30vw neon landed 31px inside that word at 920px), and
   vertically the paragraph below starts about 130px down, so height plus the
   negative top has to stay under that.

   Deliberately ONE rule with no breakpoint. A tight-band media query fixed
   920px but put a 37px step at its own edge, and at 1230px the neon jumped
   back up and clipped the paragraph by 5px - the fix had simply moved the
   bug to the boundary. 19vw capped at 16rem clears everything from 500 to
   1680. The cap is 16 and not 17 because the outer drop-shadow throws ~22px:
   at 17rem the box cleared the paragraph by 10px and the GLOW still landed on
   it. Clearance is measured to the ink, so it has to beat the shadow radius,
   not zero. Re-run the width sweep if this changes. */
/* CENTRED ON THE HOURS BLOCK, not floated above it. It used to sit at
   top: -4.4rem, which put its top 74px ABOVE the column's own top edge - it was
   not in the hours block at all, it hung over the section boundary, leaving a
   diagonal void between the sign and the words it labels.

   The maths, all of it measured rather than eyeballed:
     the HOURS eyebrow starts at (--tape-h + --tape-gap) below the column top,
     because .hours reuses the tape offset to line up with FIND US opposite;
     the eyebrow + its .9rem margin + ALWAYS OPEN = 20.1 + 14.4 + 32 = 66.5px;
     so the pair's centre is (--tape-h + --tape-gap) + 33.25px,
     and the sign's own top is that, minus half the sign's height.

   NOT done with transform: translateY(-50%), which is the obvious way and is
   wrong here. neon-shake animates `transform` on this exact element, so the
   animation would replace the centring outright the moment it starts at 3.7s
   and the sign would jump up half its own height. The height is derived from
   the width instead, via the viewBox ratio, so this stays centred at every
   width without touching transform.

   --neon-h tracks the viewBox: 316.2 / 157.41 = 2.009. Re-derive it if the
   viewBox changes, and re-measure 66.5px if the hours type ever changes. */
.neon {
  position: absolute; right: 0; z-index: 0;
  top: calc(var(--tape-h) + var(--tape-gap) + var(--hours-pair-h) / 2 - var(--neon-h) / 2);
  width: var(--neon-w); height: auto;
  pointer-events: none; overflow: visible;
}
/* Below the two-column breakpoint the columns STACK, and an absolutely
   positioned sign pulled up 4.4rem no longer hangs in its own empty column -
   it hangs over the bottom of the column above, landing squarely on the
   "Open in Maps" button. Measured on a 390px viewport: the sign overlapped
   the button by most of its height.

   So on mobile it stops being absolute and simply becomes the first thing in
   the hours column, right-aligned. Nothing overlaps because nothing is pulled
   out of flow.

   Stacking it ABOVE the hours block was not enough, and lopsided margins were
   not enough either. Both were tried. The sign is right-aligned and HOURS is
   left-aligned, so however tight the vertical gap got, an empty diagonal
   triangle stayed between the sign's lower-left and the word HOURS - the exact
   thing that was wrong on desktop before it was centred.

   So mobile now does what desktop does: the sign sits BESIDE the hours block,
   vertically centred against it, rather than above it. Two columns inside the
   hours column, with the paragraph spanning both underneath. Same relationship
   at every width, and the diagonal gap has nowhere to form. */
@media (max-width: 899px) {
  /* ~45% of the desktop radii, matching the ~45% sign width. */
  .loc { --g1: 2px; --g2: 6px; --g3: 15px; --g4: 31px; --g5: 58px; }
  .loc__when {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    column-gap: clamp(.75rem, 3vw, 1.5rem);
  }
  /* Height is stated, not left to `auto`. With height:auto the SVG's box
     resolved to 61.9px where the viewBox ratio predicts 58.7, and the grid
     centred that fuzzy box - leaving the sign 8.5px low against HOURS, which
     is exactly the kind of nearly-right that reads as sloppy. Deriving the
     height from the width makes the box deterministic and the centring exact. */
  .neon {
    --neon-w-m: clamp(6.5rem, 30vw, 10rem);
    position: static;
    display: block;
    grid-column: 2; grid-row: 1;
    width: var(--neon-w-m);
    height: calc(var(--neon-w-m) / 2.009);
    margin: 0;
  }
  .loc__when .hours { grid-column: 1; grid-row: 1; margin-top: 0; }
  .loc__when .lede  { grid-column: 1 / -1; grid-row: 2; }
}
.neon text {
  font-family: var(--f-display); font-weight: 400; font-size: 118px;
  fill: none; stroke-linejoin: round;
}
/* Deliberately FATTER than the lit core, so the red sits inside the glass the
   way a real tube does rather than next to it. Warm-dark, not neutral black -
   unlit neon glass over a dark wall still picks up a little colour. */
.neon__glass { stroke: #2b2321; stroke-width: 4.2; }
.neon__lit   { stroke: var(--red); stroke-width: 2.6; }
/* The core burns inside the red, so it must be thinner than it and sit on top.
   Not pure white - a warm pink-white, because a neutral white core over red
   reads as a printing error rather than as heat. */
.neon__core  { stroke: #ffdad4; stroke-width: .9; }
.loc__when > *:not(.neon) { position: relative; z-index: 1; }

/* Inline hero neon ---------------------------------------------------------
   This wrapper replaces only the four glyphs in the existing H1. It inherits
   the headline's font size and line height; the SVG inside it is a runtime
   clone of the Hours sign, so its tube metrics and WebKit-safe tspans cannot
   drift apart. The wrapper owns layout while the inner SVG owns motion. */
.hero__neon {
  position: relative; display: inline-block;
  width: 2.17em; height: 0; margin-right: .08em;
  line-height: 0; vertical-align: baseline;
  /* Same glow-to-sign proportions as the Hours version, scaled in em so the
     bloom shrinks with the responsive headline instead of overwhelming it. */
  --g1: .025em; --g2: .08125em; --g3: .2125em;
  --g4: .4375em; --g5: .8125em;
}
.hero__neon[data-ready] .hero__neon-label {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
/* These declarations intentionally beat the location-only .neon geometry.
   The sign stays inside the inline wrapper, so its flicker and optical
   transform cannot alter the H1's line box or move surrounding copy. */
.hero__neon .hero__neon-svg {
  position: absolute; top: auto; right: 0; bottom: -.11em; left: 0;
  display: block; width: 100%; height: 1.08em;
  transform: translateY(.038em) scaleY(1.18); transform-origin: 50% 0;
  pointer-events: none; overflow: visible;
}

.addr { font-style: normal; }
.addr__line {
  font-family: var(--f-display); font-weight: 400;
  font-size: clamp(1.7rem, 3.4vw, 2.5rem); line-height: 1.04;
  letter-spacing: .015em; text-transform: uppercase; margin: 0 0 .6rem;
}
.addr__meta { color: var(--dim); margin: 0; }
/* No border-top. There used to be a full-column hairline here, and once the
   24/7 went neon it ran straight through the numerals like a wire across a
   sign. The <dt> now carries the same short red eyebrow rule as "Find us" on
   the other column, so the two headings match instead of one being a label and
   the other a divider. */
.hours { margin-top: 2rem; }
/* Two-column only. Stacked below 900px there is no tape above this column, so
   the plain 2rem is correct there and matching it would open a gap. */
@media (min-width: 900px) {
  .hours { margin-top: calc(var(--tape-h) + var(--tape-gap)); }
}
/* Stacked, the sign now sits BESIDE this block in a two-column grid rather
   than above it, so the old 2rem top margin would just push the whole row down
   away from the address. Zeroed in the grid block above, which is scoped
   `.loc__when .hours` so it beats this bare `.hours` on specificity rather than
   relying on source order. */
/* Centring the sign on the hours pair drops it 74px lower than it used to sit,
   and the sign is 135px tall inside a 175px column - so the paragraph has to
   move or the text runs under it. MEASURED at 1440 before this rule: the
   paragraph's first line reached x=1206.6 while the sign's left edge was at
   x=1002.6, so 200px of that line was underneath the glass.

   Derived, not guessed. The sign's bottom sits at
     (--tape-h + --tape-gap) + --hours-pair-h/2 + --neon-h/2
   and ALWAYS OPEN's bottom sits at
     (--tape-h + --tape-gap) + --hours-pair-h
   so the paragraph has to clear the difference between them, which reduces to
   --neon-h/2 - --hours-pair-h/2, plus real breathing room on top. It scales
   with the sign because both terms do.

   28px is clearance to the BOX. The outer glow throws further and will wash the
   first line softly - that is intended and reads as light falling on the copy.
   If it ever reads as a smudge instead, raise this number, do not shrink the
   glow. Desktop only: below 900px the sign is in flow and this cannot apply. */
@media (min-width: 900px) {
  .loc__when .lede {
    margin-top: calc(var(--neon-h) / 2 - var(--hours-pair-h) / 2 + 28px);
  }
}
.hours dt {
  font-family: var(--f-label); font-size: 13px; font-weight: 700;
  letter-spacing: .18em; text-transform: uppercase; color: var(--red-text);
  margin: 0 0 .9rem;
  display: flex; align-items: center; gap: .75rem;
}
/* Deliberately narrower than .eyebrow's 4.5rem: this rule sits under the neon,
   and a long one starts reading as the divider we just removed. */
.hours dt::after {
  content: ""; height: 1px; flex: 1; max-width: 2.75rem; background: var(--red-deep);
}
.hours dd {
  margin: .3rem 0 0;
  font-family: var(--f-display); font-size: 2rem; line-height: 1;
  letter-spacing: .02em; text-transform: uppercase;
}

/* --- 7. footer CTA --- */
.final { position: relative; overflow: hidden; text-align: center; }
.final__media { position: absolute; inset: 0; }
.final__media img { width: 100%; height: 100%; object-fit: cover; }
.final__media::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to bottom, rgba(10,10,10,.92) 0%, rgba(10,10,10,.80) 50%, rgba(10,10,10,.95) 100%);
}
.final .shell { position: relative; z-index: 2; }
.final .eyebrow { justify-content: center; }
.final .eyebrow::after { display: none; }
.final .h2 { margin-inline: auto; }
.final .lede { margin-inline: auto; }
.final .cta-row { justify-content: center; }

.foot { border-top: 1px solid var(--rule); padding-block: 2.5rem 3rem; }
.foot__in { display: grid; gap: 1.5rem; }
@media (min-width: 760px) { .foot__in { grid-template-columns: 1fr auto; align-items: center; } }
.foot p { margin: 0; font-size: 14px; color: var(--faint-text); }
.foot nav { display: flex; flex-wrap: wrap; gap: 1.25rem; }
.foot nav a {
  font-family: var(--f-label); font-size: 13px; font-weight: 600;
  letter-spacing: .13em; text-transform: uppercase; text-decoration: none; color: var(--dim);
  display: inline-flex; align-items: center; min-height: 44px;  /* touch target */
}
.foot nav a:hover { color: var(--paper); }

/* ============================================================
   Motion

   Hover and focus states live with their components above. This block
   is load- and scroll-driven motion only.

   THE RULE THIS SECTION IS BUILT AROUND: no animation may be capable of
   leaving content invisible. Two mechanisms enforce it -
     1. anything that starts at opacity:0 is scoped to `.js` (set by the
        inline script in <head>), so a JS failure means visible, not blank;
     2. index.html sets data-seen on every reveal after 3s regardless.
   Keyframe animations need neither: they run to completion on their own.
   ============================================================ */

@media (prefers-reduced-motion: no-preference) {

  /* --- scroll reveals --- */
  .js .reveal { opacity: 0; transform: translateY(16px); transition: opacity .6s ease, transform .6s cubic-bezier(.2,.7,.3,1); }
  .js .reveal[data-seen] { opacity: 1; transform: none; }

  /* Stagger: the container stays visible and hands the reveal to its
     children. --i is assigned by JS; var(--i, 0) means losing that script
     costs the stagger, not the content. */
  .js .reveal--stagger { opacity: 1; transform: none; }
  .js .reveal--stagger > * {
    opacity: 0; transform: translateY(14px);
    transition: opacity .55s ease, transform .55s cubic-bezier(.2,.7,.3,1);
    transition-delay: calc(var(--i, 0) * 65ms);
  }
  .js .reveal--stagger[data-seen] > * { opacity: 1; transform: none; }

  /* Background photography: fade the layer, settle the scale. No translate -
     these are inset:0 layers and any shift would open a gap at the edge. */
  .js .reveal--media { opacity: 0; transition: opacity .9s ease; }
  .js .reveal--media[data-seen] { opacity: 1; }
  /* :not(.stage__img) is load-bearing. figure.stage is ALSO a .reveal--media
     (it fades in on scroll like any other photo block), so without the
     exclusion this rule reaches its eleven stacked images at (0,2,1) and
     outranks `.stage__img { transition: opacity }` at (0,1,0) - which
     replaced the gallery crossfade with `transition: transform 1.6s` and made
     every photo swap an instant cut. The stage runs its own motion. */
  .js .reveal--media img:not(.stage__img) { transform: scale(1.07); transition: transform 1.6s cubic-bezier(.2,.7,.3,1); }
  .js .reveal--media[data-seen] img { transform: none; }

  /* Section headings use the hero's clipped reveal, so the whole page speaks
     one language rather than a hero plus some content. */
  .js .reveal .h2 {
    clip-path: inset(0 0 100% 0); transform: translateY(20px);
    transition: clip-path .95s cubic-bezier(.2,.7,.3,1) .08s,
                transform .95s cubic-bezier(.2,.7,.3,1) .08s;
  }
  .js .reveal[data-seen] .h2 { clip-path: inset(0 0 -25% 0); transform: none; }

  /* The closing wall parallaxes instead of settling from a scale - one
     transform per element, so the two effects can never fight. */
  .js .final__media.reveal--media img,
  .js .final__media.reveal--media[data-seen] img {
    position: absolute; left: 0; top: -14%; height: 128%;
    transform: translateY(calc(var(--par, 0) * 1%));
  }

  /* The rule beside a section label draws itself in. */
  .js .reveal .eyebrow::after,
  .js .reveal--stagger .eyebrow::after {
    transform: scaleX(0); transform-origin: left;
    transition: transform .7s .2s cubic-bezier(.2,.7,.3,1);
  }
  .js .reveal[data-seen] .eyebrow::after,
  .js .reveal--stagger[data-seen] .eyebrow::after { transform: scaleX(1); }

  /* --- hero, arriving ---
     Gated on [data-lit] rather than firing at stylesheet parse. The old
     version started the moment the CSS landed, which meant the copy animated
     while the webfont was still swapping in underneath it and while the hero
     photo had not decoded - the headline visibly re-set mid-rise. Now nothing
     moves until the face is ready AND the photo can be painted, so the whole
     thing plays once, in the right typeface, over an image that is actually
     there. index.html sets data-lit and carries a 2s backstop. */
  .js .hero__copy > * { opacity: 0; }
  .js .hero[data-lit] .hero__copy > * { animation: rise .85s cubic-bezier(.2,.7,.3,1) both; }
  .js .hero__copy > :nth-child(1) { animation-delay: .10s; }
  .js .hero__copy > :nth-child(2) { animation-name: unmask; animation-duration: 1.05s; animation-delay: .20s; }
  .js .hero__copy > :nth-child(3) { animation-delay: .46s; }
  .js .hero__copy > :nth-child(4) { animation-delay: .60s; }
  .js .hero__copy > :nth-child(5) { animation-delay: .72s; }
  .js .hero__copy > :nth-child(6) { animation-delay: .84s; }

  /* The lights only strike once the photo has DECODED. Previously this ran at
     stylesheet parse, so on any real connection the image arrived partway
     through its own brightening and the dark-to-lit arc was simply missed -
     which is the "delay" in the fluorescent effect. */
  .js .hero[data-lit] .hero__media img { animation: lights-on 2600ms linear both; }

  /* The veil is what makes the page start dark rather than merely dark-themed.
     It sits over everything including the header, and lifts on the same signal
     that strikes the lights, a touch slower so the room reads as coming up
     rather than as a curtain being pulled. */
  .js .veil {
    position: fixed; inset: 0; z-index: 200; pointer-events: none;
    background: #000; opacity: 1; visibility: visible;
    transition: opacity 1.25s cubic-bezier(.3,.5,.2,1), visibility 0s linear 1.25s;
  }
  /* html.js[data-lit], NOT `.js html[data-lit]` - .js lives ON <html>, so a
     descendant selector could never match. */
  html.js[data-lit] .veil { opacity: 0; visibility: hidden; }

  /* --- hero, leaving ---
     --out runs 0→1 across the first screen of scroll. Unset resolves to 0,
     so no JS means no drift - never a hero stranded at zero opacity. */
  .hero__inner {
    opacity: calc(1 - var(--out, 0));
    transform: translateY(calc(var(--out, 0) * -3rem));
  }

  /* --- photo stage ---
     Two motions for two events. An AUTO-advance pushes: the incoming shot
     slides in from the right and shoves the old one out, which makes the
     autonomous change clear. A CLICK dissolves: the incoming shot settles as
     it fades up because the visitor already supplied the direction.

     The push and the drift are one 9s keyframe rather than two animations
     handed off at 0.62s - a handoff would restart the scale and visibly jump. */
  .stage__img { transition: opacity .9s ease; }
  .stage__img[data-on] { animation: drift 9s both; }
  .stage[data-push] .stage__img[data-on] { transition: none; }
  .stage[data-push="fwd"]  .stage__img[data-on]  { animation: push-drift 9s both; }
  .stage[data-push="fwd"]  .stage__img[data-out] { animation: push-out .62s cubic-bezier(.32,0,.1,1) both; }
  .stage[data-push="back"] .stage__img[data-on]  { animation: push-drift-back 9s both; }
  .stage[data-push="back"] .stage__img[data-out] { animation: push-out-back .62s cubic-bezier(.32,0,.1,1) both; }

  /* --- marquee ---
     THE keyframe, not a fallback. A JS driver used to override this and
     couple the speed to scroll velocity; Darian asked for one constant pace
     (2026-08-03), which is exactly what a linear keyframe already is. Running
     it here also puts it on the compositor and drops the scroll listener.

     15s vs the old 31s: at rest the scroll-coupled version crawled. Duration
     is per-set, and the set narrows with the viewport, so the shorter mobile
     duration keeps the words-per-second roughly matched rather than letting
     the band slow down on a phone.

     NO hover-to-pause, deliberately. A `.marquee:hover { animation-play-state:
     paused }` rule sat here for a long time and never once ran, because the JS
     driver killed the very keyframe it hung off. Deleting the driver brought
     it back to life, and Darian killed it on sight (2026-08-03): the band
     should not stop just because a cursor is resting on it. Do not re-add it. */
  /* 44s, not 15s. At 15s the band ran 126.7 px/s across a 1900px set, which is
     faster than you can comfortably read a phrase before it leaves - it read as
     urgency rather than as a sign. 44s puts it at ~43 px/s, a slow drift you can
     actually finish a line in. Mobile keeps the same 1.5x ratio it always had. */
  .marquee__track { animation: slide 44s linear infinite; }
  @media (max-width: 699px) { .marquee__track { animation-duration: 30s; } }

  /* --- neon 24/7 sketches itself in ---
     Starts at full dashoffset, i.e. invisible, so it MUST be .js-scoped and
     it MUST hang off [data-seen], which index.html sets on every reveal after
     3s no matter what. Losing the script leaves it plainly drawn, not blank.

     1200 is a deliberate overshoot of any single glyph's outline length.
     getTotalLength() does not exist on SVG <text>, so rather than guess a
     number that has to be exact, the dash is set longer than every glyph -
     each one then draws as its own subpath, and because their perimeters
     differ they finish a beat apart, which is what stops it reading as four
     letters wiped by one bar.

     The glow comes up AFTER the outline lands (1.5s delay), so it reads as a
     tube striking and then warming, not as a light already on. */
  /* Only the LIT layers are drawn in. The glass is always present. The core
     draws with the red so the two never separate mid-stroke. */
  .js .neon__lit,
  .js .neon__core { stroke-dasharray: 1200; stroke-dashoffset: 1200; }
  .js .loc__when[data-seen] .neon__lit,
  .js .loc__when[data-seen] .neon__core,
  .js .hero[data-lit] .hero__neon-svg .neon__lit,
  .js .hero[data-lit] .hero__neon-svg .neon__core {
    animation: laser 2.1s cubic-bezier(.24,.6,.2,1) forwards,
               neon-strike 1.6s steps(1, end) 2.05s both;
  }
  /* --- the tube ages ---
     A real diner sign does not pulse on a timer; it holds steady for a long
     while and then stutters for a fraction of a second. So these keyframes are
     ~95% flat with brief irregular dropouts, NOT a sine wave.

     FOUR tubes on four co-prime-ish durations (7.3 / 5.9 / 8.1 / 11.3s) so
     they drift permanently out of phase and the sign never visibly repeats.
     The slash runs `tube-dying`: it is DARK most of the time and only
     occasionally struggles alight, which is a different thing from flickering
     and is what actually reads as a broken sign rather than a stylised one.

     Delay 2.9s on all four - the draw finishes at 2.1s and the glow warms
     until 2.9s, so the tubes only start misbehaving once properly lit.

     Two animations on the svg carry no conflict: neon-warm owns `filter`,
     neon-buzz owns `opacity`, neon-shake owns `transform`. */
  .js .loc__when[data-seen] .neon .n1 { animation: tube-a 7.3s steps(1, end) 2.9s infinite; }
  .js .loc__when[data-seen] .neon .n2 { animation: tube-b 5.9s steps(1, end) 2.9s infinite; }
  .js .loc__when[data-seen] .neon .n3 { animation: tube-dying 8.1s steps(1, end) 2.9s infinite; }
  .js .loc__when[data-seen] .neon .n4 { animation: tube-c 11.3s steps(1, end) 2.9s infinite; }
  .js .hero[data-lit] .hero__neon-svg .n1 { animation: tube-a 7.3s steps(1, end) 2.9s infinite; }
  .js .hero[data-lit] .hero__neon-svg .n2 { animation: tube-b 5.9s steps(1, end) 2.9s infinite; }
  .js .hero[data-lit] .hero__neon-svg .n3 { animation: tube-dying 8.1s steps(1, end) 2.9s infinite; }
  .js .hero[data-lit] .hero__neon-svg .n4 { animation: tube-c 11.3s steps(1, end) 2.9s infinite; }
  /* Physical instability, separate from the electrical kind. A sign bolted to
     a wall for fifteen years shakes a little when the ballast kicks. Runs on
     `transform`, which nothing else on this element touches. */
  .js .loc__when[data-seen] .neon { animation: neon-warm 1.4s ease-out 1.5s both,
                                               neon-buzz 110ms linear 3.7s infinite,
                                               neon-shake 9.7s steps(1, end) 3.7s infinite; }
  /* The Hours sign keeps its wall-mounted shake and permanent tilt. Inline in
     a sentence that tilt reads as a crooked word, so the hero carries only
     the electrical motion: warmup, buzz and the four independent tubes. */
  .js .hero[data-lit] .hero__neon-svg { animation: neon-warm 1.4s ease-out 1.5s both,
                                                   neon-buzz 110ms linear 3.7s infinite; }

  /* --- drawer links ---
     Safe to start at 0: the drawer is [hidden] until JS opens it. */
  .js .drawer a {
    opacity: 0; transform: translateY(12px);
    transition: opacity .3s ease, transform .35s cubic-bezier(.2,.7,.3,1);
  }
  .js .drawer[data-open] a {
    opacity: 1; transform: none;
    transition-delay: calc(var(--i, 0) * 45ms + 80ms);
  }
}

/* The band crosses in the first 18% and then parks off-stage for the rest of
   the cycle - roughly 0.9s of movement, 4.1s of stillness. A continuously
   travelling shimmer reads as a loading state; a long pause between passes
   reads as a highlight. skewX gives it the diagonal a real gloss has. */
@keyframes sweep {
  0%        { transform: translateX(-160%) skewX(-18deg); }
  18%, 100% { transform: translateX(280%)  skewX(-18deg); }
}
/* Rest state repeats the base transform exactly, so there is no jump when the
   animation is dropped on open. */
@keyframes chev-nudge {
  0%, 74%, 100% { transform: rotate(45deg) translate(-2px, -2px); }
  82%           { transform: rotate(45deg) translate(1.5px, 1.5px); }
  90%           { transform: rotate(45deg) translate(-2px, -2px); }
}

@keyframes rise {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}
/* Rests at -25% rather than 0 so descenders and letter-spacing never clip. */
@keyframes unmask {
  from { opacity: 0; clip-path: inset(0 0 100% 0); transform: translateY(26px); }
  to   { opacity: 1; clip-path: inset(0 0 -25% 0); transform: none; }
}
@keyframes slide {
  from { transform: none; }
  to   { transform: translateX(-50%); }
}
/* Manual dissolve. The first 8% (~0.72s) is the counter-scale settling inward
   while the crossfade runs, so the shot arrives rather than just appearing;
   the remaining ~8.3s is the slow push in that makes a still phone photo read
   as footage of the room. Timing is per-keyframe - the settle eases out, the
   drift is dead linear so it never looks like it is accelerating. */
@keyframes drift {
  0%   { transform: scale(1.072); animation-timing-function: cubic-bezier(.16,.84,.28,1); }
  8%   { transform: scale(1.012); animation-timing-function: linear; }
  100% { transform: translateX(0) scale(1.085); }
}

/* Automatic advance. Identical to drift after the 7% mark; the difference is the
   slide in from the right that precedes it. Enters from exactly 100%, not
   more: at 102% a 2%-wide sliver of empty stage shows through at the start,
   because the outgoing shot leaves at a slower 58%. */
@keyframes push-drift {
  0%   { transform: translateX(100%) scale(1.012); animation-timing-function: cubic-bezier(.32,0,.1,1); }
  7%   { transform: translateX(0) scale(1.012); animation-timing-function: linear; }
  100% { transform: translateX(0) scale(1.085); }
}
/* Lags the incoming shot deliberately - a 1:1 slide reads as two flat cards
   on a conveyor, a lag reads as one shoving the other.

   --out-scale is captured in JS from the outgoing shot's live transform right
   before the automatic push starts. Without it these keyframes had no scale component,
   so an image mid-drift at scale 1.05 SNAPPED back to 1.0 the instant the push
   began: a visible shrink on every automatic slide. The fallback of
   1 only applies if the script did not get there. */
@keyframes push-out {
  from { transform: translateX(0)    scale(var(--out-scale, 1)); }
  to   { transform: translateX(-58%) scale(var(--out-scale, 1)); }
}

/* Same pair mirrored, for the back arrow. Keeping these as separate keyframes
   rather than flipping a sign in a custom property because the -58% lag has
   to mirror too, and a single --dir would make the outgoing shot lead instead
   of lag in one of the two directions. */
@keyframes push-drift-back {
  0%   { transform: translateX(-100%) scale(1.012); animation-timing-function: cubic-bezier(.32,0,.1,1); }
  7%   { transform: translateX(0) scale(1.012); animation-timing-function: linear; }
  100% { transform: translateX(0) scale(1.085); }
}
@keyframes push-out-back {
  from { transform: translateX(0)   scale(var(--out-scale, 1)); }
  to   { transform: translateX(58%) scale(var(--out-scale, 1)); }
}

@keyframes laser { to { stroke-dashoffset: 0; } }

/* Ignition. A cold tube does not come on cleanly - it tries, fails, catches,
   drops, and only then holds. Four failed strikes at irregular intervals,
   never reaching zero because the glass tube behind stays visible anyway. */
@keyframes neon-strike {
  0%    { opacity: 1; }
  5%    { opacity: .06; }
  9%    { opacity: 1; }
  12%   { opacity: .04; }
  15%   { opacity: 1; }
  16.5% { opacity: .08; }
  24%   { opacity: 1; }
  27%   { opacity: .05; }
  29%   { opacity: 1; }
  30.5% { opacity: .3; }
  36%   { opacity: 1; }
  100%  { opacity: 1; }
}
/* Linear, not steps: a hum is a shimmer, not a strobe. Deepened from .985 -
   at that value the hum was technically present and visually absent. .962 is
   about as far as this can go before 110ms starts reading as a flashing light
   rather than as a tired ballast. */
@keyframes neon-buzz {
  0%, 100% { opacity: 1; }
  50%      { opacity: .962; }
}

/* The near-dead tube. Mostly OUT, with two failed attempts and one short
   moment where it actually catches before giving up again. Because the dark
   glass layer sits behind it, "out" is a visible unlit tube rather than a
   hole in the sign. */
@keyframes tube-dying {
  0%, 11%    { opacity: .06; }
  11.4%      { opacity: .55; }
  11.9%      { opacity: .06; }
  38%        { opacity: .06; }
  38.5%      { opacity: .8; }
  39.2%      { opacity: .1; }
  39.8%      { opacity: .7; }
  46%        { opacity: .62; }   /* it holds, briefly */
  46.6%      { opacity: .08; }
  73%        { opacity: .06; }
  73.5%      { opacity: .4; }
  74%        { opacity: .06; }
  100%       { opacity: .06; }
}

/* The ballast kicking. Sub-pixel, rare, and always paired with the tilt so it
   reads as the whole sign moving on its mounts rather than the type jittering. */
@keyframes neon-shake {
  0%, 18%    { transform: rotate(-1.8deg) translate(0, 0); }
  18.3%      { transform: rotate(-2.1deg) translate(-1.5px, .5px); }
  18.8%      { transform: rotate(-1.8deg) translate(0, 0); }
  40%        { transform: rotate(-1.8deg) translate(0, 0); }
  40.4%      { transform: rotate(-2.25deg) translate(-2px, 1px); }
  40.9%      { transform: rotate(-1.45deg) translate(2px, -1px); }
  41.4%      { transform: rotate(-2deg) translate(-1px, .5px); }
  42%        { transform: rotate(-1.8deg) translate(0, 0); }
  71%        { transform: rotate(-1.8deg) translate(0, 0); }
  71.4%      { transform: rotate(-1.55deg) translate(1px, -.5px); }
  72%        { transform: rotate(-1.8deg) translate(0, 0); }
  86%        { transform: rotate(-1.8deg) translate(0, 0); }
  86.3%      { transform: rotate(-2.1deg) translate(1px, 1px); }
  86.9%      { transform: rotate(-1.8deg) translate(0, 0); }
  100%       { transform: rotate(-1.8deg) translate(0, 0); }
}

/* steps(1, end) on the animation means each stop HOLDS until the next one -
   a real tube snaps between states, it does not crossfade. The percentages
   are irregular on purpose; evenly spaced dropouts read as a blinking cursor.
   Never fully 0: a dead tube still shows a faint filament. */
@keyframes tube-a {
  0%, 19%   { opacity: 1; }
  19.4%     { opacity: .12; }
  19.9%     { opacity: 1; }
  46%       { opacity: 1; }
  46.6%     { opacity: .1; }
  47.1%     { opacity: 1; }
  47.5%     { opacity: .3; }
  48.2%     { opacity: 1; }
  48.7%     { opacity: .16; }
  49.4%     { opacity: 1; }
  83%       { opacity: 1; }
  83.4%     { opacity: .08; }
  84.1%     { opacity: 1; }
  100%      { opacity: 1; }
}
/* The tired tube: dimmer at rest, stutters more, and holds a long brown-out. */
@keyframes tube-b {
  0%, 22%   { opacity: .68; }
  22.5%     { opacity: .1; }
  23.4%     { opacity: .68; }
  41%       { opacity: .68; }
  41.5%     { opacity: .24; }
  42.1%     { opacity: .68; }
  57%       { opacity: .68; }
  57.5%     { opacity: .14; }
  58%       { opacity: .68; }
  58.6%     { opacity: .07; }
  60.4%     { opacity: .34; }   /* the long brown-out, ~110ms */
  61%       { opacity: .68; }
  100%      { opacity: .68; }
}
@keyframes tube-c {
  0%, 33%   { opacity: 1; }
  33.4%     { opacity: .18; }
  33.9%     { opacity: 1; }
  68%       { opacity: 1; }
  68.5%     { opacity: .16; }
  69%       { opacity: 1; }
  91%       { opacity: 1; }
  91.3%     { opacity: .1; }
  91.7%     { opacity: .5; }
  92.2%     { opacity: 1; }
  100%      { opacity: 1; }
}
/* FIVE radii, and they are five different physical things, which is why one or
   two of them alone only ever reads as "blurry red":
     4px   the heat right at the glass, tinted toward the core's pink-white
     13px  the tube's own body glow
     34px  the near bloom hanging in the air around it
     70px  light landing on the wall behind the sign
     130px the room lighting up - very low alpha, very wide

   The 130px radius deliberately breaks the old "clearance must beat the shadow
   radius" rule that governed the earlier version. That rule existed to stop a
   hard-edged glow from sitting ON the paragraph. At .14 alpha spread over
   130px the per-pixel contribution down there is a faint warm wash, which is
   what a sign this bright would actually do to the copy beside it. If it ever
   starts reading as a smudge rather than as light, drop this radius first. */
/* The `from` is NOT `filter: none`, and that matters more than it looks.
   Interpolating from `none` to a filter LIST forces the engine to synthesise
   the missing start value, and WebKit does not survive it: measured on iOS's
   engine, the sign painted ZERO pixels for the whole animation and after it
   finished - the entire 24/7 was simply absent, while Chromium was fine.
   Isolated by stripping one property at a time; `animation: none` on .neon was
   the single change that brought it back.

   Both keyframes now carry the SAME five shadows in the SAME order and only
   the alpha moves, from 0 to full. That is a plain numeric interpolation every
   engine agrees on. Do not shorten this back to `from { filter: none }`. */
@keyframes neon-warm {
  from { filter: drop-shadow(0 0 var(--g1) rgba(255, 150, 140, 0))
                 drop-shadow(0 0 var(--g2) rgba(236, 26, 40, 0))
                 drop-shadow(0 0 var(--g3) rgba(220, 20, 32, 0))
                 drop-shadow(0 0 var(--g4) rgba(220, 20, 32, 0))
                 drop-shadow(0 0 var(--g5) rgba(206, 22, 34, 0)); }
  to   { filter: drop-shadow(0 0 var(--g1) rgba(255, 150, 140, .95))
                 drop-shadow(0 0 var(--g2) rgba(236, 26, 40, .92))
                 drop-shadow(0 0 var(--g3) rgba(220, 20, 32, .68))
                 drop-shadow(0 0 var(--g4) rgba(220, 20, 32, .38))
                 drop-shadow(0 0 var(--g5) rgba(206, 22, 34, .14)); }
}
/* A fluorescent tube coming on: ONE strike, then a long steady warm-up.
   Timing is per-keyframe - the strike and dropout are fast (the tube
   catching), the ramp to full is slow (the gas warming). The earlier
   version dipped twice at 1100ms, which read as dropped frames rather
   than a light. Measured curve of this one: .20 → .74 → .30 → 1.14,
   a single direction change, then monotonic for the remaining ~2.1s. */
@keyframes lights-on {
  0%   { filter: brightness(.20) contrast(1.22) saturate(.88); transform: scale(1.055);
         animation-timing-function: cubic-bezier(.15,.85,.3,1); }
  9%   { filter: brightness(.74) contrast(1.14) saturate(.94);
         animation-timing-function: cubic-bezier(.5,0,.75,.2); }
  17%  { filter: brightness(.30) contrast(1.20) saturate(.90);
         animation-timing-function: cubic-bezier(.22,.55,.2,1); }
  100% { filter: brightness(1.14) contrast(1.06) saturate(1.08); transform: scale(1); }
}

/* Reduced motion keeps every hover state - it just arrives instantly and
   without travel. Colour is not a vestibular trigger; movement is. */
@media (prefers-reduced-motion: reduce) {
  .btn:hover, .btn:focus-visible,
  .disc li:hover i, .disc li:hover span,
  .brand:hover .brand__emblem { transform: none; }
  .btn::before, .callout::after, .price::after,
  .nav a:not(.btn)::after,
  .disc li::after, .disc i, .disc span, .disc em, .more::before,
  .callout, .callout h2, .callout p, .price,
  .stage__arrow, .stage__arrow::before, .stage__arrow i,
  .js .plans-wrap, .js .plans-wrap .plan,
  .spec > div::before, .burger span { transition: none; }
  /* The drawer still opens and closes, it just arrives at full height. */
  .stage__arrow--next:hover i, .stage__arrow--next:focus-visible i { transform: rotate(45deg); }
  .stage__arrow--prev:hover i, .stage__arrow--prev:focus-visible i { transform: rotate(-135deg); }
  /* Neon stays lit and drawn, it simply never sketches itself and never
     flickers - a stuttering sign is exactly the kind of motion this query
     exists to suppress. The tape is already inert here; its driver is behind
     the `reduced` guard in JS. */
  .neon text, .neon__lit { stroke-dasharray: none; stroke-dashoffset: 0; }
  .neon, .neon__lit,
  .neon .n1, .neon .n2, .neon .n3, .neon .n4 { animation: none; opacity: 1; }
  /* Lit, glowing and still hung slightly crooked - the tilt is not motion. */
  .neon { filter: drop-shadow(0 0 6px rgba(220, 20, 32, .85))
                  drop-shadow(0 0 26px rgba(220, 20, 32, .5));
          transform: rotate(-1.3deg); }
  /* No veil, no lights-up sequence: the page is simply lit on arrival. */
  .veil { display: none; }
  .js .hero__copy > * { opacity: 1; }
  /* Cards still respond - background, dim and the red rule all stay. What goes
     is the travel and the blur, which is the part this query is about. */
  .callouts:hover .callout:hover,
  .prices:hover .price:hover { transform: none; }
  /* (The sibling blur neutraliser that used to live here is gone with the
     sibling dims themselves - there is nothing left to switch off.) */
  /* Keep the red rule and the detail line as STATES, just without the draw
     and the slide - scaleX(0)→(1) and a translate are exactly the travel this
     query is asking us to drop. */
  .disc li:hover::after { transform: scaleX(1); }
  .disc li:hover em { transform: none; }
}
