/* ==========================================================================
   MayBolt Media — Base styles, layout primitives & shared components
   --------------------------------------------------------------------------
   Consumes the custom properties defined in tokens.css. Load tokens.css
   before this file. See DESIGN-SPEC.md for the component contract.
   ========================================================================== */

/* ---- Reset ------------------------------------------------------------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}
* {
  margin: 0;
}
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}
body {
  min-height: 100svh;
}
img,
svg,
video {
  display: block;
  max-width: 100%;
}
input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}
a {
  color: inherit;
  text-decoration: none;
}
ul[class],
ol[class] {
  list-style: none;
  padding: 0;
}

/* ---- Base type / color --------------------------------------------------- */
html {
  background: var(--color-bg);
}
body {
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-text);
  font-size: var(--text-body);
  line-height: var(--leading-normal);
  overflow-wrap: anywhere; /* long unbroken strings (URLs, etc.) wrap instead
    of overflowing narrow viewports; inherited site-wide */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: var(--leading-tight);
  letter-spacing: -0.02em;
  color: var(--color-text);
}
h1 {
  font-size: var(--text-display-1);
  font-weight: 500; /* lighter than h2/h3's 600 - a huge headline at 600 reads as a heavy block of ink */
}
h2 {
  font-size: var(--text-display-2);
}
h3 {
  font-size: var(--text-display-3);
}
p {
  color: inherit;
}

/* ---- Focus visibility --------------------------------------------------- */
/* Never remove outlines without replacing them — this is the replacement,
   applied uniformly to every focusable element on the site. */
:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ---- Skip link ------------------------------------------------------------ */
.skip-link {
  position: absolute;
  left: var(--space-sm);
  top: -100%;
  z-index: 100;
  display: inline-flex;
  align-items: center;
  min-height: 44px; /* was ~39px with padding+line-height alone */
  background: var(--color-bg-elevated);
  color: var(--color-text);
  padding-inline: var(--space-sm);
  border-radius: var(--radius-sm);
  font-size: var(--text-small);
  font-weight: 600;
  transition: top var(--duration-fast) var(--ease-standard);
}
.skip-link:focus {
  top: var(--space-sm);
}

/* ---- Layout primitives --------------------------------------------------- */
.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 4vw, 3rem);
}
.section {
  padding-block: var(--space-section);
}
.section-header {
  max-width: var(--measure);
  margin-inline: auto;
  margin-bottom: var(--space-2xl);
  text-align: center;
}
.section-header > * + * {
  margin-top: var(--space-sm);
}
.measure {
  max-width: var(--measure);
}

/* ---- Type utilities ------------------------------------------------------ */
.eyebrow {
  display: inline-block;
  font-family: var(--font-text);
  font-size: var(--text-small);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-accent);
}
.text-display-1 {
  font-size: var(--text-display-1);
}
.text-display-2 {
  font-size: var(--text-display-2);
}
.text-body-lg {
  font-size: var(--text-body-lg);
  font-family: var(--font-text);
  font-weight: 400;
  color: var(--color-text-secondary);
  line-height: var(--leading-snug);
}
.text-small {
  font-size: var(--text-small);
}
.text-secondary {
  color: var(--color-text-secondary);
}

/* ---- Scroll-reveal utility -------------------------------------------------
   Add `.reveal` to any element that should fade/slide in on scroll. Wrap a
   set of sibling `.reveal` elements in `.reveal-group` to stagger them.

   Default (below, NO media query) is fully visible/legible — this is what
   EVERY visitor gets unconditionally, including one whose JS is blocked
   or fails to load. That matters because a media query alone cannot fix
   this: `prefers-reduced-motion` only ever reports an OS setting, never
   whether script.js actually ran, so scoping the hidden starting state to
   `@media (prefers-reduced-motion: no-preference)` alone would still hide
   `.reveal` content forever for the single largest group of visitors —
   anyone with the (default) "no preference" motion setting whose JS is
   blocked — since that media query matches regardless of JS.

   The hidden state is therefore additionally gated on `html.js`, a class
   the tiny inline `<script>` at the top of each page's <head> adds
   synchronously (see index.html) — NOT the deferred script.js. If that
   inline script never runs (JS disabled/blocked), `html.js` is never
   added, `html.js .reveal` never matches, and every `.reveal` element
   simply stays at its visible default. Only when both (a) motion is
   allowed AND (b) JS is confirmed running does the hidden/animate-in
   state apply — at which point the deferred script.js's
   IntersectionObserver is what adds `.is-visible`.
   ------------------------------------------------------------------------ */
.reveal {
  opacity: 1;
  transform: none;
}
.reveal-group > .reveal:nth-child(1) {
  transition-delay: 0ms;
}
.reveal-group > .reveal:nth-child(2) {
  transition-delay: 90ms;
}
.reveal-group > .reveal:nth-child(3) {
  transition-delay: 180ms;
}
.reveal-group > .reveal:nth-child(4) {
  transition-delay: 270ms;
}
.reveal-group > .reveal:nth-child(5) {
  transition-delay: 360ms;
}
@media (prefers-reduced-motion: no-preference) {
  html.js .reveal {
    opacity: 0;
    transform: translateY(var(--reveal-distance));
    transition: opacity var(--duration-reveal) var(--ease-standard),
      transform var(--duration-reveal) var(--ease-standard);
  }
  html.js .reveal.is-visible {
    opacity: 1;
    transform: none;
  }
}

/* ---- Site header / nav ---------------------------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--color-bg-overlay);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid var(--color-border);
}
.site-header .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm);
  min-height: var(--header-height);
  padding-block: var(--space-xs);
}
.wordmark {
  display: inline-block;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.25rem;
  letter-spacing: -0.01em;
  padding-block: var(--space-2xs);
}
.wordmark .accent {
  color: var(--color-accent);
}
/* Footer's smaller wordmark — was a repeated inline `style="font-size:
   1.05rem"` on 4 pages; moved into a real class instead. Still a
   bespoke value rather than a type-scale token (it doesn't correspond to
   any existing --text-* size), which is fine for a one-off brand-mark
   tweak, but keep it here — in style.css — not back in an inline style,
   if it's ever touched again. */
.wordmark--footer {
  font-size: 1.05rem;
}
.site-nav ul {
  /* This <ul> has no class, so the `ul[class]` reset above skips it and
     the browser's default disc bullets + indent showed in the header. */
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: var(--space-lg);
  flex-wrap: wrap;
}
.site-nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: var(--space-2xs);
  font-size: var(--text-small);
  font-weight: 500;
  color: var(--color-text);
  transition: color var(--duration-fast) var(--ease-standard);
}
.site-nav a:hover {
  color: var(--color-accent);
}
@media (max-width: 640px) {
  .site-nav {
    flex-basis: 100%;
    order: 3;
  }
  .site-nav ul {
    justify-content: center;
    gap: var(--space-md);
  }
}

/* ---- Buttons --------------------------------------------------------------
   Apple-style: small, understated pills. Never louder than the copy next
   to them. `.btn-primary` is solid-accent, `.btn-secondary` is outlined —
   use secondary far more often than primary.
   ------------------------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  min-height: 44px;
  padding-inline: var(--space-lg);
  border-radius: var(--radius-full);
  font-family: var(--font-text);
  font-size: var(--text-small);
  font-weight: 600;
  border: 1px solid transparent;
  transition: background var(--duration-fast) var(--ease-standard),
    color var(--duration-fast) var(--ease-standard),
    border-color var(--duration-fast) var(--ease-standard),
    filter var(--duration-fast) var(--ease-standard);
}
.btn-primary {
  background: var(--color-accent);
  color: var(--color-on-accent);
}
.btn-primary:hover {
  filter: brightness(1.08);
}
.btn-secondary {
  background: transparent;
  color: var(--color-text);
  border-color: var(--color-border-strong);
}
.btn-secondary:hover {
  border-color: var(--color-accent);
  color: var(--color-accent);
}
@media (max-width: 480px) {
  .hero__actions .btn {
    width: 100%;
  }
}

/* Text link with a trailing arrow — used for card/footer "learn more"
   affordances. `.link-cta` is often a plain decorative span (see
   `.app-card__cta`) sitting inside an element that is ALREADY a link via
   the stretched-link technique; only make it a real <a> when it isn't
   nested inside another interactive element. */
.link-cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  font-size: var(--text-small);
  font-weight: 600;
  color: var(--color-accent);
}
/* Tag-qualified on purpose: `.link-cta` is used two ways — a real
   standalone <a> (privacy/support cross-links) that needs a real touch
   target, and a decorative `<span class="link-cta" aria-hidden="true">`
   inside app-cards (not interactive itself; the whole card is already
   the click target via the stretched-link). Scoping the 44px minimum to
   `a.link-cta` only means it doesn't inflate the non-interactive span. */
a.link-cta {
  min-height: 44px;
}
.link-cta .arrow {
  display: inline-block;
  transition: transform var(--duration-fast) var(--ease-standard);
}
.app-card:hover .link-cta .arrow {
  transform: translateX(3px);
}
@media (prefers-reduced-motion: reduce) {
  .app-card:hover .link-cta .arrow {
    transform: none;
  }
}

/* ---- Badges -----------------------------------------------------------
   Base badge is a bordered chip on the page background (NOT a tinted
   fill) so its text keeps full contrast against --color-bg in both
   themes — a `--color-accent-soft` tint behind small accent-colored text
   was measured and fails AA in dark mode, hence this shape.
   ------------------------------------------------------------------------ */
.badge {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  padding: var(--space-3xs) var(--space-sm);
  border-radius: var(--radius-full);
  background: var(--color-bg);
  border: 1px solid var(--color-border-strong);
  color: var(--color-text);
  font-size: var(--text-small);
  font-weight: 600;
  line-height: 1;
}
.badge--coming-soon {
  color: var(--color-accent);
  border-color: var(--color-accent);
}
a.badge {
  /* A `.badge` that is a real link (e.g. the tablet band's "iPad · Out
     now") needs the same 44px touch target every other interactive
     element gets, without visually growing the 28px pill — same
     invisible hit-area-extension pattern as `.video-toggle::before`. */
  position: relative;
}
a.badge::before {
  content: "";
  position: absolute;
  inset: -8px 0;
}
a.badge:hover {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

/* ---- Hero ---------------------------------------------------------------- */
.hero {
  position: relative;
  overflow: hidden;
  padding-block: calc(var(--space-section) + var(--space-lg)) var(--space-section);
}
.hero::before {
  /* A soft ambient color wash behind the hero, the same radial-gradient
     language `.app-card__visual` already uses for its product-card
     background -- reused here rather than inventing a new treatment.
     This is the kind of quiet, colored glow apple.com puts behind its
     hero product shots; a flat white hero by contrast reads as plain
     and a little cold. Purely decorative: negative z-index keeps it
     behind all real content, pointer-events:none keeps it inert. */
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(120% 60% at 50% 0%, var(--color-accent-soft), transparent 65%);
  pointer-events: none;
}
.hero__content {
  max-width: 840px;
}
.hero__content > * + * {
  margin-top: var(--space-md);
}
.hero h1 {
  max-width: 16ch;
}
.hero__lead {
  max-width: var(--measure);
}
.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center; /* a .btn (44px) and a .badge (28px) can share this row */
  gap: var(--space-md);
  margin-top: var(--space-xl) !important;
}

/* ---- Storefront hero (homepage) --------------------------------------
   `.hero--storefront` turns the hero into a two-column grid: copy on the
   left, ONE featured `.app-card` on the right (`.hero__featured`). The
   featured app is always the most recently shipped one — a new launch
   replaces it and the old app stays in the #apps grid. Collapses to one
   column (card under copy, centered) below 800px. The base `.hero__content`
   rhythm (`> * + *` margins) is switched off on the grid children and
   re-applied inside `.hero__copy` so the card column doesn't inherit a
   stray top margin. See DESIGN-SPEC.md → "Storefront hero".
   ---------------------------------------------------------------------- */
.hero--storefront .hero__content {
  max-width: none;
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: var(--space-2xl);
  align-items: center;
}
.hero--storefront .hero__content > * + * {
  margin-top: 0;
}
.hero__copy > * + * {
  margin-top: var(--space-md);
}
.hero__copy .store-block {
  /* Same slot `.hero__actions` fills on the Sliceball page: one step
     more air above the call to action than between the text blocks. */
  margin-top: var(--space-xl);
}
.hero--storefront .store-block__badges {
  /* The copy column is ~500px wide in the two-column hero and the badge,
     the "Get help" button and the QR need ~535px on one line, so the QR
     always wraps here. Make that wrap deliberate: the badge row takes the
     full line so the QR is on its own row at every width. The basis goes
     on the badges, not the QR: the QR's `max-width: 220px` clamps its
     flex-basis before line-breaking, so `flex-basis: 100%` on it would
     not force the wrap. */
  flex-basis: 100%;
}
.hero__featured {
  display: flex;
  justify-content: flex-end;
}
.hero__featured .app-card {
  width: min(400px, 100%);
}
@media (max-width: 800px) {
  .hero--storefront .hero__content {
    grid-template-columns: minmax(0, 1fr);
  }
  .hero__featured {
    justify-content: center;
  }
}

/* ---- Help strip (homepage) ----------------------------------------------
   A slim band between the hero and the apps grid: one sentence plus the
   Support / Privacy hub links and the studio email, so help is reachable
   without scrolling to the footer. Hairline top+bottom, no fill — it is a
   divider with links, not a section. Links are real `a.link-cta`s (44px
   targets). `.help-strip__links` is a `ul[class]`, so the reset already
   strips its bullets.
   ---------------------------------------------------------------------- */
.help-strip {
  border-block: 1px solid var(--color-border);
  padding-block: var(--space-lg);
}
.help-strip .container {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md) var(--space-2xl);
}
.help-strip p {
  margin: 0;
}
.help-strip__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-lg);
  margin: 0;
}

/* Progressive-enhancement scroll parallax on the hero — the one flourish
   beyond the standard .reveal fade. Guarded by @supports AND reduced
   motion, and purely decorative: nothing depends on it running.

   IMPORTANT: this uses a document SCROLL-progress timeline
   (`scroll(root)`) with an explicit pixel range, not a `view()`
   timeline. A `view()` timeline computes progress from the subject
   element's position relative to the scrollport (phases like "entry"/
   "cover") — that model assumes the user scrolls the element into view.
   The hero is the very first section on the page, so it's already fully
   "entered" and already partway through "covering" the scrollport at
   scroll position zero, purely because of its height vs. the viewport.
   That meant `entry 0% cover 45%` was already past its end keyframe at
   scroll=0, so the hero rendered permanently at 40% opacity from first
   paint, on every page load, with no scrolling required to reproduce it
   (confirmed by checking getComputedStyle(...).opacity at scrollTop 0 in
   real Chromium). `scroll(root)` ties progress to actual document scroll
   position instead, so 0px scroll is guaranteed to be exactly the `from`
   keyframe regardless of the hero's own height — this same CSS is shared
   by two heroes of different heights (home vs. Sliceball), so it must not
   depend on the subject element's geometry. Verify any future change to
   this block by checking computed opacity at scrollTop 0, not by reading
   the range values and assuming they're correct.

   Storefront hero: the parallax applies to the COPY column only, never
   to the featured `.app-card`. The 400px range was tuned for a text-only
   hero; the featured card is ~760px tall and, on a phone, starts ~700px
   down the page, so fading the whole `.hero__content` dimmed the
   Sliceball screenshot to 40% before it had even scrolled into view
   (Jonathan, 2026-09-13: "it fades too quickly, the preview of
   sliceball"). The product shot is the point of the hero — it stays at
   full opacity until it scrolls away like any other content. */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .hero:not(.hero--storefront) .hero__content,
    .hero--storefront .hero__copy {
      animation: hero-parallax linear both;
      animation-timeline: scroll(root);
      animation-range: 0px 400px;
    }
    @keyframes hero-parallax {
      from {
        opacity: 1;
        transform: none;
      }
      to {
        opacity: 0.4;
        transform: translateY(-16px) scale(0.98);
      }
    }
  }
}

/* ---- Apps grid & app card ----------------------------------------------
   `.app-card` is the reusable product-card component for the "All apps"
   grid (and the homepage hero's single featured card). The class name
   `.games-grid` predates the studio's "apps, not just games" wording and
   is kept for stability. Adding an app is just another `.app-card` inside
   `.games-grid`, no redesign. See DESIGN-SPEC.md for the markup contract.
   ------------------------------------------------------------------------ */
.games-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 400px));
  justify-content: center;
  gap: var(--space-xl);
  margin-top: var(--space-2xl);
}

.app-card {
  position: relative; /* containing block for the stretched-link ::after */
  display: flex;
  flex-direction: column;
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: transform var(--duration-base) var(--ease-standard),
    box-shadow var(--duration-base) var(--ease-standard),
    border-color var(--duration-base) var(--ease-standard);
}
.app-card:hover {
  transform: translateY(-4px);
  border-color: var(--color-border-strong);
  box-shadow: 0 24px 48px -24px var(--shadow-color);
}
@media (prefers-reduced-motion: reduce) {
  .app-card:hover {
    transform: none;
  }
}

.app-card__visual {
  /* `width: 100%` is load-bearing for Safari. Without it WebKit resolves
     the aspect-ratio the other way round on this column-flex item —
     width FROM the content height (shot + padding = 499px → 666px wide
     inside a 400px card) — so the centered screenshot was shoved right
     and clipped by the card's overflow (Jonathan, 2026-09-13, Safari
     screenshot). An explicit width makes the ratio compute height from
     width in every engine; the automatic min-height still lets a taller
     shot grow the box. Chrome never showed the bug. */
  width: 100%;
  aspect-ratio: 4 / 3;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-lg);
  border-bottom: 1px solid var(--color-border);
  background: radial-gradient(120% 100% at 50% 15%, var(--color-accent-soft), transparent 60%),
    var(--color-bg);
}

.app-card__body {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-2xs);
  padding: var(--space-lg);
}
.app-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-2xs);
}
.app-card__platforms {
  font-size: var(--text-small);
  color: var(--color-text-secondary);
}
.app-card__title {
  font-size: var(--text-display-3);
}
/* Stretched-link: the title link is the ONLY real link in the card. Its
   ::after is absolutely positioned and stretched to `inset: 0`, so the
   whole card is clickable while there is exactly one accessible link per
   card — no nested/duplicate links, no separate "Learn more" anchor.
   `.app-card__link` deliberately has NO `position` of its own: an
   absolutely positioned element resolves against its nearest positioned
   ANCESTOR, not its parent, so leaving this un-positioned lets the
   `::after` skip past the anchor's own small box and stretch against
   `.app-card` (which IS `position: relative`) instead — that's what
   makes the whole card clickable rather than just the title text. Giving
   `.app-card__link` its own `position: relative` was a real bug shipped
   here once already: it made the anchor itself the containing block,
   shrinking the clickable `::after` down to the title's own ~137×35px
   box. Do not add `position` back to this selector. */
.app-card__link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}
.app-card__description {
  color: var(--color-text-secondary);
  font-size: var(--text-body);
}
.app-card__footer {
  margin-top: auto;
  padding-top: var(--space-sm);
}

/* ---- Device frame inside .app-card__visual -----------------------------
   A shipped app's card shows a real screenshot inside the shared
   `.device--phone` frame (below) instead of an abstract CSS scene.
   `.app-card__device` only sets the frame's width: `min(180px, 100%)`
   is the widest the phone can be while the whole card still reads as a
   card and not a hero (Jonathan approved this from the "direction C"
   mockup, 2026-09-13). The frame's height comes from the screenshot's
   own width/height attributes, so the 4:3 visual box simply grows to
   fit it and nothing is ever cropped. Do NOT size it by height: the
   box's aspect-ratio height is not definite for a flex child, so a
   height-based size falls back to the image's 900px natural width and
   breaks the card (that bit the previous glass-ring shot). Use the
   island-free `screen-*.webp` crops here, never the raw store shots.
   The selector is two classes on purpose: `.device--phone` (below, later
   in the file) sets its own `--device-w: 250px` at single-class
   specificity, so a bare `.app-card__device` would lose on source order
   and the card would get the 250px Sliceball-page phone.
   ---------------------------------------------------------------------- */
.app-card__visual .app-card__device {
  --device-w: min(180px, 100%);
}

/* ---- Generic device frames (shared: homepage card + Sliceball page) -----
   Jonathan, 2026-09-13: "screengrabs need to all be on generic smart
   devices, not iPhone-looking ones with an island". One phone, one
   tablet, one graphite finish, used everywhere footage or a screenshot
   is shown. Deliberately NOTHING but a uniform bezel and rounded corners:
   no island, no notch, no home button, no side buttons, no camera
   housing. That is the line Apple's marketing guidelines draw for
   "generic devices" (no detail unique to Apple hardware), and it is what
   keeps the frame from reading as either an iPhone or a Pixel.

   Sizing: set `--device-w` on the element (any length, `min()` is fine);
   bezel and radius are percentages of it so every size looks like the
   same object. The SCREEN's height comes from the media's own
   width/height attributes (`height: auto`), so put the real pixel size
   on every <img>/<video> inside and nothing is ever cropped:
     phone gameplay clip  444 x 960   (assets/showcase/gameplay-iphone.*)
     tablet gameplay clip 720 x 960   (assets/showcase/gameplay-ipad.*)
     screenshots          900 x 1843  (assets/showcase/screen-*.webp —
                          the store shots with the island cropped off
                          the top, 112px -- the black pill spans rows 30-104 in every store shot, and an earlier 72px cut left its bottom third showing; the originals live in _to_delete/)
   Radii: phone bezel 3.2% / corner 17.6%; tablet bezel 3.6% / corner 6%
   (a real tablet's corners are much tighter than a phone's). The screen
   radius is corner minus bezel so the two curves stay concentric.
   ---------------------------------------------------------------------- */
.device {
  position: relative;
  box-sizing: border-box;
  flex-shrink: 0;
  background: #2b2b30;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14),
    0 40px 80px -30px var(--shadow-color);
}
.device--phone {
  --device-w: 250px;
  width: var(--device-w);
  padding: calc(var(--device-w) * 0.032);
  border-radius: calc(var(--device-w) * 0.176);
}
.device--phone .device__screen {
  border-radius: calc(var(--device-w) * 0.144);
}
.device--tablet {
  --device-w: 440px;
  width: var(--device-w);
  padding: calc(var(--device-w) * 0.036);
  border-radius: calc(var(--device-w) * 0.06);
}
.device--tablet .device__screen {
  border-radius: calc(var(--device-w) * 0.024);
}
.device__screen {
  display: block;
  width: 100%;
  height: auto;
  object-fit: cover;
  background: #000;
}

/* ---- Store block (badges + QR) -----------------------------------------
   The primary call to action on the Sliceball page (and, pre-launch, a
   plain badge + button on the homepage's Skim card — see
   `.store-block__badges` below). Google's Play badge
   (assets/store/google-play-badge.png, downloaded from
   play.google.com/intl/en_us/badges with Jonathan's OK, 2026-09-13) and
   Apple's App Store badge (assets/store/apple-app-store-badge.svg, pulled
   from Apple's own badge-generator endpoint with his OK, 2026-09-15) are
   each OTHER COMPANIES' artwork and must never be recoloured, re-drawn,
   cropped, stretched, or animated.

   **Both badges are sized so their visible PILLS are the same WIDTH
   (`--store-pill-w`), not so their `<img>` boxes match.** Measured
   pixel content of each asset (canvas bbox against a magenta
   background): Google's PNG is a 646×250 canvas whose pill is 564×168
   with exactly 41px of transparent clear space on every side; Apple's
   SVG has NO built-in padding — the pill IS the full 119.66×40 canvas.
   The two pills also have different aspect ratios (Apple 2.99:1,
   Google 3.36:1), so they can never match in both dimensions without
   stretching one, which both companies forbid. Width is the dimension
   that matters here: the pairs stack into a column on the homepage
   (and on narrow Sliceball viewports), where equal widths put both
   pill edges on the same lines; the ~6px height difference is
   centred by the row's `align-items: center` and reads as nothing.
   (History: 2026-09-15 matched the pill HEIGHTS instead — 46px Apple /
   68.45px Google box — after Jonathan flagged a 60px-box mismatch;
   2026-09-16 he flagged the resulting 17px width mismatch, hence the
   switch.) Google's `<img>` is scaled so its PILL, not its canvas,
   is `--store-pill-w` wide, and its transparent 41px side gutters are
   pulled out with a matching negative `margin-inline` so the pill
   edge — not the invisible canvas edge — sits on the grid column.
   The pixels are untouched; only the transparent padding overlaps
   the gap. Both still clear their own guideline's minimum (Apple: ≥40pt
   tall — 53px here; Google: "not smaller than the App Store badge" —
   equal width).

   Since 2026-09-15 (two stores live) each store gets its own
   `.store-block__store` — badge directly beside ITS OWN QR code, so it's
   unambiguous which QR opens which store (Jonathan flagged the earlier
   layout, one row of badges above a separate row of QR codes, as
   ambiguous). The divider that used to sit before each QR now sits
   BETWEEN store groups instead (`.store-block__store + .store-block__store`).
   Each QR (assets/store/qr-app-store.svg, qr-google-play.svg) points at
   that same store's listing and is hidden on coarse-pointer devices and
   at narrow widths (a phone/tablet in the hand cannot usefully scan
   itself) — at that point only the two badges remain, still visually
   separated by the same divider.
   ---------------------------------------------------------------------- */
.store-block {
  --store-pill-w: 160px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* No column gap: the two store groups meet at their shared divider
     (see `.store-block__store` padding) so it lands on the exact
     centre; the row gap still separates stacked rows. */
  gap: var(--space-sm) 0;
}
.store-block__badges {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
}
.store-badge {
  display: inline-flex;
  border-radius: var(--radius-sm);
}
.store-badge img {
  /* Google's PNG: 646 canvas / 564 pill / 41 clear space per side.
     See the comment above this block. */
  display: block;
  flex: none;
  width: calc(var(--store-pill-w) * 646 / 564);
  max-width: none; /* the global img cap would clamp it to the anchor's pill width */
  height: auto;
  margin-inline: calc(var(--store-pill-w) * -41 / 564);
}
.store-badge--apple img {
  width: var(--store-pill-w);
  margin-inline: 0;
}
.store-block__store {
  /* Grid, not flex, for the badge+QR pairing: a badge column exactly
     one pill wide and a QR column, so when the two pairs stack (the
     homepage's narrow copy column) both badges and both QRs sit on the
     same vertical lines. Centering badge+QR as a flex row put each
     row's QR at a different x back when the badges had different
     widths (Jonathan caught this 2026-09-16). */
  display: grid;
  grid-template-columns: var(--store-pill-w) auto;
  column-gap: var(--space-sm);
  flex: 1 1 0;
  /* Floor = a pair's content (160 pill + 16 gap + 72 QR = 248) plus its
     24px divider padding, plus the 1px line. Two groups then need 546px,
     comfortably inside the narrowest block the ≥641px desktop range can
     produce (measured: 300px per group already wrapped at 641px in
     WebKit), so there is NO width at which they wrap on the Sliceball
     page before the ≤640px phone layout takes over — a wrapped pair
     would inherit the end/start divider alignment below and land on the
     wrong side of its row. */
  min-width: 273px;
  align-items: center;
  justify-content: center;
}
.store-block__store .store-badge {
  justify-self: end;
}
/* Divider between the two groups when they share a line. Symmetric
   padding on both sides of it (plus zero column gap on `.store-block`)
   is what puts the 1px line on the block's exact centre: with
   `flex: 1 1 0` both groups get equal content boxes, so the boundary
   between them — where the border lives — is the midpoint. A one-sided
   padding-left (the earlier version) sat the divider 4.5px left of
   centre and the second group's content 9px further from it than the
   first's. */
.store-block__store:first-child {
  /* Each group's content hugs the divider rather than floating in the
     middle of its own half — otherwise a 900px block leaves ~100px of
     air either side of the line and the two pairs read as pushed to
     the edges (Jonathan, 2026-09-16: "bring these close to the
     center"). Both pairs are the same width (pill + gap + QR), so
     end/start alignment is symmetric about the divider. */
  padding-right: var(--space-md);
  justify-content: end;
}
.store-block__store + .store-block__store {
  padding-left: var(--space-md);
  border-left: 1px solid var(--color-border);
  justify-content: start;
}
.hero--storefront .store-block__store {
  /* The divider reads fine between two groups sitting side by side
     (Sliceball's hero, `.hero--story`, where its ~900px width fits
     both) — but `.hero--storefront`'s narrow copy column always stacks
     the two groups onto separate lines, where a leftover border/padding
     from a "these are side by side" rule just pushes one row's badge
     column off the other's, undoing the alignment above.
     `:first-child`/`+` match on DOM order, not visual line, so this
     needs an explicit override rather than assuming it's a no-op once
     wrapped. Same for the end/start alignment: stacked rows must both
     centre, or the two badges land on different lines. `flex-basis:
     100%` makes the stack unconditional — the one-column hero below
     800px is wide enough for two groups, and side by side without a
     divider they'd just float apart. */
  flex-basis: 100%;
  padding-inline: 0;
  border-left: none;
  justify-content: center;
}
.store-block__qr img {
  display: block;
  width: 72px;
  height: 72px;
}
@media (pointer: coarse), (max-width: 640px) {
  .store-block {
    /* Two 160px pills plus the divider need ~369px, more than a 360–375px
       phone has between its gutters, so the pill shrinks here: 140px on
       anything ≥ 375px (2×140 + 2×12 padding + 1px line = 305px), and
       38vw below that so a 360px Android still fits (137px pills → 299px
       in a 320px content box). Apple's pill stays ≥ 40pt tall down to
       320px viewports (121px wide → 40.6px). `nowrap` guarantees the two
       never fall onto separate lines, where the DOM-order divider
       padding would knock the second one out of alignment. */
    --store-pill-w: min(140px, 38vw);
    flex-wrap: nowrap;
  }
  .store-block__qr {
    display: none;
  }
  .store-block__store {
    /* The 340px min-width exists to force each store's badge+QR pair to
       wrap onto its own line rather than squeezing (see the comment
       above `.store-block__store`) — but with the QR hidden here a bare
       badge is only one pill wide, so that floor would force an
       unnecessary stack. Drop it back to natural content width so both
       badges sit side by side with their divider, like before either
       QR existed. Back to flex too — the fixed badge column above
       exists only to align badges with their QR, which isn't on screen
       here. */
    display: flex;
    flex: 0 1 auto;
    min-width: 0;
  }
  .store-block__store:first-child {
    padding-right: var(--space-xs);
  }
  .store-block__store + .store-block__store {
    padding-left: var(--space-xs);
  }
  .hero--storefront .store-block__store {
    flex: 0 1 auto; /* re-cancel the desktop stack's `flex-basis: 100%` */
    padding-inline: 0;
  }
  .hero--storefront .store-block {
    /* No divider on the homepage (see `.hero--storefront .store-block__store`
       above), so the two badges need a gap of their own here. */
    column-gap: var(--space-sm);
  }
}

/* ---- Sliceball abstract visual -----------------------------------------
   Pure CSS/SVG-free illustration of the core mechanic (a card stack
   holding a ball above a floor line) — no screenshots or product art
   exist yet. `.sliceball-scene` is specific to this one game; a future
   game's card should get its own `.<gamename>-scene` visual following the
   same pattern (see DESIGN-SPEC.md).

   Default (below) is a static "mid-slice" illustrative frame — this IS
   the prefers-reduced-motion fallback, applied unconditionally. Motion is
   only ever ADDED on top of it inside the `no-preference` media query.
   ------------------------------------------------------------------------ */
.sliceball-scene {
  width: min(220px, 100%);
}
.sliceball-scene__phone {
  position: relative;
  aspect-ratio: 9 / 18.5;
  padding: 10px;
  background: var(--color-bg);
  border: 1px solid var(--color-border-strong);
  border-radius: clamp(28px, 10%, 40px);
  box-shadow: 0 30px 60px -30px var(--shadow-color);
}
.sliceball-scene__island {
  position: absolute;
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
  width: 38%;
  height: 8px;
  background: var(--color-text);
  opacity: 0.15;
  border-radius: var(--radius-full);
}
.sliceball-scene__screen {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  padding-bottom: 20%;
  background: var(--color-bg-elevated);
  border-radius: clamp(20px, 8%, 30px);
  overflow: hidden;
}
.sliceball-scene__floor {
  position: absolute;
  left: 14%;
  right: 14%;
  bottom: 14%;
  height: 2px;
  background: var(--color-accent);
  opacity: 0.55;
}
.sliceball-scene__stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.sliceball-scene__card {
  display: block;
  height: 10px;
  border-radius: 4px;
  background: var(--color-border-strong);
}
.sliceball-scene__card:nth-child(1) {
  width: 46px;
  /* static fallback: topmost card already mid-slice */
  opacity: 0.4;
  transform: scale(0.75) translateX(-8px);
}
.sliceball-scene__card:nth-child(2) {
  width: 56px;
}
.sliceball-scene__card:nth-child(3) {
  width: 66px;
}
.sliceball-scene__ball {
  position: absolute;
  bottom: calc(20% + 34px);
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: 0 0 0 6px var(--color-accent-soft);
  /* static fallback: ball mid-drop, between the top two cards */
  transform: translateY(14px);
}

@media (prefers-reduced-motion: no-preference) {
  .sliceball-scene__ball {
    animation: sliceball-drop 7s var(--ease-standard) infinite alternate;
  }
  .sliceball-scene__card:nth-child(1) {
    animation: sliceball-slice-1 7s var(--ease-standard) infinite alternate;
  }
  .sliceball-scene__card:nth-child(2) {
    animation: sliceball-slice-2 7s var(--ease-standard) infinite alternate;
  }
}
@keyframes sliceball-drop {
  0%,
  28% {
    transform: translateY(0);
  }
  38%,
  58% {
    transform: translateY(22px);
  }
  70%,
  100% {
    transform: translateY(44px);
  }
}
@keyframes sliceball-slice-1 {
  0%,
  28% {
    opacity: 1;
    transform: none;
  }
  36%,
  100% {
    opacity: 0;
    transform: scale(0.6) translateX(-10px);
  }
}
@keyframes sliceball-slice-2 {
  0%,
  58% {
    opacity: 1;
    transform: none;
  }
  68%,
  100% {
    opacity: 0;
    transform: scale(0.6) translateX(10px);
  }
}

/* ---- Skim abstract visual -----------------------------------------------
   Pure CSS illustration of the core mechanic (a stone skipping across
   still water) for the homepage card, following the same pattern as
   `.sliceball-scene` above — no screenshots or product art exist yet,
   since Skim is still in development (see DESIGN-SPEC.md → "Skim
   placeholder"). Unlike `.sliceball-scene`, this one has NO animation at
   all: the site's motion budget is deliberately capped at the two
   existing moments (the Sliceball card and the hero parallax) — see the
   note at the end of the `.sliceball-scene` comment above. A single
   frozen frame (stone mid-hop, one ripple already fading, the next just
   forming) implies the motion without adding a third animated element.
   ------------------------------------------------------------------------ */
.skim-scene {
  width: min(220px, 100%);
}
.skim-scene__phone {
  position: relative;
  aspect-ratio: 9 / 18.5;
  padding: 10px;
  background: var(--color-bg);
  border: 1px solid var(--color-border-strong);
  border-radius: clamp(28px, 10%, 40px);
  box-shadow: 0 30px 60px -30px var(--shadow-color);
}
.skim-scene__island {
  position: absolute;
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
  width: 38%;
  height: 8px;
  background: var(--color-text);
  opacity: 0.15;
  border-radius: var(--radius-full);
}
.skim-scene__screen {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--color-bg-elevated);
  border-radius: clamp(20px, 8%, 30px);
}
.skim-scene__water {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 40%;
  background: linear-gradient(to bottom, transparent, var(--color-accent-soft));
}
.skim-scene__waterline {
  position: absolute;
  left: 10%;
  right: 10%;
  bottom: 40%;
  height: 2px;
  background: var(--color-accent);
  opacity: 0.55;
}
.skim-scene__ripple {
  position: absolute;
  bottom: 34%;
  width: 16px;
  height: 16px;
  border: 2px solid var(--color-accent);
  border-radius: 50%;
  transform: translateX(-50%);
}
.skim-scene__ripple--1 {
  left: 30%;
  opacity: 0.15;
  transform: translateX(-50%) scale(1.6);
}
.skim-scene__ripple--2 {
  left: 56%;
  opacity: 0.45;
  transform: translateX(-50%) scale(1);
}
.skim-scene__stone {
  position: absolute;
  bottom: calc(40% + 30px);
  left: 74%;
  width: 16px;
  height: 9px;
  background: var(--color-text);
  border-radius: 3px;
  opacity: 0.85;
  transform: translateX(-50%) rotate(-10deg);
}

/* ---- Device figure + pause chip (Sliceball page) -----------------------
   `.device-figure` is the unit the Sliceball page repeats: one `.device`
   (see "Generic device frames" above) with, when the screen is a looping
   <video>, a `.video-toggle` chip directly beneath it. A flex column so
   the chip is always centred under the device whatever `--device-w` is.

   Why the pause control is a chip UNDER the device and not a glyph on
   the screen (2026-09-13): the game footage already shows Sliceball's
   own in-game pause button in the corner, so an overlaid pause glyph
   read as a second copy of that control and it was not obvious which
   one was the page's. The chip stays for WCAG 2.2.2 (Pause, Stop, Hide:
   any auto-starting motion longer than 5s needs a visible stop) -
   script.js wires it, keeps `data-state` / `aria-pressed` / `aria-label`
   in sync with the video's own play/pause events, and never auto-starts
   the clip under `prefers-reduced-motion: reduce`.

   Chip shape: the `.badge--coming-soon` pill language (hairline border,
   `--radius-full`, `--text-small`, 600 weight) in `--color-text-secondary`
   so it reads as a quiet utility control, not a call to action. 32px
   tall visually; the transparent `::before` extends the hit area to 44px
   (WCAG 2.5.8 / Apple HIG) without making the pill itself taller. Both
   the icon and the label are drawn twice (pause + play variants) and
   swapped by `data-state` in CSS, so script.js only flips one attribute.
   ---------------------------------------------------------------------- */
.device-figure {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
}
.video-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  height: 32px;
  padding: 0 var(--space-sm);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-full);
  background: var(--color-bg);
  color: var(--color-text-secondary);
  font-family: var(--font-text);
  font-size: var(--text-small);
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
}
.video-toggle::before {
  /* Invisible hit-area extension: 6px above and below the 32px pill
     makes the target 44px tall. Horizontal extent is already > 44px. */
  content: "";
  position: absolute;
  inset: -6px 0;
}
.video-toggle:hover {
  color: var(--color-text);
}
.video-toggle__icon,
.video-toggle__text {
  display: none;
}
.video-toggle[data-state="playing"] .video-toggle__icon--pause,
.video-toggle[data-state="playing"] .video-toggle__text--pause,
.video-toggle[data-state="paused"] .video-toggle__icon--play,
.video-toggle[data-state="paused"] .video-toggle__text--play {
  display: block;
}
.video-toggle__icon--pause {
  width: 3px;
  height: 10px;
  background: currentColor;
  box-shadow: 5px 0 0 currentColor;
  margin-right: 5px; /* room for the box-shadow bar, which takes no layout space */
}
.video-toggle__icon--play {
  width: 0;
  height: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-left: 8px solid currentColor;
}

/* ---- Story hero (Sliceball page) ---------------------------------------
   `.hero--story` centres the base hero; the phone sits entirely inside
   it on the plain hero background, same as the tablet below sits
   entirely inside its own grey band. (Direction C originally let the
   phone hang down over the grey band below it, straddling both
   backgrounds. Jonathan flagged that seam 2026-09-13 -- it made the
   hero phone the only device on the page with a mixed white/grey
   backdrop instead of one flat color -- so the overlap was removed.)
   ---------------------------------------------------------------------- */
.hero--story {
  text-align: center;
}
.hero--story .hero__content {
  margin-inline: auto;
  /* Wide enough for two full store-block__store groups (badge + its own
     QR, both stores) on one line at desktop widths — two groups measure
     ~831px plus their gap, a hair over the base 840px cap, which forced
     an unwanted line-wrap (2026-09-15). h1/lead stay centred and short
     either way, so the extra width doesn't hurt their readability. */
  max-width: 900px;
}
.hero--story h1,
.hero--story .hero__lead {
  margin-inline: auto;
}
.hero--story .store-block,
.hero--story .store-block__badges {
  /* Both levels centre: the block itself, and the badge row inside it
     once the Play badge and the coming-soon pill wrap onto two lines. */
  justify-content: center;
}
.hero--story .store-block {
  margin-top: var(--space-xl);
}
.hero__device {
  margin-top: var(--space-2xl);
}
.hero__device .device--phone {
  --device-w: min(330px, 70vw);
}
.hero__device .skim-scene {
  width: min(280px, 70vw);
}

/* ---- Tablet band (Sliceball page) --------------------------------------
   The `--color-bg-elevated` section below the hero. Two columns
   (copy | tablet) on desktop; copy-first, centred, single column at
   800px and below - the same breakpoint the storefront hero collapses
   at.
   ---------------------------------------------------------------------- */
.story-tablet {
  background: var(--color-bg-elevated);
  padding-block: var(--space-section);
}
.story-tablet__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
  gap: var(--space-2xl);
  align-items: center;
}
.story-tablet__copy > * + * {
  margin-top: var(--space-md);
}
.story-tablet__copy h2 {
  margin-top: var(--space-2xs);
}
.story-tablet__pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}
.story-tablet__device .device--tablet {
  /* `100%` (the grid column) rather than a vw: at 1280 the column is
     ~500px, so a fixed 520px would spill 10px into the gutter. */
  --device-w: min(520px, 100%);
}

/* ---- Feature rows (Sliceball page) ------------------------------------
   Three rows, each copy + one framed screenshot, alternating sides
   (text | phone, phone | text, text | phone). The swap is `order` on
   even rows' copy, so the DOM is always copy-then-image and reading
   order never changes. `.feature-row__index` is the old step-card's
   "01/02/03" style, kept as the row's small accent counter. At 800px
   and below each row stacks: centred copy, then a 220px phone.
   ---------------------------------------------------------------------- */
.feature-rows .container > * + * {
  margin-top: var(--space-3xl);
}
.feature-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-2xl);
  align-items: center;
}
.feature-row__copy {
  max-width: var(--measure);
}
.feature-row__copy > * + * {
  margin-top: var(--space-md);
}
.feature-row:nth-child(even) .feature-row__copy {
  order: 2;
}
.feature-row__index {
  display: block;
  font-family: var(--font-text);
  font-size: var(--text-small);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--color-accent);
}
.feature-row__copy h2 {
  margin-top: var(--space-2xs);
}
.feature-row__device {
  display: flex;
  justify-content: center;
}
.feature-row__device .device--phone {
  --device-w: min(260px, 70vw);
}

@media (max-width: 800px) {
  .story-tablet__grid {
    grid-template-columns: minmax(0, 1fr);
    text-align: center;
  }
  .story-tablet__pills {
    justify-content: center;
  }
  .feature-rows .container > * + * {
    margin-top: var(--space-2xl);
  }
  .feature-row {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-lg);
    text-align: center;
  }
  .feature-row__copy {
    margin-inline: auto;
  }
  .feature-row:nth-child(even) .feature-row__copy {
    order: 0;
  }
  .feature-row__device .device--phone {
    --device-w: min(220px, 70vw);
  }
}

/* ---- Contact ------------------------------------------------------------- */
.contact-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-lg);
  margin-top: var(--space-2xl);
}
.contact-card {
  padding: var(--space-lg);
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.contact-card__label {
  font-size: var(--text-small);
  color: var(--color-text-secondary);
  margin-bottom: var(--space-2xs);
}
.contact-card a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--color-accent);
  word-break: break-word;
}

/* ---- Footer ---------------------------------------------------------------- */
.site-footer {
  border-top: 1px solid var(--color-border);
  background: var(--color-bg-elevated);
  padding-block: var(--space-2xl);
}
.footer-grid {
  /* Explicit equal-width columns, not auto-fit's greedy sizing -- auto-fit
     with minmax(160px, 1fr) can size the 4 columns unevenly depending on
     exactly how much space is left over, which read as lopsided. Fixed
     column counts per breakpoint keep every column in a row the same
     width, always. */
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-xl);
  margin-bottom: var(--space-xl);
}
@media (max-width: 700px) {
  .footer-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (max-width: 420px) {
  .footer-grid {
    grid-template-columns: 1fr;
  }
}
.footer-col h3 {
  font-family: var(--font-text);
  font-size: var(--text-small);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-secondary);
  margin-bottom: var(--space-xs);
}
.footer-col ul {
  /* This <ul> has no class attribute (it's reached via this descendant
     selector, not `ul[class]`), so the site-wide bullet/padding reset near
     the top of this file doesn't touch it -- without these three lines it
     keeps the browser default disc bullet and ~40px indent, throwing off
     alignment under each column heading. */
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
}
.footer-col a {
  /* Deliberately NOT the 44px touch target used for primary nav/buttons
     elsewhere on the site (see the Accessibility checklist in
     DESIGN-SPEC.md) -- Jonathan asked for these three columns visually
     tighter. 32px still clears WCAG 2.2's 24x24px minimum target size
     (the AA bar; 44px is Apple HIG's stricter recommendation, not an AA
     requirement), so this stays accessible while reading denser, closer
     to how apple.com's own footer links sit close together. */
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  font-size: var(--text-small);
  color: var(--color-text);
  transition: color var(--duration-fast) var(--ease-standard);
}
.footer-col a:hover {
  color: var(--color-accent);
}
.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--color-border);
  font-size: var(--text-small);
  color: var(--color-text-secondary);
}

/* ---- Legal / support long-form content ------------------------------------
   `.legal-content` is the flowing-prose wrapper used by the privacy and
   support pages (sliceball/privacy/, sliceball/support/): a long run of
   headings, paragraphs and lists that doesn't break into homepage-style
   `.section-header` blocks. Vertical rhythm applies between direct children
   only; a `h2` gets extra space above it to mark a new major topic. Pair
   with `.measure` on the same element to keep line length readable.
   `.legal-list` is the matching bullet-list treatment (the global reset
   strips list-style/padding from any `ul[class]`, so this restores both).
   ---------------------------------------------------------------------- */
.legal-content > * + * {
  margin-top: var(--space-lg);
}
.legal-content > h2 {
  margin-top: var(--space-2xl);
}
.legal-content > h2:first-child {
  margin-top: 0;
}
.legal-list {
  list-style: disc;
  padding-left: 1.25em;
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

/* Plain prose links inside .legal-content (e.g. the Google policy links
   and cross-links to support/privacy in the privacy/support pages) were
   invisible as links: the global reset sets `a { color: inherit;
   text-decoration: none; }`, and nothing in .legal-content overrode it,
   so these read as plain text with no affordance that they're clickable.
   Every other link pattern on the site (.link-cta, .contact-card a,
   .footer-col a, .site-nav a) already gets an explicit accent/underline
   treatment — this brings plain in-paragraph links into line. Does not
   apply to .legal-list markers or headings, only actual <a> elements. */
.legal-content a {
  color: var(--color-accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
}
.legal-content a:hover {
  text-decoration-thickness: 2px;
}
