/*
 * The sample pages' design, extracted once.
 *
 * --accent is the only thing that varies per recipe, and it is set as an
 * inline custom property on <body> from recipes.accent_color. Everything
 * downstream - kicker, section rules, step titles, hero gradient - reads it
 * from there, so there is one stylesheet rather than four palettes.
 */

:root {
  --charcoal: #2a2622;
  --cream:    #f8f3e9;
  /* The same cream, see-through, for the pinned header. Kept beside it rather
     than written inline because the chip band's two edge fades have to end on
     exactly this value or they show as opaque patches on a see-through bar. */
  --cream-glass: rgba(248, 243, 233, 0.60);   /* see editorial.css */
  /*
   * A shade lighter than the page, for a surface that sits ON the page: every
   * text field in the editor, the banner a save answers with, the timer tray.
   *
   * It was referenced in eight rules and defined in none - not in this file,
   * not anywhere, not in any commit - so every one of them resolved to an
   * invalid background and rendered transparent. Editor fields were a border
   * with the page showing through, which reads as a box drawn on the page
   * rather than a box you can type in. Found 2026-09-20 while styling the
   * delete dialog, which reached for the same token.
   *
   * #fffdf8 is the value docs/punch-list.html has been using all along; that
   * page copies its palette from this block by hand, and it copied a token
   * this block never had.
   */
  --paper:    #fffdf8;
  --line:     #e2d9c4;
  /* Was #8a8270, which is 3.45:1 on the cream and carries real text - chip
     counts, row notes, every small-caps label. It failed WCAG AA for small
     text and had done since the first stylesheet. 4.70:1 now, and the change
     is barely visible at this size. */
  --muted:    #746c5b;
  --soft:     #5a5447;

  /*
   * THE SCHEME - French enamel, on the red already in the mark.
   *
   * Three colours, and every one of them passes 4.5:1 as small text on the
   * cream, which is why there is one value per hue and no darkened sibling to
   * keep in step. That fell out of going enamel rather than bakelite: the
   * pastel versions of this scheme were mostly fills that could never be text.
   *
   *   Flame      4.80   the dot in the mark. What the app is doing to a recipe:
   *                     the primary button, the step you are on, a number that
   *                     scaling moved.
   *   Caribbean  5.59   the one cool thing on a warm page. Anything you can
   *                     touch - links, hovers, focus rings, timers.
   *   Nutmeg     5.21   the deep end of the beige. Quiet metadata, the way out
   *                     of a filter, a label that names a thing rather than
   *                     asking for a click.
   *
   * No gradients. There were, briefly; enamel catches the light on a casserole
   * and not on a button, and one gradient on a flat typographic page reads as
   * a gradient rather than as enamel.
   */
  --flame:     #b8462f;
  --caribbean: #1b6b73;

  /* Nutmeg is gone, 2026-09-19. It held the quiet metadata - the labels over
   * the times, the section headings, the capsule on a split step - and every
   * one of those reads better as a grey or as a tint of Flame. Three hues on
   * a page with almost nothing interactive on it was one more than the page
   * had jobs for, and the third kept turning up where a grey belonged.
   *
   * The token is not kept "just in case": an unused colour in :root is an
   * invitation to reach for it, and this decision is the reach being refused.
   */

  /* Interactive. --accent was a per-recipe colour scraped at import, so every
     control was a different brown on every page; it is one colour now, and the
     70 places that already reach for it are almost all hovers, links and focus
     rings - which is exactly what Caribbean is for. */
  --accent:   var(--caribbean);

  /* The mark's own two colours. Tokens rather than values inside the SVG,
     because the lockup is inline now and its paint belongs with the palette. */
  --mark-ink: #23211c;
  --mark-dot: var(--flame);

  /* One gutter for the page edge, used by .wrap, .row-head and .row-strip so
     headings and the first card in a row line up with each other. */
  --gutter:   clamp(20px, 4vw, 48px);

  /* How many cards fit across, and the space between them. The grid and the
     theme strips both read these, which is the only way their cards can be the
     same size: a strip cannot compute auto-fill's column count, so the count
     is stated once and both obey it. The two crossovers are where auto-fill
     used to change its mind - measured, not chosen - so the grid lays out
     exactly as it did before. */
  --cols:     2;
  --card-gap: 18px;

  /*
   * The page's outer width, and it is derived rather than chosen.
   *
   * 1180 was the handoff's content column, and it capped the cards at 340 -
   * three tracks in a 1084px content box. But 340 is not the biggest a card
   * ever gets: at 982px of viewport the grid is still two columns and each one
   * is **436**, which is the widest a multi-column card reaches anywhere in the
   * range. Past 1180 the page stopped growing and the cards shrank back to
   * three-quarters of that and stayed there, however big the screen was.
   *
   * Gabe, 2026-09-22: the final size of the cards, past our max-width, should
   * be as large as they are at the two-column snap - their largest.
   *
   * So this is 3 x 436 + 2 x 32 of gap = 1372 of content, plus the 48px gutter
   * either side, because `.wrap` is border-box. Change the track or the gap and
   * this number has to be recomputed; it is not a round number and is not
   * meant to look like one.
   */
  --wide:     1468px;

  /* The reading measure - what a single column of prose is allowed to be.
     A token rather than a number in `.wrap` because it is a *serif* decision:
     Vollkorn at this size wants about 66 characters, and a theme that sets
     the page in something else, or in two columns, is not bound by it. The
     editorial theme overrides exactly this and nothing else. */
  --measure:  760px;

  /*
   * The chip row's height, and where a filter panel starts under it.
   *
   * `58px` was written into five rules by hand and was short in both designs:
   * the row is 66px in kitchen and 74px in editorial, so every panel opened
   * inside the bottom of the row it was meant to hang below and its top corner
   * cut across the filter cells. Reported with a picture - "could use a nudge".
   *
   * One token, measured, and the designs disagree about it because their rows
   * really are different heights. `--panel-top` is derived from it so the five
   * rules say "under the row" instead of each carrying its own arithmetic.
   */
  --chiprow-h: 66px;

  /* `--band-bottom` is where the chip row actually ends, published by nav.js
     on every scroll and resize. The sum is the fallback for the first paint
     and for no script at all: it is right while the page is at the top, and
     the top is where a page starts. */
  --band-bottom: calc(var(--topbar-h, 88px) + var(--chiprow-h));
  --panel-top: calc(var(--band-bottom) + 10px);
}
/*
 * The column count, stated rather than computed.
 *
 * `.row-strip` - the themed rows - is a flex strip that has to produce the
 * same card width as the grid, so switching views rewraps the cards without
 * resizing them, and a flex child cannot ask how many columns `auto-fill`
 * chose. So the count is stated once and both obey it.
 *
 * **These numbers are the kitchen design's**, and they are not the editorial
 * design's. For about an hour they were: the handoff's grid went in here, in
 * the base, which gave *both* designs three big cards and the same gaps -
 * Gabe, 2026-09-22: "not sure which i prefer bc they are the same now." A
 * design's grid belongs to that design. The editorial numbers are in
 * `designs/editorial.css` and are scoped to it.
 *
 * 724px and 972px are where a third and fourth 210px column stopped fitting
 * under the old auto-fill rule; 520px is the existing narrow layout.
 */
@media (min-width: 724px) { :root { --cols: 3; } }
@media (min-width: 972px) { :root { --cols: 4; } }
@media (max-width: 520px) { :root { --card-gap: 14px; } }


* { box-sizing: border-box; }

/* Safari on a phone turned sideways enlarges text by its own guess unless
   told not to. Focus mode's fill measured the step at one size and Safari
   drew it at another, so the step ran off the screen, or the timer grew
   and the words beside it did not. The page sets its own sizes. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

body {
  margin: 0;
  /* The ground is on <body> and <body> is at least the window tall. Without the
     min-height a short page - cook mode is one step and a lot of air - leaves
     the body ending partway down, and anything painting behind it shows below
     that line. Harmless while the ground is cream on cream; the moment there is
     a dark palette it is a grey band across the bottom of the screen. */
  min-height: 100vh;
  background: var(--cream);
  color: var(--charcoal);
  font-family: 'Vollkorn', Georgia, 'Times New Roman', serif;
  line-height: 1.55;
}

.wrap { max-width: var(--measure); margin: 0 auto; padding: 0 var(--gutter) 80px; }

a.back {
  display: inline-block;
  margin: 20px 24px 0;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.85rem;
  color: var(--accent);
  text-decoration: none;
}
a.back:hover { text-decoration: underline; }
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ---------------------------------------------------------------- shot -- */

/* Contained, never full-bleed. Fixed aspect so a portrait and a landscape
   photo occupy the same block and the page does not jump as images load. */
.shot {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  display: block;
  border-radius: 3px;
  margin-top: 24px;
  background: var(--line);
}

/* --------------------------------------------------------------- title -- */

header.title-block {
  padding: 36px 0 20px;
  border-bottom: 2px solid var(--charcoal);
}

.kicker {
  text-transform: uppercase;
  letter-spacing: 0.18em;
  font-size: 0.72rem;
  color: var(--accent);
  font-family: Helvetica, Arial, sans-serif;
  font-weight: 600;
  margin-bottom: 10px;
}

h1 {
  font-size: 2.05rem;
  margin: 0 0 12px;
  line-height: 1.15;
  font-weight: 500;
  text-wrap: balance;
}

.desc {
  font-style: italic;
  color: var(--soft);
  font-size: 1.02rem;
  margin: 0;
}

.meta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  margin: 22px 0 0;
  font-family: Helvetica, Arial, sans-serif;
}
.meta-item  { min-width: 90px; }
.meta-label {
  text-transform: uppercase;
  font-size: 0.65rem;
  letter-spacing: 0.1em;
  color: var(--muted);
  margin-bottom: 3px;
}
.meta-value { font-size: 1rem; font-weight: 600; color: var(--charcoal); }
/* Estimated times, said once under the row. A full-width line of the row's
   own flex, so it wraps under the numbers at every width rather than
   squeezing in beside them - and last in the row (`order`), after the
   rating, which it pushed off the times' line and into the Focus Mode bar
   while it sat in between. */
.meta-est {
  order: 1;
  flex-basis: 100%;
  margin: -18px 0 0;
  color: var(--muted);
  font-size: 0.76rem;
}

/* ---------------------------------------------------------------- body -- */

.body-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 40px;
  margin-top: 36px;
}
@media (min-width: 640px) {
  .body-grid { grid-template-columns: 250px 1fr; }
}

h2 {
  font-family: Helvetica, Arial, sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.85rem;
  color: var(--accent);
  border-bottom: 1px solid var(--line);
  padding-bottom: 8px;
  margin: 24px 0 16px;
}
h2:first-child { margin-top: 0; }

/* A component heading - "Jasmine rice", "Curry". Present only when the
   recipe actually has components; a flat recipe renders no group headings
   at all rather than an empty one. */
.component { margin-bottom: 28px; }
.component:last-child { margin-bottom: 0; }
.component-head {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  font-weight: 600;
  margin: 0 0 12px;
}

ul.ingredients { list-style: none; margin: 0; padding: 0; }
ul.ingredients li {
  padding: 9px 0;
  border-bottom: 1px dotted var(--line);
  font-size: 0.97rem;
}
ul.ingredients li.optional { color: var(--soft); }
/* **Containers** (cook_containers()): the number left of the box, as on a
   dinner plan's mise, and the lines one container holds read as one block -
   no rule between them. The switch is over the mise side of the heading bar
   (.cols-head-containers), and in the column (with Check all) only when the
   columns stack. */
.mise-tools { position: absolute; top: 4px; right: 6px; display: flex; align-items: center; gap: 18px; }
.mise-tools > .gather-clear { position: static; }
.mise-tools > .grain { margin: 0; }
/* The rule that closes a container is solid and a shade darker than the
   dotted one it replaces (Gabe, 2026-10-03: "just slightly more obvious"):
   the ordinary rule is near the page's own ground, and containers are the
   one place the rule is doing the work of a divider. 10% toward the ink so
   it follows whichever design's --line. */
ul.ingredients.is-containers li { border-bottom: 1px solid color-mix(in srgb, var(--line), var(--charcoal) 10%); }
ul.ingredients.is-containers li:has(+ li.is-with) { border-bottom-color: transparent; padding-bottom: 1px; }
ul.ingredients.is-containers li.is-with { padding-top: 1px; }
/* The container a step tips in, under the step, by the number the ingredient
   column gives it - so the line on the counter and the line in the method
   are the same number (punch 177). Only while the mise is in containers. */
.step-box { margin: 8px 0 0; font: 700 0.6875rem/1.4 Helvetica, Arial, sans-serif; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
.step-box b { font-size: 0.8125rem; letter-spacing: 0; color: var(--charcoal); font-variant-numeric: tabular-nums; }
.mise-n { flex: none; min-width: 1.3em; padding-top: 3.5px; font: 700 0.78rem/1.5 Helvetica, Arial, sans-serif; color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }

/* Taped (punch 177): the number is a solid block of the tape's color, and a
   container's later lines carry the same block without the number, so each
   container reads as one strip of tape down the list. White and yellow get
   a hairline so they show on the page. */
ul.is-taped .mise-n { align-self: stretch; min-width: 1.75em; margin: -3px 0; padding: 4px 0 0; text-align: center; font-size: 0.875rem; background: var(--tape); color: var(--tape-ink); box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18); }
ul.is-taped li:has(+ li.is-with) .mise-n { margin-bottom: -5px; }
ul.is-taped li.is-with .mise-n { margin-top: -5px; }
/* The container's size from the v3 read: S, M or L under its number. */
.mise-size { display: block; font-size: 0.62rem; font-weight: 700; line-height: 1.1; opacity: 0.75; }
/* The word to bold in a line (punch 207). */
.ingredients .core { font-weight: 700; }
.step-box[style] b { display: inline-block; min-width: 1.6em; margin: 0 1px; padding: 1px 4px; text-align: center; background: var(--tape); color: var(--tape-ink); box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18); }
/* Converted (punch 224): "bin 4" in a step wears its tape, the number the
   bin column shows. A mix row and a "fill during" row are notes on the bin,
   not things to measure. */
.bin-chip { display: inline-block; min-width: 1.5em; padding: 0 4px; font: 700 0.875em/1.35 Helvetica, Arial, sans-serif; text-align: center; background: var(--tape); color: var(--tape-ink); box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18); font-variant-numeric: tabular-nums; }
/* In Focus mode's Have ready, where the row's text is muted: the chip keeps its ink. */
.cookview .mise .bin-chip { color: var(--tape-ink); }
[data-design="editorial"] .ingredients.is-converted .ing { display: block; min-width: 0; }
ul.is-converted li.is-mix .ing, ul.is-converted li.is-when .ing { font-style: italic; color: var(--muted); }
.step-needs .bin-fill { list-style: none; }
/* What to serve alongside, the conversion's suggestion: under the last step. */
.step-side { color: var(--muted); font-style: italic; margin: 18px 0 0; }
.tape { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 12px; margin: 0 0 10px; font: 700 0.6875rem/1.4 Helvetica, Arial, sans-serif; letter-spacing: 0.06em; text-transform: uppercase; }
.tape-fold { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; }
.tape-now { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; }
.tape-now::-webkit-details-marker { display: none; }
.tape-fold[open] > .tape-now .tape-chip { outline: 2px solid var(--charcoal); outline-offset: 2px; }
.tape-chip { width: 14px; height: 14px; background: var(--tape); box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.25); }
.tape-pick { display: inline-flex; gap: 4px; }
.tape-pick a { width: 18px; height: 18px; background: var(--tape); box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.25); }
.tape-pick a[aria-current] { outline: 2px solid var(--charcoal); outline-offset: 2px; }

ol.steps { list-style: none; counter-reset: step; margin: 0; padding: 0; }
/* `>` and not a descendant selector. A step now contains its own Have Ready
   list, and `ol.steps li` matched those too - so every ingredient line bumped
   the step counter and Turkish Coffee Ice Cream numbered its five steps
   1, 5, 9, 12, 14. The gap is the number of lines in the step above, which is
   why it looked like a constant offset when the first two steps happened to
   have three lines each. */
ol.steps > li {
  counter-increment: step;
  position: relative;
  padding: 0 0 26px 46px;
  margin-bottom: 4px;
}
ol.steps > li > .step-no {
  position: absolute;
  left: 0;
  top: 0;
  width: 30px;
  height: 30px;
  /* Flame, not charcoal. A step number is the app saying "here" - the same
     job the filled circle does in the mark - and thirty grey discs down a
     page was the single biggest reason this read as drab. */
  background: var(--flame);
  color: var(--cream);
  border-radius: 50%;
  text-decoration: none;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.85rem;
  font-weight: 600;
}
.step-title {
  font-weight: 600;
  display: block;
  margin-bottom: 4px;
  color: var(--accent);
}

footer.note,
.note-open {
  /* Clear of the columns above it.
   *
   * The mise column is a sticky scroller, so it ends wherever the viewport
   * does rather than where its content does - which put the note's first line
   * hard against a half-cut ingredient with nothing between them. The rule
   * above the note is the separation, and it needs air on both sides of it to
   * read as one. */
  margin-top: 40px;
  padding-top: 22px;
  border-top: 1px solid var(--line);
  font-size: 0.9rem;
  color: #6a6354;
  font-style: italic;
}
/* "Adapted from", at the foot (templates/recipe.php). Its own rule above
   it when there is no note; straight under the note when there is. */
.recipe-adapted {
  margin-top: 40px;
  padding-top: 22px;
  border-top: 1px solid var(--line);
  font-size: 0.85rem;
  color: var(--muted);
}
footer.note p { margin: 0; }
footer.note p + p { margin-top: 0.75em; }
footer.note + .recipe-adapted {
  margin-top: 12px;
  padding-top: 0;
  border-top: 0;
}
.recipe-adapted a { color: inherit; text-decoration-color: var(--line); }
.source {
  margin-top: 10px;
  font-size: 0.82rem;
  color: #9a9284;
  font-family: Helvetica, Arial, sans-serif;
}

/* --------------------------------------------------------------- index -- */

.index-head { padding: 40px 0 18px; border-bottom: 2px solid var(--charcoal); }
.index-head h1 { font-size: 2.4rem; }
.index-head .desc { margin-top: 8px; }

ul.recipe-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0;
}
ul.recipe-list li { border-bottom: 1px solid var(--line); }

/* The whole row is the link, not just the title: the <a> is the grid, so it
   covers thumbnail and text alike. The negative margin lets its hover state
   bleed to the edge of the list rather than stopping at the text. */
a.recipe-card {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: 18px;
  padding: 14px 12px;
  margin: 0 -12px;
  text-decoration: none;
  color: inherit;
  align-items: center;
  border-radius: 3px;
  transition: background-color 120ms ease;
}
a.recipe-card:hover { background: rgba(42, 38, 34, 0.045); }
a.recipe-card:hover h2 { text-decoration: underline; }
a.recipe-card:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

@media (prefers-reduced-motion: reduce) {
  a.recipe-card { transition: none; }
}

@media (max-width: 520px) {
  a.recipe-card { grid-template-columns: 96px 1fr; gap: 14px; }
  .recipe-card .thumb { width: 96px; height: 72px; }
}

.recipe-card .thumb {
  /* Both dimensions must be set in CSS. An <img>'s width/height attributes
     are presentational hints mapped onto the CSS width/height properties, so
     setting only width here leaves the attribute's height in force - which
     resolves both dimensions and makes aspect-ratio a no-op. That is what
     turned these into full-height strips. */
  width: 120px;
  height: 90px;
  object-fit: cover;
  display: block;
  border-radius: 2px;
  /* Neutral, not the accent. This is what fills a frame for the moment before
     a photograph arrives, on every card in every scrolling row - and once the
     accent became a real scheme colour the grid flashed teal on every scroll.
     A placeholder should not wear a brand colour. Still dark, because the star
     ratings sit on it in white. */
  background: linear-gradient(135deg, var(--soft) 0%, var(--charcoal) 100%);
}
.recipe-card h2 {
  font-family: 'Vollkorn', Georgia, 'Times New Roman', serif;
  text-transform: none;
  letter-spacing: 0;
  font-size: 1.25rem;
  font-weight: 500;
  color: var(--charcoal);
  border: none;
  padding: 0;
  margin: 0 0 4px;
}
.recipe-card .card-kicker {
  font-family: Helvetica, Arial, sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 0.62rem;
  font-weight: 600;
  color: var(--accent);
  margin-bottom: 6px;
}
.recipe-card .card-desc {
  font-style: italic;
  color: var(--soft);
  font-size: 0.92rem;
  margin: 0;
  /* Two lines maximum, so a long description cannot stretch a row and the
     list stays scannable several recipes at a time. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}


.alias {
  font-style: italic;
  color: var(--soft);
}

.usage-count {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  font-weight: 600;
  margin: 28px 0 0;
}

/* A recipe title, not a section head - so it opts out of the global h2, which
   is the uppercase sans rule used for "Ingredients" and "Instructions". */
.usage { margin-top: 24px; }
.usage h2 {
  font-family: inherit;
  text-transform: none;
  letter-spacing: normal;
  font-size: 1.3rem;
  color: var(--charcoal);
  border-bottom: none;
  padding-bottom: 0;
  margin: 0 0 4px;
}
.usage h2 a { color: inherit; text-decoration: none; }
.usage h2 a:hover { color: var(--accent); }

/* How the line matched, for when a resolution looks wrong and you want to
   know whether a canonical name or an alias did it. */
.via {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin-left: 6px;
}

.empty { color: var(--soft); font-style: italic; }

/* The citation links out when the recipe has a source_url. Kept as quiet as
   the plain text it replaces - it is a credit, not a call to action. */
.source a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--line);
  text-underline-offset: 3px;
}
.source a:hover { color: var(--accent); text-decoration-color: var(--accent); }

/* ---------------------------------------------------------- collection -- */

/* The card grid and the themed rows share one card. A wider wrap than the
   reading pages: a recipe wants a measure, a collection wants the screen. */
.wrap.wide { max-width: var(--wide); }

/* The header block on the index is followed by the rows, not by the end of the
   page, so it must not carry .wrap's 80px bottom padding - that gap was what
   pushed "Recently added" a screen away from the search box. */
.wrap.flush { padding-bottom: 0; }

/* The stroke belongs to the whole control, not to the input.
 *
 * It used to live on the input, with the button flush against its open right
 * edge - so the two only *looked* like one outlined box, and focus put a
 * separate ring around the input alone, which broke the line across the seam.
 *
 * Owning the border here makes it continuous by construction: there is one
 * rectangle, so there is nothing for two edges to disagree about, and focus
 * lights the whole control because that is what has focus. */
/* The masthead search, from the handoff of 2026-09-24, option 7F.

   **Three parts on one 44px row with no gaps between them**: the count block,
   the field, the button. Square corners, because the masthead's own rule is
   square and a 3px radius on a 44px control beside it reads as a mistake
   rather than as a decision.

   **Closed is the resting state and it is only a width.** The field goes to
   zero and takes its padding with it; nothing is hidden, nothing is removed,
   so the transition is one property animating and the tab order never changes
   shape. `.is-closed` is put on before the first paint by layout.php, and only
   when there is a script to open it again - with no script this whole block
   renders as a plain open field, which is what it has always been.

   The count block goes with the field rather than staying put: it is a
   caption for the thing that is not there, and 1,365 sitting on its own beside
   a magnifier says nothing. */
.searchbox {
  position: relative;
  display: flex;
  align-items: stretch;
  gap: 0;
  margin-top: 0;
  max-width: none;
}

.searchbox-count {
  display: flex;
  flex: none;
  align-items: center;
  height: 44px;
  padding: 0 14px;
  background: var(--charcoal);
  color: #fff;
  font-family: var(--sans, inherit);
  font-size: 13px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  /* It closes with the field rather than vanishing the instant the class
     lands. `max-width` because there is no width here to animate from - the
     block is as wide as its number - and 12ch is past any count this
     collection will hold. */
  overflow: hidden;
  /* Wide enough for the longest thing it will ever hold - "1,365 recipes" is
     13 characters and 12ch cut the s off. It is a ceiling for the transition
     to travel to, not a width, so there is no cost to it being generous. */
  max-width: 20ch;
  transition: max-width 160ms ease-out, padding 160ms ease-out;
}

/* 240 rather than the handoff's 320. Gabe, 2026-09-24: "lets reduce the width
   so it's not so quick to stack as we get narrower." The field is the only
   part of the masthead that can give ground, and 80px of it buys about 80px
   of window before the whole control has to drop to a row of its own. */
/* **It shrinks before the masthead breaks.** `0 1` rather than `0 0`: between
   the width where 240px stops fitting and the width where the whole control
   drops to its own row, the field gives up pixels instead of hanging over the
   right edge - which is what it did, with the button half off the screen. */
.searchbox-input {
  flex: 0 1 240px;
  width: 240px;
  min-width: 64px;
  height: 44px;
  /* Room on the right for the clear control, which is drawn over the field
     rather than beside it - a fourth box in the row would push the button
     along. */
  padding: 0 34px 0 14px;
  border: none;
  border-radius: 0;
  background: var(--paper);
  color: var(--charcoal);
  font-family: var(--sans, inherit);
  font-size: 15px;
  font-weight: 400;
  /* Opacity is quicker than the width on purpose: the words go first and the
     box follows, so the field is never caught halfway with "Search re"
     legible under the button. */
  transition: flex-basis 160ms ease-out, width 160ms ease-out,
              padding 160ms ease-out, opacity 90ms ease-out;
}
.searchbox-input::placeholder { color: var(--muted); }

/* WebKit draws its own cancel button in a search field. Ours is styled, sits
   where the handoff's parts sit, and is the one the script knows about; two of
   them in the same corner is the bug this removes. */
.searchbox-input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
  display: none;
}

/* **Over the field's right end, not another cell in the row.** The three parts
   are drawn edge to edge and a fourth would push the button along; absolute
   keeps the row the row. `right: 44px` is the button's width, so it lands
   inside the field's own corner.

   Gone whenever the field is empty - `:placeholder-shown` is that test in CSS,
   and it needs no script to stay true while typing. */
.searchbox-clear {
  position: absolute;
  top: 0;
  right: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  cursor: pointer;
}
.searchbox-clear:hover { color: var(--charcoal); }
.searchbox-clear:focus-visible { outline: 2px solid var(--charcoal); outline-offset: -3px; }
.searchbox-clear svg {
  width: 12px;
  height: 12px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}
.searchbox-input:placeholder-shown ~ .searchbox-clear,
.searchbox.is-closed .searchbox-clear { display: none; }

/* A 0-width field is not a focus ring's problem, but a field that is open and
   focused still has to say so, and an outline outside a 44px control in a
   44px row is clipped by whatever is beside it. Drawn inside. */
.searchbox-input:focus,
.searchbox-input:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--charcoal);
}

.searchbox-go {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: none;
  border-radius: 0;
  background: var(--accent);
  color: #fff;
  cursor: pointer;
}
.searchbox-go:hover { background: var(--charcoal); }

/* The + beside the search: the same square, outlined rather than filled, so
   the search stays the loud one. */
/* Import recipe wears the tray chip's style (layout.php). */

/* The name a new recipe starts with, asked at the top of the index - and,
   on the line under it, where it came from, with the buttons at its end. A
   grid, so the two fields start at the same x whatever the labels measure. */
.add-recipe {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 10px;
  margin: 18px 0 8px;
  padding: 16px 0;
  border-bottom: 1px solid var(--line);
}
.add-recipe-title { grid-column: 2 / -1; }
.add-recipe .add-recipe-source { grid-column: 2; font-size: 1rem; }
.add-recipe-label {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--flame);
}
.add-recipe-title {
  min-width: 0;
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-radius: 0;
  background: #fff;
  font: inherit;
  font-size: 1.1rem;
}
.add-recipe .sb-save,
.add-recipe .sb-cancel {
  display: inline-flex;
  align-items: center;
  padding: 9px 16px;
  border: 1px solid;
  border-radius: 4px;
  font: inherit;
  font-weight: 600;
  font-size: 0.92rem;
  text-decoration: none;
  cursor: pointer;
}
/* A pasted recipe: the same field grown into a box (add-recipe.js), small
   type so a page of JSON reads as a page, and capped so the buttons under it
   stay on screen. */
.add-recipe-title.is-paste {
  font-family: ui-monospace, Menlo, monospace;
  font-size: 0.82rem;
  line-height: 1.45;
  resize: vertical;
  max-height: 55vh;
}
.add-recipe-problem { grid-column: 1 / -1; margin: 0; color: var(--flame); }
.searchbox-go:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--cream); }

.searchbox-go svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

.searchbox.is-closed .searchbox-input {
  flex-basis: 0;
  width: 0;
  min-width: 0;
  padding-left: 0;
  padding-right: 0;
  opacity: 0;
}
.searchbox.is-closed .searchbox-count {
  max-width: 0;
  padding-left: 0;
  padding-right: 0;
}

/* The motion is a suggestion in the handoff, not the feature. Anyone who has
   asked for less of it gets the same two states and no travel between them. */
@media (prefers-reduced-motion: reduce) {
  .searchbox-input,
  .searchbox-count { transition: none; }
}

/* **A narrow window gets the field on its own row rather than 240px it has
   not got.** The handoff says under 640px; it stacks at 560 instead, which is
   the width the masthead already wrapped at long before this control existed
   and the point where the lockup, the field and the button stop fitting side
   by side. That they now do fit down to 560 is what the field losing 80px
   bought. Gabe, 2026-09-24: "lets reduce the width so it's not so quick to
   stack as we get narrower."

   Closed, it is a 44px button on the logo's row at every width, which is the
   whole reason the control collapses. */
@media (max-width: 560px) {
  .topbar:has(.searchbox:not(.is-closed)) .topbar-find { flex: 1 0 100%; }
  .topbar .topbar-find > .searchbox:not(.is-closed) { flex: 1 1 auto; width: 100%; }
  .searchbox:not(.is-closed) .searchbox-input { flex: 1 1 auto; width: auto; }

  /* Scrolled, the wordmark goes and the bar comes back to one line - so the
     slot stops claiming a row of its own. See html.is-stuck below. */
  html.is-stuck .topbar .topbar-find { flex: 1 1 auto; }
  html.is-stuck .topbar .topbar-find > .searchbox:not(.is-closed) { width: auto; }
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  /* Tighter than before: the cards now have their own edges, so the gutter no
     longer has to do the separating on its own. The editorial design sets its
     own row gap - see designs/editorial.css. */
  gap: 22px var(--card-gap);
  /* 4px, matching .row-strip's top padding, so the first card's top edge is at
     the same height in both views - the two headers above them already end on
     the same line. Switching views should rewrap the cards, not move them. */
  margin-top: 4px;
}

/* ------------------------------------------------------------- one card -- */

/* A polaroid: white stock, the photograph near the top, and a deep bottom edge
 * the text sits in.
 *
 * The white is doing the work. Against the cream page a white card reads as an
 * object laid on the paper rather than as a region of it, which is what gives
 * a grid of them any structure at all - the old cards shared the page's
 * background and the only thing separating one from the next was the gutter.
 *
 * The margin is even on three sides and deeper at the bottom. That asymmetry is
 * the whole reason a polaroid reads as a polaroid, and it is also just useful:
 * the deep edge is where the title, cuisine and source go, so the shape and the
 * content want the same thing. */
.card {
  display: flex;
  flex-direction: column;
  /* A grid track of `1fr` will not shrink below its item's min-content width,
     and a flex row refuses to go below the sum of its unshrinkable parts. The
     source line is such a row, so without this the cards pushed the grid 40px
     wider than a 375px window. `min-width: 0` is what lets a grid item be
     narrower than the widest thing inside it. */
  min-width: 0;
  background: #fff;
  /* No bottom padding: the source line carries its own, so that it can sit on
     the card's bottom edge rather than floating above a strip of white. */
  padding: 9px 9px 0;
  border-radius: 2px;
  /* A warm hairline as well as the shadow - accent-tinted rather than
     charcoal, so it belongs to the paper instead of ruling a line on it. On a
     low-contrast display the shadow alone can vanish into the cream and the
     card loses its edge entirely; on its own the border would read as a table.
     It is underneath the shadow, not instead of it. */
  border: 1px solid rgba(122, 106, 85, 0.14);

  /* Chosen against three weights on real photographs: 30% of the way from
     "soft halo" to "sitting on the table". Written out rather than left as a
     round number so it can be re-derived - the endpoints were
     0 1px 2px/0.08 + 0 6px 16px/0.07 and 0 2px 5px/0.10 + 0 12px 28px/0.11.
     Two layers because one cannot be both a contact shadow and a cast: the
     tight layer holds the card's edge, the wide one lifts it off the page. */
  box-shadow: 0 1px 3px rgba(42, 38, 34, 0.086), 0 8px 20px rgba(42, 38, 34, 0.082);
}
.card-link { text-decoration: none; color: inherit; display: block; }

.card-shot {
  position: relative;
  aspect-ratio: 4 / 3;
  /* Square inside the frame: the white border is the card's shape, and a
     rounded photo inside a square frame reads as two shapes fighting. */
  border-radius: 0;
  overflow: hidden;
  /* Neutral, not the accent. This is what fills a frame for the moment before
     a photograph arrives, on every card in every scrolling row - and once the
     accent became a real scheme colour the grid flashed teal on every scroll.
     A placeholder should not wear a brand colour. Still dark, because the star
     ratings sit on it in white. */
  background: linear-gradient(135deg, var(--soft) 0%, var(--charcoal) 100%);
}
/* A light passing across the frame while the photograph is on its way - Gabe,
   2026-09-28, on the new server being slower to fill a page: "need to add some
   loading anims". On the frame itself, under the image, so the photograph
   covers it the moment it paints and nothing has to be told it has loaded -
   no script. Not on a drawn card, which has nothing coming. Still for anyone
   who asked for less motion. */
.card-shot:not(.card-shot-drawn) {
  background:
    linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, 0.14) 50%, transparent 70%) 0 0 / 250% 100% no-repeat,
    linear-gradient(135deg, var(--soft) 0%, var(--charcoal) 100%);
  animation: card-shot-wait 1.6s ease-in-out infinite;
}
@keyframes card-shot-wait {
  from { background-position: 150% 0, 0 0; }
  to   { background-position: -150% 0, 0 0; }
}
/* Stopped once the photograph is in (nav.js adds .is-in): a light still
   crossing forty frames behind forty finished photographs is repaint work
   for nothing, and a phone's battery pays for it. */
.card-shot.is-in { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .card-shot:not(.card-shot-drawn) { animation: none; }
}
.card-shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.card-link:focus-visible .card-shot { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Temporary: a converted recipe's photo, card and hero, while they are
   checked by eye (lib/onepass4.php, one_pass4_marks). Comes out with it. */
.v4-dot {
  position: absolute;
  right: 8px;
  bottom: 8px;
  z-index: 2;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #d62828;
  box-shadow: 0 0 0 2px #fff;
  pointer-events: none;
}

/* On the photo rather than under the title: the rating is the one thing on a
   card that is mine rather than the publisher's. */
/* Positioned back over the photograph.

   It used to be a child of .card-shot and is a child of .card-body now, so
   that the editorial theme can put it on the rule line - a stylesheet cannot
   change an element's parent, so the markup has to be the one both themes
   can reach. Nothing about this rule's result changed: 8px and 6px were
   measured from the shot's top-left, and the shot begins at the card's 9px
   padding, so the offsets are that much larger against .card.

   .card is the containing block, which it already was - it carries no
   transform and no filter - and it needs position: relative for that to be
   true of an absolute child. */
.card { position: relative; }
.card-rating {
  position: absolute;
  left: 17px;
  top: 15px;
  z-index: 1;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: #fff;
  /* A dark outline as well as the scrim. A drop shadow alone disappears
     against a pale photograph - which is most food photography - and the
     scrim cannot be tall enough to cover every case without darkening the
     picture. Between them the stars hold on anything. */
  text-shadow:
     0 1px 3px rgba(0, 0, 0, 0.7),
     0 0 1px rgba(0, 0, 0, 0.9),
     1px 0 1px rgba(0, 0, 0, 0.55),
    -1px 0 1px rgba(0, 0, 0, 0.55);
}
.card-rating .dim { opacity: 0.45; }

/* The scrim, only where there is a rating to hold up and only over a real
   photograph - a generated card is already a flat dark colour. */
.card-shot-rated:not(.card-shot-drawn)::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  /* Follows the stars to the top. A scrim at the bottom of the photo with the
     rating at the top holds up nothing and darkens the wrong half. */
  top: 0;
  height: 42%;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0));
  pointer-events: none;
}

/* The deep edge. Bottom padding is roughly double the sides, which is what
   makes the frame read as deliberate rather than as an off-centre photo. */
.card-body { padding: 10px 1px 15px; }
.card-title {
  font-family: 'Vollkorn', Georgia, 'Times New Roman', serif;
  text-transform: none;
  letter-spacing: 0;
  font-size: 1.02rem;
  font-weight: 500;
  line-height: 1.28;
  color: var(--charcoal);
  border: none;
  padding: 0;
  margin: 0;
}
.card-link:hover .card-title { text-decoration: underline; text-underline-offset: 2px; }

.card-meta {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
  margin-top: 5px;
}

/* Why this recipe matched, when the title does not already say. */
.card-match {
  font-size: 0.82rem;
  color: var(--soft);
  margin: 7px 0 0;
  padding-left: 8px;
  border-left: 2px solid var(--line);
  line-height: 1.4;
}
.card-match-label {
  display: block;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
  margin-bottom: 2px;
}

/*
 * Provenance, on the card's bottom edge.
 *
 * `margin-top: auto` inside the card's column flexbox is what does it. Titles
 * here run to one, two and three lines, so without it the source floated up to
 * wherever the text above happened to end and a row of cards had its
 * provenance at four different heights - which reads as ragged rather than as
 * information. The grid stretches every card in a row to the tallest, so one
 * line across the row lands at one height.
 *
 * A card with no source has no line at all and simply ends in space. That is
 * the right outcome: the cards that do have one still agree with each other.
 */
.card-source {
  display: flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.66rem;
  color: var(--muted);
  margin-top: auto;
  padding: 8px 2px 11px;
}
.card-source a { color: inherit; text-decoration: none; }
.card-source a:hover { color: var(--accent); text-decoration: underline; }

/* The publisher's name gives way before the arrow does: the name is often long
   and can be read from the card's other information, where a truncated arrow
   is nothing at all. */
.card-source-name {
  /* Shrinks first and truncates rather than wrapping: it is the one part of
     the line whose beginning still identifies it. */
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Off to the original. Sized in em so it tracks the line rather than needing a
   second set of numbers, and given a target bigger than the mark it draws -
   10px of arrow is not something anyone can hit. */
.card-source-out {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6em;
  height: 1.6em;
  margin: -0.4em 0;
}
.card-source-out svg {
  width: 0.95em;
  height: 0.95em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.3;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.card-source-out:hover { color: var(--accent); }

/* Written here rather than found somewhere. In the accent, because it is the
   one line on a card that is about this collection rather than about a
   publisher - and because there is no arrow beside it to carry the colour. */
.card-source-own { color: var(--accent); }

/* The publisher's rating, as a number. Never stars - see source_rating_text().
   Last, and pushed to the far end, so the ratings line up down a column the
   way the names line up across a row. */
.card-source-rating { white-space: nowrap; margin-left: auto; }

/* Nothing pinned and nothing in play. It keeps the row's height - which the
   filter panel and the sticky bar are both measured from - and it is the only
   place that says what the pins in the panel do. Dashed, so it reads as an
   outline of a chip rather than a chip that will not respond. */
/* `.chip-loose` was here: the rule under a chip that was on the row because
   you had used it rather than put it there. There is no such chip now. */

.chip-ghost {
  border-style: dashed;
  color: var(--soft);
  cursor: pointer;
  font: inherit;
  text-align: inherit;
}
.chip-ghost:hover { color: var(--charcoal); border-color: var(--charcoal); }

/* The chip being dragged, and the row while it is happening.

   `cursor: grabbing` on the row rather than the chip, because the pointer is
   captured and may be nowhere near it. `user-select: none` for the same
   reason a drag anywhere selects text otherwise.

   **A swipe scrolls the row; a held finger drags** (nav.js, HOLD). This was
   `touch-action: pan-y`, which made every sideways swipe on a pinned chip a
   reorder, so on a phone the row did not scroll - Gabe, 2026-10-03. No
   callout either: on iOS a held link opens its preview, which is the hold
   that lifts a chip here. */
.chip-slot[data-pin] {
  touch-action: manipulation;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  /* Belt as well as the dragstart handler: Safari's own link drag is driven by
     this rather than by the event, and the two disagree often enough to set
     both. */
  -webkit-user-drag: none;
}
.chip-slot .chip { -webkit-user-drag: none; }
.chipbar.is-ordering { cursor: grabbing; user-select: none; }
.chip-slot.is-dragging { z-index: 2; }
.chip-slot.is-dragging > .chip { box-shadow: 0 6px 18px rgba(42, 38, 34, 0.22); }

/* A chip and its x. The slot is the thing in the row - it takes the chip's
   place in the flex line and the drag moves it - and the x sits inside the
   chip's own right edge, in padding the chip keeps for it, so the chip is the
   same width whether the x is showing or not. */
.chip-slot {
  position: relative;
  display: inline-flex;
  flex: none;
}
.chip.has-x { padding-right: 34px; }
.chip-x,
.chip-pin {
  position: absolute;
  top: 50%;
  right: 7px;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transform: translateY(-50%);
  transition: opacity 90ms, color 90ms, background 90ms;
}
.chip-x svg {
  width: 10px;
  height: 10px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}
/* The chip's pin, where the x was: hollow on a temporary chip, solid on a
   kept one, always drawn - it is the only thing that says which a chip is. */
.chip-pin svg {
  width: 13px;
  height: 13px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.chip-pin.is-pinned svg { fill: currentColor; }
.chip-pin:hover { color: var(--flame); background: rgba(42, 38, 34, 0.08); }
.chip-pin:focus-visible { outline: 2px solid var(--caribbean); outline-offset: 0; }
.chip-on + .chip-pin { color: rgba(248, 243, 233, 0.8); }
.chip-on + .chip-pin:hover { color: var(--cream); background: rgba(255, 255, 255, 0.16); }
.chip-x:hover { color: var(--flame); background: rgba(42, 38, 34, 0.08); }
.chip-x:focus-visible { outline: 2px solid var(--caribbean); outline-offset: 0; }
.chip-on + .chip-x { color: rgba(248, 243, 233, 0.75); }
.chip-on + .chip-x:hover { color: var(--cream); background: rgba(255, 255, 255, 0.16); }

/* With a mouse, only on the chip being pointed at - eleven x's in a row is
   a row of x's. A touch screen has no hover to reveal it with, so there it
   is always drawn. The space is kept either way. */
@media (hover: hover) and (pointer: fine) {
  .chip-x { opacity: 0; }
  .chip-slot:hover .chip-x,
  .chip-x:focus-visible { opacity: 1; }
}
/* And a touch screen ANYWHERE brings them back. An iPad with a trackpad or
   Magic Keyboard paired reports a fine, hovering pointer, so the rule above
   hid every x from the finger that cannot hover - Gabe, 2026-09-25: "cant
   delete chips in situ on ios?". The device that can be touched keeps them
   drawn, whatever else is plugged into it. */
@media (any-pointer: coarse) {
  .chip-x { opacity: 1; }
}

/* ---------------------------------------------------------- theme rows -- */

.rows { margin-top: 20px; }

/* The strip runs to the window edge, so the last visible card is usually cut
   in half. The fade says that is deliberate and there is more to the right;
   without it a sliced card just looks broken. */
.row { margin: 0 0 38px; position: relative; }
.row::after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 12px;
  width: clamp(28px, 6vw, 64px);
  background: linear-gradient(to right, rgba(248, 243, 233, 0), var(--cream));
  pointer-events: none;
}
.row-head {
  max-width: var(--wide);
  margin: 0 auto;
  /* 4px, not 10. The heading names the row directly underneath it and the
     strip adds 4px of its own, so ten put fifteen pixels of nothing between a
     label and the thing it labels - enough that the eye read them as two
     blocks rather than one. */
  padding: 0 var(--gutter) 4px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}
/* The grid's copy of the row header: same width, same gutter, same vertical
   offset, and the same shape - so the toggle sits in one place whichever view
   you are in.

   **It says what you are looking at now.** It was empty of text on purpose,
   on the argument that a heading here would be inventing a label for "all of
   them" - which was true only while the grid was always all of them. Filter
   it and the grid said nothing at all: Gabe, 2026-09-23, "we don't show what
   we're looking at in grid view now... I think we just borrow the row header
   that makes this work in the theme view and slap it at the top of the grid."

   Title, then the count, then the toggle pushed to the far end. `auto` on the
   count rather than `space-between` on the row, which put the count in the
   middle of the bar with nothing either side of it. */
.row-head-bare { justify-content: flex-start; align-items: baseline; }
/* The toggle takes the far end now, not the count: "Clear all" follows the
   count it clears, rather than sitting out beside Grid/Themes. Gabe,
   2026-09-25: "move 'clear all' to after recipe count". */
/* A long filter description wraps inside its own column and everything to
   its right keeps its shape: the count is one piece ("17 recipes." was
   stacking as "17" over "recipes." when the heading squeezed it), and the
   heading splits its lines evenly rather than orphaning the last word.
   Gabe, 2026-09-25: "we need to lay long filter descriptions out better". */
/* Sized up to stand beside the big heading - at 0.8rem against 2.1rem the
   count and the way out read as fine print. Gabe, 2026-09-25: "bigger". */
.row-head-bare .resultcount { margin-right: 0; flex: none; white-space: nowrap; font-size: 1rem; }
.row-head-bare .row-clear { font-size: 0.85rem; }
.row-head-bare .row-title h2 { text-wrap: balance; }
.row-head-bare .row-clear { margin-left: 16px; }
.row-head-bare .viewtoggle { margin-left: auto; }

/* The same gap under the bar in both views. The grid sat 4px under it and the
   themes' first row 20px, so switching views moved everything below the bar
   by 16px. Gabe, 2026-09-25: "hold the space in the header when switching
   views so no jump". */
.rows-bar + [data-swap="results"] > .wrap > .card-grid:first-child { margin-top: 20px; }
/* And the last 4px of that jump: a headerless leading row (the catch-all,
   which defers to the bar) keeps the strip's little gap-under-a-heading for
   a heading it does not have. With it gone, Grid and Themes start their
   first card on the same line. */
.rows .row:first-child:not(:has(.row-head)) > .row-strip { padding-top: 0; }

/* Red, because it undoes rather than goes somewhere, and every other "this is
   on" on the page is red now. Same words and same colour as the panel's, so
   the two read as one control in two places rather than two controls. */
.row-clear {
  flex: none;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--flame);
  text-decoration: none;
  white-space: nowrap;
}
.row-clear:hover { text-decoration: underline; }

/* Under the chips and staying there. "we need a way to switch grid/themes
   always on screen" - and the same bar is the only thing naming the filter, so
   both answers are the same fixture. `--panel-top` is the chip row's own
   bottom edge, measured at runtime by nav.js.

   The background is not optional: a sticky bar over a scrolling grid with a
   transparent one shows cards through the text. It is the page's colour rather
   than a panel's, because this is part of the page rather than something over
   it. */
.rows-bar {
  position: sticky;
  top: var(--panel-top, 0px);
  z-index: 3;
  background: var(--cream);
}

/* The divider a widened search draws where the exact answers stop. A theme
   row's header, spanning every column of the grid it sits inside, so the
   results carry on below it exactly as a theme row's cards do. */
.row-head-split {
  grid-column: 1 / -1;
  /* .row-head centres itself in a --wide box, which inside a grid cell makes
     it one column wide. Here the grid has already set the width. */
  max-width: none;
  margin: 18px 0 0;
  padding-left: 0;
  padding-right: 0;
}
.row-head-split .row-more { color: var(--muted); }
/* The first one sits directly under the result line, which already has its
   own space above it. */
.row-head-first { margin-top: 0; }


/* Narrow enough and the title and the toggle cannot share a line - they
   overlapped, the toggle sitting on top of "See all (1,369)". The toggle takes
   its own line above the title instead, which is exactly where the grid's bare
   header already puts it, so it still does not move between the two views. */
@media (max-width: 620px) {
  .row-head { flex-wrap: wrap; row-gap: 10px; }
  .row-head > .viewtoggle { order: -1; width: 100%; justify-content: flex-end; }
  /* The spacer that used to be here reserved the line the row title would
     have occupied, back when this header had no title. It has one now, so the
     two views are the same height without help and the spacer was adding a
     blank line above the cards. */
}

/* "Recently added · See all (1,368)": one line, baseline-aligned. */
.row-title {
  display: flex;
  align-items: baseline;
  gap: 10px;
  min-width: 0;
}
.row-sep { color: var(--muted); font-size: 1.1rem; }
.row-head h2 {
  font-family: 'Vollkorn', Georgia, 'Times New Roman', serif;
  text-transform: none;
  letter-spacing: 0;
  font-size: 1.32rem;
  font-weight: 500;
  color: var(--charcoal);
  border: none;
  padding: 0;
  margin: 0;
}
.row-more {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--accent);
  text-decoration: none;
  white-space: nowrap;
}
.row-more:hover { text-decoration: underline; }

/* CSS-only horizontal scrolling: no arrows, no JavaScript, no build step.
   The padding matches .wrap so the first card lines up with the heading while
   the strip itself still runs to the edge of the window - which is the cue
   that there is more to the right. */
.row-strip {
  display: flex;
  gap: var(--card-gap);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding: 4px max(var(--gutter), (100vw - var(--wide)) / 2 + var(--gutter));
  /* scroll-snap-align: start snaps to the scroll-port edge and ignores the
     container's padding, which dragged the first card flush against the window
     on load - scrollLeft sat at exactly the padding width. scroll-padding tells
     snapping where the content actually begins. */
  scroll-padding-inline-start: max(var(--gutter), (100vw - var(--wide)) / 2 + var(--gutter));
  /* No scrollbar. The fade at the right edge already says there is more, and
     it was built to be that cue; a grey bar under every row says it a second
     time and louder. Both properties are needed - scrollbar-width for Firefox,
     the pseudo-element for Safari and Chrome - and the strip keeps tabindex=0,
     so arrow keys still move it. */
  scrollbar-width: none;
}
.row-strip::-webkit-scrollbar { display: none; }
.row-strip > .card {
  /* The grid's track width, worked out the same way the grid works it out.
     The percentage resolves against the strip's content box, which is the
     same 1084px the grid divides up - the strip's padding is built from
     --gutter and --wide for exactly that reason. So card n sits at the same
     x in both views, and switching views rewraps the cards without resizing
     or moving them. */
  flex: 0 0 calc((100% - (var(--cols) - 1) * var(--card-gap)) / var(--cols));
  scroll-snap-align: start;
}
.row-strip:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Small thumbnails for the rows about recency - Recently added on the plain
   index, Recently opened at its foot (row.php, `small`). A fixed width
   rather than the grid's track: these rows are for finding your place, not
   for lining up with the grid, and eight across is the point. Less room
   under them too, or a short row sits in a tall gap - Gabe, 2026-09-26:
   "reduce the gap between first row and next". */
.row-small { margin-bottom: 20px; }
.row-small > .row-strip { gap: 14px; }
.row-small > .row-strip > .card { flex: 0 0 168px; width: 168px; }
.row-small .card-shot { aspect-ratio: 4 / 3; height: auto; }
.row-small .card-shot img { width: 100%; height: 100%; object-fit: cover; }
/* Three classes deep, to outrank editorial.css's own `[data-design] .card-title`. */
.row-small .card .card-title { font-size: 14px; line-height: 1.2; letter-spacing: -0.01em; }
/* **Every row the same height** is nav.js's job now (equalRows()): it sets
   every theme row to the tallest one's height, and the cards' spare room falls
   at their foot. It was three lines of room under every title, and that put
   the room *between* a short title and its source line - "Pan con tomate",
   then a gap, then the stars and the time. Gabe, 2026-09-30: "I think footer
   info always snug up, don't you?" */
/* The Recently row swaps its cards in place, and the row is as tall as its
   longest title: one three-line title in Recently opened made it 17px taller
   than Recently added, and everything below moved on every switch. So there
   every title stops at three - and since the two tabs share one grid cell
   (.recency-panel), the slot is always the taller of the two. Gabe,
   2026-09-29: "there's still a shift in size when switching between recent
   modes." */
[data-swap="recency"] .row-small .card .card-title {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.row-small .card *:not(.card-title):not(img) { font-size: 11px; }
/* A small card is the photograph, the name right under it, the source and
   the rating - nothing else. Gabe, 2026-09-26: "lose servings in small rows",
   then "lose cuisine and time too. We just want name right under image,
   name, source, rating." Three classes deep to outrank the designs' own
   card rules; `.rows` in front where editorial.css ties on specificity. */
.row-small .card .card-meta { display: none; }
.rows .row-small .card::before { display: none; }
.rows .row-small .card .card-title { grid-row: 2; margin-top: 8px; }
.rows .row-small .card .card-source { grid-row: 3; }
/* No stars of your own on the two small rows: on a 168px card they were a
   line of red under the source, the one extra line that made Recently opened
   look different from Recently added. Gabe, 2026-09-27: "This looks crappy.
   maybe we leave user stars off for those two 'mini' rows" (punch 100). The
   full-size cards and the recipe page still show them. */
.rows .row-small .card .card-rating { display: none; }
@media (max-width: 900px) {
  .row-small > .row-strip > .card { flex-basis: 140px; width: 140px; }
}

/* Recently added and Recently opened: one small row with two tabs at the top
   of the plain index (row.php, `tabs`). Each tab is the row's heading, at the
   heading's own size - the one on is ink with a bar under it, the other is
   grey. Gabe, 2026-09-29: "can we have both at the top, in two tabs?" */
.row-tabs {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 0.9em;
  row-gap: 2px;
}
.row-tab {
  display: inline-block;
  padding-bottom: 5px;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
  box-shadow: inset 0 -3px 0 transparent;
  transition: color 90ms, box-shadow 90ms;
}
.row-tab:hover { color: var(--charcoal); box-shadow: inset 0 -3px 0 var(--line); }
.row-tab.is-on { color: var(--charcoal); box-shadow: inset 0 -3px 0 var(--accent); }
/* The tab not chosen sits back further - Gabe, 2026-09-29: "fade 'recently
   opened' back 15%". Full strength again under the pointer. */
.row-tab:not(.is-on) { opacity: 0.85; }
.row-tab:not(.is-on):hover { opacity: 1; }
.row-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Both Recently tabs are on the page, one on top of the other in a single
   grid cell, and a tab is a dissolve between them (nav.js). The one not
   shown is invisible rather than gone, so its photographs load while you
   read the other and the cell is always as tall as the taller one - which is
   what stops the page below from moving. Gabe, 2026-09-29: the switch was
   "very slow" (a 300 KB fetch of the whole index) and the photographs landing
   afterwards were "abrupt and feel like an error".

   Visibility waits out the fade on the way out, so the old row dissolves
   rather than vanishing, and it keeps the hidden row's links out of the tab
   order and the accessibility tree. */
[data-swap="recency"] { display: grid; }
.recency-panel {
  grid-area: 1 / 1;
  min-width: 0;
  transition: opacity 0.18s ease, visibility 0s;
}
.recency-panel:not(.is-on) {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.18s ease, visibility 0s linear 0.18s;
}
@media (prefers-reduced-motion: reduce) {
  .recency-panel, .recency-panel:not(.is-on) { transition: none; }
}
/* A tab with nothing in it yet keeps a shelf's height, so choosing it does
   not pull every row under it up the page. The height is a small card's:
   photograph (4:3 of the card's width) and two lines of name. */
.row-empty {
  box-sizing: border-box;
  max-width: var(--wide);
  min-height: calc(168px * 3 / 4 + 64px);
  margin: 0 auto;
  padding: 8px var(--gutter) 0;
  color: var(--soft);
  font-style: italic;
}
@media (max-width: 900px) {
  .row-empty { min-height: calc(140px * 3 / 4 + 64px); }
}

/* ------------------------------------------------------------- browsing -- */

.count-line,
.notice {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.76rem;
  color: var(--muted);
  margin-top: 10px;
}
.notice { color: var(--soft); }

.all-link {
  margin-top: 10px;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8rem;
}
.all-link a { color: var(--accent); text-decoration: none; }
.all-link a:hover { text-decoration: underline; }

.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 40px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8rem;
}
/* The app's own vocabulary rather than a blog's: Helvetica small caps with
   letter-spacing for the controls, the same treatment as every other quiet
   control here, and Vollkorn for the readout because a count of recipes is
   content. The rule above it and the accent are the collection's, not a
   plugin's. */
.pager-step {
  font-size: 0.7rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  padding-bottom: 2px;
  white-space: nowrap;
}
.pager a.pager-step:hover { border-bottom-color: var(--accent); }
.pager .disabled { color: var(--line); }
.pager-pos {
  font-family: 'Vollkorn', Georgia, serif;
  font-size: 0.95rem;
  color: var(--soft);
  font-variant-numeric: tabular-nums;
}
.pager-of { color: var(--muted); }

@media (max-width: 520px) {
  .pager { flex-direction: column; gap: 8px; }
}

/* ------------------------------------------------------- generated card -- */

/* For the 28 recipes with no photograph, or one too small to show. The colour
   is the recipe's own, derived from its title, so it is stable and adjacent
   cards rarely collide. */
.card-shot-drawn {
  background: var(--accent);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px;
  text-align: center;
}
.card-drawing {
  width: 34px;
  height: 34px;
  flex: none;
  fill: none;
  stroke: rgba(255, 255, 255, 0.72);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.card-drawn-title {
  font-family: 'Vollkorn', Georgia, 'Times New Roman', serif;
  font-size: 0.88rem;
  line-height: 1.25;
  color: rgba(255, 255, 255, 0.94);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The detail page equivalent, taller and with room for a longer title. */
.shot-drawn {
  width: 100%;
  aspect-ratio: 3 / 2;
  margin-top: 24px;
  border-radius: 3px;
  background: var(--accent);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
}
.shot-drawn .card-drawing { width: 56px; height: 56px; stroke-width: 1.3; }
/* `hidden` must win. The rules above say display: flex, which quietly beats
   the attribute's own display: none - so picking a photo on a drawn recipe
   showed the preview WITH the teal drawing still under it (Gabe's soup
   screenshot, 2026-09-25: "Photo import bug still there"). Same guard on
   the photo, whose hidden the Cancel path sets. */
.shot[hidden],
.shot-drawn[hidden] { display: none; }

/* --------------------------------------------------------------- topbar -- */

/* Brand and search on one line, a rule under them, chips under that. Measured
   off the Figma frame at 1440: 44px gutter, ~58px brand, a 520+150 search at
   56px tall, a 2px rule, 50px chips and a 36px toggle - 310px in total, about
   half what the old title block and eyebrow took. */
/* The search, with whatever the page puts under it.

   flex: none on the field, because this is a *column* and .searchbox carries
   flex: 1 for the row it used to sit in directly - which in a column means
   grow vertically, and it did: a grey box the full height of the masthead.

   Wider than it was, at Gabe's ask: 400px against the 340 it had, still
   under .searchbox's own 520 cap. */
/* The control is its own width now - 44px closed, and as much as it needs
   open - so this stopped being a column that stretches and became a slot that
   ends at the right edge of the masthead. */
.topbar-find {
  display: flex;
  /* `0 1 auto`, not `none`: the slot is as wide as the control wants and no
     wider, and it gives ground before the masthead does. With `none` the
     control simply hung off the right edge between 560 and the width where
     240px fits. */
  flex: 0 1 auto;
  justify-content: flex-end;
  align-items: flex-end;
  min-width: 0;
}
/* The chips sit by the logo and the search keeps the right-hand end: the slot
   takes the row and the field pushes itself right. Gabe, 2026-10-06: "Move
   these three to the left (closer to logo)". */
.topbar .topbar-find { flex: 1 1 auto; justify-content: flex-start; }
.topbar .topbar-find > .searchbox { margin-left: auto; }
/* Three classes deep on purpose. `.topbar .searchbox` is declared further
   down this file with `flex: 0 1 420px`, written for the row the field used
   to sit in directly - and in a column that 420px is a *height*, which is
   exactly what it became: a grey square the full depth of the masthead. Equal
   specificity would have lost on source order. */
.topbar .topbar-find > .searchbox { flex: 0 1 auto; min-width: 0; width: auto; max-width: none; margin-top: 0; }

/* Pinned, with the chips under it - see .chipband.
   Browsing a filtered grid is narrowing something down, and the two controls
   that narrow it were the first things to scroll off the screen. Getting back
   to them meant scrolling to the top, which on an endless grid is a long way
   back from wherever the interesting thing was. Both stay. */
.topbar {
  display: flex;
  /* Bottom-aligned, which is the handoff's `align-items: flex-end`: the search
     control sits on the logo's baseline rather than floating in the middle of
     a taller row. The lockup is the tallest thing here, so nothing else
     moves. */
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  max-width: var(--wide);
  margin: 0 auto;
  padding: 22px var(--gutter) 18px;
  border-bottom: 2px solid var(--charcoal);
}

/* Pinned only where there is something to narrow.
   The class goes on for a page that has a chip band, which means the browse
   pages and not a recipe - and a recipe is where this would cost the most and
   buy the least: a fifth of a phone screen held by a search field, permanently,
   while you are reading a method off the counter. The masthead scrolls away
   there exactly as it always has. */
/* One paint path for the lockup, on every page.
   
   **This is what made the recipe page clip while the index did not.** A
   pinned header is sticky, z-indexed and backdrop-filtered, which makes it a
   stacking context and gets it its own compositing layer; a recipe page has
   no chip band, so it is not pinned, and its topbar was a plain static
   element. Two pages, two paint paths, and the filter on the lockup survived
   one of them - which is exactly the shape of a bug you cannot reproduce by
   looking at one page.
   
   So the bar is a stacking context everywhere. It changes nothing about how
   either page looks, and it means a blur tested on one is tested on both. */
.topbar {
  position: relative;
  z-index: 1;
  /* Opaque on every page, not only a pinned one.
     
     This is the last difference between the two bars, and the index is the
     one that never clipped: pinned, it gets `background: var(--cream)` and a
     backdrop-filter; on a recipe page it was transparent, and a filter
     painted over nothing is a different job from a filter painted over a
     background. Cream on a cream body, so nothing looks different - it just
     stops the two pages being two cases. */
  background: var(--cream);
}

html.has-pinned-header .topbar {
  position: sticky;
  top: 0;
  /* Above the chips, which pin under it, and above the cards, which pass
     beneath both. It needs a background of its own now that they do. */
  z-index: 30;
  background: var(--cream);
}

/* Glass, where the browser can actually blur.
   A see-through bar with no blur behind it is a photograph of a dinner with
   the word "Vegetarian" laid over it, so the translucency is only ever turned
   on inside @supports - everywhere else the bar stays the solid cream above.
   saturate lifts the colour the blur washes out; without it the cream goes
   grey over a busy grid. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html.has-pinned-header .topbar,
  html.has-pinned-header .chipband {
    background: var(--cream-glass);
    -webkit-backdrop-filter: blur(22px) saturate(1.25);
    backdrop-filter: blur(22px) saturate(1.25);
  }
}

/* Scrolled, on a phone, the lockup gives up the wordmark and keeps the mark.
   The words are the largest thing in the bar and the least useful once you are
   past the top of the page - you know what site you are on. The mark still
   goes home, and the name is still in the markup for a screen reader, which
   never saw the drawn version anyway.

   Only where the height is actually scarce, which is a phone and nothing else:
   a laptop and a tablet have the room, and there the full lockup is the better
   thing to be looking at.

   Two conditions rather than one because a phone on its side is 812px wide -
   wider than the tablet this is meant to leave alone - and 375 tall. Width
   catches it upright, height catches it sideways, and a tablet is caught by
   neither: 768x1024 upright, 1024x768 over. */
@media (max-width: 560px), (max-height: 500px) {
  html.is-stuck .topbar-name {
    display: none;
  }
  html.is-stuck .topbar-mark {
    width: 34px;
    height: 34px;
  }
  /* The blur keeps the mark's proportion: 5px of 52 is 3.3 of 34. */
  html.is-stuck .topbar-brand { --brand-blur: 3.3px; }
  html.is-stuck .topbar {
    padding-top: 10px;
    padding-bottom: 8px;
  }
}
/* Short, and on the things that change size rather than on the bar's height:
   --topbar-h is remeasured throughout by the ResizeObserver, so the chips
   follow the bar down instead of jumping once it has finished. */
.topbar,
/* No transition. This animated width, height and padding - three layout
   properties - so every frame of the shrink relaid the whole header while the
   page was already scrolling, and the mark visibly lagged behind the bar it
   sits in. Snapping costs nothing: the bar changes height at the same instant
   the class lands, which is what the scroll already did. */
.topbar-mark { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .topbar,
  .topbar-mark { transition: none; }
}
/* The lockup: the four-hob mark beside a two-line wordmark.
   Sizes are the brand package's - 52px mark against 25px Gloock - rather than
   the fluid clamp the words used, because a mark and a typeface drawn together
   stop being a lockup when one of them scales and the other does not. */
/* A page's name beside the lockup (the review page), as the tools page sets
   "Tools": a rule, then small capitals in the accent. It takes the row's
   spare room on its right, so it sits by the lockup rather than in the
   middle of the bar. */
.topbar-label {
  align-self: center;
  margin-right: auto;
  padding-left: 18px;
  border-left: 1px solid var(--line);
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
}
/* Below this the bar has no room for it and it ran under the Plans chip; the
   office's row of pages says where you are anyway. */
@media (max-width: 820px) { .topbar-label { display: none; } }
.topbar-brand {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  color: var(--charcoal);
  text-decoration: none;
}
/* The mark is two layers in one 52px box: the three rings in one <svg>, the
   dot in another on top. Two, because the blur has to land on an <svg> root.
   Safari applies no CSS filter to anything inside an SVG (WebKit 246106), and
   where Chrome and Firefox do, the radius is in viewBox units - blur(5px) on
   a <g> in a 100-unit box drawn at 52px is 2.6px on screen, and 1.9px at 38.
   On the root it is CSS px, the same px the words get, at every size the mark
   is drawn at.

   Each svg is drawn twice the size of the box and half of it outside, against
   a viewBox padded by the same 50 units a side, so the artwork sits exactly
   where it always did and the margin is room for the blur. Room inside the
   viewport rather than overflow: visible, because Safari clips a root svg's
   filter at the svg's own edge whatever overflow says.

   **The margin has to beat three sigma, not look generous.** A CSS blur's
   radius is its standard deviation, so blur(5px) is visible about 15px out.
   The first version gave 25 units - 9.5px against the 10.8px a 3.6px blur
   needs - so it still clipped, just less, which is how it survived a look.
   50 units is 26px at every size the mark is drawn.

   pointer-events: none so the enlarged svgs do not widen the hover target;
   the 52px span is the target, as the svg was. */
.topbar-mark {
  position: relative;
  width: 52px;
  height: 52px;
  display: block;
  flex: none;
}
.topbar-mark > svg {
  position: absolute;
  inset: -50%;
  width: 200%;
  height: 200%;
  display: block;
  pointer-events: none;
  /* The UA stylesheet puts overflow: hidden on every <svg>, so an svg clips
     its own filter output at its viewport. The padded viewBox means the blur
     fits inside anyway - but belt as well as braces costs nothing here, and
     this override existed before the two-layer version and was dropped with
     the markup it was written for. */
  overflow: visible;
}
.topbar-name {
  display: flex;
  flex-direction: column;
  font-family: Gloock, 'Vollkorn', Georgia, serif;
  font-size: 25px;
  line-height: 1.06;
  white-space: nowrap;
  /* Room for the blur inside the element's own box.
     
     **Safari clips a filter's output to the border box of the element it is
     on.** The mark was given room by an oversized viewBox and that half of it
     worked; the words are plain HTML with no viewBox to pad, and they were
     still being cut off square where the text box ends - which is what the
     screenshot showed, a hard vertical edge to the right of "Kitchen".
     
     Padding makes the border box bigger than the text, so the spread has
     somewhere to land; the matching negative margin puts the content back
     exactly where it was, because a flex item is laid out by its margin box.
     20px against the 15px a 5px blur spreads. */
  padding: 20px;
  margin: -20px;
}
/* The four circles, drawn here rather than in the file, so the hover can reach
   them. Geometry stays in /brand/mark-small.svg; this is only paint. */
.mark-ring {
  fill: none;
  stroke: var(--mark-ink);
  stroke-width: 5;
}
.mark-dot { fill: var(--mark-dot); }

/* On hover the lockup goes out of focus and the red dot does not. The words
   and the three rings blur - nothing fades and nothing changes colour - and
   the filled circle is left as the one sharp thing on the bar: the mark doing
   what the name says.

   One rule for the words and the rings, so they cannot drift apart: the same
   property and the same clock, with the radius each reads from --lockup-blur.
   The rings take a smaller radius and the stroke thickens as they go, both
   set by eye against the words: a 2.6px line at the words' full radius does
   not read as soft, it reads as gone, and the thicker line keeps the ink
   where the eye expects a ring. It has been 0.6 and 0.7 and is 0.85 - the
   number is not a measurement, it is the point at which a thick soft ring
   stops reading as sharper than the words beside it. A ring carries more ink
   per pixel than a letter does, so matched radii do not look matched.

   At rest the filter is none rather than blur(0). A zero blur is still a
   filter: Chrome paints the words through an offscreen layer where Skia
   cannot do subpixel antialiasing, so on platforms with LCD text (not macOS)
   the wordmark would sit a shade softer than the text around it, for
   nothing. */
.topbar-brand {
  --brand-blur:  5px;
  --brand-clock: 220ms ease;
}
.topbar-name { --lockup-blur: var(--brand-blur); }
.mark-rings  { --lockup-blur: calc(var(--brand-blur) * 0.85); }
.topbar-name,
.mark-rings {
  filter: none;
  transition: filter var(--brand-clock);
  /* Each on its own compositing layer, and this is the line that fixed the
     clip Safari had on recipe pages for four attempts.
     
     Measured, on the real page, against five other mechanisms: a blur on an
     un-promoted element is clipped there, and the tell was that it sometimes
     came good a moment later - which is lazy layerization, not geometry. So
     none of the padding was the answer, though the padding was right for its
     own reasons and stays. `transform: translateZ(0)` fixed it identically
     and is not used: it would rasterize the wordmark through a transform at
     rest, and this is a display serif that spends most of its life sharp.
     
     Two small elements, so two small layers. */
  will-change: filter;
}
.mark-ring { transition: stroke-width var(--brand-clock); }
/* The dot draws down a size on the same clock the blur runs on - a focus
   pull: the rest of the lockup goes soft while the one sharp thing gets
   smaller and denser. Gabe, 2026-09-25: "shrink the dot smaller as we blur
   the text". fill-box so the scale is about the dot's own centre, not the
   corner of a padded viewBox. */
.mark-dot {
  transform-box: fill-box;
  transform-origin: center;
  transition: transform var(--brand-clock);
}
/* Hover only, and only where hover is a real thing.
   
   Keyboard focus does not blur: the thing you have just focused is the one
   thing that has to stay readable, and the outline every link gets already
   says where you are. It was in this rule and came out.

   @media (hover: hover) because iOS Safari leaves :hover on the last thing
   tapped. The link normally navigates, so the state goes with the page - but
   tap the mark, then swipe back, and the recipe returns from the cache with
   its lockup still out of focus, pointing at a touch nobody remembers making.

   .brand-cold is the other half of that, and it is set by the inline script
   under the header: with the mark just clicked to go home, the cursor is
   still on it when the next page paints, so without this every trip home
   arrived already blurred. It comes off the first time the pointer leaves. */
@media (hover: hover) {
  .topbar-brand:not(.brand-cold):hover .topbar-name,
  .topbar-brand:not(.brand-cold):hover .mark-rings {
    filter: blur(var(--lockup-blur));
  }
  .topbar-brand:not(.brand-cold):hover .mark-ring {
    stroke-width: 7.5;
  }
  .topbar-brand:not(.brand-cold):hover .mark-dot {
    /* 0.82 first; "can I see it a tad smaller" the same night. */
    transform: scale(0.74);
  }
}

@media (prefers-reduced-motion: reduce) {
  .topbar-name,
  .mark-rings,
  .mark-ring,
  .mark-dot { transition: none; }
}

@media (max-width: 640px) {
  .topbar-brand { gap: 10px; --brand-blur: 3.6px; }
  .topbar-mark { width: 38px; height: 38px; }
  /* 3.6px spreads 10.8; 14 covers it, and keeps the bar's height. */
  .topbar-name { font-size: 19px; padding: 14px; margin: -14px; }
}

/* Field, count and button are one unit - no gap between any of them. The
   sizing that used to be here (`flex: 0 1 420px`) went with the tinted field:
   the control is its own width now, 44px closed and about 340 open, and
   `.topbar .topbar-find > .searchbox` is where that is said. */
.topbar .searchbox { margin-top: 0; gap: 0; }

/* ---------------------------------------------------------------- chips -- */

/* One line that scrolls sideways rather than wrapping. Wrapping would let the
   header grow by a row every time a filter set is wide, which is exactly the
   height this redesign is removing - and it would move the chips vertically,
   which the last fix was about. Scrolling keeps the header a fixed height at
   every width, and the fade says there is more. */
/* The band reaches the ends of the rule under the masthead rather than
   stopping a gutter short of it, so both fades finish exactly where that line
   does. Pulled out of .wrap.wide's padding rather than moved in the markup;
   .chipbar puts the gutter back, so the first pill still starts level with the
   row titles and the cards. */
/* Now a sibling of .topbar rather than the first thing inside .wrap, so this
   is the box .wrap used to give it: the same max-width and centring the
   negative margins used to reach. Both fades still end where the rule does. */
.chipband {
  position: relative;
  max-width: var(--wide);
  margin: 0 auto;
}

/* Pinned directly under the masthead.
   --topbar-h is measured and kept current by assets/nav.js, because the bar's
   height is not one number: the mark is 52px and then 38px, and under 560px
   the search field takes a line of its own. A hard-coded offset would be right
   at one width and wrong at the other two.
   Gated on the class that script adds, so with JavaScript off the chips simply
   scroll as they always did - the alternative is a top of 0 that parks them
   underneath the masthead, which is worse than not pinning them. */
html.has-pinned-header .chipband {
  position: sticky;
  top: var(--topbar-h);
  z-index: 20;
  background: var(--cream);
}

/* Only once something is actually behind them. At rest the chips sit on the
   same cream as everything else and a rule there would be a line drawn under
   nothing; scrolled, it is the edge the cards pass under. */
html.is-stuck .chipband {
  border-bottom: 1px solid rgba(0, 0, 0, 0.10);
}
.chipbar {
  display: flex;
  align-items: center;
  gap: 10px;
  /* No horizontal padding and no max-width of its own: .chipbar sits inside
     .wrap.wide, which has already applied both. Setting them again indented
     the whole bar by a second gutter, so the chips and the recipe count stood
     to the right of the row titles and every card below them. */
  padding: 14px var(--gutter) 12px;
  /* The clear slot is taken *out of* the gutter rather than added in front of
     it, so where there is room the first chip still starts exactly level with
     the row titles and the cards below - which is what the note above is
     protecting. Both the 26px slot and the 10px gap after it come out, because
     the gap pushes the first chip just as the slot does; subtracting only the
     slot left the chips 10px adrift, which is measured, not guessed.
     Below a 36px gutter there is nothing left to take, so the chips sit in by
     whatever is missing - 20px at 375px wide, nothing from about 900px up. It
     is a constant indent at any one width, which is the point: it does not
     change when you tap a chip.
     The 4px floor is why it is max() and not a bare calc. Without it the slot
     starts at x=0 on a phone and the X has 7px of air while everything else on
     the page has 20px, which reads as falling off the screen. 4px is under the
     5px the laptop gutter already leaves, so the floor costs the wide layout
     nothing and the first chip still lands exactly on the content edge. */
  padding-left: max(4px, calc(var(--gutter) - 36px));
  overflow-x: auto;
  /* Sideways only. `overflow-x: auto` makes the other axis auto too, and on
     iOS a pixel of a chip below the bar was enough for the row to give a
     little up and down under the finger - Gabe, 2026-10-03: "it also feels
     like it wants to scroll vertically a bit". */
  overflow-y: hidden;
  scrollbar-width: none;
}
.chipbar::-webkit-scrollbar { display: none; }
/* One at each end. A row scrolled along used to run hard into the left edge
   while the right still promised more; both now say the same thing. Solid at
   the outside so a pill is gone by the time it reaches the end of the rule,
   clear by the time the first one starts. */
/* The row ends by dissolving, not by having cream painted over it.
   
   This was two gradient tabs sitting on top of the chips - a wash of the
   page colour, wide enough to read as haze rather than as an edge, and it
   had to be re-tinted every time the bar's own background changed. Worse, a
   backdrop-filter was added to them to blur the chips underneath and could
   never have worked: .chipband carries backdrop-filter for its glass, which
   makes it a backdrop root, so a pseudo-element inside it cannot see its own
   siblings. It rendered as a hard cut with dead CSS on top.

   A mask needs no backdrop and no colour. The chip's own pixels go
   transparent, so it dissolves into whatever is actually behind the bar -
   cream at rest, blurred cards when pinned - and it stays correct if either
   ever changes. The ramp is short on purpose: he asked for less fade. */
/* Each end fades only while there is something past it.

   It used to fade at both ends always, and the empty clear slot absorbed the
   left ramp so nothing was ever eaten by it. With the slot gone and the
   circle outside the bar, the first chip starts 4px in - inside the 22px
   ramp - so an active chip at rest had its left edge dissolved. Reported
   from Safari, with a picture of a flame chip fading into the page.

   Both ends are now classes nav.js sets from the scroll position, which is
   also what the fade was always claiming: a row that has not been scrolled
   has nothing hidden to the left, and saying otherwise was decoration
   pretending to be information. */
.chipbar {
  --chip-fade-l: 0px;
  --chip-fade-r: 0px;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--chip-fade-l), #000 calc(100% - var(--chip-fade-r)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 var(--chip-fade-l), #000 calc(100% - var(--chip-fade-r)), transparent 100%);
}
.chipbar.has-left  { --chip-fade-l: 22px; }
.chipbar.has-right { --chip-fade-r: 22px; }
/* The chip row, with one control in the margin in front of it.

   .chipbar scrolls sideways and an overflow box clips what hangs out of it,
   so the control cannot live inside it. This wrapper is what lets it sit in
   the gutter beside the bar instead. */
.chiprow {
  display: flex;
  align-items: center;
  min-width: 0;
}
.chiprow > .chipbar { flex: 1 1 auto; min-width: 0; padding-left: 4px; }

/* The filter control: a square in the left margin.

   It replaces a chip that read as another filter rather than as the way to
   all of them - which is the thing a row of pills cannot say about one of
   its pills.

   Square since 2026-10-04, a circle until then. Gabe: "can we try the filter
   options icon in a square" (punch 198). Square corners are what the
   header's Plans, + and search buttons have, and the editorial chip cells.

   The numbers keep the chips where they were. The square is 30px and sits in
   the 36px the clear slot used to hold, so the first chip still starts level
   with the row titles and the cards below, which is what .chipbar's padding
   note is protecting. */
.filterdock {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  margin-left: max(4px, calc(var(--gutter) - 34px));
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 0;
  background: var(--paper);
  color: var(--soft);
  cursor: pointer;
  anchor-name: --filterdock;
}
.filterdock:hover { border-color: var(--caribbean); color: var(--caribbean); }
.filterdock:focus-visible {
  outline: 2px solid var(--caribbean);
  outline-offset: 2px;
}
.fd-icon {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
}
/* Something is on. A ring rather than a badge with a number on it: the number
   is in the panel, on the axis it belongs to, and a count here would be a
   second place to keep it right. */
.filterdock.is-on {
  border-color: var(--caribbean);
  color: var(--caribbean);
  box-shadow: inset 0 0 0 1px var(--caribbean);
}
.filterdock:is([popovertarget]):is(:has(+ [popover]:popover-open)) { color: var(--caribbean); }

/* Over the page, and see-through, which is why it is a popover.

   A panel nested in .chipband could not be translucent at all: the band
   carries backdrop-filter for its own glass, which makes it a backdrop root,
   so nothing inside it can filter what is behind the band. That is the trap
   the fade tabs fell into, written up a few rules above. A popover is drawn
   in the **top layer**, outside the document's paint order, so its backdrop
   is the page - and the grid blurs behind it the way this was always
   supposed to work.

   The top layer brings Escape and click-outside with it, in HTML, with no
   script. */
.filterpanel {
  position: fixed;
  /* Where the panel hangs when the browser cannot anchor it. --topbar-h is
     measured by nav.js and is the masthead's real height; 58px is the chip
     row under it. Both are overridden below wherever anchor positioning
     works, which is where this is exact rather than close. */
  top: var(--panel-top);
  left: max(var(--gutter), calc(50vw - (var(--wide) / 2)));
  margin: 0;
  width: min(94vw, 54rem);
  max-height: min(72vh, 40rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 14px 16px 16px;
  border: 1px solid var(--line);
  border-radius: 12px;
  /* Translucent on purpose, and this time it works. Narrowing a collection is
     a loop - choose, see what is left, choose again - so the cards stay
     visible and move while the panel is open. */
  background: rgba(248, 243, 233, 0.82);
  -webkit-backdrop-filter: blur(26px) saturate(1.3);
  backdrop-filter: blur(26px) saturate(1.3);
  box-shadow: 0 18px 50px rgba(42, 38, 34, 0.22);
}
.filterpanel::backdrop { background: transparent; }

@supports (anchor-name: --a) {
  .filterpanel {
    position-anchor: --filterdock;
    top: calc(anchor(bottom) + 10px);
    left: anchor(left);
  }
}

/* The head: what this is, how much is left, and the way out of all of it. */
/* Filter at the left; the way out and the count stacked at the right, in that
   order. Gabe, 2026-09-22: "move clear all in the filter panel above the
   count", and "move the whole unit down closer to the horizontal line".

   A grid rather than a flex row, because the two on the right have to stack
   and there is nothing wrapping them - and because `Clear all` is absent
   unless something is on, which auto-placement handles: the count simply
   takes the row above. */
/* **One line, three things, and the same three things either way.** It was two
   rows - the title and Clear all above, the count hanging under it - which is
   a head taller than the thing it heads and reads as two decisions when it is
   one line of information and one way out. Gabe, 2026-09-23: "i feel like the
   header is too convoluted."

   The count sits beside the word rather than under it because it says what
   this panel is currently yielding: "Filter, 70 of 1,365". Clear all keeps the
   right edge, where the one destructive control belongs and where the results
   bar puts the same words. */
.fp-head {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  column-gap: 12px;
  margin: 0 0 10px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--line);
}
/* Every cell stated, none of it auto-placed. Auto-placement is what made the
   head one line unfiltered and two lines filtered: with `Clear all` missing,
   the count took the row above it and the whole list moved up. */
.fp-head > .fp-lead      { grid-column: 1; grid-row: 1; }
.fp-head > .fp-total     { grid-column: 2; grid-row: 1; justify-self: start; }
.fp-head > .fp-clearall  { grid-column: 3; grid-row: 1; justify-self: end; }

/* Hidden, not absent - it holds its row either way. */
.fp-clearall.is-off { visibility: hidden; pointer-events: none; }

/* The word and the way out, on one baseline. */
.fp-lead {
  display: flex;
  align-items: baseline;
  gap: 5px;
  min-width: 0;
}
/* The chevron and the word are one control now, so pressing "Filters"
   closes the panel - Gabe, 2026-09-25: "the whole thing recollapses". The
   button carries both and the hover lights both, which is what says the
   word is pressable. */
.fp-close {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin: -0.2em 0 -0.2em -0.35em;
  padding: 0.15em 0.45em 0.15em 0.3em;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--charcoal);
  cursor: pointer;
  align-self: center;
  font: inherit;
}
.fp-close svg {
  width: 0.82em;
  height: 0.82em;
  fill: none;
  stroke: currentColor;
  /* Chunky, on his word. The stroke scales with the box, so this is a ratio
     rather than a weight: 4 against a 12-unit viewBox drawn at 0.82em of a
     1.05rem title is about 3.6px, half again the stem of "Filter" at 600 -
     which is what makes it read as a control rather than as punctuation. */
  stroke-width: 3.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.fp-close:hover { color: var(--flame); background: rgba(184, 70, 47, 0.10); }
.fp-title {
  font-family: var(--serif, inherit);
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--charcoal);
}
.fp-clearall {
  color: var(--accent);
  text-decoration: none;
  font-size: 0.82rem;
  font-weight: 600;
}
.fp-clearall:hover { text-decoration: underline; }
.fp-total {
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

/* Several axes at once, without becoming a form.

   A grid that fills by width rather than a fixed number of columns: the panel
   is 54rem on a laptop and 94vw on a phone, and the same rule gives four
   columns and one. align-items: start so a short axis does not stretch to the
   height of the tallest. */
.fp-axes {
  columns: 11.5rem;
  column-gap: 22px;
}
/* Columns rather than a grid, and the difference is the ragged bottom.

   A grid puts every axis in a cell of a row, so one tall axis - cuisine is
   thirty-nine values and every other axis is four - sets the height of its
   whole row and the short ones sit over a column of air. Columns flow: the
   next axis starts where the last one ended, so four short lists stack in the
   space one long one takes. */
.fp-axis {
  min-width: 0;
  margin: 0 0 14px;
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
}
.fp-axis-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 8px;
  margin: 0 0 3px;
  padding: 0 9px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}
/* `.fp-any` was here - the per-axis "turn this axis off", which was a filter
   action in a panel that no longer filters. */
/* Not built, and saying so rather than looking live. A placeholder that could
   be mistaken for a control gets tapped once and disbelieved afterwards. */
.fp-soon {
  white-space: nowrap;
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0 4px;
}
.fp-axis.is-soon .fp-row { color: var(--muted); cursor: default; }

/* The cuisine list is thirty-nine rows and every other axis is four. Without
   a ceiling it sets the height of the whole panel and the rest float at the
   top of a column of air. */
.fp-axis:not(.is-soon) .fp-list {
  max-height: 19rem;
  overflow-y: auto;

  /* **No `overscroll-behavior: contain` here, and that is the point.** The
     panel has it, which stops a wheel over the panel reaching the cards
     behind; the list had it too, which stopped a wheel over the list reaching
     the *panel*. The cuisine list covers most of the panel, so with the panel
     scrolled down at all there was nowhere left to put the cursor and no way
     back up to "Filter" and "Clear all". Gabe, 2026-09-23: "im in a state here
     where i cant scroll to the top."

     Chaining is what is wanted: the list scrolls, then the panel scrolls, then
     the panel's own `contain` stops it going any further. */

  /* **A rule, not a fade.** It used to dissolve into the panel, the way the
     chip row's ends do - and a half-faded row of words is a row you try to
     read. Gabe, 2026-09-23: "lose the fade match the line above." The line
     above is the one under "Filter", so the list ends on the same hairline
     and the next axis begins under it. A scrollbar is not needed to say there
     is more: the list is cut mid-row, which says it more plainly than a
     gradient did. */
  border-bottom: 1px solid var(--line);
  padding-bottom: 2px;
}

/* **Room for the scrollbar, on every list rather than the one that scrolls.**
   macOS draws an overlay scrollbar, which reserves nothing and lands the thumb
   on top of the counts - "40" under a grey bar. Gabe, 2026-09-23: "we need
   space for the scrollbars."

   On every list because the counts of one axis line up with the counts of the
   next, and a gutter on the cuisine list alone would step that column in by
   12px halfway down the panel. `scrollbar-gutter` is not the answer for the
   same reason it is not the problem: it reserves nothing when the scrollbar is
   an overlay, which is the case being fixed. */
.fp-list { margin: 0; padding: 0 12px 0 0; list-style: none; }

/* The row is the pin and the link, side by side.

   The slot on the left used to hold a tick saying the filter was on. Gabe,
   2026-09-22: "sticky meant pinned we need another indicator for the tick bc I
   want to put the pin icon and control there. Maybe weight/color?" So the slot
   is the pin's, and on is said by weight and colour - which the row was
   already doing underneath the tick, so nothing had to be invented to replace
   it.

   The pin is a sibling of the link rather than inside it, because it is a
   submit button and a button inside an anchor is not valid HTML. That is the
   same constraint that put the card's source arrow outside its card link.

   The slot is always the same width, filled or empty, so a row with nothing to
   pin lines its name up with every other row's. */
/* **One row, one control, one verb: put this on the chip row or take it off.**

   It was three elements - a pin, sometimes an x, and a link whose text turned
   the filter on - and the seams between them are where the underline, the
   session chip and the per-axis "Any" all came from. The row is a single
   button now and the whole of it is the target. */
.fp-item { display: flex; }

.fp-row {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: 7px;
  width: 100%;
  min-width: 0;
  padding: 4px 9px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--charcoal);
  font: inherit;
  font-size: 0.875rem;
  line-height: 1.35;
  text-align: left;
  cursor: pointer;
}
button.fp-row:hover { background: rgba(27, 107, 115, 0.10); }
.fp-row:focus-visible { outline: 2px solid var(--caribbean); outline-offset: -2px; }

/* **A pin and a word again** (2026-09-30, templates/browse-page.php,
   $pinControl): the row is a box holding two controls, so the row's own
   padding moves onto them and each has its own hover. */
div.fp-row { padding: 0; gap: 0; cursor: default; }
.fp-pin-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  padding: 4px 0 4px 9px;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
}
.fp-word {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  padding: 4px 9px 4px 7px;
  border-radius: 6px;
  color: inherit;
  text-decoration: none;
}
.fp-word:hover { background: rgba(27, 107, 115, 0.10); }
.fp-pin-btn:focus-visible, .fp-word:focus-visible { outline: 2px solid var(--caribbean); outline-offset: -2px; }
.fp-pin-btn:hover .fp-pin { opacity: 0.7; }

/* No control and no hover: the cuisines with no theme behind them and the axes
   that are not built. It holds its place in the list and does nothing. */
.fp-row.is-none { cursor: default; color: var(--muted); }

/* The rows that are filtering the page right now. */
.fp-row.is-on { color: var(--flame); font-weight: 700; }

.fp-pin {
  flex: none;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--soft);
  /* **Always visible, at two strengths.** The decided gesture, 21 September,
     was a pin that appears on hover and is otherwise not there. It does not
     survive contact: Gabe could not find the control at all - "there's no way
     to pin" - and once he had, a screenshot showed pins on four rows, a filled
     one on a fifth, a tinted empty box on a sixth and nothing on the rest.
     Every one of those was a different visibility rule firing, and a control
     whose presence is itself a state cannot be read.

     Faint is "not on your row", solid is "on it", and there is no third thing
     for it to say any more - the filter's own on/off lives on the chip. */
  opacity: 0.22;
  transition: opacity 90ms linear, color 90ms linear;
}
.fp-pin svg {
  width: 13px;
  height: 13px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.fp-row:hover .fp-pin { opacity: 0.55; }

.fp-row.is-pinned .fp-pin {
  opacity: 1;
  color: var(--caribbean);
}
.fp-row.is-pinned .fp-pin svg { fill: currentColor; }
/* On and not kept: the hollow pin at full strength - a temporary chip. */
.fp-row.is-temp .fp-pin { opacity: 1; }
.fp-row.is-none .fp-pin { opacity: 0; }

.fp-name { flex: 1 1 auto; min-width: 0; }
.fp-count {
  flex: none;
  font-variant-numeric: tabular-nums;
  font-size: 0.8em;
  color: var(--muted);
}
.fp-row.is-on .fp-count { color: inherit; }

/* Depth is an indent, on the whole row rather than the name - otherwise a
   child's pin sat under its parent's and the column of pins stopped being a
   column. */
.fp-item.is-d1 { padding-left: 16px; }
.fp-item.is-d2 { padding-left: 32px; }
.fp-item.is-d1 .fp-name, .fp-item.is-d2 .fp-name { color: var(--soft); }

/* ---------------------------------------------------------------------------
   Two more shapes for the same panel, while we decide which one it is.

   ?panel=rail and ?panel=band, remembered in a cookie the way ?view is. The
   markup does not change and neither does the filtering: same links, same
   counts, same control opening it. Everything below is a stylesheet.

   This is temporary. When the layout is chosen, two of these three go and the
   parameter goes with them.
   -------------------------------------------------------------------------*/

/* RAIL. Down the left edge, and the page moves over rather than under.

   The one layout where choosing a filter never covers what you are watching.
   It costs about 210px of width, which is why it opens and closes from the
   same circle instead of always being there.

   Still a popover, and that is load-bearing: the top layer is what lets a
   fixed element ignore .chipband's backdrop root. Positioned by hand rather
   than by the anchor, since it is pinned to the window and not to the
   control.

   Opaque, unlike the other two. Nothing is behind it to see. */
.filterpanel[data-panel="rail"] {
  /* Below the chip row, not beside it. Shifting the chips along would move
     the square that opens this, so the control would jump out from under the
     cursor that had just clicked it. */
  inset: var(--panel-top) auto 0 0;
  width: var(--rail-w);
  /* **The rail scrolls itself, and it took a max-height to make it.**
     Gabe, 2026-09-23: "you can't scroll the filter list until you get to the
     end of the right frame." The popover UA stylesheet sets `height:
     fit-content`, which beats `bottom: 0` - so the panel grew to its content,
     1,550px against a 768px window, hung off the bottom of the screen and
     could only be reached by scrolling the *page* to the end. `max-height:
     none` here was helping it.

     `dvh` rather than `vh`: on a phone `vh` is the height with the browser's
     chrome hidden, so the last rows sit under the address bar until you
     scroll it away. */
  height: auto;
  max-height: calc(100dvh - var(--panel-top));
  padding: 14px 10px 24px;
  border-width: 0 1px 0 0;
  border-radius: 0;
  background: var(--cream);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: 4px 0 24px rgba(42, 38, 34, 0.10);
}
.filterpanel[data-panel="rail"] .fp-axes { columns: auto; }
/* **The rail is 13.5rem and the head does not fit on one line there.** The
   popover is 54rem and gets the single line; a rail that tried it would put
   "Clear all" off its own edge. So the count goes back under, which is what
   the rail has always drawn - and `flex-wrap`, which this said while the head
   was a flex row, did nothing at all once it became a grid. */
.filterpanel[data-panel="rail"] .fp-head {
  grid-template-columns: 1fr auto;
  row-gap: 2px;
}
.filterpanel[data-panel="rail"] .fp-head > .fp-clearall { grid-column: 2; grid-row: 1; }
.filterpanel[data-panel="rail"] .fp-head > .fp-total    { grid-column: 2; grid-row: 2; justify-self: end; }

/* The page steps aside for it, and leaves a gutter of air.

   :has() on the open state rather than a class a script would have to keep in
   sync - the panel being open is already in the DOM, and reading it there
   cannot drift.

   **The width is a token because two places need the same number.** It was
   written twice, and the page's copy was the rail's width exactly - so the
   first card began at 216px and the panel ended at 216px, and a card that
   starts where the panel stops does not look moved, it looks cut off. Gabe,
   2026-09-22: "lets also move the cards over enough to create a margin between
   them and the filter card. Want to make what's happening very obvious."

   `--gutter` for the air rather than a number of its own: it is the same
   distance the page already keeps from the window, so the grid stands off the
   rail exactly as far as it stands off the edge. */
:root { --rail-w: 13.5rem; }

html:has(.filterpanel[data-panel="rail"]:popover-open) .wrap,
html:has(.filterpanel[data-panel="rail"]:popover-open) .rows {
  padding-left: calc(var(--rail-w) + var(--gutter));
  transition: padding-left 140ms ease;
}

/* BAND. Across the top, one row of columns, nothing anywhere that scrolls
   except the cuisine list.

   Every axis is on screen at once, which is the thing the other two buy with
   either a scroll or a second click. The cost is the top of the grid: this
   one lies over the first row of cards rather than beside them. */
.filterpanel[data-panel="band"] {
  top: var(--panel-top);
  left: max(var(--gutter), calc(50vw - (var(--wide) / 2)));
  right: max(var(--gutter), calc(50vw - (var(--wide) / 2)));
  width: auto;
  max-height: none;
}
.filterpanel[data-panel="band"] .fp-axes { columns: 8.5rem; }
/* Tall enough to stand level with the longest placeholder column rather than
   half its height: cuisine is the axis with something to say and it should
   not be the shortest thing on screen. */
.filterpanel[data-panel="band"] .fp-axis:not(.is-soon) .fp-list { max-height: 15rem; }

@supports (anchor-name: --a) {
  .filterpanel[data-panel="band"] {
    position-anchor: none;
    top: var(--panel-top);
    left: max(var(--gutter), calc(50vw - (var(--wide) / 2)));
  }
  .filterpanel[data-panel="rail"] { position-anchor: none; }
}

/* A phone has no room for a rail and no room for eight columns, so both fall
   back to the panel that already works there. */
@media (max-width: 720px) {
  .filterpanel[data-panel="rail"],
  .filterpanel[data-panel="band"] {
    inset: auto;
    top: var(--panel-top);
    left: max(4px, calc(var(--gutter) - 34px));
    right: auto;
    width: min(94vw, 54rem);
    max-height: min(72vh, 40rem);
    padding: 14px 16px 16px;
    border-width: 1px;
    border-radius: 12px;
    background: rgba(248, 243, 233, 0.82);
    -webkit-backdrop-filter: blur(26px) saturate(1.3);
    backdrop-filter: blur(26px) saturate(1.3);
    box-shadow: 0 18px 50px rgba(42, 38, 34, 0.22);
  }
  html:has(.filterpanel[data-panel="rail"]:popover-open) .wrap,
  html:has(.filterpanel[data-panel="rail"]:popover-open) .rows {
    padding-left: 0;
  }
}

/* Always in the row, empty or not. This is the thing that keeps the chips
   still: layout asks the slot how wide it is, never whether it has an X in
   it. Do not make it conditional again. */
.chiplead {
  flex: none;
  width: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The X is 11px and the thing you hit is 26 by 32, which is the slot. Both
   numbers matter: small enough to read as a dismissal rather than a button,
   big enough for a thumb. */
.chiplead-clear {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 32px;
  color: var(--caribbean);
  text-decoration: none;
}
.chiplead-clear:hover { color: var(--accent); }
.chipicon {
  width: 11px;
  height: 11px;
  flex: none;
  fill: currentColor;
  stroke: none;
}
.chiplead-clear .chipicon {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
}
.chip {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  flex: none;
  padding: 9px 16px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: #fff;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8rem;
  color: var(--charcoal);
  text-decoration: none;
  white-space: nowrap;
}
.chip:hover { border-color: var(--accent); color: var(--accent); }

/* Only ever rendered when a job did not run, so it is allowed to be quiet
   rather than loud - it is a note, not an alert, and nothing here is broken.
   Pushed to the end of the bar so its appearing cannot move the search box. */
.topbar-waiting {
  margin-left: auto;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  color: var(--muted);
  white-space: nowrap;
}

/* One slot, in tabular figures, wide enough for the largest number the bar can
 * ever show. Without it, tapping a filter drops a neighbour from 364 to 24,
 * that chip narrows, and every chip to its right slides left under the cursor.
 *
 * --count-w comes from the template and is measured from the *unfiltered*
 * collection size, not from the counts on screen. Two earlier versions of this
 * were wrong in opposite directions: sizing from the visible counts shrank the
 * slot on the very tap that was meant to leave the chips alone, and replacing
 * that with a hard-coded 3ch quietly assumed a four-figure collection would
 * never arrive. The fallback is only for a page that forgets to set it.
 */
.chip-count {
  color: var(--muted);
  font-size: 0.72rem;
  min-width: var(--count-w, 4ch);
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}
.chip-on {
  background: var(--flame);
  border-color: var(--flame);
  color: var(--cream);
}
.chip-on:hover { background: var(--accent); border-color: var(--accent); color: var(--cream); }
.chip-on .chip-count { color: rgba(248, 243, 233, 0.65); }
/* Always present, so a chip does not widen when it is switched on - only the
   tick's visibility changes, never the space it occupies. */
.chip-tick {
  font-size: 0.72rem;
  flex: none;
  width: 0.85em;
  text-align: center;
}
.chip-tick.is-off { visibility: hidden; }

/* ------------------------------------------------------- rows/grid view -- */

/* Both states, joined, current one filled. */
.resultline {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  /* Inside .wrap.wide too - see .chipbar above. */
  padding: 0 0 14px;
}
/* The utility face, not the heading one. It sat in Vollkorn when it had a line
   to itself and read as a sentence about the results; on the toggle's row it
   is a readout beside a control, and Gabe asked for it in sans. */
.resultcount {
  margin: 0;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8rem;
  color: var(--muted);
}
.viewtoggle { display: inline-flex; flex: none; }
.viewseg {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-weight: 600;
  color: var(--soft);
  text-decoration: none;
  padding: 8px 14px;
  border: 1px solid var(--line);
  background: #fff;
}
.viewseg:first-child { border-radius: 3px 0 0 3px; }
.viewseg:last-child  { border-radius: 0 3px 3px 0; border-left: none; }
.viewseg:hover { color: var(--accent); }
.viewicon { width: 10px; height: 10px; flex: none; fill: currentColor; }
.viewseg.is-on {
  background: var(--charcoal);
  border-color: var(--charcoal);
  color: var(--cream);
}
.viewseg.is-on:hover { color: var(--cream); }

@media (max-width: 560px) {
  .topbar { flex-wrap: wrap; gap: 10px; padding-bottom: 14px; }

  /* Scrolled, the two halves come back onto one line.
     Dropping the wordmark alone barely helped here: the mark still held a full
     row of its own and the field still had the next one, so a phone held
     upright saved three per cent. Without the words there is room for the mark
     and the field side by side, and that is a whole row - a fifth of what the
     header was taking. */
  html.is-stuck .topbar {
    flex-wrap: nowrap;
    padding-bottom: 8px;
  }
  html.is-stuck .topbar .topbar-find {
    flex-basis: auto;
    min-width: 0;
  }
}

/* The publisher's rating, beside the publisher's name and labelled, so it can
   never be read as the stars above - which are mine. */
.source-rating {
  margin-left: 10px;
  padding-left: 10px;
  border-left: 1px solid var(--line);
  white-space: nowrap;
}

.crumbs { color: var(--soft); font-weight: 400; }

/* ---------------------------------------------------------------- review ---
 *
 * Clustered review. Deliberately plain: this is a working surface, not part of
 * the collection, and the one thing it must do is let you see 78 near-identical
 * proposals as one row without hiding what is in them.
 */
.reviewbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding: 0.9rem 0 1.1rem;
}
.reviewbar-home {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  margin-right: 0.4rem;
}
.reviewbar-home.is-on { color: var(--charcoal); }

/* The laptop's practice copies - review, and the title editor - say so in
   the flame red, boxed, the way Tools' practice tools do (Gabe, 2026-09-27:
   "slap a big(ger) warning on any local tool that should only be used for
   practice"). */
.review-practice,
.titles-lab {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.95rem;
  color: var(--charcoal);
  background: color-mix(in srgb, var(--flame, #b8462f) 7%, transparent);
  border: 2px solid var(--flame, #b8462f);
  border-radius: 3px;
  padding: 0.75rem 0.9rem;
  margin: 0 0 1rem;
}
.review-practice strong,
.titles-lab strong { color: var(--flame, #b8462f); text-transform: uppercase; letter-spacing: 0.06em; }
.review-practice-reset {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.8rem;
  font-family: Helvetica, Arial, sans-serif; font-size: 0.82rem;
  color: var(--muted);
  margin: -0.5rem 0 1rem;
  padding-left: calc(0.8rem + 3px);
}

.review-blurb,
.review-empty,
.review-notice {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.82rem;
  color: var(--muted);
  margin: 0 0 1.1rem;
}
.review-notice {
  color: var(--charcoal);
  border-left: 2px solid var(--accent);
  padding-left: 0.7rem;
}
.review-failures {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8rem;
  border-left: 2px solid #a33;
  padding: 0.1rem 0 0.1rem 0.7rem;
  margin: 0 0 1.2rem;
}
.review-failures ul { margin: 0.3rem 0 0; padding-left: 1rem; color: var(--muted); }

.review-queues { list-style: none; margin: 0; padding: 0; }
.review-queues li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: 0.7rem 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
.review-queues a { font-size: 1.05rem; color: var(--charcoal); text-decoration: none; }
.review-queues a:hover { color: var(--accent); }
.review-count,
.review-hint,
.review-why {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.76rem;
  color: var(--muted);
}

/* The decided cuisines as an index. Each one is a target big enough to tap,
   and the count is what tells you where an outlier can hide: 172 American is
   a list to read, 3 Malaysian is not. */
.cuisine-groups { margin: 4px 0 0; }
.cg-head {
  font-family: var(--sans);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  margin: 22px 0 8px;
}
.cg-head:first-child { margin-top: 6px; }
.cg-one {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  margin: 0 8px 8px 0;
  padding: 7px 12px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--paper);
  text-decoration: none;
  color: var(--charcoal);
}
.cg-one:hover { border-color: var(--caribbean); }
.cg-name { font-weight: 600; font-size: 0.94rem; }
.cg-n { font-variant-numeric: tabular-nums; color: var(--muted); font-size: 0.84rem; }

.review-table { width: 100%; border-collapse: collapse; }
.review-table td {
  padding: 0.65rem 0.5rem;
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
  vertical-align: top;
}
.review-table td:first-child { padding-left: 0; }
.review-table td:last-child { padding-right: 0; }

.review-what a { color: var(--charcoal); text-decoration: none; }
.review-what a:hover { color: var(--accent); }
.review-what label { cursor: pointer; }
.review-why { display: block; margin-top: 0.15rem; }

/* Tabular figures and a right edge, so a column of counts reads as a column
   rather than as ragged text - the same reason the chip counts use them. */
.review-n {
  width: 7rem;
  text-align: right;
  white-space: nowrap;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.76rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.review-do { width: 13rem; text-align: right; white-space: nowrap; }
.review-pick { width: 1.6rem; padding-top: 0.8rem; }

.review-context {
  list-style: none;
  margin: 0.3rem 0 0;
  padding: 0;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.74rem;
  color: var(--muted);
}
.review-context li { padding: 0.05rem 0; }

.review-actions {
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.9rem 0;
  background: var(--cream);
  border-top: 1px solid rgba(0, 0, 0, 0.08);
}

/* Sticky, because a cluster of 78 is taller than the window and the buttons
   being at the bottom of it would mean scrolling past everything to act on
   what you just decided. */
.btn-approve,
.btn-dismiss {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.76rem;
  padding: 0.4rem 0.9rem;
  border-radius: 999px;
  border: 1px solid var(--charcoal);
  cursor: pointer;
}
.btn-approve { background: var(--charcoal); color: var(--cream); }
.btn-approve:hover { background: var(--accent); border-color: var(--accent); }
.btn-dismiss { background: transparent; color: var(--muted); border-color: rgba(0, 0, 0, 0.2); }
.btn-dismiss:hover { color: var(--charcoal); border-color: var(--charcoal); }
.topbar-waiting a { color: var(--charcoal); text-decoration: none; border-bottom: 1px solid rgba(0,0,0,0.2); }
.topbar-waiting a:hover { color: var(--accent); border-color: var(--accent); }
/* The labels now carry a count, so they vary in width and must not wrap. */
.btn-approve, .btn-dismiss { white-space: nowrap; }
/* The word takes whatever the fixed columns leave. Without this the actions'
   min-width won and the term column collapsed to 91px - the count was
   rendering all along, and the row was simply unreadable. */
.review-what { width: 100%; }
.review-n { width: 8rem; }
.review-do { width: 1%; white-space: nowrap; min-width: 15rem; }
.review-blurb code { font-size: 0.78rem; background: rgba(42,38,34,0.05); padding: 1px 4px; border-radius: 2px; }
.reviewtabs { display: flex; gap: 1rem; padding: 0 0 0.9rem; }
.reviewtabs a {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.74rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  padding-bottom: 3px;
  border-bottom: 2px solid transparent;
}
.reviewtabs a.is-on { color: var(--charcoal); border-bottom-color: var(--accent); }

/* Ambient status, at the foot of the page. Quiet by construction: it is only
   rendered when something is actually waiting, and when it is, it is the last
   thing on the page rather than a neighbour of the search button. */
.statusbar {
  max-width: var(--wide);
  margin: 3rem auto 0;
  padding: 0.9rem var(--gutter) 1.6rem;
  border-top: 1px solid rgba(42, 38, 34, 0.08);
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  color: var(--muted);
}
.statusbar a { color: var(--soft); text-decoration: none; border-bottom: 1px solid rgba(0,0,0,0.18); }
.statusbar a:hover { color: var(--accent); border-color: var(--accent); }
.statusbar-how { opacity: 0.75; }
.statusbar code { font-size: 0.7rem; }
.vocab-edit { display: flex; flex-direction: column; gap: 1.1rem; margin-bottom: 1.8rem; }
.vocab-form { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.vocab-form label {
  font-family: Helvetica, Arial, sans-serif; font-size: 0.72rem; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--muted); min-width: 5rem;
}
.vocab-form input[type=text], .vocab-form select, .vocab-inline select {
  font: inherit; font-size: 0.9rem; padding: 6px 10px;
  border: 1px solid var(--line); border-radius: 3px; background: #fff; color: var(--charcoal);
}
.vocab-form .review-hint { flex-basis: 100%; }
.vocab-inline { display: inline-flex; gap: 0.35rem; margin-left: 0.4rem; }
.vocab-inline select { font-size: 0.74rem; padding: 3px 6px; }
.vocab-h { font-size: 1rem; margin: 1.6rem 0 0.5rem; font-weight: 500; }
/* The over-match figure. Quiet when it is only information, and marked when
   the verb would refuse - the two need to look different at a glance. */
.review-risk { display: block; margin-top: 0.2rem; font-size: 0.7rem; color: var(--muted); }
.review-risk.is-risky { color: #a33; }
.browse-find { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem; }
.browse-find input[type=search] {
  font: inherit; font-size: 0.88rem; padding: 6px 10px; min-width: 16rem;
  border: 1px solid var(--line); border-radius: 3px; background: #fff;
}
.browse-heads { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 1.2rem; }
.browse-verbs { display: flex; align-items: center; gap: 0.35rem; justify-content: flex-end; flex-wrap: wrap; }
.browse-verbs input[type=text] {
  font: inherit; font-size: 0.78rem; padding: 4px 8px;
  border: 1px solid var(--line); border-radius: 3px; background: #fff;
}

/* Twirl-down evidence. Quiet when shut, because a list of thousands should
   read as a list of terms and not as a wall of ingredient lines. */
.peek { margin-top: 0.15rem; }
.peek > summary {
  display: inline-block;
  cursor: pointer;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  color: var(--muted);
  list-style: none;
}
.peek > summary::-webkit-details-marker { display: none; }
.peek > summary::before { content: '▸ '; }
.peek[open] > summary::before { content: '▾ '; }
.peek > summary:hover { color: var(--accent); }
.peek[open] > summary { color: var(--charcoal); }
.peek-more {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  color: var(--muted);
  margin: 0.3rem 0 0;
}
/* The panel a link points at, so arriving somewhere in a long list shows you
   where you landed rather than leaving you to find it. */
.peek:target > summary { color: var(--accent); }

/* The ordering guard. Loudness scales with what would be swallowed - "inside 4
   lines" and "inside 392" are not the same warning and must not look alike. */
.review-risk.is-strong { color: #a33; font-weight: 600; }
.review-risk.is-severe { color: #fff; background: #a33; padding: 1px 6px; border-radius: 2px; font-weight: 600; }

/* The refusal, under the word it is about. Deliberately not a toast: it names
   what would be taken and links to each one, so the fix is a click away. */
.blocked {
  margin: 0.6rem 0 0.2rem;
  padding: 0.7rem 0.9rem;
  border-left: 3px solid #a33;
  background: rgba(170, 51, 51, 0.05);
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.78rem;
  /* Wide enough for a word, its count and three verbs on one line. At 34rem
     the verbs wrapped and every row was twice the height it needed, which
     turned a twelve-row working list into a page of scrolling. */
  max-width: 52rem;
}
.blocked p { margin: 0 0 0.5rem; }
.blocked ul { margin: 0 0 0.7rem; padding-left: 1.1rem; color: var(--muted); }
.blocked a { color: var(--charcoal); }

/* Not styled like the ordinary buttons: getting past this should not look like
   the click you were already making. */
.btn-anyway {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.74rem;
  padding: 5px 12px;
  border-radius: 3px;
  border: 1px solid #a33;
  background: transparent;
  color: #a33;
  cursor: pointer;
}
.btn-anyway:hover { background: #a33; color: #fff; }

/* A dismissal is not a decision, so it is not shaped like one: the verdict is a
   button, skipping a suggestion is a link. Words alone were not carrying it -
   "reject" and "ignore" both sound like refusing. */
.btn-skip {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.76rem;
  padding: 0.4rem 0.2rem;
  border: none;
  background: none;
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  white-space: nowrap;
}
.btn-skip:hover { color: var(--charcoal); }

/* One line under a button, because there is no hover on a phone. */
.act { display: flex; flex-direction: column; gap: 0.2rem; }
.act-note {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  color: var(--muted);
  max-width: 30rem;
}
.review-actions { align-items: flex-start; gap: 1.6rem; }

/* The thing being decided, at the size of the thing being decided. */
.group-h {
  font-family: 'Vollkorn', Georgia, serif;
  font-size: 1.6rem;
  font-weight: 500;
  margin: 0.2rem 0 0.3rem;
  color: var(--charcoal);
}

/* The word and what it became, at the same size with the arrow between. */
.became { display: inline-flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.became-arrow { color: var(--muted); }
.became-to { color: var(--charcoal); }
.became-out { color: var(--muted); font-style: italic; }

/* A decided row that is a recipe. Reads as the label beside it until you are
   on it, because the list is for scanning and an underline on every line would
   be a page of rules. */
.became-link { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; }
.became-link:hover { color: var(--caribbean); border-bottom-color: currentColor; }

/* The refusal's working list. */
.blocked-list { width: 100%; border-collapse: collapse; margin-bottom: 0.5rem; }
.blocked-list td { padding: 0.3rem 0.4rem 0.3rem 0; vertical-align: middle; }
.blocked-list td:first-child { font-family: 'Vollkorn', Georgia, serif; font-size: 0.9rem; }
.blocked-n { color: var(--muted); white-space: nowrap; font-size: 0.72rem; }
.blocked-list .browse-verbs { justify-content: flex-start; flex-wrap: nowrap; }
.blocked-list .browse-verbs input[type=text] { width: 7rem; }

/* The first column is the tick, so the word is the second one. */
.blocked-list .pick-box { width: 1.4rem; padding-right: 0; }
.blocked-list td:first-child { font-family: inherit; font-size: inherit; width: auto; }
.blocked-list label {
  font-family: 'Vollkorn', Georgia, serif;
  font-size: 0.9rem;
  cursor: pointer;
}

/* Why a row arrived unticked, next to the row rather than in a legend - the
   decision is per word, so the reason has to be per word too. */
.pick-why {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.68rem;
  color: #a33;
  margin-left: 0.5rem;
  white-space: nowrap;
}
.pick-compound label, .pick-unclear label { color: var(--muted); }

.batch-do {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  flex-wrap: wrap;
  margin: 0.6rem 0 0.8rem;
  padding-top: 0.6rem;
  border-top: 1px solid rgba(170, 51, 51, 0.2);
}
.batch-do input[type=text] { width: 9rem; }

/* The way into the batch panel. Shaped as a control rather than as one more
   word in the sentence above it, because it is the thing to do about that
   sentence and was previously reachable only by being refused. */
.review-together {
  display: inline-block;
  margin-top: 0.35rem;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--charcoal);
  border-bottom: 1px solid rgba(0, 0, 0, 0.25);
  text-decoration: none;
}
.review-together:hover { border-bottom-color: var(--charcoal); }
/* Buttons size to their label; the note under them is what may wrap. */
.act { align-items: flex-start; }
.act > button { align-self: flex-start; }
/* Landing on a panel should show its heading, not sit it against the top edge. */
.blocked { scroll-margin-top: 2rem; }
.peek { scroll-margin-top: 2rem; }
/* A ruled-out word, said by striking it rather than by labelling it. */
.struck { text-decoration: line-through; text-decoration-thickness: 1px; color: var(--muted); }
.peek-all { display: flex; gap: 1rem; margin: 0 0 0.6rem; }
.review-makes { color: #8a5a2b; display: block; }
.review-makes-note {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8rem;
  border-left: 3px solid #8a5a2b;
  padding: 0.5rem 0.8rem;
  margin: 0 0 1.2rem;
  background: rgba(138, 90, 43, 0.06);
  max-width: 44rem;
}
.btn-approve[disabled] { opacity: 0.4; cursor: not-allowed; }

/* ------------------------------------------------------- review nav ------- */
/* The bar is where you are; the tabs belong to the page under its heading.
   Previously both were rows of similar chips and nothing said which was which. */
.reviewbar { padding-bottom: 0.5rem; }
/* One row that scrolls sideways, below a desktop's width. The one pass took
   the bar from three queues to twelve, and wrapped they were four rows - over
   half of a landscape phone's 390px before the page began. The review index
   lists every queue with its count, so the bar is the way across, not the
   only way in; review.php keeps the open queue's chip in view. */
@media (max-width: 1100px) {
  .reviewbar {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .reviewbar::-webkit-scrollbar { display: none; }
  .reviewbar > * { flex: none; }
}
.reviewbar-sep {
  width: 1px;
  height: 1.1rem;
  background: rgba(42, 38, 34, 0.15);
  margin: 0 0.35rem;
}
.review-h1 {
  font-family: 'Vollkorn', Georgia, serif;
  font-size: 1.5rem;
  font-weight: 500;
  color: var(--charcoal);
  margin: 0.7rem 0 0.1rem;
}
/* Indented under the heading it belongs to, so the nesting is visible rather
   than implied. */
.reviewtabs { padding: 0 0 1rem 0.1rem; gap: 1.1rem; }

/* Outlined, for the safe half of a pair. Red is for destructive, and skipping
   a suggestion destroys nothing. */
.btn-outline {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.76rem;
  padding: 0.4rem 0.9rem;
  border-radius: 999px;
  border: 1px solid var(--charcoal);
  background: transparent;
  color: var(--charcoal);
  cursor: pointer;
  white-space: nowrap;
}
.btn-outline:hover { border-color: var(--accent); color: var(--accent); }

/* Under the main button on the group list, not beside it. */
.review-do .btn-skip { padding-top: 0.15rem; }
.group-reason {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.82rem;
  color: var(--muted);
  margin: -0.1rem 0 0.9rem;
}
.review-notice { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.review-notice p { margin: 0; }

/* ------------------------------------------------------------- grid ------
 * A group as photographs (templates/review-grid.php). Every card starts in;
 * a tap takes it out, greys it and says so across the photo; a card that is
 * out shows where it can go. The state is the checkbox itself, read by
 * :has(), so none of this needs a script. Columns by width: 5 on a phone
 * held sideways, 6 on a tablet, 8 on a desktop. */
a.btn-approve { display: inline-block; text-decoration: none; }
.grid-next {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.9rem;
  color: var(--charcoal);
  display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  margin: 0 0 1.1rem;
}
.grid-notice .grid-stop {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8rem;
  color: var(--muted);
}
.grid {
  --cols: 3;
  list-style: none;
  margin: 0.4rem 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  gap: 16px 12px;
}
@media (min-width: 640px)  { .grid { --cols: 5; } }
@media (min-width: 900px)  { .grid { --cols: 6; } }
@media (min-width: 1150px) { .grid { --cols: 8; } }
.gcard { position: relative; display: flex; flex-direction: column; min-width: 0; }
.gcard-tap { display: flex; flex-direction: column; gap: 5px; cursor: pointer; }
.gcard-in {
  position: absolute; top: 6px; left: 6px; z-index: 2;
  width: 18px; height: 18px; margin: 0;
  accent-color: var(--charcoal);
}
.gcard-photo {
  position: relative; display: block;
  aspect-ratio: 4 / 3; overflow: hidden;
  background: var(--line); border-radius: 4px;
}
.gcard-photo img { display: block; width: 100%; height: 100%; object-fit: cover; }
.gcard.is-look .gcard-photo { box-shadow: 0 0 0 2px var(--accent); }
.gcard-title {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8rem; font-weight: 600; line-height: 1.25;
  color: var(--charcoal);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.gcard-why {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem; color: var(--accent);
}
/* A tag's values under a recipe on Review, each a toggle that saves on the
   tap (punch 190): on is red text on a red rule, as edit mode's chips; a
   suggestion still waiting is dashed. "Saved." floats under the row rather
   than taking a line, so nothing below it moves. */
.tg {
  position: relative;
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
  margin-top: 6px;
  font-family: Helvetica, Arial, sans-serif;
}
.tg-also { font-size: 0.72rem; color: var(--soft); margin-right: 2px; }
.tg-b {
  padding: 3px 7px;
  border: 1px solid var(--line); border-radius: 0;
  background: none; color: var(--charcoal);
  font: 400 0.75rem/1.2 Helvetica, Arial, sans-serif;
  cursor: pointer;
}
@media (pointer: coarse) { .tg-b { padding: 6px 9px; } }
.tg-b.is-waiting { border-style: dashed; border-color: var(--muted); }
.tg-b.is-on { color: var(--flame); border-color: var(--flame); font-weight: 700; }
.tg-b:disabled { color: var(--muted); opacity: 0.45; cursor: default; }
.tg-b:focus-visible { outline: 2px solid var(--flame); outline-offset: 2px; }
.tg[aria-busy="true"] .tg-b { cursor: progress; }
@media (hover: hover) {
  .tg-b:hover:not(:disabled) { border-color: var(--charcoal); }
  .tg-b.is-on:hover { border-color: var(--flame); }
}
.tg-said {
  position: absolute; left: 0; top: 100%; z-index: 3;
  margin-top: 2px; padding: 1px 5px;
  background: var(--paper);
  font-size: 0.72rem; font-weight: 700; color: var(--soft);
  white-space: nowrap; pointer-events: none;
}
.tg-said:empty { display: none; }
.tg-said.is-err { color: var(--flame); white-space: normal; min-width: 12em; }
.review-what .tg { margin-top: 4px; }

/* The No meal list: a recipe, and the meals to tap under it, one line each
   at every width rather than a squeezed second column. */
.review-none, .review-none tbody, .review-none tr, .review-none td { display: block; }
.review-none tr { padding: 8px 0; border-bottom: 1px solid var(--line); }
.review-none td { padding: 0; border: 0; }
.review-none .none-was { display: block; font-size: 0.8rem; color: var(--soft); }
.gcard-detail {
  display: block;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.75rem; font-weight: 700; line-height: 1.25;
  color: var(--charcoal);
}
@supports selector(:has(a)) {
  .gcard:has(.gcard-in:not(:checked)) .gcard-detail { color: var(--muted); text-decoration: line-through; }
}
.gcard-not {
  display: none;
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 4px 6px;
  background: rgba(30, 27, 24, 0.78); color: #fff;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem; font-weight: 700; text-align: center;
}
.gcard-open {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  width: 24px; height: 24px; display: grid; place-items: center;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.92); color: var(--charcoal);
  text-decoration: none; font-size: 0.8rem; line-height: 1;
}
.gcard-open:hover { color: var(--accent); }
.gcard-move {
  margin-top: 5px; width: 100%; max-width: 100%;
  font-family: Helvetica, Arial, sans-serif; font-size: 0.75rem;
}
/* Where :has() is understood the menu waits until the card is out, and the
   card says it is out. Elsewhere the menu is always there and the tick box
   says it - the same decisions, less pretty. Hidden, not gone: its room is
   kept, so taking a card out never pushes the cards below it down. */
@supports selector(:has(a)) {
  .gcard-move { visibility: hidden; }
  .gcard:has(.gcard-in:not(:checked)) .gcard-move { visibility: visible; }
  .gcard:has(.gcard-in:not(:checked)) .gcard-photo img { filter: grayscale(1); opacity: 0.35; }
  .gcard:has(.gcard-in:not(:checked)) .gcard-not { display: block; }
  .gcard:has(.gcard-in:not(:checked)) .gcard-title { color: var(--muted); text-decoration: line-through; }
}
.grid-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 0;
  font-family: Helvetica, Arial, sans-serif; font-size: 0.85rem; color: var(--charcoal);
}
/* Pinned to the foot where there is height to spare. A phone held sideways
   has 390px, so there the bar sits at the top and the bottom of the batch
   and scrolls with it. */
@media (min-height: 560px) {
  .grid-bar-foot {
    position: sticky; bottom: 0; z-index: 3;
    background: var(--cream);
    border-top: 1px solid var(--line);
  }
}
.grid-rest { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin: 1.4rem 0 0; }

/* A group just acted on. Greyed in place and cleared by the next page load,
   never by a timer - rows sliding away under the cursor is what makes acting
   on several in a row unpleasant. */
.is-done > td { opacity: 0.55; }
.is-done .review-what { text-decoration: line-through; text-decoration-color: rgba(42,38,34,0.3); }
.is-done .review-what .review-why { text-decoration: none; }
.done-mark {
  display: block;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.74rem;
  color: var(--charcoal);
  opacity: 1;
}
.done-undo { display: inline-block; }
.context-n { color: rgba(138, 130, 112, 0.7); }
.review-risk a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.peek-all { margin: 0.2rem 0 0.8rem; }
/* The legend quotes the style inline, so it must not take a line of its own the
   way the real notes do. */
.review-blurb .review-risk { display: inline; margin: 0; }
.statusbar { display: flex; align-items: baseline; gap: 0.9rem; flex-wrap: wrap; }
.statusbar > a { font-weight: 600; letter-spacing: 0.04em; }
.statusbar-waiting { color: var(--muted); }
.alsocalled {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8rem;
  color: var(--muted);
  margin: 0.2rem 0 0;
  padding: 0 var(--gutter);
}

/* ------------------------------------------------------------- cook mode ---
   One step, as large as the room needs. This is the across-the-kitchen view:
   a laptop or an iPad on the counter, read from six feet away with your hands
   full. Everything here is sized for that distance, which is why it is the one
   part of the app that ignores the measure the rest of it keeps to. */

.cookview {
  /* Overridden by the slider, remembered per device. This default is already
     large - the slider goes from large to really large, not from small. */
  --cook-size: 3rem;

  /* Full width. A 640px column read from across a kitchen is a column of
     wasted wall, and the whole argument for this view is that the text is as
     big as the space allows. */
  max-width: none;
  margin: 0;
  padding: 20px clamp(16px, 4vw, 56px) 60px;
}

/* Not sticky. It was for an hour, to keep the way out on screen, and it cost
   61px - 16% of a phone on its side, in the view whose whole job is one big
   step. The way out and the text size are on the step's own line now
   (`.cookview-bar`), which is what stays; this row is the name and the grain,
   things set once before cooking. */
.cookview-top {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  /* No rule here. The name carries its own and the two of them 19px apart
     read as a mistake. */
  padding-bottom: 14px;
}

/* Where the masthead was: small, and out of the way of the step.

   The name is text, not a link. It was the way back until 2026-09-21 - arrow
   on it, rule under it saying so - and the way back is `.cookview-close` now,
   because the editorial drawing wants the word CLOSE in the corner and a
   theme cannot rewrite an element's words. The rule stays: it is what keeps
   the name from reading as a caption to the step under it. */
.cookview-title {
  display: inline-flex;
  align-items: baseline;
  gap: 7px;
  margin: 0;
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--charcoal);
}
.cookview-name {
  border-bottom: 1px solid var(--line);
  padding-bottom: 2px;
}

.cookview-tools { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; }

.cook-views { display: flex; gap: 14px; font-family: Helvetica, Arial, sans-serif; font-size: 0.8rem; }
.cook-views a { color: var(--muted); text-decoration: none; }
.cook-views a.on { color: var(--charcoal); border-bottom: 1px solid var(--accent); }

/* Two A's and two carets. No number and no percentage: the setting has no
   units anyone thinks in, and the only test that matters is whether you can
   read it from where you are standing.

   A stepper since 2026-09-21, and it was a slider from 2 to 7 in quarters. A
   drag is the wrong gesture with floury hands and nobody needed 21 positions
   to answer one question. The stops are in cook.php, which is where the size
   is applied. */
.cookview-size { display: flex; align-items: center; gap: 6px; }
.size-step {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  /* 34px is the smallest a wet finger finds reliably, and it is the number
     the drawing's own servings stepper uses. */
  min-width: 34px;
  height: 34px;
  padding: 0 7px;
  background: none;
  border: 1px solid var(--line);
  border-radius: 5px;
  color: var(--muted);
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.86rem;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
}
.size-step:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }

/* The end of the range says so. A control that still looks pressable and does
   nothing is the same bug as a step that will not advance - you press it
   again, harder, with a wet hand. */
.size-step:disabled { opacity: 0.35; cursor: default; }

/* Fill the screen (punch 115): four corners, and red while it is on - a mode
   that stays, so it shows it has stayed. */
.size-fill svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: square;
}
.size-fill[aria-pressed="true"],
.size-fill[aria-pressed="true"]:hover:not(:disabled) {
  background: var(--flame);
  border-color: var(--flame);
  color: #fff;
}

.size-caret {
  width: 9px;
  height: 6px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The way out. Quiet in this theme - it is the one control here you press
   deliberately rather than while cooking, so it does not compete with the
   grain or the size. */
.cookview-close {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: none;
  height: 34px;
  color: var(--muted);
  text-decoration: none;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.cookview-close:hover { color: var(--accent); }
.cookview-x {
  width: 11px;
  height: 11px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
}

.cook-note {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.82rem;
  color: var(--soft);
  border-left: 2px solid var(--line);
  padding-left: 12px;
}

/* **The step's line: where you are, then bigger, smaller and Close.** It
   stays at the top of the window while a long step scrolls under it - the
   one thing here that is sticky, and it earns it by carrying the way out.
   Gabe, 2026-09-27: a floating box for these "always obscures content. need
   it on the page, inline with step number and name, on the right." It has
   the page's own ground, so words passing under it go under, not through. */
.cookview-bar {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px 18px;
  margin: 0 0 0.5em;
  padding: 8px 0;
  background: var(--cream);
}
.cookview-bar > .cookview-where { margin: 0; min-width: 0; }
.cookview-controls { display: flex; align-items: center; flex: none; gap: 6px; }
/* A hairline between the two A's and Close, so the way out reads as its own
   thing rather than a third size. */
.cookview-rule { width: 1px; align-self: stretch; margin: 3px 8px; background: var(--line); }

/* Where you are, in the smallest thing on the page. It is a label, not the
   content: you glance at it once on arriving at a step and never again. */
.cookview-where {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 0.5em;
}

.cookview-now { margin: 0; min-width: 0; }

/* The whole point of the view. */
.cook-body {
  /* `--cook-fill` only while the fill button is on (cook.php): the step's
     own words at the size that fills the screen, Have Ready still on
     `--cook-size`. */
  font-size: var(--cook-fill, var(--cook-size));
  line-height: 1.22;
  margin: 0;
  /* Long words break rather than push the line off the screen - at this size a
     single overflow costs you the end of a sentence. */
  overflow-wrap: break-word;
}

/* Have Ready, at reading size.
   
   It is the one thing on this page you act on rather than read, and it was set
   at list size at the bottom of the page - so the only part you walk back to
   the counter for was the only part you had to walk back to the counter to
   read. It is now a proportion of the current step like everything else here,
   so the slider moves it too. */
.cookview-ready { margin: 1.1em 0 0; }
.cookview-ready .mise { margin: 0 0 0.7em; }
.cookview-ready .mise h2 {
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  padding-top: 0.9em;
}
.cookview-ready .mise ul { gap: 0.34em; }
.cookview-ready .mise li {
  font-size: calc(var(--cook-size) * 0.55);
  line-height: 1.28;
}
.cookview-ready .mise .from { font-size: calc(var(--cook-size) * 0.3); }
/* The box grows with the text it belongs to, or it stops being the thing your
   eye lands on and starts being a speck beside a headline. */
.cookview-ready .gather > input[type="checkbox"] {
  width: calc(var(--cook-size) * 0.4);
  height: calc(var(--cook-size) * 0.4);
}
/* **Standing on the line, as tall as its capitals.** The page-wide offset
   below centres a 15px box in the line; this box is 0.4 of the step size, so
   at the big sizes it sat a quarter of the text's height low - its top by
   the lowercase, its foot past the baseline. Gabe, 2026-09-30, with a
   picture: "Focus mode misalignment". Baseline alignment puts a checkbox's
   foot on the first line's baseline, and 0.7em is the face's cap height, so
   the box runs from the capitals' top to their foot at every size. */
.cookview-ready .gather { align-items: baseline; }
.cookview-ready .gather > input[type="checkbox"] {
  width: 0.7em;
  height: 0.7em;
  top: 0;
}

/* **Have ready folds away** (cook.php, `.mise-twirl`). The heading is the
   handle, with a chevron that points down while open. The summary is only
   as wide as its words, so a tap beside it still walks to the next step. */
.mise-twirl > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  list-style: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.mise-twirl > summary::-webkit-details-marker { display: none; }
/* The rule above the list is the section's, not the heading's: the summary
   is only as wide as its words, and a rule on the heading would be too. */
.cookview-ready .mise-twirl { border-top: 1px solid var(--line); padding-top: 0.9em; }
/* And the line under the heading moves to the top of the list, so it still
   runs the full width while open and goes with the list when closed. */
.cookview-ready .mise-twirl > summary h2 { margin: 0; border-top: 0; border-bottom: 0; padding-top: 0; }
.cookview-ready .mise-twirl > ul { border-top: 1px solid var(--line); padding-top: 10px; }
.mise-twirl-mark {
  flex: none;
  width: 0.9rem;
  height: 0.9rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.5;
  stroke-linecap: square;
  color: var(--muted);
  transform: rotate(90deg);
}
.mise-twirl:not([open]) .mise-twirl-mark { transform: none; }
.mise-twirl > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

/* **Closed, it waits below the fold.** Gabe, 2026-10-01: a closed Have
   ready still spent its heading line and the air under it on every step, and
   that room is the gain - "go on Have Ready". So once it is closed, the steps
   after it open with the step a screen tall and the heading just under the
   fold, one scroll away. `.ready-below` is set from the remembered state when
   a step arrives, not on the tap that closes it: the line you just tapped
   stays under your finger until you walk on. svh, the window with Safari's
   bars showing, which is the window a step lands in. A step longer than the
   screen is unchanged - the heading follows its last line as before. */
.cookview.ready-below [data-swap="step"]:has(.mise-twirl) .cookview-now {
  min-height: 100vh;
  min-height: 100svh;
}

/* Fill the screen has no Next preview: the step takes the room. */
.cookview.is-filling .cookview-next { display: none; }

/* A shade under Have Ready: still readable across the room, and quieter than
   the two things you are actually working from.

   **And right-justified behind a chevron, because it is a door rather than a
   paragraph.** From the handoff of 2026-09-24. Gabe: "having the next step as
   it is now kept leading me to read it and do two chunks at a time." Left
   aligned and directly under the step, the preview joins the column of things
   you are reading now; pushed to the right edge with an arrow beside it, the
   same words read as what happens when you press it.

   The type sizes are the ones already here - the handoff says keep them, and
   in this app "the existing size" is `--cook-size`, which the stepper writes
   and the cook owns. A fixed 20px would be the mock's width, not the
   kitchen's. */
.cookview-next {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 28px;
  margin: 1.1em 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  color: var(--soft);
  text-align: right;
  text-decoration: none;
}
/* Capped, so a long preview sets in a column rather than running the full
   width of a worktop-sized screen. */
.cookview-next-say { min-width: 0; max-width: 900px; }

.cookview-next p:last-child {
  font-size: calc(var(--cook-size) * 0.5);
  line-height: 1.42;
  margin: 0;
  overflow-wrap: break-word;
}
/* **Two lines at most.** A long next step - a recipe's "Do ahead"
   paragraph - stood half a screen tall or more, pinned to the window's foot
   over the step it follows (Gabe, 2026-10-08, Old-School Dinner Rolls). The
   preview is a door: its first words say where it goes, and the step is one
   tap away, whole. */
.cookview-next p:last-child {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

/* The whole control lights up, label included - it is one target and it says
   so by moving as one. */
.cookview-next:hover,
.cookview-next:hover .cookview-where { color: var(--charcoal); }

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

/* **Sized off `--cook-size`, not in pixels.** The handoff draws it 44 by 88
   beside 20px text, which is a chevron four times the height of the words it
   stands next to - that proportion is the drawing, and a fixed 88px would be
   right at one viewport and wrong at every other. The stroke rides the
   viewBox, so it thickens with it and stays the 5 units it was drawn at. */
.cookview-next-arrow {
  flex: none;
  width: calc(var(--cook-size) * 0.9);
  height: calc(var(--cook-size) * 1.8);
  fill: none;
  stroke: var(--accent);
  stroke-width: 5;
  stroke-linecap: square;
  stroke-linejoin: miter;
}

/* The way out, down both edges of the screen.
   
   Fixed to the viewport rather than sized to the step. This is a target you hit
   while leaning across a worktop with flour on your hands, and the whole point
   is that you do not have to aim: a rail down the side of the screen is aimed at
   by putting your hand somewhere on the right.
   
   Fixed rather than a tall grid cell, because a full-height *cell* makes the
   row a screenful and pushes Have Ready and the preview below the fold - which
   undoes the thing they were just moved up for. This way the bars are always
   there whatever you have scrolled to, and the reading column is padded out of
   their way rather than run underneath them. */
.cookview {
  padding-left: 128px;
  padding-right: 128px;
}

.cookview-go {
  position: fixed;
  top: 0;
  bottom: 0;
  width: 108px;
  z-index: 3;

  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  color: var(--accent);
  text-decoration: none;
  font-family: Helvetica, Arial, sans-serif;
}
.cookview-go.prev { left: 0; border-left: none; border-radius: 0 7px 7px 0; }
.cookview-go.next { right: 0; border-right: none; border-radius: 7px 0 0 7px; }
.cookview-go.next { background: var(--accent); border-color: var(--accent); color: var(--cream); }

/*
 * The bars go under the notch; their labels do not.
 *
 * Holding the whole column off the inset left a cream margin down the side of
 * a notched phone - the page stopped short of the glass and the black bezel
 * showed through, which reads as a bug rather than as a safe area.
 *
 * So each bar starts at the physical edge and grows by its own inset, and the
 * word inside is pushed clear by the same amount. The result reaches the glass
 * and the label still misses the camera.
 *
 * Both sides carry their own inset rather than one side being special, because
 * a phone turned the other way puts the notch on the right - and a rule that
 * only knew about the left would be right half the time.
 */
.cookview-go.prev {
  left: 0;
  width: calc(108px + var(--sa-l, env(safe-area-inset-left)));
  padding-left: var(--sa-l, env(safe-area-inset-left));
}
.cookview-go.next {
  right: 0;
  width: calc(108px + var(--sa-r, env(safe-area-inset-right)));
  padding-right: var(--sa-r, env(safe-area-inset-right));
}
.cookview-go:hover { border-color: var(--accent); }

.cookview-go span {
  /* Turned, so the bar can stay narrow and still say a whole word. Both read
     top to bottom: two directions would be a puzzle, not a symmetry. */
  writing-mode: vertical-rl;
  font-size: 1.15rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

/* Step one has no previous, and with the bars out of the flow there is nothing
   to hold open - the reading column keeps its position either way. */
.cookview-go.is-off { display: none; }

/* The arm's-length half of the page. Back to a readable measure, because this
   is read with the tablet in your hands rather than across the room. */
.cookview-hand {
  max-width: 640px;
  margin: 48px 0 0;
  padding-top: 24px;
  border-top: 1px solid var(--line);
}

.mise { margin: 0 0 26px; }
.mise h2 {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 10px;
  border-top: 1px solid var(--line);
  padding-top: 12px;
}
.mise ul { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 7px; }
.mise li { font-size: 1.02rem; line-height: 1.4; padding-left: 15px; position: relative; }
.mise li::before {
  content: ""; position: absolute; left: 0; top: 0.62em;
  width: 6px; height: 1px; background: var(--muted);
}
/* No dash where there is a checkbox. The box is the list marker now, and a
   dash beside it is a second bullet for the same line. */
.mise li:has(> .gather) { padding-left: 0; }
.mise li:has(> .gather)::before { display: none; }
.mise li.optional { color: var(--muted); }

/* Something the recipe made, rather than something you bought. Marked, because
   the difference is what tells you whether to reach for a bag or a bowl. */
.mise li.made { color: var(--accent); }
.mise li.made::before { background: var(--accent); }
.mise .from {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.7rem;
  color: var(--muted);
  text-decoration: none;
  margin-left: 7px;
  border-bottom: 1px solid var(--line);
}

.produces { margin: 0 0 26px; font-family: Helvetica, Arial, sans-serif; font-size: 0.8rem; }
.produces > summary { color: var(--muted); cursor: pointer; }
.produces > summary b { color: var(--soft); font-weight: 600; }
.produces form { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0 8px; }
.produces input[type=text] {
  flex: 1 1 14rem;
  font-family: 'Vollkorn', Georgia, serif;
  font-size: 0.95rem;
  padding: 6px 9px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: #fff;
  color: var(--charcoal);
}
.produces button {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.78rem;
  padding: 6px 14px;
  border-radius: 3px;
  border: 1px solid var(--accent);
  background: var(--accent);
  color: var(--cream);
  cursor: pointer;
}
.produces .btn-clear { background: transparent; color: var(--muted); border-color: var(--line); }
.produces .hint { color: var(--muted); margin: 0; }


/* A duration in the step text, made startable. Untouched it looks like the
   words it replaced; the controls only exist once it is running. */
.timer-set {
  /* The clock and its controls are one object and must never be split across a
     line break in the middle of a sentence. */
  white-space: nowrap;
  position: relative;
}
.timer {
  font: inherit;
  color: inherit;
  background: transparent;
  border: none;
  /* Scales with the text. A 1px dashed rule is a clear affordance at body size
     and invisible under 96px type in cook mode, which is what made a duration
     up there read as ordinary prose rather than as something you can press. */
  border-bottom: max(1px, 0.045em) dashed var(--muted);
  padding: 0;
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}

/* Colour carries the state, so it reads from across the kitchen without
   arithmetic. Running is a filled chip that changes colour as the time goes. */
.timer-set.is-running .timer,
.timer-set.is-paused  .timer {
  border-bottom: none;
  border-radius: 3px;
  padding: 1px 7px;
  font-weight: 600;
}
.timer-set.is-running .timer { color: #fff; background: #3f6b45; }
.timer-set.is-running.is-soon  .timer { background: #b2701c; }
.timer-set.is-running.is-close .timer { background: #a33; }

/*
 * The controls take up no room in the sentence at all.
 *
 * They were laid out in the line, which meant the chip changed width whenever
 * it changed state - and a chip that changes width mid-paragraph reflows the
 * text after it. On a pointer that was a loop: revealing the buttons widened
 * the chip, which pushed it to the next line, which moved it out from under
 * the pointer, which hid them again. Reserving the width instead stopped the
 * loop and cost a permanent gap mid-sentence, and made a finished chip wide
 * enough to wrap on its own with the full stop stranded after it.
 *
 * So they are lifted out of flow entirely. The chip is exactly as wide as the
 * clock whether it is running, paused, finished or hovered; nothing after it
 * ever moves; and the buttons sit over the following word or two only while
 * they are on screen, which is a place you are already looking.
 */
.t-ctl {
  position: absolute;
  left: 100%;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  z-index: 2;
}

.t-btn {
  /* Not the body serif. Its oldstyle "1" has a serif top and foot, so "+1" set
     in it reads as "+I" at chip size - which is the one thing a control that
     adds a minute must not do. */
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8em;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1.5;
  cursor: pointer;
  border: none;
  padding: 1px 7px;
  color: var(--charcoal);
  /* Opaque, because it is standing on top of a sentence. Plain in every state
     and never flashing: the colour on the clock is status, and a control that
     borrows it is claiming to be status too. */
  background: var(--cream);
  box-shadow: inset 0 0 0 1px var(--muted);
}
/* The sentence's own punctuation, kept inside the chip so it cannot be widowed
   on the next line. Not part of the control: it must look like the text it came
   from, whatever the chip is doing. */
.t-dot {
  font-style: normal;
  color: var(--charcoal);
}

.t-btn:first-child { border-radius: 3px 0 0 3px; }
.t-btn:last-child  { border-radius: 0 3px 3px 0; }

/* On a pointer device the add-time controls wait for the pointer; on a
   touchscreen they are always there, because revealing them would cost the tap
   that was meant to add the minute. `hover` is the question actually being
   asked - screen width says nothing about the input, and a laptop in a kitchen
   is still a laptop. Resume, dismiss and the overrun ✕ are never hidden: a
   stopped timer with no visible controls is one you have to remember how to
   work. Hiding is now free either way, because none of it is in the flow. */
@media (hover: hover) and (pointer: fine) {
  /* :not(.is-over), because a timer past zero carries both classes - it is
     still running, it is just running upwards - and hiding its +1 and +5
     behind a hover left the ✕ standing alone with a gap beside it where two
     invisible buttons were still holding their space. A timer that has gone
     off is the one moment nothing about it should need finding. */
  .timer-set.is-running:not(.is-over) .t-add {
    visibility: hidden;
    opacity: 0;
    transition: opacity 0.12s ease-out;
  }
  .timer-set.is-running:not(.is-over):hover .t-add,
  .timer-set.is-running:not(.is-over):focus-within .t-add { visibility: visible; opacity: 1; }
}

/* Paused drops the fill rather than changing its colour. Hollow against solid
   reads at arm's length; two greens do not, and nobody is going to study a
   chip in the middle of a sentence while something is in the pan. */
.timer-set.is-paused .timer {
  color: var(--charcoal);
  background: transparent;
  opacity: 0.75;
  box-shadow: inset 0 0 0 1px var(--muted);
}

/* Past zero it counts up rather than stopping. A timer that quietly ends is one
   you find out about from the smell, so this keeps asking until it is tapped.
   The clock flashes; the controls beside it do not. */
.timer-set.is-running.is-over .timer {
  background: #a33;
  animation: timer-over 1s steps(1, end) infinite;
}
@keyframes timer-over {
  50% { background: var(--cream); color: #a33; box-shadow: inset 0 0 0 1px #a33; }
}
@media (prefers-reduced-motion: reduce) {
  /* No flashing, but it must still not look like a running timer. */
  .timer-set.is-running.is-over .timer {
    animation: none;
    box-shadow: inset 0 0 0 2px var(--charcoal);
  }
}

/* Cook mode is the exception, and for the reason the overlay exists elsewhere.
   Nothing hides there - that view is worked from six feet away, where a control
   you have to find with a pointer first is a control you do not have - so
   nothing can reflow by appearing, and putting them back in the line means they
   never stand on top of 96px type. */
.cookview .t-ctl {
  position: static;
  transform: none;
  display: inline-flex;
  vertical-align: baseline;
}
.cookview .timer-set.is-running .timer,
.cookview .timer-set.is-paused .timer { border-radius: 3px 0 0 3px; }
.cookview .timer-set.is-running .t-add { visibility: visible; opacity: 1; }


/* A cook holding a tablet at arm's length rather than propped across the room.
   The slider still works; this is only where it starts from. */
@media (max-width: 620px) {
  .cookview { --cook-size: 2rem; padding-left: 10px; padding-right: 10px; }
  /* Narrower bars, not no bars: on a phone the screen is small enough that a
     control at the edge is still under your thumb. */
  .cookview { padding-left: 62px; padding-right: 62px; }
  .cookview-go.prev { width: calc(54px + var(--sa-l, env(safe-area-inset-left))); }
  .cookview-go.next { width: calc(54px + var(--sa-r, env(safe-area-inset-right))); }
  .cookview-go span { font-size: 0.92rem; letter-spacing: 0.14em; }
}

/* Named for a screen reader, invisible on screen. The slider is two letter A's
   to a sighted reader, which is nothing to a spoken one. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* ------------------------------------------------- gathering, and opening ---
   A checkbox that is only a checkbox: it holds its own state, so nothing is
   stored and nothing is in the URL. Gathered-ness lasts as long as the page is
   open, which is as long as it means anything. No script involved. */

.gather {
  display: flex;
  /* Not `baseline`. A checkbox has no text baseline of its own, so the browser
     aligns its bottom edge to the first line's baseline and the box floats
     above the words. `flex-start` plus a nudge sits it against the cap height,
     which is where the eye expects it. */
  align-items: flex-start;
  gap: 9px;
  cursor: pointer;
}
.gather input[type=checkbox] {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
  /* Inherited so that `1em` in the offset below means the TEXT's em.
     A form control does not inherit font by default: this box was rendering at
     the browser's 13.3px while its label read at 16.3px, so the offset was
     computed against the wrong size and every checkbox sat 2.1px high. Width
     and height are set explicitly above, so the size does not follow the font. */
  font-size: inherit;
  /* No padding trick here. A native checkbox ignores padding, so the `padding:
     8px; margin: -8px` that used to sit here enlarged nothing and pulled the
     box eight pixels up - which was the whole of the alignment complaint.
     The tap target is the <label>, which wraps the text and is already the
     width of the line. */
}
/* Struck in red, the text still grey.

   The rule is the thing that changed - Gabe, 2026-09-22, cooking, with a
   picture of Have Ready: "how feasible is it to have steps like these strike
   out red as they're checked off?" Colouring the *words* red would say wrong;
   colouring the line that crosses them says dealt with, and at arm's length in
   a kitchen it is the only part you can see at all. 2px because a hairline in
   an accent colour disappears at that distance.

   `--flame` in both designs, where most of this sheet's accents are the teal:
   red is what was asked for and it is the one mark on the page that means "you
   already did this one". */
.gather input[type=checkbox]:checked + span {
  color: var(--muted);
  text-decoration: line-through;
  text-decoration-color: var(--flame);
  text-decoration-thickness: 2px;
}
.gather input[type=checkbox]:checked + span a { color: var(--muted); }
/* In containers, the strike is the tape's color (Gabe, 2026-10-04), so a
   checked line still says which dish it belongs to. White tape strikes in
   black; nothing shows white on the page. */
ul.is-taped .gather input[type=checkbox]:checked + span { text-decoration-color: var(--tape-strike, var(--tape)); text-decoration-thickness: 3px; }

/* Centred on the FIRST line of the label, not on the block. A two-line
   ingredient keeps its box beside the line the eye starts on, rather than
   floating halfway down a tall row. Half the leading less half the box is
   exactly that position - and it is only correct because the input inherits
   font-size above, so `1em` is the text's em and not the control's. */
.gather input[type=checkbox] { position: relative; top: calc((1em * var(--lh, 1.5) - 15px) / 2); }
.mise .gather input[type=checkbox] { --lh: 1.4; }
.ingredients .gather input[type=checkbox] { --lh: 1.5; }

/* A step, openable in place. Collapsed it reads exactly as it did before. */
/* The heading on the left, the grain on the right, one rule under both.
 *
 * The rule is on the row rather than on the h2, which is the whole of the fix:
 * every other h2 on the page is a block and rules the full width of its
 * column, and this one is a flex item, so it shrank to its own text and ruled
 * about a third of the way across. Mise en Place and Steps now start the same
 * way, and the grain's right edge is the right edge of the line under it.
 */
.instructions-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
  padding-bottom: 8px;
  margin: 0 0 16px;
}
.instructions-head h2 { border-bottom: 0; padding-bottom: 0; margin: 0; }

/* Already in the row; its old bottom margin was the gap under a line of its
   own. The links keep their own underlines - the current one is Accent and
   sits above the rule rather than on it. */
.instructions-head .grain { margin: 0; }

/* Wrapped, the grain drops under the heading and wants a little air, or it
   sits on the rule. */
@media (max-width: 420px) {
  .instructions-head { gap: 6px 16px; }
}
/* The door to cook mode: full width, the brown of the Next bar, under the
   times and above the two columns. */
.cook-it {
  display: block;
  box-sizing: border-box;
  width: 100%;
  margin: 22px 0 30px;
  padding: 15px 16px;
  text-align: center;
  background: var(--flame);
  color: var(--cream);
  font-family: Helvetica, Arial, sans-serif;
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-decoration: none;
  border-radius: 7px;
}
.cook-it:hover { filter: brightness(0.94); }

.step-open > summary {
  cursor: pointer;
  list-style: none;
}
.step-open > summary::-webkit-details-marker { display: none; }
/* The affordance is a rule that thickens, not a triangle - the step text is
   the content and should not be prefixed by a control. */
.step-open > summary { border-left: 2px solid transparent; padding-left: 0; margin-left: -12px; padding-left: 10px; }
.step-open[open] > summary { border-left-color: var(--line); }
.step-open > summary:hover { border-left-color: var(--line); }

.step-open .mise { margin: 14px 0 12px 10px; }
.step-open .mise h3 {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.64rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 8px;
  border-top: 1px solid var(--line);
  padding-top: 10px;
  font-weight: 400;
}
.step-open .mise li { font-size: 0.92rem; padding-left: 0; }
.step-open .mise li::before { display: none; }

.produces-inline {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 0 0 6px 10px;
  /* Two points up from the 0.72rem it used to be. It was sized as a footnote
     and it is not one - it names the thing a later step will ask for. */
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.88rem;
  color: var(--muted);
}

/* The name is set exactly as an ingredient line is - Vollkorn at 0.97rem in
   charcoal, matching ul.ingredients li - because that is what it has become.
   A later step asking for "the marinade" is asking for something the cook now
   has, and it should read like the other things they have. Not bold: an
   ingredient line is not, and matching means matching. */
.produces-inline b,
.produces-note b {
  font-family: 'Vollkorn', Georgia, 'Times New Roman', serif;
  font-size: 0.97rem;
  font-weight: 400;
  color: var(--charcoal);
}
.produces-inline input[type=text] {
  flex: 1 1 12rem;
  font-family: 'Vollkorn', Georgia, serif;
  font-size: 0.88rem;
  padding: 5px 8px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: #fff;
  color: var(--charcoal);
}
.produces-inline button {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  padding: 5px 12px;
  border-radius: 3px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--soft);
  cursor: pointer;
}
.produces-inline button:hover { border-color: var(--accent); color: var(--accent); }


/* ------------------------------------------------ where you are, and what ---
   for. Two taps on a step do two things: the number sets your position, the
   text opens the step. */

ol.steps > li > .step-no:hover { background: var(--accent); }

/* Three tiers, and only once a step has been made current. Behind you is done
   with; ahead is still coming; the one you are on is the only one at full
   weight. Reading a recipe with no position set shows none of this. */
ol.steps > li.behind { opacity: 0.38; }
ol.steps > li.behind > .step-open > summary { text-decoration: line-through; text-decoration-thickness: 1px; }
ol.steps > li.behind > .step-no { background: var(--muted); }
ol.steps > li.ahead  { opacity: 0.72; }
ol.steps > li.current > .step-no { background: var(--flame); }

/* The dot in flight - steps.js clones the disc it is leaving and slides the
   clone to the one that was tapped. Snappy: fast out of the blocks, a
   whisker of overshoot on arrival, done in a third of a second. The real
   target stays in its "ahead" clothes until the clone lands, so there is
   one flame dot on screen the whole way. */
.step-no-ghost {
  position: fixed;
  z-index: 20;
  margin: 0;
  pointer-events: none;
  background: var(--flame) !important;
  /* The logo's dot, whatever it was cloned from: round, flame, no number.
     In editorial the discs are squares and the current one is the circle,
     so a square ghost flew and snapped round on landing. The thing that
     travels is the dot itself. */
  border-radius: 50% !important;
  border: 0 !important;
  color: transparent !important;
  /* The flight itself is a Web Animation in recipe.php - keyframes can say
     "bigger in the middle" and need no flush trick to run on Safari, both
     of which the transition that used to live here could not. */
  transform-origin: center;
}
/* The disc the dot is flying to. It was greyed as a landing pad; since
   2026-09-26 it clears out of the way instead, by a Web Animation in fly()
   (recipe.php) - so all this keeps is no transition fighting that. */
ol.steps > li.is-arriving > .step-no { transition: none; }
/* And the disc it left is empty while the dot is in the air: the dot WAS
   that disc, and it has gone. Its done face zooms in on touchdown (fly() in
   recipe.php). Gabe, 2026-09-25: "nothing grey appears at the outgoing step
   until the dot has settled at the new step." */
ol.steps > li.is-leaving > .step-no { visibility: hidden; transition: none; }

/* No check on the disc the dot left: it comes back as its plain "behind"
   self. The check was dropped 2026-09-25 - "It should just be the original
   step #" - and with it the absolute ::after that drew the mark on top of
   the number wherever a later rule won the colour back. */
@media (prefers-reduced-motion: reduce) {
  .step-no-ghost { transition: none; }
}

/* Ingredient highlighting, with no script.
   `:has()` on the common ancestor looks down into the steps subtree for an open
   panel, then styles a line in the ingredients subtree that carries the same
   step. CSS cannot correlate two selectors on a shared value, so the pairing is
   enumerated - 30 rules, which covers the longest recipe in the collection at
   24 steps. Verified in the browser before writing any of it. */
:root { --hl: rgba(122, 106, 85, 0.16); }
.body-grid:has(details.step-open[data-step="0"][open]) .ingredients li[data-step="0"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="1"][open]) .ingredients li[data-step="1"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="2"][open]) .ingredients li[data-step="2"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="3"][open]) .ingredients li[data-step="3"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="4"][open]) .ingredients li[data-step="4"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="5"][open]) .ingredients li[data-step="5"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="6"][open]) .ingredients li[data-step="6"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="7"][open]) .ingredients li[data-step="7"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="8"][open]) .ingredients li[data-step="8"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="9"][open]) .ingredients li[data-step="9"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="10"][open]) .ingredients li[data-step="10"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="11"][open]) .ingredients li[data-step="11"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="12"][open]) .ingredients li[data-step="12"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="13"][open]) .ingredients li[data-step="13"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="14"][open]) .ingredients li[data-step="14"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="15"][open]) .ingredients li[data-step="15"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="16"][open]) .ingredients li[data-step="16"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="17"][open]) .ingredients li[data-step="17"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="18"][open]) .ingredients li[data-step="18"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="19"][open]) .ingredients li[data-step="19"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="20"][open]) .ingredients li[data-step="20"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="21"][open]) .ingredients li[data-step="21"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="22"][open]) .ingredients li[data-step="22"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="23"][open]) .ingredients li[data-step="23"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="24"][open]) .ingredients li[data-step="24"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="25"][open]) .ingredients li[data-step="25"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="26"][open]) .ingredients li[data-step="26"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="27"][open]) .ingredients li[data-step="27"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="28"][open]) .ingredients li[data-step="28"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }
.body-grid:has(details.step-open[data-step="29"][open]) .ingredients li[data-step="29"] > .gather > span:not(.mise-n) { background: var(--hl); box-shadow: 0 0 0 3px var(--hl); border-radius: 2px; }

/* The rating on the recipe page: the right-hand end of the times row.
   
   It was top left of the photograph until 2026-09-21 - the corner a card puts
   it in - white, behind a scrim, with a four-way text shadow so it would hold
   against pale food photography. None of that is needed on a row of type, and
   the scrim went with it: the photograph carries nothing now. The cards still
   put theirs on the picture, and that is fine - a card is a thumbnail and has
   nowhere else to put it. */
.shot-wrap { position: relative; }

.meta-item-rating { margin-left: auto; min-width: 0; text-align: right; }
.meta-rating {
  display: block;
  font-size: 0.95rem;
  letter-spacing: 0.08em;
  color: var(--accent);
  line-height: 1.3;
}
.meta-rating .dim { color: var(--line); }

/* Prev/next is a blog's furniture at the foot of an endless grid, and the
   script says so by taking it away. `is-live` is added only once the scroll
   handler is in place and removed the moment a fetch fails, so the links are
   hidden exactly while something else is doing their job - never when the page
   is relying on them. */
.pager.is-live { display: none; }

/* What stands in its place: one quiet line, in the same Helvetica small caps
   as every other control here rather than a spinner. The cards arriving are
   the real feedback; this only has to say that more are coming. It keeps the
   pager's own rule and spacing, so nothing at the foot of the grid moves when
   one replaces the other. */
.pager-status {
  margin: 40px 0 0;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.7rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--muted);
  text-align: center;
  min-height: 1em;
}
/* Empty between "loading" and "all of them" - the rule and the space stay, so
   the grid does not shift under the reader at the moment it finishes. */
.pager-status:empty::before { content: ""; }

/* How finely to read the steps. Real links, one per level, so the choice is in
   the URL like everything else. */
.grain {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin: 0 0 18px;
}
/* On is said the way the filter panel says it - weight and colour - rather
   than with a rule under the word. Gabe, 2026-09-22: "lets lose the underline
   and use the emphasis from the filter panel instead." One idiom for "this one
   is chosen" across the app, and it was already the better of the two: the
   underline sat 2px under small caps and read as a link decoration. */
.grain a {
  color: var(--muted);
  text-decoration: none;
  font-weight: 500;
}
.grain a:hover { color: var(--charcoal); }
.grain a.on { color: var(--accent); font-weight: 700; }

/* A piece of a step carries a dotted number - 1.5 - which does not fit a
   30px circle. Wider, and lighter, so it reads as a subdivision of the step
   above rather than as a step in its own right. */
ol.steps > li > .step-no.is-piece {
  width: auto;
  min-width: 30px;
  /* 5px, not 7. Almost every label here is three characters - "2.6" - and at
     7px the padding pushed it to 32.9 against a 30px min-width, so the circle
     came out 3px wider than it was tall. At 5px the three-character case fits
     inside the minimum and is a circle; "2.10" still grows, which is the
     capsule doing its job. */
  padding: 0 5px;
  border-radius: 15px;
  font-size: 0.76rem;
  /* Outlined, not filled - and that is the hierarchy, not the colour.
   *
   * A piece is a quieter step, not a different sort of object. It was Nutmeg
   * for that reason, and a tint of Flame was the obvious replacement once the
   * brown went - but no tint works: at 90% Flame the cream number on it is
   * 4.10:1, and only the full colour reaches 4.5. So the capsule keeps the
   * colour and gives up the fill, which says "quieter" without saying it in a
   * second hue. Flame on cream is 4.80:1. */
  background: none;
  color: var(--flame);
  border: 1px solid var(--flame);
  line-height: 28px;
}
/* Current beats the default on both shapes: a piece you are standing on is
   Flame like any other current step, so "here" is one colour everywhere. */
ol.steps > li.current > .step-no.is-piece { background: var(--flame); color: var(--cream); }
/* A piece you have been past looks like a step you have been past: the same
   solid muted fill and cream label, not the outline over a grey fill it used
   to keep. Gabe, 2026-09-25: "used steps should always look like 2". */
ol.steps > li.behind > .step-no.is-piece {
  background: var(--muted);
  color: var(--cream);
  border-color: var(--muted);
}

.produces-note {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.96rem;
  color: var(--muted);
  margin: 0 0 26px;
}
.produces-note a { color: var(--accent); }

/* The whole step is the tap target now, so it should look like one. */
ol.steps > li { cursor: pointer; }
ol.steps > li .mise,
ol.steps > li .produces-inline { cursor: auto; }

/* ------------------------------------------------------------- scaling ---
   A stepper made of links. Servings are a state of the page, every state of
   the page is an address, and this works with no script at all. */
.scaler {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.82rem;
  margin: 0 0 18px;
}
/* Half, as it is, twice - at the right end of the stepper's line while they
   fit, flush left under it when they do not. Three rulings, one day
   (2026-09-25): a push-right margin left a wrapped row hanging ("if we have
   to stack, stack correctly"); always stacking wasted a row wherever one line
   fit ("make these one line until we need to break"); and sitting beside the
   stepper lost the right alignment ("right align fractional piece as
   before").

   `space-between` on the row satisfies all three with no width threshold,
   which matters because the unit - "servings", "large cookies" - changes the
   stepper's width per recipe. With the stepper grouped (.scaler-stepper), a
   shared line holds two items and they go to opposite ends; a wrapped line
   holds one, and a lone item under space-between sits at the start. The
   reset sentence has a line of its own either way. */
.scaler { justify-content: space-between; }
.scaler-stepper { display: flex; align-items: center; gap: 4px; }
.scaler-factors {
  display: flex;
  gap: 2px;
  /* After the stepper, before the reset sentence (order: 10). */
  order: 9;
}
.scaler-factor {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 30px;
  height: 30px;
  padding: 0 6px;
  border: 1px solid var(--line);
  color: var(--charcoal);
  text-decoration: none;
  font-weight: 700;
  font-size: 0.78rem;
  line-height: 1;
}
.scaler-factor:hover { border-color: var(--accent); color: var(--accent); }

/* Where you are. Filled rather than outlined, because the three of them are a
   set and the one in force has to be readable across a worktop rather than
   found by comparing borders. */
.scaler-factor.is-on {
  background: var(--charcoal);
  border-color: var(--charcoal);
  color: var(--cream);
}
.scaler-factor.is-on:hover { color: var(--cream); }

.scaler-step {
  width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  color: var(--charcoal);
  text-decoration: none;
  font-size: 1rem;
  line-height: 1;
}
.scaler-step:hover { border-color: var(--accent); color: var(--accent); }
.scaler-now {
  min-width: 92px;
  text-align: center;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* "Serves 4" is one word that never changes, so it needs no slot kept wide
   for the plural - its natural width, which with the tighter factors below
   is what puts the stepper and the factors on one line at tablet width
   (Gabe, 2026-09-30: "save 2 chars with serves 4 and tighten up the
   padding"). The other units keep the slot. */
.scaler-now.is-serves { min-width: 0; padding: 0 1px; }
/* Snug to "Serves", for air between the stepper and the factors - Gabe,
   2026-09-30: "tighten up the serving side to allow a bit of separation
   from fractions". Two digits and a half still fit. */
.scaler-now.is-serves .scaler-input { width: 2.4ch; }
.scaler-stepper:has(.is-serves) { gap: 3px; }
/* The number is an input that looks like the bold figure it replaced: no box,
   no spinner, the same size and weight, so typing a size looks like reading
   one. The spinner is dropped because the − and + beside it already are one. */
.scaler-input {
  width: 3.2ch;
  font: inherit;
  font-size: 1rem;
  font-weight: 700;
  color: var(--charcoal);
  text-align: center;
  border: none;
  border-bottom: 1px solid transparent;
  border-radius: 0;
  background: transparent;
  padding: 0;
  font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
  appearance: textfield;
}
.scaler-input::-webkit-outer-spin-button,
.scaler-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.scaler-input:focus { outline: none; border-bottom-color: var(--accent); }
.scaler.is-on .scaler-input { color: var(--accent); }

/* For a browser with no script: the way to send a typed number. With script
   a change submits itself, and the button is hidden by the class the script
   sets once it is ready to do that. */
.scaler-go {
  height: 24px;
  padding: 0 9px;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--charcoal);
  cursor: pointer;
}
.scaler-go:hover { border-color: var(--accent); color: var(--accent); }
.has-script .scaler-go { display: none; }

/* This class was the "scaled from 2 - reset" link, dropped 2026-09-30 (Gabe:
   "yes, drop the reset line"; 1x does the same). It now styles only the
   estimated-yield sentence, which keeps the look.

   Its own line, under the controls rather than trailing off the end of them.

   Gabe, 2026-09-21: "move 'scaled from 2 - reset' under the serving group." It
   sat between the + and the factor buttons, which made one row of four
   different kinds of thing - two steppers, a sentence and three buttons - and
   the sentence is the only one of them that is not a control.

   `flex-basis: 100%` rather than a block: the row is a flex container and
   this is the one child that takes a line to itself, which is a fact about
   this item rather than about the row. `order` keeps it under the steppers
   even though the factor buttons come after it in the markup. */
.scaler-reset {
  flex-basis: 100%;
  order: 10;
  margin: 2px 0 0;
  color: var(--muted);
  font-size: 0.76rem;
  text-decoration: none;
  border-bottom: 1px dotted var(--line);
  align-self: flex-start;
}
.scaler-reset:hover { color: var(--accent); border-bottom-color: var(--accent); }

/* An estimated yield, said where the reset is said: a sentence about the
   number, in the same small grey, and not underlined because it is not a
   link. See the one pass (MODEL-PASS.md). */
.scaler-est,
.scaler-est:hover { border-bottom: 0; color: var(--muted); }

/* A line with no amount to scale. Marked rather than left looking scaled -
   "Pinch of salt" beside a halved recipe is wrong relative to everything around
   it, and only the cook can decide by how much. No number is invented for it. */
.ingredients .unscaled > .gather > span:not(.mise-n)::after {
  content: " not scaled";
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.66rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  margin-left: 6px;
}

/* A number that scaling moved. The colour is the whole point: at a glance you
   can see what followed the servings and what stayed where the publisher put
   it, so a time or an oven temperature standing still reads as deliberate
   rather than as something that failed to update. */
.moved {
  font-weight: 600;
  color: var(--flame);
  font-variant-numeric: tabular-nums;
}
.cook-body .moved,
.cookview-next .moved { font-weight: 700; }

/* **An ingredient struck out of the step that uses it.**

   Gabe, 2026-09-22: "how feasible is it to have steps like these strike out red
   as they're checked off?" Invisible until it happens - a step full of
   underlined words would be a step you read twice - and red and struck the
   moment the Have Ready line under it is ticked.

   `text-decoration-color` as well as `color`: the line is the signal and the
   ink coming with it is what makes a word read as done rather than as a
   correction. */
.used { text-decoration: none; }
.used.is-done {
  color: var(--flame);
  text-decoration: line-through;
  text-decoration-color: var(--flame);
  text-decoration-thickness: 2px;
}

/* Mid-swap. Just enough to say something is happening on a slow connection -
   the columns are still readable and still the right ones until they are
   replaced, so this must not blank them out. */
/* Held back 350ms, so a quick answer never shows it: at once, a click on
   Baby steps greyed every word before anything moved (Gabe, 2026-09-29,
   with a picture). Going back is instant - the base rule has no transition. */
.body-grid.is-swapping { opacity: 0.55; transition: opacity 0.12s ease-out 0.35s; }

/* Timers left running on a step you have walked away from. Cook mode shows one
   step, so without this a pot that is timing disappears the moment you press
   Next - and that is the one thing on the page you cannot afford to lose sight
   of. Absent entirely until there is something in it. */
.cook-tray {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  /* Space underneath as well as above. With none, a running timer sat directly
     on top of "STEP 3.4 OF 18" and the label read as part of the timer. */
  margin: 18px 0 30px;
}
/* A pinned timer is the same element as any other timer, so it inherits every
   state and every control from .timer-set above - and the step it belongs to
   is its own link, because the clock now has a job of its own. Two
   destinations, stacked: clock and controls on top, the way back underneath.

   Beside it, the step label was competing with the +1 and +5 for the same
   horizontal room and pushing the row wider every time a timer started. Under
   it, the row is as wide as the controls and no wider, and the label reads as
   belonging to the timer above it rather than as a third button. */
.tray-row {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 5px;
  padding: 6px 10px 7px;
  border-radius: 4px;
  background: var(--paper);
  box-shadow: inset 0 0 0 1px var(--line);
}
.tray-row .timer-set { font-size: 1.2rem; }

/* In the line, not overlaid. Everywhere else the controls are lifted out of
   flow so they can stand over the sentence that holds the timer without
   reflowing it - see .t-ctl above. There is no sentence here: the tray is a
   strip of its own, nothing follows the clock on that row, and leaving them
   absolute put them outside the box's own border. The same reasoning, and the
   same three lines, as cook mode. */
.tray-row .t-ctl {
  position: static;
  transform: none;
  display: inline-flex;
  vertical-align: baseline;
}
.tray-row .timer-set.is-running .timer,
.tray-row .timer-set.is-paused .timer { border-radius: 3px 0 0 3px; }

/* Never hidden behind a hover here, whatever the pointer is.
   The tray exists to be glanced at from across a kitchen while something is in
   the pan - that is the whole reason a pinned timer is pinned - and a control
   you have to find with a pointer first is a control you do not have at the
   moment you need it. The hover rule above is right for a timer sitting inside
   a sentence, where hiding keeps the prose readable; there is no prose here. */
.tray-row .timer-set.is-running .t-add {
  visibility: visible;
  opacity: 1;
}
.tray-step {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  color: var(--soft);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  white-space: nowrap;
}
.tray-step:hover { color: var(--charcoal); }


/* A second destination on the citation line: the same idea as the card's
   arrow, said in words because this line has the room and no icon to lean on.
   Set apart with a rule rather than a dot, because the citation beside it is
   often a full sentence and a middle dot would read as part of it. */
.source-more {
  display: inline-block;
  margin-left: 12px;
  padding-left: 12px;
  border-left: 1px solid var(--line);
  color: var(--accent);
  text-decoration: none;
  white-space: nowrap;
}
.source-more:hover { text-decoration: underline; }

/* ------------------------------------------------ navigation in flight -- */

/*
 * The bar that says a page is on its way. See public/assets/nav.js for when it
 * appears; this is only what it looks like.
 *
 * Fixed to the top of the window rather than the document, because a slow
 * answer is most often asked for from the bottom of a long grid, and a bar
 * scrolled off the top of the page would be feedback nobody sees.
 */
.navbar-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  width: 0;
  background: var(--accent);
  z-index: 100;
  /* Off until asked for, and not merely transparent: a zero-width strip that
     is still painted is one more thing compositing on every scroll. */
  opacity: 0;
  pointer-events: none;
}

/*
 * Fast to most of the way, then a crawl.
 *
 * Nothing here knows how far along the request is - a page load is not
 * measurable from inside the page it is leaving - so the shape is a promise
 * about attention rather than about progress: most of the bar arrives at once,
 * saying "heard you", and the rest creeps, saying "still going". It never
 * reaches the end, because the only thing that finishes it is the new page,
 * and that repaints everything anyway.
 */
:root.is-navigating .navbar-progress {
  opacity: 1;
  animation: navbar-progress 20s linear forwards;
}

/*
 * The shape is in the keyframes, not in an easing curve - it has to be legible
 * as numbers, because the first attempt was 62% at 20% of twelve seconds and
 * that is 0px wide for the first second of a request. A bar nobody sees during
 * the wait it exists for is worse than none.
 *
 * So: most of the way in 400ms, three quarters by two seconds, and then a
 * twenty-second crawl that never arrives.
 */
@keyframes navbar-progress {
  0%   { width: 0; }
  2%   { width: 58%; }
  10%  { width: 78%; }
  100% { width: 96%; }
}

/* Somebody who has asked not to be shown motion still gets the bar - it is
   information, not decoration - but it arrives rather than travels. */
@media (prefers-reduced-motion: reduce) {
  :root.is-navigating .navbar-progress { animation: none; width: 100%; opacity: 0.55; }
}

/* Clearing the check marks. Printed only when the script is there, so it never
   appears on a page that remembered nothing.

   The line is a slot: always in the layout, empty or not. It used to be
   printed only when something was ticked, so the first check mark inserted a
   whole row above the list and moved every ingredient down under the finger
   that had just tapped one - reported as feeling like a bug, and it is the
   same fault `.chiplead` carries a "do not make it conditional again" note
   for. What comes and goes is the button's `visibility`, which keeps its box,
   so this slot is exactly as tall as the control it holds and there is no
   number here to go stale against the button's type.

   And the slot costs no height: it rides the heading's own line, right-aligned
   across from "Mise en Place", instead of holding a 39px row open under the
   rule. That row of air was Gabe's gap - inspector screenshot, 2026-09-25,
   with p.gather-clear selected: the first thing under the heading of every
   recipe was an invisible button's empty seat. Same no-shift guarantee - the
   slot exists whether or not the button shows - at a height of zero. */
[data-swap="ingredients"] { position: relative; }
.gather-clear { position: absolute; top: 4px; right: 6px; margin: 0; }
.gather-clear button {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.74rem;
  letter-spacing: 0.04em;
  color: var(--muted);
  background: none;
  border: 0;
  border-bottom: 1px solid var(--line);
  padding: 0 0 2px;
  cursor: pointer;
}
.gather-clear button:hover { color: var(--accent); border-bottom-color: var(--accent); }

/* ------------------------------------------------- the column that follows -- */

/*
 * The mise column stays with you as the steps scroll past.
 *
 * Reported from cooking: reading a step and checking an amount meant scrolling
 * up, finding the line and scrolling back - and it happened even with the
 * per-step Have Ready list, because that answers "what goes in" rather than
 * "how much, and what else is beside it".
 *
 * Two columns above 640px, so there is a column to hold still. Below that the
 * layout stacks and the ingredients sit above the steps, where sticking would
 * pin a list over the thing you are reading - so this does nothing there, and
 * the per-step list is what serves a phone.
 *
 * 83% of the collection has 16 ingredient lines or fewer, which fits a laptop
 * screen. The rest get their own scrollbar rather than a column taller than the
 * window, which would stick with its foot off the bottom and never show the
 * last lines at all.
 */
@media (min-width: 640px) {
  [data-swap="ingredients"] {
    position: sticky;
    top: 12px;
    align-self: start;
    max-height: calc(100vh - 24px);
    overflow-y: auto;
    /* Room for the highlight's 3px halo, which an overflow box would clip. */
    padding-right: 6px;
    /* And room at the foot, or the column cuts a line in half at whatever
       height the viewport happens to be - "1 cup finely shredded green
       cabbage" guillotined through the word, which reads as the page being
       broken rather than as a list you can scroll. */
    padding-bottom: 16px;
    scrollbar-width: thin;
    /* A swipe on the list scrolls the list and stops at its end. Chained, it
       ran on into the page: on an iPad, a long list flicked to its foot kept
       going and the steps beside it scrolled away. Gabe, 2026-09-28: "can we
       make each panel only recognize swipes/scrolls in it's own side?" A
       list short enough not to scroll is not a scroll box to the gesture, so
       it still moves the page.
       What this cannot do: a touch that lands while the page is still
       coasting only stops the page - iOS spends it there and never hands it
       to the page's scripts (tried 2026-09-28 with a readout on the iPad
       simulator: no touchstart at all), so no script can steer it into the
       list either. The next swipe goes where it lands. */
    overscroll-behavior-y: contain;
  }
}

/* A phone in landscape is short, and the steps are what it is for. */
/* `relative`, not `static`: either one stops the sticking, but static also
   takes away the box "Clear all check marks" is placed in, and it went to the
   top of the page - over the masthead on a phone held sideways. And `top:
   auto`, because `relative` honours the 12px the sticking rule set, and
   pushed the whole column 12px below the steps beside it. */
@media (min-width: 640px) and (max-height: 520px) {
  [data-swap="ingredients"] { position: relative; top: auto; max-height: none; overflow: visible; }
}

/* No steps yet - a recipe mid-entry, ingredients pasted first - and there is
   nothing to hold still for. Worse, the page is too short to scroll, so the
   scrollbox clips the mise's tail with no scrollbar anyone can see: the
   cookies showed "for the base" as a heading over nothing, its one line past
   the fold. A section boundary at the clip line reads as an empty section,
   not as more below. */
@media (min-width: 640px) {
  .body-grid:not(:has(> [data-swap="steps"] li)) > [data-swap="ingredients"] {
    position: relative;   /* not static - see the landscape rule above */
    max-height: none;
    overflow: visible;
  }
}

/* ---------------------------------------------- focus mode, lying sideways -- */

/*
 * A phone propped on a worktop is usually in landscape, and that is the one
 * shape this view had never been measured at.
 *
 * Everything here sized itself by width - `--cook-size` drops at 620px across -
 * so a handset turned over stayed at the full 3rem while the height fell to
 * 375px. Measured: 653px of page in a 375px window. The step text alone ran
 * past the bottom, and Have Ready and the next-step preview were off the
 * screen entirely, which is the whole view failing at its one job.
 *
 * Height, not orientation, because the thing that has gone is height: a short
 * laptop window has the same problem and an `orientation` query would miss it.
 *
 * The type comes down, and that is the honest trade rather than a compromise -
 * a phone lying on the worktop is two feet from you, not across the room, and
 * the slider is still there for anyone who wants it bigger.
 */
@media (max-height: 480px) {
  /* The type itself is fitted in cook.php, because the slider writes an inline
     custom property and an inline property beats anything written here. */
  .cookview {
    padding-top: 10px;
    padding-bottom: 6px;
    /* The bars were 108px each inside a 128px gutter - 256px of an 812px
       screen, a third of it, to two controls you hit with a thumb. Narrower
       bars are still thumb-sized at this height, and the 130px they give back
       goes into line length, which is the cheapest height there is: a wider
       measure is fewer lines is a shorter step. */
    padding-left: 64px;
    padding-right: 64px;
  }
  .cookview-go.prev { width: calc(56px + var(--sa-l, env(safe-area-inset-left))); }
  .cookview-go.next { width: calc(56px + var(--sa-r, env(safe-area-inset-right))); }
  .cookview-go span { font-size: 0.86rem; letter-spacing: 0.12em; }

  /* The chrome pays first. The title, the grain and the slider are all things
     you set once and then cook, so they give up their space to the step. */
  .cookview-top { margin-bottom: 8px; }
  .cookview-title { font-size: 0.95rem; }
  .cookview-tools { gap: 14px; }
  .cookview-where { margin-bottom: 4px; }

  /* Still previewed, because knowing what is coming is what stops you reading
     ahead - but as a hint rather than a second step. */
  .cookview-next { padding-top: 10px; }
  .cookview-next .cook-body { font-size: calc(var(--cook-size) * 0.62); }

  /* Have Ready is a list of short lines; it can afford to be tighter than the
     step it sits under. */
  .cookview-ready { margin-top: 10px; }
  .cookview-ready .mise li { margin: 2px 0; }

  /* "This step makes the cooked beef" was carrying a 48px gap above it and
     taking the last 57px of a 375px screen on its own - the difference between
     a step that fits and a step you have to scroll. */
  .cookview-hand { margin-top: 12px; font-size: 0.78rem; }
}

/*
 * The header on a narrow screen: two rows, stated.
 *
 * Everything else in cook mode is tuned by *height*, because the surface that
 * matters is a phone on its side - Gabe, 2026-09-21: "phone horizontal is the
 * smallest rational surface. If someone uses portrait here we don't care."
 * So this is not a case being optimised; it is a case being kept from
 * collapsing. The header is a flex row of three things, and at 375px across
 * it wrapped to four lines and took 213px off the top of a view that exists
 * to show one step.
 *
 * The name and the way out on the first row, the controls on the second, and
 * whatever the controls do inside that is fine. No fitting beyond this - the
 * width it would be fitted to is one nobody is asked to cook from.
 */
@media (max-width: 620px) {
  .cookview-top {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 8px 12px;
    padding-bottom: 10px;
  }

  /* One line, clipped. In this view you know what you are cooking - the name
     is the least useful thing on the screen and the only one that can take
     two rows of it. */
  .cookview-title {
    grid-column: 1;
    grid-row: 1;
    min-width: 0;
    font-size: 0.95rem;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .cookview-tools { grid-column: 1 / -1; grid-row: 2; gap: 12px; }

  /* A control may not break inside itself. The row above can wrap; "Detail
     mode" going over two lines is a different thing and reads as damage. */
  .cook-views a { white-space: nowrap; }
}

/*
 * Standalone, and the notch.
 *
 * With `viewport-fit=cover` the page reaches the physical edges, so the insets
 * have to be given back by hand. In landscape they land on the left and right -
 * which is where focus mode puts Previous and Next, so without this the notch
 * sits on top of a control.
 */
/* Each breakpoint keeps its own padding and takes the inset only when the inset
   is the larger of the two. Written last and in the same order as the rules
   above, because a single blanket rule here overrode the narrow-phone padding
   and put the text under the bars - caught at 375px. */
/* **128px, not the 56px the shorthand at the top of this view sets.** The bars
   are fixed and the rule a few hundred lines above pads the reading column out
   of their way; a blanket rule written later with the smaller number put the
   step text underneath PREVIOUS on every tall window.

   Added to rather than max()'d against the inset, because the bar now grows by
   the inset too - the column has to clear the bar's full width, not whichever
   of the two numbers happens to be larger. */
.cookview {
  padding-left: calc(128px + var(--sa-l, env(safe-area-inset-left)));
  padding-right: calc(128px + var(--sa-r, env(safe-area-inset-right)));
}

@media (max-width: 620px) {
  .cookview {
    padding-left: calc(62px + var(--sa-l, env(safe-area-inset-left)));
    padding-right: calc(62px + var(--sa-r, env(safe-area-inset-right)));
  }
}

@media (max-height: 480px) {
  .cookview {
    padding-left: calc(64px + var(--sa-l, env(safe-area-inset-left)));
    padding-right: calc(64px + var(--sa-r, env(safe-area-inset-right)));
  }
}

/*
 * **Tabs at every size, not only sideways.** Gabe, 2026-10-01, on seeing the
 * phone's tabs: "lose the full height bars everywhere, this is neater" - and
 * "all next buttons red and all back buttons black". The bars above stay as
 * the base the safe-area rules hang off; this sets them to the FOCUS MODE
 * tab's size and place - 44px by 180, centered on the edge, read bottom to
 * top - so the way into Focus mode and the way through it look alike. The
 * column keeps the tab's width and 20px of air, plus the inset on its side.
 *
 * `:root body` and `.prev`/`.next` outrank both designs' bar rules, which
 * load after this file or set the widths that way. The sideways phone below
 * moves them to the foot of the edge, under the island, when it has one.
 */
:root body .cookview {
  padding-left: calc(64px + var(--sa-l, env(safe-area-inset-left)));
  padding-right: calc(64px + var(--sa-r, env(safe-area-inset-right)));
}
:root body .cookview-go {
  top: calc(50% - 90px);
  bottom: auto;
  height: 180px;
  padding: 0;
  border: 0;
  color: #fff;
}
:root body .cookview-go.prev,
:root body .cookview-go.next { width: 44px; }
:root body .cookview-go.prev { background: var(--charcoal); }
:root body .cookview-go.next { background: var(--flame); }
/* A touchscreen keeps :hover on whatever was tapped last, so a tapped NEXT
   stayed black until the next tap elsewhere. The swap is for a mouse; on
   touch the hover color is the resting one. `:hover` here outranks both
   designs' own hover rules, which are what turned it. */
:root body .cookview-go.prev:hover { background: var(--charcoal); }
:root body .cookview-go.next:hover { background: var(--flame); }
/* `.go-cold` is the brand mark's trick (`.brand-cold`): the tab you just
   clicked is still under the pointer when the next step paints, so it would
   arrive already turned. The step script sets it on the click and takes it
   off the first time the pointer leaves a tab. Gabe, 2026-10-01: "after
   click it stays red until roll off and on again". */
@media (hover: hover) {
  :root body .cookview:not(.go-cold) .cookview-go.prev:hover { background: var(--flame); }
  :root body .cookview:not(.go-cold) .cookview-go.next:hover { background: var(--charcoal); }
}
:root body .cookview-go span {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-family: var(--sans, Helvetica, Arial, sans-serif);
  font-size: 0.6875rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  line-height: 1;
}

/*
 * **A phone on its side: tabs, not bars.** Gabe, 2026-10-01: "We need to
 * rework landscape phone focus, it's not good ... make those buttons the
 * same idea as the focus mode tab on recipe pages. A floating tab that can
 * always fit below the island." Full-height bars were 56px each plus the
 * island's inset - a quarter of the width, on the screen with the least of
 * it - for Previous, which is a target, and Next, which is a hint: a tap
 * anywhere already walks on.
 *
 * So each is a 40px tab at the foot of its edge, as FOCUS MODE is on the
 * recipe page, read bottom to top. Low enough that the one on the island's
 * side sits under the island (Dynamic Island, centered on that edge), high
 * enough to clear the screen's rounded corner. The column keeps only the
 * tab's width, or the island's inset on its side when that is wider - text
 * at mid-height still has to miss the camera.
 *
 * `:root body` outranks the editorial design's bar rules, which load after
 * this file and match the same elements; the width needs `.prev`/`.next`
 * too, because the editorial widths at 900px and up are set that way.
 */
@media (max-height: 480px) {
  :root body .cookview {
    padding-left: max(52px, calc(var(--sa-l, env(safe-area-inset-left)) + 8px));
    padding-right: max(52px, calc(var(--sa-r, env(safe-area-inset-right)) + 8px));
  }
  /* **Centered when there is no island.** Gabe, 2026-10-01: "Do the tabs
     stay centered in phones without the island?" The foot of the edge is
     only for dodging the camera, so a phone with no side inset (a home
     button iPhone, most Androids) gets them centered like everywhere else.
     CSS cannot ask whether an inset is zero, so the inset times 1000 does
     it: zero leaves the whole lift, any real inset clamps it to nothing. */
  :root body .cookview-go {
    --go-low: calc(env(safe-area-inset-bottom, 0px) + 16px);
    --go-inset: max(var(--sa-l, env(safe-area-inset-left)), var(--sa-r, env(safe-area-inset-right)));
    top: auto;
    bottom: calc(var(--go-low) + clamp(0px, 50% - 52px - var(--go-low) - var(--go-inset) * 1000, 50%));
    width: 40px;
    height: 104px;
    padding: 0;
  }
  :root body .cookview-go.prev,
  :root body .cookview-go.next { width: 40px; }
  :root body .cookview-go span {
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    font-size: 0.6875rem;
    font-weight: 800;
    letter-spacing: 0.16em;
    line-height: 1;
  }
}



/*
 * The step can always reach the top of the screen.
 *
 * The chrome above it - the title, the grain, the slider - is meant to scroll
 * out of the way and stay there. On a short step there was nothing to scroll,
 * so it sat in front of the step anyway and the behaviour changed depending on
 * how long the step happened to be, which is the worst kind of inconsistent.
 *
 * A screen's worth of minimum height means the step's own line can always come
 * to the top. What is under it is cream, which is the right thing to find below
 * a step you have finished reading.
 */
[data-swap="step"] {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* **Bottom right.** Gabe, 2026-09-27: "Move next step in focus mode to
   bottom right of viewport." The screen's worth of height above is what puts
   it there on a short step (`margin-top: auto`); on a long one it sticks to
   the foot of the window while the step scrolls, and its own place is still
   at the end - so scrolling down always uncovers what it was over, unlike the
   floating box that "always obscures content". Not stuck on a phone on its
   side: with the step's line pinned at the top, a pinned foot too would leave
   the step a letterbox. */
.cookview-next {
  margin-top: auto;
  position: sticky;
  bottom: 0;
  z-index: 1;
  padding-bottom: 16px;
  background: var(--cream);
}
@media (max-height: 480px) {
  .cookview-next { position: static; padding-bottom: 0; }
}

/* --------------------------------------------------------- editing the text --

   Owner-only, at ?edit=1, and a distinct rendering of the page rather than the
   reading page with fields in it - see templates/recipe-edit.php for why. The
   palette is the app's: flame for the thing you are doing, caribbean for what
   you can touch, and grey for the quiet metadata. */

.edit-said {
  /* Clear of the photograph above it. Flush, the coloured rule ran straight
     out of the bottom of the hero and read as part of it. */
  margin: 14px 0 22px;
  padding: 14px 16px;
  border-left: 3px solid var(--flame);
  background: var(--paper);
  color: var(--soft);
  font-size: 0.94rem;
}
.edit-said p { margin: 0 0 6px; max-width: 68ch; }
.edit-said p:last-child { margin-bottom: 0; }
.edit-said-head { color: var(--charcoal); font-weight: 600; }
.edit-said.is-problem { border-left-color: var(--flame); color: var(--flame); }

/* As it arrived.
 *
 * Nutmeg rather than flame, and that is the whole of the signal: flame marks
 * something you are doing or something that went wrong, and this is neither -
 * it is the quiet, historical register the palette already reserves for
 * metadata. The page under it is a normal recipe with different words in it,
 * so the only thing saying you are not looking at the live one is this band
 * and the link at the foot. It has to be legible and it must not look like an
 * error.
 */
/* Flame, like every other one of these. Nutmeg was meant to read as the
   quiet historical register, and next to a photograph it read as a brown line
   somebody forgot to change. */

/* A section, edited where it is.
 *
 * There is no screen for this and nothing to scroll back from: the heading
 * becomes a field in the column it is in, and every row in the recipe grows a
 * tick where it already sits.
 */
.component-head.is-editing { margin-bottom: 10px; }
.component-name {
  font: inherit;
  width: 100%;
  box-sizing: border-box;
  padding: 4px 8px;
  color: var(--charcoal);
  background: var(--paper);
  border: 1px solid var(--caribbean);
  border-radius: 3px;
  text-transform: inherit;
  letter-spacing: inherit;
}

/* The same section, seen again in the other column. Marked as the one being
   edited without pretending to be a second field for it. */
.component-name-echo { border-bottom: 1px dotted var(--caribbean); padding-bottom: 2px; }

/* The row's tick. In the ingredients it takes the gather checkbox's place, so
   it is the same box in the same spot; in the steps it takes the number's,
   which is where the eye already goes. */
.row-pick { display: inline-flex; align-items: baseline; gap: 7px; flex: none; }
.row-pick input { width: 17px; height: 17px; accent-color: var(--caribbean); }

/* The number is absolutely positioned in a 46px gutter, so the tick goes in
   the same gutter rather than on a line of its own above the step. */
ol.steps > li > .row-pick { position: absolute; left: 4px; top: 2px; }

/* A section that does not exist yet. The register of the meta labels: a way
   in rather than a control. */
.section-new { margin: -8px 0 14px; }
.section-new a {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--muted);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.section-new a:hover { color: var(--caribbean); border-bottom-color: currentColor; }

/* The title, for the owner: the way into edit mode. Nothing shows until it
   is pointed at. */
.title-edit { color: inherit; text-decoration: none; }
.title-edit:hover { color: var(--flame); }

/* The one way into editing, under the title. Same register as New section:
   a way in, not a control. */
.edit-way { margin: 14px 0 0; }
.edit-way a {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--muted);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.edit-way a:hover { color: var(--caribbean); border-bottom-color: currentColor; }

/* Edit mode's one Save and one Cancel. Pinned to the top so the Save is never
   a scroll away from the line you just fixed, and said again at the foot. */
.edit-mode-bar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 calc(var(--gutter) * -1) 18px;
  padding: 10px var(--gutter);
  background: var(--cream);
  border-bottom: 1px solid var(--line);
  /* Across both columns wherever the page is a grid. */
  grid-column: 1 / -1;
}
.edit-mode-bar.is-foot {
  position: static;
  margin-top: 28px;
  margin-bottom: 0;
  border-top: 1px solid var(--line);
  border-bottom: 0;
}
.edit-mode-label {
  margin-right: auto;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--flame);
}
/* Needs a new model pass (punch 222): a quiet box beside Save. */
.edit-pass {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: 0.85rem;
  color: var(--muted);
  cursor: pointer;
}
.edit-pass input { margin: 0; }
.edit-mode-bar .sb-save,
.edit-mode-bar .sb-cancel {
  display: inline-flex;
  align-items: center;
  font: inherit;
  font-weight: 600;
  font-size: 0.92rem;
  line-height: 1.2;
  padding: 7px 16px;
  border: 1px solid;
  border-radius: 4px;
  text-decoration: none;
  cursor: pointer;
}

/* Edit mode's times and servings: the meta row, with fields for values. */
.meta-row.is-editing { flex-wrap: wrap; row-gap: 12px; }
.meta-item.is-editing .meta-label { display: block; }
/* The examples in an empty field are examples, not values. */
.meta-input::placeholder { color: #b3b3b3; font-weight: 400; }
/* The field looks like the value it edits - same size, weight and colour -
   with a dashed rule under it to say it can be typed in. Edit in place. */
.meta-input {
  width: 7.5em;
  margin-top: 2px;
  padding: 1px 0 2px;
  border: 0;
  border-bottom: 1px dashed var(--muted);
  border-radius: 0;
  background: none;
  font: inherit;
  font-size: 1rem;
  font-weight: 600;
  color: var(--charcoal);
}
.meta-input:focus { outline: none; border-bottom: 1px solid var(--flame); }

/* The cuisine, as the kicker it is: the select wears the kicker's type, with
   a small caret and the same dashed rule. "Cuisine" greyed when there is none. */
.kicker-select {
  appearance: none;
  -webkit-appearance: none;
  padding: 0 14px 1px 0;
  border: 0;
  border-bottom: 1px dashed currentColor;
  border-radius: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 5px center, right 1px center;
  background-size: 4px 4px;
  background-repeat: no-repeat;
}
.kicker-select:focus { outline: none; border-bottom-style: solid; }
/* As wide as the name showing, not the longest name on the list - where the
   browser can (field-sizing); elsewhere it is simply wider. */
.kicker-select, .meta-input { field-sizing: content; }
.meta-input { min-width: 4.5em; max-width: 100%; }
.kicker-select:has(option[value=""]:checked) { color: var(--muted); }
.kicker-select option { color: var(--charcoal); text-transform: none; letter-spacing: 0; }
/* Serves or Makes: the label is the kicker's dropdown in the label's type,
   and the field grows with what it holds - Makes is free text, "about 12
   muffins", where the others are a number and a unit. */
.meta-select { padding-bottom: 0; }
.meta-item-yield .meta-input { width: auto; min-width: 7.5em; }

/* Edit mode's categories (templates/recipe.php, punch 112): the meta row's
   labels over dropdowns and rows of boxes. A ticked box is red text on a red
   rule - on, in the app's one colour for it; a suggestion nobody has decided
   is dashed. */
.edit-cats { margin: 18px 0 8px; padding-top: 14px; border-top: 1px solid var(--line); }
.edit-cats-head { margin: 0 0 12px; border: 0; padding: 0; }
.edit-cats-grid { display: flex; flex-wrap: wrap; gap: 16px 32px; }
.edit-cat { margin: 0; padding: 0; border: 0; min-width: 0; }
.edit-cat .meta-label { display: block; padding: 0; margin-bottom: 6px; }
.edit-cat-most { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--muted); }
.edit-cat-select { font-size: 1rem; font-weight: 600; color: var(--charcoal); text-transform: none; letter-spacing: 0; }
.edit-cat-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.edit-chip { position: relative; display: inline-flex; cursor: pointer; }
.edit-chip input { position: absolute; opacity: 0; pointer-events: none; }
.edit-chip span {
  display: inline-block;
  padding: 6px 12px;
  border: 1px solid var(--line);
  font-size: 0.9rem;
  line-height: 1.2;
  color: var(--charcoal);
  background: none;
}
.edit-chip.is-suggested span { border-style: dashed; border-color: var(--muted); }
.edit-chip input:checked + span { color: var(--flame); border-color: var(--flame); font-weight: 700; }
.edit-chip input:focus-visible + span { outline: 2px solid var(--flame); outline-offset: 2px; }
@media (hover: hover) {
  .edit-chip:hover span { border-color: var(--charcoal); }
  .edit-chip:hover input:checked + span { border-color: var(--flame); }
}

/* Edit mode's photo control, across the foot of the picture. */
.shot-edit {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 16px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, 0.92);
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.8rem;
}
.shot-pick { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-weight: 700; }
.shot-pick input { max-width: 100%; font: inherit; font-weight: 400; }
.shot-remove { display: flex; align-items: center; gap: 6px; color: var(--flame); }
/* A photo by its address, for one on another page. Takes the rest of the row,
   and a row of its own when there is none left. */
.shot-url { display: flex; align-items: center; gap: 8px; flex: 1 1 16em; min-width: 0; color: var(--muted); }
.shot-url input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 4px 8px;
  border: 1px solid var(--line);
  border-radius: 0;
  background: #fff;
  font: inherit;
  color: var(--charcoal);
}
.shot-drawn-wrap { position: relative; }
/* Drop hint only once the script is there to take the drop. */
.shot-drop-hint { display: none; color: var(--muted); }
.shot-wrap.can-drop .shot-drop-hint { display: inline; }
.shot-wrap.is-dropping { outline: 3px dashed var(--flame); outline-offset: -3px; }
.shot-wrap.is-dropping .shot { opacity: 0.55; }
.shot-wrap.is-removing .shot { opacity: 0.25; filter: grayscale(1); }
.shot-undo {
  margin-left: auto;
  padding: 4px 12px;
  border: 1px solid var(--flame);
  border-radius: 4px;
  background: none;
  color: var(--flame);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.shot-undo[hidden] { display: none; }
.photo-undo { margin: 8px 0 0; }
.photo-undo .sb-cancel { padding: 6px 14px; border: 1px solid; border-radius: 4px; font: inherit; font-weight: 600; cursor: pointer; }

/* The heading, once it is a way in. */
.component-open { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; }
.component-open:hover { border-bottom-color: currentColor; }

/* Save, Cancel and the section bar all use this shape. */
.section-bar { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 18px; }
.section-bar button,
.section-bar .sb-cancel,
/* The delete confirmation's Cancel is an <a> wearing .sb-remove, so it needs
   naming here too - .section-bar button does not reach it, and without the
   shape it renders as a line of plain text beside a filled button. */
.section-bar .sb-remove,
.edit-bar .sb-cancel {
  font: inherit;
  font-weight: 600;
  font-size: 0.92rem;
  line-height: 1.2;
  padding: 7px 13px;
  border-radius: 4px;
  cursor: pointer;
  border: 1px solid;
}

/* The editors' Cancel is an <a> - it navigates and writes nothing, which is
   what a link is - but it sits in a row with a Save and has to be the same
   shape under a thumb. Stretched to the row rather than sized to match: a
   <button> and an <a> with identical type and padding still come out a few
   pixels apart. */
.section-bar .sb-cancel { display: inline-block; text-decoration: none; }

.edit-bar .sb-cancel {
  align-self: stretch;
  display: inline-flex;
  align-items: center;
  font-size: inherit;
  line-height: normal;
  padding: 10px 20px;
  text-decoration: none;
}

.sb-save   { background: var(--caribbean); border-color: var(--caribbean); color: var(--cream); }
.sb-save:hover { filter: brightness(1.08); }
.sb-remove { background: none; border-color: var(--line); color: var(--soft); }
.sb-remove:hover { border-color: var(--muted); }
.sb-cancel { background: none; border-color: var(--flame); color: var(--flame); }
.sb-cancel:hover { background: color-mix(in srgb, var(--flame) 8%, transparent); }

/* The only filled Flame button in the app, on the only act that cannot be
   taken back. Everything else that touches a recipe is a Save (filled
   Caribbean) or a way out (outlined). An outlined Flame already means Cancel
   here, so the delete had to be a step further in rather than a fourth
   outline nobody could rank against the other three. */
.sb-delete { background: var(--flame); border-color: var(--flame); color: var(--cream); }
.sb-delete:hover { filter: brightness(1.08); }

/* An <a> and a <button> with identical padding still come out a few pixels
   apart, so the link is stretched to the row rather than sized to match -
   the same fix .sb-cancel needs, for the same reason. */
.section-bar .sb-remove { display: inline-block; text-decoration: none; }

.edit-aside { margin: 6px 0 0; color: var(--muted); font-size: 0.84rem; max-width: 60ch; }

/* A section's name, standing over its fields.
 *
 * The same face, size and colour as `.component-head` in the column you
 * clicked, so the editor is recognisably the page you were just reading. Not a
 * field: a section is a row in `recipe_components` shared by both columns, and
 * nothing in lib/edit.php writes one. It is here so a list of fields cannot
 * quietly deny that the recipe has sections at all.
 *
 * `list-style: none` and the negative counter reset are for the steps, where
 * this sits inside the `<ol>` - a heading must not take a step number, and the
 * numbers are drawn as `.edit-no` discs anyway.
 */
.edit-section {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  font-weight: 600;
  list-style: none;
  margin: 18px 0 8px;
  padding: 0;
}
.edit-lines > .edit-section:first-child,
.edit-steps > .edit-section:first-child { margin-top: 0; }

/* The steps list lays its rows out with the disc beside the field; a heading
   has no disc and must not be indented to where one would be. */
.edit-steps > .edit-section { display: block; padding-left: 0; }

/* The title, still the title: same face, same size, so the thing being
   corrected does not move or change shape when you start correcting it. */
h1.is-editing { margin-bottom: 0; }
.edit-title {
  font: inherit;
  color: inherit;
  width: 100%;
  padding: 2px 8px;
  margin-left: -9px;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 4px;
}

/* Wider than the reading grid's 250px rail, and only a little narrower than
   the steps. A 250px field holds about four words of an ingredient line, so
   reading the reading widths across to the editor would have you correcting
   text you cannot see. */
/* A heading that is also the way in. The word "edit" appears beside it rather
   than on it, so the heading still reads as a heading - and it is the only
   thing that moves on hover, because the heading itself moving would make the
   page feel like it was about to do something. */
.edit-open {
  color: inherit;
  text-decoration: none;
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
}
.edit-open-hint {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.6rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--muted);
  opacity: 0;
  transition: opacity 140ms ease;
}
/* Open, the heading is the save button. A <button> has to be told it is not
   one; everything else about it is the heading it was a moment ago. */
button.edit-open {
  font: inherit;
  /* Both said out loud: a UA stylesheet sets `text-transform: none` and
     `letter-spacing: normal` on form controls, and `font: inherit` does not
     reach either - so the heading went from MISE EN PLACE to Mise en Place
     the moment it became a button. */
  text-transform: inherit;
  letter-spacing: inherit;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  color: inherit;
  text-align: left;
}
/* The way out is always visible, unlike the way in: you are in a mode and
   the word is what gets you out of it. */
.edit-open.is-saving .edit-open-hint { opacity: 1; color: var(--flame); }
h1 .edit-open-hint { font-size: 0.55em; }
.edit-open:hover .edit-open-hint,
.edit-open:focus-visible .edit-open-hint { opacity: 1; }
/* No hover on a touch screen, so the invitation has to be permanent there. */
@media (hover: none) {
  .edit-open-hint { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .edit-open-hint { transition: none; }
}

/* The note, as a target. Underlined only on hover: it is a paragraph you can
   write in, not a link to somewhere else. */
.note-open {
  display: block;
  color: inherit;
  text-decoration: none;
}
.note-open:hover { text-decoration: underline; text-decoration-color: var(--line); }
.note-add { margin: 34px 0 0; font-size: 0.88rem; }
.note-add a { color: var(--caribbean); }
.edit-note {
  width: 100%;
  margin-top: 10px;
  font-family: var(--serif);
  font-size: 1rem;
  line-height: 1.55;
  color: var(--charcoal);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 10px 12px;
  resize: vertical;
  overflow-y: hidden;
  min-height: 5em;
}


/* A row that does not exist yet: dashed, quieter, and its number hollow -
   so the list says where the recipe ends without a heading saying so. It
   fills in the moment there are words in it. */
.edit-lines li.is-new textarea,
.edit-steps li.is-new textarea {
  border-style: dashed;
  background: transparent;
}
.edit-lines li.is-new textarea:focus-visible,
.edit-steps li.is-new textarea:focus-visible,
.edit-lines li.is-new textarea:not(:placeholder-shown),
.edit-steps li.is-new textarea:not(:placeholder-shown) {
  border-style: solid;
  background: var(--paper);
}
.edit-no.is-new {
  background: none;
  color: var(--muted);
  border: 1px dashed var(--line);
}
.edit-lines li.is-new textarea::placeholder,
.edit-steps li.is-new textarea::placeholder { color: var(--muted); opacity: 1; }

.edit-lines { list-style: none; margin: 14px 0 0; padding: 0; }

/* Sections in edit mode: the name as a field with Remove beside it, a
   dropdown on every row, and "Add a section" at the foot of the column. */
.edit-section:has(.edit-section-name) { display: flex; align-items: center; gap: 10px; }
.edit-section-name {
  flex: 1 1 auto;
  min-width: 0;
  padding: 5px 8px;
  border: 1px solid var(--line);
  border-radius: 0;
  background: #fff;
  font: inherit;
  font-weight: 800;
  letter-spacing: inherit;
  text-transform: inherit;
}
.edit-section-drop {
  flex: none;
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--flame);
  cursor: pointer;
}
.edit-section:has(.edit-section-drop input:checked) .edit-section-name {
  text-decoration: line-through;
  color: var(--muted);
}
.edit-row-section {
  max-width: 100%;
  margin-top: 3px;
  padding: 2px 4px;
  border: 1px solid var(--line);
  border-radius: 0;
  background: #fff;
  color: var(--muted);
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
}
.edit-steps li > .edit-row-section { flex-basis: 100%; margin-left: 40px; max-width: calc(100% - 40px); }
/* Idle dropdowns (recipe with no sections yet) wait for edit.js to offer the
   first typed name. Stated because hidden loses to any author display rule. */
.edit-row-section[hidden] { display: none; }
.edit-section-new { margin: 12px 0 0; }
.edit-section-new input {
  width: 100%;
  padding: 6px 8px;
  border: 1px dashed var(--line);
  border-radius: 0;
  background: none;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.edit-section-new input::placeholder { color: var(--muted); }
.edit-lines li { margin: 0 0 6px; }
.edit-steps { list-style: none; margin: 14px 0 0; padding: 0; counter-reset: none; }
.edit-steps li { display: flex; flex-wrap: wrap; gap: 12px; margin: 0 0 12px; align-items: start; }
/* Basis 0, not auto. The textarea is width:100% elsewhere, so an auto basis
   is a 100% basis - in a wrapping row that pushes it onto its own line and
   leaves the step number stranded above it. */
.edit-steps li > textarea { flex: 1 1 0; min-width: 0; }

/* On its own line under the step, not beside it: the step text is the thing
   being edited and must not give up width to a label. Indented to the
   textarea's left edge, past the 30px disc and its 12px gap, so the two read
   as one row rather than two. */
/* Padding, not margin. A 100% flex basis plus a left margin is wider than the
   row that holds it, which pushed the page 17px sideways. */
/* What a step makes, under the step that makes it. Deliberately smaller and
   quieter: it is not the recipe, it is a name later steps point at. */
.edit-makes {
  flex: 0 0 100%;
  padding-left: 42px;
  box-sizing: border-box;
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.edit-makes input {
  flex: 1 1 auto;
  min-width: 0;
  font: inherit;
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.86rem;
  color: var(--charcoal);
  padding: 5px 8px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--paper);
}
.edit-makes input::placeholder { color: var(--muted); font-style: italic; }
.edit-makes input:focus { outline: 2px solid var(--caribbean); outline-offset: 1px; }
@media (max-width: 560px) { .edit-makes { padding-left: 0; } }

/* The step's disc, as the reading page draws it - flame, because this is the
   app doing something to a recipe. Its number is the position in the written
   steps, which is what a textarea here edits. */
.edit-no {
  flex: none;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--flame);
  color: var(--cream);
  font-family: var(--sans);
  font-size: 0.82rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 3px;
}

.edit-lines textarea,
.edit-steps textarea,
.edit-note {
  width: 100%;
  font-family: var(--serif);
  font-size: 1rem;
  line-height: 1.5;
  color: var(--charcoal);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 8px 10px;
}
/* The floor. edit.js grows each one to its content on load and as you type;
   without the script these are ordinary textareas you can drag taller. */
.edit-steps textarea { resize: vertical; min-height: 4.5em; overflow-y: hidden; }
.edit-lines textarea { resize: vertical; min-height: 2.6em; overflow-y: hidden; }
/* The focus ring is drawn INSIDE the field.
   
   Two goes at this were spent on rings painted outside the border box - an
   outline, then an outer box-shadow - and both came back clipped in Safari.
   Rather than find the third thing that clips them, put the ring somewhere
   nothing can: an inset shadow and the border itself are inside the element,
   so no ancestor's overflow, no filter region and no paint order can cut
   them. It costs a pixel of width and takes the whole class of bug away.
   
   The transparent outline stays for forced-colors mode, where shadows are
   dropped and an outline is the only ring that survives. */
.edit-lines textarea:focus-visible,
.edit-steps textarea:focus-visible,
.edit-note:focus-visible,
/* The section name shipped a day after this rule was written and did not join
   it, so it kept the browser's own ring - drawn outside, and clipped on the
   left and top by the ingredients column, which is a scroll box above 640px.
   Exactly the bug the paragraph above describes, in the one field that was
   not in the list. */
.component-name:focus-visible,
.edit-title:focus-visible {
  outline: 2px solid transparent;
  outline-offset: -2px;
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* Pinned, because the line you just corrected is rarely the last one in the
   column, and a save button at the bottom of forty fields is a scroll each
   time you want to be sure. */
.edit-bar {
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  /* Centred under the column it belongs to. Ranged left, the bar read as the
     next thing in the list rather than as the foot of it - and the column is
     narrow enough that a left-aligned Save sat under the start of every line
     above it, which is where the eye already is. */
  justify-content: center;
  gap: 16px;
  flex-wrap: wrap;
  margin-top: 28px;
  padding: 14px var(--gutter);
  margin-left: calc(var(--gutter) * -1);
  margin-right: calc(var(--gutter) * -1);
  border-top: 1px solid var(--line);
  background: var(--cream);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .edit-bar {
    background: var(--cream-glass);
    -webkit-backdrop-filter: blur(22px) saturate(1.25);
    backdrop-filter: blur(22px) saturate(1.25);
  }
}
/* Save is Caribbean, everywhere.
 *
 * It was Flame, which is the colour this palette uses for the thing that
 * costs you something - "Keep this original version", "Yes, revert". A Save
 * costs nothing: it is the reason you opened the editor. Having the safest
 * button on the page wear the warning colour left Flame meaning "a button",
 * which is not a meaning.
 */
.edit-bar button {
  font: inherit;
  font-weight: 600;
  padding: 10px 20px;
  border: 1px solid var(--caribbean);
  border-radius: 4px;
  background: var(--caribbean);
  color: var(--cream);
  cursor: pointer;
}
.edit-bar button:hover { filter: brightness(1.08); }
.edit-bar button:hover { background: var(--accent); border-color: var(--accent); }
.edit-bar button.sb-delete, .edit-bar button.sb-delete:hover { background: var(--flame); border-color: var(--flame); }
/* And Cancel is Flame, for the same reason from the other end: it throws away
   everything typed since the editor opened, which is the only work in the
   room. It was Accent, the same brown as everything else, on a control whose
   whole job is to lose something. */
.edit-bar-note { color: var(--muted); font-size: 0.84rem; }

/* One column's form sits inside that column, so the bar is the column's
   width rather than the page's. */
.edit-form { display: block; }

/* ------------------------------------------------- the stars, as a control --

   Same corner, same size and same shadow as the read-only version above, so
   nothing moves when a recipe goes from being read to being rated. What the
   owner gets is five buttons instead of a span.

   Drawn 5 to 1 and reversed here. CSS can reach the siblings that follow an
   element and not the ones before it, so backwards in the DOM is what makes
   `:hover ~ button` mean "every star to the left of this one" - the set that
   should light up while you are choosing. */
/* `row-reverse` is what makes "every star to the left of the one you are on"
   possible with no script - the sibling combinator only reaches forwards, so
   the stars are emitted 5-to-1 and turned round here. Do not straighten it. */
.meta-rating.is-settable {
  display: flex;
  flex-direction: row-reverse;
  justify-content: flex-start;
  gap: 1px;
  letter-spacing: 0;
}
.meta-rating.is-settable button {
  font: inherit;
  font-size: 1.15rem;
  line-height: 1;
  padding: 2px 1px;
  border: 0;
  background: none;
  color: var(--accent);
  opacity: 0.3;
  cursor: pointer;
  transition: opacity 120ms ease, transform 120ms ease;
}
/* Where the rating actually is. */
.meta-rating.is-settable button.on { opacity: 1; }

/* Where it would be if you let go. Only under a real pointer: on a phone
   :hover sticks to the last thing tapped, and a row of stars that stayed lit
   after a tap would be saying the rating is something it is not. And not on
   the stars just pressed, until the pointer moves to another star or off
   them (.is-just-set, the rating script in templates/recipe.php): the
   preview under a still pointer hid the rating just stored. */
@media (hover: hover) {
  .meta-rating.is-settable:not(.is-just-set):hover button { opacity: 0.3; }
  .meta-rating.is-settable:not(.is-just-set) button:hover,
  .meta-rating.is-settable:not(.is-just-set) button:hover ~ button { opacity: 1; }
  .meta-rating.is-settable:not(.is-just-set) button:hover { transform: scale(1.12); }
}
/* A finger, not a cursor. 20x22 is a comfortable target for a mouse and a
   guess for a thumb, and this is a control used with one hand in a kitchen -
   so on a touch screen each star gets a real target, about 32x44, and the row
   spreads to 160px of a 375px screen. Padding rather than a bigger glyph: the
   stars should stay the size they are drawn everywhere else. */
@media (pointer: coarse) {
  .meta-rating.is-settable button { padding: 11px 6px; }
  .meta-rating.is-settable { gap: 0; margin: -9px -6px 0 0; }
}
.meta-rating.is-settable button:focus-visible {
  opacity: 1;
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .meta-rating.is-settable button { transition: none; }
  .meta-rating.is-settable button:hover { transform: none; }
}

/* The way out, at the foot of an open section. Nutmeg rather than flame: it
   is the way back from something, which is what that colour is for, and it
   must not read as the primary act beside Save. */
.edit-undo {
  font: inherit;
  font-size: 0.84rem;
  padding: 6px 0;
  margin-left: auto;
  border: 0;
  background: none;
  color: var(--flame);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  text-align: left;
}
.edit-undo:hover { color: var(--flame); }
.edit-undo-form { display: none; }

/* ------------------------------------------------------- delete a recipe -- */

/* The way in, under the title editor's own bar. Quiet on purpose: it is the
   last thing in that editor and the only one you cannot undo, so it should
   read as a way out of the collection rather than as a fifth button. */
.delete-way { margin: 14px 0 0; font-size: 0.86rem; }
.delete-way a { color: var(--muted); text-decoration: underline; text-underline-offset: 2px; }
.delete-way a:hover { color: var(--flame); }

/* The confirmation, sitting over the recipe it is about - which stays on
   screen below, because what you confirm against should be the thing itself. */
.delete-confirm {
  border: 2px solid var(--flame);
  border-radius: 6px;
  padding: 18px 20px 20px;
  margin: 22px 0 8px;
  background: var(--cream);
  max-width: 60ch;
}
.delete-confirm h2 {
  margin: 0 0 10px;
  font-size: 1.25rem;
  color: var(--flame);
}
.dc-takes { margin: 0 0 14px; color: var(--soft); }
/* A whole-label hit area: a 13px box is not a thumb target. */
.dc-check {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  margin: 0 0 16px;
  padding: 11px 12px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--cream);
  cursor: pointer;
  font-size: 0.92rem;
  line-height: 1.45;
}
.dc-check:hover { border-color: var(--muted); }
.dc-check input {
  width: 17px;
  height: 17px;
  margin: 2px 0 0;
  accent-color: var(--flame);
  flex: none;
  cursor: pointer;
}
.dc-check:focus-within { outline: 2px solid var(--caribbean); outline-offset: 1px; }

/* A phone holds the confirmation at one card width and the buttons stack
   rather than shrinking - a delete is not a control to make small. */
@media (max-width: 560px) {
  .delete-confirm { padding: 15px 15px 17px; }
  .delete-confirm .section-bar { flex-direction: column; align-items: stretch; }
  .delete-confirm .section-bar button,
  .delete-confirm .section-bar .sb-remove { text-align: center; }
}

/* The reminder after a delete. A plain dialog, centred, with the backdrop only
   when a script has upgraded it - unstyled `open` is a block in flow, which is
   still readable and still dismissible. */
.gone-dialog {
  border: 2px solid var(--flame);
  border-radius: 6px;
  /* --cream, not --paper: --paper is used in ten places in this file and has
     never been defined anywhere, so every one of them is transparent. Not
     fixed here - defining it would change ten surfaces at once. */
  background: var(--cream);
  color: var(--charcoal);
  padding: 20px 22px;
  max-width: 34ch;
  font-family: var(--sans);
  text-align: center;
}
.gone-dialog p { margin: 0 0 16px; font-size: 1rem; }
.gone-dialog .sb-save {
  display: inline-block;
  text-decoration: none;
  font: inherit;
  font-weight: 600;
  padding: 7px 26px;
  border: 1px solid var(--caribbean);
  border-radius: 4px;
}
.gone-dialog::backdrop { background: rgb(42 38 34 / 45%); }

/* ------------------------------------------------------- the foot --

/* The foot of every page but cook mode: the links, whose the app is and whose
   the recipes are, on one rule. */
.sitefoot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 14px 28px;
  margin-top: 52px;
  padding: 34px var(--gutter) 44px;
  border-top: 1px solid var(--line);
}
.colophon {
  margin: 0;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  color: var(--muted);
}
/* On the endless grid the line stands alone under "No more recipes.", centered like it, with no rule
   of its own - the status line above already drew one. */
body:has(.card-grid) .sitefoot {
  justify-content: center;
  margin-top: 0;
  padding: 22px var(--gutter) 8px;
  border-top: 0;
}



/* A big A and a small A. The pair is its own legend - no caret to read, and it
   is the control every word processor has had for thirty years. */
/* Centered in the square both ways. `justify-content` because the square is
   a min-width with a letter narrower than it, which sat to the left; the
   trim because a capital has no descender, so a line box centered on it put
   the letter low. Gabe, 2026-09-29: "center these in the squares please, and
   make the big 'A' a bit bigger" - 1.02rem to 1.3rem. */
.size-up, .size-down, .size-fill { justify-content: center; gap: 0; }
.size-up > span[aria-hidden], .size-down > span[aria-hidden] { text-box: trim-both cap alphabetic; }
.size-up   { font-size: 1.3rem; }
.size-down { font-size: 0.76rem; }
.size-step .size-caret { display: none; }


/* A run of steps with no section, after one with a heading: a hairline and
   air end the section above, so these do not read as more of it. */
.component.is-loose {
  margin-top: 26px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}
[data-design="editorial"] .component.is-loose { border-top-color: #d0d0d0; }

/* Edit mode: an ingredient line is a grip, the field, and its section
   dropdown under the field. The grip is the only handle, so the field still
   takes a click and a selection like any text box. */
.edit-lines li.edit-line { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0 6px; }
.edit-lines li.edit-line > textarea { flex: 1 1 0; min-width: 0; }
.edit-lines li.edit-line > .edit-row-section { flex-basis: 100%; margin-left: 20px; max-width: calc(100% - 20px); }
.edit-grip {
  flex: none;
  width: 14px;
  padding-top: 8px;
  color: var(--muted);
  font-size: 0.8rem;
  letter-spacing: -0.2em;
  line-height: 1;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
  touch-action: none;
}
.edit-grip:hover { color: var(--flame); }
/* A step: the grip, then its disc. The grip gives back half the row's gap so
   the pair reads as one mark, and sits level with the disc's middle. The
   rows under the step - Makes, the section dropdown - move right by the
   grip's 20px so they still start at the field's left edge. */
.edit-steps li.edit-step > .edit-grip { padding-top: 11px; margin-right: -6px; }
.edit-steps li.edit-step > .edit-makes { padding-left: 62px; }
.edit-steps li.edit-step > .edit-row-section { margin-left: 60px; max-width: calc(100% - 60px); }
@media (max-width: 560px) { .edit-steps li.edit-step > .edit-makes { padding-left: 0; } }
/* A heading's grip, level with its name field. */
.edit-section > .edit-grip { padding-top: 0; }
.edit-lines > li.is-dragging,
.edit-lines > li.is-carried,
.edit-steps > li.is-dragging,
.edit-steps > li.is-carried {
  position: relative;
  z-index: 3;
  background: var(--cream);
  box-shadow: 0 6px 18px rgba(42, 38, 34, 0.18);
}
/* A heading and its rows travel as one slab: each row's cream spreads over
   the gap to the next, so the shadow shows round the block and not in
   stripes between its rows. */
.edit-lines > li.is-block,
.edit-lines > li.is-carried,
.edit-steps > li.is-block,
.edit-steps > li.is-carried {
  box-shadow: 0 0 0 6px var(--cream), 0 8px 20px 6px rgba(42, 38, 34, 0.14);
}
html.edit-line-dragging, html.edit-line-dragging * { cursor: grabbing; user-select: none; -webkit-user-select: none; }

/* The recipe's source, under its title: the card's source line, larger. The
   parts are the card's own classes, so both designs dress it the way they
   dress a card - the editorial one puts the arrow first and a red star before
   the rating. */
.byline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
  margin: 10px 0 0;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.9rem;
  color: var(--muted);
}
.byline a { color: inherit; text-decoration: none; }
.byline a:hover { color: var(--accent); }
.byline .card-source-name { white-space: normal; overflow: visible; }
.byline .card-source-rating { margin-left: 0; }
/* The card puts the arrow first in Editorial; the byline keeps it after the
   name, where Gabe asked for it. */
[data-design="editorial"] .byline .card-source-out { order: 0; margin-left: -4px; }
.byline-more { font-size: 0.8em; text-decoration: underline; text-underline-offset: 3px; }
[data-design="editorial"] .byline { font-family: var(--sans); font-weight: 700; letter-spacing: 0.01em; }
[data-design="editorial"] .byline a:hover { color: var(--flame); }
[data-design="editorial"] .byline .card-source-rating { font-weight: 500; color: var(--muted); font-variant-numeric: tabular-nums; }
/* Edit mode's byline: the same line, its name and link as fields - the meta
   row's dashed rule under each, the byline's own type in them. */
.byline.is-editing { gap: 6px 8px; }
.byline-input {
  min-width: 12em;
  max-width: 100%;
  padding: 1px 0 2px;
  border: 0;
  border-bottom: 1px dashed var(--muted);
  border-radius: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  color: var(--charcoal);
  field-sizing: content;
}
.byline-input::placeholder { color: #b3b3b3; font-weight: 400; }
.byline-input:focus { outline: none; border-bottom: 1px solid var(--flame); }
.byline-link { display: flex; align-items: center; gap: 4px; min-width: 0; max-width: 100%; }
.byline-input-link { min-width: 14em; font-weight: 400; }
.byline-link-mark { cursor: text; }
/* The edit bar's Delete: the same shape as its neighbours, filled flame. */
.edit-mode-bar .sb-delete {
  display: inline-flex;
  align-items: center;
  padding: 7px 16px;
  border: 1px solid var(--flame);
  border-radius: 4px;
  font-weight: 700;
  font-size: 0.92rem;
  line-height: 1.2;
  text-decoration: none;
}

/* "Deleted X." with Undo beside it. */
.notice-gone { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.undo-form { display: inline; margin: 0; }
.undo-btn {
  padding: 4px 14px;
  border: 1px solid var(--flame);
  border-radius: 4px;
  background: none;
  color: var(--flame);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.undo-btn:hover { background: var(--flame); color: #fff; }
/* Delete forever: there, and quieter than Undo. */
.undo-btn.is-quiet { border-color: transparent; color: var(--soft); font-weight: 400; }
.undo-btn.is-quiet:hover { border-color: var(--flame); background: none; color: var(--flame); }

/* "Sichuan filter unpinned", top center, three seconds - the receipt for an x
   that removed a chip in place, where nothing else on screen says it worked.
   Built by nav.js; with no script the x is a form post and a whole page.
   Fixed under the masthead, whose height nav.js already measures. */
.chip-toast {
  position: fixed;
  /* Under the chip band, not over it - the band is the thing the x just
     changed, and a receipt that covers the change is reporting on something
     it is hiding. `--band-bottom` is the band's own measured edge (nav.js);
     the fallback is masthead + band at their usual heights. */
  top: calc(var(--band-bottom, 130px) + 12px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 16px;
  max-width: min(92vw, 480px);
  padding: 10px 18px;
  background: var(--charcoal);
  color: var(--cream);
  border-radius: 8px;
  box-shadow: 0 6px 24px rgba(42, 38, 34, 0.35);
  font-size: 0.95rem;
  animation: chip-toast-in 0.18s ease-out;
}
@keyframes chip-toast-in {
  from { opacity: 0; transform: translate(-50%, -8px); }
}
/* The flame outline reads muddy on charcoal; on the dark card the undo is
   simply light.

   **The button is its own fuse.** The flame fills it left to right over the
   same three seconds the script gives the toast - full is the moment it goes -
   so the countdown is drawn on the control it counts down for. A hard-stop
   gradient twice the button's width slides in rather than growing a bar: one
   paint, no extra element, and the text sits still on top of it. Gabe,
   2026-09-25. */
.chip-toast .undo-btn {
  color: var(--cream);
  border-color: rgba(248, 243, 233, 0.6);
  background: linear-gradient(to right, var(--flame) 50%, transparent 50%);
  background-size: 200% 100%;
  background-position: 100% 0;
  animation: chip-toast-fuse 3s linear forwards;
}
@keyframes chip-toast-fuse {
  to { background-position: 0 0; }
}
.chip-toast .undo-btn:hover { border-color: var(--cream); color: #fff; }
.dc-note { color: var(--muted); font-size: 0.9rem; margin: 6px 0 12px; }



/* The recipe page redesign's parts (punch 93) are the editorial design's.
   The kitchen design keeps its own page: no fact cells beyond its times, no
   Focus mode tab (its bar stays), no rating on the photograph, no always-open checklists,
   no switches. */
.meta-more, .focus-tab, .hero-rating, .step-needs, .grain-switch, .kicker-tab, .ing-amt-short { display: none; }

/* ------------------------------------------------ long titles, renamed -- */
/* public/titles.php: every title over 50 characters as a field, saved on
   Tab. A list for going down quickly, so each row is one line: the way to
   the recipe, the field, how long it is, and what the save said. */
.titles-page { max-width: 980px; }
.titles-head { margin: 28px 0 6px; }
.titles-sub { margin: 0 0 18px; color: var(--muted); }
.titles-list { list-style: none; margin: 0 0 60px; padding: 0; }
.titles-list > li { border-top: 1px solid var(--line); }
.title-row {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr) 3em auto 5.5em;
  align-items: center;
  column-gap: 10px;
  padding: 6px 0;
}
.title-open { color: var(--muted); text-decoration: none; text-align: center; }
.title-open:hover { color: var(--flame, #b8462f); }
.title-field {
  width: 100%;
  box-sizing: border-box;
  padding: 7px 9px;
  border: 1px solid transparent;
  border-radius: 3px;
  background: transparent;
  font: inherit;
  font-size: 1rem;
  color: inherit;
}
.title-field:hover { border-color: var(--line); }
.title-field:focus { border-color: var(--charcoal, #111); background: #fff; outline: none; }
.title-chars { text-align: right; font-variant-numeric: tabular-nums; color: var(--muted); font-size: 0.85rem; }
.title-row.is-short .title-chars { color: var(--check, #4a6b52); }
.title-save[hidden] { display: none; }
.title-said { font-size: 0.85rem; color: var(--muted); }
.title-row.is-problem .title-said { color: var(--flame, #b8462f); }
@media (max-width: 640px) {
  .title-row { grid-template-columns: 22px minmax(0, 1fr) 2.6em; }
  .title-said { grid-column: 2 / -1; }
}

/* ------------------------------------------------------- signing in -- */
/* public/signin.php, forgot.php, reset.php, password.php - templates/
   signin.php. design/login round 2, "2a", on a white card (Gabe, 2026-09-28):
   the lockup over a heavy ink rule, a Gloock heading, Archivo capitals for
   the labels, fields that are a line to write on, Forgot at the left and the
   red button at the right - over a wall of food photographs, dimmed.
   Sized for a phone held in one hand as much as a laptop: the fields are
   16px or more, which also stops iOS zooming into a field on focus. */
.auth-page {
  position: relative;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px var(--gutter);
  box-sizing: border-box;
  background: var(--charcoal);
  isolation: isolate;
}
.auth-wall { position: fixed; inset: 0; z-index: -1; overflow: hidden; background: var(--charcoal); }
.auth-wall-img {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  transform: scale(1.04);
  transition: opacity 1.6s ease, transform 14s linear;
}
.auth-wall-img.is-on { opacity: 1; transform: scale(1); }
/* No dimming: the photographs at full strength. It was 42% ink, "so the
   card is what is read and the food is what is felt"; Gabe, 2026-09-28,
   "try no fade on the photos". The white card and its shadow hold their own
   against them. */
@media (prefers-reduced-motion: reduce) {
  .auth-wall-img { transition: none; transform: none; }
}
.auth-card {
  width: 100%;
  max-width: 400px;
  min-width: 0;
  box-sizing: border-box;
  padding: 26px 28px 28px;
  background: #fff;
  color: var(--charcoal);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
}
.auth-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  padding-bottom: 14px;
  border-bottom: 3px solid var(--charcoal);
}
.auth-brand img { display: block; }
.auth-brand-name {
  display: flex;
  flex-direction: column;
  font-family: 'Gloock', Georgia, serif;
  font-size: 1.05rem;
  line-height: 1.02;
}
/* Two classes, to outrank the editorial design's rule for every h1. */
/* The recipe titles' face, not the wordmark's: Archivo at its heaviest,
   set tight. Gabe, 2026-09-28: "lets lose the logo serif font in login and
   password set/recovery" - Gloock is the logo's lettering, and the logo is
   right above. */
.auth-card .auth-head {
  margin: 18px 0 6px;
  font-family: 'Archivo', Helvetica, Arial, sans-serif;
  font-weight: 800;
  font-size: 2rem;
  line-height: 1.05;
  letter-spacing: -0.02em;
  text-transform: none;
}
.auth-sub { margin: 0 0 10px; color: var(--soft); }
.auth-rules { font-size: 0.88rem; }
.auth-said { margin: 0 0 10px; color: var(--soft); }
.auth-error { margin: 0 0 10px; color: var(--flame); font-weight: 500; }
.auth-form { display: grid; gap: 18px; margin: 16px 0 0; }
.auth-field { display: grid; gap: 4px; min-width: 0; }
.auth-label,
.auth-keep span {
  font-family: 'Archivo', Helvetica, Arial, sans-serif;
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--soft);
}
.auth-field input {
  width: 100%;
  box-sizing: border-box;
  padding: 6px 0 7px;
  border: 0;
  border-bottom: 1px solid var(--charcoal);
  border-radius: 0;
  background: transparent;
  font: inherit;
  font-size: max(1.05rem, 16px);
  color: var(--charcoal);
}
.auth-field input:focus { outline: none; border-bottom-color: var(--flame); box-shadow: 0 1px 0 var(--flame); }
.auth-keep { display: flex; align-items: center; gap: 9px; cursor: pointer; }
.auth-keep input { width: 16px; height: 16px; margin: 0; accent-color: var(--charcoal); }
.auth-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 4px;
}
.auth-aside {
  font-style: italic;
  color: var(--charcoal);
  text-decoration: underline;
  text-decoration-color: var(--flame);
  text-underline-offset: 3px;
}
.auth-aside:hover { color: var(--flame); }
.auth-go {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 18px;
  border: 0;
  border-radius: 0;
  background: var(--flame);
  color: #fff;
  font-family: 'Archivo', Helvetica, Arial, sans-serif;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}
.auth-go:hover { background: var(--charcoal); }
/* A phone on its side has 375px of height: the card sits at the top, widens,
   pairs its fields two to a row, and closes its gaps, so the button is on
   screen without scrolling. */
@media (max-height: 520px) {
  .auth-page { align-items: flex-start; padding-top: 12px; padding-bottom: 12px; }
  .auth-card { max-width: 640px; padding: 14px 20px 16px; }
  .auth-brand { padding-bottom: 8px; border-bottom-width: 2px; }
  .auth-brand img { width: 26px; height: 26px; }
  .auth-brand-name { flex-direction: row; gap: 0.3em; font-size: 0.95rem; }
  .auth-card .auth-head { font-size: 1.4rem; margin: 10px 0 4px; }
  .auth-sub { margin-bottom: 4px; }
  .auth-rules { font-size: 0.8rem; }
  .auth-form { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 18px; margin-top: 8px; }
  .auth-keep, .auth-row { grid-column: 1 / -1; }
}

/* Signed in as, People, Sign out - a third item in the foot's row, in the
   colophon's small sans. Sign out is a button made to read as a link: a
   form, because a sign-out a link could trigger is one any page could. */
.footsign {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 14px;
  margin: 0;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  color: var(--muted);
}
.footsign a,
.footsign button {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--soft);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.footsign a:hover,
.footsign button:hover { color: var(--flame); }

/* ------------------------------------------------------------ people -- */
/* public/people.php: one line per person, reading first; Change opens the
   fields. The line is a wrapping row, so on a phone it folds rather than
   pushing the page sideways. */
.people-page { max-width: 980px; }
.people-list { list-style: none; margin: 18px 0 60px; padding: 0; }
.person { border-top: 1px solid var(--line); padding: 12px 0; }
.person-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 14px;
  min-width: 0;
}
.person-name { font-weight: 600; }
button.person-name {
  font: inherit;
  font-weight: 600;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-align: left;
}
button.person-name:hover { text-decoration: underline; text-underline-offset: 2px; }
.people-js .person-edit > summary { display: none; }
.people-js .person-edit { margin-top: 0; }
.person-tag {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--flame);
}
.person-login { font-family: ui-monospace, Menlo, monospace; font-size: 0.85rem; color: var(--soft); }
.person-email { color: var(--soft); overflow-wrap: anywhere; min-width: 0; }
.person-email.is-missing { color: var(--muted); font-style: italic; }
.person-when { margin-left: auto; font-size: 0.85rem; color: var(--muted); }
.people-said { margin: 8px 0 0; font-size: 0.9rem; color: var(--soft); }
.people-said.is-bad { color: var(--flame); }
.person-edit,
.people-add { margin-top: 6px; }
.person-edit > summary {
  display: inline-block;
  font-size: 0.85rem;
  color: var(--soft);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
/* Add new user is a button, the dark one the forms end with. Gabe,
   2026-10-01: "make 'ADD NEW USER' a button". */
.people-add > summary { display: inline-block; list-style: none; }
.person-edit > summary::-webkit-details-marker,
.people-add > summary::-webkit-details-marker { display: none; }
.people-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px 14px;
  margin: 12px 0 0;
}
.people-form label { display: grid; gap: 4px; min-width: 0; flex: 1 1 160px; }
.people-form label > span { font-size: 0.8rem; color: var(--muted); }
.people-form input[type="text"],
.people-form input[type="email"] {
  width: 100%;
  box-sizing: border-box;
  padding: 7px 9px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--paper);
  font: inherit;
  font-size: max(1rem, 16px);
}
.people-form input[readonly] { color: var(--muted); background: transparent; }
.people-form .people-check { display: flex; align-items: center; gap: 8px; flex: 1 1 100%; }
.people-check > span { font-size: 0.9rem; color: var(--soft); }
/* What a person may do, as switches in the Baby steps knob (2026-10-06). A
   flip saves at once; Save is only for a browser with no script. */
.people-switches { display: flex; flex-direction: column; gap: 10px; margin: 12px 0 16px; }
.people-switch { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; font-size: 0.95rem; color: var(--charcoal); }
.people-switch > input {
  appearance: none; -webkit-appearance: none; margin: 0; flex: none; cursor: pointer;
  position: relative; width: 32px; height: 16px; border: 2px solid var(--charcoal); background: transparent;
}
.people-switch > input::after {
  content: ''; position: absolute; top: 2px; left: 2px; width: 8px; height: 8px;
  background: var(--muted); transition: transform 0.15s ease;
}
.people-switch > input:checked::after { transform: translateX(16px); background: var(--flame); }
.people-switch > input:checked + span { color: var(--flame); }
.people-switch > input:focus-visible { outline: 2px solid var(--flame); outline-offset: 2px; }
.people-js .people-switches-save { display: none; }
.people-go {
  padding: 8px 16px;
  border: 0;
  border-radius: 4px;
  background: var(--charcoal);
  color: var(--cream);
  font: inherit;
  cursor: pointer;
}
.people-go:hover { background: var(--flame); }
.people-note { font-size: 0.8rem; color: var(--muted); align-self: center; }
.people-actions { display: flex; flex-wrap: wrap; gap: 8px 18px; margin: 14px 0 4px; }
.people-actions form { margin: 0; }
.people-quiet {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.9rem;
  color: var(--soft);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.people-quiet:hover { color: var(--flame); }
.people-quiet.is-danger { color: var(--flame); }
.person-must { color: var(--flame); }
.people-all { margin: 28px 0 0; padding-top: 14px; border-top: 1px solid var(--line); }
/* The three orders: plain links, the one you are on underlined in red - the
   Recently tabs' mark, at the size of the page's other quiet controls. */
.people-sort { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 16px; margin: 0 0 18px; font-size: 0.9rem; }
.people-sort-by { color: var(--muted); }
.people-sort a { color: var(--soft); text-decoration: none; padding-bottom: 3px; box-shadow: inset 0 -2px 0 transparent; }
.people-sort a:hover { color: var(--charcoal); box-shadow: inset 0 -2px 0 var(--line); }
.people-sort a.is-on { color: var(--charcoal); box-shadow: inset 0 -2px 0 var(--flame); }

/* --------------------------------------------- from a cookbook, in edit -- */
/* Under the edit bar: pick photographs of a book's pages and they are read
   into the form. A dashed box, the same invitation the empty photo gets, so
   it reads as a place to put something rather than a setting. */
.pages-read {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 16px;
  margin: 0 0 18px;
  padding: 12px 14px;
  border: 1px dashed var(--line);
  border-radius: 6px;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.85rem;
  min-width: 0;
}
.pages-pick { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; min-width: 0; }
.pages-label { font-weight: 700; }
.pages-pick input { max-width: 100%; font: inherit; }
.pages-go {
  padding: 6px 14px;
  border: 1px solid var(--charcoal);
  border-radius: 4px;
  background: transparent;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.pages-go[hidden] { display: none; }
.pages-hint { flex: 1 1 100%; color: var(--muted); }
.pages-said { flex: 1 1 100%; margin: 0; color: var(--charcoal); font-weight: 600; }
.pages-read.is-reading { border-style: solid; border-color: var(--charcoal); }
.pages-read.is-reading .pages-hint { color: var(--charcoal); }
/* The printed section a read or pasted row sits under, shown once at the top
   of its run, since the section field itself is hidden. */
.edit-new-sec {
  display: block;
  margin-top: 2px;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.person-old { color: var(--muted); }

/* ORIGINAL RECIPE, as a tag in the facts row (templates/recipe.php) - one
   outlined label in the row's own caps, standing where the source would.
   An inline-block in the cell's value line, so its text sits on the same
   baseline as the times and servings beside it. */
.meta-tag {
  vertical-align: baseline;
  display: inline-block;
  padding: 5px 9px 4px;
  border: 1.5px solid var(--flame);
  color: var(--flame);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  line-height: 1.2;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}
.meta-tag:hover { background: var(--flame); color: #fff; }

/* The recipe photograph, whole, over the page (templates/recipe.php).
   The link is invisible chrome: the photograph looks as it did, with a
   zoom cursor where a pointer can hover. */
.shot-open { display: block; cursor: zoom-in; }
.shot-view {
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: zoom-out;
}
.shot-view[open] { display: flex; align-items: center; justify-content: center; }
.shot-view::backdrop { background: rgba(24, 22, 18, 0.9); }
.shot-view img {
  display: block;
  max-width: calc(100vw - 32px);
  max-height: calc(100dvh - 32px);
  width: auto;
  height: auto;
  object-fit: contain;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}

/* The description as a field in edit mode, in the paragraph's own type
   (templates/recipe.php). */
textarea.desc-edit {
  display: block;
  width: 100%;
  box-sizing: border-box;
  min-height: 3.2em;
  padding: 6px 8px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--paper);
  font: inherit;
  color: inherit;
  resize: vertical;
}
textarea.desc-edit:focus { outline: none; border-color: var(--charcoal); }

/* **One heading bar across both columns** (templates/recipe.php,
   .cols-head): Mise en Place over the ingredients, Steps and its switches
   over the steps, in the grid's own columns (subgrid), pinned as one piece
   and held until the end of the recipe - the grid is its container, and the
   grid ends where the longer column does. Gabe, 2026-09-28: "lock this at the
   top on the way up", then "go one bar" after two separately pinned headings
   let go at different moments and Pumpkin bread's long ingredient list had
   nothing to pin against. It replaced an afternoon's version with a sticky
   Steps heading, a sticky mise column and a script keeping them in step.

   Two columns only (640px and up); a narrower page stacks the lists, so each
   keeps its own heading, printed in the column and hidden here. Pinned only
   on a screen 520px tall or more - on a phone held sideways it would take a
   fifth of the screen - where it simply sits above the columns. The 12px
   above it is air between the window's edge and the headings (40px in the
   editorial design, for Baby steps, which rises above the heading's line);
   the negative margin keeps the unpinned page where it was. */
.cols-head { display: none; }
@media (min-width: 640px) {
  .body-grid.has-cols-head > .cols-head {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    align-items: end;
    background: var(--cream);
  }
  .cols-head-mise, .cols-head-steps { min-width: 0; }
  .cols-head-mise > h2 { margin: 0 0 16px; }
  /* Check all / Remove checks, on the heading's line at the right of the
     mise cell - where "Clear all check marks" used to ride the column's
     heading. The column's own copy is for the stacked layout only. */
  .cols-head-mise { position: relative; }
  .cols-head-mise > .gather-clear { position: absolute; right: 0; top: auto; bottom: 26px; }
  .body-grid.has-cols-head > [data-swap="ingredients"] > .gather-clear { display: none; }
  /* The column's Containers switch too: the bar's is over the mise side. */
  .body-grid.has-cols-head > [data-swap="ingredients"] > .mise-tools { display: none; }
  /* The column's own copies, for the stacked layout only. */
  .body-grid.has-cols-head > [data-swap="ingredients"] > h2,
  .body-grid.has-cols-head > [data-swap="steps"] > .instructions-head { display: none; }
}
@media (min-width: 640px) and (min-height: 520px) {
  .body-grid.has-cols-head > .cols-head {
    position: sticky;
    top: 0;
    z-index: 6;
    padding-top: 12px;
    margin-top: -12px;
  }
  /* The mise column still pins, now under the bar rather than at the
     window's edge: --cols-head-h is the bar's height, measured by the page
     (a guess until then). It lets go before the bar does, and its list
     slides up beneath the bar - the bar has the page's ground behind it. */
  .body-grid.has-cols-head > [data-swap="ingredients"] {
    top: var(--cols-head-h, 64px);
    max-height: calc(100vh - var(--cols-head-h, 64px) - 12px);
  }
}

/* On a recipe page the bar and the foot take the page's own width, so the
   logo and the search sit on the photograph's edges rather than out past
   them. Gabe, 2026-09-28: "when the window is very wide we limit the content
   at some point ... but the logo and search keep going wider which breaks
   the alignment with the edges". The bar was capped at --wide (1468px), the
   index's width, and a recipe is --measure (1180px editorial, 760px kitchen)
   - off by 146px a side at any window past about 1,200px. Same gutter both
   sides, so the insides line up. The index keeps --wide: its cards run
   that far. */
body[data-slug] .topbar { max-width: var(--measure); box-sizing: border-box; }
body[data-slug] .sitefoot { max-width: var(--measure); margin-left: auto; margin-right: auto; box-sizing: border-box; }

/* Recently opened at the foot of a recipe (public/recipe.php): the home
   page's small row inside the recipe's own frame, which already has the
   gutter - so the row's head and strip drop theirs, and the first card sits
   on the recipe's left edge. A rule above it, like the note's. */
.recipe-opened { margin-top: 40px; padding-bottom: 0; }
.recipe-opened > .row { border-top: 1px solid var(--line); padding-top: 18px; }
.recipe-opened .row-head,
.recipe-opened .row-strip { padding-left: 0; padding-right: 0; }

/* Baby steps on: the master whose cards are coming out, lit for a moment
   in the brand red with its words in white (templates/recipe.php, lit()).
   Important because the deal paints each card's ground inline. */
ol.steps > li.is-lit { background: var(--flame, #C92A12) !important; }
ol.steps > li.is-lit, ol.steps > li.is-lit * { color: #fff !important; }
ol.steps > li.lit-fade, ol.steps > li.lit-fade * {
  transition: background-color var(--lit-fade, 0.18s) ease-out, color var(--lit-fade, 0.18s) ease-out !important;
}

/* A row that scrolls sideways says so (nav.js, the end): the last card in
   view cut about in half, and round arrows at each end. Gabe, 2026-09-29. */
.rows .row .row-strip.has-peek > .card { flex-basis: var(--peek); width: var(--peek); }
.row-arrow {
  position: absolute;
  z-index: 3;
  width: 44px;
  height: 44px;
  margin-top: -22px;
  padding: 0;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--cream);
  color: var(--charcoal);
  box-shadow: 0 2px 10px rgba(42, 38, 34, 0.18);
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.15s ease-out, visibility 0s linear 0.15s, background-color 0.1s, color 0.1s;
}
.row-arrow.is-on { opacity: 1; visibility: visible; transition: opacity 0.15s ease-out, background-color 0.1s, color 0.1s; }
.row-arrow:hover { background: var(--flame, #C92A12); border-color: var(--flame, #C92A12); color: #fff; }
.row-arrow-next { right: 12px; }
.row-arrow-prev { left: 12px; }
.row-arrow-prev svg { transform: scaleX(-1); }
.row-small .row-arrow { width: 36px; height: 36px; margin-top: -18px; }

/* ----------------------------------------------- dinner plans (116) -- */
/* public/meal.php and meals.php. Laid out after the hand-made Miso Black Cod
   Dinner page: the clock leads each stage and is the biggest thing in it,
   the whole schedule reads at a glance first, the ingredients sit in a mise
   column, and a key step or an either/or is a box. Both designs read these;
   editorial.css only changes the face and squares the corners. */
.meal-page { max-width: 1080px; padding-bottom: 48px; }
.meal-title { margin: 6px 0 8px; }
.meal-desc { color: var(--soft); max-width: 62ch; margin: 0 0 16px; font-size: 1.1rem; line-height: 1.5; }
.meal-said { color: var(--flame); font-weight: 600; margin: 0 0 8px; }
/* Plan deleted. Undo - the word a link, in the notice's own type. */
.meal-undo button { margin-left: 4px; padding: 0; background: none; border: 0; color: inherit; font: inherit; text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
.meal-why { color: var(--flame); }

/* **A menu card** (punch 160): the Claude Design mock B, 2026-10-03.
   Gabe, 2026-10-02: "Let's see the plan title in a menu-esque layout".
   Everything above At a glance is set on the center line, as a menu is:
   the plan's name and size in the kicker, Dinner Plan, when it is on the
   table, the photographs, then the menu itself between two short rules.
   Gloock for the names, Newsreader italic for the words a menu would set
   under them (layout.php loads it for this page only, $menuFace). The
   steps below keep the page's left edge: they are read, not looked at. */
header.meal-head { margin: 0; padding: 40px 0 0; border-bottom: 0; text-align: center; }
.meal-kicker { margin: 0 0 14px; font: 700 0.6875rem/1.4 Helvetica, Arial, sans-serif; letter-spacing: 0.22em; text-transform: uppercase; color: var(--flame); }
.meal-head h1 { margin: 0 auto 10px; font-family: Gloock, Georgia, serif; font-weight: 400; font-size: clamp(2.75rem, 7vw, 4.5rem); line-height: 1.02; letter-spacing: 0; }
/* "on the table at 7:00 pm", one line in the menu's italic, the time in
   it as a field with no box - underlined when pointed at or typed in, so
   it still reads as one. The hours open under it in the page's own face. */
.meal-head .meal-serve { font: italic 400 1.1875rem/1.4 Newsreader, Georgia, serif; color: var(--muted); }
.meal-serve-label { margin-right: 0.3em; }
.meal-head .meal-serve-row { display: inline-flex; vertical-align: baseline; }
.meal-head .meal-serve .meal-serve-input {
  width: 4.4em; padding: 0; border: 0; border-bottom: 1px solid transparent; border-radius: 0;
  background: none; color: var(--charcoal); font: inherit; text-align: left;
}
.meal-head .meal-serve .meal-serve-input:hover,
.meal-head .meal-serve .meal-serve-input:focus { border-bottom-color: var(--charcoal); outline: 0; }
.meal-head .meal-serve .meal-serve-input[aria-invalid] { border-bottom-color: var(--flame); }
.meal-head .meal-serve .meal-serve-pick { padding: 0 2px 0 4px; border: 0; background: none; }
.meal-head .meal-serve .meal-serve-pick:hover,
.meal-head .meal-serve .meal-serve-pick[aria-expanded="true"] { background: none; }
.meal-head .meal-hours { font: normal 400 1rem/1.2 var(--sans, Helvetica, Arial, sans-serif); text-align: left; }
.meal-head .meal-serve-back { font-size: 0.95rem; }

.meal-serve-row { display: flex; align-items: center; gap: 8px; }
.meal-serve input { font: inherit; font-size: 1.15rem; padding: 2px 6px; border: 1px solid var(--line); border-radius: 6px; background: var(--paper); color: var(--charcoal); }
.meal-serve button { font: 700 0.75rem/1 Helvetica, Arial, sans-serif; letter-spacing: 0.1em; text-transform: uppercase; padding: 9px 12px; border: 0; border-radius: 6px; background: var(--charcoal); color: var(--cream); cursor: pointer; }
.meal-serve button:hover { background: var(--flame); }
.meal-serve-back { display: block; margin-top: 4px; font-size: 0.8rem; color: var(--muted); }
/* Typed, with the hours a click away (templates/meal.php). Set is for a page
   with no script; with one, a change sends itself. */
.has-script .meal-serve-go { display: none; }
.meal-serve-row { position: relative; gap: 0; }
.meal-serve .meal-serve-input { width: 6.2em; border-right: 0; border-radius: 6px 0 0 6px; }
.meal-serve .meal-serve-input[aria-invalid] { border-color: var(--flame); color: var(--flame); }
.meal-serve .meal-serve-pick {
  display: inline-flex; align-items: center; justify-content: center; align-self: stretch;
  padding: 0 9px; border: 1px solid var(--line); border-radius: 0 6px 6px 0;
  background: var(--paper); color: var(--muted);
}
.meal-serve .meal-serve-pick[hidden] { display: none; }
.meal-serve .meal-serve-pick:hover,
.meal-serve .meal-serve-pick[aria-expanded="true"] { background: var(--paper); color: var(--flame); border-color: var(--flame); }
.meal-serve-go { margin-left: 8px; }
.meal-hours {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 30;
  display: grid; grid-template-columns: 1fr 1fr; width: 15rem;
  max-height: 27rem; overflow-y: auto;
  background: var(--paper); border: 1px solid var(--line); border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.meal-hours-col { display: flex; flex-direction: column; padding: 6px 0; position: relative; }
.meal-hours-col + .meal-hours-col { border-left: 1px solid var(--line); }
.meal-hours-h {
  margin: 0 0 4px; padding: 4px 0; text-align: center;
  font: 700 0.7rem/1 Helvetica, Arial, sans-serif; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
}
.meal-serve .meal-hours button {
  display: block; width: 100%; padding: 6px 0 6px 1.4rem; border: 0; border-radius: 0;
  background: none; color: var(--charcoal); text-align: left; letter-spacing: 0; text-transform: none;
  font-family: inherit; font-size: 0.95rem; font-weight: 400; line-height: 1.2; font-variant-numeric: tabular-nums;
}
.meal-serve .meal-hours button small { font-size: 0.75em; color: var(--muted); }
.meal-serve .meal-hours button:hover,
.meal-serve .meal-hours button:focus-visible { background: var(--cream); color: var(--flame); outline: 0; }
.meal-serve .meal-hours button.is-on { font-weight: 700; background: var(--cream); }
.meal-serve .meal-hours button.is-last { border-bottom: 1px solid var(--line); }

/* The photographs, square and in a row, each the way to its recipe: up to
   180px, the row no wider than 760, a course's numeral under each when the
   dinner is in courses. Gabe chose this (B) over a card with none (A),
   2026-10-03: it "bridges the classy/editorial gap". */
.meal-shots { list-style: none; display: flex; justify-content: center; gap: 16px; max-width: 760px; margin: 34px auto 0; padding: 0; }
.meal-shots li { flex: 0 1 180px; min-width: 0; }
.meal-shots a { display: block; color: var(--muted); text-decoration: none; }
.meal-shots img { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 6px; background: var(--line); }
.meal-shot-n { display: block; margin-top: 8px; font: italic 400 0.9375rem/1 Newsreader, Georgia, serif; text-align: center; }

.meal-rule { width: 40px; height: 1px; margin: 44px auto; background: var(--charcoal); }

/* The menu: each course under its numeral, name and time, the main and
   then its sides under "with", and under each dish its line in italic -
   Gabe, 2026-10-03: "that sells the menu idea". */
.meal-card { list-style: none; margin: 0; padding: 0; text-align: center; }
.meal-card-date { margin: -14px 0 34px; text-align: center; font: italic 400 1.125rem/1 Newsreader, Georgia, serif; color: var(--muted); }
.meal-course + .meal-course { margin-top: 46px; }
.meal-course-h {
  display: flex; align-items: center; justify-content: center; gap: 8px; margin: 0 0 16px;
  font: 700 0.6875rem/1.4 Helvetica, Arial, sans-serif; letter-spacing: 0.22em; text-transform: uppercase; color: var(--muted);
}
.meal-course-h .meal-dish-dot { width: 8px; height: 8px; margin: 0; }
.meal-card-name { margin: 0; font: 400 2rem/1.1 Gloock, Georgia, serif; text-wrap: balance; }
.meal-card-name a { color: var(--charcoal); text-decoration: none; }
.meal-card-name a:hover { color: var(--flame); }
.meal-card-dish.is-side .meal-card-name { font-size: 1.625rem; }
.meal-card-line { max-width: 34em; margin: 8px auto 0; font: italic 400 1.125rem/1.45 Newsreader, Georgia, serif; color: var(--soft); text-wrap: balance; }
.meal-card-with { margin: 14px 0; font: italic 400 1.125rem/1 Newsreader, Georgia, serif; color: var(--muted); }
.meal-card-dish + .meal-card-dish { margin-top: 22px; }
.meal-dish-dot { display: inline-block; width: 10px; height: 10px; margin-right: 8px; border-radius: 50%; background: var(--dish, var(--muted)); }
.meal-dish-remove { margin: 6px 0 0; text-align: center; }

/* Under the menu, the dinner's note and Start, still on the center line. */
.meal-intro { text-align: center; }
.meal-head + .meal-intro { margin-top: 36px; }
.meal-intro .meal-desc { max-width: 620px; margin: 0 auto 26px; font: 400 1.0625rem/1.6 Newsreader, Georgia, serif; color: var(--soft); }
.meal-intro .meal-run { justify-content: center; }

/* A column's heading, as the recipe page's MISE EN PLACE and STEPS. */
.meal-col-h { font: 800 0.75rem/1.2 Helvetica, Arial, sans-serif; letter-spacing: 0.14em; text-transform: uppercase; color: var(--charcoal); margin: 0 0 4px; padding: 0 0 10px; border-bottom: 2px solid var(--charcoal); }

/* **At a glance.** A list - the time, then what happens - and on a screen
   700px wide or more, when meal_glance() found columns, a table: a column
   to a course (or a dish), each cell what that dish has done to it then.
   Gabe, 2026-10-03: "I like the table visualization". A course going out
   tints its own cell only: "the double band erodes clarity". The cells are
   hidden from a screen reader, which reads the time and the title. */
.meal-glance { margin: 56px 0 0; max-width: 760px; }
.meal-glance.is-grid { max-width: none; }
.meal-glance-top { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 4px 16px; margin: 0 0 4px; padding: 0 0 10px; border-bottom: 2px solid var(--charcoal); }
.meal-glance-top .meal-col-h { margin: 0; padding: 0; border: 0; }
.meal-glance-now { margin: 0; font-weight: 700; font-size: 0.9rem; color: var(--flame); }
.meal-glance-now[hidden] { display: none; }
.meal-glance ol { list-style: none; margin: 0; padding: 0; }
.meal-glance a { display: grid; grid-template-columns: 5.2em 3.8em minmax(0, 1fr); gap: 12px; align-items: baseline; padding: 9px 0; border-bottom: 1px solid var(--line); color: var(--charcoal); text-decoration: none; }
.meal-glance a:hover .meal-glance-what,
.meal-glance a:hover .meal-glance-clock { color: var(--flame); }
.meal-glance-time { display: contents; }
.meal-glance-clock { font-weight: 800; font-size: 1.1rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.meal-glance-clock small, .meal-clock small { font-size: 0.6em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; }
.meal-glance-off { color: var(--muted); font-size: 0.85rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.meal-glance-nowtag { display: none; }
.meal-glance-cols, .meal-glance-cell { display: none; }
.meal-glance li.is-serving .meal-glance-clock { color: var(--flame); }
@media (min-width: 700px) {
  .meal-glance.is-grid .meal-glance-cols,
  .meal-glance.is-grid a { display: grid; grid-template-columns: 10em repeat(var(--cols), minmax(0, 1fr)); gap: 0; align-items: stretch; padding: 0; }
  .meal-glance-cols { border-bottom: 1px solid var(--charcoal); }
  .meal-glance-cols > span {
    padding: 10px 16px; font: 800 0.6875rem/1.35 Helvetica, Arial, sans-serif; letter-spacing: 0.2em; text-transform: uppercase;
  }
  .meal-glance-cols > span + span { border-left: 1px solid var(--charcoal); }
  .meal-glance-cols .meal-dish-dot { width: 8px; height: 8px; }
  .meal-glance.is-grid .meal-glance-time { display: flex; align-items: baseline; gap: 10px; padding: 9px 16px 9px 0; }
  .meal-glance.is-grid .meal-glance-what { display: none; }
  .meal-glance.is-grid .meal-glance-cell { display: block; padding: 9px 16px; border-left: 1px solid var(--charcoal); font-size: 0.875rem; line-height: 1.35; }
  .meal-glance-cell.is-span { grid-column: 2 / -1; }
  .meal-glance-cell.is-serve { background: color-mix(in srgb, var(--flame) 10%, var(--paper)); font-weight: 800; }
  .meal-glance-cell .meal-dish-dot { width: 8px; height: 8px; margin: 0; }
}

.meal-body { display: grid; grid-template-columns: minmax(0, 1fr); gap: 36px; margin-top: 44px; }
/* The bar (templates/meal.php, .meal-cols-head) is the recipe page's
   .cols-head: both columns' headings in the grid's own columns, pinned as
   one piece until the plan ends. Two columns only; stacked, each column
   keeps its own heading. Pinned only on a screen 520px tall or more, as
   there. The mise column is not pinned: it scrolls with the page. Pinned,
   it scrolled inside itself, and Gabe, 2026-10-01: the ingredients are
   always shorter than the steps, so a box of their own only hid the bottom
   of the list. */
.meal-cols-head { display: none; }
@media (min-width: 900px) {
  .meal-body { grid-template-columns: minmax(240px, 300px) minmax(0, 1fr); column-gap: 48px; row-gap: 0; align-items: start; }
  .meal-body.has-cols-head > .meal-cols-head {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    background: var(--cream);
    padding-bottom: 4px;
  }
  .meal-cols-head .meal-col-h { margin: 0; }
  .meal-body.has-cols-head > section > .meal-col-h {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  /* The bar has the column's Check all; this copy sat on the stepper. */
  .meal-body.has-cols-head > .meal-mise > .gather-clear { display: none; }
}
@media (min-width: 900px) and (min-height: 520px) {
  .meal-body.has-cols-head > .meal-cols-head {
    position: sticky;
    top: 0;
    z-index: 6;
    padding-top: 12px;
    margin-top: -12px;
  }
  .meal-stage { scroll-margin-top: 58px; }
}

.meal-mise ul { list-style: none; margin: 0; padding: 0; }
/* Each line a tick, as on a recipe (.gather, gather.js): the box, then the
   amount and the name in their two columns, struck through when ticked. */
.meal-mise { position: relative; }
.meal-mise li { padding: 10px 0; border-bottom: 1px solid var(--line); line-height: 1.35; }
.meal-mise .gather { gap: 10px; cursor: pointer; }
.meal-mise .gather input[type=checkbox] { --lh: 1.35; }
.meal-line { flex: 1 1 auto; min-width: 0; display: grid; grid-template-columns: 7em minmax(0, 1fr); gap: 12px; }
.meal-mise > .gather-clear { top: 0; right: 0; }
/* The servings stepper (.scaler, the recipe page's) at the column's head. */
.meal-scaler { margin: 0 0 6px; }
.meal-cols-mise { position: relative; }
.meal-cols-mise > .gather-clear { top: auto; right: 0; bottom: 12px; }
/* In the pan: the lines a step quotes, small, under its text. */
.meal-pan { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 20px; font-size: 0.9rem; }
.meal-pan .gather { gap: 8px; cursor: pointer; color: var(--soft); }
.meal-pan .gather input[type=checkbox] { --lh: 1.4; }
.meal-amt { font-weight: 700; color: var(--charcoal); }
/* By dish, in the order the lines go in (meal_mise_groups(), punch 129). */
.meal-mise-group { margin: 22px 0 4px; font: 800 0.7rem/1.2 Helvetica, Arial, sans-serif; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }
.meal-mise-group:first-of-type { margin-top: 8px; }
/* Lines that share a container read as one block: no rule between them. */
.meal-mise li:has(+ li.is-with) { border-bottom-color: transparent; padding-bottom: 2px; }
.meal-mise li.is-with { padding-top: 2px; }
.meal-mise-n { flex: none; min-width: 1.6em; padding-top: 1px; font: 700 0.78rem/1.35 Helvetica, Arial, sans-serif; color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
.meal-ing { color: var(--soft); }
.meal-note { display: block; font-size: 0.85rem; color: var(--muted); }
/* A converted dish's bins (punch 224): the number in the dish's color with
   its size under it, as on the recipe page; each item one sentence, the
   thing in bold. The same number marks the line in a step's pan. `.gather`
   in these: editorial greys everything in a `.gather`. */
.meal-bins .gather .meal-mise-n { color: var(--dish, var(--muted)); }
.meal-do { flex: 1 1 auto; min-width: 0; color: var(--soft); }
.meal-do .core { font-weight: 700; }
.meal-bins li.is-mix .meal-do { font-style: italic; color: var(--muted); }
.meal-bins li.unscaled .meal-do::after { content: " not scaled"; font: 0.66rem Helvetica, Arial, sans-serif; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
.meal-bench-h { margin: 14px 0 0; font: 700 0.68rem/1.2 Helvetica, Arial, sans-serif; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.meal-pan .gather .bin-chip { background: var(--dish, var(--muted)); color: #fff; box-shadow: none; }

.meal-stages { list-style: none; margin: 0; padding: 0; }
/* The clock beside the stage on a wide screen, above it on a narrow one. */
.meal-stage { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; padding: 24px 0; border-bottom: 1px solid var(--line); scroll-margin-top: calc(var(--topbar-h, 88px) + 12px); }
.meal-stage:last-child { border-bottom: 0; }
@media (min-width: 640px) {
  .meal-stage { grid-template-columns: 7.5em minmax(0, 1fr); gap: 20px; }
}
.meal-clock-time { display: block; font-size: 2.1rem; font-weight: 800; line-height: 1; color: var(--flame); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; white-space: nowrap; }
.meal-clock-off { display: block; margin-top: 6px; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
@media (max-width: 639px) {
  .meal-clock { display: flex; align-items: baseline; gap: 12px; }
  .meal-clock-off { margin-top: 0; }
}
.meal-stage-body h3 { font-size: 1.3rem; line-height: 1.2; margin: 0 0 12px; }
.meal-step { max-width: 66ch; margin: 0 0 14px; }
.meal-step p { margin: 0; line-height: 1.6; }
.meal-step.is-key { background: color-mix(in srgb, var(--flame) 8%, var(--cream)); border-left: 4px solid var(--flame); border-radius: 0 6px 6px 0; padding: 12px 16px; }
.meal-step.is-conditional { border: 1px dashed var(--muted); border-radius: 6px; padding: 12px 16px; }
.meal-tag-row { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 0 0 4px !important; }
.meal-tag { font-size: 0.7rem; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; color: var(--flame); }

/* Each dish its color, set on the step as --dish (lib/meal.php,
   meal_dish_colors(): blue for seafood, green for vegetables, a wine red for
   beef, plain colors otherwise). A plain step takes it as a rule down its
   left side; a key step keeps its flame rule, and says its dish in the
   label. */
.meal-dish { display: inline-flex; align-items: center; gap: 6px; font-size: 0.7rem; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; color: var(--dish); }
.meal-dish::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--dish); }
.meal-step.has-dish:not(.is-key):not(.is-conditional) { border-left: 3px solid var(--dish); padding-left: 14px; }
.meal-step.is-conditional.has-dish { border-left: 3px solid var(--dish); }
.meal-foot { color: var(--muted); font-size: 0.9rem; margin-top: 32px; }

/* Start, and keeping up once started (templates/meal.php, .meal-run). */
.meal-run { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; margin: 16px 0 0; }
.meal-start { display: inline-flex; align-items: center; height: 46px; padding: 0 26px; background: var(--flame); color: #fff; font: 700 0.75rem/1 Helvetica, Arial, sans-serif; letter-spacing: 0.18em; text-transform: uppercase; text-decoration: none; border-radius: 6px; }
.meal-start:hover { background: var(--charcoal); }
.meal-run-hint { color: var(--muted); font-size: 0.9rem; }
/* Write the plan, on a rough plan (punch 151): in the Dinner page's step
   buttons' clothes, since Start now is this page's flame. */
.meal-write { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px 14px; margin: 14px 0 0; }
.meal-write button {
  height: 44px; padding: 0 18px;
  border: 1px solid var(--line); border-radius: 0; background: none; color: var(--charcoal);
  font: 700 0.8rem/1 Helvetica, Arial, sans-serif; letter-spacing: 0.12em; text-transform: uppercase;
  cursor: pointer;
}
.meal-write button:hover { border-color: var(--flame, #b8462f); color: var(--flame, #b8462f); }
.meal-write-note { color: var(--muted); font-size: 0.9rem; max-width: 62ch; }
.meal-run.is-live a { font-weight: 700; color: var(--charcoal); }
.meal-run.is-live a:hover { color: var(--flame); }
.meal-run .meal-stop { color: var(--muted); font-weight: 400; }
.meal-live { display: inline-flex; align-items: center; gap: 8px; font: 800 0.75rem/1 Helvetica, Arial, sans-serif; letter-spacing: 0.16em; text-transform: uppercase; color: var(--flame); }
.meal-live::before { content: ""; width: 10px; height: 10px; border-radius: 50%; background: var(--flame); animation: meal-pulse 2s ease-in-out infinite; }
@keyframes meal-pulse { 50% { opacity: 0.25; } }
@media (prefers-reduced-motion: reduce) { .meal-live::before { animation: none; } }
.meal-stage-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 16px; margin: 0 0 12px; }
.meal-stage-head h3 { margin: 0; }
.meal-here-row { display: flex; align-items: baseline; gap: 12px; margin: 0; }
.meal-here { font-size: 0.8rem; font-weight: 700; color: var(--flame); white-space: nowrap; }

/* The rabbit: a time label the script runs down the times column at where
   the plan says the cook should be - no line across the steps, which it
   would cross out. Gabe, 2026-10-01: "no bar just label moving down the
   times column". Its point faces the steps. Nothing fades behind it (punch
   179). */
/* Down the right edge since 2026-10-02, in a lane of its own. Gabe: "the
   descending 'time now' indicator it can just run down the right side of the
   screen" - on anything narrower than the desktop the times column stacks
   over each stage, and a label in it sat on the stage's title. The lane is
   there only while the plan runs (data-live); the time stacks over its
   am/pm so the lane stays narrow. Its point faces the steps, now leftward. */
.meal-stages { position: relative; }
.meal-stages[data-live] { padding-right: 64px; }

/* Editing a plan's steps (punch 125): the step's words in a field where the
   step stands, and its Key step switch under it. */
.meal-edit-text { display: block; box-sizing: border-box; width: 100%; min-height: 4.5em; margin: 0; padding: 8px 10px; border: 1px solid var(--line); background: var(--paper, #fff); color: inherit; font: inherit; line-height: 1.5; resize: vertical; overflow-y: hidden; }
.meal-edit-text:focus { outline: 2px solid var(--flame); outline-offset: 1px; }
.meal-key-switch { display: flex; width: fit-content; align-items: center; gap: 8px; margin: 8px 0 4px; font: 700 0.72rem/1 Helvetica, Arial, sans-serif; letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer; }
.meal-key-switch input { width: 18px; height: 18px; margin: 0; accent-color: var(--flame); }
.meal-now { position: absolute; right: 0; z-index: 3; pointer-events: none; transform: translateY(-50%); transition: top 0.6s ease; box-sizing: border-box; width: 56px; padding: 6px 4px; background: var(--flame); color: #fff; font: 800 0.7rem/1.2 Helvetica, Arial, sans-serif; letter-spacing: 0.06em; text-align: center; text-transform: uppercase; white-space: normal; border-radius: 0 3px 3px 0; }
.meal-now::after { content: ""; position: absolute; right: 100%; top: 50%; border: 0.6rem solid transparent; border-right: 0.5rem solid var(--flame); border-left: 0; height: 0; transform: translateY(-50%); }
@media (prefers-reduced-motion: reduce) { .meal-now { transition: none; } }

/* There is no score. A count of minutes behind, flame-red from five, sat here
   until 2026-10-03; Gabe: "yes drop the behind count. 'grading the cook
   against the clock.'" What stays: a quiet Marked on the stage the cook said
   they are at, and Dinner h:mm in the live bar, shown only at five minutes or
   more off, which moves the table to where the cook is. See LOCKED.md, *Plans
   do not rush the cook*. */
.meal-marked { font-size: 0.8rem; font-weight: 700; color: var(--muted); white-space: nowrap; }
.meal-run.is-live .meal-retime[hidden], .meal-marked[hidden] { display: none; }
/* No fading of the stages the clock has passed (punch 179). Gabe, 2026-10-03:
   "no fading - we don't want to stress people." The marker says where the
   clock is; nothing is dimmed for being behind it. See LOCKED.md, *Plans do
   not rush the cook*. */
/* The stage the rabbit is on: a black row, the offset turned to Now. The
   black runs 10px past the row both sides by shadow, so nothing moves. */
.meal-glance li.is-now a { background: var(--charcoal); color: var(--cream); box-shadow: -10px 0 0 var(--charcoal), 10px 0 0 var(--charcoal); }
.meal-glance li.is-now .meal-glance-clock,
.meal-glance li.is-now a:hover .meal-glance-what { color: var(--cream); }
.meal-glance li.is-now .meal-glance-what { font-weight: 700; }
.meal-glance li.is-now .meal-glance-off { display: none; }
.meal-glance li.is-now .meal-glance-nowtag { display: inline; color: #ff8a73; font-size: 0.85rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
.meal-glance li.is-now .meal-glance-cell { border-left-color: rgba(255, 255, 255, 0.35); }
.meal-glance li.is-now .meal-glance-cell.is-serve { background: none; color: #ff8a73; }
.meal-list { list-style: none; margin: 0 0 24px; padding: 0; }
.meal-list li { padding: 10px 0; border-bottom: 1px solid var(--line); }
.meal-list a { font-weight: 600; font-size: 1.1rem; color: var(--charcoal); }
.meal-list-meta { display: block; color: var(--muted); font-size: 0.9rem; }
/* Dinner plans: rough on the left, written on the right, each under its
   own heading (Gabe, 2026-10-03); stacked when two would be too narrow. */
.meals-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0 40px; align-items: start; }
.meals-group { margin: 8px 0 0; }
.meals-none { padding: 10px 0; }
@media (max-width: 640px) { .meals-cols { grid-template-columns: 1fr; } }
.sitefoot-links { margin: 0 0 6px; text-align: center; font-size: 0.9rem; }
.sitefoot-links a { color: var(--muted); }

/* ------------------------------------------------------- office (live) -- */
/* public/office.php: the owner's jobs on the live site, one page. */
/* The office's own row of pages, under the masthead on each of them
   (templates/office-doors.php). The masthead's width and gutter, so it lines
   up under the logo on every office page whatever width the page below is. */
.office-bar { border-bottom: 1px solid var(--line); background: var(--cream); }
.office-bar-in { position: relative; display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 8px; max-width: var(--wide); margin: 0 auto; padding: 10px var(--gutter); box-sizing: border-box; }
.office-bar a { padding: 6px 12px; border: 1px solid var(--line); border-radius: 999px; color: var(--charcoal); text-decoration: none; font-weight: 600; font-size: 0.92rem; }
.office-bar a:hover { border-color: var(--charcoal); }
.office-bar a[aria-current="page"] { background: var(--charcoal); border-color: var(--charcoal); color: var(--cream); }
.office-bar .office-bar-out { border-color: transparent; color: var(--muted); font-weight: 500; }
/* The chips centered (Gabe, 2026-10-06); the way out sits at the right edge
   while there is room beside them, and joins the row when there isn't. */
@media (min-width: 760px) { .office-bar .office-bar-out { position: absolute; right: var(--gutter); top: 50%; transform: translateY(-50%); } }
.office-bar .office-bar-out:hover { border-color: transparent; color: var(--charcoal); }
.office-page { max-width: 980px; padding-bottom: 48px; }
.office-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 16px; }
.office-title { margin: 28px 0 10px; }
/* Open all / Close all, beside the page's title. */
.office-folds { display: flex; gap: 6px; margin: 0; }
.office-folds button, .office-folds a { text-decoration: none; font: inherit; font-size: 0.85rem; padding: 4px 10px; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--muted); cursor: pointer; }
.office-folds button:hover, .office-folds a:hover { border-color: var(--charcoal); color: var(--charcoal); }
.office-folds button.is-on, .office-folds a.is-on { color: var(--charcoal); border-color: var(--charcoal); }
.punch-sort { margin-left: auto; }
/* A section is a <details>; its heading is the summary, with a count that
   still says something when it is folded. */
.office-section { padding: 14px 0 8px; border-top: 1px solid var(--charcoal); margin-top: 18px; scroll-margin-top: calc(var(--topbar-h, 88px) + 12px); }
.office-section > summary { display: flex; align-items: baseline; gap: 8px; cursor: pointer; list-style: none; margin: 0 0 8px; }
.office-section > summary::-webkit-details-marker { display: none; }
.office-section:not([open]) > summary { margin-bottom: 0; }
.office-section h2 { margin: 0; font-size: 1.2rem; }
.office-count { color: var(--muted); font-size: 0.85rem; font-variant-numeric: tabular-nums; }
.office-page .twirl { flex: none; display: inline-block; width: 0.8em; color: var(--charcoal); font-size: 1.5rem; line-height: 0.6; transition: transform 0.15s ease; }
.office-page details[open] > summary > .twirl { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .office-page .twirl { transition: none; } }
/* A box on each line that can be acted on, and All above them. */
.office-tick { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.office-tick input { width: 18px; height: 18px; margin: 0; accent-color: var(--charcoal); cursor: pointer; }
.office-bulk { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 4px 0 8px; border-bottom: 1px solid var(--line); }
.office-bulk .office-btn { margin-top: 0; }
.office-all { font-weight: 600; margin-right: 4px; }
.office-sub { color: var(--soft); margin: 0 0 12px; max-width: var(--measure, 760px); }
.office-said { font-weight: 600; color: var(--charcoal); margin: 0 0 10px; }
.office-said.is-problem { color: var(--flame); }
.office-counts { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 4px 24px; }
.office-counts li { display: flex; gap: 10px; align-items: baseline; padding: 4px 0; }
.office-counts li.is-clear { color: var(--muted); }
.office-n { min-width: 3.5em; text-align: right; font-variant-numeric: tabular-nums; font-weight: 700; }
.office-counts a { color: var(--charcoal); font-weight: 600; }
/* Duplicates (punch 197): a name that might be one recipe twice, its copies
   side by side, each with the lines only it has; a copy ticked to delete
   carries the flame border while it is ticked. */
.office-dupe { padding: 10px 0 14px; border-bottom: 1px solid var(--line); }
.office-dupe-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 12px; }
.office-dupe-head h3 { margin: 0; font-size: 1.05rem; }
.office-why { margin: 2px 0 8px; font-size: 0.9rem; color: var(--muted); max-width: var(--measure, 760px); }
.office-copies, .office-names, .office-trash, .office-photos { list-style: none; margin: 0; padding: 0; }
.office-copies { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; }
.office-copy { display: grid; grid-template-columns: 72px 1fr; gap: 8px 12px; align-items: start; padding: 10px; border: 1.5px solid var(--line); border-radius: 8px; }
.office-copy:has(input:checked) { border-color: var(--flame); }
.office-copy img, .office-copy .office-nophoto { width: 72px; height: 72px; }
.office-copy-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.office-copy-text a { color: var(--charcoal); font-weight: 600; }
.office-copy .office-tick { margin-top: 6px; font-size: 0.9rem; }
.office-copy:has(input:checked) .office-tick { color: var(--flame); font-weight: 600; }
.office-only { grid-column: 1 / -1; font-size: 0.85rem; }
.office-only p { margin: 0; color: var(--muted); font-weight: 600; }
.office-only ul { margin: 2px 0 0; padding-left: 18px; }
/* The names that are different recipes, and the ones marked Not the same:
   each a closed line under the list. */
.office-apart { margin-top: 14px; }
.office-apart > summary { display: flex; align-items: baseline; gap: 8px; cursor: pointer; list-style: none; font-weight: 600; }
.office-apart > summary::-webkit-details-marker { display: none; }
.office-apart > .office-sub { margin: 6px 0 4px; }
.office-names li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; padding: 6px 0; border-bottom: 1px solid var(--line); }
.office-names .office-meta { display: inline; width: auto; flex: 1 1 220px; }
.office-names .office-btn, .office-dupe-head .office-btn, .office-apart .office-sub .office-btn { margin-top: 0; }
.office-keep { color: var(--muted); font-size: 0.9rem; }
.office-meta { display: block; width: 100%; color: var(--muted); font-size: 0.85rem; }
.office-meta a { color: inherit; }
.office-btn { font: inherit; padding: 6px 12px; border: 1px solid var(--charcoal); border-radius: 6px; background: var(--charcoal); color: #fff; cursor: pointer; margin-top: 10px; }
.office-btn.is-quiet { background: transparent; color: var(--flame); border-color: var(--flame); }
.office-btn.is-plain { background: transparent; color: var(--charcoal); border-color: var(--line); padding: 4px 10px; font-size: 0.85rem; }
.office-btn.is-plain:hover { border-color: var(--charcoal); }
.office-trash li { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 8px 0; border-bottom: 1px solid var(--line); }
.office-trash img, .office-nophoto { width: 56px; height: 56px; object-fit: cover; border-radius: 6px; background: var(--line); flex: none; }
.office-what { flex: 1 1 200px; min-width: 0; }
.office-trash .office-btn { margin-top: 0; }
.office-photos { columns: 2 260px; column-gap: 24px; }
.office-photos li { padding: 3px 0; break-inside: avoid; }

/* --------------------------------------------- the punch list (office) -- */
/* public/punch.php, moved from the laptop's tools page (tools.css) on
   2026-10-04, punch 195. Sections and rows are <details>; a row's summary is
   also where a drag starts (assets/punch.js). */
.punch-sub { color: var(--soft); margin: 4px 0 14px; max-width: var(--measure, 760px); font-size: 0.92rem; }
.punch-page .twirl {
  flex: none;
  display: inline-block;
  width: 0.8em;
  color: var(--muted);
  font-size: 0.85em;
  line-height: 1;
  transition: transform 160ms ease;
}
.punch-page details[open] > summary > .twirl { transform: rotate(90deg); }
.punch-group > summary,
:is(.punch, .punch-closed) details.more > summary { list-style: none; }
.punch-group > summary::-webkit-details-marker,
:is(.punch, .punch-closed) details.more > summary::-webkit-details-marker { display: none; }
.punch-group > summary { display: flex; align-items: baseline; gap: 8px; margin: 22px 0 0; cursor: pointer; }
.punch-group > summary h2 { margin: 0; font-size: 1.2rem; }
.punch-group > summary .count { color: var(--muted); font-size: 0.85rem; }

.punch, .punch-closed { list-style: none; margin: 8px 0 0; padding: 0; position: relative; }
:is(.punch, .punch-closed) .row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 9px 8px 9px 4px;
  border-top: 1px solid var(--line);
  border-radius: 4px;
  background: var(--cream);
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  scroll-margin-top: calc(var(--topbar-h, 88px) + 12px);
}
:is(.punch, .punch-closed) .row:last-child { border-bottom: 1px solid var(--line); }
:is(.punch, .punch-closed) details.more { flex: 1; min-width: 0; }
/* The title says click; the rest of the row says drag. */
:is(.punch, .punch-closed) details.more > summary { display: flex; align-items: baseline; gap: 8px; cursor: pointer; }
:is(.punch, .punch-closed) details.more > summary:focus-visible { outline: 2px solid var(--flame); outline-offset: 2px; border-radius: 3px; }
/* The paragraphs in a box, in the title's column. Monospace, because the
   text carries the file's own tags. */
:is(.punch, .punch-closed) .why-edit { display: block; margin: 8px 0 6px; padding-left: calc(0.8em + 8px); }
/* Plain text since 2026-10-04 (punch_text()), so the body face rather than
   a code face: "I would like to stop seeing raw HTML in the text boxes." */
:is(.punch, .punch-closed) .why-title { display: block; margin: 8px 0 0; padding-left: calc(0.8em + 8px); }
:is(.punch, .punch-closed) .why-title input {
  font-family: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1.4;
  width: 100%;
  max-width: 46rem;
  box-sizing: border-box;
  padding: 7px 11px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: #fff;
  color: inherit;
  user-select: text;
  -webkit-user-select: text;
}
:is(.punch, .punch-closed) .why-title input:focus-visible { outline: 2px solid transparent; border-color: var(--flame); box-shadow: inset 0 0 0 1px var(--flame); }
:is(.punch, .punch-closed) .why-edit textarea {
  font-family: inherit;
  font-size: 0.92rem;
  line-height: 1.55;
  width: 100%;
  max-width: 46rem;
  box-sizing: border-box;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: #fff;
  color: inherit;
  resize: vertical;
  cursor: auto;
  user-select: text;
  -webkit-user-select: text;
}
:is(.punch, .punch-closed) .why-edit textarea:focus-visible { outline: 2px solid transparent; border-color: var(--flame); box-shadow: inset 0 0 0 1px var(--flame); }
:is(.punch, .punch-closed) .why-go {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 14px;
  align-items: baseline;
  margin: 0 0 10px;
  padding-left: calc(0.8em + 8px);
  cursor: auto;
}
/* Quiet, not filled: saving a note is undone by typing again. */
:is(.punch, .punch-closed) .why-go button {
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  padding: 6px 14px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #fff;
  color: var(--charcoal);
  cursor: pointer;
}
:is(.punch, .punch-closed) .why-go button:hover { border-color: var(--flame); color: var(--flame); }
:is(.punch, .punch-closed) .why-said { font-size: 0.85rem; font-weight: 600; color: var(--charcoal); }
:is(.punch, .punch-closed) .why-said.problem { color: var(--flame); }
:is(.punch, .punch-closed) .why-go .aside { font-size: 0.78rem; color: var(--muted); }
/* The grip is the one place a finger drags rather than scrolls. Baseline,
   so an open row's grip stays on its title line. */
:is(.punch, .punch-closed) .grip { flex: none; color: var(--muted); font-size: 1.05rem; line-height: 1; letter-spacing: -0.2em; touch-action: none; }
/* The item's number: a name, not a place, so it does not move with the order. */
:is(.punch, .punch-closed) .no {
  flex: none;
  min-width: 36px;
  height: 26px;
  padding: 0 6px;
  box-sizing: border-box;
  border-radius: 13px;
  background: var(--flame);
  color: var(--cream);
  font-size: 0.78rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  display: flex;
  align-items: center;
  justify-content: center;
}
:is(.punch, .punch-closed) .p-chip {
  flex: 0 0 5.6ch;
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  text-align: center;
  padding: 3px 0;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: var(--paper);
  color: var(--flame);
}
:is(.punch, .punch-closed) .p-chip.check { color: var(--caribbean); }
:is(.punch, .punch-closed) .p-chip:is(.park, .done) { color: var(--muted); }
:is(.punch, .punch-closed) .what { min-width: 0; overflow-wrap: anywhere; }
:is(.punch, .punch-closed) .cost { display: block; margin-top: 2px; color: var(--muted); font-size: 0.8rem; }
/* The row in hand: lifted, above its neighbours. Its position is a transform
   the script sets every frame. */
:is(.punch, .punch-closed) .row.dragging {
  position: relative;
  z-index: 2;
  cursor: grabbing;
  background: var(--paper);
  border-color: transparent;
  box-shadow: 0 12px 28px rgba(42, 38, 34, 0.18), 0 1px 3px rgba(42, 38, 34, 0.12);
}
.punch.is-dragging .row { cursor: grabbing; }
/* Kept in the tree while empty: a live region that appears with its first
   sentence is routinely not read out. */
.punch-status { min-height: 1.4em; margin: 10px 0 0; font-weight: 600; }
.punch-status.problem { color: var(--flame); }
@media (prefers-reduced-motion: reduce) { .punch-page .twirl { transition: none; } }

/* A note, straight onto the list, above it: a thought stopped for should
   not cost a scroll. */
.punch-note { display: grid; gap: 8px; margin: 0 0 8px; padding: 14px 16px; border-left: 3px solid var(--flame); background: var(--paper); }
.punch-note input,
.punch-note textarea,
.punch-note select {
  font: inherit;
  width: 100%;
  box-sizing: border-box;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: #fff;
  color: inherit;
}
.punch-note-title { font-weight: 600; }
.punch-note-body { resize: vertical; min-height: 3.2em; }
.punch-note-go { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.punch-note-go label { flex: 1 1 14rem; min-width: 0; }
.punch-note-go button { font: inherit; font-weight: 600; padding: 9px 18px; border: 1px solid var(--flame); border-radius: 4px; background: var(--flame); color: #fff; cursor: pointer; }

/* Finished and retired, under the open ones, quieter, with the day each
   closed. They do not drag: each keeps its place in the file. */
.punch-closed { margin-top: 18px; }
.punch-closed::before {
  content: 'Closed and retired';
  display: block;
  margin: 0 0 4px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
.punch-closed .row { cursor: default; }
.punch-closed .row .what { color: var(--muted); }
.punch-closed .row .no { opacity: 0.6; }
.punch-closed .when { flex: none; min-width: 3.6em; font-size: 0.78rem; color: var(--muted); font-variant-numeric: tabular-nums; }
/* Safari drags an element natively when this allows it, whatever the script
   does with dragstart - see punch.js. And no selection while a row is in hand. */
.punch .row, .punch .row summary { -webkit-user-drag: none; }
html.punch-dragging, html.punch-dragging * { -webkit-user-select: none; user-select: none; }

/* **Only the island's side.** In landscape iOS reports the same inset on both
   sides, so the bar away from the camera carried about 60px of nothing and
   the step lost that much line length. The cook page's script reads which
   way the phone is turned and zeroes the other side; with no script both
   sides keep the inset, which is the safe way to be wrong. Gabe, 2026-10-01:
   "check which side needs the extra width". */
.island-left  { --sa-r: 0px; }
.island-right { --sa-l: 0px; }

/* ------------------------------------------------------- dinner tray -- */
/* The dinner tray (lib/tray.php): "+ Dinner" on a recipe, the count beside
   the search, and the page that makes a dinner plan from it. The chip is
   the + square's outline with words in it, so it reads as one of the two
   ways into something rather than a third loud thing. */
.topbar-tray {
  display: flex;
  flex: none;
  align-items: center;
  gap: 6px;
  height: 44px;
  margin-right: 8px;
  padding: 0 12px;
  border: 1px solid var(--line);
  color: var(--charcoal);
  font: 700 0.72rem/1 Helvetica, Arial, sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}
.topbar-tray[hidden] { display: none; }
/* The chip for a plan running (templates/layout.php): the next step and its
   time, with the pulse the plan's own Live label has. Same chip, flame text. */
/* The one chip that gives way: on a landscape phone it takes the squeeze
   (the search keeps its square), its words ending in an ellipsis. */
.topbar-running { flex: 0 100 auto; min-width: 0; color: var(--flame); border-color: var(--flame); gap: 8px; }
.topbar-running-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--flame); animation: meal-pulse 2s ease-in-out infinite; }
.topbar-running .topbar-tray-name { min-width: 0; max-width: 16em; }
@media (prefers-reduced-motion: reduce) { .topbar-running-dot { animation: none; } }
/* Not under 780px: beside the Dinner chip the words get less than the clock
   needs ("7:55 pm"), and a dot or three letters looks broken. The plan
   itself, and the Plans page, still lead back to it. */
@media (max-width: 780px) { .topbar-running { display: none; } }
.topbar-tray:hover { border-color: var(--accent); color: var(--accent); }
/* The Dinner chip's flyout (templates/layout.php): the dishes in the tray,
   each a way to its recipe, on hover or keyboard focus. Gabe, 2026-10-02:
   "hover over the Dinner chip flies out a clickable list of the dishes
   inside with thumbs". It hangs from the chip with no gap to cross - the
   list's top padding is the gap, and the box is drawn behind it - so the
   pointer can travel down into it. The chip is still the way to the Dinner
   page, so a touch, which has no hover, loses nothing. */
.topbar-dinner { position: relative; display: flex; flex: 0 1 auto; min-width: 0; margin-right: 8px; }
/* The plan's name takes the squeeze now Import recipe is words, not a plus. */
.topbar-dinner .topbar-tray { flex: 0 1 auto; min-width: 0; }
.topbar-dinner .topbar-tray-name { min-width: 3em; }
/* "Import" alone where the row is short. */
@media (max-width: 900px) { .topbar-add-more { display: none; } }
.topbar-dinner[hidden] { display: none; }
.topbar-dinner .topbar-tray { margin-right: 0; }
/* The list hangs under the chip. Its 6px of clear padding on top is the
   bridge the pointer crosses from chip to list without leaving the hover.
   The box is the list itself so that a tall one can scroll: six dishes are
   59px each and a landscape phone is 375px tall, and the sixth sat below
   the screen with no way to reach it. */
.tray-fly {
  position: absolute; top: 100%; right: 0; z-index: 40;
  display: none; padding-top: 6px;
  min-width: 260px; max-width: min(380px, calc(100vw - 2 * var(--gutter)));
}
.tray-fly ul {
  margin: 0; padding: 0; list-style: none;
  max-height: calc(100vh - 110px); overflow-y: auto;
  background: var(--cream); border: 1px solid var(--line);
  box-shadow: 0 10px 24px rgba(42, 38, 34, 0.14);
}
/* Hover opens it only where there is a hover: on a touch screen Safari
   treats a tap that reveals hovered content as the hover, and the chip took
   two taps to reach the Dinner page. Keyboard focus opens it everywhere.
   While it is open the whole bar comes up above the page, because the
   list's own z-index lives inside the bar's stacking context (z-index 1,
   above) and could not get past the recipe page's rating stars, which are
   z-index 1 later in the markup: they painted over the first row, and a
   click there set a rating. */
@media (hover: hover) {
  .topbar-dinner:hover .tray-fly { display: block; }
  .topbar:has(.topbar-dinner:hover) { z-index: 40; }
}
.topbar-dinner:focus-within .tray-fly { display: block; }
.topbar:has(.topbar-dinner:focus-within) { z-index: 40; }
.tray-fly a {
  display: flex; align-items: center; gap: 10px; padding: 7px 16px 7px 7px;
  color: var(--charcoal); text-decoration: none; font-weight: 600; font-size: 0.9rem; line-height: 1.25;
}
.tray-fly li + li a { border-top: 1px solid var(--line); }
.tray-fly a:hover { color: var(--flame, #b8462f); }
.tray-fly img { flex: none; display: block; width: 44px; height: 44px; object-fit: cover; border-radius: 4px; background: var(--line); }
.topbar-tray-n {
  min-width: 1.5em;
  padding: 3px 0;
  border-radius: 999px;
  background: var(--flame, #b8462f);
  color: #fff;
  text-align: center;
  letter-spacing: 0;
}

/* The chip names the plan picked (punch 150); a long made-up name -
   "Chicken Au Poivre, Caesar Salad and ..." - ends in an ellipsis, and the
   whole of it is the chip's title. */
.topbar-tray-name { display: inline-block; max-width: 14em; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }

/* Add to, on a recipe (templates/tray-add.php): the button, then the plan
   it adds to as a dropdown of the plans being put together. Gabe,
   2026-10-02: "a styled dropdown where you can select the plan". */
.tray-add { position: relative; display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; }
.tray-add form { margin: 0; }
.tray-add button {
  padding: 0;
  border: 0;
  background: none;
  color: var(--charcoal);
  font: inherit;
  cursor: pointer;
  text-align: left;
}
.tray-add-go { color: var(--flame, #b8462f) !important; font-weight: 700 !important; white-space: nowrap; }
.tray-add-go:hover { text-decoration: underline; text-underline-offset: 3px; }
.tray-add.is-in .tray-add-go { color: var(--charcoal) !important; }
.tray-add-go[disabled] { color: var(--muted) !important; cursor: default; text-decoration: none; }
.tray-pick { position: relative; min-width: 0; }
.tray-pick summary {
  display: flex; align-items: center; gap: 6px;
  max-width: 16em; padding: 2px 8px 2px 10px;
  border: 1px solid var(--line); border-radius: 999px;
  font-weight: 600; cursor: pointer; list-style: none;
}
.tray-pick summary::-webkit-details-marker { display: none; }
.tray-pick summary span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tray-pick summary::after {
  content: ""; flex: none; width: 6px; height: 6px; margin-top: -3px;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(45deg);
}
.tray-pick[open] summary::after { margin-top: 3px; transform: rotate(-135deg); }
.tray-pick summary:hover, .tray-pick[open] summary { border-color: var(--flame, #b8462f); color: var(--flame, #b8462f); }
/* Open, the whole control comes up above the facts after it: the rating
   stars are z-index 1 later in the markup, as they were over the chip's
   flyout. */
.tray-add:has(.tray-pick[open]) { z-index: 30; }
.tray-pick-list {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 40;
  width: max-content; min-width: 220px; max-width: min(340px, calc(100vw - 2 * var(--gutter)));
  max-height: calc(100vh - 140px); overflow-y: auto;
  margin: 0; padding: 4px 0; list-style: none;
  background: var(--cream); border: 1px solid var(--line);
  box-shadow: 0 10px 24px rgba(42, 38, 34, 0.14);
}
.tray-pick-list button { display: flex; align-items: baseline; gap: 14px; width: 100%; padding: 8px 14px !important; font-weight: 600; }
.tray-pick-list button:hover:not([disabled]) { color: var(--flame, #b8462f); }
/* The plan picked is in the flame; a check on the right is "this recipe is in it". */
.tray-pick-list button[aria-current] { color: var(--flame, #b8462f); }
.tray-pick-list button[disabled] { color: var(--muted); cursor: default; }
.tray-pick-name { flex: 1 1 auto; min-width: 0; }
.tray-pick-n { flex: none; color: var(--muted); font-size: 0.85rem; font-variant-numeric: tabular-nums; }
.tray-pick-new { border-top: 1px solid var(--line); margin-top: 4px; padding-top: 4px; }
.tray-pick-new button { color: var(--flame, #b8462f) !important; }
.meta-item-tray .meta-label { white-space: nowrap; }
.share-copy { color: inherit; text-decoration: none; }
.share-copy:hover { text-decoration: underline; }
.share-said { margin-left: 6px; font-size: 0.85rem; font-weight: 400; color: var(--muted); }

.tray-dishes { list-style: none; margin: 8px 0 28px; padding: 0; display: grid; gap: 10px; }
.tray-dishes li { display: flex; align-items: center; gap: 12px; }
.tray-dish {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 12px;
  min-height: 64px; padding-right: 14px;
  border: 1px solid var(--line); border-radius: 6px;
  color: var(--charcoal); text-decoration: none;
}
.tray-dish:not(:has(img)) { padding-left: 14px; }
.tray-dish:hover { border-color: var(--flame, #b8462f); }
.tray-dish img { width: 64px; height: 64px; object-fit: cover; border-radius: 5px 0 0 5px; display: block; flex: none; }
.tray-dish-name { flex: 1 1 auto; min-width: 0; font-weight: 600; font-size: 1.05rem; }
.tray-dish-time { flex: none; color: var(--muted); font-size: 0.9rem; font-variant-numeric: tabular-nums; }
.tray-dishes form { margin: 0; flex: none; }
.tray-remove, .tray-clear button {
  padding: 0; border: 0; background: none; cursor: pointer;
  color: var(--muted); font: inherit; font-size: 0.9rem;
  text-decoration: underline; text-underline-offset: 3px;
}
.tray-remove:hover, .tray-clear button:hover { color: var(--flame, #b8462f); }

.tray-make { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 16px; margin: 0 0 12px; }
.tray-serve { display: flex; flex-direction: column; gap: 4px; }
.tray-serve span {
  font: 700 0.65rem/1.2 Helvetica, Arial, sans-serif; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
}
.tray-serve input {
  height: 44px; padding: 0 10px; box-sizing: border-box;
  border: 1px solid var(--line); border-radius: 0; background: var(--paper, #fff); color: var(--charcoal);
  font: inherit; font-size: 1.05rem; font-variant-numeric: tabular-nums;
}
/* All at once / In courses: two halves of one control, as tall as the
   fields beside it, the chosen half filled. */
.tray-courses { display: grid; grid-template-columns: auto auto; margin: 0; padding: 0; border: 0; min-width: 0; }
.tray-courses legend {
  grid-column: 1 / -1; margin: 0 0 4px; padding: 0;
  font: 700 0.65rem/1.2 Helvetica, Arial, sans-serif; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
}
.tray-courses label {
  display: flex; align-items: center; height: 44px; padding: 0 14px; box-sizing: border-box;
  border: 1px solid var(--line); background: var(--paper, #fff); color: var(--charcoal);
  font-size: 0.95rem; white-space: nowrap; cursor: pointer;
}
.tray-courses label + label { border-left: 0; }
.tray-courses input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.tray-courses label:has(input:checked) { background: var(--charcoal); border-color: var(--charcoal); color: var(--cream, #fff); font-weight: 600; }
.tray-courses label:has(input:focus-visible) { outline: 2px solid var(--flame, #b8462f); outline-offset: 2px; }
.tray-make > button {
  height: 44px; padding: 0 18px;
  border: 0; border-radius: 0; background: var(--flame, #b8462f); color: #fff;
  font: 700 0.8rem/1 Helvetica, Arial, sans-serif; letter-spacing: 0.12em; text-transform: uppercase;
  cursor: pointer;
}
.tray-make > button:hover { background: var(--charcoal); }
.tray-make > button[disabled] { opacity: 0.6; cursor: default; }
.tray-note { flex-basis: 100%; margin: 0; color: var(--muted); font-size: 0.9rem; }
/* A whole row each, so they never sit beside the fields; the text held to
   about 62 characters by padding, since a max-width let them fit beside. */
.tray-note, .tray-cost { padding-right: max(0px, calc(100% - 62ch)); box-sizing: border-box; }
/* What "Write the plan" will cost, on its own line just above the buttons. */
.tray-cost { flex-basis: 100%; margin: 4px 0 0; font-size: 0.95rem; }
.tray-over { color: var(--flame, #b8462f); }
/* A plan's name, typed (punch 150): the field stands where the title does,
   at the title's size. */
.tray-name { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; margin: 0 0 8px; }
.tray-name-input {
  flex: 1 1 18em; min-width: 0; width: 100%; box-sizing: border-box;
  padding: 4px 8px; border: 1px solid var(--line); border-radius: 4px; background: #fff;
  font: inherit; font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 700; line-height: 1.2; color: var(--charcoal);
}
.tray-name button { padding: 8px 16px; border: 0; border-radius: 4px; background: var(--flame, #b8462f); color: #fff; font: inherit; font-weight: 700; cursor: pointer; }
.tray-name a { color: var(--muted); }
.meals-new { justify-content: flex-start; margin: 0 0 18px; }
.tray-ready { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; margin: 18px 0 22px; font-weight: 600; }
.tray-ready p { margin: 0; }
.tray-ready a { color: var(--flame, #b8462f); }
/* Start a new plan, beside Plan ready: the next step once a plan exists, in
   the Rough plan button's clothes - a step, not the spend. Try again, beside
   why a plan could not be written, wears the same. */
.tray-new, .tray-retry { margin: 0; }
.tray-new button, .tray-retry button {
  height: 44px; padding: 0 18px;
  border: 1px solid var(--line); border-radius: 0; background: none; color: var(--charcoal);
  font: 700 0.8rem/1 Helvetica, Arial, sans-serif; letter-spacing: 0.12em; text-transform: uppercase;
  cursor: pointer;
}
.tray-new button:hover, .tray-retry button:hover { border-color: var(--flame, #b8462f); color: var(--flame, #b8462f); }
.tray-retry button[disabled] { opacity: 0.6; cursor: default; }
.tray-clear { margin: 18px 0 0; }
.tray-serves input { width: 5.5rem; }
.tray-make > .tray-rough { background: none; color: var(--charcoal); border: 1px solid var(--line); }
.tray-make > .tray-rough:hover { background: none; border-color: var(--flame, #b8462f); color: var(--flame, #b8462f); }

/* The stirred pot (templates/working-pot.php) - the tools page's, here for
   the wait while Claude writes a dinner plan. */
.working[hidden] { display: none; }
.working {
  display: flex; align-items: center; gap: 12px;
  flex-basis: 100%; margin: 12px 0 0;
  color: var(--soft); font-size: 1rem;
}
.tray-writing { margin: 4px 0 24px; align-items: flex-start; }

/* The wait while a plan is written (punch 144): what Claude is doing, a bar,
   and the minutes against the usual, beside the pot. The bar glides between
   the page's questions, four seconds apart, rather than stepping; every line
   is one line, so nothing under it moves as the words change. */
.tray-meter { flex: 1 1 auto; min-width: 0; max-width: 34rem; }
.tray-phase { margin: 2px 0 10px; color: var(--charcoal); font-weight: 600; font-size: 1.05rem; }
.tray-bar { height: 8px; background: var(--line); overflow: hidden; }
.tray-bar > span { display: block; height: 100%; background: var(--flame, #b8462f); }
@media (prefers-reduced-motion: no-preference) {
  .tray-bar > span { transition: width 4s linear; }
}
.tray-time { margin: 8px 0 0; color: var(--muted); font-size: 0.9rem; font-variant-numeric: tabular-nums; }
.tray-time + .tray-time { margin-top: 2px; }

/* A plan that could not be written: why, in plain words, and Try again. */
.tray-failed { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; margin: 4px 0 22px; }
.tray-failed p { margin: 0; color: var(--flame, #b8462f); font-weight: 600; }
.working-pot { width: 52px; height: 52px; flex: none; overflow: visible; }
.working-body { fill: var(--charcoal); }
.working-rim  { stroke: var(--charcoal); stroke-width: 3; stroke-linecap: round; fill: none; }
.working-spoon path { stroke: var(--flame); stroke-width: 2.5; stroke-linecap: round; fill: none; }
.working-spoon ellipse { fill: var(--flame); }
.working-steam { stroke: var(--muted); stroke-width: 2.2; stroke-linecap: round; fill: none; opacity: 0; }
@media (prefers-reduced-motion: no-preference) {
  .working-spoon {
    transform-box: view-box;
    transform-origin: 24px 34px;
    animation: working-stir 1.4s ease-in-out infinite alternate;
  }
  .working-steam   { animation: working-steam 2.4s ease-out infinite; }
  .working-steam-2 { animation-delay: 1.2s; }
}
@media (prefers-reduced-motion: reduce) {
  .working-steam { opacity: 0.6; }
}
@keyframes working-stir {
  from { transform: translateX(-5px) rotate(-38deg); }
  to   { transform: translateX(5px)  rotate(0deg); }
}
@keyframes working-steam {
  0%   { opacity: 0;   transform: translateY(4px); }
  40%  { opacity: 0.8; }
  100% { opacity: 0;   transform: translateY(-6px); }
}

/* Mise mode (public/assets/mise-mode.js): click the Mise en Place heading and
   the ingredients fill the screen, one line current. Gabe, 2026-10-03:
   "big modal of ingredients. spacebar toggles check on current item and
   advances, arrow keys ... move up and down, clicking off anywhere
   dismisses." The dialog is a full-screen, see-through box and the panel sits
   in it, so a click on the dialog itself is a click off the panel. A checked
   line is the page's own: muted, with the 2px flame line through it. */
.has-mise-mode [data-mise-mode] { cursor: pointer; }
/* On a plan the focusable heading is clipped to a pixel (the bar prints the
   visible one), so the bar's copy wears its focus ring. */
@media (min-width: 900px) {
  .meal-body:has(> .meal-mise > h2:focus-visible) > .meal-cols-head .meal-cols-mise > .meal-col-h { outline: 2px solid var(--flame); outline-offset: 4px; }
}
[data-mise-mode]:hover { text-decoration: underline; text-decoration-color: var(--line); text-decoration-thickness: 2px; text-underline-offset: 6px; }
[data-mise-mode]:focus-visible { outline: 2px solid var(--flame); outline-offset: 4px; }

.mise-mode {
  width: 100vw; height: 100vh; height: 100dvh; max-width: none; max-height: none;
  margin: 0; padding: 0; border: 0; background: transparent; color: var(--charcoal);
}
.mise-mode[open] { display: flex; align-items: center; justify-content: center; }
.mise-mode::backdrop { background: rgba(24, 22, 18, 0.74); }
.mm-panel {
  box-sizing: border-box; display: flex; flex-direction: column;
  width: min(46rem, calc(100vw - 48px)); max-height: calc(100vh - 48px); max-height: calc(100dvh - 48px);
  background: var(--paper); border: 1px solid var(--line); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); outline: none;
}
.mm-bar {
  flex: none; display: flex; justify-content: space-between; align-items: baseline; gap: 16px;
  padding: 16px 28px 12px; border-bottom: 1px solid var(--line);
  font: 800 0.74rem/1 Helvetica, Arial, sans-serif; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted);
}
.mm-count { font-variant-numeric: tabular-nums; }
.mm-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; padding: 4px 28px 16px; }
.mm-list { list-style: none; margin: 0; padding: 0; outline: none; }
.mm-text { min-width: 0; overflow-wrap: anywhere; }
/* The page behind does not scroll while it is up - wheel, thumb or Page keys.
   scrollbar-gutter keeps the width, so nothing shifts when the bar goes. */
html:has(dialog.mise-mode[open]) { overflow: hidden; scrollbar-gutter: stable; }
.mm-head {
  padding: 18px 0 4px; font: 800 0.74rem/1.2 Helvetica, Arial, sans-serif;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted);
}
.mm-row {
  display: flex; align-items: flex-start; gap: 14px; margin: 0 -14px; padding: 12px 14px;
  border-left: 4px solid transparent; border-bottom: 1px solid var(--line);
  font-size: clamp(1.2rem, 0.7rem + 1.6vw, 1.9rem); line-height: 1.3; cursor: pointer;
}
.mm-row.is-current { background: color-mix(in srgb, var(--flame) 8%, var(--paper)); border-left-color: var(--flame); }
.mm-box {
  flex: none; box-sizing: border-box; position: relative; width: 1em; height: 1em; margin-top: 0.14em;
  border: 2px solid var(--charcoal); background: var(--paper);
}
.mm-row.is-done .mm-box { background: var(--charcoal); }
.mm-row.is-done .mm-box::after {
  content: ""; position: absolute; left: 0.26em; top: 0.04em; width: 0.26em; height: 0.52em;
  border: solid var(--paper); border-width: 0 2px 2px 0; transform: rotate(40deg);
}
.mm-n { flex: none; min-width: 1.2em; padding-top: 0.28em; font: 700 0.6em/1.3 Helvetica, Arial, sans-serif; color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
.mm-amt { font-family: Helvetica, Arial, sans-serif; font-weight: 700; }
.mm-note { display: block; margin-top: 2px; font-size: 0.68em; line-height: 1.35; color: var(--muted); }
.mm-row.unscaled .mm-text::after {
  content: " not scaled"; font: 400 max(0.66rem, 0.4em) Helvetica, Arial, sans-serif; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--muted); white-space: nowrap; margin-left: 6px;
}
.mm-row.is-done .mm-text { color: var(--muted); text-decoration: line-through; text-decoration-color: var(--flame); text-decoration-thickness: 2px; }
@media (max-height: 520px) {
  .mm-panel { max-height: calc(100dvh - 24px); }
  .mm-bar { padding: 10px 24px 8px; }
  .mm-row { padding: 8px 14px; font-size: clamp(1.15rem, 0.6rem + 1.2vw, 1.5rem); }
}
