/* fairhandapp.com — the homepage layer
   ============================================================================
   Loaded ONLY by index.html. style.css is left alone deliberately: /support,
   /privacy and /terms share it, those three pages are linked from inside the
   app itself, and their contrast numbers have been measured against its exact
   values. Nothing on the homepage is worth reopening that.

   So this file may add, and may restyle what only the homepage sets, but it
   inherits every colour, font and grain value from style.css's :root rather
   than declaring its own. One palette, one theme, still taken from the app's design tokens — and the theme is LIGHT, in every light. See style.css,
   "one theme only": nothing here may introduce a prefers-color-scheme query.

   The app's standing rules still apply here:
     * nothing is set in caps;
     * the editorial serif SPEAKS, the system sans WORKS;
     * nothing floats on a drop shadow — cards sit on surface and a hairline.
   The one shadow on the page is the light on the 3D phone, which is a rendered
   object rather than a CSS card.
   ========================================================================== */

/* ------------------------------------------------------------- structure --- */

.wrap {
  width: min(100% - 2 * var(--gutter), var(--wide));
  margin-inline: auto;
}

.section {
  padding-block: clamp(3.5rem, 9vw, 7rem);
}

/* ⚠️ THE FOOTER IS SHARED AND ITS COLUMN IS NOT THIS PAGE'S. It is built for the
   document pages, where the gutter is padding INSIDE a `--wide` box; here the
   gutter is taken out of the width instead, so on a wide screen the footer's
   words sat a gutter further in than every section above them. Same spine now,
   and the hairline (style.css) runs the full column because the padding that
   used to inset it is gone. */
footer.site {
  width: min(100% - 2 * var(--gutter), var(--wide));
  max-width: none;
  padding-inline: 0;
}

footer.site::before { inset: 0 0 auto; }

/* The head holds two things that want different measures: a display line that
   wants to break early and a lede that wants a reading width. Sizing the block
   to the headline starved the lede to about 38 characters a line. */
.section-head {
  max-width: 48ch;
  margin-bottom: clamp(2rem, 4vw, 3rem);
}

.section-head h2 {
  font: 600 clamp(2rem, 5.2vw, 3.25rem)/1.04 "Fairhand Display", Georgia, serif;
  letter-spacing: -0.02em;
  margin: 0;
  max-width: 16ch;
  text-wrap: balance;
}

/* The line under a section's heading, in the working sans: the app's voice for
   everything it says back. It was a handwriting face, and set in it these lines
   were the hardest thing on the page to read, so the site dropped it.

   The top margin clears the BRUSHED UNDERLINE on the heading above, which is
   painted into the heading's own box and hangs below its baseline. */
.subtitle {
  font: 500 clamp(1.125rem, 1.9vw, 1.3rem)/1.45 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  color: var(--slate);
  margin: 0.75rem 0 0;
  text-wrap: pretty;
}

/* ⚠️ A LITTLE HEAVIER THAN THE BODY, 450 against 400: at 17px on warm paper the
   regular weight went thin, and the site's sans carries the half step. The line
   under a heading takes 500, a step above that again.

   ⚠️ ONE SIZE FOR RUNNING TEXT, the body's 1.0625rem (17px): a lede, a note, a
   caption and a table row all read at the size of the page's paragraphs. They
   had drifted between 14 and 20px, and the small end is what made the pricing
   section and the closing panel feel like fine print. UI labels (a pill, a
   price's small print, a group name) stay smaller on purpose. */
.section-head p.lede {
  color: var(--slate);
  margin: 1rem 0 0;
  max-width: 46ch;
  font-size: 1.0625rem;
  font-weight: 450;
}

/* ------------------------------------------------------------------ hero ---
   Two columns on a wide screen, stacked on a narrow one. The phone is second
   in the source so that a screen reader and a no-CSS render both get the
   sentence before the ornament. */

/* style.css already centres .hero and gives it the page gutter, so only the
   two-column behaviour and the vertical rhythm are set here. */
.hero {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  padding-block: clamp(2.5rem, 7vw, 5.5rem) clamp(3rem, 7vw, 6rem);
  /* The stage's glow is inset -6% and blurred, and on a window where the stage
     meets the wrap's 24px gutter it reached 2px past the edge: a sideways
     scrollbar at 1024px, measured. The hero is the full width of the window,
     so clipping here clips nothing the window was not already clipping. `clip`
     rather than `hidden` so it makes no scroll container and leaves the glow
     free vertically; and here rather than on <body>, for style.css's reason. */
  overflow-x: clip;
}

/* ⚠️ ONE CENTRED COLUMN, where this was two columns from 62rem — copy beside a
   phone — until 2026-09-20. It is now the copy, then BOTH devices, then the
   button, because the tablet is landscape and a landscape device in half a row
   is a small device.

   Everything carrying a measure needs auto margins as well as centred text, or
   it centres its box and leaves the words where they were. */
.hero { justify-items: center; text-align: center; }
.hero-eyebrow img { margin-inline: auto; }
.hero .features { justify-content: center; }
.hero .cta { display: flex; justify-content: center; width: 100%; }


/* The logo opens the hero, small, as its eyebrow. */
.hero-eyebrow { margin: 0 0 clamp(1rem, 2.4vw, 1.5rem); line-height: 0; }
.hero-eyebrow img { display: block; width: auto; height: clamp(1.35rem, 2.6vw, 1.7rem); }

/* In ink, the way the app sets its greeting, with the ochre stroke under "can
   actually" as the one colour in the line. 13.99:1 on the grained paper.
   `balance` keeps the sentence from leaving one word on its last line. */
.hero h1 {
  font: 600 clamp(2.3rem, 4.2vw, 3.2rem)/1.04 "Fairhand Display", Georgia, serif;
  color: var(--ink);
  letter-spacing: -0.025em;
  margin: 0;
  max-width: 20ch;
  text-wrap: balance;
}

.hero .lede {
  color: var(--slate);
  max-width: 40ch;
  margin: 1.5rem 0 0;
  font-size: 1.0625rem;
  font-weight: 450;
  text-wrap: pretty; /* no lone "them." on a phone */
}

/* ⚠️ AFTER the two rules above, and that is the whole point. Both set `margin`
   as a SHORTHAND, which writes 0 into the left and right — so an auto margin
   declared earlier, at the same one-class-plus-element specificity, loses on
   source order and the box stays pinned to the left of the column while its
   text centres inside it. Measured when it was wrong: the lede's centre sat 55px
   left of the window's. The same trap took the stage's height earlier the same
   day; it is a shorthand overwriting a longhand, twice. */
.hero h1,
.hero .lede,
.hero .features { margin-inline: auto; }

/* ---------------------------------------------------------- the features ---
   The app's tag pill, as its filters draw one that is not chosen: a word in ink,
   regular weight at 13, inside a 1px outline in the app's control border
   (#8C8271, the site's --edge), with no fill, so the paper shows through as it
   does in the app. 12 across and 8 down, the app's own padding.

   The app deckles the capsule's outline and then presses the whole tag, word
   and outline together. Both passes are here: the outline is its own layer
   (::before) run through `#deckle`, so the edge undulates without moving the
   word, and the tag as a whole takes `#ink-press-fine`, the press at a tag's
   size (the heading press would tear a 13px label). See index.html for both. */
.features {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  max-width: 34rem;
  margin: 1.5rem 0 0;
  padding: 0;
  list-style: none;
}

.features li {
  position: relative;
  padding: calc(0.5rem + 1px) calc(0.75rem + 1px); /* the outline's pixel, now drawn by ::before */
  border-radius: 999px;
  color: var(--ink);
  font: 400 0.8125rem/1.2 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  white-space: nowrap;
  filter: url(#ink-press-fine);
}

.features li::before {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--edge);
  border-radius: inherit;
  filter: url(#deckle);
  pointer-events: none;
}

/* The pills answer a pointer the way the table's rows do: the word and its
   drawn outline both take the household blue, nothing moves, and the deckle
   stays. Behind `hover: hover` so a finger scrolling past never triggers it,
   and on `color`/`border-color` alone so it costs no layout. */
@media (hover: hover) {
  .features li {
    transition: color 0.16s ease;
  }
  .features li::before {
    transition: border-color 0.16s ease;
  }
  .features li:hover {
    color: var(--accent);
  }
  .features li:hover::before {
    border-color: var(--accent);
  }
}

/* ------------------------------------------------------------ the stage ---
   The phone's ground. A painted wash rather than a box: two soft radial fields
   in the brand's own blue and ochre at the PaintedWash densities the app uses,
   so the phone sits in light instead of on a card. */

.stage {
  position: relative;
  aspect-ratio: 3 / 4;
  max-height: 78vh;
  min-height: 22rem;
  isolation: isolate;
}

/* ⚠️ AFTER the base rule above, deliberately: both selectors are a single
   class, so this only wins on source order. Placed before it, the 3/4 and the
   78vh cap below took over and the stage came out 809px tall instead of 624.

   THE STAGE CHANGES SHAPE, and it has to. Its 3/4 is sized for a phone
   standing up, and that is still what a narrow window gets: the pair is gated
   off below 48rem (home.js), so a phone-width stage holds a phone. From 48rem
   the box goes wide for the two of them — 100/60, the proportion that was
   settled on the bench. A wide box on a narrow screen would have shrunk the
   flat phone to its height and made the hero worse for the readers most likely
   to be on one. */
@media (min-width: 48rem) {
  .stage {
    aspect-ratio: 100 / 60;
    max-height: none;
    width: 100%;
  }
}

.stage::before {
  content: "";
  position: absolute;
  inset: -6%;
  z-index: -1;
  background:
    radial-gradient(58% 46% at 62% 30%,
      rgba(var(--accent-rgb), var(--wash-a)) 0%, transparent 70%),
    radial-gradient(52% 44% at 30% 76%,
      rgba(216, 149, 47, var(--wash-c)) 0%, transparent 72%);
  filter: blur(6px);
}

.stage canvas {
  display: block;
  width: 100%;
  height: 100%;
  cursor: grab;
  touch-action: pan-y;        /* the page still scrolls through the phone on a touch screen */
}

.stage.dragging canvas { cursor: grabbing; }

/* The flat phone. It is what everyone sees first, what a browser with no
   WebGL keeps, and what `prefers-reduced-motion` is given: the same capture,
   in a drawn bezel, with no canvas behind it. `.flat` is set on <body> by
   home.js the moment WebGL fails, and never unset. */
/* OUT OF FLOW. The canvas home.js appends is an in-flow child of .stage, so a
   flat phone that is also in flow stacks above it and pushes the render out of
   the hero entirely — which is exactly what happened when this was briefly
   `position: relative`. Absolute with inset:0 also gives the <img> below a
   definite box to resolve its percentage max-height against. */
.stage-flat {
  position: absolute;
  inset: 0;
}

/* Absolutely positioned with inset:0 and margin:auto, rather than centred in a
   grid. A percentage max-height on a GRID ITEM resolves against a grid area
   that is only definite when the item stretches — and this one is centred, so
   `max-height: 100%` was ignored and the capture drew at its natural 480x1043,
   spilling 143px past the stage on a phone. Against an absolutely positioned
   box the percentage resolves against the padding box, which is always
   definite here because .stage sizes itself. */
.stage-flat img {
  position: absolute;
  inset: 0;
  margin: auto;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  border-radius: clamp(1.2rem, 3.4vw, 2rem);
  border: 1px solid var(--rule);
  background: var(--surface);
}

/* Once the canvas is live the flat phone steps aside — but it stays in the DOM
   so that a WebGL context lost later has something to fall back to. */
body.webgl .stage-flat { visibility: hidden; }
body.flat .stage canvas { display: none; }

/* -------------------------------------------------------------- the badge ---
   Inherited from style.css; the homepage only places it. */

.cta { margin-top: clamp(1.75rem, 4vw, 2.5rem); }
.cta-note { color: var(--slate); font-size: 0.9375rem; margin: 0.9rem 0 0; }

/* ------------------------------------------------------- set the table ---
   One person at a time, beside the week. See the top of the table code in
   home.js for what this replaced and why; the short version is that a tap and
   its answer were never on screen together, and now they are on every screen.

   With no script, every panel shows (stacked) and there are no tabs — see
   index.html. home.js adds the tabs and hides all but one panel. */

.table-grid {
  display: grid;
  gap: clamp(1.5rem, 4vw, 2.5rem);
}

@media (min-width: 62rem) {
  .table-grid {
    grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
    gap: 3rem;
    align-items: start;
  }
}

/* ---- the tabs: a face per person ---------------------------------------- */

/* Under the card, and moved there in the markup. */
.person-tabs {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.45rem;
  margin-top: 1rem;
}

.person-tabs[hidden] { display: none; }

/* ⚠️ No box and no ring on the chosen face: they were hard black borders
   around one person. The choice reads by contrast with the others instead: the chosen face at
   full strength and its name in semibold ink, the rest washed back with their
   names in stone. Hover stops short of full strength, so pointing at a face
   never looks like having chosen it. */
.person-tab {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  padding: 0.45rem 0.3rem 0.55rem;
  border: 0;
  border-radius: 12px; /* the focus ring's shape */
  background: none;
  cursor: pointer;
  font: 500 0.875rem/1 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  color: var(--slate);
}

.person-tab .face { width: 3.4rem; height: 3.4rem; opacity: 0.6; transition: opacity 0.15s ease; }
.person-tab:hover .face { opacity: 0.8; }
.person-tab[aria-selected="true"] { color: var(--ink); font-weight: 600; }
.person-tab[aria-selected="true"] .face { opacity: 1; }
.person-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A face, not a figure: the small portrait scaled up and centred on the face.
   Each centre was located by eye on the exported art, so a re-export that
   moves a figure in its frame moves these too. */
.face {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: 999px;
  background: #f2e6d8;
  flex: none;
}

.face img {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 230%;
  max-width: none;
  height: auto;
  transform: translate(calc(var(--fx) * -1), calc(var(--fy) * -1));
}

.face.p1 { --fx: 52%; --fy: 30%; }
.face.p2 { --fx: 61%; --fy: 21%; }
.face.p3 { --fx: 61%; --fy: 31%; }
.face.p4 { --fx: 61%; --fy: 26%; }

/* ---- a person ----------------------------------------------------------- */

.household { display: grid; gap: 0.875rem; }

.person-panel {
  display: grid;
  gap: 1rem;
  padding: 1rem;
  border: 1px solid var(--rule);
  border-radius: 16px;
  background: var(--surface);
}

.person-panel[hidden] { display: none; }

/* The painted person, large. Their ground is #F2E6D8 against a #FCF8EF card —
   close but not equal — so the portrait keeps its own rounded field rather than
   floating on the card, the way a dish tile keeps its own paper. */
.portrait {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 4.2;
  object-fit: cover;
  object-position: 50% 20%;
  border-radius: 12px;
  background: #f2e6d8;
}

/* A person's name is SANS: no serif for people's names or dish titles. The
   app agrees for rows: its row titles are DM Sans, and the serif is kept for
   display type. The site's sans rather than DM Sans, as for
   every other piece of UI text here. */
.person-name {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 0.35rem;
  font: 600 1.25rem/1.2 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}

/* The member's token beside the name, at the app's `Wedge.medium` (34pt) — the
   size it draws a member in a form row. */
.person-name .token { font-size: 2.15rem; }

/* ---- a member's token: mark + initial ----------------------------------------
   The app's member initial. The letter is 0.44 of the mark, as in the app,
   centred, system sans semibold, and white. The mark is 1.05em of the token's
   font-size, so 0.44 x 1.05 = 0.462em sets the letter against the mark rather
   than against the token. Both occupy one grid cell, so they share a centre.

   ⚠️ The initial wears the ink press, as the app's does. The press has a measured ~22px floor,
   but that floor is about THIN SERIFS breaking into holes; a semibold sans
   capital has none, and it was checked at 2x before this shipped. */
.token {
  display: inline-grid;
  place-items: center;
  flex: none;
  line-height: 1;
}

.token > * { grid-area: 1 / 1; }

.token-initial {
  color: var(--on-accent);
  font: 600 0.462em/1 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  transform: translateY(0.03em); /* a capital's optical centre sits a hair low of its box */
  filter: url(#ink-press);
}

.person-prompt { margin: 0 0 0.7rem; color: var(--slate); font-size: 1.0625rem;
  font-weight: 450;
}
.person-prompt[hidden] { display: none; }

/* ⚠️ THE PHONE KEEPS THE POSTER. This was a 7rem thumbnail beside the words, and
   it cost the section its argument: the painted household IS why anyone believes
   a solver knows who is eating, and at 112px the person was furniture. The card
   stays stacked — picture, who it is, what they can't eat — and the picture takes
   a shallower crop so the card is about 430px rather than 500. 4/3.4 at 14% is the
   measured limit: any shallower loses the hands and the bowl, which is the half of
   these drawings that says the person is at a table. */
@media (max-width: 40rem) {
  .portrait { aspect-ratio: 4 / 3.4; object-position: 50% 14%; }
  .person-name { font-size: 1.15rem; }
}

/* ---- a constraint --------------------------------------------------------

   FILLED MEANS ON, OUTLINED MEANS OFF — one look for the state, whatever the
   kind. It used to have two: an allergy filled crimson while a diet only
   darkened its outline, so a reader could not tell at a glance what was ticked.

   ⚠️ A DIET FILLS WITH INK, NEVER WITH THE MEMBER'S PAINT. It filled with the
   member's colour once and could not be made to pass: on the vermillion, bone
   measures 4.14:1 and ink 3.93:1, so no label is legible on that fill at any
   ink, and repainting a person's identity colour to suit a chip is the wrong
   trade. Bone on ink is 15.3:1. Whose rule it is needs no colour now — the
   panel is one person.

   An ALLERGY fills with the alarm, because it is a hard limit rather than a
   preference — the difference this page is about. Bone on the alarm is 8.56:1,
   and the alarm sits outside the member palette so it can never read as a
   person.

   Two spellings of one state: `.is-on` on the authored spans, `aria-pressed` on
   the live buttons. They must look identical — a reader can see one replaced by
   the other. */

.chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.5rem 0.75rem 0.5rem 0.6rem;
  border: 1.5px solid var(--edge);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font: 500 0.875rem/1 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.chip:hover { border-color: var(--ink); }
button.chip:active { transform: translateY(1px); }

/* The site's own diamond — hollow when off, solid when on. */
.chip-box {
  flex: none;
  width: 0.55rem;
  height: 0.55rem;
  border: 1.5px solid currentColor;
  border-radius: 1px;
  transform: rotate(45deg);
  opacity: 0.55;
}

.chip[aria-pressed="true"],
.chip.is-on {
  background: var(--ink);
  border-color: transparent;
  color: var(--on-accent);
}

.chip[aria-pressed="true"] .chip-box,
.chip.is-on .chip-box { background: currentColor; opacity: 1; }

.chip.allergy[aria-pressed="true"],
.chip.allergy.is-on { background: var(--alarm); }

/* The authored chips are spans and do nothing; the cursor says so. */
.chip:not(button) { cursor: default; }

.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- the week ------------------------------------------------------------ */

/* ⚠️ style.css has a `.week` of its own — a full-width section from the page
   before this one ("Seven dinners is what the app makes"), with section padding
   and a max-width — and this column shares the name. It was quietly putting
   52px above the status line, so the week started well below the card beside
   it, and 24px either side, narrowing every row. Found when the boxes came off
   and the rows no longer filled the column. index.html is the only page with a
   `.week`, so this is the only place it bites. */
.table-grid .week { max-width: none; margin: 0; padding: 0; }

/* What the last tap did, in words. Crimson while the dinners that had to go
   are on screen; ink once the week has re-planned. */
.week-status {
  min-height: 1.5em;
  margin: 0 0 0.9rem;
  color: var(--ink);
  font: 600 1rem/1.4 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}

.week-status[hidden] { display: none; }

/* What just happened, arriving rather than being swapped: the line is the
   answer to a tap and reads as one. */
@keyframes said {
  from { opacity: 0; transform: translateY(0.3rem); }
  to   { opacity: 1; transform: none; }
}

.js .week-status.said { animation: said 0.32s cubic-bezier(0.22, 0.61, 0.36, 1) both; }

/* A person arrives the same way when their face is chosen. */
@keyframes arrive-panel {
  from { opacity: 0; transform: translateY(0.4rem); }
  to   { opacity: 1; transform: none; }
}

.js .person-panel.arrive { animation: arrive-panel 0.34s cubic-bezier(0.22, 0.61, 0.36, 1) both; }
.week-status[data-tone="hint"] { color: var(--slate); font-weight: 500; }
.week-status[data-tone="alarm"] { color: var(--alarm); }

/* Seven ROWS beside the person, not seven cards below them. Cards were right
   when the week was the only thing on the screen; beside a panel, a row is what
   keeps all seven in view at once — which is the whole point of the change. */
.nights {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* ⚠️ NO BOXES. Each row was a bordered card on the surface colour, and seven
   of them read as crowded; now the rows sit on the paper and the app's brushed
   hairline divides them, the same generated strokes the pricing
   table uses (make-hairlines.py), BETWEEN rows only.

   The row reaches 0.6rem past its column on both sides and pads it back, so its
   content lines up with the status line above it while the edge mark below has
   somewhere to stand. Named areas, because the flag shares a cell with the
   marks. */
.night {
  position: relative;
  display: grid;
  /* The day column is 3.75rem because "Tonight" is 54.7px, measured: at 3.1rem
     it overflowed its column and sat half a pixel from the dish name. Fixed
     rather than auto, because each row is its own grid — an auto column would
     widen only the Tonight row and knock the names out of line. */
  grid-template-columns: 3rem 3.75rem minmax(0, 1fr) auto;
  grid-template-areas: "img day name marks";
  align-items: center;
  gap: 0.8rem;
  margin-inline: -0.6rem;
  padding: 0.7rem 0.6rem;
}

.night img {
  grid-area: img;
  display: block;
  width: 3rem;
  height: auto;
  transition: transform 0.35s cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* The dish leans forward under a pointer. Nothing else in the row moves: a
   dinner's name is a fact, not a control. */
@media (hover: hover) {
  .night:hover img { transform: scale(1.06); }
}

@media (prefers-reduced-motion: reduce) {
  .night img { transition: none; }
  .night:hover img { transform: none; }
}

/* Centred on the boundary between two rows. Four seeds for six lines: rows six
   and seven take the first two strokes mirrored, so no two lines match. */
.night + .night::before {
  content: "";
  position: absolute;
  top: -3px;
  left: 0.6rem;
  right: 0.6rem;
  height: 6px;
  background: var(--hairline-0) 0 0 / 100% 6px no-repeat;
  pointer-events: none;
}

.night:nth-child(4n+3)::before { background-image: var(--hairline-1); }
.night:nth-child(4n)::before { background-image: var(--hairline-2); }
.night:nth-child(4n+5)::before { background-image: var(--hairline-3); }
.night:nth-child(n+6)::before { transform: scaleX(-1); }

.night-day {
  grid-area: day;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
  color: var(--slate);
  font: 500 0.78rem/1 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  letter-spacing: 0.02em;
}

/* Tonight. Applied by home.js from the reader's own clock, never baked into
   the HTML — a day written at build time is wrong six days out of seven. With
   the boxes gone the tag is the whole signal; the row's navy border went with
   them. */
.today-tag {
  padding: 0.18rem 0.38rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 0.62rem;
  font-weight: 600;
}

/* In the household blue, and SANS. The app's rows agree: a row title there is
   DM Sans medium at 16pt; the serif is for its dish PAGE
   title, not a row in a list. */
.night-name {
  grid-area: name;
  color: var(--accent);
  text-wrap: pretty; /* no lone last word: "Crunchy / Veg" */
  font: 500 1rem/1.3 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}

.night .fit-row { grid-area: marks; }

/* The edge mark: a short painted bar at the row's left edge, for the two
   moments a row is being acted on — crimson while it is flagged, navy fading
   out once it has been replaced. It took over from the boxes' border and ring.

   ⚠️ A BAR, NOT A TINT, and that was measured rather than chosen. The obvious
   move with no box is to wash the row, and the day label had no headroom for
   one while it was the old warm grey: 4.70:1 on the grained paper, 4.32 under a
   5% accent wash, 4.40 under 4% of the alarm. The quiet text colour is 6.59:1
   now, so a faint tint would clear the bar, but a bar beside the content
   touches nothing that has to be read at all, so it stays. */
.night::after {
  content: "";
  position: absolute;
  top: 0.8rem;
  bottom: 0.8rem;
  left: 0;
  width: 3px;
  border-radius: 3px;
  background: var(--accent);
  opacity: 0;
  pointer-events: none;
}

/* The beat before a re-plan: these are the dinners that had to go, and why.
   ⚠️ The NAME keeps its full colour and is only struck through. Fading it too
   would take a dish name under the contrast bar for the one moment it matters
   most; the image fades instead, and the flag carries the reason at 8.56:1.
   The flag takes the marks' place rather than floating over the row: whose
   dinner it was stops mattering the moment it is leaving, and the cell is wider
   than any flag ("Has sesame" ~80px against the marks' ~108), so nothing moves. */
.night.conflict::after { background: var(--alarm); opacity: 1; }
.night.conflict img { opacity: 0.45; }
.night.conflict .night-name { text-decoration: line-through; text-decoration-thickness: 1.5px; }
.night.conflict .fit-row { visibility: hidden; }

.night-flag {
  grid-area: marks;
  justify-self: end;
  padding: 0.25rem 0.45rem;
  border-radius: 999px;
  background: var(--alarm);
  color: var(--on-accent);
  font: 600 0.7rem/1 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}

/* A re-planned night lands rather than snaps — the same easing the app uses
   when a card is replaced — and its edge mark holds for a moment so the eye can
   find what is new.

   ⚠️ The CONTENT settles, not the row. The row carries the hairline above it,
   and a line that dropped and faded in with every replaced dinner made the
   dividers flicker while the rows around them stood still. */
@keyframes settle {
  from { opacity: 0; transform: translateY(0.4rem); }
  to   { opacity: 1; transform: none; }
}

@keyframes arrive {
  from { opacity: 1; }
  to   { opacity: 0; }
}

.night.changed > * { animation: settle 0.42s cubic-bezier(0.22, 0.61, 0.36, 1) both; }
.night.changed::after { animation: arrive 1.6s ease-in both; }

/* ⚠️ BESIDE A PERSON, THE WEEK IS AS TALL AS THEY ARE, so the dishes can be
   bigger. The column
   stretches to the card-and-faces beside it and the seven rows share that
   height; the dish is 72px, up from 48, which the 240px pool art covers at 2x
   and past it.

   The marks go UNDER the dish name to pay for it, and they had to: beside a
   72px dish the name column would have lost
   another 24px to the marks, and at 1024 most names would have run to two
   lines. Under the name, they use height the row now has.

   The stretch waits for home.js (`.is-live`). With no script every person's
   card shows, stacked, and a week stretched to that would be seven rows of
   300px. The row layout itself does not wait, so the week does not re-flow
   when the script arrives. */
@media (min-width: 62rem) {
  .night {
    grid-template-columns: 4.5rem 3.75rem minmax(0, 1fr);
    grid-template-rows: 1fr auto auto 1fr;
    grid-template-areas:
      "img day ."
      "img day name"
      "img day marks"
      "img day .";
    row-gap: 0;
    padding-block: 0.5rem;
  }
  .night img { width: 4.5rem; }
  .night .fit-row,
  .night-flag { margin-top: 0.45rem; }
  .night-flag { justify-self: start; }

  .table-grid.is-live { align-items: stretch; }
  .table-grid.is-live .week { display: flex; flex-direction: column; }
  .table-grid.is-live .nights { flex: 1; grid-auto-rows: 1fr; }
}

/* ⚠️ On a phone the day, name and marks STACK beside the thumbnail rather than
   sitting in four columns. In columns, measured on a 375px screen, the row was
   301px and the thumbnail, day, marks, gaps and padding left the dish name 84px:
   "Charred Poblano Corn Chowder with Tortilla Crisps" ran to six lines, and the
   "Tonight" tag was wider than its 46px day column and ran into the name.
   Stacked, the name gets the whole width beside the thumbnail. */
@media (max-width: 40rem) {
  .night {
    grid-template-columns: 2.8rem minmax(0, 1fr);
    grid-template-areas: "img day" "img name" "img marks";
    column-gap: 0.75rem;
    row-gap: 0.28rem;
    padding: 0.8rem 0.6rem;
  }
  .night img { width: 2.8rem; }
  .night-day { flex-direction: row; align-items: center; gap: 0.4rem; }
  .night-name { font-size: 0.92rem; }
  .night-flag { justify-self: start; }
}

/* The state nobody else in this category shows you: the table is so narrow
   that seven different dinners do not exist for it. Saying so is the product. */
.short {
  margin: 1.1rem 0 0;
  padding: 0.85rem 1rem;
  border: 1px solid var(--edge);
  border-radius: 12px;
  color: var(--ink);
  font-size: 1.0625rem;
  background: rgba(var(--accent-rgb), 0.06);
  font-weight: 450;
}

.short[hidden] { display: none; }

.note-under {
  color: var(--slate);
  font-size: 1.0625rem;
  margin: 1.4rem 0 0;
  max-width: 66ch;
  font-weight: 450;
}

/* The seven-plate grid that used to live here went with the art section on
   2026-09-15, and the drifting rail that replaced it went too on 2026-09-16.
   What says "there are a lot of these" now is the number itself — see the
   count section — with ONE dish beside it rather than seven or thirty-two.

   ⚠️ Six of `/art/*.webp` and their PNG fallbacks are UNREFERENCED by the page;
   the seventh is the count section's dish. They are kept rather than deleted:
   `tools/export-art.py` owns them,
   its docstring records why those particular seven (seven formats, seven
   proteins, seven cuisines, no two neighbours sharing a vessel colour), and
   they are the high-resolution set if a plate grid ever comes back. They are
   never fetched, so they cost a visitor nothing. */



/* --------------------------------------------------------------- closing --- */

.closing {
  text-align: center;
  padding-block: clamp(3.5rem, 9vw, 6.5rem);
}

.closing h2 {
  font: 600 clamp(2rem, 5.6vw, 3.5rem)/1.02 "Fairhand Display", Georgia, serif;
  letter-spacing: -0.02em;
  margin: 0 auto 1.5rem;
  max-width: 18ch;
  text-wrap: balance;
}

.closing .cta { display: inline-block; }

/* ---------------------------------------------------------------- reveal ---
   Sections rise as they arrive. Applied by home.js only — the class is never
   in the HTML, so a page with no JavaScript is never left holding invisible
   content. That is also why the hidden state lives under .js. */

.js .reveal {
  opacity: 0;
  transform: translateY(1.1rem);
  transition: opacity 0.7s cubic-bezier(0.22, 0.61, 0.36, 1),
              transform 0.7s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.js .reveal.in { opacity: 1; transform: none; }

/* ---- arrivals, one after another ------------------------------------------
   A section's parts come in together but not at once: each child carries its
   own index (`--i`, set by home.js) and waits that many beats. It is the
   difference between a block appearing and a table being laid.

   Applied by hand, on the lists where the order means something: the seven
   dinners down the week, the rows inside each group of the comparison, the two
   promises, the plans, and the hero's own lines as the page opens. The delay is
   55ms and the index is capped at 8, so nothing ever waits half a second for
   its turn.

   ⚠️ Under `.js`, like the section reveal, so a page with no script has nothing
   hidden. Only opacity and transform move, which is what keeps a cascade of
   sixteen table rows on the compositor. */
.js [data-cascade] > * { opacity: 0; transform: translateY(0.55rem); }

.js [data-cascade].is-in > * {
  opacity: 1;
  transform: none;
  transition: opacity 0.68s cubic-bezier(0.22, 0.61, 0.36, 1),
              transform 0.68s cubic-bezier(0.22, 0.61, 0.36, 1);
  transition-delay: calc(var(--i, 0) * 95ms);
}

/* The head's deadline: if the module has not run in 1.2s, nothing stays hidden
   waiting for it. */
.js.js-late [data-cascade] > *,
.js.js-late .reveal { opacity: 1; transform: none; }
.js.js-late .brushed::after { clip-path: none; }

/* A table row is not a box to move about; the fade alone reads as the rows
   being written in, and the painted tick lands on a beat of its own. */
.js [data-cascade="rows"] > * { transform: none; }
.js [data-cascade="rows"] > * .tick { transform: scale(0.82); opacity: 0.2; }
.js [data-cascade="rows"].is-in > * .tick {
  transform: none;
  opacity: 1;
  transition: transform 0.5s cubic-bezier(0.22, 0.61, 0.36, 1),
              opacity 0.5s ease-out;
  transition-delay: calc(var(--i, 0) * 95ms + 120ms);
}

/* The painted stroke under a heading is drawn on rather than switched on: it
   sweeps from the first letter to the last, the direction a hand would take
   it. */
.js .brushed::after { clip-path: inset(0 100% 0 0); }
.js .hero-copy.is-in .brushed::after,
.js .reveal.in .brushed::after {
  clip-path: inset(0 0 0 0);
  transition: clip-path 0.75s cubic-bezier(0.22, 0.61, 0.36, 1) 0.12s;
}

@media (prefers-reduced-motion: reduce) {
  .js .week-status.said,
  .js .person-panel.arrive { animation: none; }
  body.billing-live .plan-stack > .plan { transition: none; }
  .js [data-cascade] > *,
  .js [data-cascade] > * .tick { opacity: 1; transform: none; transition: none; }
  .js .brushed::after { clip-path: none; transition: none; }
  .js .reveal { opacity: 1; transform: none; transition: none; }
  .night.changed > *,
  .night.changed::after { animation: none; }
}

/* ------------------------------------------------------ visually hidden ---
   The wedge row is decoration and is marked aria-hidden; the sentence beside
   it ("A stretch for Téo.") is the actual information, and it is here for
   anyone who is not reading colour. Not `display:none` — that would take it
   out of the accessibility tree too, which is the whole point of having it. */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ------------------------------------------------------- the two promises ---
   Two articles, side by side, because the whole point is that they are NOT the
   same promise. Side by side is the layout that says so. */

.promises {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
  column-gap: clamp(1.5rem, 4vw, 3.5rem);
}

/* No rule above each promise: the pictures open the articles now, and a line
   over a picture only boxed it in. */
.promises article { padding: 0 0 1.75rem; }

.promises h2 {
  font: 600 1.375rem/1.2 "Fairhand Display", Georgia, serif;
  letter-spacing: -0.01em;
  margin: 0 0 0.5rem;
}

.promises p { margin: 0; color: var(--slate); max-width: 44ch; font-weight: 450; }

/* The allergy promise is set in the alarm, the one colour this palette keeps
   for a hard limit: it is the sentence the page exists to make, and the app
   says the same thing in the same red. 7.37:1 on the grained paper. The diet
   promise stays in ink, because a preference is not an alarm. */
.promises h2.is-alarm { color: var(--alarm); }

/* Each promise's picture (tools/export-promises.py). Its ground is #F2E6D8, near
   the paper but not equal to it, so it keeps its own rounded field, as the
   portraits do. Capped where the articles stack: a tablet's single column would
   otherwise draw it 700px square. */
.promise-art {
  display: block;
  width: 100%;
  max-width: 34rem;
  height: auto;
  aspect-ratio: 1;
  margin: 0 0 1.4rem;
  border-radius: 16px;
  background: #f2e6d8;
}
.promises .wedge-row { margin: 0 0 0.9rem; }

/* The washed wedge in the second article is the same "stretch" state the week
   above draws, and it has to mean the same thing here. style.css sizes .wedge;
   only the wash is set, and it is lifted out of .wedges so any wedge on the
   page can carry it. */
.wedge.stretch { opacity: 0.32; }

/* ---------------------------------------------------------------- the shop ---
   One card: the heading and a sentence on one side, and the top of the phone
   rising out of the card's bottom edge on the other. The top of the screen is
   the part that argues (the store, its prices, the first aisle, two things
   ticked), so the rest of the phone is cropped by the card instead of shrunk to
   stand whole beside the copy, where nothing on it could be read.

   The crop is a HEIGHT on the phone's column, not on the image: the device
   keeps its full size and the card's overflow cuts it. Change how much of the
   screen shows with `.shop-phone`'s height, never by scaling the phone. */

.shop-card {
  display: grid;
  overflow: hidden;
  border: 1px solid var(--rule);
  border-radius: 22px;
  background: var(--surface);
}

.shop-copy {
  align-self: center;
  padding: clamp(2rem, 6vw, 3.5rem) clamp(1.5rem, 5vw, 3.5rem) 0;
}

.shop .section-head { margin: 0; }

/* The retailer's mark beside the price line, under a hairline of the card's own
   rule. Sized as a caption, not a badge: it names where the prices come from,
   it is not a partner lockup. */
.storenote {
  display: flex;
  align-items: center;
  gap: 1rem;
  max-width: 46ch;
  margin: 1.5rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
  color: var(--slate);
  font-size: 1.0625rem;
  line-height: 1.5;
  font-weight: 450;
}

.storemark { flex: none; width: 5.25rem; height: auto; }

@media (max-width: 30rem) {
  .storenote { flex-direction: column; align-items: flex-start; gap: 0.6rem; }
}
.shop .lede { text-wrap: pretty; }

/* Tall enough, at every width, to reach the first two ticked items: the red
   ticks are the one thing on the screen that says "while you shop". */
.shop-phone {
  height: clamp(28rem, 125vw, 34rem);
  margin: 0;
  padding: 2.25rem clamp(1.25rem, 5vw, 3.5rem) 0;
  overflow: hidden;
}

@media (min-width: 62rem) {
  .shop-card { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .shop-copy { padding: 3.5rem 1rem 3.5rem 3.5rem; }
  .shop-phone { height: 36rem; padding-top: 2.5rem; }
}

/* The phone: the screen in a plain black bezel, drawn rather than
   photographed, so it is sharp at any size and costs nothing to load. The
   screenshot carries its own status bar and island, so the frame has neither.
   The inner radius is the outer one less the bezel, which is what keeps the two
   curves concentric. */
.device {
  width: min(100%, 21rem);
  margin-inline: auto;
  padding: 0.55rem;
  border-radius: 3.1rem;
  background: #1c1b18;
  box-shadow:
    inset 0 0 0 1.5px #5b564d,
    0 1.5rem 3rem -1.25rem rgba(28, 27, 24, 0.32);
}

.device img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 2.55rem;
}

/* ======================================================= the member marks ===
   A person's token, as the app actually draws it.

   ⚠️ **These were four Bézier "wedges" once — petal shapes from the app's old
   wedge drawing.** That was right once and stopped being: when the member cap
   went from four to eight the tokens were repainted as eight brushed marks, and
   the wedge now survives only in the brand mark, which is not a person. The site was drawing a shape and a palette the app had retired.

   THE ART SHIPS AS COVERAGE, NOT AS COLOUR, and everything here follows from
   that — it is the app's own rule. The same silhouette has to carry
   how well a dinner suits somebody, which a colour-baked PNG cannot do. So each
   mark is an alpha mask and the colour arrives here, which is why one 7 KB file
   serves every state of one member. */

.mk {
  display: inline-block;
  width: 1.05em;
  height: 1.05em;
  vertical-align: -0.16em;
  background-color: var(--mk-paint);
  -webkit-mask: var(--mk-art) center / contain no-repeat;
  mask: var(--mk-art) center / contain no-repeat;
}

.mk-1 { --mk-paint: var(--member-1); --mk-art: url("/art/marks/member-mark-0.png"); }
.mk-2 { --mk-paint: var(--member-2); --mk-art: url("/art/marks/member-mark-1.png"); }
.mk-3 { --mk-paint: var(--member-3); --mk-art: url("/art/marks/member-mark-2.png"); }
.mk-4 { --mk-paint: var(--member-4); --mk-art: url("/art/marks/member-mark-3.png"); }

/* Fit, as thinning — never as a different colour. The app is explicit: a
   member's colour is their identity, and recolouring it to show fit would erase
   whose mark it is. 0.22 is the app's own thinnest step, and it is meant
   to be faint; the sentence in `.visually-hidden` beside it is what actually
   carries the information. */
.mk.stretch { opacity: 0.22; }

/* The drawn edge the two palest paints need to be found on paper. Ochre
   measures 2.47:1 on the grained ground, under the 3:1 floor for non-text.

   The app strokes the mark's rim; a CSS mask has no stroke, so this is a pair
   of tight ink drop-shadows instead — doubled because one at this radius is too
   faint to close the shape. An approximation of the app's rim, and the only
   thing on this page that is. */
.mk.edged {
  filter: drop-shadow(0 0 0.5px #1c1b18) drop-shadow(0 0 0.5px #1c1b18);
}

/* Sizes. The mark scales with its context's font-size, so these set the box.

   A night's marks are TOKENS, with the initial inside, at the app's row size:
   24pt there, and 1.43rem
   puts the mark at 24px (x 1.05) and the letter at 10.6px (x 0.462) — the same
   fractions as the token beside a person's name. At the 16px the marks were,
   the letter would have been 7px. */
.fit-row { display: inline-flex; gap: 0.25rem; font-size: 1.43rem; }

/* A stretch thins the paint, so the letter turns ink; see tokenHTML in home.js. */
.token.stretch .token-initial { color: var(--ink); }

/* ⚠️ No ink press at this size. Measured at 2x and 3x: on a 10.6px capital the
   displacement shears whole strokes — a gap through the middle of the M, a cut
   in the G — where on the 16px initial beside a person's name it only roughens
   the edge. The app's own notes name this exact fallback: no press at the row size,
   rather than dropping it everywhere. */
.fit-row .token-initial { filter: none; }

/* ============================================================ the loud button ===
   The app's painted button: a painted stroke as the ground, the label drawn
   in type on top. It replaced Apple's store badge.

   The paint is the app's button stroke, alpha only, tinted here, and it is
   stretched to the frame as the app stretches it: "Cap insets do not scale… Uniform stretch is
   the honest trade." The frame is held at the painted 5.5:1 (52 x 286px) so the fray is the length it was painted, and grows only if the label must.

   ⚠️ THE LABEL STAYS ON SOLID PAINT, measured rather than hoped. Across the
   stroke's middle 40% of height the alpha is 100% solid from 10% to 80% of the
   width; only the ragged start (87%) and the dissolving tail (49%) thin out.
   The label is ~165px centred in 286px, so it spans 21-79% — all of it on full
   paint, where bone reads 10.04:1. The app's own notes name the failure this avoids: "paint broken under type is unreadable while the button still works
   perfectly — a failure nothing would report."

   The type is the site's sans rather than the app's DM Sans Bold. Every other
   piece of UI text on the site is the system sans; one button in a webfont
   nothing else uses would be the odd one out, and a request for one label. */

/* CTA-MASK — generated by tools/export-marks.py; do not hand-edit */
:root { --cta-mask: url("data:image/png;base64,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"); }
/* /CTA-MASK */

.brush-cta {
  position: relative;
  isolation: isolate; /* keeps the paint behind the label but inside the button */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 3.25rem;
  min-width: 17.875rem;
  padding: 0.75rem 1.5rem;
  font: 700 1.0625rem/1.2 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  letter-spacing: 0.005em;
  color: var(--on-accent);
  text-align: center;
  text-decoration: none;
}

.brush-cta::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--accent);
  -webkit-mask: var(--cta-mask) center / 100% 100% no-repeat;
          mask: var(--cta-mask) center / 100% 100% no-repeat;
}

/* The Apple logo leads the label, in the label's own colour, sized to its
   capitals and nudged up a hair: the logo's leaf rides above the cap height. */
.cta-apple {
  flex: none;
  width: 0.95em;
  height: 1.17em;
  margin: 0 0.5em 0 0;
  fill: currentColor;
  transform: translateY(-0.08em);
}

/* The inverse for a dark ground: the app button's paint and ink, swapped. An
   accent stroke on the navy panel would simply vanish. */
.brush-cta.on-dark { color: var(--accent); }
.brush-cta.on-dark::before { background: var(--on-accent); }

/* Only a LINK responds; the button is a <span> until the app is live, and a
   control that reacts to a press and then does nothing is worse than none.

   Pressed paint goes DARKER, not paler, as in the app: "paint gets deeper
   under pressure, it does not go transparent." */
a.brush-cta::before { transition: filter 0.12s ease-out; }
a.brush-cta:hover::before { filter: brightness(0.96); }
a.brush-cta:active::before { filter: brightness(0.94); }
a.brush-cta:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 6px; }
a.brush-cta.on-dark:focus-visible { outline-color: var(--on-accent); }

/* A THUMB-WIDTH BUTTON. Sized to its label the painted button stopped at 286px
   in a 353px column, which on a phone reads as a button that missed its box
   rather than as the page's one action. It takes the column instead.

   ⚠️ Capped at 26rem, so this is a phone rule and not a tablet one: the column
   at 640px would otherwise give a single button 604px of brush, and the mask is
   `100% 100%` — the stroke's torn ends stretch with it and the paint thins out
   in the middle. Below the cap the container wins and the button fills it.
   ⚠️ And `min-width` goes to 0 here, or the 17.875rem floor outranks a
   container narrower than itself and puts the button through the gutter. */
@media (max-width: 47.99rem) {
  .brush-cta {
    display: flex;
    width: 100%;
    min-width: 0;
    max-width: 26rem;
    margin-inline: auto;
  }
  /* The closing panel's wrapper is inline-block so the centred panel can centre
     it; a full-width button inside an inline-block is as wide as its label. */
  .closing .cta { display: block; }
}

/* ---------------------------------------------------- how much is in there ---
   The one place the catalogue's size is stated, set as a FIGURE rather than as
   a sentence. The number is the picture: the page carries no illustration grid
   any more, and a numeral at display size does what the seven-plate row did
   without asking for seven more images.

   Accent, not ink — the same blue as the hero's h1, which makes the two biggest
   type moments on the page rhyme instead of introducing a third treatment.

   ⚠️ ONE HEADING, not a figure beside a heading. The numeral and the phrase are
   two spans inside the same h2, so the accessible name is the whole sentence.
   Split them and a screen reader is read the phrase alone, losing the only
   number on the page.
   (This note used to quote the sentence itself and went stale the first time the
   wording changed. It describes the structure instead, which is the part that
   must not change.) */
/* Copy and one dish, on the `.shop` pattern: stacked by default, side by side
   once there is room. The dish column is the narrower one — it is an ornament
   next to a claim, not the subject. */
.count {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
}

@media (min-width: 62rem) {
  /* The dish reads at the size of the two illustrations a section above, which
     is what it is: a painting, not a spot. */
  .count { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

.count .section-head { max-width: 32ch; margin-bottom: 1.5rem; }

.count-dish { margin: 0; text-align: center; }

.count-dish img {
  display: block;
  width: 100%;
  max-width: 34rem;
  height: auto;
  margin-inline: auto;
}

/* The scrubbed dish. home.js lays a canvas over the <img> and fills the tacos as the
   section is scrolled through; `is-scrub` is only ever set once every frame has decoded,
   so the picture never goes blank waiting for one. The canvas takes the <img>'s exact
   box, because they are the same square and the caption below must not move when one
   replaces the other. */
.count-dish canvas {
  display: block;
  width: 100%;
  max-width: 34rem;
  height: auto;
  margin-inline: auto;
}

.count-dish.is-scrub img { display: none; }

/* ⚠️ The one piece of art on this page whose ground IS the page. Its frames are
   keyed onto the paper colour, flat, so at this size the square read as a
   slightly smoother rectangle on a grained page. The page's own tooth, multiplied
   back over it, is what closes that seam; every other painting here sits on its
   own field and must not get this. */
.count-dish { position: relative; }

.count-dish::after {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  aspect-ratio: 1;
  max-width: 34rem;
  margin-inline: auto;
  background-image: var(--grain);
  background-size: 220px 220px;
  mix-blend-mode: multiply;
  pointer-events: none;
}

.count-dish figcaption {
  color: var(--slate);
  font-size: 1.0625rem;
  margin-top: 0.7rem;
}

/* The numeral sits up off the phrase under it: at 0.82 line-height the digits'
   baseline was a few pixels from "dinners", and a figure this size needs the
   air. The margin is in em, so it scales with the numeral rather than with the
   page. */
.count-n {
  display: block;
  margin-bottom: 0.12em;
  /* ⚠️ 26vw, not 13. At 13 the clamp's floor did all the work on every phone —
     the numeral sat at a flat 72px from 360px to 550px wide, which is smaller
     than the headline above it and makes the one figure on the page the
     quietest thing in its own section. 26vw reaches the 9rem ceiling at about
     555px and scales below it, so the number is the picture on a phone too. */
  font-size: clamp(4.5rem, 26vw, 9rem);
  line-height: 0.82;
  letter-spacing: -0.045em;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

/* Its own measure, so the phrase breaks under the numeral rather than running
   past it — the h2's 16ch is sized for a heading, not for a figure. */
.count-rest { display: block; max-width: 15ch; }

.count-body {
  color: var(--slate);
  max-width: 54ch;
  margin: 0;
  font-weight: 450;
}

.count .cta { margin-top: clamp(1.5rem, 3vw, 2rem); }

/* ===================================================== the brushed underline ===
   The app's painted underline: its two underline strokes, alpha only, tinted
   from a token.

   ⚠️ This was a CSS gradient once: a bar thinning at both ends
   under a soft radial mask, "drawn rather than shipped" because the site had no
   stroke to use. The app has since painted two, and its own history is the
   argument for using them: its procedural mark "is 100% solid with a single
   strand, where a real mark dips to about 47% fill with two to five and paper
   between them." The gradient here had even less than that — no strands at all.

   ⚠️ FIT, NEVER STRETCHED. The painted marks are true underlines, 24.6:1 and
   18.2:1, and the app fits them rather than filling the frame: "Stretched
   to fill… they would be forced to about 7.5:1, tripling their weight and
   smearing the fray that is the whole character of them." Here the stroke's
   width is the phrase's and its height comes from `aspect-ratio`, so the mask
   is drawn at exactly its painted proportions whatever the heading.

   Behind the type, so descenders cross the paint rather than disappear into it. */

.brushed {
  position: relative;
  display: inline-block;
}

.brushed::after {
  content: "";
  position: absolute;
  left: -0.03em;
  right: -0.06em;
  top: 100%;
  margin-top: -0.2em;
  aspect-ratio: 1000 / 41;
  z-index: -1;
  pointer-events: none;
  background: var(--brush, var(--member-3));
  -webkit-mask: url("/art/brush/underline-1.png") center / 100% 100% no-repeat;
          mask: url("/art/brush/underline-1.png") center / 100% 100% no-repeat;
}

/* The second painted mark — a raked wedge where the first is a long taper. The
   app gives each instance its own stroke by seed so "two of them side by side
   are not the same mark stamped twice"; here the four brushed headings run
   3, 1, 3, 1 down the page, so no two in a row share one. */
.brushed.brush-3::after {
  aspect-ratio: 1000 / 55;
  -webkit-mask-image: url("/art/brush/underline-3.png");
          mask-image: url("/art/brush/underline-3.png");
}

/* The stroke hangs below the heading's box, so the subtitle under a brushed
   heading needs clearance — set on the heading rather than on the subtitle, so
   it only applies where there is a stroke to clear. */
.section-head h2:has(.brushed) { margin-bottom: 0.5rem; }

/* Navy under the line that answers, ochre under the line that asks — the app's
   own division, which is why the two are not interchangeable. */
.brushed.ink { --brush: var(--member-1); }

/* The second voice in a two-line title. The app sets "Shopping for" in ink and
   "the rest of the week" in navy, and the colour change is what makes a long
   title read as one sentence over two lines rather than as two headings. */
.said {
  color: var(--accent);
  font-style: normal;
}



/* ============================================================== the cloth ===
   The washed rose gingham the app lays across the top of Home, doing the same
   job here.

   ⚠️ IT IS A BAND FROM THE TOP OF THE PAGE, NOT A BACKGROUND, and that is the
   app's design rather than a web compromise. The app runs it 560pt down the SCREEN — behind the status bar and the wordmark, not just behind the
   headline — and masks it out before the content starts, so the greeting sits
   on cloth and the ink below it sits on plain paper. A cloth behind body text
   is a contrast problem; a cloth behind a headline is a ground.

   On `body::after` rather than on `.hero`, for that reason: the header is a
   sibling of the hero, so a band scoped to the hero starts under the wordmark
   and leaves a strip of bare paper above it. It was built that way first and
   the seam was obvious. `home.css` is loaded by the homepage alone, so scoping
   it to the body here still keeps it off /support, /privacy and /terms, which
   are plain paper in the app's world too.

   It paints above the page's grain rather than under it — the grain is a
   background-image on <body> and a negative-z pseudo-element sits on top of
   that. At 0.85 the tooth still reads through, and the cloth carries its own. */

body::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: min(38rem, 64vh);
  z-index: -1;
  pointer-events: none;
  background: url("/art/home-cloth.webp") center top / cover no-repeat;
  /* The fade. Full strength at the top, gone by the bottom — the app's mask has
     the same shape, and it is what stops the cloth becoming a hard seam. */
  -webkit-mask-image: linear-gradient(#000 0%, #000 30%, transparent 97%);
  mask-image: linear-gradient(#000 0%, #000 30%, transparent 97%);
  opacity: 0.85;
}

/* The wide crop: the top half of the cloth at its full 2048px, which is all a
   band at least twice as wide as it is deep can show — and at the same scale the
   square would have been drawn at, because `cover` is bound by the width in both.
   Half the pixels, where the square would have been stretched across a retina
   window. See export-marks.py for why neither is the app's 1200px copy.

   The query is that 2:1 condition, exactly. The band is min(38rem, 64vh) deep,
   so it is at most half the window's width once the window is 76rem wide (38 x 2)
   or 1.28x as wide as it is tall (0.64 x 2). Anything short of both gets the
   square. Nothing breaks at a boundary either way: `cover` never leaves a gap, it
   only changes which image is scaled. */
@media (min-width: 76rem), (min-aspect-ratio: 32/25) {
  body::after { background-image: url("/art/home-cloth-wide.webp"); }
}

/* ================================================== the comparison table ===
   Free against Plus: large plan names with the price set small beneath,
   branded checks, a warm featured column, and every material from the app.

   ⚠️ The Plus prices are `.plan[data-plan]` spans, which setUpBilling selects
   document-wide — so the monthly/yearly control drives this table. And
   `:not([hidden])` on their display rule is load-bearing: a plain
   `.compare .plan` (0,2,0) outranks the UA's `[hidden]` (0,1,0), and both prices
   rendered at once until that was caught by driving the toggle. */

/* HAIRLINES — generated by tools/make-hairlines.py; do not hand-edit */
:root {
  --hairline-0: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 6' preserveAspectRatio='none'%3E%3Cdefs%3E%3ClinearGradient id='g' gradientUnits='userSpaceOnUse' x1='0' y1='0' x2='1000' y2='0'%3E%3Cstop offset='0' stop-color='%23b8aa90' stop-opacity='.35'/%3E%3Cstop offset='.5' stop-color='%23b8aa90' stop-opacity='1'/%3E%3Cstop offset='1' stop-color='%23b8aa90' stop-opacity='.45'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M0 2.88L6 3.04L12 3.19L19 3.30L25 3.35L31 3.36L38 3.32L44 3.24L50 3.14L56 3.01L62 2.88L69 2.76L75 2.66L81 2.60L88 2.58L94 2.61L100 2.69L106 2.80L112 2.94L119 3.10L125 3.25L131 3.39L138 3.50L144 3.57L150 3.59L156 3.58L162 3.52L169 3.43L175 3.32L181 3.20L188 3.10L194 3.01L200 2.96L206 2.95L212 2.98L219 3.04L225 3.14L231 3.26L238 3.38L244 3.49L250 3.58L256 3.64L262 3.65L269 3.61L275 3.53L281 3.42L288 3.27L294 3.10L300 2.94L306 2.79L312 2.67L319 2.59L325 2.55L331 2.56L338 2.61L344 2.70L350 2.82L356 2.95L362 3.07L369 3.17L375 3.24L381 3.27L388 3.26L394 3.21L400 3.13L406 3.01L412 2.89L419 2.77L425 2.66L431 2.58L438 2.53L444 2.53L450 2.57L456 2.64L462 2.74L469 2.85L475 2.97L481 3.07L488 3.14L494 3.17L500 3.15L506 3.09L512 2.98L519 2.83L525 2.65L531 2.46L538 2.28L544 2.11L550 1.97L556 1.87L562 1.82L569 1.81L575 1.85L581 1.91L588 2.00L594 2.09L600 2.17L606 2.23L612 2.26L619 2.25L625 2.20L631 2.11L638 1.99L644 1.86L650 1.72L656 1.59L662 1.49L669 1.42L675 1.39L681 1.41L688 1.47L694 1.57L700 1.70L706 1.84L712 1.97L719 2.09L725 2.18L731 2.23L738 2.23L744 2.19L750 2.10L756 1.98L762 1.84L769 1.69L775 1.54L781 1.42L788 1.33L794 1.29L800 1.29L806 1.33L812 1.40L819 1.51L825 1.62L831 1.74L838 1.84L844 1.91L850 1.95L856 1.95L862 1.90L869 1.82L875 1.72L881 1.60L888 1.49L894 1.38L900 1.31L906 1.28L912 1.29L919 1.35L925 1.45L931 1.59L938 1.76L944 1.93L950 2.10L956 2.25L962 2.36L969 2.44L975 2.47L981 2.44L988 2.36L994 2.26L1000 2.15L1000 2.40L994 2.51L988 2.61L981 2.69L975 2.73L969 2.76L962 2.73L956 2.68L950 2.59L944 2.48L938 2.36L931 2.26L925 2.18L919 2.13L912 2.12L906 2.16L900 2.24L894 2.36L888 2.50L881 2.65L875 2.79L869 2.92L862 3.01L856 3.07L850 3.08L844 3.04L838 2.96L831 2.85L825 2.72L819 2.59L812 2.47L806 2.37L800 2.32L794 2.30L788 2.33L781 2.41L775 2.52L769 2.66L762 2.82L756 2.97L750 3.10L744 3.21L738 3.28L731 3.32L725 3.31L719 3.26L712 3.20L706 3.12L700 3.04L694 2.98L688 2.94L681 2.95L675 3.00L669 3.09L662 3.22L656 3.39L650 3.57L644 3.76L638 3.93L631 4.09L625 4.20L619 4.27L612 4.29L606 4.26L600 4.19L594 4.09L588 3.98L581 3.86L575 3.76L569 3.68L562 3.63L556 3.63L550 3.67L544 3.76L538 3.87L531 4.00L525 4.13L519 4.26L512 4.37L506 4.44L500 4.47L494 4.47L488 4.42L481 4.34L475 4.24L469 4.14L462 4.04L456 3.96L450 3.92L444 3.92L438 3.97L431 4.06L425 4.19L419 4.35L412 4.53L406 4.71L400 4.87L394 5.00L388 5.10L381 5.14L375 5.14L369 5.10L362 5.01L356 4.90L350 4.77L344 4.65L338 4.54L331 4.46L325 4.42L319 4.42L312 4.45L306 4.52L300 4.61L294 4.71L288 4.81L281 4.89L275 4.95L269 4.96L262 4.93L256 4.86L250 4.74L244 4.60L238 4.43L231 4.27L225 4.11L219 3.99L212 3.89L206 3.85L200 3.84L194 3.89L188 3.97L181 4.07L175 4.19L169 4.31L162 4.41L156 4.47L150 4.50L144 4.48L138 4.42L131 4.31L125 4.18L119 4.02L112 3.86L106 3.70L100 3.57L94 3.48L88 3.42L81 3.40L75 3.43L69 3.48L62 3.55L56 3.63L50 3.71L44 3.76L38 3.77L31 3.75L25 3.68L19 3.56L12 3.44L6 3.29L0 3.12Z' fill='url(%23g)'/%3E%3C/svg%3E");
  --hairline-1: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 6' preserveAspectRatio='none'%3E%3Cdefs%3E%3ClinearGradient id='g' gradientUnits='userSpaceOnUse' x1='0' y1='0' x2='1000' y2='0'%3E%3Cstop offset='0' stop-color='%23b8aa90' stop-opacity='.35'/%3E%3Cstop offset='.5' stop-color='%23b8aa90' stop-opacity='1'/%3E%3Cstop offset='1' stop-color='%23b8aa90' stop-opacity='.45'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M0 3.93L6 3.86L12 3.76L19 3.63L25 3.48L31 3.35L38 3.24L44 3.17L50 3.15L56 3.18L62 3.25L69 3.35L75 3.48L81 3.62L88 3.74L94 3.85L100 3.93L106 3.96L112 3.95L119 3.90L125 3.80L131 3.68L138 3.54L144 3.40L150 3.27L156 3.16L162 3.09L169 3.06L175 3.08L181 3.12L188 3.20L194 3.29L200 3.39L206 3.47L212 3.53L219 3.55L225 3.53L231 3.46L238 3.35L244 3.20L250 3.03L256 2.85L262 2.68L269 2.52L275 2.40L281 2.32L288 2.29L294 2.30L300 2.36L306 2.45L312 2.56L319 2.68L325 2.79L331 2.87L338 2.92L344 2.93L350 2.90L356 2.83L362 2.72L369 2.59L375 2.45L381 2.31L388 2.19L394 2.10L400 2.05L406 2.04L412 2.07L419 2.13L425 2.22L431 2.31L438 2.41L444 2.48L450 2.52L456 2.53L462 2.48L469 2.39L475 2.26L481 2.09L488 1.91L494 1.71L500 1.53L506 1.37L512 1.25L519 1.17L525 1.14L531 1.15L538 1.21L544 1.29L550 1.40L556 1.50L562 1.60L569 1.67L575 1.71L581 1.70L588 1.66L594 1.58L600 1.48L606 1.36L612 1.24L619 1.14L625 1.06L631 1.02L638 1.02L644 1.06L650 1.15L656 1.27L662 1.41L669 1.56L675 1.70L681 1.82L688 1.91L694 1.95L700 1.95L706 1.90L712 1.81L719 1.69L725 1.55L731 1.41L738 1.28L744 1.18L750 1.12L756 1.10L762 1.12L769 1.19L775 1.29L781 1.42L788 1.56L794 1.69L800 1.81L806 1.89L812 1.94L819 1.95L825 1.91L831 1.85L838 1.76L844 1.67L850 1.58L856 1.51L862 1.47L869 1.47L875 1.51L881 1.61L888 1.74L894 1.91L900 2.09L906 2.28L912 2.46L919 2.61L925 2.73L931 2.80L938 2.83L944 2.82L950 2.76L956 2.69L962 2.60L969 2.52L975 2.44L981 2.39L988 2.38L994 2.41L1000 2.49L1000 2.74L994 2.66L988 2.63L981 2.64L975 2.69L969 2.77L962 2.89L956 3.01L950 3.12L944 3.22L938 3.27L931 3.29L925 3.27L919 3.20L912 3.10L906 2.98L900 2.85L894 2.72L888 2.62L881 2.54L875 2.51L869 2.52L862 2.57L856 2.66L850 2.78L844 2.92L838 3.05L831 3.17L825 3.26L819 3.31L812 3.31L806 3.27L800 3.19L794 3.07L788 2.92L781 2.77L775 2.62L769 2.49L762 2.40L756 2.34L750 2.33L744 2.37L738 2.45L731 2.55L725 2.67L719 2.80L712 2.91L706 2.99L700 3.05L694 3.06L688 3.04L681 2.98L675 2.89L669 2.79L662 2.68L656 2.59L650 2.53L644 2.50L638 2.52L631 2.59L625 2.70L619 2.84L612 3.01L606 3.19L600 3.37L594 3.52L588 3.65L581 3.73L575 3.76L569 3.75L562 3.69L556 3.61L550 3.49L544 3.38L538 3.27L531 3.18L525 3.13L519 3.12L512 3.14L506 3.21L500 3.31L494 3.44L488 3.57L481 3.69L475 3.80L469 3.88L462 3.92L456 3.91L450 3.87L444 3.79L438 3.69L431 3.58L425 3.47L419 3.38L412 3.32L406 3.30L400 3.33L394 3.41L388 3.53L381 3.69L375 3.87L369 4.06L362 4.23L356 4.39L350 4.51L344 4.58L338 4.62L331 4.60L325 4.55L319 4.46L312 4.36L306 4.26L300 4.17L294 4.11L288 4.08L281 4.09L275 4.14L269 4.23L262 4.34L256 4.47L250 4.59L244 4.70L238 4.79L231 4.83L225 4.84L219 4.79L212 4.71L206 4.59L200 4.45L194 4.29L188 4.15L181 4.02L175 3.93L169 3.88L162 3.88L156 3.92L150 4.00L144 4.11L138 4.24L131 4.37L125 4.49L119 4.57L112 4.63L106 4.63L100 4.60L94 4.51L88 4.40L81 4.26L75 4.11L69 3.97L62 3.84L56 3.75L50 3.69L44 3.68L38 3.70L31 3.76L25 3.83L19 3.92L12 4.01L6 4.11L0 4.18Z' fill='url(%23g)'/%3E%3C/svg%3E");
  --hairline-2: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 6' preserveAspectRatio='none'%3E%3Cdefs%3E%3ClinearGradient id='g' gradientUnits='userSpaceOnUse' x1='0' y1='0' x2='1000' y2='0'%3E%3Cstop offset='0' stop-color='%23b8aa90' stop-opacity='.35'/%3E%3Cstop offset='.5' stop-color='%23b8aa90' stop-opacity='1'/%3E%3Cstop offset='1' stop-color='%23b8aa90' stop-opacity='.45'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M0 3.40L6 3.35L12 3.34L19 3.37L25 3.42L31 3.51L38 3.61L44 3.72L50 3.82L56 3.89L62 3.93L69 3.92L75 3.87L81 3.78L88 3.65L94 3.49L100 3.32L106 3.16L112 3.01L119 2.89L125 2.80L131 2.76L138 2.76L144 2.80L150 2.86L156 2.94L162 3.01L169 3.08L175 3.11L181 3.11L188 3.06L194 2.97L200 2.84L206 2.68L212 2.50L219 2.32L225 2.14L231 2.00L238 1.88L244 1.82L250 1.80L256 1.83L262 1.90L269 2.00L275 2.11L281 2.23L288 2.33L294 2.40L300 2.44L306 2.44L312 2.39L319 2.30L325 2.18L331 2.03L338 1.89L344 1.75L350 1.63L356 1.54L362 1.49L369 1.49L375 1.52L381 1.59L388 1.67L394 1.77L400 1.85L406 1.92L412 1.95L419 1.94L425 1.89L431 1.79L438 1.66L444 1.49L450 1.32L456 1.14L462 0.98L469 0.85L475 0.76L481 0.71L488 0.72L494 0.77L500 0.87L506 0.99L512 1.12L519 1.25L525 1.37L531 1.46L538 1.51L544 1.52L550 1.49L556 1.43L562 1.35L569 1.25L575 1.15L581 1.08L588 1.02L594 1.01L600 1.04L606 1.11L612 1.22L619 1.36L625 1.52L631 1.68L638 1.82L644 1.94L650 2.02L656 2.06L662 2.05L669 2.00L675 1.90L681 1.79L688 1.66L694 1.53L700 1.42L706 1.34L712 1.30L719 1.31L725 1.36L731 1.46L738 1.59L744 1.74L750 1.89L756 2.04L762 2.17L769 2.26L775 2.32L781 2.33L788 2.31L794 2.25L800 2.18L806 2.10L812 2.03L819 1.98L825 1.96L831 1.99L838 2.06L844 2.17L850 2.32L856 2.50L862 2.69L869 2.88L875 3.05L881 3.20L888 3.30L894 3.36L900 3.37L906 3.34L912 3.27L919 3.18L925 3.09L931 3.00L938 2.93L944 2.90L950 2.90L956 2.96L962 3.05L969 3.19L975 3.35L981 3.52L988 3.67L994 3.80L1000 3.91L1000 4.16L994 4.05L988 3.92L981 3.77L975 3.62L969 3.50L962 3.39L956 3.32L950 3.29L944 3.30L938 3.36L931 3.44L925 3.55L919 3.67L912 3.78L906 3.88L900 3.94L894 3.96L888 3.94L881 3.88L875 3.79L869 3.66L862 3.53L856 3.39L850 3.28L844 3.19L838 3.14L831 3.13L825 3.16L819 3.24L812 3.35L806 3.47L800 3.60L794 3.72L788 3.82L781 3.87L775 3.89L769 3.85L762 3.77L756 3.65L750 3.50L744 3.33L738 3.17L731 3.02L725 2.90L719 2.81L712 2.77L706 2.77L700 2.81L694 2.89L688 2.98L681 3.07L675 3.16L669 3.23L662 3.26L656 3.26L650 3.21L644 3.13L638 3.01L631 2.88L625 2.75L619 2.62L612 2.52L606 2.45L600 2.43L594 2.46L588 2.53L581 2.65L575 2.79L569 2.95L562 3.11L556 3.26L550 3.38L544 3.46L538 3.50L531 3.49L525 3.44L519 3.35L512 3.23L506 3.11L500 2.99L494 2.89L488 2.82L481 2.78L475 2.79L469 2.84L462 2.92L456 3.03L450 3.15L444 3.26L438 3.36L431 3.43L425 3.46L419 3.46L412 3.40L406 3.32L400 3.20L394 3.07L388 2.94L381 2.82L375 2.74L369 2.69L362 2.69L356 2.74L350 2.83L344 2.96L338 3.13L331 3.30L325 3.47L319 3.63L312 3.76L306 3.84L300 3.89L294 3.89L288 3.85L281 3.78L275 3.69L269 3.60L262 3.52L256 3.46L250 3.43L244 3.44L238 3.50L231 3.59L225 3.71L219 3.85L212 4.00L206 4.13L200 4.24L194 4.32L188 4.35L181 4.33L175 4.28L169 4.18L162 4.06L156 3.92L150 3.78L144 3.66L138 3.57L131 3.52L125 3.51L119 3.55L112 3.64L106 3.75L100 3.89L94 4.03L88 4.16L81 4.27L75 4.34L69 4.38L62 4.36L56 4.31L50 4.21L44 4.09L38 3.96L31 3.83L25 3.71L19 3.62L12 3.59L6 3.60L0 3.65Z' fill='url(%23g)'/%3E%3C/svg%3E");
  --hairline-3: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 6' preserveAspectRatio='none'%3E%3Cdefs%3E%3ClinearGradient id='g' gradientUnits='userSpaceOnUse' x1='0' y1='0' x2='1000' y2='0'%3E%3Cstop offset='0' stop-color='%23b8aa90' stop-opacity='.35'/%3E%3Cstop offset='.5' stop-color='%23b8aa90' stop-opacity='1'/%3E%3Cstop offset='1' stop-color='%23b8aa90' stop-opacity='.45'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M0 3.27L6 3.37L12 3.46L19 3.52L25 3.54L31 3.52L38 3.44L44 3.31L50 3.16L56 2.98L62 2.80L69 2.62L75 2.46L81 2.34L88 2.26L94 2.22L100 2.23L106 2.27L112 2.33L119 2.40L125 2.48L131 2.53L138 2.55L144 2.54L150 2.49L156 2.39L162 2.26L169 2.10L175 1.92L181 1.75L188 1.60L194 1.47L200 1.38L206 1.34L212 1.35L219 1.40L225 1.49L231 1.60L238 1.73L244 1.85L250 1.96L256 2.03L262 2.06L269 2.05L275 1.99L281 1.90L288 1.77L294 1.63L300 1.49L306 1.36L312 1.25L319 1.18L325 1.15L331 1.16L338 1.21L344 1.29L350 1.39L356 1.49L362 1.58L369 1.65L375 1.69L381 1.68L388 1.63L394 1.54L400 1.42L406 1.27L412 1.12L419 0.97L425 0.84L431 0.75L438 0.70L444 0.71L450 0.76L456 0.85L462 0.99L469 1.14L475 1.31L481 1.46L488 1.60L494 1.71L500 1.77L506 1.80L512 1.78L519 1.73L525 1.66L531 1.57L538 1.49L544 1.43L550 1.40L556 1.41L562 1.46L569 1.55L575 1.67L581 1.82L588 1.98L594 2.14L600 2.28L606 2.39L612 2.46L619 2.49L625 2.46L631 2.40L638 2.30L644 2.18L650 2.05L656 1.93L662 1.84L669 1.78L675 1.76L681 1.79L688 1.86L694 1.98L700 2.12L706 2.29L712 2.45L719 2.60L725 2.73L731 2.82L738 2.87L744 2.88L750 2.85L756 2.80L762 2.72L769 2.65L775 2.58L781 2.54L788 2.53L794 2.56L800 2.64L806 2.76L812 2.91L819 3.08L825 3.26L831 3.43L838 3.58L844 3.70L850 3.77L856 3.79L862 3.77L869 3.71L875 3.61L881 3.50L888 3.38L894 3.27L900 3.19L906 3.15L912 3.14L919 3.19L925 3.27L931 3.39L938 3.53L944 3.68L950 3.82L956 3.95L962 4.05L969 4.10L975 4.12L981 4.09L988 4.01L994 3.89L1000 3.75L1000 4.00L994 4.14L988 4.26L981 4.38L975 4.47L969 4.51L962 4.50L956 4.44L950 4.35L944 4.23L938 4.11L931 3.98L925 3.88L919 3.81L912 3.78L906 3.79L900 3.84L894 3.92L888 4.03L881 4.16L875 4.28L869 4.39L862 4.47L856 4.51L850 4.50L844 4.46L838 4.38L831 4.27L825 4.14L819 4.01L812 3.89L806 3.80L800 3.74L794 3.72L788 3.75L781 3.83L775 3.93L769 4.07L762 4.20L756 4.34L750 4.45L744 4.52L738 4.56L731 4.54L725 4.48L719 4.38L712 4.24L706 4.08L700 3.92L694 3.76L688 3.63L681 3.53L675 3.47L669 3.45L662 3.48L656 3.53L650 3.61L644 3.69L638 3.76L631 3.82L625 3.84L619 3.83L612 3.77L606 3.67L600 3.54L594 3.39L588 3.22L581 3.07L575 2.93L569 2.83L562 2.76L556 2.75L550 2.79L544 2.86L538 2.98L531 3.11L525 3.25L519 3.39L512 3.50L506 3.58L500 3.61L494 3.60L488 3.55L481 3.46L475 3.34L469 3.21L462 3.07L456 2.95L450 2.86L444 2.81L438 2.79L431 2.82L425 2.88L419 2.97L412 3.07L406 3.17L400 3.26L394 3.32L388 3.35L381 3.33L375 3.27L369 3.17L362 3.03L356 2.88L350 2.72L344 2.57L338 2.45L331 2.36L325 2.31L319 2.32L312 2.37L306 2.47L300 2.60L294 2.74L288 2.89L281 3.03L275 3.15L269 3.23L262 3.27L256 3.27L250 3.22L244 3.15L238 3.05L231 2.95L225 2.86L219 2.79L212 2.75L206 2.75L200 2.79L194 2.87L188 2.99L181 3.13L175 3.27L169 3.42L162 3.54L156 3.63L150 3.68L144 3.68L138 3.64L131 3.56L125 3.44L119 3.31L112 3.18L106 3.06L100 2.96L94 2.90L88 2.88L81 2.91L75 2.98L69 3.09L62 3.23L56 3.37L50 3.51L44 3.63L38 3.72L31 3.77L25 3.79L19 3.77L12 3.71L6 3.62L0 3.52Z' fill='url(%23g)'/%3E%3C/svg%3E");
}
/* /HAIRLINES */

/* TICK-MASK — generated by tools/export-marks.py; do not hand-edit */
:root { --tick-mask: url("data:image/png;base64,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"); }
/* /TICK-MASK */

/* A safety net, not the plan: the columns are sized to fit a 375px screen. */
.compare-scroll { overflow-x: auto; }

/* Fixed layout with the widths on <col>, so every column is exactly as wide in
   every row — the group rows and the hairlines depend on it. Separate borders,
   because a collapsed table cannot round the Plus panel's corners. */
.compare {
  width: 100%;
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 1.0625rem;
  text-align: left;
}

/* 13.5rem, up from 10, because the columns felt compressed. It is also what
   the wordmark lockup needs: "FAIRHAND Plus" is ~172px, and at 10rem the column
   gave it 136px. */
.plan-col { width: 13.5rem; }

/* ---- the names ------------------------------------------------------------
   The painted wordmark, as the app's header draws it: "Fairhand", and
   "Fairhand Plus", never "Fairhand Free". $0 already says what the first plan
   costs; a second name would only repeat it.

   "Plus" is set in the display serif in the household blue, sized so its capital
   stands as tall as the painted letters: the wordmark's caps fill ~85% of its
   height, and Playfair's cap height is ~0.71em, so a 1.3rem mark wants a ~1.55rem
   word. There is no "Fairhand Plus" lockup in the app to copy (its paywall sets
   the name as a small label), so this pairing is the site's, built from the
   app's two pieces. */

.tier-wordmark {
  display: inline-block;
  width: auto;
  height: 1.3rem;
  vertical-align: middle;
}

/* One line where there is room, the word wrapping under the mark where not. */
.tier-lockup {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.15rem 0.4rem;
}

.tier-plus-word {
  color: var(--accent);
  font: 600 1.55rem/1 "Fairhand Display", Georgia, serif;
  letter-spacing: -0.01em;
  transform: translateY(0.04em); /* the serif's optical centre sits a hair high of the painted caps */
}

/* ---- the plans: the control, the promise, the prices ------------------------
   One row above the table. On a wide screen its columns ARE the table's
   columns (the same three widths, and the table is fixed-layout with no border
   spacing), so each plan stands over its ticks and the Plus panel runs from its
   card straight down into the table. */

.plans {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 13.5rem 13.5rem;
  margin-top: clamp(0.5rem, 2vw, 1rem);
}

.plans-intro {
  align-self: end;
  padding: 0 2.5rem 1.75rem 0;
}

.plans-note {
  max-width: 44ch;
  margin: 1rem 0 0;
  color: var(--slate);
  font-size: 1.0625rem;
  line-height: 1.55;
  font-weight: 450;
}

.plan-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 1.5rem 0.75rem 1.75rem;
  text-align: center;
}

/* `--surface` rather than an accent wash: the accent is a dark navy, so a wash
   of it DARKENS the ground and took stone text to 4.32:1 on the old featured
   card. */
.plan-card.is-plus { background: var(--surface); border-radius: 18px 18px 0 0; }

/* The pill's row is held open in both plans, so the two names sit level. */
.plan-flag { display: block; min-height: 1.65rem; margin-bottom: 0.85rem; }

.plan-pill {
  display: inline-block;
  padding: 0.4rem 0.75rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent);
  font: 600 0.75rem/1 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.plan-pill.quiet {
  background: transparent;
  color: var(--slate);
  box-shadow: inset 0 0 0 1px var(--edge);
}

.plan-name { display: block; margin-bottom: 1.1rem; }

/* The number is the headline: display serif, large, in ink, with its period
   small beside it in the working sans. */
.plan-price {
  display: block;
  color: var(--slate);
  font: 500 1.0625rem/1 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  white-space: nowrap;
}

/* ⚠️ LINING figures. The display serif's default numerals are old-style, which
   is right in running text and wrong here: "$0" read as "$o", and the 3 and 9 of
   $39.99 hung below the line. The self-hosted subset keeps `lnum` for this. */
.plan-price strong {
  margin-right: 0.1em;
  color: var(--ink);
  font: 600 2.6rem/1 "Fairhand Display", Georgia, serif;
  font-variant-numeric: lining-nums;
  letter-spacing: -0.02em;
}

.plan-sub {
  display: block;
  margin-top: 0.55rem;
  color: var(--slate);
  font-size: 0.9375rem;
  line-height: 1.35;
}

/* The two Plus prices, and the two pills, hold their height when the control
   switches, so nothing beneath them jumps.

   ⚠️ By STACKING, not by a min-height. A min-height was a guess once, and
   measured it came up short: every row jumped 12px on the switch. Once the
   control is live both plans share one grid cell and the unselected one is only
   INVISIBLE, so the cell is always as tall as the taller plan, whatever the
   fonts do. Scoped to .billing-live because with no script both plans show,
   one after the other, and stacked they would overlap. visibility:hidden also
   takes the unselected plan out of the accessibility tree, as [hidden] did.

   ⚠️ And `:not([hidden])` on the display rule is load-bearing: a plain
   `.plan-stack > .plan` outranks the UA's `[hidden]`, and both prices rendered
   at once until that was caught by driving the control. */
.plan-stack > .plan:not([hidden]) { display: block; }
body.billing-live .plan-stack { display: grid; }
body.billing-live .plan-stack > .plan { grid-area: 1 / 1; }
body.billing-live .plan-stack > .plan[hidden] { display: block; visibility: hidden; opacity: 0; }
body.billing-live .plan-stack > .plan { transition: opacity 0.22s ease; }

/* With no script, both prices show, the second divided so the two cannot be read
   as one offer; one pill is enough. */
.plan-stack > .plan + .plan { margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px dashed var(--rule); }
body.billing-live .plan-stack > .plan + .plan { margin-top: 0; padding-top: 0; border-top: 0; }
body:not(.billing-live) .plan-flag > .plan[data-plan="monthly"] { display: none; }

/* ---- the table's own header -------------------------------------------------
   Only the names. A wide screen hides them visually, because the plans above
   head those columns there; a phone shows them over the ticks, where the plan
   cards no longer line up with the columns. */

.compare thead td:empty { padding: 0; }

.compare thead th {
  padding: 1rem 0.3rem 0.5rem;
  text-align: center;
  vertical-align: bottom;
  font-weight: 400;
}

@media (min-width: 48rem) {
  .compare thead th { padding: 0; }
  .compare thead th > * {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* ---- the Plus panel: one card, top to bottom ------------------------------- */

.compare thead .tier-plus,
.compare tbody td:last-child { background: var(--surface); }

.compare thead th.tier-plus { border-radius: 18px 18px 0 0; }
.compare tbody:last-of-type tr:last-child td:last-child { border-radius: 0 0 18px 18px; }

/* ---- the answers, under a pointer ------------------------------------------
   A row of sixteen is read across, and the eye loses the line it is on. The
   hover is the faintest wash of the household blue, the page's own colour for
   "this one": 4% over the paper, where the quiet text still clears the bar and
   the row labels are ink. Pointer devices only, and never on the group
   headings, which are not rows anybody reads across. */
@media (hover: hover) {
  .compare tbody tr > * { transition: background-color 0.18s ease; }
  .compare tbody tr:not(.group):hover > * { background-color: rgba(var(--accent-rgb), 0.04); }
  .compare tbody tr:not(.group):hover > td:last-child { background-color: rgba(var(--accent-rgb), 0.055); }
}

/* ---- groups, divided by the app's hairline ---------------------------------
   The app's brushed hairline, generated from its own formula (see
   make-hairlines.py): a
   stroke with a slow drift and a fine tremor, fading at both ends, one seed per
   group so no two are the same. BETWEEN GROUPS, not under every row — the app
   divides a page this way, and fifteen straight rules were most of what made the
   table read as a spreadsheet.

   ⚠️ It is the GROUP LABEL CELL's background, and that cell spans the label and
   Free columns. It was the row group's background first, on the reasoning that
   a <tbody> background paints one continuous image across its row — measured
   on the page, Chrome restarted the image in EVERY cell, so each cell drew its
   own thin-thick-thin taper and the line broke into a long stroke, a gap, and a
   faint fragment inside the Free column. One cell, one line. It tapers to
   nothing exactly at the Plus card's edge, so it reads as tucking under it.

   ⚠️ Tuned, not ported — see make-hairlines.py. The app's literal numbers
   rendered here as a straight, even line; the shape that reads as brushed is
   a filled taper, a hairline at both ends and fuller mid-line. Its colour,
   #B8AA90, is darker than --rule for the same reason: at the app's #E2D9C8 a
   line that fades to 35% at its ends all but vanished on the web. Decorative
   still — never a boundary anything depends on. */

.compare .group th { background: 0 0 / 100% 6px no-repeat; }
.compare .h0 .group th { background-image: var(--hairline-0); }
.compare .h1 .group th { background-image: var(--hairline-1); }
.compare .h2 .group th { background-image: var(--hairline-2); }
.compare .h3 .group th { background-image: var(--hairline-3); }

/* In the household blue, the same navy as the dish names and "Plus": the
   group names are the table's headings, and stone left them reading as notes. */
.compare .group th {
  padding: 1.65rem 0 0.35rem;
  color: var(--accent);
  font: 600 0.8125rem/1.2 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.compare .group td { padding: 0; }

.compare tbody th[scope="row"] {
  padding: 0.6rem 1.5rem 0.6rem 0;
  color: var(--ink);
  font-weight: 450;
  vertical-align: middle;
}

.compare tbody td {
  padding: 0.6rem 0.5rem;
  text-align: center;
  vertical-align: middle;
}

.compare tbody:last-of-type tr:last-child > * { padding-bottom: 1.2rem; }

/* ---- the marks --------------------------------------------------------------
   Included is the app's painted tick, the one its grocery list checks an item
   off with, in the household blue.

   ⚠️ Its mask is INLINED, for the same reason the loud button's is. A mask that
   has not loaded paints nothing, and here an invisible tick is not cosmetic: it
   is an empty cell, which reads as NOT INCLUDED — the table would silently say
   the opposite of the truth until the file arrived, or for good if it failed. */

.tick {
  display: inline-block;
  width: 1.1rem;
  height: 1.19rem;
  vertical-align: middle;
  background: var(--member-1);
  -webkit-mask: var(--tick-mask) center / contain no-repeat;
          mask: var(--tick-mask) center / contain no-repeat;
}

/* Not included: a short quiet stroke rather than a glyph — present, so the cell
   reads as answered, and faint, so the ticks carry the column. */
.dash {
  display: inline-block;
  width: 0.75rem;
  height: 1.5px;
  border-radius: 1px;
  background: var(--edge);
  opacity: 0.55;
  vertical-align: middle;
}

/* Kroger's own mark in the row that names them. alt="Kroger", so the row still
   reads aloud as a sentence. Sized to the line, as attribution rather than as a
   partner badge — see site/README.md, "Third-party marks". */
.inline-logo {
  display: inline-block;
  width: auto;
  height: 1.15em;
  margin: 0 0.1em;
  vertical-align: -0.28em;
}

/* Under 48rem the plans stop lining up with the table: the control and its
   promise take the full width, and the two plans become cards side by side. */
@media (max-width: 47.99rem) {
  .plans { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0.6rem; }
  .plans-intro { grid-column: 1 / -1; padding: 0 0 1rem; }
  /* The control takes the whole row and splits it in half. Sized to its words it
     ended somewhere arbitrary — short of the right edge, level with nothing —
     which on a phone reads as a stray object rather than a switch with two
     sides. Two equal halves say what it is before it is read, and because the
     plans below are also two equal halves of the same row, each word lands over
     the price it changes. */
  .billing { display: flex; width: 100%; }
  .billing button { flex: 1; justify-content: center; }
  .plan-card { padding: 1.1rem 0.5rem 1.25rem; border: 1px solid var(--rule); border-radius: 18px; }
  .plan-card.is-plus { border-color: transparent; border-radius: 18px; }
  .plan-flag { margin-bottom: 0.7rem; }
  .plan-name { margin-bottom: 0.8rem; }
  .plan-price { white-space: normal; }
  .plan-price strong { font-size: 2rem; white-space: nowrap; }
  .compare-scroll { margin-top: 1rem; }
}

@media (max-width: 40rem) {
  /* 5.4rem, not 6.2: the rows read at body size now, and the label column needs
     the room more than a column holding one tick does. The name lockup still
     fits, the word wrapping under the mark. */
  .plan-col { width: 5.4rem; }
  /* The names over the ticks, sized to the narrower column: at 0.95rem the
     wordmark is 81px and the column holds 76px inside its padding. */
  .compare thead .tier-wordmark { height: 0.8rem; }
  .tier-wordmark { height: 0.95rem; }
  /* 1.4rem, not smaller: the ink press has a measured ~22px floor for the serif. */
  .tier-plus-word { font-size: 1.4rem; }
  .compare tbody th[scope="row"] { padding-right: 0.75rem; }
}

/* ============================================================== the maker ===
   The page's one section in the first person, after the price.

   ⚠️ ITS WIDTH IS THE POINT. Every other section on this spine fills it — two
   promises, two columns of count, a table the full width — and this one is a
   single 36rem column with the rest of the row left empty. That is not an
   unfinished layout: a note is one column, and the space beside it is what
   changes the voice before a word is read. Do not balance it.

   Set at the page's running size in the page's quiet colour, like every other
   paragraph here. A personal section that also changed size and colour would be
   asking for attention twice. */

/* The heading beside the note, the way the count section sets its number beside
   its copy — the page's existing rhythm rather than a new one. A single column
   was tried first and left half the spine empty, which reads as a layout that
   did not finish rather than as a pause. */
/* Not halves. The heading is three words and the note is six paragraphs, so an
   even split left the short side mostly empty — the column is sized to what
   stands in it. */
.maker-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.72fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: start;
}

.maker h2 {
  font: 600 clamp(2rem, 5.2vw, 3.25rem)/1.04 "Fairhand Display", Georgia, serif;
  letter-spacing: -0.02em;
  margin: 0;
  max-width: 8ch;
  text-wrap: balance;
}

.maker-copy {
  max-width: 40ch;
}

/* The free-recipes line, in the accent and a step up. It is the one sentence in
   this note that is a fact rather than a feeling, and it answers the question
   the price above it just raised — so it takes the page's second voice, the
   navy it uses for the half of a title that answers. 9.46:1 on the paper.

   ⚠️ No brush stroke under it, and that was measured rather than chosen. The
   stroke's box is an inline-block, so a phrase that wraps takes the column's
   full width and the paint runs on past the words — and this phrase wraps on
   every phone. Colour and weight carry emphasis at any width; a stroke does not. */
.maker-copy .maker-free {
  color: var(--accent);
  font-size: clamp(1.125rem, 1.7vw, 1.25rem);
  font-weight: 600;
  line-height: 1.35;
  margin: 1.1rem 0 0;
  max-width: 27ch;
  text-wrap: pretty;
}

@media (max-width: 47.99rem) {
  .maker-grid { grid-template-columns: minmax(0, 1fr); }
  .maker h2 { max-width: 16ch; }
}

.maker-copy p {
  color: var(--slate);
  font-size: 1.0625rem;
  font-weight: 450;
  line-height: 1.6;
  margin: 1.1rem 0 0;
  text-wrap: pretty;
}

/* Name and mark on one line, in ink: this is the sentence that says who is
   talking, so it sits a step above the paragraphs under it rather than beside
   them. The token is the household demo's own component at the size the app
   draws a member in a form row. */
/* ⚠️ The mark is INLINE here, not a flex sibling. The household demo sets its
   name lockup with flex because a name is one line; this sentence wraps to two
   on a phone, and a centred flex item then floats the mark into the gap between
   them, beside neither line. Inline, it sits on the first line where a person
   reading "I'm" expects it. */
.maker-copy .maker-who {
  margin: 1rem 0 0.4rem;
  color: var(--ink);
  font-weight: 500;
}

.maker-copy .maker-who .token {
  font-size: 2.15rem;
  vertical-align: -0.34em;
  margin-right: 0.3rem;
}

/* ======================================================= the closing panel ===
   The page's one block of colour, holding the last call to action: the app's
   painted button in bone, `.brush-cta.on-dark`. It held Apple's white store
   badge until 2026-09-16. */

.closing {
  /* The painted ultramarine wash (C5 from the feature-background round), not a
     flat fill — the brush is still in it, which is the difference between the
     page's one block of colour reading as painted or as a CSS rectangle. The
     solid accent stays underneath as the fallback for a browser that does not
     get the image, and it is close enough that nothing about the panel breaks
     if it never arrives. */
  background: var(--accent) url("/art/ultramarine.webp") center / cover no-repeat;
  color: var(--on-accent);
  /* ⚠️ THE SAME COLUMN AS EVERY OTHER SECTION, taken from `.wrap`. This panel is
     not a `.wrap` — it inherited the document pages' column instead, which caps
     at `--wide` and puts the gutter INSIDE as padding. Past a desktop that caps
     and the panel is inset, so it looked right where it was built; below 68rem
     nothing capped and the one block of colour on the page ran edge to edge
     while every section above it stopped at the gutter. Its own 22px corners
     said so the whole time — a radius pressed against the screen edge is a card
     that was meant to sit in from it. */
  width: min(100% - 2 * var(--gutter), var(--wide));
  margin-inline: auto;
  border-radius: 22px;
  padding: clamp(3rem, 7vw, 4.5rem) clamp(1.5rem, 4vw, 3rem);
  margin-block: clamp(1.5rem, 4vw, 2.5rem);
}

/* From 48rem the panel is about 2.2:1 and shows only the middle band of the
   square, so it gets that band at the full 2048px instead of a whole square
   stretched to reach it (export-marks.py). The square stays for phones, where
   the panel is roughly as deep as it is wide. */
@media (min-width: 48rem) {
  .closing { background-image: url("/art/ultramarine-wide.webp"); }
}

.closing h2 { color: var(--on-accent); }

/* Bone at 78% measures 6.81:1 on the flat accent and 4.99:1 on the wash's
   lightest half-percent — the panel's body copy is the same subordinate voice
   the quiet text colour gives it on paper, without going grey on blue. Measured,
   not guessed: the first draft of this comment claimed 8.0 and the checker
   disagreed. It said 6.48 on the wash until the wash was exported at full
   resolution (2026-09-16), which lets its pale streaks through; see
   export-marks.py. */
.closing .note-under { color: rgba(252, 248, 239, 0.78); }

.closing .cta-note { color: rgba(252, 248, 239, 0.78); }
.closing .cta-note a { color: var(--on-accent); }

/* ----------------------------------------------------------- the billing ---
   Both plans are visible until home.js takes over; see its setUpBilling. The
   control lives in the plans row, beside the prices it changes. */

.billing {
  display: inline-flex;
  gap: 0.25rem;
  padding: 0.25rem;
  border-radius: 999px;
  background: var(--surface);
  flex: none;
}

.billing[hidden] { display: none; }

.billing button {
  font: inherit;
  font-size: 1rem;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.5rem 0.95rem;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--slate);
  cursor: pointer;
  transition: background-color 0.16s ease, color 0.16s ease;
}

/* ⚠️ A DARKER CREAM, not the ochre and not the navy before it. The navy made
   the chosen plan one more blue thing on a page already carrying blue in the
   headline, the prices and the ticks; the ochre answered that and read as loud.
   This is the paper's own hue taken down in lightness, so the control says
   "this one" without introducing a colour.

   ⚠️ AND IT IS THE QUIETEST OF THE THREE BY SOME WAY. Against the control's own
   bone it measures 1.60, where the ochre was 2.87 and both are under the 3.0 a
   boundary owes when the boundary is what says which plan is chosen. Cream on
   cream cannot clear that bar at any lightness that still reads as cream — the
   darkest that does is about 2.09. So the state is carried by the LABEL, which
   goes slate 500 to ink 600 at 10.18 on this fill, and check-contrast.py prints
   the fill's number rather than calling it a pass. */
.billing button[aria-pressed="true"] {
  background: var(--chosen);
  color: var(--ink);
  font-weight: 600;
}

.billing button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.billing .save {
  font-size: 0.8125rem;
  padding: 0.12rem 0.45rem;
  border-radius: 999px;
  /* ⚠️ INK-FILLED, with the word in bone: 16.25:1, and it reads on BOTH states.
     It was a bone chip with an ink word, which worked while the chosen pill was
     navy or ochre and disappears on a cream one — bone on this fill is 1.32.
     A dark chip on a light pill is the one arrangement that holds either way. */
  background: var(--ink);
  color: var(--on-accent);
  font-weight: 600;
}

/* No line on the chip in either state any more: it is ink-filled, so it stands
   off the bone control at 15.30 and off the cream pill at 10.18 on its own. */


/* ================================================================ ink press ===
   The app's ink press, on the web. The filter itself is at the top of
   index.html; this is where it is applied, and why only here.

   HOW THE NUMBERS WERE CHOSEN. Rendered at 2x in headless Chrome using the
   site's own display face at its own sizes, then judged two ways — enlarged
   with nearest-neighbour to see real edge pixels, and downscaled with Lanczos
   to approximate what the eye resolves on a retina screen at normal distance.
   Four rounds:

     app parity  0.9 freq / 0.4 disp / 1.6% grain   INVISIBLE. ±0.2 CSS px.
     1-octave    0.6 / 1.6 / 6% and up              SPIKY. The edges tore into
                 crumbs — exactly the failure the app's shader notes warn about ("white
                 noise would read as a fuzzy or dirty edge"). The cause is the
                 RATIO: the app wobbles over ~5x the distance it displaces, and
                 these wobbled over ~2x, so neighbouring pixels pulled apart.
     smooth      0.2 / 2 oct / 1.8 / 6%             INK when enlarged, but
                 WORN LETTERPRESS at perceived size. Overdone — the shader's own
                 note: "easy to overdo, and the numbers are smaller than they
                 look."
     ⇒ chosen    0.25 / 2 oct / 1.0 / 5%            Pressed into paper. Edges
                 gently irregular, fill with a hint of tooth, nothing distressed.

   Two octaves rather than one, because real ink has both scales at once: a
   slow meander in the edge and finer detail riding on it.

   ⚠️ DISPLAY HEADINGS ONLY, AND THE FLOOR IS ~22px. Measured, not assumed: at
   17-18px this same setting breaks thin serifs into small holes, while at 22px
   and up it reads as printed. So the week's dish names (16px) stay crisp,
   and so do the feature pills and the small initials in the week's marks. That
   is the app's own rule meeting the web's resolution: "Titles earn it; running
   text does not", and a roughness that suits a masthead "destroys a caption". Body copy, the script
   hand and the painted wordmark are never touched: the first is text, the
   other two are already drawn.

   It does not cost what it costs in the app. Each use there rasterises a layer
   per text run; here it is five headings on one page. */

.ink-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.hero h1,
.section-head h2,
.closing h2,
.promises h2,
.maker h2,
.tier-plus-word { filter: url(#ink-press); }

/* Off when the reader has asked for more contrast. Roughening an edge softens
   it, which is the opposite of that request — and this is decoration, so it is
   the first thing that goes. The app does the same on colorSchemeContrast
   .increased. forced-colors too: a high-contrast palette is the OS redrawing
   the page, and a filter would fight it. */
@media (prefers-contrast: more), (forced-colors: active) {
  .token-initial,
  .hero h1,
  .section-head h2,
  .closing h2,
  .promises h2,
  .tier-plus-word,
  .features li,
  .features li::before { filter: none; }
}
