/*
 * The Monsho page.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * NOTHING HERE IS A NEW COLOUR, AND THAT IS THE POINT OF LIVING IN THE MONOREPO
 * ─────────────────────────────────────────────────────────────────────────────
 *
 * Every value in the palette below is copied from the app's own tokens
 * (`apps/mobile/src/theme/palette.ts` and `apps/mobile/global.css`), because a
 * site that invents its own greys is a second opinion about what the product
 * looks like. `src/__tests__/page-rules.test.ts` reads the app's palette and
 * asserts this file still agrees with it, so the two cannot drift quietly.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * THE GROUND RULE DECIDES THE STRUCTURE OF THE PAGE
 * ─────────────────────────────────────────────────────────────────────────────
 *
 * Pure white behind any surface carrying garment imagery; the warm ground only
 * on surfaces with no imagery on them. On the app that separates a plate from a
 * page. Here it is what the page is built out of: a band that contains a picture
 * is white, a band that is only words is warm, and the reader can see at a glance
 * which parts of this page make a claim with a picture. The phone never sits on
 * the warm ground.
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * THERE ARE NO DIVIDING RULES, AND THAT IS THE v2 LANGUAGE
 * ─────────────────────────────────────────────────────────────────────────────
 *
 * `Design/look-v2/monsho-look-v2.html`, 17 August 2026: "Every dividing rule is gone:
 * separation is carried by surface, corner radius, shadow and space." It names
 * four swaps and this file took all four, inside the phone and outside it:
 *
 *   image to plate      radius, inset and shadow
 *   button outlines     quiet fill
 *   bar border          blur and shadow
 *   settings dividers   grouped rows and space
 *
 * Nineteen hairlines went. TWO SURVIVE AND BOTH ARE DELIBERATE: `.phone` keeps
 * its outline, because that is the edge of a device rather than a divider inside
 * a user interface, and `.skip` keeps its own, because a focus affordance that
 * only appears when focused is not separating anything.
 *
 * Shadow used to be forbidden outside the phone on the argument that floating
 * cards are what a software marketing page looks like. That argument was against
 * DECORATIVE shadow and it still holds; what the design uses shadow for is
 * elevation, of which there are exactly two steps: resting for cards, chips and
 * rows, lifted for the render and the floating bar.
 */

/* ══════════════════════════════════════════════════════════════════════════
   TYPEFACES — self-hosted, EU or not, because a hosted font is a third-party
   request that discloses every visitor's address. Licences and the date they
   were verified are in `public/fonts/README.md`.
   ══════════════════════════════════════════════════════════════════════════ */

/* Display: Familjen Grotesk, Letters from Sweden. Variable, 400 to 700. */
@font-face {
  font-family: "Familjen Grotesk";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/familjen-grotesk-latin.woff2") format("woff2");
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Familjen Grotesk";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/familjen-grotesk-latin-ext.woff2") format("woff2");
  unicode-range:
    U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Body: Schibsted Grotesk, Bakken & Bæck. Variable, 400 to 700. */
@font-face {
  font-family: "Schibsted Grotesk";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/schibsted-grotesk-latin.woff2") format("woff2");
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Schibsted Grotesk";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/schibsted-grotesk-latin-ext.woff2") format("woff2");
  unicode-range:
    U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ══════════════════════════════════════════════════════════════════════════
   TOKENS
   ══════════════════════════════════════════════════════════════════════════ */

:root {
  /* The app's five surface colours, verbatim. */
  --ground: #ededed; /* app `bg`   — neutral; see palette.ts for why the warmth went */
  --white: #ffffff; /* app `surf`  — behind anything carrying imagery */
  --recess: #e4e3de; /* app `surf2` — an empty image field, a chip */
  --ink: #0c0d0f; /* app `fg`    — text and the crest */
  --ink-2: #585e67; /* app `fg2`   — 5.73:1 on the ground, over the 4.5:1 floor */

  /* One ink at two strengths, per the app's HAIRLINE constant (0.14 / 0.30). */
  --line: rgb(12 13 15 / 0.14);
  --line-2: rgb(12 13 15 / 0.3);
  /* The app's SHADE, light theme: 0.07. Used once, inside the phone. */
  --shade: rgb(12 13 15 / 0.07);

  --display:
    "Familjen Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --body:
    "Schibsted Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /*
   * The PAGE scale, which is not the app's scale and says so.
   *
   * The app's seven steps (11, 13, 15, 18, 21, 25, 29) are drawn at phone size
   * on a phone. A page read at arm's length on a 1440 display needs a display
   * step the app has no use for, and stretching a 1.175 ratio that far would
   * take a dozen steps nobody would use. So there are two scales in this file
   * and the boundary between them is the phone's own frame: everything inside
   * `.phone` uses the app's literal pixel values, everything outside uses these.
   */
  --t-xs: 0.8125rem; /* 13px — the letterspaced labels */
  --t-sm: 0.9375rem; /* 15px — captions, footer, small print */
  --t-body: 1.0625rem; /* 17px — body copy */
  --t-lead: clamp(1.125rem, 1.05rem + 0.35vw, 1.3125rem); /* 18 → 21 */
  --t-head: clamp(1.75rem, 1.3rem + 1.9vw, 2.5rem); /* 28 → 40 */
  --t-display: clamp(2.5rem, 1.4rem + 4.6vw, 4.5rem); /* 40 → 72 */

  --gutter: clamp(1.5rem, 5vw, 6rem);
  --page-max: 90rem; /* 1440 */
  --measure: 34rem; /* the prose column */
  --band-y: clamp(3.5rem, 7vw, 7rem);
  /*
   * The masthead's height, stated once.
   *
   * It is 44px of tappable navigation plus 0.75rem of padding above and below,
   * which are both real numbers in `.masthead` rather than a measurement of a
   * screenshot. The hero's device is positioned under it and sized against what
   * it leaves, and those two calculations were about to be two hardcoded 68s.
   */
  --mast-h: calc(44px + 1.5rem);
}

/* ══════════════════════════════════════════════════════════════════════════
   BASE
   ══════════════════════════════════════════════════════════════════════════ */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  /* No smooth scrolling. There is nothing on this page worth animating to. */
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--body);
  font-size: var(--t-body);
  line-height: 1.55;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3,
p,
ul,
ol,
figure,
dl {
  margin: 0;
}

ul,
ol {
  padding: 0;
  list-style: none;
}

a {
  color: inherit;
  text-decoration-color: var(--line-2);
  text-underline-offset: 0.28em;
  text-decoration-thickness: 1px;
  transition: text-decoration-color 120ms linear;
}

a:hover {
  text-decoration-color: var(--ink);
}

/*
 * Focus is visible and it is the same treatment everywhere. `:focus-visible`
 * rather than `:focus` so a mouse click on a link does not draw a ring, but a
 * keyboard tab always does.
 */
a:focus-visible,
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  border-radius: 2px;
}

img,
svg {
  display: block;
  max-width: 100%;
}

/*
 * The one motion rule.
 *
 * Today this page animates exactly one thing: a link's underline colour. The
 * blanket reset is still the right shape, because its job is to cover the
 * transition somebody adds next year without remembering this block exists. A
 * rule that only names what is currently animated stops being a guarantee the
 * moment anything is added.
 *
 * `!important` is what makes that true, and it is the one place on this page
 * where it is used. Suppressed by range rather than per line so the reason is
 * stated once and the four declarations stay readable as one rule.
 */
/* biome-ignore-start lint/complexity/noImportantStyles: a reduced-motion reset has to beat every transition declared anywhere, including ones added after it. That is the entire mechanism. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
/* biome-ignore-end lint/complexity/noImportantStyles: end of the reduced-motion reset */

/* Visually hidden, still read aloud. Used for the skip link and one heading. */
.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip {
  position: absolute;
  left: var(--gutter);
  top: -4rem;
  z-index: 10;
  background: var(--white);
  border: 1px solid var(--ink);
  padding: 0.6rem 1rem;
  font-size: var(--t-sm);
  text-decoration: none;
}

.skip:focus {
  top: 0.75rem;
}

/* ══════════════════════════════════════════════════════════════════════════
   BANDS — the page is a stack of full-width bands separated by hairlines.
   `band--white` is the ground rule: it carries imagery, so it is pure white.
   ══════════════════════════════════════════════════════════════════════════ */

.band {
  /*
     No rule between bands. One is white and the next is warm, and that change of
     surface is the separation; a hairline on top of it was drawing the same
     boundary twice. v2: "separation is carried by surface, corner radius, shadow
     and space."
  */
}

.band--white {
  background: var(--white);
}

.band:first-of-type {
  border-top: 0;
}

.wrap {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: var(--band-y) var(--gutter);
}

.measure {
  max-width: var(--measure);
}

/* The small letterspaced label that opens most bands. Uppercase is set here and
   not typed into the HTML, so the copy stays sentence case in the source and
   stays translatable. */
.label {
  font-family: var(--display);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-2);
  margin-bottom: 1.75rem;
}

h1,
h2 {
  font-family: var(--display);
  font-weight: 400;
  letter-spacing: -0.022em;
  line-height: 1.06;
  text-wrap: balance;
}

h1 {
  font-size: var(--t-display);
}

h2 {
  font-size: var(--t-head);
  line-height: 1.1;
}

h3 {
  font-family: var(--display);
  font-size: var(--t-lead);
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.25;
}

.lead {
  font-size: var(--t-lead);
  line-height: 1.45;
  color: var(--ink);
}

/*
 * Body copy is ink, not the secondary grey.
 *
 * The app uses `fg2` for lines that support something else: a merchant, a
 * status, a caption. A paragraph somebody is meant to read is `fg`. Setting a
 * whole page in secondary grey is how a design gets described as quiet when it
 * is actually just faint.
 */
.prose {
  color: var(--ink);
}

.prose > * + * {
  margin-top: 1.15em;
}

.prose strong {
  color: var(--ink);
  font-weight: 500;
}

/* ══════════════════════════════════════════════════════════════════════════
   MASTHEAD
   ══════════════════════════════════════════════════════════════════════════ */

/*
 * THE MASTHEAD IS WHITE, AND IT IS AN EXCEPTION TO THE GROUND RULE ABOVE.
 *
 * That rule sorts BANDS: one carrying pictures is white, one that is only words
 * is warm. The masthead is not a band, it is chrome, and it sat on the warm
 * ground directly above a white hero — a grey bar with a hard edge under it,
 * which was the first thing on the page and the least flattering.
 *
 * White here continues into the hero instead of announcing itself, and the rule
 * it appears to break is intact: no band changed colour, and a reader can still
 * tell at a glance which parts of this page make a claim with a picture.
 */
header {
  background: var(--white);
}

.masthead {
  max-width: var(--page-max);
  margin: 0 auto;
  /* 0.75rem, not 1.5: the nav links now carry 44px of their own height, so the
     bar keeps the height it had before they became tappable. */
  padding: 0.75rem var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
}

/*
 * THE NAVIGATION IS SET BACK FROM THE WORDMARK RATHER THAN LEVEL WITH IT.
 *
 * Five links in full ink, each underlined at 30%, gave the top of the page two
 * things shouting at the same volume: the mark and a row of controls. The links
 * are `fg2` now, which is the app's own second text colour and 6.4:1 on white,
 * and their underline is the 14% hairline rather than the 30% one. They darken
 * to full ink on hover and on focus, so the affordance is still there and the
 * resting state is quiet.
 */
/*
 * A NAVIGATION LINK IS A TAP TARGET, AND THESE WERE 19px TALL.
 *
 * Measured on a phone: "Privacy" and "Contact" were 51x19 and 54x19, and the
 * footer's were the same. WCAG 2.5.8 asks for 24 by 24 as a floor, and the
 * exception for links inline in a sentence does not apply to these: they are
 * standalone navigation, sitting on their own in a row.
 *
 * The padding grows the hit area without moving the text, and the masthead's own
 * vertical padding comes down by the same amount so the bar does not get taller.
 * Inline links inside prose are deliberately left alone: they are exempt, and
 * padding them would break the line spacing of the paragraph they sit in.
 */
.masthead nav a,
.footer nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--ink-2);
  text-decoration-color: var(--line);
}

.footer nav a {
  color: inherit;
}

.masthead nav a:hover,
.masthead nav a:focus-visible {
  color: var(--ink);
  text-decoration-color: var(--line-2);
}

/*
 * The masthead is the wordmark, alone.
 *
 * It used to be the crest and the word side by side. A mark-plus-wordmark lockup
 * is the software convention; the shops this page sits beside identify
 * themselves with a wordmark and nothing else. It is also set large enough to
 * read as the masthead rather than as the first item in the navigation, which is
 * what it looked like at the old 20px beside a 22px glyph.
 */
/*
 * THE WORDMARK IS THE APP ICON'S WORDMARK, and until 2026-08-15 it was not.
 *
 * `Design/monsho-icon-light.svg` is the locked mark: Schibsted Grotesk, medium,
 * tracking -0.02em. This drew it in Familjen Grotesk at -0.005em — a different
 * typeface and four times looser — so the name at the top of the page and the
 * name on the app icon were two different drawings of the same word. That is the
 * one thing a wordmark cannot afford, because the wordmark IS the mark.
 *
 * The size is the page's own; everything else here is the master's, and
 * `page-rules.test.ts` reads that file and asserts it.
 *
 * IT IS SET LARGER THAN IT WAS, AND THE LETTERFORMS DID NOT CHANGE. Reported as
 * not looking like the design file, so the two were rendered at the same size
 * and diffed pixel by pixel: they agree everywhere but the antialiased edge. The
 * geometry was never the problem. What differed was presence — 22px of wordmark
 * beside five 15px links reads as the first item in the navigation, which is the
 * exact failure this comment records the crest version having. Bigger, and the
 * navigation set back, is the fix. Nothing about the drawing moved.
 */
.wordmark {
  font-family: var(--body);
  font-size: clamp(1.5rem, 1.15rem + 1.15vw, 2rem);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1;
  text-decoration: none;
}

/* In the footer it is a signature rather than a masthead, so it sits at body
   size and does not compete with the page above it. */
.wordmark--footer {
  font-size: 1.125rem;
}

.masthead nav ul {
  display: flex;
  gap: clamp(1rem, 2.5vw, 2rem);
  font-size: var(--t-sm);
  flex-wrap: wrap;
}

/* ══════════════════════════════════════════════════════════════════════════
   HERO — two columns at width, and the phone is cropped by the bottom of the
   band rather than floated in the middle of it. A magazine crops a photograph;
   a software page centres a device and adds a shadow under it.
   ══════════════════════════════════════════════════════════════════════════ */

/*
 * NOTHING IS CROPPED HERE ANY MORE.
 *
 * The device used to run off the bottom of the band on purpose — the idea being
 * that a thing which continues past an edge does not need a shadow to lift it
 * off the page. What it actually produced was a phone with its bottom bezel and
 * corner sliced off, which reads as a rendering fault rather than as a crop, and
 * it was reported as one twice.
 *
 * So the band has ordinary padding under it, the device is whole, and `--u`
 * carries the difference: the phone is scaled to what is left after the masthead
 * and this padding, rather than being cut to fit.
 */
.hero .wrap {
  padding-bottom: clamp(2rem, 3vw, 3rem);
  display: grid;
  gap: clamp(2.5rem, 5vw, 4rem);
  /* The headline starts level with the top of the screen beside it. Bottom
     alignment was tried first and left the first thing on the page sitting
     under half a screen of nothing. */
  align-items: start;
}

.hero h1 {
  margin-bottom: 1.5rem;
}

/*
 * The reading measure sits on the lead, not on the column.
 *
 * With it on the column the headline was capped at 34rem too, which broke this
 * sentence over four lines at the display step and stranded "a photograph" on
 * one of them. A headline is scanned and takes the full column; the paragraph
 * under it is read, so it keeps the measure.
 */
.hero .lead {
  max-width: var(--measure);
}

/*
 * The line under the lead, where the loop's caption used to sit.
 *
 * It was a `figcaption` beneath the picture. The picture is inside a phone now
 * and the phone is cropped by the bottom of the band, so there is no "beneath"
 * to put it in. The words are unchanged and so is their job: they say what the
 * first frame is, which is the one thing a still cannot show.
 */
/*
 * The same automatic-minimum trap as `.two-col > *`, in the hero this time.
 *
 * A grid item's `min-width` is `auto`, and an `<input>` reports an intrinsic
 * width of about twenty characters, which at a 32px root is 554px. The day a
 * form was put in the hero, this column was floored at 554px inside a 390px
 * viewport and the page scrolled sideways by 212px at double text size: the
 * exact figure a fix three weeks earlier had removed from the OTHER grid.
 *
 * On the `.hero .wrap` rather than on one child, because the field has now
 * lived in two of them.
 */
.hero .wrap > * {
  min-width: 0;
}

.hero__note {
  margin-top: 1.5rem;
  font-family: var(--display);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-2);
}

/*
 * The crop.
 *
 * The device runs past the bottom edge of the band by a fixed amount, which is
 * what removes the need for a shadow to lift it off the page: it is not floating,
 * it is continuing. A negative margin rather than a `max-height`, because the
 * amount cropped then does not depend on how tall the screen's content happens to
 * be. The first version of this used a height cap and quietly cut off the price,
 * the brand line and the whole tab bar, which are the parts of the app this page
 * exists to show.
 */
/*
 * No `overflow: hidden`. There is nothing left to clip, and leaving it would
 * mean the next person to give the phone a shadow or a focus ring finds it
 * silently sliced off at the column edge.
 */
.device {
  display: flex;
  justify-content: flex-start;
}

/*
 * Enhanced, and only where motion is welcome: the two panes stack into one
 * frame and the worn one fades in over the flat one, once.
 *
 * Fading the worn pane IN over the flat one rather than cross-fading both: two
 * opacities passing through 0.5 would show the page through the gap for a
 * moment. The worn render covers the frame completely, so a one-way fade is a
 * clean dissolve with no dip.
 *
 * The labels are handed over rather than cross-faded, for the same reason in
 * reverse: they are transparent, so any overlap is two words on top of each
 * other. The first goes, then the second arrives.
 */

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE LOOP — one subject, the garment changing, INSIDE THE PHONE.

   Seven frames stacked in one box, each fading up in turn and back down, and
   seven titles on the same seven slots beneath them. The frames and the words
   change together; the header, the buttons and the tab bar around them do not.
   That is the difference between a picture cross-fading and a feed working, and
   it is the entire reason the loop moved inside the device.

   ─────────────────────────────────────────────────────────────────────────
   THE STILL IS THE DEFAULT AND IT IS FRAME ZERO
   ─────────────────────────────────────────────────────────────────────────

   With no JavaScript, or with reduced motion asked for, every frame and every
   title after the first is `display: none`. What holds is a phone showing a card
   with the subject in her own clothes and the word for it, which reads as the
   app sitting still rather than as something that failed to start. The line
   under the lead says what that first frame is, because that is the one thing a
   still cannot show.

   Twelve beats at 1.4s each, 16.8s in total: four people, and for each of them
   their photograph, the same photograph while the app works, and the garment.
   1.4s is long enough to register a garment and short enough that a viewer who
   waits sees the loop close.
   ══════════════════════════════════════════════════════════════════════════ */

/*
 * The image region of the card, which is the app's own `flex-1 min-h-0`: the
 * photograph takes whatever the commerce rows leave, and its shape follows from
 * the device rather than from the picture.
 */
/*
 * No rule under the picture any more. It separated the image from the words when
 * they shared one box; the words are on the paper now and the card's own edge
 * does the separating, which is v2's "image to plate: radius, inset and shadow".
 */
.loop__stage {
  display: grid;
  background: var(--recess);
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}

.loop__frame {
  grid-area: 1 / 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/*
 * THE PENDING BEAT IS THE APP'S BLURHASH, AND IT IS NOT DECORATION.
 *
 * `cardDisplay` returns `imageUri: null` while a render is in flight, so the app
 * shows NO photograph — `Card.tsx` passes a blurhash placeholder with no source
 * and the plate holds a soft field of the garment's own colours. This page held
 * a photograph there until 2026-08-16, which was a state the product does not
 * have.
 *
 * The file is the garment's own picture at 24x32, about a third of a kilobyte,
 * drawn across the whole plate. `object-fit: cover` plus a blur is the same
 * thing a blurhash is by a different route: the low frequencies of the real
 * image, at the right average colour. `scale` hides the soft edge that a blur
 * leaves at the boundary of the element.
 *
 * There is no spinner, because the app has none. If one is ever wanted it
 * belongs in `Card.tsx` first and here second.
 */
.loop__frame--blur {
  filter: blur(16px);
  transform: scale(1.15);
}

/* The twelve titles and the twelve status lines, stacked the same way the
   frames are, so no row can change height as they swap. A card that grows a line
   when the garment changes moves the buttons under a reader's thumb. */
.loop__titles,
.loop__metas {
  display: grid;
}

.loop__title,
.loop__meta {
  grid-area: 1 / 1;
}

/* Default, and the reduced-motion and no-JavaScript reading: beat zero only,
   which is the garment flat and the offer to see it on you. */
.loop__frame + .loop__frame,
.loop__title + .loop__title,
.loop__meta + .loop__meta {
  display: none;
}

/*
 * THE PAIR: one garment, flat and worn, held still and side by side.
 *
 * The section it sits in argues that the picture is honest about your shape, and
 * until now it argued in prose and showed nothing. The hero has both of these
 * files and cross-fades them, which a reader watches; a comparison is a thing a
 * reader makes, and it needs both at once.
 *
 * TWO COLUMNS AT EVERY WIDTH, including 320. Stacking them on a phone would turn
 * the comparison back into a sequence, which is the defect this fixes. Each shot
 * is about 140px wide there, which is small and still legible as the same coat.
 *
 * 3:4 on both, because the flat lay is 576x864 and the render is 576x768. Equal
 * heights are the whole point of a comparison, so the flat is cropped by 11%
 * rather than letterboxed. Safe because the catalogue frames every garment at
 * about 80% of the frame with even margins: the crop takes backdrop.
 *
 * No radius. The rail's 12px is a stated exception for a row of cards and this
 * is not one.
 */
.pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(0.5rem, 1.5vw, 1rem);
  align-items: start;
}

/*
 * FOUR STORIES, NOT ONE, AND THE STACK IS WHAT MAKES THEM ONE PICTURE.
 *
 * A single pair was a stale photograph: one coat on one person, arguing that
 * the render is honest about anybody. Four subjects in four garments argue it
 * about four people, and they are the four the hero already tells, so no file
 * enters the page for this and no marks review is owed.
 *
 * Both stacks run the same animation on the same delays, so the flat and the
 * worn shot are always the same garment on the same person. One list of delays
 * for both, which is what stops them being edited apart. It is the hero loop's
 * mechanism exactly, at four beats instead of twelve.
 */
.pair__stack {
  display: grid;
  background: var(--recess);
  /*
   * ROUNDED, WHICH IS THE SECOND EXCEPTION TO THE RULE AT THE TOP OF THIS FILE
   * AND THE SAME ONE.
   *
   * v2's language is "image to plate: radius, inset and shadow", and these two
   * are images sitting directly on paper, which is what a plate is. The rail's
   * cards are rounded for the same reason. The hero's frames are NOT, and that
   * is not an inconsistency: they sit inside a card that is already rounded, so
   * a radius there would be a corner inside a corner.
   */
  border-radius: 12px;
  overflow: hidden;
}

.pair__shot {
  grid-area: 1 / 1;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  /* The recess, so a picture that has not decoded is a surface and not a hole. */
  background: var(--recess);
}

/*
 * Frame zero with nothing enhanced, exactly as the hero does it. Without a
 * script the section shows one pair, still, which is what it showed before this
 * and is a complete argument on its own.
 */
.pair__shot + .pair__shot {
  display: none;
}

@media (prefers-reduced-motion: no-preference) {
  [data-enhanced] .loop__frame + .loop__frame,
  [data-enhanced] .loop__title + .loop__title,
  [data-enhanced] .loop__meta + .loop__meta {
    display: block;
  }

  /*
    Every frame runs the same animation with the same duration, offset by one
    slot. `opacity` only: nothing moves, nothing scales. The keyframes hold a
    beat opaque for most of its slot and cross-fade over the join, so two are
    only ever both partly visible for a fraction of a second.

    The titles and the status lines run the identical animation on the identical
    delays, which is what keeps the word and the picture the same thing. One list
    of delays for all three, so they cannot be edited apart.

    TWELVE SLOTS IN FOUR STORIES OF THREE, AND EACH STORY REPEATS ITS FIRST
    PICTURE. Beat one is a person's photograph with "Not on you yet"; beat two is
    the same photograph with "Putting this on you…"; beat three is the garment.
    Nothing about the image changes while the app says it is working, because
    nothing about it changes in the app either. Then the next person.
  */
  /*
    THE PAIR CYCLES FOUR STORIES, ON THE HERO'S MECHANISM AND ITS ARITHMETIC.

    3.6s a beat, 14.4s for four, and a 0.4s cross-fade. Slower than the hero's
    1.4s on purpose: the hero is a demonstration somebody watches go past, and
    this is a comparison somebody reads, so a beat here has to be long enough to
    look from one side to the other and back.

    THE DELAYS ARE NEGATIVE BY ONE CROSS-FADE, for the reason written out at
    length below this block: written as 0s the first beat starts at keyframe 0%,
    which is `opacity: 0`, so the section would open on two empty plates and
    fade the first pair in. That was measured on the hero at 230ms and it is the
    same mistake here.
  */
  [data-enhanced] .pair__shot + .pair__shot {
    display: block;
  }

  [data-enhanced] .pair__shot {
    animation: pair-frame 14.4s linear infinite;
    opacity: 0;
  }

  [data-enhanced] .pair__shot:nth-child(1) {
    animation-delay: -0.4s;
  }

  [data-enhanced] .pair__shot:nth-child(2) {
    animation-delay: 3.2s;
  }

  [data-enhanced] .pair__shot:nth-child(3) {
    animation-delay: 6.8s;
  }

  [data-enhanced] .pair__shot:nth-child(4) {
    animation-delay: 10.4s;
  }

  [data-enhanced] .loop__frame,
  [data-enhanced] .loop__title,
  [data-enhanced] .loop__meta {
    animation: loop-frame 16.8s linear infinite;
    opacity: 0;
  }

  /*
    THE DELAYS ARE NEGATIVE BY ONE CROSS-FADE, AND THAT IS THE WHOLE FIX FOR A
    FLICKER ON LOAD.

    Written as 0s, 1.4s, 2.8s the first beat starts at keyframe 0%, which is
    `opacity: 0`. Everything below `[data-enhanced]` starts hidden and beat one
    then fades itself in — so the phone opened on an empty plate and the
    photograph arrived a moment later. Measured in a browser: **the card carried
    nothing from first paint until 230ms**, and the title blinked with it.

    It is worse than a slow start, because the unenhanced page shows beat one
    sitting still at full opacity. Enhancing the page therefore took a picture
    that was already on screen, removed it, and faded it back in.

    Shifting every delay back by exactly one cross-fade (0.224s) starts beat one
    at the TOP of its plateau rather than at the bottom of its fade-in. Nothing
    else moves: the spacing between beats is still 1.4s, the cycle is still
    16.8s, every cross-fade is still 0.224s, and beat one still gets the
    same 1.176s of full opacity as the other eleven. The loop is joined
    already open instead of being started from nothing.
  */
  [data-enhanced] .loop__frame:nth-child(1),
  [data-enhanced] .loop__title:nth-child(1),
  [data-enhanced] .loop__meta:nth-child(1) {
    animation-delay: -0.224s;
  }
  [data-enhanced] .loop__frame:nth-child(2),
  [data-enhanced] .loop__title:nth-child(2),
  [data-enhanced] .loop__meta:nth-child(2) {
    animation-delay: 1.176s;
  }
  [data-enhanced] .loop__frame:nth-child(3),
  [data-enhanced] .loop__title:nth-child(3),
  [data-enhanced] .loop__meta:nth-child(3) {
    animation-delay: 2.576s;
  }
  [data-enhanced] .loop__frame:nth-child(4),
  [data-enhanced] .loop__title:nth-child(4),
  [data-enhanced] .loop__meta:nth-child(4) {
    animation-delay: 3.976s;
  }
  [data-enhanced] .loop__frame:nth-child(5),
  [data-enhanced] .loop__title:nth-child(5),
  [data-enhanced] .loop__meta:nth-child(5) {
    animation-delay: 5.376s;
  }
  [data-enhanced] .loop__frame:nth-child(6),
  [data-enhanced] .loop__title:nth-child(6),
  [data-enhanced] .loop__meta:nth-child(6) {
    animation-delay: 6.776s;
  }
  [data-enhanced] .loop__frame:nth-child(7),
  [data-enhanced] .loop__title:nth-child(7),
  [data-enhanced] .loop__meta:nth-child(7) {
    animation-delay: 8.176s;
  }
  [data-enhanced] .loop__frame:nth-child(8),
  [data-enhanced] .loop__title:nth-child(8),
  [data-enhanced] .loop__meta:nth-child(8) {
    animation-delay: 9.576s;
  }
  [data-enhanced] .loop__frame:nth-child(9),
  [data-enhanced] .loop__title:nth-child(9),
  [data-enhanced] .loop__meta:nth-child(9) {
    animation-delay: 10.976s;
  }
  [data-enhanced] .loop__frame:nth-child(10),
  [data-enhanced] .loop__title:nth-child(10),
  [data-enhanced] .loop__meta:nth-child(10) {
    animation-delay: 12.376s;
  }
  [data-enhanced] .loop__frame:nth-child(11),
  [data-enhanced] .loop__title:nth-child(11),
  [data-enhanced] .loop__meta:nth-child(11) {
    animation-delay: 13.776s;
  }
  [data-enhanced] .loop__frame:nth-child(12),
  [data-enhanced] .loop__title:nth-child(12),
  [data-enhanced] .loop__meta:nth-child(12) {
    animation-delay: 15.176s;
  }
}

/*
 * One slot is a twelfth of the cycle, which is 8.3333%.
 *
 * The cross-fade either side of it is held at the 0.224s it was when the loop
 * ran eight beats, which is 1.3333% of a 16.8s cycle rather than the 2% it
 * was of 11.2s. Scaling the percentage instead of the duration would have made
 * every dissolve half again as long the moment the loop grew, which is the sort
 * of thing that gets called "it feels slower now" and never gets traced.
 */
/*
 * Four beats: in over 0.4s, hold to 3.6s, out by 4.0s, then out of the way.
 * 2.7778% of 14.4s is 0.4s; 25% is 3.6s; 27.7778% is 4.0s.
 */
@keyframes pair-frame {
  0% {
    opacity: 0;
  }
  2.7778% {
    opacity: 1;
  }
  25% {
    opacity: 1;
  }
  27.7778% {
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

@keyframes loop-frame {
  0% {
    opacity: 0;
  }
  1.3333% {
    opacity: 1;
  }
  8.3333% {
    opacity: 1;
  }
  9.6667% {
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   SECTIONS ARRIVING

   A band lifts a little and fades in the first time it is reached, its children
   a hair apart. Nothing slides in from the side, nothing scales, nothing
   repeats: `enhance.js` stops watching an element the moment it fires.

   Gated on `prefers-reduced-motion: no-preference`, so the reduced-motion
   reader never has content at `opacity: 0` waiting for a scroll event.
   ══════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: no-preference) {
  [data-enhanced] .reveal > * {
    opacity: 0;
    transform: translateY(10px);
  }

  [data-enhanced] .reveal.is-in > * {
    opacity: 1;
    transform: none;
    transition:
      opacity 550ms ease-out,
      transform 550ms ease-out;
  }

  [data-enhanced] .reveal.is-in > *:nth-child(2) {
    transition-delay: 80ms;
  }

  [data-enhanced] .reveal.is-in > *:nth-child(n + 3) {
    transition-delay: 160ms;
  }
}

/*
 * PRINT SEES EVERYTHING, SCROLLED OR NOT.
 *
 * A reveal that waits for a scroll event has no scroll event to wait for on
 * paper, so without this a printed page or a PDF would come out with one band
 * of content and four blank ones. A network reviewer printing this to attach to
 * an application is exactly the reader who would hit it.
 */
/* biome-ignore-start lint/complexity/noImportantStyles: a print reset has to beat the enhanced rules above it, which is the whole mechanism. Same argument as the reduced-motion reset. */
/* biome-ignore-start lint/style/noDescendingSpecificity: these selectors are lower specificity than the `[data-enhanced]` rules they undo, which is exactly why they carry `!important`. Reordering would not help; the print block belongs at the end. */
@media print {
  .reveal > * {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }

  /* Paper cannot hold a loop, so it holds frame zero. Every later frame is
     removed rather than faded, because a stack of seven overprinted bodies is
     what "animation: none" alone would produce. The titles go the same way, and
     for the same reason: seven overprinted garment names is worse than one. */
  .loop__frame,
  .loop__title,
  .loop__meta,
  .pair__shot {
    animation: none !important;
    opacity: 1 !important;
  }

  .loop__frame + .loop__frame,
  .loop__title + .loop__title,
  .loop__meta + .loop__meta,
  .pair__shot + .pair__shot {
    display: none !important;
  }

  /* A ticker on paper is a row of clothes standing still, which is the correct
     thing for it to be. The transform is reset so the row starts at its first
     card rather than wherever the animation had reached when the page was sent
     to the printer. */
  .marquee__track {
    animation: none !important;
    transform: none !important;
  }
}
/* biome-ignore-end lint/style/noDescendingSpecificity: end of the print reset */
/* biome-ignore-end lint/complexity/noImportantStyles: end of the print reset */

@media (min-width: 60rem) {
  .hero .wrap {
    grid-template-columns: minmax(0, 1fr) minmax(0, 36rem);
    /*
      THE WORDS AND THE PHONE, AND THERE WAS A THIRD AREA HERE.

      A second waitlist form lived in the hero from 2026-08-17 to 2026-08-19, so
      this grid had a `form` row under `text` and a device spanning both. Two
      fields posting to one endpoint read as asking twice, and the one that
      survived is the one below the paragraph that says Monsho is not finished.

      Kept as areas rather than reverted to bare columns: the source order still
      has to put the device after the words for a narrow screen, and naming them
      is what stops the next person reordering the markup to fix the layout.
    */
    grid-template-areas: "text device";
    /*
      Centred against the device.

      This was `stretch` while a caption sat at the far end of the column. With
      the caption gone the column holds two short blocks, and top-aligning them
      beside an 870px phone left most of a screen of nothing underneath. The
      device sets the height here, so the words sit against its middle.
    */
    align-items: center;
  }

  .hero__text {
    grid-area: text;
  }

  /*
   * THE HERO'S DEVICE RISES INTO THE BAND'S TOP PADDING.
   *
   * The padding is there for the words. The device is a picture of a phone and
   * it wants the top of the window, so it takes back all but 1.5rem of that
   * padding and starts a fixed distance under the masthead. The words are
   * centred against it, so they move with it rather than being left at the top.
   *
   * Pulled up rather than given a smaller band, because the band's padding is
   * shared with every other section and the hero is the only one with a 900px
   * object in it.
   */
  .hero .device {
    grid-area: device;
    /*
     * Centred in its column rather than flush to the page's right edge.
     *
     * Flush right put the phone in the corner with most of a column of nothing
     * between it and the words, which reads as two pages that happen to be side
     * by side. Centred, the space falls on both sides of it and the object sits
     * in a column of its own.
     */
    justify-content: center;
    /*
     * Its own alignment, against the window rather than against the words.
     *
     * The wrap centres its items, and at 1280x800 the words are the taller
     * column: the device was centred against THEM, which put it 51px lower than
     * the margin asked for and 29px of it under the fold. The words still
     * centre; the device starts where it is put.
     */
    align-self: start;
    margin-top: calc(1.5rem - var(--band-y));
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE PHONE — the app's chrome, rebuilt. Only the picture inside the card is a
   placeholder file; every other pixel here is drawn from the app's own values.
   Internal dimensions are all multiples of `--u`, so one number scales the whole
   device and the app's type scale stays in proportion to its own chrome.
   ══════════════════════════════════════════════════════════════════════════ */

/*
 * ONE NUMBER SCALES THE WHOLE DEVICE, AND IT ANSWERS TO BOTH VIEWPORT AXES.
 *
 * It was a flat `1px`, which made the device 868px tall — taller than the
 * content area of an ordinary laptop window. The consequence was not a crop, it
 * was that the app was never visible whole at any scroll position: at the top of
 * the page the card and the buttons were below the fold, and scrolling far
 * enough to reach them took the header off the top. A device frame whose whole
 * job is to say "this is an app running" cannot be a thing you assemble from two
 * scroll positions.
 *
 * The deliberate bottom crop is untouched. What this removes is the crop nobody
 * chose.
 *
 * `svh` rather than `vh`, so a mobile browser's collapsing toolbar cannot make
 * the reservation wrong the moment the page is scrolled. The 15rem covers the
 * masthead and the hero band's own top padding; the comment said 11rem for as
 * long as the value was 11rem, and then outlived it by a rewrite.
 *
 * The hero overrides this on a wide screen, where the space above the device is
 * known exactly rather than reserved generously. Everything below the fold keeps
 * this formula, because a reader has already scrolled by the time they meet it.
 */
.phone {
  --u: clamp(0.62px, (100svh - 15rem) / 844, 1px);
  width: calc(414 * var(--u));
  flex: none;
  background: var(--white);
  border: 1px solid var(--line-2);
  border-radius: calc(44 * var(--u));
  padding: calc(12 * var(--u));
  font-family: var(--body);
  color: var(--ink);
}

/*
 * THE CROP IS GONE, AND THE RULES FOR IT WITH IT.
 *
 * There used to be a `margin-bottom: calc(-18 * var(--u))` here, so the hero's
 * device ran off the bottom of the band by a fixed amount. The reasoning was
 * that a thing which continues past an edge does not need a shadow to lift it
 * off the page, and a second rule existed purely to undo it for the phone lower
 * down.
 *
 * What it produced was a phone with its bottom bezel and corner sliced off. That
 * does not read as a crop, it reads as a rendering fault, and it was reported as
 * one twice. `--u` scales the device to the window instead, which answers the
 * same problem — a device too tall for the page — without cutting anything.
 *
 * Deleted rather than commented out, per the repository's rule about superseded
 * prose. This paragraph is here because the crop was a deliberate idea and the
 * next person to have it should know it was tried.
 */

/*
 * On a narrow screen the width is what binds, because the phone sits under the
 * words rather than beside them and the band is free to be tall. The height term
 * stays, with a smaller reservation, for the short-and-narrow case a landscape
 * phone produces — where without it the device is taller than the screen it is
 * being drawn on.
 */
@media (max-width: 30rem) {
  .phone {
    --u: min((100vw - 3.5rem) / 414, (100svh - 6rem) / 844);
  }
}

/*
 * Declared here rather than beside the rest of the wide hero, so specificity
 * ascends down the file: this is `.hero .phone` at (0, 2, 0) and the rule it
 * overrides is `.phone` at (0, 1, 0). Written above it first, which reads as
 * though the later and weaker rule wins.
 */
@media (min-width: 60rem) {
  /*
   * AND IT IS SIZED AGAINST WHAT IS ACTUALLY ABOVE IT.
   *
   * The shared formula reserves 15rem for "the masthead and the band's padding",
   * which is generous by about 70px because it has to hold at every width. Here
   * both numbers are known: the masthead is `--mast-h`, the gap under it is
   * 1.5rem, and 1.5rem is left at the bottom so the device does not touch the
   * fold. The result is a phone about 15% larger with its whole self on screen.
   *
   * 868 rather than 844: 844 is the screen, and the device is the screen plus
   * 12 units of bezel on each side. Dividing by the screen height sized the
   * phone as though its own frame were free, which is the arithmetic that put
   * the bottom bezel under the fold at 1280x800.
   *
   * The 1.3px ceiling is the column, not a taste: 414 units at 1.3 is 538px
   * inside a 36rem track, and past that the device would decide the layout.
   */
  .hero .phone {
    --u: clamp(0.62px, (100svh - var(--mast-h) - 3rem) / 868, 1.3px);
  }
}

/*
 * 844 units tall, which is a 390pt phone's logical height.
 *
 * Stated rather than left to content, because the app's Home surface is one card
 * per viewport: the photograph takes whatever the commerce rows leave, and its
 * shape follows from the device rather than from the picture. A screen sized by
 * its content would show a landscape picture of a body.
 */
/*
 * THE SCREEN IS PAPER, AND IT WAS WHITE.
 *
 * White made the card invisible: a white plate on a white screen is one surface,
 * and v2's whole separation strategy is surface against surface. The app's own
 * `bg` is the warm ground, the card is `surf` white, and the difference between
 * them is what makes the render read as an object standing on something.
 *
 * This also un-breaks the page's own ground rule, which says the phone never
 * sits on the warm ground. It still does not: the warm ground is now INSIDE the
 * phone, where the app puts it, and the band behind the phone is still white.
 */
.screen {
  width: 100%;
  height: calc(844 * var(--u));
  border-radius: calc(32 * var(--u));
  overflow: hidden;
  background: var(--ground);
  display: flex;
  flex-direction: column;
}

/* Header — `Chrome.tsx` Header: px-5 pb-3 pt-2, display step, optional divider. */
/*
 * ══════════════════════════════════════════════════════════════════════════
 * THE PHONE IS DRAWN IN THE v2 LANGUAGE, WHICH HAS NO DIVIDING RULES AT ALL.
 * ══════════════════════════════════════════════════════════════════════════
 *
 * `Design/look-v2/monsho-look-v2.html`, 17 August 2026, supersedes the prototype this
 * phone was drawn from. Its first line is the whole change: "Every dividing rule
 * is gone: separation is carried by surface, corner radius, shadow and space."
 *
 * What replaced each rule, in the design's own words:
 *
 *   image to plate      radius, inset and shadow
 *   button outlines     quiet fill
 *   bar border          blur and shadow
 *   settings dividers   grouped cards and space
 *
 * Two elevations and no more: resting for cards, chips and rows; lifted for the
 * render and the floating bar.
 *
 * THE QUIET FILL IS THE APP'S `surf2`, NOT THE DESIGN'S E6E4DF. Two units
 * apart and indistinguishable, and this file's opening rule is that it never
 * invents a colour the app does not have. When the app's palette gains the
 * design's value this follows it, rather than leading it.
 */
.scr-head {
  padding: calc(14 * var(--u)) calc(20 * var(--u)) calc(12 * var(--u));
}

/*
 * ONE QUIET LINE WHERE A HEADING WAS. The card used to open with "Today" at the
 * app's largest type step over "On you", above a hairline. v2 has no page title
 * inside the card and no rule under it: a small grey line says what the set is,
 * and the dashes say how far through it you are.
 */
.scr-head__line {
  font-size: calc(14 * var(--u));
  line-height: calc(20 * var(--u));
  color: var(--ink-2);
}

/*
 * TEN DASHES, AND THE COUNT LIVES IN THE MARKUP. The set has an end, and this
 * is the only place on the page that shows it rather than saying it.
 */
.progress {
  display: flex;
  gap: calc(4 * var(--u));
  margin-top: calc(12 * var(--u));
}

.progress li {
  height: calc(3 * var(--u));
  flex: 1 1 0;
  border-radius: 999px;
  background: var(--line);
}

.progress li.is-done {
  background: var(--ink);
}

/*
 * THE CARD IS THE PICTURE, AND EVERYTHING ELSE STANDS ON THE PAPER.
 *
 * It was one white box with a 0.30 hairline holding the image, the words and the
 * buttons. In v2 the white card carries the render and nothing else, and the
 * title, the merchant line and the actions sit on the screen's own ground below
 * it. That is what "the render is an object with edges sitting on paper" means,
 * and it is the reason the card reads as a photograph of somebody rather than as
 * a screen background.
 *
 * No border. Radius 26, inside the design's stated 22 to 30. Lifted, which is
 * the second of the two elevations.
 */
.plate {
  position: relative;
  margin: calc(6 * var(--u)) calc(16 * var(--u)) 0;
  background: var(--white);
  border-radius: calc(26 * var(--u));
  box-shadow: 0 calc(12 * var(--u)) calc(38 * var(--u)) var(--shade);
  overflow: hidden;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/*
 * THE STATUS IS A PILL ON THE PICTURE, WHICH IS WHERE v2 PUTS IT.
 *
 * It was a text row under the title. On the picture it is unmissable and it
 * costs the card no height, which is what lets the render fill the plate.
 *
 * `:empty` DRAWS NOTHING, AND THAT IS THE READY STATE. `cardStatusLine`
 * returns null once a render is on screen, so the third beat of every story has
 * an empty span here and no pill appears. A caption about whether a render
 * exists, sitting on a render, is the incoherence the app's own module was
 * written to remove.
 */
/*
 * THE OFFER, ON THE PICTURE IT CHANGES.
 *
 * This was a CAPTION here and in the app: "Not on you yet", top left, in the
 * secondary ink, saying what the card was rather than what could be done about
 * it. `Card.tsx` moved the offer out of the action row and onto the media on
 * 2026-08-24, because the row's third slot held two acts -- spend $0.03 here,
 * and leave for the shop -- and a slot that means two things cannot be the one
 * that means the most. The caption said the same thing the button said, twenty
 * points apart, so it went with it.
 *
 * BOTTOM LEFT, which is where the app puts it. v2 keeps bottom right for the
 * flip control and top left for a caption; this is neither, and the corner is
 * free until the flip control ships.
 *
 * A CONTROL'S TREATMENT AND NOT A CAPTION'S: the quiet fill and the full ink,
 * which is what `Chrome.tsx` gives a secondary button. Not the ink fill -- v2
 * allows one filled action per screen and that one is Buy.
 */
.plate__status {
  position: absolute;
  bottom: calc(14 * var(--u));
  left: calc(14 * var(--u));
  z-index: 1;
}

.plate__status .loop__meta {
  padding: calc(7 * var(--u)) calc(14 * var(--u));
  border-radius: calc(22 * var(--u));
  background: var(--recess);
  color: var(--ink);
  font-weight: 600;
  font-size: calc(13 * var(--u));
  line-height: calc(16 * var(--u));
  white-space: nowrap;
  box-shadow: 0 calc(2 * var(--u)) calc(10 * var(--u)) var(--shade);
}

.plate__status .loop__meta:empty {
  background: none;
  box-shadow: none;
  padding: 0;
}

/* The trust device: one gesture to the shop's own photograph, in the app. */
.plate__real {
  position: absolute;
  right: calc(14 * var(--u));
  bottom: calc(14 * var(--u));
  z-index: 1;
  display: flex;
  align-items: center;
  gap: calc(2 * var(--u));
  padding: calc(8 * var(--u)) calc(14 * var(--u)) calc(8 * var(--u)) calc(8 * var(--u));
  border-radius: 999px;
  background: var(--white);
  color: var(--ink);
  font-size: calc(13 * var(--u));
  font-weight: 500;
  line-height: 1;
  box-shadow: 0 calc(2 * var(--u)) calc(10 * var(--u)) var(--shade);
}

.plate__real svg {
  width: calc(16 * var(--u));
  height: calc(16 * var(--u));
}

/*
 * THE SECOND PHONE'S PICTURE, AND ITS RULE WAS DELETED BY ACCIDENT ON 2026-08-16.
 *
 * The v2 pass removed the dead `.steps` block by slicing between two markers and
 * took `.slot` out with it, because `.slot` sat between them. Nothing failed:
 * the image kept rendering, inherited `object-fit: fill` from nobody, and was
 * drawn 16% narrower than its true shape. A man in a burgundy cardigan, visibly
 * stretched, reported by eye rather than by any test.
 *
 * Restored in the v2 language: no border, `cover` like every other picture in a
 * plate, and it still takes what the card leaves.
 *
 * A DISTORTED BODY IS THE WORST THING THIS PAGE CAN SHOW. The argument two
 * sections down is that nothing here is slimmed or stretched, and a stretched
 * render standing beside that sentence contradicts it in the one way a reader
 * notices without being told.
 */
.slot {
  display: block;
  width: 100%;
  object-fit: cover;
  background: var(--recess);
  flex: 1 1 auto;
  min-height: 0;
}

/*
 * On the paper, under the card. The horizontal padding matches the plate's own
 * margin so the title lines up with the picture's left edge.
 */
.card-body {
  padding: calc(16 * var(--u)) calc(20 * var(--u)) calc(2 * var(--u));
}

.card-title {
  font-size: calc(21 * var(--u));
  line-height: calc(27 * var(--u));
  font-weight: 500;
  letter-spacing: -0.01em;
}

/*
 * THE PRICE ROW AND THE DISCOUNT CHIP ARE GONE, AND SO ARE THEIR RULES.
 *
 * They drew `129,00`, `159,00` and `−19%` next to a brand line reading "Example
 * brand", beside renders that are real output of the real path. Every one of
 * those numbers was invented, which made the only fabricated thing on the page
 * the part that looks most like a commercial claim.
 *
 * There is nothing to put back in their place. The garments are generated
 * fixtures from `fixtures/garments/generated/`; they have a description and no
 * price, because no shop is selling them.
 *
 * THE MERCHANT LINE WENT THE SAME WAY, 2026-08-20, AND FOR THE SAME REASON.
 * `.meta` drew the app's shop name, and on this page there is no shop. It said
 * "Test catalogue", which told a shopper nothing, and then "Not from a shop",
 * which told them something and the something was wrong: in the slot where a
 * shop's name goes, any sentence that is not a shop's name reads as a notice
 * that this card is defective. The problem was the slot rather than the words,
 * so there was no third wording to try.
 *
 * It stays true where a reader can act on it: the paragraph under "Be there on
 * the first day" says the catalogue is a test set with no shop links, and
 * `public/media/README.md` names the source of every file on the page.
 *
 * The rules are deleted rather than left unused. Dead CSS for an element that
 * was removed on purpose is an invitation to put the element back.
 */

/* The action row — three equal buttons, exactly one of them filled. */
.acts {
  display: flex;
  align-items: center;
  gap: calc(8 * var(--u));
  padding: calc(4 * var(--u)) calc(16 * var(--u)) calc(16 * var(--u));
}

/*
 * QUIET FILL REPLACED THE OUTLINE, WHICH IS ONE OF v2'S FOUR NAMED SWAPS.
 *
 * A secondary button was a 0.30 hairline around nothing. It is the app's own
 * `surf2` now, which reads as a control without drawing a line, and the radius
 * is a full pill rather than 12.
 */
.btn {
  flex: 1 1 0;
  min-height: calc(46 * var(--u));
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(6 * var(--u));
  border-radius: 999px;
  font-size: calc(14 * var(--u));
  font-weight: 500;
  white-space: nowrap;
  background: var(--recess);
  color: var(--ink);
}

/* One filled action per screen, which is the design's own rule. */
.btn--primary {
  background: var(--ink);
  color: var(--white);
}

/*
 * A FLOATING BAR: BLUR AND SHADOW WHERE A BORDER WAS.
 *
 * The last of v2's four named swaps. It was a flat row across the bottom of the
 * screen with a hairline above it and a word under every glyph. It is a pill
 * standing on the paper now, lifted like the render, and the labels are gone:
 * five icons a stranger reads without being taught, which is the same argument
 * that turned Saved into a heart.
 *
 * `backdrop-filter` is the design's blur. It degrades to a plain white pill
 * where it is unsupported, which is the correct failure.
 */
.tabs {
  margin: calc(10 * var(--u)) calc(16 * var(--u)) calc(14 * var(--u));
  display: flex;
  align-items: center;
  padding: calc(8 * var(--u));
  border-radius: 999px;
  background: rgb(255 255 255 / 0.82);
  backdrop-filter: blur(calc(14 * var(--u)));
  box-shadow: 0 calc(6 * var(--u)) calc(24 * var(--u)) var(--shade);
}

.tab {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: calc(40 * var(--u));
  border-radius: 999px;
  color: var(--ink-2);
}

.tab svg {
  width: calc(21 * var(--u));
  height: calc(21 * var(--u));
}

/* The current tab is a quiet fill behind the glyph, not a word in bold. */
.tab--on {
  background: var(--recess);
  color: var(--ink);
}

/* ══════════════════════════════════════════════════════════════════════════
   SEQUENCE — genuinely a sequence, so it is numbered. Hairlines and a hanging
   ordinal, not three columns with icons in them.
   ══════════════════════════════════════════════════════════════════════════ */

.steps li::before {
  content: counter(step);
  font-family: var(--display);
  font-size: var(--t-sm);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
  padding-top: 0.35em;
}

.steps p {
  margin-top: 0.5rem;
  color: var(--ink-2);
}

/* A plain list of facts, one per line, separated by hairlines. Used for what
   Monsho does not do, and for the shop-facing band. */
.facts {
  max-width: 48rem;
}

/*
 * GROUPED BY SPACE, NOT CLOSED BY A LINE. The privacy notice's fact lists were
 * rows between hairlines, which is the "settings dividers" pattern v2 replaced
 * with grouped cards and space. A notice somebody has to read is exactly where
 * that matters: a stack of ruled rows reads as a form to be endured.
 */
.facts li {
  padding: 0 0 1.6rem;
}

.facts li:last-child {
  padding-bottom: 0;
}

.facts p {
  margin-top: 0.4rem;
  color: var(--ink-2);
}

/* ══════════════════════════════════════════════════════════════════════════
   THE SPEC LIST — for shops and networks.

   A different audience reading for a different reason. A merchant compliance
   team or a network reviewer wants to answer "what is this, who pays whom, and
   where does the data sit" in about ten seconds, and prose is the wrong shape
   for that. A term and a fact per row, hairline separated, nothing to read
   twice. It is the same austerity as the rest of the page pointed at somebody
   scanning rather than somebody being persuaded.
   ══════════════════════════════════════════════════════════════════════════ */

.spec {
  margin-top: 2rem;
  max-width: 54rem;
}

.spec dt {
  font-family: var(--display);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-2);
  padding-top: 1rem;
}

.spec dd {
  margin: 0;
  /*
     GROUPED ROWS AND SPACE, WHICH IS v2'S FOURTH NAMED SWAP: "settings dividers
     → grouped cards and space". Eight rows each closed by a hairline read as a
     spreadsheet; the space between them does the same work and lets the pairs
     group.
  */
  padding: 0.35rem 0 1.6rem;
}

@media (min-width: 40rem) {
  .spec {
    display: grid;
    grid-template-columns: 11rem minmax(0, 1fr);
    column-gap: 2.5rem;
  }

  /*
     At width the pairs sit side by side, and the row rule under the label was
     the last divider on the page. The column gap and the space below each pair
     separate them now, which is the same swap the rest of this file took.
  */
  .spec dt {
    padding-bottom: 1.6rem;
  }

  .spec dd {
    padding-top: 0;
  }
}

/*
 * A heading and the block under it are one unit, and they were touching.
 *
 * Declared here, after every one of the four blocks it can precede, so the
 * cascade reads base-then-context throughout the file. It only applies where
 * the two stack; in the two-column bands they are siblings in different columns
 * and this never fires.
 */
h2 + .prose,
h2 + .lead,
h2 + .steps,
h2 + .facts {
  margin-top: 1.5rem;
}

.two-col {
  display: grid;
  gap: clamp(2rem, 4vw, 3.5rem);
}

/*
 * GRID ITEMS CANNOT SHRINK BELOW THEIR CONTENT UNLESS TOLD TO, AND THAT COST US
 * THE PAGE AT LARGE TEXT.
 *
 * A grid item's `min-width` defaults to `auto`, which means its min-content
 * width, not zero. The waitlist's column contains an `<input>`, and an input
 * carries an intrinsic width of about twenty characters that grows with the font
 * size. At a 32px root, which is what a reader gets when they double the text
 * size, that column demanded 554px inside a 294px container and the whole page
 * scrolled sideways: **602px of content in a 390px viewport**. WCAG 1.4.4 and
 * 1.4.10 both fail on exactly that.
 *
 * The field already had `min-width: 0`, which is why this took measuring to
 * find: the field could shrink and its COLUMN could not.
 */
.two-col > * {
  min-width: 0;
}

@media (min-width: 60rem) {
  .two-col {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: start;
  }

  /* A column of words beside a device. The device sets the height, so the words
     sit against its middle rather than at the top of a column twice their
     length. */
  .two-col--device {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
  }
}

/* The picture band runs closer to the page edge than a band of words does. */
.wrap--wide {
  padding-left: clamp(0.75rem, 2vw, 2rem);
  padding-right: clamp(0.75rem, 2vw, 2rem);
}

/* ══════════════════════════════════════════════════════════════════════════
   THE MARQUEE — eight garments on one body, going past.

   A continuous ticker, right to left, the way a page shows the partners it works
   with. Not a carousel: no arrows, no dots, no pause control and nothing to
   operate.

   THIS BAND HAS HAD THREE SHAPES IN A DAY, and each failed for the reason the
   next one answers. Eight tiles at once was a photo dump — everything at the
   same volume, nothing to stop on. One at a time with controls made a reader
   click eight times to see eight garments, so most saw one. Autoplay with a
   pause control fixed the clicking and left the band still looking like
   something to operate. A ticker is not operated; it is watched or ignored,
   which is the right relationship to have with a rail of clothes.

   ─────────────────────────────────────────────────────────────────────────
   ONE ANIMATION ON ONE ELEMENT, AND NO SCRIPT
   ─────────────────────────────────────────────────────────────────────────

   `enhance.js` no longer touches this band at all. It runs identically with the
   script blocked, and there is no state that can fall out of step with a scroll
   position — which is what the previous two versions each had to keep in line.

   THE SET IS IN THE MARKUP TWICE AND THAT IS THE WHOLE MECHANISM. Translating
   the track by exactly -50% lands on the identical frame, so the loop has no
   seam. The spacing is a MARGIN on each card rather than a `gap` on the track,
   because a gap is not part of an element's width the way a margin is: with a
   gap, -50% lands one gap short of the seam and the row stutters once per cycle.
   ══════════════════════════════════════════════════════════════════════════ */

.band--rail > .wrap {
  /* The label names the rail, so nothing belongs between them. The band's own
     bottom padding moves below the rail instead, where the gap is wanted. */
  padding-bottom: 0;
}

.band--rail {
  padding-bottom: var(--band-y);
}

.marquee {
  /* Full bleed, edge to edge, outside `.wrap`. A ticker that stops at a page
     gutter reads as a component rather than as a rail. */
  overflow: hidden;
  margin-top: 1.25rem;
}

.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee 64s linear infinite;
}

@keyframes marquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

.marquee__card {
  flex: 0 0 auto;
  width: clamp(11rem, 18vw, 15rem);
  margin-right: clamp(0.5rem, 1vw, 0.9rem);
}

.marquee__shot {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  /*
   * ROUNDED, WHICH IS AN EXCEPTION TO THE RULE AT THE TOP OF THIS FILE.
   *
   * That rule says the device frame is the only skeuomorphic element and that
   * nothing else gets a rounded card. Square corners on a rail of clothes read
   * as unnaturally hard next to how every shop that sells this way draws them,
   * and the exception was asked for by name.
   *
   * It is 12px, not the app's 16: these are pictures at a third of the phone's
   * width, and the app's own radius at this size reads as a bubble. Applied to
   * the picture rather than to a card, because there is no card — no border, no
   * fill, no shadow. The rule the page still holds to is that nothing outside
   * the phone floats.
   */
  border-radius: 12px;
  /* The app's `surf2`, so a picture that has not decoded yet is the same grey
     the product shows rather than a flash of white. */
  background: var(--recess);
}

.marquee__name {
  margin-top: 0.7rem;
  font-family: var(--display);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-2);
  /*
   * Two lines reserved, wrapping rather than truncating.
   *
   * It was one line with an ellipsis, which cut "Striped navy and white cotton
   * long-sleeve top" in half — the longest name being the one most worth
   * reading. The reserved height is what keeps the cards level: a ticker whose
   * cards change height as they pass wobbles.
   */
  line-height: 1.35;
  min-height: calc(2 * 1.35em);
}

/*
 * NOTHING MOVES FOR A READER WHO ASKED IT NOT TO, and the row becomes scrollable
 * so every garment is still reachable.
 *
 * There is no visible pause control, by request. The preference is the
 * mechanism, which is the usual accommodation for a ticker — and it is why this
 * block does more than stop the animation: an unmoving marquee inside
 * `overflow: hidden` would put six of the eight garments permanently out of
 * reach, which is worse than the motion.
 */
/* biome-ignore-start lint/complexity/noImportantStyles: this has to beat the animation declared above it, the same mechanism as the other two reduced-motion blocks in this file. */
@media (prefers-reduced-motion: reduce) {
  .marquee {
    overflow-x: auto;
  }

  .marquee__track {
    animation: none !important;
    transform: none !important;
  }
}
/* biome-ignore-end lint/complexity/noImportantStyles: end of the reduced-motion marquee reset */

/* ══════════════════════════════════════════════════════════════════════════
   THE WAITLIST — one field, one button, and five sentences that are already
   here.

   The form posts to a same-origin endpoint and works with nothing running. What
   the script adds is that a submission does not navigate away; what it does NOT
   add is any text, because all five outcomes are in the markup and the
   stylesheet reveals the one the endpoint named. That is what keeps
   `enhance.js` free of `textContent`, and the no-JavaScript page complete.

   No white surface here. This band carries no imagery, so the field is the warm
   ground with a hairline round it, and the ground rule holds.
   ══════════════════════════════════════════════════════════════════════════ */

.signup {
  margin-top: 0.5rem;
}

.signup__label {
  display: block;
  font-family: var(--display);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-2);
  margin-bottom: 0.85rem;
}

.signup__row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/*
 * The same automatic-minimum trap as `.two-col > *`, one level down. A flex
 * item's `min-width` is `auto`, so the button could not shrink below its own
 * label: at a 32px root "Email me when it opens" plus its padding wanted 489px
 * in a 294px row, and the page scrolled sideways again after the column above it
 * had already been fixed. It wraps onto two lines now instead.
 */
.signup__row > * {
  min-width: 0;
}

.signup__field {
  flex: 1 1 15rem;
  min-width: 0;
  font: inherit;
  color: var(--ink);
  /*
     Quiet fill and a pill, the same swap the card's buttons took. A field drawn
     as an outlined rectangle was the last square corner on the page.
  */
  background: var(--recess);
  border: 0;
  border-radius: 999px;
  padding: 0.85rem 1.25rem;
}

.signup__field::placeholder {
  color: var(--ink-2);
}

.signup__field:hover {
  border-color: var(--ink);
}

.signup__go {
  /*
     `0 1 auto`, not `0 0 auto`. Shrink was off, which meant the button kept its
     natural width no matter what: at a 32px root it wanted 489px in a 294px row
     and pushed the page sideways. `min-width: 0` on the row did nothing while
     shrink was disabled, which is why this took a second measurement to find.
     It wraps its label onto two lines now rather than the page scrolling.
  */
  flex: 0 1 auto;
  font: inherit;
  font-weight: 500;
  color: var(--white);
  background: var(--ink);
  border: 0;
  border-radius: 999px;
  padding: 0.85rem 1.75rem;
  cursor: pointer;
}

.signup__go:disabled {
  opacity: 0.45;
  cursor: default;
}

/* Every outcome is present and none is shown until the endpoint has named one.
   `aria-live` is on the container, so the line that appears is announced
   without the reader losing where they were. */
.signup__line {
  display: none;
  margin: 0;
  margin-top: 1rem;
  color: var(--ink);
}

.signup[data-state="joined"] .signup__line[data-result="joined"],
.signup[data-state="invalid"] .signup__line[data-result="invalid"],
.signup[data-state="limited"] .signup__line[data-result="limited"],
.signup[data-state="busy"] .signup__line[data-result="busy"],
.signup[data-state="unavailable"] .signup__line[data-result="unavailable"] {
  display: block;
}

/* Once somebody is on the list there is nothing left to ask them for, so the
   field and its label go and the sentence stands on its own. */
.signup[data-state="joined"] .signup__label,
.signup[data-state="joined"] .signup__row {
  display: none;
}

.signup__terms {
  margin-top: 1.25rem;
  font-size: var(--t-sm);
  color: var(--ink-2);
}

/* ══════════════════════════════════════════════════════════════════════════
   A BAND OF WORDS IS SHORTER THAN A BAND OF PICTURES.

   The bands that carry no image were the same height as the ones that do, which
   left four hundred pixels of nothing under three paragraphs. The answer is less
   band, not more padding.
   ══════════════════════════════════════════════════════════════════════════ */

.band--tight > .wrap {
  padding-top: clamp(2.75rem, 4.5vw, 4.25rem);
  padding-bottom: clamp(2.75rem, 4.5vw, 4.25rem);
}

/* ══════════════════════════════════════════════════════════════════════════
   FOOTER
   ══════════════════════════════════════════════════════════════════════════ */

.footer {
  /* Space rather than a rule. The band above it ends; that is enough. */
  margin-top: var(--band-y);
}

.footer .wrap {
  padding-top: 3rem;
  padding-bottom: 3rem;
}

/*
 * The small print sits above the wordmark rather than beside it, so it reads as
 * a closing note rather than as a third column competing with the two links.
 */
.footer__note {
  max-width: var(--measure);
  margin-bottom: 2.5rem;
  font-size: var(--t-sm);
  color: var(--ink-2);
}

.footer__row {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem 3rem;
  align-items: baseline;
  justify-content: space-between;
}

.footer ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 2rem;
  font-size: var(--t-sm);
}

.footer .note {
  max-width: 30rem;
}

/* ══════════════════════════════════════════════════════════════════════════
   PRIVACY — one column, the same bands, no phone.
   ══════════════════════════════════════════════════════════════════════════ */

.doc .wrap {
  max-width: 48rem;
  margin-inline: 0;
  padding-left: var(--gutter);
}

.doc h2 {
  margin-top: 3.5rem;
  margin-bottom: 1rem;
  font-size: var(--t-lead);
  font-weight: 500;
  letter-spacing: -0.01em;
}

.doc h2:first-of-type {
  margin-top: 2.5rem;
}

.doc .facts li {
  padding: 0.9rem 0;
}

/* ══════════════════════════════════════════════════════════════════════════
   PARTNERS — the merchant page. Same bands, no phone, no loop.

   ONE RULE, AND IT IS ABOUT THE GAP UNDER A HEADING. Every other page puts its
   headings inside `.prose`, which spaces its own children, or inside `.doc`,
   which sets the margins itself. This page puts a heading directly above a
   `.measure` or a `.spec`, and the global reset leaves headings at margin zero,
   so the lead sat against the descenders of the headline above it.

   Deliberately not solved by adding `.doc`: that class also pins a 48rem column
   and a left-aligned wrap, which is the privacy notice's shape rather than this
   one's. The spec tables want the wider measure they already have.
   ══════════════════════════════════════════════════════════════════════════ */

.partners h1,
.partners h2 {
  margin-bottom: 1.25rem;
}

/*
  AND THE FIRST SCREEN IS A MEASUREMENT, NOT A HOPE.

  The three answers a compliance review checks first have to be on it. They were
  not: at 390 by 844, the phone this whole site is drawn against, "Paid links"
  started at 856 and the fold is at 844. Twelve points, which is the difference
  between a page that answers a reviewer and a page that asks them to scroll to
  find out whether it does.

  The list is tightened rather than the facts cut. A term and its answer cost
  their own height plus the space around them, and the space was set for a table
  read on its own rather than one that has to clear a fold. Nothing here changes
  what the page says. `.spec` has exactly one user, so this is scoped to it by
  name rather than by accident.
*/

.partners .spec {
  margin-top: 1.25rem;
}

.partners .spec dt {
  padding-top: 0.5rem;
}

.partners .spec dd {
  padding-bottom: 1.1rem;
}
