/* ===========================================================================
   LYNDRY — the component layer.

   The design system in css/ds/ gives us the tokens: colours, type sizes,
   spacing, border weights, shadows, easings. This file turns those into the
   handful of things the website actually puts on a page.

   THE ONE-SENTENCE RULE, from the design system:
   everything is a line drawing on cream paper, and it casts a hard shadow.
   If an element has no ink outline, it is either type, a divider, or wrong.

   Three rules that are easy to break by accident:

     1. There is ONE outline colour, --ink-900. Never grey, never a tint of
        the fill.
     2. Shadows are hard offsets in pure ink. No blur, no transparency.
        (--shadow-float is the single exception and is for overlays only.)
     3. Text is ink on every brand colour. Suds green, Sunbeam yellow and
        Lilac are all light enough that white text on them is a bug. The only
        light-on-dark pairing is paper on ink.

   Everything here is a plain CSS class, on purpose. The alternative was
   Tailwind utility strings, which for a system this opinionated turn into
   forty characters of border-and-shadow noise on every element.
   =========================================================================== */

/* ---------------------------------------------------------------------------
   The logo
   --------------------------------------------------------------------------- */
/* THE LOGO IS ARTWORK NOW, not a bubble built out of CSS.
   
   It used to be a wordmark inside a border-radius box with a hand-built tail,
   and there was a long note here about how fiddly that tail was. All of it is
   gone: the mark Neil supplied draws a laundry bag and a speech bubble as ONE
   shape, with the wordmark inside it and a drawstring tied at the top. That is
   not something to re-derive in CSS, and trying would only produce a worse
   copy of a file we already have.

   The source was a 1254px PNG on a green ground with no alpha channel, so it
   could not be dropped on a page as-is - it would have been a green square.
   public/css/logo.png is that artwork cut out, un-greened at the edges and
   quantised to 64 colours: 600px wide and 25 KB, against 1 MB for the original.

   IT LIVES IN public/css SO IT IS FINGERPRINTED. Referenced relatively from
   this file, it resolves under /css/<hash>/ and inherits the same immutable
   year-long cache as the stylesheets. assets.js hashes every file in this
   directory, not just the .css ones, precisely so that changing the logo
   changes the URL - without that a new logo would never reach anybody who had
   visited before, which is the exact bug the fingerprint exists to prevent.

   Scale any lockup with --ly-h. Nothing else needs setting. */

.ly-logo {
  display: inline-block;
  line-height: 0;
  font-size: 0;
  text-decoration: none;
}

.ly-logo__mark {
  display: block;
  height: var(--ly-h, 44px);
  /* Width comes from the artwork's own proportions, so a variant only ever has
     to say how tall it is and can never squash the mark. */
  aspect-ratio: 600 / 546;
  background: url(logo.png) center / contain no-repeat;
}

/* The hard ink offset, for marketing placements. A drop-shadow filter rather
   than box-shadow, because the mark is an irregular silhouette and box-shadow
   would trace its rectangle instead. */
.ly-logo--offset .ly-logo__mark {
  filter: drop-shadow(9px 9px 0 var(--ink-900));
}

/* Nav - the 68px ink header. 44px leaves proper breathing room in the bar and
   the wordmark still reads; it was checked at size rather than calculated. */
.ly-logo--nav    { --ly-h: 44px; }

/* Footer and secondary placements. */
.ly-logo--footer { --ly-h: 62px; }

/* The smallest lockup we use. Below about 34px the wordmark inside the bag
   stops being readable, so this is the floor rather than a preference. */
.ly-logo--compact { --ly-h: 38px; }

/* Marketing placements: the sign-in page, and anywhere the mark is the subject
   rather than the furniture. */
.ly-logo--offset { --ly-h: 132px; }

@media (max-width: 600px) {
  .ly-logo--nav    { --ly-h: 38px; }
  .ly-logo--offset { --ly-h: 104px; }
}

/* The avatar — the L in a Suds bubble. Used in the phone mock. */
.ly-avatar { position: relative; display: inline-block; line-height: 0; font-size: 0; }
.ly-avatar__box {
  display: grid;
  place-items: center;
  width: var(--ly-av, 52px);
  height: var(--ly-av, 52px);
  background: var(--suds-500);
  border: 3px solid var(--ink-900);
  border-radius: calc(var(--ly-av, 52px) * 0.35);
  font-family: 'Grandstander', var(--font-display), system-ui, sans-serif;
  font-weight: 900;
  font-size: calc(var(--ly-av, 52px) * 0.5);
  line-height: 1;
  letter-spacing: -0.045em;
  color: var(--ink-900);
  /* Same optical correction as the wordmark — the "L" is a cap too.
     Padding works here where it doesn't on the wordmark: this box has a fixed
     height, so padding shrinks the content area instead of growing the box,
     and a centred glyph moves down by half the padding. Hence 0.19em to buy
     the same 0.095em nudge. */
  padding-top: 0.19em;
}
.ly-avatar__tail {
  position: absolute;
  left: calc(var(--ly-av, 52px) * 0.23);
  bottom: calc(var(--ly-av, 52px) * -0.19 + 3px);
  width: calc(var(--ly-av, 52px) * 0.31);
  height: calc(var(--ly-av, 52px) * 0.19);
}
.ly-avatar__tail svg { display: block; width: 100%; height: 100%; overflow: visible; }
.ly-avatar__tail path { fill: var(--suds-500); stroke: var(--ink-900); stroke-width: 3px; stroke-linejoin: miter; }

/* ---------------------------------------------------------------------------
   Page furniture
   --------------------------------------------------------------------------- */

.container {
  max-width: var(--container-max);
  margin: 0 auto;
  padding-left: var(--gutter-desktop);
  padding-right: var(--gutter-desktop);
}

.container-narrow {
  max-width: 760px;
  margin: 0 auto;
  padding-left: var(--gutter-desktop);
  padding-right: var(--gutter-desktop);
}

/* Section rhythm. The design calls for 88–104px; these are the two sizes. */
.section { padding-top: 88px; padding-bottom: 96px; }
.section-lg { padding-top: 104px; padding-bottom: 112px; }

/* A full-width 3px ink rule is one of the two ways sections are separated
   (the other is the scallop). */
.rule-top { border-top: var(--border-2); }
.rule-bottom { border-bottom: var(--border-2); }

/* The recurring stack at the top of nearly every section:
   mono eyebrow -> Outfit headline -> Schibsted paragraph. */
.eyebrow {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  line-height: var(--leading-label);
  text-transform: uppercase;
  color: var(--ink-500);
  margin: 0 0 18px;
}
.eyebrow-brand { color: var(--suds-700); }

/* A labelled line on the driver's stop card - "TASK: Pick up order #1940".
   Neil's format, to the colon. The label is the eyebrow inline rather than
   sitting above the value, so the whole thing reads as one sentence; it does
   not shrink when the value wraps, so a long address stays lined up under
   itself rather than under the label. */
.stop-line { display: flex; gap: 8px; align-items: baseline; margin: 0 0 12px; }
.stop-line > .eyebrow { flex: none; margin: 0; }
.stop-line > .stop-value { font-size: 20px; font-weight: 700; line-height: 1.35; }
@media (max-width: 380px) {
  .stop-line > .stop-value { font-size: 18px; }
}
/* Neil's call on the pickup card: the LABEL is the bold half, set at least as
   large as the value, and the value is plain. A modifier rather than a change
   to .stop-line, because it is the one card he asked for - the card that
   replaces it after "I'm here" keeps the weight on its value.

   14px is the ceiling the design system sets for a mono label, so this is as
   big as TASK: and WHERE: can go without breaking that rule; the value comes
   down to meet it rather than the label going up past it. */
/* THE CLIP TILE IS A SWITCH, NOT A LABEL.

   Neil's step: the driver taps the number to say he has that clip in his hand,
   it goes from yellow "not in use" to green "in use", and only then will the
   form let him confirm it is on the bag. Two deliberate acts, because the clip
   is the only thing that finds the bag again.

   NO JAVASCRIPT. It is a real checkbox with `required`, so the browser itself
   refuses the submit until it is ticked - that is the actual guard, and it
   works on a phone with no signal and a half-loaded page. The colour and the
   wording are CSS on :checked, and the button is dimmed through :has(). A
   browser without :has() shows a normal-looking button and the native "please
   tick this" prompt: worse looking, still correct. */
/* ONE BOX, THE WIDTH OF THE BUTTON UNDER IT. Neil: the number and its state
   belong inside the same box, and the two controls should be the same size -
   they are the two taps of one action, so they read as a pair. */
.clip-toggle {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 100%;
  padding: 14px 18px;
  border: 2px solid var(--ink-900);
  border-radius: 16px;
  /* RED UNTIL IT IS TRUE. Neil's call, and it earns the exception to "stain is
     for errors only": a clip not in the driver's hand, or a bag not in the van,
     is the state this step exists to catch - a bag left on a doorstep. Paper on
     stain rather than ink, the same pairing the map-failure notice uses. */
  background: var(--stain-500);
  color: var(--paper-050);
  box-shadow: var(--shadow-pop-xs);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease, transform 120ms ease, box-shadow 120ms ease;
}
.clip-toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.clip-toggle:active { transform: translate(3px, 3px); box-shadow: 1px 1px 0 var(--ink-900); }
.clip-toggle:focus-within { outline: 3px solid var(--lilac-500); outline-offset: 3px; }

/* "VAN CLIP 1" over "NOT IN USE" - Neil's arrangement. The clip is named in
   full because that is what he says out loud at a laundromat counter, and 26px
   is as large as the whole phrase goes on a 375px screen. */
.clip-toggle .clip-number {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 26px;
  line-height: 1.1;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.clip-toggle .clip-state {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.clip-toggle .clip-state-on { display: none; }

.clip-toggle:has(input:checked) { background: var(--suds-500); color: var(--ink-900); }

/* THE SAME SWITCH, DRIVEN BY THE SERVER RATHER THAN A CHECKBOX.
   The collect-from-laundromat tiles each post on their own, so what is on or
   off is a fact in the database rather than a box that is ticked - but a driver
   should not be able to tell that apart by looking. is-on is what the page adds
   when the bag has been collected. */
button.clip-toggle { font: inherit; text-align: center; }
.clip-toggle.is-on { background: var(--suds-500); color: var(--ink-900); }
.clip-toggle:has(input:checked) .clip-state-off { display: none; }
.clip-toggle:has(input:checked) .clip-state-on { display: inline; }

/* Until the clip is in his hand there is nothing to confirm. */
.clip-form:has(.clip-toggle input:not(:checked)) .btn-primary {
  opacity: 0.35;
  pointer-events: none;
}

.stop-line-plain > .eyebrow { color: var(--ink-900); font-size: 14px; }
.stop-line-plain > .stop-value { font-weight: 400; font-size: 14px; }
@media (max-width: 380px) {
  .stop-line-plain > .stop-value { font-size: 14px; }
}
.eyebrow-lilac { color: var(--lilac-600); }
.eyebrow-paper { color: var(--ink-400); }

/* Display headlines. Outfit 900, set almost too big and almost too tight. */
.display-1 {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(58px, 6.6vw, 96px);
  line-height: 0.9;
  letter-spacing: -0.045em;
  color: var(--ink-900);
  margin: 0 0 22px;
}

/* The hero headline holds its two-line break at every width.
   It is sized from the COLUMN it sits in, not the viewport, because on
   desktop that column is only about half the screen — a viewport-based size
   overflows it and wraps to a third line, which is what "Text us. We got your
   laundry." did at 1280px while looking fine at 800px.
   10.8cqi is the largest size at which the longer line still fits. */
@supports (font-size: 1cqi) {
  .hero-copy { container-type: inline-size; }
  .hero-copy .display-1 { font-size: clamp(30px, 10.8cqi, 96px); }
}
.display-2 {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(46px, 5.4vw, 76px);
  line-height: 0.92;
  letter-spacing: -0.04em;
  color: var(--ink-900);
  text-wrap: balance;
  margin: 0 0 20px;
}
.display-3 {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(40px, 4.4vw, 60px);
  line-height: 0.94;
  letter-spacing: -0.035em;
  color: var(--ink-900);
  margin: 0 0 24px;
}
.display-4 {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(36px, 4vw, 54px);
  line-height: 0.96;
  letter-spacing: -0.03em;
  color: var(--ink-900);
  margin: 0 0 20px;
}

.lead {
  font-size: 19px;
  line-height: 1.5;
  color: var(--ink-800);
  max-width: 46ch;
  margin: 0 0 30px;
}

.measure-narrow { max-width: 38ch; }
.measure { max-width: 52ch; }

/* ---------------------------------------------------------------------------
   Button
   --------------------------------------------------------------------------- */
/* The signature interaction, on every filled button and interactive card:
   hover lifts up-and-left and deepens the shadow; press pushes it down-and-
   right onto a 1px shadow. It should feel like a latch closing. */

/* ---------------------------------------------------------------------------
   THE PAGE IS LIGHT, AND THE BROWSER HAS TO BE TOLD.

   Neil's phone is in dark mode. The four sticker buttons on the laundromat page
   rendered as empty white boxes there and perfectly on a desktop, because a
   bare <button> with no colour of its own gets the UA's `buttontext` system
   colour - and on a dark-mode iOS device that resolves to WHITE. White text on
   our white button. The markup was identical; only the device differed.

   Two fixes, because either alone leaves a gap:

   `color-scheme: light` tells the browser this document is light whatever the
   device is set to, so every system colour it picks - button text, form
   controls, scrollbars, the tap highlight - is chosen for a light page. LYNDRY
   is a line drawing on cream paper and deliberately has no dark variant, so
   this is a statement of fact rather than a preference.

   And a reset on <button> itself, because system colours are not the only way
   a native control diverges: iOS also draws its own border, corner radius and
   font on an un-reset button. Everything on this site that is a button either
   carries .btn - which sets all of that and wins on specificity - or is a
   hand-styled control like those stickers, and the hand-styled ones were
   relying on defaults that are not the same on every device.
   --------------------------------------------------------------------------- */
:root { color-scheme: light; }

button {
  -webkit-appearance: none;
  appearance: none;
  color: var(--ink-900);
  font-family: inherit;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--control-h-md);
  padding: 0 var(--control-pad-md);
  font-family: var(--font-display);
  font-weight: var(--weight-extrabold);
  font-size: 16px;
  letter-spacing: -0.01em;
  line-height: 1;
  text-decoration: none;
  border: var(--border-w-2) solid var(--ink-900);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-pop-sm);
  cursor: pointer;
  transition: var(--transition-control);
  -webkit-tap-highlight-color: transparent;
}

.btn:hover { transform: var(--lift-hover); box-shadow: var(--shadow-pop-md); filter: brightness(1.06); }
.btn:active { transform: var(--lift-press); box-shadow: var(--shadow-pop-pressed); filter: none; }
.btn:disabled, .btn[aria-disabled='true'] { opacity: 0.45; cursor: not-allowed; }
/* LYNDRY never greys out a control's colour — disabled is opacity only. */
.btn:disabled:hover { transform: none; box-shadow: var(--shadow-pop-sm); filter: none; }

.btn-sm { min-height: var(--control-h-sm); padding: 0 var(--control-pad-sm); font-size: 14px; gap: 6px; box-shadow: 3px 3px 0 var(--ink-900); }
.btn-lg { min-height: var(--control-h-lg); padding: 0 var(--control-pad-lg); font-size: 18px; gap: 10px; box-shadow: var(--shadow-pop-md); }
.btn-lg:hover { box-shadow: var(--shadow-pop-lg); }

.btn-primary   { background: var(--suds-500);    color: var(--ink-900); }
.btn-accent    { background: var(--sunbeam-500); color: var(--ink-900); }
.btn-secondary { background: var(--lilac-500);   color: var(--ink-900); }
.btn-ink       { background: var(--ink-900);     color: var(--paper-050); }
.btn-outline   { background: var(--paper-050);   color: var(--ink-900); }

/* The ink button gets a LILAC offset instead of an ink one.
   An ink shadow behind an ink button reads as part of the button rather than
   under it — the whole shape becomes one black blob and the lift is lost.

   Lilac specifically, out of the design system's three permitted coloured
   offsets, because .btn-ink appears on four different grounds: the Suds hero,
   the cream cards, the Lilac-100 pricing band, and inside the Sunbeam plan
   card. Suds would vanish on the hero and Sunbeam would vanish on the plan
   card. Lilac is the only one visible against all four.

   These come after the .btn rules on purpose — same specificity, later wins. */
.btn-ink { box-shadow: 4px 4px 0 var(--lilac-500); }
.btn-ink.btn-sm { box-shadow: 3px 3px 0 var(--lilac-500); }
.btn-ink.btn-lg { box-shadow: 6px 6px 0 var(--lilac-500); }
.btn-ink:hover { box-shadow: 6px 6px 0 var(--lilac-500); }
.btn-ink.btn-lg:hover { box-shadow: 10px 10px 0 var(--lilac-500); }
.btn-ink:active,
.btn-ink.btn-lg:active { box-shadow: 1px 1px 0 var(--lilac-500); }
.btn-ink:disabled:hover { box-shadow: 4px 4px 0 var(--lilac-500); }

/* Ghost is the one exception to the whole signature: no border, no shadow,
   no lift. It underlines on hover and that is all. */
.btn-ghost {
  background: transparent;
  color: var(--ink-900);
  border-color: transparent;
  box-shadow: none;
}
.btn-ghost:hover { transform: none; box-shadow: none; filter: none; text-decoration: underline; text-underline-offset: 4px; }
.btn-ghost:active { transform: none; box-shadow: none; }

.btn-full { display: flex; width: 100%; }

/* ---------------------------------------------------------------------------
   Card
   --------------------------------------------------------------------------- */

.card {
  background: var(--paper-050);
  color: var(--ink-900);
  border: var(--border-w-2) solid var(--ink-900);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-pop-md);
  padding: var(--space-6);
}

.card-sm { box-shadow: var(--shadow-pop-sm); }
.card-lg { box-shadow: var(--shadow-pop-lg); }
.card-xl { border-radius: var(--radius-xl); }

.card-sunken    { background: var(--paper-200); }
.card-brand     { background: var(--suds-500); }
.card-brandsoft { background: var(--suds-100); }
.card-accent    { background: var(--sunbeam-500); }
.card-lilac     { background: var(--lilac-300); }
.card-ink       { background: var(--ink-900); color: var(--paper-050); }

/* A card sitting ON ink needs a coloured offset, because an ink shadow on an
   ink ground is invisible. These three tokens are the only permitted
   non-ink shadows in the system. */
.card-on-ink { box-shadow: var(--shadow-pop-brand); }

/* ---------------------------------------------------------------------------
   Input, and the consent checkbox
   --------------------------------------------------------------------------- */
/* Pure white is reserved for input fields, so a field reads as a hole punched
   in the cream page. It is the one place --paper-000 is used. */

.field { display: flex; flex-direction: column; gap: 6px; width: 100%; }

.field-label {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-700);
}

.field-hint { font-size: var(--text-caption); color: var(--ink-500); }

.input, .select, .textarea {
  display: block;
  width: 100%;
  min-height: var(--control-h-md);
  padding: 10px 14px;
  background: var(--paper-000);
  border: var(--border-w-1) solid var(--ink-900);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: var(--text-body-md);
  font-weight: 500;
  color: var(--ink-900);
  transition: box-shadow var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}

.input-lg { min-height: var(--control-h-lg); font-size: var(--text-body-lg); }
.textarea { min-height: 0; line-height: 1.5; resize: vertical; }

.input::placeholder, .textarea::placeholder { color: var(--ink-400); }

/* The error state is the only place Stain red appears. */
.input-error { border-color: var(--stain-500); box-shadow: 2px 2px 0 var(--stain-500); }

/* A real checkbox, visually hidden, with a drawn box beside it. Hiding it with
   opacity rather than display:none keeps it reachable by keyboard and by
   screen readers — and it still gates form submission, which is the whole
   point of the consent box. */
.check {
  position: relative; display: inline-flex; align-items: flex-start; gap: 12px; cursor: pointer; }
/* THE INPUT SITS ON TOP OF ITS OWN BOX, INVISIBLE BUT REAL.
   It used to be width:0 height:0, which looks identical and breaks the one
   thing a required checkbox is for: a browser CANNOT show "Please check this
   box" on a control it cannot focus, so Chrome refused the submit and said
   nothing at all. Click Text me with the box unticked and the page simply did
   not respond - no error, no text, no explanation.
   Real dimensions, still invisible, still styled by the span next to it. */
.check input {
  position: absolute;
  left: 0;
  top: 1px;
  width: 24px;
  height: 24px;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.check-box {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  flex: 0 0 auto;
  margin-top: 1px;
  background: var(--paper-000);
  border: var(--border-w-1) solid var(--ink-900);
  border-radius: var(--radius-xs);
  transition: var(--transition-control);
}
.check-box .icon { opacity: 0; }
.check input:checked + .check-box { background: var(--suds-500); box-shadow: var(--shadow-pop-xs); }
.check input:checked + .check-box .icon { opacity: 1; }
.check input:focus-visible + .check-box { box-shadow: var(--focus-ring); }

/* Radios use the same drawn box, rounded. The tick reads clearly enough at
   this size that a separate filled-dot state is not worth the extra rule. */
.check-box-round { border-radius: var(--radius-circle); }

.check-text { font-size: 13px; line-height: 1.5; color: var(--ink-700); }
.check-text a { color: var(--suds-700); text-decoration: underline; }

/* A BUTTON IN THERE IS NOT A LINK IN THIS SENSE, AND THE :not() IS LOAD-BEARING.

   This rule is `.card-brand a`, which is a class plus an element and therefore
   beats `.btn-ink`, which is one class. So the ink button on the green CTA band
   had its paper text overwritten with ink and rendered as a black rectangle
   with black writing on it - invisible, on "Book a Pickup", the most important
   control on all seventy town pages. Its arrow went with it, because an icon
   mask paints in currentColor.

   The bug was half-noticed once already: the rule underneath used to be
   `.hero a.btn, .card-brand a.btn { text-decoration: none; }`, which is
   somebody hitting exactly this collision, fixing the underline it caused and
   not seeing that the same line was also setting a colour. Excluding buttons
   from the match rather than patching each property they leak into is what
   stops the next property doing it again - and it makes that rule unnecessary,
   because .btn already sets text-decoration: none and now has nothing beating
   it.

   Links sitting ON Suds green are ink, not the usual --suds-700.
   ONE DEPARTURE FROM THE SPEC, and a deliberate one: the handoff asks for
   --suds-700 links on the green consent block, which measures 2.39:1 against
   --suds-500. That fails WCAG AA for body text by a wide margin. Ink on the
   same green is 5.92:1 and passes. The underline stays, so the link is still
   obviously a link. */
.hero a:not(.btn),
.card-brand a:not(.btn) { color: var(--ink-900); text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px; }
.hero a:not(.btn):hover,
.card-brand a:not(.btn):hover { color: var(--suds-700); }

/* ---------------------------------------------------------------------------
   Badge
   --------------------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 12px;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--text-label-sm);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  border: var(--border-w-1) solid var(--ink-900);
  border-radius: var(--radius-pill);
  background: var(--paper-050);
  color: var(--ink-900);
}
.badge-ink { background: var(--ink-900); color: var(--paper-050); }
.badge-accent { background: var(--sunbeam-500); }

/* ---------------------------------------------------------------------------
   Icons
   --------------------------------------------------------------------------- */
/* Lucide glyphs, applied as a CSS mask so they inherit their colour from
   surrounding text and need no per-colour variants. See css/icons.css — every
   glyph the site uses is in that one file, so swapping icon sets is a
   one-file change. */

.icon {
  display: inline-block;
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}
.icon-16 { width: 16px; height: 16px; }
.icon-22 { width: 22px; height: 22px; }
.icon-26 { width: 26px; height: 26px; }
.icon-30 { width: 30px; height: 30px; }

/* The recurring pattern for feature icons: an outlined square, filled with
   the palest Suds, glyph centred in ink. */
.icon-tile {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  flex: 0 0 auto;
  background: var(--suds-100);
  border: var(--border-w-2) solid var(--ink-900);
  border-radius: 16px;
  color: var(--ink-900);
}
.icon-tile-52 { width: 52px; height: 52px; border-radius: 14px; }
.icon-tile-lilac { background: var(--lilac-300); }
.icon-tile-suds { background: var(--suds-200); }

/* ---------------------------------------------------------------------------
   The scallop
   --------------------------------------------------------------------------- */
/* A half-circle mask hanging off the bottom of a coloured field, with a 3px
   ink lip. 58px repeat, 28px radius. ONE PER PAGE — it is a full stop, and a
   page with two of them has two endings. */

.scallop { position: relative; height: 30px; }
.scallop i {
  position: absolute;
  left: 0;
  right: 0;
  height: 30px;
  -webkit-mask: radial-gradient(circle at 50% 0, #000 28px, transparent 29px) 0 0 / 58px 30px repeat-x;
  mask: radial-gradient(circle at 50% 0, #000 28px, transparent 29px) 0 0 / 58px 30px repeat-x;
}
.scallop .scallop-lip { top: 3px; background: var(--ink-900); }
.scallop .scallop-fill { top: 0; background: var(--suds-500); }

/* ---------------------------------------------------------------------------
   The lilac dot grid
   --------------------------------------------------------------------------- */
/* The single repeating texture in the whole system. No gradients anywhere. */

.dotfield {
  background-color: var(--lilac-100);
  background-image: radial-gradient(var(--ink-200) 1.5px, transparent 1.5px);
  background-size: 18px 18px;
  border-top: var(--border-2);
  border-bottom: var(--border-2);
}

/* ---------------------------------------------------------------------------
   Chrome: the ink header and footer
   --------------------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--ink-900);
}
.site-header-bar {
  display: flex;
  align-items: center;
  gap: 36px;
  height: 68px;
}
/* The advert page's header: one mark, centred, and nothing else in the bar.
   A modifier rather than an inline style on the element, because that is where
   layout lives here - and because the bar itself is shared with the real
   navigation, which must stay left aligned. */
.site-header-bar-center { justify-content: center; }

.site-nav { display: flex; align-items: center; gap: 26px; margin-right: auto; }
.site-nav a {
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 600;
  color: var(--paper-200);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.site-nav a:hover, .site-nav a[aria-current='page'] { color: var(--suds-300); }

/* Mobile menu, built on <details> so it needs no JavaScript at all. */
.nav-toggle { display: none; margin-left: auto; position: relative; }
.nav-toggle > summary { list-style: none; cursor: pointer; }
.nav-toggle > summary::-webkit-details-marker { display: none; }
.nav-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 10px);
  width: 220px;
  background: var(--paper-050);
  border: var(--border-2);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-pop-md);
  overflow: hidden;
}
.nav-panel a {
  display: block;
  padding: 12px 16px;
  font-size: 16px;
  font-weight: 600;
  color: var(--ink-900);
  text-decoration: none;
  border-bottom: 1px solid var(--ink-100);
}
.nav-panel a:last-child { border-bottom: none; background: var(--suds-500); }

/* The ops header. Same ink bar, but its nav must never collapse into a
   hamburger — it is three short links and a driver is holding a phone. It
   wraps to a second row instead. */
.site-header-bar.ops-bar {
  height: auto;
  flex-wrap: wrap;
  gap: 14px 20px;
  padding-top: 12px;
  padding-bottom: 12px;
}
.ops-bar .site-nav { gap: 18px; }

/* The ops nav scrolls sideways instead of pushing the page.
   Ten screens is wider than a phone, and because this nav never collapses to
   the marketing hamburger it was dragging the whole document to 556px at a
   375px viewport - every ops page scrolled horizontally, on the device the
   driver actually holds. Grouping into four menus fixed most of that; the
   scroll stays as the backstop for a narrow phone. */
.ops-bar .site-nav {
  overflow-x: visible;
  flex: 1 1 auto;
  min-width: 0;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.ops-bar .site-nav::-webkit-scrollbar { display: none; }
.ops-bar .site-nav a { white-space: nowrap; }

/* ---------------------------------------------------------------------------
   The ops menus.
   Built on <details>, exactly like the marketing hamburger above, so they open
   and close with NO JAVASCRIPT AT ALL. That matters more here than on the
   website: this is the chrome around a driver's action pages, and a nav that
   needs a script to open is a nav that can fail to open.
   --------------------------------------------------------------------------- */
.ops-menu { position: relative; flex: none; }

/* A GROUP WITH ONE ITEM IN IT renders as a plain link rather than a dropdown
   you have to open to find a single entry. It has to LOOK like the summaries
   beside it - same size, weight and colour - or the nav reads as one odd item
   among four rather than five of the same thing. */
.ops-menu-solo {
  flex: none;
  white-space: nowrap;
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 600;
  color: var(--paper-200);
  padding: 6px 0;
  text-decoration: none;
}
.ops-menu-solo:hover { color: var(--suds-300); }
.ops-menu-solo[aria-current='page'] { color: var(--suds-300); }

.ops-menu > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  white-space: nowrap;
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 600;
  color: var(--paper-200);
  padding: 6px 0;
}
.ops-menu > summary::-webkit-details-marker { display: none; }
.ops-menu > summary::marker { content: ''; }
.ops-menu > summary:hover { color: var(--suds-300); }

/* The group holding the page you are on. Same colour the individual tabs used
   to go, so "where am I" survives the regrouping. */
.ops-menu[data-here='1'] > summary { color: var(--suds-300); }

/* A caret drawn in borders rather than an icon: icons.css has no chevron, and
   adding a whole Lucide glyph for a triangle is not worth the mask. */
.ops-menu > summary::after {
  content: '';
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
  transition: transform var(--dur-fast) var(--ease-out);
}
.ops-menu[open] > summary::after { transform: rotate(180deg); }

.ops-menu-panel {
  position: absolute;
  left: 0;
  top: calc(100% + 12px);
  min-width: 210px;
  background: var(--paper-050);
  border: var(--border-2);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-pop-md);
  overflow: hidden;
  /* Above the page, and above a Leaflet map, which sets its own stacking. */
  z-index: 60;
}
.ops-menu-panel a {
  display: block;
  padding: 12px 16px;
  font-size: 16px;
  font-weight: 600;
  color: var(--ink-900);
  text-decoration: none;
  border-bottom: 1px solid var(--ink-100);
}
.ops-menu-panel a:last-child { border-bottom: none; }
.ops-menu-panel a:hover { background: var(--paper-200); color: var(--ink-900); }
.ops-menu-panel a[aria-current='page'] { background: var(--suds-500); color: var(--ink-900); }

/* On a phone a left-anchored panel hangs off the right edge of the screen, and
   a panel that overflows makes the whole page scroll sideways - which is the
   one thing the layout is never allowed to do.

   It is the last TWO menus, not just the last one. The panel is 210px wide, so
   any menu whose left edge sits within 210px of the right edge overflows, and
   on a 360px screen that is Tools as well as Resources. Anchoring both to the
   right makes them open inwards. The clamp is the backstop: whatever the
   screen, the panel can never be wider than it. */
@media (max-width: 560px) {
  /* THE ROW WRAPS RATHER THAN OVERFLOWING. Four menus need about 358px of row
     and a 360px phone gives the nav 320px, so the last one hung past the edge
     and took the whole page's horizontal scroll with it. Tightening the gap
     would buy a few pixels and lose them again the next time a label got
     longer - "Economics" became "Unit economics" and the row grew. Wrapping
     cannot be outgrown. */
  .ops-bar .site-nav { flex-wrap: wrap; gap: 10px 16px; position: relative; }

  /* THE PANEL ANCHORS TO THE NAV, NOT TO ITS OWN MENU. Anchored to the menu it
     is always wrong somewhere: left-aligned it hangs off the right edge for the
     last menu, right-aligned it hangs off the LEFT edge for a menu that has
     wrapped onto the second row. Anchored to the nav and given its full width,
     it cannot leave the screen in either direction however the row wraps or
     however long the labels get. */
  .ops-menu { position: static; }
  .ops-menu-panel { left: 0; right: 0; width: auto; min-width: 0; }
}

.site-footer { background: var(--ink-900); color: var(--paper-200); }
.footer-head {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--text-label-sm);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-400);
  margin: 0 0 14px;
}
.site-footer a { color: var(--paper-200); text-decoration: none; font-size: 15px; font-weight: 600; }
.site-footer a:hover { color: var(--suds-300); }
.footer-legal {
  font-family: var(--font-mono);
  font-size: var(--text-label-sm);
  line-height: 1.7;
  letter-spacing: 0.04em;
  color: var(--ink-400);
}

/* ---------------------------------------------------------------------------
   The hero, and its bubbles
   --------------------------------------------------------------------------- */

.hero { background: var(--suds-500); position: relative; overflow: hidden; }
.hero-inner {
  min-height: calc(100vh - 68px);
  padding-top: 44px;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
}
.hero-row {
  /* `1 0 auto`, not `1`. With a plain `flex: 1` the row is allowed to shrink
     below its own content on a short viewport, and the vertically centred
     column then overflows down into the fact rail. */
  flex: 1 0 auto;
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: 56px;
  align-items: center;
  position: relative;
  z-index: 1;
}

/* The fact rail pinned to the bottom of the green. */
.fact-rail {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 20px;
  margin-top: 40px;
  padding: 22px 0 34px;
  border-top: var(--border-2);
  position: relative;
  z-index: 1;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-900);
}
.fact-rail span { display: inline-flex; align-items: center; gap: 14px; }
.fact-rail span::after {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ink-900);
}
.fact-rail span:last-child::after { display: none; }

/* The lilac bar under the hero. It needs the same lift off the page as the row
   and the fact rail above it: the bubble layer is POSITIONED and this was not,
   and a positioned element paints over an unpositioned one whatever order they
   are written in - so bubbles drifted across the front of the button. */
.order-online {
  position: relative;
  z-index: 1;

  /* THE PRICING BAND'S TREATMENT, ON THE ONE BUTTON. Neil's ask, twice: the
     same dot grid, and the same purple under it. So both values are the ones
     .dotfield uses rather than a second set that could drift from them - there
     is one purple on this page and one texture, not two of each.

     NOT the .dotfield class itself, because it also draws a rule across the
     top and bottom. That is right for a full-width band and wrong inside a
     card that already has an ink outline all the way round.

     And not .card-lilac either: that is --lilac-300, a deeper purple than the
     band's --lilac-100. The class comes off the element in home.html so this
     rule is the only thing setting the fill and the two cannot fight. */
  background-color: var(--lilac-100);
  background-image: radial-gradient(var(--ink-200) 1.5px, transparent 1.5px);
  background-size: 18px 18px;
}
/* Rising soap bubbles. Decorative, so the layer is aria-hidden and cannot be
   clicked through. Each bubble starts with a NEGATIVE animation delay, which
   is what makes the field already full when the page loads instead of
   filling up over the first half minute. */
.bubbles { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; }
.bubbles span {
  position: absolute;
  border-radius: 50%;
  border: 2px solid rgba(16, 18, 16, 0.3);
  background: rgba(255, 255, 255, 0.22);
  animation-name: lyRise;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
@keyframes lyRise {
  from { transform: translateY(0) scale(0.4); }
  to   { transform: translateY(-104vh) scale(1); }
}

/* ---------------------------------------------------------------------------
   The phone mock
   --------------------------------------------------------------------------- */
/* Laid out at real iPhone size (402 x 874) and scaled down as one piece.
   Building it small instead would mean shrinking every font, radius and
   bubble by hand and getting them wrong.

   It is deliberately static. An animated version was built, reviewed and
   removed during the design. Do not reintroduce motion here. */

.phone-wrap { display: flex; justify-content: center; }
.phone-bezel {
  width: 402px;
  background: var(--ink-900);
  padding: 12px;
  border-radius: 60px;
  box-shadow: var(--shadow-pop-xl);
  line-height: 0;
  /* Scaled from its centre, which leaves 20% of the height as empty space
     above and below. The negative margins claim that space back so the hero
     row lays out around what you can actually see. Changing the scale means
     recalculating the margin: it is (height x (1 - scale)) / 2. */
  transform: scale(0.6);
  margin: -175px 0;
}
.phone-screen {
  position: relative;
  height: 874px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: 48px;
  background: var(--paper-050);
  line-height: var(--leading-body);
}
.phone-island {
  position: absolute;
  left: 50%;
  top: 12px;
  transform: translateX(-50%);
  width: 126px;
  height: 37px;
  border-radius: 20px;
  background: var(--ink-900);
  z-index: 2;
}
.phone-status {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 18px 30px 0;
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 700;
  color: var(--ink-900);
}
.phone-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 24px 16px 12px;
  background: var(--paper-100);
  border-bottom: var(--border-2);
}
/* The conversation header uses the logo's .ly-avatar, defined at the top of
   this file — there is no separate phone-only avatar. */
.phone-name {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: var(--tracking-label);
  color: var(--ink-900);
}
/* Bottom-anchored, so the thread sits at the base of the screen the way a
   real conversation does. The bottom padding clears the home indicator. */
.phone-thread {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 10px;
  padding: 14px 16px 40px;
}
.phone-day {
  align-self: center;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: var(--tracking-label);
  color: var(--ink-400);
  padding-bottom: 4px;
}
.bubble {
  max-width: 82%;
  padding: 9px 14px;
  border: var(--border-w-1) solid var(--ink-900);
  font-size: 16px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--ink-900);
}
.bubble-out { align-self: flex-end; max-width: 78%; background: var(--suds-500); border-radius: 18px 18px 4px 18px; }
.bubble-in  { align-self: flex-start; background: var(--paper-200); border-radius: 18px 18px 18px 4px; }
.phone-receipt {
  align-self: flex-end;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: var(--tracking-label);
  color: var(--ink-400);
}
.phone-home {
  width: 140px;
  height: 5px;
  border-radius: 3px;
  background: var(--ink-900);
  align-self: center;
  margin-top: 10px;
}

/* ---------------------------------------------------------------------------
   Small repeated pieces
   --------------------------------------------------------------------------- */

/* Grid ratios are classes, not inline styles, on purpose: an inline
   grid-template-columns beats the media query below and the page then refuses
   to collapse on a phone. If you need a new ratio, add it here. */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 26px; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 26px; }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; }

.grid-2-wide { grid-template-columns: 1.1fr 0.9fr; }   /* copy left, panel right */
.grid-2-narrow { grid-template-columns: 0.75fr 1.25fr; } /* sticky heading, content */
.grid-3-address { display: grid; grid-template-columns: 2fr 0.8fr 1.2fr; gap: 16px; }

/* TICKING A VAN CLIP OFF AT THE LAUNDROMAT.

   A checkbox drawn as the big number the driver says out loud. The input is
   the control - it is only visually hidden, never display:none - so it keeps
   its focus ring, its tap target and, crucially, its `required` behaviour:
   the browser refuses to submit and points at the first unticked one. No
   JavaScript decides anything here. */
.clip-tick { position: relative; display: inline-block; cursor: pointer; }

.clip-tick input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.clip-tick span {
  display: block;
  min-width: 56px;
  padding: 12px 14px;
  border: 2px solid var(--ink-900);
  border-radius: 12px;
  background: var(--paper-000);
  text-align: center;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 22px;
  line-height: 1.1;
  transition: background 90ms linear, transform 90ms linear;
}

/* Ticked: it has been taken off the bag and handed over. Ink on suds, like
   every other "done" on this system. */
.clip-tick input:checked + span {
  background: var(--suds-500);
  box-shadow: inset 0 0 0 2px var(--paper-000);
}

.clip-tick input:focus-visible + span {
  outline: 3px solid var(--lilac-500);
  outline-offset: 2px;
}

.clip-tick input:active + span { transform: translate(1px, 1px); }

/* THE DAY'S ROUTES ON /ops/run. Five equal cards on one line, always.

   A class rather than an inline grid-template-columns for the reason stated
   above, and it matters more here than anywhere: this is the strip a driver
   looks at on a phone. It was flex-wrap, so the fifth card dropped onto its own
   row at full width and the five stopped reading as one set of five.

   They stay on one line at every width. The type shrinks instead, and the
   labels are written compactly - "10am-12pm" rather than "10am and 12pm" - so
   there is something to shrink. Below 380px the padding goes rather than the
   line breaking: a wrapped round strip is a worse answer than a tight one. */
.run-routes {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 9px;
}

.run-route {
  padding: 13px 11px;
  border: 2px solid;
  border-radius: 14px;
  text-align: left;
  text-decoration: none;
  color: inherit;
  display: block;
  min-width: 0;               /* let a grid cell actually shrink */
}

.run-route .rr-when {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin-bottom: 5px;
  white-space: nowrap;
}

.run-route .rr-what { font-size: 13px; }

@media (max-width: 620px) {
  .run-routes { gap: 6px; }
  .run-route { padding: 10px 7px; }
  .run-route .rr-when { font-size: 9px; letter-spacing: 0.02em; }
  .run-route .rr-what { font-size: 11px; }
}

@media (max-width: 400px) {
  .run-routes { gap: 4px; }
  .run-route { padding: 8px 5px; border-radius: 11px; }
  .run-route .rr-when { font-size: 8px; letter-spacing: 0; }
  .run-route .rr-what { font-size: 10px; }
}

.stack > * + * { margin-top: 18px; }

/* A question-and-answer list, used on the FAQ-shaped sections. */
.qa { padding: 28px 0; border-top: 1px solid var(--ink-200); }
.qa:first-of-type { border-top: var(--border-2); }
.qa:last-of-type { border-bottom: var(--border-2); }
.qa h3 { font-family: var(--font-display); font-weight: 800; font-size: 22px; margin: 0 0 10px; }
.qa p { font-size: 16px; line-height: 1.55; color: var(--ink-700); max-width: 58ch; margin: 0; }

/* A price row: label left, figure right. */
.price-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
  padding: 18px 0;
  border-bottom: 1px solid var(--ink-100);
}
.price-row:last-child { border-bottom: none; }
.price-row .amount { font-family: var(--font-display); font-weight: 800; font-size: 22px; color: var(--ink-900); }

.helper {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--ink-500);
  margin: 18px 0 0;
}
.helper-brand { color: var(--suds-700); }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------------------
   Long-form legal pages
   --------------------------------------------------------------------------- */

.prose { max-width: 68ch; }
.prose h2 {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 28px;
  letter-spacing: -0.02em;
  margin: 44px 0 14px;
}
.prose h3 { font-size: 21px; margin: 32px 0 10px; }
.prose p, .prose li { font-size: 17px; line-height: 1.7; color: var(--ink-800); }
.prose ul, .prose ol { padding-left: 22px; margin: 0 0 16px; }
.prose li { margin-bottom: 8px; }
.prose blockquote {
  margin: 20px 0;
  padding: 16px 22px;
  background: var(--paper-200);
  border: var(--border-w-1) solid var(--ink-900);
  border-radius: var(--radius-md);
  font-style: italic;
}
.prose a { color: var(--suds-700); }

/* ---------------------------------------------------------------------------
   Motion
   --------------------------------------------------------------------------- */
/* Scroll reveal. The hiding is applied by JavaScript at runtime, never in
   plain CSS — a browser that never runs the script must show the page
   normally rather than a blank one. See the script in src/web/layout.js. */

.js-anim [data-reveal] {
  opacity: 0.4;
  transform: translate3d(0, 26px, 0);
}
.js-anim [data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-entrance) var(--ease-pop), transform var(--dur-entrance) var(--ease-pop);
}

/* Anyone whose device asks for less motion gets none of it. This kills the
   bubbles, the parallax and the reveal in one go. Any new animation must be
   listed here too. */
@media (prefers-reduced-motion: reduce) {
  .bubbles span { animation: none; }
  [data-parallax], [data-parallax-x] { transform: none !important; }
  .js-anim [data-reveal] { opacity: 1 !important; transform: none !important; }
  html { scroll-behavior: auto; }
}

/* ---------------------------------------------------------------------------
   Responsive
   --------------------------------------------------------------------------- */
/* The design was drawn desktop-only and every grid was a fixed column count.
   These breakpoints are ours, not the designer's — a service sold on "the
   tool you already have is your phone" deserves a proper mobile pass, and
   this is a sound collapse rather than that pass. */

@media (max-width: 1080px) {
  .grid-4 { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 900px) {
  .container, .container-narrow { padding-left: 20px; padding-right: 20px; }
  .section { padding-top: 64px; padding-bottom: 68px; }
  .section-lg { padding-top: 72px; padding-bottom: 76px; }

  /* The nav collapses to the Menu button. The :not() matters — the Menu
     control is itself a .btn, and without it the header would end up with
     nothing in it at all. */
  .site-nav { display: none; }
  .site-header .btn:not(summary) { display: none; }
  .nav-toggle { display: block; }

  /* …except in the ops bar, which keeps its links and its sign-out button at
     every width. There is no hamburger there to fall back to. */
  .ops-bar .site-nav { display: flex; }
  .ops-bar .btn:not(summary) { display: inline-flex; }

  .hero-row { grid-template-columns: 1fr; gap: 32px; }
  /* The phone mock is the first thing to go: it is decorative, and at this
     width it would be a 240px-wide illegible sliver. */
  .phone-wrap { display: none; }
  .hero-inner { min-height: 0; padding-bottom: 0; }

  .fact-rail { font-size: 11px; gap: 10px; margin-top: 28px; padding: 18px 0 28px; }
  .fact-rail span::after { display: none; }

  /* The hero form stacks. Side by side, a 375px screen leaves the field about
     150px wide, which is not enough to see a phone number in. */
  .hero-form { flex-direction: column; }
  .hero-form > * { width: 100%; }
  .hero-form .btn { width: 100%; }

  .grid-2, .grid-3, .grid-4,
  .grid-2-wide, .grid-2-narrow, .grid-3-address { grid-template-columns: 1fr; }
  .sticky-col { position: static !important; }
}

@media (max-width: 600px) {
  .btn-lg { min-height: var(--control-h-md); padding: 0 var(--control-pad-md); font-size: 16px; }

  /* Fallback for browsers without container query units — the rule above
     handles this properly everywhere else. Without it the base clamp bottoms
     out at 58px, at which the longer line needs more width than a phone has. */
  .display-1 { font-size: clamp(30px, 9.6vw, 58px); }
}


/* ==========================================================================
   The laundromat page: a flow strip and an FAQ list.
   Both are here rather than inline on the page, for the same reason
   everything else is: a page file holds its own words, not its own styling.
   ========================================================================== */

/* A row of numbered steps with an arrow between them. Used to show a
   laundromat how little of the day is theirs - one box in the row is theirs
   and the rest are ours, which is the entire sales argument made visually. */
.flow {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
  align-items: stretch;
}

.flow-step {
  position: relative;
  background: var(--paper-050);
  border: 2px solid var(--ink-900);
  border-radius: 16px;
  box-shadow: var(--shadow-pop-sm);
  padding: 22px 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* Whose step it is. Ours are paper, theirs is Suds - so the one box that is
   their job is the one that stands out on the row. */
.flow-step-yours { background: var(--suds-200); box-shadow: var(--shadow-pop-md); }

.flow-who {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-500);
}
.flow-step-yours .flow-who { color: var(--ink-900); }

.flow-step h3 {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 19px;
  line-height: 1.2;
  margin: 0;
}
.flow-step p { font-size: 15px; line-height: 1.5; color: var(--ink-700); margin: 0; }

/* The arrow between steps. A pseudo-element rather than markup, so the page
   file stays a list of steps and nobody has to hand-write connectors. */
.flow-step + .flow-step::before {
  content: "";
  position: absolute;
  left: -11px;
  top: 50%;
  width: 8px;
  height: 8px;
  border-top: 2px solid var(--ink-900);
  border-right: 2px solid var(--ink-900);
  transform: translateY(-50%) rotate(45deg);
}

/* An FAQ, built on <details> so it opens with no JavaScript - the same reason
   the navigation is. A partner reading this on a bad connection in a shop
   still gets working answers. */
.faq { display: flex; flex-direction: column; gap: 12px; }

.faq details {
  background: var(--paper-050);
  border: 2px solid var(--ink-900);
  border-radius: 14px;
  box-shadow: var(--shadow-pop-xs);
  overflow: hidden;
}

.faq summary {
  cursor: pointer;
  list-style: none;
  padding: 20px 22px;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 18px;
  line-height: 1.3;
  display: flex;
  justify-content: space-between;
  gap: 16px;
  align-items: baseline;
}
.faq summary::-webkit-details-marker { display: none; }

/* The open/closed sign. Text rather than an icon mask, because it has to
   change with the element's state and a mask cannot. */
.faq summary::after {
  content: "+";
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 22px;
  line-height: 1;
  flex: 0 0 auto;
}
.faq details[open] summary::after { content: "3"; }

.faq summary:focus-visible {
  outline: 3px solid var(--lilac-500);
  outline-offset: -3px;
}

.faq-answer { padding: 0 22px 22px; }
.faq-answer p { font-size: 16px; line-height: 1.6; color: var(--ink-700); margin: 0 0 12px; }
.faq-answer p:last-child { margin: 0; }

@media (max-width: 900px) {
  /* Two by two rather than a single column: the flow reads as a sequence and
     four stacked boxes lose that entirely. */
  .flow { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .flow-step + .flow-step::before { display: none; }
}

@media (max-width: 520px) {
  .flow { grid-template-columns: minmax(0, 1fr); }
}

/* ==========================================================================
   /bergen — the paid-advert landing page.
   Two classes it needs that nothing else did. Everything else on that page is
   the existing system.
   ========================================================================== */

/* An inline validation message. Sits under the field it belongs to and is
   hidden with the `hidden` attribute rather than a class, so a form that never
   runs JavaScript simply never shows one. */
.form-error {
  font-size: 14px;
  line-height: 1.45;
  font-weight: 600;
  color: var(--stain-500);
  margin: 6px 0 0;
}
.form-error[hidden] { display: none; }

/* The "text JOIN instead" line. HIDDEN ON DESKTOP ON PURPOSE: an sms: link on
   a desktop browser either does nothing at all or hands the visitor to some
   app they did not want, and a dead link under the button on the one page we
   are paying for traffic to is worse than no link. */
.sms-fallback {
  display: none;
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink-600);
  margin: 16px 0 0;
}
.sms-fallback a {
  color: var(--ink-900);
  font-weight: 700;
  text-decoration: underline;
  /* A phone number is a tap target on the device where this shows. */
  padding: 2px 0;
}

@media (max-width: 900px) {
  .sms-fallback { display: block; }
}

/* --- The pickup spot, and the box that only matters sometimes -------------

   The portal asks where the driver should find the bag from a short list, with
   "Somewhere else" at the end and a text box under it. The box is only useful
   for that one choice, so it is hidden for the others.

   NO JAVASCRIPT, AND IT FAILS SAFE. Both rules live inside the @supports, so a
   browser without :has() never applies the display:none either and shows both
   fields - which is mildly untidy and always usable. Hiding it unconditionally
   and revealing it conditionally would leave anyone on an older browser with a
   field they can never fill in.

   option:checked tracks the live selection, so this updates as the menu
   changes with no script involved. */
@supports selector(:has(*)) {
  .spot-group .spot-other { display: none; }
  .spot-group:has(select[name="spot"] option[value="OTHER"]:checked) .spot-other { display: block; }
}

/* How often, which only matters once "Yes" is chosen. Same shape as the
   .spot-group rule above and the same reason for the @supports: a browser
   that cannot reveal it must not hide it either. */
@supports selector(:has(*)) {
  .repeat-group .repeat-cadence { display: none; }
  .repeat-group:has(input[name="regular"][value="yes"]:checked) .repeat-cadence { display: flex; }
}

/* ---------------------------------------------------------------------------
   The offer popup
   ---------------------------------------------------------------------------
   Neil's ask, 12 September. One dialog, over the whole page, carrying the
   first-order offer and the same phone box the hero has.

   IT IS display:none UNTIL A SCRIPT OPENS IT, and there is no CSS-only way in.
   That is the opposite of the .js-anim rule above and deliberately so: a reveal
   that fails safe means showing the content, and a popup that fails safe means
   never appearing. A browser with scripting off gets the page it came for, and
   the hero form is the same form.

   The HTML `hidden` attribute is not used for this. A class that sets `display`
   beats it, which is the bug already recorded for .btn - so the open state is a
   class and there is nothing for an attribute to lose to.

   The card sits on an ink scrim, so its shadow is the coloured offset rather
   than an ink one: an ink shadow on ink is invisible. Same reasoning as
   .card-on-ink. */
.ly-pop {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 200;
  padding: 20px;
  overflow-y: auto;
  background: rgba(16, 18, 16, 0.55);
  align-items: center;
  justify-content: center;
}

.ly-pop.is-open { display: flex; }

/* Nothing behind the dialog scrolls while it is open. Removed on close, so a
   visitor who dismisses it never inherits a page that will not move. */
body.ly-pop-locked { overflow: hidden; }

.ly-pop-sheet {
  position: relative;
  width: 100%;
  max-width: 460px;
  margin: auto;
  padding: 30px 28px 26px;
  background: var(--paper-050);
  border: var(--border-w-2) solid var(--ink-900);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-pop-brand);
}

.ly-pop-sheet:focus { outline: none; }
.ly-pop-sheet:focus-visible { box-shadow: var(--focus-ring); }

/* 44px, the floor for anything a thumb has to hit, and in the corner nobody
   has to hunt for. The glyph is type rather than an icon: the icon set has no
   close mark, and adding one to icons.css for a single multiplication sign is
   a worse trade than setting the character. */
.ly-pop-close {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 26px;
  line-height: 1;
  color: var(--ink-700);
  background: none;
  border: none;
  border-radius: var(--radius-circle);
  cursor: pointer;
}

.ly-pop-close:hover { color: var(--ink-900); background: var(--paper-200); }

.ly-pop-title {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 30px;
  line-height: 1.08;
  text-wrap: balance;
  margin: 0 0 10px;
}

.ly-pop-lede {
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink-700);
  margin: 0 0 20px;
}

.ly-pop-terms {
  margin: 16px 0 0;
  padding: 0;
  list-style: none;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-500);
}

.ly-pop-terms li + li { margin-top: 3px; }

/* The number and the button stack on a narrow screen rather than squeezing a
   ten-digit field down beside a button. */
.ly-pop-row { display: flex; flex-direction: column; gap: 10px; }

@media (min-width: 420px) {
  .ly-pop-row { flex-direction: row; }
  .ly-pop-row .ly-pop-field { flex: 1; min-width: 0; }
}

/* Anyone whose device asks for less motion gets none, the same as every other
   animation on this site. */
@media (prefers-reduced-motion: no-preference) {
  .ly-pop.is-open .ly-pop-sheet { animation: ly-pop-in 220ms ease-out both; }
}

@keyframes ly-pop-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
