/* =========================================================================
 * DealsSpy — public stylesheet (v2 rebuild)
 * =========================================================================
 *
 * Loaded on every server-rendered public page, AFTER `tokens.css`, and by the
 * community shell before `community.css`. It has NO literal colour, no
 * literal font stack and no `@import`: every value is a token.
 *
 * MOBILE FIRST. Every `@media` in this file is `min-width`, so the narrow
 * layout is the one that survives when a query is mis-typed.
 *
 * THE FOUR RESPONSIVE STOPS, AND WHAT EACH ONE IS FOR
 * ---------------------------------------------------
 *   30em / 480px   the row's foot goes from stacked to one line
 *   48em / 768px   the header gains its search field; two-up grids appear
 *   64em / 1024px  THE RAIL APPEARS. Below this it is a block under the feed
 *   75em / 1200px  the container reaches `--ds-container` and stops growing
 *
 * WHAT THIS FILE IS ORGANISED AROUND (DESIGN-SYSTEM.md §1)
 * --------------------------------------------------------
 * The v1 diagnosis was that the site felt EMPTY, and the cause was structural
 * rather than chromatic: one large card per deal, no second column, and
 * nothing on a card communicating momentum. So the three things this file
 * exists to produce are a dense two-column shell (§3–§7), a deal ROW that
 * carries its whole story in one glance (§8), and a heat score that is the
 * loudest element on that row (§8.9).
 *
 * THE ROW IS THE CENTREPIECE AND ITS HEIGHT IS A BUDGET, NOT AN OUTCOME.
 * `--ds-row-media` is 88px and the row's block padding is 8px, so a row is
 * 104px tall at desktop whatever the body contains, and 111px in the worst
 * case where a title wraps to its second clamped line. Every size inside it
 * was chosen against that budget. If a rule here makes a row taller, it is
 * the rule that is wrong.
 *
 * THE ONE RULE ABOUT MOTION
 * -------------------------
 * NOTHING IN THIS FILE EVER STARTS AT `opacity: 0` AND WAITS FOR JAVASCRIPT.
 * Every component is fully painted by CSS alone; JS may add a class to make
 * something nicer, never to make it visible. And every transition and
 * animation is switched off under `prefers-reduced-motion` (§18).
 *
 * NO INLINE `style=` ANYWHERE IN THE VIEWS. `Config\ContentSecurityPolicy`
 * carries `unsafe-inline` on `style-src` only as a temporary concession for
 * the editor (D73) and it is coming out; anything that needs a style needs a
 * rule here. §17 is where the last inline block in the codebase landed.
 *
 * CONTENTS
 * --------
 *   0.  Skip link and hidden text
 *   1.  Reset, motion, focus
 *   2.  Typography and prose
 *   3.  Shell: the page frame
 *   4.  Shell: the header
 *   5.  Shell: the nav bar
 *   6.  Shell: main, feed, rail
 *   7.  Shell: the mobile drawer
 *   8.  Shell: the footer
 *   9.  THE ROW — the component six pages share
 *   10. Heat
 *   11. Strips and tiles
 *   12. Buttons
 *   13. Pills and badges
 *   14. Panels, tabs, empty states, breadcrumbs
 *   15. Forms and the auth card
 *   16. Detail pages, tables, pagination, notices
 *   17. Retained v1 components, and the legal preview banner
 *   18. Reduced motion and print
 * ========================================================================= */

/* =========================================================================
 * 0. Skip link and hidden text — FIRST, DELIBERATELY
 * =========================================================================
 * v1 shipped a skip link whose CSS lived four hundred lines down and got lost
 * in a merge, so it rendered as visible text above the logo on every page.
 * It is the first rule in the file so that cannot happen quietly again.
 * ========================================================================= */

.ds-skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: var(--ds-z-skiplink);
    padding: var(--ds-space-3) var(--ds-space-5);
    background: var(--ds-color-surface);
    color: var(--ds-color-link);
    font-weight: var(--ds-weight-semibold);
    text-decoration: underline;
    border: var(--ds-border-width-thick) solid var(--ds-color-focus);
    border-radius: 0 0 var(--ds-radius-md) 0;
    box-shadow: var(--ds-shadow-3);
}

.ds-skip-link:focus,
.ds-skip-link:focus-visible {
    left: 0;
}

.ds-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* =========================================================================
 * 1. Reset, motion, focus
 * ========================================================================= */

*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
    scroll-behavior: smooth;

    /* An in-page anchor must clear the STICKY BAR, and the bar is the only
     * sticky chrome in v2 — the nav row scrolls away (§4.1), so this is the
     * header alone and not header + nav. */
    scroll-padding-top: calc(var(--ds-header-height) + var(--ds-space-4));
}

body,
h1, h2, h3, h4, h5, h6,
p, figure, blockquote, dl, dd,
ul, ol {
    margin: 0;
}

ul[class],
ol[class] {
    list-style: none;
    padding: 0;
}

body {
    margin: 0;
    background: var(--ds-color-bg);
    color: var(--ds-color-text);
    font-family: var(--ds-font-sans);
    font-size: var(--ds-text-base);
    line-height: var(--ds-leading-normal);
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;

    /* Inter's contextual alternates fix the `1`/`l` collision that a dense
     * price column makes obvious. It costs nothing and only applies once the
     * face has loaded. */
    font-feature-settings: "cv05" 1, "cv11" 1;
}

img,
picture,
video,
canvas,
svg {
    display: block;
    max-width: 100%;
    height: auto;
}

input,
button,
textarea,
select {
    font: inherit;
    color: inherit;
}

button {
    cursor: pointer;
}

table {
    border-collapse: collapse;
    width: 100%;
}

hr {
    border: 0;
    border-top: var(--ds-border-width) solid var(--ds-color-border);
    margin: var(--ds-space-8) 0;
}

/* THE GLOBAL FOCUS RING, AND IT IS NEVER REMOVED — ONLY REPLACED.
 * `:focus-visible` rather than `:focus` so a mouse click on a button does not
 * leave a ring behind, but every keyboard path still shows one. Components
 * that need a different ring (a control on the dark bar, an input that owns
 * its own border) override the COLOUR below and never the presence. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: var(--ds-border-width-thick) solid var(--ds-color-focus);
    outline-offset: var(--ds-focus-offset);
    border-radius: var(--ds-radius-sm);
}

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* =========================================================================
 * 2. Typography and prose
 * =========================================================================
 * THE LADDER IS RE-CUT FOR THE TIGHTENED SCALE. `--ds-text-lg` is now the
 * 15px TITLE step and `--ds-text-md` is 17px (tokens.css §2), so the old
 * h1=2xl / h2=xl / h3=lg / h4=md ladder would have put h3 at body size and h4
 * ABOVE it. Headings now run 2xl → xl → md → base and separate by weight at
 * the bottom, which is what a dense feed site needs anyway: an h4 on a deal
 * page is a section label, not a display size.
 * ========================================================================= */

h1, h2, h3, h4, h5, h6 {
    color: var(--ds-color-heading);
    font-weight: var(--ds-weight-bold);
    line-height: var(--ds-leading-tight);
    letter-spacing: var(--ds-tracking-tight);
    text-wrap: balance;
}

h1 { font-size: var(--ds-text-2xl); }
h2 { font-size: var(--ds-text-xl); }
h3 { font-size: var(--ds-text-md); }
h4 { font-size: var(--ds-text-base); }
h5 {
    font-size: var(--ds-text-sm);
    letter-spacing: var(--ds-tracking-normal);
}
h6 {
    font-size: var(--ds-text-2xs);
    text-transform: uppercase;
    letter-spacing: var(--ds-tracking-wide);
    color: var(--ds-color-text-muted);
}

@media (min-width: 48em) {
    h1 { font-size: var(--ds-text-3xl); }
    h2 { font-size: var(--ds-text-2xl); }
    h3 { font-size: var(--ds-text-xl); }
}

p {
    margin-block: 0 var(--ds-space-4);
}

a {
    color: var(--ds-color-link);
    text-decoration-thickness: 1px;
    text-underline-offset: 0.16em;
    transition: color var(--ds-duration-fast) var(--ds-ease);
}

a:hover {
    color: var(--ds-color-link-hover);
}

a:visited {
    color: var(--ds-color-link-visited);
}

small,
.ds-text-sm {
    font-size: var(--ds-text-sm);
}

.ds-text-xs {
    font-size: var(--ds-text-xs);
}

.ds-muted {
    color: var(--ds-color-text-muted);
}

code,
kbd,
samp,
pre {
    font-family: var(--ds-font-mono);
    font-size: 0.925em;
}

code {
    background: var(--ds-color-surface-quiet);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-sm);
    padding: 0.1em 0.35em;
}

pre {
    background: var(--ds-color-surface-sunken);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-md);
    padding: var(--ds-space-4);
    overflow-x: auto;
}

pre code {
    background: none;
    border: 0;
    padding: 0;
}

blockquote {
    border-left: var(--ds-border-width-thick) solid var(--ds-color-secondary);
    padding-left: var(--ds-space-4);
    color: var(--ds-color-text-muted);
}

/* --- `.ds-prose` — the legal and long-form treatment (§7.5) -------------
 * ONE treatment for nine legal pages, three partials and any editorial body.
 * The measure is capped at 68ch because a policy page inside a 1200px shell
 * is otherwise a 110-character line, which is unreadable however good the
 * copy is. Everything else here is rhythm: headings that sit closer to what
 * follows them than to what precedes them, lists that indent, tables that
 * look like tables.
 * --------------------------------------------------------------------- */

.ds-prose {
    max-width: var(--ds-prose);
    font-size: var(--ds-text-base);
    line-height: var(--ds-leading-relaxed);
    color: var(--ds-color-text);
}

.ds-prose > :first-child {
    margin-top: 0;
}

.ds-prose > :last-child {
    margin-bottom: 0;
}

.ds-prose :is(h2, h3, h4) {
    margin-top: var(--ds-space-8);
    margin-bottom: var(--ds-space-3);
    line-height: var(--ds-leading-snug);
}

.ds-prose h2 {
    padding-bottom: var(--ds-space-2);
    border-bottom: var(--ds-border-width) solid var(--ds-color-border);
}

.ds-prose :is(ul, ol) {
    padding-left: var(--ds-space-6);
    margin-bottom: var(--ds-space-4);
}

.ds-prose li + li {
    margin-top: var(--ds-space-2);
}

.ds-prose a {
    font-weight: var(--ds-weight-medium);
}

.ds-prose img {
    border-radius: var(--ds-radius-md);
    margin-block: var(--ds-space-5);
}

.ds-prose blockquote {
    margin-block: var(--ds-space-5);
}

/* A legal page's tables are inventories — a cookie list, a retention
 * schedule — and they scroll rather than squash on a phone. */
.ds-prose table {
    display: block;
    max-width: 100%;
    overflow-x: auto;
    margin-block: var(--ds-space-5);
    font-size: var(--ds-text-sm);
}

.ds-prose :is(th, td) {
    padding: var(--ds-space-2) var(--ds-space-3);
    text-align: left;
    vertical-align: top;
    border: var(--ds-border-width) solid var(--ds-color-border);
}

.ds-prose th {
    background: var(--ds-color-surface-sunken);
    color: var(--ds-color-heading);
    font-weight: var(--ds-weight-semibold);
}

/* =========================================================================
 * 3. Shell: the page frame
 * ========================================================================= */

.ds-shell,
.ds-page {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    min-height: 100dvh;
}

/* =========================================================================
 * 4. Shell: the header
 * =========================================================================
 * STICKY, DARK, ONE ROW AT EVERY WIDTH.
 *
 * §0 of DESIGN-SYSTEM.md records that BOTH reference sites run near-black
 * chrome — the assumption that Slickdeals had a blue bar was wrong and was
 * corrected by measuring. So the bar is `--ds-color-header-bg` (navy-900) and
 * the nav shelf under it is one step lighter, which is how two dark rows read
 * as two things instead of one band.
 *
 * IT IS THE ONLY STICKY CHROME. §4.1: DesiDime keeps the bar and the nav
 * sticky together and it costs 88px of a phone screen. Here the nav scrolls
 * away and the bar is 56px.
 *
 * THE BAR NEVER WRAPS. Brand, search and actions are one flex row; below
 * 48em the search FIELD is replaced by an icon link to /search rather than
 * shrunk, because a 90px-wide text input is a worse control than a button.
 * ========================================================================= */

.ds-header {
    position: sticky;
    top: 0;
    z-index: var(--ds-z-header);
    background: var(--ds-color-header-bg);
    color: var(--ds-color-header-text);
    border-bottom: var(--ds-border-width) solid var(--ds-overlay-line);
    transition: box-shadow var(--ds-duration-base) var(--ds-ease);
}

.ds-header.is-scrolled {
    box-shadow: var(--ds-shadow-3);
}

.ds-header__inner {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: var(--ds-space-2);
    min-height: var(--ds-header-height);
    max-width: var(--ds-container);
    margin-inline: auto;
    padding-inline: var(--ds-space-4);
}

@media (min-width: 48em) {
    .ds-header__inner {
        gap: var(--ds-space-4);
        padding-inline: var(--ds-space-6);
    }
}

.ds-header a {
    color: var(--ds-color-header-text);
    text-decoration: none;
}

.ds-header a:visited {
    color: var(--ds-color-header-text);
}

.ds-header a:hover {
    color: var(--ds-color-header-text-strong);
}

/* On the dark bar the powder-blue ring is nearly invisible, so the ring goes
 * white. The ring is not removed — only recoloured. */
.ds-header :is(a, button, input):focus-visible,
.ds-nav :is(a, button):focus-visible,
.ds-footer :is(a, button):focus-visible {
    outline-color: var(--ds-color-header-text-strong);
}

/* --- The brand ------------------------------------------------------- */

/* `flex: 0 1 auto` AND NOT `flex: none`. At 360px the bar carries a burger,
 * the brand, two icon buttons and Submit, and that adds up to more than the
 * viewport — so something has to give, and the wordmark ellipsizing is a much
 * better failure than the Submit button being pushed off the edge. */
.ds-header__brand {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-space-2);
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    padding: var(--ds-space-1);
    font-size: var(--ds-text-md);
    font-weight: var(--ds-weight-bold);
    letter-spacing: var(--ds-tracking-tight);
    color: var(--ds-color-header-text-strong);
    white-space: nowrap;
}

.ds-header__brand:hover {
    color: var(--ds-color-header-text-strong);
}

.ds-header__brand img {
    height: 1.75rem;
    width: auto;
}

.ds-brand__word {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ds-brand__word em {
    font-style: normal;
    color: var(--ds-color-accent);
}

.ds-brand__mark {
    display: grid;
    place-items: center;
    flex: none;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: var(--ds-radius-md);
    background: var(--ds-color-accent);
    color: var(--ds-color-text-on-accent);
    font-size: var(--ds-text-2xs);
    font-weight: var(--ds-weight-bold);
    letter-spacing: var(--ds-tracking-wide);
    box-shadow: var(--ds-shadow-1);
    transition: transform var(--ds-duration-base) var(--ds-ease);
}

.ds-header__brand:hover .ds-brand__mark {
    transform: translateY(-1px);
}

/* --- The search pill --------------------------------------------------
 * A single rounded field with the submit tucked inside its right edge, which
 * is the shape both reference sites use. It is a `form`, so it works with
 * JavaScript switched off.
 * --------------------------------------------------------------------- */

.ds-header__search {
    display: none;
}

@media (min-width: 48em) {
    .ds-header__search {
        position: relative;
        display: flex;
        align-items: center;
        flex: 1 1 12rem;
        min-width: 0;
        max-width: 34rem;
        margin: 0;
    }
}

.ds-header__search-input {
    flex: 1 1 auto;
    width: 100%;
    min-width: 0;
    height: 2.25rem;
    padding: 0 2.75rem 0 var(--ds-space-4);
    border: var(--ds-border-width) solid transparent;
    border-radius: var(--ds-radius-pill);
    background: var(--ds-color-surface);
    color: var(--ds-color-text);
    font-size: var(--ds-text-sm);
    box-shadow: none;
    transition:
        border-color var(--ds-duration-fast) var(--ds-ease),
        box-shadow var(--ds-duration-fast) var(--ds-ease);
}

.ds-header__search-input::placeholder {
    color: var(--ds-color-text-faint);
}

.ds-header__search-input:hover {
    border-color: var(--ds-color-secondary);
}

.ds-header__search-input:focus,
.ds-header__search-input:focus-visible {
    outline: none;
    border-color: var(--ds-color-secondary);
    box-shadow: var(--ds-focus-ring);
}

.ds-header__search-btn {
    position: absolute;
    right: 3px;
    top: 50%;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 1.875rem;
    height: 1.875rem;
    padding: 0;
    border: 0;
    border-radius: var(--ds-radius-circle);
    background: var(--ds-color-primary);
    color: var(--ds-color-text-on-primary);
    transition: background-color var(--ds-duration-fast) var(--ds-ease);
}

.ds-header__search-btn:hover {
    background: var(--ds-color-primary-hover);
    color: var(--ds-color-text-on-primary);
}

.ds-header__search-btn:active {
    background: var(--ds-color-primary-hover);
    transform: translateY(-50%) scale(0.94);
}

.ds-header__search-btn svg {
    width: 1rem;
    height: 1rem;
}

/* --- The right cluster ------------------------------------------------ */

.ds-header__actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--ds-space-1);
    flex: none;
    margin-left: auto;
}

@media (min-width: 48em) {
    .ds-header__actions {
        gap: var(--ds-space-2);
    }
}

/* One circular icon button — 36px square, which is what makes the bar 56px.
 *
 * `min-width` RATHER THAN `width`, and that came out of testing the harness at
 * 360px: an icon-only action still measures 36×36 and is still a circle,
 * because the icon plus its padding is narrower than the minimum. But a
 * markup author who puts a WORD in one gets a pill that fits the word instead
 * of a 36px circle with the text spilling out of it, which is what a fixed
 * width produced. */
.ds-header__action {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--ds-space-2);
    flex: none;
    min-width: 2.25rem;
    height: 2.25rem;
    padding: 0 var(--ds-space-2);
    border: var(--ds-border-width) solid var(--ds-overlay-border);
    border-radius: var(--ds-radius-circle);
    background: transparent;
    color: inherit;
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-semibold);
    white-space: nowrap;
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        border-color var(--ds-duration-fast) var(--ds-ease),
        color var(--ds-duration-fast) var(--ds-ease),
        transform var(--ds-duration-fast) var(--ds-ease);
}

.ds-header__action:hover {
    background: var(--ds-overlay-hover);
    border-color: var(--ds-color-secondary);
    color: var(--ds-color-header-text-strong);
}

.ds-header__action:active {
    background: var(--ds-overlay-active);
    transform: translateY(1px);
}

.ds-header__action svg {
    width: 1.125rem;
    height: 1.125rem;
    flex: none;
}

/* An unread count riding the bell. It is a pseudo-element on an attribute so
 * the markup carries a number and no extra span. */
.ds-header__action[data-count]:not([data-count="0"])::after {
    content: attr(data-count);
    position: absolute;
    top: -2px;
    right: -2px;
    min-width: 1rem;
    padding: 0 0.2rem;
    border-radius: var(--ds-radius-pill);
    background: var(--ds-color-cta);
    color: var(--ds-color-text-on-cta);
    font-size: var(--ds-text-2xs);
    font-weight: var(--ds-weight-bold);
    line-height: 1rem;
    text-align: center;
}

/* THE SUBMIT BUTTON, AND IT IS THE CTA COLOUR AND NOT GOLD.
 * Posting a deal is the one action on the bar that the whole site depends on
 * a member taking, so it is the only filled control up there. It widens into
 * a pill from 48em because at that width the label fits and a labelled button
 * is pressed more than an icon. */
/* THE `.ds-header` PREFIX IS LOAD-BEARING AND IS NOT DECORATION.
 * `.ds-header a` (0,1,1) sets every link in the bar to
 * `--ds-color-header-text`, and it BEATS a bare `.ds-header__action--primary`
 * (0,1,0). So the one filled button up there was drawing powder-blue type on
 * orange — 2.60:1 on dark, 4.17:1 on light — while this block sat there
 * declaring the correct colour and losing the cascade. Measured in the
 * browser, not read off the file: the computed colour was `#CFE0F2` even
 * though `--ds-color-text-on-cta` resolved to `#061223` on the same element.
 *
 * `.ds-header .ds-header__action--primary` is (0,2,0), which wins, and it
 * matches a `<button>` as well as an `<a>` — "Sign out" is a form submit and
 * "Join" is a link, and they must not diverge. */
.ds-header__action--primary {
    padding-inline: var(--ds-space-3);
    border-radius: var(--ds-radius-pill);
    background: var(--ds-color-cta);
    border-color: var(--ds-color-cta);
}

.ds-header .ds-header__action--primary,
.ds-header .ds-header__action--primary:visited {
    color: var(--ds-color-text-on-cta);
}

.ds-header .ds-header__action--primary:hover {
    background: var(--ds-color-cta-hover);
    border-color: var(--ds-color-cta-hover);
    color: var(--ds-color-text-on-cta);
}

.ds-header__action--primary:active {
    background: var(--ds-color-cta-active);
}

@media (min-width: 48em) {
    .ds-header__action--primary {
        padding-inline: var(--ds-space-5);
    }
}

/* The burger. Mobile only — at 64em the drawer's contents are the nav bar and
 * the rail, so there is nothing left for it to open. */
.ds-header__burger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: var(--ds-border-width) solid var(--ds-overlay-border);
    border-radius: var(--ds-radius-md);
    background: transparent;
    color: inherit;
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        border-color var(--ds-duration-fast) var(--ds-ease);
}

.ds-header__burger:hover {
    background: var(--ds-overlay-hover);
    border-color: var(--ds-color-secondary);
}

.ds-header__burger[aria-expanded="true"] {
    background: var(--ds-overlay-active);
    border-color: var(--ds-color-secondary);
}

.ds-header__burger svg {
    width: 1.25rem;
    height: 1.25rem;
}

@media (min-width: 64em) {
    .ds-header__burger {
        display: none;
    }
}

/* --- 4b. The theme control, and the header's unknown state ------------
 * =========================================================================
 * TWO FEATURES, ONE SUBSECTION, BECAUSE THEY ARE THE SAME MOMENT: the first
 * second of a page load, in the one strip of chrome that is on every page.
 *
 * =========================================================================
 * THE CONTROL IS HIDDEN UNTIL SOMETHING CAN ANSWER IT
 * =========================================================================
 *
 * Three rules and they are a ladder, not a preference:
 *
 *   1. `display: none` by default. With JavaScript off there is no theme
 *      control at all, which is the same argument `partials/site-header.php`
 *      makes about the hamburger: "a control that opens a panel which is not
 *      in the document is worse than no control".
 *   2. `[data-ds-theme]` — written by the pre-paint script in
 *      `partials/head.php` — lays the control out but leaves it INVISIBLE.
 *      That reserves its 36px before the first paint.
 *   3. `[data-ds-theme-ui]` — written by `@site/theme.js` once the radios are
 *      actually wired — shows it.
 *
 * So a reader whose module 404s sees no switch rather than a dead one, and a
 * reader whose module arrives sees it appear in a box that was already there.
 * `visibility` rather than `display` between steps 2 and 3 is the whole point:
 * `display` would shift the bar at the moment the module lands.
 *
 * =========================================================================
 * THE MENU IS A LIGHT PANEL HANGING OFF DARK CHROME
 * =========================================================================
 *
 * Which breaks the header's own focus rule. §4 recolours every focus ring
 * inside `.ds-header` to white, because a powder-blue ring on navy is nearly
 * invisible — and a white ring on the menu's `--ds-color-surface` is worse
 * than nearly invisible. The ring is not removed, only recoloured BACK, which
 * is what §1 says a component may do.
 *
 * =========================================================================
 * THE UNKNOWN HEADER
 * =========================================================================
 *
 * `data-ds-session` is on `<html>` and its three states are described in
 * `partials/head.php`. Here it does one thing: while the answer is `pending`
 * or is known to be `member`, the two SIGNED-OUT controls are `visibility:
 * hidden` — present, measured, occupying their box, and not saying anything.
 *
 * NOT `display: none`, because the width of "Sign in  Join" is roughly the
 * width of "arihanr745  Sign out" and collapsing it would move the search
 * pill twice. NOT a skeleton either: on the common path — an anonymous
 * visitor — the request is answered in tens of milliseconds, long before the
 * first paint, so a shimmer would be a loading animation nobody ever sees
 * except when something is wrong.
 *
 * WHEN THE ATTRIBUTE IS ABSENT THIS SECTION DOES NOTHING, which is the state
 * the failsafe timer always returns to. That is the important sentence: every
 * failure mode ends with a fully visible signed-out header.
 * --------------------------------------------------------------------- */

.ds-theme {
    position: relative;
    display: none;
    flex: none;
}

:root[data-ds-theme] .ds-theme {
    display: inline-flex;
    visibility: hidden;
}

:root[data-ds-theme-ui] .ds-theme {
    visibility: visible;
}

.ds-theme__toggle[aria-expanded="true"] {
    background: var(--ds-overlay-active);
    border-color: var(--ds-color-secondary);
}

.ds-theme__menu {
    position: absolute;
    top: calc(100% + var(--ds-space-2));
    right: 0;
    z-index: var(--ds-z-drawer);
    min-width: 12rem;
    padding: var(--ds-space-2);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-lg);
    background: var(--ds-color-surface);
    color: var(--ds-color-text);
    box-shadow: var(--ds-shadow-3);
}

/* The ring, put back. See the header comment for why white is wrong here. */
.ds-theme__menu :is(input, label):focus-visible,
.ds-theme__option:focus-within {
    outline-color: var(--ds-color-focus);
}

.ds-theme__title {
    margin: 0 0 var(--ds-space-1);
    padding: var(--ds-space-1) var(--ds-space-2);
    color: var(--ds-color-text-muted);
    font-size: var(--ds-text-2xs);
    font-weight: var(--ds-weight-semibold);
    letter-spacing: var(--ds-tracking-wide);
    text-transform: uppercase;
}

.ds-theme__group {
    display: flex;
    flex-direction: column;
}

.ds-theme__option {
    display: flex;
    align-items: center;
    gap: var(--ds-space-2);
    min-height: 2.25rem;
    padding: var(--ds-space-1) var(--ds-space-2);
    border-radius: var(--ds-radius-md);
    font-size: var(--ds-text-sm);
    cursor: pointer;
}

.ds-theme__option:hover {
    background: var(--ds-color-surface-quiet);
}

/* The native radio is the control AND the indicator. `accent-color` paints it
 * in the brand colour without replacing it, so the dot, the keyboard arrow
 * behaviour and the grouping all stay the browser's. */
.ds-theme__radio {
    flex: none;
    width: 1rem;
    height: 1rem;
    margin: 0;
    accent-color: var(--ds-color-primary);
}

.ds-theme__name {
    min-width: 0;
}

.ds-theme__radio:checked + .ds-theme__name {
    font-weight: var(--ds-weight-semibold);
    color: var(--ds-color-heading);
}

/* --- The header whose answer has not arrived -------------------------- */

:root[data-ds-session="pending"] .ds-header__link,
:root[data-ds-session="pending"] .ds-header__action--primary,
:root[data-ds-session="member"] .ds-header__link,
:root[data-ds-session="member"] .ds-header__action--primary {
    visibility: hidden;
}

/* =========================================================================
 * 5. Shell: the nav bar
 * =========================================================================
 * 40px, one step lighter than the header, and it SCROLLS AWAY. It is a
 * horizontal scroller below 48em rather than a wrap, because a nav that
 * becomes two rows changes the height of the page's chrome as you navigate.
 * ========================================================================= */

.ds-nav {
    background: var(--ds-color-nav-bg);
    color: var(--ds-color-nav-text);
    border-bottom: var(--ds-border-width) solid var(--ds-overlay-line-soft);
}

.ds-nav__inner {
    display: flex;
    align-items: stretch;
    gap: var(--ds-space-1);
    min-height: var(--ds-nav-height);
    max-width: var(--ds-container);
    margin-inline: auto;
    padding-inline: var(--ds-space-2);
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scrollbar-width: none;
}

.ds-nav__inner::-webkit-scrollbar {
    display: none;
}

@media (min-width: 48em) {
    .ds-nav__inner {
        padding-inline: var(--ds-space-5);
        overflow-x: visible;
    }
}

.ds-nav__link {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-space-2);
    flex: none;
    padding-inline: var(--ds-space-3);
    border-bottom: var(--ds-border-width-thick) solid transparent;
    color: var(--ds-color-nav-text);
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-medium);
    letter-spacing: var(--ds-tracking-normal);
    text-decoration: none;
    white-space: nowrap;
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        border-color var(--ds-duration-fast) var(--ds-ease),
        color var(--ds-duration-fast) var(--ds-ease);
}

.ds-nav__link:visited {
    color: var(--ds-color-nav-text);
}

.ds-nav__link:hover {
    background: var(--ds-overlay-hover);
    color: var(--ds-color-header-text-strong);
    text-decoration: none;
}

.ds-nav__link:active {
    background: var(--ds-overlay-active);
}

.ds-nav__link svg {
    width: 1rem;
    height: 1rem;
    flex: none;
}

/* THE ACTIVE TAB IS UNDERLINED IN GOLD AND THAT IS THE ONE PLACE ON THE
 * CHROME GOLD APPEARS. It is not a fill: a filled tab on a dark bar competes
 * with the Submit button two inches to the right. */
.ds-nav__link--active,
.ds-nav__link[aria-current="page"] {
    border-bottom-color: var(--ds-color-accent);
    color: var(--ds-color-header-text-strong);
    font-weight: var(--ds-weight-semibold);
}

.ds-nav__link--active:visited,
.ds-nav__link[aria-current="page"]:visited {
    color: var(--ds-color-header-text-strong);
}

/* =========================================================================
 * 6. Shell: main, feed, rail
 * =========================================================================
 * TWO COLUMNS FROM 64em UP; BELOW THAT THE RAIL IS A BLOCK UNDER THE FEED.
 *
 * The left categories drawer is gone from desktop entirely (§4.1) — a left
 * nav AND a right rail is three columns of chrome around one column of
 * content. It survives as the mobile drawer, §7.
 *
 * `.ds-main` IS the container. There is no nested `.ds-container` in the v2
 * shell: v1 had a max-width inside a max-width and the result was two
 * alignment axes and a dead gutter.
 * ========================================================================= */

.ds-main {
    flex: 1 1 auto;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: var(--ds-space-6);
    width: 100%;
    max-width: var(--ds-container);
    margin-inline: auto;
    padding-block: var(--ds-space-5) var(--ds-space-12);
    padding-inline: var(--ds-space-4);
}

.ds-main:focus {
    outline: none;
}

@media (min-width: 48em) {
    .ds-main {
        padding-inline: var(--ds-space-6);
        padding-block: var(--ds-space-6) var(--ds-space-16);
    }
}

@media (min-width: 64em) {
    .ds-main {
        grid-template-columns: minmax(0, 1fr) var(--ds-rail-width);
        gap: var(--ds-space-8);
    }

    /* A page with no rail must not leave 300px of nothing on the right.
     * `--wide` is the declared way to say so (§9.1). */
    .ds-main--wide {
        grid-template-columns: minmax(0, 1fr);
    }

    /* Belt-and-braces for a page that simply has no rail element. IT IS ITS
     * OWN RULE AND NOT GROUPED WITH THE ONE ABOVE, which matters: a browser
     * that does not understand `:has()` discards an ENTIRE selector list
     * containing it, so grouping the two would take `--wide` down with it on
     * exactly the browsers that need the fallback most. Alone, it degrades to
     * doing nothing. */
    .ds-main:not(:has(> .ds-rail)) {
        grid-template-columns: minmax(0, 1fr);
    }
}

.ds-main--wide {
    max-width: var(--ds-container);
}

/* The left column. `min-width: 0` is load-bearing: without it a long
 * unbreakable title in a row makes the grid track wider than 1fr and pushes
 * the rail off the screen. */
.ds-feed {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-5);
    min-width: 0;
}

/* --- The rail (§9.1) --------------------------------------------------
 * Sticky from 64em, and `align-self: start` is what makes sticky work inside
 * a grid — a stretched item has nowhere to stick to. `max-height` plus
 * `overflow-y` stops a long rail from pinning a region taller than the
 * viewport, which silently disables the stickiness.
 * --------------------------------------------------------------------- */

.ds-rail {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-4);
    min-width: 0;
}

@media (min-width: 64em) {
    .ds-rail {
        position: sticky;
        top: calc(var(--ds-header-height) + var(--ds-space-4));
        align-self: start;
        max-height: calc(100dvh - var(--ds-header-height) - var(--ds-space-8));
        overflow-y: auto;
        overscroll-behavior: contain;
        scrollbar-width: thin;
    }
}

/* `flex: none` IS LOAD-BEARING AND IT IS NOT TIDINESS.
 *
 * `.ds-rail` is a flex COLUMN with a `max-height` and `overflow-y: auto`, and a
 * flex item's default `flex-shrink: 1` means the panels compress to fit that
 * height instead of letting the rail scroll. Combined with the `overflow:
 * hidden` below — which is there to keep the panel's children inside its
 * radius — the compression is SILENT: the panel keeps its heading and its
 * border, the last row is sliced through the middle, and nothing scrolls
 * because the container thinks its content fits.
 *
 * Measured on a 639px-tall viewport at 1521px wide, with the rail capped at
 * 551px: New deals wanted 384px and got 256, Stores wanted 301 and got 201,
 * Expiring soon wanted 87 and got 58 — one item cut in half, on the panel whose
 * only item it was. `scrollHeight === clientHeight` on the rail the whole time,
 * so the scrollbar the max-height exists to produce never appeared.
 *
 * With `flex: none` the panels keep their intrinsic height, the rail overflows
 * honestly, and `overflow-y: auto` does what its own comment above says it is
 * for. This was already wrong with two panels; the third made it visible. */
.ds-rail__panel {
    flex: none;
    background: var(--ds-color-rail-bg);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-lg);
    overflow: hidden;
}

.ds-rail__title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ds-space-2);
    margin: 0;
    padding: var(--ds-space-3) var(--ds-space-4);
    border-bottom: var(--ds-border-width) solid var(--ds-color-border);
    background: var(--ds-color-surface-sunken);
    color: var(--ds-color-heading);
    font-size: var(--ds-text-2xs);
    font-weight: var(--ds-weight-bold);
    letter-spacing: var(--ds-tracking-wide);
    text-transform: uppercase;
}

.ds-rail__list {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* ONE RAIL ITEM. The markup inside it is deliberately unconstrained — the
 * rail carries three different things (a mini deal, a store, an expiring
 * coupon) and inventing three class sets for a 300px column is how a
 * stylesheet doubles in size for no visible gain. What is styled is the
 * SHAPE: a full-bleed link, an optional 32px thumbnail, a two-line clamped
 * label, and any trailing `<b>`/`<strong>` as the number on the right. */
.ds-rail__item {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
    min-height: 2.75rem;
    padding: var(--ds-space-2) var(--ds-space-4);
    border-top: var(--ds-border-width) solid var(--ds-color-border);
    font-size: var(--ds-text-sm);
    line-height: var(--ds-leading-snug);
    color: var(--ds-color-text);
    transition: background-color var(--ds-duration-fast) var(--ds-ease);
}

.ds-rail__list > li:first-child > .ds-rail__item,
.ds-rail__list > .ds-rail__item:first-child {
    border-top: 0;
}

.ds-rail__item:hover {
    background: var(--ds-color-surface-quiet);
}

.ds-rail__item a {
    flex: 1 1 auto;
    min-width: 0;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    color: var(--ds-color-text);
    font-weight: var(--ds-weight-medium);
    text-decoration: none;
}

.ds-rail__item a:visited {
    color: var(--ds-color-text);
}

.ds-rail__item a:hover {
    color: var(--ds-color-link-hover);
    text-decoration: underline;
}

.ds-rail__item img {
    flex: none;
    width: 2rem;
    height: 2rem;
    border-radius: var(--ds-radius-sm);
    background: var(--ds-color-surface-sunken);
    object-fit: contain;
}

.ds-rail__item :is(b, strong) {
    flex: none;
    color: var(--ds-color-text-muted);
    font-size: var(--ds-text-xs);
    font-weight: var(--ds-weight-semibold);
    font-variant-numeric: tabular-nums;
}

.ds-rail__more {
    display: block;
    padding: var(--ds-space-3) var(--ds-space-4);
    border-top: var(--ds-border-width) solid var(--ds-color-border);
    background: var(--ds-color-surface-sunken);
    color: var(--ds-color-link);
    font-size: var(--ds-text-xs);
    font-weight: var(--ds-weight-semibold);
    text-align: center;
    text-decoration: none;
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        color var(--ds-duration-fast) var(--ds-ease);
}

.ds-rail__more:hover {
    background: var(--ds-color-primary-soft);
    color: var(--ds-color-primary-soft-text);
    text-decoration: underline;
}

/* Below 40em the rail's tile lists become a horizontal scroller (§4.1) so a
 * "Top Stores" panel is one swipe rather than ten rows of a phone screen. */
@media (max-width: 39.99em) {
    .ds-rail__panel--scroll .ds-rail__list {
        display: flex;
        gap: var(--ds-space-2);
        padding: var(--ds-space-3);
        overflow-x: auto;
        overscroll-behavior-inline: contain;
    }

    .ds-rail__panel--scroll .ds-rail__item {
        flex: 0 0 9rem;
        flex-direction: column;
        align-items: flex-start;
        border-top: 0;
        border: var(--ds-border-width) solid var(--ds-color-border);
        border-radius: var(--ds-radius-md);
    }
}

/* =========================================================================
 * 7. Shell: the mobile drawer
 * =========================================================================
 * The v1 left sidebar, kept for exactly the job it is still good at: an
 * off-canvas category list on a phone. MOBILE-FIRST AND CLOSED BY DEFAULT
 * means the `hidden` attribute is honoured, so with JavaScript switched off
 * the drawer is simply absent rather than covering the page.
 * ========================================================================= */

.ds-drawer {
    position: fixed;
    inset: var(--ds-header-height) 0 0 0;
    z-index: var(--ds-z-drawer);
    background: var(--ds-color-scrim);
}

.ds-drawer[hidden] {
    display: none;
}

/* THE PAGE BEHIND THE OPEN DRAWER DOES NOT SCROLL.
 *
 * Measured before this rule: with the drawer open at 375px, `window.scrollY`
 * went 0 → 500 on a flick. The drawer covers everything below the bar, so the
 * reader is scrolling something they cannot see, and closing the menu drops
 * them somewhere else in the feed.
 *
 * `:has()` RATHER THAN A CLASS TOGGLED FROM JAVASCRIPT, for two reasons. The
 * state already exists in the document — `hidden` on the panel is the single
 * source of truth `@site/toggle.js` flips — so a `.is-drawer-open` class on
 * `<html>` would be a second copy of it that can drift, and §9 lists no such
 * class. And a browser without `:has()` simply drops this rule and keeps
 * today's behaviour, which is the right failure mode for a scroll lock.
 *
 * SCOPED TO THE WIDTHS THE DRAWER EXISTS AT. From 64em `.ds-drawer` is
 * `display: none` and `@site/toggle.js` re-syncs `hidden` back on at that
 * breakpoint; if either ever failed, an unscoped rule would lock the scroll of
 * a desktop page with no visible menu on it. */
@media (max-width: 63.99em) {
    html:has(.ds-drawer:not([hidden])) {
        overflow: hidden;
    }
}

.ds-drawer__panel {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-5);
    width: min(20rem, 86vw);
    height: 100%;
    padding: var(--ds-space-4) var(--ds-space-3) var(--ds-space-8);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--ds-color-rail-bg);
    border-right: var(--ds-border-width) solid var(--ds-color-border);
    box-shadow: var(--ds-shadow-4);
}

.ds-drawer__close {
    align-self: flex-end;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-md);
    background: var(--ds-color-surface);
    color: var(--ds-color-text-muted);
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        color var(--ds-duration-fast) var(--ds-ease);
}

.ds-drawer__close:hover {
    background: var(--ds-color-surface-sunken);
    color: var(--ds-color-text);
}

.ds-drawer__close svg {
    width: 1.125rem;
    height: 1.125rem;
}

.ds-drawer__section {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-1);
}

.ds-drawer__section > :is(h2, h3, p):first-child {
    margin: 0 0 var(--ds-space-1);
    padding: 0 var(--ds-space-3) var(--ds-space-2);
    border-bottom: var(--ds-border-width) solid var(--ds-color-border);
    font-size: var(--ds-text-2xs);
    font-weight: var(--ds-weight-bold);
    letter-spacing: var(--ds-tracking-wide);
    text-transform: uppercase;
    color: var(--ds-color-text-muted);
}

.ds-drawer__link {
    display: flex;
    align-items: center;
    gap: var(--ds-space-2);
    min-height: 2.5rem;
    padding: var(--ds-space-2) var(--ds-space-3);
    border-left: 3px solid transparent;
    border-radius: var(--ds-radius-md);
    color: var(--ds-color-text);
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-medium);
    text-decoration: none;
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        border-color var(--ds-duration-fast) var(--ds-ease),
        color var(--ds-duration-fast) var(--ds-ease);
}

.ds-drawer__link:visited {
    color: var(--ds-color-text);
}

.ds-drawer__link:hover {
    background: var(--ds-color-surface-quiet);
    border-left-color: var(--ds-color-border-strong);
    color: var(--ds-color-primary-soft-text);
    text-decoration: none;
}

.ds-drawer__link:active {
    background: var(--ds-color-primary-soft);
}

.ds-drawer__link--active,
.ds-drawer__link[aria-current="page"] {
    background: var(--ds-color-primary-soft);
    border-left-color: var(--ds-color-primary);
    color: var(--ds-color-primary-soft-text);
    font-weight: var(--ds-weight-semibold);
}

@media (min-width: 64em) {
    .ds-drawer {
        display: none;
    }
}

/* =========================================================================
 * 8. Shell: the footer
 * ========================================================================= */

.ds-footer {
    background: var(--ds-color-footer-bg);
    color: var(--ds-color-footer-text);
    padding-block: var(--ds-space-10) var(--ds-space-5);
    font-size: var(--ds-text-sm);
    border-top: var(--ds-border-width) solid var(--ds-overlay-line);
}

.ds-footer__inner {
    display: grid;
    gap: var(--ds-space-8);
    grid-template-columns: 1fr;
    max-width: var(--ds-container);
    margin-inline: auto;
    padding-inline: var(--ds-space-4);
}

@media (min-width: 40em) {
    .ds-footer__inner {
        grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    }
}

@media (min-width: 48em) {
    .ds-footer__inner {
        padding-inline: var(--ds-space-6);
    }
}

.ds-footer a {
    color: var(--ds-color-footer-text);
    text-decoration: none;
}

.ds-footer a:visited {
    color: var(--ds-color-footer-text);
}

.ds-footer a:hover {
    color: var(--ds-color-footer-heading);
    text-decoration: underline;
}

.ds-footer__col {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-3);
    min-width: 0;
}

.ds-footer__title {
    margin: 0;
    font-size: var(--ds-text-2xs);
    font-weight: var(--ds-weight-bold);
    text-transform: uppercase;
    letter-spacing: var(--ds-tracking-wide);
    color: var(--ds-color-footer-heading);
}

.ds-footer__link {
    display: inline-block;
    padding-block: 1px;
    color: var(--ds-color-footer-text);
    font-size: var(--ds-text-sm);
    text-decoration: none;
    transition: color var(--ds-duration-fast) var(--ds-ease);
}

.ds-footer__link:hover {
    color: var(--ds-color-footer-heading);
    text-decoration: underline;
}

/* The bottom bar. It is INSIDE the grid but spans every column, so the rule
 * above it lines up with the columns rather than the viewport. */
.ds-footer__legal {
    grid-column: 1 / -1;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--ds-space-2) var(--ds-space-5);
    margin-top: var(--ds-space-4);
    padding-top: var(--ds-space-5);
    border-top: var(--ds-border-width) solid var(--ds-overlay-line);
    font-size: var(--ds-text-xs);
    color: var(--ds-color-footer-text);
}

.ds-footer__legal p {
    margin: 0;
}

/* =========================================================================
 * 9. THE ROW — the component six pages share (§4.3, §9.2)
 * =========================================================================
 * Home, Deals, Category, Store, Search and Tag all render this. That is the
 * point of it, and it is also the risk: a mistake here is visible on every
 * listing page at once.
 *
 * THE HEIGHT BUDGET, MEASURED RATHER THAN HOPED
 * ---------------------------------------------
 *   media           88px   (`--ds-row-media`)
 *   block padding  2×8px
 *   ------------------------
 *   row            104px   whenever the body fits inside 88px
 *
 * The body is four lines and they add up:
 *
 *   meta   11px × 1.4                     15.4
 *   title  15px × 1.3 × 1 line            19.5   (× 2 lines = 39.0)
 *   price  17px × 1.15                    19.6
 *   foot   the CTA's min-height           26.0
 *   gaps   3 × 1px                         3.0
 *   ------------------------------------------
 *   one-line title                        83.5  → row is 104px, media-governed
 *   two-line title                       103.0  → row is 119px
 *
 * So the common row is 104px and a wrapped title costs 15px. The 96–104 target
 * is the one that holds for the row you actually see most of; a row whose title
 * genuinely needs two lines is 119px and the alternative is truncating product
 * names, which on a deal feed is worse than fifteen pixels.
 *
 * That is still the whole density argument: four of these fit in the vertical
 * space v1 gave to one card.
 *
 * THE WHOLE ROW IS A LINK. `.ds-row__title a::after` is a transparent
 * rectangle over the entire row, so a click anywhere goes to the deal. The
 * CTA and the save button sit ABOVE it on `z-index: 2` and are the only
 * nested interactive elements — a stretched link with three things poking
 * through it stops being a link and becomes a hit-testing puzzle.
 *
 * WHERE THE MARKUP MAY PUT THINGS
 * -------------------------------
 * `.ds-row__heat` is positioned against the ROW, not against its parent, so
 * it lands top-right whether the markup nests it in `__meta`, in `__body` or
 * directly in the row. `.ds-row__cta` does the same at bottom-right when it
 * is a direct child, and floats to the end of `__foot` when it is inside one.
 * Both arrangements are supported on purpose: two agents are writing this
 * markup and the rule should not care which reading of §9.2 they took.
 * ========================================================================= */

/* The list container owns the frame; the rows own the hairlines between
 * them. A stack of separately-bordered cards with 8px gaps is exactly the
 * airy treatment v1 had. */
.ds-rows {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
    list-style: none;
    background: var(--ds-color-surface);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-lg);
    box-shadow: var(--ds-shadow-1);
    overflow: hidden;
}

.ds-rows > * + * {
    border-top: var(--ds-border-width) solid var(--ds-color-border);
}

.ds-rows > li {
    display: block;
}

/* THE HEIGHT BUDGET IS ONLY REAL IF NOTHING INSIDE BRINGS ITS OWN MARGIN.
 *
 * This rule was added after MEASURING the row rather than reasoning about it.
 * §2 gives every `<p>` a 16px bottom margin, and the natural markup for the
 * meta line, the price line and the foot is three paragraphs — so the first
 * build of this component came out at 134px instead of 104px, and every part
 * of it looked correct in isolation. Zeroing the margins on the elements
 * individually is not enough either, because it only covers the tags the
 * stylesheet author guessed at; two other agents are writing this markup and
 * an `<h3>` or a `<ul>` in the body would reintroduce the same 30px.
 *
 * So the row zeroes flow margins for everything inside it, once, and the
 * `gap` on `.ds-row__body` is the only vertical spacing in the component.
 */
.ds-row :is(p, h1, h2, h3, h4, h5, h6, ul, ol, dl, figure) {
    margin: 0;
}

.ds-row {
    position: relative;
    display: grid;
    grid-template-columns: var(--ds-row-media) minmax(0, 1fr);
    align-items: start;
    gap: var(--ds-space-3);
    padding: var(--ds-space-2);
    background: var(--ds-color-surface);
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        box-shadow var(--ds-duration-fast) var(--ds-ease);
}

/* A row that is NOT inside `.ds-rows` still needs an edge. */
.ds-rows > .ds-row,
.ds-rows > li > .ds-row {
    border-radius: 0;
}

.ds-row:hover {
    background: var(--ds-color-surface-quiet);
}

/* `:focus-within` is what makes the row visible to a keyboard: the stretched
 * link has no box of its own to ring. */
.ds-row:focus-within {
    background: var(--ds-color-surface-quiet);
    box-shadow: inset 0 0 0 2px var(--ds-color-focus);
}

@media (min-width: 30em) {
    .ds-row {
        gap: var(--ds-space-4);
    }
}

/* --- Media ----------------------------------------------------------- */

.ds-row__media {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: var(--ds-row-media);
    height: var(--ds-row-media);
    padding: var(--ds-space-1);
    background: var(--ds-color-surface-sunken);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-md);
    overflow: hidden;
}

.ds-row__img {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
    transition: transform var(--ds-duration-base) var(--ds-ease);
}

.ds-row:hover .ds-row__img {
    transform: scale(1.04);
}

/* A corner flag, not another pill in the meta row — the meta row is already
 * full (§4.3). It is a strip across the top of the image because a rotated
 * 45° ribbon at 88px is four legible characters at best. */
.ds-row__ribbon {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 1;
    padding: 1px var(--ds-space-1);
    background: var(--ds-color-cta);
    color: var(--ds-color-text-on-cta);
    font-size: var(--ds-text-2xs);
    font-weight: var(--ds-weight-bold);
    letter-spacing: var(--ds-tracking-wide);
    line-height: 1.4;
    text-align: center;
    text-transform: uppercase;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ds-row__ribbon[data-flag="loot"] {
    background: var(--ds-color-accent);
    color: var(--ds-color-text-on-accent);
}

.ds-row__ribbon[data-flag="sponsored"] {
    background: var(--ds-color-surface-inverse);
    color: var(--ds-color-text-inverse);
}

/* --- Body ------------------------------------------------------------ */

/* `min-height` is what makes the 104px budget a floor rather than a hope: the
 * grid is `align-items: start`, so without it a short row would collapse to
 * its text and the feed would have four different row heights in it.
 *
 * THE GAP IS 1px AND NOT A SPACING STEP. Four stacked lines multiply a gap by
 * three, and `--ds-space-1` here would add 12px to every row in the feed — the
 * line-heights already carry the rhythm. */
.ds-row__body {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
    min-height: var(--ds-row-media);
}

/* The top line. The right padding is the heat block's footprint — the heat is
 * absolutely positioned, so nothing else reserves room for it. */
.ds-row__meta {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: var(--ds-space-2);
    min-width: 0;
    margin: 0;
    padding-right: 4.5rem;
    font-size: var(--ds-text-2xs);
    line-height: 1.4;
    color: var(--ds-color-text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ds-row__meta > * {
    flex: none;
}

/* `muted`, NOT `faint`. `faint` is `--ds-neutral-400`, which is 2.47:1 on the
 * row's white surface — it is a role for RULES, ICONS AND SEPARATORS, and the
 * moment it carries a word it fails. The age is a word. */
.ds-row__age {
    color: var(--ds-color-text-muted);
    font-variant-numeric: tabular-nums;
}

/* The store is a real link and one of the few coloured words on the row, so
 * it needs to be above the stretched link to be clickable. */
.ds-row__store {
    position: relative;
    z-index: 2;
    max-width: 12rem;
    color: var(--ds-color-link);
    font-weight: var(--ds-weight-semibold);
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ds-row__store:visited {
    color: var(--ds-color-link);
}

.ds-row__store:hover {
    color: var(--ds-color-link-hover);
    text-decoration: underline;
}

/* Separators between meta items, drawn rather than typed, so the markup does
 * not carry decorative middots that a screen reader announces.
 *
 * The heat block is excluded on both sides. §9.2 puts it on the top line, so a
 * markup author may reasonably nest it inside `__meta` — and it is positioned
 * against the ROW rather than the meta line, so a separator would render
 * inside the heat pill itself, in front of the number. */
.ds-row__meta > *:not(.ds-row__heat) + *:not(.ds-row__heat)::before {
    content: "·";
    margin-right: var(--ds-space-2);
    color: var(--ds-color-text-faint);
}

.ds-row__meta > .ds-row__heat::before {
    content: none;
}

/* --- Title ----------------------------------------------------------- */

.ds-row__title {
    margin: 0;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: var(--ds-text-lg);
    font-weight: var(--ds-weight-semibold);
    line-height: var(--ds-leading-snug);
    letter-spacing: var(--ds-tracking-normal);
    color: var(--ds-color-heading);
    text-wrap: pretty;
}

.ds-row__title a {
    color: inherit;
    text-decoration: none;
}

.ds-row__title a:visited {
    color: var(--ds-color-text-muted);
}

.ds-row:hover .ds-row__title,
.ds-row:hover .ds-row__title a {
    color: var(--ds-color-link-hover);
}

/* THE STRETCHED LINK. `::after` rather than the anchor itself so the anchor
 * keeps its own text box for the underline and the focus ring. */
.ds-row__title a::after,
a.ds-row__title::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
}

/* --- Price ------------------------------------------------------------
 * ONE LINE, NEVER STACKED, AND IT MUST NOT WRAP AT 360px. `nowrap` on the
 * container plus `flex: none` on the parts is what guarantees that; the
 * was-price is the first thing allowed to disappear if the width really is
 * not there, because the current price is the number that matters.
 * -------------------------------------------------------------------- */

.ds-row__price {
    display: flex;
    align-items: baseline;
    flex-wrap: nowrap;
    gap: var(--ds-space-2);
    min-width: 0;
    margin: 0;
    white-space: nowrap;
    overflow: hidden;
}

.ds-row__price > * {
    flex: none;
}

.ds-row__price-now {
    font-size: var(--ds-text-md);
    font-weight: var(--ds-weight-bold);
    line-height: var(--ds-leading-tight);
    letter-spacing: var(--ds-tracking-tight);
    color: var(--ds-color-heading);
    font-variant-numeric: tabular-nums;
}

/* STRUCK THROUGH IN RED, AND THAT IS SLICKDEALS' SIGNATURE RATHER THAN A
 * DECORATION (§4.3). Grey reads as "ignore this"; on a deal site the old
 * price is half of what the page is selling. `line-through` is drawn by the
 * text decoration so it inherits the colour. */
.ds-row__price-was {
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-regular);
    color: var(--ds-color-price-was);
    text-decoration: line-through;
    text-decoration-thickness: 1px;
    font-variant-numeric: tabular-nums;
}

.ds-row__price-off {
    padding: 1px var(--ds-space-2);
    border-radius: var(--ds-radius-sm);
    background: var(--ds-color-cta-soft);
    color: var(--ds-color-cta-text);
    font-size: var(--ds-text-2xs);
    font-weight: var(--ds-weight-bold);
    letter-spacing: var(--ds-tracking-wide);
    font-variant-numeric: tabular-nums;
}

/* THE LAST RESORT, AND IT IS SET WHERE MEASURING PUT IT RATHER THAN WHERE IT
 * LOOKED SAFE. At 360px the body column is 227px and the widest realistic
 * price line — `$1,099.99  $1,449.99  −24%` — measures 210px, so it fits and
 * the was-price stays. This was 22.5em (360px) on the first pass and hid the
 * strike on exactly the phone width it did not need to. Below 320px there is
 * genuinely not enough room, and the current price is the number that
 * matters. */
@media (max-width: 20em) {
    .ds-row__price-was {
        display: none;
    }
}

.ds-row__note {
    margin: 0;
    font-size: var(--ds-text-xs);
    color: var(--ds-color-text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --- Foot ------------------------------------------------------------ */

.ds-row__foot {
    display: flex;
    align-items: center;
    gap: var(--ds-space-3);
    min-width: 0;
    margin: auto 0 0;
    font-size: var(--ds-text-xs);
    color: var(--ds-color-text-muted);
}

.ds-row__author {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-space-1);
    min-width: 0;
    max-width: 10rem;
    position: relative;
    z-index: 2;
    color: var(--ds-color-text-muted);
    font-weight: var(--ds-weight-medium);
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ds-row__author:visited {
    color: var(--ds-color-text-muted);
}

.ds-row__author:hover {
    color: var(--ds-color-link-hover);
    text-decoration: underline;
}

.ds-row__avatar {
    flex: none;
    width: 1.125rem;
    height: 1.125rem;
    border-radius: var(--ds-radius-circle);
    background: var(--ds-color-primary-soft);
    object-fit: cover;
}

.ds-row__comments {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-space-1);
    flex: none;
    position: relative;
    z-index: 2;
    color: var(--ds-color-text-muted);
    text-decoration: none;
    font-variant-numeric: tabular-nums;
}

.ds-row__comments:visited {
    color: var(--ds-color-text-muted);
}

.ds-row__comments:hover {
    color: var(--ds-color-link-hover);
}

.ds-row__comments svg,
.ds-row__save svg {
    width: 0.875rem;
    height: 0.875rem;
    flex: none;
}

/* LAST ACTIVITY — §7.2's "rows with reply count and last-activity", which the
 * discussion listing is the first row type to carry. It is the ONE class §9.2's
 * vocabulary gains for this, on the same footing §9.4's `.ds-preview-banner`
 * was added: a real element with a real job, listed in the contract rather than
 * smuggled in.
 *
 * NO COLOUR OF ITS OWN THAT ISN'T THE FOOT'S. `.ds-row__foot` already sets
 * `--ds-color-text-muted` and this inherits it — the date is context beside the
 * byline, not a second voice — so this rule adds only the three properties that
 * are about SHAPE: tabular figures so a column of dates does not shimmer, and
 * shrink-to-ellipsis so it is the part that gives way when the foot is tight.
 *
 * IT IS THE THING THAT SHRINKS. The byline and the CTA are `flex: none` or
 * carry their own max-width; this one takes `min-width: 0` so the flex item can
 * go below its content width instead of pushing the button out of the row. */
.ds-row__activity {
    min-width: 0;
    font-variant-numeric: tabular-nums;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* MEASURED IN A BROWSER, NOT GUESSED, and in the same spirit as the was-price
 * rule above. At a 480px viewport the discussion row's foot is 311px wide and
 * its four parts measure "by demo_editor" 77px + the comment count 26px +
 * "Updated 8 Sep 2026" 103px + the button 69px = 275px, plus three 12px gaps =
 * 311px. It fits EXACTLY, which is what makes 480px the right place to draw the
 * line rather than a round number near it.
 *
 * Below it the date is the first thing that stops earning its width: the byline
 * says who, the count says whether anyone replied, and the button is the
 * action. Ellipsising a date to "Updat…" would be worse than not printing it,
 * and letting it squeeze the byline instead would lose the more useful of the
 * two. Measured at 430px with this rule in force: author 89px, count 26px,
 * button 61px, no overflow and no ellipsis on the name. */
@media (max-width: 29.99em) {
    .ds-row__activity {
        display: none;
    }
}

.ds-row__save {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    position: relative;
    z-index: 2;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    border: 0;
    border-radius: var(--ds-radius-sm);
    background: none;
    color: var(--ds-color-text-faint);
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        color var(--ds-duration-fast) var(--ds-ease);
}

.ds-row__save:hover {
    background: var(--ds-color-surface-sunken);
    color: var(--ds-color-link);
}

.ds-row__save[aria-pressed="true"] {
    color: var(--ds-color-accent);
}

/* THE CTA IS A REAL BUTTON AND THE LABEL IS THE ACTION — "Get deal",
 * "Reveal code", never "View". It is `flex: none` and last, so it holds its
 * width while the author's name ellipsizes. */
.ds-row__cta {
    position: relative;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--ds-space-1);
    flex: none;
    margin-left: auto;
    min-height: 1.625rem;
    padding: 0 var(--ds-space-3);
    border: var(--ds-border-width) solid var(--ds-color-cta);
    border-radius: var(--ds-radius-md);
    background: var(--ds-color-cta);
    color: var(--ds-color-text-on-cta);
    font-size: var(--ds-text-xs);
    font-weight: var(--ds-weight-semibold);
    line-height: 1;
    text-decoration: none;
    white-space: nowrap;
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        border-color var(--ds-duration-fast) var(--ds-ease),
        transform var(--ds-duration-fast) var(--ds-ease);
}

.ds-row__cta:visited {
    color: var(--ds-color-text-on-cta);
}

.ds-row__cta:hover {
    background: var(--ds-color-cta-hover);
    border-color: var(--ds-color-cta-hover);
    color: var(--ds-color-text-on-cta);
    text-decoration: none;
}

.ds-row__cta:active {
    background: var(--ds-color-cta-active);
    transform: translateY(1px);
}

.ds-row__cta svg {
    width: 0.875rem;
    height: 0.875rem;
}

/* When the markup hangs the CTA off the row instead of the foot. */
.ds-row > .ds-row__cta {
    position: absolute;
    right: var(--ds-space-2);
    bottom: var(--ds-space-2);
    margin: 0;
}

/* --- Row modifiers --------------------------------------------------- */

/* EXPIRED ROWS ARE SCANNABLE AS DEAD (§4.3). 60% opacity plus a struck,
 * greyed price; the CTA is replaced in the markup by a muted "Expired"
 * label, which is a `.ds-pill--expired`. */
.ds-row--expired {
    opacity: 0.6;
}

.ds-row--expired .ds-row__price-now {
    color: var(--ds-color-text-muted);
    text-decoration: line-through;
}

.ds-row--expired:hover {
    opacity: 0.75;
}

.ds-row--featured {
    border-left: 3px solid var(--ds-color-accent);
    background: var(--ds-color-surface);
}

/* --- The row at 64em and below --------------------------------------- */

@media (max-width: 29.99em) {
    .ds-row {
        --ds-row-media: 72px;
        padding: var(--ds-space-2) var(--ds-space-2) var(--ds-space-3);
    }

    .ds-row__meta {
        padding-right: 3.75rem;
    }

    .ds-row__author {
        max-width: 6rem;
    }

    /* Taller on a phone, not shorter. 26px is a fine target for a mouse and a
     * poor one for a thumb, and this is the only control on the row that is
     * not the stretched link. */
    .ds-row__cta {
        min-height: 2rem;
        padding-inline: var(--ds-space-2);
    }
}

/* =========================================================================
 * 10. Heat — the loudest thing on the row (§2.2, §9.5)
 * =========================================================================
 * `RankingService` already computes hotness; v1 never surfaced it, and that
 * absence is why the feed read as a catalogue rather than a live wall.
 *
 * FOUR BANDS AND THE PRESENTER PICKS. A view must never compare a number.
 *
 * WHY IT IS A TINT WITH DARK TYPE RATHER THAN A SOLID SLAB. The four ramp
 * values in §2.2 are graphic colours: on white they measure 3.7:1 (cold),
 * 2.0:1 (warm), 2.6:1 (hot) and 5.8:1 (blazing) — so a solid fill would need
 * white type on two bands and dark type on the other two, and the scale would
 * stop reading as one scale. Tint + border + the band's darkened text step is
 * legible on all four and still the only coloured, bordered, right-aligned
 * block on the row.
 * ========================================================================= */

.ds-row__heat {
    position: absolute;
    top: var(--ds-space-2);
    right: var(--ds-space-2);
    z-index: 2;
    margin: 0;
    display: inline-flex;
    align-items: baseline;
    justify-content: center;
    gap: 1px;
    min-width: 3.5rem;
    padding: 2px var(--ds-space-2);
    border: var(--ds-border-width) solid var(--ds-heat-cold);
    border-radius: var(--ds-radius-pill);
    background: var(--ds-heat-cold-bg);
    color: var(--ds-heat-cold-text);
    font-variant-numeric: tabular-nums;
    line-height: 1.2;
    white-space: nowrap;
}

.ds-row__heat-value {
    font-size: var(--ds-text-md);
    font-weight: var(--ds-weight-bold);
    letter-spacing: var(--ds-tracking-tight);
}

/* 13px and not 11px. At the smaller step the degree sign renders as a dot
 * hanging off the number — `342·` rather than `342°` — which is exactly the
 * detail that makes a temperature read as a temperature. */
.ds-row__heat-unit {
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-semibold);
}

.ds-row__heat--cold {
    border-color: var(--ds-heat-cold);
    background: var(--ds-heat-cold-bg);
    color: var(--ds-heat-cold-text);
}

.ds-row__heat--warm {
    border-color: var(--ds-heat-warm);
    background: var(--ds-heat-warm-bg);
    color: var(--ds-heat-warm-text);
}

.ds-row__heat--hot {
    border-color: var(--ds-heat-hot);
    background: var(--ds-heat-hot-bg);
    color: var(--ds-heat-hot-text);
}

/* The top band gets a shadow as well as a colour. It is the one state that
 * should be visible from the other side of the page. */
.ds-row__heat--blazing {
    border-color: var(--ds-heat-blazing);
    background: var(--ds-heat-blazing-bg);
    color: var(--ds-heat-blazing-text);
    box-shadow: var(--ds-shadow-1);
}

@media (max-width: 29.99em) {
    .ds-row__heat {
        min-width: 3rem;
        padding-inline: var(--ds-space-1);
    }

    .ds-row__heat-value {
        font-size: var(--ds-text-base);
    }
}

/* =========================================================================
 * 11. Strips and tiles (§4.2, §9.3)
 * =========================================================================
 * THE GRID IS NOT DISCARDED — it is used where a SHORT list is expected, so
 * three items look deliberate rather than like a half-empty five-column
 * layout. Hottest-today, store tiles, category tiles. All three are
 * horizontal scrollers at every width, which is what makes that true.
 * ========================================================================= */

.ds-strip {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-3);
    min-width: 0;
}

.ds-strip__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--ds-space-3);
}

.ds-strip__title {
    margin: 0;
    font-size: var(--ds-text-md);
    font-weight: var(--ds-weight-bold);
    letter-spacing: var(--ds-tracking-tight);
    color: var(--ds-color-heading);
}

.ds-strip__scroller {
    display: flex;
    gap: var(--ds-space-3);
    margin: 0;
    padding: 0 0 var(--ds-space-2);
    list-style: none;
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: thin;
}

.ds-strip__scroller > * {
    scroll-snap-align: start;
}

.ds-tile {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-2);
    flex: 0 0 10.5rem;
    padding: var(--ds-space-3);
    background: var(--ds-color-surface);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-lg);
    text-decoration: none;
    transition:
        border-color var(--ds-duration-fast) var(--ds-ease),
        box-shadow var(--ds-duration-fast) var(--ds-ease),
        transform var(--ds-duration-fast) var(--ds-ease);
}

.ds-tile:hover,
.ds-tile:focus-within {
    border-color: var(--ds-color-border-strong);
    box-shadow: var(--ds-shadow-2);
    transform: translateY(-2px);
}

@media (min-width: 48em) {
    .ds-tile {
        flex-basis: 12rem;
    }
}

.ds-tile__img {
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: contain;
    padding: var(--ds-space-2);
    background: var(--ds-color-surface-sunken);
    border-radius: var(--ds-radius-md);
}

.ds-tile__title {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin: 0;
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-semibold);
    line-height: var(--ds-leading-snug);
    color: var(--ds-color-heading);
}

.ds-tile__title a {
    color: inherit;
    text-decoration: none;
}

.ds-tile__title a::after {
    content: "";
    position: absolute;
    inset: 0;
}

.ds-tile__price {
    margin: 0 0 0;
    margin-top: auto;
    font-size: var(--ds-text-base);
    font-weight: var(--ds-weight-bold);
    color: var(--ds-color-heading);
    font-variant-numeric: tabular-nums;
}

.ds-store-tile {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--ds-space-2);
    flex: 0 0 7.5rem;
    min-height: 6rem;
    padding: var(--ds-space-3);
    background: var(--ds-color-surface);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-lg);
    text-align: center;
    text-decoration: none;
    transition:
        border-color var(--ds-duration-fast) var(--ds-ease),
        box-shadow var(--ds-duration-fast) var(--ds-ease);
}

.ds-store-tile:hover {
    border-color: var(--ds-color-primary);
    box-shadow: var(--ds-shadow-2);
}

.ds-store-tile__logo {
    width: 3rem;
    height: 3rem;
    object-fit: contain;
}

.ds-store-tile__name {
    font-size: var(--ds-text-xs);
    font-weight: var(--ds-weight-semibold);
    color: var(--ds-color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 100%;
}

.ds-store-tile__name a {
    color: inherit;
    text-decoration: none;
}

.ds-store-tile__name a::after {
    content: "";
    position: absolute;
    inset: 0;
}

/* =========================================================================
 * 12. Buttons (§9.4)
 * =========================================================================
 * THREE STATES THAT DIFFER BY MORE THAN A BORDER. A button whose only hover
 * feedback is a one-pixel line changing colour is a button that feels dead on
 * a trackpad. Every button rests with a shadow, LIFTS on hover and PRESSES on
 * active, and §18 removes the movement for anyone who asked for less motion.
 * ========================================================================= */

.ds-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--ds-space-2);
    min-height: 2.25rem;
    padding: var(--ds-space-2) var(--ds-space-4);
    border: var(--ds-border-width) solid var(--ds-color-border-strong);
    border-radius: var(--ds-radius-md);
    background: var(--ds-color-surface);
    color: var(--ds-color-text);
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-semibold);
    line-height: var(--ds-leading-tight);
    text-align: center;
    text-decoration: none;
    white-space: nowrap;
    box-shadow: var(--ds-shadow-1);
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        border-color var(--ds-duration-fast) var(--ds-ease),
        color var(--ds-duration-fast) var(--ds-ease),
        box-shadow var(--ds-duration-fast) var(--ds-ease),
        transform var(--ds-duration-fast) var(--ds-ease);
}

.ds-btn:hover {
    text-decoration: none;
    box-shadow: var(--ds-shadow-2);
    transform: translateY(-1px);
}

.ds-btn:active {
    box-shadow: var(--ds-shadow-0);
    transform: translateY(1px);
}

.ds-btn svg {
    width: 1em;
    height: 1em;
    flex: none;
}

.ds-btn[disabled],
.ds-btn[aria-disabled="true"] {
    opacity: 0.55;
    cursor: not-allowed;
    pointer-events: none;
    box-shadow: none;
    transform: none;
}

.ds-btn--primary {
    background: var(--ds-color-primary);
    border-color: var(--ds-color-primary);
    color: var(--ds-color-text-on-primary);
    box-shadow: var(--ds-shadow-2);
}

.ds-btn--primary:visited,
.ds-btn--primary:hover {
    color: var(--ds-color-text-on-primary);
}

.ds-btn--primary:hover {
    background: var(--ds-color-primary-hover);
    border-color: var(--ds-color-primary-hover);
    box-shadow: var(--ds-shadow-3);
}

.ds-btn--primary:active {
    box-shadow: var(--ds-shadow-1);
}

/* THE OUTBOUND BUTTON. `Get deal`, `Reveal code`, `Go to store`. It is the
 * loudest control the site has and it is deliberately a different hue from
 * every navigational primary, so "this one leaves the site" is legible before
 * the label is read. */
.ds-btn--cta {
    background: var(--ds-color-cta);
    border-color: var(--ds-color-cta);
    color: var(--ds-color-text-on-cta);
    box-shadow: var(--ds-shadow-2);
}

.ds-btn--cta:visited,
.ds-btn--cta:hover {
    color: var(--ds-color-text-on-cta);
}

.ds-btn--cta:hover {
    background: var(--ds-color-cta-hover);
    border-color: var(--ds-color-cta-hover);
    box-shadow: var(--ds-shadow-3);
}

.ds-btn--cta:active {
    background: var(--ds-color-cta-active);
    border-color: var(--ds-color-cta-active);
    box-shadow: var(--ds-shadow-1);
}

/* Gold. It survives as a button ONLY where the action is community-facing —
 * upvote, follow, save — and never on an outbound path. */
.ds-btn--accent {
    background: var(--ds-color-accent);
    border-color: var(--ds-color-accent);
    color: var(--ds-color-text-on-accent);
}

.ds-btn--accent:visited,
.ds-btn--accent:hover {
    color: var(--ds-color-text-on-accent);
}

.ds-btn--accent:hover {
    background: var(--ds-color-accent-hover);
    border-color: var(--ds-color-accent-hover);
    box-shadow: var(--ds-shadow-3);
}

/* The quiet one: a real button that does not ask to be pressed. Secondary
 * actions in a form row, "Cancel", "Not now". */
.ds-btn--quiet {
    background: transparent;
    border-color: transparent;
    color: var(--ds-color-link);
    box-shadow: var(--ds-shadow-0);
}

.ds-btn--quiet:hover {
    background: var(--ds-color-primary-soft);
    color: var(--ds-color-primary-soft-text);
    box-shadow: var(--ds-shadow-1);
}

.ds-btn--quiet:active,
.ds-btn--outline:active {
    box-shadow: var(--ds-shadow-0);
}

.ds-btn--outline {
    background: transparent;
    border-color: var(--ds-color-border-strong);
    color: var(--ds-color-primary);
    box-shadow: var(--ds-shadow-0);
}

.ds-btn--outline:hover {
    background: var(--ds-color-primary-soft);
    border-color: var(--ds-color-primary);
    color: var(--ds-color-primary-soft-text);
    box-shadow: var(--ds-shadow-1);
}

.ds-btn--danger {
    background: var(--ds-color-danger);
    border-color: var(--ds-color-danger);
    color: var(--ds-color-text-inverse);
}

.ds-btn--danger:visited,
.ds-btn--danger:hover {
    color: var(--ds-color-text-inverse);
}

.ds-btn--danger:hover {
    box-shadow: var(--ds-shadow-2);
}

.ds-btn--sm {
    min-height: 1.875rem;
    padding: var(--ds-space-1) var(--ds-space-3);
    font-size: var(--ds-text-xs);
}

.ds-btn--lg {
    min-height: 2.75rem;
    padding: var(--ds-space-3) var(--ds-space-6);
    font-size: var(--ds-text-md);
}

.ds-btn--block {
    display: flex;
    width: 100%;
}

/* =========================================================================
 * 13. Pills and badges (§9.4)
 * =========================================================================
 * `.ds-pill` is the §9.4 name. `.ds-badge` is the v1 name for the same shape
 * and it is KEPT rather than renamed — thirty templates emit it, and §9.4's
 * instruction is "extend what exists, do not rename". They share a rule.
 * ========================================================================= */

.ds-pill,
.ds-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-space-1);
    padding: 1px var(--ds-space-2);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-pill);
    background: var(--ds-color-surface-sunken);
    color: var(--ds-color-text-muted);
    font-size: var(--ds-text-2xs);
    font-weight: var(--ds-weight-semibold);
    letter-spacing: var(--ds-tracking-wide);
    line-height: 1.5;
    text-transform: uppercase;
    white-space: nowrap;
}

.ds-pill svg,
.ds-badge svg {
    width: 0.75em;
    height: 0.75em;
}

.ds-pill--verified {
    background: var(--ds-color-success-bg);
    border-color: var(--ds-color-success-border);
    color: var(--ds-color-success);
}

.ds-pill--unverified {
    background: var(--ds-color-warning-bg);
    border-color: var(--ds-color-warning-border);
    color: var(--ds-color-warning);
}

.ds-pill--hot {
    background: var(--ds-heat-hot-bg);
    border-color: var(--ds-heat-hot);
    color: var(--ds-heat-hot-text);
}

.ds-pill--new {
    background: var(--ds-color-info-bg);
    border-color: var(--ds-color-info-border);
    color: var(--ds-color-info);
}

.ds-pill--expired {
    background: var(--ds-color-surface-sunken);
    border-color: var(--ds-color-border-strong);
    color: var(--ds-color-text-muted);
    text-decoration: none;
}

.ds-badge--code {
    background: var(--ds-color-accent-soft);
    border-color: var(--ds-color-accent);
    color: var(--ds-color-accent-text);
}

.ds-badge--code {
    border-style: dashed;
    font-family: var(--ds-font-mono);
    font-size: var(--ds-text-xs);
    letter-spacing: var(--ds-tracking-normal);
    text-transform: none;
}

/* A chip is a pill you can click — a category filter, a tag. */
.ds-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-space-2);
    padding: var(--ds-space-1) var(--ds-space-3);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-pill);
    background: var(--ds-color-surface);
    color: var(--ds-color-text-muted);
    font-size: var(--ds-text-xs);
    font-weight: var(--ds-weight-medium);
    text-decoration: none;
    white-space: nowrap;
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        border-color var(--ds-duration-fast) var(--ds-ease),
        color var(--ds-duration-fast) var(--ds-ease);
}

a.ds-chip:hover,
button.ds-chip:hover {
    background: var(--ds-color-primary-soft);
    border-color: var(--ds-color-primary);
    color: var(--ds-color-primary-soft-text);
    text-decoration: none;
}

.ds-chip[aria-pressed="true"] {
    background: var(--ds-color-primary);
    border-color: var(--ds-color-primary);
    color: var(--ds-color-text-on-primary);
}

/* =========================================================================
 * 14. Panels, tabs, empty states, breadcrumbs (§9.4)
 * ========================================================================= */

/* THE PANEL OWNS ITS PADDING AND THE TWO PARTS CANCEL IT.
 *
 * `.ds-panel` is used two ways in the templates: as a bare box with content
 * dropped straight into it, and as a titled panel with `__title` + `__body`.
 * Padding on the block with `:has()` to remove it again is the obvious answer
 * and it is the wrong one — a browser without `:has()` drops the rule and one
 * of the two shapes loses its padding entirely, silently, on the layout that
 * is hardest to notice. So the padding is unconditional and each part pulls
 * itself back out to the panel's edge with a negative margin, which works
 * everywhere and needs no feature query.
 */
.ds-panel {
    padding: var(--ds-space-4);
    background: var(--ds-color-surface);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-lg);
    overflow: hidden;
}

/* The `:not()` is load-bearing rather than tidy: `.ds-panel > :last-child`
 * scores higher than `.ds-panel__body`, so without it this rule would win and
 * strip the body's negative bottom margin, leaving the panel's own padding
 * showing under a block that is supposed to reach the edge. */
.ds-panel > :last-child:not(.ds-panel__body):not(.ds-panel__title) {
    margin-bottom: 0;
}

.ds-panel__title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ds-space-3);
    margin: calc(var(--ds-space-4) * -1) calc(var(--ds-space-4) * -1) var(--ds-space-4);
    padding: var(--ds-space-3) var(--ds-space-4);
    border-bottom: var(--ds-border-width) solid var(--ds-color-border);
    background: var(--ds-color-surface-sunken);
    color: var(--ds-color-heading);
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-bold);
    letter-spacing: var(--ds-tracking-normal);
}

/* A title with nothing after it would otherwise leave the panel's own
 * padding-bottom stacked under its border. */
.ds-panel__title:last-child {
    margin-bottom: calc(var(--ds-space-4) * -1);
}

.ds-panel__body {
    margin: calc(var(--ds-space-4) * -1);
    padding: var(--ds-space-4);
}

/* Directly under a title the two negative margins would double up. */
.ds-panel__title + .ds-panel__body {
    margin-top: calc(var(--ds-space-4) * -1);
}

.ds-panel__body > :last-child {
    margin-bottom: 0;
}

.ds-panel--quiet {
    background: var(--ds-color-surface-quiet);
    border-color: var(--ds-color-info-border);
}

/* --- Tabs (Hot | New | Discussed) ------------------------------------- */

.ds-tabs {
    display: flex;
    align-items: stretch;
    gap: var(--ds-space-1);
    min-width: 0;
    margin: 0;
    padding: 0;
    list-style: none;
    border-bottom: var(--ds-border-width) solid var(--ds-color-border);
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scrollbar-width: none;
}

.ds-tabs::-webkit-scrollbar {
    display: none;
}

.ds-tabs__link {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-space-2);
    flex: none;
    padding: var(--ds-space-2) var(--ds-space-3);
    margin-bottom: -1px;
    border-bottom: var(--ds-border-width-thick) solid transparent;
    color: var(--ds-color-text-muted);
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-semibold);
    text-decoration: none;
    white-space: nowrap;
    transition:
        border-color var(--ds-duration-fast) var(--ds-ease),
        color var(--ds-duration-fast) var(--ds-ease);
}

.ds-tabs__link:visited {
    color: var(--ds-color-text-muted);
}

.ds-tabs__link:hover {
    border-bottom-color: var(--ds-color-border-strong);
    color: var(--ds-color-text);
    text-decoration: none;
}

.ds-tabs__link--active,
.ds-tabs__link[aria-current="page"],
.ds-tabs__link[aria-selected="true"] {
    border-bottom-color: var(--ds-color-cta);
    color: var(--ds-color-heading);
}

.ds-tabs__link--active:visited {
    color: var(--ds-color-heading);
}

/* --- Empty states -----------------------------------------------------
 * A REAL DESIGN, NOT A SENTENCE (§7.1). An empty search result is the page a
 * visitor is most likely to leave from, so it gets an icon, a title, a line
 * of copy and — where the caller supplies one — an action.
 * -------------------------------------------------------------------- */

.ds-empty {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--ds-space-2);
    padding: var(--ds-space-8) var(--ds-space-5);
    border: var(--ds-border-width) dashed var(--ds-color-border-strong);
    border-radius: var(--ds-radius-lg);
    background: var(--ds-color-surface);
    color: var(--ds-color-text);
    text-align: center;
    justify-items: center;
}

.ds-empty__icon {
    display: block;
    box-sizing: border-box;
    width: 2.75rem;
    height: 2.75rem;
    padding: var(--ds-space-2);
    margin: 0 0 var(--ds-space-1);
    border-radius: var(--ds-radius-circle);
    background: var(--ds-color-primary-soft);
    color: var(--ds-color-primary-soft-text);
}

.ds-empty__title {
    margin: 0;
    font-size: var(--ds-text-md);
    font-weight: var(--ds-weight-bold);
    color: var(--ds-color-heading);
}

.ds-empty__text {
    margin: 0;
    max-width: 52ch;
    font-size: var(--ds-text-sm);
    line-height: var(--ds-leading-relaxed);
    color: var(--ds-color-text-muted);
}

.ds-empty__action {
    margin-top: var(--ds-space-3);
}

/* --- Breadcrumb ------------------------------------------------------- */

.ds-breadcrumb {
    margin-bottom: var(--ds-space-3);
    font-size: var(--ds-text-xs);
    color: var(--ds-color-text-muted);
}

.ds-breadcrumb ol,
.ds-breadcrumb ul {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ds-space-2);
    list-style: none;
    margin: 0;
    padding: 0;
}

.ds-breadcrumb__item {
    display: inline-flex;
    align-items: center;
    gap: var(--ds-space-2);
    min-width: 0;
    color: inherit;
}

.ds-breadcrumb li + li::before,
.ds-breadcrumb__item + .ds-breadcrumb__item::before {
    content: "/";
    color: var(--ds-color-text-faint);
}

.ds-breadcrumb a {
    color: inherit;
    text-decoration: none;
}

.ds-breadcrumb a:hover {
    color: var(--ds-color-link-hover);
    text-decoration: underline;
}

.ds-breadcrumb [aria-current="page"] {
    color: var(--ds-color-text);
    font-weight: var(--ds-weight-medium);
}

/* =========================================================================
 * 15. Forms and the auth card (§9.4, §7.3)
 * =========================================================================
 * `.ds-field__label`, `__hint` and `__error` are the §9.4 names, and they are
 * now the ONLY names. The v1 twins that used to share these three rules —
 * ds-label, ds-help, ds-field-error, plus ds-label__optional, ds-fieldset,
 * ds-check, ds-error and ds-form--wide — were emitted by nothing and are
 * deleted; §9.7 records them. ds-field-error against ds-field__error is the
 * near-miss pair A13 exists to catch, and one of the two is now gone.
 * ========================================================================= */

.ds-form {
    max-width: 34rem;
}

.ds-field {
    display: block;
    margin-bottom: var(--ds-space-4);
}

.ds-field__label {
    display: block;
    margin-bottom: var(--ds-space-1);
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-semibold);
    color: var(--ds-color-heading);
}

.ds-input,
.ds-select,
.ds-textarea {
    display: block;
    width: 100%;
    min-height: 2.5rem;
    padding: var(--ds-space-2) var(--ds-space-3);
    background: var(--ds-color-surface);
    border: var(--ds-border-width) solid var(--ds-color-border-strong);
    border-radius: var(--ds-radius-md);
    color: var(--ds-color-text);
    font-size: var(--ds-text-base);
    line-height: var(--ds-leading-snug);
    box-shadow: var(--ds-shadow-inset);
    transition:
        border-color var(--ds-duration-fast) var(--ds-ease),
        box-shadow var(--ds-duration-fast) var(--ds-ease);
}

.ds-input:hover,
.ds-select:hover,
.ds-textarea:hover {
    border-color: var(--ds-color-focus);
}

/* The field owns its focus treatment — a ring drawn OUTSIDE a bordered box
 * that already changes colour reads as two rings. It is a replacement, not a
 * removal: the box-shadow is the ring. */
.ds-input:focus,
.ds-select:focus,
.ds-textarea:focus,
.ds-input:focus-visible,
.ds-select:focus-visible,
.ds-textarea:focus-visible {
    outline: none;
    border-color: var(--ds-color-focus);
    box-shadow: var(--ds-focus-ring);
}

.ds-textarea {
    min-height: 8rem;
    resize: vertical;
}

.ds-input::placeholder,
.ds-textarea::placeholder {
    color: var(--ds-color-text-muted);
}

.ds-input[aria-invalid="true"],
.ds-select[aria-invalid="true"],
.ds-textarea[aria-invalid="true"] {
    border-color: var(--ds-color-danger);
}

.ds-field__hint {
    display: block;
    margin-top: var(--ds-space-1);
    font-size: var(--ds-text-xs);
    color: var(--ds-color-text-muted);
}

.ds-field__error {
    display: block;
    margin-top: var(--ds-space-1);
    font-size: var(--ds-text-xs);
    font-weight: var(--ds-weight-medium);
    color: var(--ds-color-danger);
}

.ds-form-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ds-space-3);
    align-items: center;
    margin-top: var(--ds-space-5);
}

/* --- The reason picker on /report (§9.8) ---------------------------------
 *
 * `app/Views/site/report/form.php` and nothing else. A radio group is the one
 * form shape §9.4's primitives do not cover: `.ds-field` is a label above one
 * control, and this is one legend above six.
 *
 * THE RADIO IS INSIDE ITS LABEL, so the whole row is the hit area rather than
 * the 13px dot. That is the difference between a control a thumb can use at
 * 375px and one it cannot, and the `min-height` below is the 44px floor made
 * explicit rather than left to the text's line box.
 *
 * The fieldset carries no border of its own: the rows are bordered and read as
 * a group without a second frame, which is the argument `account/password.php`
 * already makes for not wrapping `.ds-form` in a panel.
 * ---------------------------------------------------------------------- */

.ds-reasons {
    margin: 0 0 var(--ds-space-4);
    padding: 0;
    border: 0;
}

.ds-reasons__row {
    display: flex;
    gap: var(--ds-space-3);
    align-items: center;
    min-height: 2.75rem;
    margin-top: var(--ds-space-2);
    padding: var(--ds-space-2) var(--ds-space-3);
    background: var(--ds-color-surface);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-md);
    cursor: pointer;
    transition: border-color var(--ds-duration-fast) var(--ds-ease);
}

.ds-reasons__row:hover {
    border-color: var(--ds-color-focus);
}

/* `:has()` rather than a class the template toggles: which row is selected is
 * the browser's business and a server-rendered `--selected` would be stale the
 * moment the member clicked a different one. */
.ds-reasons__row:has(input:checked) {
    border-color: var(--ds-color-focus);
}

.ds-reasons__row input {
    flex: none;
    width: 1.125rem;
    height: 1.125rem;
    accent-color: var(--ds-color-focus);
}

/* THERE IS NO `.ds-search` RULE HERE ANY MORE, AND ITS ABSENCE IS THE FIX.
 *
 * A v1 search-FORM component lived at this spot — `.ds-search, .ds-search__form
 * { display: flex; flex-wrap: wrap; … }` — and no view has emitted either class
 * since the v2 rebuild. What DOES emit `ds-search` is
 * `Site\SearchController`, as `$body_class`: `<body class="ds-search">`.
 *
 * So the dead component rule was turning the BODY ELEMENT of `/search` into a
 * flex container. `.ds-shell` then became a flex ITEM, whose automatic minimum
 * size (`min-width: auto`) is its min-content width — and the widest thing in
 * the shell is `.ds-nav__inner`, whose five `flex: none` links come to 398px.
 * The whole page was therefore laid out 399px wide inside a 360px viewport and
 * scrolled sideways; at 768px and 1024px the same mechanism made it 1056px.
 * Measured: `documentElement.scrollWidth` 399 vs 360, 399 vs 375, 1056 vs 768,
 * 1056 vs 1024. Only 414px, 1280px and 1600px happened to be wide enough to
 * hide it.
 *
 * THE PAGE-SCOPE CLASSES SHARE THE `ds-` NAMESPACE WITH THE COMPONENTS — every
 * public controller sets one (`ds-home`, `ds-deals`, `ds-coupon`, `ds-legal`,
 * …) — so a component named after a page is a collision waiting to happen and
 * `arch:check` cannot see it: A13 only checks view → stylesheet, and this rule
 * had no view. The only other body class with a rule is `.ds-deal`, which is
 * written as a DESCENDANT selector (`.ds-deal .ds-row__price-now`) and is
 * correct for that reason. Any future page-scope rule goes the same way.
 * ------------------------------------------------------------------------ */

/* --- The auth card ----------------------------------------------------
 * §7.3: login and register are the FIRST screens a new member sees and they
 * currently look like forms. A centred narrow card, the logo, one clear CTA.
 * -------------------------------------------------------------------- */

.ds-auth-card {
    width: 100%;
    max-width: 25rem;
    margin-inline: auto;
    margin-block: var(--ds-space-6);
    padding: var(--ds-space-6);
    background: var(--ds-color-surface);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-xl);
    box-shadow: var(--ds-shadow-2);
}

@media (min-width: 40em) {
    .ds-auth-card {
        padding: var(--ds-space-8);
        margin-block: var(--ds-space-10);
    }
}

.ds-auth-card .ds-form {
    max-width: none;
}

.ds-auth-card__logo {
    display: block;
    height: 2.25rem;
    width: auto;
    margin: 0 auto var(--ds-space-5);
}

.ds-auth-card__title {
    margin: 0 0 var(--ds-space-5);
    font-size: var(--ds-text-xl);
    font-weight: var(--ds-weight-bold);
    text-align: center;
    color: var(--ds-color-heading);
}

.ds-auth-card__foot {
    margin-top: var(--ds-space-5);
    padding-top: var(--ds-space-4);
    border-top: var(--ds-border-width) solid var(--ds-color-border);
    font-size: var(--ds-text-sm);
    color: var(--ds-color-text-muted);
    text-align: center;
}

.ds-auth-card__foot p {
    margin: 0 0 var(--ds-space-2);
}

.ds-auth-card__foot > :last-child {
    margin-bottom: 0;
}

/* =========================================================================
 * 16. Detail pages, tables, pagination, notices
 * =========================================================================
 * §7.2 calls the deal and coupon pages a RESTYLE — the structure is intact
 * and the price block is matched to the row's so the two agree.
 * ========================================================================= */

.ds-page-head {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-2);
    margin-bottom: var(--ds-space-5);
    padding-bottom: var(--ds-space-4);
    border-bottom: var(--ds-border-width) solid var(--ds-color-border);
}

.ds-page-head__title {
    margin: 0;
    font-size: var(--ds-text-2xl);
}

@media (min-width: 48em) {
    .ds-page-head__title {
        font-size: var(--ds-text-3xl);
    }
}

.ds-page-head__lede {
    margin: 0;
    max-width: var(--ds-prose);
    font-size: var(--ds-text-md);
    line-height: var(--ds-leading-relaxed);
    color: var(--ds-color-text-muted);
}

/* The deal hero. A 4/3 image beside the price block from 48em up. */
.ds-grid {
    display: grid;
    gap: var(--ds-space-4);
    grid-template-columns: minmax(0, 1fr);
}

.ds-grid--cards {
    gap: var(--ds-space-4);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
}

@media (min-width: 48em) {
    .ds-grid--deal-hero {
        gap: var(--ds-space-6);
        grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
        align-items: start;
    }
}

.ds-deal__media {
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 4 / 3;
    padding: var(--ds-space-4);
    background: var(--ds-color-surface-sunken);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-lg);
    overflow: hidden;
}

.ds-deal__img {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
}

.ds-deal__placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--ds-color-text-faint);
}

/* --- The hero's heat (§7.2, §9.5) --------------------------------------
 * §7.2 asks for the heat score in the hero, and the pill itself is §10's --
 * `site/deal/show.php` emits `ds-row__heat` and the presenter's band modifier,
 * class for class with the feed row. So EVERY colour, border, radius and type
 * step below comes from §10 and a fifth band added there lands here for free.
 *
 * THE ONE THING RE-STATED IS THE POSITION. `.ds-row__heat` is `position:
 * absolute` because on a row it is pinned to the row's own top-right corner
 * (§9's note). Dropped into the hero it would resolve against whatever
 * positioned ancestor happened to be nearest, which is not this block and is
 * not stable. `.ds-deal__heat .ds-row__heat` is (0,2,0) and beats the (0,1,0)
 * rule in §10 on specificity rather than on source order, so it survives the
 * two files being edited independently.
 *
 * `ds-deal__heat-none` IS THE ABSENT STATE AND IT IS NOT AN EMPTY BOX. A
 * product `deals:rerank` has never scored has no heat, which is not `0°` and is
 * the common case on a fresh install. The row omits the pill entirely; the hero
 * has one slot that §7.2 promised a number in, so it says "Not scored yet"
 * instead. It is DASHED and unfilled on purpose — a solid pill in the ramp's
 * neutral would read as a fifth, coldest band, which is the fake number
 * `HeatPresenter` exists to refuse.
 * --------------------------------------------------------------------- */

.ds-deal__heat {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ds-space-2);
    margin: 0;
}

.ds-deal__heat-label {
    font-size: var(--ds-text-2xs);
    font-weight: var(--ds-weight-bold);
    letter-spacing: var(--ds-tracking-wide);
    text-transform: uppercase;
    color: var(--ds-color-text-muted);
}

.ds-deal__heat .ds-row__heat {
    position: static;
    top: auto;
    right: auto;
}

.ds-deal__heat-none {
    display: inline-flex;
    align-items: center;
    padding: 2px var(--ds-space-3);
    border: var(--ds-border-width) dashed var(--ds-color-border-strong);
    border-radius: var(--ds-radius-pill);
    background: var(--ds-color-surface-sunken);
    color: var(--ds-color-text-muted);
    font-size: var(--ds-text-xs);
    line-height: 1.6;
}

/* --- The sticky mobile CTA (§7.2) --------------------------------------
 * BELOW 48em ONLY, AND IT IS THE ONLY CONTROL ON THE SITE THAT GETS THIS
 * TREATMENT. On a deal page the outbound button is the page's whole purpose,
 * and on a phone it is roughly 700px above the fold by the time somebody has
 * read the description.
 *
 * `position: sticky` RATHER THAN `fixed`, WHICH IS THE WHOLE REASON THE FOOTER
 * STAYS READABLE. A fixed bar overlays the document for its entire length and
 * has to be paid for with a `padding-bottom` somewhere further out — a magic
 * number in another region of this file that stops matching the moment the
 * bar's own padding changes. Sticky leaves the element IN FLOW as the last
 * child of `<article>`: pinned to the bottom of the viewport while the article
 * is scrolling past, then parked in its own space. The closing panel and the
 * whole footer live outside that article, so nothing this bar could cover is
 * ever underneath it.
 *
 * IT IS NOT FULL-BLEED. Insetting it inside the page gutter keeps the rounded
 * card reading as a floating shortcut rather than as a second, permanent piece
 * of chrome competing with the header.
 * --------------------------------------------------------------------- */

.ds-deal__dock {
    display: none;
}

@media (max-width: 47.99em) {
    .ds-deal__dock {
        display: block;
        position: sticky;
        bottom: var(--ds-space-3);
        z-index: var(--ds-z-sticky);
        padding: var(--ds-space-2);
        border: var(--ds-border-width) solid var(--ds-color-border);
        border-radius: var(--ds-radius-lg);
        background: var(--ds-color-surface);
        box-shadow: var(--ds-shadow-3);
    }
}

.ds-article__media {
    margin: var(--ds-space-5) 0;
    aspect-ratio: 16 / 9;
    background: var(--ds-color-surface-sunken);
    border-radius: var(--ds-radius-lg);
    overflow: hidden;
}

.ds-article__img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The coupon reveal. THE CODE IS THE PAGE (§7.2). */
.ds-code-panel {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-4);
    margin-block: var(--ds-space-4);
    padding: var(--ds-space-5);
    border: var(--ds-border-width-thick) dashed var(--ds-color-cta);
    border-radius: var(--ds-radius-lg);
    background: var(--ds-color-cta-soft);
}

.ds-code-panel__code {
    margin: 0;
    text-align: center;
}

.ds-code-panel__code code {
    display: block;
    padding: var(--ds-space-4) var(--ds-space-3);
    background: var(--ds-color-surface);
    border: var(--ds-border-width) solid var(--ds-color-border-strong);
    border-radius: var(--ds-radius-md);
    font-size: var(--ds-text-3xl);
    font-weight: var(--ds-weight-bold);
    line-height: var(--ds-leading-tight);
    letter-spacing: var(--ds-tracking-wide);
    color: var(--ds-color-heading);
    overflow-wrap: anywhere;
    -webkit-user-select: all;
    user-select: all;
}

.ds-code-panel .ds-code-panel__copy {
    margin: 0;
}

.ds-code-panel__copy .ds-input {
    font-family: var(--ds-font-mono);
}

@media (min-width: 40em) {
    .ds-code-panel {
        flex-direction: row;
        align-items: center;
    }

    .ds-code-panel__code,
    .ds-code-panel__copy {
        flex: 1 1 0;
        min-width: 0;
    }
}

.ds-stat {
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-1);
}

.ds-stat__value {
    font-size: var(--ds-text-xl);
    font-weight: var(--ds-weight-bold);
    line-height: var(--ds-leading-tight);
    letter-spacing: var(--ds-tracking-tight);
    color: var(--ds-color-heading);
    font-variant-numeric: tabular-nums;
}

.ds-stat__label {
    font-size: var(--ds-text-2xs);
    text-transform: uppercase;
    letter-spacing: var(--ds-tracking-wide);
    color: var(--ds-color-text-muted);
}

/* --- The comment thread: the depth indent -----------------------------
 * `site/discussion/_comments.php` emits `data-depth` on every comment and
 * nothing in this file indented on it, so a reply drew exactly like a
 * top-level comment and the thread read as a flat list. That partial's own
 * docblock says so, and says why the attribute is the hook: the array the
 * presenter returns is FLAT with a depth number per row, so nesting is
 * presentation over a value that was already decided, and one selector turns
 * it on without inventing a class per level (which would fail A13) or a
 * recursive render (which would be logic in a template).
 *
 * SCOPED TO `#comments`, WHICH IS THE ONLY PLACE THE ATTRIBUTE EXISTS on a
 * public page. `community.css` has a depth vocabulary of its own for the
 * private wall; it is not loaded here, and an unscoped `[data-depth]` in this
 * file would be a standing invitation for the two to collide the day some
 * other view adopts the attribute.
 *
 * THE CAP IS THE POINT, AND IT IS EXPRESSED AS "EVERYTHING ELSE".
 * `Comment::MAX_DEPTH` is 3 — "a public page is read at 360 pixels wide" — so
 * the ladder below runs 0, 1, 2, 3 and the BARE `[data-depth]` rule sits FIRST
 * as the catch-all: any value not named (a 4 that a future cap allows, a stale
 * row, a number nobody planned) lands on the level-3 indent instead of walking
 * off the screen. The four selectors are all (1,1,0), so it is source order
 * that makes the general rule lose to the specific ones — which is why it is
 * written above them and must stay there.
 *
 * THE STEP IS A CUSTOM PROPERTY so the narrow-viewport reduction is one
 * declaration rather than four. Measured at 375px: the step drops from 24px to
 * 12px, so the deepest comment starts 36px in and keeps 307 of 343 available
 * pixels. At the desktop step it would have surrendered 72px — a fifth of a
 * phone's line length, for a reply that is only three deep.
 *
 * THE LEFT BORDER IS WHAT ACTUALLY SAYS "REPLY". Indentation alone is
 * ambiguous once a comment wraps, and it is invisible to anyone who has zoomed
 * far enough that the step is a fraction of a word. The rule is on the panel
 * the comment already is, so no new class is emitted and A13 has nothing new
 * to check.
 * --------------------------------------------------------------------- */

#comments [data-depth] {
    --ds-thread-step: var(--ds-space-6);

    margin-inline-start: calc(var(--ds-thread-step) * 3);
    border-inline-start: 3px solid var(--ds-color-border-strong);
}

#comments [data-depth="0"] {
    margin-inline-start: 0;
    border-inline-start: var(--ds-border-width) solid var(--ds-color-border);
}

#comments [data-depth="1"] {
    margin-inline-start: var(--ds-thread-step);
}

#comments [data-depth="2"] {
    margin-inline-start: calc(var(--ds-thread-step) * 2);
}

#comments [data-depth="3"] {
    margin-inline-start: calc(var(--ds-thread-step) * 3);
}

@media (max-width: 29.99em) {
    #comments [data-depth] {
        --ds-thread-step: var(--ds-space-3);
    }
}

/* --- The comment thread: the section and its composer -----------------
 * `site/partials/comments.php` is now rendered by TWO pages — the article and
 * the deal page — so the block needs a name of its own rather than borrowing
 * whatever the surrounding template happened to provide. On `/discussion/` it
 * sat inside an editorial column that gave it its spacing; on `/deal/` it is a
 * top-level grid item of `.ds-main`, beside panels rather than prose.
 *
 * THE SELECTORS ARE THE CLASS AND NOT `#comments`, EXCEPT FOR THE DEPTH LADDER
 * ABOVE. That ladder is scoped to the id deliberately (see its own note: an
 * unscoped `[data-depth]` would collide with `community.css`'s wall
 * vocabulary), but a class is what everything else in this file styles, and an
 * id selector's specificity is a fight the next author should not have to have.
 *
 * THE COMPOSER IS SEPARATED BY A RULE RATHER THAN BY SPACE. What sits below it
 * is a WRITE control under a list of other people's words, and the two are
 * different kinds of thing; a gap alone reads as more thread. It is the same
 * hairline the panels use, so nothing new enters the palette — and per the
 * file's own rule, NO LITERAL COLOUR appears here or anywhere in this
 * stylesheet.
 *
 * `.ds-comments__prompt` IS A CLUSTER AND NOT A PARAGRAPH. It holds a button
 * and a sentence, which wrap independently at 360px; `margin: 0` because the
 * `padding-top` above already owns the separation and a `<p>`'s own 16px would
 * double it.
 *
 * WHAT THE COMPOSER LOOKS LIKE WHEN JAVASCRIPT HAS RUN IS NOT STYLED HERE, and
 * that is not an omission. `@site/comments.js` builds `ds-form`, `ds-field`,
 * `ds-field__label`, `ds-textarea`, `ds-btn ds-btn--primary` and
 * `ds-alert ds-alert--{info,danger}` — every one of them an existing component
 * with an existing rule. A composer with its own private form vocabulary is
 * how two form systems start.
 * --------------------------------------------------------------------- */

.ds-comments {
    display: block;
}

.ds-comments > h2 {
    margin-bottom: var(--ds-space-4);
}

.ds-comments__composer {
    margin-top: var(--ds-space-5);
    padding-top: var(--ds-space-5);
    border-top: var(--ds-border-width) solid var(--ds-color-border);
}

.ds-comments__prompt {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ds-space-3);
    margin: 0;
}

/* --- The comment thread: the byline, the reply control, and your own
 *     comments that are not on the page yet ----------------------------
 * THREE OF THESE FOUR ARE BUILT BY JAVASCRIPT AND ONE BY THE VIEW, and the
 * split is ARCHITECTURE.md §6.5 rather than taste. `partials/comments.php`
 * prints `.ds-comments__author` into an edge-cached document — a byline is a
 * fact about the comment and is the same for every visitor. The reply button
 * and the pending block are facts about WHO IS READING, so they cannot be in
 * bytes served to thousands and are added by `@site/comment-thread.js` after
 * `GET /api/session`. With no JavaScript neither exists and the thread is
 * exactly what the server sent.
 *
 * `.ds-comments__author` IS NOT A PILL. The date beside it is one, because a
 * date is a small closed fact; a name is the subject of the sentence and reads
 * as a label the moment it gets a chip around it. It takes weight instead —
 * the same move `.ds-row__author` makes in the feed.
 *
 * `.ds-comments__reply` IS A `<button>` DRAWN AS A QUIET LINK, and it is a
 * button because it performs an action on this page rather than going
 * anywhere. It sits in the same `.ds-cluster` as the date and the permalink,
 * at the same size as the permalink beside it, because "reply to this" is not
 * more important than the comment it hangs off. The touch-target block at the
 * end of this file gives it the same bleed the permalink gets.
 *
 * `.ds-comments__pending` IS SEPARATED BY A RULE, ABOVE the composer and BELOW
 * the thread — SEO-SPEC.md §5.3 item 3 puts it there and item 1 requires the
 * container to arrive EMPTY. What lands in it is your own words, which is a
 * different kind of thing from the approved thread above; a gap alone would
 * read as more thread.
 *
 * `.ds-comments__mine` IS DIMMED AND DASHED. It is a `.ds-panel` like an
 * approved comment and must not be mistaken for one: the dashed border says
 * "provisional" without a colour carrying the meaning, which matters because
 * the two states inside it — waiting and refused — are told apart by WORDS in
 * a pill, never by hue. NO LITERAL COLOUR appears here, per this file's rule.
 * --------------------------------------------------------------------- */

.ds-comments__author {
    font-weight: var(--ds-weight-semibold);
    color: var(--ds-color-text);
}

.ds-comments__reply {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    font-size: var(--ds-text-xs);
    color: var(--ds-color-text-muted);
    text-decoration: underline;
    text-underline-offset: 0.15em;
    cursor: pointer;
}

.ds-comments__reply:hover,
.ds-comments__reply:focus-visible {
    color: var(--ds-color-primary);
}

.ds-comments__pending {
    margin-top: var(--ds-space-5);
    padding-top: var(--ds-space-5);
    border-top: var(--ds-border-width) solid var(--ds-color-border);
}

.ds-comments__pending > h3 {
    margin-bottom: var(--ds-space-3);
    font-size: var(--ds-text-sm);
    text-transform: uppercase;
    letter-spacing: var(--ds-tracking-wide);
    color: var(--ds-color-text-muted);
}

.ds-comments__mine {
    border-style: dashed;
    background: var(--ds-color-surface-sunken);
}

/* --- Notices --------------------------------------------------------- */

.ds-alert {
    display: flex;
    gap: var(--ds-space-3);
    padding: var(--ds-space-3) var(--ds-space-4);
    margin-bottom: var(--ds-space-4);
    border: var(--ds-border-width) solid var(--ds-color-info-border);
    border-left-width: 3px;
    border-radius: var(--ds-radius-md);
    background: var(--ds-color-info-bg);
    color: var(--ds-color-info);
    font-size: var(--ds-text-sm);
    line-height: var(--ds-leading-normal);
}

.ds-alert__icon {
    flex: none;
    width: 1.25rem;
    height: 1.25rem;
    margin-top: 0.1rem;
}

.ds-alert__body {
    flex: 1 1 auto;
    min-width: 0;
}

.ds-alert__body > :last-child {
    margin-bottom: 0;
}

.ds-alert a {
    color: inherit;
    font-weight: var(--ds-weight-semibold);
}

.ds-alert--info {
    border-color: var(--ds-color-info-border);
    background: var(--ds-color-info-bg);
    color: var(--ds-color-info);
}

.ds-alert--success {
    border-color: var(--ds-color-success-border);
    background: var(--ds-color-success-bg);
    color: var(--ds-color-success);
}

.ds-alert--warning {
    border-color: var(--ds-color-warning-border);
    background: var(--ds-color-warning-bg);
    color: var(--ds-color-warning);
}

.ds-alert--danger {
    border-color: var(--ds-color-danger-border);
    background: var(--ds-color-danger-bg);
    color: var(--ds-color-danger);
}

/* --- Tables ---------------------------------------------------------- */

.ds-table-wrap {
    overflow-x: auto;
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-lg);
    background: var(--ds-color-surface);
}

.ds-table {
    min-width: 32rem;
    font-size: var(--ds-text-sm);
}

.ds-table :is(th, td) {
    padding: var(--ds-space-2) var(--ds-space-4);
    text-align: left;
    vertical-align: top;
    border-bottom: var(--ds-border-width) solid var(--ds-color-border);
}

.ds-table th {
    background: var(--ds-color-surface-sunken);
    color: var(--ds-color-heading);
    font-weight: var(--ds-weight-semibold);
    white-space: nowrap;
}

.ds-table tbody tr:last-child :is(th, td) {
    border-bottom: 0;
}

.ds-table tbody tr:hover :is(th, td) {
    background: var(--ds-color-surface-quiet);
}

/* --- Pagination ------------------------------------------------------ */

.ds-pager {
    margin-top: var(--ds-space-8);
}

.ds-pager ul,
.ds-pager__list {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-start;
    align-items: center;
    gap: var(--ds-space-1);
    list-style: none;
    margin: 0;
    padding: 0;
}

.ds-pager__link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2.25rem;
    min-height: 2.25rem;
    padding: var(--ds-space-1) var(--ds-space-3);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-md);
    background: var(--ds-color-surface);
    color: var(--ds-color-link);
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-medium);
    text-decoration: none;
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        border-color var(--ds-duration-fast) var(--ds-ease),
        color var(--ds-duration-fast) var(--ds-ease);
}

.ds-pager__link:hover {
    background: var(--ds-color-primary-soft);
    border-color: var(--ds-color-primary);
    color: var(--ds-color-primary-soft-text);
    text-decoration: none;
}

.ds-pager__link[aria-current="page"] {
    background: var(--ds-color-primary);
    border-color: var(--ds-color-primary);
    color: var(--ds-color-text-on-primary);
}

/* =========================================================================
 * 16b. The notification inbox, and the header bell (§9.8)
 * =========================================================================
 * `NOTIFICATIONS-SPEC.md` §3.1: "a row in `notifications`, A BELL WITH AN
 * UNREAD COUNT, a `/notifications` page". This is the styling for both halves.
 *
 * THE ROW IS NOT `.ds-row`. §9.2's row is a deal: a thumbnail, a price, a heat
 * pill and a CTA, arranged in a grid that exists to be scanned at 104px a
 * time. A notification is a sentence with a date under it and sometimes a
 * button, it is read rather than compared, and forcing it into the deal row
 * would mean overriding almost every rule in that component. It is a short
 * stack of text in a bordered box, which is what it looks like.
 *
 * UNREAD IS THREE SIGNALS AND ONLY ONE OF THEM IS COLOUR. §2.2's rule — the
 * left border thickens and goes navy, the surface lifts to the quiet tint, and
 * the markup adds a `ds-pill` whose WORD is "New" plus a "Mark read" button
 * that a read row does not have. Somebody who cannot separate the tint from
 * white still has a word and a control.
 *
 * THE BORDER IS 3px ON EVERY ROW, READ OR UNREAD, and only its COLOUR changes.
 * Growing the border on the unread state would move the text three pixels to
 * the right as a row is marked read, which is a whole list reflowing under the
 * reader's finger for no reason.
 * ========================================================================= */

.ds-notice-list {
    list-style: none;
    margin: var(--ds-space-4) 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-3);
}

.ds-notice {
    padding: var(--ds-space-3) var(--ds-space-4);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-left: 3px solid var(--ds-color-border-strong);
    border-radius: var(--ds-radius-md);
    background: var(--ds-color-surface);
}

.ds-notice--unread {
    border-left-color: var(--ds-color-primary);
    background: var(--ds-color-surface-quiet);
}

/* THE WHOLE HEADLINE IS THE LINK — the sentence, the subject and the verb.
 *
 * `color: inherit` AND `text-decoration: none` ARE THE POINT OF THIS RULE.
 * §6's base `a` paints every link `--ds-color-link` and underlines it, which
 * on a block this size would repaint the row's heading and its subject line
 * blue and underline two paragraphs. The affordance lives on
 * `.ds-notice__action` below instead: one coloured, underlined verb that says
 * where the row goes, under a headline that still reads as a headline.
 *
 * NO FOCUS RULE HERE AND NONE IS NEEDED. §6's `:where(a, button, …)
 * :focus-visible` is zero-specificity and applies to this anchor untouched, so
 * the keyboard ring is the site's and not a second opinion about it. A row
 * with no destination emits no anchor at all, so it has no focus stop and
 * nothing to suppress. */
.ds-notice__open {
    display: block;
    color: inherit;
    text-decoration: none;
}

.ds-notice__text {
    margin: 0;
    font-weight: var(--ds-weight-semibold);
    color: var(--ds-color-heading);
}

.ds-notice__subject {
    margin: var(--ds-space-1) 0 0;
    font-size: var(--ds-text-sm);
}

/* THE VERB, AND IT IS THE ONLY PART OF THE ROW THAT IS PAINTED LIKE A LINK.
 *
 * It is a `<span>` inside the anchor and never a control of its own: a second
 * focusable thing pointing at the same href would be two tab stops and two
 * announcements for one destination. The underline is permanent rather than
 * on hover, because the complaint this answers is that the rows read as text —
 * a cue that only appears once the pointer is already on it answers a question
 * nobody got far enough to ask, and answers nothing at all on a phone. */
.ds-notice__action {
    display: inline-block;
    margin: var(--ds-space-2) 0 0;
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-semibold);
    color: var(--ds-color-link);
    text-decoration: underline;
}

.ds-notice__open:hover .ds-notice__action {
    color: var(--ds-color-link-hover);
}

/* The arrow is decoration and is generated rather than written into the
 * markup, so it never reaches the accessible name: a screen reader announces
 * "Open the moderation queue, link" and not "…right arrow". */
.ds-notice__action::after {
    content: ' \2192';
}

.ds-notice__detail {
    margin: var(--ds-space-1) 0 0;
    font-size: var(--ds-text-sm);
    color: var(--ds-color-text-muted);
}

/* The line a row prints when its subject has been deleted. QUIET AND ITALIC
 * AND NOT AN ALERT: the notification is still correct and still the member's
 * to read — `DATA-MODEL.md` §13.1 has no foreign keys precisely so it survives
 * — and dressing it as an error would suggest something went wrong with THEIR
 * news rather than with the thing it pointed at. */
.ds-notice__gone {
    margin: var(--ds-space-1) 0 0;
    font-size: var(--ds-text-sm);
    font-style: italic;
    color: var(--ds-color-text-faint);
}

.ds-notice__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ds-space-2);
    margin: var(--ds-space-2) 0 0;
    font-size: var(--ds-text-xs);
    color: var(--ds-color-text-muted);
}

/* The per-row form. `margin-inline-start: auto` pushes it to the far end of the
 * meta line, and `flex-wrap` on the parent is what stops it colliding with the
 * date on a 320px screen — it drops to its own line instead. */
.ds-notice__actions {
    margin-inline-start: auto;
}

/* THE BELL IS NEVER IN THE SERVED HTML. `partials/site-header.php` is
 * edge-cached and user-neutral (ARCHITECTURE.md §6.5), so `site/bell.js` builds
 * this element from `GET /api/session` — the same contract
 * `.ds-header__account` already has. These rules exist for the moment after it
 * does, and A13 counts the class because the module emits it. */
.ds-header__bell {
    position: relative;
}

/* The count, over the bell's top-right corner.
 *
 * ACCENT AND NOT CTA. §2.2 reserves the CTA role for "act now" — the one role
 * that may fill a button sending a visitor to a merchant — and an unread count
 * is not that. Accent "means WHAT THE COMMUNITY THINKS", which is exactly what
 * a pile of replies is, and gold on the near-black bar is the highest-contrast
 * pairing the palette has up there. */
.ds-header__bell-count {
    position: absolute;
    top: -0.25rem;
    inset-inline-end: -0.25rem;
    min-width: 1.125rem;
    height: 1.125rem;
    padding-inline: 0.25rem;
    border-radius: var(--ds-radius-circle);
    background: var(--ds-color-accent);
    color: var(--ds-color-text-on-accent);
    font-size: var(--ds-text-2xs);
    font-weight: var(--ds-weight-bold);
    line-height: 1.125rem;
    text-align: center;
}

/* =========================================================================
 * 16c. The saved-items list — `/account/saves`
 * =========================================================================
 * IT IS THE INBOX'S SHAPE AND NOT THE DEAL ROW'S, for the reason the block
 * above gives: a saved item is a title, a kind and a date with one control on
 * it — read rather than compared — and forcing it into `.ds-row` would mean
 * overriding almost every rule of a component built around a thumbnail, a
 * price and a heat band.
 *
 * IT IS NOT `.ds-notice` EITHER, and the difference is worth a block of its
 * own rather than a modifier. A notification has an UNREAD STATE and this has
 * none, so `.ds-notice`'s whole left-border mechanism — 3px on every row,
 * recoloured by `--unread` — would be three pixels of inset on a list where
 * nothing ever changes colour. These rows are flat cards; what varies on them
 * is a pill and a button, both of which say their state in words.
 *
 * THE KIND IS ON EVERY ROW BECAUSE THE PAGE IS TELLING THE TRUTH.
 * `VoteTarget::SAVEABLE` names four types and exactly one of them has a Save
 * control in the product today, so a list headed "Saved items" that silently
 * held one kind of thing would be a promise about the other three. The chip is
 * `--ds-color-surface-sunken` and not a hue: it is a CATEGORY, not a state, and
 * §2.2 keeps colour for what the community thinks.
 * ========================================================================= */

/* The alert ratio, above the list. It is a sentence and not a stat value:
 * `.ds-stat__value` is 21px tabular and a whole clause set that way reads as a
 * headline. Quiet, because it is context for the controls below it rather than
 * the reason the page exists. */
.ds-saved-quota {
    margin: 0 0 var(--ds-space-2);
    font-size: var(--ds-text-sm);
    color: var(--ds-color-text-muted);
}

.ds-saved-list {
    list-style: none;
    margin: var(--ds-space-4) 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-3);
}

.ds-saved {
    padding: var(--ds-space-3) var(--ds-space-4);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-md);
    background: var(--ds-color-surface);
}

.ds-saved__title {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--ds-space-2);
    margin: 0;
    font-weight: var(--ds-weight-semibold);
    color: var(--ds-color-heading);
}

/* WHAT KIND OF THING THIS IS. `align-self` is not set, so it sits on the
 * title's baseline and wraps under it on a narrow screen rather than squeezing
 * the title into one word per line. */
.ds-saved__kind {
    padding: 0.1rem 0.5rem;
    border-radius: var(--ds-radius-pill);
    background: var(--ds-color-surface-sunken);
    color: var(--ds-color-text-muted);
    font-size: var(--ds-text-xs);
    font-weight: var(--ds-weight-medium);
    white-space: nowrap;
}

/* The line a row prints when there is nothing left to open — `.ds-notice__gone`
 * at the same weight and for the same reason. The save is still correct and
 * still the member's to clear, so it is quiet and italic rather than an alert
 * about something that did not go wrong. */
.ds-saved__gone {
    margin: var(--ds-space-1) 0 0;
    font-size: var(--ds-text-sm);
    font-style: italic;
    color: var(--ds-color-text-faint);
}

/* What the saved post's own status means to somebody holding a bookmark on it.
 * Not italic: this one is a fact about the thing, not about the row. */
.ds-saved__note {
    margin: var(--ds-space-1) 0 0;
    font-size: var(--ds-text-sm);
    color: var(--ds-color-text-muted);
}

.ds-saved__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ds-space-2);
    margin: var(--ds-space-2) 0 0;
    font-size: var(--ds-text-xs);
    color: var(--ds-color-text-muted);
}

/* The per-row form, at the far end of the meta line. `flex-wrap` on the parent
 * is what stops it colliding with the date and the pill at 320px — it drops to
 * its own line instead, which is the behaviour `.ds-notice__actions` already
 * relies on. */
.ds-saved__actions {
    margin-inline-start: auto;
}

/* =========================================================================
 * 17. Retained v1 components, and the legal preview banner
 * =========================================================================
 * EVERY RULE IN THIS SECTION EXISTS BECAUSE A TEMPLATE STILL EMITS THE CLASS.
 * `arch:check` A13 fails the build for a class with no rule anywhere, and the
 * markup for §7's 52 views is being rewritten in parallel with this file — so
 * the v1 vocabulary stays defined until the last template that names it is
 * converted, rather than the build going red on somebody else's half-finished
 * page. They are all re-pointed at the v2 tokens, so a retained component
 * looks like the new design rather than like the old one.
 * ========================================================================= */

/* --- v1 shell and containers ----------------------------------------- */

.ds-container {
    width: 100%;
    margin-inline: auto;
    padding-inline: var(--ds-space-4);
}

.ds-container { max-width: var(--ds-container); }

@media (min-width: 48em) {
    .ds-container {
        padding-inline: var(--ds-space-6);
    }
}

/* Inside the v2 `.ds-main`, which is already the container, a nested
 * container must not add a second max-width or a second gutter. */
.ds-main .ds-container {
    max-width: none;
    margin-inline: 0;
    padding-inline: 0;
}

/* --- v1 left sidebar, superseded by `.ds-drawer` ---------------------- */

.ds-sidebar {
    position: fixed;
    top: var(--ds-header-height);
    left: 0;
    bottom: 0;
    z-index: var(--ds-z-drawer);
    display: flex;
    flex-direction: column;
    gap: var(--ds-space-5);
    width: min(19rem, 84vw);
    padding: var(--ds-space-4) var(--ds-space-3) var(--ds-space-5);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--ds-color-rail-bg);
    border-right: var(--ds-border-width) solid var(--ds-color-border);
    box-shadow: var(--ds-shadow-4);
}

.ds-sidebar[hidden] {
    display: none;
}

@media (min-width: 64em) {
    .ds-sidebar {
        display: none;
    }
}

/* --- v1 header controls ---------------------------------------------- */

.ds-nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--ds-space-2);
    flex: none;
    min-width: 2.25rem;
    min-height: 2.25rem;
    padding: var(--ds-space-2);
    background: transparent;
    border: var(--ds-border-width) solid var(--ds-overlay-border);
    border-radius: var(--ds-radius-md);
    color: inherit;
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-medium);
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        border-color var(--ds-duration-fast) var(--ds-ease);
}

.ds-nav-toggle:hover {
    background: var(--ds-overlay-hover);
    border-color: var(--ds-color-secondary);
}

.ds-nav-toggle[aria-expanded="true"] {
    background: var(--ds-overlay-active);
    border-color: var(--ds-color-secondary);
}

@media (min-width: 64em) {
    .ds-nav-toggle { display: none; }
}

.ds-header__link {
    display: none;
    padding: var(--ds-space-2);
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-medium);
    white-space: nowrap;
}

@media (min-width: 40em) {
    .ds-header__link { display: inline-block; }
}

.ds-header__cta {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 2.25rem;
    padding: var(--ds-space-2) var(--ds-space-4);
    background: var(--ds-color-cta);
    border: var(--ds-border-width) solid var(--ds-color-cta);
    border-radius: var(--ds-radius-pill);
    color: var(--ds-color-text-on-cta);
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-semibold);
    white-space: nowrap;
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        border-color var(--ds-duration-fast) var(--ds-ease);
}

.ds-header__cta:hover,
.ds-header__cta:visited {
    color: var(--ds-color-text-on-cta);
}

.ds-header__cta:hover {
    background: var(--ds-color-cta-hover);
    border-color: var(--ds-color-cta-hover);
}

.ds-header__cta:active,
.ds-nav-toggle:active,
.ds-header__search-link:active {
    background: var(--ds-overlay-active);
    transform: translateY(1px);
}

.ds-header__account {
    display: inline-block;
    max-width: 6rem;
    padding: var(--ds-space-2);
    overflow: hidden;
    font-size: var(--ds-text-sm);
    font-weight: var(--ds-weight-semibold);
    text-overflow: ellipsis;
    white-space: nowrap;
    border-radius: var(--ds-radius-md);
    transition: background-color var(--ds-duration-fast) var(--ds-ease);
}

.ds-header__account:hover {
    background: var(--ds-overlay-hover);
    color: var(--ds-color-header-text-strong);
}

@media (min-width: 40em) {
    .ds-header__account { max-width: 8rem; }
}

.ds-header__signout {
    display: none;
    margin: 0;
}

@media (min-width: 40em) {
    .ds-header__signout { display: inline-flex; }
}

.ds-header__search-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 2.25rem;
    height: 2.25rem;
    border: var(--ds-border-width) solid var(--ds-overlay-border);
    border-radius: var(--ds-radius-md);
    background: transparent;
    color: inherit;
    transition:
        background-color var(--ds-duration-fast) var(--ds-ease),
        border-color var(--ds-duration-fast) var(--ds-ease);
}

.ds-header__search-link:hover {
    background: var(--ds-overlay-hover);
    border-color: var(--ds-color-secondary);
}

@media (min-width: 48em) {
    .ds-header__search-link { display: none; }
}

/* --- The brand inside the footer -------------------------------------- */

/* All that is left of the v1 footer. Its seven `ds-footer__{grid,cols,heading,
 * list,blurb,bottom}` names and `ds-footer__col--brand` were a second footer
 * vocabulary that lost to `__inner __col __title __link __legal` (§8) and are
 * deleted; this one rule is about the LIVE brand class and stays. */
.ds-footer .ds-header__brand { color: var(--ds-color-footer-heading); }

/* --- v1 layout primitives -------------------------------------------- */

.ds-stack > * + * { margin-top: var(--ds-space-4); }
.ds-stack--tight > * + * { margin-top: var(--ds-space-2); }

.ds-cluster {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ds-space-2);
}

.ds-section { margin-block: var(--ds-space-8); }
.ds-section:first-child { margin-top: 0; }

.ds-section__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--ds-space-3);
    margin-bottom: var(--ds-space-4);
}

.ds-community__main { min-width: 0; }

/* --- v1 card, superseded by `.ds-row` --------------------------------- */

.ds-card {
    position: relative;
    display: flex;
    flex-direction: column;
    background: var(--ds-color-surface);
    border: var(--ds-border-width) solid var(--ds-color-border);
    border-radius: var(--ds-radius-lg);
    box-shadow: var(--ds-shadow-1);
    overflow: hidden;
    transition:
        box-shadow var(--ds-duration-base) var(--ds-ease),
        border-color var(--ds-duration-base) var(--ds-ease),
        transform var(--ds-duration-base) var(--ds-ease);
}

.ds-card:hover,
.ds-card:focus-within {
    transform: translateY(-2px);
    border-color: var(--ds-color-border-strong);
    box-shadow: var(--ds-shadow-3);
}

.ds-card:active {
    transform: translateY(0);
    box-shadow: var(--ds-shadow-2);
}

.ds-card__media {
    position: relative;
    aspect-ratio: 4 / 3;
    background: var(--ds-color-surface-sunken);
    overflow: hidden;
}

.ds-card__img,
.ds-card__media img {
    position: relative;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* THE DEAL PAGE'S PRICE IS THE ROW'S PRICE, TWO STEPS LOUDER.
 *
 * DESIGN-SYSTEM.md 4.3 says the saving gets the strongest treatment on the
 * site, and the deal page is where somebody has already decided to look. The
 * ROW is deliberately restrained -- it has forty neighbours -- so the hero has
 * to re-state the same component at hero size rather than invent a second
 * price block that could drift from it.
 *
 * `ds-card__price` was the v1 name and the deal page no longer emits it; the
 * markup now emits the 9.2 row names. THE CARD SELECTOR WAS KEPT HERE AS A
 * FALLBACK AND HAS BEEN DELETED — no view, controller, presenter or client
 * module emits `ds-card__price` any more, so the fallback protected nothing
 * and only made two names look current. §9.7 records the deletion. */
.ds-deal .ds-row__price-now {
    font-size: var(--ds-text-2xl);
    line-height: var(--ds-leading-tight);
}

@media (min-width: 48em) {
    .ds-deal .ds-row__price-now { font-size: var(--ds-text-3xl); }
}

/* --- v1 odds and ends ------------------------------------------------- */

.ds-legal__entity {
    font-style: normal;
    margin-block: var(--ds-space-4);
    padding-inline-start: var(--ds-space-3);
    border-inline-start: 3px solid var(--ds-color-border-strong);
    line-height: var(--ds-leading-relaxed);
}

.ds-legal__entity-name { font-weight: var(--ds-weight-semibold); }

.ds-skeleton {
    min-height: 1rem;
    border-radius: var(--ds-radius-md);
    background: linear-gradient(
        100deg,
        var(--ds-color-surface-sunken) 30%,
        var(--ds-color-surface-quiet) 50%,
        var(--ds-color-surface-sunken) 70%
    );
    background-size: 300% 100%;
    animation: ds-shimmer 1.6s var(--ds-ease) infinite;
}

@keyframes ds-shimmer {
    from { background-position: 150% 0; }
    to   { background-position: -150% 0; }
}

@keyframes ds-reveal-rise {
    from {
        opacity: 0;
        transform: translateY(0.75rem);
    }
}

/* `.is-in` is added by JS; the element is fully painted without it. */
.ds-reveal.is-in {
    animation: ds-reveal-rise var(--ds-duration-slow) var(--ds-ease) backwards;
}

.ds-reveal.is-in:nth-child(2) { animation-delay: 40ms; }
.ds-reveal.is-in:nth-child(3) { animation-delay: 80ms; }
.ds-reveal.is-in:nth-child(4) { animation-delay: 120ms; }
.ds-reveal.is-in:nth-child(5) { animation-delay: 160ms; }
.ds-reveal.is-in:nth-child(6) { animation-delay: 200ms; }
.ds-reveal.is-in:nth-child(7) { animation-delay: 240ms; }
.ds-reveal.is-in:nth-child(8) { animation-delay: 280ms; }

/* --- THE LEGAL DEVELOPMENT PREVIEW BANNER ----------------------------
 * `app/Views/site/legal/_head.php` carried the LAST inline `style=` block in
 * the codebase, and §5 of DESIGN-SYSTEM.md says it should move into a
 * stylesheet as part of this step "rather than left as precedent" — the CSP
 * carries `unsafe-inline` on `style-src` only until the editor's output can be
 * hashed (D73), and one hand-written exception is how a policy stops being a
 * policy.
 *
 * IT IS STILL DELIBERATELY UGLY, AND THAT IS THE SPECIFICATION. The banner
 * renders only under `CI_ENVIRONMENT = development`, on a page that answers
 * 503 anywhere else. A tidy amber notice reads as part of the design and gets
 * screenshotted into a review as though the page were finished. This has to be
 * impossible to mistake for content and impossible to leave in — so it is the
 * one component on the site that ignores the radius scale, the type scale and
 * the shadow set on purpose.
 *
 * It does use the DANGER tokens rather than a literal red, because "no literal
 * colour in this file" has no exceptions and the danger ramp is already the
 * loudest thing the palette has.
 * -------------------------------------------------------------------- */

.ds-preview-banner {
    margin: var(--ds-space-4) 0;
    padding: var(--ds-space-4);
    border: 4px dashed var(--ds-color-danger);
    background: var(--ds-color-danger-bg);
    color: var(--ds-color-danger);
    font-family: var(--ds-font-mono);
    font-size: var(--ds-text-sm);
    line-height: var(--ds-leading-normal);
}

.ds-preview-banner__title {
    display: block;
    font-weight: var(--ds-weight-bold);
    text-transform: uppercase;
    letter-spacing: var(--ds-tracking-wide);
}

.ds-preview-banner__text {
    margin: var(--ds-space-2) 0 0;
    font-family: var(--ds-font-mono);
}

/* `ul.` RATHER THAN THE BARE CLASS, AND THE REASON IS SPECIFICITY.
 *
 * The reset carries `ul[class] { list-style: none; padding: 0 }` -- an
 * attribute selector, so (0,1,1). A bare `.ds-preview-banner__list` is (0,1,0)
 * and LOSES: the markers and the indent never render, silently, on the one
 * banner whose whole job is to look unfinished. Qualifying with the tag makes
 * it (0,1,1) and wins on order. */
ul.ds-preview-banner__list {
    margin: var(--ds-space-1) 0 0;
    padding-inline-start: var(--ds-space-6);
    list-style: square;
}

/* =========================================================================
 * 17b. Touch targets
 * =========================================================================
 * EVERY RULE IN THIS SECTION IS `(max-width: 63.99em), (pointer: coarse)` AND
 * THAT PAIR IS THE WHOLE DESIGN OF IT.
 *
 * THE WIDTH HALF STOPS AT 64em AND NOT AT 48em. 48em is where the search pill
 * replaces the search icon; 64em is where §4.1 turns the page into two columns
 * and puts the drawer away, and everything below it is the phone-and-tablet
 * shape of this design. A 768px viewport is a tablet held in the hand, and it
 * was the width where the first pass of this section left every control at its
 * desktop size — 36px header buttons, a 26px "Get deal" — on the one device
 * class that has no cursor at all.
 *
 * THE POINTER HALF IS THE ONE THAT IS ACTUALLY CORRECT, and it is `pointer`
 * rather than `any-pointer`: `any-pointer: coarse` is true of a laptop with a
 * touchscreen that its owner drives with a trackpad, and giving that person
 * 44px controls would be answering a question they did not ask. `pointer`
 * describes the PRIMARY input, so a 1280px tablet gets the big targets and a
 * 1280px desktop does not.
 *
 * §4.3's row is a DENSE row — 104px with a 26px CTA — and it is dense on
 * purpose, because on a 1280px screen the reader is holding a mouse and the
 * thing that costs them is how many deals fit above the fold. None of that is
 * true on a phone, where the pointer is a finger with a contact patch around
 * 9mm and the same 26px button is a coin toss. So the desktop density is
 * untouched below and the phone gets 44px, which is the number both platform
 * guidelines use and the one WCAG 2.5.5 names.
 *
 * MEASURED BEFORE (375px viewport, /deals unless noted):
 *   #site-nav-toggle            36 x 36     the control that opens the drawer
 *   .ds-header__search-link     36 x 36     the only search entry on a phone
 *   .ds-header__action--primary 52 x 36     "Join"
 *   .ds-header__link            58 x 36.1   "Sign in", from 40em
 *   .ds-nav__link               58.8 x 40
 *   .ds-tabs__link              46.2 x 38.1
 *   .ds-row__cta                65 x 32     the money button, x17 per page
 *   .ds-btn                     ... x 36    incl. the deal page's sticky dock
 *   .ds-input                   ... x 40
 *   .ds-chip                    71.4 x 27.9
 *   .ds-rail__more              311 x 43.3
 *   .ds-footer__link            213 x 22.1  x11
 *   .ds-rail__item a            39.8 x 33.8 x13, in the <40em store scroller
 *   .ds-theme__option           192 x 36    x3, the theme menu's rows
 *
 * A SECOND PASS ADDED FIVE MORE, and the reason the first pass missed them is
 * worth keeping because it generalises: A CONTROL THAT IS `hidden` WHEN THE
 * PAGE LOADS IS NOT IN A SWEEP OF THE PAGE AS IT LOADS.
 *
 * MEASURED AFTER OPENING THE THING (375px, /deals unless noted):
 *   .ds-drawer__link            295 x 40    x13 — THE DRAWER HAS TO BE OPENED
 *   .ds-pager__link             36 x 36     styled, not on screen — see below
 *   .ds-auth-card__foot a       160.4 x 16  /login, x2; 41.6 x 16 on /register
 *   .ds-input & friends         ... 15px    a font-size problem, not a size one
 *   .ds-header__search-input    ... 13px    same
 *
 * `.ds-drawer__link` IS THE WORST OF THE SET AND IT IS THE ONE THIS SECTION
 * EXISTS FOR. The drawer is the ONLY component on the site that exists at no
 * other width — `site.css` §7 gives it `display: none` from 64em up — so its
 * thirteen 40px rows are the phone's entire category and section navigation
 * and there is no desktop reading of them to protect. The first pass swept the
 * loaded document, the drawer ships `hidden` (`partials/site-sidebar.php`
 * writes it by hand), and `getBoundingClientRect()` on a `hidden` subtree
 * returns zeroes that a "< 44" test quietly skips.
 *
 * The drawer's OTHER five behaviours were measured at the same time and are
 * all correct, which is why only the link height changed: `html` goes
 * `overflow: hidden` while it is open, `.ds-nav`/`.ds-main`/`.ds-footer` go
 * `inert`, Escape closes it, focus returns to `#site-nav-toggle`, and the
 * scrim closes it on a tap. The theme menu measured 44 x 44 with 174 x 44 rows
 * and needed nothing.
 *
 * THE THEME BUTTON ITSELF IS NOT LISTED and needs no rule of its own: it
 * carries `.ds-header__action`, so it is already covered three rules down by
 * the same `min-width`/`height` that grows "Join". Its MENU rows are listed,
 * because a 36px row in a popup is the one part of that control a finger
 * actually has to aim at.
 *
 * TWO OF THESE DELIBERATELY DO NOT REACH 44 AND ARE HANDLED SEPARATELY, AT
 * THE BOTTOM: `.ds-row__store` and `.ds-row__comments` are metadata inside a
 * row whose whole surface is already a link, and a 44px comment count would
 * add 26px to every row on six pages to make a secondary target easier to hit
 * than the primary one. They get a bled hit area instead, which clears WCAG
 * 2.5.8's 24px floor without moving a pixel of layout.
 *
 * `.ds-header__search-btn` (30 x 30) IS NOT HERE EITHER: it only exists from
 * 48em up, inside the search pill, on the widths this section does not cover.
 *
 * ONE OF THESE CONTRADICTS THE DOCUMENT AND IT IS DELIBERATE.
 * `DESIGN-SYSTEM.md` §4.1 fixes the nav shelf at 40px. Below 48em this section
 * makes it 44, so the five section links are hittable; the shelf still scrolls
 * away, so the 4px is not 4px of permanent chrome. Above 48em it is 40px
 * exactly as written.
 * ========================================================================= */

@media (max-width: 63.99em), (pointer: coarse) {
    .ds-header__burger,
    .ds-header__search-link {
        width: 2.75rem;
        height: 2.75rem;
    }

    /* `.ds-header__bell` IS NAMED HERE EVEN THOUGH IT ALSO CARRIES
     * `.ds-header__action` AND WOULD BE COVERED WITHOUT IT.
     *
     * It is the one control on the bar that this file's original sweep could
     * not have measured, for the reason the drawer link taught: it is not in
     * the served HTML at all — `site/bell.js` creates it after `/api/session`
     * answers — so a 375px audit of the loaded document finds nothing to
     * measure and reports the bar clean. Naming it explicitly means the day
     * somebody gives the bell its own class list instead of borrowing
     * `__action`'s, the 44px does not silently go with it.
     *
     * The COUNT is not listed and needs nothing: it is an 18px label sitting on
     * top of the 44px target, not a target of its own, and it is
     * `pointer-events` transparent by being inside the button. */
    .ds-header__action,
    .ds-header__bell {
        min-width: 2.75rem;
        height: 2.75rem;
    }

    /* The brand is a link home and was 120 x 36. It used to share its rules
     * with a v1 ds-brand, which was the auth card's logo and not a control, so
     * this rule named the header class alone. ds-brand is emitted by nothing
     * and has been deleted (§9.7); the reasoning is kept because the
     * distinction — a control gets 44px, a logo does not — still applies. */
    .ds-header__brand {
        min-height: 2.75rem;
    }

    /* The shelf, not the link: `.ds-nav__inner` is `align-items: stretch`, so
     * raising the container is what makes all five links 44px tall without a
     * padding value that has to be kept in step with the shelf height. */
    .ds-nav__inner {
        min-height: 2.75rem;
    }

    /* `.ds-btn--sm` IS LISTED BESIDE `.ds-btn` AND IT IS NOT REDUNDANT.
     * §12 gives the small button `min-height: 1.875rem` — 30px — with the same
     * specificity as `.ds-btn`, so which of the two wins here is decided
     * purely by SOURCE ORDER. It happens to be this block, because §17b is
     * further down the file. That is a true statement and a fragile one: move
     * this section above §12 and every small button on a phone silently goes
     * back to 30px, with no rule failing and nothing to grep for. Naming the
     * modifier makes the outcome explicit instead of incidental.
     *
     * The control that brought this up is the inbox's "Mark read", which is
     * `.ds-btn .ds-btn--sm` once per unread row. */
    .ds-tabs__link,
    .ds-chip,
    .ds-btn,
    .ds-btn--sm,
    .ds-row__cta,
    .ds-rail__more,
    .ds-input,
    .ds-select,
    .ds-textarea,
    .ds-theme__option,
    .ds-drawer__link,
    .ds-pager__link {
        min-height: 2.75rem;
    }

    /* THE PAGER IS A ROW OF ADJACENT TARGETS, so its width matters as much as
     * its height. `.ds-pager__link` is `min-width: 2.25rem` — 36 x 36 — with
     * `--ds-space-1` between two of them, so raising only the height would
     * leave a 36px-wide "7" four pixels from a 36px-wide "8". Both dimensions
     * go to 44 and the flex row wraps, which it already does.
     *
     * It is not on screen on the current database: `/deals?page=2` renders no
     * `.ds-pager` at all because 17 rows fit on one page. It is styled, it is
     * §9.4's component, and it appears on six listings the moment the feed
     * outgrows a page — which is the wrong moment to discover the buttons are
     * 36px. */
    .ds-pager__link {
        min-width: 2.75rem;
    }

    /* THE INBOX ROW'S LINK. It wraps a sentence, sometimes a title and always
     * a verb, so at 375px it measures around 90px tall and this rule changes
     * nothing on any row that exists today.
     *
     * It is written anyway, and the reason is the one `.ds-header__bell` above
     * records: a sweep measures what happened to be on the page. The shortest
     * possible row here is a one-line sentence with no subject — "Your password
     * was changed." with a destination, the day one is added — and that is a
     * two-line block whose height depends on a font size this file's §3 has
     * already tightened once. The floor costs nothing and removes the question. */
    .ds-notice__open {
        min-height: 2.75rem;
    }

    /* iOS SAFARI ZOOMS THE WHOLE PAGE when it focuses a field set below 16px,
     * AND IT DOES NOT ZOOM BACK OUT when the field is blurred. The reader is
     * left on a page 1.4x too wide, scrolled sideways, with no control that
     * undoes it — on a site whose §4.1 shell is otherwise exactly 375px wide.
     *
     * EVERY FIELD ON THE SITE TRIGGERS IT, because every field is
     * `--ds-text-base` and §3 tightened that to 15px for density. Measured at
     * 375px: 15px on `/search`, on `/login` (x2), on `/register` (x5) and on
     * the coupon reveal's copy box; 13px on the header pill, which is the
     * search control from 48em up.
     *
     * 16px IS A BROWSER THRESHOLD, NOT A SCALE STEP, and that is why this is
     * `1rem` rather than a token. §3's ladder has no 16px step, and minting
     * one so a field can clear Safari's rule is the scale drift §3 spends two
     * corrections warning about. It is scoped to this block, so the desktop
     * density §3 chose is untouched.
     *
     * The label, the hint and the error keep their sizes: Safari measures the
     * focused control and nothing around it. */
    .ds-input,
    .ds-select,
    .ds-textarea,
    .ds-header__search-input {
        font-size: 1rem;
    }

    /* `.ds-rail__item` is already 44px, but below 40em its Top Stores panel
     * becomes a horizontal scroller: the item turns into a `flex-direction:
     * column` card with `align-items: flex-start`, and the anchor shrinks to
     * the width of the store name. Stretching it back is what restores the
     * target the row-shaped version already had. */
    .ds-rail__item a {
        min-height: 2.75rem;
        align-self: stretch;
    }

    /* `display: inline-block` with a 1px block padding. Padding rather than a
     * min-height because these are text links in a stacked list and a flex box
     * would take the column's full width whether the label filled it or not. */
    .ds-footer__link {
        padding-block: 0.75rem;
    }

    /* THE SEARCH PILL, WHICH ONLY EXISTS FROM 48em — so on a 768px tablet it
     * is the search control and it was a 36px field with a 30px icon button
     * absolutely positioned inside it.
     *
     * The FIELD goes to 44, because that is the thing a reader taps. The
     * BUTTON goes to 40 and stops there: it is inset 3px from the right edge
     * of the pill, so 44 would be taller than the pill it sits in and the only
     * way to reach 44 is to delete the pill's visible right edge. 40 x 40 from
     * 30 x 30 is the improvement that does not redraw the component, and
     * Enter submits the form from the field either way. */
    .ds-header__search-input {
        min-height: 2.75rem;
    }

    .ds-header__search-btn {
        width: 2.5rem;
        height: 2.5rem;
    }
}

/* "Sign in" is `display: none` below 40em, so it is raised in the window where
 * it is actually on screen. Overriding `display` inside the block above would
 * have put it back on a 360px bar it was removed from. */
@media (min-width: 40em) and (max-width: 63.99em),
       (min-width: 40em) and (pointer: coarse) {
    .ds-header__link {
        display: inline-flex;
        align-items: center;
        min-height: 2.75rem;
    }
}

/* --- THE THREE TARGETS THAT GROW WITHOUT MOVING ANYTHING ---------------
 * Padding enlarges the border box — which is the hit area — and an equal
 * negative margin gives the space back to the layout, so the measured height
 * of `.ds-row__foot`, `.ds-row__meta` and `.ds-breadcrumb` is unchanged and
 * the finger gets a bigger box than the ink.
 *
 * The breadcrumb reaches 44; the two row links reach the 24px floor of WCAG
 * 2.5.8 and stop there, for the reason in this section's header. Both already
 * carry `position: relative; z-index: 2`, so the enlarged boxes stay above
 * `.ds-row__title a::after` — the stretched link — rather than under it.
 * -------------------------------------------------------------------- */

@media (max-width: 63.99em), (pointer: coarse) {
    .ds-breadcrumb a {
        display: inline-block;
        padding-block: 0.9375rem;
        margin-block: -0.9375rem;
        padding-inline: 0.375rem;
        margin-inline: -0.375rem;
    }

    .ds-row__store,
    .ds-row__comments {
        padding-block: 0.3125rem;
        margin-block: -0.3125rem;
        padding-inline: 0.25rem;
        margin-inline: -0.25rem;
    }

    /* "Link to this comment" — `discussion/_comments.php` line 153, the one
     * link on the site written in the two shared type utilities rather than a
     * component class. Measured 117.8 x 18.6: wide enough already, 25px short
     * on height, and it sits alone in a `.ds-cluster` under a comment where
     * 13px of bleed collides with nothing. Qualified with the tag so it beats
     * the two single-class utility rules it is built from. */
    a.ds-muted.ds-text-xs {
        display: inline-block;
        padding-block: 0.8125rem;
        margin-block: -0.8125rem;
    }

    /* The Reply button sits in that same cluster at that same size, so it
     * takes the same bleed. It is a `<button>` and needs no tag qualifier:
     * nothing else in this file styles it. */
    .ds-comments__reply {
        display: inline-block;
        padding-block: 0.8125rem;
        margin-block: -0.8125rem;
    }

    /* THE AUTH CARD'S FOOT, WHICH IS THE ONE PLACE A BLED BOX CAN COLLIDE WITH
     * THE NEXT BLED BOX. Measured at 375px: "Forgotten your password?" is
     * 160.4 x 16 and "Create one" is 41.6 x 16, and §7.3 calls login and
     * register the FIRST screens a new member sees. A 16px-tall link is what a
     * reader reaches for when the password did not work.
     *
     * 14px of bleed each way turns 16 into 44. The two links on `/login` and
     * `/register` are in SEPARATE `<p>`s, though, and at the foot's own
     * paragraph spacing their 44px boxes would overlap by roughly 20px — a tap
     * near the bottom of "Forgotten your password?" would land on "Create
     * one", which is worse than a small target because it is a WRONG one. So
     * the gap is set to exactly twice the bleed and the boxes meet instead.
     *
     * `> :last-child` above is (0,2,0) and still zeroes the final paragraph's
     * margin, so the card does not grow a trailing 28px. The other four auth
     * screens carry a single "Back to sign in" and are unaffected by the gap. */
    .ds-auth-card__foot p {
        margin-bottom: 1.75rem;
    }

    .ds-auth-card__foot a {
        display: inline-block;
        padding-block: 0.875rem;
        margin-block: -0.875rem;
    }
}

/* =========================================================================
 * 18. Reduced motion and print
 * =========================================================================
 * §1 already flattens every duration to 0.01ms. This block removes the
 * TRANSFORMS as well — a button that teleports one pixel is not less motion,
 * it is faster motion — and stops the two looping animations outright.
 * ========================================================================= */

@media (prefers-reduced-motion: reduce) {
    .ds-skeleton,
    .ds-reveal.is-in {
        animation: none;
    }

    .ds-btn:hover,
    .ds-btn:active,
    .ds-tile:hover,
    .ds-tile:focus-within,
    .ds-card:hover,
    .ds-card:focus-within,
    .ds-card:active,
    .ds-row:hover .ds-row__img,
    .ds-row__cta:active,
    .ds-header__action:active,
    .ds-header__cta:active,
    .ds-nav-toggle:active,
    .ds-header__search-link:active,
    .ds-header__brand:hover .ds-brand__mark {
        transform: none;
    }

    .ds-header__search-btn:active {
        transform: translateY(-50%);
    }
}

@media print {
    .ds-header,
    .ds-nav,
    .ds-footer,
    .ds-rail,
    .ds-drawer,
    .ds-sidebar,
    .ds-skip-link,
    .ds-nav-toggle,
    .ds-tabs,
    .ds-pager {
        display: none !important;
    }

    .ds-main {
        display: block;
        max-width: none;
        padding: 0;
    }

    .ds-shell,
    .ds-page {
        min-height: 0;
    }

    .ds-row,
    .ds-card {
        box-shadow: none;
        break-inside: avoid;
    }
}
