/* ==========================================================================
   CRAWLLOWEEN base
   Reset, type, layout primitives, and the reveal system.

   Depends on tokens.css. Load order is tokens, base, components, page.
   ========================================================================== */

/* ------------------------------------------------------------------- reset */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

/* A scripted scrollTo() fights smooth scrolling, and the screenshot harness
   drives the page that way. Kill it for anyone who asked for stillness. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  font-weight: var(--fw-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* The page must never scroll sideways. Wide content scrolls inside its own
     container instead, see .scroll-x below. */
  overflow-x: hidden;
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

img { height: auto; }

button, input, select, textarea {
  font: inherit;
  color: inherit;
}

a { color: inherit; }

h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd {
  margin: 0;
}

ul, ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Fixed nav offset lives here and only here. A full-bleed hero opts out with
   .main--flush. Never add a second offset on a section. */
main {
  padding-top: var(--nav-h);
}

main.main--flush {
  padding-top: 0;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

::selection {
  background: var(--accent);
  color: var(--text-on-accent);
}

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

.display,
h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: var(--fw-black);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
  text-wrap: balance;
}

/* SECTION HEADINGS TAKE THE STENCIL. h1 DOES NOT.

   This is where Scare City uses theirs: their section headings are stencil while
   their hero is a different display face entirely. So h2 is the faithful match,
   and it keeps a real hierarchy, page title in one voice and section headings in
   another, instead of setting every heading on the site in the same novelty cut.

   It is also the safe half. h1 runs at --fs-hero, up to 9rem, and this face is 37%
   wider than the display face at the same size, so stencilling the hero would
   reflow the largest type on the site at every breakpoint for no gain.

   Keeping h1 out also keeps it CONSISTENT: eight pages set their h1 with
   .hero-type, which is a class and would win over an element selector anyway, so
   putting h1 in this rule would have stencilled the five h1s that lack that class
   and left the other eight alone.

   h3 and below stay in Big Shoulders too: a stencil's bridges are a fixed
   proportion of the stroke, so at small sizes they stop reading as cuts and start
   reading as broken glyphs.

   font-weight MUST be 400 here. Saira Stencil One ships one weight, and inheriting
   900 from the rule above gets a synthesised bold that closes the stencil bridges
   and erases the effect. tokens.css has the detail.

   Tracking goes back to normal from --track-tight, because negative tracking on a
   stencil pulls the cut edge of one letter into the next and the words start to
   look welded together. */
h2 {
  font-family: var(--font-stencil);
  font-weight: var(--fw-body);
  letter-spacing: var(--track-normal);
}

h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); line-height: var(--lh-snug); }

h4 {
  font-family: var(--font-sans);
  font-size: var(--fs-h4);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
}

.hero-type {
  font-family: var(--font-display);
  font-weight: var(--fw-black);
  font-size: var(--fs-hero);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
  text-transform: uppercase;
}

/* The small label above a heading. Plain descriptive labels only, never a pun
   and never a sentence.

   Set in the typewriter face, which is the label voice borrowed from Scare City
   (they use Bulletin Typewriter for exactly this role). It reads as something
   stamped onto the page rather than typeset into it, which is the whole point.

   Two adjustments the swap needs, and both matter. Tracking drops from --widest
   to --wide, because a typewriter is already monospaced with generous
   sidebearings and 0.18em on top of that falls apart into loose letters. And the
   weight goes back to normal, because Special Elite ships one weight and asking
   for 700 gets a synthesised bold that smears the distressed edges into mud. */
.eyebrow {
  font-family: var(--font-type);
  font-size: var(--fs-xs);
  font-weight: var(--fw-body);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--sp-3);
}

.lead {
  font-size: var(--fs-lead);
  color: var(--text-dim);
  text-wrap: pretty;
}

.prose p + p { margin-top: var(--sp-4); }
.prose { max-width: 68ch; }

.dim   { color: var(--text-dim); }
.faint { color: var(--text-faint); }
.accent { color: var(--accent); }

.num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* Screen-reader-only, but focusable when it needs to be (skip link). */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: -100%;
  left: var(--sp-4);
  z-index: calc(var(--z-modal) + 1);
  padding: var(--sp-3) var(--sp-4);
  background: var(--accent);
  color: var(--text-on-accent);
  font-weight: var(--fw-bold);
  border-radius: var(--r-sm);
}
.skip-link:focus {
  top: var(--sp-4);
}

/* ------------------------------------------------------------------ layout */

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

.wrap--narrow { max-width: var(--max-narrow); }
.wrap--wide   { max-width: var(--max-wide); }

.section {
  padding-block: var(--sp-section);
}

.section--sunk { background: var(--bg-sunk); }
.section--soft { background: var(--bg-soft); }

.section-head {
  margin-bottom: var(--sp-7);
}

/* Headings and paragraphs are both margin:0 from the reset, so a section head
   collapses into a solid block without this. Spacing lives here rather than on
   h1/.lead so it cannot leak into other layouts. */
.section-head > * + * {
  margin-top: var(--sp-4);
}

/* An eyebrow is a label attached to the heading below it, not a separate block,
   so that one step is tighter than the --sp-4 body rhythm.

   Without this the rule above wins and the pattern ships at two distances.
   `.eyebrow` carries `margin-bottom: --sp-3` for standalone use; adjacent
   margins collapse to the larger of the two, so inside a .section-head the
   eyebrow sat --sp-4 off its heading while an identical eyebrow in a hero, where
   nothing sets a sibling margin, sat at --sp-3. 16px in one place and 12px in the
   other, from markup that looks the same. Measured, not guessed. */
.section-head > .eyebrow + * {
  margin-top: var(--sp-3);
}

.stack > * + * { margin-top: var(--sp-4); }
.stack-lg > * + * { margin-top: var(--sp-6); }

.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  align-items: center;
}

.grid {
  display: grid;
  gap: var(--sp-5);
}

/* Auto-fit grids: the min is the card's comfortable minimum, so these never
   need a breakpoint to collapse. */
.grid--cards { grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr)); }
.grid--wide  { grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); }
.grid--tight { grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); }

/* Anything that can outgrow the viewport (a wide table, a route timeline)
   scrolls inside itself. The body never scrolls sideways. */
.scroll-x {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}

.divider {
  height: 1px;
  border: 0;
  background: var(--line);
  margin-block: var(--sp-7);
}

/* ---------------------------------------------------------------- textures */

/* The shared brand base: a burning skyline glow off the bottom of a dark
   section. Used behind heroes and feature bands. Purely decorative, so it is
   a pseudo-element and never carries content. */
.emberwash {
  position: relative;
  isolation: isolate;
}
.emberwash::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 55%;
  pointer-events: none;
  z-index: -1;
  background:
    radial-gradient(120% 100% at 50% 100%, var(--accent-glow) 0%, transparent 68%);
}

/* Fine grain, so the large flat blacks do not band on cheap panels. */
.grain::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.05;
  z-index: 1;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* --------------------------------------------------------------- reveal

   CONTRACT: every hidden start state below is gated on html.js-motion, which
   motion.js adds only after it has confirmed IntersectionObserver exists and
   that the user has not asked for reduced motion. With JS broken, absent, or
   reduced-motion set, none of these rules apply and the page renders fully
   visible. Never write a bare `[data-reveal] { opacity: 0 }`.
   -------------------------------------------------------------------- */

.js-motion [data-reveal] {
  opacity: 0;
  transition:
    opacity var(--dur-reveal) var(--ease-entrance),
    transform var(--dur-reveal) var(--ease-entrance);
  will-change: opacity, transform;
}

.js-motion [data-reveal="up"],
.js-motion [data-reveal=""]      { transform: translate3d(0, 26px, 0); }
.js-motion [data-reveal="down"]  { transform: translate3d(0, -26px, 0); }
.js-motion [data-reveal="left"]  { transform: translate3d(-30px, 0, 0); }
.js-motion [data-reveal="right"] { transform: translate3d(30px, 0, 0); }
.js-motion [data-reveal="scale"] { transform: scale(0.955); }
.js-motion [data-reveal="fade"]  { transform: none; }

.js-motion [data-reveal].is-in {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* Parallax targets are moved by script. Give them their own layer so the
   transform does not force a repaint of the section around them. */
.js-motion [data-parallax] {
  will-change: transform;
}

/* A video background that was skipped (phone, save-data, reduced motion)
   leaves its poster in place. Hide the element so the poster attribute on the
   parent picture or the CSS background shows through cleanly. */
.is-poster-only {
  visibility: hidden;
}

/* --------------------------------------------------------------- utilities */

.center      { text-align: center; }
.center-x    { margin-inline: auto; }
.nowrap      { white-space: nowrap; }
.full-bleed  { width: 100vw; margin-inline: calc(50% - 50vw); }
.relative    { position: relative; }
.hide        { display: none !important; }

@media (max-width: 860px) {
  .hide-mobile { display: none !important; }
}
@media (min-width: 861px) {
  .hide-desktop { display: none !important; }
}

/* Print: this page gets printed as a route sheet more often than you would
   think, so make that not embarrassing. */
@media print {
  body { background: #fff; color: #000; }
  .site-nav, .site-footer, .drop-modal, .mobile-cta { display: none !important; }
  main { padding-top: 0; }
  .js-motion [data-reveal] { opacity: 1 !important; transform: none !important; }
}
