/* ==========================================================================
   Yard template - base
   Fonts, reset, type, layout primitives, the four bands, the grain.
   Every value comes from tokens.css. Nothing is hardcoded here.
   ========================================================================== */

/* --- Fonts -----------------------------------------------------------------
   One variable file, Latin subset, self hosted, preloaded from each page head
   because it carries everything.
   ------------------------------------------------------------------------ */

@font-face {
  font-family: "Work Sans";
  src: url("../assets/fonts/work-sans-latin-wght-normal.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* Metric matched shim, so body copy does not jump when Work Sans lands.
   Values computed from the font's own metrics against Arial, weighted by
   English letter frequency. */
@font-face {
  font-family: "Work Sans fallback";
  src: local("Arial"), local("Helvetica Neue"), local("Helvetica");
  size-adjust: 113.2%;
  ascent-override: 82.15%;
  descent-override: 21.47%;
  line-gap-override: 0%;
}


/* --- Reset ----------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

* { -webkit-tap-highlight-color: rgb(0 0 0 / 0); }

html {
  font-size: 100%;                     /* never 62.5% */
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;      /* stops iOS landscape zoom */
  text-size-adjust: 100%;
  scroll-padding-top: calc(var(--header-h) + var(--space-5));
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: var(--w-body);
  font-size: var(--step-0);
  line-height: var(--lh-body);
}

@media (max-width: 40rem) {
  body { line-height: 1.5; }
}

/* Room for the fixed phone bar, reserved from first paint so nothing shifts. */
@media (max-width: 47.99rem) {
  body { padding-block-end: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px)); }
}

img, video, svg { max-width: 100%; height: auto; display: block; }

ul[class], ol[class] { list-style: none; margin: 0; padding: 0; }

button, input, select, textarea { font: inherit; color: inherit; }
input, select, textarea, button { font-size: max(16px, 1rem); }  /* iOS zooms under 16px */


/* --- Type ------------------------------------------------------------------ */

h1, h2, h3, h4 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--w-display);
  line-height: var(--lh-heading);
  letter-spacing: var(--ls-heading);
  text-wrap: balance;
  max-width: var(--measure-head);
}

h1 { font-size: var(--step-5); line-height: var(--lh-display); letter-spacing: var(--ls-display); }
h2 { font-size: var(--step-4); }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }

.h-hero {
  font-size: var(--step-hero);
  font-weight: var(--w-black);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-display);
  max-width: 14ch;
}

p { margin: 0; max-width: var(--measure-prose); }
p, li { text-wrap: pretty; }
p + p { margin-block-start: 1.2em; }
/* ...but not inside a container that already spaces its children with `gap`.
   A gap and a margin are two ways of saying the same thing and a paragraph in
   a gapped grid collects both, so the space between two paragraphs came out
   at 24px + 19px where every other pair on the page got 19px. On a phone that
   reads as a missing element rather than as a paragraph break. Found in three
   places by measuring, not by looking: .see-it__copy on the homepage,
   .prow__copy on the range page and .enquiry__foot on the form. */
:where(.see-it__copy, .prow__copy, .enquiry__foot) > p + p { margin-block-start: 0; }

.lead {
  font-size: var(--step-1);
  line-height: 1.45;
  max-width: var(--measure-lead);
}

.muted { color: var(--steel); }

.tabular { font-variant-numeric: tabular-nums; }

strong { font-weight: var(--w-medium); }

a {
  color: var(--red-text);
  text-decoration-thickness: 0.08em;
  text-underline-offset: 0.18em;
  transition: color var(--dur-fast) var(--ease-out);
}
@media (hover: hover) { a:hover { color: var(--red-press); } }


/* --- Focus ----------------------------------------------------------------- */

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus-on-light);
  outline-offset: 3px;
}

:where(a, button, input, select, textarea, summary) { scroll-margin-block: 6rem; }

/* Anything the nav jumps to. The header is sticky, so without this the browser
   scrolls the section to the very top of the window and the header then sits
   on top of its heading. 6rem clears the header and leaves a little air. */
:where(section, article, div)[id] { scroll-margin-block-start: 6rem; }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  inset-block-start: var(--space-2);
  inset-inline-start: var(--space-2);
  z-index: calc(var(--z-header) + 1);
  padding: var(--space-3) var(--space-4);
  background: var(--yellow);
  color: var(--ink);
  font-weight: var(--w-medium);
  text-decoration: none;
  transform: translateY(-200%);
}
.skip-link:focus { transform: none; }


/* --- Layout primitives ----------------------------------------------------- */

.container {
  width: min(var(--container-wide), 100% - 2 * var(--page-gutter));
  margin-inline: auto;
}
.container--content { width: min(var(--container-content), 100% - 2 * var(--page-gutter)); }
.container--prose   { width: min(var(--container-prose),   100% - 2 * var(--page-gutter)); }

.section { padding-block: var(--section-padding); }

.stack > * + * { margin-block-start: var(--space-4); }
.stack--lg > * + * { margin-block-start: var(--space-6); }


/* --- The four bands --------------------------------------------------------
   Every section sits on one of these. The page rhythm is set in the plan:
   dark, light, light, dark, light, tint, light, tint, yellow, dark.
   Never two --ink bands together. --yellow as a band once only.
   ------------------------------------------------------------------------ */

.band--paper   { background: var(--paper);   color: var(--ink); }
.band--paper-2 { background: var(--paper-2); color: var(--ink); }

.band--ink {
  background-color: var(--ink);
  color: var(--on-dark);
}

.band--yellow { background: var(--yellow); color: var(--ink); }

/* Dark context. Links and focus rings change, and red text is not allowed. */
.band--ink a            { color: var(--on-dark); }
.band--ink .muted       { color: var(--mist); }
@media (hover: hover) { .band--ink a:hover { color: var(--yellow); } }
.band--ink :where(a, button, input, select, textarea, summary):focus-visible {
  outline-color: var(--focus-on-dark);
}

/* Yellow context. Ink links, ink focus ring. */
.band--yellow a { color: var(--ink); }

/* There is no button override for the dark bands any more.

   The buttons on charcoal went through white fills, white borders, white
   shadows and a hollow outline between the 21st's first and last commit, and
   every one of them was a second treatment that had to be explained. They are
   now exactly what they are on paper: a yellow fill, an --ink border and a
   solid black slab when they lift. Ben, closing it: "I want black borders as
   well."

   It works on charcoal for a reason that is easy to miss. The border is the
   same colour as the band it sits on, so it does not read as an outline at
   all: it reads as a dark seam between the yellow and the background, and the
   thing that makes the control findable is the yellow fill itself, which is
   11.32:1 against --ink. The border only has to do contrast work on paper,
   where yellow is 1.32:1 and the edge is the only thing separating a button
   from the page.

   So one button, one shadow, one set of tokens, everywhere. --shadow-hard and
   the --btn-* values in tokens.css are now the whole story. */



/* The grain overlay came off on 19 September, with the ribs. A 5% noise tile
   over the whole page is a third layer of texture, and it was working against
   the flat surfaces rather than with them. assets/noise-128.png is still
   there, and scripts/make_noise.py still rebuilds it.
   ------------------------------------------------------------------------ */


/* --- Sections arriving --------------------------------------------------------
   A section rises into place as it comes up the screen. Ben asked for it on
   21 September, which overrides the "no reveal on scroll" line in the plan;
   that line is now struck out there rather than quietly ignored.

   It follows the recipe in picsel-design-standard.md, and the two rules that
   matter are both about what happens when it does NOT run:

   1. **The default state is visible.** Nothing here can hide content: the
      keyframes move the section and never touch its opacity, for the reason
      written above them. The whole thing is also wrapped in @supports, so a
      browser without scroll driven animation is handed a finished page.
   2. **No script.** `animation-timeline: view()` is the browser doing this on
      the compositor. An IntersectionObserver would mean shipping JavaScript
      whose failure mode is the blank page above.

   The hero is excluded: it is already on screen when the page loads, so there
   is nothing for it to arrive from.

   Sections holding a `position: sticky` child are excluded too. An element
   with a transform becomes the containing block for its descendants, which
   stops sticky working inside it for as long as the animation is live. That is
   the enquiry page's contact panel.
   ------------------------------------------------------------------------ */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    main > section:not(.hero):not(:has(.contact)) {
      animation: section-arrive linear both;
      animation-timeline: view();
      /* Done by the time the section's top edge is 40% up the viewport, not
         tied to `cover`, which on a section taller than the screen would
         still be moving while the reader is halfway down it. Finishing early
         is what makes it read as arrival rather than as drift. */
      animation-range: entry 0% entry 40%;
    }
  }
}

/* It rises. It does not fade, and that is not a stylistic preference.

   The first version faded from opacity 0, which is what the standard's example
   does, and `node scripts/qa.js` failed it: 42 elements under axe
   colour-contrast at 1440px, starting at #range-title. The reason is
   `fill-mode: both`. Before a section reaches its range it holds the `from`
   frame, so every section below the fold sat at opacity 0 at rest. Scrolling
   fixed it, and nothing else did: not a screen reader's virtual cursor, not
   find-in-page, not a browser that stalled the compositor. Text that is
   invisible until you scroll is invisible text.

   Taking opacity out of the keyframes removes the failure at its cause rather
   than hiding it. A section below the fold now sits 24px low instead of
   invisible, which nobody can see and no audit can object to, and the arrival
   still reads because the movement was always doing that work. */
@keyframes section-arrive {
  from { translate: 0 2.5rem; }
  to   { translate: 0 0; }
}


/* --- Motion floor ---------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
