
/* Native cross-document View Transitions (2026-09-05) -- one line turns on
   a browser-native fade between full page navigations (category to
   category, etc.), no router, no JS, no bundle-size cost. Chrome/Edge
   support it today and cross-fade automatically once opted in; every other
   browser just ignores the unknown at-rule and does its normal navigation
   -- there is no broken or degraded state to handle. Gated behind
   prefers-reduced-motion the same way every other animation on this site
   is, since @view-transition can sit inside a media query like any other
   at-rule. */
@media not (prefers-reduced-motion: reduce){
  @view-transition{navigation:auto}
}
:root{
  --dur-fast:.12s; --dur-base:.15s; --dur-med:.18s;
  /* Heading/title scale (2026-09-05): pulled every ad-hoc heading/title
     font-size on the site into named tokens after the design audit counted
     9 undocumented sizes doing heading work, two of them ("section title"
     at 1.4rem and prose h2 at 1.32rem) only 6% apart -- the same role
     declared twice, independently, by drift rather than intent. Values kept
     identical to what already shipped (this centralizes the scale, it
     doesn't redesign it) except that one pair, which now shares --fs-h2
     since both are literally a section heading. */
  --fs-display:clamp(2rem,5vw,2.9rem); --fs-h2:1.32rem; --fs-h3:1.05rem;
  --fs-title-lg:1.5rem; --fs-title-md:1.1rem; --fs-title-sm:.98rem; --fs-title-xs:.85rem;
  --fs-lede:1.15rem;
  --signal:#D93A12; --signal-dark:#B8331A; --gate:#00a863; --gate-soft:#5C9B81;
  /* White text on full-saturation --gate only clears 3.1:1 (light) / 2.4:1
     (dark, see the theme override below) with white text -- both fail
     WCAG AA's 4.5:1. This shade clears 5.4:1 in both themes; verified by
     hand against the sRGB relative-luminance formula, same method as the
     --signal-dark contrast fix earlier this session (2026-08-24). */
  --gate-dark:#0B7A4C;
  /* Amazon Picks' gold/orange (picked to stay clear of --signal and --gate,
     see SOURCE_META comment above) was three repeated hex literals with no
     name. Promoted to tokens here. White text on --picks/--picks-light
     measures 2.6:1/2.2:1 against WCAG's 4.5:1 floor -- every place these
     paint a background now sets --ink for the label instead of white. */
  --picks:#E8890C; --picks-dark:#C97108; --picks-light:#F0A500;
  --paper:#FFFFFF; --paper-2:#F9F9F7; --paper-3:#EFEFEA; --card:#FFFFFF;
  --ink:#1a1a1a; --ink-soft:#3B3B36; --muted:#666; --rule:#E0E0E0;
  /* Shape scale (2026-09-03 taste-skill audit): consolidates ~11 stray
     border-radius values (4-24px) that drifted apart across sessions --
     .card was 16px, .pick was 14px, .compare-card was 12px, .td-card was
     20px, all doing the same "product/stat card" job but reading as
     slightly different rounding depending on which page you're on. Small =
     controls/dropdowns/buttons/badges, medium = cards and card-sized media,
     large = page-level section containers. --r-pill (999px) already existed
     inline everywhere and stays a literal, it was never inconsistent. */
  --r-sm:6px; --r-md:12px; --r-lg:20px;
  --display:-apple-system,BlinkMacSystemFont,Roboto,"Segoe UI","Helvetica Neue",Arial,sans-serif;
  --mono:ui-monospace,"SF Mono",SFMono-Regular,"Cascadia Mono",Consolas,"Liberation Mono",monospace;
  /* Wordmark only ("HALF"/"NOTHING" in the nav logo and hero H1) -- the rest
     of the site keeps --display's native-OS stack on purpose (this is a
     dense, task-first deal grid; system fonts are the right call there, see
     operate.md). The wordmark is the one place brand identity is worth a
     deliberate face instead of whatever the visitor's OS happens to ship --
     self-hosted below so it never depends on a third-party font CDN the
     site's CSP doesn't otherwise allow. */
  --brand-font:"Archivo",var(--display);
  /* Editorial accent for the blog's own intro copy (the .lede under each
     page's H1) -- a second deliberate face, same self-hosted/no-CDN
     reasoning as Archivo above, picked to match the serif italic subtitle
     style already used in the hand-designed blog cover art (e.g.
     christmas-catalog-2026.webp's "Every Gift at 50% Off or More"), so the
     live page reads as one voice with its own cover images instead of the
     cover doing all the editorial work alone. Scoped to blog ledes only
     (.blog-intro .lede) -- the shared .lede class is also used on About/
     Privacy/the deals index, which stay on --display on purpose (Che,
     2026-09-05: only the blog gets the editorial treatment). */
  --editorial-font:"Playfair Display",Georgia,serif;
}
@font-face{
  font-family:"Archivo";font-style:normal;font-weight:900;font-display:swap;
  src:url("/assets/fonts/archivo-900-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,
                U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,
                U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:"Playfair Display";font-style:italic;font-weight:600;font-display:swap;
  src:url("/assets/fonts/playfair-display-italic-600.woff2") format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,
                U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,
                U+2212,U+2215,U+FEFF,U+FFFD;
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0;background:var(--paper-2);color:var(--ink);
     font:400 16px/1.55 var(--display);font-synthesis-weight:none}
.wrap{max-width:1180px;margin:0 auto;padding:0 28px}

/* ---- 2026-08-10 Tactile Brutalism redesign: sharp corners, thick solid
   borders (2-4px), hard offset shadows (no blur), dense grid layout, scaled-down
   badges and icons per DealNews measurements. Real urgency from discount% and
   freshness signals, not fabricated timers. All data-backed, all real. ---- */

/* ---- sticky top nav: clean and minimal ---- */
.topnav{position:sticky;top:0;z-index:20;background:var(--card);border-bottom:1px solid var(--rule)}
.topnav .inner{max-width:1200px;margin:0 auto;padding:14px 28px;display:flex;align-items:center;gap:32px}
.topnav .logo{display:flex;align-items:center;gap:9px;text-decoration:none;color:var(--ink);flex-shrink:0}
.logomark{width:23px;height:23px;color:var(--signal);flex-shrink:0;display:block}
.lg-half{font-family:var(--brand-font);font-weight:900;letter-spacing:.02em;font-size:15px}
.lg-or{font:italic 500 11px/1 var(--display);color:var(--muted)}
.lg-nothing{font-family:var(--brand-font);font-weight:900;letter-spacing:.02em;font-size:15px;color:var(--muted)}
@supports(-webkit-text-stroke:1px red){
  .lg-nothing{color:transparent;-webkit-text-stroke:1.1px var(--ink)}
}
.topnav .herosearch{flex:1 1 auto;min-width:0;max-width:430px;margin:0}
.topnav .herosearch input{padding:10px 0;font-size:14px}
.topnav .hero-hint{font-size:14px;left:44px}
.topnav .links{display:flex;gap:24px;font-size:13px;font-weight:600;flex-shrink:0}
.topnav .links a{color:var(--muted);text-decoration:none}
.topnav .links a:hover{color:var(--ink)}
.topnav .links a[aria-current]{color:var(--signal);font-weight:700}
/* Theme toggle (2026-09-05) -- dark mode only followed the OS preference
   before this; most visitors never knew the site had one. Same hover:hover/
   :active split as every other icon button on the site (navBtn/megaBtn/
   over-menu/catFilterBtn). */
.theme-toggle{width:38px;height:38px;flex-shrink:0;border-radius:50%;
              background:var(--card);border:1px solid var(--rule);color:var(--ink);
              display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;
              transition:border-color var(--dur-base) ease,color var(--dur-base) ease}
.theme-toggle svg{width:18px;height:18px}
.theme-toggle .theme-icon-moon{display:none}
.theme-toggle:active{border-color:var(--signal);color:var(--signal)}
@media(hover:hover){.theme-toggle:hover{border-color:var(--signal);color:var(--signal)}}
@media(prefers-color-scheme:dark){
  :root:not([data-theme="light"]) .theme-toggle .theme-icon-sun{display:none}
  :root:not([data-theme="light"]) .theme-toggle .theme-icon-moon{display:block}
}
:root[data-theme="dark"] .theme-toggle .theme-icon-sun{display:none}
:root[data-theme="dark"] .theme-toggle .theme-icon-moon{display:block}
/* ---- #navBtn: the <=640px hamburger toggle for .links above. Hidden at
   desktop widths; .links itself flips from a horizontal row to a full-width
   dropdown panel exactly where it used to just disappear. ---- */
#navBtn{display:none;align-items:center;justify-content:center;width:44px;height:44px;
        flex-shrink:0;background:var(--card);border:1px solid var(--rule);border-radius:var(--r-sm);
        color:var(--ink);cursor:pointer;padding:0}
/* hover:hover excludes touch: a bare :hover rule on a click-toggle trigger
   makes iOS Safari/WebKit treat the first tap as a hover-simulation and only
   fire the real click on a second tap -- confirmed on a real phone 2026-08-25
   (see the @media(hover:hover)-gated rules in this file). */
@media(hover:hover){#navBtn:hover{border-color:var(--signal);color:var(--signal)}}
/* :active is unconditional (not hover:hover-gated): it fires immediately on
   touchstart with no click delay, so tap still gets the same red flash the
   old bare :hover rule gave it -- gating :hover alone removed that feedback
   on touch entirely (Che caught this on his phone 2026-08-25). */
#navBtn:active{border-color:var(--signal);color:var(--signal)}
#navBtn svg{width:20px;height:20px}
@media(max-width:640px){
  #navBtn{display:inline-flex}
  .topnav .links{display:flex;position:absolute;top:100%;left:0;right:0;z-index:50;
                 flex-direction:column;gap:0;background:var(--card);
                 border-bottom:1px solid var(--rule);box-shadow:0 10px 30px rgba(0,0,0,.16);
                 opacity:0;visibility:hidden;transform:translateY(-4px);
                 transition:opacity var(--dur-fast) ease,transform var(--dur-fast) ease,visibility var(--dur-fast)}
  .topnav .links.open{opacity:1;visibility:visible;transform:none}
  .topnav .links a{padding:14px 28px;border-top:1px solid var(--rule);font-size:15px}
  .topnav .links a:first-child{border-top:none}
}
/* Phone: the search moved into the nav, and at 375px the full wordmark plus a
   usable field do not both fit (measured: logo 172px + padding left 24px of
   375). The mark alone still identifies the site, and the wordmark is repeated
   in the hero directly below anyway. */
@media(max-width:560px){
  .topnav .inner{gap:12px}
  .lg-half,.lg-or,.lg-nothing{display:none}
  /* icon-only logo needs a touch target >= 24x24; the mark itself is 23x23 */
  .topnav .logo{padding:2px}
  /* 44px minimum on the two controls a thumb actually aims at */
  .topnav .herosearch{max-width:none}
  .topnav .herosearch input{padding:13px 0}
}

/* ---- hero: compact. Cut down 2026-08-10 from a full-screen brand statement:
   the visitor already knows the site from the nav logo and the tab title, and
   a deal site is judged on how fast a product appears. Wordmark ~1/3 its old
   size, search promoted in, stats reduced to one thin line. ---- */
/* overflow:hidden here (dropped 2026-08-10) was clipping the Categories
   dropdown -- it now lives inside .mast, next to the search bar, and its
   open panel is ~550px tall, far past the header's own height. The ghost
   mark below doesn't actually need the clip: it's inset a fixed 20px from
   the right edge (can't overflow horizontally) and hidden outright on
   mobile, so nothing was relying on this. */
/* The banner was flat white with a grey rule until 2026-08-13, which made the
   whole top of the site read as black-and-white (Che: "come on you can do
   better"). The colour is put in as a bloom weighted to the right, not as a
   flat fill: the wordmark and tagline live in the left third, so that third
   stays on plain --card and keeps its full ink-on-white contrast, while the
   empty right half of the band (which carried nothing but the ghost mark)
   does the work of looking alive. Three radial washes rather than one
   gradient so the hues meet at soft edges instead of a visible seam. */
.mast{color:var(--ink);padding:44px 0 40px;position:relative;border-bottom:0;
      background:
        radial-gradient(900px 340px at 86% 14%, rgba(217,58,18,.26), transparent 62%),
        radial-gradient(720px 320px at 62% 104%, rgba(240,165,0,.32), transparent 60%),
        radial-gradient(700px 320px at 104% 86%, rgba(0,168,99,.24), transparent 62%),
        linear-gradient(100deg, var(--card) 0%, var(--card) 32%, #FFF2E4 68%, #FFE6D2 100%)}
/* Same gradient hairline that closes the Amazon Picks band, so the two
   coloured edges on the page read as one motif rather than two accidents. */
.mast::after{content:"";position:absolute;left:0;right:0;bottom:0;height:3px;
             background:linear-gradient(90deg,var(--signal),var(--picks) 42%,var(--picks-light) 62%,var(--gate))}
/* Sits at the content column's right edge -- the x where the Top Deals
   carousel ends -- which is Che's call (2026-08-13) and pure arithmetic off
   .wrap's own max-width and padding, so the JS that used to measure the nav's
   "Deals" link and place this by hand is gone. */
.mast-ghost{position:absolute;top:50%;transform:translateY(-48%);
            right:max(28px, calc((100% - 1180px) / 2 + 28px));
            font:900 clamp(160px,16vw,230px)/1 Georgia,serif;
            color:rgba(0,168,99,.1);
            pointer-events:none;user-select:none;z-index:0}
/* Gradient-filled rather than outlined: at this size an outline reads as a
   stray hairline, a fill reads as brand colour. Kept low-alpha so it stays
   behind the stats line it overlaps. */
@supports((-webkit-background-clip:text) or (background-clip:text)){
  .mast-ghost{color:transparent;-webkit-text-stroke:0;
              background:linear-gradient(150deg,var(--signal),var(--picks-light) 55%,var(--gate));
              -webkit-background-clip:text;background-clip:text;opacity:.3}
}
@media(max-width:760px){.mast-ghost{display:none}}
.brand{font-weight:800;letter-spacing:-.02em;line-height:.95;position:relative;
       font-size:clamp(2rem,4.8vw,3.1rem);margin:0;color:var(--ink)}
.brand .half{font-family:var(--brand-font);font-weight:900;color:var(--signal)}
.brand-or{font-style:italic;font-weight:400;font-size:max(.42em,11px);color:var(--muted);
          vertical-align:.35em;margin:0 .15em}
.brand-nothing{font-family:var(--brand-font);font-weight:900;color:var(--muted)}
@supports(-webkit-text-stroke:2px red){
  .brand-nothing{color:transparent;-webkit-text-stroke:1.6px var(--ink)}
}
.rule-line{font-size:1.15rem;font-weight:400;margin:10px 0 0;color:var(--muted);
           position:relative;max-width:640px;line-height:1.5}
/* Homepage-only <h1> kicker: the wordmark stays exactly as it is, this is a
   second, smaller line appended inside the same <h1> so a real, visible
   value-prop phrase sits in the page's one heading -- index.html only, see
   page_shell()'s h1_kicker param. No new type step: Body weight/size (same
   as .rule-line right above it) on the native stack, per DESIGN.md's One
   Wordmark Font Rule (Archivo stays in exactly the two places it's already
   in). .pct reuses the Numeric/Label weight+color, already reserved for
   "the discount percentage, the highest-attention numeric signal" -- this
   is that same number. */
.kw-sub{display:block;font-weight:400;color:var(--ink-soft);margin-top:6px;
        font-size:1.15rem;line-height:1.5}
.kw-sub .pct{color:var(--signal);font-weight:800}

/* search, with the rotating hint layered over a transparent input */
.herosearch{position:relative;display:flex;align-items:center;gap:10px;
            margin:16px 0 0;max-width:560px;background:var(--paper-2);
            border:1px solid var(--rule);border-radius:999px;padding:0 18px;
            transition:border-color var(--dur-base) ease,box-shadow var(--dur-base) ease}
.herosearch:focus-within{border-color:var(--signal);box-shadow:0 0 0 3px rgba(217,58,18,.12)}
.herosearch>svg{width:18px;height:18px;stroke:var(--muted);flex-shrink:0}
.herosearch input{flex:1;min-width:0;background:transparent;border:0;outline:none;
                  font:400 15px/1 var(--display);color:var(--ink);padding:15px 0;
                  position:relative;z-index:1}
.hero-hint{position:absolute;left:46px;top:50%;transform:translateY(-50%);
           pointer-events:none;color:var(--muted);font:400 15px/1 var(--display);
           white-space:nowrap;overflow:hidden;max-width:calc(100% - 60px)}
.hero-hint.gone{display:none}
.hero-hint b{color:var(--ink-soft);font-weight:600}
.hero-hint b::after{content:'';display:inline-block;width:1px;height:1em;
                    background:var(--signal);margin-left:2px;vertical-align:-.14em;
                    animation:caret 1s steps(1) infinite}
@keyframes caret{50%{opacity:0}}

/* Amazon-style suggestion dropdown: absolutely positioned off .herosearch
   (already position:relative), so it floats below the input without
   pushing the nav layout around. Hidden by default -- interaction.js toggles
   .open the same way it does for every other panel on the site (see
   closeAll()), so Escape/outside-click close this one for free. */
.herosearch-suggest{position:absolute;left:0;right:0;top:calc(100% + 8px);
  background:var(--card);border:1px solid var(--rule);border-radius:var(--r-md);
  box-shadow:0 14px 32px rgba(0,0,0,.14);padding:6px;z-index:40;
  max-height:360px;overflow-y:auto;display:none}
.herosearch-suggest.open{display:block}
.herosearch-suggest button{display:block;width:100%;text-align:left;
  background:transparent;border:0;border-radius:var(--r-sm);padding:10px 12px;
  font:400 13.5px/1.35 var(--display);color:var(--ink);cursor:pointer}
.herosearch-suggest button:hover,.herosearch-suggest button.active{background:var(--paper-2)}
.herosearch-suggest button b{color:var(--signal);font-weight:700}
/* Marks a suggestion that comes from the Picks feed, not this page's own
   grid -- clicking it navigates to amazon-picks.html instead of filtering
   in place, which is different enough behavior to flag before the click. */
.sugg-tag{display:block;margin-top:2px;font:600 9.5px/1 var(--mono);
  letter-spacing:.05em;text-transform:uppercase;color:var(--gate)}

.stats{margin-top:22px;font:500 12px/1 var(--mono);letter-spacing:.07em;
       text-transform:uppercase;color:var(--muted);display:flex;flex-wrap:wrap;
       gap:8px 18px;align-items:baseline;position:relative}
.stats>span{display:flex;align-items:baseline;gap:6px}
.stats b{color:var(--ink);font-weight:700;font-size:16px;font-variant-numeric:tabular-nums}
/* Track-record row (2026-09-15): same bar, deliberately smaller/quieter than
   the live-count row above it -- it's a supporting credibility fact, not the
   page's main number. */
.stats.track-record{margin-top:8px}
.stats.track-record b{font-size:13px}
/* Category page freshness line ("Verified 2 hours ago, last refresh...") --
   a real sentence, not a stat pill, so it resets .stats' uppercase mono
   styling back to normal running text (Task 2, Che's competitor-gap SEO
   review, 2026-09-20). */
.freshness-line{font:400 12px/1.4 var(--display);letter-spacing:0;text-transform:none;color:var(--muted)}

/* ---- trust strip: clean and minimal ---- */
/* ---- footer trust line + disclosure accordion + social ---- */
.trust-line{font-size:13.5px;line-height:1.65;color:var(--ink-soft);max-width:76ch;margin:0}
.trust-line b{color:var(--ink);font-weight:700}

/* Footer: stacked accordions on the left, social icons pinned right. The
   icon list is a <ul> so adding Instagram/X later is one <li>, and the icons
   carry no visible label -- the accessible name lives on aria-label. */
.footer-grid{display:flex;align-items:flex-start;justify-content:space-between;gap:32px}
.footer-main{flex:1;min-width:0}
.footer-social{list-style:none;display:flex;flex-wrap:wrap;gap:10px;margin:0;padding:0;flex-shrink:0}
/* footer.site.footer-social (not .footer-social alone) so this reliably beats
   `footer.site a`'s color:var(--gate) below on specificity -- the plain-class
   version used to lose that fight, which is why the social icons rendered
   solid Gate Green instead of the intended muted gray (found 2026-09-05). */
footer.site .footer-social a{display:flex;align-items:center;justify-content:center;
                 width:44px;height:44px;text-decoration:none;color:var(--ink-soft);
                 border:1px solid var(--rule);border-radius:50%;
                 transition:border-color var(--dur-base) ease,color var(--dur-base) ease,background var(--dur-base) ease}
footer.site .footer-social a:hover{border-color:var(--signal);color:var(--signal);background:var(--paper-2)}
.footer-social svg{width:19px;height:19px;fill:currentColor}
@media(max-width:640px){
  .footer-grid{flex-direction:column-reverse;gap:20px}
  .footer-social{align-self:flex-start}
}

/* ---- disclosure: normal body size, plain border ---- */
/* Was border-left:3px solid var(--signal) -- every one of these four
   accordions got the same red stripe regardless of content, which is the
   tell that it was decorating rather than signalling anything. The bold
   <summary> label already carries the hierarchy; matches .card's own
   plain-border convention elsewhere on the page (2026-08-24 polish). */
.disclosure{margin:20px 0;padding:0;background:var(--card);
            border:1px solid var(--rule);
            border-radius:var(--r-md);max-width:76ch;overflow:hidden}
.disclosure>summary{cursor:pointer;list-style:none;padding:14px 18px;
                    font-size:13.5px;font-weight:700;color:var(--ink);
                    display:flex;align-items:center;justify-content:space-between;gap:12px}
.disclosure>summary::-webkit-details-marker{display:none}
.disclosure>summary::after{content:'\002B';font:400 18px/1 var(--mono);color:var(--signal);flex-shrink:0}
.disclosure[open]>summary::after{content:'\2212'}
.disclosure>summary:hover{color:var(--signal)}
.disclosure-body{padding:0 18px 16px;font-size:13.5px;line-height:1.65;color:var(--ink-soft)}
/* Native <details> shows/hides content itself -- height can't be animated
   reliably across browsers without JS, so this only softens the pop-in,
   it doesn't try to animate the layout jump. */
.disclosure[open]>.disclosure-body{animation:disclosure-in 160ms ease-out}
@keyframes disclosure-in{from{opacity:0}to{opacity:1}}

/* ---- grid card prices (the .best-deal-hero rules that used to live here went
   with the component, 2026-08-10 -- these two are still the grid's) ---- */
.price-now{font-size:20px;font-weight:900;color:var(--signal);font-family:var(--mono);
           font-variant-numeric:tabular-nums}
.price-was{font-size:13px;color:var(--muted);text-decoration:line-through;font-family:var(--mono);
           font-variant-numeric:tabular-nums}

/* ---- filter bar: category dropdown + sort (search lives in the hero now) ----
   The old #dealSearch block that sat here was deleted 2026-08-10 along with the
   .searchwrap it styled: an ID selector outranks .herosearch input, so its
   background:var(--card) was still winning and painting an opaque panel over
   the rotating hint sitting behind the transparent field. */
.filterbar{margin:26px 0 12px;display:flex;flex-wrap:wrap;gap:10px;align-items:center}
#dealSearch::placeholder{color:#AAA}
#dealSearch::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
#dealSort,#picksSort{font:500 13px/1 var(--display);color:var(--ink);background:var(--card);
          border:1px solid var(--rule);border-radius:999px;padding:10px 16px;cursor:pointer}
.no-results{padding:40px 12px;color:var(--muted);text-align:center;
            border:1px dashed var(--rule);border-radius:var(--r-md);display:none}

/* ---- category filter dropdown (checkbox panel, DealNews-style) ---- */
.catfilter-wrap{position:relative}
#catFilterBtn{font:500 13px/1 var(--display);color:var(--ink);background:var(--card);
              border:1px solid var(--rule);border-radius:999px;padding:10px 16px;cursor:pointer}
@media(hover:hover){#catFilterBtn:hover{border-color:var(--signal)}}
#catFilterBtn:active{border-color:var(--signal)}
.catfilter-panel{display:block;position:absolute;top:calc(100% + 6px);left:0;z-index:40;
                 background:var(--card);border:1px solid var(--rule);border-radius:var(--r-sm);
                 box-shadow:0 8px 24px rgba(0,0,0,.15);width:290px;
                 opacity:0;visibility:hidden;transform:translateY(-4px);
                 transition:opacity var(--dur-fast) ease,transform var(--dur-fast) ease,visibility var(--dur-fast)}
.catfilter-panel.open{opacity:1;visibility:visible;transform:none}
.catfilter-list{max-height:320px;overflow-y:auto;padding:10px 14px}
.catfilter-list label{display:flex;gap:9px;align-items:center;padding:6px 0;
                      font-size:13px;cursor:pointer;color:var(--ink)}
.catfilter-list label:hover{color:var(--signal)}
.catfilter-list label.sub{padding-left:26px;font-size:12.5px;color:var(--ink-soft)}
.catfilter-list .cnt{color:var(--muted);font-family:var(--mono);font-size:11px;margin-left:auto}
.catfilter-list input{accent-color:var(--signal);width:15px;height:15px;flex-shrink:0}
/* Parent row + its caret share a line; the subcategory block below it is
   collapsed until the caret is pressed. Ten categories now carry
   subcategories, so expanded-by-default would be ~70 rows in a 320px
   scroller -- worse than the flat list this replaced. */
.catfilter-group{display:flex;align-items:center;gap:4px}
.catfilter-group label{flex:1;min-width:0}
/* padding 7px on a 10px glyph = 24x24px real hit box, the WCAG 2.2 web
   touch-target minimum -- was 4px 6px (~18x22px), under the floor. */
.catfilter-caret{background:none;border:none;cursor:pointer;padding:7px;line-height:1;
                 color:var(--muted);font-size:10px;flex-shrink:0;
                 transition:transform var(--dur-fast) ease,color var(--dur-fast) ease}
@media(hover:hover){.catfilter-caret:hover{color:var(--signal)}}
.catfilter-caret:active{color:var(--signal)}
.catfilter-caret[aria-expanded=true]{transform:rotate(90deg);color:var(--signal)}
.catfilter-subs{display:none}
.catfilter-subs.open{display:block}
.catfilter-actions{display:flex;gap:8px;padding:10px 14px;border-top:1px solid var(--rule)}
#catApply{flex:1;background:var(--signal);color:#fff;border:none;border-radius:999px;
          padding:9px 0;font-weight:700;font-size:13px;cursor:pointer}
#catApply:hover{background:var(--signal-dark)}
#catClear{background:var(--card);color:var(--ink);border:1px solid var(--rule);border-radius:999px;
          padding:9px 14px;font-size:13px;cursor:pointer}
#catClear:hover{border-color:var(--signal)}

/* amazon-picks.html's category filter sits inline in .picks-meta next to
   plain informational text ("N picks / Curated by hand / Amazon associate
   link"). An earlier version matched that text's own quiet styling so
   closely it read as a fifth line of copy, not a control -- Che, 2026-08-26:
   "ahi ni se diferencia" (couldn't tell it was clickable at all). Now it's
   deliberately the odd one out in that row: solid orange fill (the Picks
   section's own accent, matching .picks::before's gradient), bigger type,
   more padding -- the one thing in .picks-meta that looks like a button
   because it's the only interactive one. */
#picksCatBtn{font:700 13px/1 var(--display);letter-spacing:.01em;
             /* was color:#fff -- white on this gradient measures 2.2-2.6:1,
                well under WCAG's 4.5:1 floor. Literal #1a1a1a, not var(--ink):
                this gradient background is the same in both themes, but
                --ink flips to a light colour in dark mode (see the theme
                override below), which would put light text back on a light
                gradient -- caught live on 2026-09-04, first fix shipped
                broken in dark mode. */
             color:#1a1a1a;background:linear-gradient(135deg,var(--picks-light),var(--picks));
             border:none;border-radius:999px;padding:9px 18px;cursor:pointer;
             box-shadow:0 2px 6px rgba(232,137,12,.35)}
@media(hover:hover){#picksCatBtn:hover{background:linear-gradient(135deg,var(--picks),var(--picks-dark))}}
#picksCatBtn:active{background:var(--picks-dark)}
/* z-index bumped past the deals-page panel's 40: the Lasso embeds in the
   grid directly below this panel render as iframes, which can create their
   own stacking context that ignores a same-level z-index and paints over a
   position:absolute sibling regardless of source order. */
#picksCatPanel{z-index:60}
#picksCatApply{flex:1;background:var(--signal);color:#fff;border:none;border-radius:999px;
               padding:9px 0;font-weight:700;font-size:13px;cursor:pointer}
#picksCatApply:hover{background:var(--signal-dark)}
#picksCatClear{background:var(--card);color:var(--ink);border:1px solid var(--rule);border-radius:999px;
               padding:9px 14px;font-size:13px;cursor:pointer}
#picksCatClear:hover{border-color:var(--signal)}

/* ---- Amazon picks: hand-curated, deliberately NOT a deal card ----
   No price, no %-off badge, no Amazon product image, by policy (see
   _amazon_picks_html). Every other card on this page is carried by a product
   photo and a big orange %-off pill; strip both and what was left read as a
   list of grey text links nobody's eye stopped on (Che, 2026-08-13: "esta muy
   simple, ni se nota").

   The fix is to stop treating the missing photo as a hole and give the card
   art we're allowed to own: a category-coloured gradient panel carrying a
   line-drawn glyph and the pick's rank numeral. That art is generated from
   the pick's category, so it costs no request, can't 404, and can never be
   Amazon Product Advertising Content. Result is a rail with real colour
   rhythm sitting inside its own banded section, so the eye reads
   "editorial shelf, not scanner output" before it reads a word. */
.picks{position:relative;margin:34px 0 8px;padding:26px 24px 28px;overflow:hidden;
       background:var(--paper-2);border:1px solid var(--rule);border-radius:var(--r-lg)}
/* The gold hairline is the section's only fixed colour. Picks are the one
   thing on the page a human chose, so they get an accent the machine-made
   cards (all --signal red) never use. */
.picks::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;
               background:linear-gradient(90deg,var(--picks-light),var(--picks) 45%,var(--signal))}
@media(max-width:640px){.picks{padding:20px 14px 22px;border-radius:var(--r-lg)}}
.picks-head{margin-bottom:18px}
.picks-eyebrow{display:inline-flex;align-items:center;gap:7px;
               font:700 10px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;
               /* was #B8791A -- 3.75:1 on --paper-2, under AA's 4.5:1 for
                  10px text. Darkened, same hue. */
               color:#8C5C0E;margin-bottom:9px}
.picks-eyebrow::before{content:"";width:16px;height:2px;background:var(--picks)}
.picks-head h2{margin:0;font:800 clamp(1.3rem,3.4vw,1.75rem)/1.15 var(--display);
               letter-spacing:-.02em;color:var(--ink)}
.picks-head p{margin:9px 0 0;font-size:13.5px;line-height:1.55;color:var(--ink-soft);max-width:62ch}
.picks-meta{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:13px}
.picks-meta span{font:600 10.5px/1 var(--mono);letter-spacing:.05em;text-transform:uppercase;
                 color:var(--muted);background:var(--card);border:1px solid var(--rule);
                 border-radius:999px;padding:6px 11px}
/* Same "odd one out" treatment as #picksCatBtn just above -- a plain-text
   link in this row reads as another info pill, not a link to follow us
   elsewhere (see that comment for the Che quote this pattern originated
   from). Solid orange fill makes it the row's one clickable action. */
.picks-follow-cta{font:700 10.5px/1 var(--display);letter-spacing:.03em;text-decoration:none;
                 color:#1a1a1a;background:linear-gradient(135deg,var(--picks-light),var(--picks));
                 border-radius:999px;padding:6px 14px;box-shadow:0 2px 6px rgba(232,137,12,.35)}
@media(hover:hover){.picks-follow-cta:hover{background:linear-gradient(135deg,var(--picks),var(--picks-dark))}}
.picks-see-all{font:700 12.5px/1 var(--display);color:var(--signal);text-decoration:none;
               display:inline-block;padding:7px 0}
.picks-see-all:hover{text-decoration:underline}
/* amazon-picks.html: same .pick/.pick-lasso cards as the homepage rail, just
   wrapped for a grid since there's no viewport to scroll-snap into. */
.picks-page-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));
                 gap:14px;margin-top:18px}
.picks-page-grid-lasso{grid-template-columns:repeat(auto-fill,minmax(360px,1fr))}
.picks-page-grid .pick{flex:none;width:100%}
/* Carousel, not a wrapping grid: past a handful of picks a flat grid pushes
   the eBay/Walmart deals grid too far down the page. Same scroll-snap rail
   mechanism as .td-rail (see that comment for why it's native, not a library). */
.picks-rail{position:relative}
.picks-viewport{overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;
                scroll-behavior:smooth;scrollbar-width:none;-ms-overflow-style:none;
                margin:0 -6px;padding:6px}
.picks-viewport::-webkit-scrollbar{display:none}
.picks-track{display:flex;gap:14px;width:max-content;padding-bottom:2px}
.pick{scroll-snap-align:start;flex:0 0 236px;width:236px;display:flex;flex-direction:column;
      text-decoration:none;overflow:hidden;
      background:var(--card);border:1px solid var(--rule);border-radius:var(--r-md);
      transition:transform var(--dur-med) ease,box-shadow var(--dur-med) ease,border-color var(--dur-med) ease}
/* hover:hover/:active split (2026-09-05 fix, same convention as navBtn/
   megaBtn/catFilterBtn/over-menu): this was bare :hover before, so touch
   visitors -- most of this card's actual traffic -- got zero visual
   acknowledgment before navigating away. :focus-visible/:active share the
   hover treatment unconditionally; :hover alone is gated. */
.pick:focus-visible,.pick:active{transform:translateY(-4px);border-color:transparent;
      box-shadow:0 12px 26px rgba(0,0,0,.15)}
@media(hover:hover){.pick:hover{transform:translateY(-4px);border-color:transparent;
      box-shadow:0 12px 26px rgba(0,0,0,.15)}}
/* --pk-a/--pk-b are set per card from the pick's category (PICK_FAMILIES). */
.pick-art{position:relative;height:132px;display:flex;align-items:flex-end;padding:12px 13px;
          background:linear-gradient(140deg,var(--pk-a,#52525B),var(--pk-b,#27272A));
          color:#fff;overflow:hidden}
/* Glyph and numeral are the two layers doing the product photo's job: the
   glyph says what kind of thing this is at thumbnail size, the numeral says
   the list is ordered and finite. Both sit under the chip, never over it. */
.pick-glyph{position:absolute;top:16px;left:14px;width:40px;height:40px;
            stroke:#fff;stroke-width:1.6;fill:none;opacity:.95;
            stroke-linecap:round;stroke-linejoin:round;
            transition:transform .25s ease}
@media(hover:hover){.pick:hover .pick-glyph{transform:scale(1.12) rotate(-4deg)}}
.pick-num{position:absolute;top:-6px;right:6px;font:800 62px/1 var(--display);
          letter-spacing:-.04em;color:rgba(255,255,255,.22);pointer-events:none;
          font-variant-numeric:tabular-nums}
.pick-cat{position:relative;z-index:2;max-width:100%;
          font:700 9.5px/1.2 var(--mono);letter-spacing:.07em;text-transform:uppercase;
          background:rgba(0,0,0,.32);border-radius:999px;padding:6px 10px;
          overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pick-body{display:flex;flex-direction:column;gap:7px;padding:13px 14px 14px;flex:1}
.pick-title{font:700 13.5px/1.38 var(--display);color:var(--ink);
            display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;
            transition:color var(--dur-base) ease}
.pick:focus-visible .pick-title,.pick:active .pick-title{color:var(--signal)}
@media(hover:hover){.pick:hover .pick-title{color:var(--signal)}}
.pick-note{font-size:12.5px;line-height:1.5;color:var(--ink-soft);
           display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.pick-cta{margin-top:auto;padding-top:11px;border-top:1px solid var(--rule);
          display:flex;align-items:center;justify-content:space-between;gap:8px;
          font:700 11px/1 var(--mono);letter-spacing:.04em;text-transform:uppercase;
          color:var(--signal)}
.pick-cta i{font-style:normal;transition:transform var(--dur-med) ease}
.pick:focus-visible .pick-cta i,.pick:active .pick-cta i{transform:translateX(4px)}
@media(hover:hover){.pick:hover .pick-cta i{transform:translateX(4px)}}
.picks-arrow{position:absolute;top:66px;transform:translateY(-50%);z-index:5;
             width:38px;height:38px;border-radius:50%;cursor:pointer;
             background:var(--card);border:1px solid var(--rule);color:var(--ink);
             font:400 22px/1 var(--display);display:flex;align-items:center;justify-content:center;
             box-shadow:0 2px 10px rgba(0,0,0,.16);
             transition:background var(--dur-base) ease,color var(--dur-base) ease,border-color var(--dur-base) ease,opacity var(--dur-base) ease}
@media(hover:hover){.picks-arrow:hover{background:var(--signal-dark);border-color:var(--signal-dark);color:#fff}}
.picks-arrow:active{background:var(--signal-dark);border-color:var(--signal-dark);color:#fff}
.picks-arrow:focus-visible{outline:2px solid var(--signal);outline-offset:2px}
.picks-arrow[disabled]{opacity:0;pointer-events:none}
.picks-prev{left:-13px}
.picks-next{right:-13px}
@media(max-width:820px){
  .pick{flex:0 0 200px;width:200px}
  .pick-art{height:118px}
  .picks-arrow{display:none}
  .picks-viewport,.picks-lasso-viewport{scroll-snap-type:x proximity}
}

/* ---- Amazon picks: real Lasso embeds (photo, price, Buy button) ----
   Lasso's own embed lays itself out as a text/image flex split sized for a
   wide column; forced into the 236px rail alongside the SVG-art picks, the
   image shrank to a ~36px sliver and the full product description stretched
   the card past 1500px tall (Che, 2026-08-15: unacceptable). Fixed 120x120
   photo + grid-templated boxes (below) solved that; these render as their own
   horizontal scroll-snap rail -- same mechanism as .picks-rail/.td-rail --
   rather than a 2-col grid, so more of them fit across without pushing the
   page down a full row per pair (Che, 2026-08-15). initRail('lassoViewport',
   ...) below drives the arrow buttons. */
.picks-lasso-rail{position:relative;margin-top:14px}
.picks-lasso-rail .picks-arrow{top:50%}
.picks-lasso-viewport{overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;
                scroll-behavior:smooth;scrollbar-width:none;-ms-overflow-style:none;
                margin:0 -6px;padding:6px}
.picks-lasso-viewport::-webkit-scrollbar{display:none}
.picks-lasso-track{display:flex;gap:14px;width:max-content;padding-bottom:2px}
.pick-lasso{scroll-snap-align:start;flex:0 0 400px;width:400px;background:var(--card);
            border:1px solid var(--rule);border-radius:var(--r-md);padding:16px;position:relative;
            overflow:hidden;transition:transform var(--dur-med) ease,box-shadow var(--dur-med) ease,border-color var(--dur-med) ease}
/* .pick-lasso is a <div> (Lasso injects the real link/button inside it), so
   only :active applies here -- a div never receives keyboard focus itself. */
.pick-lasso:active{transform:translateY(-4px);border-color:transparent;box-shadow:0 12px 26px rgba(0,0,0,.15)}
@media(hover:hover){.pick-lasso:hover{transform:translateY(-4px);border-color:transparent;box-shadow:0 12px 26px rgba(0,0,0,.15)}}
@media(max-width:640px){.pick-lasso{flex:0 0 320px;width:320px}}
/* Category chip + rank numeral, carried over from the SVG-art picks (same
   --pk-a/--pk-b/_pick_art() as those cards) so the two rails read as one
   family. Both sit in the card's own chrome -- absolutely positioned over
   the *card*, never over the <img> itself -- so nothing here touches,
   recolors or overlays Amazon's product photo (Che, 2026-08-15: wanted the
   old carousel's look back without risking Amazon's content-alteration
   rules; keeping our art off the photo entirely is the line). */
.pick-lasso-num{position:absolute;top:2px;right:14px;font:800 54px/1 var(--display);
            letter-spacing:-.03em;color:var(--pk-a,var(--rule));opacity:.16;
            pointer-events:none;font-variant-numeric:tabular-nums;z-index:0}
.pick-lasso-cat{position:absolute;top:14px;right:14px;z-index:2;max-width:150px;
            font:700 9px/1.2 var(--mono);letter-spacing:.06em;text-transform:uppercase;
            color:#fff;background:linear-gradient(rgba(0,0,0,.45),rgba(0,0,0,.45)),
            linear-gradient(135deg,var(--pk-a,#52525B),var(--pk-b,#27272A));
            border-radius:999px;padding:5px 10px;overflow:hidden;text-overflow:ellipsis;
            white-space:nowrap}
.pick-lasso .lasso-embed{position:relative;z-index:1}
/* Lasso's Cutter theme splits box-1 (title/price/desc) and box-2 (image) as a
   50/50 *proportion* of the card, so the photo shrinks every time the card
   does -- the opposite of every other product photo on this site, which is a
   fixed 120x120 .thumb that never shrinks (Che, 2026-08-15: wants the eBay/
   Walmart card shape, photo pinned at .thumb size, not scaling down). Grid
   areas take over box placement entirely (ignoring Lasso's own flex
   percentages, all !important, via a more specific selector -- same
   technique already proven for the color variables above) so the photo
   column is a hard 120px like .thumb and only the text column flexes.

   Which box is which is NOT consistent: embeds minted via the Lasso
   WordPress plugin (the first 20) put title/price in box-1 and the image in
   box-2, but a "Boosted"/badged product (2026-08-15: caught on Soundcore,
   which shows a "Limited time deal" ribbon) swaps to box-1=image,
   box-2=everything-else instead. Targeting by :has() on what's actually
   inside the box, rather than the box's number, makes this correct either
   way instead of only for whichever layout happened to get tested. box-4 is
   Lasso's own always-empty box. box-5 (commission disclosure) and box-6 (the
   "price accurate as of <date>" stamp) both stay -- Che flagged 2026-08-15
   that a price served live off Amazon's API needs that "as of" timestamp
   visible to stay inside Amazon's own pricing-display terms, so this only
   compacts them to one line each rather than dropping either. Description
   dropped per the same note -- these are one-line picks now, not blurbs. */
.pick-lasso .embed-lasso-display{display:grid !important;grid-template-columns:120px 1fr !important;
            grid-template-areas:"img text" "btn btn" "meta meta" "date date" !important;
            column-gap:14px !important;row-gap:4px !important;align-items:center !important;
            margin:0 !important;padding:0 !important}
.pick-lasso .embed-lasso-box-1:has(.embed-lasso-title),
.pick-lasso .embed-lasso-box-2:has(.embed-lasso-title){
            grid-area:text !important;flex:none !important;max-width:none !important;
            padding:26px 0 0 !important;align-self:center !important}
.pick-lasso .embed-lasso-box-1:has(.embed-lasso-image),
.pick-lasso .embed-lasso-box-2:has(.embed-lasso-image){
            grid-area:img !important;flex:none !important;max-width:none !important;
            margin:0 !important;padding:0 !important;align-self:start !important}
.pick-lasso .embed-lasso-box-3{grid-area:btn !important;flex:none !important;max-width:none !important;padding:0 !important}
.pick-lasso .embed-lasso-box-4{display:none !important}
.pick-lasso .embed-lasso-box-5{grid-area:meta !important;flex:none !important;
            max-width:none !important;padding:0 !important;display:block !important}
.pick-lasso .embed-lasso-box-6{grid-area:date !important;flex:none !important;
            max-width:none !important;padding:0 !important;display:block !important}
/* The "boosted"/badged structure (see the :has() comment above) bundles
   disclosure+date together in one .embed-lasso-end div, nested INSIDE the
   box that also holds title/price/button -- so on a narrow (mobile) card
   that div only gets the ~186px text column, not the full card width, and
   "Half or Nothing may earn commissions..." wrapped into 5+ ragged lines
   (Che, 2026-08-16: only picks using the OTHER structure, box-5/6, looked
   right). CSS Grid can't hand a nested element its own grid-area without
   dissolving every sibling around it too (title/price would need their own
   areas and start overlapping) -- so retheme() below physically moves
   .embed-lasso-end out to be a direct child of .embed-lasso-display instead,
   the same DOM-surgery approach already used there for the title/button
   text. This rule is just where it lands once moved. */
.pick-lasso .embed-lasso-end{grid-area:meta !important;width:100% !important}
.pick-lasso .embed-lasso-image{display:block !important;margin:0 !important;padding:0 !important}
.pick-lasso .embed-lasso-image-no-lazy{width:120px !important;height:120px !important;
            object-fit:contain !important;border-radius:var(--r-md);border:1px solid var(--rule);
            background:var(--paper-3)}
/* Title is the full raw Amazon product name (often 15+ words) with no
   truncation of its own -- inside the 120px-image/1fr-text grid that leaves
   maybe 150-250px for text, so a long one ran 8-10 ragged lines and blew the
   card's height out (Che, 2026-09-02: "se ve rotisimo", the E.L.F. pick and
   "todos" on mobile). Clamped to 4 lines with an ellipsis at every card
   width -- same fix whether the text column is narrow (mobile) or the wider
   desktop 1fr -- so the card height stays predictable and the title never
   wraps past what a shopper needs to recognize the product. */
.pick-lasso .embed-lasso-title,.pick-lasso a.embed-lasso-title{font-size:14px !important;
            line-height:1.4 !important;font-weight:600 !important;margin:0 0 4px !important;
            display:-webkit-box !important;-webkit-line-clamp:4 !important;
            -webkit-box-orient:vertical !important;overflow:hidden !important;
            text-overflow:ellipsis !important}
.pick-lasso .embed-lasso-price{font-size:13px !important;margin:0 !important}
.pick-lasso .embed-lasso-description{display:none !important}
.pick-lasso a.embed-lasso-button-1{grid-area:btn !important;padding:9px 14px !important;
            font-size:12px !important;border-radius:var(--r-sm) !important}
.pick-lasso .embed-lasso-date,.pick-lasso .embed-lasso-disclosure{font-size:10.5px !important;
            padding:0 !important;color:var(--muted) !important}
/* Price gets its own accent (Che, 2026-08-15: bigger, different color if
   possible) -- reusing --signal, the same red the eBay/Walmart cards use for
   their own price numbers, so it reads as "the number that matters" rather
   than introducing a third accent color to the section. Lasso's own price
   rule hardcodes color:rgb(0,0,0) (not a variable), so it has to be beaten
   the same specificity way as everything else here, not just relied on to
   inherit --embed-lasso-title. */
.pick-lasso .embed-lasso-price{font-size:20px !important;font-weight:800 !important;
            color:var(--signal) !important;margin:2px 0 8px !important;display:flex !important;
            align-items:baseline !important;gap:8px !important;flex-wrap:wrap !important}
/* .price-was/.pct-pill are appended into .embed-lasso-price by the retheme()
   script below, from a list price Che hand-verified on Amazon itself (Lasso's
   own base_price field is not reliable enough to trust for this -- caught it
   reporting "no discount" on items Amazon's own page showed at -50% the same
   day, 2026-08-15). Reused classes, not new ones, so a real discount here
   looks exactly like a real discount on every other card on the site.
   .pick-lasso-coupon is ours: a coupon Amazon offers on top of that price,
   which nothing else on the page has a slot for. */
.pick-lasso-coupon{font:700 11px/1.3 var(--mono);color:var(--gate);margin:-4px 0 8px;
            letter-spacing:.02em}
/* Blog posts only: the deals-grid/amazon-picks.html .thumb convention (120px,
   pinned so cards never grow) is right for a dense grid of many picks, but
   inside a single post's ~600px-wide prose column a lone pick next to a big
   eBay/compare-card photo read as a tiny thumbnail next to a poster (Che,
   2026-09-02). Higher-specificity override, blog posts only -- the grid page
   keeps its 120px .thumb sizing untouched.
   :not(.picks-lasso-rail) is the actual gate for that, not the class name
   alone -- html_output.py's amazon-picks.html grid and the brands.html
   "Amazon Picks in a known brand" rail both carry picks-page-grid-lasso
   AND picks-lasso-rail together (see write_amazon_picks_page /
   _tagged_deals_html), while blog.py's single-pick wrapper only ever
   carries picks-page-grid-lasso alone. Before this :not(), both grid pages
   matched too -- confirmed live 2026-09-04: a 120px-fixed image forced
   into a 200px column left ~71px for title/price/coupon, which wrapped
   into a near-vertical ladder and overflowed the card's right/bottom
   edges. That's what actually produced the "words outside the card" bug,
   not the box-1/box-2 sizing rules below (their width was fine once the
   image column stopped eating the rest of the card). */
.picks-page-grid-lasso:not(.picks-lasso-rail) .pick-lasso .embed-lasso-display{grid-template-columns:200px 1fr !important}
.picks-page-grid-lasso:not(.picks-lasso-rail) .pick-lasso .embed-lasso-image-no-lazy{width:200px !important;height:200px !important}
/* Mobile: the 120px-image/1fr-text side-by-side grid above leaves the title
   column too narrow on a phone-width card (Che, 2026-09-02: "todos se ven
   rotisimos en el celular" -- title crammed beside the photo instead of
   under it). Below 640px (same breakpoint .pick-lasso already shrinks its
   own width at) the image moves to its own full-width row above the title
   instead, centered, same as every other product card style on the site. */
@media(max-width:640px){
.pick-lasso .embed-lasso-display,
.picks-page-grid-lasso .pick-lasso .embed-lasso-display{grid-template-columns:1fr !important;
            grid-template-areas:"img" "text" "btn" "meta" "date" !important;
            justify-items:center !important}
.pick-lasso .embed-lasso-box-1:has(.embed-lasso-image),
.pick-lasso .embed-lasso-box-2:has(.embed-lasso-image){justify-self:center !important}
.pick-lasso .embed-lasso-box-1:has(.embed-lasso-title),
.pick-lasso .embed-lasso-box-2:has(.embed-lasso-title){width:100% !important;padding-top:8px !important}
}
/* These 20 embeds were minted through Lasso's WordPress plugin flow, not the
   Websites/halfornothing.com project Che configures in Lasso's own "Display
   Settings" -- so the badge/button colors he set and saved there (verified
   persisted, 2026-08-15) never reach them; each embed carries its own frozen
   color snapshot from creation, injected as `.lasso-embed{--embed-lasso-*:
   ... !important}`. Same values, applied here instead: this selector is more
   specific than Lasso's own, so it wins the cascade regardless of !important
   on both sides. Update these if Che changes the palette in Lasso again --
   this does not read from there live. Title is tied to the site's own --ink
   instead of a fixed hex so it stays legible in whichever theme the visitor
   has (Che, 2026-08-15: background should follow light/dark, not stay
   hardcoded white). Background is transparent rather than var(--card): the
   embed no longer needs to paint its own card surface -- .pick-lasso already
   does, in the same theme-aware color -- and painting it a second time here
   opaquely covered the category chip/rank numeral sitting behind it.
   Updated 2026-09-02: --embed-lasso-main (badge/border accent) matches the
   site's brand red so the card frame lines up with the eBay [[shop:...]]
   card's border. --embed-lasso-button stays Amazon's own orange (#FF9900)
   on purpose -- Che, 2026-09-02: "el boton de amazon se queda naranja
   porque es el color de amazon", the button should read as Amazon's own
   Buy button, not recolored to match the site. */
.pick-lasso .lasso-embed{
  --embed-lasso-main:#D93A12 !important;
  --embed-lasso-title:var(--ink) !important;
  --embed-lasso-button:#FF9900 !important;
  --embed-lasso-button-text:#ffffff !important;
  --embed-lasso-background:transparent !important;
}

/* ---- mega menu (top nav Categories, DealNews-style flyout) ---- */
.megamenu-wrap{position:relative;margin:24px 0 0}
.mast .megamenu-wrap{display:inline-block}
#megaBtn{font:600 14px/1 var(--display);color:var(--ink);background:var(--card);
         border:1px solid var(--rule);border-radius:999px;padding:11px 20px;cursor:pointer}
@media(hover:hover){#megaBtn:hover{border-color:var(--signal);color:var(--signal)}}
#megaBtn:active{border-color:var(--signal);color:var(--signal)}
.megamenu{display:block;position:absolute;top:calc(100% + 10px);left:0;z-index:50;
          background:var(--card);border:1px solid var(--rule);border-radius:var(--r-sm);
          box-shadow:0 10px 30px rgba(0,0,0,.16);min-width:270px;
          opacity:0;visibility:hidden;transform:translateY(-4px);
          transition:opacity var(--dur-fast) ease,transform var(--dur-fast) ease,visibility var(--dur-fast)}
.megamenu.open{opacity:1;visibility:visible;transform:none}
.mega-left{list-style:none;margin:0;padding:6px 0}
.mega-left>li{position:relative;margin:0}
.mega-left>li>a{display:flex;justify-content:space-between;align-items:center;gap:18px;
                padding:9px 16px;font-size:13px;font-weight:500;color:var(--ink);
                text-decoration:none;cursor:pointer}
.mega-left>li>a:hover{background:var(--paper-3);color:var(--signal)}
.mega-left .cnt{color:var(--muted);font-family:var(--mono);font-size:11px;font-weight:400}
.mega-left .arr{color:var(--muted);font-size:10px;margin-left:4px}
/* Two balanced columns of plain links, filled top-to-bottom. Was one
   <h4>-headed column per subcategory, which was fine at Fashion's three and
   unreadable at Home's eleven: the flyout grew sideways off the viewport. */
.flyout{display:none;position:absolute;left:100%;top:-1px;z-index:51;
        background:var(--card);border:1px solid var(--rule);border-radius:var(--r-sm);
        box-shadow:0 10px 30px rgba(0,0,0,.16);padding:14px 18px;
        grid-template-columns:repeat(2,minmax(max-content,1fr));grid-auto-flow:row;
        column-gap:26px;row-gap:1px;white-space:nowrap;max-width:min(460px,60vw)}
.mega-left>li.has-fly:hover>.flyout{display:grid}
/* The "All X" row heads the list and spans both columns, so the flyout still
   offers the parent page itself -- hovering a parent must never make its own
   landing page unreachable. */
.flyout>a:first-child{grid-column:1/-1;margin-bottom:6px;padding-bottom:7px;
                      border-bottom:1px solid var(--rule)}
.flyout a{display:flex;justify-content:space-between;align-items:center;gap:14px;
          padding:5px 0;font-size:13px;color:var(--ink);
          text-decoration:none;cursor:pointer}
.flyout a:hover{color:var(--signal)}
.flyout .cnt{color:var(--muted);font-family:var(--mono);font-size:11px}
@media(max-width:800px){
  .flyout{position:static;border:none;box-shadow:none;padding:0 16px 10px;
          grid-template-columns:1fr;max-width:none;gap:4px}
  .mega-left>li.has-fly:hover>.flyout{display:none}
  .mega-left>li.has-fly.open>.flyout{display:grid}
  .megamenu{min-width:230px}
}

/* ---- pager: hard-edged buttons ---- */
.pager{display:flex;flex-direction:column;align-items:center;gap:10px;
      margin:26px 0 10px;font:500 13px/1 var(--display)}
.pager button{font:800 14px/1 var(--display);letter-spacing:.01em;color:#fff;
              background:var(--signal-dark);border:2px solid var(--signal-dark);border-radius:999px;
              padding:14px 34px;cursor:pointer;transition:background var(--dur-base) ease,color var(--dur-base) ease}
.pager button:hover{background:var(--card);color:var(--signal)}
.pager button:focus-visible{outline:2px solid var(--signal);outline-offset:3px}
.pager-status{color:var(--muted);font:500 12px/1 var(--mono);letter-spacing:.04em}

/* ---- the grid: dense layout, cards with hard borders and shadows ---- */
.divider-banner{grid-column:1/-1;padding:8px 2px;font:600 11px/1 var(--mono);
                letter-spacing:.11em;text-transform:uppercase;color:var(--muted);
                border-bottom:2px solid var(--ink);margin-bottom:8px}
/* ---- horizontal deal cards: fixed 120x120 thumbnail left, details right ---- */
.grid{margin-top:12px;display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
@media(max-width:980px){.grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:560px){.grid{grid-template-columns:1fr}}

/* Portrait cards, same shape/size as the Today's Top Deals rail (.td-card)
   so the main grid and the rail read as one card language -- image on top,
   details stacked below, instead of the old thumbnail-left row. */
.card{background:var(--card);border-radius:var(--r-md);border:1px solid var(--rule);
      box-shadow:0 1px 3px rgba(0,0,0,.06);display:flex;flex-direction:column;min-width:0;
      transition:box-shadow var(--dur-base) ease,border-color var(--dur-base) ease;position:relative}
/* min-width:0 above overrides the grid-item default of min-width:auto, which
   otherwise floors each card at its own content's min-content width. A card
   whose title/price/button text happens to need more room than its fair
   1fr share was enough to blow out 3 of the 4 grid columns to ~427px each
   while the 4th got squeezed to ~176px -- since the underlying deal data
   refreshes hourly, this was latent and only actually surfaced once a
   specific listing's content tripped it (caught 2026-09-05, live site). */
/* .card is a <div> (the real link is the inner title/thumb <a>), so only
   :active applies -- it still fires correctly since a mouse-down inside the
   card bubbles :active up to it, giving touch a visible response before the
   inner link navigates (2026-09-05 fix, same gap as .pick/.pick-lasso). */
.card:active{box-shadow:0 4px 14px rgba(0,0,0,.12);border-color:var(--signal)}
@media(hover:hover){.card:hover{box-shadow:0 4px 14px rgba(0,0,0,.12);border-color:var(--signal)}}
/* Was border-left:3px solid var(--gate) -- redundant with .newpill, which
   already says "New" in real text on the same card; a colour-only stripe
   with no label doesn't add information and is the side-tab tell on top
   of it (2026-08-24 polish). */
.card.linked{box-shadow:0 0 0 3px rgba(0,168,99,.25)}
/* .v = a genuinely new listing (see _card()'s css_class), not a fake "new"
   flag. One-shot on page load, no color, no loop -- the .newpill text
   already says "New"; this is just weight, not another badge. Prototyped
   as five directions (Stamp/Number/Live/Quiet/Proof) plus two riff rounds
   off Live and Glow before landing here -- picked over a color-based glow
   or a border flash because it reads as "alive" without implying urgency,
   which is the same line .newpill's real text already draws. */
.card.v{animation:card-breathe 900ms cubic-bezier(0.23,1,0.32,1)}
@keyframes card-breathe{
  0%{transform:scale(1)}
  40%{transform:scale(1.018)}
  100%{transform:scale(1)}
}
@media(prefers-reduced-motion:reduce){.card.v{animation:none}}

/* Scroll-reveal (2026-09-05): fade+slide each card in as it crosses into
   view (interaction.js's IntersectionObserver adds .reveal then .reveal-in
   -- see its own comment for why .reveal alone, not a default base-state,
   so a JS failure never leaves a card stuck invisible). Staggered per
   column so a row cascades in left-to-right instead of popping in as one
   block; the two narrower breakpoints get their own stagger matching
   .grid's own 2-col/1-col column counts one breakpoint up. Reduced motion
   still fades (opacity is in this site's allowed-transition list under
   that media query) but snaps the slide instantly -- no movement, still a
   fade, same contract as everything else added this session. */
.card.reveal{opacity:0;transform:translateY(24px);
             transition:opacity .5s ease,transform .5s cubic-bezier(.23,1,.32,1)}
.card.reveal.reveal-in{opacity:1;transform:translateY(0)}
.card.reveal:nth-child(4n+2).reveal-in{transition-delay:.06s}
.card.reveal:nth-child(4n+3).reveal-in{transition-delay:.12s}
.card.reveal:nth-child(4n+4).reveal-in{transition-delay:.18s}
@media(max-width:980px){
  .card.reveal:nth-child(4n+3).reveal-in{transition-delay:0s}
  .card.reveal:nth-child(2n).reveal-in{transition-delay:.08s}
}
@media(max-width:560px){
  .card.reveal:nth-child(n).reveal-in{transition-delay:0s}
}

.thumb{width:100%;height:170px;flex-shrink:0;margin:0;border-radius:var(--r-md) var(--r-md) 0 0;
       overflow:hidden;background:#fff;display:block;
       border:0;border-bottom:1px solid var(--rule)}
.thumb img{width:100%;height:100%;object-fit:contain;display:block;padding:10px;
           box-sizing:border-box;transition:transform .2s ease}
.card:hover .thumb img{transform:scale(1.05)}
.thumb.noimg{display:flex;align-items:center;justify-content:center;
             color:#bbb;font-size:11px;font-family:var(--mono)}

/* Skeleton placeholders (2026-09-05) -- appended by interaction.js at the
   tail of the grid while the rest of the catalogue loads from deals.json in
   the background (see its own comment for why). Deliberately its own class,
   not .card, so it's invisible to every piece of card-selection logic
   (search, filters, the variant modal) -- a fake listing in that set would
   be clickable and countable as real inventory. Dimensions match .card/
   .thumb exactly so nothing shifts when a real card takes its place. */
.card-skeleton{background:var(--card);border-radius:var(--r-md);border:1px solid var(--rule);
               overflow:hidden}
.card-skeleton-thumb{height:170px;background:var(--paper-3)}
.card-skeleton-body{padding:14px 16px 16px;display:flex;flex-direction:column;gap:10px}
.card-skeleton-line{height:12px;border-radius:var(--r-sm);background:var(--paper-3)}
.card-skeleton-price{height:18px;margin-top:4px}
.card-skeleton-thumb,.card-skeleton-line{
  background-image:linear-gradient(90deg,var(--paper-3) 0%,var(--rule) 50%,var(--paper-3) 100%);
  background-size:200% 100%;animation:card-skeleton-shimmer 1.4s ease-in-out infinite}
@keyframes card-skeleton-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
@media(prefers-reduced-motion:reduce){.card-skeleton-thumb,.card-skeleton-line{animation:none}}

/* Amplifying the discount pill (2026-09-05), not adding a new signal -- this
   is the site's own Two-Signal Rule ("red means look at this number") given
   more weight on the number that rule already exists for, not a new
   decorative element. Bounces on hover along with the card's other hover
   feedback (transform, hover:hover-gated to match .card itself). */
/* No background here either, same reasoning as .td-off below -- every
   .pct-pill always also carries a pct-t1..pct-t5 tier class that paints it. */
.pct-pill{color:#fff;font:800 11px/1 var(--mono);
          padding:5px 9px;border-radius:999px;letter-spacing:.03em;white-space:nowrap;
          transition:transform var(--dur-med) ease}
.card:active .pct-pill{transform:scale(1.08)}
@media(hover:hover){.card:hover .pct-pill{transform:scale(1.08)}}
/* 5-step color-by-tier (2026-09-05, Che's ask): the pill's own background
   now steps warmer/darker as the real discount % climbs, instead of one
   flat red for every qualifying deal from 50% to 99%. Applies to both
   .pct-pill and .td-off (see _pct_tier_class() in html_output.py) -- a
   bare class, not scoped under either parent, so one set of rules covers
   both templates. Anchored on the two colors that already exist for other
   reasons (--signal, --signal-dark) for the middle-high bands, so the
   "typical" 70-89% deal still reads as the site's own established accent
   rather than a color invented just for this. */
.pct-t1{background:#A66A00}
.pct-t2{background:#C1440E}
.pct-t3{background:var(--signal)}
.pct-t4{background:var(--signal-dark)}
.pct-t5{background:#7A1204}
/* Real discounts only (data-discount/data-off is the actual computed %,
   never fabricated) -- applies to both .pct-pill (main grid) and .td-off
   (Today's Top Deals rail, a separate template, easy to miss when adding
   this the first time). First version here was an 18%-opacity ring plus a
   one-shot pop tied to scroll-reveal -- correct in theory, invisible in
   practice (Che: "no veo un carajo"), since a card already on screen at
   load has its pop finished before anyone looks. Replaced with a real,
   continuous, slow breathing glow so there's no timing window where it's
   simply not visible -- still just amplifying the number that's already
   the page's one real signal, not inventing a new one, and slow/steady
   reads as "notice this" rather than the frantic strobe a fake-urgency
   countdown would use. */
/* 2026-09-05: widened further -- Che still couldn't spot the glow at the
   first (18%-opacity, one-shot) or second (30-70%, 5-8px ring) pass. This
   round adds a real transform pulse on the pill itself (not just the ring)
   so there's actual size movement in peripheral vision, not only an
   opacity/blur change that's easy to miss against a busy grid. */
/* Bounded to 3 cycles (~4.8s), not infinite -- it has already made its point
   by then, and an infinite box-shadow animation is a real per-frame paint
   cost that outlives its purpose once every card carrying it is well past
   scroll-reveal. Ends on the base ring (box-shadow set on .pct-pill-extreme
   itself), never mid-pulse. */
.pct-pill-extreme{box-shadow:0 0 0 2px var(--card),0 0 0 7px rgba(217,58,18,.6);
                   animation:pct-pill-glow 1.6s ease-in-out 3}
.card.reveal-in .pct-pill-extreme{animation:pct-pill-pop .6s cubic-bezier(.23,1,.32,1),
                                    pct-pill-glow 1.6s ease-in-out 3}
@keyframes pct-pill-glow{
  0%,100%{box-shadow:0 0 0 2px var(--card),0 0 0 4px rgba(217,58,18,.55);transform:scale(1)}
  50%{box-shadow:0 0 0 2px var(--card),0 0 0 11px rgba(217,58,18,.95);transform:scale(1.07)}
}
@keyframes pct-pill-pop{0%{transform:scale(1)}40%{transform:scale(1.35)}
                          60%{transform:scale(.92)}100%{transform:scale(1)}}
.src-tag{background:#555;color:#fff;font-size:11px;font-weight:700;letter-spacing:.05em;
         text-transform:uppercase;padding:3px 8px;border-radius:999px}
.newpill{background:var(--gate);color:#fff;font-size:11px;font-weight:700;
         letter-spacing:.05em;text-transform:uppercase;padding:3px 8px;border-radius:999px}
/* Static confirmation that a listing cleared the site's own 50% filter --
   deliberately carries no date, price history or trend so it can't read as
   price tracking/alerting functionality (Amazon Associates Participation
   Requirements 6(y): "your Site must not have price tracking and/or price
   alerting functionality", confirmed 2026-09-15 against the live policy
   page -- broader than "Amazon products only", applies site-wide since the
   site holds an Associates account regardless of what's currently shown). */
.verified-badge{color:var(--muted);font-size:11px;font-weight:600;text-decoration:none;
                 display:inline-flex;align-items:center;gap:3px}
.verified-badge:hover{color:var(--ink);text-decoration:underline}

.card .body{padding:14px 16px 16px;position:relative;flex:1;min-width:0;
            display:flex;flex-direction:column;gap:7px}
/* Was scoped to ".card .over-menu" -- unscoped from that ancestor 2026-08-20
   so Amazon Picks cards (.pick-lasso, not .card) and their durable share
   pages (.deal-page-body) get the same collapsed/positioned menu instead of
   an unstyled, permanently-expanded list of buttons. .over-menu is specific
   enough as a class name that this doesn't need a .card qualifier at all. */
.over-menu{position:absolute;top:10px;right:12px;width:28px;height:28px;
                  background:var(--card);border:1px solid var(--rule);border-radius:var(--r-md);
                  display:flex;align-items:center;justify-content:center;cursor:pointer;
                  font-size:18px;line-height:1;color:var(--ink);z-index:10;
                  transition:background var(--dur-base),border-color var(--dur-base)}
/* Same hover:hover/:active split as navBtn/megaBtn/catFilterBtn/picksCatBtn
   (2026-08-25 fix) -- this is the fifth click-toggle trigger, missed in that
   pass. Bare :hover on a toggle makes iOS Safari eat the first tap. */
@media(hover:hover){.over-menu:hover{background:var(--paper-3);border-color:var(--signal)}}
.over-menu:active{background:var(--paper-3);border-color:var(--signal)}
/* Invisible ring that widens the tap target to ~46px without growing the
   drawn button: a 28px control is under the 44px minimum for a fingertip, and
   this card is dense enough that a visibly bigger button would crowd the
   title. Sits only around the button, never over the dropdown below it. */
.over-menu::before{content:'';position:absolute;inset:-9px;border-radius:var(--r-sm)}
/* .dots is a real <button> so the menu is keyboard-operable; it fills the
   .over-menu box and inherits its look rather than drawing a second one. */
.over-menu .dots{position:absolute;inset:0;width:100%;height:100%;padding:0;margin:0;
                        background:none;border:0;font:inherit;color:inherit;line-height:1;
                        cursor:pointer;display:flex;align-items:center;justify-content:center}
.over-menu .dots:focus-visible,.over-menu .dropdown a:focus-visible,
.over-menu .dropdown button:focus-visible{outline:2px solid var(--signal);outline-offset:2px}
/* an OPEN menu must out-stack the ⋮ buttons of later cards -- its dropdown
   overlaps the card below, and z-index ties break by DOM order (later wins),
   which made Share unclickable on every card except the last row */
.over-menu.active{z-index:1000}
.over-menu .dropdown{display:block;position:absolute;top:100%;right:0;
                            background:var(--card);border:1px solid var(--rule);border-radius:var(--r-md);
                            min-width:180px;margin-top:6px;box-shadow:0 4px 12px rgba(0,0,0,.12);
                            z-index:20;opacity:0;visibility:hidden;transform:translateY(-4px);
                            transition:opacity var(--dur-fast) ease,transform var(--dur-fast) ease,visibility var(--dur-fast)}
.over-menu.active .dropdown{opacity:1;visibility:visible;transform:none}
.over-menu .dropdown a,.over-menu .dropdown button{
                              display:flex;align-items:center;gap:10px;padding:11px 14px;
                              width:100%;background:none;font-family:inherit;text-align:left;
                              border:0;color:var(--ink);text-decoration:none;font-size:13px;
                              font-weight:500;border-bottom:1px solid var(--rule);cursor:pointer;
                              transition:background var(--dur-base),color var(--dur-base)}
.over-menu .dropdown>*:last-child{border-bottom:none}
.over-menu .dropdown a:hover,.over-menu .dropdown button:hover{
                              background:var(--paper-3);color:var(--signal)}
.over-menu .has-sub{position:relative}
.over-menu .has-sub>button{border-bottom:none}
.over-menu .subdrop{display:block;position:absolute;right:100%;top:0;margin-right:4px;
                          background:var(--card);border:1px solid var(--rule);border-radius:var(--r-md);
                          min-width:130px;box-shadow:0 4px 12px rgba(0,0,0,.12);z-index:30;
                          opacity:0;visibility:hidden;transform:translateY(-4px);
                          transition:opacity var(--dur-fast) ease,transform var(--dur-fast) ease,visibility var(--dur-fast)}
.over-menu .has-sub:hover .subdrop,
.over-menu .has-sub.open .subdrop{opacity:1;visibility:visible;transform:none}

/* ---- variant picker: grouped cards (deal_grouping.py) open a stacked
   modal instead of a new tab, same mechanism DealSeek uses on the same
   "one part, many fitments" pattern -- confirmed live 2026-09-03: their
   swatch clicks push a NEW dialog on top rather than swapping the current
   one, so closing the top one reveals the previous choice still there
   underneath instead of losing it. #vmodalStack holds every open dialog in
   DOM order (later = on top, no z-index math needed); openVariantModal()/
   closeTopVariantModal() in interaction.js own the push/pop. */
.variant-pill{display:block;width:100%;margin:8px 0 0;padding:8px 10px;
               background:var(--paper-2);border:1px solid var(--rule);border-radius:var(--r-sm);
               color:var(--ink);font:600 12px/1 var(--display);text-align:center;cursor:pointer}
@media(hover:hover){.variant-pill:hover{border-color:var(--signal);color:var(--signal)}}
/* pointer-events:none here, :auto on the two children below -- #vmodalStack
   itself is never removed once created (only its children are, on close), so
   without this the full-viewport fixed container keeps swallowing every
   click on the ENTIRE page after the first close, invisibly (it is empty,
   nothing to see) but totally: confirmed live 2026-09-03, "vuelvo y nada en
   la pagina sirve" -- not just the pill, everything, including nav links
   nowhere near where the modal had been. */
#vmodalStack{position:fixed;inset:0;z-index:2000;pointer-events:none}
#vmodalStack .vmodal-backdrop,#vmodalStack .vmodal{pointer-events:auto}
/* Appearing/disappearing is a real state transition (a dialog entering or
   leaving the page), not decoration -- the global reduced-motion rule above
   already neutralizes both keyframes for anyone who asked for that. */
@keyframes vmodalFade{from{opacity:0}to{opacity:1}}
@keyframes vmodalPop{from{opacity:0;transform:translate(-50%,-50%) scale(.96)}
                      to{opacity:1;transform:translate(-50%,-50%) scale(1)}}
.vmodal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.5);
                  animation:vmodalFade var(--dur-med) ease}
.vmodal{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:1;
        width:min(920px,94vw);max-height:90vh;overflow:auto;background:var(--card);
        border:1px solid var(--rule);border-radius:var(--r-lg);
        box-shadow:0 20px 60px rgba(0,0,0,.35);padding:28px;
        animation:vmodalPop var(--dur-med) ease}
.vmodal-close{position:absolute;top:14px;right:14px;width:32px;height:32px;border-radius:50%;
              border:1px solid var(--rule);background:var(--paper-2);color:var(--ink);
              font-size:16px;line-height:1;cursor:pointer}
@media(hover:hover){.vmodal-close:hover{border-color:var(--signal);color:var(--signal)}}
.vmodal-body{display:flex;gap:32px;flex-wrap:wrap}
/* Gallery lives in its own column: a big frame plus a thumb rail underneath,
   same "main photo + strip" shape as every marketplace PDP (DealSeek
   included) -- confirmed live 2026-09-03. Only rendered when the variant
   actually has 2+ real photos (vDialogHTML), so a single-image listing just
   gets the plain frame, no empty rail. */
.vmodal-gallery{flex:0 0 360px;max-width:100%}
.vmodal-img{border-radius:var(--r-md);overflow:hidden;
            background:var(--paper-2);border:1px solid var(--rule);aspect-ratio:1/1;
            display:flex;align-items:center;justify-content:center}
.vmodal-img img{display:block;width:100%;height:100%;object-fit:contain}
.vmodal-thumbs{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.vmodal-thumb{width:56px;height:56px;padding:0;border-radius:var(--r-sm);overflow:hidden;
              background:var(--paper-2);border:2px solid var(--rule);cursor:pointer}
.vmodal-thumb img{display:block;width:100%;height:100%;object-fit:contain}
.vmodal-thumb.is-active{border-color:var(--signal)}
@media(hover:hover){.vmodal-thumb:not(.is-active):hover{border-color:var(--muted)}}
.vmodal-info{flex:1 1 340px;min-width:0}
.vmodal-title{font:700 18px/1.4 var(--display);color:var(--ink);margin:0 40px 12px 0}
.vmodal-prices{display:flex;align-items:baseline;gap:10px;margin:0 0 16px}
.vmodal-price-now{font:800 26px/1 var(--mono);color:var(--gate-dark)}
.vmodal-price-was{font:400 15px/1 var(--mono);color:var(--muted);text-decoration:line-through}
.vmodal-pct{background:var(--signal);color:#fff;font:700 11px/1 var(--display);
            padding:4px 8px;border-radius:var(--r-pill)}
.vmodal-cta{display:inline-block;padding:12px 24px;border-radius:var(--r-pill);
            background:var(--gate-dark);color:#fff;font:700 14px/1 var(--display);
            text-decoration:none;margin:0 0 22px}
@media(hover:hover){.vmodal-cta:hover{background:var(--gate)}}
.vmodal-swatch-label{font:700 11px/1 var(--display);letter-spacing:.04em;text-transform:uppercase;
                      color:var(--muted);margin:0 0 10px}
.vmodal-swatches{display:flex;gap:8px;flex-wrap:wrap}
.vmodal-swatch{width:56px;height:56px;padding:0;border-radius:var(--r-sm);overflow:hidden;
               background:var(--paper-2);border:2px solid var(--rule);cursor:pointer}
.vmodal-swatch img{display:block;width:100%;height:100%;object-fit:contain}
.vmodal-swatch.is-active{border-color:var(--signal)}
@media(hover:hover){.vmodal-swatch:not(.is-active):hover{border-color:var(--muted)}}
/* Similar Deals: same category/subcategory siblings already sitting in the
   grid's own DOM (readSimilarDeals() in interaction.js), so this costs no
   extra fetch -- just a second look at cards already on the page. */
.vmodal-similar{flex-basis:100%;margin-top:24px;padding-top:20px;border-top:1px solid var(--rule)}
.vmodal-similar-label{font:700 13px/1 var(--display);letter-spacing:.02em;
                       color:var(--ink);margin:0 0 12px}
.vmodal-similar-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:14px}
.vmodal-similar-card{display:block;text-decoration:none;color:inherit;border:1px solid var(--rule);
                      border-radius:var(--r-md);padding:10px;background:var(--paper-2)}
@media(hover:hover){.vmodal-similar-card:hover{border-color:var(--signal)}}
.vmodal-similar-card img{display:block;width:100%;aspect-ratio:1/1;object-fit:contain;
                          margin:0 0 8px;background:var(--card);border-radius:var(--r-sm)}
.vmodal-similar-title{font:600 12px/1.35 var(--display);color:var(--ink);margin:0 0 6px;
                       display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.vmodal-similar-price{font:700 13px/1 var(--mono);color:var(--gate-dark)}
@media(max-width:640px){
  .vmodal{padding:18px}
  .vmodal-gallery{flex-basis:100%}
}

.card h3{font-size:14px;line-height:1.4;font-weight:600;margin:0;color:var(--ink);padding-right:34px}
.card h3 a{color:inherit;text-decoration:none}
.card h3 a:hover{color:var(--signal)}
.clamp2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;max-height:2.8em}

/* Che's call, per Amazon/eBay's own price-emphasis convention (2026-08-24)
   -- the discount badge stays --signal, only the price itself goes green.
   --gate-dark in the base rule, not plain --gate: full-saturation --gate
   (#00a863) only clears 3.1:1 against a white card, failing WCAG AA's
   4.5:1 the same way it did as a button background. --gate-dark clears
   5.4:1. The dark-mode override below uses plain --gate instead, because
   in that theme it's the OPPOSITE problem: --gate-dark (tuned for a white
   background) only clears ~3.2:1 against the dark card, while dark mode's
   own brighter --gate (#1fbf7f) already clears 7.2:1 there unmodified. */
.prices{display:flex;align-items:center;gap:8px;margin:0;flex-wrap:wrap}
.price-now{font-size:18px;font-weight:800;letter-spacing:-.01em;font-variant-numeric:tabular-nums;color:var(--gate-dark)}
.price-was{font-size:12px;color:var(--muted);text-decoration:line-through;font-variant-numeric:tabular-nums;font-weight:500}
.card .meta{font-size:11px;color:var(--muted);font-family:var(--mono);margin:0;font-weight:500;
            display:flex;gap:8px;align-items:center;flex-wrap:wrap}

/* Green, not the brand orange: the price and the "% off" badge above are
   already --signal, and a same-color button just piled a third orange
   element into the same small area. Green marks the action distinctly
   from the deal info -- also the near-universal "buy/go" convention, and
   consistent with how this site already uses green (.newpill "New",
   .card.linked's ring). --gate-dark, not --gate: see its definition for
   why (2026-08-24, per Che's request to cut the orange overload here). */
.cta{display:inline-block;align-self:flex-start;text-align:center;padding:9px 20px;border-radius:999px;
     background:var(--gate-dark);color:#fff;font-weight:700;font-size:12.5px;text-decoration:none;
     border:none;cursor:pointer;transition:filter var(--dur-base),transform var(--dur-fast) ease-out;margin-top:2px}
.cta:hover{filter:brightness(.85)}
.cta:active{transform:scale(.97)}
/* Windows/browser forced-colors mode strips background-only buttons down to
   the page's default text colour with no visible edge -- .cta, .pct-pill,
   #catApply and #picksCatBtn all signal "button" purely through fill, so
   without this they'd be unreadable rectangles in that mode. Border-only;
   doesn't touch normal rendering. */
@media(forced-colors:active){
  .cta,.pct-pill,#catApply,#picksCatBtn{border:1px solid ButtonText}
}

.empty-state{padding:60px 12px;color:var(--muted);text-align:center;grid-column:1/-1}
/* Shown when a ?deal= link from a social post outlives its listing. Uses the
   signal colour because it is answering a question the visitor actually
   asked, not decorating the page. Was a 6px left stripe on a 2px border --
   the side-tab tell, plus a thicker-than-anywhere-else border; a uniform
   1px border plus a light signal tint gives it the same real weight
   without either (2026-09-05 cleanup, matches .disclosure's plain-border
   precedent from the 2026-08-24 polish). */
.deal-gone{margin:0 0 18px;padding:14px 16px;border:1px solid var(--signal);
           background:rgba(217,58,18,.08);color:var(--ink);
           font-size:14px;line-height:1.5}
.deal-gone b{display:block;margin-bottom:2px}
/* Appended by interaction.js's dealGoneNotice() only when the deal's durable
   /d/ page actually exists (HEAD-checked). Inline-block so it reads as an
   action rather than as more body copy. */
.deal-gone-link{display:block;width:fit-content;margin-top:10px;font-weight:600;
           color:var(--signal);text-decoration:underline;text-underline-offset:3px}
.deal-gone-link:hover,.deal-gone-link:focus-visible{text-decoration-thickness:2px}

/* max-width/margin/padding merged in directly (2026-09-05) -- footer used to
   inherit them for free by sitting inside <main class="wrap">, but that also
   meant it had no implicit `contentinfo` landmark (a <footer> nested inside
   <main> per the HTML-AAM spec doesn't get one). Moved footer to be a sibling
   of main instead, so it needs its own copy of .wrap's constraint. */
footer.site{max-width:1180px;margin:48px auto 0;padding:24px 28px 56px;
            border-top:2px solid var(--ink);font-size:14px;color:var(--muted)}
footer.site a{color:var(--gate);font-weight:600}
footer.site .runline{font:400 12px/1.6 var(--mono);color:var(--muted);margin-top:12px}
footer.site .footer-legal{margin-top:18px}
footer.site .disclosure{margin:14px 0}

/* ---- prose pages ---- */
.prose{max-width:68ch;margin:0 auto;padding:44px 0 0}
/* Archivo 900, not var(--display) (the system-font stack) -- every other
   heading/label on the site already runs on --display fine at UI sizes,
   but a prose H1 is the single largest, most prominent text on any blog
   page, and a generic system face reads as flat/cheap once you scale it up
   to display size. --brand-font (Archivo) is already self-hosted and
   loaded on every page for the wordmark, so this ties the blog's biggest
   moment to the one deliberate typographic choice the site already made,
   at zero extra network cost, instead of it defaulting to none at all
   (Che, 2026-09-05: "lo mas blando, generico" -- the H1 was the one place
   on the site with no real typographic identity). Tracking loosened from
   the old -.035em: that was tuned to compensate a thin system face at
   large size, Archivo's own weight doesn't need as much negative tracking
   to read as intentional. */
.prose h1{font-family:var(--brand-font);font-size:var(--fs-display);font-weight:900;
          letter-spacing:-.015em;line-height:1.1;margin:0 0 8px;text-wrap:balance}
.prose .lede{font-size:var(--fs-lede);color:var(--ink-soft);margin:0 0 30px}
/* scroll-margin-top on both (2026-09-05 fix): the sticky topnav (67px) was
   covering the heading a TOC/anchor link jumps to -- every one of a post's
   [[toc]] links landed the reader on a hidden heading with no way to tell
   why the page looked "empty" right after the jump. */
.prose h2{font-size:var(--fs-h2);font-weight:800;letter-spacing:-.015em;margin:40px 0 16px;
          scroll-margin-top:84px}
.prose h3{font-size:var(--fs-h3);font-weight:700;letter-spacing:-.01em;margin:26px 0 8px;
          scroll-margin-top:84px}
.prose p{margin:0 0 16px}
/* Boxed callouts (Key Takeaways, the [[toc]] jump-list). Was a left-accent
   stripe shared with .deal-gone's old language -- replaced 2026-09-05 with
   a uniform 1px border in the same two accent colors, so the two box types
   still read as visually distinct sitting back to back, without the
   side-tab tell. */
.prose .callout{margin:0 0 26px;padding:18px 20px 20px;background:var(--card);
                border:1px solid var(--signal);border-radius:var(--r-sm)}
.prose .callout.callout-toc{border-color:var(--gate)}
.prose .callout h2{margin:0 0 12px;font-size:1.15rem}
.prose .callout ul{margin:0}
.prose .callout li:last-child{margin-bottom:0}
.prose nav.post-toc ul{margin:0;padding-left:0;list-style:none;columns:1}
.prose nav.post-toc li{position:relative;padding-left:16px;margin-bottom:8px;font-size:.92rem}
.prose nav.post-toc li:last-child{margin-bottom:0}
.prose nav.post-toc li::before{content:'';position:absolute;left:0;top:.6em;width:8px;height:2px;
                    border-radius:999px;background:var(--gate)}
.prose nav.post-toc a{font-weight:600}
.prose ul{margin:0 0 16px;padding-left:0;list-style:none}
.prose li{position:relative;padding-left:20px;margin-bottom:10px}
.prose li::before{content:'';position:absolute;left:0;top:.62em;width:12px;height:2px;
                  border-radius:999px;background:var(--signal)}
.prose strong{font-weight:700;background:rgba(217,58,18,.14);padding:.05em .3em;
              border-radius:var(--r-sm);box-decoration-break:clone;-webkit-box-decoration-break:clone}
/* --gate-dark, not raw --gate (2026-09-05 fix): green-on-paper measured
   ~2.94:1 live, below WCAG AA's 4.5:1 floor for normal text -- the same
   contrast problem --gate-dark already exists to solve for white-on-green,
   just never applied to green-as-text-color until now. --gate-dark clears
   5.4:1 regardless of theme (see the Gate Rule note near :root's tokens). */
.prose a{color:var(--gate-dark);text-underline-offset:3px;font-weight:600;
         transition:color var(--dur-base) ease}
.prose a:hover{color:var(--signal-dark)}
.prose .stat{font-family:var(--mono);font-variant-numeric:tabular-nums}
.prose figure.post-img{margin:22px auto;padding:14px;background:var(--card);max-width:600px;
                       border:1px solid var(--rule);border-radius:var(--r-md);text-align:center}
.prose figure.post-img img{max-width:600px;width:100%;height:auto;border-radius:var(--r-sm);display:block;margin:0 auto}
.prose figure.post-img figcaption{margin-top:10px;font-size:.85rem;color:var(--ink-soft)}
/* Full-width: the infographic is already a designed, self-contained dark
   card (own frame, own padding) -- the 280px product-photo cap and card
   wrapper above would just shrink it into a postage stamp. */
.prose figure.post-infographic{margin:26px 0}
.prose figure.post-infographic img{width:100%;height:auto;border-radius:var(--r-md);display:block}
.prose figure.post-hero{margin:0 0 26px}
/* aspect-ratio (2026-09-05 fix): this is the post's LCP candidate
   (fetchpriority=high in blog.py) and the one image on the page with no
   intrinsic-size hint before it decodes -- every other image type here
   (cards, covers) already reserves space via aspect-ratio. */
.prose figure.post-hero img{width:100%;height:auto;aspect-ratio:16/9;
                             object-fit:cover;border-radius:var(--r-md);display:block}
.prose table{width:100%;margin:0 0 22px;border-collapse:collapse;font-size:.92rem}
.prose th,.prose td{padding:10px 12px;text-align:left;border-bottom:1px solid var(--rule)}
.prose th{font:600 11px/1 var(--mono);letter-spacing:.06em;text-transform:uppercase;
          color:var(--muted);border-bottom:2px solid var(--ink)}
.prose td{font-variant-numeric:tabular-nums}
.prose tbody tr:hover{background:var(--paper-2)}
/* [[compare:left|...]]/[[compare:right|...]] -- a real discounted item next
   to a real regular-price (or lesser-discount) comparison item, both real
   affiliate links. Deliberately plainer than the site's main .card (no
   hover ring, no grid-item sizing assumptions) since this lives inside
   prose, not the deals grid -- see half-or-nothing-price-comparison-
   monetization-idea memory for why this exists and stays blog-only. */
.prose-compare{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:0 0 26px}
@media (max-width:560px){.prose-compare{grid-template-columns:1fr}}
/* Same grid shape as .pick-lasso .embed-lasso-display (photo left, title+
   price top right, button below, disclosure below that) -- Che, 2026-09-02:
   the eBay card had the photo on top and everything below it, unlike
   Amazon's side-by-side layout, and wanted them structurally the same.
   Colors deliberately do NOT match Amazon's: eBay keeps the site's normal
   green price/button (--gate-dark, the same as every eBay/Walmart deal
   card), Amazon keeps its own orange Buy button and red price -- only the
   6px accent border is shared between the two systems. */
.compare-card{position:relative;padding:18px;background:var(--card);border:1px solid var(--rule);
              border-radius:var(--r-md);text-align:left;display:grid;grid-template-columns:200px 1fr;
              grid-template-areas:"img text" "img btn" "img meta";column-gap:14px;row-gap:4px;
              align-items:center}
@media (max-width:480px){
  .compare-card{grid-template-columns:1fr;
                grid-template-areas:"img" "text" "btn" "meta";justify-items:center;text-align:center}
}
/* [[shop:...]] only (not the [[compare:left/right]] pair, where a thick
   accent border on both sides would misleadingly frame the non-discounted
   "Regular Price" side as a hot deal too) -- matches the Lasso Amazon Pick
   card's own 6px brand-red border exactly (Che, 2026-09-02). */
.compare-card-solo{border:6px solid var(--signal)}
/* Same 200px paper-framed photo box as .picks-page-grid-lasso .pick-lasso
   (see the block above) -- same size, same border/radius/background, so an
   Amazon Pick and an eBay [[shop:...]] card in the same post read as one
   consistent product-card language, not two different systems. */
.compare-card img{grid-area:img;width:200px;height:200px;object-fit:contain;border-radius:var(--r-md);
                  border:1px solid var(--rule);background:var(--paper-3);align-self:start}
.compare-badge{position:absolute;top:12px;left:12px;padding:4px 10px;border-radius:999px;
               font:700 11px/1 var(--mono);letter-spacing:.02em}
.compare-badge-off{background:var(--signal);color:#fff}
.compare-badge-reg{background:var(--paper-3);color:var(--muted);border:1px solid var(--rule)}
.compare-text{grid-area:text;align-self:center}
.compare-title{font-size:14px;font-weight:600;color:var(--ink);margin:0;line-height:1.35}
/* Site's normal deal-card price color (--gate-dark, same as .price-now/
   .td-now everywhere else), not Amazon's red -- eBay pricing keeps its own
   established identity. .price-was/.pct-pill are the same markup the Lasso
   card uses for a real discount, just recolored to match. */
.compare-price{font-size:20px;font-weight:800;color:var(--gate-dark);
               margin:2px 0 0;font-variant-numeric:tabular-nums;display:flex;align-items:baseline;
               gap:8px;flex-wrap:wrap}
.compare-card .cta{grid-area:btn;margin-top:4px}
/* .prose a{color:var(--gate-dark)} (blog body links) has higher specificity
   than .cta{color:#fff} (element+class beats a lone class) and was winning,
   turning the button's text the same --gate-dark as its own background --
   invisible in light mode, barely-there in dark. The dark-mode override
   (:not([data-theme="light"]) .prose a{color:var(--gate)}) is even more
   specific (0,2,1), so this needs div.prose a.cta at 0,2,2 to clear both,
   not just .prose .cta (Che caught this on the Karhu post, 2026-09-09). */
div.prose a.cta{color:#fff}
.compare-disclosure{grid-area:meta;font-size:10.5px;color:var(--muted);margin:2px 0 0}

/* ---- blog intro banner ---- */
/* The blog's breadcrumb/H1/lede render INSIDE .mast's own wrap div (passed
   via page_shell()'s stats_html slot, same mechanism the deals index uses
   for its live-stats line) instead of a separately-styled block below it.
   First attempt (2026-09-05) recreated .mast's gradient on a second,
   width-constrained box further down the page -- .mast itself is
   full-bleed (edge to edge) but that box sat inside <main class="wrap">
   (1180px, padded), so the two gradients were positioned against
   different-sized boxes and never lined up: a patchy rectangle, not a
   continuation (Che: "el banner esta completamente roto"). Reusing
   .mast's actual background instead of copying it means there is nothing
   to keep in sync and no seam to line up -- it's the same element. */
.mast .lede{font-family:var(--editorial-font);font-style:italic;font-weight:600;
            font-size:1.3rem;color:var(--ink);letter-spacing:-.005em;max-width:70ch;
            margin:10px 0 0}

/* ---- blog index ---- */
/* Full .wrap width (1180px) instead of one centered 68ch column, so the
   index doesn't leave most of a desktop viewport empty. Two references,
   synthesized rather than copied: dealseek.com/deal-news (a featured card
   paired with a few more, sections below) and koupon.ai/articles/deal-news
   (categories as side-by-side columns instead of one long vertical stack,
   which is the part that actually solves "too much scroll between
   categories" -- Che, 2026-09-05). */
.blog-search{position:relative;max-width:420px;margin:8px 0 28px}
.blog-search svg{position:absolute;left:14px;top:50%;transform:translateY(-50%);
                  width:18px;height:18px;color:var(--muted);pointer-events:none}
.blog-search input{width:100%;box-sizing:border-box;padding:11px 14px 11px 42px;
                    border:1px solid var(--rule);border-radius:var(--r-sm);
                    background:var(--card);color:var(--ink);font-size:.95rem;
                    transition:border-color .15s,box-shadow .15s}
.blog-search input:hover{border-color:var(--muted)}
.blog-search input:focus{outline:none;border-color:var(--signal);
                          box-shadow:0 0 0 3px rgba(217,58,18,.12)}
.blog-search-empty{display:none;color:var(--ink-soft);margin:-8px 0 28px}

/* Spotlight: one big featured story (the latest post) beside a short stack
   of the next few, site-wide -- the one asymmetric moment on an otherwise
   dense, grid-driven page. */
.blog-spotlight{display:grid;grid-template-columns:1.6fr 1fr;gap:24px;margin:0 0 40px;
                 animation:blog-spotlight-in .5s cubic-bezier(.23,1,.32,1)}
@keyframes blog-spotlight-in{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}
@media(max-width:860px){.blog-spotlight{grid-template-columns:1fr}}
.blog-featured{display:grid;grid-template-columns:1fr;border:1px solid var(--rule);
                border-radius:var(--r-lg);overflow:hidden;background:var(--card);
                transition:border-color var(--dur-base) ease,box-shadow var(--dur-base) ease}
/* The biggest, most prominent link on the page had zero press feedback
   before this (2026-09-05 fix) -- no :hover, no :active, nothing -- so a
   touch tap gave no acknowledgment before navigating. It's an <article>, not
   a link itself, so only :active applies (bubbles up from the inner <a> on
   press); the border/shadow language matches .blog-card-compact's own. */
.blog-featured:active{border-color:var(--muted);box-shadow:0 4px 14px rgba(0,0,0,.1)}
@media(hover:hover){.blog-featured:hover{border-color:var(--muted);box-shadow:0 4px 14px rgba(0,0,0,.1)}}
.blog-featured-cover{display:block;aspect-ratio:16/9;background:var(--paper-3)}
.blog-featured-cover img{width:100%;height:100%;object-fit:cover;display:block;
                          transition:transform .3s}
.blog-featured:active .blog-featured-cover img{transform:scale(1.03)}
@media(hover:hover){.blog-featured:hover .blog-featured-cover img{transform:scale(1.03)}}
.blog-featured-body{padding:20px 24px 24px}
.blog-featured-title{display:block;font-size:var(--fs-title-lg);font-weight:800;letter-spacing:-.015em;
                      line-height:1.2;color:var(--ink);text-decoration:none}
.blog-featured-title:hover{color:var(--signal)}
.blog-featured-desc{margin:10px 0 0;color:var(--ink-soft);max-width:60ch}
.blog-featured-meta{margin:12px 0 0;font:500 11px/1 var(--mono);letter-spacing:.06em;
                     text-transform:uppercase;color:var(--muted)}
/* Was justify-content:space-between: with exactly 3 cards and no
   description, that stretched them into 3 visibly separated islands with
   big dead gaps between -- flagged by the visual audit and by Che directly
   (2026-09-05). Real content (bigger cover + a description, via
   _blog_row_compact's show_desc=True) plus a plain top-aligned gap gives
   each card enough height to close most of that gap on its own, instead of
   faking it with distributed whitespace. */
.blog-spotlight-secondary{display:flex;flex-direction:column;gap:20px}
.blog-spotlight-secondary .blog-card-compact{display:flex;align-items:flex-start;gap:14px}
.blog-spotlight-secondary .blog-card-cover{flex-shrink:0;width:104px;height:104px;aspect-ratio:auto}
.blog-spotlight-secondary .blog-card-body{padding:0}
.blog-spotlight-secondary .blog-card-title{padding:0;font-size:.95rem}
.blog-spotlight-secondary .blog-card-desc{margin:6px 0 0;font-size:.82rem;color:var(--ink-soft);
                                           display:-webkit-box;-webkit-line-clamp:2;
                                           -webkit-box-orient:vertical;overflow:hidden}
.blog-spotlight-secondary .blog-card-date{padding:6px 0 0}

/* --fs-h2, not its own literal (2026-09-05 consolidation): this was 1.4rem,
   the audit's flagship "scale drift" example -- a second, independently
   declared "section heading" size only 6% from .prose h2's 1.32rem. Same
   role, now the same token. */
.blog-section-title{font-size:var(--fs-h2);font-weight:800;letter-spacing:-.015em;margin:0 0 20px}
/* One column per category, side by side -- this (not a taller single-file
   list) is what actually cuts the scroll: every category is visible in the
   same viewport instead of one below the other. Fixed column counts per
   breakpoint (not auto-fit) on purpose: auto-fit wraps a short category
   (1-2 posts) onto the same row as a full one (3 posts), and a CSS grid
   row's height always matches its tallest cell -- so the short column sits
   inside a tall row-track with visible dead space under its own content,
   before the next row even starts. Five categories fit one row at .wrap's
   1180px, so this keeps every category in a single row there and avoids
   the mismatched-row-height gap entirely for the common desktop case
   (Che caught this 2026-09-05: empty space between Gift Guides and the
   Trust & How It Works row below it). */
.blog-category-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:28px;align-items:start}
@media(max-width:1100px){.blog-category-grid{grid-template-columns:repeat(3,1fr)}}
@media(max-width:700px){.blog-category-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:480px){.blog-category-grid{grid-template-columns:1fr}}
.blog-category-col-head{display:flex;align-items:baseline;justify-content:space-between;
                         gap:8px;margin:0 0 12px;padding-bottom:10px;border-bottom:2px solid var(--rule)}
.blog-category-title{font-size:var(--fs-title-sm);font-weight:800;letter-spacing:-.005em;margin:0}
/* The title used to be plain text while "View all" right below it was a
   link -- felt inconsistent (Che, 2026-09-05). Every category now has a
   real page (see build_all()'s comment) so the title links there too. */
.blog-category-title a{color:inherit;text-decoration:none}
.blog-category-title a:active{color:var(--signal)}
@media(hover:hover){.blog-category-title a:hover{color:var(--signal)}}
.blog-category-count{font:500 10px/1 var(--mono);letter-spacing:.05em;
                      text-transform:uppercase;color:var(--muted);white-space:nowrap}
.blog-category-col-list{display:flex;flex-direction:column;gap:12px}
.blog-category-more{display:inline-block;margin-top:12px;font-weight:600;color:var(--signal);
                     text-decoration:none;font-size:.88rem}
.blog-category-more:hover{text-decoration:underline}
.blog-category-more i{font-style:normal;display:inline-block;transition:transform var(--dur-med) ease}
.blog-category-more:hover i{transform:translateX(3px)}
.blog-category-note{margin:12px 0 0;font-size:.85rem;font-style:italic;color:var(--muted)}
.blog-category-desc{margin:0 0 12px;font-size:.85rem;color:var(--muted)}

/* Compact card -- every post inside a category column, and the spotlight's
   secondary list (sized differently there via the overrides above). */
.blog-card-compact{display:block;border:1px solid var(--rule);border-radius:var(--r-md);
                    overflow:hidden;background:var(--card);transition:border-color .15s}
.blog-card-compact:active{border-color:var(--muted);transform:translateY(1px)}
@media(hover:hover){.blog-card-compact:hover{border-color:var(--muted)}}
.blog-card-cover{display:block;aspect-ratio:16/9;background:var(--paper-3);overflow:hidden}
.blog-card-cover img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .3s}
.blog-card-compact:active .blog-card-cover img{transform:scale(1.04)}
@media(hover:hover){.blog-card-compact:hover .blog-card-cover img{transform:scale(1.04)}}
.blog-card-body{padding:10px 12px}
.blog-card-title{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
                  overflow:hidden;font-size:var(--fs-title-xs);font-weight:700;
                  color:var(--ink);text-decoration:none;letter-spacing:-.003em;line-height:1.35}
.blog-card-title:hover{color:var(--signal)}
.blog-card-date{margin:6px 0 0;font:500 10px/1 var(--mono);letter-spacing:.05em;
                 text-transform:uppercase;color:var(--muted)}

/* The original row layout -- still used by author pages and category
   archive pages, which list posts one at a time with a real description. */
.blog-list{max-width:68ch;margin:8px auto 40px}
.blog-row{padding:20px 0;border-top:1px solid var(--rule);display:flex;gap:18px;align-items:flex-start}
.blog-row:last-child{border-bottom:1px solid var(--rule)}
.blog-row-cover{flex-shrink:0;display:block;width:140px;height:74px;border-radius:var(--r-sm);
                 overflow:hidden;border:1px solid var(--rule)}
.blog-row-cover img{width:100%;height:100%;object-fit:cover;display:block}
.blog-row-body{min-width:0}
.blog-row-title{display:block;font-size:var(--fs-title-md);font-weight:700;color:var(--ink);
                 text-decoration:none;letter-spacing:-.01em}
.blog-row-title:hover{color:var(--signal)}
.blog-row-desc{margin:6px 0 0;color:var(--ink-soft)}
.blog-row-date{margin:6px 0 0;font:500 11px/1 var(--mono);letter-spacing:.06em;
                text-transform:uppercase;color:var(--muted)}

/* "You might also like" -- a post used to end straight into the sitewide
   footer with no recirculation at all (2026-09-05 fix, see render_post()'s
   own comment). Full .wrap width (not .prose's 68ch) so it can run 3 real
   cards across, same visual language as the index's category-grid cards. */
.blog-store-cta{max-width:68ch;margin:28px auto 8px;padding:16px 20px;border:1px solid var(--rule);
                 border-radius:10px;display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap}
.blog-store-cta p{margin:0;font-size:.95rem;color:var(--ink-soft)}
.blog-store-cta .picks-follow-cta{font-size:12px;padding:9px 18px;white-space:nowrap}
.blog-related{max-width:1180px;margin:8px auto 40px}
.blog-related-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}
@media(max-width:860px){.blog-related-grid{grid-template-columns:1fr}}
.blog-related .blog-card-desc{margin:6px 0 0;font-size:.82rem;color:var(--ink-soft);
                               display:-webkit-box;-webkit-line-clamp:2;
                               -webkit-box-orient:vertical;overflow:hidden}

/* ---- blog author profile ---- */
.author-head{display:flex;gap:16px;align-items:center;margin:8px 0 20px}
.author-avatar{width:64px;height:64px;border-radius:50%;object-fit:cover;flex-shrink:0;
                border:1px solid var(--rule)}
.author-avatar-initials{display:flex;align-items:center;justify-content:center;
                         background:var(--signal);color:#fff;font-weight:800;
                         font-size:1.3rem;letter-spacing:.02em}
.author-head h1{margin:0;font-size:clamp(1.6rem,4vw,2.1rem)}
.author-bio{max-width:64ch;color:var(--ink-soft);margin:0 0 34px}

a:focus-visible,button:focus-visible{outline:2px solid var(--signal);outline-offset:2px;border-radius:var(--r-sm)}

@media (max-width:760px){
  .wrap{padding:0 18px}
  .topnav .inner{padding:10px 18px}
  /* must live here, after #megaBtn's base rule: equal specificity, so source
     order decides and an override placed earlier silently loses */
  #megaBtn{padding:15px 18px}
  .mast{padding:28px 0 26px}
  .disclosure{font-size:13px}
  .disclosure>summary{padding:12px 14px}
  .disclosure-body{padding:0 14px 14px}
  .herosearch{max-width:none}
  .pager{gap:10px}
  /* was 22px here -- shrinking the one control on the card that fingers have
     to hit was backwards; keep it full size on phones and let ::before carry
     the tap area past it */
  .over-menu{width:28px;height:28px;font-size:17px}
}
/* ---- brand motif: the diagonal cut from the logo, echoed in chips + CTAs ---- */
/* the diagonal notch was dropped 2026-08-10: against 16-24px rounded corners
   it read as a rendering glitch rather than the logo's cut. The mark itself
   still carries the motif. */
::selection{background:var(--signal);color:#fff}
html{scroll-behavior:smooth}

/* ---- Today's Top Deals rail: portrait cards on a snap-scrolling track ----
   Native CSS scroll-snap, not a carousel library: the page is one self-contained
   static file and the Artifact/GitHub Pages copy can't pull a script off a CDN,
   plus native scroll gives touch swipe and keyboard scrolling for free. The
   arrows just call scrollBy() on the same element the finger already drives. */
.topdeals{margin:28px 0 4px;background:var(--card);border:2px solid var(--signal);
          border-radius:var(--r-lg);padding:20px 22px 22px}
@media(max-width:640px){.topdeals{padding:16px 14px 18px}}
.td-head{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:14px}
.td-head h2{font-size:clamp(1.25rem,3vw,1.6rem);font-weight:800;letter-spacing:-.015em;margin:0;color:var(--ink)}
.td-count{font:500 12px/1 var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}

.td-rail{position:relative}
/* Right-edge fade hinting "swipe for more" -- on mobile the scroll arrows
   are hidden (.td-arrow{display:none} below 820px) and the scrollbar is
   hidden everywhere (scrollbar-width:none above), so a card cut off flush
   at the container edge was the ONLY visual signal a rail scrolls at all --
   read as a broken/cut-off layout rather than "swipe for more" (Che,
   2026-09-14, seen on his own phone). This fade is the replacement signal;
   only shown where the arrows are hidden, so desktop is unaffected. */
@media(max-width:820px){
  .td-rail::after,.br-rail::after{content:'';position:absolute;top:0;right:0;
    bottom:0;width:28px;background:linear-gradient(to right,transparent,var(--card));
    pointer-events:none}
}
.td-viewport{overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;
             scroll-behavior:smooth;scrollbar-width:none;-ms-overflow-style:none;
             margin:0 -4px;padding:4px}
.td-viewport::-webkit-scrollbar{display:none}
.td-track{display:flex;gap:14px;width:max-content;padding-bottom:2px}

.td-card{scroll-snap-align:start;flex:0 0 218px;width:218px;position:relative;
         display:flex;flex-direction:column;text-decoration:none;color:inherit;
         background:var(--card);border:1px solid var(--rule);border-radius:var(--r-lg);
         box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden;
         transition:transform var(--dur-med) ease,box-shadow var(--dur-med) ease,border-color var(--dur-med) ease}
.td-card:hover,.td-card:focus-visible{transform:translateY(-4px);
         box-shadow:0 10px 22px rgba(0,0,0,.16);border-color:var(--signal);outline:none}

.td-cat{position:absolute;top:8px;left:8px;z-index:2;font:700 11px/1 var(--mono);
        letter-spacing:.07em;text-transform:uppercase;color:#fff;
        background:rgba(26,26,26,.78);padding:5px 9px;border-radius:999px;
        backdrop-filter:blur(2px);max-width:calc(100% - 16px);
        overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

.td-shot{display:block;height:168px;background:#fff;border-bottom:1px solid var(--rule);
         overflow:hidden;position:relative}
.td-shot img{width:100%;height:100%;object-fit:contain;display:block;padding:10px;
             transition:transform .25s ease}
.td-card:hover .td-shot img{transform:scale(1.06)}
.td-noimg{display:flex;align-items:center;justify-content:center;height:100%;
          color:#bbb;font:400 11px/1 var(--mono);background:var(--paper-3)}

.td-body{display:flex;flex-direction:column;gap:8px;padding:11px 12px 12px;flex:1}
.td-title{font-size:13px;line-height:1.35;font-weight:600;color:var(--ink);
          display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
          overflow:hidden;min-height:2.7em}
.td-card:hover .td-title{color:var(--signal)}
.td-prices{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap;margin-top:auto}
.td-now{font:800 17px/1 var(--mono);color:var(--gate-dark);font-variant-numeric:tabular-nums}
.td-was{font:400 12px/1 var(--mono);color:var(--muted);font-variant-numeric:tabular-nums}
/* No background here on purpose -- every .td-off always also carries one of
   the pct-t1..pct-t5 tier classes (see _pct_tier_class()), which is what
   actually paints it. Keeping a background here too would need to win a
   same-specificity cascade fight against rules declared earlier in the
   file, for a value that's always immediately overridden anyway. */
.td-off{font:800 11px/1 var(--mono);color:#fff;
        padding:5px 8px;border-radius:999px;letter-spacing:.03em;white-space:nowrap}
.td-src{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
        color:var(--muted)}

.td-arrow{position:absolute;top:84px;transform:translateY(-50%);z-index:5;
          width:38px;height:38px;border-radius:50%;cursor:pointer;
          background:var(--card);border:1px solid var(--rule);color:var(--ink);
          font:400 22px/1 var(--display);display:flex;align-items:center;justify-content:center;
          box-shadow:0 2px 10px rgba(0,0,0,.16);
          transition:background var(--dur-base) ease,color var(--dur-base) ease,border-color var(--dur-base) ease,opacity var(--dur-base) ease}
.td-arrow:hover{background:var(--signal-dark);border-color:var(--signal-dark);color:#fff}
.td-arrow:focus-visible{outline:2px solid var(--signal);outline-offset:2px}
.td-arrow[disabled]{opacity:0;pointer-events:none}
.td-prev{left:-13px}
.td-next{right:-13px}
@media(max-width:820px){
  .td-card{flex:0 0 168px;width:168px}
  .td-shot{height:132px}
  .td-arrow{display:none}
  .td-viewport{scroll-snap-type:x proximity}
}

/* ---- From the Blog rail: quieter cousin of .topdeals ----
   Neutral border instead of --signal orange (that color means "deal" on
   every other section of the page) and no scroll arrows -- 4 cards is
   never more than a short swipe/scroll, and this section is meant to sit
   in the background next to the grid, not compete with it for attention. */
.blograil{margin:20px 0 4px;background:var(--card);border:1px solid var(--rule);
          border-radius:var(--r-lg);padding:16px 18px 18px}
@media(max-width:640px){.blograil{padding:14px 12px 16px}}
.blograil .td-head{margin-bottom:12px}
.blograil .td-head h2{font-size:clamp(1.05rem,2.4vw,1.3rem)}
.br-viewall{font:600 12px/1 var(--mono);color:var(--muted);text-decoration:none;
            margin-left:auto;white-space:nowrap}
.br-viewall:hover{color:var(--signal)}
.br-rail{position:relative}
.br-viewport{overflow-x:auto;overflow-y:hidden;scroll-snap-type:x proximity;
          scrollbar-width:none;-ms-overflow-style:none;margin:0 -4px;padding:4px}
.br-viewport::-webkit-scrollbar{display:none}
.br-card{scroll-snap-align:start;flex:0 0 168px;width:168px;position:relative;
         display:flex;flex-direction:column;text-decoration:none;color:inherit;
         background:var(--card);border:1px solid var(--rule);border-radius:var(--r-lg);
         overflow:hidden;transition:transform var(--dur-med) ease,border-color var(--dur-med) ease}
.br-card:hover,.br-card:focus-visible{transform:translateY(-3px);
         border-color:var(--muted);outline:none}
.br-card .td-shot{height:110px}
.br-card .td-title{font-size:12.5px;min-height:2.6em}
.br-meta{font:500 10.5px/1 var(--mono);letter-spacing:.02em;color:var(--muted);
         text-transform:uppercase;margin-top:auto}

/* ---- category landing pages: breadcrumb + fashion subnav ---- */
.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
                 overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
/* Keyboard-only skip link -- first focusable element in the document, so a
   keyboard/screen-reader user doesn't have to tab through the full nav +
   search + hamburger on every single page load just to reach the content. */
.skip-link{position:absolute;top:-48px;left:8px;z-index:1000;background:var(--signal);
           color:#fff;padding:10px 16px;border-radius:var(--r-sm);font-weight:700;
           text-decoration:none;transition:top var(--dur-base) ease}
.skip-link:focus{top:8px}
.crumbs{margin:26px 0 2px;font-size:13px;color:var(--muted)}
.crumbs a{color:var(--muted);text-decoration:none}
.crumbs a:hover{color:var(--signal)}
.crumbs b{color:var(--ink);font-weight:600}
.cat-intro{margin:14px 0 6px;max-width:78ch}
.cat-intro h2{font:700 22px/1.25 var(--display);color:var(--ink);margin:0 0 8px}
.cat-intro p{font:400 15px/1.6 var(--display);color:var(--ink-soft);margin:0}
.cat-intro a{color:var(--signal);text-decoration:none;border-bottom:1px solid currentColor}
.cat-intro a:hover{color:var(--signal-dark)}
/* Phone homepage (SEO audit 2026-09-20): the answer-first intro pushed the first deal card to
   y=903 on a 375x812 screen, so no deal was visible without scrolling. Visual order only: the
   intro stays first in the HTML for crawlers and LLMs, the Top Deals rail is pulled above it. */
@media (max-width:600px){
  main.wrap:has(> .topdeals){display:flex;flex-direction:column}
  main.wrap:has(> .topdeals) > *{min-width:0}
  main.wrap:has(> .topdeals) > .topdeals{order:-1}
}
/* Category page SEO sections (How We Verify / Why 50% / FAQ / Related) --
   same type ramp as .cat-intro, just spaced out as separate blocks below
   the grid instead of one intro paragraph (Task 2, Che's competitor-gap
   SEO review, 2026-09-20). */
.cat-seo-section{margin:28px 0;max-width:78ch}
.cat-seo-section h2{font:700 20px/1.25 var(--display);color:var(--ink);margin:0 0 8px}
.cat-seo-section p{font:400 15px/1.6 var(--display);color:var(--ink-soft);margin:0 0 8px}
.cat-seo-section a{color:var(--signal);text-decoration:none;border-bottom:1px solid currentColor}
.cat-seo-section a:hover{color:var(--signal-dark)}
.cat-seo-section details{margin:0 0 10px;font:400 15px/1.5 var(--display);color:var(--ink-soft)}
.cat-seo-section summary{font-weight:600;color:var(--ink);cursor:pointer}
.cat-seo-section.related-cats p{display:flex;flex-wrap:wrap;gap:8px 16px}
/* brands.html's full brand-hub link list -- same idea as related-cats but
   many more items, so a tighter gap and smaller type keep it from running
   pages long (Task 3, Che's competitor-gap SEO review, 2026-09-20). */
.brand-hub-links{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:13.5px}
.site-faq{margin:22px 0;max-width:78ch}
.site-faq h2{font:700 22px/1.25 var(--display);color:var(--ink);margin:0 0 12px}
.site-faq details{border-top:1px solid var(--rule);padding:12px 0}
.site-faq details:last-of-type{border-bottom:1px solid var(--rule)}
.site-faq summary{font:600 15px/1.4 var(--display);color:var(--ink);cursor:pointer;list-style:none}
.site-faq summary::-webkit-details-marker{display:none}
.site-faq summary:after{content:'+';float:right;color:var(--muted);font-weight:400}
.site-faq details[open] summary:after{content:'2'}
.site-faq details p{font:400 15px/1.6 var(--display);color:var(--ink-soft);margin:8px 0 0}
.site-faq details a{color:var(--signal);text-decoration:none;border-bottom:1px solid currentColor}
/* Pill-style subnav rows -- Shop / Shop by price / Shop by type. Same
   border-radius:999px pill language as #dealSort ("Highest discount") and
   the category filter button, so this reads as the same control family
   instead of a plain link list a fast-scanning eye slides right past. Price
   and type get their own accent (--signal red, --gate green -- both already
   load-bearing colors on the page: --signal is the %-off badge, --gate is
   this row's own original link color) so the two rows are visually distinct
   from each other, not just distinct in label text. Added 2026-08-22. */
.subnav{margin:10px 0 0;font-size:13px;display:flex;flex-wrap:wrap;align-items:center;gap:8px}
.subnav-label{font-weight:800;color:var(--muted);text-transform:uppercase;
              letter-spacing:.03em;font-size:11.5px;margin-right:2px}
.subnav a{display:inline-block;font-weight:700;text-decoration:none;border-radius:999px;
          padding:7px 14px;border:1.5px solid var(--rule);color:var(--ink-soft);
          background:var(--card)}
.subnav a:hover{transform:translateY(-1px)}
.subnav a.active{background:var(--ink-soft);color:var(--card);border-color:var(--ink-soft)}
.subnav-price a{border-color:var(--signal);color:var(--signal-dark)}
.subnav-price a:hover,.subnav-price a.active{background:var(--signal);color:#fff}
.subnav-type a{border-color:var(--gate);color:var(--gate)}
.subnav-type a:hover,.subnav-type a.active{background:var(--gate);color:#fff}
.footer-cats{font-size:12.5px;line-height:2;color:var(--muted);max-width:80ch}
.footer-cats a{color:var(--muted);font-weight:500}
.footer-cats a:hover{color:var(--signal)}

.back-to-top{position:fixed;right:20px;bottom:20px;z-index:60;
             width:44px;height:44px;border-radius:50%;cursor:pointer;
             background:var(--card);border:1px solid var(--rule);color:var(--ink);
             font:400 20px/1 var(--display);display:flex;align-items:center;justify-content:center;
             box-shadow:0 2px 10px rgba(0,0,0,.16);
             opacity:0;visibility:hidden;transform:translateY(8px);
             transition:background var(--dur-base) ease,color var(--dur-base) ease,border-color var(--dur-base) ease,
                        opacity .2s ease,transform .2s ease,visibility .2s}
.back-to-top.show{opacity:1;visibility:visible;transform:none}
.back-to-top:hover{background:var(--signal-dark);border-color:var(--signal-dark);color:#fff}
.back-to-top:focus-visible{outline:2px solid var(--signal);outline-offset:2px}

/* ---- dark mode: same identity, inverted paper ---- */
/* Dark theme (2026-09-05): now reachable two ways -- the OS preference
   below, or the explicit nav toggle (#themeToggle in interaction.js, which
   sets data-theme on <html> and remembers it in localStorage). Every rule
   in the OS-preference block is guarded with :not([data-theme="light"]) so
   an explicit "light" pick can override a dark OS, and mirrored verbatim
   under :root[data-theme="dark"] (no media query) so an explicit "dark"
   pick works even on a light-OS visitor. Same content twice is the
   documented pattern for this exact "manual override + system preference"
   case -- a media query can't be satisfied by an attribute, so there is no
   single-selector way to gate it once. */
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){color-scheme:dark;--card:#1C1C1A;--paper-2:#131312;--paper-3:#232320;--ink:#ECEAE4;--ink-soft:#C7C5BD;
        --muted:#9B998F;--rule:#2D2D29;--signal:#FF4F29;--signal-dark:#D93A12;--gate:#1FBF7F}
  :root:not([data-theme="light"]) .thumb,:root:not([data-theme="light"]) .best-img{background:#fff}
  :root:not([data-theme="light"]) .pct-pill,:root:not([data-theme="light"]) .newpill{color:#161511}
  /* .cta used to be bundled into this same dark-mode override -- the actual
     bug behind "the button still shows orange" (2026-08-24): the base
     stylesheet's .cta{background:var(--gate-dark)} was correct, but this
     rule, matching the identical selector at higher specificity via the
     dark-mode media query, silently put it back to orange whenever the
     visitor's OS was in dark mode. --gate-dark needs no theme override in
     the first place -- it's white text against a fixed literal background
     color, not text against the theme's own card color, so light and dark
     mode give the exact same 5.4:1 either way. #catApply (the category
     filter's Apply button, unrelated to the deal CTA) keeps its own
     dark-mode treatment; only .cta is removed from it. */
  :root:not([data-theme="light"]) #catApply{background:var(--signal-dark)}
  :root:not([data-theme="light"]) #catApply:hover{background:#A82D0C}
  :root:not([data-theme="light"]) .price-now{color:var(--gate)}
  :root:not([data-theme="light"]) .td-now{color:var(--gate)}
  :root:not([data-theme="light"]) .compare-price{color:var(--gate)}
  :root:not([data-theme="light"]) .prose a{color:var(--gate)}
  :root:not([data-theme="light"]) .best-cta{background:var(--signal-dark);border-color:var(--signal-dark)}
  :root:not([data-theme="light"]) .best-cta:hover{background:var(--card);color:var(--signal)}
  :root:not([data-theme="light"]) .src-tag{background:#45453F}
  /* The picks eyebrow gold is mixed for white paper; on the dark band it
     drops below the contrast floor, so it lightens rather than inverts. */
  :root:not([data-theme="light"]) .picks-eyebrow{color:#E8A33A}
  :root:not([data-theme="light"]) .picks-eyebrow::before{background:#E8A33A}
  /* The banner's warm paper tint would glow grey-white here, so the base
     ramps into a warm near-black instead and the washes carry more alpha to
     stay visible against it. Same three hues, same right-weighted bloom. */
  :root:not([data-theme="light"]) .mast{background:
        radial-gradient(900px 340px at 86% 14%, rgba(255,79,41,.20), transparent 62%),
        radial-gradient(720px 320px at 62% 104%, rgba(240,165,0,.16), transparent 60%),
        radial-gradient(700px 320px at 104% 86%, rgba(31,191,127,.15), transparent 62%),
        linear-gradient(100deg, var(--card) 0%, var(--card) 34%, #241B17 72%, #2B1F19 100%)}
  :root:not([data-theme="light"]) .mast-ghost{opacity:.38}
  :root:not([data-theme="light"]) footer.site{border-top-color:var(--rule)}
}
:root[data-theme="dark"]{color-scheme:dark;--card:#1C1C1A;--paper-2:#131312;--paper-3:#232320;--ink:#ECEAE4;--ink-soft:#C7C5BD;
      --muted:#9B998F;--rule:#2D2D29;--signal:#FF4F29;--signal-dark:#D93A12;--gate:#1FBF7F}
:root[data-theme="dark"] .thumb,:root[data-theme="dark"] .best-img{background:#fff}
:root[data-theme="dark"] .pct-pill,:root[data-theme="dark"] .newpill{color:#161511}
:root[data-theme="dark"] #catApply{background:var(--signal-dark)}
:root[data-theme="dark"] #catApply:hover{background:#A82D0C}
:root[data-theme="dark"] .price-now{color:var(--gate)}
:root[data-theme="dark"] .td-now{color:var(--gate)}
:root[data-theme="dark"] .compare-price{color:var(--gate)}
:root[data-theme="dark"] .prose a{color:var(--gate)}
:root[data-theme="dark"] .best-cta{background:var(--signal-dark);border-color:var(--signal-dark)}
:root[data-theme="dark"] .best-cta:hover{background:var(--card);color:var(--signal)}
:root[data-theme="dark"] .src-tag{background:#45453F}
:root[data-theme="dark"] .picks-eyebrow{color:#E8A33A}
:root[data-theme="dark"] .picks-eyebrow::before{background:#E8A33A}
:root[data-theme="dark"] .mast{background:
      radial-gradient(900px 340px at 86% 14%, rgba(255,79,41,.20), transparent 62%),
      radial-gradient(720px 320px at 62% 104%, rgba(240,165,0,.16), transparent 60%),
      radial-gradient(700px 320px at 104% 86%, rgba(31,191,127,.15), transparent 62%),
      linear-gradient(100deg, var(--card) 0%, var(--card) 34%, #241B17 72%, #2B1F19 100%)}
:root[data-theme="dark"] .mast-ghost{opacity:.38}
:root[data-theme="dark"] footer.site{border-top-color:var(--rule)}

@media (prefers-reduced-motion:reduce){
  /* Animations (all movement-based on this site: card-breathe, the blog
     spotlight's entrance, the disclosure fade) stay fully off. Transitions
     used to be blanket-killed too, but that took out short opacity/color
     hover feedback along with real movement -- reduced motion means fewer/
     gentler transitions, not zero, per improve-animations' 2026-09-05
     review. Restricting transition-property to this safe list keeps color/
     background/border/shadow/opacity easing alive while any transform-based
     transition on the same rule becomes instant. */
  *{animation:none!important;scroll-behavior:auto!important}
  *{transition-property:color,background-color,border-color,box-shadow,opacity!important}
}

/* ---- durable per-deal pages (/d/{id}.html), pilot 2026-08-14 ---- */
.deal-page{display:flex;gap:32px;margin:22px 0 40px;flex-wrap:wrap}
.deal-page-media{flex:0 0 320px;max-width:100%;background:var(--card);
                  border:1px solid var(--rule);border-radius:var(--r-sm);overflow:hidden;
                  display:flex;align-items:center;justify-content:center;min-height:240px;
                  position:relative}
.deal-page-media img{width:100%;height:auto;display:block}
.deal-page-body{flex:1 1 340px;min-width:0}
.deal-page-body h1{font-size:1.4rem;line-height:1.3;margin:0 0 10px;font-weight:800}

/* ---- durable per-pick pages (/d/pick-{asin}.html), 2026-08-20 ---- */
/* Not inside .deal-page-body, since this page's product widget is the
   Lasso embed itself, not the media+body split deal pages use. */
.pick-page-title{font-size:1.4rem;line-height:1.3;margin:18px 0 10px;font-weight:800}
/* Empty strip above the Lasso embed for the Share button to live in --
   position:relative gives .over-menu (position:absolute) something to
   anchor to that is NOT the product photo. See amazon_pick_pages.py's
   render_pick_page docstring for why this can never be inside the embed. */
.pick-page-toolbar{position:relative;height:36px;margin-bottom:4px}
.deal-status{font:600 12px/1 var(--mono);letter-spacing:.03em;text-transform:uppercase;
             margin:0 0 14px}
.deal-status-live{color:var(--gate)}
.deal-status-expired{color:var(--muted)}
.deal-page-price{font-size:1.15rem;margin:0 0 18px}
.deal-page-price b{color:var(--ink);font-size:1.5rem;margin-right:8px}
.deal-page-price s{color:var(--muted);margin-right:8px}
.deal-page-pct{font:800 12px/1 var(--mono);color:#fff;background:var(--signal);
               padding:5px 8px;border-radius:999px;letter-spacing:.03em}
.deal-cta{display:inline-block;background:var(--signal);color:#fff;font-weight:700;
          text-decoration:none;padding:13px 22px;border-radius:var(--r-sm);font-size:14.5px;
          transition:background var(--dur-base),transform var(--dur-fast) ease-out}
.deal-cta:hover{background:var(--signal-dark)}
.deal-cta:active{transform:scale(.97)}
.deal-cta-secondary{background:transparent;color:var(--signal);border:1px solid var(--signal)}
.deal-cta-secondary:hover{background:var(--signal-dark);color:#fff}
.archive-list{list-style:none;margin:0 0 28px;padding:0;font-size:14px;line-height:2}
.archive-list a{color:var(--ink-soft);text-decoration:none}
.archive-list a:hover{color:var(--signal)}

/* ---- Discount Index (discount-index.html), 2026-08-14 ---- */
.idx-stat-row{display:flex;flex-wrap:wrap;gap:12px;margin:8px 0 30px}
.idx-stat{background:var(--card);border:1px solid var(--rule);border-radius:var(--r-sm);
          padding:14px 18px;min-width:130px;flex:1 1 130px}
.idx-stat b{display:block;font:800 1.6rem/1.1 var(--mono);color:var(--signal);
            font-variant-numeric:tabular-nums;margin-bottom:2px}
.idx-stat span{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
.idx-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;margin:0 0 34px}
.idx-table{width:100%;border-collapse:collapse;font-size:13.5px;min-width:520px}
.idx-table caption{text-align:left;font-weight:700;font-size:1rem;margin-bottom:10px;color:var(--ink)}
.idx-table th,.idx-table td{padding:9px 12px;text-align:right;border-bottom:1px solid var(--rule);
                             white-space:nowrap}
.idx-table th:first-child,.idx-table td:first-child{text-align:left;white-space:normal}
.idx-table th{font:600 11px/1.3 var(--mono);letter-spacing:.04em;text-transform:uppercase;
               color:var(--muted);vertical-align:bottom}
.idx-table td{font-variant-numeric:tabular-nums;color:var(--ink-soft)}
.idx-table td:first-child{color:var(--ink);font-weight:600}
.idx-table td a{color:var(--ink);text-decoration:none}
.idx-table td a:hover{color:var(--signal)}
.idx-table tbody tr:hover td{background:var(--paper-2)}

/* ---- Curator mode (Pinterest multi-select), 2026-08-31 ----
   .curator-only elements only ever appear once body carries .curator-mode,
   a class interaction.js adds after a one-time ?curator=<secret> URL visit
   sets a localStorage flag. A normal visitor's DOM never gets that class,
   so these rules never paint for them -- see interaction.js for the gate. */
.curator-only{display:none}
body.curator-mode .curator-only{display:inline-flex}
.pin-btn{align-items:center;justify-content:center;width:30px;height:30px;
         border-radius:50%;border:1px solid var(--rule);background:var(--card);
         cursor:pointer;font-size:14px;line-height:1;position:absolute;top:8px;right:44px}
.pin-btn[aria-pressed="true"]{background:var(--signal);border-color:var(--signal)}
body.curator-mode #curator-badge{display:flex}
#curator-badge{position:fixed;right:18px;bottom:18px;z-index:500;align-items:center;gap:6px;
               background:var(--signal);color:#fff;border:none;border-radius:var(--r-lg);
               padding:10px 18px;font:700 14px/1 var(--mono);cursor:pointer;
               box-shadow:0 4px 14px rgba(0,0,0,.25)}
#curator-panel{position:fixed;right:18px;bottom:70px;z-index:500;width:300px;max-height:60vh;
               overflow-y:auto;background:var(--card);border:1px solid var(--rule);
               border-radius:var(--r-sm);box-shadow:0 8px 24px rgba(0,0,0,.3);padding:12px;display:none}
#curator-panel.open{display:block}
#curator-panel h4{margin:0 0 8px;font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}
#curator-panel ul{list-style:none;margin:0 0 10px;padding:0}
#curator-panel li{display:flex;justify-content:space-between;gap:8px;padding:6px 0;
                   border-bottom:1px solid var(--rule);font-size:13px}
#curator-panel li span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#curator-panel li button{border:none;background:none;color:var(--muted);cursor:pointer;font-size:14px}
#curator-panel .curator-actions{display:flex;gap:8px}
#curator-panel .curator-actions button{flex:1;padding:8px;border-radius:var(--r-sm);border:1px solid var(--rule);
                   background:var(--paper-2);cursor:pointer;font-size:12.5px;font-weight:600}
#curator-panel .curator-actions button.primary{background:var(--signal);color:#fff;border-color:var(--signal)}
