/* ZipAisle redesigned UI — shared styles (see UX_REDESIGN_SPEC.md) */
/* Design tokens. Each one says what it may carry, because the palette shipped
   for a year with three below the WCAG AA floor for normal text (#56): --faint
   at 2.7:1, --muted at 3.8:1, and white on --green at 3.8:1 -- the last being
   every primary button on the site, the first carrying the Terms assent line.
   Ratios below are WCAG 2.1 against --bg (#f7faf7); tests/test_css_contrast.py
   measures them out of this block against every surface they actually land on,
   and fails if an edit here drops one.

   TEXT (>= 4.5:1 on every surface it lands on):
     --ink 17.2  body copy
     --muted 5.7  ALL sub-copy: captions, provenance, footers, the assent, the
                  inactive stepper, disabled control labels. #46's design board
                  calls this colour --muted-strong; here it is just --muted.
                  There is deliberately no quieter text tone: --faint was the
                  one, and it was retired rather than raised because all 26 of
                  its usages turned out to be text or a text-sized glyph and
                  there is no room between the 4.5 floor and 5.7 for a tone
                  anyone could tell apart. Adding --muted-strong alongside a
                  --muted left at 3.8:1 was the other option and would have
                  left ~40 rules still failing. Quiet is size and weight now.
     --green-dark 5.2   links and any text or glyph that must read as the brand
     --green-press 5.7  the pressed/hover step below --green-dark, and the
                        "this one won" voice on --tint

   GRAPHICS ONLY -- never `color:` on text (>= 3:1 where it carries meaning,
   1.4.11):
     --green 3.6  fills, borders, icons. As a text colour it fails AA in both
                  directions, which is what #56 was; the one exception is the
                  logo mark, which the standard exempts as a logotype.
     --pale 3.4   the same rule for a neutral: the remove control's glyph, the
                  disabled button's edge, and decorative ul bullets. It
                  was 2.1:1 and painting placeholders, .plan-line .note and
                  both unit labels until #59 moved those to --muted and
                  darkened what was left. A marker carrying a *number* is not
                  decoration: terms.html's contents list is the one, and it is
                  the only place a section number sits beside its title, which
                  is what a citation like "Terms 4.3" resolves against. That
                  one is --muted, in that page's own <style>.
     --green-soft, --line, --line-soft, --line-input, --chip-bg, --tint, --row,
     --bg, --disabled  surfaces and hairlines.

   MEASURED, BELOW 3:1, DELIBERATELY LEFT (#59): .bar-row .fill on its track is
   1.3:1, and the winning-cell tint is 1.1:1 on white -- .cmp-table td.best on
   the desktop table and .cmp-card .crow.best on the phone, the same #eef7ee. Neither is the only carrier
   of what it shows -- every bar sits under its .bwords and .btotal (#245) giving the
   standing in words and in money, and a cell's .best class and the check_circle
   inside it are set from one `cell.won`, on top of a 700 weight. 1.4.11 exempts
   a graphic that is not required to understand the content, so these pass
   *because of* that redundancy -- which makes the redundancy load-bearing.
   tests/test_css_contrast.py asserts it is still there, so deleting .bwords or
   the tick fails the suite rather than quietly removing the grounds for this
   paragraph. Raising them instead was measured and rejected: the lightest fill
   reaching 3:1 on that track is #28a128, 1.11:1 away from --green, which is
   what .fill.best already uses -- every bar would read as the winner.

   A literal is how a colour escapes every check here: #56 found one at 1.77:1
   and #59 moved it onto --pale. Add a token, do not type a hex.

   TEXT, added with accounts (#45):
     --danger 6.2   the error voice on the auth screens and the one destructive
                    control (delete account). 6.5 as white-on-it for .btn-danger.
                    admin.css had this hex inline; the two #c0392b literals in
                    this sheet predate it and are measured in PAIRS, not moved. */
:root{
  --green:#149914; --green-dark:#0f7a0f; --green-press:#127412; --green-soft:#b9dcb9;
  --ink:#111811; --muted:#4d6b4d; --pale:#788e78; --danger:#b3261e;
  --bg:#f7faf7; --line:#e5ece5; --line-soft:#eef3ee; --line-input:#dbe5db;
  --chip-bg:#eaf2ea; --tint:#dff3df; --row:#f2f6f2; --disabled:#dde7dd;
}
*{box-sizing:border-box}
[hidden]{display:none !important}
body{margin:0;background:var(--bg);font-family:'Work Sans',sans-serif;color:var(--ink)}
a{color:var(--green-dark);text-decoration:none}
a:hover{color:var(--green-press);text-decoration:underline}
input{font-family:inherit;color:var(--ink)}
input::placeholder{color:var(--muted)}
.ms{font-family:'Material Symbols Outlined';font-weight:normal;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased}

/* ---------- header + stepper ---------- */
.pp-header{display:flex;align-items:center;justify-content:space-between;padding:14px 32px;background:#fff;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20;gap:12px}
.pp-logo{display:flex;align-items:center;gap:8px;color:var(--ink)}
.pp-logo:hover{text-decoration:none;opacity:.85}
.pp-logo .mark{width:22px;height:22px;border-radius:6px;background:var(--green);display:flex;align-items:center;justify-content:center}
.pp-logo .mark .ms{font-size:14px;color:#fff}
.pp-logo .name{font-weight:700;font-size:16px}
/* Every rule below is scoped to the header on purpose. `.steps`, `.step` and
   `.conn` are far too generic to own globally: how-it-works.html numbers its
   own list `.steps`, and an unscoped `display:flex` here laid those four
   paragraphs out as columns (#38). The stepper is the only consumer -- these
   classes appear nowhere but inside `.pp-header` -- so it names its container. */
.pp-header .steps{display:flex;align-items:center;gap:26px}
.pp-header .step{display:flex;align-items:center;gap:7px;font-size:13px;font-weight:600;color:var(--muted)}
.pp-header .step .dot{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:#e7eee7;color:var(--muted);font-size:11px;font-weight:700}
.pp-header .step .dot .ms{font-size:13px}
.pp-header .step.done{color:var(--green-dark)}
.pp-header .step.done .dot{background:var(--green-dark);color:#fff}
.pp-header a.step.done:hover{text-decoration:none;opacity:.85}
.pp-header .step.active{color:var(--ink);font-weight:700}
.pp-header .step.active .dot{background:var(--ink);color:#fff}
.pp-header .step.working .dot{background:transparent;border:2px dashed var(--green);color:var(--green-dark)}
.pp-header .conn{width:32px;height:2px;border-radius:1px;background:var(--line-input)}
.pp-header .conn.on{background:var(--green)}
.pp-header .side{display:flex;align-items:center;gap:16px;min-width:96px;justify-content:flex-end}
.zip-badge{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px}
.zip-badge .ms{font-size:16px}
.header-action{display:flex;align-items:center;gap:6px;font-size:13px;font-weight:600;color:var(--muted);cursor:pointer;background:none;border:none;font-family:inherit}
.header-action:hover{color:var(--green-dark)}
.header-action .ms{font-size:16px}

/* ---------- primitives ---------- */
.wrap{width:100%;max-width:1100px;margin:0 auto}
.btn{display:flex;align-items:center;justify-content:center;gap:8px;font-weight:700;font-size:15px;border-radius:10px;border:none;cursor:pointer;font-family:inherit;padding:0 24px;height:52px}
.btn-primary{background:var(--green-dark);color:#fff}
.btn-primary:hover{filter:brightness(.94)}
/* Disabled pulls two ways. The old slab was a visible shape (1.9:1 against the
   card it sits in) carrying white text at the same 1.9:1, which nobody could
   read (#56); lightening the fill far enough for a --muted label at 4.7:1 left
   a control with almost no edge. So the label passes on the light fill and the
   border carries the shape -- one of --pale's three non-text jobs, at 3.5:1
   against white since #59 darkened the token from 2.2:1. Disabled controls are
   exempt from 1.4.11, so no test guards the edge: change either value and look
   at it. */
.btn-primary:disabled{background:var(--disabled);color:var(--muted);border:1.5px solid var(--pale);cursor:default;filter:none}
.btn .ms{font-size:18px}
.field{display:flex;align-items:center;gap:10px;background:#fff;border:1.5px solid var(--line-input);border-radius:10px;padding:0 16px;height:52px}
.field .ms{font-size:20px;color:var(--muted)}
.field input{border:none;outline:none;background:transparent;font-size:15px;width:100%}
.field.accent{border-color:var(--green);box-shadow:0 1px 4px rgba(20,153,20,.12)}
.field.accent .ms{color:var(--green)}
.card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:24px}
.chip{font-size:12.5px;font-weight:600;color:var(--muted);background:var(--chip-bg);border-radius:999px;padding:7px 14px;cursor:pointer;border:none;font-family:inherit;flex:none}
.chip:hover{background:var(--tint);color:var(--green-press)}
.tipbox{background:var(--chip-bg);border-radius:12px;padding:16px 18px;font-size:13px;line-height:1.5;color:#3f5c3f}
.list-main .tipbox{display:none}
.seg{display:flex;background:var(--chip-bg);border-radius:10px;padding:4px;gap:4px}
.seg button{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:9px 18px;border-radius:8px;font-size:13.5px;cursor:pointer;font-weight:600;color:#537553;background:transparent;border:none;font-family:inherit}
.seg button.on{background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.12);font-weight:700;color:var(--ink)}
.muted{color:var(--muted)}
.privacy{display:flex;align-items:flex-start;justify-content:center;gap:7px;font-size:12.5px;color:var(--muted);line-height:1.5;text-align:center}
.privacy .ms{font-size:15px;line-height:1.3}
/* Underlined, not coloured: --green-dark on --muted text is ~1.08:1, so colour alone cannot mark it (#400). */
.privacy a{color:var(--muted);text-decoration:underline}
.h1{font-size:28px;font-weight:900;letter-spacing:-.02em;margin:0}
.sub{font-size:14px;color:var(--muted);margin:0}

/* ---------- step 1: location ---------- */
.loc-page{display:flex;flex-direction:column;align-items:center;padding:88px 32px 96px}
.loc-col{width:460px;max-width:100%;display:flex;flex-direction:column;gap:22px}
.loc-col .headline{display:flex;flex-direction:column;gap:8px;text-align:center}
.loc-col .headline .h1{font-size:32px}
.loc-col .headline .sub{font-size:14.5px;line-height:1.55}
.loc-row{display:flex;gap:10px}
.loc-row .field{flex:1;height:56px}
.loc-row .field input{font-size:17px;font-weight:600;letter-spacing:.04em}
.loc-row .btn{height:56px;padding:0 26px}
.or-sep{display:flex;align-items:center;gap:12px;color:var(--muted);font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;margin:-4px 0}
.or-sep::before,.or-sep::after{content:"";flex:1;height:1px;background:var(--line)}
.btn-geo{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;height:50px;font-family:inherit;font-weight:600;font-size:14.5px;color:var(--green-dark);background:#fff;border:1.5px solid var(--line-input);border-radius:10px;cursor:pointer}
.btn-geo:hover{border-color:var(--green);background:var(--tint)}
.btn-geo:disabled{cursor:default;color:var(--muted);background:#fff;border-color:var(--line-input)}
.btn-geo .ms{font-size:19px}
.geo-status{margin:-8px 0 0;font-size:12.5px;line-height:1.45;text-align:center;color:var(--muted)}
.geo-status.ok{color:var(--green-press)}
.geo-status.err{color:#c0392b}

/* ---------- step 2: list ---------- */
.list-page{display:grid;grid-template-columns:1fr 300px;gap:28px;padding:32px 32px 48px}
.list-main{display:flex;flex-direction:column;gap:18px}
.add-row{display:flex;gap:10px}
.add-row .field{flex:1}
.chips{display:flex;gap:8px;flex-wrap:wrap}
.items{display:flex;flex-direction:column;gap:8px;margin-top:6px}
.item-row{display:flex;align-items:center;gap:14px;background:#fff;border:1px solid var(--line);border-radius:10px;padding:14px 16px}
.item-row .name{flex:1;font-size:15px;font-weight:500;line-height:1.3}
.qty{display:flex;align-items:center;gap:12px;background:var(--row);border-radius:8px;padding:5px 10px;font-size:13px;font-weight:700}
.qty button{border:none;background:none;cursor:pointer;font-family:inherit;font-size:14px;font-weight:700;padding:2px 6px}
.qty .minus{color:var(--muted)} .qty .minus:hover{color:var(--ink)}
.qty .plus{color:var(--green-dark)} .qty .plus:hover{color:var(--green-press)}
/* The only control on this row that destroys something, and it was #b7c7b7:
   1.77:1 on the row's white, the worst mark on the site. It was a literal,
   which is how it escaped both the token block and its test until #59. */
.item-row .remove{border:none;background:none;cursor:pointer;color:var(--pale);padding:4px}
.item-row .remove:hover{color:#c0392b}
.item-row .remove .ms{font-size:18px}
.list-side{display:flex;flex-direction:column;gap:12px}
/* Scoped to the sidebar for the same reason `.pp-header .steps` is scoped to
   the header (#38, #49): `.summary` is far too generic to own globally, and it
   had two other consumers. terms.html and privacy.html name their plain-language
   box `.summary` in their own <style> and never set `display`, so this rule's
   `display:flex` and `gap:14px` survived into a block of prose and stopped its
   paragraph margins collapsing. admin.js renders `<p class="summary">` in the
   chat answer bubble and was picking up the whole rule, `padding:20px` included
   -- 20px of inset on a paragraph whose siblings all sit flush against the
   bubble's own padding. Both now get what they actually declare. */
.list-side .summary{display:flex;flex-direction:column;gap:14px;padding:20px}
.list-side .summary .label{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}
.list-side .summary .row{display:flex;justify-content:space-between;font-size:14px}
.list-side .summary .row b{font-weight:700}
.list-side .summary hr{border:none;height:1px;background:var(--line);margin:0}
.mobile-cta{display:none}

/* ---------- working ---------- */
.working-page{padding:32px;display:flex;flex-direction:column;gap:20px;max-width:640px}
.progress{height:10px;background:#e7eee7;border-radius:999px;overflow:hidden}
.progress>div{height:100%;background:var(--green);border-radius:999px;transition:width .25s linear;width:0}
.progress-meta{display:flex;justify-content:space-between;font-size:13px;color:var(--muted)}
.savedbox{background:var(--chip-bg);border-radius:12px;padding:16px 18px;font-size:13px;line-height:1.5;color:#3f5c3f;display:flex;gap:9px}
.savedbox .ms{font-size:17px;line-height:1.3;color:var(--green)}
.errbox{display:flex;flex-direction:column;gap:12px;align-items:flex-start;padding:24px}
.work-stores{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px}
.wstore{display:flex;align-items:center;gap:10px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:12px 14px;font-size:13.5px;transition:border-color .2s,box-shadow .2s,opacity .2s}
.wstore .ms{font-size:19px;color:var(--muted);flex:none}
/* A status line longer than its card wraps inside it, under the store's name
   (#570): with the line at flex:none, "3 of 4 found · from Monday's check · 1
   item still checking" ran 74px past a 320px card, and so did the deadline's
   "· 3 didn't respond in time" (#267). The name's floor is what sends a line
   that only just fits under it: otherwise the name gave way, to 1px beside the
   deadline's line and to 6px beside "Checking…" and Skip. Measured at every
   width in tests/test_results_layout.py. */
.wstore .wname{font-weight:600;flex:1;min-width:60px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wstore .wstat{font-size:12px;color:var(--muted);flex:0 1 auto;min-width:0}
.wstore.queued{opacity:.65}
.wstore.checking{border-color:var(--green);box-shadow:0 1px 5px rgba(20,153,20,.12)}
.wstore.checking .ms{color:var(--green);animation:pp-spin 1s linear infinite}
.wstore.checking .wstat{color:var(--green-press);font-weight:600}
/* No `.wstore.done .wstat` rule: it used to darken the status line from --faint
   to --muted on completion, and --faint's retirement (#56) made the base colour
   the same. Completion is the green tick and the text itself changing from
   "Checking..." to a count -- not a third, now-invisible, cue. */
.wstore.done .ms{color:var(--green)}
/* A store that withheld its page did not fail and did not answer "none" -- it declined. Neutral, no error red, no blame. */
.wstore.declined{opacity:.8}
.wstore.declined .ms{color:var(--muted)}
.wstore.declined .wstat{color:var(--muted);font-style:italic}
/* Skip a store (#264). New rules name their container (#38): `.work-stores
   .wstore`, never a bare `.wstore`. The chooser opens in place, on a row of its
   own under the name. A skipped card reads as finished and set aside; its icon
   is skip_next, not the declined store's do_not_disturb_on, so each meaning
   keeps its own mark (block stays unused: an opted-out store has no card, #389). */
.work-stores .wstore{flex-wrap:wrap}
.work-stores .wstore .wskip{margin-left:auto;flex:none;background:#fff;border:1px solid var(--line-input);border-radius:999px;padding:3px 11px;font:inherit;font-size:12px;font-weight:600;color:var(--green-dark);cursor:pointer}
.work-stores .wstore .wskip:hover{border-color:var(--green);color:var(--green-press)}
.work-stores .wstore .wskip:disabled{cursor:default;color:var(--muted);border-color:var(--line)}
.work-stores .wstore .wskip-choose{flex-basis:100%;display:flex;flex-wrap:wrap;gap:6px}
.work-stores .wstore .wskip-opt,.work-stores .wstore .wskip-cancel{background:#fff;border:1px solid var(--line-input);border-radius:8px;padding:6px 10px;font:inherit;font-size:12.5px;font-weight:600;color:var(--green-dark);cursor:pointer}
.work-stores .wstore .wskip-opt:hover{border-color:var(--green);color:var(--green-press)}
.work-stores .wstore .wskip-cancel{border-color:transparent;color:var(--muted)}
.work-stores .wstore .wskip-opt:disabled,.work-stores .wstore .wskip-cancel:disabled{cursor:default;color:var(--muted)}
.work-stores .wstore .wskip-note{flex-basis:100%;font-size:12px;color:var(--muted)}
.work-stores .wstore.skipped{opacity:.8}
.work-stores .wstore.skipped .ms{color:var(--muted)}
.work-stores .wstore.skipped .wstat{color:var(--muted);font-style:italic;flex:0 1 auto;min-width:0;max-width:55%;text-align:right}
.work-stores .wstore.skipped .wskip-link{font-style:normal}
/* An added item still out at a finished store (#570, results.js ZA.work.adding):
   the search is done with it, so the card keeps `.done`; its icon turns as a
   checking card's does. */
.work-stores .wstore.adding .ms{animation:pp-spin 1s linear infinite}
/* The search's items (#365, ZA.add.paintItems): read-only, in list order, in
   columns so a long list does not push the Add control off the screen; an
   added item's row takes the full width, since it carries its state ("Eggs ·
   Checking…") and is always last. A row the quota held back stays in its
   cell: it can sit mid-list (a free item after the cut still runs), and a
   full-width row there would leave holes in the columns before it. A name may
   be one unbreakable run (L-EDGE's 120 characters), so it wraps anywhere.
   Every rule names .working-page (#38). */
.working-page .work-items{background:#fff;border:1px solid var(--line);border-radius:12px;padding:14px 16px}
.working-page .work-items-head{margin:0 0 8px;font-size:14px;font-weight:700;color:var(--ink)}
.working-page .work-items-list{margin:0;padding:0;list-style:none;display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:4px 16px;font-size:13.5px;color:var(--ink)}
.working-page .work-items-list .work-item{min-width:0;overflow-wrap:anywhere}
.working-page .work-items-list .work-item.added{grid-column:1/-1}
.working-page .work-items-list .work-item.checking{color:var(--green-press)}
.working-page .work-items-list .work-item.withheld,.working-page .work-items-list .work-item.failed{color:var(--muted)}
.working-page .work-waiting{margin:0;font-size:14px;line-height:1.5;color:var(--muted);overflow-wrap:anywhere}
.working-page .work-waiting:not(:empty){margin:4px 0 12px}
/* The clock and the time left (#618) drop under the status line when the two
   do not fit side by side, the overrun line on a phone; the words never change. */
.working-page .progress-meta{flex-wrap:wrap;column-gap:12px}
.boundary{display:flex;align-items:center;gap:6px;margin:10px 0 0;font-size:12px;color:var(--muted)}
.boundary .ms{font-size:15px}
@keyframes pp-spin{to{transform:rotate(360deg)}}
/* notify-me panel (results.html's working state; trip.html has none, #514) */
.notify{background:#fff;border:1px solid var(--line);border-radius:14px;padding:18px 20px;display:flex;flex-direction:column;gap:12px}
.notify-head{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:700}
.notify-head .ms{font-size:19px;color:var(--green)}
.notify-copy{margin:0;font-size:13px;line-height:1.5;color:var(--muted)}
.notify-row{display:flex;gap:10px}
.notify-row .field{flex:1}
.notify-row .btn{flex:none;height:52px}
.notify-status{margin:0;font-size:12.5px;line-height:1.45;color:var(--muted)}
.notify-status.ok{color:var(--green-press)}
.notify-status.err{color:#c0392b}

/* ---------- results ---------- */
.tabs{display:flex;gap:28px;padding:0 32px;background:#fff;border-bottom:1px solid var(--line)}
.tab{display:flex;align-items:center;gap:6px;padding:12px 2px 10px;font-size:13.5px;cursor:pointer;font-weight:600;color:var(--muted);border:none;background:none;font-family:inherit;border-bottom:2.5px solid transparent}
.tab .ms{font-size:16px}
.tab:hover{color:var(--ink)}
.tab.on{font-weight:700;color:var(--ink);border-bottom-color:var(--green)}
.tab.on .ms{color:var(--green)}
.results-pane{padding:28px 32px 48px;display:flex;flex-direction:column;gap:24px}
/* Where the prices came from and when they were read. Was .fresh, which claimed
   a recency the week-long shared cache cannot deliver — renamed so the name
   can't drift back into a freshness stamp. align-items:flex-start, not center:
   this is two lines now and a centred icon floats between them. */
.readout{display:flex;align-items:flex-start;gap:9px;background:var(--tint);border-radius:10px;padding:11px 14px;font-size:13px;color:var(--green-press);line-height:1.45;align-self:flex-start;max-width:72ch}
.readout .ms{font-size:17px;color:var(--green);flex:none}
.readout-age{display:block;opacity:.82;margin-top:2px}
.readout-age:empty{display:none}
/* #709, #781: the multi-zip sentence lives in the header above 960px, and here up to 960px
   and in print, which has no header. */
.readout-across{display:none}
/* The stores the shopper skipped (#264), under the readout on results.html and
   trip.html: a quiet column, one store per entry on its two lines, then the
   link to the Skipped stores page. */
.results-pane .skipped-line{display:flex;align-items:flex-start;gap:9px;padding:0 14px;font-size:13px;color:var(--muted);line-height:1.45;max-width:72ch}
.results-pane .skipped-line .ms{font-size:17px;color:var(--muted);flex:none}
.results-pane .skipped-line .sk-body{display:flex;flex-direction:column;gap:4px}
.results-pane .skipped-line .sk-head{font-weight:600}
.results-pane .skipped-line .sk-sep{display:none}
.results-pane .skipped-line .sk-store .s-chain{font-weight:600;color:var(--ink)}
/* #711: the info boxes (stamps, readout, skipped stores) follow the plan as one group.
   The stamps' own bottom margin is the pane's gap here, so none is left orphaned at the end. */
.results-pane .plan-notes{display:flex;flex-direction:column;gap:12px}
.results-pane .plan-notes .saved-stamp{margin:0}
.plan-head{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;flex-wrap:wrap}
.plan-total{font-size:44px;font-weight:900;letter-spacing:-.03em}
.save-chip{background:var(--tint);color:var(--green-press);font-weight:700;font-size:14px;border-radius:999px;padding:6px 14px}
.eyebrow{font-size:13px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
/* minmax(0,…), never a bare `fr` (#245): `1fr` is minmax(auto,1fr), and `auto`
   is the widest thing in the column that cannot wrap -- a plan line or a store
   bar -- so the column outgrew the page and the price sat off-screen on every
   phone, and at 761-990px on the desktop layout too. Each column is its share
   of the page; what is inside it wraps to fit (.plan-line, .bar-row below). */
.plan-grid{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);gap:20px}
.store-head{display:flex;align-items:center;gap:10px;background:var(--row);border-radius:8px;padding:10px 14px}
.store-head .ms{font-size:18px;color:var(--green)}
.store-head .total{margin-left:auto;font-weight:700;font-size:14px}
/* The name takes the line; the verdict and price (.pl-tail, one box) sit at its
   right end, and when the card is too narrow for both (#245: a phone, or the
   desktop column under ~1000px) the box wraps onto a line of its own, still
   right-aligned under the store's total -- the price is never pushed out of the
   card. The 12em basis is where that wrap happens. The verdict used to float
   midway along its row (space-between); it now sits against the price. */
.plan-line{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:2px 14px;padding:6px 14px 6px 42px;font-size:14px}
.plan-line .pl-left{flex:1 1 12em;display:flex;flex-direction:column;gap:2px;min-width:0;overflow-wrap:break-word}
/* Too narrow even for the box alone, the verdict goes above the price. */
.plan-line .pl-tail{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:2px 14px}
/* Per-row provenance (issue #20): an agent's report is checkable, so every
   price says when it was read and links to the page it was read from. */
.plan-line .prov{font-size:11.5px;color:var(--muted);line-height:1.45}
/* Wraps like the name does (#245): "See it at" carries the store's street line,
   and unbroken it was the widest thing on the plan. */
.plan-line .prov a{color:var(--green-dark);text-decoration:none}
.plan-line .prov a:hover{text-decoration:underline}
.plan-line .note{color:var(--muted);font-size:12.5px}
.plan-line .price{font-weight:600;flex:none;font-variant-numeric:tabular-nums;min-width:56px;text-align:right}
/* What a plan line is: the row's best unit price, how far behind it is, or why
   it was not compared. Silence here reads as a win (issue #28 V-10). */
.plan-line .pl-verdict{font-size:12.5px;color:var(--muted);white-space:nowrap;flex:none;
  display:flex;align-items:center;gap:4px}
.plan-line .pl-verdict.won{color:var(--green-press);font-weight:600}
.plan-line .pl-verdict .ms{font-size:14px}
.store-head .count{font-size:12.5px;font-weight:500;color:var(--muted);white-space:nowrap}
.unfound{margin:4px 0 0;font-size:12.5px;color:var(--muted)}
.bars{display:flex;flex-direction:column;gap:12px;font-size:13px}
/* Name and total on top, the standing under the total, the bar the card's full
   width beneath (#245). It was one line -- a 150px name, the bar, the words,
   the total -- which needed ~450px and the side column never has that: the
   column was forced wider instead (the phone's sideways scroll), and a bar
   beside "about 34% more expensive" shrank to nothing. Now every bar is the
   same length, so the fills compare. The right column is at most half the row,
   so a long standing wraps before it takes the store's name. */
.bar-row{display:grid;grid-template-columns:minmax(0,1fr) fit-content(50%);
  grid-template-areas:"name total" "name words" "track track";gap:2px 10px;align-items:center}
.bar-row .bname{grid-area:name;min-width:0;font-weight:600}
/* Two lines since #208's street lines (review P3, 2026-09-24): each line cut
   on its own, never the street folded into the chain's line. */
.bar-row .bname .s-chain,.bar-row .bname .s-street{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bar-row .track{grid-area:track;margin-top:4px;height:16px;background:var(--line-soft);border-radius:999px;overflow:hidden}
.bar-row .fill{height:100%;border-radius:999px;background:var(--green-soft)}
.bar-row .fill.best{background:var(--green)}
.bar-row .btotal{grid-area:total;text-align:right;font-weight:700;font-variant-numeric:tabular-nums}
/* The bar is how close this store's unit prices are to the best (full = best),
   the words are its standing, the money is money. It used to encode the cart
   total while the list was ordered by value -- two scales in one widget. */
/* May wrap (the column above caps it): "none found; 2 couldn't be checked" is a
   standing too, and unbroken it would take the name's width. */
.bar-row .bwords{grid-area:words;justify-self:end;text-align:right;font-size:12px;color:var(--muted)}
.bar-row .bwords.best{color:var(--green-press);font-weight:700}
/* compare table (desktop) */
.cmp-table{background:#fff;border:1px solid var(--line);border-radius:14px;overflow:hidden}
.cmp-table table{width:100%;border-collapse:collapse;font-size:13.5px}
.cmp-table th{padding:12px 14px;text-align:left;font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);border-bottom:1px solid var(--line)}
.cmp-table th:first-child,.cmp-table td:first-child{padding-left:20px}
.cmp-table td{padding:13px 14px;border-bottom:1px solid var(--line-soft);vertical-align:top}
.cmp-table td.best{background:#eef7ee}
.cmp-price{display:flex;align-items:center;gap:5px;font-weight:600}
td.best .cmp-price{font-weight:700}
.cmp-price .ms{font-size:14px;color:var(--green)}
.cmp-unit{font-size:11.5px;color:var(--muted)}
/* #569: the product line links to the retailer's page and reads as text until hover or
   keyboard focus. --green-dark, never --green, is the text form (#59). */
.results-pane .cmp-table .cmp-unit a,.results-pane .cmp-cards .unit a{color:inherit;text-decoration:none;cursor:pointer}
.results-pane .cmp-table .cmp-unit a:hover,.results-pane .cmp-cards .unit a:hover,
.results-pane .cmp-table .cmp-unit a:focus-visible,.results-pane .cmp-cards .unit a:focus-visible{text-decoration:underline;color:var(--green-dark)}
/* R-13: one row, one unit, said once at the head of the row. */
.cmp-table td.item{width:170px}
.cmp-ul{font-size:11.5px;color:var(--muted);margin-top:2px}
.cmp-price{font-variant-numeric:tabular-nums}
/* A store that found nothing is one line under the table, not a column of
   "not found" cells and a $0.00 total (issue #28 V-7). */
.collapsed{display:flex;flex-direction:column;gap:6px}
.collapsed-store{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--muted)}
.collapsed-store .ms{font-size:16px;color:var(--muted);flex:none}
.cmp-card .ctitle-ul{font-weight:500;color:var(--muted);font-size:11.5px;margin-left:8px}
/* a cell shown but deliberately outside the ranking (issue #28 R1) */
.cmp-note{font-size:11.5px;color:var(--muted);font-style:italic;margin-top:2px}
.cmp-table td.aside .cmp-price{color:var(--muted);font-weight:500}
.cmp-card .crow.aside .price{color:var(--muted);font-weight:500}
/* The fourth empty state (#208): a store this watched item was never asked at.
   Muted and italic -- not an outage and not an empty shelf -- and never marked. */
.cmp-table td.out-of-area .cmp-unit{color:var(--muted);font-style:italic}
.cmp-card .crow.out-of-area .price{color:var(--muted);font-style:italic;font-weight:500}
.cmp-table tfoot td{font-weight:700;background:var(--bg);border-bottom:none}
.cmp-table tfoot td.win{background:var(--tint);color:var(--green-press);font-weight:900}
.legend{display:flex;align-items:center;gap:6px;font-size:12.5px;color:var(--muted)}
.legend .swatch{width:11px;height:11px;border-radius:3px;background:#eef7ee;border:1px solid var(--green);flex:none}
/* compare cards (mobile) */
.cmp-cards{display:none;flex-direction:column;gap:12px}
.cmp-card{background:#fff;border:1px solid var(--line);border-radius:14px;overflow:hidden}
.cmp-card .ctitle{padding:12px 15px 10px;font-size:14px;font-weight:700;border-bottom:1px solid var(--line-soft)}
.cmp-card .crow{display:flex;align-items:center;gap:8px;padding:9px 15px;border-bottom:1px solid var(--row);font-size:13px}
.cmp-card .crow.best{background:#eef7ee}
.cmp-card .crow .store{flex:1;font-weight:600;color:#3f5c3f}
.cmp-card .crow .unit{font-size:11.5px;color:var(--muted);flex:1;text-align:right}
.cmp-card .crow .price{width:auto;min-width:64px;text-align:right;font-weight:600;white-space:nowrap}
/* #521: the Store standing card puts a sentence in .price ("none of these 5 prices could be
   checked"), and nowrap ran it under .unit on a phone. Only that card's price may wrap, so an
   item row's "≈ $1.99/lb" stays whole; max-width keeps a sentence from taking the row from the
   store and the cart. */
.cmp-card .crow .price.standing{max-width:45%;flex:0 1 auto;white-space:normal;overflow-wrap:break-word}
.cmp-card .crow.best .price{font-weight:700}
.cmp-card .crow .ms{font-size:15px;color:var(--green);width:16px;text-align:right}

/* ---------- tablet and phone header (#781) ----------
   The whole desktop header -- every step's label, "Checked 12 stores across" three zips,
   Print plan and the account -- first fits at about 890px with the page's own fonts, its zip
   sentence wrapped to three lines (Chromium, WebKit and Firefox, measured 2026-10-10; on one
   line it needs about 1180px); narrower, "3 Your plan" ran under the zip badge on an iPad
   (768px). So up to 960px the stepper keeps only the current step's label and a run over
   several zips says so in the readout, as a phone always has (#709). */
@media (max-width:960px){
  .pp-header .steps{gap:10px}
  /* visually hidden, not display:none: a done step's link keeps its name for a screen reader */
  .pp-header .step:not(.active) span.lbl{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
  .pp-header .conn{width:18px}
  .pp-header .zip-badge.across{display:none}
  .readout-across:not(:empty){display:block}
}

/* ---------- mobile ---------- */
@media (max-width:760px){
  .pp-header{padding:12px 16px}
  .pp-header .side{min-width:0;gap:10px;flex:0 1 auto}
  /* #709: nothing in the header wraps or runs over the stepper. The across sentence is in the
     readout (above); a single zip stays, cut with an ellipsis if it must be. */
  .pp-header .zip-badge{min-width:0;white-space:nowrap}
  .pp-header .zip-badge #zip-badge{overflow:hidden;text-overflow:ellipsis}
  .pp-header .header-action{white-space:nowrap}
  .pp-header #print-btn .lbl{display:none}
  .pp-header{gap:8px}
  /* #791: the stepper never shrinks below its content (it would overflow its box and run under
     the badge); the badge, a flex item of .side, gives way first and ellipsizes */
  .pp-header .steps{flex:0 0 auto}
  .loc-page{padding:56px 20px 64px}
  .list-page{grid-template-columns:1fr;padding:20px 16px 120px;gap:14px}
  .list-side{display:none}
  .list-main .tipbox{display:block}
  .mobile-cta{display:flex;flex-direction:column;gap:8px;position:fixed;left:0;right:0;bottom:0;background:#fff;border-top:1px solid var(--line);padding:12px 16px calc(12px + env(safe-area-inset-bottom));z-index:15}
  .mobile-cta .meta{font-size:12.5px;color:var(--muted)}
  .chips{flex-wrap:nowrap;overflow-x:auto;padding-bottom:2px}
  .working-page,.results-pane{padding:20px 16px 48px}
  .tabs{padding:0 16px;gap:20px}
  .plan-grid{grid-template-columns:minmax(0,1fr)}
  .plan-total{font-size:36px}
  .cmp-table{display:none}
  .cmp-cards{display:flex}
  .add-row .btn{padding:0 18px}
  .notify-row{flex-direction:column}
  .notify-row .btn{width:100%}
}
@media print{
  .pp-header,.tabs,.legend,.mobile-cta{display:none}
  /* the header is not printed, so its multi-zip sentence is printed in the readout (#781) */
  .readout-across:not(:empty){display:block}
  body{background:#fff}
  .card{border-color:#ccc}
}

/* ---------- identity in the frame (issue #22) ---------- */
.pp-foot{display:flex;flex-wrap:wrap;justify-content:center;gap:8px 22px;
  padding:28px 24px 34px;margin-top:8px;border-top:1px solid var(--line);
  font-size:12.5px;color:var(--muted)}
.pp-foot a{color:var(--muted);text-decoration:none}
.pp-foot a:hover{color:var(--ink);text-decoration:underline}
/* The assent under the run button (Terms 3.2). Quiet, but not hidden: it is
   the sentence that makes the button an appointment rather than a search. */
.assent{margin:10px 0 0;font-size:11.5px;line-height:1.5;color:var(--muted)}
.assent a{color:var(--muted);text-decoration:underline}
/* Terms 3.3 in one line. A disclosure that is also the selling point. */
.scope{display:flex;gap:7px;margin:14px 0 0;font-size:11.5px;line-height:1.5;color:var(--muted)}
.scope .ms{font-size:15px;color:var(--green);flex:none}
@media print{.pp-foot{display:none}}

/* The total is an estimate read off public pages, not a quote (Terms 5). */
.est-note{margin:2px 0 0;font-size:12.5px;color:var(--muted)}
.excl{margin-top:6px;font-size:12.5px;color:var(--muted)}
.excl summary{cursor:pointer;color:var(--green-dark);font-weight:600;list-style:none}
.excl summary::-webkit-details-marker{display:none}
.excl summary::before{content:'+ ';font-weight:700}
.excl[open] summary::before{content:'− '}
.excl p{margin:8px 0 0;max-width:52ch;line-height:1.6}
.excl a{color:var(--green-dark)}
@media print{.excl{display:block}.excl summary{display:none}.excl p{margin-top:4px}}

/* The saved-plan stamp on trip.html (issue #22). */
.saved-stamp{display:flex;align-items:center;gap:8px;margin:0 0 12px;padding:10px 14px;
  background:var(--chip-bg);border-radius:10px;font-size:12.5px;color:var(--muted)}
.saved-stamp .ms{font-size:16px;color:var(--green);flex:none}

/* ---------- accounts (#45, drawn on the #46 board) ---------- */
/* The sheet had no visible focus style; the auth screens need one. One rule,
   global, on :focus-visible only so a mouse click does not paint it. */
:focus-visible{outline:2px solid var(--green-dark);outline-offset:2px}
/* The one header slot that changes: "Sign in" or "Your account", words not an
   avatar. Scoped to the header like the stepper (#38). */
.pp-header .auth-link{color:var(--muted);white-space:nowrap}
.pp-header .auth-link.signed-in{color:var(--green-dark)}
.pp-header .auth-link .ms{font-size:18px}
/* the single column every auth page uses: 420 wide, 460 for the account */
.auth-page{display:flex;flex-direction:column;align-items:center;padding:64px 32px 96px}
.auth-col{width:420px;max-width:100%;display:flex;flex-direction:column;gap:22px}
.auth-col.wide{width:460px}
.auth-col section{display:flex;flex-direction:column;gap:16px}
.auth-col .headline{display:flex;flex-direction:column;gap:8px}
.auth-col .sub{font-size:14.5px;line-height:1.55}
.auth-col form{display:flex;flex-direction:column;gap:12px}
.auth-col .h1:focus{outline:none}
.ms.auth-icon{font-size:40px;color:var(--green)}
.auth-error{margin:0;display:flex;gap:6px;align-items:flex-start;font-size:13px;line-height:1.45;color:var(--danger)}
.auth-error .ms{font-size:16px;flex:none;color:var(--danger)}
.auth-actions{display:flex;flex-direction:column;gap:10px}
.btn-secondary{background:#fff;color:var(--green-dark);border:1.5px solid var(--line-input)}
.btn-secondary:hover{border-color:var(--green);background:var(--tint)}
.btn-secondary:disabled{color:var(--muted);border-color:var(--line-input);background:#fff;cursor:default}
.btn-danger{background:var(--danger);color:#fff}
.btn-danger:hover{filter:brightness(.94)}
.btn-danger:disabled{background:var(--disabled);color:var(--muted);border:1.5px solid var(--pale);cursor:default;filter:none}
.auth-alt{display:block;background:none;border:none;font-family:inherit;font-size:14px;font-weight:600;color:var(--green-dark);cursor:pointer;padding:6px 0;text-align:center;text-decoration:none}
.auth-alt:hover{text-decoration:underline;color:var(--green-press)}
.auth-line{display:flex;gap:8px;margin:0;font-size:13.5px;line-height:1.5;color:var(--muted)}
.auth-line .ms{font-size:18px;color:var(--green);flex:none}
.auth-privacy{display:flex;gap:7px;margin:0;font-size:12.5px;line-height:1.5;color:var(--muted)}
.auth-privacy .ms{font-size:15px;color:var(--green);flex:none}
.auth-privacy a{color:var(--muted);text-decoration:underline}
.auth-pressed{display:flex;align-items:center;justify-content:center;gap:8px;height:52px;font-weight:700;color:var(--muted)}
.auth-pressed .ms{font-size:20px;color:var(--green);animation:pp-spin 1s linear infinite}
/* A link's offered list (#574) may be sixty items; capped so its two buttons stay on screen. */
.auth-col #offer-items{max-height:40vh;overflow:auto}
/* "Email me the results when the run finishes" (D9): a real checkbox in a
   label, the whole 44px row the target. */
.pp-optin{display:flex;align-items:center;gap:10px;min-height:44px;font-size:14px;line-height:1.4;cursor:pointer}
.pp-optin input{width:20px;height:20px;margin:0;flex:none;accent-color:var(--green-dark)}
.pp-optin-dest{display:flex;gap:6px;align-items:center;margin:-8px 0 0;font-size:12.5px;color:var(--muted)}
.pp-optin-dest .ms{font-size:15px;color:var(--green)}
/* the email requirement, one line above the run button (D1) */
.pp-gate-note{display:flex;gap:8px;margin:0;background:var(--chip-bg);border-radius:12px;padding:12px 14px;font-size:13px;line-height:1.5;color:var(--muted)}
.pp-gate-note .ms{font-size:17px;color:var(--green);flex:none}
.pp-signed-status{margin:0;font-size:13.5px;line-height:1.5;color:var(--green-press);font-weight:600}
/* the gate sheet and the delete sheet: centred on desktop, a bottom sheet on a phone */
.pp-gate{position:fixed;inset:0;z-index:40;display:flex;align-items:center;justify-content:center}
.pp-gate-backdrop{position:absolute;inset:0;background:rgba(17,24,17,.45)}
.pp-gate-sheet{position:relative;background:#fff;border-radius:16px;padding:28px;width:460px;max-width:calc(100% - 32px);max-height:calc(100% - 32px);overflow:auto;box-shadow:0 12px 40px rgba(0,0,0,.18);display:flex;flex-direction:column;gap:14px}
.pp-gate-sheet section{display:flex;flex-direction:column;gap:14px}
.pp-gate-sheet form{display:flex;flex-direction:column;gap:12px}
.pp-gate-sheet .h2:focus,.pp-gate-sheet .pp-gate-title:focus{outline:none}
.pp-gate-title{margin:0;font-size:22px;font-weight:900;letter-spacing:-.02em}
.pp-gate-handle{display:none}
/* the account page */
.acct-signed{display:flex;flex-direction:column;gap:4px}
.acct-signed .label{font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}
.acct-signed .acct-email{font-size:18px;font-weight:700;word-break:break-all}
.acct-actions{display:flex;flex-direction:column;gap:18px}
.acct-action{display:flex;flex-direction:column;gap:6px}
.acct-action .btn{width:100%}
.acct-action .note{margin:0;font-size:13px;line-height:1.5;color:var(--muted)}
/* The Skipped stores page (#265): one row per store, the chain over its street
   and zip, and "Include again" beside it. Named by its container (#38). */
.skip-page .sk-rows{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
.skip-page .sk-row{display:flex;align-items:center;justify-content:space-between;gap:12px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:12px 14px}
.skip-page .sk-names{display:flex;flex-direction:column;gap:2px;min-width:0}
.skip-page .sk-chain{font-weight:600;color:var(--ink)}
.skip-page .sk-where{font-size:13px;color:var(--muted);overflow-wrap:anywhere}
.skip-page .sk-note{font-size:12px;color:var(--muted);font-style:italic}
.skip-page .sk-include{flex:none;width:auto}
.skip-page .sk-back{margin:4px 0 0;font-size:14px}
/* "Delete my account" is a text link, not a red button: reachable, not prominent. */
.acct-delete{background:none;border:none;font-family:inherit;font-size:14px;font-weight:600;color:var(--danger);cursor:pointer;padding:6px 0;text-align:left}
.acct-delete:hover{text-decoration:underline}
.pp-inventory{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}
.pp-inventory li{display:flex;gap:8px;font-size:14px;line-height:1.45}
.pp-inventory li .ms{font-size:18px;color:var(--danger);flex:none}
.pp-inventory.quiet li{font-size:13px;color:var(--muted)}
.pp-inventory.quiet li .ms{color:var(--muted)}
.pp-inventory-head{margin:4px 0 0;font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}
/* the continuity line above the progress bar, and the dismissible history note */
.pp-join{display:flex;align-items:center;gap:8px;margin:0 0 10px;font-size:13.5px;font-weight:600;color:var(--green-press)}
.pp-join .ms{font-size:18px;color:var(--green)}
.saved-stamp .dismiss{margin-left:auto;display:flex;background:none;border:none;cursor:pointer;color:var(--muted);padding:2px}
.saved-stamp .dismiss .ms{font-size:16px;color:var(--muted)}
.saved-stamp .dismiss:hover{color:var(--ink)}
/* ---------- the weekly search quota (#110, design #109) ---------- */
/* Every rule here names its container, per the rule this sheet's own #38 and
   #49 wrote: `.mark`, `.quota-line` and `.quota-cost` are exactly the kind of
   generic names a prose page would reach for on its own. Colours are tokens
   only, and every foreground/background pair below is one tests/test_css_
   contrast.py already measures. */
/* The count, always visible where a run starts. The same tinted strip as the
   results readout, because it does the same job: a fact about this run, stated
   before it happens. */
.list-side .quota-line{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin:0;
  background:var(--tint);border-radius:10px;padding:10px 12px;font-size:12.5px;
  line-height:1.45;color:var(--green-press);font-weight:600}
.list-side .quota-line .quota-more{color:var(--green-press);text-decoration:underline;font-weight:700}
.list-side .quota-line .quota-info{margin-left:auto;display:flex;align-items:center;
  background:none;border:none;padding:2px;cursor:pointer;font-family:inherit}
.list-side .quota-line .quota-info .ms{font-size:16px;color:var(--green)}
.list-side .quota-help{margin:-6px 0 0;font-size:12px;line-height:1.5;color:var(--muted)}
/* The cost line, tied to the button below it. Not an error voice when the list
   is over: the run still happens, for what is left. */
.list-side .quota-zip{margin:0;font-size:12.5px;line-height:1.45;color:var(--muted)}
.list-side .quota-cost{margin:0;font-size:13px;line-height:1.45;color:var(--muted);font-weight:600}
.list-side .quota-cost.over{color:var(--green-press);font-weight:700}
.mobile-cta .quota-cost{margin:0;font-size:12.5px;line-height:1.4;color:var(--muted);font-weight:600}
.mobile-cta .quota-cost.over{color:var(--green-press);font-weight:700}
/* The count could not be read (#140): the run is held and this is why. The
   same voice as the over line -- not an error voice, the page is fine. */
.list-side .quota-cost.unreadable,.mobile-cta .quota-cost.unreadable{color:var(--green-press);font-weight:700}
/* The same line inside the phone's fixed run bar (#141), where the side card
   is not rendered at all. Compact -- one line, tighter padding -- because the
   bar sits over the list and every pixel of it hides an item. The pairs are
   the side card's (--green-press on --tint, --green on --tint, --muted on the
   bar's white), all already in tests/test_css_contrast.py. list.js sets the
   page's bottom padding from the bar's real height, so nothing here has to
   guess how tall a signed-in bar is. */
.mobile-cta .quota-line{display:flex;align-items:center;flex-wrap:wrap;gap:4px 6px;margin:0;
  background:var(--tint);border-radius:8px;padding:7px 10px;font-size:12.5px;
  line-height:1.35;color:var(--green-press);font-weight:600}
.mobile-cta .quota-line .quota-more{color:var(--green-press);text-decoration:underline;font-weight:700}
.mobile-cta .quota-line .quota-info{margin-left:auto;display:flex;align-items:center;
  background:none;border:none;padding:2px;cursor:pointer;font-family:inherit}
.mobile-cta .quota-line .quota-info .ms{font-size:16px;color:var(--green)}
.mobile-cta .quota-help{margin:-4px 0 0;font-size:12px;line-height:1.45;color:var(--muted)}
.mobile-cta .quota-zip{margin:0;font-size:12px;line-height:1.4;color:var(--muted)}
/* The per-item mark on a list the shopper has seen before. */
.item-row .mark{flex:none;font-size:11.5px;font-weight:600;border-radius:999px;
  padding:4px 10px;background:var(--chip-bg);color:var(--muted);white-space:nowrap}
.item-row .mark.free{background:var(--tint);color:var(--green-press)}
/* The over-quota sheet: a section of the gate sheet, so it inherits the sheet's
   own layout. The withheld list is deliberately NOT a scroll box of its own (#670):
   a 34vh `overflow:auto` list drew a scrollbar of two dead arrow buttons when its
   fractional rows (13px x 1.45) rounded a pixel past the box, and nested a second
   scroller in the sheet's. The sheet (`max-height` + `overflow:auto`) scrolls as a
   whole, so the run and upgrade buttons stay reachable under a long list. */
.pp-gate #gate-quota .pp-inventory li .ms{color:var(--muted)}
/* The over-quota sheet's second button carries the card sentence for a lapsed
   member (#286): three lines on a phone, so it grows past .btn's 52px. */
.pp-gate #gate-quota .btn.lapsed{height:auto;min-height:52px;padding:12px 24px;
  line-height:1.35;text-align:center}
/* Results: the partial-run stamp's link, the once-only row footnote, and the
   saving beside the price of Plus. */
.saved-stamp .stamp-link{margin-left:auto;flex:none;background:none;border:none;
  font-family:inherit;font-size:12.5px;font-weight:700;color:var(--green-dark);
  cursor:pointer;padding:0;text-decoration:none;white-space:nowrap}
.saved-stamp .stamp-link:hover{text-decoration:underline}
/* A lapsed member's stamp link is the card sentence (#286), far longer than
   "See what Plus includes". Unbreakable beside the stamp's own sentence it ran
   a 320px phone sideways, so it takes a line of its own under it, indented to
   the text (the 16px icon and the 8px gap). The sentence keeps the first line
   with the icon: a basis of 0, or it would wrap away from it. */
.results-pane .saved-stamp.lapsed{flex-wrap:wrap}
.results-pane .saved-stamp.lapsed > span:not(.ms){flex:1 1 0;min-width:0}
.results-pane .saved-stamp.lapsed .stamp-link{flex:1 1 100%;margin-left:24px;white-space:normal}
.results-pane .quota-footnote{margin:0;font-size:12.5px;line-height:1.5;color:var(--muted)}
/* V-19 (#42): the one footnote a ≈ total carries, under the plan and under the
   table. V-22 (#243): the `*` footnote under the plan reads the same way. */
.results-pane .approx-note,.results-pane .value-note{margin:6px 0 0;font-size:12.5px;line-height:1.5;color:var(--muted)}
/* V-11 (#243): the stop toggle. Each button is as wide as its own words, not
   half the pair -- `.seg`'s flex:1 split them evenly and wrapped the bracket
   on a desktop -- and on a phone the bracket wraps to its own line, whole,
   never truncated. `.seg` stretches both buttons to one height. Scoped to the
   plan head: admin.html has a `.seg` of its own. */
.plan-head .seg button{flex:1 1 auto;flex-wrap:wrap;row-gap:2px;text-align:center}
/* "1 store" broke to "1 / store" when the pair's bracket took the width, so
   the label line -- the count and, since #251, the plan's total -- is kept
   whole wherever it fits. Letting it break at every width was measured and not
   taken (docs/price-comparability.md §6.1): flex shrinks both buttons in
   proportion, so at 360-390px it broke "1 store ·" / "≈ $37.39" and the
   bracket mid-phrase where both fit whole. */
.plan-head .seg .seg-label{white-space:nowrap}
/* #251: what a total leaves out ("· 1 not in total", a plan saved before
   measure version 3) wraps at any width; kept whole, it made the toggle over
   520px wide on every phone. */
.plan-head .seg .seg-excl{white-space:normal}
/* #251, #257: below 360px the two label lines do not fit whole side by side,
   so there the label may break between the count and the total, and only
   there -- or the page scrolls sideways. */
@media (max-width:359px){
  .plan-head .seg .seg-label{white-space:normal}
  .plan-head .seg .seg-stops,.plan-head .seg .seg-total{white-space:nowrap}
}
/* V-12 (#243): the side card's big line names a store; its street at card size. */
.results-pane #side-big .s-street{font-size:14px;font-weight:600}
.results-pane .plus-pitch{margin:2px 0 0;font-size:13px;line-height:1.5;color:var(--green-press);font-weight:600}
/* A lapsed member's card sentence stands in the pitch's place (#286) as a
   link, underlined as the list page's offer links are: the sheet's global
   `a` rule would leave it reading as plain text. */
.results-pane .plus-pitch a{color:inherit;text-decoration:underline;font-weight:700}
/* pricing.html links to itself from every footer; nothing else is needed here,
   the page styles itself in its own <style> like the other prose pages. */

@media (max-width:760px){
  .auth-page{padding:40px 20px 72px}
  .pp-header .auth-link.signed-in .lbl{display:none}
  .pp-header .auth-link.signed-in{width:44px;height:44px;justify-content:center;margin:-10px -8px -10px 0}
  .pp-header .auth-link.signed-in .ms{font-size:24px}
  .pp-header .auth-link:not(.signed-in) .ms{display:none}
  .pp-gate{align-items:flex-end}
  .pp-gate-sheet{width:100%;max-width:100%;border-radius:18px 18px 0 0;padding:10px 20px calc(24px + env(safe-area-inset-bottom));max-height:86%}
  .pp-gate-handle{display:block;width:44px;height:20px;padding:0;margin:0 auto;border:none;background:none;cursor:pointer;position:relative}
  .pp-gate-handle::before{content:"";position:absolute;left:2px;right:2px;top:8px;height:4px;border-radius:2px;background:var(--line-input)}
  .mobile-cta .pp-optin{min-height:36px;font-size:13px}
}
/* #709: on the narrowest phones the logo's word and the stepper's slack give way, so the
   stepper, Print and the account icon share one row without meeting. */
@media (max-width:430px){
  /* visually hidden, not display:none: the link keeps its name for a screen reader */
  .pp-header .pp-logo .name{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
  .pp-header .steps{gap:6px}
  .pp-header .conn{width:10px}
}

/* ---------- the run's zips (#205) ----------
   index.html's extra zip rows and the "+" beside them, and list.html's zip
   chips in the trip summary. Every rule names its container (tests/test_css_scope.py);
   the counts the "+" is limited to come from zipaisle-plans.js, never from here. */
.loc-page .zip-extra{display:flex;flex-direction:column;gap:10px}
.loc-page .zip-extra:empty{display:none}
.loc-page .zip-extra-row{display:flex;align-items:center;gap:10px}
.loc-page .zip-extra-row .field{flex:1;height:48px}
.loc-page .zip-extra-row .field input{font-size:16px;font-weight:600;letter-spacing:.04em}
.loc-page .zip-add-row{display:flex;align-items:center;flex-wrap:wrap;gap:4px 12px}
.loc-page .zip-add,.list-side .zips-edit .zip-add{display:inline-flex;align-items:center;gap:6px;align-self:flex-start;
  font-family:inherit;font-weight:600;font-size:14px;color:var(--green-dark);background:none;border:none;padding:4px 0;cursor:pointer}
.loc-page .zip-add:hover,.list-side .zips-edit .zip-add:hover{color:var(--green-press);text-decoration:underline}
.loc-page .zip-add:disabled,.list-side .zips-edit .zip-add:disabled{color:var(--muted);cursor:default;text-decoration:none}
.loc-page .zip-add .ms,.list-side .zips-edit .zip-add .ms{font-size:18px}
.loc-page .zip-hint,.list-side .zips-edit .zip-hint{font-size:12.5px;line-height:1.45;color:var(--muted)}
/* The Upgrade link after the hint (#247), and after the watch list's zip
   sentence (#252): --green-dark, the "+"'s own text colour. */
.loc-page .zip-upgrade,.list-side .zips-edit .zip-upgrade,.pp-watchpage .wzipplus .zip-upgrade{color:var(--green-dark);font-weight:700;text-decoration:underline}
.loc-page .zip-upgrade:hover,.list-side .zips-edit .zip-upgrade:hover,.pp-watchpage .wzipplus .zip-upgrade:hover{color:var(--green-press)}
.loc-page .zip-status,.list-side .zips-edit .zip-status{margin:0;font-size:12.5px;line-height:1.45;color:var(--muted)}
.loc-page .zip-status.err,.list-side .zips-edit .zip-status.err{color:var(--danger)}
.loc-page .zip-remove,.list-side .zips-edit .zip-remove{border:none;background:none;cursor:pointer;padding:4px;display:flex}
.loc-page .zip-remove .ms,.list-side .zips-edit .zip-remove .ms{color:var(--pale)}
.loc-page .zip-remove:hover .ms,.list-side .zips-edit .zip-remove:hover .ms{color:var(--danger)}
.loc-page .zip-remove .ms{font-size:20px}
.list-side .zips-edit{display:flex;flex-direction:column;gap:8px;margin-top:-6px}
.list-side .zips-edit .zips-chips{display:flex;flex-wrap:wrap;gap:6px}
.list-side .zips-edit .zip-chip{display:inline-flex;align-items:center;gap:2px;padding:2px 4px 2px 10px;border-radius:999px;
  background:var(--chip-bg);font-size:13px;font-weight:600;color:var(--ink)}
.list-side .zips-edit .zip-chip .zip-remove{padding:2px}
.list-side .zips-edit .zip-chip .zip-remove .ms{font-size:16px}
.list-side .zips-edit .zips-add-row{display:flex;gap:8px}
.list-side .zips-edit .zips-add-row .field{flex:1;height:40px;padding:0 10px}
.list-side .zips-edit .zips-add-row .btn{height:40px;padding:0 14px}

/* ── The watch list (#210; the design pass on #210) ─────────────────────
   Three surfaces, one row: the landing card (.pp-watch on index.html), the
   page (.pp-watchpage, watchlist.html) and the results-row control
   (.pp-watchctl). The card is visibly NOT a shopping list -- its own tint and
   rule, read-only rows, one link and no button to run it -- because a
   shopper who mistakes it for their list runs it and is charged fifteen
   searches. Every rule names its container (tests/test_css_scope.py); text is
   --ink / --muted / --green-dark only, --green and --tint for surfaces
   (tests/test_css_contrast.py). */
.pp-watch{background:var(--tint);border-left:4px solid var(--green);border-radius:12px;padding:16px 18px;margin:0 0 28px}
.pp-watch .wtitle{font-size:17px;font-weight:700;margin:0 0 2px;color:var(--ink)}
.pp-watch .wsub,.pp-watch .wlast,.pp-watch .wzips,.pp-watch .wpartial{margin:0 0 8px;font-size:13.5px;color:var(--muted)}
.pp-watch .wrows,.pp-watchpage .wrows{list-style:none;margin:8px 0;padding:0}
.pp-watch .wrow,.pp-watchpage .wrow{background:#fff;border-radius:10px;padding:10px 12px;margin:0 0 8px}
.pp-watch .wrow.unchecked,.pp-watch .wrow.none{color:var(--muted)}
.pp-watch .whead{display:flex;flex-wrap:wrap;justify-content:space-between;gap:4px 10px;align-items:baseline}
.pp-watch .witem{font-weight:600;color:var(--ink)}
.pp-watch .wbadge{font-size:12.5px;font-weight:700;color:var(--green-dark);background:#fff;border:1px solid var(--green-soft);border-radius:999px;padding:1px 8px}
.pp-watch .wwhere{display:flex;flex-direction:column;font-size:13.5px;margin-top:2px}
.pp-watch .wchain{color:var(--ink)}
.pp-watch .wstreet,.pp-watch .wunit,.pp-watch .wnote{color:var(--muted);font-size:13px}
.pp-watch .wnote{margin:4px 0 0}
.pp-watch .whidden,.pp-watch .wlost{font-size:13.5px;color:var(--ink);margin:6px 0}
.pp-watch .wemail{display:block;background:none;border:none;padding:0;margin:6px 0;font:inherit;font-size:13.5px;color:var(--green-dark);cursor:pointer;text-decoration:underline}
.pp-watch .wlink{display:inline-block;margin-top:6px;font-weight:600}

.pp-watchpage{max-width:720px;margin:0 auto;padding:24px 16px 48px}
.pp-watchpage .wh1{font-size:24px;margin:0 0 4px}
.pp-watchpage .wsub,.pp-watchpage .wshownof,.pp-watchpage .wzipplus,.pp-watchpage .wemailnote{color:var(--muted);font-size:13.5px;margin:0 0 12px}
.pp-watchpage .wbanner{background:var(--tint);border-radius:10px;padding:10px 12px;margin:0 0 16px;color:var(--ink)}
.pp-watchpage .wgroup{font-size:15px;margin:22px 0 4px}
.pp-watchpage .wrow{border:1px solid var(--line);display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px}
.pp-watchpage .wname{font-weight:600;flex:1 1 160px;color:var(--ink)}
.pp-watchpage .wzipchips{display:flex;gap:6px;flex-wrap:wrap}
.pp-watchpage .wzip{background:var(--chip-bg);border-radius:999px;padding:2px 9px;font-size:12.5px;color:var(--ink)}
.pp-watchpage .wctl{background:#fff;border:1px solid var(--line-input);border-radius:8px;padding:4px 10px;font:inherit;font-size:13px;color:var(--green-dark);cursor:pointer}
.pp-watchpage .wctl:disabled{color:var(--muted);cursor:default}
.pp-watchpage .wctl.on{background:var(--tint)}
.pp-watchpage .wzipedit{flex-basis:100%;display:flex;gap:6px;flex-wrap:wrap;align-items:center}
.pp-watchpage .wzipedit input{width:90px;height:34px;border:1.5px solid var(--line-input);border-radius:8px;padding:0 8px;font:inherit}
.pp-watchpage .wrefusal{flex-basis:100%;color:var(--danger);font-size:13px;margin:0}
.pp-watchpage .wemailrow{display:flex;gap:10px;align-items:center;margin-top:24px}

/* #717: quiet (weight 400, no fill -- pale green means "cheapest here" in the compare table), left-aligned
   in its own slot after the unit line, and the Full sentence wraps inside the cell instead of widening it. */
.pp-watchctl{display:inline-flex;align-items:center;gap:4px;max-width:100%;box-sizing:border-box;text-align:left;background:#fff;border:1px solid var(--line-input);border-radius:12px;padding:1px 9px;font:inherit;font-size:12.5px;font-weight:400;color:var(--green-dark);cursor:pointer;vertical-align:middle}
.pp-watchctl .ms{font-size:15px;flex:none}
.pp-watchctl .wc-stop{display:none}
.pp-watchctl.watching:focus-visible .wc-label{display:none}
.pp-watchctl.watching:focus-visible .wc-stop{display:inline}
@media (hover:hover){.pp-watchctl.watching:hover .wc-label{display:none}.pp-watchctl.watching:hover .wc-stop{display:inline}}
.cmp-table td.item .cmp-watch,.cmp-card .ctitle .cmp-watch{margin-top:4px;font-weight:400}
.cmp-table td.item .cmp-watch:empty,.cmp-card .ctitle .cmp-watch:empty{display:none}
.pp-watchctl:disabled{color:var(--muted);cursor:default;border-style:dashed}
.pp-watchnote{display:block;font-size:12.5px;color:var(--muted);margin-top:2px}
/* A store's name on two lines (#210, ZA.storeLines): the chain, then the street
   line under it. The ", " between them stays in the text -- read aloud and
   copied, it is the whole label -- and is only hidden from sight. */
.store-lines .s-sep{display:none}
.store-lines .s-street{display:block;font-weight:400;font-size:.86em;color:var(--muted)}
/* The compare table's header is uppercase; a street is an address, and "10116 NE
   8TH ST STE 1" reads as shouting (review P3 on #210, 2026-09-24). */
.cmp-table th .s-street{text-transform:none;letter-spacing:normal}
/* Add an item to a search (#346, zipaisle-add.js): under the store cards on the
   working screen and under the plan. Its own words for the SEARCH -- the Watch
   control (.pp-watchctl, #210) is per row and says something else. Every rule
   names its container (.working-page / .results-pane, then .search-add-wrap),
   never a bare class a page could also claim (#38, #49). */
.working-page .search-add-wrap,.results-pane .search-add-wrap{margin-top:16px}
.search-add-wrap .search-add{display:flex;flex-direction:column;gap:8px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:14px 16px}
.search-add-wrap .search-add-label{font-size:14px;font-weight:700;color:var(--ink)}
.search-add-wrap .search-add-row{display:flex;gap:10px;flex-wrap:wrap}
.search-add-wrap .search-add-input{flex:1 1 180px;min-width:0;height:44px;border:1.5px solid var(--line-input);border-radius:10px;padding:0 12px;font:inherit;font-size:15px;background:#fff;color:var(--ink)}
.search-add-wrap .search-add-btn{height:44px;padding:0 16px;font-size:14px}
.search-add-wrap .search-add-cost{margin:0;font-size:13px;color:var(--muted)}
/* The count could not be read, or the server refused: list.html's quota-cost voice. */
.search-add-wrap .search-add-cost.unreadable{color:var(--green-press);font-weight:700}
.search-add-wrap .search-add-list{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px;font-size:13px;color:var(--muted)}

/* ---------- the plan rating and note (#678) ---------- */
.plan-feedback{display:flex;flex-direction:column;gap:10px;padding:20px 24px 8px}
.plan-feedback .fb-head{margin:0;font-size:17px;font-weight:700;color:var(--ink)}
.plan-feedback .fb-faces{display:flex;flex-wrap:wrap;gap:10px}
.plan-feedback .fb-face{display:inline-flex;align-items:center;gap:8px;min-height:44px;padding:0 16px;border:1px solid var(--line);border-radius:999px;background:#fff;color:var(--ink);font:inherit;font-weight:600;cursor:pointer}
.plan-feedback .fb-face:hover{border-color:var(--green-dark)}
.plan-feedback .fb-face.on{border-color:var(--green-dark);background:var(--bg);box-shadow:inset 0 0 0 1px var(--green-dark)}
.plan-feedback .fb-prompt{margin:0;min-height:1.4em;font-size:14px;color:var(--ink)}
.plan-feedback .fb-note{display:flex;flex-direction:column;gap:6px;max-width:520px}
.plan-feedback .fb-label{font-size:12.5px;font-weight:600;color:var(--muted)}
.plan-feedback .fb-text,.plan-feedback .fb-email{width:100%;box-sizing:border-box;padding:10px 12px;border:1px solid var(--line);border-radius:8px;font:inherit;color:var(--ink)}
.plan-feedback .fb-hint{font-size:12px;color:var(--muted)}
.plan-feedback .fb-count{align-self:flex-end;font-size:12px;color:var(--muted)}
.plan-feedback .fb-send{align-self:flex-start;min-height:44px;padding:0 22px;border:0;border-radius:8px;background:var(--green-dark);color:#fff;font:inherit;font-weight:700;cursor:pointer}
.plan-feedback .fb-send:disabled{opacity:.5;cursor:default}
.plan-feedback .fb-error{margin:0;font-size:13px;color:var(--ink);font-weight:600}
.plan-feedback .fb-kept{margin:0;font-size:12px;color:var(--muted)}
.plan-feedback .fb-kept a{color:var(--muted);text-decoration:underline}
@media print{.plan-feedback{display:none}}
.plan-feedback .fb-top,.plan-feedback .fb-body{display:flex;flex-direction:column;gap:10px}
