/* ==========================================================================
   AGORIVA — theme
   --------------------------------------------------------------------------
   Every colour, radius, shadow and type scale is a custom property declared
   once in :root. Requirement 32: rebranding means editing this block, not
   hunting hex codes through templates.
   ========================================================================== */

:root {
    /* Brand ---------------------------------------------------------------
       A deep ink blue-green with a warm brass accent. Reads as considered
       and independent rather than as a marketplace template.               */
    --brand-900: #0b1f24;
    --brand-800: #102c33;
    --brand-700: #163b44;
    --brand-600: #1d4d59;
    --brand-500: #26646f;
    --brand-400: #3c8391;
    --brand-300: #6aa9b3;
    --brand-200: #a5cbd1;
    --brand-100: #d6e8ea;
    --brand-050: #eef6f7;

    --accent-600: #a8763a;
    --accent-500: #c28b45;
    --accent-400: #d9a463;
    --accent-100: #f6ead8;

    /* Neutrals */
    --ink-900: #14181a;
    --ink-800: #23292c;
    --ink-700: #3a4246;
    --ink-600: #566065;
    --ink-500: #78838a;
    --ink-400: #9aa4aa;
    --ink-300: #c3cace;
    --ink-200: #e0e5e7;
    --ink-100: #f0f3f4;
    --ink-050: #f8fafa;
    --white: #ffffff;

    /* Status */
    --ok-600: #1f7a4d;
    --ok-100: #e2f3ea;
    /* WCAG 1.4.3: 4.14:1 on the warn-100 tint was short of AA; this is 4.98:1. */
    --warn-600: #8a5f0e;
    --warn-100: #fbf0d8;
    --error-600: #a32b2b;
    --error-100: #fbe6e6;
    --info-600: #1f5f8b;
    --info-100: #e3eff7;

    /* Semantic aliases — components reference these, never raw brand steps */
    --page-bg: var(--ink-050);
    --surface: var(--white);
    --surface-sunken: var(--ink-100);
    --border: var(--ink-200);
    /* WCAG 1.4.11: form control boundaries need 3:1. ink-300 (#c3cace)
       was 1.66:1; this is 3.29:1. Used for input borders. */
    --border-strong: #868f95;
    --text: var(--ink-800);
    --text-muted: var(--ink-600);
    /* WCAG 1.4.3: ink-500 (#78838a) is only 3.88:1 on white. This value
       is 4.86:1 on white and 4.64:1 on the ink-050 surface, so hints,
       breadcrumbs and .link-button controls all clear AA. */
    --text-faint: #697377;
    --text-inverse: var(--white);
    --link: var(--brand-600);
    --link-hover: var(--brand-800);

    /* Typography */
    --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
                 "Helvetica Neue", Arial, sans-serif;
    --font-display: "Inter", Georgia, "Times New Roman", serif;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

    --text-xs: 0.75rem;
    --text-sm: 0.875rem;
    --text-base: 1rem;
    --text-lg: 1.125rem;
    --text-xl: 1.375rem;
    --text-2xl: 1.75rem;
    --text-3xl: 2.25rem;
    --text-4xl: 3rem;

    /* Spacing — 4px scale */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;
    --space-7: 3rem;
    --space-8: 4rem;
    --space-9: 6rem;

    /* Shape */
    --radius-sm: 4px;
    --radius: 8px;
    --radius-lg: 14px;
    --radius-full: 999px;

    --shadow-sm: 0 1px 2px rgba(20, 24, 26, 0.06);
    --shadow: 0 2px 8px rgba(20, 24, 26, 0.08);
    --shadow-lg: 0 12px 32px rgba(20, 24, 26, 0.12);

    --container: 1200px;
    --container-narrow: 760px;

    --transition: 160ms cubic-bezier(0.4, 0, 0.2, 1);
}

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

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

@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
    }
}

body {
    margin: 0;
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: 1.6;
    color: var(--text);
    background: var(--page-bg);
    -webkit-font-smoothing: antialiased;

    /* Long unbroken strings — order numbers, SKUs, tracking references, the
       supplier URLs in the log viewer — have no space to wrap at and will push
       a phone layout sideways on their own. */
    overflow-wrap: break-word;
}

/* Last line of defence against sideways scroll. `clip` rather than `hidden`
   deliberately: overflow-x:hidden on html or body makes it a scroll container,
   which silently kills `position: sticky` on the site header and the basket
   summary. `clip` cuts the overflow without creating one.

   This is a safety net, not the fix — anything actually overflowing is dealt
   with at its source below. */
html { overflow-x: clip; }
body { overflow-x: clip; }

h1, h2, h3, h4 {
    font-family: var(--font-display);
    line-height: 1.2;
    margin: 0 0 var(--space-4);
    color: var(--ink-900);
    letter-spacing: -0.015em;
    font-weight: 650;
}

/* Fluid headings.
   --text-3xl is 36px, which is right on a desktop and far too heavy at 320px,
   where an h1 of any length takes three lines before the page has said
   anything. clamp() scales between the two off the viewport width, so there is
   no breakpoint to maintain and no size in between that looks wrong.

   The lower bounds are still comfortably above body text, so the heading
   hierarchy survives at every width. */
h1 { font-size: clamp(1.625rem, 1.25rem + 1.9vw, var(--text-3xl)); }
h2 { font-size: clamp(1.375rem, 1.19rem + 0.9vw, var(--text-2xl)); }
h3 { font-size: clamp(1.125rem, 1.06rem + 0.3vw, var(--text-xl)); }

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

a {
    color: var(--link);
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
    transition: color var(--transition);
}
a:hover { color: var(--link-hover); }

img { max-width: 100%; height: auto; display: block; }

/* Media never wider than the column it sits in. video and iframe do not get
   this from the browser the way img now does. */
video, iframe, canvas, svg { max-width: 100%; }

/* No form control may be wider than its container.
   The admin order screen sets explicit pixel widths on its courier and
   tracking inputs — width:260px on a 320px phone is wider than the space the
   panel has, and it took the page sideways with it. This caps every one of
   them without touching the desktop sizing they were written for, because
   none of them intends to exceed its container. */
input, select, textarea { max-width: 100%; }

/* A <pre> of log output or a long URL is the other reliable source of
   sideways scroll. */
pre { overflow-x: auto; max-width: 100%; }

/* Visible focus for keyboard users — never removed without a replacement. */
:focus-visible {
    outline: 2px solid var(--brand-500);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

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

.skip-link {
    position: absolute;
    left: -9999px;
    top: var(--space-2);
    background: var(--brand-800);
    color: var(--white);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius);
    z-index: 100;
}
.skip-link:focus { left: var(--space-2); }

/* --------------------------------------------------------------------------
   Layout
   -------------------------------------------------------------------------- */
.container {
    width: 100%;
    max-width: var(--container);
    margin-inline: auto;
    padding-inline: var(--space-5);
}

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

/* 24px of gutter each side costs 48px of a 320px screen. Below that width the
   content matters more than the margin. */
@media (max-width: 480px) {
    .container { padding-inline: var(--space-4); }
}

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

/* --------------------------------------------------------------------------
   Responsive foundations
   -------------------------------------------------------------------------- */

/* A grid or flex item's default minimum size is its CONTENT's size, not zero.
   That is what makes one long word or a wide table burst a column instead of
   being contained by it — the track refuses to shrink below the thing inside
   it. Every layout wrapper in this file that holds arbitrary content needs
   min-width:0 for its children to behave. */
.cart-layout > *,
.checkout-layout > *,
.shop-layout > *,
.product-detail > *,
.account-grid > *,
.site-footer__grid > * {
    min-width: 0;
}

/* Full-height panels use dvh, set where each is defined — see .centred-page
   and .coming-soon below. */

/* --------------------------------------------------------------------------
   Header
   -------------------------------------------------------------------------- */
.site-header {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 50;
}

.site-header__inner {
    display: flex;
    align-items: center;
    /* Not space-between: with only three browse links that strands them in the
       middle of the header. They read as part of the brand's group, and the
       tools are pushed right by their own auto margin instead. */
    justify-content: flex-start;
    gap: var(--space-5);
    min-height: 72px;
}

.brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--brand-800);
    text-decoration: none;
}
.brand:hover { color: var(--brand-600); }

/* The mark is drawn entirely inside the SVG, so this only reserves the space.
   The mark has no tile of its own — it sits directly on the page — so giving
   this box a background would put a panel behind a logo that does not want
   one. */
.brand__mark {
    width: 34px; height: 34px;
    flex: 0 0 34px;
    display: block;
}

.brand__mark .logo-mark { display: block; width: 100%; height: 100%; }

/* The name and its strapline stack beside the mark, not under it — .brand
   itself stays a row so the mark keeps sitting to the left of both lines. */
.brand__text {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    /* The anchor's inherited line-height would space these like body copy and
       break the lockup apart. */
    line-height: 1.1;
    gap: 0.2em;
}

/* Supporting copy, so it is set well below the name in both size and weight.
   At anything close to the name's weight the two lines compete and the eye
   stops treating them as one mark.

   Uppercasing is inherited from .brand; the letter-spacing is what stops small
   uppercase text reading as a solid block. */
.brand__strapline {
    font-family: var(--font-sans);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.14em;
    color: var(--text-muted);
    /* The strapline is wider than the word above it. Without this the header
       can push the basket link onto a second row on a narrow phone. */
    white-space: nowrap;
}

/* The mark's navy stroke is close enough to --brand-900 to disappear on it, so
   every dark surface the mark appears on repaints it a pale tint. The bronze
   stroke reads on both and is left alone.

   This works because the mark is inlined into the document rather than served
   as an <img>, so these rules reach its gradient stops. The stops carry their
   light-surface colours as presentation attributes, which any rule here beats.
   Add new dark surfaces to this selector. See views/partials/logo.php. */
.site-footer .logo-ink-1,
.coming-soon .logo-ink-1 { stop-color: #f4f7f8; }

.site-footer .logo-ink-2,
.coming-soon .logo-ink-2 { stop-color: #c6d4d8; }

.nav { display: flex; gap: var(--space-5); align-items: center; }

.nav a {
    color: var(--text-muted);
    text-decoration: none;
    font-size: var(--text-sm);
    font-weight: 500;
    padding: var(--space-2) 0;
    border-bottom: 2px solid transparent;
}
.nav a:hover, .nav a[aria-current="page"] {
    color: var(--brand-700);
    border-bottom-color: var(--accent-500);
}

/* Browse links sit next to the brand; search, account and basket cluster on
   the right. Left to right, that is the order a shopper moves through:
   browse, find, sign in, pay. */
.header-tools {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    margin-left: auto;
}

.nav--actions { gap: var(--space-4); }

/* The basket is the end of the funnel, so it is the one header link that
   looks like a control rather than a word.

   Written as `.nav a.nav-basket` deliberately. `.nav a` above is a class plus
   an element and therefore outranks a lone `.nav-basket`, which left this with
   `padding: 8px 0` and a 2px transparent bottom border — text jammed against
   the frame, and a box with its bottom edge missing. */
.nav a.nav-basket {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--brand-800);
}
.nav a.nav-basket:hover,
.nav a.nav-basket[aria-current="page"] {
    border-color: var(--brand-500);
    background: var(--brand-050);
    /* The shared rule paints an accent underline on hover. On a bordered box
       that reads as a mismatched thick bottom edge. */
    border-bottom-color: var(--brand-500);
}

@media (max-width: 860px) {
    /* Wrap onto more lines rather than hiding anything. The previous rules took
       the whole nav away below 720px and the search below 640px, which left a
       phone with a logo and no route to the basket at all. */
    .site-header__inner {
        flex-wrap: wrap;
        gap: var(--space-3) var(--space-4);
        padding-bottom: var(--space-2);
    }

    /* The search field and the account links need to land on different rows,
       and they are siblings inside .header-tools rather than of each other.
       display:contents drops that wrapper's box so both become direct items of
       the header and can be ordered independently. */
    .header-tools { display: contents; }

    /* Row 1: brand, pushed apart from account and basket — the two a customer
       needs at any moment, so they never move off the first line. */
    .brand        { order: 1; margin-right: auto; }
    .nav--actions { order: 2; }

    /* Row 2: search, using the full width. */
    .header-search { order: 3; width: 100%; }
    .header-search input,
    .header-search input:focus { width: 100%; }

    /* Row 3: the browse links, ruled off from the controls above them.
       Excluding the actions nav explicitly — it carries the .nav class too, and
       a bare .nav here would drag it onto this row as a full-width block. */
    .nav:not(.nav--actions) {
        order: 4;
        width: 100%;
        gap: var(--space-5);
        padding-top: var(--space-2);
        border-top: 1px solid var(--border);
    }
}

@media (max-width: 400px) {
    /* The strapline is the widest element in the brand lockup — wider than the
       name itself — and row one also has to hold account and basket. Shrink it
       rather than letting it wrap or push them down. */
    .brand__strapline {
        font-size: 0.625rem;
        letter-spacing: 0.1em;
    }

    /* At the narrowest sizes the browse links can still crowd. Let them scroll
       sideways instead of wrapping into a ragged block. */
    .nav:not(.nav--actions) {
        flex-wrap: nowrap;
        overflow-x: auto;
        scrollbar-width: none;
    }
    .nav:not(.nav--actions)::-webkit-scrollbar { display: none; }
    .nav a { white-space: nowrap; }
}

/* Landscape on a phone.
   The wrapped header is three rows and around 140px tall. On a handset turned
   sideways the viewport is only ~360px high, so a sticky header would hold
   nearly 40% of the screen permanently and leave a letterbox to read the shop
   through. Let it scroll away instead — it is a short scroll back up.

   Keyed on height rather than orientation alone so a tablet in landscape,
   which has the room, keeps its sticky header. */
@media (max-height: 500px) {
    .site-header { position: static; }
}

/* The same reasoning for the on-screen keyboard: on a short viewport the
   sticky basket summary would cover the field being typed into. */
@media (max-height: 500px) {
    .cart-summary { position: static; }
}

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-5);
    border: 1px solid transparent;
    border-radius: var(--radius);
    font-size: var(--text-sm);
    font-weight: 600;
    font-family: inherit;
    line-height: 1.2;
    cursor: pointer;
    text-decoration: none;
    transition: background var(--transition), border-color var(--transition), transform var(--transition);
}
.btn:active { transform: translateY(1px); }

.btn--primary {
    background: var(--brand-700);
    color: var(--text-inverse);
}
.btn--primary:hover { background: var(--brand-800); color: var(--text-inverse); }

.btn--accent {
    background: var(--accent-500);
    color: var(--ink-900);
}
.btn--accent:hover { background: var(--accent-600); color: var(--white); }

.btn--ghost {
    background: transparent;
    border-color: var(--border-strong);
    color: var(--text);
}
.btn--ghost:hover { background: var(--surface-sunken); }

.btn--danger { background: var(--error-600); color: var(--white); }

.btn--block { width: 100%; }
.btn--sm { padding: var(--space-2) var(--space-3); font-size: var(--text-xs); }

/* --------------------------------------------------------------------------
   Forms
   -------------------------------------------------------------------------- */
.field { margin-bottom: var(--space-4); }

.field label {
    display: block;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--ink-700);
    margin-bottom: var(--space-2);
}

.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="number"],
.field input[type="search"],
.field select,
.field textarea {
    width: 100%;
    padding: var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    font-family: inherit;
    font-size: var(--text-base);
    color: var(--text);
    background: var(--surface);
    transition: border-color var(--transition), box-shadow var(--transition);
}

.field input:focus, .field select:focus, .field textarea:focus {
    outline: none;
    border-color: var(--brand-500);
    box-shadow: 0 0 0 3px var(--brand-100);
}

.field textarea { min-height: 140px; resize: vertical; }

.field__hint {
    font-size: var(--text-xs);
    color: var(--text-faint);
    margin-top: var(--space-1);
}

/* A row of fields that collapses to one per line when there is no room.
   flex-basis is the wrap trigger: 140px means three fields sit together on a
   desktop form and go one-per-line on a phone without a media query. */
.field-row { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.field-row .field { flex: 1 1 140px; min-width: 0; }

/* Checkbox and its label as one row, so a label that runs to two lines aligns
   under itself rather than under the box. Used on checkout for the terms and
   marketing consents as well as throughout the admin. */
.checkbox {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    font-size: var(--text-sm);
    padding: var(--space-1) 0;
    cursor: pointer;
}
.checkbox input { margin-top: 3px; flex: 0 0 auto; }

/* WCAG 2.5.8 and plain usability: the native control is around 13px, which is
   below the 24px minimum and far below what a thumb can hit reliably. Only on
   touch pointers — on a mouse the default size is correct and enlarging it
   would look wrong. */
@media (pointer: coarse) {
    .checkbox input[type="checkbox"],
    .checkbox input[type="radio"],
    .payment-option input[type="radio"] {
        width: 20px;
        height: 20px;
    }
}

/* --------------------------------------------------------------------------
   Notices
   -------------------------------------------------------------------------- */
.notice {
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius);
    border: 1px solid transparent;
    font-size: var(--text-sm);
    margin-bottom: var(--space-4);
}
.notice--success { background: var(--ok-100);    border-color: #b8e0ca; color: var(--ok-600); }
.notice--error   { background: var(--error-100); border-color: #f0c4c4; color: var(--error-600); }
.notice--warning { background: var(--warn-100);  border-color: #ecd6a4; color: var(--warn-600); }
.notice--info    { background: var(--info-100);  border-color: #c2dcee; color: var(--info-600); }

/* --------------------------------------------------------------------------
   Shared primitives — utilities, pills and data tables

   These lived in admin.css, but the storefront account pages, the legal pages
   and the whole supplier portal reference them too, and neither of those
   surfaces loads admin.css. `.muted` on a customer's order history was
   therefore doing nothing, and `table.data` was rendering as a bare browser
   table — no padding, no collapsed borders and, with five columns of order
   data, wider than a phone.

   They belong in the base stylesheet because all three surfaces use them.
   admin.css keeps only what is genuinely admin chrome.
   -------------------------------------------------------------------------- */
.muted { color: var(--text-faint); }
.right { text-align: right; }
.nowrap { white-space: nowrap; }

/* SKUs, order numbers and hashes have no spaces to break at, so they are the
   strings that push a phone layout sideways. */
.mono {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    overflow-wrap: anywhere;
}

.pill {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 2px var(--space-3);
    border-radius: var(--radius-full);
    font-size: var(--text-xs);
    font-weight: 600;
    white-space: nowrap;
}
.pill::before {
    content: "";
    width: 7px; height: 7px;
    border-radius: 50%;
    background: currentColor;
    flex: 0 0 auto;
}
.pill--green { background: var(--ok-100);    color: var(--ok-600); }
.pill--amber { background: var(--warn-100);  color: var(--warn-600); }
.pill--red   { background: var(--error-100); color: var(--error-600); }
.pill--grey  { background: var(--ink-100);   color: var(--text-muted); }

.thumb {
    width: 40px; height: 40px;
    border-radius: var(--radius-sm);
    background: var(--surface-sunken);
    overflow: hidden;
    border: 1px solid var(--border);
    flex: 0 0 auto;
}
.thumb img { width: 100%; height: 100%; object-fit: cover; }

/* Tables ------------------------------------------------------------------
   A data table has a width its columns actually need, and on a phone that is
   usually more than the viewport. The rule everywhere is that the TABLE
   scrolls, never the page: .table-wrap is the scroll container, and every
   table in the application is inside one.

   overscroll-behavior-inline stops a sideways flick that reaches the end of
   the table from being handed to the browser as a back-navigation gesture. */
.table-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-inline: contain;
}

table.data {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
}

table.data th {
    text-align: left;
    padding: var(--space-3) var(--space-4);
    background: var(--ink-050);
    border-bottom: 1px solid var(--border);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-faint);
    font-weight: 600;
    white-space: nowrap;
}

table.data td {
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--ink-100);
    vertical-align: middle;
}

table.data tbody tr:last-child td { border-bottom: 0; }
table.data tbody tr:hover { background: var(--ink-050); }

/* Stacked tables ----------------------------------------------------------
   Sideways scrolling is the right answer for a dense admin table an operator
   is scanning, but it is the wrong answer for the four customer-facing tables
   — order history, order lines, the cookie register. A customer reading one
   order should not have to discover that a panel scrolls.

   .table--stack turns each row into its own labelled block below 640px. The
   column heading is repeated per cell from a data-label attribute, so the
   value never appears without the word that explains it. Opt-in: tables
   without the class keep scrolling. */
@media (max-width: 640px) {
    .table--stack thead {
        /* Not display:none — that removes the header from the accessibility
           tree as well, and the per-cell labels below are visual only. */
        position: absolute;
        width: 1px; height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
    }

    .table--stack, .table--stack tbody, .table--stack tr, .table--stack td {
        display: block;
        width: 100%;
    }

    .table--stack tr {
        border: 1px solid var(--border);
        border-radius: var(--radius);
        padding: var(--space-2) var(--space-3);
        margin-bottom: var(--space-3);
        background: var(--surface);
    }
    .table--stack tr:last-child { margin-bottom: 0; }
    .table--stack tbody tr:hover { background: var(--surface); }

    .table--stack td {
        padding: var(--space-2) 0;
        border-bottom: 1px solid var(--ink-100);
        text-align: left;
    }
    .table--stack tr td:last-child { border-bottom: 0; }

    /* Only a LABELLED cell becomes a label/value row. An unlabelled one — the
       product name and its variant, a cell holding a form — stays a block, so
       its own children are not flattened into a row beside each other. */
    .table--stack td[data-label] {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: var(--space-4);
    }

    .table--stack td[data-label]::before {
        content: attr(data-label);
        flex: 0 0 auto;
        font-size: var(--text-xs);
        text-transform: uppercase;
        letter-spacing: 0.06em;
        color: var(--text-faint);
        font-weight: 600;
    }

    /* The value side takes the slack and stays hard right, which keeps a
       column of money aligned down the card. */
    .table--stack td > * { min-width: 0; }
    .table--stack td.right { text-align: right; }

    /* A cell with nothing but a label would read as a broken row. */
    .table--stack td:empty { display: none; }
}

/* --------------------------------------------------------------------------
   Cards & product grid
   -------------------------------------------------------------------------- */
.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-5);
    box-shadow: var(--shadow-sm);
}

/* Product grid.
   min() rather than a bare minmax: on a viewport narrower than the track
   minimum, plain minmax(230px, 1fr) overflows the container and forces sideways
   scrolling. min(100%, 230px) lets the track shrink instead — WCAG 1.4.10
   (Reflow) expects no horizontal scroll at 320px. */
.grid {
    display: grid;
    gap: var(--space-5);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 230px), 1fr));
}

/* Two across on phones. Small goods read better paired than one enormous card
   per screen, and it halves the scrolling to reach the same number of items. */
@media (max-width: 560px) {
    .grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--space-4);
    }
    .product-card__body { padding: var(--space-3); }
    .product-card__price { font-size: var(--text-base); }
}

.product-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    text-decoration: none;
    color: inherit;
    transition: box-shadow var(--transition), transform var(--transition);
}
.product-card:hover {
    box-shadow: var(--shadow-lg);
    transform: translateY(-2px);
    color: inherit;
}

.product-card__media {
    aspect-ratio: 1 / 1;
    background: var(--surface-sunken);
    display: grid;
    place-items: center;
    overflow: hidden;
}
.product-card__media img { width: 100%; height: 100%; object-fit: cover; }

.product-card__body { padding: var(--space-4); flex: 1; display: flex; flex-direction: column; }

.product-card__title {
    font-size: var(--text-sm);
    font-weight: 600;
    margin: 0 0 var(--space-2);
    color: var(--ink-800);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.product-card__price {
    margin-top: auto;
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--brand-800);
}
.product-card__price del { color: var(--text-faint); font-weight: 400; font-size: var(--text-sm); margin-right: var(--space-2); }

.empty-state {
    text-align: center;
    padding: var(--space-8) var(--space-5);
    color: var(--text-muted);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-lg);
    background: var(--surface);
}

/* --------------------------------------------------------------------------
   Featured

   Sits across the top of the store, above the grid. A tinted band rather
   than another white section, so it reads as a separate shelf without
   needing a heavy divider — and it only ever renders when something is
   actually flagged, so the store never opens on an empty state.
   -------------------------------------------------------------------------- */
.featured {
    background:
        radial-gradient(900px 300px at 12% -30%, var(--brand-600) 0%, transparent 60%),
        linear-gradient(160deg, var(--brand-900), var(--brand-700));
    color: var(--brand-050);
    padding: var(--space-7) 0 var(--space-8);
}
.featured__heading {
    color: var(--white);
    font-size: var(--text-xl);
    margin: 0 0 var(--space-5);
    letter-spacing: .01em;
}

/* Cards keep their own light surface so they stay readable on the band. */
.featured .product-card { background: var(--white); }
.featured .product-card__title { color: var(--text); }

@media (max-width: 720px) {
    .featured { padding: var(--space-6) 0 var(--space-7); }
}

/* --------------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------------- */
.site-footer {
    background: var(--brand-900);
    color: var(--brand-200);
    padding: var(--space-7) 0 var(--space-5);
    margin-top: var(--space-9);
    font-size: var(--text-sm);
}
.site-footer a { color: var(--brand-100); text-decoration: none; }
.site-footer a:hover { color: var(--white); text-decoration: underline; }
.site-footer .footer__heading { color: var(--white); font-size: var(--text-sm); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: var(--space-3); }

.site-footer__grid {
    display: grid;
    gap: var(--space-6);
    grid-template-columns: 2fr 1fr 1fr 1fr;
}
@media (max-width: 780px) { .site-footer__grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 460px) { .site-footer__grid { grid-template-columns: 1fr; } }

.site-footer ul { list-style: none; margin: 0; padding: 0; }
.site-footer li + li { margin-top: var(--space-2); }

.site-footer__base {
    border-top: 1px solid var(--brand-700);
    margin-top: var(--space-6);
    padding-top: var(--space-4);
    display: flex;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
    color: var(--brand-300);
    font-size: var(--text-xs);
}

/* --------------------------------------------------------------------------
   Minimal / centred pages (coming soon, login, errors)
   -------------------------------------------------------------------------- */
.centred-page {
    /* 100vh on a mobile browser measures the viewport with the address bar
       hidden, so the panel is always taller than what is on screen and a
       sign-in page scrolls for no reason. dvh is the live height; the vh
       value stays first as the fallback. */
    min-height: 100vh;
    min-height: 100dvh;
    display: grid;
    place-items: center;
    padding: var(--space-5);
    background:
        radial-gradient(900px 500px at 50% -20%, var(--brand-700) 0%, transparent 65%),
        var(--brand-900);
}

.centred-page__panel {
    width: 100%;
    max-width: 440px;
    background: var(--surface);
    border-radius: var(--radius-lg);
    padding: var(--space-6);
    box-shadow: var(--shadow-lg);
}

.centred-page__panel--wide { max-width: 620px; }

.centred-page__brand {
    text-align: center;
    margin-bottom: var(--space-6);
}
.centred-page__brand .brand { font-size: var(--text-2xl); }

/* --------------------------------------------------------------------------
   Coming soon
   -------------------------------------------------------------------------- */
.coming-soon {
    min-height: 100vh;
    min-height: 100dvh;
    display: grid;
    place-items: center;
    text-align: center;
    padding: var(--space-6);
    color: var(--brand-100);
    background:
        radial-gradient(1000px 600px at 50% 0%, var(--brand-600) 0%, transparent 60%),
        linear-gradient(170deg, var(--brand-900) 0%, var(--brand-800) 100%);
}
.coming-soon__inner { max-width: 620px; }
.coming-soon .brand {
    font-size: var(--text-2xl);
    color: var(--white);
    justify-content: center;
    margin-bottom: var(--space-6);
}
.coming-soon h1 { color: var(--white); font-size: var(--text-3xl); }
.coming-soon p { color: var(--brand-200); font-size: var(--text-lg); }
.coming-soon__rule {
    width: 48px; height: 3px;
    background: var(--accent-500);
    border: 0;
    margin: var(--space-6) auto;
    border-radius: var(--radius-full);
}
.coming-soon__contact { font-size: var(--text-sm); color: var(--brand-300); }
.coming-soon__contact a { color: var(--accent-400); }

/* --------------------------------------------------------------------------
   Supplier portal chrome

   A third surface, deliberately not the admin shell — see views/layouts/
   supplier.php. Its header was a row of inline styles that could not carry a
   media query, so on a phone the brand, four nav links, the supplier name and
   the sign-out button were all competing for one line.
   -------------------------------------------------------------------------- */
.portal-header {
    border-bottom: 1px solid var(--border);
    background: var(--surface);
}

.portal-header__inner {
    display: flex;
    align-items: center;
    gap: var(--space-3) var(--space-5);
    padding-block: var(--space-4);
    flex-wrap: wrap;
}

.portal-header__brand { font-size: var(--text-lg); }

/* Takes the slack on a wide screen so the user block is pushed right; on a
   narrow one it drops to its own full-width row. */
.portal-nav {
    display: flex;
    gap: var(--space-4);
    flex: 1 1 auto;
    flex-wrap: wrap;
}

.portal-nav a {
    color: var(--text-muted);
    text-decoration: none;
    font-size: var(--text-sm);
    font-weight: 500;
    padding: var(--space-2) 0;
    border-bottom: 2px solid transparent;
}
.portal-nav a:hover,
.portal-nav a[aria-current="page"] {
    color: var(--brand-700);
    border-bottom-color: var(--accent-500);
}

.portal-header__user {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--text-sm);
}

@media (max-width: 640px) {
    /* Brand and sign-out share the first row — the two things needed at any
       moment — and the links take a full row of their own beneath, ruled off
       the same way the storefront header does it. */
    .portal-header__inner { gap: var(--space-3); }
    .portal-header__brand { flex: 1 1 auto; }
    .portal-header__user { order: 2; }

    .portal-nav {
        order: 3;
        flex: 1 0 100%;
        gap: var(--space-4);
        padding-top: var(--space-2);
        border-top: 1px solid var(--border);
        /* Four links plus their padding do not fit at 320px. Scrolling beats
           a ragged two-line block. */
        flex-wrap: nowrap;
        overflow-x: auto;
        scrollbar-width: none;
    }
    .portal-nav::-webkit-scrollbar { display: none; }
    .portal-nav a { white-space: nowrap; }

    /* A long supplier name would otherwise push the sign-out button off. */
    .portal-header__user .muted {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        max-width: 14ch;
    }
}

/* --------------------------------------------------------------------------
   Breadcrumbs
   -------------------------------------------------------------------------- */
.breadcrumbs {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: center;
    font-size: var(--text-sm);
    color: var(--text-faint);
    margin-bottom: var(--space-5);
}
.breadcrumbs a { color: var(--text-muted); text-decoration: none; }
.breadcrumbs a:hover { color: var(--link); text-decoration: underline; }

/* --------------------------------------------------------------------------
   Shop listing
   -------------------------------------------------------------------------- */
.shop-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: var(--space-5);
    flex-wrap: wrap;
    margin-bottom: var(--space-6);
}
.shop-head h1 { margin-bottom: var(--space-2); }

.shop-sort { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); }
.shop-sort label { color: var(--text-muted); }
.shop-sort select {
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface);
    font-family: inherit;
    font-size: var(--text-sm);
    color: var(--text);
}

.shop-layout { display: grid; grid-template-columns: 220px 1fr; gap: var(--space-6); align-items: start; }

/* No categories to filter by means no sidebar, so the results take the full
   width. Without this the listing sat in the 220px sidebar column — narrower
   than a single card — and every product stacked vertically. */
.shop-layout--full { grid-template-columns: 1fr; }

@media (max-width: 860px) {
    .shop-layout { grid-template-columns: 1fr; }
}

.shop-filters h2 {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-faint);
    margin-bottom: var(--space-3);
}
.shop-filters ul { list-style: none; margin: 0; padding: 0; }
.shop-filters li + li { margin-top: var(--space-1); }
.shop-filters a {
    display: block;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius);
    color: var(--text-muted);
    text-decoration: none;
    font-size: var(--text-sm);
}
.shop-filters a:hover { background: var(--surface); color: var(--brand-700); }

/* The sidebar on a phone.
   --------------------------------------------------------------------------
   Stacking puts the category tree above the products, and an unbounded tree
   can be a full screen of links before the first thing for sale. Most
   branches arrive collapsed (see .cat-tree below), but a deep category page
   opens the trail down to itself, so the height is capped and the panel
   scrolls within itself — the grid stays within a short scroll of the top.

   This block MUST stay below the base .shop-filters rules above. It matches
   them on specificity, so if it sits earlier in the file the base rules win
   and the overrides here silently do nothing.

   Deliberately NOT sticky. The heading was pinned here at first so it stayed
   visible while the list moved, but a sticky element inside an overflow
   container repaints unreliably on iOS and the category names smeared through
   it as they scrolled past. The panel is only 45vh and carries a border and a
   surface of its own, so its heading is never far away and never ambiguous —
   the effect was not worth a rendering artifact on the store's main page. */
@media (max-width: 860px) {
    .shop-filters {
        max-height: 45vh;
        overflow-y: auto;

        /* No overscroll-behavior: contain here. Containment is right for a
           modal drawer, but this panel sits inline in the page and covers
           much of a phone screen — trapping the scroll would mean a swipe
           that starts on it stops dead at the end of the list instead of
           carrying on down to the products. Chaining is the default and is
           what this wants. */

        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--radius-lg);
        padding: var(--space-4);
        margin-bottom: var(--space-5);
    }

    /* On the surface the panel now has, the base hover tint paints white on
       white. The sunken shade is the one that reads here. */
    .shop-filters a:hover { background: var(--surface-sunken); }
}

/* --------------------------------------------------------------------------
   Category tree

   The sidebar used to be one flat list indented with an inline padding-left
   per depth, so a shop with any real depth to it presented every subcategory
   at once. It is now nested, and branches collapse.
   -------------------------------------------------------------------------- */

/* The link and its toggle share a row; the link takes what is left so the
   whole width of it stays clickable, not just the text. */
.cat-tree__row { display: flex; align-items: center; gap: var(--space-1); }
.cat-tree__link { flex: 1 1 auto; min-width: 0; }

.cat-tree__link[aria-current="page"] {
    background: var(--brand-050);
    color: var(--brand-700);
    font-weight: 600;
}

/* Indent by nesting rather than by depth arithmetic. The border gives the eye
   a line back to the parent, which matters once a branch is long enough to
   push its parent off the top of the list. */
.cat-tree__children {
    margin-left: var(--space-3);
    padding-left: var(--space-2);
    border-left: 1px solid var(--border);
}

.cat-tree__toggle {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px; height: 28px;
    padding: 0;
    border: 0;
    border-radius: var(--radius);
    background: none;
    color: var(--text-faint);
    cursor: pointer;
}
.cat-tree__toggle:hover { background: var(--surface); color: var(--brand-700); }

.cat-tree__chevron { width: 14px; height: 14px; transition: transform 0.15s ease; }
.cat-tree__toggle[aria-expanded="true"] .cat-tree__chevron { transform: rotate(90deg); }

@media (prefers-reduced-motion: reduce) {
    .cat-tree__chevron { transition: none; }
}

/* Cutting the mustard. The markup always carries the honest state — `hidden`
   on every closed branch — but a toggle nobody can operate is worse than no
   toggle, so until storefront.js announces itself the buttons are not rendered
   and `hidden` is overruled. That leaves the no-JS sidebar as a fully expanded
   tree, which is what it has always been.

   Both rules are undone by .cat-tree--enhanced, which the script adds as its
   first act. storefront.js is deferred, so a slow load can show the open tree
   for a moment before it collapses. That is the deliberate trade: keying this
   off a class the script sets means a script that is blocked, broken or never
   arrives leaves working navigation behind, not a column of dead buttons —
   the same bargain the rest of this file's enhancements make. */
.cat-tree__toggle { display: none; }
.cat-tree__children[hidden] { display: block; }

.cat-tree--enhanced .cat-tree__toggle { display: inline-flex; }
.cat-tree--enhanced .cat-tree__children[hidden] { display: none; }

.product-card__media { position: relative; }

.product-card__flag {
    position: absolute;
    top: var(--space-3);
    left: var(--space-3);
    background: var(--ink-800);
    color: var(--white);
    font-size: var(--text-xs);
    font-weight: 600;
    padding: 2px var(--space-3);
    border-radius: var(--radius-full);
}
.product-card__flag--sale { background: var(--accent-500); color: var(--ink-900); }

/* --------------------------------------------------------------------------
   Product detail
   -------------------------------------------------------------------------- */
.product-detail { display: grid; grid-template-columns: 1.1fr 1fr; gap: var(--space-7); align-items: start; }
@media (max-width: 860px) { .product-detail { grid-template-columns: 1fr; gap: var(--space-5); } }

.product-detail__main {
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    background: var(--surface-sunken);
    border-radius: var(--radius-lg);
    border: 1px solid var(--border);
}
.product-detail__main--empty { display: block; }

.product-detail__thumbs { display: flex; gap: var(--space-3); margin-top: var(--space-3); flex-wrap: wrap; }

.product-thumb {
    width: 72px; height: 72px;
    padding: 0;
    border: 2px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface-sunken);
    cursor: pointer;
    transition: border-color var(--transition);
}
.product-thumb.is-active { border-color: var(--brand-500); }
.product-thumb img { width: 100%; height: 100%; object-fit: cover; }

/* Product video, below the gallery. Sized by aspect-ratio rather than a fixed
   height so a portrait clip filmed on a phone is not letterboxed into a
   landscape box; object-fit: contain keeps whatever shape it really is. */
.product-detail__videos { margin-top: var(--space-5); display: grid; gap: var(--space-4); }

.product-video { margin: 0; }
.product-video video {
    display: block;
    width: 100%;
    max-height: 70vh;
    aspect-ratio: 16 / 9;
    object-fit: contain;
    background: #000;
    border-radius: var(--radius-lg);
    border: 1px solid var(--border);
}
.product-video figcaption {
    margin-top: var(--space-2);
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.product-detail__brand {
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-size: var(--text-xs);
    color: var(--text-faint);
    font-weight: 600;
    margin-bottom: var(--space-2);
}

.product-detail__price {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-bottom: var(--space-1);
}
.product-detail__price strong { font-size: var(--text-2xl); color: var(--brand-800); font-family: var(--font-display); }
.product-detail__price del { color: var(--text-faint); font-size: var(--text-lg); }

.product-detail__vat { font-size: var(--text-sm); margin-bottom: var(--space-5); }
.product-detail__summary { font-size: var(--text-lg); color: var(--text-muted); }

.product-detail__stock {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-bottom: var(--space-5);
}

.product-detail__actions { margin-bottom: var(--space-6); }

.product-detail__description, .product-detail__specs {
    border-top: 1px solid var(--border);
    padding-top: var(--space-5);
    margin-top: var(--space-5);
}
.product-detail__description h2, .product-detail__specs h2 { font-size: var(--text-base); }

.product-detail__specs dl { margin: 0; }
.product-detail__specs dl > div {
    display: grid;
    grid-template-columns: 1fr 1.4fr;
    gap: var(--space-4);
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--ink-100);
    font-size: var(--text-sm);
}
.product-detail__specs dt { color: var(--text-faint); }
.product-detail__specs dd { margin: 0; }

/* --------------------------------------------------------------------------
   Pagination
   -------------------------------------------------------------------------- */
.pagination {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
    margin-top: var(--space-6);
}
.pagination__summary { margin: 0; font-size: var(--text-sm); color: var(--text-faint); }
.pagination__list { display: flex; gap: var(--space-1); list-style: none; margin: 0; padding: 0; }

.pagination__list a, .pagination__list span {
    display: inline-block;
    min-width: 38px;
    text-align: center;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius);
    font-size: var(--text-sm);
    text-decoration: none;
    color: var(--text-muted);
    border: 1px solid transparent;
}
.pagination__list a:hover { background: var(--surface); border-color: var(--border); color: var(--brand-700); }
.pagination__list [aria-current="page"] { background: var(--brand-700); color: var(--white); font-weight: 600; }
.pagination__list [aria-disabled="true"] { opacity: 0.4; }
.pagination__gap { border: 0 !important; }

/* --------------------------------------------------------------------------
   Header search
   -------------------------------------------------------------------------- */
.header-search { display: flex; gap: var(--space-2); align-items: center; }
.header-search input {
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    font-family: inherit;
    font-size: var(--text-sm);
    background: var(--surface);
    color: var(--text);
    width: 160px;
    transition: width var(--transition), border-color var(--transition);
}
.header-search input:focus { width: 220px; outline: none; border-color: var(--brand-500); }

/* Narrow desktops only. Below 860px the header wraps and the field takes the
   full row instead — see the header rules above. Search is never hidden:
   on a phone it is the fastest route to a product. */
@media (max-width: 900px) and (min-width: 861px) {
    .header-search input { width: 120px; }
}

/* --------------------------------------------------------------------------
   Basket & checkout
   -------------------------------------------------------------------------- */
.cart-layout,
.checkout-layout {
    display: grid;
    grid-template-columns: 1fr 340px;
    gap: var(--space-6);
    align-items: start;
}
@media (max-width: 900px) {
    .cart-layout, .checkout-layout { grid-template-columns: 1fr; }
}

.cart-line {
    display: grid;
    grid-template-columns: 88px 1fr auto auto;
    gap: var(--space-4);
    align-items: center;
    padding: var(--space-4) 0;
    border-bottom: 1px solid var(--border);
}
@media (max-width: 560px) {
    .cart-line { grid-template-columns: 64px 1fr; }
    .cart-line__qty, .cart-line__price { grid-column: 2; }

    /* Stacked, everything in the second column shares one left edge. Keeping
       the price right-aligned here would drag the "Remove" button under it
       over to the right as well, leaving the row's controls split between two
       edges for no reason. */
    .cart-line__price { text-align: left; }
}

.cart-line__media {
    width: 88px; height: 88px;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface-sunken);
    border: 1px solid var(--border);
    display: block;
}
.cart-line__media img { width: 100%; height: 100%; object-fit: cover; }
@media (max-width: 560px) { .cart-line__media { width: 64px; height: 64px; } }

.cart-line__body a { text-decoration: none; color: inherit; }
.cart-line__body a:hover { color: var(--link); }

.cart-line__qty input {
    width: 72px;
    padding: var(--space-2);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    font-family: inherit;
    text-align: center;
}

.cart-line__price { text-align: right; }

.link-button {
    background: none;
    border: 0;
    /* WCAG 2.5.8 (Target Size Minimum): these are standalone controls —
       "Remove", "Sign out", "Make default" — not links inside a sentence, so
       the 24x24 minimum applies. Padding plus min-height gets them there
       without changing where the text sits. */
    padding: var(--space-1) 0;
    min-height: 24px;
    color: var(--text-faint);
    font-family: inherit;
    font-size: var(--text-sm);
    text-decoration: underline;
    cursor: pointer;
}
.link-button:hover { color: var(--error-600); }

.cart-summary {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-5);
    position: sticky;
    /* Clears the 72px header plus a little air. */
    top: 92px;
}

/* Below 900px the layout is one column and the summary is simply the last
   block on the page, so there is nothing for it to stick to. Worse, the header
   wraps to three rows on a phone and is well over 92px tall, so the offset
   would tuck the top of the summary underneath it. Static is correct here. */
@media (max-width: 900px) {
    .cart-summary { position: static; }
}
.cart-summary h2 { font-size: var(--text-base); margin-bottom: var(--space-4); }
.cart-summary dl { margin: 0 0 var(--space-4); }
.cart-summary dl > div {
    display: flex;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-2) 0;
    font-size: var(--text-sm);
}
.cart-summary dt, .cart-summary dd { margin: 0; }
.cart-summary__total {
    border-top: 1px solid var(--border);
    margin-top: var(--space-2);
    padding-top: var(--space-3) !important;
    font-size: var(--text-lg) !important;
    font-weight: 700;
}

.checkout-line {
    display: flex;
    justify-content: space-between;
    gap: var(--space-3);
    font-size: var(--text-sm);
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--ink-100);
}

.payment-option {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-4);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    margin-bottom: var(--space-3);
    cursor: pointer;
    transition: border-color var(--transition), background var(--transition);
}
.payment-option:hover { border-color: var(--brand-400); background: var(--ink-050); }
.payment-option:has(input:checked) { border-color: var(--brand-600); background: var(--brand-050); }
.payment-option input { margin-top: 4px; }

/* --------------------------------------------------------------------------
   Order confirmation
   -------------------------------------------------------------------------- */
.order-hero {
    padding: var(--space-6);
    border-radius: var(--radius-lg);
    margin-bottom: var(--space-5);
    border: 1px solid transparent;
}
.order-hero h1 { font-size: var(--text-2xl); margin-bottom: var(--space-2); }
.order-hero p { margin: 0; }
.order-hero--ok      { background: var(--ok-100);   border-color: #b8e0ca; }
.order-hero--ok h1   { color: var(--ok-600); }
.order-hero--pending { background: var(--warn-100); border-color: #ecd6a4; }
.order-hero--pending h1 { color: var(--warn-600); }

.order-meta {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 140px), 1fr));
    gap: var(--space-4);
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--border);
}
.order-meta span { display: block; font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.06em; }

.parcel {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-4);
    margin-top: var(--space-4);
    background: var(--ink-050);
}
.parcel h3 { font-size: var(--text-sm); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); }
.parcel ul { margin: 0 0 var(--space-2); padding-left: var(--space-5); font-size: var(--text-sm); }

.cart-count {
    display: inline-block;
    min-width: 20px;
    padding: 0 6px;
    border-radius: var(--radius-full);
    background: var(--accent-500);
    color: var(--ink-900);
    font-size: var(--text-xs);
    font-weight: 700;
    text-align: center;
    margin-left: var(--space-1);
}

/* Added by storefront.js each time an item goes in. The count changing from
   one digit to another is too quiet to notice on its own. Reduced-motion
   users get no pop — the global rule near the top of this file collapses it —
   and still have the notice under the button. */
.cart-count--bump { animation: cart-count-bump 450ms ease-out; }

@keyframes cart-count-bump {
    0%   { transform: scale(1); }
    35%  { transform: scale(1.5); box-shadow: 0 0 0 6px rgba(194, 139, 69, 0.35); }
    100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(194, 139, 69, 0); }
}

/* Confirmation under the "Add to basket" button. */
.basket-feedback .notice {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2) var(--space-4);
    margin: var(--space-3) 0 0;
    animation: basket-feedback-in 200ms ease-out;
}
.basket-feedback .notice a { color: inherit; font-weight: 600; white-space: nowrap; }

@keyframes basket-feedback-in {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: none; }
}

/* `.btn.btn--added` so it outranks .btn--accent and its :hover whatever the
   source order. */
.btn.btn--added,
.btn.btn--added:hover {
    background: var(--ok-600);
    border-color: var(--ok-600);
    color: var(--white);
}

/* --------------------------------------------------------------------------
   Customer account
   -------------------------------------------------------------------------- */
.account-nav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-4);
    align-items: center;
    padding: var(--space-3) 0 var(--space-5);
    border-bottom: 1px solid var(--border);
    margin-bottom: var(--space-6);
}
.account-nav a { color: var(--text-muted); text-decoration: none; }
.account-nav a:hover { color: var(--text); }
.account-nav a[aria-current="page"] { color: var(--text); font-weight: 600; }

/* Main column and a narrower aside; stacks on small screens. */
.account-grid { display: grid; grid-template-columns: 1fr 320px; gap: var(--space-6); align-items: start; }
.account-grid .card + .card { margin-top: var(--space-5); }

@media (max-width: 860px) {
    .account-grid { grid-template-columns: 1fr; }
}

/* Optional account offer at checkout. Set apart so it reads as an extra,
   not as another required step. */
.account-offer {
    background: var(--ink-050);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-4);
    margin: var(--space-4) 0;
}
.account-offer .field { margin-bottom: 0; }


/* Selection is shown by border AND a check mark, never by colour alone. */


/* --------------------------------------------------------------------------
   Previous / next product

   A real <nav> with links carrying the product NAME, so the destination is
   clear from the link text alone rather than from an arrow. The thumbnails are
   decorative (alt="") because the name beside them already says where it goes;
   announcing both would repeat it.
   -------------------------------------------------------------------------- */
.product-nav {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-4);
    margin-top: var(--space-8);
    padding-top: var(--space-6);
    border-top: 1px solid var(--border);
}

.product-nav__link {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    text-decoration: none;
    color: inherit;
    transition: border-color var(--transition), box-shadow var(--transition);
}
.product-nav__link:hover { border-color: var(--brand-400); box-shadow: var(--shadow); color: inherit; }
.product-nav__link--next { grid-column: 2; justify-content: flex-end; text-align: right; }

.product-nav__media {
    flex: 0 0 56px;
    width: 56px;
    aspect-ratio: 1 / 1;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface-sunken);
}
.product-nav__media img { width: 100%; height: 100%; object-fit: cover; display: block; }

.product-nav__direction {
    display: block;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
}
.product-nav__name {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-weight: 600;
    font-size: var(--text-sm);
}

/* Stack on narrow screens; a 56px thumbnail plus two lines of text does not
   fit two-up on a phone. */
@media (max-width: 560px) {
    .product-nav { grid-template-columns: 1fr; }
    .product-nav__link--next { grid-column: 1; }
}


/* Variant dropdown. A native <select>: keyboard support, screen-reader
   semantics and the mobile picker all come from the platform, and there is no
   ARIA to get wrong. Each option carries its own price in the text so the
   choice is legible with the list closed and with scripting off. */
.variant-select { margin-bottom: var(--space-5); }
.variant-select label { display: block; margin-bottom: var(--space-2); font-weight: 600; font-size: var(--text-sm); }
/* min() rather than a bare 380px: this selector out-specifies the blanket
   `select { max-width: 100% }` in the reset, so a fixed value here would let
   the variant picker run past the edge of a 320px screen. */
.variant-select select { width: 100%; max-width: min(380px, 100%); }

/* --------------------------------------------------------------------------
   Cookie notice

   A notice, not a consent dialogue — see app/Core/CookieNotice.php. That is
   why there is no overlay and nothing is dimmed: the page underneath stays
   fully readable and usable, because nothing on it is waiting for an answer.

   Fixed to the bottom rather than pinned above the content, so it never
   reflows the page on load.
   -------------------------------------------------------------------------- */
.cookie-notice {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 60;              /* Over the sticky header's 50, under nothing. */
    background: var(--brand-900);
    color: var(--brand-100);
    border-top: 2px solid var(--accent-500);
    box-shadow: 0 -4px 20px rgb(0 0 0 / 18%);
    padding: var(--space-4) 0;

    /* Clears the iPhone home indicator, which otherwise sits on top of the
       dismiss button. Resolves to 0 everywhere else. */
    padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));

    /* A long notice on a short landscape screen could otherwise cover the
       whole viewport with no way to reach the button that dismisses it. */
    max-height: 60vh;
    max-height: 60dvh;
    overflow-y: auto;
}

.cookie-notice__inner {
    max-width: var(--container);
    margin-inline: auto;
    padding-inline: var(--space-5);
    display: flex;
    align-items: center;
    gap: var(--space-5);
}

.cookie-notice__text {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.5;
}

.cookie-notice__text a {
    color: var(--brand-100);
    text-decoration: underline;
}

/* Keeps the button on one line and hard against the right edge, so the text
   takes the slack rather than the control moving about between pages. */
.cookie-notice__form {
    margin: 0;
    margin-left: auto;
    flex: 0 0 auto;
}

@media (max-width: 640px) {
    /* Stack, and let the button go full width — a small target at the bottom
       of a phone screen is the one that gets mis-tapped. */
    .cookie-notice__inner {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-3);
    }

    .cookie-notice__form { margin-left: 0; }
    .cookie-notice__form .btn { width: 100%; }
}

/* Accept / Reject, shown while Google Analytics is switched on. Both buttons
   share one class and one size on purpose: making "reject" the quieter of the
   two is the dark pattern the ICO names, and a refusal that is harder to give
   than an acceptance is not valid consent. Do not restyle one without the
   other. */
.cookie-notice__form--choice {
    display: flex;
    gap: var(--space-3);
}
.cookie-notice__form--choice .btn { flex: 1 1 0; white-space: nowrap; }

/* The same choice on the Cookies page, where it can be changed later. */
.cookie-choice {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3) var(--space-4);
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--ink-050);
}
.cookie-choice p { margin: 0; flex: 1 1 16rem; }

/* --------------------------------------------------------------------------
   Two-factor authentication

   In agoriva.css, NOT admin.css: the admin panel and the supplier portal both
   use these, and only the admin layout loads admin.css. Putting them there
   would leave the supplier portal's enrolment page unstyled — which is how
   the portal ended up unstyled once already.
   -------------------------------------------------------------------------- */

.twofa-steps {
    margin: 0 0 var(--space-5);
    padding-left: var(--space-5);
    display: grid;
    gap: var(--space-2);
}

.twofa-steps li { padding-left: var(--space-1); }

/* The QR itself. White is forced behind it rather than inherited: the SVG
   paints its own quiet zone white, and a dark page showing a white square
   with no border reads as a rendering fault rather than a deliberate frame. */
.twofa-qr {
    display: flex;
    justify-content: center;
    padding: var(--space-4);
    background: #fff;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin-bottom: var(--space-4);
}

/* Capped in absolute terms as well as relative. A QR code has no detail to
   reward extra size, and a 320px square on a phone pushes the code input —
   the thing the page is actually asking for — below the fold. */
.twofa-qr svg {
    width: 100%;
    max-width: 15rem;
    height: auto;
    display: block;
}

.twofa-secret {
    text-align: center;
    margin-bottom: var(--space-5);
}

/* word-break, because the secret is one unbroken 32-character token and a
   narrow screen would otherwise widen the whole page to fit it. */
.twofa-secret code {
    display: inline-block;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    letter-spacing: 0.08em;
    word-break: break-all;
    padding: var(--space-2) var(--space-3);
    background: var(--surface-sunken);
    border-radius: var(--radius-sm);
}

/* The code entry. Wide tracking and a large size because this is transcribed
   digit by digit from a phone held in the other hand. inputmode numeric brings
   up the number pad without type="number", which would add spinners and let a
   browser strip a leading zero from an otherwise valid code. */
.twofa-code-input {
    font-family: var(--font-mono);
    font-size: var(--text-lg);
    letter-spacing: 0.35em;
    text-align: center;
}

.twofa-codes {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-2);
    margin: var(--space-4) 0;
    padding: var(--space-4);
    background: var(--surface-sunken);
    border-radius: var(--radius);
    list-style: none;
}

.twofa-codes li {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    letter-spacing: 0.04em;
    text-align: center;
    padding: var(--space-1) 0;
}

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

@media (max-width: 480px) {
    /* One column below this width: a recovery code plus its tracking does not
       fit two-up on a narrow phone, and a wrapped code is a mistyped code. */
    .twofa-codes { grid-template-columns: 1fr; }
}

@media print {
    /* Recovery codes get printed — that is the point of them. Everything that
       is not a code is noise on the page that ends up in a drawer. */
    .twofa-no-print { display: none !important; }

    .twofa-codes {
        background: none;
        border: 1px solid #000;
    }
}

/* --------------------------------------------------------------------------
   Touch overrides — LAST IN THE FILE ON PURPOSE

   Everything below intentionally overrides component defaults declared
   earlier: the compact font sizes on the search and sort controls, and the
   text-sized tap targets on links. Most of these selectors tie on specificity
   with the component rule they are correcting (`.shop-filters a` against
   `.shop-filters a`), so the cascade decides on source order alone and this
   block only wins from the bottom of the file.

   Moving it back up silently disables it. That is not hypothetical: it was
   written near the top first, and .shop-sort select kept its 14px there.
   -------------------------------------------------------------------------- */

/* iOS Safari zooms the whole page in when a focused form control's text is
   under 16px, and it does not zoom back out afterwards. The result is a
   viewport wider than the screen and a layout the user now has to pan around
   — the single most common way a mobile form "breaks".

   The controls that were under 16px: the header search, the sort dropdown,
   the basket quantity box and the admin variant editor. Raising them only on
   coarse pointers keeps the intended compact sizing on a mouse.

   Buttons are deliberately excluded: only a focusable TEXT entry triggers the
   zoom, and forcing 16px on every button would inflate the intentionally tight
   controls in the admin variant editor on a tablet for no benefit. */
@media (pointer: coarse) {
    input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]),
    select,
    textarea {
        font-size: max(16px, 1em);
    }

    /* Specificity, not source order, is what decides here — moving this block
       to the end of the file is not enough on its own.

       The `input` selector above carries four :not() clauses, and :not() takes
       the specificity of its argument, so it scores (0,4,1) and outranks any
       ordinary component rule by itself. The bare `select` is only (0,0,1),
       which loses to a single class. Every component rule that sets a sub-16px
       size on a <select> therefore has to be named:

         .shop-sort select      the storefront sort dropdown
         .variant-editor select \ both in admin.css, which loads after this
         .video-grid__poster    / file, so they are corrected at the end of it */
    .shop-sort select { font-size: max(16px, 1em); }
}

/* Touch targets. WCAG 2.5.8 asks for 24x24 as a minimum; 44px is the size a
   thumb actually hits without care. Applied as a minimum height with centred
   content so nothing is repositioned on a mouse-driven desktop. */
@media (pointer: coarse) {
    .btn,
    .nav a,
    .account-nav a,
    .shop-filters a,
    .pagination__list a,
    .pagination__list span {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    .btn--sm { min-height: 40px; }

    /* Filters are a full-width list, so their text should stay left. */
    .shop-filters a { justify-content: flex-start; }

    /* The expand/collapse control on a category branch is 28px square, which
       is a hard target next to the link it sits beside — the two are a couple
       of pixels apart and mis-tapping opens the category instead of the
       branch. Widened only for touch. */
    .cat-tree__toggle { width: 44px; height: 44px; }

    /* "Remove", "Sign out", "Make default" — standalone controls that carry
       real consequences and were sized as text. */
    .link-button { min-height: 44px; }
}
