/**
 * Elegant - design system and components.
 *
 * Structure: tokens -> reset -> primitives -> layout -> components.
 * Viewport-level layout lives in responsive.css; motion lives in animations.css.
 *
 * Two rules this file follows throughout:
 *  1. Every modern feature is ADDITIVE. Delete any @supports block and the page is
 *     still complete, legible and usable.
 *  2. No raster images. The visual language is built from gradients, SVG and type, so
 *     there is nothing to lazy-load, nothing to shift layout, and no stock photography
 *     pretending to be this business. Real photography replaces it when the client
 *     supplies it - see docs/CLIENT-HANDOVER.md.
 */

/* ========================================================================== */
/* Tokens                                                                      */
/* ========================================================================== */

:root {
  /**
   * Typography.
   *
   * Deliberately system-only. Every Serbian diacritic (č ć š ž đ) lives in Latin
   * Extended-A, which the Google Fonts "latin" subset does not contain - a webfont
   * loaded carelessly renders "hemijsko čišćenje" with boxes or a mismatched fallback.
   * System stacks cover Latin Extended-A on Windows, macOS, iOS and Android, cost zero
   * requests, and give the best possible LCP.
   *
   * To swap in a self-hosted brand font later, add the @font-face and change only these
   * two properties. docs/DEPLOYMENT.md has the subsetting recipe.
   */
  --font-display:
    ui-serif, "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
    Georgia, "Times New Roman", serif;
  --font-ui:
    system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --font-mono:
    ui-monospace, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", monospace;

  /* Fluid type scale. clamp() keeps every size readable at 320px and generous at 1920px. */
  --step--2: clamp(0.74rem, 0.71rem + 0.12vw, 0.82rem);
  --step--1: clamp(0.82rem, 0.79rem + 0.13vw, 0.9rem);
  --step-0: clamp(1rem, 0.96rem + 0.18vw, 1.08rem);
  --step-1: clamp(1.2rem, 1.12rem + 0.38vw, 1.45rem);
  --step-2: clamp(1.45rem, 1.3rem + 0.72vw, 1.95rem);
  --step-3: clamp(1.8rem, 1.5rem + 1.4vw, 2.85rem);
  --step-4: clamp(2.2rem, 1.6rem + 2.9vw, 4.4rem);
  --step-5: clamp(2.6rem, 1.5rem + 5.2vw, 6.5rem);

  /* Spacing, on a 4px grid with fluid steps for section rhythm. */
  --space-2xs: 0.25rem;
  --space-xs: 0.5rem;
  --space-s: 0.75rem;
  --space-m: 1rem;
  --space-l: 1.5rem;
  --space-xl: 2rem;
  --space-2xl: 3rem;
  --space-3xl: clamp(3.5rem, 2rem + 6vw, 7rem);

  --measure: 62ch;
  --radius-s: 6px;
  --radius-m: 12px;
  --radius-l: 20px;
  --radius-full: 999px;

  /**
   * Colour. One brand hue in OKLCH; every other state is derived from it with
   * color-mix(), which is what keeps a small site looking like it has a design system.
   * OKLCH and color-mix are both Baseline widely available; a hex fallback is declared
   * first for anything older.
   */
  --ink: #17191c;
  --ink-soft: #4a4f55;
  --ink-faint: #676d75;
  --paper: #faf8f4;
  --paper-raised: #ffffff;
  --paper-sunken: #f1ede6;
  --line: #e2ddd3;
  --brand: #f38120;
  --brand-text: #f38120;
  --brand-ink: #9a2f00;
  --accent: #9a5a2c;
  --danger: #a32b1f;
  --success: #1f6b4a;
}

@supports (color: oklch(0% 0 0)) {
  :root {
    --ink: oklch(0.21 0.008 250);
    --ink-soft: oklch(0.46 0.01 250);
    --ink-faint: oklch(0.505 0.012 250);
    --paper: oklch(0.977 0.006 85);
    --paper-raised: oklch(1 0 0);
    --paper-sunken: oklch(0.945 0.011 85);
    --line: oklch(0.89 0.012 85);

    /* #f38120 - the exact value the client's brand sheet names, not a sample of it.
       Measured: 6.69:1 against ink, 2.63:1 against white. Hence the rule that anything
       filled with it carries dark type, never white. */
    /* The exact value, not an OKLCH approximation of it. The client named two
       candidates and asked which is closer; #f38120 is the colour inside their own
       logo artwork (all three SVG files), so the distance is zero, while #cf8137 is
       43 units away and reads as a duller orange. Written as the hex because the
       OKLCH round trip was rendering rgb(240,126,43) against the logo's
       rgb(243,129,32) - three units of drift against the artwork it sits next to. */
    --brand: #f38120;
    /* Orange as TEXT: the brand orange itself, on the client's instruction.
     *
     * This was a darker orange chosen to clear WCAG AA. The client rejected it three
     * times in three separate rounds, in their own words: "ona tamnija narandzasta boja
     * u tekstu, znakovima, znaci sve gde postoji treba je promeniti". It is their brand
     * and their site, and the decision is theirs to make.
     *
     * What it costs, measured against the three page surfaces: 2.54, 2.31 and 2.72 to
     * one. AA asks 4.5 for body text and 3.0 for large text, so this clears neither. It
     * is used for accents - eyebrows, the hero accent, step numbers, the FAQ marker,
     * prices on the tiles - and NOT for running text, which stays on --ink.
     *
     * --focus deliberately does not follow it; see the focus token below. */
    --brand-text: #f38120;
    --brand-ink: oklch(0.46 0.165 51.3);
    --accent: oklch(0.55 0.07 55);
    --danger: oklch(0.49 0.16 27);
    --success: oklch(0.47 0.093 158);
  }
}

:root {
  /* Derived states. Changing --brand above re-derives all of these. */
  --brand-tint: color-mix(in oklch, var(--brand) 9%, var(--paper));
  --brand-tint-strong: color-mix(in oklch, var(--brand) 18%, var(--paper));
  --brand-hover: color-mix(in oklch, var(--brand) 84%, black);
  --brand-border: color-mix(in oklch, var(--brand) 28%, var(--line));
  --accent-tint: color-mix(in oklch, var(--accent) 14%, var(--paper));
  --danger-tint: color-mix(in oklch, var(--danger) 8%, var(--paper));
  --success-tint: color-mix(in oklch, var(--success) 10%, var(--paper));
  /* The ring has to clear 3:1 against paper AND against the orange it outlines,
     so it is the dark orange, not the vivid one. */
  /* The focus ring stays dark. It is not a brand accent, it is the only thing telling a
     keyboard user where they are, and at the brand orange's 2.3:1 it would be close to
     invisible on paper. The client's instruction was about text and signs. */
  --focus: var(--brand-ink);

  /* The header's own row height. The hero reaches up by exactly this much, so the two
     have to agree on one number rather than each carrying its own copy. */
  --header-h: 4.5rem;

  /* Text drawn ON the brand fill. Both schemes want a dark glyph, but "dark" is a
     different token in each, so it is named once here. */
  --on-brand: var(--ink);

  --shadow-s: 0 1px 2px color-mix(in oklch, var(--ink) 8%, transparent);
  --shadow-m: 0 4px 16px -4px color-mix(in oklch, var(--ink) 14%, transparent);
  --shadow-l: 0 24px 60px -24px color-mix(in oklch, var(--ink) 30%, transparent);

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --dur-fast: 140ms;
  --dur: 260ms;
  --dur-slow: 620ms;

  color-scheme: light;
}

/**
 * There is deliberately no dark scheme.
 *
 * The brand direction is a white environment, dark type and orange accents - the shop
 * itself is white walls and orange signage. A dark variant would hand roughly half of
 * mobile visitors, whose phones sit in auto-dark, the exact near-black page this
 * redesign was commissioned to replace.
 *
 * `color-scheme: light` below is what stops the browser from darkening form controls
 * and scrollbars on a dark-mode device. To bring a dark variant back, restore a
 * `@media (prefers-color-scheme: dark)` block that overrides the tokens above; nothing
 * else in this file assumes one exists.
 */

/* ========================================================================== */
/* Reset and base                                                              */
/* ========================================================================== */

*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 5.5rem;
  /*
   * The root, not just the body.
   *
   * `overflow-x: hidden` on <body> alone does not stop a phone panning sideways: on a
   * mobile browser the viewport scrolls the documentElement, and the body's own clip has
   * nothing to say about it. That is the whole of the "the site slides off to one side on
   * my phone" report, and it is why the desktop never showed it.
   *
   * `clip`, not `hidden`. `overflow: hidden` on the root turns it into a scroll container,
   * which breaks `position: sticky` on the header and makes `scroll-behavior` this
   * element's business rather than the viewport's. `clip` cuts the overflow without any of
   * that. A browser too old to know the keyword ignores the line and behaves exactly as it
   * did before, so there is nothing to fall back to.
   */
  overflow-x: clip;
  /* No sideways rubber-banding, and no accidental back gesture from a diagonal swipe. */
  overscroll-behavior-x: none;
}

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--step-0);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  min-height: 100dvh;
  /* No overflow here. `overflow-x: hidden` on the body computes to `overflow: hidden auto`
     and makes the body a scroll container, and a sticky descendant then sticks to the
     body's scrollport rather than the viewport - which never scrolls, so it never sticks.
     The root's `overflow-x: clip` above is what keeps the page from sliding sideways, and
     it does it without turning anything into a scroller. */
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

h1 {
  font-size: var(--step-5);
}
h2 {
  font-size: var(--step-3);
}
h3 {
  font-size: var(--step-1);
}

p {
  text-wrap: pretty;
  max-width: var(--measure);
}

a {
  color: inherit;
  text-underline-offset: 0.22em;
  text-decoration-thickness: 1px;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: var(--radius-s);
}

svg {
  display: block;
}

/**
 * The `hidden` attribute is only `display: none` in the UA stylesheet, so ANY author rule
 * that sets a display value silently defeats it. That bit this project: the order footer,
 * the "Nazad" button, the step indicator and the submit-error box all carry a display
 * value from a component rule, so setting `.hidden = true` in JS did nothing and they
 * stayed on screen - including on the confirmation step, where the customer saw a "Dalje"
 * button after their order had already been sent.
 *
 * `!important` is the correct tool here: `hidden` is a statement about the document, not
 * a style preference, and nothing should be able to override it.
 */
[hidden] {
  display: none !important;
}

/* Visible to screen readers only. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: var(--space-xs);
  left: var(--space-xs);
  z-index: 100;
  padding: var(--space-xs) var(--space-m);
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--radius-s);
  transform: translateY(-200%);
  transition: transform var(--dur) var(--ease-out);
}

.skip-link:focus-visible {
  transform: translateY(0);
}

/* ========================================================================== */
/* Layout primitives                                                           */
/* ========================================================================== */

.shell {
  width: min(100% - 2rem, 76rem);
  margin-inline: auto;
}

.shell--narrow {
  width: min(100% - 2rem, 58rem);
}

.section {
  padding-block: var(--space-3xl);
  position: relative;
}

.section--sunken {
  background: var(--paper-sunken);
}

.section__head {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
  margin-bottom: var(--space-2xl);
}

.eyebrow {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--brand-text);
}

.lede {
  font-size: var(--step-1);
  color: var(--ink-soft);
  line-height: 1.45;
}

.stack > * + * {
  margin-top: var(--space-m);
}

/* ========================================================================== */
/* Buttons                                                                     */
/* ========================================================================== */

.btn {
  --btn-bg: var(--ink);
  --btn-fg: var(--paper);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  padding: 0.85em 1.6em;
  min-height: 48px;
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-size: var(--step-0);
  font-weight: 550;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition:
    transform var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out);
}

.btn:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-m);
}

.btn:active {
  transform: translateY(0);
}

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.btn--primary {
  --btn-bg: var(--brand);
  /* White on this orange is 2.8:1 and fails. Ink on it is 6.25:1 - and the
     black-on-orange fill is the stronger editorial look anyway. */
  --btn-fg: var(--on-brand);
}

.btn--primary:hover {
  --btn-bg: var(--brand-hover);
}

.btn--ghost {
  --btn-bg: transparent;
  --btn-fg: var(--ink);
  border-color: var(--line);
}

.btn--ghost:hover {
  --btn-bg: var(--paper-raised);
  border-color: var(--brand-border);
}

.btn--quiet {
  --btn-bg: transparent;
  --btn-fg: var(--ink-soft);
  min-height: 40px;
  padding: 0.5em 0.9em;
}

.btn--quiet:hover {
  --btn-fg: var(--ink);
  box-shadow: none;
  transform: none;
  text-decoration: underline;
}

.btn--block {
  width: 100%;
}

.btn__spinner {
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  display: none;
}

.btn.is-busy .btn__spinner {
  display: block;
  animation: spin 720ms linear infinite;
}

.btn.is-busy .btn__label {
  opacity: 0.75;
}

/* ========================================================================== */
/* Header                                                                      */
/* ========================================================================== */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in oklch, var(--paper) 88%, transparent);
  border-bottom: 1px solid transparent;
  transition:
    border-color var(--dur) var(--ease-out),
    background-color var(--dur) var(--ease-out);
}

@supports (backdrop-filter: blur(1px)) {
  .site-header {
    backdrop-filter: saturate(140%) blur(14px);
  }
}

.site-header[data-scrolled="true"] {
  border-bottom-color: var(--line);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  min-height: var(--header-h);
}

/* The lockup the client drew: hanger on top, small; ELEGANT under it as the anchor;
   the trade under that. A column, all three centred on each other.
   Sized to sit inside the header's existing 4.5rem without touching it: the three parts
   and their gaps come to roughly 3.4rem, which leaves real air above and below rather
   than the squeeze the client asked us to avoid. */
.brand {
  display: inline-grid;
  justify-items: center;
  gap: 0.1rem;
  text-decoration: none;
  font-family: var(--font-display);
  font-size: var(--step-1);
  letter-spacing: 0.01em;
  line-height: 1;
}

/* The hanger, as an <img> of an SVG that paints itself in currentColor. Sized by height
   so the header's rhythm holds whatever the artwork's aspect ratio turns out to be. */
/* The hanger sits on top of the word, small.
 *
 * The brief: "ofinger da bude iznad Elegant, moze biti dosta manji jer znam da tu nema
 * mnogo prostora da smestite sve, i da je centrirano po sredini ofinger elegant i ispod
 * hemijska cistiona i perionica." The centring comes from `.brand` being a grid with
 * `justify-items: center`, so all three parts sit on one axis.
 *
 * 0.85rem against a 2.2rem wordmark is "dosta manji" and keeps the whole lockup inside
 * the header's own 4.5rem row without the header growing. */
.brand__mark {
  display: block;
  width: auto;
  height: 0.85rem;
  flex: none;
  color: var(--ink);
}

.brand__type {
  display: grid;
  gap: 0.12rem;
  justify-items: center;
}

.brand__wordmark {
  display: block;
  width: auto;
  /* The wordmark has to out-measure the tagline under it, or the lockup reads upside
     down. Stacking made that worse, not better: centred, a 96px word over a 167px line
     is a funnel. The word grew and the line below tightened until they measure the
     same. */
  height: 2.2rem;
}

.brand__sub {
  display: block;
  font-family: var(--font-ui);
  font-size: 0.46rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-top: 1px;
  white-space: nowrap;
}

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--space-l);
}

.site-nav a {
  font-size: var(--step--1);
  text-decoration: none;
  color: var(--ink-soft);
  transition: color var(--dur-fast) var(--ease-out);
  /* A wrapped nav item or a phone number broken across two lines looks broken.
     Between 768px and about 1000px both were happening. */
  white-space: nowrap;
}

.header-actions .btn {
  white-space: nowrap;
}

.site-nav a:hover {
  color: var(--ink);
}

.header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

/* A 44px target, which is the WCAG minimum and the size of the menu button beside it,
   so the row keeps one rhythm. */
.icon-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex: none;
  border-radius: var(--radius-full);
  color: var(--ink-soft);
  transition: color var(--dur-fast) var(--ease-out);
}

.icon-link:hover {
  color: var(--brand-text);
}

.icon-link[data-config-missing] {
  display: none;
}

/* Mobile navigation uses the native popover API, so there is no JS on this path. */
.nav-toggle {
  display: none;
  border: 1px solid var(--line);
  background: transparent;
  border-radius: var(--radius-full);
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

/* HIDDEN BY A RULE, not only by the `popover` attribute.
 *
 * This element carried no `display` of its own and relied on the UA stylesheet hiding a
 * popover until it is opened. In a browser without the Popover API the attribute means
 * nothing, so the menu rendered as an ordinary block right under the header, pushed the
 * hero 514px down the page, and left most of the screen empty. That is the "ceo ekran
 * prazan, a levo stoji ovaj meni" the client reported twice; it never showed up here
 * because this browser supports popovers.
 *
 * The rules below are deliberately NOT one selector list. An unknown pseudo-class
 * invalidates the whole list it appears in, so pairing `:popover-open` with `.is-open`
 * would throw away the fallback in exactly the browsers that need it. */
.mobile-nav {
  display: none;
  border: none;
  padding: 0;
  background: transparent;
  /* The UA stylesheet centres a popover with `margin: auto` and `inset: 0`. The reset in
     this file sets `margin: 0` on everything, which silently won that fight and pinned
     the panel to the top-left corner - diagonally opposite the button that opens it.
     So place it on purpose: just under the sticky header, along the right edge where
     the toggle actually is. 5rem clears the 73px header at every width the toggle is
     visible at, because that header is a fixed-height row (a 44px button plus padding)
     and does not grow with the viewport. */
  inset: 5rem var(--space-m) auto auto;
  width: min(19rem, calc(100vw - var(--space-m) * 2));
  /* A long menu on a short phone in landscape has to scroll rather than run off. */
  max-height: calc(100dvh - 6.5rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.mobile-nav:popover-open {
  display: block;
}

/* The fallback path: js/main.js toggles this class when `showPopover` is missing. */
.mobile-nav.is-open {
  display: block;
  position: fixed;
  z-index: 60;
}

.mobile-nav::backdrop {
  background: color-mix(in oklch, var(--ink) 45%, transparent);
}

.mobile-nav__panel {
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-l);
  padding: var(--space-l);
  display: grid;
  gap: var(--space-s);
  box-shadow: var(--shadow-l);
}

.mobile-nav__panel a {
  padding: var(--space-s) var(--space-m);
  border-radius: var(--radius-m);
  text-decoration: none;
  font-size: var(--step-1);
  font-family: var(--font-display);
}

.mobile-nav__panel a:hover {
  background: var(--paper-sunken);
}

/* ========================================================================== */
/* Hero                                                                        */
/* ========================================================================== */

.hero {
  position: relative;
  /* The warm wash and the weave are clipped to this box, so when the box started below
     the header the page had a hard horizontal seam exactly where the header ended, with
     an empty pale band above it. The client asked for the background either to run from
     the very top or to go altogether. This reaches up under the sticky header, which is
     88% opaque paper, so the wash carries through it and there is no edge to see. The
     padding puts the content back where it was. */
  margin-block-start: calc(var(--header-h) * -1);
  /* Tighter than it was. The hero is a landscape band now rather than a column of type
     with a picture beside it, and 128px of air above the eyebrow plus 112px below the
     tradition line pushed the whole thing a third of a screen past the fold - which is
     the one shape a landscape hero must not have. */
  padding-block: calc(var(--header-h) + clamp(2rem, 1rem + 5vw, 5rem))
    var(--space-2xl);
  overflow: hidden;
  isolation: isolate;
  /* A landscape band, not a box that happens to be as tall as its text. The floor is
     what makes the picture read as a photograph the page is standing on rather than a
     texture behind a paragraph; the cap stops it from eating a laptop screen whole.
     `svh` rather than `vh` so a phone's collapsing address bar does not resize it
     mid-scroll. Content is centred inside, so a short viewport crops the picture rather
     than stranding the type at the top. */
  min-height: min(100svh, 48rem);
  display: grid;
  align-content: center;
}

/* --- hero background ------------------------------------------------------ */

/* Everything behind the words lives in here, so the hero itself keeps one child to
   reason about and the whole background can be clipped and muted in one place. */
.hero__bg {
  position: absolute;
  inset: 0;
  z-index: -3;
  overflow: hidden;
  pointer-events: none;
}

.hero__still,
.hero__video,
.hero__still img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Stock footage is graded for its own purposes, which is usually more saturated and more
   contrasty than a page built on warm paper and one orange. Pulling the colour down and
   the black point up makes it sit behind the brand instead of competing with it, and it
   costs nothing: the GPU composites this once. */
.hero__still,
.hero__video {
  filter: saturate(0.62) brightness(1.08) contrast(0.92);
  /* The hero is taller than the clip is, so cover crops it left and right. Pinning the
     right edge keeps the machine - the only thing in the frame worth seeing - inside the
     crop at every width, and spends the cropping on the empty wall instead, which is the
     half the wash covers with paper anyway. The frame was mirrored at build time so that
     subject sits on the right in the first place. */
  object-position: 100% 50%;
}

/* No fade, on purpose. A <video> paints nothing until it has decoded a frame, so the
   picture behind it shows through by itself and the clip simply starts. Since the still
   is cut from this clip, the frame it starts on is the frame already on screen.

   It used to start at `opacity: 0` and fade in. That broke it: an element at zero opacity
   is invisible, Chrome pauses invisible muted video to save power, and the pending play()
   came back rejected. A background that is already correct without a transition does not
   get one. */

/* The wash. The picture now runs the whole width of the screen, so this is no longer
   one small opening in a paper field - it is a scrim over a photograph, and it has two
   jobs, listed top layer first:

   edges   kill both seams. The hero reaches up under the sticky header and ends against
           the next section, and the client has already had to report one hard line
           there. Solid paper at both ends means there is nothing to see at either, and
           the band along the bottom is also what the figures and the tradition line
           stand on.
   column  holds the left side at paper for the headline, the lede and the buttons, and
           lets go across the middle so the right half of the frame is simply the
           photograph. Near-black type on a photograph needs either a scrim this heavy
           or white text, and white text would mean rebuilding the hero in the one
           colour scheme the rest of this page does not use.

   Measured, not guessed: the numbers below keep every piece of hero text at 15:1 or
   better against what is actually behind it, while the open side of the frame sits
   around 70% of the picture's own contrast. */
.hero__veil {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(
      to bottom,
      var(--paper) 0%,
      color-mix(in oklch, var(--paper) 40%, transparent) 13%,
      transparent 28%,
      transparent 58%,
      color-mix(in oklch, var(--paper) 46%, transparent) 78%,
      color-mix(in oklch, var(--paper) 86%, transparent) 92%,
      var(--paper) 100%
    ),
    linear-gradient(
      92deg,
      var(--paper) 0%,
      var(--paper) 22%,
      color-mix(in oklch, var(--paper) 88%, transparent) 38%,
      color-mix(in oklch, var(--paper) 58%, transparent) 56%,
      color-mix(in oklch, var(--paper) 30%, transparent) 74%,
      color-mix(in oklch, var(--paper) 22%, transparent) 100%
    );
}

.hero__glow {
  position: absolute;
  z-index: -2;
  inset-inline-end: -10%;
  inset-block-start: -18%;
  width: min(60vw, 40rem);
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    color-mix(in oklch, var(--brand) 26%, transparent),
    transparent 68%
  );
  pointer-events: none;
}

.hero__inner {
  display: grid;
  gap: var(--space-2xl);
  align-items: end;
}

.hero h1 {
  max-width: 14ch;
}

/* Three lines, as written on the brief, rather than three lines that happen to fall out
   of this column width and become two at the next one. */
.hero__line {
  display: block;
}

.hero__accent {
  display: block;
  color: var(--brand-text);
  font-style: italic;
}

.hero__lede {
  max-width: 46ch;
  margin-top: var(--space-l);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s);
  margin-top: var(--space-xl);
}

.hero__facts {
  display: grid;
  gap: var(--space-l);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--line);
  text-align: center;
}

/* Each fact centres its own pair, so a caption that wraps to two lines does not drag its
   number off the line its neighbours sit on. */
.fact {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.fact__value {
  font-family: var(--font-display);
  font-size: var(--step-2);
  display: block;
  line-height: 1;
}

.fact__label {
  font-size: var(--step--1);
  color: var(--ink-faint);
  margin-top: var(--space-2xs);
  display: block;
}

/* ========================================================================== */
/* Steps                                                                       */
/* ========================================================================== */

.steps {
  display: grid;
  gap: var(--space-l);
  list-style: none;
  padding: 0;
}

.step {
  position: relative;
  padding: var(--space-l);
  border: 1px solid var(--line);
  border-radius: var(--radius-l);
  background: var(--paper-raised);
}

/* The step number is a real element in the markup, not a CSS counter.
   It WAS `content: counter(step, decimal-leading-zero)` on ::before, and the client saw
   "00" on all four cards while it rendered 01 to 04 here. The cause was `container-type:
   inline-size` on .step: a container type brings style containment with it, and under
   style containment an element is its own counter scope, so ::before read a counter that
   had just been reset rather than the one .steps was counting. Engines disagree about
   this, which is the worst kind of bug: correct in the browser you test in, wrong in the
   one the client opens.
   Nothing queried that container - only .tile has an @container rule - so the containment
   is gone too, and the number is now plain markup that no engine can reinterpret. The
   sibling section has always numbered its steps this way (.story__num). */
.step__num {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--brand-text);
  letter-spacing: 0.1em;
}

/* The icon sits in the corner opposite the step number, so both markers are out of the
   text's way and the card reads number, subject, sentence. Purely decorative: the heading
   underneath says the same thing, which is why it is aria-hidden. */
.step__icon {
  position: absolute;
  inset-block-start: var(--space-l);
  inset-inline-end: var(--space-l);
  width: 1.75rem;
  height: 1.75rem;
  color: var(--brand-text);
}
.step h3 {
  margin-block: var(--space-s) var(--space-xs);
  /* Clears the icon. Without it a two-line heading runs underneath one. */
  padding-inline-end: 2.75rem;
}

.step p {
  color: var(--ink-soft);
  font-size: var(--step--1);
}

/* ========================================================================== */
/* Price list                                                                  */
/* ========================================================================== */

.pricing__controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s);
  align-items: center;
  margin-bottom: var(--space-l);
  position: sticky;
  top: 4.5rem;
  z-index: 20;
  padding-block: var(--space-s);
  background: linear-gradient(
    to bottom,
    var(--paper) 72%,
    color-mix(in oklch, var(--paper) 0%, transparent)
  );
}

.section--sunken .pricing__controls {
  background: linear-gradient(
    to bottom,
    var(--paper-sunken) 72%,
    color-mix(in oklch, var(--paper-sunken) 0%, transparent)
  );
}

.search {
  flex: 1 1 16rem;
  position: relative;
  display: flex;
  align-items: center;
}

.search input {
  width: 100%;
  padding: 0.75rem 2.5rem 0.75rem 2.75rem;
  min-height: 48px;
  border: 1px solid var(--line);
  border-radius: var(--radius-full);
  background: var(--paper-raised);
}

.search__icon {
  position: absolute;
  inset-inline-start: 1rem;
  width: 1.05rem;
  height: 1.05rem;
  color: var(--ink-faint);
  pointer-events: none;
}

.search__clear {
  position: absolute;
  inset-inline-end: 0.5rem;
  border: 0;
  background: transparent;
  cursor: pointer;
  color: var(--ink-faint);
  padding: 0.5rem;
  border-radius: var(--radius-full);
  display: none;
}

.search:has(input:not(:placeholder-shown)) .search__clear {
  display: block;
}

/*
 * The chips wrap; they do not scroll sideways.
 *
 * This rule used to carry `flex-wrap: wrap` AND `overflow-x: auto`, which contradict each
 * other: a wrapping flex row never overflows horizontally, so the `auto` could never show
 * a scrollbar. What it did do was make the element a horizontal scroll container, and on a
 * phone that is a swipe target sitting across the width of the order form. A thumb moving
 * even slightly sideways while scrolling grabs it, and the whole row slides - which reads
 * exactly like "the site slides off to one side". Desktop never showed it because a mouse
 * wheel does not do diagonal.
 */
.chips {
  display: flex;
  gap: var(--space-2xs);
  flex-wrap: wrap;
}

.chip {
  border: 1px solid var(--line);
  background: var(--paper-raised);
  color: var(--ink-soft);
  border-radius: var(--radius-full);
  padding: 0.5rem 0.95rem;
  min-height: 40px;
  font-size: var(--step--1);
  cursor: pointer;
  white-space: nowrap;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

.chip:hover {
  border-color: var(--brand-border);
  color: var(--ink);
}

.chip[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

.category {
  border-block-end: 1px solid var(--line);
}

.category > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  padding-block: var(--space-m);
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-size: var(--step-1);
}

.category > summary::-webkit-details-marker {
  display: none;
}

/* A signpost to items the shop's spreadsheet files under a different heading. Quiet on
   purpose: it is navigation, not a price. */
.category__note {
  margin: 0;
  padding: 0 0 var(--space-s);
  font-size: var(--step--1);
  color: var(--ink-faint);
}

.category__meta {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  color: var(--ink-faint);
  display: flex;
  align-items: center;
  gap: var(--space-s);
}

.category__chevron {
  width: 1rem;
  height: 1rem;
  transition: transform var(--dur) var(--ease-out);
}

.category[open] .category__chevron {
  transform: rotate(180deg);
}

.item-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: var(--space-m);
  align-items: center;
  padding-block: var(--space-s);
  border-block-start: 1px dashed var(--line);
  container-type: inline-size;
}

.item-row:first-child {
  border-block-start: 0;
}

.item-row__name {
  font-size: var(--step-0);
}

.item-row__code {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--ink-faint);
  margin-inline-end: var(--space-xs);
}

.item-row__price {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
}

.item-row__unit {
  font-weight: 400;
  color: var(--ink-faint);
  font-size: var(--step--1);
}

.item-row__actions {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  justify-content: flex-end;
}

.item-row__add {
  border: 1px solid var(--line);
  border-radius: var(--radius-full);
  padding: 0.4em 0.9em;
  min-height: 36px;
  font-size: var(--step--1);
  /* Only useful once JS is running; the static page hides it. */
  display: none;
}

.js .item-row__add {
  display: inline-flex;
}

.item-row__add:hover {
  border-color: var(--brand-border);
  color: var(--brand-text);
  text-decoration: none;
}

/* A dense list can afford a compact button with a mouse, but a finger cannot hit 36px
   reliably. Touch devices get the full 44px target. */
@media (pointer: coarse) {
  .item-row__add {
    min-height: 44px;
    padding-inline: 1.1em;
  }

  .qty button {
    width: 44px;
    height: 44px;
  }

  .icon-btn {
    min-width: 44px;
    min-height: 44px;
  }

  .chip {
    min-height: 44px;
  }
}

.turnaround {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  font-size: 0.72rem;
  padding: 0.2em 0.6em;
  border-radius: var(--radius-full);
  background: var(--paper-sunken);
  color: var(--ink-soft);
  white-space: nowrap;
}

.section--sunken .turnaround {
  background: var(--paper-raised);
}

.turnaround::before {
  content: "";
  width: 0.45em;
  height: 0.45em;
  border-radius: 50%;
  background: var(--turnaround-colour, var(--ink-faint));
}

.empty-state {
  padding: var(--space-2xl) 0;
  text-align: center;
  color: var(--ink-faint);
}

/* ========================================================================== */
/* Order flow                                                                  */
/* ========================================================================== */

.order {
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-l);
  box-shadow: var(--shadow-m);
  overflow: hidden;
}

.order__progress {
  display: flex;
  gap: 1px;
  background: var(--line);
  border-block-end: 1px solid var(--line);
}

.order__progress li {
  flex: 1;
  list-style: none;
  background: var(--paper-sunken);
  padding: var(--space-s) var(--space-xs);
  text-align: center;
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  transition:
    background-color var(--dur) var(--ease-out),
    color var(--dur) var(--ease-out);
}

.order__progress li[data-state="current"] {
  background: var(--paper-raised);
  color: var(--brand-text);
  font-weight: 600;
}

.order__progress li[data-state="done"] {
  background: var(--brand-tint);
  color: var(--brand-ink);
}

.order__body {
  padding: var(--space-xl);
}

.order__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  padding: var(--space-m) var(--space-xl) var(--space-xl);
  flex-wrap: wrap;
}

.panel[hidden] {
  display: none;
}

.panel__title {
  margin-bottom: var(--space-xs);
}

.panel__hint {
  color: var(--ink-soft);
  font-size: var(--step--1);
  margin-bottom: var(--space-l);
}

/* --- selection --- */

.picker {
  display: grid;
  gap: var(--space-m);
}

.selected {
  display: grid;
  gap: var(--space-xs);
}

/* The order summary's empty state is tighter than a full-page one: it sits between two
   controls, not alone in a section. Defined here rather than inline in renderSelected so
   the copy shipped in index.html and the copy JavaScript inserts are the same height. */
.selected .empty-state {
  padding: var(--space-l) 0;
}

/* One article: the priced row, and underneath it the note the shop requires for it.
   They share a background so the note reads as belonging to the article above it rather
   than as a second, unrelated field. */
.selected__line {
  background: var(--paper-sunken);
  border-radius: var(--radius-m);
  overflow: hidden;
}

.selected__row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: var(--space-m);
  align-items: center;
  padding: var(--space-s) var(--space-m);
  background: var(--paper-sunken);
  border-radius: var(--radius-m);
}

.selected__line .selected__row {
  border-radius: 0;
}

.selected__note {
  display: grid;
  gap: var(--space-xs);
  padding: 0 var(--space-m) var(--space-s);
  border-top: 1px dashed var(--line);
  padding-top: var(--space-s);
}

.note__label {
  font-size: var(--step--1);
  color: var(--ink-soft);
}

.note__starters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
}

/* Smaller than a filter chip: these are a convenience under a field, not navigation. */
.chip--ghost {
  min-height: 32px;
  padding: 0.25rem 0.7rem;
  font-size: 0.78rem;
  background: transparent;
}

.note__input {
  width: 100%;
  resize: vertical;
  min-height: 3.2rem;
  padding: var(--space-xs) var(--space-s);
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  background: var(--paper-raised);
  line-height: 1.45;
}

.note__input:focus-visible {
  border-color: var(--brand-border);
}

.note__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-s);
  min-height: 1.2em;
}

.note__counter {
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Not red while the customer is still typing: an error colour on a field nobody has
   finished filling reads as a mistake rather than as progress. */
.note__counter[data-state="short"] {
  color: var(--ink-faint);
}

.note__counter[data-state="ok"] {
  color: var(--success);
}

.qty {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--line);
  border-radius: var(--radius-full);
  background: var(--paper-raised);
  overflow: hidden;
}

.qty button {
  width: 40px;
  height: 40px;
  border: 0;
  background: transparent;
  cursor: pointer;
  font-size: var(--step-1);
  line-height: 1;
  color: var(--ink-soft);
}

.qty button:hover {
  background: var(--paper-sunken);
  color: var(--ink);
}

.qty input {
  width: 3.4rem;
  text-align: center;
  border: 0;
  background: transparent;
  font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
}

.qty input::-webkit-outer-spin-button,
.qty input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.icon-btn {
  border: 0;
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
  padding: 0.5rem;
  border-radius: var(--radius-full);
  min-width: 40px;
  min-height: 40px;
}

.icon-btn:hover {
  color: var(--danger);
  background: var(--danger-tint);
}

/* --- fields --- */

.field-grid {
  display: grid;
  gap: var(--space-m);
}

.field {
  display: grid;
  gap: var(--space-2xs);
}

.field label,
.field__label {
  font-size: var(--step--1);
  font-weight: 550;
}

.field__optional {
  color: var(--ink-faint);
  font-weight: 400;
}

.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 0.7rem 0.9rem;
  min-height: 48px;
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--paper);
  transition:
    border-color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out);
}

.field textarea {
  min-height: 7rem;
  resize: vertical;
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--brand-border);
}

.field__error {
  font-size: var(--step--1);
  color: var(--danger);
  min-height: 0;
}

.field__error:empty {
  display: none;
}

/* :has() lets the wrapper react to its own control without any JS class toggling. */
.field:has(.field__error:not(:empty)) input,
.field:has(.field__error:not(:empty)) select,
.field:has(.field__error:not(:empty)) textarea {
  border-color: var(--danger);
  background: var(--danger-tint);
}

.field__hint {
  font-size: var(--step--1);
  color: var(--ink-faint);
}

.choice-grid {
  display: grid;
  gap: var(--space-xs);
}

.choice {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-s);
  padding: var(--space-s) var(--space-m);
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  cursor: pointer;
  background: var(--paper);
  transition:
    border-color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.choice:hover {
  border-color: var(--brand-border);
}

.choice:has(input:checked) {
  border-color: var(--brand);
  background: var(--brand-tint);
}

.choice input {
  accent-color: var(--brand-text);
  width: 1.15rem;
  height: 1.15rem;
  flex: none;
}

.choice__label {
  font-size: var(--step-0);
}

.choice__meta {
  margin-inline-start: auto;
  font-size: var(--step--1);
  color: var(--ink-faint);
}

.consent {
  display: flex;
  gap: var(--space-s);
  align-items: flex-start;
  font-size: var(--step--1);
  color: var(--ink-soft);
}

.consent input {
  margin-top: 0.22rem;
  accent-color: var(--brand-text);
  width: 1.15rem;
  height: 1.15rem;
  flex: none;
}

/* The honeypot must be reachable by a bot and invisible to a person. */
.honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --- summary --- */

.summary {
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  overflow: hidden;
}

.summary__row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-m);
  padding: var(--space-s) var(--space-m);
  border-block-end: 1px solid var(--line);
  font-size: var(--step--1);
}

.summary__row:last-child {
  border-block-end: 0;
}

.summary__row dt {
  color: var(--ink-faint);
}

.summary__row dd {
  text-align: end;
  margin: 0;
}

.total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-m);
  padding: var(--space-m);
  background: var(--brand-tint);
  border-radius: var(--radius-m);
  margin-block: var(--space-m);
}

.total__value {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.notice {
  display: flex;
  gap: var(--space-s);
  padding: var(--space-m);
  border-radius: var(--radius-m);
  font-size: var(--step--1);
  background: var(--paper-sunken);
  color: var(--ink-soft);
  border-inline-start: 3px solid var(--line);
}

.notice--warn {
  background: var(--accent-tint);
  border-inline-start-color: var(--accent);
  color: var(--ink);
}

.notice--error {
  background: var(--danger-tint);
  border-inline-start-color: var(--danger);
  color: var(--ink);
}

.notice--success {
  background: var(--success-tint);
  border-inline-start-color: var(--success);
  color: var(--ink);
}

/* --- result --- */

.result {
  text-align: center;
  padding-block: var(--space-xl);
}

.result__badge {
  width: 4rem;
  height: 4rem;
  margin-inline: auto;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--success-tint);
  color: var(--success);
  margin-bottom: var(--space-l);
}

.result__id {
  font-family: var(--font-mono);
  font-size: var(--step-2);
  letter-spacing: 0.04em;
  padding: var(--space-xs) var(--space-m);
  background: var(--paper-sunken);
  border-radius: var(--radius-m);
  display: inline-block;
  margin-block: var(--space-s) var(--space-l);
  user-select: all;
}

.next-steps {
  text-align: start;
  max-width: 34rem;
  margin-inline: auto;
  display: grid;
  gap: var(--space-s);
  padding: 0;
  list-style: none;
}

.next-steps li {
  display: flex;
  gap: var(--space-s);
  font-size: var(--step--1);
  color: var(--ink-soft);
}

.next-steps li::before {
  content: "";
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--brand);
  margin-top: 0.5em;
}

/* ========================================================================== */
/* Locations, FAQ, footer                                                      */
/* ========================================================================== */

.cards {
  display: grid;
  gap: var(--space-m);
}

.card {
  padding: var(--space-l);
  border: 1px solid var(--line);
  border-radius: var(--radius-l);
  background: var(--paper-raised);
  container-type: inline-size;
}

.card h3 {
  margin-bottom: var(--space-2xs);
}

.card__meta {
  color: var(--ink-soft);
  font-size: var(--step--1);
}

.card__link {
  display: inline-block;
  margin-top: var(--space-s);
  font-size: var(--step--1);
  color: var(--brand-text);
}

.faq {
  display: grid;
  gap: 0;
}

.faq details {
  border-block-end: 1px solid var(--line);
}

.faq summary {
  padding-block: var(--space-m);
  cursor: pointer;
  font-family: var(--font-display);
  font-size: var(--step-1);
  list-style: none;
  display: flex;
  justify-content: space-between;
  gap: var(--space-m);
  align-items: center;
}

.faq summary::-webkit-details-marker {
  display: none;
}

.faq summary::after {
  content: "+";
  font-family: var(--font-ui);
  font-size: var(--step-2);
  color: var(--brand-text);
  line-height: 1;
}

.faq details[open] summary::after {
  /* A plain hyphen. This was an en dash, which breaks the no-dash rule and which
     the QA probe cannot catch: ::after content never reaches innerText. */
  content: "-";
}

.faq p {
  padding-bottom: var(--space-m);
  color: var(--ink-soft);
}

.site-footer {
  background: #000;
  color: var(--paper);
  padding-block: var(--space-2xl);
}

.site-footer a {
  color: inherit;
}

.footer-grid {
  display: grid;
  gap: var(--space-xl);
}

.footer__title {
  font-size: var(--step--1);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: color-mix(in oklch, var(--paper) 55%, transparent);
  margin-bottom: var(--space-s);
}

.footer-list {
  list-style: none;
  padding: 0;
  display: grid;
  gap: var(--space-2xs);
  font-size: var(--step--1);
}

/* Inline footer links are only as tall as their text, which fails the WCAG 2.5.8
   minimum target size. Padding gives them a real hit area without changing the look. */
.footer-list a {
  display: inline-block;
  padding-block: 0.4rem;
}

.footer__legal {
  margin-top: var(--space-xl);
  padding-top: var(--space-m);
  border-top: 1px solid color-mix(in oklch, var(--paper) 16%, transparent);
  font-size: 0.75rem;
  color: color-mix(in oklch, var(--paper) 55%, transparent);
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-s);
}

/* An element whose config value is missing is hidden rather than shown empty. */
[data-config-missing] {
  display: none !important;
}

/* Sticky mobile call-to-action. */
.sticky-cta {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 40;
  display: none;
  gap: var(--space-xs);
  padding: var(--space-s) var(--space-m)
    calc(var(--space-s) + env(safe-area-inset-bottom));
  background: color-mix(in oklch, var(--paper) 92%, transparent);
  border-top: 1px solid var(--line);
}

@supports (backdrop-filter: blur(1px)) {
  .sticky-cta {
    backdrop-filter: blur(12px);
  }
}

/* -------------------------------------------------------------------------- */
/* Basket bar                                                                  */
/* -------------------------------------------------------------------------- */

/* Dark, not paper: this is the one element that must read as "something changed"
   against a page that is otherwise off-white from top to bottom. */
.basket-bar {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  /* Above .sticky-cta (40), which is hidden while this is up. */
  z-index: 45;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  padding: var(--space-s) var(--space-l)
    calc(var(--space-s) + env(safe-area-inset-bottom));
  background: var(--ink);
  color: var(--paper);
  box-shadow: var(--shadow-l);
  animation: basket-rise 0.28s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

/* `hidden` is only a UA-stylesheet `display: none`, which the rule above would beat. */
.basket-bar[hidden] {
  display: none;
}

.basket-bar__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2xs) var(--space-s);
  margin: 0;
  min-width: 0;
}

.basket-bar__count {
  font-size: var(--step--1);
  color: color-mix(in oklch, var(--paper) 74%, var(--ink));
}

.basket-bar__total {
  font-size: var(--step-1);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.basket-bar__cta {
  flex: none;
}

/* Leave room so the bar never covers the last row of whatever is being read. Set on
   <html> by renderBasketBar(); the class also stands down the mobile sticky CTA, since
   two stacked bars would eat half a phone screen. */
.basket-open body {
  padding-bottom: 5.75rem;
}

.basket-open .sticky-cta {
  display: none;
}

@keyframes basket-rise {
  from {
    transform: translateY(100%);
  }
  to {
    transform: none;
  }
}

/* -------------------------------------------------------------------------- */

/* Confirmation at the point of the press. The bar answers "what is in my basket";
   this answers "did THAT button register", which is the question actually being
   asked when a finger leaves the screen. */
.item-row__add[data-added] {
  background: var(--brand);
  border-color: var(--brand);
  /* Black on orange. White on this orange fails AA; see docs/DESIGN.md. */
  color: var(--ink);
}

/* Both labels have to fit without moving the price beside them. */
.item-row__add {
  min-width: 6.4em;
  justify-content: center;
}

/*
 * `.section--deferred` no longer defers anything, and the class is kept only so the
 * markup and js/main.js do not have to change.
 *
 * It used to carry `content-visibility: auto` with an 800px estimate, which is a real
 * first-paint win and was not worth what it cost here. Three separate faults came out of
 * it. Off-screen sections reported an estimated height, so ScrollTrigger measured a
 * document thousands of pixels shorter than the real one and a pinned section engaged
 * 861px early. The motion layer then switched the estimates off on `load`, and the
 * document grew by about 1 200px in one frame - under the reader, if they had started
 * scrolling already, which is exactly the jolt the client reported after the hero. And
 * fragment navigation needed a special case (`.is-jumping`) to force real layout before
 * every anchor jump.
 *
 * This page paints in about half a second with seventeen images that all carry explicit
 * dimensions. It does not need the trick, and without it every measurement on the page is
 * correct from the first frame and stays correct.
 */

/* ========================================================================== */
/* Photography                                                                 */
/* ========================================================================== */

/**
 * Every photograph here is the shop's own, taken on a phone in the shop. The art
 * direction leans into that rather than hiding it: a clean crop, a thin rule, a
 * technical label. No filters, no duotone, no overlay pretending it is a campaign
 * shoot - those treatments are exactly what makes an honest photo look fake.
 *
 * Markup for every <picture> is generated by tools/build_images.py.
 */

.tag {
  display: inline-block;
  padding: 0.25em 0.6em;
  border-radius: var(--radius-s);
  background: var(--brand);
  color: var(--on-brand);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.link {
  color: var(--brand-text);
  font-weight: 600;
  text-underline-offset: 0.28em;
  text-decoration-thickness: 1px;
}

.link::after {
  content: "\2009\2192";
  display: inline-block;
  transition: transform var(--dur) var(--ease-out);
}

.link:hover::after {
  transform: translateX(0.25em);
}

/* One rule for every photograph: fill its frame, never distort, and reserve the box
   with a flat tone so a slow connection does not show a white hole. */
.tile__media img,
.plate img,
.band img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: cover;
  background: var(--paper-sunken);
}

/* --- what we clean -------------------------------------------------------- */

.tiles {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-m);
}

.tile {
  container-type: inline-size;
  display: grid;
  gap: var(--space-s);
  align-content: start;
  color: inherit;
  text-decoration: none;
}

.tile__media {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--radius-m);
}

.tile__media img {
  aspect-ratio: 3 / 4;
  transition: transform var(--dur-slow) var(--ease-out);
}

.tile__media::after {
  content: "+";
  position: absolute;
  top: var(--space-xs);
  inset-inline-end: var(--space-xs);
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-full);
  background: var(--brand);
  color: var(--on-brand);
  font-size: var(--step-1);
  line-height: 1;
  opacity: 0;
  transform: scale(0.7);
  transition:
    opacity var(--dur) var(--ease-out),
    transform var(--dur) var(--ease-out);
}

.tile:hover .tile__media img,
.tile:focus-visible .tile__media img {
  transform: scale(1.045);
}

.tile:hover .tile__media::after,
.tile:focus-visible .tile__media::after {
  opacity: 1;
  transform: scale(1);
}

/* Label and price sit on one line when both fit and stack when they do not: on a
   phone two tiles across, "Kaputi i mantili" wraps, and a baseline-aligned price then
   floats next to the first of its two lines. */
.tile__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0 var(--space-xs);
  padding-top: var(--space-xs);
  border-top: 1px solid var(--line);
  transition: border-color var(--dur) var(--ease-out);
}

@container (max-width: 15rem) {
  .tile__meta {
    display: grid;
    gap: 0.15rem;
    justify-items: start;
  }
}

.tile:hover .tile__meta {
  border-top-color: var(--brand);
}

.tile__label {
  font-weight: 600;
  text-wrap: balance;
}

.tile__price {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--brand-text);
  white-space: nowrap;
}

.tiles__foot {
  margin-top: var(--space-xl);
  /* Left, with the rest of the section. Centred, it read as a caption for the grid
     rather than the next thing to click. */
  text-align: start;
}

/* --- inside the shop ------------------------------------------------------ */

.plates {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-l);
}

.plate {
  display: grid;
  gap: var(--space-s);
  margin: 0;
  /* The glass overlay is absolutely positioned inside this figure. Without a positioned
     ancestor it anchors to the viewport instead, and as a static grid item it was adding
     a whole implicit row the height of the photograph. */
  position: relative;
}

.plate img {
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-m);
}

/* The caption is a step marker now, not a label: a number and the name of the stage. */
.plate__caption {
  display: flex;
  align-items: baseline;
  gap: 0.5em;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.plate__num {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  color: var(--brand-text);
  flex: none;
}

.plate__title {
  color: var(--ink);
}

/* "sitniji tekst!", written under the photographs heading on the client's marked-up page.
   The photographs are the argument this section makes; the words around them are a label
   for them, and at the page's default sizes the label was competing with the picture.
   Every size in here steps down one rung, the same treatment the story section got. */
#u-radnji .section__head h2 {
  font-size: var(--step-2);
}
#u-radnji .section__head .lede {
  font-size: var(--step-0);
}

.band {
  margin: var(--space-3xl) 0 0;
}

.band img {
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-l);
}

.band__caption {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-s);
  margin-top: var(--space-m);
  color: var(--ink-soft);
  font-size: var(--step--1);
}

/* Motion here is decoration. Someone who asked for less of it loses nothing. */
@media (prefers-reduced-motion: reduce) {
  .tile__media img,
  .tile__media::after,
  .tile__meta,
  .link::after {
    transition: none;
  }

  .tile:hover .tile__media img,
  .tile:focus-visible .tile__media img {
    transform: none;
  }

  /* The bar still has to APPEAR - it is the confirmation - it just must not slide. */
  .basket-bar {
    animation: none;
  }
}

/* The full lockup on the footer's dark panel. The file is the brand sheet's OWN reversed
   artwork, not the light one run through a CSS invert: an invert turns #f38120 blue, and
   hue-rotate never brings back exactly the orange the sheet specifies. */
/* The footer lockup is built from the wordmark plus a line of type rather than the
   single combined artwork, because the client asked for "samo natpis Elegant i ispod
   hemijsko ciscenje bez ofingera" and ringed exactly those two parts on the screenshot.
   The combined file is 42 paths in one group; cropping it would have been guesswork. */
/* The hanger over the footer wordmark. The shape comes from the file, the colour from
   here: the artwork is a single `currentColor` path, so masking it gives a hanger that
   is whatever the footer needs it to be. `aspect-ratio` rather than a width, so the
   proportions survive the mark being re-exported at another scale. */
.footer__mark {
  display: block;
  /* Centred in the box above, which is the wordmark's width. Lands within a pixel of
     the wordmark's own centre, and nothing else in the lockup moves. */
  justify-self: center;
  /* Sized against the wordmark, not chosen by eye. Measured off the client's own logo
     artwork: the hanger's cross bar is 170px wide against a 221px "Elegant", so 0.77.
     It shipped at 0.21 - a quarter of that - which is what she spotted. Tying it to the
     same variable the wordmark uses means the pair cannot drift again. */
  width: calc(var(--footer-logo) * 0.77);
  height: auto;
  aspect-ratio: 284 / 129;
  /* The artwork leaves 15px between the bar and the top of the wordmark against its
     221px width; at ours that is ~12px, and the grid already contributes 0.2rem. */
  margin-bottom: 0.55rem;
  background-color: color-mix(in oklch, var(--paper) 88%, transparent);
  -webkit-mask: url("../assets/logo-mark.svg") no-repeat center / contain;
  mask: url("../assets/logo-mark.svg") no-repeat center / contain;
}

.footer__logo {
  display: block;
  /* A stated width, not `auto` with a cap. Once the lockup became `fit-content` the box
     sized itself to the trade line underneath - 154px - and an auto-width image happily
     shrank to match, taking the logo from 176px down to 154. Stating the width makes the
     wordmark the widest thing in the box again, which is what the box should measure. */
  width: var(--footer-logo);
  max-width: 100%;
  height: auto;
}

.footer__brand {
  /* The one number the lockup is built from: the wordmark takes it, the hanger takes a
     measured fraction of it. */
  --footer-logo: 11rem;
  display: grid;
  justify-items: start;
  /* Shrinks the lockup to the width of its widest part - the wordmark, 176px - so that
     centring anything inside it centres it over the logo rather than over the whole
     footer column. Without this the box stretches to the column's 390px and the hanger
     sat 69px left of where it belongs, which is what the client circled. */
  width: fit-content;
  gap: 0.2rem;
  margin-bottom: var(--space-m);
}

.footer__sub {
  font-family: var(--font-ui);
  font-size: 0.56rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: color-mix(in oklch, var(--paper) 72%, transparent);
}

/* The collection charge, under the district field. Two states, and the free one is the
   one worth noticing - so it gets the brand colour and the charged one stays quiet. */
.delivery-note {
  margin-top: var(--space-xs);
  padding: var(--space-xs) var(--space-s);
  border-radius: var(--radius-s);
  border-inline-start: 3px solid var(--line);
  background: var(--paper-sunken);
  font-size: var(--step--1);
  color: var(--ink-soft);
}

.delivery-note[data-state="free"] {
  border-inline-start-color: var(--brand);
  color: var(--ink);
  font-weight: 600;
}

/* -------------------------------------------------------------------------- */
/* CueFlow watermark                                                           */
/* -------------------------------------------------------------------------- */

/* The agency's own signature, kept in its own yellow rather than recoloured to the
   client's orange: it is a mark, not part of the Elegant palette, and it reads as a
   credit precisely because it does not match. It sits on the dark footer, where
   #FFCC00 measures well past AA. */
/* `.site-footer a { color: inherit }` is one class and one type, which outranks a bare
   class: the credit was being painted in the footer's muted grey and the #FFCC00 below
   never applied at all. It only ever looked right on hover, because the hover rule is
   a class plus a pseudo-class and does outrank it. Qualifying the selector fixes the
   resting state, which is the one the client was pointing at. */
.site-footer .podnozje__autor {
  position: relative;
  display: inline-block;
  color: #ffcc00;
  font-weight: 600;
  text-decoration: none;
  padding-bottom: 2px;

  transition:
    transform 0.3s cubic-bezier(0.25, 1, 0.5, 1),
    color 0.3s ease,
    text-shadow 0.3s ease;
}

.podnozje__autor::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 50%;
  width: 0;
  height: 2px;
  background-color: #ffcc00;
  border-radius: 2px;

  transform: translateX(-50%);
  transition: width 0.3s cubic-bezier(0.25, 1, 0.5, 1);
}

.podnozje__autor:hover,
.podnozje__autor:focus-visible {
  transform: translateY(-2px);
  color: #ffe066;
  text-shadow: 0 0 12px rgba(255, 204, 0, 0.4);
}

.podnozje__autor:hover::after,
.podnozje__autor:focus-visible::after {
  width: 100%;
  box-shadow: 0 0 8px rgba(255, 204, 0, 0.6);
}

/* Keyboard users get the same treatment as the mouse, and anyone who asked for less
   motion keeps the colour change without the lift. */
@media (prefers-reduced-motion: reduce) {
  .podnozje__autor,
  .podnozje__autor::after {
    transition: color 0.3s ease;
  }

  .podnozje__autor:hover,
  .podnozje__autor:focus-visible {
    transform: none;
  }
}

/* -------------------------------------------------------------------------- */
/* The statement between the sections                                          */
/* -------------------------------------------------------------------------- */

/* The headline's face, at a size that carries on its own without competing with the
   headline itself: the hero h1 runs to about 99px on a laptop, this to about 46px, and
   the tradition line above it to 23px. Three clear steps rather than two things nearly
   the same size arguing. The padding is what makes it read as being BETWEEN the hero and
   the four steps rather than belonging to either.

   One line only. It held two for a version - the promise and then the credential - and
   the client cut the promise: the hero headline already says it, so the band was saying
   the same thing twice. The sentence that stayed inherits this rule unchanged rather
   than copying its numbers, which is why the two cannot drift apart. */
.statement {
  margin: 0;
  /* Same reason as .hero__tradition: a statement is not a paragraph and the global
     measure would park it off to one side of the shell. */
  max-width: none;
  padding-block: clamp(3rem, 1.5rem + 5vw, 6rem);
  text-align: center;
  font-family: var(--font-display);
  font-size: var(--step-3);
  line-height: 1.15;
  color: var(--ink);
}

/* -------------------------------------------------------------------------- */
/* Reading progress                                                            */
/* -------------------------------------------------------------------------- */

/* Sits on the header's own bottom edge, so it costs no vertical space and cannot shift
   anything below it. The bar is a child rather than the container itself because the
   animation scales a transform, and scaling the container would scale its border. */
.scroll-progress {
  position: absolute;
  inset-inline: 0;
  bottom: -1px;
  height: 2px;
  pointer-events: none;
  overflow: hidden;
}

.scroll-progress span {
  display: block;
  height: 100%;
  background: var(--brand);
  /* Zero-width until a timeline drives it; a browser without scroll-driven animations
     simply never shows it, which is the correct degradation for a decoration. */
  transform: scaleX(0);
}

/* -------------------------------------------------------------------------- */
/* Magnetic call to action                                                     */
/* -------------------------------------------------------------------------- */

/* The pull itself is set from JS as a custom property, so the transition - and the
   reduced-motion opt-out - stay in CSS where the rest of the motion lives. */
.btn[data-magnetic] {
  transition:
    translate var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
  translate: var(--magnet-x, 0) var(--magnet-y, 0);
}

@media (prefers-reduced-motion: reduce) {
  .btn[data-magnetic] {
    translate: none;
  }
}

/* -------------------------------------------------------------------------- */
/* Sound toggle                                                                */
/* -------------------------------------------------------------------------- */

.basket-bar__mute {
  flex: none;
  color: color-mix(in oklch, var(--paper) 70%, var(--ink));
  border-color: color-mix(in oklch, var(--paper) 24%, transparent);
}

.basket-bar__mute:hover,
.basket-bar__mute:focus-visible {
  color: var(--paper);
}

/* Unmuted is the default: waves showing, cross hidden. */
.basket-bar__mute .mute__cross {
  opacity: 0;
}

.basket-bar__mute[data-muted="true"] .mute__waves {
  opacity: 0;
}

.basket-bar__mute[data-muted="true"] .mute__cross {
  opacity: 1;
}

.basket-bar__mute .mute__waves,
.basket-bar__mute .mute__cross {
  transition: opacity var(--dur-fast) var(--ease-out);
}

/* -------------------------------------------------------------------------- */
/* Word mask, for the heading stagger                                          */
/* -------------------------------------------------------------------------- */

/* js/motion/sections.js wraps each word so it can rise out from behind its own line
   box. `overflow: hidden` is what turns that into a mask rather than a slide; without
   it the words visibly come from outside the heading.

   The wrapper is inline-flex so descenders are not clipped: on `inline-block` the line
   box stops at the baseline and the tail of a "j" or "ž" is cut off. */
.word {
  display: inline-flex;
  overflow: hidden;
  vertical-align: bottom;
}

.word > span {
  display: inline-block;
  white-space: pre;
  /* A hair of room below the baseline, so the mask never shaves a descender. */
  padding-bottom: 0.08em;
}

/* -------------------------------------------------------------------------- */
/* The order, told as a scene                                                  */
/* -------------------------------------------------------------------------- */

/* The story section. It is not `section--deferred` and no longer pins either; both of
   those mechanisms were removed after each caused a visible jolt. */
.story {
  background: var(--paper-sunken);
  overflow: clip;
}

.story__pin {
  position: relative;
  /* Tightened with the row above: the section was carrying a full 2xl of air at both
     ends on top of the empty row, and the drawing ended up halfway down the screen. */
  padding-block: var(--space-xl);
}

.story__layout {
  display: grid;
  gap: var(--space-xl);
  align-items: start;
}

@media (min-width: 48rem) {
  .story__layout {
    grid-template-columns: 1.05fr 0.95fr;
    gap: var(--space-2xl);
    /* `min-height: 64vh` with centred items held the drawing and the step list in the
       middle of a row far taller than either of them, which is the "previse praznog
       prostora, spusteno na dole" the client marked up. The row is now as tall as its
       content and both columns start at the top. */
  }
}

.story__stage {
  position: relative;
}

.story__svg {
  display: block;
  width: 100%;
  height: auto;
  /* Up from the top of its column now that the row no longer centres it. */
  margin-block-start: 0;
  /* Taller than it was. At 58vh the drawing sat in the top-left with the bottom half of
     the stage empty, which read as an unfinished layout rather than as a composition. */
  max-height: 72vh;
}

/* Centre the drawing in whatever height the row ends up with, so it never floats at the
   top of an over-tall box. */
.story__stage {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
}

/* Every beat occupies the same space; only one is visible at a time. Stacking them with
   a grid rather than absolute positioning keeps the SVG's own aspect ratio doing the
   sizing, so nothing jumps when a beat changes. */
.story__svg > g {
  transform-box: fill-box;
}

/* On a phone the drawing sits above the steps, so by step three or four the reader had
   scrolled it off the top and could no longer see what the step looked like, which is
   what the client reported. The drawing sticks under the header instead: whichever step
   you scroll to or tap, its picture is on screen next to it. It is shorter here than on
   a wide screen for the same reason, so it leaves room for the words. */
@media (max-width: 47.99rem) {
  /* Block, not grid. A sticky grid item can only travel inside its own grid area, which
     here is exactly as tall as the drawing, so it had nowhere to stick to and scrolled
     away like anything else. As blocks, the drawing's containing block is the whole
     section and it can ride the full height of the step list. */
  .story__layout {
    display: block;
  }

  /* `overflow: clip` on the section makes it the sticky element's scrollport, and a box
     that does not scroll gives sticky nothing to stick to. The clip is only there to keep
     the drawing inside the section on wide screens; `html { overflow-x: clip }` is what
     actually guards the page against sideways scroll, and that is untouched. */
  .story {
    overflow: visible;
  }

  .story__stage {
    position: sticky;
    top: 4.25rem;
    z-index: 1;
    margin-bottom: var(--space-l);
    padding-block: var(--space-xs);
    background: var(--paper-sunken);
  }

  .story__svg {
    max-height: 30vh;
  }
}

.story__beats {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-m);
}

/* A rail down the left of the list, with the current step marked on it. The gap between
   the rail and the words was var(--space-m) and the client said the line looked like it
   was hitting the text; it is a rung wider now, and the marker is inset from the rail so
   the two read as separate things. */
.story__beats {
  position: relative;
  padding-inline-start: var(--space-l);
  border-inline-start: 2px solid var(--line);
}

.story__beat {
  position: relative;
  padding: var(--space-s) 0;
  border-top: 1px solid var(--line);
}

.story__beat.is-current {
  border-top-color: var(--brand);
}

.story__beat.is-current::before {
  content: "";
  position: absolute;
  inset-inline-start: calc(var(--space-l) * -1 - 2px);
  width: 2px;
  height: 100%;
  background: var(--brand);
}

/* The whole row is the control, number included: the client asked to be able to click
   the number or the step. It is a button inside the heading, which is the pattern a
   screen reader already knows, and it keeps the step list a list of headings. */
.story__beat-head {
  margin: 0;
  font-size: inherit;
  font-weight: inherit;
}

.story__pick {
  position: relative;
  display: block;
  width: 100%;
  /* One gutter width, shared with the paragraph below so the two line up without either
     of them having to know how wide the number is. */
  padding-inline-start: 2.6rem;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
}

.story__pick:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 4px;
  border-radius: var(--radius-s);
}

.story__beat-title {
  font-family: var(--font-display);
  font-size: var(--step-0);
  line-height: 1.25;
}

.story__num {
  position: absolute;
  inset-inline-start: 0;
  top: 0.3em;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--ink-faint);
  letter-spacing: 0.08em;
}

.story__beat.is-current .story__num {
  color: var(--brand-text);
}

.story__beat p {
  margin: var(--space-2xs) 0 0;
  padding-inline-start: 2.6rem;
  color: var(--ink-soft);
  font-size: var(--step--2);
}

/* Hover and the current step both lift the title, so a pointer user can tell the rows
   are live before clicking one. */
.story__pick:hover .story__beat-title,
.story__beat.is-current .story__beat-title {
  color: var(--ink);
}

.story__beat:not(.is-current) .story__beat-title {
  color: var(--ink-soft);
}

/* This section is a walk-through, not a headline, and at the page's default sizes it was
   shouting over the drawing beside it. Every size in here steps down one rung. */
/* Half the page-wide gap under the heading: this section's own content starts close
   under it rather than a screen below. */
.story .section__head {
  margin-bottom: var(--space-l);
}

.story .section__head h2 {
  font-size: var(--step-2);
}
.story .section__head .lede {
  font-size: var(--step-0);
}

/* The cross-fade between drawings. It was a GSAP tween; a transition does the same job
   and, unlike the tween, it is still here when the motion layer is switched off. */
/* Without JavaScript the first drawing shows and the rest stay out of the way, so the
   section is a finished list of steps with a picture beside it. The module adds
   `story--live` to the section when it takes over, and only then does visibility become
   something that changes. This used to hang off `.motion-ready`, which was wrong the
   moment the steps stopped being part of the motion layer: at the lowest tier that class
   is never set, and steps two to four would have been unreachable by a reader who had
   asked for reduced motion and was clicking the buttons. */
.story--live .story__svg > g[data-beat] {
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 0.45s var(--ease-out),
    visibility 0s linear 0.45s;
}

.story--live .story__svg > g[data-beat].is-shown {
  opacity: 1;
  visibility: visible;
  transition:
    opacity 0.45s var(--ease-out),
    visibility 0s;
}

html:not(.story-js) .story__svg > g[data-beat]:not([data-beat="1"]) {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .story__svg > g[data-beat] {
    transition: none;
  }
}

/* In flow, under the steps. It used to be pinned to the bottom right corner of the
   section, which was fine while the section was pinned and the layout could not change
   height. The pin is gone, the section grows with its text, and the button was landing
   on top of the last step. Nothing here overlaps anything now because nothing here is
   taken out of flow. */
.story__cta {
  display: flex;
  justify-content: center;
  margin-top: var(--space-xl);
}

/* Without the motion layer the drawing is a single finished picture rather than five
   stacked ones, and the beats are a plain numbered list. */

/* -------------------------------------------------------------------------- */
/* Measurements, once the motion layer is running                              */
/* -------------------------------------------------------------------------- */

/* The `html.motion-ready .section--deferred` override lived here. There is nothing left
   to override: content-visibility is gone from this stylesheet entirely. */

/* -------------------------------------------------------------------------- */
/* Picker: load more                                                           */
/* -------------------------------------------------------------------------- */

/* The count and the button sit on one line, so the number explains the button rather
   than floating above an unexplained control. */
.picker__more {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
  padding-top: var(--space-m);
  margin-top: var(--space-s);
  border-top: 1px dashed var(--line);
}

.picker__more-btn {
  min-height: 44px;
}

/* The per-article note in the review summary. Quieter than the line it belongs to, and
   allowed to wrap: a thirty-character minimum means these are sentences, not labels. */
.summary__row--note {
  margin-top: calc(-1 * var(--space-2xs));
}

.summary__row--note dt,
.summary__row--note dd {
  font-size: var(--step--1);
  color: var(--ink-soft);
}

.summary__row--note dd {
  text-align: right;
  white-space: normal;
  max-width: 28rem;
}

/* -------------------------------------------------------------------------- */
/* Motion scaffolding                                                          */
/* -------------------------------------------------------------------------- */

/*
 * The pin wrapper, borrowed from madewithgsap.com after inspecting it live.
 *
 * A pinned section normally needs `end: "+=" + innerHeight * n`, which is a number
 * computed in JavaScript and therefore wrong the moment the viewport changes, the fonts
 * swap, or an image finally decodes. The better pattern is to let CSS state the duration:
 * the wrapper is as tall as the pin should last, the trigger runs from its top to its
 * bottom, and the child is what gets pinned. Resize handling comes free, because the
 * wrapper's height is just layout.
 *
 * Height is set per section; this only provides the mechanism.
 */
.pin-height {
  position: relative;
}

.pin-height > .pin-target {
  min-height: 100vh;
  min-height: 100dvh;
}

/*
 * Nothing here for the split headings.
 *
 * This block used to declare `.split__line` / `.split__word` / `.split__char` and a
 * perspective on `[data-split]`. It was written before the splitter existed and guessed
 * both the class names and the ownership wrong: the module emits `.kin-word` / `.kin-char`
 * and sets display, white-space and perspective inline on each one, which is the right
 * call because a splitter whose output depends on a stylesheet produces a broken heading
 * the day the stylesheet is reordered. The rules are gone rather than renamed, so nobody
 * maintains two sources of truth for the same geometry.
 */

/*
 * Hover handover.
 *
 * Where a JavaScript effect drives an element's transform, the CSS :hover transform on
 * the SAME element is dead code: an inline style set by GSAP outranks a regular rule, so
 * the CSS rule silently never applies again. Rather than leave a stylesheet that lies,
 * the rule stands down explicitly once the motion layer is live.
 *
 * Note this is the opposite direction from the animation handover in animations.css. A
 * CSS *animation* outranks an inline style and wins; a CSS *declaration* does not and
 * loses. Two different cascade levels, two different failure modes, and the reason both
 * handovers had to be written out by hand.
 */
.motion-ready [data-hover] {
  will-change: transform;
}

.motion-ready [data-hover]:hover,
.motion-ready [data-hover]:focus-visible {
  transform: none;
}

/*
 * The custom cursor's cosmetics only. Every structural style - position, size, layer,
 * pointer-events - is set inline by js/motion/effects/cursor.js, deliberately: a cursor
 * that depends on a stylesheet is a cursor that becomes two stray blocks in the top-left
 * corner the day someone reorders the CSS.
 */
.cursor__ring {
  border: 1px solid currentColor;
  mix-blend-mode: difference;
  color: var(--paper);
}

.cursor__dot {
  background: currentColor;
  mix-blend-mode: difference;
  color: var(--paper);
}

.cursor__label {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-ui);
  font-size: 0.5rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* Belt and braces: the module refuses to mount on touch and under reduced motion, and the
   layer is hidden here as well in case a future edit loosens that check. */
@media (pointer: coarse), (prefers-reduced-motion: reduce) {
  .cursor {
    display: none;
  }
}

/* -------------------------------------------------------------------------- */
/* Motion handover, part two                                                   */
/* -------------------------------------------------------------------------- */

/*
 * The magnet in js/main.js writes `--magnet-x/y` into the `translate` property. The
 * motion layer's own magnet writes `transform`. Those are two separate properties and the
 * browser applies both, so a button under the cursor moved by the sum of two springs
 * tuned differently. Once the motion layer is live, this channel is switched off and the
 * JavaScript one stops writing to it as well.
 */
.motion-ready .btn[data-magnetic] {
  translate: none;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

/*
 * The custom cursor hides the system one. Keyed on a class the module adds, so the arrow
 * comes back the instant the module is torn down or fails to start - a page with no
 * pointer at all is far worse than a page with an ordinary arrow.
 */
.is-cursor-custom,
.is-cursor-custom a,
.is-cursor-custom button,
.is-cursor-custom [role="button"] {
  cursor: none;
}

/* Never inside the order form. Someone typing an address needs a real text caret, and a
   select needs its arrow; a decorative dot is not worth a field nobody can aim at. */
.is-cursor-custom input,
.is-cursor-custom textarea,
.is-cursor-custom select,
.is-cursor-custom #poruci,
.is-cursor-custom #poruci * {
  cursor: auto;
}

/* The glass overlay rules lived here. The module is gone; see js/motion/index.js. */

/* Kept after the steam canvas was removed: the band's photograph has a corner radius and
   its parallax moves the picture inside the frame, so the frame has to clip either way. */
.band {
  overflow: hidden;
}

/*
 * The hero headline is deliberately NOT split.
 *
 * Splitting a heading into one inline-block span per word changes how the browser breaks
 * the lines. On every section title here that made no difference - they are two to five
 * short words on one line either way - but the hero headline went from two lines to three
 * and grew 69px, a second after it had already been painted, taking everything below it
 * down with it. `text-wrap: balance` was the first suspect and was not the cause; the
 * inline-block boxes are.
 *
 * So the H1 keeps the word-mask reveal in js/motion/sections.js, which animates whole
 * words without restructuring them, and the kinetic per-character treatment applies to the
 * section headings, where it costs nothing. Measured at 768px: total layout shift from
 * splitting, with the H1 excluded, is zero.
 */

/* -------------------------------------------------------------------------- */
/* Reserved space for the article picker                                       */
/* -------------------------------------------------------------------------- */

/*
 * The picker renders its first twelve articles from data/pricing.json, which is fetched.
 * Until it arrives `#item-results` is empty and the order form is 305px tall; once it
 * renders the form is 1 422px tall. That is 1 117px of the page moving down, a second or
 * so after the first paint, right where a reader who has just scrolled past the hero is
 * looking. It was the largest single layout shift on the site.
 *
 * So the space is reserved. The two values below are MEASURED, not guessed: twelve rows
 * plus the "show more" button, at a width where the article names fit on one line and at
 * a width where they wrap onto two. Re-measure them with
 *   document.querySelector('#item-results').getBoundingClientRect().height
 * if the default result count or the row layout ever changes.
 *
 * Being slightly short is the safe direction: the remainder is a small shift rather than a
 * permanent gap under the list.
 */
/*
 * Four bands rather than one, because the reserved height genuinely depends on the width:
 * the article names wrap onto two and three lines as the viewport narrows, so the same
 * twelve rows are 985px at 1920 and 1846px at 320. One value would leave 500px of empty
 * space at 430.
 *
 * MEASURED at each band, with this rule temporarily disabled:
 *   320: 1846px   375: 1495px   430: 1313px
 *   768:  959px  1024:  975px  1440:  985px  1920: 985px
 * Re-measure with `document.querySelector('#item-results').getBoundingClientRect().height`
 * if the default result count or the row layout changes.
 */
#item-results {
  min-height: 116rem;
}

@media (min-width: 22.5rem) {
  #item-results {
    min-height: 94rem;
  }
}

@media (min-width: 26rem) {
  #item-results {
    min-height: 83rem;
  }
}

@media (min-width: 48rem) {
  #item-results {
    min-height: 62rem;
  }
}
