/* The public site — the landing page + legal pages served by Caddy at the root
   (GO_LIVE.md C9). It shares the app's design tokens (services/frontend/web/src/styles.css)
   so the marketing surface and the product read as one brand. Static, no JS required; the app
   itself lives under /app.

   **The token block below must match the app's, value for value.** It has drifted twice: once
   when the app was recoloured on 6 Sep and this file was not, and again the next day when the
   app's paper went a step deeper and this file kept the lighter one — so the page you arrive on
   was a different cream from the product it opens. Nothing enforces it but this note and a
   check in scripts/check_docs.py; when you change a colour in styles.css, change it here.

   **Daylight is the default here too** (owner, 8 Sep 2026). The app dropped "follow the machine"
   the same day, and this page has no switch at all — so keying it off the machine's dark setting
   was the one way left for a visitor to meet the product in a palette nobody chose for it. */

:root {
  --paper: #f1ebe1;
  --raise: #fffdfa;
  --ink: #2a211c;
  --stone: #726558;
  --faint: #7d7062;
  --line: #e6ddd1;
  --claret: #7d2340;
  --claret-ink: #5d1a2f;
  --claret-tint: #ece0e3;
  --tan: #b87f4e;
  --caution: #9a663a;
  --on-accent: #ffffff;
  --p-maya: #a24f6a;
  --p-jules: #4c6b7d;
  --p-victor: #6b6046;
  --p-iris: #7d4a72;
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
  --sans: "Avenir Next", "Segoe UI", system-ui, -apple-system, Roboto, "Helvetica Neue", sans-serif;
  /* The app's motion tokens, value for value from styles.css. The panel below plays the same
     choreography the product does, so it has to use the same numbers — a landing page that
     moves differently from the product it opens reads as a different product. */
  --spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --snap: cubic-bezier(0.2, 0.9, 0.3, 1.3);
  --dur-press: 140ms;
  --dur-enter: 620ms;
  --dur-voice: 480ms;
  /* The opener's film. One duration for every moving part in it, so nothing can
     drift out of step with anything else. */
  --film-loop: 13000ms;
}
/* No `prefers-color-scheme` default: this page opens in daylight for everyone, the way the app
   does since 8 Sep. **The switch promised here arrived on 10 Sep 2026** — one button in the bar,
   and the choice it makes is the app's own remembered choice, so the two agree. */
:root[data-theme="dark"] {
  --paper: #1a1512; --raise: #241d18; --ink: #f2ebe1; --stone: #ab9d8e; --faint: #8a7d70;
  --line: #362d25; --claret: #d4788c; --claret-ink: #e2919f; --claret-tint: #2e1e22;
  --tan: #c99a68; --caution: #d4a06a; --on-accent: #1a1512;
  --p-maya: #d98da0; --p-jules: #93b2c4; --p-victor: #bcae86; --p-iris: #c58fc0;
}
:root[data-theme="light"] {
  --paper: #f1ebe1; --raise: #fffdfa; --ink: #2a211c; --stone: #726558; --faint: #7d7062;
  --line: #e6ddd1; --claret: #7d2340; --claret-ink: #5d1a2f; --claret-tint: #ece0e3;
  --tan: #b87f4e; --caution: #9a663a; --on-accent: #ffffff;
  --p-maya: #a24f6a; --p-jules: #4c6b7d; --p-victor: #6b6046; --p-iris: #7d4a72;
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
/* Somebody who has asked their machine not to animate things must not be thrown down the page
   when they click a link in the list on the left. */
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body {
  margin: 0;
  /* **Flat paper, the same flat paper as the app** (owner, 8 Sep 2026: "the landing page has
     still the old palette"). It was right about the colours and wrong on the screen: this line
     used to wash 5% claret across the top of the page, which turns #f1ebe1 into about #ebe1d9 —
     a pinker, darker cream than the product it links to, exactly where a visitor lands. The
     sixteen shared tokens matched the whole time, and the check in scripts/check_docs.py
     compares tokens, so nothing caught it. A gradient is not a token. */
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
a { color: var(--claret-ink); }
img { max-width: 100%; }
/* **The page uses the whole window** (owner, 8 Sep 2026: "I feel that the page doesn't use the
   whole screen to be displayed. This is wrong."). It was capped at 960px and centred, which on
   the 1920-wide screen he judges it on left roughly half the width as empty cream down the two
   sides — and stranded the list of sections in the middle of the page rather than at the left of
   the screen, which is where the eye looks for it.

   The cap does not disappear, it moves: the *frame* is now the window, and the maximums live on
   the things that need them — a paragraph, a heading, the staged preview. A sentence running the
   full width of a 1920 monitor is about 200 characters, and people stop reading those. */
.wrap { width: 100%; margin: 0 auto; padding: 0 clamp(22px, 3vw, 60px); }

/* ---- top bar ----------------------------------------------------------------------------
   **Frozen** (owner, 8 Sep 2026): it keeps its place at the top of the window all the way down
   the page. It is a full-bleed element with the page's own `.wrap` *inside* it rather than the
   other way round — the background has to reach the window's edges, and inside the wrap it would
   stop 60px short of them on a wide screen.

   Solid paper, not the old fade to transparent: a bar that is meant to be frozen has to look
   opaque, and content half-visible through it reads as a rendering fault.

   "How it works" used to sit here next to "Open the app". It is in the rail now — the same link
   in two places is a choice a visitor has to make for no reason. */
.bar {
  position: sticky; top: 0; z-index: 30;
  background: var(--paper);
}
.bar-in {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding-block: 16px 14px;
}
.mark {
  font-family: var(--serif); font-size: 1.5rem; letter-spacing: 0.01em; font-weight: 600;
  text-decoration: none; color: var(--ink);
}
.mark .dot { color: var(--claret); }
.bar-right { display: flex; align-items: center; gap: 10px; }


/* ---- how big the middle of the page is --------------------------------------------------
   **Everything between the top bar and the last line came down about 15% on 10 Sep 2026**
   (owner: "everything is too big in the main screen ... not the navigation, not the top and
   bottom lines"). So the bar, the list of sections down the left and the footer are untouched
   on purpose, and every number below that changed is the old one times 0.85.

   The one deliberate exception is the small print. Body copy and captions come down about 8%
   rather than 15%, because 15% off 0.76rem is 10px and nobody reads 10px. */

/* ---- buttons ---- */
.btn {
  font: inherit; font-size: 0.9rem; font-weight: 600; cursor: pointer;
  border-radius: 999px; padding: 10px 20px; text-decoration: none;
  /* A button says its words on one line or it is not a button. Without this the bar's pair
     break across two lines on a 360px phone and the bar grows to 87px to hold them. */
  white-space: nowrap;
  display: inline-flex; align-items: center; gap: 8px;
  border: 1px solid var(--claret); background: var(--claret); color: var(--on-accent);
  transition: background .2s, border-color .2s, color .2s, transform .2s;
}
.btn:hover { background: var(--claret-ink); border-color: var(--claret-ink); transform: translateY(-1px); }
.btn:focus-visible { outline: 2px solid var(--claret); outline-offset: 2px; }
.btn.ghost {
  background: transparent; color: var(--stone); border-color: var(--line);
}
.btn.ghost:hover { color: var(--ink); border-color: var(--stone); background: transparent; }
.btn .soon {
  font-size: 0.58rem; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 700;
  color: var(--caution); background: rgb(from var(--caution) r g b / 0.14);
  padding: 2px 6px; border-radius: 5px;
}
.bar .btn { padding: 8px 15px; font-size: 0.82rem; }
/* Two ways in and a brand, on a phone. They give up padding rather than the brand giving up
   its place at the left, and the pair still fits at 320px, the narrowest screen worth having. */
@media (max-width: 430px) {
  .bar-in { gap: 8px; }
  .bar-right { gap: 6px; }
  .bar .btn { padding: 7px 11px; font-size: 0.76rem; }
}

/* ---- day or dark ------------------------------------------------------------------------
   A round icon button, sized and coloured to match the two pickers beside it so the three read
   as one group of preferences. **Which icon shows is decided by `data-theme` alone**, which the
   head script sets before the first paint — so the button never shows the wrong one, not even
   for a frame, and it is still right with the page's own script blocked. */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; flex: none; padding: 0; cursor: pointer;
  border: 1px solid var(--line); border-radius: 999px;
  background: transparent; color: var(--stone);
  transition: border-color .2s, color .2s;
}
.theme-toggle:hover { border-color: var(--stone); color: var(--ink); }
.theme-toggle:focus-visible { outline: 2px solid var(--claret); outline-offset: 2px; }
.theme-toggle svg { width: 17px; height: 17px; }
.i-moon { fill: currentColor; }
.i-sun { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; }
/* The moon in daylight, the sun after dark: the button shows where it takes you. */
.i-sun { display: none; }
:root[data-theme="dark"] .i-moon { display: none; }
:root[data-theme="dark"] .i-sun { display: inline; }
/* Said to a screen reader, and to nobody else. The button's own `aria-label` says what it does
   once the script has run; this is the sentence that is there before it, and if it never does. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* ---- the country and language pickers in the bar ----------------------------------------
   Display, not control: one row of each is live and the rest say "soon". They sit before the
   two buttons and leave the bar below 640px, where the brand and the two buttons already use
   the whole line. */
.pick { position: relative; }
.pick > summary {
  list-style: none; cursor: pointer; user-select: none;
  display: inline-flex; align-items: center; gap: 6px; line-height: 1;
  padding: 7px 10px; border: 1px solid var(--line); border-radius: 999px;
  color: var(--stone); transition: border-color .2s, color .2s;
}
/* Safari draws its own triangle from a pseudo-element `list-style` does not reach. */
.pick > summary::-webkit-details-marker { display: none; }
.pick > summary:hover, .pick[open] > summary { border-color: var(--stone); color: var(--ink); }
.pick > summary:focus-visible { outline: 2px solid var(--claret); outline-offset: 2px; }
/* **Both say what they are in full** (owner, 10 Sep 2026: "it's not clear what is selected
   there"). A flag and "CH" beside a flag and "EN" tells you two things are chosen and
   nothing about which is a country and which is a language. The codes are kept for the
   window widths where two full words and two buttons will not share a line. */
.pick-name { font-size: 0.82rem; font-weight: 600; }
.pick-code { display: none; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; }
/* The chevron is what says these open at all. A bordered pill with a word in it otherwise
   reads as one more button beside the two real ones. */
.pick-caret {
  width: 9px; height: 6px; flex: none; fill: none;
  stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
  opacity: 0.7; transition: transform .18s;
}
.pick[open] .pick-caret { transform: rotate(180deg); }
/* The list says which question it is answering, so an open menu is unambiguous even to
   somebody who cannot tell the flags apart. */
.pick-head {
  display: block; padding: 4px 9px 7px;
  font-size: 0.6rem; letter-spacing: 0.14em; text-transform: uppercase; font-weight: 700;
  color: var(--faint);
}
@media (max-width: 900px) {
  .pick-name { display: none; }
  .pick-code { display: inline; }
}
/* A hairline round each flag, so a white edge (Switzerland, France, Italy) still reads as
   a flag against the cream rather than dissolving into it. */
.flag {
  width: 20px; height: 14px; flex: none; border-radius: 2px;
  box-shadow: 0 0 0 1px rgb(from var(--ink) r g b / 0.16);
}
.pick-menu {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
  min-width: 178px; padding: 6px;
  background: var(--raise); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 18px 40px -28px rgb(from var(--ink) r g b / 0.6);
}
.pick-row {
  display: flex; align-items: center; gap: 8px; white-space: nowrap;
  padding: 7px 9px; border-radius: 8px; font-size: 0.84rem; color: var(--faint);
}
.pick-row.on { color: var(--ink); font-weight: 600; background: var(--claret-tint); }
.pick-row i {
  margin-left: auto; font-style: normal; font-size: 0.6rem; letter-spacing: 0.1em;
  text-transform: uppercase; font-weight: 700; color: var(--caution);
  background: rgb(from var(--caution) r g b / 0.14); padding: 2px 6px; border-radius: 5px;
}
@media (max-width: 640px) { .pick { display: none; } }

/* ---- hero ---- */
/* The preview is a *card*, so it takes a fixed maximum and the words take everything left over —
   `1fr` on both sides would blow it up to 830px on a wide screen, where a staged phone-sized
   conversation reads as a billboard. The gap grows with the window instead, which is what turns
   the extra room into air rather than into stretch. */
.hero { padding: 34px 0 17px; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); gap: clamp(40px, 3.5vw, 72px); align-items: center;
  /* The one place on the page with a maximum of its own. Two columns that keep growing put a
     hole in the middle of the hero — the headline stops at its readable width, the card stops at
     its own, and the window's extra room ends up as a gap between them rather than as anything a
     visitor gets. The cap keeps the pair a pair. It is *not* centred: left-aligned, the hero
     starts on the same line as every section below it, and one left edge down the page is worth
     more than a symmetrically placed hero. It does not bind at all until about 1830px. */
  max-width: 1600px; }
/* A grid item's default `min-width: auto` is its *content's* minimum, so one stubborn element
   inside the preview can push the whole column wider than the window rather than being
   squeezed. Zeroing it makes the track the authority. */
.hero > * { min-width: 0; }
/* The opener lost its card on 9 Sep 2026 — the panel below now serves both customers itself,
   so a card up here showed the same dress twice on one page. One column, and the headline and
   the sentence keep their own measures: what goes is the empty right-hand side, not the left
   edge the whole page is built on. */
.hero.solo { grid-template-columns: minmax(0, 1fr); }
/* And two columns again from 9 Sep 2026, with the film in the wider one. The words keep their
   own measure — a headline that reads well is worth more than a matched pair of halves — so the
   split is deliberately uneven rather than 1fr each.

   `.solo` above is what the markup ships; the script swaps this in once the film is built, so a
   visitor with JavaScript blocked gets one column of words rather than a blank half. */
.hero.opener { grid-template-columns: minmax(0, 0.76fr) minmax(0, 1.38fr); }
.hero.solo > .film { display: none; }
/* The second example, laid out the other way round so the pair reads as a pair rather
   than as the same block twice. The column ratio flips with it, or the preview would be
   the narrow one. */
.hero.flip { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); }
@media (max-width: 820px) {
  .hero { grid-template-columns: 1fr; gap: 30px; padding-top: 26px; }
  .hero.opener, .hero.flip { grid-template-columns: 1fr; }
  /* Stacked, the words have to come first: a preview with nothing above it reads as a
     stray screenshot. `order` moves it without moving it in the markup, where the
     preview being first is what mirrors the two sections on a wide screen. */
  .hero.flip > .preview { order: 2; }
}
/* **The opening block came down another 20% on the owner's second look** (10 Sep 2026),
   on top of the 15% the whole middle of the page took earlier the same day. `.lede` is
   the opener's headline only; the second section's heading has its own size below. */
.lede {
  font-family: var(--serif); font-weight: 500;
  font-size: clamp(1.36rem, 0.68rem + 2.45vw, 3.4rem);
  line-height: 1.08; letter-spacing: -0.015em; text-wrap: balance;
  margin: 0 0 16px; max-width: 16ch;
}
.lede em { font-style: italic; color: var(--claret); }
/* The second section's heading. A section heading rather than the page's one big
   statement, so it is set a size down, but it is still the lede face because it does the
   same job. */
.lede.small { font-size: clamp(1.36rem, 0.9rem + 1.95vw, 2.65rem); max-width: 17ch; }
.sub { color: var(--stone); max-width: 50ch; margin: 0 0 22px; font-size: clamp(0.95rem, 0.86rem + 0.27vw, 1.13rem); }
.cta-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.cta-note { color: var(--faint); font-size: 0.8rem; margin: 16px 0 0; }
/* The rest of the opening block, down the same 20%. Scoped, because the sentence class
   and the small print are shared with the second section, which was not asked to move. */
.hero.solo .sub, .hero.opener .sub { font-size: clamp(0.76rem, 0.69rem + 0.22vw, 0.9rem); }
.hero.solo .cta-note, .hero.opener .cta-note { font-size: 0.68rem; }

/* ---- the staged app preview ---- */
/* **Everything inside this panel is sized in `em`, and this line is the em.** On a wide screen
   the panel is nearly 800px, and a card whose type stayed at 14px would read as a small card
   stretched — the thing that makes a page look unfinished. Scaling the base font instead makes
   it a *bigger card*: the same proportions, larger. The floor is today's size, so nothing below
   about 1300px changes at all. */
.preview {
  background: var(--raise); border: 1px solid var(--line); border-radius: 22px;
  padding: 1.15em; box-shadow: 0 30px 60px -40px rgb(from var(--claret) r g b / 0.4);
  font-size: clamp(0.85rem, 0.58rem + 0.34vw, 1.01rem);
}
.pv-head {
  display: flex; align-items: center; gap: 8px; padding: 2px 4px 14px;
  border-bottom: 1px solid var(--line); margin-bottom: 14px;
}
.pv-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--line); }
/* The page's own mark, sized for the app's chrome rather than for the page header — at header
   size it dominates a card that is meant to look like the product. `em`, because the whole
   preview scales with its own font size and the brand has to scale with it. */
.pv-mark {
  margin-left: auto; font-family: var(--serif); font-weight: 600; font-size: 0.95em;
  letter-spacing: 0.01em; color: var(--ink); line-height: 1;
}
.pv-mark .dot { color: var(--claret); }
.pv-msg { display: flex; gap: 10px; margin-bottom: 14px; }
.pv-msg.you { flex-direction: row-reverse; }
.pv-ava {
  flex: none; width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center; font-family: var(--serif); font-weight: 600;
  font-size: 0.78em; background: var(--claret); color: var(--on-accent);
}
.pv-msg.you .pv-ava { background: transparent; color: var(--stone); border: 1px solid var(--line); }
.pv-bubble { font-size: 0.9em; max-width: 80%; }
/* The shopper's side carries a product card, and 80% of a narrow preview leaves it too thin to
   read — the value-for-money label alone wrapped onto three lines. The customer's own bubble
   keeps its 80%, because a short line of speech running the full width does not read as speech. */
.pv-msg:not(.you) .pv-bubble { max-width: 100%; }
.pv-msg.you .pv-bubble {
  background: var(--claret-tint); border: 1px solid var(--line);
  padding: 9px 12px; border-radius: 14px 14px 3px 14px;
}
.pv-who { font-size: 0.64em; letter-spacing: 0.09em; text-transform: uppercase; color: var(--stone); margin-bottom: 3px; }

/* **A real garment, photographed** (owner, 8 Sep 2026). This was a drawn shirt outline, which
   showed less than a real card does in the one place a visitor sees the product before signing
   up. The photograph is a file of ours rather than a link to the shop's, so the page does not
   break the day a retailer changes its image addresses.

   Wider than it was: a 96px column was enough for a symbol and is not enough for a person in a
   dress. `aspect-ratio` with `object-fit: cover` crops rather than squashes, the same bargain
   the app's own card makes. */
.pv-card { display: grid; grid-template-columns: clamp(132px, 24%, 200px) 1fr; border: 1px solid var(--line); border-radius: 14px; overflow: hidden; background: var(--paper); margin-top: 10px; }
.pv-media { position: relative; border-right: 1px solid var(--line); background: var(--paper); }
/* Absolutely positioned, not `height: 100%`. The media column stretches to whatever height the
   facts beside it come to, but that height is not *definite* — so a percentage height on the
   image resolved to zero and the photograph did not appear at all. Filling the box from its
   edges gives the same result and does not ask the browser a question it cannot answer. This is
   what the app's own card does, for the same reason. */
.pv-media img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.pv-body { padding: 11px 13px; }
.pv-vendor { font-size: 0.62em; letter-spacing: 0.13em; text-transform: uppercase; color: var(--stone); }
.pv-title { font-family: var(--serif); font-size: 0.98em; font-weight: 600; margin: 2px 0 5px; line-height: 1.2; }
.pv-price { font-weight: 600; font-variant-numeric: tabular-nums; }
.pv-reason { font-size: 0.8em; color: var(--ink); margin: 6px 0 0; }
.pv-reason b { color: var(--claret-ink); }
/* What it is made of, in the same fixed place the app's card gives it. */
.pv-fabric { font-size: 0.74em; color: var(--stone); margin: 7px 0 0; }
/* The value meter's label row, so the preview says what the meter means and how much we knew —
   the app's card says both, and a bare row of bars says neither. */
/* `flex-wrap` is load-bearing, not tidiness. Both labels are `nowrap`, so on one line they set
   a minimum width for the whole card — measured at 390px, "VALUE FOR MONEY" and "HIGH
   CONFIDENCE" side by side gave the card a 227px floor, which pushed the preview 71px past the
   window and made the entire page scroll sideways on a phone. Allowed to wrap, the floor is
   whichever label is wider. */
.pv-vfm {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 2px 8px; margin-top: 9px; flex-wrap: wrap;
}
.pv-vfm-label {
  font-size: 0.6em; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--stone); font-weight: 600; white-space: nowrap;
}
.pv-vfm-conf {
  font-size: 0.58em; letter-spacing: 0.06em; text-transform: uppercase; font-weight: 600;
  color: var(--stone); white-space: nowrap;
}
/* A thin answer has to look thin. ZARA publish no rating, so that card's meter rests on price
   alone, and the word says so in the same caution colour the app's own card uses. */
.pv-vfm-conf.low { color: var(--caution); }
.pv-segs { display: flex; gap: 3px; max-width: 8em; margin-top: 5px; }
.pv-seg { height: 6px; flex: 1; border-radius: 3px; background: var(--line); }
.pv-seg.on { background: var(--tan); }

/* ---- the panel plays the turn -------------------------------------------------------------
   Added 9 Sep 2026. The choreography is the app's own (styles.css, decision AT): the same
   spring, the same card entrance, the same press under a control. Every delay below is measured
   from the moment the sent message lands in the thread — the typing and the button press happen
   before that and are timed in the script, because their length depends on the question.

   `--card-at` is set per turn by the script: the garment cannot arrive until the last word of
   the sentence above it has landed, and the two sentences are not the same length. */

/* All three turns share one grid cell, so the thread is as tall as the tallest of them and the
   panel never changes height when they swap — and the swap happens under the claret anyway. */
/* `overflow: hidden` is here for the claret, which sweeps in from outside the panel. The
   padding is here for the card, which springs up from 46px below its place: the clip cuts at
   the padding edge, so without it the taller turn's card was sliced off along the bottom and
   the shorter turn's was not — one entrance behaving two different ways. The negative margin
   gives the room back to the layout, so the card rises from behind the message box instead.
   The z-index pair is what keeps the box in front of it, the claret included. */
.pv-thread {
  position: relative; overflow: hidden; display: grid; z-index: 0;
  padding-bottom: 46px; margin-bottom: -46px;
}
.pv-composer { position: relative; z-index: 1; }
.turn { grid-area: 1 / 1; visibility: hidden; }
.turn.on { visibility: visible; }
.pv-thread .pv-msg:last-child { margin-bottom: 4px; }

/* The voice changes and the surface says so, in the persona hues the app already uses. */
.turn.maya .pv-msg:not(.you) .pv-ava { background: var(--p-maya); }
.turn.jules .pv-msg:not(.you) .pv-ava { background: var(--p-jules); }
.turn.victor .pv-msg:not(.you) .pv-ava { background: var(--p-victor); }

/* **The photograph the customer sent**, in their own bubble above their words. Height rather
   than width, and in `em` so it scales with the panel: at the bubble's full width a portrait
   photo came to 347px and owned the panel. At 7em it is about five lines tall, which reads as
   "I attached this" and leaves the answer the room.

   `block` with `margin-left: auto` pushes it to the bubble's right edge, where the bubble's own
   square corner is, so the photo and the text share an edge instead of drifting apart.

   `object-fit: cover` is not decoration here. The height is fixed and the width follows the
   photo's own shape, so a landscape photo would run past `max-width: 100%` and be squashed
   flat; cover crops it instead. It matters because these two files are meant to be swapped for
   whatever the owner photographs, at whatever shape his camera gives. */
.pv-shot {
  display: block; height: 7em; width: auto; max-width: 100%;
  margin: 0 0 7px auto; border-radius: 9px; border: 1px solid var(--line);
  object-fit: cover;
}

.pv-say { position: relative; display: block; }
/* One word of the shopper's sentence. The script cuts the sentence up and numbers each piece. */
.pv-say .w { display: inline-block; }
/* The thinking dots sit on top of where the first word will be, so nothing shifts when they go. */
.pv-think { position: absolute; left: 0; top: 0.35em; display: flex; gap: 5px; opacity: 0; }
.pv-think i { width: 5px; height: 5px; border-radius: 50%; background: var(--stone); display: block; }

/* The message box, copied from the app's own dock, so the panel ends where the product begins. */
.pv-composer {
  display: flex; align-items: flex-end; gap: 8px; margin-top: 4px;
  background: var(--raise); border: 1px solid var(--line); border-radius: 16px; padding: 8px;
}
/* Two lines tall whatever it holds: no question fits on one line at every width, and a box that
   grows mid-sentence drags the whole panel with it.

   **2.9em was one line, not two, and had been since this was written.** The box is
   `border-box`, so 2.9em is the whole thing including 16px of padding: about 21px of room for
   an 18.5px line. Nothing caught it because the first two questions happen to fit on one line
   on a wide screen. The photograph in the last two turns is what made it show, and 4.2em is
   the honest figure: two lines plus the padding. */
.pv-field {
  flex: 1; min-width: 0; padding: 8px 6px; font-size: 0.9em; line-height: 1.45;
  min-height: 4.2em; color: var(--ink);
}
.pv-ph { color: var(--faint); }
.pv-field:not(.empty) .pv-ph { display: none; }
/* The attached photograph, sitting in the message box while the question is typed. Floated, so
   the words wrap round it the way they do beside any thumbnail, and sized to fit inside the two
   lines the box already stands at: the box must not grow when a photo is attached, or the panel
   below it moves twice every loop. */
.pv-clip { display: none; }
.pv-field.has-clip .pv-clip {
  display: block; float: left; width: 2.4em; height: 2.4em; object-fit: cover;
  margin: 0 8px 0 0; border-radius: 7px; border: 1px solid var(--line);
}
.pv-caret { display: none; width: 2px; height: 1.05em; background: var(--claret); vertical-align: text-bottom; margin-left: 2px; }
.pv-field.typing .pv-caret { display: inline-block; animation: pv-caret 1s steps(2, start) infinite; }
/* Not a real control: it is part of a picture of the product, so it takes no focus and answers
   no click. The script plays its press at the moment the message is sent. */
.pv-send {
  font: inherit; font-size: 0.86em; font-weight: 600; color: var(--on-accent);
  background: var(--claret); border: none; border-radius: 11px; padding: 10px 16px;
  cursor: default; flex: none;
  transition: transform var(--dur-press) var(--snap), box-shadow var(--dur-press) var(--snap);
  box-shadow: 0 3px 0 -1px rgb(from var(--claret-ink) r g b / 0.35);
}
.pv-send.pressed { transform: translateY(2px); box-shadow: 0 1px 0 -1px rgb(from var(--claret-ink) r g b / 0.35); }

@keyframes pv-caret { to { visibility: hidden; } }
@keyframes pv-you { from { transform: translateY(7px); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes pv-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes pv-word { from { transform: translateY(4px); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes pv-thinking { 0%, 100% { opacity: 0; } 8%, 92% { opacity: 1; } }
@keyframes pv-dot { 0%, 60%, 100% { transform: none; } 30% { transform: translateY(-4px); } }
@keyframes pv-voice { from { transform: scale(0.94) rotate(-6deg); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes pv-card { from { transform: translateY(46px) rotate(-1deg) scale(0.94); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes pv-seg { from { background: var(--line); } to { background: var(--tan); } }
@keyframes pv-wipe { from { transform: translateX(-108%); } to { transform: translateX(108%); } }

/* Armed: on screen, waiting to play. Everything the sequence brings in starts hidden — and
   because it is hidden rather than absent, the panel is already its full height. */
.turn.armed .pv-msg.you,
.turn.armed .pv-msg:not(.you) .pv-ava,
.turn.armed .pv-who,
.turn.armed .pv-say .w,
.turn.armed .pv-card { opacity: 0; }

.turn.playing .pv-msg.you { animation: pv-you 300ms ease-out both; }
.turn.playing .pv-msg:not(.you) .pv-ava { animation: pv-voice var(--dur-voice) var(--spring) 520ms both; }
.turn.playing .pv-who { animation: pv-fade 220ms ease-out 520ms both; }
.turn.playing .pv-think { animation: pv-thinking 900ms linear 640ms both; }
.turn.playing .pv-think i { animation: pv-dot 900ms ease-in-out 640ms 1; }
.turn.playing .pv-think i:nth-child(2) { animation-delay: 780ms; }
.turn.playing .pv-think i:nth-child(3) { animation-delay: 920ms; }
.turn.playing .pv-say .w { animation: pv-word 260ms ease-out both; animation-delay: calc(1540ms + var(--i) * 52ms); }
.turn.playing .pv-card { animation: pv-card var(--dur-enter) var(--spring) var(--card-at) both; }
/* The words about a garment settle 130ms before its photograph arrives. Type leads image, and
   it is a rule rather than a flourish: it holds everywhere the two arrive together. */
.turn.playing .pv-media img { animation: pv-fade 420ms ease-out calc(var(--card-at) + 130ms) both; }
.turn.playing .pv-seg.on { animation: pv-seg 200ms ease-out calc(var(--card-at) + 720ms) both; }
.turn.playing .pv-seg.on:nth-child(2) { animation-delay: calc(var(--card-at) + 810ms); }
.turn.playing .pv-seg.on:nth-child(3) { animation-delay: calc(var(--card-at) + 900ms); }
.turn.playing .pv-seg.on:nth-child(4) { animation-delay: calc(var(--card-at) + 990ms); }

/* The claret carries the change of customer, the way it carries any change of content on this
   page: the swap happens underneath it, so the thread is never seen half-changed. */
.pv-wipe {
  position: absolute; inset: 0; z-index: 3; pointer-events: none; transform: translateX(-108%);
  background: linear-gradient(
    100deg,
    rgb(from var(--claret) r g b / 0) 0%,
    var(--claret) 16%,
    var(--claret) 84%,
    rgb(from var(--claret) r g b / 0) 100%
  );
}
.pv-thread.wiping .pv-wipe { animation: pv-wipe 900ms cubic-bezier(0.66, 0, 0.34, 1) both; }

/* ---- the opener's film --------------------------------------------------------------------
   The first screen's picture: a shelf of real clothes arrives, two squares light up and fly out
   of it — one hers, one his — each is read whole with four notes drawn onto it, and the two then
   travel on into a split shortlist with prices, a value bar each and a line from the shopper.
   Thirteen seconds, then it starts again.

   **Everything is scoped under `.film`**, and that is not tidiness. Three of these class names
   already mean something else on this page — `.rule` is the line between sections, `.dot` is the
   full stop in the brand, `.w` is a word in the conversation below — and unscoped they would
   reach out and restyle all three.

   **It is one clock.** Every moving part shares `--film-loop` and runs forever, and every
   keyframe below is a percentage of that one loop, so nothing can drift out of step with
   anything else. There is no timeline in JavaScript. The script's whole job is arithmetic: it
   works out where each shelf square sits and hands each travelling garment the exact journey
   from its own square to the reading bench and on to its half of the shortlist, as custom
   properties in `cqw`. Because every distance is a fraction of the panel's own width, the
   travel lands on the square it came from at any size. */
.film {
  position: relative; width: 100%; container-type: inline-size;
  /* The large photographs are 340px wide. Past about 760px the film would be drawing them
     bigger than they are, and on a 1600px hero it would be a billboard rather than a picture —
     the same reason the conversation panel below has a cap of its own. It binds above roughly a
     1370px window and does nothing at all below that. */
  max-width: 645px; margin-inline: auto;
}
/* **No border** (owner, 10 Sep 2026). The shelf is a picture, and a hairline box round it
   drew a frame the page does not need: everything else on this page that carries a border
   is a card you could click. */
.film .frame {
  position: relative; aspect-ratio: 43 / 32; border-radius: 14px; overflow: hidden;
  background: var(--paper);
}

/* ---- the shelf ---- */
.film .field {
  position: absolute; inset: 0; display: grid; grid-template-columns: repeat(8, 1fr);
  gap: 2.4%; align-content: center; padding: 3%; opacity: 0;
  animation: film-grid var(--film-loop) linear infinite;
}
/* Nothing happens across the shelf as a whole — no wash, and no square-by-square landing. It
   arrives, and the only thing that moves in it is the ring on the two squares about to leave.
   The squares are the same shape as the garments that will be lifted out of them, so the travel
   is one photograph growing rather than one crop becoming another. */
.film .tile {
  position: relative; aspect-ratio: 2 / 3; border-radius: 3px; overflow: hidden;
  background: var(--line);
}
.film .tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.film .tile::after {
  content: ""; position: absolute; inset: -3px; border: 2px solid var(--claret);
  border-radius: 5px; opacity: 0; pointer-events: none;
}
.film .tile.picked::after { animation: film-ring var(--film-loop) linear infinite; }

/* ---- the four garments ---- */
.film .picks { position: absolute; inset: 0; z-index: 2; }
.film .pick { position: absolute; top: 0; bottom: 0; left: var(--l); width: var(--w); opacity: 0; }
.film .shot {
  position: absolute; left: 0; right: 0; top: 50%; translate: 0 -50%;
  aspect-ratio: 2 / 3; overflow: hidden; background: var(--raise);
  /* A shadow, not a colour: `--ink` rather than the claret every other shadow on this page
     uses, because these fall directly under a photograph and the claret read as a pink glow
     around each garment rather than as the photograph standing off the paper. */
  box-shadow: 0 16px 30px -22px rgb(from var(--ink) r g b / 0.7);
}
.film .shot img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
/* The two being read travel: from their own square in the shelf, out to the bench, then into
   their half of the shortlist. One photograph the whole way — it grows, and nothing is ever
   cropped or zoomed, so what you are shown is what was on the shelf. */
.film .pick.lead { animation: film-lead var(--film-loop) linear infinite; }
.film .pick.mate { animation: film-mate var(--film-loop) linear infinite; }

/* Every size in here is `cqw` with a floor and no ceiling: the film's own width is capped above,
   so the drawing scales as one piece and the floor is the only thing that has to be said. The
   floor is what keeps the smallest labels legible on a phone. */
.film .meta {
  position: absolute; left: 0; right: 0; top: calc(50% + 15cqw); opacity: 0;
  animation: film-meta var(--film-loop) linear infinite;
}
.film .meta .rule { height: 1px; background: var(--ink); opacity: 0.5; }
.film .meta .row { display: flex; justify-content: space-between; align-items: baseline; gap: 1cqw; margin-top: 1cqw; }
.film .meta .vendor {
  font-size: max(0.36rem, 1.4cqw); letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--stone);
}
.film .meta .price { font-family: var(--serif); font-size: max(0.52rem, 2cqw); color: var(--ink); }
/* Always two lines tall, whether the name needs them or not — otherwise a short name pulls its
   value bar up and the four bars no longer read as one row. */
.film .meta .name {
  font-family: var(--serif); font-size: max(0.5rem, 1.9cqw); line-height: 1.24;
  margin-top: 0.4cqw; color: var(--ink); min-height: 2.48em;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.film .vfm { margin-top: 1.3cqw; }
.film .vfm .k {
  font-size: max(0.32rem, 1.25cqw); letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--claret); font-weight: 650;
}
.film .vfm .k i { font-style: normal; color: var(--stone); }
.film .vfm .track { height: 3px; background: var(--line); margin-top: 0.6cqw; }
.film .vfm .track i {
  display: block; height: 100%; background: var(--tan); transform: scaleX(0); transform-origin: left;
  animation: film-bar var(--film-loop) linear infinite;
}
/* On a phone the panel is around 340px, which leaves each garment about 65px. The shop's name
   and the price will not share a line that narrow, and the wrap was pushing the value bar out
   through the bottom of the panel. The price wins the row. */
@container (max-width: 480px) {
  .film .meta .vendor { display: none; }
  .film .meta .row { justify-content: flex-start; }
}

/* ---- the reading ----
   The notes live in a layer of their own, at the exact place the two garments stand while they
   are being read. **They are never inside anything that is being scaled** — scaling type
   rasterises it at its small natural size, and this is why the readings are crisp. */
.film .annolayer { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
.film .agroup {
  position: absolute; top: var(--gt); height: var(--gh); left: var(--gl); width: var(--gw);
  overflow: hidden; animation: film-annos var(--film-loop) linear infinite;
}
.film .anno { position: absolute; top: var(--y); height: 0; opacity: 0; animation: film-anno var(--film-loop) linear infinite; }
.film .anno.right { left: var(--x); right: 4%; }
.film .anno.left { left: 4%; right: calc(100% - var(--x)); }
.film .anno .line {
  position: absolute; left: 0; right: 0; top: 0; height: 1px; background: var(--claret);
  opacity: 0.75; transform: scaleX(0); animation: film-aline var(--film-loop) linear infinite;
}
.film .anno.right .line { transform-origin: left center; }
.film .anno.left .line { transform-origin: right center; }
.film .anno .dot { position: absolute; top: -2px; width: 4px; height: 4px; border-radius: 50%; background: var(--claret); }
.film .anno.right .dot { left: -2px; }
.film .anno.left .dot { right: -2px; }
/* No plate behind the words. The faint halo is not a box — it is what keeps the type legible
   where a reading falls on the darker part of a photograph. */
.film .anno .lab {
  position: absolute; bottom: 2px; max-width: 150%;
  font-size: max(0.44rem, 1.7cqw); line-height: 1.28; color: var(--ink);
  text-shadow: 0 0 3px rgb(from var(--raise) r g b / 0.95), 0 0 7px rgb(from var(--raise) r g b / 0.8);
}
.film .anno.right .lab { right: 0; text-align: right; }
.film .anno.left .lab { left: 0; text-align: left; }
.film .anno .lab b {
  display: block; font-size: 0.78em; letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--claret); font-weight: 650;
}

/* ---- the split ----
   Two panels rather than one hairline down the middle: her two garments sit in a box of their
   own and his in another, with real space between them, so the boundary is something you see
   rather than something you infer. */
.film .half {
  position: absolute; top: var(--ht); height: var(--hh); left: var(--hl); width: var(--hw);
  border: 1px solid var(--line); border-radius: 12px; background: var(--raise);
  box-shadow: 0 16px 34px -30px rgb(from var(--ink) r g b / 0.5);
  opacity: 0; z-index: 1; animation: film-split var(--film-loop) linear infinite;
}

/* ---- the line under the frame ---- */
.film .strip { display: grid; margin-top: 3cqw; }
.film .strip > * { grid-area: 1 / 1; opacity: 0; }
.film .eb {
  font-size: max(0.42rem, 1.6cqw); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--claret); font-weight: 650;
}
.film .cap { text-align: center; }
.film .cap .asks { display: flex; justify-content: center; gap: 6cqw; }
.film .cap .asks span { max-width: 34cqw; }
.film .cap .l {
  font-family: var(--serif); font-size: max(0.76rem, 2.9cqw); line-height: 1.32;
  color: var(--ink); max-width: 40ch; margin: 1.4cqw auto 0;
}
.film .cap.one { animation: film-cap1 var(--film-loop) linear infinite; }
.film .cap.two { animation: film-cap2 var(--film-loop) linear infinite; }
.film .verdicts { display: grid; grid-template-columns: 1fr 1fr; gap: 5cqw; animation: film-verd var(--film-loop) linear infinite; }
.film .verdicts > div { text-align: center; }
.film .verdicts .v {
  font-family: var(--serif); font-style: italic; font-size: max(0.7rem, 2.6cqw);
  line-height: 1.34; color: var(--ink); margin: 1.2cqw 0 0;
}
.film .verdicts .v .w { display: inline-block; opacity: 0; animation: film-vword var(--film-loop) linear infinite; }
.film .verdicts .sig {
  font-size: max(0.38rem, 1.45cqw); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--stone); margin-top: 1.2cqw;
}
.film .verdicts .sig i { font-style: normal; color: var(--claret); }

/* Off screen, it stops. The conversation panel below already does this with timers it clears;
   this one is pure CSS with no timers, so it is the play state that goes. Pausing keeps the
   phase, so it resumes exactly where it stopped. Without it the page would carry two animations
   looping at once and never let a phone's processor idle. */
.film.paused, .film.paused * { animation-play-state: paused !important; }

/* ---- the clock ----
   The shelf leaves with the two garments it gave up. Nothing of it is left behind the reading. */
@keyframes film-grid {
  0% { opacity: 0; transform: scale(1.07); }
  4.6%, 22.3% { opacity: 1; transform: scale(1); }
  30%, 100% { opacity: 0; transform: scale(1); }
}
@keyframes film-ring {
  0%, 15.4% { opacity: 0; transform: scale(1.6); }
  17%, 21% { opacity: 1; transform: scale(1); }
  23.5%, 100% { opacity: 0; transform: scale(1); }
}
/* square → bench → half of the screen */
@keyframes film-lead {
  0%, 20% { opacity: 0; transform: translate(var(--fx), var(--fy)) scale(var(--fs)); }
  22.3% { opacity: 1; transform: translate(var(--fx), var(--fy)) scale(var(--fs)); animation-timing-function: cubic-bezier(0.5, 0, 0.15, 1); }
  31.5%, 56.5% { opacity: 1; transform: translate(var(--ax), 0) scale(var(--as)); }
  56.51% { animation-timing-function: cubic-bezier(0.5, 0, 0.15, 1); }
  63.5%, 94.6% { opacity: 1; transform: none; }
  97%, 100% { opacity: 0; transform: none; }
}
@keyframes film-mate {
  0%, 57.5% { opacity: 0; transform: translateY(18px) scale(0.94); animation-timing-function: var(--spring); }
  63.5%, 94.6% { opacity: 1; transform: none; }
  97%, 100% { opacity: 0; transform: none; }
}
/* **The gate for the whole reading, and it exists because of a real defect.** The four notes are
   drawn one after another, and that stagger is an `animation-delay`. A delay makes a looping
   animation start late exactly *once* — from the second loop it is a permanent phase shift, so
   the top of every film after the first was showing the tail of the one before it: eight labels,
   over the shelf. This group carries no delay of its own, so it is at the same point of the film
   every time. **Never gate a looping animation on an element that is itself delayed** — and note
   that no screenshot of the first loop can catch this. Freeze at `--film-loop + t`. */
@keyframes film-annos {
  0%, 32.8% { opacity: 0; }
  33.1%, 53.1% { opacity: 1; }
  56%, 100% { opacity: 0; }
}
@keyframes film-anno { 0%, 33.1% { opacity: 0; } 34.5%, 100% { opacity: 1; } }
@keyframes film-aline { 0%, 33.1% { transform: scaleX(0); } 35.8%, 100% { transform: scaleX(1); } }
/* Nothing appears inside a panel until the garment it belongs to has stopped moving: the price
   is a child of the garment, so an early entrance draws it at twice its size and outside the box. */
@keyframes film-meta {
  0%, 64.3% { opacity: 0; transform: translateY(5px); }
  67.8%, 94.6% { opacity: 1; transform: none; }
  97%, 100% { opacity: 0; transform: none; }
}
@keyframes film-bar {
  0%, 67.3% { transform: scaleX(0); }
  71.3%, 94.6% { transform: scaleX(var(--fill)); }
  97%, 100% { transform: scaleX(0); }
}
@keyframes film-split { 0%, 62.8% { opacity: 0; } 65.3%, 94.6% { opacity: 1; } 97%, 100% { opacity: 0; } }
@keyframes film-cap1 {
  0%, 4% { opacity: 0; transform: translateY(6px); }
  5.5%, 20% { opacity: 1; transform: none; }
  22%, 100% { opacity: 0; transform: none; }
}
@keyframes film-cap2 {
  0%, 30% { opacity: 0; transform: translateY(6px); }
  32%, 55% { opacity: 1; transform: none; }
  57%, 100% { opacity: 0; transform: none; }
}
@keyframes film-verd {
  0%, 69% { opacity: 0; transform: translateY(7px); }
  72%, 94.6% { opacity: 1; transform: none; }
  97%, 100% { opacity: 0; transform: none; }
}
@keyframes film-vword {
  0%, 72% { opacity: 0; transform: translateY(3px); }
  73.5%, 92% { opacity: 1; transform: none; }
  95%, 100% { opacity: 0; transform: none; }
}

/* ---- the rail down the left --------------------------------------------------------------
   **A reserved area, not a list beside one section** (owner, 8 Sep 2026). It begins below the
   logo, runs past everything on the page — both examples, the three sections, the teaser, the
   closing block and the footer — and everything else is pushed right of it. More entries are
   going in it later, which is the reason it is an area rather than a decoration.

   No panel, no border and no ground of its own, at the owner's choice: just the names in the
   space, with the one you are reading marked by a short claret rule. The gliding is
   `scroll-behavior: smooth` on `html`, switched off for anyone who has asked their machine not
   to animate.

   Nothing is marked until you reach a section. The mark used to be on "How it works" in the
   markup, which was fine when the rail only appeared alongside that section and is wrong now
   that it is visible from the top of the page. */
.shell { display: grid; grid-template-columns: 1fr; }
@media (min-width: 900px) {
  .shell { grid-template-columns: 190px minmax(0, 1fr); gap: 40px; align-items: start; }
}
.rail { display: flex; gap: 4px; }
@media (min-width: 900px) {
  .rail {
    flex-direction: column;
    /* Pinned clear of the frozen bar. `top` is the bar's height (68px) plus room to breathe;
       `padding-top` is what sets where it starts when the page is at the top — below the logo,
       which is what was asked for. */
    position: sticky; top: 84px; padding-top: 34px;
  }
}
.rail a {
  color: var(--stone); text-decoration: none; font-size: 0.88rem; line-height: 1.35;
  padding: 7px 0 7px 14px; border-left: 2px solid var(--line);
  transition: color .2s, border-color .2s;
}
.rail a:hover { color: var(--ink); border-color: var(--stone); }
.rail a.on { color: var(--claret-ink); border-color: var(--claret); font-weight: 600; }
.rail a:focus-visible { outline: 2px solid var(--claret); outline-offset: 2px; }
/* On a phone there is no left-hand side, so the same list lies along the top and scrolls
   sideways if it has to. The rule moves from the left edge of each name to under it. */
@media (max-width: 899px) {
  .rail {
    overflow-x: auto; gap: 18px; margin-bottom: 8px;
    padding-bottom: 2px; -webkit-overflow-scrolling: touch;
  }
  .rail a {
    white-space: nowrap; border-left: 0; border-bottom: 2px solid var(--line);
    padding: 6px 0;
  }
}
/* Clear of the frozen top bar when a link jumps here, or the heading lands underneath it. */
.shell-body > section[id] { scroll-margin-top: 88px; }

/* ---- generic section ---- */
section { padding: 40px 0; }
.section-head { text-align: center; max-width: 46ch; margin: 0 auto 34px; }
.kicker {
  font-size: 0.72rem; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 700;
  color: var(--claret); margin-bottom: 10px;
}
.section-head h2 {
  font-family: var(--serif); font-weight: 500; font-size: clamp(1.28rem, 0.89rem + 1.7vw, 2.13rem);
  margin: 0 0 8px; line-height: 1.15; letter-spacing: -0.01em; text-wrap: balance;
}
.section-head p { color: var(--stone); margin: 0; font-size: 0.94rem; }
/* The accent in a section heading, the same claret italic the opening headline uses. It needs
   its own rule because `.lede em` cannot reach a heading that is not a lede. */
.section-head h2 em { font-style: italic; color: var(--claret); }
/* A section head may end in a button. Only "How it looks" does, and it centres with the words
   above it rather than starting at the left edge of a centred block. */
.section-head .cta-row { justify-content: center; margin-top: 16px; }
/* The buttons inside the page come down with everything else around them. The bar's own button
   and the footer's links are deliberately not in here. */
.shell-body .btn { font-size: 0.84rem; padding: 9px 18px; }

/* The second section's name, centred over both its columns. It is the only kicker on the
   page outside a `.section-head`, which is what centres the other three. */
.looks-name { text-align: center; }
#looks > .hero { padding-top: 8px; }
.rule { border: 0; border-top: 1px solid var(--line); margin: 0; }

/* ---- how it works ---- */
.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
@media (max-width: 720px) { .steps { grid-template-columns: 1fr; } }
.step {
  background: var(--raise); border: 1px solid var(--line); border-radius: 16px; padding: 19px 17px;
}
/* Everything on this page answers the pointer now, not only the shopper cards (owner, 8 Sep
   2026). The same lift, the same border, the same shadow — one gesture repeated, which is what
   makes it read as a considered page rather than as three separate effects.

   Behind `hover: hover` so it never fires on a touchscreen, where a "hover" is a tap and the
   card would stay lifted after you had moved on. The durations go to nothing under
   `prefers-reduced-motion`, at the foot of this file. */
@media (hover: hover) {
  .step, .feat, .band {
    transition: transform .25s cubic-bezier(0.2, 0.7, 0.2, 1), border-color .2s, box-shadow .25s;
  }
  .step:hover, .feat:hover, .band:hover {
    transform: translateY(-3px);
    border-color: var(--stone);
    box-shadow: 0 16px 30px -24px rgb(from var(--ink) r g b / 0.5);
  }
  /* The band is claret-tinted, so a stone border on it reads as a smudge; it warms to the
     accent it is already made of instead. */
  .band:hover { border-color: var(--claret); }
}
.step-n {
  font-family: var(--serif); font-size: 0.95rem; font-weight: 700; color: var(--on-accent);
  width: 29px; height: 29px; border-radius: 50%; background: var(--claret);
  display: grid; place-items: center; margin-bottom: 14px;
}
.step h3 { font-family: var(--serif); font-weight: 600; font-size: 1rem; margin: 0 0 6px; }
/* The panels stretch with the window; the sentence inside one does not. Without this a step
   reads across 90-odd characters on a wide screen. */
.step p { color: var(--stone); margin: 0; font-size: 0.88rem; max-width: 52ch; }

/* ---- persona roster ---- */
.roster-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
@media (max-width: 760px) { .roster-grid { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); } }
.persona {
  background: var(--raise); border: 1px solid var(--line); border-radius: 16px;
  padding: 15px 10px 14px; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 9px;
  transition: transform .25s cubic-bezier(0.2, 0.7, 0.2, 1), border-color .2s, box-shadow .25s;
}
.persona:hover { transform: translateY(-3px); border-color: var(--stone); box-shadow: 0 16px 30px -24px rgb(from var(--ink) r g b / 0.5); }
.persona .ava {
  width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center; font-family: var(--serif); font-weight: 600;
  font-size: 1rem; background: var(--pc, var(--claret)); color: var(--on-accent);
}
.persona.auto .ava {
  background: conic-gradient(from 210deg, var(--p-maya), var(--p-jules), var(--p-victor), var(--p-iris), var(--p-maya));
}
.persona .pname { font-family: var(--serif); font-weight: 600; font-size: 0.94rem; }
.persona .ptag { font-size: 0.72rem; color: var(--stone); line-height: 1.3; }

/* ---- why / differentiators ---- */
.why { display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px; }
@media (max-width: 720px) { .why { grid-template-columns: 1fr; } }
.feat {
  background: var(--raise); border: 1px solid var(--line); border-radius: 16px; padding: 19px 19px;
  display: flex; gap: 13px; align-items: flex-start;
}
.feat .ico {
  flex: none; width: 34px; height: 34px; border-radius: 11px; display: grid; place-items: center;
  font-size: 1.05rem; background: var(--claret-tint); color: var(--claret);
}
.feat h3 { font-family: var(--serif); font-weight: 600; font-size: 0.98rem; margin: 0 0 5px; }
.feat p { color: var(--stone); margin: 0; font-size: 0.86rem; max-width: 60ch; }

/* ---- design-your-own teaser band ---- */
.band {
  background: var(--claret-tint); border: 1px solid var(--line); border-radius: 22px;
  padding: 29px 27px; text-align: center; margin: 8px 0;
}
.band .soon {
  font-size: 0.62rem; letter-spacing: 0.12em; text-transform: uppercase; font-weight: 700;
  color: var(--caution); background: rgb(from var(--caution) r g b / 0.16);
  padding: 3px 8px; border-radius: 6px; margin-left: 8px;
}
.band h2 { font-family: var(--serif); font-weight: 500; font-size: clamp(1.19rem, 0.94rem + 1.19vw, 1.62rem); margin: 0 0 10px; }
.band p { color: var(--stone); max-width: 56ch; margin: 0 auto; font-size: 0.94rem; }

/* ---- final call to action ---- */
.finale { text-align: center; padding: 48px 0 34px; }
.finale h2 { font-family: var(--serif); font-weight: 500; font-size: clamp(1.45rem, 1.02rem + 1.7vw, 2.13rem); margin: 0 0 10px; letter-spacing: -0.01em; text-wrap: balance; }
.finale p { color: var(--stone); margin: 0 auto; max-width: 44ch; font-size: 0.94rem; }

/* ---- footer ---- */
/* The band runs the full width of the window and carries no rule above it — the white space is
   the separation. Everything sits on one line at the very bottom: the brand signs the page on
   the left, the copyright is centred, and the links close it on the right. The middle column is
   `auto` between two `1fr`s so the copyright is centred on the *page*, not merely between its
   two neighbours, which are different widths. "Open the app" is deliberately not among the
   links — it is the frozen button in the top bar and the button that ends the page, and a third
   quiet copy of it here would only dilute those two.

   The links are slate blue, not claret: they are quiet text links, and claret is the colour of
   the one action the page is actually asking for. Slate is the app's own Jules blue, so it is
   still the brand's palette. */
footer { padding: 18px 0 22px; margin-top: 16px; }
.foot-end { display: grid; grid-template-columns: 1fr auto 1fr; align-items: baseline; gap: 10px 24px; }
.foot-id { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.foot-brand { font-family: var(--serif); font-size: 1.15rem; font-weight: 600; }
.foot-tag { color: var(--faint); font-size: 0.85rem; }
.foot-copy { color: var(--faint); font-size: 0.8rem; text-align: center; margin: 0; }
.foot-links { display: flex; gap: 22px; flex-wrap: wrap; justify-content: flex-end; }
/* `var()`, not the hex it used to be: every other colour on this page has a dark value and
   this one did not, so the footer links stayed navy on near-black. */
.foot-links a { color: var(--p-jules); font-weight: 600; text-decoration: none; font-size: 0.9rem; }
.foot-links a:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 4px; }
/* On a narrow screen one line is three cramped ones, so they stack and all go left. */
@media (max-width: 899px) {
  .foot-end { grid-template-columns: 1fr; justify-items: start; gap: 12px; }
  .foot-copy { text-align: left; }
  .foot-links { justify-content: flex-start; }
}

/* Nothing on this page moves for somebody who has asked their machine to hold still. The
   hover states stay — the colour change is what tells you a thing is interactive — it is only
   the movement that goes. */
@media (prefers-reduced-motion: reduce) {
  .btn, .step, .feat, .band, .persona {
    transition-duration: 1ms !important;
  }
  /* The panel's turn is switched **off**, not shortened (decision CE, 9 Sep 2026): a 1ms
     animation still moves, and a machine under load still catches it mid-flight. The script
     stands down as well, so nothing changes on a timer either. What is left has to show every
     customer without moving, so the three conversations stop sharing a cell and simply stack —
     a taller panel, and every word of it readable at rest. A rule between them, because three
     transcripts running straight into each other read as one long muddle. */
  .turn, .turn *, .pv-caret, .pv-wipe { animation: none !important; }
  .pv-thread { display: block; }
  .turn { visibility: visible; }
  .turn + .turn { border-top: 1px solid var(--line); padding-top: 16px; margin-top: 4px; }
  /* The film goes off the same way, and what is left is its last frame — the shelf gone, four
     garments in two panels, both shoppers' lines. That frame carries the whole argument standing
     still, which is why switching the film off costs the page nothing. */
  .film .field, .film .tile, .film .tile::after, .film .pick, .film .meta, .film .track i,
  .film .agroup, .film .anno, .film .anno .line, .film .half, .film .cap, .film .verdicts,
  .film .v .w { animation: none !important; }
  .film .field, .film .agroup, .film .cap { opacity: 0; }
  /* Written out in full, and the two long ones are why: `animation: none` is `!important` and
     wins on its own, but these are ordinary declarations that have to out-rank the rule they are
     overriding. Shortened to `.film .v .w` and `.film .track i` they lost, and the frame came out
     with no verdicts and four empty value bars. */
  .film .pick, .film .meta, .film .half, .film .verdicts { opacity: 1; transform: none; }
  .film .verdicts .v .w { opacity: 1; transform: none; }
  .film .vfm .track i { transform: scaleX(var(--fill)); }
  .btn:hover, .step:hover, .feat:hover, .band:hover, .persona:hover {
    transform: none;
  }
}

/* ---- legal pages ---- */
.legal { max-width: 720px; margin: 0 auto; padding: 20px 22px 60px; }
.legal h1 { font-family: var(--serif); font-weight: 500; font-size: clamp(1.7rem, 1.3rem + 1.6vw, 2.3rem); margin: 20px 0 4px; }
.legal .updated { color: var(--faint); font-size: 0.85rem; margin: 0 0 26px; }
.legal h2 { font-family: var(--serif); font-weight: 600; font-size: 1.25rem; margin: 30px 0 8px; }
.legal p, .legal li { color: var(--ink); font-size: 0.97rem; }
.legal ul { padding-left: 22px; }
.legal li { margin: 6px 0; }
.legal .back { display: inline-block; margin-top: 8px; color: var(--stone); text-decoration: none; font-size: 0.9rem; }
.legal .back:hover { color: var(--claret); }
.beta-note {
  background: var(--raise); border: 1px solid var(--line); border-left: 3px solid var(--caution);
  border-radius: 12px; padding: 14px 16px; margin: 20px 0; font-size: 0.92rem; color: var(--stone);
}
