/* ===== Design tokens =====

   Navy carries the page; green only ever means money.

   Two palettes have been wrong here. The first ran five saturated hues at once — indigo,
   orange, rose, amber, sky — so nothing read as important. The second fixed that by cutting
   to one green, then handed that green the brand, the links, the CTA fill, the sale badge,
   the savings line and the section markers all at once. One hue is the right idea; making it
   the page's mass is not. A grid of cards came out as three solid green blocks per row.

   The distribution here follows the SHRA landlord portal, which answers the same question:
   a dark, desaturated navy does the structural work — header, hero, footer, primary action,
   active states — and colour appears in small amounts, as thin rules and tinted chips. Green
   is reserved for the two places money is actually discussed: the savings figure and the
   price verdict. What is left is mostly ink and paper, with the accent where the meaning is.

   Every pair below is measured against WCAG 2.1 AA. The first palette shipped a price at
   2.80:1, which is why this comment keeps saying so. */
:root, .admin-shell {
    color-scheme: light;

    /* Neutrals: a cool blue-grey ramp, so navy sits in the same family rather than on top
       of a warm paper that fights it. */
    --c-bg: #f6f7fa;
    --c-surface: #ffffff;
    --c-surface-alt: #eef0f6;
    --c-border: #dde1ea;
    --c-border-strong: #c6ccda;
    --c-text: #2c3550;
    --c-text-strong: #131a2e;   /* prices and headings — the figures that must land first */
    --c-text-muted: #5a6480;
    --c-text-subtle: #6b7390;

    /* Structure. Header, hero, footer, primary action, active states. */
    --c-navy: #233066;
    --c-navy-600: #33437f;
    --c-navy-800: #1b2450;

    /* The solid action fill, and the only text colour legible on it. */
    --c-cta: #233066;
    --c-cta-hover: #1b2450;
    --c-on-cta: #ffffff;

    /*  Money. Links, the savings line, price verdicts — text and tint, never a large fill.
        Links hover to navy rather than to a lighter green, which is how the portal does it
        and stops green spreading on interaction. */
    --c-brand: #00542d;
    --c-brand-hover: #233066;
    --c-brand-soft: #e7f3ec;
    --c-brand-dark: #0a5b33;
    --c-brand-ring: rgba(35, 48, 102, .22);

    /*  The few places that still paint a filled accent surface take the structural colour
        now, not the green. */
    --c-brand-fill: #233066;
    --c-on-brand: #ffffff;

    /* Time pressure only. */
    --c-brass: #8a4b06;
    --c-brass-soft: #fef3e5;

    /* Errors, out of stock, destructive. Never "sale". */
    --c-accent: #92300f;
    --c-accent-soft: #fdece6;

    /* The deep ground: hero, footer, and the page itself in dark mode. */
    --c-ink: #233066;
    --c-on-ink: #ffffff;
    --c-on-ink-muted: #c3cadf;
    --c-on-ink-subtle: #9aa4c4;
    --c-on-ink-accent: #8bc440;   /* the one bright note, used once per surface */

    /*  The brand mark, inlined in MainLayout so it can inherit these. Navy tile, lime arrow;
        in dark mode they swap, because a navy tile on a navy-black page is invisible. */
    --mark-ground: #233066;
    --mark-line: #8bc440;
    --mark-floor: rgba(255, 255, 255, .45);

    /*  ===== Accents =====
        Five hues that appear only as a 3px card edge and a small filled icon tile, cycled
        so a row of cards is varied rather than uniform. This is where the colour on the
        page comes from now: the portal's dashboard gets its life from exactly this and
        almost nothing else, and it works because the quantity is tiny and the card body
        underneath stays neutral.

        Accessibility note, because two of these are light: the edge is decorative. It is
        never the only signal — every card it sits on carries its own heading — so WCAG's
        3:1 for non-text contrast does not apply to it (that rule covers components you
        must see to identify or operate). What does carry meaning is the glyph on the tile,
        and every ink/tint pair below clears 4.5:1 in both themes. If an accent ever becomes
        the sole carrier of information, it needs re-checking against that 3:1 bar. */
    --a-green-line: #00713c;  --a-green-tint: #e7f3ec;  --a-green-ink: #0a5b33;
    --a-navy-line:  #233066;  --a-navy-tint:  #e8ebf5;  --a-navy-ink:  #233066;
    --a-blue-line:  #22a7e0;  --a-blue-tint:  #e9f6fd;  --a-blue-ink:  #0d5c7d;
    --a-amber-line: #f69220;  --a-amber-tint: #fef3e5;  --a-amber-ink: #8a4b06;
    --a-flame-line: #f05a22;  --a-flame-tint: #fdece6;  --a-flame-ink: #92300f;

    /*  One gradient, on the hero only. Everything else is flat — a gradient on a button is
        part of what made both earlier palettes read as a template. */
    --g-hero: linear-gradient(135deg, #1b2450 0%, #233066 55%, #2c3d80 100%);

    /* Type */
    --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
                  "Helvetica Neue", Arial, "Noto Sans", sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

    /* Geometry — tighter than before, matching the portal's 4 / 8 / 12. */
    --radius-sm: 4px;
    --radius: 8px;
    --radius-lg: 12px;
    --radius-pill: 999px;
    --shadow-sm: 0 1px 2px rgba(19, 26, 46, .06);
    --shadow: 0 6px 20px -6px rgba(19, 26, 46, .18);
    --shadow-lg: 0 12px 34px -10px rgba(19, 26, 46, .28);

    /* Translucent header fill — a token, since it inverts with the theme. */
    --c-header-bg: rgba(255, 255, 255, .90);

    --container: 1200px;
}

/* ===== Dark theme =====

   Two selectors, one palette. The media query covers the reader who has never touched the
   toggle and whose OS is dark; the attribute covers the reader who chose.
   `:not([data-theme="light"])` is what lets an explicit light choice win on a dark OS.

   The values are duplicated because CSS cannot apply one block from two places. If you
   change one, change both; they are kept adjacent so the diff shows it.

   The portal is light-only, so this half is ours. Navy has to lighten to #536abe for the
   action fill: the brand navy on a navy-black page measures 2.71:1 against the background,
   so the button dissolves into it. #536abe is the window where white text still clears
   4.5:1 and the fill still clears 3:1 against the page — the two pull against each other,
   so there is not much room either side. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        color-scheme: dark;
        --c-bg: #0e1220;
        --c-surface: #161c2e;
        --c-surface-alt: #1e2740;
        --c-border: #2a3450;
        --c-border-strong: #3c4869;
        --c-text: #e6e9f2;
        --c-text-strong: #ffffff;
        --c-text-muted: #a2abc4;
        --c-text-subtle: #7b849e;
        --c-navy: #33437f;
        --c-navy-600: #41569f;
        --c-navy-800: #1b2450;
        --c-cta: #536abe;
        --c-cta-hover: #4a60b0;
        --c-on-cta: #ffffff;
        --c-brand: #4ec98a;
        --c-brand-hover: #7fdcab;
        --c-brand-soft: #0f2e22;
        --c-brand-dark: #4ec98a;
        --c-brand-ring: rgba(83, 106, 190, .35);
        --c-brand-fill: #536abe;
        --c-on-brand: #ffffff;
        --c-brass: #f0a84a;
        --c-brass-soft: #2d2211;
        --c-accent: #f3897a;
        --c-accent-soft: #2f1a17;
        --c-ink: #1b2450;
        --c-on-ink-muted: #c3cadf;
        --c-on-ink-subtle: #9aa4c4;
        --mark-ground: #8bc440;
        --mark-line: #141b3d;
        --mark-floor: rgba(20, 27, 61, .45);
        --a-green-line: #4ec98a;  --a-green-tint: #0f2e22;  --a-green-ink: #6fd7a1;
        --a-navy-line:  #6d86d8;  --a-navy-tint:  #1a2340;  --a-navy-ink:  #97a9e6;
        --a-blue-line:  #4fc2f0;  --a-blue-tint:  #0e2836;  --a-blue-ink:  #7fd4f5;
        --a-amber-line: #f0a84a;  --a-amber-tint: #2d2211;  --a-amber-ink: #f5bf72;
        --a-flame-line: #f3897a;  --a-flame-tint: #2f1a17;  --a-flame-ink: #f7a99d;
        --g-hero: linear-gradient(135deg, #141b3d 0%, #1b2450 55%, #263566 100%);
        --shadow-sm: 0 1px 2px rgba(0, 0, 0, .40);
        --shadow: 0 6px 20px -6px rgba(0, 0, 0, .50);
        --shadow-lg: 0 12px 34px -10px rgba(0, 0, 0, .60);
        --c-header-bg: rgba(14, 18, 32, .88);
    }
}

:root[data-theme="dark"] {
    color-scheme: dark;
    --c-bg: #0e1220;
    --c-surface: #161c2e;
    --c-surface-alt: #1e2740;
    --c-border: #2a3450;
    --c-border-strong: #3c4869;
    --c-text: #e6e9f2;
    --c-text-strong: #ffffff;
    --c-text-muted: #a2abc4;
    --c-text-subtle: #7b849e;
    --c-navy: #33437f;
    --c-navy-600: #41569f;
    --c-navy-800: #1b2450;
    --c-cta: #536abe;
    --c-cta-hover: #4a60b0;
    --c-on-cta: #ffffff;
    --c-brand: #4ec98a;
    --c-brand-hover: #7fdcab;
    --c-brand-soft: #0f2e22;
    --c-brand-dark: #4ec98a;
    --c-brand-ring: rgba(83, 106, 190, .35);
    --c-brand-fill: #536abe;
    --c-on-brand: #ffffff;
    --c-brass: #f0a84a;
    --c-brass-soft: #2d2211;
    --c-accent: #f3897a;
    --c-accent-soft: #2f1a17;
    --c-ink: #1b2450;
    --c-on-ink-muted: #c3cadf;
    --c-on-ink-subtle: #9aa4c4;
    --mark-ground: #8bc440;
    --mark-line: #141b3d;
    --mark-floor: rgba(20, 27, 61, .45);
    --a-green-line: #4ec98a;  --a-green-tint: #0f2e22;  --a-green-ink: #6fd7a1;
    --a-navy-line:  #6d86d8;  --a-navy-tint:  #1a2340;  --a-navy-ink:  #97a9e6;
    --a-blue-line:  #4fc2f0;  --a-blue-tint:  #0e2836;  --a-blue-ink:  #7fd4f5;
    --a-amber-line: #f0a84a;  --a-amber-tint: #2d2211;  --a-amber-ink: #f5bf72;
    --a-flame-line: #f3897a;  --a-flame-tint: #2f1a17;  --a-flame-ink: #f7a99d;
    --g-hero: linear-gradient(135deg, #141b3d 0%, #1b2450 55%, #263566 100%);
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .40);
    --shadow: 0 6px 20px -6px rgba(0, 0, 0, .50);
    --shadow-lg: 0 12px 34px -10px rgba(0, 0, 0, .60);
    --c-header-bg: rgba(14, 18, 32, .88);
}


/* ===== Reset / base ===== */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
    font-family: var(--font-sans);
    color: var(--c-text);
    background: var(--c-bg);
    line-height: 1.55;
    font-size: 16px;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}
img { max-width: 100%; height: auto; display: block; }
a { color: var(--c-brand); text-decoration: none; }
a:hover { color: var(--c-brand-hover); text-decoration: underline; }

h1, h2, h3, h4 {
    margin: 0 0 .5rem;
    line-height: 1.2;
    color: var(--c-text);
    font-weight: 700;
    letter-spacing: -0.01em;
}
h1 { font-size: clamp(1.75rem, 1.2rem + 1.6vw, 2.5rem); }
h2 { font-size: 1.5rem; margin-top: 2rem; }
h3 { font-size: 1.125rem; }
p  { margin: 0 0 1rem; }
small, .small { font-size: 0.85rem; color: var(--c-text-muted); }

h1:focus { outline: none; }

/* ===== Layout helpers ===== */
.container { max-width: var(--container); margin: 0 auto; padding: 0 1.25rem; }
.stack-sm > * + * { margin-top: 0.5rem; }
.stack > * + * { margin-top: 1rem; }
.stack-lg > * + * { margin-top: 1.75rem; }
.muted { color: var(--c-text-muted); }
.flex { display: flex; }
.flex-between { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.gap-sm { gap: .5rem; } .gap { gap: 1rem; } .gap-lg { gap: 1.5rem; }
.text-center { text-align: center; }

/* ===== Buttons ===== */
.btn {
    display: inline-flex; align-items: center; justify-content: center;
    gap: .5rem;
    font-family: inherit; font-weight: 600; font-size: 0.95rem;
    padding: .625rem 1.125rem;
    border-radius: var(--radius);
    border: 1px solid transparent;
    cursor: pointer;
    transition: background-color .15s, color .15s, border-color .15s, box-shadow .15s, transform .05s;
    text-decoration: none;
    line-height: 1.2;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--c-brand); outline-offset: 2px; }

.btn-primary {
    /*  Flat and structural. This was a green gradient with a coloured glow under it, which
        put a large saturated block on every card in the grid — the single biggest reason
        the page read as "too green". The action is the navy now; green stayed behind on
        the savings line where it means something. */
    background: var(--c-cta); color: var(--c-on-cta);
}
.btn-primary:hover {
    background: var(--c-cta-hover);
    color: var(--c-on-cta); text-decoration: none;
}

.btn-ghost { background: transparent; color: var(--c-text); border-color: var(--c-border); }
.btn-ghost:hover { background: var(--c-surface-alt); color: var(--c-text); text-decoration: none; }

.btn-link { background: transparent; color: var(--c-brand); padding: 0; border: 0; }
.btn-link:hover { color: var(--c-brand-hover); text-decoration: underline; }

.btn-danger-link { background: transparent; color: var(--c-accent); border: 0; padding: 0; cursor: pointer; font: inherit; }
.btn-danger-link:hover { text-decoration: underline; }

.btn-sm { padding: .375rem .75rem; font-size: .85rem; }
.btn-lg { padding: .875rem 1.5rem; font-size: 1rem; }

/* ===== Cards ===== */
.card {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
}
.card-body { padding: 1.25rem; }
.card-hover { transition: transform .2s cubic-bezier(.2,.8,.3,1.2), box-shadow .2s, border-color .2s; }
.card-hover:hover {
    transform: translateY(-5px);
    box-shadow: var(--shadow-lg);
    border-color: var(--c-navy);
}

/* ===== Badges / pills ===== */
.badge {
    display: inline-flex; align-items: center;
    font-size: .75rem; font-weight: 700; letter-spacing: .02em;
    padding: .2rem .55rem;
    border-radius: var(--radius-pill);
    background: var(--c-surface-alt);
    color: var(--c-text-muted);
    text-transform: uppercase;
}
/* A saving is good news, so the sale badge is the accent rather than the old hot-rose
   gradient — which competed with the orange price directly beneath it. */
.badge-sale {
    /*  Tint and ink rather than a solid fill. A discount badge sits on top of a product
        photo on every card; as a saturated pill it was competing with the image and with
        the button below it. As a tint it still reads as good news and stops shouting. */
    background: var(--c-brand-soft); color: var(--c-brand-dark);
    box-shadow: none;
}
.badge-low  { background: var(--c-brand-soft); color: var(--c-brand-dark); }
.badge-stock { background: var(--c-brand-soft); color: var(--c-brand-dark); }
.badge-out  { background: var(--c-accent-soft); color: var(--c-accent); }
.badge-ends { background: var(--c-brass-soft); color: var(--c-brass); }

/* ===== Forms ===== */
.input, input[type=text], input[type=email], input[type=password], input[type=number], input[type=search],
select, textarea {
    font-family: inherit; font-size: 1rem;
    width: 100%;
    padding: .625rem .85rem;
    background: var(--c-surface);
    color: var(--c-text);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--radius);
    transition: border-color .15s, box-shadow .15s;
}
.input:focus, input:focus, select:focus, textarea:focus {
    outline: none;
    border-color: var(--c-brand);
    box-shadow: 0 0 0 3px var(--c-brand-ring);
}
label.field { display: block; margin-bottom: 1rem; }
label.field > span { display: block; font-weight: 600; font-size: .9rem; margin-bottom: .35rem; color: var(--c-text); }

.valid.modified:not([type=checkbox]) { outline: none; border-color: var(--c-brand); }
.invalid { border-color: var(--c-accent); }
.validation-message { color: var(--c-accent); font-size: .85rem; margin-top: .25rem; }

/* ===== Tables ===== */
.table {
    width: 100%; border-collapse: collapse;
    background: var(--c-surface);
    border-radius: var(--radius-lg);
    overflow: hidden;
    box-shadow: var(--shadow-sm);
    border: 1px solid var(--c-border);
}
.table th, .table td { padding: .85rem 1rem; text-align: left; border-bottom: 1px solid var(--c-border); }
.table th { background: var(--c-surface-alt); font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; color: var(--c-text-muted); font-weight: 600; }
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr.is-best { background: var(--c-brand-soft); }
.table tbody tr.is-best td { font-weight: 600; }

/* ===== Alerts ===== */
.alert { padding: .85rem 1rem; border-radius: var(--radius); border: 1px solid; }
.alert-success { background: var(--c-brand-soft); border-color: var(--c-brand); color: var(--c-brand-dark); }
.alert-error { background: var(--c-accent-soft); border-color: var(--c-accent); color: var(--c-accent); }

/* ===== Hero ===== */
/*  The hero used to be a purple gradient that animated its background position forever,
    with four fake sparkle dots and a floating lavender blob. Three separate tells of a
    template, two of them running animation frames on every visit for no information.

    What it is now: a ledger. Ruled lines on deep ink, one soft green glow where the
    headline sits, and nothing moving. The ruling is drawn with repeating-linear-gradient
    rather than an image, so it costs no request and scales with the box. */
.hero {
    position: relative;
    background: var(--g-hero);
    color: var(--c-on-ink);
    border-radius: 20px;
    padding: clamp(2.5rem, 5vw, 4.5rem) clamp(1.5rem, 4vw, 3.5rem);
    margin: 1.5rem 0 2.25rem;
    overflow: hidden;
    box-shadow: 0 24px 48px -16px rgba(6, 22, 18, .55);
}
.hero::before {
    content: ""; position: absolute; inset: 0;
    background-image:
        /* the glow, behind the headline */
        radial-gradient(720px circle at 18% 8%, rgba(139, 196, 64, .20) 0, transparent 55%),
        /* ruled lines, horizontal then vertical */
        repeating-linear-gradient(to bottom, rgba(255,255,255,.05) 0 1px, transparent 1px 34px),
        repeating-linear-gradient(to right, rgba(255,255,255,.035) 0 1px, transparent 1px 34px);
    pointer-events: none;
}
/*  A single hairline rule down the left, the way a ledger column is ruled off. Sits inside
    the padding so it reads as part of the page, not as a border on the box. */
.hero::after {
    content: "";
    position: absolute; left: clamp(.75rem, 2vw, 1.75rem); top: 0; bottom: 0;
    width: 2px;
    background: linear-gradient(to bottom, transparent, rgba(139, 196, 64, .60) 20%,
                                rgba(139, 196, 64, .60) 80%, transparent);
    pointer-events: none;
}
.hero h1 {
    color: var(--c-on-ink); max-width: 22ch;
    font-size: clamp(2.4rem, 1.2rem + 4vw, 4rem);
    font-weight: 800; letter-spacing: -0.03em; line-height: 1.05;
    margin-bottom: .75rem;
}
/*  Was a three-stop gradient clipped to the text. Flat lime instead: the gradient added
    nothing a solid colour does not, and clipped-gradient headlines are one of the more
    recognisable template tells. */
.hero h1 .grad { color: var(--c-on-ink-accent); }
.hero p { color: var(--c-on-ink-muted); max-width: 56ch; font-size: 1.15rem; }
/*  The running totals line. Uncapped and a size down, because at 56ch it wrapped onto a
    second line and spent a whole row of hero on eight words — real estate that belongs to
    the deals below it. Still wraps on a phone, where two lines is the only option. */
.hero p.hero-stat { max-width: none; font-size: 1.02rem; }
.hero .hero-form { display: flex; gap: .6rem; max-width: 520px; margin-top: 1.75rem; position: relative; }
.hero .hero-form input {
    background: #fff; color: #2c3550; border-color: transparent;
    padding: .8rem 1.1rem; border-radius: var(--radius); font-size: 1rem;
}
/*  On the navy hero the primary button would be navy on navy. The portal answers this with
    a light button on its dark band, which is also the highest-contrast option available —
    so here the fill and the text swap. */
.hero .hero-form .btn-primary {
    background: #fff; color: var(--c-navy-800);
    border-radius: var(--radius);
    padding: .8rem 1.4rem; font-weight: 700; white-space: nowrap;
    box-shadow: none;
}
.hero .hero-form .btn-primary:hover { background: var(--c-surface-alt); color: var(--c-navy-800); }
.hero .hero-msg { margin-top: .85rem; color: #b6dd7a; font-size: .95rem; font-weight: 600; }
.hero .hero-msg.err { color: #ffc9bd; }
.hero-stats {
    display: flex; gap: 2.5rem; margin-top: 2.25rem; position: relative; flex-wrap: wrap;
}
.hero-stats .stat .num {
    display: block; font-size: 1.9rem; font-weight: 800; letter-spacing: -.02em;
    color: var(--c-on-ink-accent);
}
.hero-stats .stat .lbl { color: var(--c-on-ink-subtle); font-size: .85rem; text-transform: uppercase; letter-spacing: .08em; }

/* ===== Product grid ===== */
.product-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 1.25rem;
}

.product-card { display: flex; flex-direction: column; }
.product-card .thumb {
    aspect-ratio: 1;
    background: radial-gradient(120% 100% at 50% 0%, var(--c-surface) 0%, var(--c-surface-alt) 100%);
    display: flex;
    align-items: center; justify-content: center; padding: 1rem;
    position: relative; overflow: hidden;
}
/* max-width/height:auto keep the image fluid despite its width/height attributes,
   which are there to reserve layout space and avoid CLS. */
.product-card .thumb img { max-width: 100%; height: auto; max-height: 100%; object-fit: contain; transition: transform .3s ease; }
.product-card:hover .thumb img { transform: scale(1.06); }
.product-card .thumb .badge { position: absolute; top: .75rem; left: .75rem; }
.product-card .body { padding: 1rem 1.25rem 1.25rem; flex: 1; display: flex; flex-direction: column; }
.product-card .brand { font-size: .75rem; text-transform: uppercase; letter-spacing: .05em; color: var(--c-text-muted); }
/*  The brand is a link now (to /brand/{slug}) but must not look like one inside a card —
    the card already has a primary link on the title and a CTA at the bottom, and a third
    blue thing competes with both. It underlines on hover, which is enough. */
a.brand { text-decoration: none; display: inline-block; }
a.brand:hover { text-decoration: underline; color: var(--c-brand); }
/* Amazon titles are SEO strings, not names — "Amazon Fire TV Stick 4K Plus (newest model)
   with AI-powered Fire TV Search, Wi-Fi 6, stream hundreds of thousands of movies and
   shows, free & live TV, find shows faster with Alexa+". Left unclamped, one card runs to
   six lines while its neighbour runs to two and the grid loses its baseline. Clamped for
   display only: the product page still shows the full title, which is what a shopper
   checking they have the right item actually needs. */
.product-card .title { font-size: 1rem; font-weight: 600; margin: .25rem 0 .75rem; line-height: 1.35;
    display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; -webkit-box-orient: vertical;
    overflow: hidden; }
.product-card .title a { color: inherit; }
/* Amazon gives every colour and size its own ASIN, so one product legitimately occupies
   several catalogue rows. The listing pages show one card and say how many it stands for —
   without this the grid reads as duplicated rather than as one product in five colours. */
.product-card .variants { display: inline-block; margin: -.5rem 0 .6rem; font-size: .72rem;
    font-weight: 600; color: var(--c-text-muted); background: var(--c-surface-alt);
    border: 1px solid var(--c-border); border-radius: var(--radius-pill); padding: .1rem .5rem; }
.product-card .title a:hover { color: var(--c-brand); text-decoration: none; }
.product-card .price-row { display: flex; align-items: baseline; gap: .5rem; margin-top: auto; padding-top: .5rem; }
/*  The price carries its weight through type, not hue. It was orange (#f97316), which is
    2.80:1 on white — the least legible thing on the card was the number the card exists
    to show, and it failed AA even at large-text size. Ink puts it at 16.5:1 and leaves
    green free for the saving, which is the part a shopper is actually pleased about. */
.product-card .price { font-size: 1.45rem; font-weight: 800; color: var(--c-text-strong); letter-spacing: -.025em; }
.product-card .was-price { font-size: .95rem; color: var(--c-text-subtle); text-decoration: line-through; }
.product-card .retailer { font-size: .85rem; color: var(--c-text-muted); }
.product-card .savings { margin: .3rem 0 0; font-size: .82rem; font-weight: 700; color: var(--c-brand); }
.product-card .cta { margin-top: 1rem; width: 100%; }

/* ===== Footer ===== */
/*  The footer keeps the ink ground in both themes — it is the same surface as the hero,
    which is what ties the top and bottom of the page together. In dark mode it sits a
    shade above the page rather than below it. */
.site-footer {
    margin-top: 4rem;
    padding: 2.5rem 0 2rem;
    background: var(--c-ink);
    color: var(--c-on-ink-muted);
}
.site-footer a { color: var(--c-on-ink-muted); }
.site-footer a:hover { color: var(--c-on-ink); }
.site-footer h4 { color: var(--c-on-ink); font-size: .9rem; text-transform: uppercase; letter-spacing: .05em; margin-bottom: .75rem; }
.footer-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 2rem; margin-bottom: 2rem; }
.footer-brand { font-size: 1.25rem; font-weight: 700; color: var(--c-on-ink); margin-bottom: .5rem; }
.footer-tagline { color: var(--c-on-ink-subtle); font-size: .9rem; max-width: 32ch; }
.footer-bottom { border-top: 1px solid rgba(255,255,255,.1); padding-top: 1.25rem; font-size: .85rem; color: var(--c-on-ink-subtle); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 1rem; }
@media (max-width: 720px) {
    .footer-grid { grid-template-columns: 1fr 1fr; }
    .footer-grid > :first-child { grid-column: 1 / -1; }
}

/* ===== Section header ===== */
.section-head { display: flex; align-items: end; justify-content: space-between; gap: 1rem; margin: 2.5rem 0 1.15rem; }
.section-head h2 {
    margin: 0; font-size: 1.65rem; letter-spacing: -.02em;
    position: relative; padding-left: .9rem;
}
.section-head h2::before {
    content: ""; position: absolute; left: 0; top: .18em; bottom: .18em;
    width: 4px; border-radius: 2px; background: var(--c-navy);
}
.section-head .link { font-weight: 700; }

/*  A section marker can take an accent too, so consecutive headings differ. Falls back to
    navy when no class is set. */
.section-head h2::before { background: var(--a-line, var(--c-navy)); }

/*  Visually hidden but still read aloud. Used for labels on controls whose purpose is obvious
    to a sighted reader from position alone — the listing sort, for one — where a visible
    label would be noise and no label at all would leave a screen reader announcing
    "combo box" with nothing else. */
.sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ===== Accent cards =====

   The device the portal's dashboard is built from: a neutral card with a 3px coloured top
   edge and a small filled tile holding one glyph. All the colour on the page comes from
   these two elements; the body underneath stays ink on white. Cycling the accent across a
   row is what makes a group read as lively rather than uniform — with the quantity small
   enough that it never becomes the page's mass, which is the mistake the all-green pass
   made.

   A variant class sets the trio (line / tint / ink) and every part below reads from it, so
   adding a sixth accent is one rule and no new component code. */
.accent-card {
    display: flex; flex-direction: column; gap: .3rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-top: 3px solid var(--a-line, var(--c-navy));
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    padding: .7rem .85rem .75rem;
}
/*  The tile sits beside the figure rather than above it: stacking head / figure / note /
    foot made four full-height rows out of what is really one number and its caption, and
    the card ended up taller than the product cards below it. */
.accent-card .head { display: flex; align-items: center; gap: .5rem; }
.accent-card .tile {
    width: 28px; height: 28px; border-radius: var(--radius-sm); flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--a-tint, var(--c-surface-alt));
    color: var(--a-ink, var(--c-navy));
}
.accent-card .tile svg { width: 16px; height: 16px; display: block; }
.accent-card .label {
    font-size: .68rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: var(--c-text-muted); line-height: 1.25;
}
.accent-card .figure {
    font-size: 1.5rem; font-weight: 800; letter-spacing: -.03em; line-height: 1.1;
    color: var(--c-text-strong);
}
.accent-card .note {
    margin: 0; font-size: .78rem; line-height: 1.35; color: var(--c-text-muted);
}
.accent-card .foot {
    margin-top: auto; padding-top: .5rem; border-top: 1px solid var(--c-border);
    display: flex; align-items: center; justify-content: space-between; gap: .5rem;
    font-size: .82rem; font-weight: 600;
}
.accent-card .foot svg { width: 15px; height: 15px; flex-shrink: 0; }
a.accent-card { text-decoration: none; color: inherit; transition: border-color .15s, box-shadow .15s, transform .15s; }
a.accent-card:hover { text-decoration: none; box-shadow: var(--shadow); transform: translateY(-2px); }


/*  A feature card's tile holds artwork rather than a glyph.

    Two modes, because the two image sources want opposite treatment. A product shot is a
    photograph and should fill the tile (cover); an admin-supplied logo is a mark on its own
    ground and must be letterboxed whole (contain) — cropping a logo to a square is how you
    turn someone's trademark into a smear. The white ground under a logo is deliberate: most
    marks are supplied as transparent PNGs drawn in a dark ink that vanishes on the tinted
    accent ground in dark mode. */
.accent-card .tile.art { padding: 0; overflow: hidden; }
.accent-card .tile.art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.accent-card .tile.art.logo { background: #fff; padding: 3px; }
.accent-card .tile.art.logo img { object-fit: contain; }

/*  Slightly larger than a glyph tile: artwork needs the room to be recognisable, and the
    feature row carries fewer, bigger cards than the dashboard's. */
.accent-card.feature .tile { width: 34px; height: 34px; }
.accent-card.feature .label {
    font-size: .82rem; font-weight: 700; letter-spacing: 0; text-transform: none;
    color: var(--c-text-strong);
}
.a-green { --a-line: var(--a-green-line); --a-tint: var(--a-green-tint); --a-ink: var(--a-green-ink); }
.a-navy  { --a-line: var(--a-navy-line);  --a-tint: var(--a-navy-tint);  --a-ink: var(--a-navy-ink);  }
.a-blue  { --a-line: var(--a-blue-line);  --a-tint: var(--a-blue-tint);  --a-ink: var(--a-blue-ink);  }
.a-amber { --a-line: var(--a-amber-line); --a-tint: var(--a-amber-tint); --a-ink: var(--a-amber-ink); }
.a-flame { --a-line: var(--a-flame-line); --a-tint: var(--a-flame-tint); --a-ink: var(--a-flame-ink); }

/*  auto-FILL, not auto-fit. With auto-fit the empty tracks collapse and a single card
    stretches the full width of the page, which is exactly the state a young catalogue is in
    — one live collection and nothing else qualifying. auto-fill keeps the tracks, so one card
    is card-sized and the row still fills edge to edge once there are four. */
.stat-row {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
    gap: 1rem; margin: 0 0 .5rem;
}
.accent-card .figure .unit {
    font-size: .78rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
    color: var(--c-text-muted); margin-left: .2rem;
}

/* ===== Notice banner =====
   The portal's top-of-page notice: a colour bar down the left, a tinted ground, a tile and
   an action. Used here for anything the reader should act on but that is not an error. */
.notice {
    display: flex; align-items: flex-start; gap: .85rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-left: 4px solid var(--a-line, var(--c-navy));
    border-radius: var(--radius); padding: .9rem 1rem;
}
.notice .tile {
    width: 32px; height: 32px; border-radius: var(--radius-sm); flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--a-tint); color: var(--a-ink);
}
.notice .tile svg { width: 17px; height: 17px; display: block; }
.notice .body { flex: 1; min-width: 0; }
.notice strong { display: block; color: var(--c-text-strong); }
.notice p { margin: .15rem 0 0; font-size: .9rem; color: var(--c-text-muted); }

/* ===== Infinite deal feed =====
   The sentinel is both the trigger for the next batch and the no-JS way forward, so it
   stays in the flow and only swaps what it shows. */
.feed-more { position: relative; display: flex; align-items: center; justify-content: center;
              padding: 2rem 0 .5rem; min-height: 3.5rem; }
.feed-more[data-loading] a { visibility: hidden; }
.feed-more[data-loading]::after {
    content: ""; position: absolute;
    width: 22px; height: 22px; border-radius: 50%;
    border: 2px solid var(--c-border-strong); border-top-color: var(--c-brand);
    animation: feed-spin .7s linear infinite;
}
/* Nothing left to fetch: the link would now lead to a page the reader has already seen. */
.feed-more[data-done] { display: none; }
@keyframes feed-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .feed-more[data-loading]::after { animation: none; border-top-color: var(--c-border-strong); }
}

/* ===== Theme toggle =====

   Which glyph shows is decided in CSS from the root attribute, never by script writing
   into the button. That matters here because Blazor's enhanced navigation swaps the
   header's DOM on every page change: anything JS had written into the button would be
   thrown away on the first link click, while <html data-theme> survives untouched. */
.theme-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    width: 36px; height: 36px; padding: 0;
    border-radius: var(--radius-pill);
    border: 1px solid var(--c-border-strong);
    background: var(--c-surface);
    color: var(--c-text-muted);
    cursor: pointer;
    transition: color .15s, border-color .15s, background-color .15s;
}
.theme-toggle:hover { color: var(--c-brand); border-color: var(--c-brand); }
.theme-toggle:focus-visible { outline: 2px solid var(--c-brand); outline-offset: 2px; }
.theme-toggle svg { width: 18px; height: 18px; display: block; }

/* The button offers the theme you would get by pressing it, so on a light page it shows
   a moon. Default (no attribute, light system): offer dark. */
.theme-toggle .i-dark  { display: block; }
.theme-toggle .i-light { display: none; }

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .theme-toggle .i-dark  { display: none; }
    :root:not([data-theme="light"]) .theme-toggle .i-light { display: block; }
}

/* After the media query on purpose: equal specificity, so source order is what lets an
   explicit choice beat the OS in both directions. */
:root[data-theme="dark"]  .theme-toggle .i-dark  { display: none; }
:root[data-theme="dark"]  .theme-toggle .i-light { display: block; }
:root[data-theme="light"] .theme-toggle .i-dark  { display: block; }
:root[data-theme="light"] .theme-toggle .i-light { display: none; }

/* ===== Motion preferences ===== */
@media (prefers-reduced-motion: reduce) {
    /* The hero's two infinite animations are gone entirely, so there is nothing left here
       to switch off — only the hover transitions, which still need it. */
    .card-hover, .product-card .thumb img { transition: none; }
    .card-hover:hover { transform: none; }
}

/* ===== Blazor error UI ===== */
#blazor-error-ui {
    background: var(--c-ink); color: var(--c-on-ink);
    bottom: 0; box-shadow: 0 -1px 2px rgba(0,0,0,0.2);
    display: none; left: 0; padding: 0.6rem 1.25rem 0.7rem 1.25rem; position: fixed;
    width: 100%; z-index: 1000;
}
#blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: 0.75rem; top: 0.5rem; }
.blazor-error-boundary { background: #92300f; padding: 1rem; color: #fff; border-radius: var(--radius); }
.blazor-error-boundary::after { content: "An error has occurred."; }

/* Price standing vs our own recorded history - see PriceVerdict. Absent until we
   hold enough observations for the claim to be true. */
.product-card .verdict { margin: .3rem 0 0; font-size: .78rem; font-weight: 700; }
.product-card .verdict::before { content: "\25CF"; margin-right: .35rem; font-size: .7em; vertical-align: .1em; }
/*  Four hardcoded hues became three tokens. "Good" used to be blue, which belonged to no
    other element on the site; both positive verdicts are the accent now and the words do
    the distinguishing, which is what they are there for. */
.verdict-best { color: var(--c-brand); }
.verdict-good { color: var(--c-brand); }
.verdict-neutral { color: var(--c-text-muted); }
.verdict-poor { color: var(--c-brass); }
