/* Reusable pieces. Nothing here knows which page it is on. */

/* ---- buttons & pills ---- */
.btn {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 9px 16px; border-radius: var(--r-pill); border: 1px solid var(--line);
  background: var(--page); font-weight: 600; font-size: var(--fs-sm); cursor: pointer;
  transition: background .12s ease, border-color .12s ease;
  /* A pill is a fixed shape and its label is one line. Without this, "Sign in"
     broke across two lines inside a 56px header the moment the row got tight,
     and the button grew taller than the header that holds it. */
  white-space: nowrap;
}
.btn:hover { background: var(--surface); }
.btn-dark { background: var(--cta); color: var(--cta-ink); border-color: var(--cta); }
.btn-dark:hover { background: #000; }
.btn-round {
  width: 44px; height: 44px; padding: 0; justify-content: center;
  background: var(--cta); color: var(--cta-ink); border: 0; border-radius: 50%;
  font-size: 20px; cursor: pointer;
}
.btn-icon {
  /* `display` is load-bearing: this class is used on its own, without `.btn`,
     so without it the element stays inline — width, height and the centring
     below are all ignored and the glyph sits on the text baseline instead of
     in the middle of the circle. */
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0; border-radius: 50%;
  background: var(--page); border: 1px solid var(--line); font-size: 16px; cursor: pointer;
}
.pillbar { display: inline-flex; gap: var(--sp-1); background: var(--surface); padding: 4px; border-radius: var(--r-pill); }
.pillbar a, .pillbar button {
  padding: 6px 14px; border-radius: var(--r-pill); border: 0; background: transparent;
  font-size: var(--fs-sm); font-weight: 600; cursor: pointer; color: var(--ink-2);
}
.pillbar .is-active { background: var(--cta); color: var(--cta-ink); }

/* ---- badges ---- */
.badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 9px; border-radius: var(--r-pill);
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: -.01em;
}
.badge-sale  { background: var(--sale-bg);  color: var(--sale-ink); }
.badge-gap   { background: var(--watch-bg); color: var(--watch-ink); }
.badge-pop   { background: var(--pop-bg);   color: var(--pop-ink); }
.badge-ok    { background: var(--ok-bg);    color: var(--ok-ink); }
.badge-plain { background: var(--surface);  color: var(--ink-2); font-weight: 600; }

/* ---- product card ---- */
/* No frame. The tile's boundary is the tinted media panel below, and the text
   sits directly on the page — the shape every price-comparison site has settled
   on, because a grid of forty framed boxes reads as forty boxes before it reads
   as forty products.

   The card keeps `position: relative` and nothing else: it is the containing
   block for `.card-link::after`, which stretches the tile's link over the whole
   thing, and for `.card-tools`. Losing it detaches both and the entire tile
   stops being clickable. */
.card { position: relative; display: flex; flex-direction: column; }
/* `isolation` is load-bearing next to the `mix-blend-mode` below: without it
   the image blends against whatever shares its stacking context, which on a
   page with a sticky header and a sticky tray means the browser repaints far
   more than the tile on every scroll. Isolated, the blend group ends here. */
.card-media {
  position: relative; background: var(--surface); border-radius: var(--r-md);
  aspect-ratio: 1; display: grid; place-items: center; overflow: hidden;
  isolation: isolate; margin-bottom: var(--sp-3);
}
.card-media img {
  width: 100%; height: 100%; object-fit: contain; padding: 10%;
  mix-blend-mode: multiply; transition: transform .18s ease;
}
/* The only hover affordance left, and it belongs on the panel rather than the
   tile. A borderless card that lifts has nothing to lift — the shadow it used
   to cast was cast by the frame, and without one the tile just slides upward
   and leaves a gap. */
.card:hover .card-media { background: #ececee; }
.card:hover .card-media img { transform: scale(1.03); }
.card-media .badge { position: absolute; top: 8px; left: 8px; }

/* The tile's link, stretched over the whole card.
   The card is a <div> so that the Compare form can be a real <button>; this
   pseudo-element is what keeps the entire tile clickable anyway. */
.card-link { display: block; color: inherit; text-decoration: none; }
.card-link::after { content: ""; position: absolute; inset: 0; z-index: 0; }

/* Above the stretched link, or the link swallows the click — which it did, for
   as long as this sat inside `.card-media`.

   `z-index: 1` was not wrong so much as powerless there: the media panel
   isolates itself for its blend mode, so this ranked only against the badge
   beside it, while `.card-link::after` — a later sibling of the PANEL — painted
   over the panel entire. The control is a direct child of `.card` now, which
   creates no stacking context, so this competes with the overlay directly.

   Positioned against `.card` rather than the panel, which lands in the same
   place: the card has no padding, so its top-right corner IS the panel's. */
.card-tools { position: absolute; top: 8px; right: 8px; z-index: 2; }
.card-tools form { margin: 0; }

/* Sits over a photograph we do not control, so it carries its own translucent
   ground rather than trusting whatever is behind it. */
.card-fav {
  width: 32px; height: 32px; font-size: 15px;
  background: rgba(255, 255, 255, .86);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  border-color: transparent; color: var(--muted);
  transition: color .12s ease, background .12s ease, transform .12s ease;
}
.card:hover .card-fav { background: #fff; }
/* The site's action colour, not a heart-red. The glyph is a heart but the
   meaning is "do something with this product", and it is the same meaning the
   filled state carries — one colour for one idea. */
.card-fav:hover { color: var(--cta); transform: scale(1.08); }
/* Saved to my list.

   This used to mean "in the comparison tray", which asked one icon to carry
   two ideas: a heart reads as kept, not as queued for a table, and the tray
   already says what is in it along the bottom of every page. The fill is
   written by `list.js` from `localStorage` after the page arrives — the
   server cannot render it, because the server does not know what is saved. */
.card-fav.is-saved { background: var(--cta); color: var(--cta-ink); }
/* Filled, not just tinted: at 32px on a photograph, colour alone is not a
   state change anyone notices. */
.card-fav.is-saved .icon { fill: currentColor; }

/* Title is a link now; it must not look like one. */
.card-title { color: inherit; text-decoration: none; }

/* Nothing may add padding or a border here. `-webkit-line-clamp` clips at the
   PADDING box, not the content box, so padding-bottom on this element reveals
   the very line the clamp just hid — a third line bleeding through the rule
   below. The divider therefore lives on .card-prices instead. */
.card-title {
  font-size: var(--fs-sm); font-weight: 600; line-height: 1.35; min-height: 2.7em;
  margin-bottom: var(--sp-2); overflow-wrap: anywhere;
}

/* The clamp, restated here because the utility class alone never worked.

   The title carries `clamp-2`, whose `display: -webkit-box` is what makes
   `-webkit-line-clamp` do anything at all — and `.card-link { display: block }`
   overrode it. Same specificity, and components.css loads after base.css, so
   block won and no card title was ever capped: `min-height` reserved two lines
   while nothing limited the maximum, and a long name ran to four. Invisible
   while each tile was a bordered box; obvious in a borderless grid, where the
   prices in one row stopped lining up with each other.

   Two classes, so restoring it cannot be undone by a reordering the way one
   class was. */
.card-link.card-title {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The rule that used to separate the name from the number is gone with the
   frame — it was the line that read most like a border in a grid, and the
   title's fixed two-line block already does the separating.

   The padding stays, and stays HERE. `-webkit-line-clamp` clips at the padding
   box, so moving this onto `.card-title` to close the gap would reveal the very
   third line the clamp just hid.

   Baseline alignment sits the struck-through price on the live one's baseline
   instead of against its cap height. */
.card-prices {
  display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  padding-top: var(--sp-2);
}
.card-price { font-size: var(--fs-lg); font-weight: 800; letter-spacing: -.02em; }
.card-was { font-size: var(--fs-sm); }
.card-foot {
  display: flex; align-items: center; gap: 5px; flex-wrap: wrap;
  margin-top: auto; padding-top: var(--sp-2);
  color: var(--muted); font-size: var(--fs-xs);
}
/* How old the figures are. Only rendered past a couple of days, so its
   presence is itself the signal — it should read as a caveat, not a label. */
/* The separator is drawn, not typed: a literal "·" in the template would
   still render when the note beside it is absent. */
.card-age { opacity: .85; }
.card-age::before { content: "·"; margin-right: 5px; }

/* ---- rails: horizontal on mobile, grid on desktop ---- */
/* `--sp-4`, not `--sp-3`: the card lost its own padding along with its
   frame, so this gap is now the entire separation between one product and
   the next. At the old value two neighbouring photographs sat close enough
   to read as one wide image. */
.rail {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(168px, 1fr);
  gap: var(--sp-4); overflow-x: auto; scroll-snap-type: x proximity;
  padding-bottom: var(--sp-2); scrollbar-width: thin;
}
.rail > * { scroll-snap-align: start; }
@media (min-width: 900px) {
  .rail { grid-auto-flow: row; grid-template-columns: repeat(5, 1fr); overflow: visible; }
  .rail > *:nth-child(n+6) { display: none; }
}

/* A rail that keeps two rows instead of one.

   The base rail is a teaser on a page with somewhere else to go, so it shows
   five and hides the rest. The alternatives rail at the foot of a product page
   is the opposite case: the next click IS the section's purpose, and half of it
   folded away behind a scrollbar that desktop does not draw. The hide moves to
   the eleventh tile rather than being removed, so a longer list can never spill
   into a ragged third row.

   Same specificity as the rule above, so this has to sit after it. */
@media (min-width: 900px) {
  .rail-2 > *:nth-child(n+6) { display: block; }
  .rail-2 > *:nth-child(n+11) { display: none; }
}

/* The rule the rail was built by, tucked under its heading. Negative top
   margin because `.section-head` already spends `--sp-4` below itself, and the
   note belongs to the heading rather than to the tiles. */
.related-note { margin: calc(-1 * var(--sp-3)) 0 var(--sp-4); }

.section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-4); margin: var(--sp-7) 0 var(--sp-4); flex-wrap: wrap;
}
.section-head h1, .section-head h2 { min-width: 0; }
.section-head a { font-size: var(--fs-sm); font-weight: 600; color: var(--link); }

/* ---- category strip ---- */
.cats {
  display: grid; grid-auto-flow: column; gap: var(--sp-2);
  overflow-x: auto; padding: var(--sp-5) 0 var(--sp-2); scrollbar-width: none;
}
.cats::-webkit-scrollbar { display: none; }
.cat {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  min-width: 76px; padding: var(--sp-2); border-radius: var(--r-md);
  font-size: var(--fs-xs); font-weight: 600; color: var(--ink-2); text-align: center;
}
.cat:hover { background: var(--surface); color: var(--ink); }
.cat .icon { width: 26px; height: 26px; stroke-width: 1.5; }

/* ---- trust chips ---- */
.chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.chip {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--fs-xs); color: var(--muted);
}
.chip .icon { width: 13px; height: 13px; }

/* ---- empty state ---- */
.empty {
  border: 1px dashed var(--line); border-radius: var(--r-md);
  padding: var(--sp-6); text-align: center; color: var(--muted);
}

/* Active category in the browse strip. */
.cat.is-current { background: var(--cta); color: var(--cta-ink); }
.cat.is-current:hover { background: var(--cta); color: var(--cta-ink); }


/* Search fell back from a category filter to plain word matching. */
.relaxed {
  border: 1px solid var(--line); border-left: 3px solid var(--cta);
  border-radius: var(--r-md); padding: var(--sp-3);
  margin-bottom: var(--sp-4);
  font-size: var(--fs-sm); color: var(--muted);
}

/* ---- header search ----
   Expands on click. Written no-JS-first: the rules below assume the input is
   visible and the toggle is not, and only the `.js` class (set by the inline
   snippet in <head>, before first paint, so there is no flash) swaps them
   round. Search on a price-comparison site must not need a script. */
.navsearch {
  position: relative; display: flex; align-items: center;
  /* Flex items default to min-width:auto, which refuses to shrink below the
     content's intrinsic size — that is what pushed the open field off the
     right edge on a narrow screen. */
  min-width: 0;
}
.navsearch-toggle { display: none; }

.navsearch-field {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 260px; max-width: 100%;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-pill); padding: 8px 14px; color: var(--ink);
}
.navsearch-field:focus-within { border-color: var(--accent-line, #c9bff0); background: var(--page); }
.navsearch-field .icon { color: var(--muted); width: 16px; height: 16px; flex: none; }
.navsearch-field input {
  flex: 1; min-width: 0; border: 0; outline: 0; background: transparent;
  font-size: var(--fs-sm); color: inherit;
}
.navsearch-clear {
  border: 0; background: transparent; cursor: pointer; padding: 0 2px;
  font-size: 18px; line-height: 1; color: var(--muted);
}
.navsearch-clear:hover { color: var(--ink); }

/* With JavaScript the field starts collapsed behind the toggle. Width, not
   `display`, so the open/close can be animated and the input stays in the
   DOM for the `/` shortcut to focus. */
.js .navsearch-toggle { display: inline-flex; }
.js .navsearch-field {
  width: 0; padding-inline: 0; border-color: transparent; background: transparent;
  opacity: 0; pointer-events: none; overflow: hidden;
  transition: width .18s ease, opacity .14s ease, padding .18s ease;
}
.js .navsearch.is-open .navsearch-toggle { display: none; }
.js .navsearch.is-open .navsearch-field {
  width: 260px; padding-inline: 14px; opacity: 1; pointer-events: auto;
  border-color: var(--line); background: var(--page);
}

@media (max-width: 720px) {
  /* Narrow screens have no room for a 260px field beside the wordmark, so the
     open field grows in flow and the wordmark drops to just its "BD" pill —
     which is still a link home.

     Deliberately NOT absolutely positioned across the row: that covered the
     brand completely, and because the field refuses to collapse while it holds
     a term, a results page left no way back to the rest of the header. */
  .navsearch-field { width: 100%; }
  .js .navsearch.is-open { flex: 1; }
  .js .navsearch.is-open .navsearch-field { width: 100%; flex: 1; min-width: 0; }
  /* The spacer's `margin-left:auto` competes with the field for the row's
     free space, leaving it too narrow to type in. Dropping it while the
     search is open hands that space to the field instead. */
  .nav:has(.navsearch.is-open) .nav-spacer { display: none; }
  .nav:has(.navsearch.is-open) .brand-name { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .js .navsearch-field { transition: none; }
}

/* ---- footer ----
   Four columns that collapse to two, then one. The brand column is wider
   because it carries a sentence rather than a list, and the category column
   is wider because at desk widths its list runs in two ranks — eight short
   links stacked in one rank leave the column twice the height of its
   neighbours and the row bottom-heavy. */
.foot-grid {
  display: grid; gap: var(--sp-6) var(--sp-5);
  grid-template-columns: 1fr;
  padding-bottom: var(--sp-6);
}
@media (min-width: 560px) { .foot-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 960px) {
  .foot-grid { grid-template-columns: 1.5fr 1fr 1.6fr 1fr; gap: var(--sp-6); }
}

.foot-brand .brand { margin-bottom: var(--sp-3); }
.foot-tagline { max-width: 32ch; line-height: 1.6; }

.foot-h {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-2); margin-bottom: var(--sp-4);
}
.foot-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
/* Two ranks of links, but only where there is width for them. Flowed as
   columns rather than a grid: a two-word category that wraps would otherwise
   set the height of its whole grid row and leave a hole beside every short
   name in it. */
@media (min-width: 1000px) {
  .foot-cats ul { display: block; columns: 2; column-gap: var(--sp-5); }
  .foot-cats li { break-inside: avoid; margin-bottom: var(--sp-2); }
}
.foot-col li { min-width: 0; }
.foot-col a {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  color: var(--muted); line-height: 1.4; transition: color .15s ease;
}
.foot-col a:hover { color: var(--ink); }

/* The category mark sits in a tinted tile so it reads as a marker beside the
   link rather than a second piece of text competing with it — the same tile
   the category chips and card media use. */
.foot-ico {
  display: grid; place-items: center; flex: none;
  width: 24px; height: 24px; border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink-2); font-size: 13px;
  transition: color .15s ease, background-color .15s ease;
}
.foot-col a:hover .foot-ico { color: var(--link); background: var(--pop-bg); }

/* The outbound arrow is decorative and trails the name, so it never grows
   with a hover and never takes the link's colour weight. */
.foot-ext { font-size: .8em; opacity: .55; flex: none; }
.foot-col a:hover .foot-ext { opacity: 1; }

/* The two standing notices, in the site's panel idiom — a tint on the page's
   own ground, no border, the same shape .alert-product uses — because they are
   read matter, not a card to act on. Two columns at desk widths so a 1280px
   footer is not one narrow ribbon of text hugging the left edge. */
.foot-note {
  display: grid; gap: var(--sp-4) var(--sp-6);
  padding: var(--sp-4) var(--sp-5);
  background: var(--surface);
  border-radius: var(--r-md);
}
@media (min-width: 900px) { .foot-note { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.foot-note-item {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-3); align-items: start;
}
.foot-note-item p { max-width: 56ch; line-height: 1.6; }
.foot-note b { color: var(--ink-2); font-weight: 600; }
/* Nudged onto the first line's baseline rather than its box top. */
.foot-note .icon { margin-top: .18em; font-size: 15px; color: var(--ink-2); }

.foot-base {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  justify-content: space-between; align-items: center;
  margin-top: var(--sp-5); padding-top: var(--sp-4);
  border-top: 1px solid var(--line); font-size: var(--fs-xs);
}
.foot-base a:hover { color: var(--ink); }
.foot-legal { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-4); }
.foot-made { display: inline-flex; align-items: center; gap: var(--sp-1); }

/* An image the browser could not fetch. Marked by nav.js; the tile's own
   ground shows through, which reads as "no picture" rather than "broken". */
img.is-broken { display: none; }

/* ---- comparison tray ----------------------------------------------------
   Docked to the bottom of every page while something is in it, because the
   selection is built across pages: a shopper adds a laptop from a grid, keeps
   browsing, and has to be able to see what they are carrying. Rendered only
   when non-empty, so it costs nothing on the pages it is not needed. */
/* The bottom stack: the toast, then the tray. Always in the DOM so an
   out-of-band swap has a target; the sticky lives here rather than on the bar
   because a sticky box can only travel inside its own containing block, and a
   bar wrapped in a div its own height would have nowhere to go. With neither a
   toast nor a tray it is a zero-height nothing. */
.dock { position: sticky; bottom: 0; z-index: 30; }
/* A sticky bar is in the flow at the END of the document, so while the page
   scrolls it is painted over whatever comes before it — including the bottom
   row of a grid, whose hearts then cannot be pressed at all. The page gives
   the bar its own height back while there is a bar to give it to. */
body:has(.compare-bar) main { padding-bottom: 4.5rem; }
.compare-bar {
  background: var(--page); border-top: 1px solid var(--line);
  box-shadow: 0 -6px 24px rgba(15, 14, 17, .08);
}
.compare-bar-inner {
  display: flex; align-items: center; gap: var(--sp-4);
  padding-top: var(--sp-3); padding-bottom: var(--sp-3); flex-wrap: wrap;
}
.compare-bar-items {
  display: flex; align-items: center; gap: var(--sp-2);
  list-style: none; margin: 0; padding: 0; flex: 1 1 auto; flex-wrap: wrap;
}
/* On a phone the four names stacked, and the dock grew to nearly half the
   window — a tray that hides the products it is there to help you choose
   between. One row that scrolls instead: the count on the Compare button says
   how many there are, so nothing is concealed by it. */
@media (max-width: 560px) {
  .compare-bar-items {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    flex-basis: 100%;
  }
  .compare-bar-item { flex: none; max-width: 11rem; }
  .compare-bar-inner { gap: var(--sp-2); }
  .compare-bar-actions { flex: 1 1 auto; }
  .compare-bar-actions .btn-dark { flex: 1 1 auto; justify-content: center; }
}
.compare-bar-item {
  display: flex; align-items: center; gap: 6px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 4px 4px 4px 10px; background: var(--surface);
  font-size: var(--fs-xs); max-width: 15rem;
}
.compare-bar-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.compare-bar-item form { margin: 0; }
.compare-bar-x {
  border: 0; background: none; cursor: pointer; color: var(--muted);
  font-size: 16px; line-height: 1; padding: 2px 4px;
}
.compare-bar-x:hover { color: var(--sale-ink); }
.compare-bar-actions { display: flex; align-items: center; gap: var(--sp-2); }
.compare-bar-actions form { margin: 0; }

/* ---- flash messages ----
   The strip is on every page whether or not it holds anything, so a refusal
   arriving over htmx has somewhere to land. `:empty` is what keeps that from
   costing a blank gap above the content. */
.flash { list-style: none; margin: 0 0 var(--sp-4); padding: 0; }
.flash:empty { display: none; }
.flash-item {
  border: 1px solid var(--line); border-left: 3px solid var(--cta);
  border-radius: var(--r-sm); background: var(--surface);
  padding: var(--sp-3) var(--sp-4); font-size: var(--fs-sm);
}

/* ---- toast --------------------------------------------------------------
   Where a refusal is said when the answer arrives over htmx.

   `messages` writes to the flash strip at the top of `<main>`. That is right
   for a page reload and wrong for a swap: a Compare button pressed halfway
   down a grid put its explanation a thousand pixels above the window, so a
   decline read as a dead button. This sits in the dock, in view, above the
   tray it is talking about.

   The pill itself is `.toast` in pages.css, which the price-alert confirmation
   already used. What is here is where a toast goes and what this one carries;
   there is one toast in this codebase, in two places. */
/* A row in the dock, not a floating box with a bottom offset of its own —
   see base.html. Centred over the bar it sits on. */
.toast-host {
  width: min(34rem, calc(100% - 2rem));
  margin: 0 auto var(--sp-3);
}
/* The host is permanent so an out-of-band swap has a target; with nothing in
   it, it must not cover the page with an invisible box.

   `:has()`, not `:empty`. The strip beside it uses `:empty` and needs its
   template kept on one line forever, because `:empty` counts whitespace and a
   swap that returns "\n" leaves a box that is visually empty and structurally
   not — the same trap base.html documents for `#flash`. Asking for the toast
   itself cannot be broken by how a template happens to be formatted. */
.toast-host:not(:has(.toast)) { display: none; }
/* Pinned to the middle of the window: the price-alert confirmation, which has
   no dock of its own to sit in. */
.toast-float {
  position: fixed; left: 50%; bottom: var(--sp-5); transform: translateX(-50%);
  z-index: 50; max-width: min(34rem, calc(100% - 2rem));
}
/* In the dock, this one carries a sentence and sometimes a button, so it is a
   rounded box rather than the pill a three-word confirmation wears. */
.toast-host .toast { border-radius: var(--r-md); }
.toast-text { margin: 0; flex: 1 1 auto; min-width: 0; }
/* The form is not a row of its own. */
.toast form { margin: 0; display: contents; }
.toast-action {
  flex: none; white-space: nowrap;
  background: var(--page); color: var(--ink); border-color: transparent;
}
/* Dismissal stays beside the message at every width. It used to be the flex
   item that got pushed onto the second row on a phone, where a lone × under a
   sentence reads like a stray character. */
.toast-x {
  flex: none; order: 1; border: 0; background: none; color: inherit;
  cursor: pointer; font-size: 1.25rem; line-height: 1;
  padding: 0 var(--sp-1); opacity: .7;
}
.toast-x:hover { opacity: 1; }
/* Too narrow for a sentence and a button side by side: the button takes its
   own full-width row under the message, which is also the easier tap. */
@media (max-width: 30rem) {
  .toast { flex-wrap: wrap; }
  .toast-action { order: 2; flex: 1 1 100%; justify-content: center; }
}

/* ---- my list ------------------------------------------------------------
   Saved products, held in the visitor's own browser. See `list.js` for why
   there is no table behind this. */

/* Controls that write `localStorage` and have no server-side fallback: the
   Save row in the actions menu, the Save button on a product page. Shown only
   once the document says scripting is on — the class is set before first
   paint, so nothing flickers. A dead control is worse than an absent one. */
html:not(.js) [data-needs-js] { display: none; }

.nav-list {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  flex: none; font-weight: 500;
}
.nav-list .icon { width: 16px; height: 16px; }
.nav-list:hover { color: var(--link); }
/* The label goes, the control stays. `.nav-links` is hidden entirely at this
   width; the saved list is the one thing in the header that is the visitor's
   own, and a phone is where most of this audience is. */
@media (max-width: 720px) {
  .nav-list-label { display: none; }
}
/* Written by `list.js`, hidden until there is a number to show. `hidden` is
   set in the markup and removed by the script, so the badge cannot flash a
   stale count on a page that arrives before storage is read — `[hidden]` in
   base.css is what enforces that against this rule's own display. */
.nav-count {
  min-width: 1.25rem; padding: 0 .3rem; border-radius: var(--r-pill);
  background: var(--cta); color: var(--cta-ink);
  font-size: var(--fs-xs); font-weight: 700; line-height: 1.35; text-align: center;
}

/* The page opens with breadcrumbs, so the heading does not also want
   `.section-head`'s --sp-7 of air above it: that space is for a section
   starting mid-page, not for the first thing under a crumb trail. Same shape
   and same reasoning as `.cmp-head` on the comparison. */
.list-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); flex-wrap: wrap; margin-bottom: var(--sp-4);
}

/* A saved tile is the ordinary card plus a way out. The wrapper is the grid
   item, so the card keeps its own dimensions and the grid keeps its tracks. */
.listed { display: flex; flex-direction: column; gap: var(--sp-2); }
.listed .card { flex: 1; }
.listed-remove {
  align-self: start; border: 0; background: none; padding: 0;
  font: inherit; font-size: var(--fs-xs); font-weight: 600;
  color: var(--muted); cursor: pointer; text-decoration: underline;
}
.listed-remove:hover { color: var(--cta); }
