/* ==========================================================================
   Grocery List — site theme
   Styles for tools/grocery-list.html.

   Loaded AFTER styles.css and tool-base.css. Every rule is scoped under
   `.gl-app` so the tool's dense UI cannot leak into the navbar, hero or footer,
   and so the site's editorial h1–h6 / p / a rules cannot blow up the tool's
   compact typography.

   VAR INDIRECTION. The tool declares its OWN custom property names on the
   wrapper and maps each onto a site design token, so no rule below references a
   site token directly. The ten names are a CONTRACT, not a convention:
   tool-base.css re-declares exactly `--bg --card --raised --ink --dim --muted
   --line --line-strong --accent --accent-soft` inside `@media print`. A token
   introduced under any other name will not flip on paper.

   NO PRINT MIRRORS, AND THAT IS STRUCTURAL. The editor list is `.no-print`;
   what prints is `#paperList`, built from state as plain text. There is not one
   form control in the printed area — the suite asserts that count is zero — so
   the print-mirror contract does not apply. Same argument as the readiness
   check, Classified Doc and the subnet planner. If a future revision ever prints the
   editor, that changes and the contract applies.

   NO PRINT-COLOR-ADJUST IS NEEDED ANYWHERE IN THIS FILE, which is a design
   decision rather than luck. The paper checkbox is a bordered box with a text
   tick, so it survives "Background graphics" being unticked — the default in
   Firefox's silent-print path — without a single exception rule. Nothing here
   is a fill or a track. If you add one, it needs its own `print-color-adjust`;
   the base's rule covers `.chart-track`/`.chart-fill` only and is not inherited
   by a different class.

   ONE HUE, AND NO CHART AT ALL. There is no progress bar. A picked-vs-remaining
   bar is a fourth series case, and DESIGN.md says one "does not get to assume it
   is allowed" — the subnet planner made that visit on 2026-08-10 with the
   identical shape (allocated vs free) and was refused under the subtraction
   default. The same answer applies here: the panel already prints both figures
   as text, so a bar would restate them and earn nothing. Done-ness is carried by
   WEIGHT — muted text and a strike — never by a green tick. A ticked item is not
   a success state, and the palette stays closed at ten colours.

   TOUCH: this tool ships WITH the 44px pass. It is the first tool on the site
   whose primary device is a phone — you write the list at a table and work it in
   a shop — so the phone layout is not a courtesy pass over a desktop tool.
   Sizes are `px`, never `rem`: styles.css:1143 drops `:root { font-size: 15px }`
   at 480px, so a rem-based 44px target renders 41px on exactly the phones that
   need it most.

   NOTHING HERE NEEDS A DOUBLED CLASS IN PRINT. tool-base.css writes its print
   rules as `.tool-app.tool-app …` so they outrank screen rules; a tool only has
   to match that specificity where it wants to OVERRIDE the base. This tool uses
   the base's light palette, its grid fix, its page-break atoms and its masthead
   as-is, so a plain `.gl-app` prefix is correct throughout. If you ever add a
   rule here that fights one in tool-base.css, double the class on that rule
   ONLY — never wholesale, which is how the planner's savings headline flipped
   colour.
   ========================================================================== */

.gl-app {
    /* ---- tool vars → site design tokens ---- */
    --bg: var(--color-bg-primary);
    --card: var(--color-surface);
    --raised: var(--color-raised);
    --ink: var(--color-text-primary);
    --dim: var(--color-text-secondary);
    --muted: var(--color-text-muted);
    --line: var(--color-border);
    --line-strong: var(--color-border-strong);
    --accent: var(--color-primary);
    --accent-soft: rgba(229, 163, 68, 0.12);

    /* DESIGN.md: flat surfaces, hairline rules, no glow — structure, not effects */
    --shadow: none;

    background: var(--bg);
    color-scheme: dark;
    /* Firefox needs this explicitly — `color-scheme: dark` alone left scrollable
       regions drawing a solid white bar across a dark card in the sibling tools. */
    scrollbar-color: var(--line-strong) transparent;

    font-family: var(--font-primary);
    font-size: 14px;
    /* dense data tool: deliberately below the site's 1.0625rem body */
    line-height: 1.5;
    color: var(--ink);
}

/* Neutralize the site's editorial typography inside the tool. `color: inherit`
   is the load-bearing line: it is also what keeps the printed masthead h1 black,
   because styles.css's own h1 colour would otherwise win over the inherited
   #111 and print the heading pale grey on white. */
.gl-app h1,
.gl-app h2,
.gl-app h3,
.gl-app p {
    font-family: inherit;
    font-size: inherit;
    font-weight: inherit;
    line-height: inherit;
    letter-spacing: normal;
    color: inherit;
    margin: 0;
    max-width: none;
}

.gl-app * {
    box-sizing: border-box;
}

/* The `hidden` ATTRIBUTE is only a UA rule of `display: none`, so ANY author
   `display` declaration beats it. This tool hides a grid (`.gl-row` in the paper
   list) and a flex row (`.stat`, `#copyNote`), which silently made
   `node.hidden = true` do nothing in the pro/con list — the blank-state element
   rendered on screen while the test asserting `.hidden === true` passed the
   whole time. Assert computed display, never the property. */
.gl-app [hidden] {
    display: none !important;
}

.gl-app .wrap {
    max-width: 1240px;
    margin: 0 auto;
    padding: 112px 2rem 96px;
    /* 112px clears the fixed navbar */
}

@media (max-width: 768px) {
    .gl-app .wrap {
        padding: 96px 1.25rem 64px;
    }
}

/* Visually hidden but reachable by assistive tech. `display:none` and
   `visibility:hidden` both suppress live-region announcements, so this has to be
   the clip-rect form. Not defined anywhere in styles.css — it lives here. */
.gl-app .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ---------- tool header ---------- */

.gl-app header.app {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 1.5rem;
    flex-wrap: wrap;
    margin-bottom: 1.75rem;
    padding-bottom: 1.25rem;
    border-bottom: 1px solid var(--line);
}

.gl-app .title {
    min-width: 0;
    flex: 1 1 320px;
}

.gl-app .doc-title {
    display: block;
    width: 100%;
    max-width: 34ch;
    background: transparent;
    border: 0;
    border-bottom: 1px solid transparent;
    color: var(--ink);
    font-family: var(--font-display);
    font-size: 1.9rem;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.15;
    padding: 0 0 4px;
}

.gl-app .doc-title::placeholder {
    color: var(--muted);
    opacity: 1;
}

/* Every :hover in this file is gated on (hover: hover) — see the block comment
   above `.gl-app button` for why. */
@media (hover: hover) {
    .gl-app .doc-title:hover {
        border-bottom-color: var(--line-strong);
    }
}

.gl-app .sub {
    color: var(--muted);
    font-family: var(--font-mono);
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    margin-top: 8px;
}

.gl-app .actions {
    display: flex;
    gap: 0.6rem;
    flex-wrap: wrap;
}

/* ---------- buttons ---------- */

/* HOVER IS GATED ON (hover: hover) THROUGHOUT THIS FILE, and that is a bug fix,
   not tidiness. A touch browser fires the hover state on tap and then leaves it
   applied until you tap something else — so a button kept an amber border after
   every press, pointing at a control the user was no longer touching. `:active`
   below is the deliberate replacement: feedback while the finger is down,
   nothing after it lifts. This tool is used on a phone more than anywhere else,
   so the gate matters here more than in the tools it was copied from. */
.gl-app button {
    font-family: var(--font-primary);
    font-size: 0.82rem;
    border-radius: var(--radius-md);
    border: 1px solid var(--line);
    background: var(--raised);
    color: var(--ink);
    padding: 0.5rem 0.9rem;
    cursor: pointer;
    /* `manipulation` drops the legacy double-tap-zoom wait, so a tap acts on
       press rather than ~300ms later. */
    touch-action: manipulation;
    /* We draw our own press state (:active, below). Without this, Chrome
       Android paints a grey box over it as well. */
    -webkit-tap-highlight-color: transparent;
    transition: border-color var(--transition-fast), background var(--transition-fast), color var(--transition-fast);
}

@media (hover: hover) {
    .gl-app button:hover {
        border-color: var(--accent);
    }
}

/* Press feedback, both pointer types. Not inside the hover gate: this is the
   only affordance a touch user gets that a press registered at all. */
.gl-app button:active {
    background: var(--card);
    border-color: var(--accent);
}

.gl-app button.primary {
    background: var(--accent);
    border-color: var(--accent);
    color: #17130b;
    font-weight: 600;
}

@media (hover: hover) {
    .gl-app button.primary:hover {
        background: var(--color-primary-dark);
        border-color: var(--color-primary-dark);
    }
}

.gl-app button.primary:active {
    background: var(--color-primary-dark);
    border-color: var(--color-primary-dark);
}

.gl-app button.ghost {
    background: transparent;
}

/* aria-disabled, not `disabled` — a disabled button cannot hold focus, so the
   browser blurs it and drops the user at the top of the document. The styling
   has to be driven off the class the JS sets, since :disabled will never match. */
.gl-app button.is-off {
    opacity: 0.45;
    cursor: not-allowed;
}

@media (hover: hover) {
    .gl-app button.is-off:hover {
        border-color: var(--line);
    }
}

.gl-app button.is-off:active {
    background: var(--raised);
    border-color: var(--line);
}

/* The two-step clear. The armed state names itself rather than borrowing the
   generic amber: this is the only control on the page that destroys work. */
.gl-app button.is-armed {
    color: var(--color-error);
    border-color: var(--color-error);
}

.gl-app button:focus-visible,
.gl-app input:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* ---------- layout ---------- */

.gl-app .grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 320px;
    gap: 1.5rem;
    align-items: start;
}

.gl-app .col-side .summary {
    position: sticky;
    top: 100px;
}

@media (max-width: 1000px) {
    .gl-app .grid {
        display: flex;
        flex-direction: column;
        /* LOAD-BEARING, and the exact trap tool-base.css documents for print.
           The rule above sets `align-items: start` so the sticky sidebar hugs
           its content. On a COLUMN flex box that same declaration is the CROSS
           axis, so it shrink-wraps every panel to its content width and the
           inputs overflow the viewport. Caught by screenshotting at 390px, not
           by reading the CSS. */
        align-items: stretch;
    }

    .gl-app .col-side .summary {
        position: static;
    }

    /* NO `order: -1` HERE, AND THAT IS THE FIX — 2026-08-10.
       This carried `.gl-app .col-side { order: -1 }` on the reasoning that "the
       count is what you look at in a shop, so it goes first". That reasoning
       describes a list you have already written; it is exactly wrong for the
       list you are writing. Measured at 390x844 the summary panel is ~570px of
       em-dashes, a "Clear what's in the cart" button that does nothing yet, and
       a paragraph about storage — which put `#newName` at y=816 on an 844px
       phone. The one control the tool exists for was below the fold on first
       load, so the tool read as "cannot add items". Reported by the user, and
       invisible to all 213 shipped assertions because every one of them reaches
       its target through a selector rather than through the viewport.

       DOM order (main, then side) is now the render order at every width. The
       cost is that on a phone the "still to get" figure sits under the list
       instead of over it; `#countBadge` in the list heading carries the running
       count in the meantime. Do not reinstate `order: -1` without moving the
       add bar above the summary first — a fold check is now part of the suite
       (`node tools/qa-grocery.mjs <engine> fold`).

       REVERTED ONCE, RESTORED 2026-08-11. Commit 44fbce5 put `order: -1` back
       and deleted `runFold()` in the same commit, so the guard above could not
       report the regression it exists for. The tool was re-reported as broken
       on a phone the next day. If you are removing this rule's protection,
       remove it knowingly: the fold suite is the thing that notices.

       This rule and the sticky add bar below are COMPLEMENTARY, not rivals.
       List-first puts the field on screen at load; sticky keeps it there once
       the list is long enough to scroll. 44fbce5 traded one for the other when
       it could have had both. */
}

/* ---------- cards ---------- */

.gl-app .panel {
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    overflow: hidden;
}

.gl-app .panel + .panel {
    margin-top: 1.25rem;
}

.gl-app .panel h2 {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    color: var(--accent);
    padding: 0.9rem 1.25rem;
    border-bottom: 1px solid var(--line);
    background: var(--raised);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.6rem;
    flex-wrap: wrap;
}

.gl-app .badge {
    font-family: var(--font-mono);
    font-size: 0.62rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--muted);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 0.15rem 0.45rem;
}

.gl-app .panel .body {
    padding: 1.25rem;
}

.gl-app .note {
    color: var(--muted);
    font-size: 0.8rem;
    line-height: 1.6;
    margin: 0 0 1rem;
    max-width: 68ch;
}

/* ---------- the add bar ---------- */

/* This is the tool's front door. Everything about it is sized for typing a
   whole list in one go: the field is the widest thing on the page, Enter
   commits, and focus returns here afterwards. */
.gl-app .add-bar {
    display: flex;
    gap: 8px;
    margin-bottom: 1rem;
}

.gl-app .add-bar input {
    flex: 1;
    min-width: 0;
    font-family: var(--font-primary);
    font-size: 0.95rem;
    background: var(--raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    color: var(--ink);
    padding: 0.6rem 0.75rem;
}

.gl-app .add-bar input::placeholder {
    color: var(--muted);
    opacity: 1;
}

.gl-app .add-bar button {
    flex: none;
}

.gl-app .add-hint {
    color: var(--muted);
    font-size: 0.76rem;
    margin: -0.5rem 0 1rem;
    min-height: 1.1em;
}

/* ---------- the list ---------- */

.gl-app .gl-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.gl-app .gl-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    align-items: center;
    gap: 10px;
    background: var(--raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    padding: 8px 10px;
}

/* The just-ticked row is highlighted for a moment, and that is the whole
   mitigation for the reflow this tool deliberately performs. Ticking sinks an
   item to the bottom of the list — the user's choice, made knowingly — which
   means the row you touched leaves the place you touched it. The highlight is
   what makes that traceable rather than startling.

   The MOVEMENT itself is deliberately not animated. Easing a list that reflows
   under a thumb widens the mis-tap window instead of closing it; the row should
   already be in its new place by the time the finger lifts. Only the background
   fades, and `prefers-reduced-motion` collapsing that to nothing is harmless —
   the JS removes the class either way, so nothing is positioned by an
   animation. Fail visible. */
.gl-app .gl-row.is-moved {
    background: var(--accent-soft);
    border-color: var(--accent);
    transition: background var(--transition-normal), border-color var(--transition-normal);
}

/* Done-ness is typographic. NOT green: see the header comment. */
.gl-app .gl-row.is-done .gl-name {
    color: var(--muted);
    text-decoration: line-through;
}

.gl-app .gl-row.is-done .gl-qty {
    color: var(--muted);
}

/* The 44px tap area is the LABEL, so the whole box toggles the checkbox
   natively — no click handler, no `for`, nothing to keep in sync. The input
   inside it stays a real checkbox: keyboard, focus and screen-reader behaviour
   are the browser's, and `accent-color` paints it in the site amber without
   `appearance: none` and a hand-drawn tick. The two engines draw slightly
   different check marks; that is not a defect. */
.gl-app .gl-checkwrap {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex: none;
    cursor: pointer;
    border-radius: var(--radius-sm);
}

.gl-app .gl-check {
    width: 20px;
    height: 20px;
    margin: 0;
    accent-color: var(--accent);
    cursor: pointer;
}

.gl-app .gl-name {
    min-width: 0;
    width: 100%;
    font-family: var(--font-primary);
    font-size: 0.9rem;
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    color: var(--ink);
    padding: 6px 8px;
}

.gl-app .gl-name::placeholder {
    color: var(--muted);
    opacity: 1;
}

@media (hover: hover) {
    .gl-app .gl-name:hover {
        border-color: var(--line);
    }
}

.gl-app .gl-name:focus {
    background: var(--card);
    border-color: var(--line-strong);
    outline: none;
}

/* ---------- the quantity stepper ---------- */

.gl-app .gl-qty-wrap {
    display: flex;
    align-items: center;
    gap: 2px;
    flex: none;
}

.gl-app .gl-step {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    font-size: 1rem;
    line-height: 1;
    background: transparent;
    border-color: transparent;
    color: var(--dim);
}

@media (hover: hover) {
    .gl-app .gl-step:hover {
        color: var(--ink);
        border-color: var(--line);
    }
}

/* At the clamp the button stays focusable (aria-disabled, never disabled) but
   must not look pressable — same rule as the pro/con move buttons. */
.gl-app .gl-step[aria-disabled="true"] {
    color: var(--muted);
    opacity: 0.45;
    cursor: not-allowed;
}

@media (hover: hover) {
    .gl-app .gl-step[aria-disabled="true"]:hover {
        color: var(--muted);
        border-color: transparent;
    }
}

.gl-app .gl-step[aria-disabled="true"]:active {
    background: transparent;
    color: var(--muted);
    border-color: transparent;
}

.gl-app .gl-qty {
    width: 44px;
    text-align: center;
    font-family: var(--font-mono);
    font-size: 0.85rem;
    font-variant-numeric: tabular-nums;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--ink);
    padding: 5px 2px;
    /* The native spinner is ~13px of unhittable target and duplicates the two
       buttons either side of it. The buttons are the affordance. */
    -moz-appearance: textfield;
    appearance: textfield;
}

.gl-app .gl-qty::-webkit-outer-spin-button,
.gl-app .gl-qty::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

/* ---------- remove ---------- */

/* Muted at rest, red only on hover and press. Red is semantic error on this
   site: a permanently-red delete reads as an error state on the page, which is
   the defect Classified Doc's first draft shipped. */
.gl-app .gl-del {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    font-size: 0.95rem;
    line-height: 1;
    background: transparent;
    border-color: transparent;
    color: var(--dim);
}

@media (hover: hover) {
    .gl-app .gl-del:hover {
        color: var(--color-error);
        border-color: var(--color-error);
    }
}

.gl-app .gl-del:active {
    color: var(--color-error);
    border-color: var(--color-error);
}

.gl-app .list-empty {
    color: var(--muted);
    font-size: 0.82rem;
    line-height: 1.6;
    padding: 6px 0 2px;
}

/* ---------- touch layout ----------
   The row re-flows to two lines so every control in it can be a real 44px
   target and the item name — the thing you read while walking — gets a full
   line to itself:

       [✓]  [ Ground beef                        ]
       [−] [ 2 ] [+]                          [✕]

   MEASURED, NOT CHOSEN. At 320px the wrap contributes 20px each side and the
   panel body 16px each side, leaving 248px; the row's own 8px padding takes it
   to 232px of content. One line would need check 44 + name + step 44 + qty 44 +
   step 44 + delete 44 = 220px of controls BEFORE the name field, which does not
   fit until well past any real phone. Two lines give the name 232 − 44 − 8 =
   180px on line 1, and put 48px of clear space between the `+` stepper and
   delete on line 2.

   That gap is the point of the layout, not a leftover: delete is the only
   destructive control in the row and it would otherwise sit against `+`, which
   is the control you tap repeatedly. `.gl-rowtools` is a flex box, so an auto
   margin on the last child banks the whole remaining line width between them —
   the same fix the pro/con list needed when delete sat 2px from "move down".

   EVERY SIZE HERE IS px, NOT rem, AND THAT IS LOAD-BEARING. styles.css:1143
   drops `:root { font-size: 15px }` at 480px, so a rem-based 44px target would
   render at 41px on exactly the phones that need it most. Same reason `.gl-name`
   below is 16px and not 1rem.

   Grid placement is explicit for both children rather than left to auto-flow, so
   the line each one lands on is stated here rather than being a consequence of
   DOM order. */
@media (max-width: 620px) {

    /* Buys back 4px of row width per side on a phone — it all goes to the name. */
    .gl-app .panel .body {
        padding: 1rem;
    }

    .gl-app .gl-row {
        grid-template-columns: 44px minmax(0, 1fr);
        column-gap: 8px;
        row-gap: 6px;
        padding: 8px;
    }

    .gl-app .gl-checkwrap {
        grid-row: 1;
        grid-column: 1;
        width: 44px;
        height: 44px;
    }

    /* A 20px checkbox in a 44px box is a mark adrift unless something shows
       where the target is — the same reading that made the pro/con row tools
       need their hairline back once they got a line of their own. */
    .gl-app .gl-checkwrap {
        border: 1px solid var(--line);
        background: var(--card);
    }

    .gl-app .gl-check {
        width: 22px;
        height: 22px;
    }

    .gl-app .gl-name {
        grid-row: 1;
        grid-column: 2;
        /* 16px LITERAL. iOS Safari zooms the viewport whenever a focused input
           computes below 16px and does not zoom back out afterwards, so every
           tap on an item would leave the page magnified. */
        font-size: 16px;
        min-height: 44px;
        border-color: var(--line);
    }

    .gl-app .gl-rowtools {
        grid-row: 2;
        grid-column: 1 / -1;
        display: flex;
        align-items: center;
        gap: 6px;
    }

    .gl-app .gl-step,
    .gl-app .gl-del {
        width: 44px;
        height: 44px;
        min-width: 44px;
        min-height: 44px;
        /* Borderless on desktop, where they read as quiet icon buttons tucked
           into a dense row. Given a 44px box on a line of their own that reading
           collapses — they become glyphs adrift in an empty band with nothing
           showing where the target is. The hairline is the affordance, and it is
           the same border the rest of the system uses. */
        background: var(--card);
        border-color: var(--line);
    }

    .gl-app .gl-qty {
        width: 56px;
        /* Typed into, so the same 16px floor applies. */
        font-size: 16px;
        min-height: 44px;
    }

    /* Banks the whole remaining line width between `+` and delete. */
    .gl-app .gl-del {
        margin-left: auto;
    }

    /* 16px minimum on anything typed into, and a real target height. Covers the
       add bar and the document title in one rule. */
    .gl-app .add-bar input,
    .gl-app .doc-title {
        font-size: 16px;
    }

    .gl-app .add-bar input {
        min-height: 44px;
    }

    /* The document title is a typed field like any other, and it was the one
       control the 44px pass missed — 33px tall on every phone width, found by
       the sweep rather than by looking. It is styled as a heading, which is
       exactly why it did not read as a control that needed a target size. */
    .gl-app .doc-title {
        font-size: 1.6rem;
        min-height: 44px;
    }

    /* Covers the header's Copy / Empty / Save as PDF and the add button in one
       rule. Width is left alone — they size to their labels. */
    .gl-app button {
        min-height: 44px;
    }

    /* THE ADD BAR PINS BELOW THE NAVBAR, AND THIS IS A CORRECTNESS FIX, NOT
       POLISH (2026-08-10).

       `.navbar` is `position: fixed; z-index: 1000` and 64px tall here, painted
       `rgba(10,10,11,0.55)` over a 14px backdrop blur. The add bar sits ~816px
       down the document — below the fold at EVERY phone width (320…430 all
       measured) — so reaching it always means scrolling, and scrolling it into
       that top band puts it under the navbar. Because the navbar is
       see-through, the field and the Add button stay plainly visible the whole
       time while every tap lands on `.nav-container`. That is the entire
       "mobile grocery list doesn't work, I can't add items" report: reproduced
       identically in Chromium and Firefox, tap ADDS at top=55 and does NOTHING
       at top=40.

       Pinning removes the dead band instead of narrowing it. It also happens to
       fix the thing that created the exposure — the bar being below the fold —
       because once you have scrolled to it, it stays.

       z-index 5 is the same figure `classified.html`'s save bar uses: enough to
       cover list rows sliding underneath, nowhere near the navbar's 1000. */
    .gl-app .add-bar {
        position: sticky;
        top: 64px;
        z-index: 5;
        /* Opaque, or ticked rows read through it as it pins. */
        background: var(--card);
        /* Bleed out to the panel edges. `.panel .body` is 1rem here, and
           without this a row scrolls through the padding either side of the
           bar rather than behind it. */
        margin-inline: -1rem;
        padding: 8px 1rem;
        /* The boundary a pinned bar needs. Without it the first row slides
           under an invisible edge and reads as a name sliced in half rather
           than as content passing behind something — the same misread the
           print page-break work kept running into. It is the system's own
           hairline, so at rest it costs nothing. */
        border-bottom: 1px solid var(--line);
    }

    /* `position: sticky` is inert inside a clipping ancestor, and `.panel`
       carries `overflow: hidden` for exactly one reason: to clip the h2's
       `--raised` band to the card's rounded top corners. Round the band itself
       instead and the clip is no longer load-bearing. Dropping the clip without
       this leaves two square corners on the panel head. */
    .gl-app .panel {
        overflow: visible;
    }

    .gl-app .panel h2 {
        border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    }
}

/* `.nav-container` padding steps up above 480px (styles.css:1142), taking the
   navbar from 64px to 71px. Measured in both engines, not assumed — at 64px
   here the bar would tuck 7px under the navbar through this band. */
@media (min-width: 481px) and (max-width: 620px) {
    .gl-app .add-bar {
        top: 71px;
    }
}

/* The iPhone SE — 320px wide with ~460px of visible height once browser chrome
   is taken off — is the tightest fold budget the tool has, and the sticky bar's
   own padding is exactly what pushes it over: measured at 461px against a 460px
   fold. One pixel.

   Trim the bar rather than the assertion. The fold suite is the thing that
   noticed this, and an assertion relaxed to fit the content it is measuring
   stops being a measurement. 8px → 4px reclaims 8px, which clears 460 with room
   rather than landing on it.

   Vertical only: the horizontal 1rem is the bleed that lets rows scroll BEHIND
   the bar instead of through the panel padding either side of it, and the 44px
   targets inside are set on the controls themselves, so neither is touched.

   Must stay AFTER the `max-width: 620px` block — a media query adds no
   specificity, both rules are (0,2,0), so source order is the only thing
   deciding this. */
@media (max-width: 360px) {
    .gl-app .add-bar {
        padding-block: 4px;
    }
}

/* ---------- summary stats ---------- */

.gl-app .stat {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    padding: 7px 0;
    border-bottom: 1px solid var(--line);
}

.gl-app .stat .k {
    color: var(--dim);
    font-size: 0.8rem;
}

.gl-app .stat .v {
    font-family: var(--font-mono);
    font-size: 0.85rem;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
}

.gl-app .stat.big {
    display: block;
    border-bottom: 0;
    padding: 0 0 10px;
}

.gl-app .stat.big .k {
    font-family: var(--font-mono);
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    color: var(--muted);
}

.gl-app .stat.big .v {
    font-family: var(--font-display);
    font-size: 2.6rem;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.05;
    color: var(--ink);
    margin: 2px 0 1px;
}

.gl-app .stat.big .u {
    color: var(--muted);
    font-size: 0.78rem;
}

/* ---------- section headings ---------- */

.gl-app .sub-head {
    font-family: var(--font-mono);
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    color: var(--muted);
    margin: 16px 0 8px;
}

.gl-app .side-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 14px;
}

.gl-app .copy-note {
    color: var(--dim);
    font-size: 0.78rem;
    line-height: 1.6;
    margin-top: 10px;
}

/* The manual-copy fallback. Shown only when the clipboard API is unavailable or
   refuses — the text has to be reachable somehow, and failing silently on a
   button labelled "Copy" is worse than showing the box. */
.gl-app .copy-fallback {
    width: 100%;
    margin-top: 10px;
    min-height: 120px;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    background: var(--raised);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    color: var(--ink);
    padding: 8px 10px;
    resize: vertical;
}

.gl-app .footnote {
    border-top: 1px solid var(--line);
    color: var(--muted);
    font-size: 0.72rem;
    line-height: 1.55;
    margin-top: 16px;
    padding-top: 12px;
}

/* ---------- the paper list ----------
   Built from state as text and hidden on screen. This is what makes the tool
   need no print mirrors: the editor never reaches paper, so no <input> ever has
   to render a value there. */

.gl-app .gl-paper {
    display: none;
}

.gl-app .paper-sec {
    margin-bottom: 14px;
}

.gl-app .paper-head {
    font-family: var(--font-mono);
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    margin-bottom: 6px;
}

.gl-app .paper-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.gl-app .paper-row {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 5px 0;
    border-bottom: 1px solid var(--line);
}

/* A bordered box with a text tick, NOT a fill. That is why nothing in this file
   needs `print-color-adjust`: borders and glyphs both survive "Background
   graphics" being unticked, which is the default in Firefox's silent-print
   path and therefore the case these PDFs are actually generated under. */
.gl-app .gl-box {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 13px;
    height: 13px;
    border: 1px solid currentColor;
    border-radius: 2px;
    font-size: 10px;
    line-height: 1;
}

.gl-app .paper-name {
    flex: 1;
    min-width: 0;
    overflow-wrap: anywhere;
}

.gl-app .paper-qty {
    flex: none;
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.gl-app .paper-row.is-done .paper-name {
    text-decoration: line-through;
}

/* ---------- lead-in to the consulting CTA ---------- */

.gl-app .tool-cta {
    margin-top: 2.5rem;
    padding-top: 1.75rem;
    border-top: 1px solid var(--line);
    max-width: 68ch;
}

.gl-app .tool-cta p {
    color: var(--dim);
    font-size: 0.9rem;
    line-height: 1.7;
    margin-bottom: 1rem;
}

/* ==========================================================================
   PRINT — the list you carry round the shop is the deliverable

   The whole editor is hidden and `#paperList` takes over. There are zero form
   controls in the printed area and the suite asserts it, so the print-mirror
   contract does not apply.

   The shared invariants — body reset, light palette, .wrap, the .print-only
   masthead, the .grid flex fix for Gecko and the page-break atoms — all come
   from tool-base.css. Only this tool's own surfaces are below, and none of them
   collides with a selector that file styles, so no rule here needs a doubled
   class.
   ========================================================================== */
@media print {

    /* THERE ARE DELIBERATELY NO RULES HERE FOR .panel, .stat, .chart-* OR THE
       TWO COLUMNS. Both `.col-main` and `.col-side` carry `.no-print` in the
       markup, so every card, figure and control on the page is already gone by
       the time this block applies — a rule for any of them would be dead CSS
       that reads as though the sidebar prints, and the next person would style
       around it. The masthead plus `.gl-paper` IS the whole document. Sibling
       tools DO carry those rules because their report column prints; this one's
       does not, and that is the difference. */

    /* The paper list is the document. */
    .gl-app .gl-paper {
        display: block;
    }

    .gl-app .paper-head {
        break-after: avoid;
    }

    .gl-app .paper-row {
        break-inside: avoid;
        border-bottom: 1px solid #d4d4d8;
    }

    .gl-app .paper-head {
        color: #8a6420;
    }

    /* An item already in the cart is struck through and greyed, so the eye skips
       it — but it stays on the sheet, because a printed list is checked against
       what is in the trolley, not just what is left. */
    .gl-app .paper-row.is-done {
        color: #666;
    }

    .gl-app .footnote {
        color: #444;
        margin-top: 10px;
        padding-top: 8px;
    }
}
