/* Grox custom styles.
   The application is built from Jelly UI components (Technology definition §12).
   Bootstrap is kept for page structure only — the grid, spacing utilities,
   tables and list groups — and everything it still renders is toned from
   Jelly's tokens here, so the app reads as one design.

   Jelly injects its tokens globally at :root inside an `@layer jelly`, so
   var(--jelly-…) resolves anywhere. These rules are unlayered, so they win over
   that layer regardless of specificity — but they lose to Bootstrap's utility
   classes, which are !important. */

/* --- Page-wide toning ----------------------------------------------------- */

/* Every colour in this application arrives through Jelly's tokens, and Jelly
   injects those from JavaScript — so between first paint and jelly.js executing
   there are no tokens at all, `background-color: var(--jelly-…)` resolves to
   nothing, and the canvas is whatever the browser paints by default: white. On
   a dark machine that is a full-page white flash before the page turns dark.

   color-scheme fixes it with no colour of its own (GX-27): `light dark` tells
   the browser to paint its default canvas to suit the machine, and the two
   pinned rules follow an explicit choice, which the inline script in the layout
   has already applied by this point. Declared here rather than left to Jelly
   because Jelly's copy of it is part of the stylesheet that has not loaded yet.

   Literal colours are deliberately absent — ColorSchemeTest forbids them, and
   this needs none. */
html {
  color-scheme: light dark;
}

html[data-jelly-mode="light"] { color-scheme: light; }
html[data-jelly-mode="dark"] { color-scheme: dark; }

/* The page background, which is NOT the same as the card background in the
   light palette any more (GX-34).

   Jelly's light palette sets background-default, background-surface and
   background-white to the same value — oklch(1 0 0), pure white. So a jelly-card,
   which fills from background-surface, was painted the same colour as the page
   behind it and had no edge at all. Its dark palette does not do this: the page
   is #181B1D and the surface #1E222A, which is why only the light palette looked
   flat.

   Only the PAGE moves. Cards keep background-surface and therefore stay white,
   which is the distinction that was asked for and the one dark already had. The
   value is 30% of Jelly's own hairline mixed into its own page white, which
   lands on #F9F9FA — Simon's suggested #f8f9fa to within a unit per channel, but
   expressed in tokens, so it follows the palette instead of pinning it and
   ColorSchemeTest stays green.

   The three rules mirror Jelly's own token structure exactly: light at :root,
   dark overriding on BOTH hooks — the attribute for an explicit choice, the
   media query for a machine that has not been overridden. Anything less would
   disagree with the switch GX-27 added, in one of its three states only. In dark
   the page returns to background-default untouched: the distinction is already
   there and lightening the page would eat it.

   Before jelly.js runs this resolves to nothing, exactly as the old rule did,
   and the canvas comes from color-scheme above. */
:root {
  --app-background-page: color-mix(in srgb, var(--jelly-color-border-default) 30%, var(--jelly-color-background-default));
}

:root[data-jelly-mode="dark"] {
  --app-background-page: var(--jelly-color-background-default);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-jelly-mode="light"]) {
    --app-background-page: var(--jelly-color-background-default);
  }
}

body {
  font-family: var(--jelly-font-text);
  color: var(--jelly-color-foreground-default);
  background-color: var(--app-background-page);
}

h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
  font-family: var(--jelly-font-display);
  color: var(--jelly-color-foreground-default);
}

.text-muted, .form-text { color: var(--jelly-color-foreground-muted) !important; }

/* Tables. Bootstrap draws them; Jelly supplies the hairline and the ink.

   --bs-table-color is set, not just `color` (GX-25). Bootstrap applies the cell
   colour at cell level — `.table > :not(caption) > * > *` — which outranks a
   `color` on .table however it is written, so the rule below was decorative and
   every cell actually painted --bs-emphasis-color, i.e. #000. Nobody could see
   that in the light palette, where #000 sits a hair from Jelly's ink; in dark it
   was black text on a near-black page, on all five pages with a table.

   Setting the variable rather than adopting Bootstrap's own data-bs-theme keeps
   one source of truth for colour: this is the only Bootstrap default left that
   paints anything (Technology definition §12.1). */
.table {
  --bs-table-bg: transparent;
  --bs-table-color: var(--jelly-color-foreground-default);
  border-color: var(--jelly-color-border-default);
  color: var(--jelly-color-foreground-default);
}

.table > thead > tr > th {
  border-bottom: 1px solid var(--jelly-color-border-default);
  font-family: var(--jelly-font-display);
  font-size: .8rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--jelly-color-foreground-muted);
}

.table > tbody > tr > td {
  border-top: 1px solid var(--jelly-color-border-default);
  border-bottom: 0;
}

/* Jelly paints each control on a canvas larger than the control itself, so the
   soft-body wobble has room to bulge into — an 85x42 button sits on a 173x130
   canvas. That bleed is decoration, but it still counts towards an ancestor's
   scrollable area, and .table-responsive is an overflow:auto box: a grid that
   fits perfectly well grows a scrollbar on both axes (found in GX-22 by driving
   the running app). Clipping it at the action cell keeps the responsive table
   honest — the wobble has far more headroom than it ever uses. */
.table > tbody > tr > td:last-child { overflow: hidden; }

/* The same clip, at the Items page's chips cell (GX-164).
   
   That cell holds the "+ list" jelly-menu, and until this Work Item the menu
   sat in the LAST cell of a two-column table — so the rule above was already
   clipping it and nobody had to think about it. Adding the Prods and delete
   columns moved it into the middle of the row, out of that rule's reach, and
   its canvas bleed promptly filled .table-responsive and grew the scrollbar
   GX-22 spent a Work Item removing. Measured in the running app, in both
   directions, as §12 requires.

   SAFE FOR THIS COMPONENT AND NOT IN GENERAL. Clipping a cell that holds a
   jelly-select would trap its open options panel, which is position: absolute
   and so reachable by an ancestor's overflow — the GX-41 defect, and why the
   Shopping List table's unit cell is deliberately left alone. jelly-menu's
   panel is position: fixed (§12, GX-71): the viewport is its containing block
   and no ancestor overflow can touch it. Confirmed here rather than assumed —
   the open panel's whole painted surface was mapped at 1440x900 with this rule
   in place, and every point inside it is the menu. ChromeInteractionTest is the
   standing guard. */
.item-lists-cell { overflow: hidden; }

/* The same bleed reaches past the viewport on a narrow screen, which scrolls
   the whole page sideways. Clipping at the content column stops it.

   `clip` rather than `hidden` on purpose: hidden would make this a scroll
   container on BOTH axes, which would trap the absolutely-positioned type-ahead
   suggestion lists inside it. clip leaves overflow-y visible.

   The max-width gives the app a comfortable reading measure on a wide screen. */
main {
  overflow-x: clip;
  max-width: 1600px;
}

/* Jelly's form controls are inline-blocks with fixed default widths (jelly-input
   is 280px), which strands them at one size inside a column meant to flex.
   Bootstrap's .w-100 is !important, so it does the work wherever it is applied;
   this covers the type-ahead inputs, which are addressed by id. */
#branch-search, #product-search { width: 100%; }

/* --- Links ---------------------------------------------------------------- */
/* The Grox standard for a hyperlink to another page in the app (GX-37). Two
   things separate it from the text around it — a heavier weight and a colour —
   and one more marks it as clickable: hover moves that colour further towards
   the accent.

   Derived from the accent token rather than picked, so the same declaration
   lands correctly in both palettes (the GX-25 rule).

   GX-138 QUIETENED THE RESTING STATE: weight 640 -> 500, and the mix 55% -> 30%.
   Both moves take a link CLOSER to the text around it, which is the point — a
   table whose every count and name shouted was reading as decoration. The cost
   is measured rather than assumed, and the figures live in the Technology
   definition (§11); the contrast against the page rises as the mix falls,
   because 0% would be the body ink itself, while the separation FROM that ink
   is what shrinks. That separation is what ShoppingListsPageTest guards.

   Hover is deliberately UNCHANGED at 80%, so the resting-to-hover step is now
   larger than it was: the state that says "this is clickable" does more of the
   work, and the resting state less. Do not push hover to the accent colour
   itself — that is 4.4:1 in light, under the 4.5:1 floor, and it is the state a
   person is looking at when they read it.

   No underline, matching .app-footer-link and the nav: weight and colour are
   the signal here. And deliberately NO :visited rule — a link that changes
   after it has been followed says something about where this Shopper has been,
   which is not information a shopping list needs to display. */
.app-link {
  font-family: var(--jelly-font-display);
  font-weight: 500;
  color: color-mix(in srgb, var(--jelly-color-background-accent) 30%, var(--jelly-color-foreground-default));
  text-decoration: none;
}

.app-link:hover,
.app-link:focus-visible {
  color: color-mix(in srgb, var(--jelly-color-background-accent) 80%, var(--jelly-color-foreground-default));
}

/* --- A numeric table column (GX-138) ------------------------------------- */
/* A column of counts or money reads right-aligned: the units line up under the
   units, so two figures can be compared by their shape without reading them.
   Worn by the HEADING as well as the cells, because a left-aligned heading over
   right-aligned numbers looks like a mistake rather than a choice.

   ONE SHARED CLASS, not a rule per page, and that is the .app-panel decision
   again (GX-37): four page-prefixed rules all saying `text-align: right` is four
   places for the next numeric column to be forgotten. The page-prefixed classes
   beside it keep doing what they are for — .gt-price-col carries a width,
   .item-products-col carries a width — because those genuinely differ per table.

   It is INERT where a table stacks on a phone, and deliberately left that way.
   Below 768px the Shopping List table turns each cell into a flex row carrying
   its column heading, and `text-align` does not place flex items — so the
   stacked layout is untouched without needing a media query to undo this. A
   number sitting beside its own label does not need aligning to anything.

   .gt-table no longer stacks there (GX-165). It sheds the badge and Comparison
   columns and keeps the other three side by side, so this rule is LIVE on it at
   every width — Total is right-aligned on a phone as it is on a desktop, and
   there is no longer a second money column beside it to line up with.

   Not applied to the trip results' "Buy" cell, which reads "1 of", nor to the
   Preference badge, which is a digit rather than a quantity anyone compares
   down the column (GX-138, AC 5). */
.app-num {
  text-align: right;
}

/* The two numeric INPUTS take the same treatment (AC 4), and they cannot take
   the class above: the number lives in an <input> inside jelly-input's shadow
   root, where a document rule cannot reach it.

   ::part(input) is how it is reached. Checked in the vendored source before
   relying on it, per §12: jelly-input renders `<input part="input" />`, and its
   own shadow stylesheet sets `text-align: start` on that element. There is NO
   --jelly-input-text-align custom property to set instead — this is the only
   route. The outer tree wins over the inner one for ::part, so the shadow rule
   is overridden rather than fought with specificity.

   Scoped by class, NOT to every jelly-input: the list-name field, the branch
   search and the product search are prose and must stay left-aligned. */
.item-amount::part(input),
.gs-amount::part(input) {
  text-align: right;
}

/* --- A link that leaves the application (GX-135) ------------------------- */
/* The Product names on the Matching Products and Go Shopping tables open the
   supermarket's own page for that Product, in a new tab. They wear .app-link
   for the colour and this for the mark that says where they go.

   The mark is HIDDEN AT REST and appears under the pointer, which is what
   GX-135 asks for: a column of Product names is what a Shopper reads, and a
   permanent icon on every row would turn reading into scanning past furniture.

   :focus-visible is here alongside :hover and is not decoration. A keyboard
   user never hovers, so a hover-only warning is hidden from precisely the
   people who cannot see where a link goes before they press Enter. .app-link
   above pairs the same two states for the same reason, so this follows a rule
   the file already keeps rather than inventing one.

   OPACITY, NOT DISPLAY, and the icon is inline-block with a fixed size — so it
   occupies its space whether it is painted or not. Revealing it with `display`
   would re-flow the cell under the pointer and shove the words left, on every
   row, every time the mouse crossed one.

   No transition. This stylesheet has none anywhere, and the first one should
   not arrive as a side effect of adding an icon.

   The size is in `em`, so the mark tracks the text beside it. That is a
   departure from every other icon in fragments/icons.html, which are sized by
   jelly-icon-button's own ::slotted(svg) rule — this one is not slotted into
   anything, and an SVG carrying only a viewBox renders at 300x150 until
   something gives it a size. */
.app-link-external-icon {
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  margin-left: 0.3em;
  vertical-align: -0.09em;
  opacity: 0;
}

.app-link-external:hover .app-link-external-icon,
.app-link-external:focus-visible .app-link-external-icon {
  opacity: 1;
}

/* A touch screen has no pointer to hover with, so the rule above can never
   fire there and a Shopper on a phone would get no warning at all — and Go
   Shopping is largely a phone page. `hover: none` is the query that asks about
   the PRIMARY input rather than the viewport width, so a small window on a
   desktop keeps the hover behaviour and a large tablet does not. */
@media (hover: none) {
  .app-link-external-icon {
    opacity: 1;
  }
}

/* --- The login button ----------------------------------------------------- */
/* Named "btn" because these began as the tone for a control that navigates,
   which stays an anchor rather than becoming a jelly-button. That reasoning now
   lives in the Technology definition §11 (GX-42) rather than here, because the
   application has no such anchor left: GX-32's login button is the one thing
   still wearing these classes, and it is a native <button>.

   GX-42 cut the rest back to what that button actually paints with. .app-btn-sm
   and .app-btn-quiet had no users at all, and the plain .app-btn:hover had none
   either — the login button carries .app-btn-accent, whose hover is written at
   the same specificity and later, so it always won. Each rule had quietly lost
   its last user to an unrelated Work Item, and each time the thing that broke
   was ColorSchemeTest's hover assertion, two changes later.

   Shape and fill are therefore split: .app-btn no longer carries the neutral
   fill, which nothing has painted with since the splash's link-buttons went. A
   second link-button would bring its own fill rather than inherit one that has
   been unused long enough to be untested. */

.app-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: .55rem 1.25rem;
  /* The user agent's own button border, removed (GX-34).

     These rules were written for anchors, and an anchor has no default border,
     so nothing here ever needed to say so. GX-32 then put the class on the login
     page's native <button> — the one control in the app that cannot be a
     jelly-button, because an un-upgraded one has no <button> inside it at all —
     and the browser's `2px outset ButtonBorder` came with it: a bevel, light on
     the top and left, black on the bottom and right. It read as an uneven dark
     ring around the only button in the application that had one.

     cursor follows for the same reason and from the same cause: a <button>
     defaults to an arrow where every anchor beside it shows a hand. .app-nav-button
     already carries both resets for the Log out button, which is the same
     situation — a native control wearing a class meant for links. */
  border: 0;
  cursor: pointer;
  border-radius: 999px;
  font-family: var(--jelly-font-display);
  font-weight: 640;
  font-size: .95rem;
  text-decoration: none;
}

.app-btn-accent {
  background-color: var(--jelly-color-background-accent);
  color: var(--jelly-color-foreground-on-accent);
}

/* Hover moves the fill towards the ink rather than dimming it (GX-25). The old
   `filter: brightness(.95)` only reads as a hover on a light palette: on a dark
   one it darkens a dark button, so the affordance goes the wrong way and all
   but disappears. Mixing towards foreground-default gets darker in light and
   lighter in dark, from the same one rule. */
.app-btn-accent:hover {
  background-color: color-mix(in srgb, var(--jelly-color-background-accent) 88%, var(--jelly-color-foreground-default));
}


/* The login form's own field, toned to sit beside Jelly's controls (GX-32).

   It is a NATIVE <input> and not a jelly-input, and this is the only place in
   the app where that is true. Everything else is behind the login, so the login
   itself cannot be behind a working JavaScript engine: an un-upgraded
   <jelly-input> has no shadow root and therefore no <input> in it, which was
   measured at zero fields and zero submit buttons with scripts off. Toning
   markup Jelly cannot supply is the §12 convention; this is that, for the one
   form that has to work before Jelly exists.

   Colours come from tokens only — app.css must contain no literal colour, which
   ColorSchemeTest enforces — so this field follows the palette like everything
   else, including before jelly.js has landed, because color-scheme is already
   set on <html> (GX-27). */
.app-field {
  display: block;
  width: 100%;
  padding: .5rem .75rem;
  border: 1px solid var(--jelly-color-border-default);
  border-radius: 12px;
  background-color: var(--jelly-color-background-default);
  color: var(--jelly-color-foreground-default);
  font: inherit;
}

.app-field:focus-visible {
  outline: 2px solid var(--jelly-color-background-accent);
  outline-offset: 1px;
}

.app-field-label {
  display: block;
  margin-bottom: .25rem;
  font-size: .9rem;
  color: var(--jelly-color-foreground-muted);
}

/* The "Remember me" checkbox on the login form (GX-168).

   Native, like the fields above it, and for the same reason: this form has to
   render before jelly.js exists and forever if scripts are off, and a
   jelly-checkbox is an empty unknown element until the module upgrades it.

   accent-color is doing the work rather than a hand-built box. It tints the
   browser's own control, so the checkbox keeps its native focus ring, its
   keyboard behaviour and its platform look, and needs no ::before, no hidden
   input and no ARIA. Colours are tokens, as everywhere — ColorSchemeTest
   forbids a literal. */
.app-check {
  display: flex;
  align-items: center;
  gap: .5rem;
}

.app-check-input {
  width: 1rem;
  height: 1rem;
  margin: 0;
  flex: none;
  accent-color: var(--jelly-color-background-accent);
}

/* Not muted like .app-field-label: this label IS the control's name rather than
   a caption above a field, and it is the click target, so it reads at full
   strength. */
.app-check-label {
  font-size: .9rem;
  color: var(--jelly-color-foreground-default);
  cursor: pointer;
}

/* --- Cards and fields ----------------------------------------------------- */
/* jelly-card brings its own surface and padding, so a heading inside one only
   needs its margin taken off to sit square. */
.app-card-title,
.add-branch-card-title {
  margin: 0 0 .75rem;
  font-family: var(--jelly-font-display);
  color: var(--jelly-color-foreground-default);
}

/* The caption above a group of checkboxes — a label for the group, not for any
   one control, so it is a paragraph rather than a <label>. */
.app-field-group-label {
  margin-bottom: .35rem;
  font-weight: 700;
  font-size: .9rem;
  color: var(--jelly-color-foreground-default);
}

.hero-title { font-size: 2.5rem; margin-bottom: .5rem; }

.hero-lead { font-size: 1.15rem; color: var(--jelly-color-foreground-muted); }

/* --- Breadcrumbs ---------------------------------------------------------- */
/* Every breadcrumb in the app, at the size the criteria page tuned (GX-78,
   made app-wide by GX-79).
   ---------------------------------------------------------------------------
   ON THE ELEMENT, NOT ON AN ID, and that is the point of GX-79. Three pages
   carry a trail — Go Shopping criteria, Shopping List, Matching Products — and
   none of the three has an h1: the trail IS the heading on each, so all three
   want the same size, and three id rules repeating three declarations is the
   arrangement where a fourth breadcrumb quietly matches none of them. The
   markup still writes `size="large"` per element, because that is what supplies
   the gap and the radius; this rule takes the scale further than large goes.

   WEIGHT IS NOT AVAILABLE, and it was measured rather than assumed (GX-78). The
   component's shadow stylesheet sets `ol{font:600 …}` and `.current{font-weight:750}`
   as literals — no custom property, no exposed part — and the `font` shorthand
   blocks inheritance, so a `font-weight` on the host does nothing at all
   (confirmed in the browser: 600/750 before and after). The current crumb is
   already heavier than bold; what made it read lightly was its size.

   SIZE IS AVAILABLE, through the component's own custom properties, which a
   document rule on the host outranks. Jelly declares five, and they are the
   knobs to turn:

       --jelly-breadcrumbs-font-size       15px at size="large"
       --jelly-breadcrumbs-padding-block    5px
       --jelly-breadcrumbs-padding-inline  10px
       --jelly-breadcrumbs-gap              5px
       --jelly-breadcrumbs-radius          10px

   The padding is the hover pill around the parent crumb, so it is scaled with
   the text rather than left at the size it was drawn for.

   No test pins these numbers, deliberately, because they are being tuned. What
   IS pinned is that the three trails agree, which is the fact GX-79 asked for
   and the one an id rule could break without anything noticing. */
jelly-breadcrumbs {
  --jelly-breadcrumbs-font-size: 22px;
  --jelly-breadcrumbs-padding-block: 7px;
  --jelly-breadcrumbs-padding-inline: 14px;
}

/* --- Shared chrome (GX-23) ------------------------------------------------ */
/* Jelly covers controls and surfaces but has no navigation or layout, so the
   sidebar, top bar and footer are plain markup wearing Jelly's tokens. */

.app-topbar {
  background-color: var(--jelly-color-background-surface);
  border-bottom: 1px solid var(--jelly-color-border-default);
  padding-block: .6rem;
}

.app-brand {
  font-family: var(--jelly-font-display);
  font-size: 1.25rem;
  font-weight: 640;
  color: var(--jelly-color-foreground-default);
  text-decoration: none;
}

.app-brand:hover { color: var(--jelly-color-background-accent); }

/* The trolley mark beside the wordmark (GX-34).

   Left as an inline image rather than making .app-brand a flex container: two of
   the four brand anchors carry Bootstrap's `d-block`, which is !important and
   would beat a `display: inline-flex` here — the layout would then be right in
   two places and wrong in the other two, which is the kind of split nobody finds
   until they open the page on a phone. An inline image flows the same inside a
   block anchor as inside an inline one, so this needs no display rule at all.

   Sized in `em` so it tracks the wordmark wherever the brand is rendered, and
   capped there: 1.4em against the 1.25rem brand is 28px, just under the source
   image's 32px, so it never upscales. The negative vertical-align sits the
   trolley's optical centre on the text's, which `middle` does not — `middle`
   aligns to the x-height and leaves the mark riding high next to a capital G. */
.app-brand-mark {
  width: 1.4em;
  height: 1.4em;
  margin-inline-end: .4rem;
  vertical-align: -.32em;
}

.app-sidebar {
  height: 100%;
  padding: 1rem;
  border-right: 1px solid var(--jelly-color-border-default);
}

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

.app-nav-link {
  display: block;
  padding: .5rem .75rem;
  margin-bottom: .15rem;
  border-radius: 12px;
  color: var(--jelly-color-foreground-default);
  text-decoration: none;
}

.app-nav-link:hover {
  background-color: color-mix(in srgb, var(--jelly-color-background-accent) 10%, transparent);
  color: var(--jelly-color-foreground-default);
}

/* The current page. Jelly's accent fill with its matching on-accent ink, so the
   pairing stays legible if the accent is ever re-tinted. */
.app-nav-link.is-active {
  background-color: var(--jelly-color-background-accent);
  color: var(--jelly-color-foreground-on-accent);
}

/* Log out is a POST form, so the control is a <button> where its neighbours are
   anchors (GX-32). These two rules are the whole of making it not look like one:
   strip the user agent's button chrome, and let it fill the row the way a block
   anchor does. It keeps .app-nav-link, so hover and spacing come from the rules
   above and cannot drift from the links beside it. */
.app-nav-form {
  margin: 0;
}

.app-nav-button {
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* Hides the drawer until it is a drawer (GX-28).

   jelly-drawer is an ordinary inline element until the Jelly module upgrades it,
   so until then it renders its whole copy of the side navigation IN FLOW — a
   duplicated menu, and a 221px taller page that collapses the moment the module
   lands. That is the "jarring re-render" on every sidebar click, in both
   palettes. (The duplication itself is right: GX-23 renders one sideNav fragment
   twice so the two cannot drift. It just has no business being visible before
   the drawer is modal.)

   :defined is the platform's own "has this upgraded yet", so the cloak lifts
   itself — nothing has to be told when.

   html.js is what keeps it honest. GX-23 refused to make navigation depend on
   JavaScript, and below lg the sidebar column is hidden, so an ungated rule
   would leave a phone with no navigation at all when scripts are off. The class
   is set by the inline boot script in the layout: no JavaScript, no class, no
   cloak, and the drawer's links stay in flow as the fallback they already
   accidentally were. */
html.js jelly-drawer:not(:defined) {
  display: none;
}

/* The same cloak, for the same reason (GX-40).

   jelly-dialog is the other component whose upgraded form is structural: it
   becomes a modal overlay, so until the module lands it renders its slotted
   content as an ordinary block in the middle of the page. Today that content
   starts empty, which would hide the defect rather than remove it — the moment
   HTMX fills it before the module has run, a confirmation would appear inline
   under the table.

   Gated on html.js like the rule above, and the gate costs nothing here: with
   no JavaScript nothing ever fills the dialog, because the row's delete control
   is a jelly-icon-button driven by HTMX and cannot start anything without it
   (GX-45 — this comment used to claim the control navigated to a confirmation
   page instead, which it never did). */
html.js jelly-dialog:not(:defined) {
  display: none;
}

/* The theme control under Go Shopping (GX-27). Indented to the same optical
   left edge as the nav links, whose own padding sits inside their hit area. */
.app-nav-theme {
  padding: .35rem .75rem;
  margin-bottom: .15rem;
}

/* The theme control itself (GX-46): a label and the button that cycles
   light → dark → system.

   The label is a sibling rather than the button's content, because
   jelly-icon-button slots its children INTO the button as the icon — text put
   there would be drawn inside the square. */
.app-theme {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
}

/* No font-size: the nav links beside it declare none either, so both take the
   body's, and the label sits at the same size as the menu it belongs to. It
   keeps the muted ink — it names the control rather than being one. */
.app-theme-label {
  color: var(--jelly-color-foreground-muted);
}

/* Only the current state's icon is shown. All three are in the markup so the
   script never has to build one and the button is never momentarily empty —
   see js/theme.js. Stated explicitly rather than left to the user agent's
   [hidden] rule, because the component's own ::slotted(svg) rule sets width
   and height on these and a sized-but-undisplayed icon is worth being sure
   about. */
[data-theme-icon][hidden] {
  display: none;
}

/* An SVG carrying only a viewBox is a replaced element with no intrinsic size,
   so until jelly-icon-button upgrades and sizes ::slotted(svg) from its own
   token, the browser draws it at the CSS default of 300x150 (GX-46). That is
   invisible on a page whose icons live in table cells, and very visible indeed
   in the sidebar and the splash top bar, where this control sits on every
   screen — in exactly the pre-upgrade window GX-28 and GX-30 are about.

   :not(:defined) is the platform's own "has this upgraded yet", so the rule
   lifts itself. Deliberately NOT gated on html.js, unlike the drawer and
   dialog cloaks: with scripts off the button never upgrades at all, and a
   small icon is the right resting state rather than a 300x150 one.

   Sized in em so it tracks the surrounding text, which is what the upgraded
   button's own token does. */
jelly-icon-button:not(:defined) svg {
  width: 1.0625em;
  height: 1.0625em;
}

/* No toning rule for the theme control, and its absence is the point.
   variant="platinum" and shape="square" are set on the element itself and both
   work — see the layout fragment. app.css carried a hand-copied platinum here
   for one revision, on the strength of GX-37's claim that jelly-icon-button
   ignores `variant`. That claim is wrong: the seven variant rules live in the
   shared base this component's styles() includes, which can be read straight
   out of its shadow root at runtime.

   Nothing replaces it. Setting --jelly-fill from a document rule would OUTRANK
   the variant it was imitating and take the focus ring with it — the component
   derives --jelly-ring from --jelly-fill, so overriding the fill silently
   re-colours the ring, which is how the previous revision needed a third
   declaration to undo the damage done by its first two. Letting the variant do
   its own job costs nothing and takes the ring with it, correctly. */

.app-nav-divider { margin-block: .75rem; }

.app-footer {
  border-top: 1px solid var(--jelly-color-border-default);
  color: var(--jelly-color-foreground-muted);
  font-size: .875rem;
}

.app-footer-link {
  color: var(--jelly-color-foreground-muted);
  text-decoration: none;
}

.app-footer-link:hover {
  color: var(--jelly-color-foreground-default);
  text-decoration: underline;
}

#app-drawer .app-nav-link { font-size: 1.05rem; }

/* --- Shopping-list chips (Items page) ------------------------------------- */
/* A chip carries only a hue (--chip-hue, set by its .lc-* class); the fill,
   border and ink are derived here (GX-25).

   Mixing the hue INTO the current surface is what makes a chip palette-agnostic:
   the same 18% lands as a pale wash on a white page and a deep one on a dark
   page, and the ink is always the palette's own foreground. The colours the
   chips replaced were hand-picked pastel hex, which stayed pastel in the dark
   palette and read as light stickers stuck on a dark page. Any future
   decorative colour set should be built the same way. */
.list-chip {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  max-width: 12rem;
  padding: .1rem .5rem;
  margin: .1rem .15rem;
  border: 1px solid color-mix(in srgb, var(--chip-hue) 45%, transparent);
  border-radius: 1rem;
  background: color-mix(in srgb, var(--chip-hue) 18%, var(--jelly-color-background-surface));
  color: var(--jelly-color-foreground-default);
  font-size: .8rem;
  line-height: 1.4;
  white-space: nowrap;
}

/* Label truncation is done server-side (16 chars + ellipsis, raised from 10 by
   GX-164); this is a width-based backstop so long labels never break the row.
   Sixteen wide characters at this size still land inside 12rem, so the two
   limits do not fight: the server decides what the label says and this only
   catches a name of capitals. */
.list-chip .list-chip-label {
  overflow: hidden;
  text-overflow: ellipsis;
}

.list-chip .list-chip-remove {
  border: 0;
  background: transparent;
  padding: 0;
  font-weight: bold;
  line-height: 1;
  cursor: pointer;
  opacity: .7;
}
.list-chip .list-chip-remove:hover { opacity: 1; }

/* Stable per-list hue. Jelly's four named hues, which are the same values in
   both palettes — they are identity, not contrast, and the mixing above is what
   makes them work against either surface. There were six of these; only four
   were ever assigned, and the two spares were the last hard-coded hex in this
   file.

   ItemsPageService assigns them, cycling by the LIST'S ID (GX-164). Not by name
   order: names move, so creating a list that sorts first would re-colour every
   list after it and repaint a page the Shopper had learned. A fifth list
   therefore repeats the first hue, which is why a chip also carries its name —
   the colour tells them apart at a glance, the label tells them apart for
   certain. */
.lc-1 { --chip-hue: var(--jelly-color-background-azure); }
.lc-2 { --chip-hue: var(--jelly-color-background-mint); }
.lc-3 { --chip-hue: var(--jelly-color-background-amber); }
.lc-4 { --chip-hue: var(--jelly-color-background-rose); }

/* --- Content panels ------------------------------------------------------- */
/* A fixed width (~half a wide screen) rather than a percentage column: the
   panel holds that width as the window narrows, and only starts shrinking with
   the window once the content area itself is narrower than this.

   Named .app-panel rather than for the one page that first needed it (GX-37).
   It was .branches-panel, and the second page to want "the same width as
   Preferred Branches" would otherwise have had to either say 60rem again — one
   number in two places, drifting the moment either moves — or wear a class
   named after a page it is not. Note that /supermarkets keeps
   id="branches-panel": that id is the HTMX swap target, and is a different fact
   from how wide the panel is. */
.app-panel { max-width: 60rem; }

/* The delete confirmation's buttons, clipped so the dialog does not scroll
   (raised in review on GX-40).

   The canvas-bleed trap (§12), in exactly the place §12 says to look for it:
   jelly-dialog's own .dialog is an overflow:auto box, and every Jelly control
   paints on a canvas larger than itself so its soft-body wobble has room —
   an 85x42 button on a 173x130 canvas. That bleed is pure decoration but it
   still counts towards an ancestor's scrollable area, and two buttons' worth
   of it gave the modal a scrollbar on BOTH axes. Measured at 17px of
   horizontal overflow on a 1440px window.

   `clip` rather than `hidden`, per §12: `hidden` would make this a scroll
   container on both axes in its own right, trading one scrollbar for another
   place one could appear. The small padding is not decoration either — Jelly
   draws its focus ring at inset -2px on the inner button, so clipping tight to
   the row would shave the ring off whichever button is at the edge. */
.delete-confirm-actions {
  overflow: clip;
  padding: 4px;
}

/* --- The Shopping List page's editable item table (GX-41) ------------------ */
/* Column widths, so a table of text fields does not give Name a third of the
   room and Amount the same. Set on the header cells, which is where a table's
   column width is decided; the fields inside fill what they are given.

   EVERY column has one, and table-layout is fixed (GX-56). Leaving Name and the
   action column without a width did not "let Name take the rest", which is what
   it looks like it should do: under automatic layout the browser distributes
   slack across the trailing columns, so Products took 360px of a 960px table to
   hold one digit and Name — the only column holding words — got 289px, under a
   third. Fixed layout makes these numbers the answer rather than a suggestion,
   and it is safe here precisely because every column now declares one.

   Name at 50% is the point of the exercise: about twice what it had, and the
   only column whose content is a sentence rather than a number, a five-value
   unit or a single control. */
#item-table { table-layout: fixed; }
.item-handle-col { width: 2.75rem; }
.item-name-col { width: auto; }
.item-amount-col { width: 7rem; }
/* Unit took 1.5rem from Products (GX-66). jelly-select sizes its options panel
   to the width of its trigger and sets white-space: nowrap on the rows, so a
   label wider than the column overflows into a horizontal scrollbar rather than
   wrapping — and "Pkg items" is wider than the five short units this column was
   sized for. Products can afford it: it holds a count and a link, and its header
   shortened to "Prods" to match. Name is untouched and still holds over half the
   table, which is the constraint that made widening Unit at Name's expense
   impossible. */
.item-unit-col { width: 10rem; }
.item-products-col { width: 3.5rem; }
.item-action-col { width: 3.5rem; }

/* --- Re-ordering a row by its handle (GX-58) ------------------------------ */

/* The grip cell. Padding trimmed on the outside so the rail of handles sits
   against the table's edge rather than a third of the way into a column that
   exists only to hold them. */
.item-handle {
  padding-inline-start: 0;
  padding-inline-end: .25rem;
}

/* touch-action: none is what makes this work with a finger at all, and it is
   the one declaration here that is not decoration.

   A pointerdown on an element the browser thinks it might scroll is followed by
   a pointercancel the moment the finger moves far enough to look like a scroll
   gesture — so the drag ends, silently, and the page scrolls instead. Saying
   the element consumes its own gestures is what stops that. It is scoped to the
   handle rather than to the row precisely so the rest of the row still scrolls
   the page normally: the grip is the only part a Shopper cannot scroll from,
   which is also how a grip should feel.

   cursor: grab is the sighted half of the same message. */
.reorder-handle {
  touch-action: none;
  cursor: grab;
}

/* The row being dragged. A tint mixed from the accent into the current surface,
   per the GX-25 rule — a literal would be both wrong in one palette and
   forbidden in this file by ColorSchemeTest.

   The `position: relative` is not decoration: a <tr> paints its background
   behind its cells, and without a stacking context of its own the tint would
   sit under the row's own cell backgrounds and show only in the gaps. */
.item-row.dragging {
  position: relative;
  background: color-mix(in srgb, var(--jelly-color-background-accent) 12%, transparent);
  cursor: grabbing;
}

.item-row.dragging .reorder-handle {
  cursor: grabbing;
}

/* This page keeps .app-panel's 60rem like every other page, and that is a
   decision rather than an omission (review of GX-56). It was tried uncapped —
   the panel taking the whole content area, so the Name field grew with the
   window — and it was rejected on sight: a page that runs the full width of a
   large monitor reads as broken beside the two pages next to it in the nav,
   whatever it buys the field.

   So Name gets its width from the other four columns instead, which are at
   their comfortable minimums above, and it lands at about 60% of the table
   rather than the 30% it had. */

/* The row holding the Full deletes switch and the two buttons.

   space-between rather than flex-end (GX-57 changed it): the switch takes the
   left end, under the table's left edge and near the rows it governs, and the
   buttons stay in their own group at the right. Save's edge therefore still
   lands on the table's edge, which is what the review of GX-56 asked for — the
   row is the full width of the panel and so is the table, so that alignment
   holds by construction rather than by a matching number. */
.list-actions { justify-content: space-between; }

/* The canvas bleed again (§12), and this time the answer is NOT to clip
   everywhere. Every cell in this table holds a Jelly control, so every cell has
   decoration reaching past its box — but the Unit cell holds a jelly-select,
   whose options panel is position:absolute inside its own host rather than
   reparented to <body> the way jelly-drawer and the toaster are. Any overflow
   value other than visible on that cell — on EITHER axis, since a non-visible
   value on one axis makes the other a scroll container too — clips the open
   dropdown to the height of a table cell.

   So: the trash cell is already clipped by the .table rule above (last-child),
   the name and amount cells clip on the horizontal axis alone, and the unit
   cell is deliberately left alone. It is also why this table is NOT wrapped in
   .table-responsive as the app's others are: that class is overflow-x:auto,
   which would clip the same dropdown from further out where no cell rule could
   rescue it. The columns above are narrow enough that it is not needed.

   NAMED rather than counted since GX-58, which added the handle column at the
   front and so moved Name and Amount from cells 1 and 2 to cells 2 and 3. The
   positional selector kept matching — it just silently clipped a different pair
   of cells, one of which was the grip. A class says which cells are meant, and
   the next column added at either end cannot quietly repoint it.

   The cells are `item-name-cell` / `item-amount-cell` and not `item-name` /
   `item-amount`, which are the classes on the FIELDS inside them. Reusing those
   would make `row.querySelector('.item-name')` in js/shopping-list.js return
   the cell rather than the input it is looking for.

   ONE AXIS IS SAFE HERE, AND THE CONDITION IS THE TABLE, NOT THE CELL (GX-70).
   GX-69 found the same single-axis clip letting the vertical bleed inflate a
   list that scrolled, and the difference is what encloses it: this table is not
   wrapped in .table-responsive (see above), so no ancestor here is a scroll
   container and there is nothing for the vertical bleed to fill. It reaches
   only the page, where the footer's own top margin leaves far more slack below
   the last row than a bleed uses. Measured at 1440x900 and 390x840 and pinned
   by CanvasBleedContainmentTest — so if this table is ever wrapped in an
   overflow box, that test fails rather than a scrollbar appearing.

   The unit cell below is the one place the bleed genuinely escapes: 18px of it,
   on the phone layout, because that cell must stay unclipped for its dropdown.
   That is a knowing trade and the test names it. */
#item-table > tbody > tr > td.item-name-cell,
#item-table > tbody > tr > td.item-amount-cell {
  overflow-x: clip;
}

/* On a phone the row stops being a row.

   Five columns of controls do not fit in 390px, and the failure is not a
   graceful one: the fixed widths above take 24rem of a 24.4rem content area, so
   Name and Amount are squeezed to about 30px each and show no text at all — an
   editable table with nothing legible in it. The page passed a "does not scroll
   sideways" assertion throughout, because everything that did not fit was being
   clipped rather than overflowing. Found by looking at a screenshot.

   Horizontal scrolling is the usual answer and is the one thing this table
   cannot have: .table-responsive is an overflow box, which clips the Unit
   dropdown (see above). So each cell becomes a full-width line with its column
   heading in front of it, and the heading row — now saying nothing the rows do
   not — is dropped. Nothing is hidden and no column is dropped: on the screen
   with the least room, the Products count is exactly as interesting as it is on
   a desktop.

   The label comes from data-label rather than from a second copy of the text in
   the markup, so the heading and the label cannot drift apart. */
@media (max-width: 767.98px) {
  #item-table,
  #item-table > tbody,
  #item-table > tbody > tr,
  #item-table > tbody > tr > td {
    display: block;
    width: auto;
  }

  #item-table > thead {
    display: none;
  }

  /* The stacked row gets a gutter down its left side, and the grip lives in it
     (GX-58).

     Every other cell becomes its own full-width line here, and the handle is
     the one that must not: a line holding nothing but a grip would be the
     tallest thing in the row and would read as a control belonging to the
     Name line under it. Taking it out of flow and pinning it to the gutter
     makes it what it is on a desktop — a grip for the whole row — except that
     the "row" is now a block of five lines, so the grip sits beside all of
     them instead of beside one.

     The padding is what reserves the gutter, so the absolutely-positioned
     handle overlaps nothing rather than being placed on top of the text and
     hoping. */
  #item-table > tbody > tr {
    position: relative;
    border-top: 1px solid var(--jelly-color-border-default);
    padding: .5rem 0 .5rem 2.5rem;
  }

  /* TOP of the gutter, level with the Name line — not centred down it, which
     is what this first shipped as (changed in review of GX-58).

     Centring is the conventional treatment for a list grip and it is wrong
     here for a reason the desktop layout does not have: the block is five
     lines tall, so the vertical middle of it lands squarely beside the UNIT
     line, and a grip level with one labelled line reads as belonging to that
     line. There is no such thing as "beside all five" to the eye — it is
     always beside whichever one it is level with. Name is the line that says
     which row this is, so that is the one to be level with.

     The height is one stacked line rather than the whole block, which is what
     centres the grip against the Name FIELD instead of against the top edge of
     its cell. It is a measured number and it is allowed to be approximate: it
     decides where in the first line the grip sits, and nothing at all about
     whether the grip and the field are on the same line. */
  #item-table > tbody > tr > td.item-handle {
    position: absolute;
    left: 0;
    top: .5rem;
    width: 2.5rem;
    height: 3rem;
    padding: 0;
    align-items: center;
    justify-content: center;
  }

  #item-table > tbody > tr > td {
    display: flex;
    align-items: center;
    gap: .75rem;
    border: 0;
    padding-inline: 0;
  }

  #item-table > tbody > tr > td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 5.5rem;
    font-family: var(--jelly-font-display);
    font-size: .8rem;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--jelly-color-foreground-muted);
  }

  /* The field takes what the label leaves, rather than its content width. */
  #item-table > tbody > tr > td > jelly-input,
  #item-table > tbody > tr > td > jelly-select {
    flex: 1 1 auto;
    min-width: 0;
  }

  /* The one cell with no label is the row's action, and it stays at the end of
     the row — Bootstrap's .text-end aligns text, which a flex container does
     not have. Without this the trash control lands under the Products line at
     the left margin, reading as a control belonging to that line. */
  #item-table > tbody > tr > td.text-end {
    justify-content: flex-end;
  }
}

/* --- The Matching Products page (GX-60) ----------------------------------- */

/* There is deliberately NO rule for .item-products-link — the Products count's
   link on the Shopping List page. It wears .app-link, which is the app's link
   treatment and already contrast-checked in both palettes (see above: 7.8:1
   light, 9.9:1 dark, and the hover mix that stays above the floor). The class
   survives in the markup as a test hook and nothing else, exactly as
   .delete-list does.

   It had its own rule for about an hour, and the rule painted NOTHING:
   `color: var(--jelly-color-foreground-accent)` names a token Jelly does not
   have — its foreground set is default, muted, on-accent, on-emphasis,
   on-neutral and on-white — so the declaration resolved to nothing and the
   count inherited the table's ink. A link that reads as plain text, in silence,
   and only findable by looking at it. §12 says to check the thing you are about
   to rely on, in the source; that rule is about component attributes, and this
   is the same failure one layer down, in a custom property.

   The general form: a var() naming a token that does not exist is not an error
   anywhere — it is a declaration that does nothing. Derive from a token the app
   already uses, or check the name. */

/* Fixed layout with a width on EVERY column, which is the GX-56 lesson taken
   first time rather than after the fact: under automatic layout the browser
   hands its slack to the trailing columns, so Image — which holds a 40px
   picture — would take a quarter of the table while Product, the only column
   holding a sentence, got squeezed.

   Product is the `auto` one for that reason: it takes whatever the other five
   leave, which lands it at about half the table. */
#matching-products-table { table-layout: fixed; }
.mp-select-col { width: 3rem; }
.mp-category-col { width: 11rem; }
.mp-product-col { width: auto; }
.mp-supermarket-col { width: 9rem; }
.mp-preference-col { width: 7rem; }
.mp-image-col { width: 5rem; }

/* The canvas bleed (§12), in the place §12 says to look: two jelly-buttons in a
   flex row at the right-hand edge of a 60rem panel. Every Jelly control paints
   on a canvas larger than itself, and that decoration still counts towards an
   ancestor's scrollable area — the same 17px of horizontal overflow the delete
   confirmation's buttons produced on GX-40.

   `clip` rather than `hidden`, and 4px of padding, for the reasons written on
   .delete-confirm-actions above: `hidden` would make this a scroll container on
   both axes, and clipping tight to the row would shave off the focus ring Jelly
   draws at inset -2px on the button at the edge. */
.mp-actions {
  overflow: clip;
  padding: 4px;
}

/* The disabled search box and its Add button. The button must not shrink — a
   flex item's default min-width is auto, but a jelly-button's canvas makes its
   content box wider than its label, and without this it is the thing that gives
   when the row is tight. */
.mp-search-row > jelly-button {
  flex: 0 0 auto;
}

/* A column heading that sorts. It is an <a>, so it arrives underlined and
   accent-coloured; neither is wanted on a table header, which should read as a
   heading first and reveal itself as a control on hover.

   The arrow sits inside the same link so it is part of the hit target rather
   than a decoration beside it. */
.mp-sort {
  color: inherit;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  white-space: nowrap;
}

.mp-sort:hover,
.mp-sort:focus-visible {
  text-decoration: underline;
}

/* The column the table is actually ordered by. Weight alone, no colour: the
   arrow beside it already says which way, and a coloured heading in a row of
   plain ones reads as a link that is somehow more of a link than its
   neighbours. */
.mp-sort.is-sorted {
  font-weight: 700;
}

.mp-sort-arrow {
  font-size: .75em;
  color: var(--jelly-color-foreground-muted);
}

/* The product picture. A fixed box, and the cell keeps it whether or not there
   is an image to put in it: PakNSave products have no image URL at all (GX-49),
   so a table of mixed supermarkets would otherwise change row heights down its
   length as the pictures that do exist arrive.

   object-fit: contain because these are supermarket pack shots at whatever
   aspect the source chose — `cover` would crop the label off a tall bottle. */
.mp-image-cell { height: 3.5rem; }

.mp-image {
  max-width: 3rem;
  max-height: 3rem;
  object-fit: contain;
}

/* The category cell's tooltip trigger. jelly-tooltip wraps its child in an
   inline box; without this the long category text pushes the column open, and
   the whole point of the tooltip is that the narrow level is what shows.

   The horizontal axis alone is deliberate and it is about the TOOLTIP, not the
   canvas bleed: this rule exists to hold a column width, and widening it to
   both axes would clip the tooltip this cell is built around. Checked in GX-70
   for the bleed as well — this table is not inside .table-responsive either, so
   nothing enclosing it is a scroll container, and CanvasBleedContainmentTest
   pins that. */
.mp-category-cell {
  overflow-x: clip;
}

/* On a phone the row stops being a row — the treatment the Shopping List table
   takes (GX-41), and for the same reason: six columns do not fit in 390px, and
   the fixed widths above would clip rather than overflow, which passes a
   "does not scroll sideways" assertion while showing nothing legible.

   .table-responsive is not the answer here either: it is an overflow box, and
   it would clip the category tooltips to the height of a cell (§12). */
@media (max-width: 767.98px) {
  #matching-products-table,
  #matching-products-table > tbody,
  #matching-products-table > tbody > tr,
  #matching-products-table > tbody > tr > td {
    display: block;
    width: auto;
  }

  #matching-products-table > thead {
    display: none;
  }

  /* Dropping the header row takes the sort controls with it, which is a real
     loss and an accepted one: there is nowhere on a 390px screen to put four
     sort links that would not cost more room than the rows they order. The sort
     still travels in the URL, so a page sorted on a desktop and opened on a
     phone is still sorted. */

  #matching-products-table > tbody > tr {
    border-top: 1px solid var(--jelly-color-border-default);
    padding: .5rem 0 .5rem 2.5rem;
    position: relative;
  }

  /* The selection box goes in the gutter, the way the Shopping List's grip does
     (GX-58) — and level with the PRODUCT line rather than centred down the
     block, for the reason recorded there: a box level with one labelled line
     reads as belonging to that line, and there is no such thing as "beside all
     five" to the eye. The Product line is the one that says which row this is;
     Category does not.

     Which is why `top` is a line down rather than at the block's top edge. The
     Product line is the SECOND stacked line here — the checkbox cell is out of
     flow, so Category is first — and each line is about 2.5rem. It is a measured
     number and it is allowed to be approximate: it decides which line the box
     sits beside, and nothing about whether the layout works.

     It first shipped at .5rem, level with Category, while the comment claimed
     Product. Found by looking at a screenshot — which is the only way to find
     it, and the reason GX-41 says to take one of any new page at both widths
     before believing either. The browser test asserts the property rather than
     the number: the box's centre falls inside the Product cell's span. */
  #matching-products-table > tbody > tr > td.mp-select-cell {
    position: absolute;
    left: 0;
    top: 2.5rem;
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  #matching-products-table > tbody > tr > td {
    display: flex;
    align-items: center;
    gap: .75rem;
    border: 0;
    padding-inline: 0;
  }

  #matching-products-table > tbody > tr > td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 5.5rem;
    font-family: var(--jelly-font-display);
    font-size: .8rem;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--jelly-color-foreground-muted);
  }

  /* The image cell's fixed height is a desktop row's business. Stacked, it is
     one line among six and takes the height of what it holds. */
  .mp-image-cell { height: auto; }
}

/* No toning rule for the delete row action either, for the same reason as the
   theme control above (GX-48). `.delete-list` carried a hand-copied rose here —
   the exact --jelly-fill / --jelly-label pair :host([variant="rose"]) sets — on
   the strength of GX-37's claim that jelly-icon-button ignores `variant`. It
   does not: variant="rose" now sits on the element in shopping-lists.html and
   paints it, and a document rule imitating a variant would take the focus ring
   with it. `.delete-list` survives in the markup as a test hook and nothing
   else, so there is deliberately no rule for it here. */

/* The suggestion lists on /supermarkets and on the Matching Products page.
   Still Bootstrap list groups underneath — Jelly has no listbox — lifted onto a
   Jelly surface so the input they drop out of and the list itself belong to each
   other.

   #product-suggestions was removed by GX-60, which took the skeletal type-ahead
   away, with a note that GX-61 would bring the markup and the rule back
   together. This is that: the selector returns because there is once again an
   element for it to match. It is worth noting that the discipline worked in both
   directions — the rule left when its last user did (GX-43's concern) and came
   back with its next one, rather than sitting unused for a Work Item. */
#branch-suggestions,
#product-suggestions {
  border: 0;
  background-color: transparent;
}

#branch-suggestions:not(:empty) {
  border-radius: 16px;
  overflow: hidden;
  box-shadow: var(--jelly-shadow-overlay);
}

/* The product list does NOT get the surface and shadow its branch counterpart
   does: since GX-61's review the panel around it is the floating surface, and a
   second shadow inside the first reads as a box in a box.

   This is the part of the panel that scrolls, when anything does: the rows
   scroll and the count line above them and the pager below them stay where they
   are.

   HOW TALL IT MAY BE IS MEASURED, NOT DECLARED (GX-69). This rule used to read
   `max-height: min(50vh, 26rem)` and both halves of it were wrong.

   The rem half could never do anything useful. The list holds exactly one page
   of results — ten, ProductSearchService.PAGE_SIZE — and never more, so a cap
   above a full page is unreachable and a cap below one is a scrollbar that is
   always there. At 26rem it was the second of those: 416px against the 524px a
   full page measures (49.8px a row on a desktop), so the list scrolled at every
   window size, including ones with room to spare. That is the defect.

   The vh half cannot be repaired either, because the question is not what
   fraction of the window the list may have. It is how much of the window is
   left UNDERNEATH a panel that hangs 167px down the page on a desktop and 237px
   down on a phone — and no viewport unit knows that distance.

   So js/product-search.js reads it off the page and publishes it here. The
   26rem fallback is exactly the old behaviour, on purpose: a panel that has
   somehow not been measured is capped rather than unbounded.

   The overflow is safe here where it would not be elsewhere in this app: the
   §12 trap is an overflow box clipping something that escapes its bounds, and
   nothing in this list does — the rows are text and checkboxes. Putting a
   jelly-select in here would reopen it. */
#product-suggestions {
  border-radius: 0;
  max-height: var(--mp-results-space, 26rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}

#branch-suggestions .list-group-item,
#product-suggestions .list-group-item {
  background-color: var(--jelly-color-background-surface);
  border-color: var(--jelly-color-border-default);
  color: var(--jelly-color-foreground-default);
}

/* The type-ahead's results, floating over the page (changed in review of GX-61).

   They sat in the flow of the card at first, and the comment here argued for it:
   a list a Shopper ticks and pages through is not a single pick, so an overlay
   would be "a popup that will not go away". What that reasoning missed is what
   the panel is FOR. It is the way to add products to the table directly beneath
   it, and in the flow it pushed that table off the screen — so the Shopper chose
   what to add without being able to see what they already had. Ten per page made
   that better and did not fix it; an overlay does, because the table stays where
   it is and the card stays the size it is.

   So it drops out of the input like #branch-suggestions above, and the closing
   that the objection was really about is handled rather than designed around:
   js/product-search.js closes it on Escape and on a click outside, and
   deliberately NOT on a tick or a page turn, which are the things a person does
   inside it.

   Positioned against #product-search-panel, the wrapper around the search row,
   so it lines up with the input rather than with the card's padding box.
   jelly-card's inner .card is position:relative and carries no overflow, so
   nothing clips it. */
/* THE CARD is what has to be raised, not the panel inside it.

   The floating results painted UNDER the "Toggle pref." button and a preference
   badge below them — through an opaque surface. Two fixes were tried and both
   failed: z-index on the results themselves, then z-index on the wrapper around
   the search row. The reason is one line of Jelly's own stylesheet, found by
   walking the ancestry in the browser and reading back every property that
   affects painting: `jelly-card` is `position: relative`.

   A positioned ancestor is the boundary the panel cannot climb out of by itself.
   Everything below the card — the two action buttons and the table's badges — is
   also positioned (Jelly's controls are), and being later in the document they
   paint after the card and everything the card contains. Raising anything INSIDE
   the card cannot change that, because the comparison happens at the card.

   So the card is raised, and the whole search — input, button and results —
   travels with it. Note what this means for the next overlay: a component that
   is quietly `position: relative` sets the ceiling for anything it contains, and
   nothing in the markup says so. Read the ancestry, do not reason about the
   element. */
.mp-search-card {
  z-index: 20;
}

.mp-results {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: .25rem;
  /* The panel itself does not scroll — the LIST inside it does (below). If the
     whole panel scrolled, the pager would scroll out of sight with the rows,
     and a Shopper who wanted page 2 would have to scroll down through page 1 to
     ask for it. */
  display: flex;
  flex-direction: column;
}

/* Nothing typed, so nothing to float. Without this the panel would hang over
   the table showing only its own border. */
.mp-results.is-empty,
#product-search-panel.is-closed .mp-results {
  display: none;
}

/* The panel is a surface in its own right now, not part of the card behind it —
   so it needs the card's fill and an edge, or the table shows through it.
   Derived from the tokens (the GX-25 rule); the shadow is Jelly's overlay one,
   the same one #branch-suggestions uses. */
.mp-results:not(.is-empty) {
  background-color: var(--jelly-color-background-surface);
  border: 1px solid var(--jelly-color-border-default);
  border-radius: 16px;
  box-shadow: var(--jelly-shadow-overlay);
  padding: .5rem .75rem;
}

/* One product suggestion: the checkbox and the line, on one row. */

.product-suggestion {
  display: flex;
  align-items: center;
  gap: .6rem;
}

/* The line takes what the checkbox leaves, and wraps rather than pushing the
   card sideways: a scraped product name plus its category is long, and the
   phone layout has 390px to fit it in. */
.product-suggestion-text {
  flex: 1 1 auto;
  min-width: 0;
}

/* The canvas bleed again (§12): a column of jelly-checkboxes, each painted on a
   canvas larger than the control so its wobble has room to bulge.

   BOTH AXES, since GX-69. This rule clipped the horizontal one alone, on the
   reasoning that `clip` on one axis is enough and leaving the other visible
   avoids making the row a scroll container. The half it left open was the half
   that mattered here: the last row's bleed hangs below the list's content box
   and counts towards its scrollable area, so the list could be scrolled 35px
   over nothing at all — and `overflow-y: auto` draws a scrollbar for 35px of
   decoration exactly as readily as for a row of text. With the rows made tall
   enough to hold a full page (above), that bleed was the ENTIRE remaining
   scrollbar: every one of the ten rows was visible and the scrollbar was still
   there.

   `clip` rather than `hidden`, which is the §12 preference and is what makes
   naming both axes safe: unlike `hidden` it creates no scroll container, so
   there is nothing here for an overlay to be trapped inside. There is no
   dropdown in these rows to trap anyway — they are text and a checkbox — but
   the distinction is why this can be widened rather than worked around.

   It does not clip the checkbox's focus ring: Jelly draws that at inset -2px on
   the inner control, and the row's own padding is 8px (the GX-40 lesson,
   checked rather than assumed). */
#product-suggestions .list-group-item { overflow: clip; }

/* Matched search tokens inside a type-ahead suggestion. Weight alone (GX-26):
   the amber highlighter background and its ring were louder than the thing they
   marked — several short matches across a name and an address turned one row
   into a row of blocks. Nothing here paints, so this emphasis needs no colour
   and cannot be wrong in a palette it has not been seen in. */
.branch-match,
.product-match {
  font-weight: 700;
}

/* The suggestion the keyboard is currently on. A tint alone is too easy to miss
   against the pale list, so it is paired with a solid inset bar down the
   leading edge — the row the next Enter will take must be unmistakable.

   Selected through #branch-suggestions rather than on its own (GX-25): as a
   bare `.branch-suggestion.is-active` it scored 0-2-0 against the 1-1-0 of the
   `#branch-suggestions .list-group-item` rule above, so the tint lost and never
   painted in either palette. Only the bar was doing the work, which is why
   nothing looked obviously broken. The box-shadow is repeated here because this
   rule now supersedes the bare one entirely. */
#branch-suggestions .branch-suggestion.is-active {
  background-color: color-mix(in srgb, var(--jelly-color-background-accent) 12%, var(--jelly-color-background-surface));
  box-shadow: inset .25rem 0 0 var(--jelly-color-background-accent);
}

/* The "you have no JavaScript" banner (GX-44).

   Every other colour in this file comes from a Jelly token, and this one
   cannot: Jelly injects its --jelly-color-* set from jelly.js, so in the one
   situation this banner exists for there are no tokens at all and a rule
   written from them would paint nothing. A literal is barred too — app.css
   holds none, which ColorSchemeTest enforces, and for a good reason: a literal
   cannot follow the palette.

   CSS system colours are the way out, and they are the right answer rather
   than a loophole. The browser supplies them with no scripting whatsoever,
   they are not hex/rgb()/hsl() so the no-literals rule still holds, and they
   follow light and dark on their own through the `color-scheme: light dark`
   already declared on html (GX-27) — which lives in this stylesheet, not in
   JavaScript, so it applies here too.

   Mark/MarkText is the pair browsers use for highlighted text, so it is
   legible against the canvas by construction rather than by our choosing two
   values and hoping.

   No `display: none` counterpart is needed for the scripts-on case: the
   contents of <noscript> are not rendered at all when scripting is enabled, so
   this rule simply never matches anything. */
.app-noscript {
  background: Mark;
  color: MarkText;
  border-bottom: 1px solid CanvasText;
  padding: .75rem 1rem;
  font-weight: 600;
  text-align: center;
}

/* ---------------------------------------------------------------------------
   Go Shopping criteria (GX-74)
   --------------------------------------------------------------------------- */

/* The hairline between the two sections sharing a card (GX-78).
   Jelly's own accordion draws exactly this between adjacent collapsibles
   (`::slotted(jelly-collapsible + jelly-collapsible){border-top:…}`), and these
   two are deliberately NOT in an accordion — that component's job is to make
   panels exclusive, which comparing branches against items must not be. So the
   separation it would have drawn is drawn here instead, from the same token. */
.gs-section-divided {
  border-bottom: 1px solid var(--jelly-color-border-default);
  padding-bottom: .5rem;
  margin-bottom: .5rem;
}

/* The two selection tables. Fixed layout with a width on the box column, so the
   name column takes everything else — the GX-56 lesson, in its simplest form:
   under automatic layout the browser hands its slack to the trailing column,
   which here is the one that wants it, but only by luck. Said explicitly, it
   stays true when a column is added. */
.gs-table { table-layout: fixed; }
.gs-select-col { width: 3rem; }

/* The quantity to buy on this trip (GX-76). Wide enough for the field and the
   unit beside it; Item keeps everything else, which is the GX-56 rule that the
   name column holds the slack. */
.gs-amount-col { width: 11rem; }

/* The field and its unit read as one quantity — "2 kg" — so they sit on one
   line with the unit taking only what it needs. min-width:0 is what lets the
   field shrink inside the flex line rather than holding the column open at
   jelly-input's own width (§12: Jelly's controls are inline-blocks with a fixed
   default width). */
.gs-amount-cell {
  display: flex;
  align-items: center;
  gap: .5rem;
}

.gs-amount-cell > .gs-amount {
  flex: 1 1 auto;
  min-width: 0;
}

.gs-amount-unit {
  flex: 0 0 auto;
  color: var(--jelly-color-foreground-muted);
}

/* Clip Jelly's canvas bleed on BOTH axes (§12, GX-69).
   Every Jelly control paints on a canvas larger than itself, and that decoration
   counts towards an ancestor's scrollable area. A checkbox per row means the
   bottom row's canvas hangs below the table, which is the half GX-69 found the
   hard way — the horizontal half is the one people remember to clip.

   `clip` rather than `hidden`, which is what makes naming both axes safe: hidden
   would make this a scroll container on both and trap anything positioned inside
   it. Nothing in these cells escapes its bounds — no dropdown, no tooltip — so
   there is nothing for the clip to cut off. Putting a jelly-select in one would
   reopen the GX-41 trap. */
.gs-select-cell,
.gs-name-cell {
  overflow: clip;
}

/* The "(2 / 3)" beside a section heading. Muted and lighter than the heading it
   follows: it is a summary of what is inside, not a second heading. */
.gs-count {
  color: var(--jelly-color-foreground-muted);
  font-weight: 400;
}

/* The switches. jelly-switch is display:inline-flex and would otherwise sit
   three-in-a-row on a wide screen with their labels running together; stacked,
   each label belongs to the control beside it. align-items keeps them at the
   left edge rather than stretching each switch across the card. */
.gs-switches {
  align-items: flex-start;
}

/* The action row's canvas bleed, the same case as .mp-actions: several
   jelly-buttons in a flex row inside a 60rem panel. `clip` plus a few pixels of
   padding, because clipping tight to the row shaves off the focus ring Jelly
   draws at inset -2px on whichever control is at the edge. */
.gs-actions {
  overflow: clip;
  padding: 4px;
}

/* On a phone each row becomes its own block, the treatment the Shopping List
   (GX-41) and Matching Products (GX-60) tables take. Two columns do fit in
   390px, unlike theirs — but the box column is 3rem of a 24rem content area and
   the names are long, so the stacked form reads better and keeps the tick in the
   gutter where the other two pages put it. */
@media (max-width: 767.98px) {
  .gs-table,
  .gs-table > tbody,
  .gs-table > tbody > tr,
  .gs-table > tbody > tr > td {
    display: block;
    width: auto;
  }

  .gs-table > thead {
    display: none;
  }

  /* Dropping the header row takes the select-all box with it. That is a real
     loss and an accepted one — there is no column heading left to hang it from
     — and the rows are individually tickable, which is the function itself. */

  .gs-table > tbody > tr {
    border-top: 1px solid var(--jelly-color-border-default);
    padding: .5rem 0 .5rem 2.5rem;
    position: relative;
  }

  /* The box goes in the gutter, level with the NAME line — the rule GX-58
     established and GX-60 caught its second page with. Here the name is the only
     stacked line, so `top` is the block's own padding rather than a line down;
     the browser test asserts the property (the box's centre falls inside the
     name cell's span) rather than the offset, because the offset changes
     whenever a column does. */
  .gs-table > tbody > tr > td.gs-select-cell {
    position: absolute;
    left: 0;
    top: .5rem;
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .gs-table > tbody > tr > td.gs-name-cell {
    border: 0;
    padding-inline: 0;
  }
}

/* ---------------------------------------------------------------------------
   Single Trip results (GX-97)
   --------------------------------------------------------------------------- */

/* The results sit under the action row, so they need air above them: this is a
   second thing on the page rather than more of the criteria. */
.gt-results {
  margin-top: 1.5rem;
}

.gt-results-heading {
  margin-bottom: .75rem;
}

/* The headline of one result, and it is slotted content — jelly-collapsible
   projects it into its own header, beside the chevron it draws. Flex with wrap
   so the figures fall under the branch name on a phone rather than squeezing it;
   `space-between` puts the money at the right edge on a wide screen, which is
   where a column of numbers is read. */
.gt-headline {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .25rem 1rem;
  width: 100%;
}

/* The name at the left of a headline: the Branch for a Single Trip, and the
   fixed "Multi-trip: …" text for a Multiple Trip (GX-130). One rule with two
   selectors rather than two rules, because "matching the Single Trip
   treatment" is something a stylesheet can either state once or let drift. */
.gt-branch,
.gt-multi-name {
  font-weight: 600;
}

.gt-figures {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem 1rem;
}

/* Each figure is its word and its number, the word first and quieter. The two
   totals are DIFFERENT NUMBERS on purpose (§3.3) — comparison is what was asked
   for, actual is what the till charges — so neither may be shown as "the price"
   with the other in small print beside it. Same size, same weight, one label
   each. */
.gt-figure {
  display: inline-flex;
  align-items: baseline;
  gap: .35rem;
}

.gt-figure-label {
  color: var(--jelly-color-foreground-muted);
  font-size: .8rem;
  letter-spacing: .04em;
  text-transform: uppercase;
}

.gt-figure-value {
  font-variant-numeric: tabular-nums;
}

/* The count of Items this branch cannot answer. Muted rather than coloured: it
   is usually a gap in what we know rather than a fault of the shop's, and a
   red number would say the opposite. */
.gt-missing-count .gt-figure-value {
  color: var(--jelly-color-foreground-muted);
}

.gt-table {
  table-layout: fixed;
}

/* Halved from 7rem by GX-119 at the reviewer's direction, and the room goes to
   Product below. It holds "2 of" or "0.7 kg" — a small number, a space and at
   most two letters — so 7rem was generous by about double, which is what the
   reviewer saw. */
.gt-buy-col { width: 3.5rem; }

/* Comparison ONLY, since GX-119's review split the two price columns apart.
   They moved together from GX-136 until then, on the reasoning that two price
   columns of different widths would read as a layout accident — but the
   reviewer asked for Total to shrink and Comparison to stay, and the columns
   are not alike: Comparison carries the percentile mark and Total carries a
   figure and nothing else. The difference is now the point rather than an
   accident.

   7.75rem is measured. The widest figure either column holds is "$1,234.56" at
   .875rem in tabular numerals; Comparison adds a 1.05em mark and a .35rem gap
   beside it, which comes to just under 7.5rem, so it keeps a quarter-rem over
   its worst case. */
.gt-price-col { width: 7.75rem; }

/* Total, halved from the shared 7.75rem at the reviewer's direction — and not
   QUITE halved, which is the one place this departs from what was asked.

   `table-layout: fixed` does not overflow a column that is too narrow: it wraps
   the figure onto a second line, silently, and only on the rows carrying the
   longest numbers. "$1,234.56" needs 4.4rem of tabular digits at .875rem, so
   3.875rem — a true half — would break exactly the rows a Shopper most wants to
   read. 4.5rem is as far down as the content goes while keeping a margin, and
   it still returns 3.25rem to Product. Raised with the reviewer rather than
   silently rounded. */
.gt-total-col { width: 4.5rem; }

/* The badge column (GX-119, AC 1 and AC 2). Its 2rem is the 2rem the two price
   columns gave up, so the table's total is unchanged and the Product column —
   the one with the long text in it — keeps every pixel it had.

   `white-space: nowrap` is AC 5 of the Description, and it is on the CELL
   rather than on a wrapper inside it because the badges are siblings with no
   element around them: the fragment yields a jelly-badge, a hidden span and a
   run of jelly-tooltips straight into the cell. Five badges at 1.05em plus
   their gaps fit 6.5rem, so the rule is a guarantee rather than a squeeze. */
.gt-badge-col { width: 6.5rem; }

/* THE PRODUCT COLUMN IS DELIBERATELY UNSIZED, which is how it collects what
   Buy and Total gave up (GX-119 review, point 4). `table-layout: fixed` shares
   the leftover width between the columns that state none, and Product is the
   only one — so every rem taken off a neighbour lands there without a rule
   saying so, and a width here would break that. It is the column with the long
   text in it, so it is the right place for the slack. */

.gt-badge-cell {
  white-space: nowrap;
}

/* THE TWO KINDS OF BADGE ARE ALIGNED TO EACH OTHER, not each to the text
   (GX-119 review). Both are inline-level boxes of different heights — a
   jelly-badge is a pill with padding, an icon mark is 1.05em of glyph — and
   inline boxes default to `vertical-align: baseline`, which lines up the
   BOTTOM of the digit inside the pill with the bottom of the icon and leaves
   the two looking a pixel or two out of step.

   `middle` on BOTH puts each box's own midpoint on the same line, so a "1"
   beside a credit-card sits centred against it whichever is taller. It is set
   on the pair rather than on one of them: aligning only the icons would move
   them onto the badges' baseline problem from the other side.

   IT GOES ON THE TOOLTIP, NOT ON THE MARK INSIDE IT. jelly-tooltip is what
   actually sits beside the jelly-badge in the cell's inline run; .gt-badge-mark
   is a child of it, so aligning that moves the glyph within its own wrapper and
   leaves the wrapper where it was. Written on the mark first, this left the two
   3.7px out of true and the test below said so. */
.gt-badge-cell .gt-badge,
.gt-badge-cell jelly-tooltip {
  vertical-align: middle;
}

/* AND THE NOWRAP ABOVE MUST NOT REACH THE TOOLTIP. `white-space` inherits, and
   it inherits THROUGH THE SHADOW BOUNDARY into jelly-tooltip's bubble — which
   is a block with `max-width: 200px` and no wrapping rule of its own, so it
   trusts what it inherits. Left alone, the longest sentence here rendered 533px
   of text inside a 218px bubble and the Shopper read "...of 2 for $".

   Found by looking at the running page and not by any test: every assertion
   about a tooltip reads its `text` attribute, which was correct throughout. It
   is the reason §11 says a green suite does not stand in for opening the page.

   The guarantee AC 5 asks for is unaffected. A break BETWEEN two badges is an
   opportunity in the CELL's inline context, governed by the cell's own
   `white-space`; this rule governs what is inside each badge. */
.gt-badge-cell jelly-tooltip {
  white-space: normal;
}

/* Which Shopping Item this row answers, above the Product that answers it. */
.gt-item-label {
  display: block;
  color: var(--jelly-color-foreground-muted);
  font-size: .8rem;
}

.gt-product-block {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
}

.gt-product {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .4rem;
}

/* Clip Jelly's canvas bleed (§12, GX-69). A badge per row paints on a canvas
   larger than itself, and that decoration counts towards an ancestor's
   scrollable area — the trap four Work Items have now paid for. Both axes, and
   `clip` rather than `hidden` so this never becomes a scroll container: nothing
   in this cell needs to escape it, there being no dropdown here (which is the
   GX-41 case that makes one cell on the Shopping List page stay unclipped). */
.gt-product-cell {
  overflow: clip;
}

.gt-price-cell,
.gt-buy-cell {
  font-variant-numeric: tabular-nums;
}

.gt-buy-unit {
  color: var(--jelly-color-foreground-muted);
}

/* The price-percentile mark beside a Comparison price (GX-136).

   The figure and the mark are two spans so the icon can sit outside the
   tabular-nums run: `font-variant-numeric` on .gt-price-cell aligns the digits
   down the column, and an icon inside that run would be one more thing the
   fixed column width has to hold in step. On a phone the cell is already a flex
   row carrying its own column heading, so the two spans become two flex items
   and the .5rem gap places them without a second rule.

   BOTH price cells wear .gt-price-figure, though only Comparison has a mark
   beside it. The cell is no longer the money — it holds the money and, on one
   column, a sentence for a screen reader — so a test or a stylesheet reaching
   for the figure needs somewhere to reach that is the figure, in both columns
   alike. Five browser assertions read .gt-price-cell's innerText before this
   and picked up the hidden sentence with it; the answer was to name the figure
   rather than to hide the sentence better. */

   COLOUR IS ON THE WRAPPER, NOT IN THE ICON. Each glyph is drawn
   stroke="currentColor", so one `color` declaration per band drives it, which
   is the same arrangement `external-link` uses to follow its link's ink. */
.gt-percentile {
  display: inline-flex;
  align-items: center;
  margin-inline-start: .35rem;
  border-radius: 3px;
}

.gt-percentile-icon {
  width: 1.05em;
  height: 1.05em;
}

/* The four icon badges in the badge column (GX-119).

   NO COLOUR DECLARATION, and that is AC 4 rather than an omission. The icons
   are drawn stroke="currentColor", so with nothing set here they take the
   cell's own ink and re-tone themselves with the palette in both schemes. The
   percentile marks beside them are the contrasting case: those DO name a colour
   per band, because there the colour is the finding.

   Sized in `em` for the reason .gt-percentile-icon is: these sit at text size
   in a table cell rather than inside a jelly-icon-button, so there is no
   component token tracking a `size` attribute and the badge should follow the
   row's own text. */
.gt-badge-mark {
  display: inline-flex;
  align-items: center;
  border-radius: 3px;
}

.gt-badge-icon {
  width: 1.05em;
  height: 1.05em;
}

/* tabindex="0" is what makes each tooltip reachable without a pointer (AC 10),
   so the ring has to be visible — a focusable element with no focus style is
   the defect that change would otherwise introduce. :focus-visible rather than
   :focus, so a pointer user who taps a badge does not get a ring they did not
   ask for. §11 records that a TEST for this must press TAB: Chromium does not
   apply :focus-visible to a programmatic focus() of a non-input element. */
.gt-badge-mark:focus-visible {
  outline: 2px solid var(--jelly-color-border-focus);
  outline-offset: 2px;
}

/* The two saturated bands take Jelly's own variant colours, which hold the same
   value in both palettes and clear 3:1 against either page background. */
.gt-percentile-mint { color: var(--jelly-color-background-mint); }
.gt-percentile-rose { color: var(--jelly-color-background-rose); }

/* GRAPHITE DOES NOT, and this is the one place GX-136 departs from the letter of
   its Description. Jelly's `graphite` is background-neutral-emphasis — a FILL,
   meant to carry foreground-on-emphasis text on top of it, which is why the
   dark palette moves it only from #5A5A5A to #565B66 while the page behind it
   drops to #181B1D. As an INK on that page it lands near 2.4:1 and a 1em
   stroked glyph disappears. foreground-muted is the token that exists for
   exactly this job and is defined per palette (#5D6474 / #A9B1C2), so the
   neutral mark is legible in both.

   The BAND is still called graphite, in PricePercentile.Band, because that is
   what the Work Item named it and what a reviewer will look for. What changed is
   the token it resolves to, and §12's standing rule is why it was checked at all:
   read the vendored source before relying on a Jelly name meaning what it
   sounds like. */
.gt-percentile-graphite { color: var(--jelly-color-foreground-muted); }

/* tabindex="0" is what makes the tooltip reachable without a pointer, so the
   ring has to be visible — a focusable element with no focus style is the defect
   that change would otherwise introduce. :focus-visible rather than :focus, so a
   pointer user who taps the mark does not get a ring they did not ask for. */
.gt-percentile:focus-visible {
  outline: 2px solid var(--jelly-color-border-focus);
  outline-offset: 2px;
}

/* One Branch's table within a Multiple Trip (GX-130) — air between it and the
   one before it, and its own heading styled like .gt-branch above so the two
   read as the same kind of name at different sizes. */
.gt-multi-branch + .gt-multi-branch {
  margin-top: 1.25rem;
}

.gt-multi-branch-heading {
  margin-bottom: .5rem;
  font-weight: 600;
}

/* The Items this branch cannot answer. A list rather than a second table: it has
   one column of names and one of reasons, and a table of two would draw
   grid lines around what is really a sentence each. */
.gt-missing {
  margin-top: 1rem;
}

.gt-missing-heading {
  margin-bottom: .25rem;
  font-weight: 600;
}

.gt-missing-list {
  margin: 0;
  padding-left: 1.25rem;
}

.gt-missing-reason {
  color: var(--jelly-color-foreground-muted);
}

/* The banner that says the results tables are showing fewer columns (GX-165).

   Rendered at EVERY width and revealed by a media query, rather than decided by
   the server. What hides the columns is a media query, and a server guessing the
   viewport from a user-agent string could say the opposite of what the
   stylesheet did. One condition, written once, so the banner and the columns
   cannot disagree.

   The second sentence is a span inside the first and is revealed only in
   portrait: "rotate to see them" is advice a landscape phone has already taken.
   The leading space lives INSIDE the span, so the landscape wording does not
   keep a trailing one. */
.gt-narrow-notice,
.gt-rotate-hint {
  display: none;
}

/* Below `md` a results table SHEDS COLUMNS rather than stacking (GX-165).

   Until this Work Item each row became its own block on a phone, every cell
   printing its column heading from data-label — the treatment the other three
   tables still take (GX-41, GX-60, GX-74). Five columns of names and money do
   not fit in 390px, and horizontal scrolling is the answer this app does not
   use: an overflow box would clip Jelly's canvas bleed into a scrollbar over
   decoration (GX-69). Stacking was the other way out, and it cost five lines per
   row — a result holding ten Products became fifty lines to scroll past.

   So this Work Item takes the opposite trade: keep the table a table, side by
   side, and drop the two columns a Shopper reads LAST. Total is what the till
   charges and stays. Comparison is the rate behind it, and the badge column is
   the small print on how the price was reached; both are a rotation or a wider
   window away, which is what the banner above the results says.

   A DELIBERATE EXCEPTION to the rule §11 states for the Shopping List table,
   that no column is hidden on the screen with the least room. That table holds a
   control in every cell and nothing in it is derived from anything else. These
   two columns are.

   data-label outlives the ::before rules that used to print it, and is not a
   leftover: it is the only thing telling the Comparison cell from the Total
   cell, since both wear .gt-price-cell. The rule below reads it. */
@media (max-width: 767.98px) {
  .gt-table > thead > tr > th.gt-badge-col,
  .gt-table > thead > tr > th.gt-price-col,
  .gt-table > tbody > tr > td.gt-badge-cell,
  .gt-table > tbody > tr > td[data-label="Comparison"] {
    display: none;
  }

  /* What is left is 3.5rem and 4.5rem of fixed columns and the remainder for
     Product — about 11rem at 390px. Bootstrap spends .5rem either side of every
     cell, which is 3rem of that remainder, so the gutters are halved here and
     the outer two dropped: three columns do not need telling apart as five did,
     and the room goes to the Product name. */
  .gt-table > thead > tr > th,
  .gt-table > tbody > tr > td {
    padding-inline: .25rem;
  }

  .gt-table > thead > tr > th:first-child,
  .gt-table > tbody > tr > td:first-child {
    padding-inline-start: 0;
  }

  .gt-table > thead > tr > th:last-child,
  .gt-table > tbody > tr > td:last-child {
    padding-inline-end: 0;
  }

  .gt-narrow-notice {
    display: block;
  }
}

@media (max-width: 767.98px) and (orientation: portrait) {
  .gt-rotate-hint {
    display: inline;
  }
}

/* ------------------------------------------------------------------------- */
/* The barcode scanning view (GX-180).                                        */
/*                                                                            */
/* Laid out for a phone held in one hand: the video takes what it can get,     */
/* and the hint and the Done button sit under it in that order because the     */
/* Work Item puts the message "above button". Nothing here paints a literal    */
/* colour — ColorSchemeTest forbids it and the tokens cover every case.        */

.mp-scan {
  display: flex;
  flex-direction: column;
  gap: .75rem;
  /* Contains this column's own overflow (GX-180 review). The dialog surface
     reported scrollWidth and scrollHeight 18px beyond its client box on a
     desktop — real scrollbars, and it genuinely scrolled — while NO element
     inside it overflowed: measured, element by element, including into every
     shadow root. The 18px came from this flex column, and containing it here
     is what removes both bars. Measured after the fix: scrollWidth equals
     clientWidth and scrollHeight equals clientHeight.

     Not done on the dialog's own ::part(dialog), which was tried and does not
     help — and hiding ITS overflow would clip the Done button on a short
     window, where the dialog is supposed to scroll. This element has no height
     of its own, so the dialog can still scroll the whole column when it must. */
  max-width: 100%;
  overflow: hidden;
}

/* Done spans the view, which is the touch target a phone wants — and the
   reason its label looked left-aligned (GX-180 review). The HOST stretches to
   460px under `align-items: stretch`, but jelly-button's real <button> is
   `inline-flex` and stays its own 68px, sitting at the left of that host. So
   the label was never mis-aligned: the button simply was not full width.
   Its own content is already centred (`justify-content: center`, read from the
   component), so filling the host is the whole fix. `part="button"` is exposed
   — checked in the vendored source, per §12 — which is what makes this
   reachable at all. */
.mp-scan jelly-button::part(button) {
  width: 100%;
}

/* The feed. `contain` rather than `cover` so a barcode near the edge of the
   frame is not cropped out of the picture the Shopper is aiming — the decoder
   reads the video element's own pixels, not what CSS shows, but a Shopper aims
   using what they can see and cropping makes them aim wrong.

   A fixed aspect ratio stops the dialog resizing when the stream starts and
   the intrinsic size arrives, which otherwise moves the Done button under the
   Shopper's thumb at the exact moment they reach for it. */
.mp-scan-video {
  width: 100%;
  aspect-ratio: 4 / 3;
  max-height: 60vh;
  object-fit: contain;
  background-color: var(--jelly-color-background-default);
  border: 1px solid var(--jelly-color-border-default);
  border-radius: 12px;
}

.mp-scan-status {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: 0;
  color: var(--jelly-color-foreground-muted);
}

/* A refused camera, or none. Toned from the rose background the app already
   uses for a problem, rather than a colour of its own. */
.mp-scan-warning {
  margin: 0;
  padding: .5rem .75rem;
  border-radius: 12px;
  background-color: var(--jelly-color-background-rose);
  color: var(--jelly-color-foreground-default);
}

.mp-scan-hint {
  margin: 0;
  font-size: .85rem;
  color: var(--jelly-color-foreground-muted);
}
