/* Austin Catering Guide — one stylesheet, no framework, no web fonts.
   Decision D-18: nothing loads from a third-party origin at runtime.

   Two families, both already on the reader's machine. The serif carries the
   editorial claim — headings, listing names, price figures — and the sans
   carries the chrome: nav, labels, counts, metadata. That split is the
   design; it is also free. Nothing here asks for a weight above 600, because
   static Segoe UI ships no 500 master and synthesises one badly, which is
   one of the few things that reliably makes a page look cheap. */

:root {
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, "Noto Sans", sans-serif;
  /* ui-serif lands on New York (macOS/iOS), Palatino Linotype or Sitka Text
     (Windows), Noto Serif (Android/ChromeOS). Nowhere in the chain does
     anybody get Times New Roman, which is the point. */
  --serif: ui-serif, "Iowan Old Style", "Palatino Linotype", Palatino,
           "Sitka Text", Cambria, "Noto Serif", serif;

  /* Type scale. Hierarchy comes from family and case as much as from size,
     so the range can stay narrow — a wide range reads as shouting. */
  --step--2: .74rem;
  --step--1: .875rem;
  --step-0:  1rem;
  --step-1:  1.125rem;
  --step-2:  1.28rem;
  --step-3:  clamp(1.4rem, 2.2vw, 1.7rem);
  --step-4:  clamp(1.85rem, 4.2vw, 2.5rem);

  /* A warm paper ground rather than not-quite-white. It also buys back
     #ffffff as a *figure* colour: white now means "this is the data" and is
     spent on two components only, the price block and the fact box.

     Every text pair, computed rather than eyeballed. Light theme, as
     ratios on --bg / --surface / --paper-alt:

       ink          16.26  17.39  15.42
       ink-soft      6.70   7.16   6.35
       ink-faint     5.19   5.55   4.92   <- the thinnest pair on the site
       accent        6.83   7.31   6.48
       accent-deep   8.76   9.37   8.31
       gold          5.65   6.04   5.36

     plus gold on --gold-soft 5.58, ink on --accent-soft 15.66, white on
     --accent 7.31 (the button) / --accent-deep 9.37 (its hover), and the
     filter box's placeholder, ink-faint on --surface: 5.55 light, 5.43
     dark. The UA's own placeholder grey was 3.63 on the dark input. */
  --bg: #faf7f1;
  --surface: #ffffff;
  --paper-alt: #f4f1ea;
  --ink: #1b1a18;
  --ink-soft: #5c574f;
  --ink-faint: #6e6857;
  --rule: #e3ded3;
  /* 3.08:1 on --bg and 3.29:1 on --surface, so it clears WCAG 1.4.11's 3:1
     floor for a graphical object and a form-control border: it draws the
     search input's edge and the dot plot's axis. Never text — the em-dash
     price placeholder is decoration, and the accessible label sits under
     it at full contrast. */
  --rule-strong: #948d81;
  --accent: #9a3412;        /* burnt orange, restrained. Action only. */
  --accent-deep: #7c2d12;
  --accent-soft: #fdf1e7;
  --gold: #7a5f16;          /* paid disclosure only */
  --gold-soft: #fbf6e4;
  --focus: #1d4ed8;
  --radius: 8px;
  --wrap: 68rem;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16150f;
    --surface: #1f1e17;
    --paper-alt: #1d1c15;
    --ink: #f0ece3;
    --ink-soft: #bdb6a8;
    /* was #8f887a, which measured 4.75:1 on --surface — under the floor once
       the fact box and price block became white-equivalent panels. */
    --ink-faint: #9a9284;   /* 5.94 on bg, 5.43 on surface */
    --rule: #33302a;
    --rule-strong: #7a7367;   /* 3.90 on bg, 3.56 on surface */
    --accent: #f0834e;
    --accent-deep: #f7a173;
    --accent-soft: #2a1d14;
    --gold: #d7bb6a;
    --gold-soft: #2a2416;
    --focus: #7aa2ff;
  }
}

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

/* controls.js hides rows with the `hidden` attribute, and the UA rule
   [hidden]{display:none} loses to any author `display` on the same element —
   .row is a grid, so without this the filter would run and hide nothing. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: var(--step-0)/1.6 var(--sans);
  font-feature-settings: "kern", "liga";
}

.wrap { max-width: var(--wrap); margin-inline: auto; padding-inline: 1.25rem; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { text-decoration-thickness: 2px; color: var(--accent-deep); }
:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; border-radius: 3px; }

.skip {
  position: absolute; left: -9999px;
  background: var(--surface); padding: .75rem 1rem; z-index: 10;
}
.skip:focus { left: 1rem; top: 1rem; }

/* Negative tracking at display sizes only. Material 3 sets tracking to zero
   for every role at 14px and up and only turns it positive at 12px; the old
   sheet applied -0.015em uniformly, which was tightening a 1.1rem heading
   that did not need it. */
h1, h2, h3 {
  font-family: var(--serif);
  font-weight: 600;
  line-height: 1.15;
  margin: 0 0 .5rem;
  text-wrap: balance;
}
h1 { font-size: var(--step-4); letter-spacing: -0.021em; max-width: 22ch; }
h2 { font-size: var(--step-3); letter-spacing: -0.012em; margin-top: 2.5rem; }
h3 { font-size: var(--step-2); letter-spacing: -0.006em; line-height: 1.25; }

/* Figures. A no-op where the active face carries no tnum table, so nothing
   below relies on it for layout: the price column is right-aligned and
   width-bounded and holds either way. */
.stats dd, .price, .strip-scale, .range-scale, .pricebox .amt, .count,
.factbox dd, .checked {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}

/* --- masthead ---------------------------------------------------------- */

/* Every in-page jump on this site lands underneath the sticky masthead
   unless the viewport is told to keep room for it: the browser puts the
   target's top edge at y=0 and the bar paints over it. Measured before this
   rule, clicking Guides cut the top 14px off the heading on desktop and 20px
   on a phone — the first line sliced mid-glyph, on the one element the jump
   existed to show. One rule on the root element covers every anchor there is
   or will be: #main behind the skip link, #cards, #guides, anything added
   later. The values are the masthead's height plus air, and are deliberately
   generous — the bar's height follows its content, so a few pixels too many
   costs nothing visible while a few too few reproduces the bug exactly. */
html { scroll-padding-top: 5rem; }

.masthead {
  border-bottom: 1px solid var(--rule);
  background: var(--bg);
  position: sticky; top: 0; z-index: 5;
}
.masthead-inner {
  display: flex; flex-wrap: wrap; gap: .5rem 1.25rem;
  align-items: center; justify-content: space-between;
  padding-block: .7rem;
}
.brand { display: flex; align-items: center; gap: .6rem; text-decoration: none; color: inherit; }
.brand-mark { display: block; flex: none; width: 2.3rem; height: auto; }
.brand-a { fill: var(--accent); }
.brand-rule { stroke: var(--ink); stroke-width: 1.6; }
.brand-text strong {
  display: block; font-family: var(--serif); font-weight: 600;
  font-size: 1.06rem; letter-spacing: -0.012em;
}
.brand-text small { display: block; color: var(--ink-faint); font-size: .78rem; }
.topnav { display: flex; flex-wrap: wrap; gap: 1rem; font-size: .9rem; }
.topnav a { color: var(--ink-soft); text-decoration: none; }
.topnav a:hover { color: var(--accent); text-decoration: underline; }

/* --- hero and page heads ----------------------------------------------- */

.hero { padding-block: 2.25rem .5rem; }
.pagehead { padding-block: 1.75rem .25rem; }
.lede {
  font-family: var(--serif); font-size: var(--step-1); line-height: 1.5;
  color: var(--ink); max-width: 54ch; text-wrap: pretty;
}
.lede-sub {
  color: var(--ink-faint); max-width: 62ch; font-size: var(--step--1);
  text-wrap: pretty;
}
.lede-sub a { color: var(--ink-faint); }
/* Paid placement, said on the page it happens on rather than only on
   /featured/. Gold to tie it to the mark it explains. */
.disclosure { color: var(--gold); max-width: 62ch; font-size: .85rem;
  margin-top: .6rem; }
.crumbs { font-size: .85rem; color: var(--ink-faint); margin: 0 0 .6rem; }
.crumbs a { color: var(--ink-faint); }

/* --- the statistics rule ------------------------------------------------
   The front page of a report: a band of figures under a double-weight rule.
   It says in one glance both what the dataset is and that a dataset exists,
   in roughly the space the old methodology paragraph took. */

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: 1rem 2rem;
  margin: 1.5rem 0 0;
  padding-block: 1rem 1.05rem;
  border-top: 2px solid var(--ink);
  border-bottom: 1px solid var(--rule);
}
.stats div { margin: 0; }
.stats dt {
  font-size: var(--step--2); font-weight: 600;
  text-transform: uppercase; letter-spacing: .085em;
  color: var(--ink-faint);
}
.stats dd {
  margin: .1rem 0 0;
  font-family: var(--serif);
  font-size: clamp(1.6rem, 3.4vw, 2.05rem);
  font-weight: 600; line-height: 1; letter-spacing: -0.02em;
}

/* --- the price distribution strip --------------------------------------
   One mark per caterer at their published per-person starting price. Built
   from the dataset at build time, inline, no library, no request; colours
   come from custom properties so dark mode is free. */

.stripfig { margin: 0; padding: 0; }
.strip { display: block; width: 100%; }
.strip-axis { stroke: var(--rule-strong); }
.strip-dot { stroke: var(--ink-soft); }
.strip-dot.is-median { stroke: var(--accent); }
.strip-scale {
  position: relative;
  display: flex; justify-content: space-between; align-items: baseline;
  margin: .3rem 0 0;
  font-size: var(--step--2); font-weight: 600; letter-spacing: .04em;
  color: var(--ink-faint);
}
.strip-key {
  position: absolute; top: 0; transform: translateX(-50%);
  color: var(--accent); font-weight: 400; letter-spacing: .02em;
  white-space: nowrap;
}
.stripfig figcaption, .rangefig figcaption {
  margin-top: .7rem; font-size: var(--step--1); line-height: 1.55;
  color: var(--ink-faint); max-width: 58ch; text-wrap: pretty;
}

/* --- the range bar (consumed by P6, category pageheads) -----------------
   The mark for a category below STRIP_MIN, where a dot plot would be a
   handful of marks pretending to be a distribution. Six figures do not
   describe a shape; they describe an extent with six known points in it, so
   that is what this draws: a labelled axis from the lowest published figure
   to the highest, with every published figure ticked on it.

   The hard requirement is that it must never be mistaken for the dot plot.
   A reader who sees the same graphic on /cuisine/bbq/ (n=25) and
   /area/round-rock/ (n=6) will compare them as though the samples were
   comparable, which is the one dishonest thing this site could do with a
   picture. Four things separate them, deliberately and all at once:

     - width. The dot plot is full measure; this is capped at 30rem and does
       not fill the column. A graphic that does not claim the width does not
       claim the authority.
     - height. 26 units against the dot plot's ~40 and up, so the two never
       stack to the same optical weight.
     - direction. The dot plot's marks rise *above* a baseline; these ticks
       *cross* a band. Nothing here is stacked, because nothing here is a
       frequency.
     - end caps. The band stops in brackets, which says "this is the observed
       extent" where an open axis says "this is the scale".

   The figcaption still has to name the denominator in words ("six of 24
   publish a price, $12–$38"); the drawing cannot say "of 24" and must not be
   trusted to.

   Geometry is the builder's (viewBox "0 0 600 26", preserveAspectRatio
   "none", band and ticks centred on y=13, ticks spanning y=3 to y=23 and
   caps y=1 to y=25, every line carrying vector-effect="non-scaling-stroke"
   for the same reason the dot plot does — the x axis stretches to the
   container and a stroke is the only primitive that survives that). Weight
   and colour are this stylesheet's, set here rather than in the markup so
   the mark can be re-weighted without a rebuild.

   Those y values were sketched here as 9, and 2 to 16, before the mark had
   been drawn; W4 corrected them, and the correction is worth keeping because
   it is the trap in mixing a non-scaling stroke with a stretched viewBox.
   The track's 10 units of stroke-width render as 10 *pixels* at any scale,
   while a tick's length is geometry and shrinks with the width — and this
   figure is used in a ~465px pagehead aside, where a 600-unit viewBox
   renders at 0.78. Ticks drawn 12 units long came out 9px and disappeared
   inside a 10px band. The marks use the full height of the viewBox for that
   reason, which leaves about 3px of tick either side of the track at the
   width the figure is actually used at; re-weighting .range-track means
   re-checking that.

   Ratios, light theme then dark, all on --bg:
     range-track  --rule-strong  3.08 / 3.90   clears 1.4.11's 3:1 for a
                                               graphical object, and it is
                                               the same ink as the dot plot's
                                               axis, so the two marks differ
                                               in shape and not in palette
     range-tick   --ink-soft     6.70 / 9.07
     is-median    --accent       6.83 / 7.02
     range-cap    --ink         16.26 / 15.51
   A --paper-alt ground would drop the track to 2.92 in light and fail that
   floor, so this figure stays on the page ground or on --surface. */

.rangefig { margin: 0; padding: 0; max-width: 30rem; }
.rangebar { display: block; width: 100%; }
.range-track { stroke: var(--rule-strong); stroke-width: 10; }
.range-cap { stroke: var(--ink); stroke-width: 2; }
.range-tick { stroke: var(--ink-soft); stroke-width: 2; }
/* The median gets the accent for the same reason it does in the dot plot,
   and at n=6 it is the one summary figure worth marking. Drawn last by the
   builder so it sits over a neighbouring tick rather than under it. */
.range-tick.is-median { stroke: var(--accent); stroke-width: 3; }
/* Flush to the caps, not to the container: the labels belong to the ends of
   the extent, and the extent is 30rem wide at most, not the measure. */
.range-scale {
  display: flex; justify-content: space-between; align-items: baseline;
  margin: .3rem 0 0;
  font-size: var(--step--2); font-weight: 600; letter-spacing: .04em;
  color: var(--ink-faint);
}

.hero-body {
  display: grid; gap: 1.5rem 3rem; margin-top: 1.6rem;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  align-items: start;
}
.hero-note > :first-child { margin-top: 0; }
.hero-note .lede { margin-block: 0 .7rem; }
.hero-note .lede-sub { margin-block: 0; }

/* --- the split pagehead (consumed by P6, category pageheads) ------------
   research/design/README.md's one open layout complaint: a category or guide
   page at 1280px is a 68rem measure with its right half empty, because the
   pagehead is a stack of a crumb trail, a heading and a 54ch lede. The fix
   recorded there is a two-column pagehead, not a wider measure — widening
   the measure would cost the lede its line length, which is the one thing
   about the pagehead that is already right.

   Crumbs and the h1 stay full width above the split: a heading that stops at
   55% of the page reads as a column head rather than a page title. Below
   them .pagehead-main keeps the lede and the provenance line at their
   existing measure, and .pagehead-aside takes the per-category stat band and
   the price graphic — the figures that were the reason the page felt like a
   shorter homepage with the data removed.

   1.15fr/1fr rather than the hero's 1.1fr/1fr: the aside here holds a stat
   band and a 30rem-capped graphic, both of which read better slightly
   narrower than the hero's dot plot, and the lede gets the difference.

   Collapses at 52rem, the same breakpoint .hero-body uses. Two split
   layouts, one breakpoint — a second one would be a second thing to
   remember. */

.pagehead-body {
  display: grid; gap: 1.5rem 3rem; margin-top: 1.4rem;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  align-items: start;
}
.pagehead-main > :first-child, .pagehead-aside > :first-child { margin-top: 0; }
.pagehead-main .lede { margin-block: 0 .7rem; }
.pagehead-main .lede-sub, .pagehead-main .disclosure { margin-block: .7rem 0; }
/* The band's own 1.5rem top margin exists to hold it off a hero above it.
   In the aside it is the first thing in the column, and its 2px rule is what
   should line up with the top of the lede.
   Its auto-fit columns would also give three in a ~28rem aside and leave the
   fourth figure alone on a second row; the band is four figures on a
   category page — listings, publish a price, median, range — so it is 2x2
   here, said with a fixed pair rather than left to chance. */
.pagehead-aside .stats {
  margin-top: 0;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.pagehead-aside .stripfig, .pagehead-aside .rangefig { margin-top: 1.5rem; }

@media (max-width: 52rem) {
  .hero-body { grid-template-columns: 1fr; gap: 1.2rem; }
  .pagehead-body { grid-template-columns: 1fr; gap: 1.2rem; }
}

/* --- filter ------------------------------------------------------------ */

.filterbar { padding-block: 1.25rem .25rem; }
.filterbar label {
  display: block; font-size: var(--step--2); font-weight: 600;
  text-transform: uppercase; letter-spacing: .085em;
  color: var(--ink-faint); margin-bottom: .4rem;
}
.filterbar input {
  width: 100%; max-width: 26rem; padding: .6rem .8rem;
  font: inherit; color: inherit;
  background: var(--surface);
  border: 1px solid var(--rule-strong); border-radius: var(--radius);
}
.filter-count { font-size: .85rem; color: var(--ink-faint); min-height: 1.2em; margin: .4rem 0 0; }

/* --- the control cluster (consumed by P3, and by P6 for FILTER_MIN) ------
   The filter box, the sort control and the published-prices-only toggle are
   three separate answers to one question — "show me less of this list, in a
   different order" — and bolted on one at a time they would read as three
   widgets that happen to share a page. They get one bar: one hairline above
   them, one baseline through them, one label recipe, and one live count
   underneath speaking for all three.

   Hidden by default and revealed by the script, the contract #filterbar
   already uses: without JS the page is the full list in its server-side
   order, so a control that cannot do anything is never shown. The
   [hidden] { display: none !important } rule at the top of this sheet is
   what makes that hold against the `display` values set below.

   The bar keeps the page ground rather than taking a --paper-alt fill. That
   is a contrast constraint, not a preference: --rule-strong is 2.92:1 on
   --paper-alt and would fail WCAG 1.4.11's 3:1 floor for a form-control
   border, where on --bg it is 3.08 and on --surface 3.29. The fields keep
   the same --surface / --rule-strong pairing the filter input already has.

   Markup P3 writes: section.controls[hidden] > div.control, each holding a
   label.control-label and its field, plus label.control-toggle for the
   checkbox and p#filter-count for the shared count. Field labels take
   .control-label rather than being styled as bare labels, because the toggle
   is itself a <label> and wants sentence case — one class each beats one
   class and an override. */

.controls {
  padding-block: 1rem .25rem;
  border-top: 1px solid var(--rule);
  display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: .9rem 1.25rem;
}
.control { display: flex; flex-direction: column; min-width: 0; }
/* The filter is the control that earns the room; the other two are as wide
   as their content. */
.control-filter { flex: 1 1 20rem; max-width: 26rem; }
.controls .control-label {
  font-size: var(--step--2); font-weight: 600;
  text-transform: uppercase; letter-spacing: .085em;
  color: var(--ink-faint); margin-bottom: .4rem;
}
/* Everything but the toggle's checkbox, which is a control the platform
   draws and must not be given a text field's padding and border. */
.controls input:not([type="checkbox"]), .controls select {
  padding: .6rem .8rem;
  font: inherit; color: inherit;
  background: var(--surface);
  border: 1px solid var(--rule-strong); border-radius: var(--radius);
}
.controls input:not([type="checkbox"]) { width: 100%; }
/* The UA placeholder stays light-scheme grey on the dark input (3.63:1).
   opacity:1 because Firefox dims placeholders by default. */
.controls input::placeholder { color: var(--ink-faint); opacity: 1; }
/* The select keeps its platform chrome, arrow and all. Resetting appearance
   would mean drawing a caret, positioning it, and re-colouring it in dark
   mode — three things to maintain so that one control can match a text input
   it is not. Safari will render its own affordance and that is the better
   trade. 1rem, like the input: below 1rem iOS zooms the page on focus. */
.controls select { max-width: 16rem; }
/* Sentence case, and no uppercase label above it: a checkbox with a micro
   label over it reads as a fourth field rather than as a switch on the bar.
   accent-color paints the checked box in --accent, whose UA tick is white at
   7.31:1 light and 6.42 dark. */
.control-toggle {
  display: flex; align-items: center; gap: .5rem;
  font-size: var(--step--1); color: var(--ink-soft);
  padding-bottom: .55rem;
}
.control-toggle input {
  width: 1.05rem; height: 1.05rem; flex: none; margin: 0;
  accent-color: var(--accent);
}
/* Quiet, and absent entirely until there is something to clear — P3 toggles
   the hidden attribute on it, not a class. */
.control-clear {
  font: inherit; font-size: var(--step--1);
  padding: .55rem .85rem; margin-bottom: .05rem;
  color: var(--ink-soft); background: var(--surface);
  border: 1px solid var(--rule-strong); border-radius: var(--radius);
  cursor: pointer;
}
.control-clear:hover { color: var(--accent); border-color: var(--accent); }
/* One count for the whole bar, on its own line under it: filtered, sorted
   and price-only are one result set, and three separate counts would invite
   a reader to add them up. */
.controls .filter-count { flex-basis: 100%; margin: .15rem 0 0; }

/* --- facet navigation --------------------------------------------------- */

.section-nav { padding-block: .25rem; }
.section-nav h2 { font-family: var(--sans); font-size: var(--step--2);
  text-transform: uppercase; letter-spacing: .085em; font-weight: 600;
  color: var(--ink-faint); margin: 1.75rem 0 .6rem; }
.pills { list-style: none; display: flex; flex-wrap: wrap; gap: .45rem; margin: 0; padding: 0; }
.pills a {
  display: inline-block; padding: .3rem .7rem;
  background: transparent; border: 1px solid var(--rule);
  border-radius: 999px; text-decoration: none;
  color: var(--ink); font-size: .875rem;
}
.pills a:hover { border-color: var(--accent); color: var(--accent); }
.pills .count { color: var(--ink-faint); font-size: .78rem; }

/* --- the index ----------------------------------------------------------
   Hairline-ruled rows, not boxed cards. One rule per listing and one aligned
   column of figures; roughly three times the listings per screen. Two rule
   weights used consistently — a full-strength rule under a section head
   against hairlines between rows — is the whole structural system. */

.index-head { border-bottom: 1px solid var(--ink); padding-bottom: .5rem; }

.index { list-style: none; margin: .25rem 0 0; padding: 0; }

.row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 9.5rem;
  column-gap: 2rem;
  align-items: baseline;
  padding-block: .8rem;
  border-top: 1px solid var(--rule);
}
.index > .row:first-child { border-top: 0; }
.row-main { min-width: 0; }

.name { margin: 0; }
.name a {
  color: var(--ink); text-decoration: none;
  text-decoration-thickness: 1px; text-underline-offset: 3px;
}
.name a:hover { color: var(--accent); text-decoration: underline; }

.note {
  margin: .15rem 0 0; font-size: var(--step--1); line-height: 1.45;
  color: var(--ink-soft); max-width: 76ch; text-wrap: pretty;
}
.facets {
  margin: .2rem 0 0; font-size: var(--step--2); line-height: 1.45;
  color: var(--ink-faint); letter-spacing: .01em;
}
.facets .sep { color: var(--rule-strong); padding-inline: .3em; }
.facets .diet { color: var(--gold); }

/* --- the row check date (consumed by P7) --------------------------------
   "Checked, not scraped" is the whole competitive position and it currently
   appears once, as boilerplate, below the list it is a claim about. Putting
   the date on the row makes it a property of each listing instead of a
   promise about the page — but a date per row, 123 times, is also the
   easiest way to turn a clean index into a noticeboard.

   So it sits in the faintest ink the site has (--ink-faint, 5.19:1 on --bg
   and 5.55 on --surface, the thinnest pair here and already documented as
   such) at the metadata size, on the same line rhythm as .facets. It is
   tabular-figured with the rest of the site's numerals, because the one
   thing worse than a date on every row is 123 dates that do not line up.
   The <time> element carries the machine-readable datetime; what a reader
   sees is whatever P7 decides reads best at this size. */

.checked {
  font-size: var(--step--2); line-height: 1.45;
  color: var(--ink-faint); letter-spacing: .01em;
}
.row-main > .checked { display: block; margin-top: .2rem; }

/* Featured is a mark in the margin, not a tinted box. A gold panel on a site
   that says "not an ad network" reads as exactly the ad unit it denies;
   Michelin's answer — a typographic rank mark in the flow of the list — is
   the right one, and it survives dark mode for free. */
.row.is-featured {
  border-left: 3px solid var(--gold);
  padding-left: 1rem; margin-left: -1rem;
}
.ribbon {
  grid-column: 1 / -1;
  font-size: var(--step--2); text-transform: uppercase; letter-spacing: .1em;
  color: var(--gold); font-weight: 600; margin: 0 0 .15rem;
}
.ribbon-inline { margin: 0 0 1rem; }

/* The price column. Right-aligned and width-bounded so the figures stack
   into a readable distribution down the page. */
.price { margin: 0; text-align: right; }
.price .amt {
  display: block;
  font-family: var(--serif); font-size: var(--step-2); font-weight: 600;
  line-height: 1.15; letter-spacing: -0.01em;
  color: var(--ink); white-space: nowrap;
}
.price .unit {
  display: block; margin-top: .1rem;
  font-size: var(--step--2); font-weight: 400; line-height: 1.3;
  text-transform: uppercase; letter-spacing: .05em;
  color: var(--ink-faint);
}
.price .min {
  display: block; margin-top: .25rem;
  font-size: .8rem; color: var(--ink-soft);
}
/* Absence keeps the slot and loses the volume: an em dash in the figure's
   optical position at 2.39:1 is decoration, and the label beneath it carries
   the accessible text at full contrast. */
.price-none .amt { color: var(--rule-strong); font-weight: 400; }
/* Sentence case, not the small caps a real unit gets: this label appears 87
   times and the whole point of it is to be learnable in three rows and then
   skippable. */
.price-none .unit { text-transform: none; letter-spacing: 0; }

.empty { color: var(--ink-faint); padding: 2rem 0; }

/* --- the example row on /featured/ (P8, D-32) ---------------------------
   The one place `.row.is-featured` renders with zero featured listings
   live. It borrows the real row styling above almost unchanged — that is
   the point, it has to demonstrate the actual product — but sits inside a
   dashed box that no real index anywhere on the site uses, so the frame
   itself says "sample" before a reader gets to the ribbon or the caption.
   `.is-example` softens the featured mark's own rule from solid to dashed
   for the same reason: the two visual differences are stacked rather than
   relying on either alone. */
.example-row {
  margin: 1.5rem 0 0;
  padding: 1rem 1.25rem 1.25rem;
  border: 1px dashed var(--rule-strong);
  border-radius: var(--radius);
  background: var(--surface);
}
.example-row .index { margin-top: 0; }
.example-row .row.is-example { border-left-style: dashed; }
.example-row .ribbon .example-tag {
  display: block;
  font-size: var(--step--2); font-weight: 400;
  text-transform: none; letter-spacing: 0;
  color: var(--ink-faint);
}
.example-row-caption {
  margin: .9rem 0 0; padding-top: .9rem;
  border-top: 1px solid var(--rule);
  font-family: var(--sans); font-size: var(--step--2); line-height: 1.55;
  color: var(--ink-faint);
}

/* --- detail page ------------------------------------------------------- */

.detail { padding-block: 1.75rem; }
/* A 52rem measure, but starting at the page's left gutter rather than
   centred, so the article's edge lines up with the masthead and the index. */
.detail > * { max-width: 52rem; }
.detail .lede { margin-top: .75rem; }

/* Cuisine and dietary terms on a detail page are navigation, so they stay
   links — but they stop being orange lozenges. Several hundred small filled
   pills per index page were the main reason the old grid read as busy, and
   metadata is not an action. */
.tags { display: flex; flex-wrap: wrap; gap: .3rem .9rem; margin: .5rem 0 0;
  padding: 0; }
.tag {
  font-size: var(--step--2); font-weight: 600;
  text-transform: uppercase; letter-spacing: .085em;
  color: var(--ink-faint); text-decoration: none;
  border-bottom: 1px solid var(--rule-strong);
}
a.tag:hover { color: var(--accent); border-bottom-color: currentColor; }
.tag-diet { color: var(--gold); }
a.tag-diet:hover { color: var(--gold); }

.pricebox {
  margin: 1.5rem 0;
  padding: 1.1rem 1.15rem 1.15rem;
  background: var(--surface);
  border-top: 2px solid var(--ink);
  border-bottom: 1px solid var(--rule);
  display: flex; flex-wrap: wrap; gap: 1.1rem 2.5rem;
  align-items: baseline;
}
.pricebox .cell { min-width: 6rem; }
.pricebox .label {
  display: block; margin-bottom: .15rem;
  font-size: var(--step--2); font-weight: 600;
  text-transform: uppercase; letter-spacing: .085em;
  color: var(--ink-faint);
}
.pricebox .amt {
  display: block;
  font-family: var(--serif); font-weight: 600; line-height: 1.05;
  font-size: clamp(1.6rem, 4vw, 2.05rem); letter-spacing: -0.02em;
  white-space: nowrap;
}
.pricebox .amt.none { color: var(--rule-strong); font-weight: 400; }
.pricebox .none-label { display: block; font-size: .82rem; color: var(--ink-faint); }
.pricebox .styles { display: block; font-size: var(--step-1); line-height: 1.3; }
.pricebox .src {
  flex-basis: 100%; margin: 0; font-size: .82rem; color: var(--ink-faint);
}
.pricebox .src a { color: var(--ink-faint); }

.factbox {
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--radius); padding: 1.25rem; margin: 1.5rem 0;
}
.factbox dl { display: grid; grid-template-columns: 9rem 1fr; gap: .5rem 1rem; margin: 0; }
.factbox dt {
  color: var(--ink-faint); font-size: var(--step--2); font-weight: 600;
  text-transform: uppercase; letter-spacing: .07em; padding-top: .28rem;
}
.factbox dd { margin: 0; }
.book { margin: 1.25rem 0 0; }
.button {
  display: inline-block; padding: .7rem 1.2rem;
  background: var(--accent); color: var(--surface);
  border-radius: var(--radius); text-decoration: none; font-weight: 600;
}
.button:hover { background: var(--accent-deep); color: var(--surface);
  text-decoration: none; }
.callout {
  border-left: 3px solid var(--rule-strong); padding: .25rem 0 .25rem 1rem;
  color: var(--ink-soft); margin: 1.5rem 0; max-width: 62ch;
}
.provenance { font-size: .85rem; color: var(--ink-faint); margin-top: 2rem; }
.related h2 { border-bottom: 1px solid var(--ink); padding-bottom: .5rem; }

/* --- prose ------------------------------------------------------------- */

/* A 42rem measure that starts at the page's left gutter, so a guide's
   first line sits under the masthead's first letter rather than floating in
   the middle of the window. */
.prose { padding-block: 1.75rem 3rem; }
.prose > * { max-width: 42rem; }
.prose > table { max-width: 46rem; }
.prose p, .prose li {
  font-family: var(--serif); font-size: 1.0625rem; line-height: 1.7;
  color: var(--ink); text-wrap: pretty;
}
.prose p { max-width: 68ch; }
.prose h2 { margin-top: 2.75rem; }
.prose h3 { margin-top: 2rem; }
/* build_site.py wraps every table in this. A five-column table is wider
   than a phone, and the wrapper scrolls where `display:block` on the table
   would have dropped its semantics. */
.table-scroll { overflow-x: auto; }
.prose table { width: 100%; border-collapse: collapse; margin: 1.5rem 0;
  font-family: var(--sans); font-size: .9rem;
  font-variant-numeric: tabular-nums lining-nums; }
.prose th, .prose td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--rule); }
.prose th {
  color: var(--ink-faint); font-size: var(--step--2); font-weight: 600;
  text-transform: uppercase; letter-spacing: .07em;
  border-bottom-color: var(--ink);
}
.prose td { color: var(--ink-soft); }
.prose hr { border: 0; border-top: 1px solid var(--rule); margin: 2.5rem 0 1.5rem; }
.updated { font-size: .85rem; color: var(--ink-faint); margin-top: -.25rem;
  font-family: var(--sans); }

/* --- guides strip and CTA ---------------------------------------------- */

.guides-strip { padding-block: 1rem; }
.guidelist { list-style: none; margin: .75rem 0 0; padding: 0; }
.guidelist a {
  display: block; padding: .8rem 0;
  border-top: 1px solid var(--rule);
  text-decoration: none; color: var(--ink);
}
.guidelist li:first-child a { border-top: 0; }
.guidelist strong {
  font-family: var(--serif); font-size: var(--step-1); font-weight: 600;
  letter-spacing: -0.008em;
}
.guidelist a:hover strong { color: var(--accent); text-decoration: underline;
  text-underline-offset: 3px; }
.guidelist span { display: block; font-size: var(--step--1);
  color: var(--ink-faint); margin-top: .15rem; max-width: 68ch; }

.cta {
  margin-block: 3rem; padding: 1.4rem 1.5rem;
  background: var(--accent-soft); border-radius: var(--radius);
}
.cta h2 { margin-top: 0; }
.cta p { color: var(--ink-soft); max-width: 58ch; margin-bottom: 0; }

/* --- footer ------------------------------------------------------------ */

.footer {
  border-top: 1px solid var(--rule);
  background: var(--paper-alt);
  margin-top: 4rem; padding-block: 2.5rem 3rem;
}
.footer .wrap {
  display: grid; gap: 2rem;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
.footcol h2 { font-family: var(--sans); font-size: var(--step--2);
  text-transform: uppercase; letter-spacing: .085em; font-weight: 600;
  color: var(--ink-faint); margin: 0 0 .6rem; }
.footcol ul { list-style: none; margin: 0; padding: 0; font-size: .9rem; }
.footcol li { margin-bottom: .3rem; }
.footcol a { color: var(--ink-soft); text-decoration: none; }
.footcol a:hover { color: var(--accent); text-decoration: underline; }
.footcol .count { color: var(--ink-faint); font-size: .78rem; }
.colophon {
  grid-column: 1 / -1; font-size: .82rem; color: var(--ink-faint);
  border-top: 1px solid var(--rule); padding-top: 1.5rem; max-width: 70ch;
}

/* --- narrow screens -----------------------------------------------------
   The masthead stays sticky here. It used to go static below 40rem, which is
   the one device where the index is a single very long column and a
   persistent way back matters most; the tagline drops instead. */

@media (max-width: 46rem) {
  .row { grid-template-columns: minmax(0, 1fr) 8rem; column-gap: 1.25rem; }
}

@media (max-width: 34rem) {
  .brand-text small { display: none; }
  .masthead-inner { padding-block: .5rem; }
  /* This was one non-wrapping line with overflow-x: auto and the scrollbar
     hidden, to keep the sticky masthead short on the device where the index
     is one very long column. It cost more than it saved: at 390px the row
     measured 381px of content in 350px of space, so the sixth link sat
     entirely past the right edge with no fade, arrow or clipped word to say
     the row scrolled. A reader with no reason to swipe simply did not have
     that link. Wrapping to a second line spends about 20px of every phone
     screen and gives all six back. */
  .topnav {
    width: 100%; gap: .5rem .95rem; font-size: .85rem;
    flex-wrap: wrap;
  }
  .topnav a { white-space: nowrap; }
  /* Taller bar, so the anchor offset above grows with it. */
  html { scroll-padding-top: 6.5rem; }
  /* At this width the key would sit on top of the low end of the scale. The
     caption names the median in words. */
  .strip-key { display: none; }
  .row { grid-template-columns: 1fr; }
  .price { text-align: left; margin-top: .45rem; }
  .price .amt, .price .unit { display: inline; }
  .price .unit { margin-left: .4em; }
  .price .min { display: inline; margin-left: .4em; }
  .hero { padding-block: 1.4rem .25rem; }
  .stats { gap: .9rem 1.5rem; }
  .stripfig figcaption, .rangefig figcaption { margin-top: .5rem; }
  /* One control per line. Side by side at this width the sort select and
     the toggle would each be a few characters wide and the bar would read as
     the three widgets it is there to stop being. */
  .controls { gap: .75rem; }
  .control, .control-filter, .controls select { width: 100%; max-width: none; }
  .control-toggle { padding-bottom: 0; }
  .control-clear { align-self: flex-start; }
  .factbox dl { grid-template-columns: 1fr; gap: .1rem .5rem; }
  .factbox dt { margin-top: .7rem; }
  .pricebox { gap: 1rem 1.75rem; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}
