/* GENERATED by build-css.php from style.css. Do not edit by hand.
   Rules removed: only those whose every selector class is used by an admin
   template and by nothing on the storefront. Anything shared or unclassifiable
   is kept, so this can be larger than necessary but never missing a rule.
   Regenerate after every style.css change: php build-css.php apply */
/* =========================================================================
   Prime Core Dynamics — Phase 1
   Strict monochrome. No accent color. No rounded corners. Hairline borders.
   Inter (UI/body) + JetBrains Mono (data).
   ========================================================================= */

:root {
    --black:      #0a0a0a;
    --surface-dark: #1c1c1c;   /* matte black for large dark panels (nav, login) */
    --border-dark:  #3a3a3a;   /* hairlines/borders drawn on --surface-dark */
    --footer-bg:    #1B4DB1;   /* brand blue (placeholder — swap for your logo hex) */
    --accent-orange: #E8590C;  /* deep premium orange — Order pill (empty cart) */
    --accent-warm:   #F5B43C;  /* warm amber — SEO card icon blocks (reusable) */
    --accent-green:  #1b8a4e;  /* Order pill when the cart has items */
    --accent-blue:   #2E5BE0;  /* login tabs/button/glows/seam accent */
    /* THE brand orange. Defined here and nowhere else: every rule that wears it
       reads this variable, so changing this one line moves every add-to-cart
       button, the hero stat figures and the footer trust ring accent together.
       Do not paste the hex anywhere else. */
    --pcd-orange:    #f7890f;
    /* THE ORANGE A LABEL SITS ON, which is not always the brand orange.
       White on --pcd-orange measures 2.46:1 and always has; near-black on it
       measures 8.05:1, which is why dark mode puts --btn-ink at #0a0a0a and
       passes. The light theme's brief is white text on every orange button, and
       white cannot be made to pass on #f7890f, so the BUTTON'S orange darkens
       instead of the label lightening.

       IT IS NOT DECLARED HERE AT ALL, and that is the point. The button rules
       read var(--btn-orange, var(--pcd-orange)), so outside an explicit light
       choice the property does not exist, the fallback resolves, and the button
       is the one that shipped. Declaring it at :root as var(--pcd-orange) would
       have rendered the same colour and still been a real difference: a custom
       property inherits to every element, so it turns up on all 35,632 of them
       in a computed-style dump and the "dark is byte-identical" check stops
       being able to say anything. Declared once, under
       :root[data-theme="light"], at the end of this file.

       NON-TEXT USES KEEP --pcd-orange and must not be switched to this: the
       footer glow, the footer hairline, the hero stat figures, the nav item
       icons and the scroll progress line carry no label, so there is nothing
       for a contrast floor to protect and the brand colour is the point. */
    --hero-glow:     46, 91, 224;  /* brand-blue RGB triplet for the hero glow blobs (adjust hex here) */
    --off-white:  #fafafa;
    --white:      #ffffff;
    --border:     #e4e4e4;
    --border-2:   #d4d4d4;
    --muted:      #6b6b6b;
    --muted-2:    #9a9a9a;
    --muted-3:    #b8b8b8;
    --ink:        #141414;

    --font-ui:   'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;

    --maxw: 1240px;
    --step: 8px;

    /* THE storefront gutter. The nav bar, the hero content and the catalog
       (filters, count, product grid, SEO + FAQ blocks) all read this, so their
       left and right edges are the same number by construction and cannot drift
       apart again. The values are the catalog's existing ones — the grid and the
       card widths are unchanged; the nav and hero moved to meet them. */
    --gutter: 60px;

    /* The other half of the alignment contract. The product grid is capped at this
       and centred, so above ~1560px viewport it stops at 1440 while a container
       with only --gutter keeps growing — that is how the nav, hero and footer
       drifted away from the grid. Anything that must line up with the grid uses
       BOTH: max-width calc(--content-max + 2 * --gutter) plus margin auto. */
    --content-max: 1440px;

    /* ---- ROLE TOKENS (Run 2) --------------------------------------------
       Every value below is the EXACT literal it replaces in the rules that now
       read it, so light mode is byte-identical: a var() that resolves to the
       same hex paints the same pixel. The role is what the DARK block opposite
       collapses; light keeps its existing values, differences and all.

       WHY THERE ARE THREE HAIRLINES AND SEVEN MUTED GREYS. Grouping by role is
       the instruction and the dark half does exactly that: every --line-* is one
       colour in dark, every --muted-* is one colour in dark. Light cannot be
       collapsed the same way without changing it, and light must not change, so
       each distinct light literal keeps its own name and they converge on the
       dark side. Nothing here is a new colour in light. */

    /* page and surfaces */
    --bg:            #fafafa;   /* body, was var(--off-white) */
    --surface:       #ffffff;   /* cards, panels, summaries */
    --surface-2:     #eef0f4;   /* raised/inset rows */
    --surface-3:     #ececf1;   /* second inset tone */

    /* PRODUCT PLATES. White in BOTH modes, per the mockup: these are the pads a
       transparent product PNG sits on, and a dark pad behind a photographed vial
       shot on white reads as a printing error, not a theme. */
    --plate:         #ffffff;
    --plate-a:       #f7f7f8;
    --plate-b:       #eeeeef;
    --plate-c:       #f8f8f9;
    --plate-d:       #efeff1;

    /* hairlines and borders */
    --line:          #e4e4e4;   /* same value as --border */
    --line-soft:     #ececec;
    --line-cool:     #e4e4e8;
    --line-strong:   #d4d4d4;   /* same value as --border-2 */

    /* text */
    --text:          #141414;   /* same value as --ink */
    --text-1:        #111418;
    --text-2:        #111111;

    /* muted text */
    --muted-cool:    #6b7382;
    --muted-slate:   #4a5560;
    --muted-steel:   #5c6880;
    --muted-warm:    #6d717c;
    --muted-grey:    #8a8a90;
    --muted-blue:    #6b7280;
    --muted-ash:     #5a6470;

    /* links */
    --link:          #2e5be0;   /* same value as --accent-blue */
    --link-hover:    #264fc4;

    /* form controls */
    --input-bg:      #ffffff;
    --input-line:    #d4d4d4;
    --input-text:    #141414;
    --placeholder:   #7d8494;

    /* state and depth */
    --focus-ring:    #0a0a0a;   /* same value as --black */
    --scrim:         rgba(0, 0, 0, 0.45);
    --shadow:        0 3px 10px rgba(0, 0, 0, 0.10);
    --shadow-soft:   rgba(20, 30, 60, .08);

    /* pills. --pill-ink is the LABEL on a pill, and it is the one that moves:
       white on these fills in light, near-black on the lighter dark fills. */
    --pill-ink:      #ffffff;
    --pill-green:    #1fa863;
    --pill-green-2:  #1b8a4e;   /* same value as --accent-green */
    --btn-green-hover: #1a9457; /* Message customer hover: --pill-green a step darker */
    --pill-amber:    #f5c518;
    --pill-amber-bg: #fdf8e6;
    --pill-amber-ink:#8a5a00;
    --pill-red:      #d83b3b;
    --pill-red-bg:   #fdeaea;
    --pill-grey:     #54545a;

    /* second pass */
    --text-3:        #1c1c1e;
    --text-link:     #0a0a0a;   /* same value as --black: the default anchor */
    --muted-plum:    #4a4a55;
    --muted-slate-2: #4a5260;
    --line-warm:     #e6e6e9;
    --line-pale:     #dedede;
    --surface-4:     #f1f1f3;   /* row/button hover */
    --tint-blue:     #eaf1fe;   /* icon chip */
    --tint-amber:    #fdf3e0;
    --tint-amber-2:  #fdf9f0;
    --tint-amber-ln: #f0e0c0;
    /* THE LABEL ON A BRAND-COLOURED BUTTON. White on --pcd-orange measures
       2.46:1 and always did; in dark the brief calls for --black on it, which is
       8.05:1 and the answer this site already shipped for the cart pill. Light
       keeps the white it has today. */
    --btn-ink:       #ffffff;
    /* the cart pill's count bubble lives on the permanently dark nav in BOTH
       modes, so it is white in both: a --surface here would have turned it into
       a dark bubble on a blue pill, which is a regression this run caught. */
    --pill-bubble:   #ffffff;
    /* SELECTED-STATE SURFACE. The tinted panel a chosen radio row wears: pale
       blue on white in light, a dark blue-black in dark, so the chosen row still
       reads as chosen without turning into a white slab on a dark page. */
    --text-body:     #3a3a3a;   /* long-form body copy */
    --selected-bg:   #f7f9ff;
    --selected-line: #9aa2ac;
    --muted-warm-2:  #6b6b70;
    --muted-mid:     #6b6b6b;   /* same value as --muted */
    /* the first-responder strip: a red tint that has to lighten in dark. */
    --rsp-tint-ink:  #6d4a4a;
    --rsp-tint-b:    #7a1d1f;
    --rsp-tint-cta:  #b42318;
    --rsp-tint-cta2: #8c1a12;

    /* ---- CHROME TOKENS (Run 3) ------------------------------------------
       THE SAME VALUE IN BOTH MODES, ON PURPOSE. The nav, the drawer, the hero
       and its cards, the footer, the catalog-dark band and the login panel are
       dark in light mode too, so a themed token on any of them is a leak: the
       element moves when the page around it did not. These names exist so that
       intent is written down at the point of use — a reader seeing
       var(--chrome-link) knows the colour is pinned, where a bare literal would
       look like a value nobody had got round to converting yet.

       They are declared here and repeated verbatim in the dark block. */
    --chrome-link:    #2e5be0;
    --chrome-muted:   #6d717c;
    --chrome-muted-2: #6b7280;
    --chrome-surface: #ffffff;
    /* The cart's gift-progress bar. Themed at BOTH ends rather than one, so the
       gradient is a single decision instead of half a decision. */

    /* ---- RUN 3: the section 9 tail, one token per light literal --------
       Generated, not hand written: the light value IS the literal each rule
       used, and the dark value is derived from the light one's role and hue, so
       a tinted notice stays tinted instead of flattening to grey. */
    --ink-01:      #a9b0bc;
    --ink-02:      #1c1c1e;
    --ink-03:      #1fa863;
    --edge-01:     #efdfa8;
    --ink-04:      #96201f;
    --ink-05:      #12703f;
    --ink-06:      #3ddc97;
    --ink-07:      #7ea0ff;
    --ink-08:      #8f1d1d;
    --ink-09:      #0f6b3d;
    --ink-10:      #8b8f9a;
    --ink-11:      #8b919c;
    --ink-12:      #9aa6bc;
    --ink-13:      #d83b3b;
    --edge-02:     #e3e6ec;
    --edge-03:     #e8732a;
    --edge-04:     #eef0f4;
    --edge-05:     #f0f0f3;
    --fill-01:     #e7f6ee;
    --fill-02:     #fafbfc;
    --fill-03:     #fdecec;
    --fill-04:     #fdf0f0;
    --ink-14:      #3d4553;
    --ink-15:      #6b5200;
    --ink-16:      #6b7480;
    --ink-17:      #6f92ff;
    --ink-18:      #7a4d05;
    --ink-19:      #8fadf5;
    --ink-20:      #9a6a00;
    --ink-21:      #b42318;
    --ink-22:      #c5311f;
    --ink-23:      #ff8b8b;
    --edge-06:     #9aa2ac;
    --edge-07:     #bfe6d1;
    --edge-08:     #c9ecd7;
    --edge-09:     #ffe2b8;
    --fill-05:     #e8f7ee;
    --fill-06:     #eceef2;
    --fill-07:     #f0eafb;
    --fill-08:     #f0f2f5;
    --fill-09:     #f2fbf6;
    --fill-10:     #fafafb;
    --ink-24:      #1a7f4b;
    --ink-25:      #3a3a40;
    --ink-26:      #3d3000;
    --ink-27:      #6b6b70;
    --ink-28:      #7d3ec0;
    --ink-29:      #8a5c00;
    --ink-30:      #8d959d;
    --ink-31:      #a9c1ff;
    --ink-32:      #c0392b;
    --ink-33:      #d8443c;
    --edge-10:     #999;
    --edge-11:     #c8c8ce;
    --edge-12:     #ccd9fb;
    --edge-13:     #cfdcfa;
    --edge-14:     #cfe6d9;
    --edge-15:     #d5dae0;
    --edge-16:     #dcdcdc;
    --edge-17:     #e0e0e4;
    --edge-18:     #e4e8ec;
    --edge-19:     #e8a9a9;
    --edge-20:     #eeeef0;
    --edge-21:     #f0c4c4;
    --edge-22:     #f0d2d2;
    --edge-23:     #f2f3f6;
    --edge-24:     #f4f4f6;
    --fill-11:     #e4e4e8;
    --fill-12:     #e7f5ec;
    --fill-13:     #ededed;
    --fill-14:     #f2f2f4;
    --fill-15:     #f4f4f6;
    --fill-16:     #f4f5f8;
    --fill-17:     #f4f6f9;
    --fill-18:     #f6f6f7;
    --fill-19:     #f7f8fa;
    --fill-20:     #fafafa;
    --fill-21:     #fbfcfd;
    --fill-22:     #fdf1dc;
    --fill-23:     #fdf1f1;
    --fill-24:     #fdf3e0;
    --ink-34:      #04310f;
    --ink-35:      #116635;
    --ink-36:      #12151c;
    --ink-37:      #1a4fa8;
    --ink-38:      #54545a;
    --ink-39:      #555;
    --ink-40:      #55555f;
    --ink-41:      #7fa0f0;
    --ink-42:      #8a94a0;
    --ink-43:      #96b0ff;
    --ink-44:      #b7791f;
    --ink-45:      #b8860b;
    --edge-25:     #1a9457;
    --edge-26:     #1fa863;
    --edge-27:     #b6bdc6;
    --edge-28:     #b79ce8;
    --edge-29:     #bbb;
    --edge-30:     #c06a15;
    --edge-31:     #c3cff5;
    --edge-32:     #c8ccd2;
    --edge-33:     #d3e2fc;
    --edge-34:     #d7dbe4;
    --edge-35:     #dcdce2;
    --edge-36:     #dcdfe3;
    --edge-37:     #ddd;
    --edge-38:     #dfb100;
    --edge-39:     #e3e3e3;
    --edge-40:     #e6e2f2;
    --edge-41:     #ececed;
    --edge-42:     #eceef1;
    --edge-43:     #eef1f4;
    --edge-44:     #eef1f5;
    --edge-45:     #f0d38a;
    --edge-46:     #f0d9a8;
    --edge-47:     #f0e3bd;
    --edge-48:     #f0f0f0;
    --edge-49:     #f2f2f4;
    --edge-50:     #f2f4f7;
    --edge-51:     #f6cfe0;
    --edge-52:     #f6dcc0;
    --fill-25:     #b9c6ea;
    --fill-26:     #dbe7fd;
    --fill-27:     #e2e2ea;
    --fill-28:     #e7f6ed;
    --fill-29:     #e8eefc;
    --fill-30:     #e8effd;
    --fill-31:     #eaf1fe;
    --fill-32:     #eaf1ff;
    --fill-33:     #eceef1;
    --fill-34:     #eeeef0;
    --fill-35:     #eef0f5;
    --fill-36:     #eef2fe;
    --fill-37:     #eef3ff;
    --fill-38:     #efe6fb;
    --fill-39:     #efe9fb;
    --fill-40:     #f0f1f4;
    --fill-41:     #f1f1f3;
    --fill-42:     #f1f3f6;
    --fill-43:     #f4f5f7;
    --fill-44:     #f4f6fa;
    --fill-45:     #f4f6fb;
    --fill-46:     #f4f7ff;
    --fill-47:     #f4f8ff;
    --fill-48:     #f5f6f8;
    --fill-49:     #f6f6f8;
    --fill-50:     #f6f7f9;
    --fill-51:     #f6f9ff;
    --fill-52:     #f7f7f8;
    --fill-53:     #f7f7f9;
    --fill-54:     #f7f8fb;
    --fill-55:     #f7faf8;
    --fill-56:     #f7faff;
    --fill-57:     #faf9fd;
    --fill-58:     #fbdada;
    --fill-59:     #fbdcdc;
    --fill-60:     #fbefe2;
    --fill-61:     #fbfbfc;
    --fill-62:     #fdeaf3;
    --fill-63:     #fdf0e3;
    --fill-64:     #fdf6e3;
    --fill-65:     #fdf6e6;
    --fill-66:     #fdf6f3;
    --fill-67:     #fdf8e6;
    --fill-68:     #fdf9ec;
    --fill-69:     #ffe2b8;
    --fill-70:     #fff4d6;
    --fill-71:     #fff4e0;
    --fill-72:     #fff8ec;
    --fill-73:     #fff9ec;
    --ink-46:      #04130c;
    --ink-47:      #0f7a4d;
    --ink-48:      #12805a;
    --ink-49:      #19733f;
    --ink-50:      #19b877;
    --ink-51:      #1a1a1a;
    --ink-52:      #1b3fa8;
    --ink-53:      #1c3f9e;
    --ink-54:      #20408f;
    --ink-55:      #2044ad;
    --ink-56:      #2743c7;
    --ink-57:      #2e9e5b;
    --ink-58:      #3d2b00;
    --ink-59:      #3d4149;
    --ink-60:      #4a3900;
    --ink-61:      #4a4a55;
    --ink-62:      #4bbf7a;
    --ink-63:      #4c5462;
    --ink-64:      #4fd08a;
    --ink-65:      #555555;
    --ink-66:      #5b2fa8;
    --ink-67:      #5b3fa8;
    --ink-68:      #5b87ff;
    --ink-69:      #5c4a10;
    --ink-70:      #5d636e;
    --ink-71:      #666;
    --ink-72:      #6b6b74;
    --ink-73:      #6b727d;
    --ink-74:      #7a5a12;
    --ink-75:      #7a5b00;
    --ink-76:      #7a5c00;
    --ink-77:      #7a7a7a;
    --ink-78:      #8a4b12;
    --ink-79:      #8a6508;
    --ink-80:      #8a6d1f;
    --ink-81:      #8a8a8a;
    --ink-82:      #8aa9ff;
    --ink-83:      #8f1d55;
    --ink-84:      #9a6212;
    --ink-85:      #9aa2b1;
    --ink-86:      #9aa2b4;
    --ink-87:      #9ca3af;
    --ink-88:      #a0a0a0;
    --ink-89:      #b06a00;
    --ink-90:      #b07d00;
    --ink-91:      #b07f10;
    --ink-92:      #b4b4ba;
    --ink-93:      #b6becd;
    --ink-94:      #c0322b;
    --ink-95:      #c04747;
    --ink-96:      #c05a12;
    --ink-97:      #c22f2f;
    --ink-98:      #d9635c;
    --ink-99:      #d99b3a;
    --ink-100:     #e0a63a;
    --ink-101:     #f87171;
    --ink-102:     #fca5a5;
    --ink-103:     #ff7a7a;
    --ink-104:     #ff8a74;
    --ink-105:     #ff8f8f;
    --ink-106:     #ff9d8a;
    --pill-part: #d97a1f;
    --pill-ink-amber-a: #3d3000;

    /* ---- RUN 3: the section 9 tail, one token per light literal --------
       Generated, not hand written: the light value IS the literal each rule
       used, and the dark value is derived from the light one's role and hue, so
       a tinted notice stays tinted instead of flattening to grey. */
    --fill-214:    #fff;
    --edge-213:    #fff;
    --ink-210:     #5a6478;
    --edge-err:    #c0392b;
    --ink-err:     #c0392b;
    --fill-btnhover: #262626;
    --ink-placeholder: #cdced3;
    --fill-sale:   #e23744;
    --pill-ink-amber-b: #1c1c1e;
}

/* =========================================================================
   DARK THEME TOKENS
   =========================================================================
   The dark half of docs/mockups/dark-mode-mockup.html, verbatim: bg, surface,
   surface-2, line, text, muted, link, the four pill colours, input-bg,
   input-line and shadow. Fourteen names, and deliberately nothing else.

   THIRTEEN OF THE FOURTEEN ARE INERT. --bg, --surface, --surface-2, --line,
   --text, --link, the four pills, --input-bg, --input-line and --shadow are new
   names that no rule in this file reads, so they define themselves on a dark page
   and change nothing.

   --muted IS THE EXCEPTION, AND IT IS NOT INERT. That name already exists in
   :root above at #6b6b6b and 104 storefront rules resolve a colour through it, so
   on a dark page those rules get #a3a7ae instead. Measured, not reasoned about:
   across home, product, cart, the COA library, the forum, contact and login that
   is 30 elements, 13 of them visible text, and they are lighter grey on a still
   light background until the run that converts those surfaces lands. It is kept
   rather than renamed because the mockup names it --muted and the conversion is
   going to want that name; the cost is confined to dark mode, which is off by
   default and which nothing else has been converted for yet.

   LIGHT MODE IS UNAFFECTED EITHER WAY. None of these declarations can apply
   without the attribute, and the attribute is never printed for light: the full
   DOM and the computed style of every element on the pages checked came back
   byte-identical to the build before this block existed.

   THE SELECTOR IS WHAT KEEPS THE ADMIN OUT. It matches only a root element
   carrying data-theme="dark", and only views/partials/head.php,
   views/calculator.php and views/unsubscribe.php ever print that attribute.
   Every admin page opens with views/partials/admin_head.php, which prints a bare
   <html lang="en"> and includes none of those three. No admin template includes
   head.php: grep says zero, and it is checked in the report by loading admin
   pages with the cookie set and reading the attribute off the rendered document.
   The cookie alone can do nothing; the attribute is the only switch.

   THERE IS NO LIGHT COUNTERPART HERE ON PURPOSE. Light is the absence of the
   attribute, so these names simply do not resolve in light mode, and a rule that
   used one before its light value existed would break light rather than fail
   quietly. The light values arrive in the run that starts consuming them. */
:root[data-theme="dark"] {
    /* page and surfaces */
    --bg:            #101113;
    --surface:       #191a1d;
    --surface-2:     #222327;
    --surface-3:     #222327;

    /* THE PLATES DO NOT MOVE. Same value as light, restated here so a reader of
       this block can see that it is a decision and not an omission. */
    --plate:         #ffffff;
    --plate-a:       #f7f7f8;
    --plate-b:       #eeeeef;
    --plate-c:       #f8f8f9;
    --plate-d:       #efeff1;

    /* One hairline, one strong border. ADJUSTED FROM THE MOCKUP: its #2c2e33
       measured 1.28:1 on --surface, under the 1.5:1 floor; #383b42 is 1.55:1
       there and 1.68:1 on --bg. */
    --line:          #383b42;
    --line-soft:     #383b42;
    --line-cool:     #383b42;
    --line-strong:   #646a75;

    /* One text colour, one muted. */
    --text:          #ececee;
    --text-1:        #ececee;
    --text-2:        #ececee;
    --muted:         #a3a7ae;
    --muted-cool:    #a3a7ae;
    --muted-slate:   #a3a7ae;
    --muted-steel:   #a3a7ae;
    --muted-warm:    #a3a7ae;
    --muted-grey:    #a3a7ae;
    --muted-blue:    #a3a7ae;
    --muted-ash:     #a3a7ae;

    --link:          #8ea2ff;
    --link-hover:    #b3c0ff;

    /* ADJUSTED FROM THE MOCKUP: its --input-line #3a3d44 measured 1.68:1 against
       its --input-bg #141518, under the 3:1 floor for a control boundary.
       #646a75 is 3.36:1 there. */
    --input-bg:      #141518;
    --input-line:    #646a75;
    --input-text:    #ececee;
    --placeholder:   #8f96a1;

    /* ---- The pre-existing tokens that were already doing a role job -------
       These six are not new names. They were carrying the page background, the
       hairline, the strong border, the body ink and the two secondary greys long
       before this run, and 269 storefront usages already read them. Giving them
       a dark value converts all 269 without touching a single rule, and light
       cannot move because their :root values are untouched. --white and --black
       are deliberately NOT here: --white is worn by text on the permanently dark
       nav and footer, and --black is the focus ring's light value, which
       --focus-ring now carries separately. */
    --off-white:     #101113;   /* body and row hovers */
    --border:        #383b42;
    --border-2:      #646a75;
    --ink:           #ececee;
    --muted-2:       #a3a7ae;
    --muted-3:       #8f96a1;

    --focus-ring:    #ffffff;
    --scrim:         rgba(0, 0, 0, 0.72);
    --shadow:        0 3px 10px rgba(0, 0, 0, 0.55);
    --shadow-soft:   rgba(0, 0, 0, .5);

    /* PILLS INVERT THEIR LABEL rather than darkening their fill. White on the
       mockup's dark pills measured 3.58 / 2.87 / 4.29 / 4.02:1, all under 4.5,
       and darkening the fill to fix that would have pushed the fill under the
       3:1 it needs against --surface. Near-black on a lighter fill clears both
       comfortably, and it is the answer this site already shipped once: the cart
       pill is #0a0a0a on the brand orange at 8.05:1. */
    --pill-ink:      #0a0a0a;
    --pill-green:    #3fbf6b;
    --pill-green-2:  #3fbf6b;
    --btn-green-hover: #36ab5f;
    --pill-amber:    #e0a020;
    --pill-amber-bg: #3a2f12;
    --pill-amber-ink:#f0c674;
    --pill-red:      #ef6a62;
    --pill-red-bg:   #3a1f1f;
    --pill-grey:     #949aa4;

    /* second pass */
    --text-3:        #ececee;
    --text-link:     #ececee;
    --muted-plum:    #a3a7ae;
    --muted-slate-2: #a3a7ae;
    --line-warm:     #383b42;
    --line-pale:     #383b42;
    --surface-4:     #222327;
    --tint-blue:     #1c2440;
    --tint-amber:    #3a2f12;
    --tint-amber-2:  #241d0e;
    --tint-amber-ln: #5a4a1c;
    --btn-ink:       #0a0a0a;
    --pill-bubble:   #ffffff;
    --text-body:     #d6d8dc;
    --selected-bg:   #172033;
    --selected-line: #5c636e;
    --muted-warm-2:  #a3a7ae;
    --muted-mid:     #a3a7ae;
    /* ADJUSTED: the light strip's #6d4a4a / #7a1d1f / #b42318 measured 2.36,
       1.75 and 2.76:1 on the darkened red tint. These are the same hues lifted
       until each clears 4.5:1 there. */
    --rsp-tint-ink:  #e8b0b0;
    --rsp-tint-b:    #ff9d9d;
    --rsp-tint-cta:  #ff8a7a;
    --rsp-tint-cta2: #ffb0a4;

    /* ---- CHROME TOKENS: identical to light, and that is the point ---------- */
    --chrome-link:    #2e5be0;
    --chrome-muted:   #6d717c;
    --chrome-muted-2: #6b7280;
    --chrome-surface: #ffffff;
    /* The one chrome-adjacent token that DOES move, because the gift bar sits on
       --surface and has to stay visible on it. Lifted from #2e5be0, which is
       3.24:1 on --surface, to 5.31:1. */

    /* ---- RUN 3 ----------------------------------------------------------- */
    --ink-01:      #9fb5db;
    --ink-02:      #ececee;
    --ink-03:      #8feabc;
    --edge-01:     #685927;
    --ink-04:      #e89291;
    --ink-05:      #8dedbb;
    --ink-06:      #8febc3;
    --ink-07:      #8ba5ee;
    --ink-08:      #e99191;
    --ink-09:      #8beebd;
    --ink-10:      #a3a7ae;
    --ink-11:      #a3a7ae;
    --ink-12:      #9fb4db;
    --ink-13:      #e99090;
    --edge-02:     #364159;
    --edge-03:     #684027;
    --edge-04:     #364159;
    --edge-05:     #363659;
    --fill-01:     #112e1f;
    --fill-02:     #162029;
    --fill-03:     #310e0e;
    --fill-04:     #310e0e;
    --ink-14:      #9fb5db;
    --ink-15:      #eed78b;
    --ink-16:      #ececee;
    --ink-17:      #8ba3ee;
    --ink-18:      #eec88b;
    --ink-19:      #8ba8ee;
    --ink-20:      #eecf8b;
    --ink-21:      #ee928b;
    --ink-22:      #ed978c;
    --ink-23:      #ee8b8b;
    --edge-06:     #383b42;
    --edge-07:     #286745;
    --edge-08:     #276841;
    --edge-09:     #684d27;
    --fill-05:     #102f1d;
    --fill-06:     #161d29;
    --fill-07:     #1b0e31;
    --fill-08:     #161e29;
    --fill-09:     #0f311e;
    --fill-10:     #161629;
    --ink-24:      #91e8bb;
    --ink-25:      #ececee;
    --ink-26:      #eed98b;
    --ink-27:      #ececee;
    --ink-28:      #bc9bdf;
    --ink-29:      #eecd8b;
    --ink-30:      #a3a7ae;
    --ink-31:      #8ba7ee;
    --ink-32:      #e79b93;
    --ink-33:      #e99590;
    --edge-10:     #383b42;
    --edge-11:     #383b42;
    --edge-12:     #273968;
    --edge-13:     #273b68;
    --edge-14:     #315e44;
    --edge-15:     #364659;
    --edge-16:     #383b42;
    --edge-17:     #383b42;
    --edge-18:     #364759;
    --edge-19:     #682727;
    --edge-20:     #383b42;
    --edge-21:     #682727;
    --edge-22:     #682727;
    --edge-23:     #363e59;
    --edge-24:     #383b42;
    --fill-11:     #222327;
    --fill-12:     #132d1c;
    --fill-13:     #222327;
    --fill-14:     #191a1d;
    --fill-15:     #191a1d;
    --fill-16:     #161b29;
    --fill-17:     #161e29;
    --fill-18:     #191a1d;
    --fill-19:     #161d29;
    --fill-20:     #191a1d;
    --fill-21:     #15202b;
    --fill-22:     #31250e;
    --fill-23:     #310e0e;
    --fill-24:     #31250e;
    --ink-34:      #8beea3;
    --ink-35:      #8decb5;
    --ink-36:      #9fb1db;
    --ink-37:      #8cb0ed;
    --ink-38:      #ececee;
    --ink-39:      #ececee;
    --ink-40:      #ececee;
    --ink-41:      #8ba8ee;
    --ink-42:      #9fbadb;
    --ink-43:      #8ba4ee;
    --ink-44:      #ecc58e;
    --ink-45:      #eed28b;
    --edge-25:     #276847;
    --edge-26:     #276847;
    --edge-27:     #364559;
    --edge-28:     #3e2768;
    --edge-29:     #383b42;
    --edge-30:     #684727;
    --edge-31:     #273768;
    --edge-32:     #383b42;
    --edge-33:     #273f68;
    --edge-34:     #364159;
    --edge-35:     #383b42;
    --edge-36:     #364559;
    --edge-37:     #383b42;
    --edge-38:     #685a27;
    --edge-39:     #383b42;
    --edge-40:     #3a2c63;
    --edge-41:     #383b42;
    --edge-42:     #364459;
    --edge-43:     #364759;
    --edge-44:     #35455a;
    --edge-45:     #685527;
    --edge-46:     #685327;
    --edge-47:     #685727;
    --edge-48:     #383b42;
    --edge-49:     #383b42;
    --edge-50:     #364459;
    --edge-51:     #682743;
    --edge-52:     #684927;
    --fill-25:     #0f1831;
    --fill-26:     #0e1b31;
    --fill-27:     #161629;
    --fill-28:     #112e1d;
    --fill-29:     #0e1931;
    --fill-30:     #0e1a31;
    --fill-31:     #0e1b31;
    --fill-32:     #0e1a31;
    --fill-33:     #161e29;
    --fill-34:     #191a1d;
    --fill-35:     #161c29;
    --fill-36:     #0e1731;
    --fill-37:     #0e1931;
    --fill-38:     #1d0e31;
    --fill-39:     #1a0e31;
    --fill-40:     #161b29;
    --fill-41:     #191a1d;
    --fill-42:     #161e29;
    --fill-43:     #161d29;
    --fill-44:     #141c2c;
    --fill-45:     #111a2f;
    --fill-46:     #0e1831;
    --fill-47:     #0e1b31;
    --fill-48:     #161d29;
    --fill-49:     #161629;
    --fill-50:     #161d29;
    --fill-51:     #0e1a31;
    --fill-52:     #191a1d;
    --fill-53:     #161629;
    --fill-54:     #151b2a;
    --fill-55:     #16291d;
    --fill-56:     #0e1b31;
    --fill-57:     #181030;
    --fill-58:     #310e0e;
    --fill-59:     #310e0e;
    --fill-60:     #31210e;
    --fill-61:     #161629;
    --fill-62:     #310e1f;
    --fill-63:     #31200e;
    --fill-64:     #31280e;
    --fill-65:     #31270e;
    --fill-66:     #31190e;
    --fill-67:     #312a0e;
    --fill-68:     #31290e;
    --fill-69:     #31230e;
    --fill-70:     #31280e;
    --fill-71:     #31250e;
    --fill-72:     #31240e;
    --fill-73:     #31260e;
    --ink-46:      #91e8c0;
    --ink-47:      #8beec5;
    --ink-48:      #8beecc;
    --ink-49:      #92e7b6;
    --ink-50:      #8beec6;
    --ink-51:      #ececee;
    --ink-52:      #8da5ed;
    --ink-53:      #8ea7eb;
    --ink-54:      #93abe7;
    --ink-55:      #8fa6ea;
    --ink-56:      #90a0e9;
    --ink-57:      #98e1b6;
    --ink-58:      #eed18b;
    --ink-59:      #ececee;
    --ink-60:      #eed88b;
    --ink-61:      #ececee;
    --ink-62:      #9ddcb7;
    --ink-63:      #9fb5db;
    --ink-64:      #96e3b9;
    --ink-65:      #ececee;
    --ink-66:      #b397e2;
    --ink-67:      #af9fdb;
    --ink-68:      #8ba6ee;
    --ink-69:      #ebd58e;
    --ink-70:      #ececee;
    --ink-71:      #ececee;
    --ink-72:      #ececee;
    --ink-73:      #ececee;
    --ink-74:      #eed08b;
    --ink-75:      #eed58b;
    --ink-76:      #eed68b;
    --ink-77:      #ececee;
    --ink-78:      #eeba8b;
    --ink-79:      #eed28b;
    --ink-80:      #e7d093;
    --ink-81:      #a3a7ae;
    --ink-82:      #8ba5ee;
    --ink-83:      #e991bc;
    --ink-84:      #eec58b;
    --ink-85:      #9fb4db;
    --ink-86:      #9fb1db;
    --ink-87:      #9fb5db;
    --ink-88:      #a3a7ae;
    --ink-89:      #eec78b;
    --ink-90:      #eed28b;
    --ink-91:      #eed08b;
    --ink-92:      #a3a7ae;
    --ink-93:      #9fb4db;
    --ink-94:      #e79793;
    --ink-95:      #dd9c9c;
    --ink-96:      #eeb48b;
    --ink-97:      #e59494;
    --ink-98:      #e69893;
    --ink-99:      #eac790;
    --ink-100:     #edcb8c;
    --ink-101:     #ee8b8b;
    --ink-102:     #ee8b8b;
    --ink-103:     #ee8b8b;
    --ink-104:     #ee9b8b;
    --ink-105:     #ee8b8b;
    --ink-106:     #ee9b8b;
    --pill-part: #e8963f;
    --pill-ink-amber-a: #0a0a0a;

    /* ---- RUN 3 ----------------------------------------------------------- */
    --fill-214:    #191a1d;
    --edge-213:    #383b42;
    --ink-210:     #9fb3db;
    --edge-err:    #e88a80;
    --ink-err:     #ff9d92;
    --fill-btnhover: #3a3a3a;
    --ink-placeholder: #6a6e77;
    --fill-sale:   #f4746f;
    --pill-ink-amber-b: #0a0a0a;
}
@media (max-width: 900px) { :root { --gutter: 32px; } }

@media (max-width: 640px) { :root { --gutter: 18px; } }

@media (max-width: 400px) { :root { --gutter: 12px; } }


/* ---- Reset ------------------------------------------------------------- */

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

/* The hidden attribute must always win over author display rules. Without this a
   rule like `.iname-chip { display: inline-block }` re-shows a hidden element (the
   chip + dashed add button were rendering stacked because of exactly that). */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }  /* reduced-motion resets this to auto */

body {
    margin: 0;
    font-family: var(--font-ui);
    font-size: 15px;
    line-height: 1.55;
    color: var(--ink);
    background: var(--off-white);
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    /* Sticky footer: full-height flex column so the footer is pushed to the
       viewport bottom on short pages and flows below content on long ones. */
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}
/* The primary content region grows to fill the space above the footer, site-wide
   (storefront <main>, admin/login shells already set their own min-height:100vh). */
body > main,
body > .admin-shell {
    flex: 1 0 auto;
}

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

a { color: var(--text-link); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -0.01em; line-height: 1.1; }

button { font-family: inherit; cursor: pointer; }

input, textarea, select, button { font-size: 15px; }

.mono { font-family: var(--font-mono); font-variant-ligatures: none; }

/* ---- Shared bits ------------------------------------------------------- */

.eyebrow {
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
    margin: 0 0 12px;
}
.eyebrow-invert { color: var(--muted-3); }

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    background: var(--black);
    color: var(--white);
    border: 1px solid var(--black);
    padding: 13px 22px;
    font-weight: 500;
    letter-spacing: 0.01em;
    line-height: 1;
    transition: background .15s ease, color .15s ease;
}
.btn:hover { background: var(--fill-btnhover); text-decoration: none; }
.btn-block { width: 100%; }
.btn-outline {
    background: transparent;
    color: var(--black);
}
.btn-outline:hover { background: var(--black); color: var(--white); }

.alert {
    padding: 12px 16px;
    border: 1px solid var(--border-2);
    margin: 0 0 20px;
    font-size: 14px;
}
.alert-error { border-color: var(--black); background: var(--surface); }
.alert-success { border-color: var(--black); background: var(--surface); }

.hint { color: var(--muted-2); font-weight: 400; }

/* =========================================================================
   FORMS
   ========================================================================= */

.field { margin-bottom: 18px; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }

.field label {
    display: block;
    font-size: 13px;
    font-weight: 500;
    margin-bottom: 7px;
    color: var(--ink);
}

.field input,
.field textarea,
.contact-form input,
.contact-form textarea {
    width: 100%;
    /* THE INPUT TOKENS, not the surface ones. A field has to read as a well cut
       into the panel rather than as another panel, which in dark means it is
       darker than the card it sits on, not the same colour. */
    background: var(--input-bg);
    border: 1px solid var(--input-line);
    padding: 12px 14px;
    color: var(--input-text);
    transition: border-color .15s ease;
}
.field input:focus,
.field textarea:focus {
    outline: none;
    border-color: var(--black);
}
textarea { resize: vertical; font-family: inherit; }

/* Error text reads as an error. The dark login panel overrides this further down. */
.field-error { color: var(--ink-32); font-size: 12.5px; margin: 6px 0 0; font-weight: 500; }
.field-error::before { content: '— '; }

/* ---- Validation feedback ------------------------------------------------
   Applied by form-validate.js on a blocked submit AND server-side on a
   rejected post, so both look identical to the customer. */
.field input.is-invalid,
.field select.is-invalid,
.field textarea.is-invalid,
input.is-invalid,
select.is-invalid,
textarea.is-invalid {
    border: 1px solid var(--edge-err);
    background: rgba(192, 57, 43, .04);
}
.field input.is-invalid:focus,
input.is-invalid:focus,
select.is-invalid:focus,
textarea.is-invalid:focus { outline: none; border-color: var(--edge-err); }

/* Group highlight, for a control the customer sees as one block rather than as an
   input: the shipping-method radios. The banner must always have something to
   point at, and a bare radio has no border to colour. */
.is-invalid-group {
    border: 1px solid var(--edge-err);
    background: rgba(192, 57, 43, .04);
    border-radius: 8px;
    padding: 8px;
}

/* Honeypot wrapper (src/FormGuard.php). Off screen, not display:none.
   A bot worth catching parses the CSS, and display:none or visibility:hidden is
   the first thing it checks before deciding whether to fill a field. Position
   plus clip leaves the input rendered and focusable-in-principle, so it reads as
   a live field to anything that is not actually looking at the page. It never
   reaches a person: aria-hidden keeps it out of the accessibility tree and
   tabindex="-1" keeps it out of the tab order, both set on the markup.
   No single-side borders, nothing painted at all. */
.fg-hp {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}


/* Add-sale customer picker. Admin only. Mirrors the order page's picker, so the
   two read as the same control; no single-side borders anywhere in it. */


.form-error-summary {
    border: 1px solid var(--edge-err);
    background: rgba(192, 57, 43, .06);
    color: var(--ink-32);
    border-radius: 8px;
    padding: 10px 14px;
    margin: 0 0 16px;
    font-size: 13.5px;
    font-weight: 500;
}

/* Inline "required" marker inside a label, so the requirement is visible before a
   failed submit rather than only in the error underneath. */
.field-required {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--muted);
    margin-left: 6px;
}

/* "Did you mean …?" under an email field. A warning, never a blocker, so it is
   deliberately quieter than .field-error and carries no error colour. */
.email-hint { font-size: 12.5px; margin: 6px 0 0; color: var(--ink-45); line-height: 1.45; }
.email-hint[hidden] { display: none; }
.email-hint-fix { color: var(--ink-45); font-weight: 600; text-decoration: underline; cursor: pointer; }
.email-hint-fix:hover { color: var(--ink-79); }

.fineprint { font-size: 12.5px; color: var(--muted); margin: 14px 0 0; }

/* Spam-folder notice. Shared by the confirmation page and the pay page, both of
   which render it through spam_note_html() so the address inside is emphasised.

   Informational blue, not warning yellow and not error red: nothing has gone wrong
   yet, and colouring it like a problem would make every successful checkout look
   like one. Solid background, no transparency, per the house rule. Type is 12.5px
   to sit on the same step as .small and .fineprint around it. */
.spam-note {
    font-size: 12.5px;
    line-height: 1.6;
    color: var(--ink-54);
    background: var(--fill-46);
    border: 1px solid var(--edge-12);
    border-radius: 10px;
    padding: 11px 14px;
    margin: 10px 0 0;
}
/* The address itself. Wraps rather than overflowing: a long address on a 360px
   screen must not push the panel wider than the column. */
.spam-note .mono {
    font-weight: 650;
    word-break: break-word;
    overflow-wrap: anywhere;
}

/* Disclaimer checkboxes */
.disclaimers {
    border: 1px solid var(--border);
    padding: 16px 16px 6px;
    margin: 8px 0 22px;
}
.disclaimers legend {
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--muted);
    padding: 0 6px;
}
.check {
    display: flex;
    gap: 11px;
    align-items: flex-start;
    font-size: 13px;
    line-height: 1.45;
    margin-bottom: 14px;
    color: var(--ink);
    cursor: pointer;
}
.check input {
    margin: 2px 0 0;
    width: 16px;
    height: 16px;
    accent-color: var(--black);
    flex: 0 0 auto;
}
.req {
    display: inline-block;
    font-family: var(--font-mono);
    font-style: normal;
    font-size: 10px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--muted-2);
    margin-left: 6px;
}

/* =========================================================================
   LOGIN — split screen (image visual + dark auth card), #0d0d0f
   ========================================================================= */

.login-body { background: var(--lg-bg); }

.login-split {
    position: relative;
    display: grid;
    grid-template-columns: 1.05fr 0.95fr;
    min-height: 100vh;
}

/* Soft blue seam blending the two panels (above panels, below content). */
.login-seam {
    position: absolute;
    top: 0; bottom: 0;
    left: 52.5%;
    width: 360px;
    transform: translateX(-50%);
    background: linear-gradient(90deg,
        rgba(46, 91, 224, 0) 0%,
        rgba(46, 91, 224, 0.11) 45%,
        rgba(46, 91, 224, 0.06) 62%,
        rgba(0, 0, 0, 0) 100%);
    filter: blur(34px);
    pointer-events: none;
    z-index: 2;
}

/* ---- LEFT: full-bleed visual ---------------------------------------------- */
.login-visual {
    position: relative;
    background-color: #0d0d0f;   /* fallback when no image is set */
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    color: #fff;
    display: flex;
    overflow: hidden;
    padding: 54px 60px 48px;
}
.login-visual-overlay {
    position: absolute;
    inset: 0;
    z-index: 1;
    background: linear-gradient(180deg,
        rgba(13, 13, 15, 0.55) 0%,
        rgba(13, 13, 15, 0.40) 42%,
        rgba(13, 13, 15, 0.82) 100%);
}

/* Drifting blue glows (no grid) */
.login-glow {
    position: absolute;
    z-index: 1;
    border-radius: 50%;
    pointer-events: none;
    filter: blur(90px);
}
.login-glow-1 {
    width: 620px; height: 620px;
    top: -190px; left: -110px;
    background: radial-gradient(circle, rgba(46, 91, 224, 0.50), transparent 68%);
    animation: login-glow-a 18s ease-in-out infinite alternate;
}
.login-glow-2 {
    width: 520px; height: 520px;
    bottom: -180px; right: -90px;
    background: radial-gradient(circle, rgba(46, 91, 224, 0.34), transparent 70%);
    animation: login-glow-b 24s ease-in-out infinite alternate;
}
.login-visual.has-bg .login-glow { opacity: 0.5; }   /* don't muddy the image */
@keyframes login-glow-a {
    from { transform: translate(0, 0); }
    to   { transform: translate(80px, 55px); }
}
@keyframes login-glow-b {
    from { transform: translate(0, 0); }
    to   { transform: translate(-85px, -40px); }
}

.login-visual-inner {
    position: relative;
    z-index: 3;
    width: 100%;
    display: flex;
    flex-direction: column;
}
.login-logo { align-self: center; display: inline-block; }
.login-visual .login-logo-img { height: 120px; width: auto; }
.login-visual .logo-wordmark { color: #fff; font-size: 30px; }

/* Three equal 80%-width glass boxes, centered between logo and bottom block */
.login-features {
    width: 80%;
    align-self: center;
    margin: auto 0;
    display: flex;
    flex-direction: column;
    gap: 12px;
}
/* Apple "liquid glass": translucent gradient fill (light catches the top edge),
   a glowing brand-blue border, and a faint top inner highlight for the glossy edge. */
.login-feature {
    background: linear-gradient(135deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.03));
    border: 1px solid rgba(46, 91, 224, 0.50);
    border-radius: 18px;
    padding: 15px 22px;
    -webkit-backdrop-filter: blur(20px) saturate(160%);
    backdrop-filter: blur(20px) saturate(160%);
    box-shadow:
        0 0 20px rgba(46, 91, 224, 0.25),
        0 0 0 1px rgba(46, 91, 224, 0.40),
        inset 0 1px 0 rgba(255, 255, 255, 0.15);
    transition: box-shadow .2s ease, border-color .2s ease;
}
/* Subtle hover: intensify the glow. */
.login-feature:hover {
    border-color: rgba(46, 91, 224, 0.65);
    box-shadow:
        0 0 28px rgba(46, 91, 224, 0.38),
        0 0 0 1px rgba(46, 91, 224, 0.55),
        inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
/* Fallback where backdrop-filter is unsupported: a semi-opaque dark fill so the
   boxes never turn into unreadable light gradients on the dark panel. */
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
    .login-feature { background: rgba(16, 20, 38, 0.88); }
}
.login-feature-title { margin: 0; font-size: 19px; font-weight: 600; color: #fff; }
.login-feature-text { margin: 5px 0 0; font-size: 15px; line-height: 1.45; color: rgba(255, 255, 255, 0.68); }

/* Bottom block: same 80% width + centering, anchored to bottom */
.login-visual-foot { width: 80%; align-self: center; margin-top: auto; }
.login-bottom-label {
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.42);
    margin: 0;
}
.login-chips { display: flex; flex-wrap: wrap; gap: 10px; margin: 14px 0 18px; }
.login-chip {
    font-size: 14px;
    color: rgba(255, 255, 255, 0.72);
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: 999px;
    padding: 7px 17px;
}
.login-social { margin: 0; font-size: 15.5px; color: rgba(255, 255, 255, 0.55); }
.login-social-stat { color: #fff; font-weight: 700; }

/* ---- RIGHT: auth card ----------------------------------------------------- */
.login-auth {
    position: relative;
    background: #0d0d0f;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 54px 60px;
}
.login-auth::before {   /* subtle wash toward the seam */
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background: linear-gradient(90deg, rgba(255, 255, 255, 0.022), rgba(255, 255, 255, 0) 60%);
}
.auth-card { position: relative; z-index: 3; width: 100%; max-width: 470px; }

.tabs {
    display: flex;
    background: var(--lg-tabs-bg);
    border: 1px solid var(--lg-tabs-line);
    border-radius: 13px;
    padding: 5px;
    margin-bottom: 28px;
}
.tab {
    flex: 1;
    background: none;
    border: none;
    border-radius: 9px;
    padding: 12px 0;
    font-size: 16px;
    font-weight: 600;
    color: var(--lg-tab-ink);
    transition: background .15s ease, color .15s ease, box-shadow .15s ease;
}
.tab.is-active {
    background: var(--accent-blue);
    color: #fff;
    box-shadow: 0 5px 18px rgba(46, 91, 224, 0.32);
}
.tab:not(.is-active):hover { color: var(--lg-tab-ink-h); }
.auth-form.is-hidden { display: none; }

/* Fields on the dark card */
.login-auth .field label { font-size: 14.5px; font-weight: 500; color: rgba(255, 255, 255, 0.80); margin-bottom: 9px; }
.login-auth .field label .hint { color: rgba(255, 255, 255, 0.40); font-weight: 400; }
.input-icon { position: relative; }
.input-icon-svg {
    position: absolute;
    left: 16px; top: 50%;
    transform: translateY(-50%);
    width: 18px; height: 18px;
    color: var(--lg-hint);
    pointer-events: none;
}
.login-auth .input-icon input {
    width: 100%;
    background: rgba(255, 255, 255, 0.035);
    border: 1px solid rgba(255, 255, 255, 0.11);
    border-radius: 12px;
    padding: 14px 46px 14px 46px;
    color: #fff;
    transition: border-color .15s ease, background .15s ease;
}
.login-auth .input-icon input::placeholder { color: rgba(255, 255, 255, 0.30); }
.login-auth .input-icon input:focus {
    outline: none;
    border-color: rgba(46, 91, 224, 0.70);
    background: rgba(255, 255, 255, 0.06);
}
.pw-toggle {
    position: absolute;
    right: 10px; top: 50%;
    transform: translateY(-50%);
    background: none;
    border: none;
    padding: 6px;
    color: var(--lg-hint);
    display: inline-flex;
}
.pw-toggle:hover { color: var(--lg-label); }
.pw-toggle svg { width: 18px; height: 18px; }
.login-auth .field-error { color: #f0a58f; }

/* Primary button — blue, white text (no cream) */
.btn-auth {
    display: block;
    width: 100%;
    background: var(--accent-blue);
    color: #fff;
    border: none;
    border-radius: 12px;
    padding: 16px;
    font-size: 17px;
    font-weight: 600;
    cursor: pointer;
    box-shadow: 0 7px 24px rgba(46, 91, 224, 0.30);
    transition: background .15s ease, transform .05s ease;
}
.btn-auth:hover { background: #264fc4; }
.btn-auth:active { transform: translateY(1px); }

/* Agreements box */
.login-auth .disclaimers {
    border: 1px solid rgba(255, 255, 255, 0.09);
    border-radius: 13px;
    padding: 18px 18px 16px;
    margin: 6px 0 22px;
    background: rgba(255, 255, 255, 0.018);
}
.login-auth .disclaimers legend {
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.40);
    margin-bottom: 4px;
    padding: 0;
}
.login-auth .check {
    gap: 11px;
    font-size: 14px;
    color: rgba(255, 255, 255, 0.68);
    line-height: 1.45;
    margin-bottom: 12px;
}
.login-auth .check:last-of-type { margin-bottom: 0; }
.login-auth .check input {
    width: 18px; height: 18px;
    border-radius: 5px;
    border: 1px solid rgba(255, 255, 255, 0.28);
    accent-color: var(--accent-blue);
}
.login-auth .req {
    font-family: var(--font-mono);
    font-size: 10px;
    color: rgba(255, 255, 255, 0.34);
    text-transform: uppercase;
}

.auth-fineprint {
    margin: 16px 0 0;
    font-size: 13.5px;
    line-height: 1.55;
    color: var(--lg-fine);
    text-align: center;
}
.auth-fineprint a { color: var(--lg-fine-a); text-decoration: underline; text-underline-offset: 2px; }
.auth-fineprint a:hover { color: var(--lg-fine-a-h); }

.auth-divider { height: 1px; background: var(--lg-rule); margin: 24px 0 18px; }

.trust-row { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px; }
.trust { display: flex; align-items: center; gap: 8px; font-size: 13px; color: rgba(255, 255, 255, 0.50); }
.trust svg { width: 16px; height: 16px; color: rgba(255, 255, 255, 0.45); flex: 0 0 auto; }

.auth-closing { text-align: center; font-size: 13.5px; color: rgba(255, 255, 255, 0.34); margin: 17px 0 0; }

/* Alerts on the dark card */
.login-auth .alert { border-color: rgba(255, 255, 255, 0.22); background: rgba(255, 255, 255, 0.05); color: #fff; }

/* Freeze glows for reduced motion (static glow still shows) */
@media (prefers-reduced-motion: reduce) {
    .login-glow { animation: none; }
}


/* =========================================================================
   TOP BAR (gated)
   ========================================================================= */

/* The <header> band. It stays a dark fill on the light storefront pages (contact,
   cart, checkout, account, FAQ, COA library) so the white-on-glass bar inside it
   stays readable; on the dark catalog it goes transparent and the bar genuinely
   floats over the page background. No backdrop-filter anywhere in here. */
.topbar {
    position: sticky;
    top: 0;
    z-index: 50;
    /* THE CONTAINER DOES NOT PAINT. The bar is a floating pill and this element
       is only the space around it, so a painted container is a full-width band
       across the top of the page with the pill sitting on it -- black across a
       light page, which is exactly the band this removes. Behind the pill the
       page's own background runs to the top.

       It was var(--surface-dark), and that value never actually reached a page:
       .catalog-dark .topbar already overrode it to transparent for the dark
       catalog, and body:not(.catalog-dark) .topbar did the same for every other
       page. Both branches said transparent, so the declaration only ever showed
       up when one of those overrides was removed. Said once, here.

       At <=767px the bar is a full-width strip rather than a pill, and the strip
       carries the colour itself -- see .topbar-mobile. */
    background: transparent;
    color: var(--white);
}

/* THE NAV'S OWN HEIGHT, PUBLISHED ONCE. Two pages (the QR pay page and the order
   pay page) fill exactly the screen below the bar, and hard-coding 86 and 76 in
   each of them is how one of them ends up stale the next time the bar changes.
   Desktop is .topbar-inner's 14px padding twice plus .navbar's 69px (option B of
   docs/mockups/nav-height-mockup.html took the bar 58 to 80, the logo 30 to 48,
   the cart pill and avatar 44 to 48; the Sept 12 nav-shop pass brought the bar
   to 73 with the logo, pill and avatar still 48; the Sept 13 pass took the bar
   to 69 and the logo to 45, pill and avatar still 48, nothing else resized); the
   mobile value is .topbar-mobile's own height, and both are set right where those
   rules are, below. .topbar is sticky, so it occupies flow space and subtracting it is
   correct. An announcement bar sits ABOVE .topbar and is not counted here: it is
   optional, its height is not fixed, and a page that scrolls by the height of an
   announcement is what every other storefront page already does. */
:root { --nav-h: 97px; }
/* THE DRAWER CONDITION, stated once here and repeated verbatim at .topbar-mobile:
   under 1200px, or any touch-primary device (pointer: coarse, the split the admin
   uses for its 44px tap floor), or a device with no pointer at all. The height
   token has to change under exactly the same condition as the bar, or a page
   would reserve the wrong amount of space in one of the two modes. */
@media (max-width: 1199px), (pointer: coarse), (pointer: none) { :root { --nav-h: 76px; } }

.topbar-inner {
    /* The bar sits INSIDE the page padding rather than edge to edge. --gutter alone
       is not enough: the grid is also capped at --content-max, so the bar has to be
       capped identically or it runs wider on a display over ~1560px. */
    max-width: calc(var(--content-max) + 2 * var(--gutter));
    margin: 0 auto;
    padding: 14px var(--gutter);
}

/* ---- Floating glass bar ------------------------------------------------ */
/* Two states. At rest the bar is barely there; once the page has scrolled it
   fills in so content passing underneath cannot read through the logo and links.
   Only background-color and border-color transition — the blur radius is a fixed
   value that switches with the class, never animated, because animating a filter
   forces a re-rasterize on every frame (the exact cost that got the old overlay
   blurs deleted). The 0.72 alpha fill carries the bar on its own if a device
   drops the filter. */
.navbar {
    position: relative;
    display: flex;
    align-items: center;
    gap: 22px;
    height: 69px;
    padding: 0 30px;
    background-color: rgba(255, 255, 255, 0.03);
    border: 1px solid rgba(255, 255, 255, 0.07);
    border-radius: 14px;
    transition: background-color 0.25s ease, border-color 0.25s ease;
}
.topbar.is-scrolled .navbar {
    background-color: rgba(16, 18, 22, 0.72);
    border-color: rgba(255, 255, 255, 0.12);
    -webkit-backdrop-filter: blur(16px) saturate(1.3);
    backdrop-filter: blur(16px) saturate(1.3);
}

/* THE PILL CARRIES THE DARK ON A LIGHT PAGE. .navbar's unscrolled fill is a 3%
   white wash, which is a treatment designed to sit ON a dark page: over a light
   one it is effectively invisible and takes the bar's white text down with it.
   Now that the container no longer paints a band, the pill has to be the dark
   thing itself, which is what "darken the bar, not the page" actually means.

   Keyed on the page theme, and only the fill and the border: the dark catalog
   keeps its wash, because there the pill is meant to blend into the page behind
   it rather than sit on top of it. Nothing here touches the page surface. */
body:not(.catalog-dark) .navbar { background-color: var(--surface-dark); }

/* AND THE SAME ANSWER FOR THE LIGHT FORUM (run 4, rescoped in run 5). A forum
   page carries catalog-dark so the bar floats, and the page under it is normally
   the forum's near-black gradient, which is what the 3% white wash is designed
   to sit on. Under an explicit light choice that ground is #fafafa, and a 3%
   white wash on #fafafa is nothing at all with white type on it: measured
   1.03:1 before the bar scrolls and picks up its own dark fill.

   So the pill carries the dark itself there, which is exactly what every other
   light storefront page already does one rule up -- the same declaration, the
   same value, reached by a different route. It is the ONE place the bar is not
   byte-identical across the three states, it is deliberate, and it is named in
   the report rather than left for the chrome check to find.

   RUN 5 NARROWED THE SELECTOR from :root:not([data-theme="dark"]) to the
   explicit light attribute. The old form also matched a page with NO attribute,
   which is now the dark original: it would have put an opaque #1c1c1c pill on
   the forum every visitor sees. */
:root[data-theme="light"] body.f-body .navbar {
    background-color: var(--surface-dark);
    border-color: rgba(255, 255, 255, 0.12);
}

/* The light topbar variant that used to live here is GONE. The dark bar is
   now the only bar: every page that renders partials/topbar.php pins
   catalog-dark on its body, so `body:not(.catalog-dark)` matched nothing and
   these rules were unreachable. Verified by removing them and re-running the
   nav tree-walk against home: 40 comparisons, zero non-active differences,
   both auth states at 1440 and 390. Only the BAR was ever darkened; nothing
   here painted the page beneath it, and that is unchanged. */

.nav-brand { display: flex; align-items: center; flex: 0 0 auto; }
.nav-brand:hover { text-decoration: none; }
.nav-logo { height: 45px; width: auto; display: block; }
.nav-brand .logo-wordmark { color: #fff; font-size: 16px; font-weight: 700; }

.nav-links { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; }

/* Shared pill treatment: nav items, the search icon and the avatar all resting
   transparent and lighting up blue on hover/focus. */
.nav-item,
.nav-icon-btn,
.nav-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 34px;
    border: 1px solid transparent;
    background: transparent;
    color: rgba(255, 255, 255, 0.55);
    cursor: pointer;
    transition: all 0.18s ease;
}
.nav-item {
    padding: 0 13px;
    border-radius: 9px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    text-decoration: none;
    white-space: nowrap;
    /* The space before the icon added below. The rule was already inline-flex and
       already 34px tall, so an icon child changes neither the item's height nor
       the bar's. Every other declaration in this block is untouched: colour,
       weight and letter spacing are the label's and the mockup does not move
       them. */
    gap: 7px;
}

/* ---- Nav item icons ------------------------------------------------------
   The approved set, bar 4 of docs/mockups/nav-icons-mockup.html, drawn 17px on a
   24 grid at stroke 1.8. Orange on every item until Sept 18, 2026; now the
   label's colour (white) on every item, see the rule.

   NO TRANSITION IS DECLARED HERE ON PURPOSE. .nav-item already carries
   transition: all 0.18s ease, which is what animates the anchor the icon sits
   inside, so the icon moves with its label for free. The icon's own colour does
   not change on hover in this design (only the active item is white), so there
   is nothing here for a second transition to animate.

   stroke-width in CSS beats the stroke-width="2" attribute $tbIco() writes, so
   the other callers of that helper keep their 2 and only this class gets 1.8. */
.nav-item-ico {
    width: 17px;
    height: 17px;
    flex: 0 0 auto;
    stroke-width: 1.8;
    /* WHITE, THE LABEL'S OWN COLOUR (docs/reports/2026-09-18-cart-nav-fixes.md).
       Orange until then. currentColor is the label's colour in every state: the
       55% white the item rests at, the full white of hover, focus and the active
       item, so the icon and its word can never be two colours. Orange stays on
       the logo and everywhere else it was. */
    color: currentColor;
}
/* .nav-drop-top declares its own 5px gap for the chevron and wins on source
   order, so the 2px here is what brings the Shop icon to the same 7px from its
   label as the other six. The chevron keeps the 5px it had. */
.nav-drop-top .nav-item-ico { margin-right: 2px; }
.nav-item.is-active .nav-item-ico { color: #ffffff; }

/* THE ICONS COST WIDTH AND THE BAR RUNS OUT OF IT BELOW 1024.
   Measured, not guessed: with the icons showing, a signed-in bar (the longest
   item set, seven items) pushes the page into horizontal scroll at every sampled
   width from 768 to 1020 (105px of overflow at 768, falling 1px per pixel of
   viewport). With them hidden the same sweep is 0 at every width from 760 to
   1120, so the icons are the whole cause. 1024 is the narrowest width at which
   they fit and keep fitting at everything wider.

   The hamburger breakpoint is NOT moved: the bar still collapses at 768 exactly
   as it did, and between 768 and the ceiling below it is simply the bar as it
   looks today. .nav-links scopes this to the desktop bar, so the drawer, which is
   only ever open below 768, keeps its seven icons. display:none also takes the
   icon out of the flex row, so the 7px gap above collapses with it and the bar
   returns to precisely its pre-change width.

   THE CEILING WAS 1023 AND IT WAS TOO LOW. Re-measured on the SIGNED-IN bar, which
   is the longest item set and the one the original sweep did not use, the page
   overflows horizontally in TWO bands with the icons showing:

       1024 to about 1110   45px of document overflow at 1024, 35 at 1050,
                            0 by 1100.  .nav-actions is the element past the edge.
       1190 to about 1218   28px at 1190, 18 at 1200, 8 at 1210, 0 by 1220.

   The second band is why the ceiling is 1280 rather than 1120: 1160 and 1180 are
   clean, so a ceiling chosen from the first band alone would have looked right and
   left the bar broken across a 30px window that a 1200px laptop sits exactly in.

   THE ICONS ARE THE WHOLE CAUSE, measured rather than assumed: with
   .nav-item-ico forced to display:none, the same signed-in sweep is 0 at every
   width from 768 to 1440, both bands included. 1280 is the narrowest width at
   which they fit and keep fitting at everything wider.

   RE-MEASURED FOR THE 80px BAR (option B, nav-height mockup). The logo is 48px
   tall now, which is 208.7px wide against the old 130.4, and the bar's own
   padding went 14 to 30 a side: 110px more bar for the same seven items. Signed
   in with the icons showing the bar needed 1257px at 1280 and had 1160, and the
   shortfall falls 1px per pixel of viewport, so the icons come back at 1460
   rather than 1280: 1380 is where they first fit with no badge, and 1460 leaves
   51px of slack with a two-digit Messages badge on the bar (the badge measures
   32px with its gap), which is the case the earlier ceiling never measured. Measured in scratch/ap-navfit.mjs, both engines, both auth
   states, before this number was written. */
@media (max-width: 1459px) {
    .nav-links .nav-item-ico { display: none; }
}

.nav-item:hover,
.nav-item:focus-visible,
.nav-icon-btn:hover,
.nav-icon-btn:focus-visible,
.nav-avatar:hover,
.nav-avatar:focus-visible,
.nav-avatar[aria-expanded="true"] {
    color: #ffffff;
    background: rgba(46, 91, 224, 0.14);
    border-color: rgba(46, 91, 224, 0.35);
    box-shadow: 0 0 12px rgba(46, 91, 224, 0.28);
    text-decoration: none;
}
.nav-item.is-active,
.nav-item.is-active:hover {
    color: #ffffff;
    background: rgba(46, 91, 224, 0.2);
    border-color: rgba(46, 91, 224, 0.5);
}

/* ---- Shop dropdown, the category tier -----------------------------------
   HIDDEN WITH CSS, NEVER BUILT BY SCRIPT. Every item is a real <a href> that
   ships in the server HTML; this only decides when it is visible. A menu
   injected on hover is a menu a crawler never sees, and linking the ten
   category pages from every page is the entire reason this exists.

   Opens on hover AND on focus-within, so it works from the keyboard with no
   JavaScript at all. visibility rather than display so the transition runs and
   so the items stay focusable in source order. */
.nav-drop { position: relative; }
.nav-drop-top { display: inline-flex; align-items: center; gap: 5px; }
.nav-drop-chev {
    width: 0; height: 0; margin-top: 1px;
    border-left: 4px solid transparent; border-right: 4px solid transparent;
    border-top: 4px solid currentColor; opacity: .7;
}
/* Colours, radii and type sizes below are the approved shop mockup's, so the
   dropdown and the shop page's class panel read as one component in two places.
   overflow:hidden is what lets the footer row sit flush in the rounded corner
   without a second radius on it. */
.nav-drop-menu {
    position: absolute; top: calc(100% + 8px); left: 0; z-index: 60;
    /* 620px. At about 460 the two columns and the certificates block read
       cramped, and the block's body wrapped to three lines. */
    width: 620px; max-width: calc(100vw - 32px);
    background: #12151c; border: 0.5px solid rgba(255, 255, 255, 0.09);
    border-radius: 12px; overflow: hidden;
    box-shadow: 0 22px 48px rgba(0, 0, 0, 0.45);
    opacity: 0; visibility: hidden; transform: translateY(-4px);
    transition: opacity .14s ease, transform .14s ease, visibility .14s;
}
.nav-drop:hover .nav-drop-menu,
.nav-drop:focus-within .nav-drop-menu {
    opacity: 1; visibility: visible; transform: translateY(0);
}
.nav-drop-cols {
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    position: relative; padding: 24px 28px 22px;
}
/* Each column is inset 28px from the centre line, so the gutter is 56px with the
   divider down the middle of it. */
.nav-drop-col { min-width: 0; }
.nav-drop-col:first-child { padding-right: 28px; }
.nav-drop-col + .nav-drop-col { padding-left: 28px; }
/* The rule between the columns is an INSET POSITIONED HAIRLINE on the container,
   not a border on either column: a border paints one side of a box, which the
   house rules forbid. left:50% centres it in the gutter; top/bottom inset so it
   stops short of the panel padding rather than running into the rounded corners. */
.nav-drop-cols::before {
    content: "";
    position: absolute; left: 50%; top: 26px; bottom: 24px;
    width: 1px; background: rgba(255, 255, 255, 0.08);
}

/* The certificates block. Fills the space under Supplies in the right column. */
/* Icon and heading on one row, body underneath at full width. */
.nav-drop-coa {
    display: block;
    margin-top: 24px; padding: 16px 17px;
    border-radius: 10px; text-decoration: none;
    background: #171b24;
}
.nav-drop-coa:hover, .nav-drop-coa:focus-visible { background: #1c212c; }
.nav-drop-coa-h { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; }
.nav-drop-coa-h svg { width: 17px; height: 17px; color: var(--chrome-link); flex: 0 0 auto; }
.nav-drop-coa-t { font-size: 13px; font-weight: 600; color: #fff; }
.nav-drop-coa-b { display: block; font-size: 12px; line-height: 1.55; color: var(--ink-10); }
.nav-drop-head {
    display: block; margin-bottom: 15px;
    font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
    color: var(--chrome-muted);
}
/* Second and later group headings in a column need air above them, or Supplies
   reads as the last row of Compounds. */
.nav-drop-head ~ .nav-drop-head { margin-top: 24px; }
.nav-drop-item {
    display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
    padding: 10px 13px; border-radius: 7px; text-decoration: none;
}
.nav-drop-label {
    font-size: 13px; color: #c9ccd3;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.nav-drop-count { font-size: 12px; color: var(--chrome-muted); flex: 0 0 auto; }
.nav-drop-item:hover, .nav-drop-item:focus-visible { background: rgba(255, 255, 255, 0.05); }
.nav-drop-item:hover .nav-drop-label,
.nav-drop-item:focus-visible .nav-drop-label { color: #fff; }
.nav-drop-item.is-active { background: rgba(255, 255, 255, 0.055); }
.nav-drop-item.is-active .nav-drop-label { color: #fff; }
/* The catch-all row, full width on its own darker ground so it reads as the way
   out of the menu rather than as a sibling of Bacteriostatic Water above it. */
.nav-drop-foot {
    position: relative;
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 15px 28px; text-decoration: none;
    background: #0f1219;
}
.nav-drop-foot::before {
    content: "";
    position: absolute; left: 0; right: 0; top: 0;
    height: 1px; background: rgba(255, 255, 255, 0.06);
}
.nav-drop-foot .nav-drop-label { font-size: 13px; font-weight: 600; color: var(--chrome-link); }
.nav-drop-foot:hover { background: #131720; }
/* The desktop nav starts at 768 (below that the hamburger takes over), so 768 is
   the tightest width this panel ever renders at. 620px starting from the Shop
   link's x position runs past the right edge there and clips the footer count, so
   it steps down until there is room for the full width. Later in the file than
   the base .nav-drop-menu rule, which is what makes it win: a media query adds no
   specificity of its own. */
@media (max-width: 1023px) {
    .nav-drop-menu { width: 552px; }
    .nav-drop-cols { padding: 20px 22px 18px; }
    .nav-drop-col:first-child { padding-right: 20px; }
    .nav-drop-col + .nav-drop-col { padding-left: 20px; }
    .nav-drop-foot { padding: 14px 22px; }
}

@media (prefers-reduced-motion: reduce) {
    .nav-drop-menu { transition: none; }
}

.nav-actions { display: flex; align-items: center; gap: 9px; flex: 0 0 auto; margin-left: auto; }
.nav-icon-btn { width: 34px; padding: 0; border-radius: 9px; }
.nav-icon-btn svg { width: 18px; height: 18px; display: block; }

/* ---- Account avatar + dropdown ----------------------------------------- */
.nav-account { position: relative; flex: 0 0 auto; }
.nav-avatar {
    width: 34px;
    padding: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.07);
    font-family: var(--font-ui);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.03em;
    line-height: 1;
}
.nav-avatar-ico { width: 18px; height: 18px; display: block; }

.nav-menu {
    position: absolute;
    top: calc(100% + 11px);
    right: 0;
    z-index: 60;
    min-width: 210px;
    padding: 6px;
    background: #11141c;
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 12px;
    box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
}
.nav-menu-item {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 40px;
    padding: 0 10px;
    border-radius: 8px;
    font-size: 13px;
    color: rgba(255, 255, 255, 0.72);
    text-decoration: none;
    transition: all 0.18s ease;
}
.nav-menu-item:hover,
.nav-menu-item:focus-visible { background: rgba(46, 91, 224, 0.14); color: #fff; text-decoration: none; }
.nav-menu-item .mnav-ico { width: 17px; height: 17px; flex: 0 0 auto; color: rgba(255, 255, 255, 0.45); }
.nav-menu-item:hover .mnav-ico { color: var(--ink-19); }
.nav-menu-sep { display: block; height: 1px; margin: 6px 4px; background: rgba(255, 255, 255, 0.09); }

/* ---- Search panel (revealed by the icon) ------------------------------- */
.nav-search {
    position: absolute;
    top: calc(100% + 10px);
    left: 0;
    right: 0;
    z-index: 55;
    display: flex;
    align-items: center;
    gap: 10px;
    height: 52px;
    padding: 0 8px 0 14px;
    background: #11141c;
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 12px;
    box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
}
.nav-search-ico { display: inline-flex; flex: 0 0 auto; color: rgba(255, 255, 255, 0.45); }
.nav-search-ico svg { width: 18px; height: 18px; display: block; }
.nav-search input {
    flex: 1 1 auto;
    min-width: 0;
    background: transparent;
    border: none;
    color: #fff;
    font-family: var(--font-ui);
    font-size: 14.5px;
}
.nav-search input::placeholder { color: rgba(255, 255, 255, 0.38); }
.nav-search input:focus { outline: none; }
.nav-search-close {
    flex: 0 0 auto;
    width: 36px; height: 36px;
    padding: 0;
    border: none;
    border-radius: 8px;
    background: transparent;
    color: rgba(255, 255, 255, 0.5);
    display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
    transition: all 0.18s ease;
}
.nav-search-close:hover { background: rgba(255, 255, 255, 0.07); color: #fff; }
.nav-search-close svg { width: 17px; height: 17px; display: block; }
/* The clear control (2026-09-18): the close button's shape, a size down, shown
   by app.js on /shop only while the box holds text. The browser's own cancel
   glyph is dropped only once script has taken over (data-live), so a field
   without script keeps exactly what it had. */
.nav-search-clear {
    flex: 0 0 auto;
    width: 30px; height: 30px;
    padding: 0;
    border: none;
    border-radius: 8px;
    background: transparent;
    color: rgba(255, 255, 255, 0.5);
    display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
    transition: all 0.18s ease;
}
.nav-search-clear[hidden] { display: none; }
.nav-search-clear:hover { background: rgba(255, 255, 255, 0.07); color: #fff; }
.nav-search-clear svg { width: 16px; height: 16px; display: block; }
.nav-search[data-live] input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

/* Solid blue pill: cart icon + inset white count badge. Matches the mobile
   .tm-cart colour (#2E5BE0) so both cart pills stay in sync.

   REVERTED FROM ORANGE, back to exactly the rules that were here before. The
   orange pill was asked for and then unasked for; the nav ITEM icons keep their
   orange, which is a different rule (.nav-item-ico) on a different element and is
   deliberately untouched by this revert. */
.order-pill {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    height: 36px;
    background: var(--accent-blue);
    color: #ffffff;
    border: none;
    border-radius: 24px;
    padding: 0 11px 0 13px;
    font-weight: 500;
    cursor: pointer;
    box-shadow: inset 0 1px 0 rgba(255,255,255,0.22);
    transition: background .2s ease;
}
.order-pill:hover { text-decoration: none; background: #2450c4; }  /* slightly darker on hover */
/* NO :focus-visible RULE HERE, which is the state this was in before the orange.
   The ring comes from the global :focus-visible and measures the same white 2px
   at 2px offset either way; the local rule existed only to survive a colour
   change that has been undone, so it goes with it. */
.order-pill-ico { display: inline-flex; flex: 0 0 auto; color: #fff; }
.order-pill-ico svg { width: 17px; height: 17px; display: block; }
.order-pill-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-mono);
    font-size: 12px;
    font-weight: 600;
    line-height: 1;
    min-width: 21px;
    height: 21px;
    padding: 0 6px;
    background: var(--pill-bubble);
    color: var(--accent-blue);
    border-radius: 20px;
}

/* =========================================================================
   HERO — dark, two columns, drifting blue glows (no grid)
   ========================================================================= */

/* The section itself is full-bleed: it is a direct flex child of <body>, so its
   width is the viewport with no 100vw (and therefore no scrollbar-width) trick.
   That is what lets the background layers reach both edges at every width. The
   cap and gutter moved to .hero-inner, so the CONTENT still lines up with the
   nav, filter pills and product grid exactly as before. */
.hero {
    position: relative;
    overflow: hidden;              /* contains the layers; no horizontal scroll */
    background: #0d0d0f;
    display: flex;
    flex-direction: column;
    /* THE CONTENT SETS THE HEIGHT. This used to be min-height:100svh with the
       content centred inside it, which was right when the right column held a
       tall photograph: the section was nearly full either way. The carousel is
       420px and the copy tightened to a 34px headline, so on a wide screen the
       content came to 569px inside a 900px box and the centring split the 331px
       of leftover evenly: 165px of nothing between the nav and the badge, and
       255px of it at 1920. The gap was the leftover, not the spacing.
       The nav-to-badge distance is .hero-col's own top padding, which is what it
       was always for, and the bottom padding below closes the section. Nothing
       here is viewport-relative any more, so no screen height can reintroduce it.
       The background layers are all percentages of this box (the blooms 105% and
       95% of its height, the dot field 100% + one cell, the vignette inset 0), so
       they follow it down rather than being clipped by it. */
    padding-bottom: clamp(20px, 2.2vw, 40px);
}
/* Same two-part container as the nav bar and the product grid: gutter AND cap. */
.hero-inner {
    position: relative;
    z-index: 2;
    width: 100%;
    display: grid;
    /* The carousel column is the wider one: the cards need the room more than
       the text does, and the copy sets its own measure through .hero-h1 and
       .hero-subhead rather than filling whatever it is given. minmax(0, …) on
       both so a wide card can never push a track past its share. */
    grid-template-columns: minmax(0, 0.78fr) minmax(0, 1.22fr);
    align-items: center;
    /* 24px, matching the prototype. The old 48 was set when the right column held
       one photograph that could absorb any width; the carousel cannot, and every
       pixel of gap comes straight off the track the cards have to fit inside. */
    gap: 24px;
    max-width: calc(var(--content-max) + 2 * var(--gutter));
    margin: 0 auto;
    padding: 0 var(--gutter);
}
.hero.no-media .hero-inner { grid-template-columns: 1fr; }   /* no image -> left column takes the space */

/* Hero "Drift" background. Replaces the two blur(95px) glow blobs: the softness
   here comes from the gradient's own falloff, so there is no filter in the
   section at all and the only animated property is transform. The blooms are
   composited layers being translated and scaled — no per-frame blur pass, which
   is what made the old treatment (and the page glows before it) expensive.

   --hero-bloom is the glow multiplier and --hero-drift-speed divides every
   duration, both per the handoff's tunables. */
.hero {
    /* Colour comes from the existing :root --hero-glow triplet (brand blue
       #2E5BE0); only the two handoff tunables are local. */
    --hero-bloom: 0.45;           /* bloom opacity multiplier, 0-1 */
    --hero-drift-speed: 1;        /* divides every drift duration */
}
.hero-drift {
    position: absolute;
    z-index: 0;
    pointer-events: none;
}
.hero-bloom-a,
.hero-bloom-b {
    border-radius: 50%;
    will-change: transform;
}
/* The gradient is already transparent well inside each element's own box, so the
   negative insets only position the light — nothing is clipped at the section
   edge even where .hero stops short of a very wide viewport. */
.hero-bloom-a {
    left: -12%; top: -22%;
    width: 68%; height: 105%;
    background: radial-gradient(closest-side, rgba(var(--hero-glow), 0.34), transparent 72%);
    opacity: calc(var(--hero-bloom) * 0.9);
    animation: hero-drift-a calc(20s / var(--hero-drift-speed)) ease-in-out infinite;
}
.hero-bloom-b {
    right: -16%; bottom: -30%;
    width: 62%; height: 95%;
    background: radial-gradient(closest-side, rgba(var(--hero-glow), 0.22), transparent 70%);
    opacity: calc(var(--hero-bloom) * 0.7);
    animation: hero-drift-b calc(27s / var(--hero-drift-speed)) ease-in-out infinite;
}
/* Fine dot field, masked so it only reads behind the headline. Oversized by one
   grid cell in each axis and drifting exactly one cell, so the loop is seamless:
   at 100% the pattern has shifted onto where its neighbour started. Transform is
   the only thing animated — the layer never fades and carries no filter. */
.hero-dots {
    top: 0;
    left: 0;
    width: calc(100% + 30px);
    height: calc(100% + 30px);
    background-image: radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.13) 1px, transparent 0);
    background-size: 30px 30px;
    -webkit-mask-image: radial-gradient(80% 70% at 22% 42%, #000 0%, transparent 78%);
    mask-image: radial-gradient(80% 70% at 22% 42%, #000 0%, transparent 78%);
    animation: hero-dot-drift calc(18s / var(--hero-drift-speed)) linear infinite;
    will-change: transform;
}
@keyframes hero-dot-drift {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(30px, 30px, 0); }
}
.hero-vignette {
    inset: 0;
    background: radial-gradient(130% 110% at 50% 50%, transparent 55%, rgba(6, 7, 12, 0.7) 100%);
}
@keyframes hero-drift-a {
    0%, 100% { transform: translate3d(-9%, -6%, 0) scale(0.94); }
    50%      { transform: translate3d(13%, 9%, 0) scale(1.26); }
}
@keyframes hero-drift-b {
    0%, 100% { transform: translate3d(11%, 7%, 0) scale(1.18); }
    50%      { transform: translate3d(-12%, -8%, 0) scale(0.86); }
}

/* Bottom padding is much smaller than the top on purpose. The stat row is the last
   thing in the hero, and 72px under it plus the catalog's own 48px left a ~120px
   band of nothing between the stats and the filter pills. */
/* Top padding is the nav-to-badge gap. Clamped rather than restated per
   breakpoint so it scales continuously and the hero adds no media queries of its
   own: 40px at 1440 down to a 20px floor on the smallest phones. */
.hero-col {
    position: relative; z-index: 2;
    padding: clamp(20px, 1.95vw + 12px, 40px) 0 16px;
}
.hero-media { position: relative; z-index: 2; display: flex; align-items: center; justify-content: center; }
/* Faint blue pool of light so the vials emerge from the dark instead of sitting on flat
   black. Sits behind the image (z-index 0); the product paints on top (z-index 1). */
.hero-media::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 88%;
    height: 78%;
    background: radial-gradient(ellipse at center, rgba(46, 91, 224, 0.16), transparent 68%);
    filter: blur(26px);
    z-index: 0;
    pointer-events: none;
}
/* Grounded soft shadow + subtle blue ambient glow so the transparent-PNG cutout edges
   read as lit and settled, not hard-pasted. Product itself stays sharp (drop-shadow only
   affects the alpha silhouette, not the pixels). */
.hero-media img {
    position: relative;
    z-index: 1;
    max-width: 100%;
    height: auto;
    object-fit: contain;
    /* The vials are scaled up 25% with a transform, not by growing the element.
       The source PNG carries 13.6%/12.3% transparent margin left/right and
       25.5%/11.3% top/bottom, so 1.25x is still inside the 1.35x at which the
       artwork would touch the box edge — the vials get bigger, nothing spills.
       A transform also leaves the layout box at its old size, so the media column
       cannot widen and the hero cannot grow taller at any width.

       The max-height caps the layout box on short viewports (a 1440x720 laptop
       window), where 1.25x of a full-width box would be taller than the hero and
       the vials would be clipped top and bottom by .hero's overflow: hidden.
       svh second, vh first, same pairing .hero uses. */
    max-height: 76vh;
    max-height: 76svh;
    transform: scale(1.25);
    /* The scaled transform box overhangs the column by ~78px per side and lands
       over the right edge of the text column. The image is decorative and is not
       wrapped in a link, so it must not intercept clicks in that strip. */
    pointer-events: none;
    filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.55)) drop-shadow(0 0 60px rgba(46, 91, 224, 0.15));
}

/* =========================================================================
   FEATURED CAROUSEL: A 3D LOOPING TRACK
   docs/mockups/hero-carousel-in-context.html, matched value for value.

   THE CARDS SIT ON A CIRCLE, not on a line. Each card's distance from the
   centre, in card-steps, is the only input: position on the ring is
   sin/cos of that distance times the radius, and everything else (the lift,
   the Y rotation, the scale, the opacity, the saturation, the stacking
   order) is a function of it too. That is why there are no per-position
   rules here any more and no width-stepped ladder of translateX values
   below: one formula covers every card at every offset at every width, and
   app.js and home.php both evaluate it from the same constants.

   TWO BOXES, AND THE OUTER ONE IS NOT THE ONE THAT CLIPS. .cf-stage is the
   layout box: it takes the column's width and sets the height, so the grid
   row is the size it always was. .cf-view is the 3D box, and it is WIDER
   THAN THE COLUMN, centred on it and clipping. A card at the far edge of
   the ring reaches about 375px from the centre at desktop geometry, which
   is past the edge of the column it nominally lives in; if the column were
   the clip, that card would be sliced down its side in mid-air. Pushing
   the clip out to 820px puts the cut well outside anything a visitor can
   see, and .hero's own overflow:hidden (it is full-bleed) is what stops
   the wide box from lengthening the page.

   WITH JAVASCRIPT OFF THE FAN IS STILL THERE. home.php writes each card's
   ring values into custom properties and the two rules below build the
   transform from them, one set per breakpoint, so the first paint is the
   arrangement rather than five cards stacked dead centre waiting to be
   corrected. app.js then writes transform and opacity directly, and an
   inline declaration beats a stylesheet one, so the handover is silent.
   ========================================================================= */
.hero-featured { position: relative; z-index: 2; width: 100%; }

.cf-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    margin-bottom: 2px;
}
.cf-head span {
    font-size: 11px;
    color: rgba(255, 255, 255, 0.42);
    letter-spacing: 0.11em;
    text-transform: uppercase;
}
.cf-head a { font-size: 12px; color: var(--chrome-link); }

/* THE TRUST SEALS, docs/mockups/hero-badges-mockup-v5.html: four inline SVGs
   in one row directly above the stage, 84px each with the mockup's 14px gap
   and 18px below. IN FLOW, ABOVE THE STAGE, ON PURPOSE. The stage keeps its
   450/400px and the track its --cf-fit ladder, so nothing about the ring's
   horizontal room changes; the column only grows by the row's height, and
   the hero's grid recentres the copy against it.
   ONE ROW AT EVERY WIDTH. The four shrink together rather than wrap: each is
   a flex item with an 84px basis that may shrink to 56px, so the row holds
   four on one line down to a 266px column (4 x 56 + 3 x 14), which is
   narrower than any phone's. Measured: 84px from 402 up, 81px at 390 (366 - 42
   = 324 for four), 73.5px at 360, 63.5px at 320. The arc text is 12.5 units of a 140
   viewBox, 7.5px at 84 and 5.7px at 63.5; the report shows both. */
.hero-badges {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 14px;
    margin: 0 0 18px;
    min-width: 0;
}
.hero-badges > span {
    display: block;
    flex: 0 1 84px;
    min-width: 56px;
    max-width: 84px;
    line-height: 0;
}
.hero-badges svg {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1;
}

/* The layout box. Height from the mockup; the width is the column's. */
.cf-stage { position: relative; height: 450px; }

/* The 3D box: wider than the column, centred on it, and the only thing here
   that clips. perspective and its origin are the mockup's.
   VERTICAL PANNING IS THE BROWSER'S, HORIZONTAL IS OURS. Without pan-y the
   browser claims the whole gesture one move into a horizontal drag: it sends
   a single pointermove and then pointercancel, so a drag can never reach its
   threshold. Nothing here calls preventDefault on a scroll. */
.cf-field {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    /* MEASURED, NOT ESTIMATED. Driven through a full revolution, the widest a
       still-visible card reaches is 423.5px from the centre (at 50 per cent
       opacity, a third of the way between two cards); 820px put the cut 13.5px
       inside that and sliced the card down its side. 860 clears the card at
       both edges; the shadow needs another 60 each side, because a card's
       0 26px 60px shadow reaches that far past its own rect and a cut shadow
       is a vertical line down the hero just the same. Measured as a column of
       pixels: at 860 the edge column differed from its neighbour by 1.6 and
       1.9 of 255 while the next column over differed by 0.1. The same
       measurement at the tighter geometry is 300.3px plus the shadow. */
    width: 980px;
    margin-left: -490px;
    overflow: hidden;
    touch-action: pan-y;
}
/* THE PERSPECTIVE SITS HERE, ON THE CARDS' OWN PARENT, and that placement is
   load-bearing rather than tidy. The mockup puts it on the outer box and gives
   this one transform-style:preserve-3d so the depth reaches through; Chromium
   then hit-tests the cards against their flattened geometry and every pointer
   event inside the rail lands on this element instead of on the card under the
   cursor. The centre card stopped being a link and a side card could not be
   clicked to the front. Bisected property by property with real clicks: turning
   preserve-3d off here was the one change that let the click through. With the
   perspective on the cards' direct parent there is no intermediate 3D context
   to reach through, so none is needed, and the projection is identical: every
   card is its own plane and nothing here intersects. */
.cf-track {
    position: absolute;
    inset: 0;
    perspective: 1400px;
    perspective-origin: 50% 46%;
    /* THE FIT, and why a ring this size needs one. The mockup's wide geometry
       reaches 424px from the centre, and the hero keeps two columns from 901px
       up: the rail's share of that row only offers 424px of clear space to the
       left of it at about 1520 and narrower every step below. The mockup hides
       this by clipping the stage at the column, which is exactly the slicing
       this rebuild exists to remove; leaving it unclipped instead puts a card
       over the headline at 1024. So the whole rail is scaled down to the room
       it actually has. The geometry is untouched, both breakpoints are the
       mockup's, and only the rendered size moves. Every number below is the
       measured requirement at the NARROW end of its band.
       Below 901 the hero stacks, the rail has the screen to itself and no
       scaling applies.

       THE LADDER IS ALSO MONOTONIC, which is why the last tight band is 0.99
       and not 1. Each value is the room measured at the NARROW end of its
       band, rounded down; the rendered card width that results only ever goes
       up as the window widens:

           901   tight 206 x 0.84 = 173.0
           1000  tight 206 x 0.94 = 193.6
           1100  tight 206 x 0.99 = 203.9
           1200  wide  252 x 0.81 = 204.1
           1320  wide  252 x 0.90 = 226.8
           1440  wide  252 x 0.98 = 246.9
           1560  wide  252 x 1.00 = 252.0

       At the geometry change the tight ring is already unscaled from 1100 up,
       so a full-size tight card (206.0) would have been a tenth of a pixel
       WIDER than the first wide card (205.9). One per cent off the last tight
       band closes that and costs two pixels. */
    transform: scale(var(--cf-fit, 1));
}
@media (min-width:  901px) and (max-width:  999.98px) { .cf-stage { --cf-fit: 0.84; } }

@media (min-width: 1000px) and (max-width: 1099.98px) { .cf-stage { --cf-fit: 0.94; } }

@media (min-width: 1100px) and (max-width: 1199.98px) { .cf-stage { --cf-fit: 0.99; } }

@media (min-width: 1200px) and (max-width: 1319.98px) { .cf-stage { --cf-fit: 0.81; } }

@media (min-width: 1320px) and (max-width: 1439.98px) { .cf-stage { --cf-fit: 0.90; } }

@media (min-width: 1440px) and (max-width: 1559.98px) { .cf-stage { --cf-fit: 0.98; } }


.cf-card {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 252px;
    height: 350px;
    margin: -175px 0 0 -126px;
    display: flex;
    flex-direction: column;
    background: var(--cf-card-bg, #1c1c1c);
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: 20px;
    padding: 20px 18px;
    color: #fff;
    text-align: center;
    text-decoration: none;
    overflow: hidden;
    cursor: pointer;
    box-shadow: 0 26px 60px rgba(10, 16, 32, 0.38);
    will-change: transform, opacity;
    /* No transition. The loop writes a new transform every frame, so a
       transition would be a second animation fighting the first; the easing
       that matters is the one in app.js, which eases the POSITION rather
       than the pixels. */
    transform:
        translate3d(var(--cf-x, 0px), var(--cf-y, 0px), var(--cf-z, 0px))
        rotateY(var(--cf-ry, 0deg))
        scale(var(--cf-s, 1));
    opacity: var(--cf-o, 1);
    z-index: var(--cf-zi, 1);
    filter: saturate(var(--cf-sat, 1));
}
.cf-card:hover { text-decoration: none; }
.cf-card:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 4px; }

/* The Lot tested chip, on the same condition it has always carried: rendered
   only where the product has a certificate. Where there is none, home.php
   emits the mockup's invisible spacer in its place, so every card is the same
   height and the ring does not step up and down as it turns. */
.cf-tested {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    align-self: center;
    background: rgba(31, 122, 62, 0.22);
    color: #69d693;
    border-radius: 999px;
    padding: 5px 12px;
    font-size: 11.5px;
    font-weight: 700;
    white-space: nowrap;
    margin-bottom: 12px;
}
.cf-tested svg { width: 11px; height: 11px; color: currentColor; }
.cf-tested.is-spacer { visibility: hidden; }

/* The product photograph, in the slot the mockup gives its drawn vial.
   FLEXIBLE ON PURPOSE, and this is what guarantees the phone card. The text
   below it is fixed height; this absorbs whatever is left, so a name that
   wraps to two lines takes its second line out of the image rather than out
   of the bottom of the card. No length of product name can overflow. */
.cf-top {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    margin-bottom: 10px;
}
/* ABSOLUTE, AND THAT IS THE WHOLE POINT. The photograph is an 800px square and
   the panel's height comes from flex, which leaves it indefinite as far as
   percentage resolution is concerned: max-height:100% against it computes to
   none, the image draws at its intrinsic size and paints straight over the
   name, the price and the button. inset:0 gives the image a definite box to
   measure itself against whatever the flex row decides, so object-fit can do
   its job and nothing can overflow the slot. */
.cf-top picture { display: contents; }
.cf-top img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    /* THE VIAL FILLS ITS PANEL, which is what the previous carousel used a 1.26
       scale on a fixed-height panel to get. The export is a square with the vial
       about 76 per cent of it, so contain alone leaves a quarter of the panel
       empty above and below the glass. 1.15 is the most this panel can take: it
       puts the vial at the mockup's proportion and still keeps the whole of it
       inside the panel, and .cf-top clips so that even a differently cropped
       export can only ever lose transparent margin, never reach the name. */
    transform: scale(1.15);
}
.cf-top { overflow: hidden; }
.cf-noimg { position: absolute; inset: 0; display: grid; place-items: center; color: #5b647d; }
.cf-noimg svg { width: 80px; height: 80px; }

/* The stock badge keeps the corner it has always had. */
.cf-stock {
    position: absolute;
    top: 11px;
    right: 11px;
    display: inline-flex;
    align-items: center;
    background: #1c1c1c;
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    padding: 5px 10px;
    border-radius: 999px;
    z-index: 3;
}
.cf-stock.is-backorder { background: var(--pill-red); color: var(--pill-ink); }

.cf-body { flex: 0 0 auto; }
.cf-name {
    font-size: 18px;
    font-weight: 600;
    color: #fff;
    margin-bottom: 3px;
    line-height: 1.2;
}
.cf-size { font-size: 13.5px; color: #9a9ea6; }
.cf-foot { display: flex; flex-direction: column; align-items: center; gap: 0; }
.cf-price { font-family: var(--font-mono); font-size: 21px; margin: 8px 0 12px; color: #fff; }
.cf-view {
    display: inline-block;
    background: var(--accent-blue);
    color: #fff;
    border-radius: 10px;
    padding: 9px 24px;
    font-weight: 700;
    font-size: 14.5px;
}

.cf-dots { display: flex; align-items: center; justify-content: center; gap: 7px; margin-top: 4px; }
.cf-dot {
    width: 7px;
    height: 7px;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: #c8cdd8;
    cursor: pointer;
    transition: width .3s ease, background-color .3s ease;
}
.cf-dot[aria-current="true"] { width: 26px; background: var(--accent-blue); }
.cf-dot:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 3px; }

/* ---- Under 1200: the mockup's tighter geometry --------------------------
   R 300 instead of 430 and 50 degrees between cards instead of 42, with the
   smaller card and the shorter perspective that go with them. The ring
   reaches about 254px from the centre here, so the clip moves in to match.

   1199.98px, NOT 999.98. The mockup's breakpoint is 1000; see the note in
   app.js for why this one is 1200. The .98 is so the stylesheet and the
   script agree at 1200 exactly: the mockup's own CSS says max-width:1000px
   while its script says innerWidth < 1000, so at exactly 1000 it lays out the small
   card and then places it on the wide ring. The two agree here instead. */
@media (max-width: 1199.98px) {
    .cf-stage { height: 400px; }
    .cf-field  { width: 740px; margin-left: -370px; }
    .cf-track  { perspective: 820px; }
    .cf-card  { width: 206px; height: 310px; margin: -155px 0 0 -103px; padding: 16px 14px; }
    .cf-name  { font-size: 16px; }
    .cf-price { font-size: 19px; }
    .cf-card  {
        transform:
            translate3d(var(--cf-mx, 0px), var(--cf-my, 0px), var(--cf-mz, 0px))
            rotateY(var(--cf-mry, 0deg))
            scale(var(--cf-ms, 1));
        opacity: var(--cf-mo, 1);
    }
}


@media (prefers-reduced-motion: reduce) {
    /* The drift does not run (app.js checks the same query); the track is
       still draggable and the dots still work, so the carousel is operable,
       it just never moves on its own. */
    .cf-dot { transition: none; }
}


.hero-badge {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    background: rgba(46, 91, 224, 0.12);
    border: 1px solid rgba(46, 91, 224, 0.30);
    border-radius: 999px;
    padding: 9px 20px;
    font-size: 15px;
    font-weight: 500;
    color: #7EA0F5;
}
/* 34px, down from 76. The copy column tightened when the right column stopped
   being one photograph and became three cards with prices on them: at 76px the
   headline dominated a hero whose subject is now the products beside it. The
   weight and tracking come down with the size, because -0.035em on 34px is a
   tightness that reads as a rendering fault rather than as a display face. */
.hero-h1 {
    margin: 30px 0 0;
    font-size: 34px;
    line-height: 1.1;
    font-weight: 600;
    letter-spacing: -0.02em;
    max-width: 560px;
    color: #fff;
}
.hero-h1-line2 { display: block; color: var(--chrome-link); }
.hero-subhead {
    margin: 28px 0 0;
    font-size: 19px;
    line-height: 1.6;
    color: rgba(255, 255, 255, 0.58);
    max-width: 560px;
}
.hero-buttons { margin-top: 40px; display: flex; gap: 16px; flex-wrap: wrap; }
/* The two CTAs are the same rendered height, measured at 54.8px each.
   On one line the flex row is what equalises them: align-items defaults to
   stretch, so the shorter box is pulled up to the taller one whatever its own
   border adds. The 1px transparent border below is not carrying that — it makes
   the primary's box IDENTICAL to the outline button's by construction (same
   padding, same border width) rather than only looking equal because the row
   stretched it, which is what keeps them matched if the row ever wraps and each
   button becomes its own flex line. */
.hero-btn {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 14px 26px;
    border-radius: 12px;
    font-size: 16px;
    font-weight: 600;
    /* Reserved, not decorative: see the note above .hero-btn. Never `none`, or the
       primary and the outline button stop being the same box. */
    border: 1px solid transparent;
    transition: background .15s ease, border-color .15s ease;
}
.hero-btn svg { width: 18px; height: 18px; }
.hero-btn:hover { text-decoration: none; }
.hero-btn-primary { background: var(--accent-blue); color: #fff; box-shadow: 0 8px 26px rgba(46, 91, 224, 0.35); }
.hero-btn-primary:hover { background: #264fc4; }
.hero-btn-secondary {
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.13);
    color: rgba(255, 255, 255, 0.88);
}
.hero-btn-secondary:hover { background: rgba(255, 255, 255, 0.10); }
/* Desktop shows the full label; the phone block below swaps them. */
.hero-btn-short { display: none; }

/* ONE BAR, not three boxes, and the dividers are inset positioned hairlines
   rather than a border on one side of a cell. Two reasons they changed:
   a single-side border is the thing this stylesheet does not do anywhere else,
   and a full-height border reads as a cell wall, where a hairline held 12px off
   the top and bottom reads as a separator inside one object. The bar stretches
   the copy column now, which is what makes it read as one object at all.

   MATTE BLACK IN BOTH THEMES (2026-09-14, docs/mockups/black-cards-mockup-2.html).
   Ground #141518, a hairline edge, white figures in the mono face, one set of
   rules: the light theme's own .hero-stat* overrides are gone, so what the
   white page shows is exactly what the dark one shows. */
.hero-stats {
    margin-top: 64px;
    display: flex;
    background: #141518;
    border: 1px solid rgba(255, 255, 255, 0.09);
    border-radius: 18px;
    overflow: hidden;
    max-width: 560px;
}
.hero-stat { flex: 1; position: relative; padding: 15px 18px; text-align: center; }
.hero-stat + .hero-stat::before {
    content: "";
    position: absolute;
    left: 0;
    top: 12px;
    bottom: 12px;
    width: 1px;
    background: rgba(255, 255, 255, 0.09);
}
.hero-stat-value { font-family: var(--font-mono); font-variant-ligatures: none; font-size: 24px; font-weight: 600; color: #ffffff; letter-spacing: -0.02em; line-height: 1; }
.hero-stat-label { margin-top: 4px; font-size: 11.5px; color: #9ba0a8; }
/* Desktop shows the full label; the phone block below swaps them. */
.hero-stat-short { display: none; }

@media (prefers-reduced-motion: reduce) {
    /* The blooms hold their keyframe start pose, which is a composition the
       handoff signs off on — the section reads as designed, just still. The
       transform is restated because animation:none alone would drop it. */
    .hero-bloom-a { animation: none; transform: translate3d(-9%, -6%, 0) scale(0.94); }
    .hero-bloom-b { animation: none; transform: translate3d(11%, 7%, 0) scale(1.18); }
    .hero-dots    { animation: none; transform: translate3d(0, 0, 0); }
}


/* =========================================================================
   CATALOG — light product section (anchor #catalog)
   ========================================================================= */

/* The sticky-bar offset for #catalog comes from html{scroll-padding-top} further
   down — do not add scroll-margin here as well, or it applies twice. */
.catalog { background: var(--fill-20); padding: 48px var(--gutter); }
/* Keep the catalog content from stretching absurdly wide + centered. */
/* .cbc-bar rather than .catalog-filters: the pill row now sits inside the bar
   alongside the Browse by class trigger, so the cap belongs on the parent. Cap
   the pills too and the trigger gets pushed outside it. */
.cbc-bar, .cbc-panel, .catalog-count, .pgrid { max-width: var(--content-max); margin-left: auto; margin-right: auto; }

.catalog-filters { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 26px; }
.catalog-filter {
    font-size: 14px;
    font-weight: 500;
    color: var(--ink-39);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 9px 18px;
    transition: border-color .15s ease, color .15s ease, background .15s ease;
}
.catalog-filter:hover { border-color: var(--edge-29); color: var(--text-2); text-decoration: none; }
.catalog-filter.is-active { background: var(--text-2); color: var(--surface); border-color: var(--text-2); }
/* margin-top/bottom as LONGHANDS. The `margin: 0 0 20px` shorthand that used to be
   here also reset margin-left/right to 0, silently cancelling the auto centring set
   on the line above — so the count alone escaped --content-max and sat a whole
   gutter wider than the filters and the grid on any display over ~1560px. */
.catalog-count { font-size: 13.5px; color: var(--muted-2); margin-top: 0; margin-bottom: 20px; }

/* Exactly 4 columns on desktop. minmax(0, 1fr) lets tracks shrink so card
   content can never force a 5th column or push past the container edge. */
.pgrid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 24px;
}
/* Reflow 4 -> 3 -> 2 -> 1 (the 2 -> 1 steps live in the 900px / 640px blocks). */
@media (max-width: 1160px) {
    .pgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}


/* Product card */
.pcard {
    background: var(--surface);
    border: 1px solid var(--line-soft);
    border-radius: 15px;   /* the framed image look: one radius with the product page */
    box-shadow: var(--shadow);
    overflow: hidden;
    min-width: 0;   /* allow the card to shrink to its grid track (no overflow) */
    transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease;
}
.pcard-name { overflow-wrap: break-word; }
.pcard:hover {
    transform: translateY(-5px);
    border-color: var(--edge-16);
    box-shadow: 0 16px 34px rgba(10, 10, 10, 0.09);
}
@media (prefers-reduced-motion: reduce) {
    .pcard:hover { transform: none; box-shadow: none; }
}

.pcard-form { display: flex; flex-direction: column; height: 100%; }

.pcard-media {
    position: relative;
    height: 210px;   /* shorter card; still enough for the vial to breathe */
    background: none;   /* cut-out vials: the card's own surface shows through */
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}
/* Absolutely centred inside the media box rather than laid out as a flex item.
   As a flex item the percentage max-height did not resolve (the box's own height
   comes from a column flex parent), so the 800px-tall <img> overflowed and the
   vial was cropped top and bottom. Positioned against .pcard-media the percentage
   always resolves, and inset:0 + margin:auto does the centring. */
.pcard-media img {
    position: absolute;
    inset: 0;
    margin: auto;
    /* Edge to edge: the image box is the media box. The catalogue's hover lift
       scales it 1.04, which the export's own transparent headroom absorbs (the
       cap sits about 8% below the top of the square, measured), so nothing of
       the vial is ever clipped by the card. */
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}
.pcard-media:has(.pcard-placeholder) { background: linear-gradient(170deg, var(--plate-a), var(--plate-b)); }
.pcard-placeholder { color: var(--ink-placeholder); }
.pcard-placeholder svg { width: 84px; height: 84px; }
.pcard-cart {
    position: absolute;
    top: 12px; right: 12px;
    z-index: 2;                  /* above the now-positioned product image */
    width: 44px; height: 44px;   /* 44px minimum tap target — do not shrink */
    border-radius: 50%;
    background: var(--accent-blue);
    border: none;
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.10);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    cursor: pointer;
    /* The 1.06 lift is the existing hover and stays; the added transition is on
       background-color only, so nothing else animates that did not before. */
    transition: transform .12s ease, background-color .16s ease;
}
.pcard-cart:hover { transform: scale(1.06); background: var(--surface-dark); }
.pcard-cart svg { width: 20px; height: 20px; }

/* SALE badge — top-left, opposite the quick-add cart. Colour is admin-set inline. */
.pcard-sale {
    position: absolute;
    top: 12px; left: 12px;
    z-index: 2;
    font-size: 11px;
    font-weight: 800;
    line-height: 1;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: #fff;
    background: var(--fill-sale);
    padding: 6px 11px;
    border-radius: 999px;
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.16);
}

/* Out-of-stock badge — centered pill over the image; same badge system as SALE.
   pointer-events:none so it never blocks the click-through to the product page. */
.pcard-media.is-oos .pcard-media-link { opacity: 0.55; }
.pcard-oos-badge {
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    z-index: 3;
    pointer-events: none;
    max-width: calc(100% - 24px);
    font-size: 12px;
    font-weight: 800;
    line-height: 1;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-align: center;
    white-space: nowrap;
    color: #fff;
    background: #1c1c1c;
    padding: 9px 20px;
    border-radius: 999px;
    box-shadow: 0 3px 12px rgba(0, 0, 0, 0.24);
}
@media (max-width: 480px) {
    .pcard-oos-badge { font-size: 10.5px; padding: 7px 15px; letter-spacing: 0.05em; }
}


.pcard-body { padding: 14px 14px 15px; display: flex; flex-direction: column; flex: 1; }
/* Admin-managed category colour palette (tint bg + darker text) */
.cat-green  { background: var(--fill-01); color: #1b8a4e; }
.cat-blue   { background: var(--fill-29); color: var(--link); }
.cat-red    { background: var(--pill-red-bg); color: var(--ink-32); }
.cat-orange { background: var(--fill-60); color: var(--ink-96); }
.cat-purple { background: var(--fill-07); color: var(--ink-28); }
.cat-gray   { background: var(--fill-13); color: var(--ink-65); }

/* Name left, price right, one row. The name takes the free width and wraps; the
   price never wraps and never shrinks. */
.pcard-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.pcard-name { margin: 0; flex: 1 1 auto; min-width: 0; font-size: 15.5px; font-weight: 700; color: var(--text-2); letter-spacing: -0.015em; line-height: 1.3; }
.pcard-price { margin: 0; flex: 0 0 auto; text-align: right; white-space: nowrap; }
.pcard-amount { display: block; font-family: var(--font-mono); font-size: 15px; font-weight: 700; color: var(--text-2); line-height: 1.35; }
.pcard-compare { display: block; font-size: 11.5px; color: var(--muted-2); font-weight: 600; text-decoration: line-through; line-height: 1.2; }

.pcard-buy { margin-top: auto; padding-top: 13px; }
.pcard-size {
    width: 100%;
    height: 44px;                     /* 44px minimum tap target */
    border: 1px solid var(--line-pale);
    border-radius: 9px;
    padding: 0 36px 0 12px;
    font-size: 14px;
    font-weight: 600;
    color: var(--text-2);
    background-color: var(--surface);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 12px center;
    background-size: 15px;
    -webkit-appearance: none;
    appearance: none;
}
.pcard-size:focus { outline: none; border-color: var(--accent-blue); }
.pcard-add {
    margin-top: 9px;
    width: 100%;
    min-height: 46px;                 /* 44px minimum tap target */
    /* THE SITE BLUE AT REST, #1c1c1c ON HOVER (2026-09-13-light-default.md).
       It was the brand orange with the blue as its hover, which put the loudest
       colour on the page on every card at once and left the blue doing nothing
       but a rollover. The rest state is the blue the primary buttons already
       use; the hover is the matte the ticker, the nav and the footer wear, so
       pressing a card reads as the card going to the chrome's colour rather than
       to another accent. White label on both, measured either side. */
    background: var(--accent-blue);
    color: #ffffff;
    border: none;
    border-radius: 9px;
    padding: 12px;
    font-size: 14.5px;
    font-weight: 700;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    /* Neutral, not tinted. The glow was rgba(46,91,224,.26), i.e. the blue this
       button no longer wears at rest; a blue halo under an orange face read as a
       colour bug. Black at a low alpha sits correctly under BOTH states and, unlike
       a tinted glow, needs no edit when --pcd-orange changes. */
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.14);
    cursor: pointer;
    transition: background-color .16s ease;
}
.pcard-add:hover { background: var(--surface-dark); }
.pcard-add svg { width: 17px; height: 17px; }

/* Empty state */
.empty { text-align: center; padding: 64px 24px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); }
.empty p { color: var(--muted); margin: 0 0 20px; }

/* =========================================================================
   GENERIC PAGE (contact, legal, error)
   ========================================================================= */

.page { background: var(--off-white); }
.page-inner { max-width: var(--maxw); margin: 0 auto; padding: 56px 24px 72px; }
.page-inner.narrow { max-width: 720px; }
.page-h1 { font-size: clamp(26px, 3vw, 38px); margin: 0 0 16px; }
.page-intro { color: var(--muted); font-size: 16px; max-width: 62ch; margin: 0 0 32px; }

.contact-form { background: var(--surface); border: 1px solid var(--border); padding: 28px; }

.legal-body-copy { color: var(--ink); font-size: 15.5px; line-height: 1.7; max-width: 68ch; }

/* Surface for the public legal / error / password-reset pages. The slim header
   these used to carry is retired: they all render partials/topbar.php now, the
   same bar as every other storefront page, so the whole .slim-* family is gone. */
.legal-body { background: var(--off-white); }

/* The "Back to sign in" link the retired slim bar used to carry, relocated into
   the body of the two password-reset pages. Left-aligned, because the pages it
   sits on are: .auth-forgot is the same idea but centred for the login card. */
.page-back { margin: 18px 0 0; font-size: 13.5px; }
.page-back a { color: var(--link); text-decoration: none; }
.page-back a:hover { text-decoration: underline; }

/* =========================================================================
   FOOTER
   ========================================================================= */

/* ---- Site footer ------------------------------------------------------- */
/* Horizontal padding lives on each band (24px), not on the footer, so the trust
   strip's dividers and bottom rule can run edge to edge. The inner wrapper only
   bites above 1440px, keeping the footer in step with the catalog grid. */
.site-footer {
    position: relative;
    /* The glow is painted by .ft-glow inside this element. isolation:isolate gives
       the footer its own stacking context so the glow's z-index cannot interact
       with anything on the page above it, and so the glow is clipped to the
       footer's own box rather than bleeding upward. */
    isolation: isolate;
    /* THE FOOTER'S OWN PALETTE, declared here and nowhere else. The footer is in
       the chrome isolation list, so it renders identically in light and dark by
       design; naming its colours on the element rather than at :root keeps them
       out of every other surface's cascade and out of the theme token blocks
       entirely, which is why this band needs no light override at all.
       docs/mockups/footer-mockup-v4.html. */
    --ft-bg:   #0d0e10;
    --ft-line: rgba(255, 255, 255, 0.10);
    --ft-ink:  #f3f3f5;
    --ft-mute: #8d929b;
    --ft-dim:  #9096a0;
    --ft-link: #dcdee2;
    --ft-meta: #c4c8cf;
    background: var(--ft-bg);
    /* THE BLOOM HANGS 25% PAST EACH SIDE, so the footer has to clip it or that
       overhang is scrollable overflow: without this the page reports a
       scrollWidth wider than its clientWidth at every width, which is a
       horizontal scrollbar on every storefront page. The mockup's footer carries
       the same overflow: hidden for the same reason. */
    overflow: hidden;
    /* Top and bottom only. A full border put a 1px inset on everything inside, so
       the footer content could never land on the same gutter as the grid; the left
       and right edges sat flush against the viewport and were invisible anyway. */
    border-top: 0;
    border-bottom: 0;
    color: var(--ft-ink);
}
/* The accent is .footer-accent at the BOTTOM edge now, not a ::after at the top.
   v4 has no top hairline: the footer's upper edge is where the page's ground
   stops and #0d0e10 starts, and the one accent line closes the page instead of
   opening the footer. */
/* EVERY piece of footer content lives inside this one wrapper, so raising the
   wrapper puts all of it above the glow. Raising individual elements instead
   would mean revisiting this list every time the footer gains a band. */
.site-footer-inner {
    position: relative;
    z-index: 2;
    max-width: calc(var(--content-max) + 2 * var(--gutter));
    margin: 0 auto;
}

/* ---- The bloom, footer only, and it does not move ----------------------
   The mockup's footer::before: two radials rising from the bottom edge, blue
   left of centre and amber right of it, on a box pulled 25% past each side and
   70% past the bottom, so no edge of either is ever on screen.

   IT IS THE .ft-glow ELEMENT rather than a pseudo-element, because that node is
   already the first child of the footer and already carries a comment saying it
   must stay there. Reusing it keeps the markup change to what the brief asks for.

   STATIC. What was here animated two orange pools on 16s and 22s cycles; v4 is a
   static gradient, so the keyframes, the opacity budget that went with them and
   the reduced-motion rule that switched them off have all gone with them. One
   paint for the life of the page.

   THE BOTTOM OFFSET IS 0 AND THE MOCKUP'S IS -70%, and this is the one piece of
   its geometry that is not copied. Both gradients are centred `at ... 100%`, the
   bottom edge of THIS element, with a radius of its own height; at bottom:-70%
   the element runs from 90% to 170% of the footer, so the only part of it on
   screen is the slice furthest from both centres, past the `transparent 70%`
   stop. Measured on the built page it contributed exactly 0 of 255 at every
   pixel: an invisible bloom. At bottom: 0 the element's lower edge IS the
   footer's lower edge, which is where the brief says the bloom rises from, and
   the colours, the two stop lists and the 80% height are the mockup's untouched. */
.ft-glow {
    position: absolute;
    inset: auto -25% 0 -25%;
    height: 80%;
    z-index: 1;
    pointer-events: none;
    background:
        radial-gradient(55% 100% at 28% 100%, rgba(46, 91, 224, 0.18), transparent 70%),
        radial-gradient(45% 100% at 80% 100%, rgba(247, 137, 15, 0.10), transparent 72%);
}

/* ---- The accent hairline, at the very bottom edge ----------------------
   The mockup's .accent: 3px, orange to blue, full width, below everything. It
   replaces the orange TOP-edge hairline .site-footer::after drew, which v4 does
   not have: the footer's top edge is now simply where the page stops and the
   ground starts. */
.footer-accent {
    height: 3px;
    background: linear-gradient(90deg, var(--pcd-orange), var(--accent-blue));
}

/* ---- Glass --------------------------------------------------------------
   ONE definition, worn by the three assurance cards and by the payment card.
   Gradient, hairline edge, inner top highlight, deep shadow, and nothing else:
   NO backdrop-filter, here or anywhere in the storefront chrome, so a band that
   appears on every page costs no per-frame blur pass. Values copied from the
   mockup rather than approximated. */
.footer-glass {
    background: linear-gradient(160deg,
        rgba(255, 255, 255, 0.085), rgba(255, 255, 255, 0.028) 46%, rgba(255, 255, 255, 0.015));
    border: 1px solid var(--ft-line);
    border-radius: 18px;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 14px 34px rgba(0, 0, 0, 0.42);
}

/* ---- The three assurance cards ----------------------------------------
   The mockup's .trio and .tcard. They were a ring badge and two lines on the
   bare footer; v4 makes each one a card of glass with the icon sitting directly
   on it at hairline weight, which is why .footer-trust-tile and its rotated
   orange arc are gone: a badge inside a card is two containers doing one job.

   Horizontal padding is var(--gutter), never a fixed value, because the gutter
   is 60/32/18/12px across the breakpoints and a fixed number would put the row
   out of line with the columns below it at every width but one. */
.footer-trust {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 16px;
    padding: 40px var(--gutter) 4px;
}
.footer-trust-cell {
    display: flex; align-items: center; gap: 16px;
    min-width: 0;
    padding: 20px 22px;
}
.footer-trust-ico { width: 30px; height: 30px; flex: 0 0 auto; color: #e3e6ea; }
.footer-trust-text { display: flex; flex-direction: column; min-width: 0; }
.footer-trust-title { font-size: 15.5px; font-weight: 600; line-height: 1.3; letter-spacing: -0.01em; color: var(--ft-ink); }
/* The mockup's --mute. On the glass at its lightest point this measures 5.08:1,
   and on the ground 6.18:1. */
.footer-trust-sub { margin-top: 3px; font-size: 13px; line-height: 1.4; color: var(--ft-mute); }
/* Blanking a cell in admin drops it, so the grid narrows to what is left
   instead of holding an empty third column open. */
.footer-trust.cells-1 { grid-template-columns: 1fr; }
.footer-trust.cells-2 { grid-template-columns: repeat(2, 1fr); }
/* Keeps the 3px gap between title and sub from becoming a top offset when only
   the sub survives. */
.footer-trust-text > :first-child { margin-top: 0; }

/* Main columns */
.footer-main {
    display: grid;
    grid-template-columns: 1.6fr 1fr 1fr 1fr;
    gap: 36px;
    padding: 44px var(--gutter) 30px;
}
.footer-col { min-width: 0; }

.footer-heading {
    margin: 3px 0 18px; font-size: 10.5px; font-weight: 600; line-height: 1.4;
    text-transform: uppercase; letter-spacing: 0.18em;
    /* WAS rgba(255,255,255,.35), WHICH WAS 3.20:1 ON THE FOOTER IN BOTH MODES.
       The footer is matte in light and #0e1014 in dark and this heading is white
       at an alpha on both, so it failed on every storefront page in both themes
       at once: 34 of the 36 surfaces the sweep covers. .55 measures 5.98:1 on
       the light footer and 6.21:1 on the dark one. Fixed in the base rather than
       per theme, because one value clears both. */
    /* THE MOCKUP'S --dim IS #6a6f78 AND IT DOES NOT PASS. It measures 3.82:1 on
       the ground and 3.14:1 on the lightest part of the glass, and this colour is
       worn by the column headings, the notice's heading and the "We accept"
       label, the last of which sits on glass.

       --ft-dim is #9096a0, and it took two goes. #878c93 clears 4.5:1 on the
       plain ground and on glass over it, and the "We accept" label is on neither:
       its card sits low in the footer, where the glass is lit by the bloom, and
       it measured 4.44:1 there at 768 and 390. #9096a0 measures 6.49:1 on the
       ground, 5.34:1 on plain glass and 5.05:1 on bloom-lit glass. It is the one
       value in this footer that is not the mockup's, and the report names it. */
    color: var(--ft-dim);
}

.footer-links { list-style: none; margin: 0; padding: 0; }
/* Row rhythm lives on the <li> as real margin, NOT as line-height on an
   inline-block anchor. An inline-block's baseline is the baseline of its LAST
   line box, so the moment a label wraps to two lines the anchor's box hangs
   above its own line and prints over the item above it. Every one of these
   labels is an admin-editable content block, so any of them can grow at any
   time. A block-level anchor with real margins cannot collide, and it gives
   the whole row a tap target instead of just the glyphs. */
.footer-links li + li { margin-top: 13px; }
.footer-links a {
    display: block; font-size: 15px; line-height: 1.4; color: var(--ft-link);
    text-decoration: none; transition: color .12s ease; overflow-wrap: anywhere;
    width: fit-content;
}
.footer-links a:hover { color: #fff; }

/* Brand column */
.footer-col-brand { min-width: 0; }
.footer-brand { display: inline-flex; align-items: center; gap: 11px; margin-bottom: 16px; }
.footer-brand:hover { text-decoration: none; }
/* The real lockup, same asset as the nav. Height only — width stays auto so the
   4.32:1 artwork is never distorted. The anchor is inline-flex, so the image's
   left edge lands on the column edge, flush with the blurb underneath. */
.footer-logo { height: 40px; width: auto; display: block; }

.footer-blurb { margin: 0 0 20px; font-size: 14.5px; line-height: 1.6; color: var(--ft-mute); max-width: 33ch; }
.footer-contact { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.footer-contact li { display: flex; align-items: center; gap: 10px; font-size: 14.5px; color: var(--ft-meta); min-width: 0; }
.footer-contact a { color: var(--ft-meta); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.footer-contact a:hover { color: #fff; }
/* The mockup's meta icons are --dim; --ft-dim is the value that passes, and an
   icon beside a label it belongs to is read through the label anyway. */
.footer-ico { width: 16px; height: 16px; flex: 0 0 auto; color: var(--ft-dim); }

/* ---- The bottom band: the notice, and the chips beside it ---------------
   Two blocks that were stacked full width. The payments block is "We accept"
   and four pills, about 400px of content, and as its own band it reserved a
   full row of footer height to put that at the left and nothing at the right;
   the notice did the same thing again underneath. Side by side, the band is as
   tall as the taller of the two rather than as tall as both.

   THE PAYMENTS COLUMN IS SIZED TO ITS CHIPS, up to a cap, and the notice takes
   whatever is left. A fixed 2fr to 1fr split looked right at 1440 and was wrong
   at 1024: the chips column came out at 293px, the four pills wrapped onto two
   rows and the block grew taller than the thing it was meant to sit beside.
   fit-content(26rem) lets the column be exactly as wide as one row of chips,
   about 420px, and never wider than 416px plus its own padding however many
   payment methods the admin turns on. The notice gets the remainder, which is
   the wider of the two at every width this grid applies at, and stays inside the
   900px measure .footer-notice-text already sets. minmax(0, 1fr) on the notice
   so a long unbroken word in it cannot push the grid wider than the footer.

   align-items: start, so the payments block sits at the top of the band beside
   the notice's heading rather than centring itself against a taller paragraph.

   THE BORDER AND THE PADDING MOVED HERE from .footer-notice. They belonged to
   whichever block came first, and now neither of them is first. */
.footer-band {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 34px;
    align-items: center;
    border-top: 1px solid var(--ft-line);
    padding: 26px var(--gutter) 28px;
}

/* ---- The payment card ---------------------------------------------------
   The plain block beside the notice becomes a piece of glass with the label and
   the chips on one row, as the mockup draws it. .footer-glass carries the
   surface; this carries the layout only. */
.footer-pay {
    display: flex;
    align-items: center;
    gap: 22px;
    padding: 18px 24px;
}
/* The heading stays an <h2> for structure and reads as an inline label. */
.footer-pay .footer-heading { margin: 0; white-space: nowrap; }
.footer-chips { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 9px; }
/* Solid brand chips. Colour only — no provider logo, wordmark or other
   trademarked asset, matching how the pay-page buttons are built. */
.footer-chip {
    display: grid; place-items: center;
    height: 38px; padding: 0 16px;
    font-size: 13.5px; font-weight: 700; line-height: 1.2;
    border: 1px solid transparent;
    border-radius: 10px;
    color: #fff;
}
/* Mirrors PaymentMethods::ALL. These four are a static "we accept" row rather
   than buttons built from order data, so they cannot read the table at render
   time; if a brand colour changes there, change it here in the same commit.
   Cash App and PayPal carry dark text: white fails contrast on both.

   VENMO NOW DOES TOO, and the brand blue is untouched. White on #008CFF measures
   3.39:1, under the 4.5:1 the other three already clear, and it was the only chip
   in the row failing. The fill is the brand's and stays exactly #008CFF; the LABEL
   moved to the same dark navy family the PayPal chip uses, which takes the pair to
   4.89:1. Measured both ways in the run report, alongside the three siblings
   (Cash App 7.32:1, PayPal 9.03:1, Zelle 7.54:1). */
.footer-chip-venmo   { background: #008CFF; color: #032038; }
.footer-chip-cashapp { background: #00D54B; color: var(--ink-34); }
.footer-chip-zelle   { background: #6D1ED4; color: #ffffff; }
.footer-chip-paypal  { background: #FFC439; color: #062b4e; }
/* Providers with no brand colour of their own (e.g. card/Square). Still reads as
   live — the dimmed treatment below is reserved for coming-soon. */
.footer-chip-plain   { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.16); color: #ffffff; }
/* Last, and two classes deep, so it wins over any brand colour above. */
.footer-chip.is-soon {
    background: rgba(255, 255, 255, 0.05);
    border-color: rgba(255, 255, 255, 0.1);
    color: rgba(255, 255, 255, 0.35);
}

/* Compliance notice */
.footer-notice { min-width: 0; }
.footer-notice .footer-heading { margin-bottom: 9px; }
/* White, and only in the notice: the column headings elsewhere in the footer keep
   their muted treatment. A compliance paragraph is the one block that should not
   be the faintest text on the page. */
.footer-notice .footer-heading { margin-bottom: 8px; color: #fff; }
.footer-notice-text { margin: 0; max-width: 78ch; font-size: 13px; line-height: 1.7; color: var(--ft-mute); }

/* Bottom bar */
.footer-bottom {
    border-top: 1px solid var(--ft-line); padding: 18px var(--gutter) 30px;
    display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;
}
.footer-copy { margin: 0; font-size: 13.5px; color: var(--ft-mute); }
.footer-powered { margin: 0; font-size: 13.5px; color: var(--ft-mute); }

/* The mockup's one breakpoint: at 980 the assurance cards stack, the link
   columns go to two, the band goes to one and the payment card is allowed to
   wrap its chips under its label. */
@media (max-width: 980px) {
    .footer-trust,
    .footer-trust.cells-1,
    .footer-trust.cells-2 { grid-template-columns: 1fr; }
    .footer-main { grid-template-columns: 1fr 1fr; gap: 24px 26px; }
    /* One column, and the notice is already first in the source. */
    .footer-band { grid-template-columns: 1fr; gap: 22px; align-items: start; }
    .footer-pay { flex-wrap: wrap; }
}

@media (max-width: 640px) {
    /* The cells-* classes are repeated here because they are more specific than a
       bare .footer-trust and would otherwise re-impose a multi-column grid. */
    .footer-trust,
    .footer-trust.cells-1,
    .footer-trust.cells-2 { grid-template-columns: 1fr; gap: 14px; }
    .footer-main { grid-template-columns: 1fr; gap: 26px; }
    .footer-bottom { flex-direction: column; align-items: flex-start; }
}


/* =========================================================================
   CHROME ISOLATION
   =========================================================================
   THE TWELVE SHELLS BELOW ARE DARK IN LIGHT MODE ALREADY, and stay dark in
   both. Run 4 cut the list from 22: the home page, the forum shell, the login
   panel and the age gate are no longer chrome, because light mode now means
   light on those three surfaces. What is left is what is dark BY DESIGN
   whatever the theme says:

     .topbar / .navbar          the bar. It is the same bar on every page.
     .mnav / .mnav-noscript     the drawer the bar opens, and its no-JS twin.
     .site-footer               the footer. Same footer on every page.
     .hero                      NEW. Home's hero is chrome in its own right now
                                that its page is not: the section, its stats
                                strip and its cover-flow cards and plates are
                                painted for a near-black ground and are measured
                                identical in both modes.
     .login-visual              the login split's photographic half. White type
                                over a photograph; it has no light form. (No
                                view renders it today, the login page is the
                                single-column .login-single, and it is kept so
                                the split layout cannot come back themed by
                                halves.)
     .splash                    the brand splash.
     #announce / .announce      the announcement strip.
     .pcd-chat                  the chat launcher and its white panel: literals
                                only, and it must not move with the page.
     .topbar-flash              the flash strip that rides under the bar.
     .avat-dark                 the avatar editor's dark stage.
     body.catalog-dark          MINUS home and the forum. The nine pages that are
     :not(.home-body)           still dark by design keep it: wholesale,
     :not(.f-body)              affiliates, first responders, responder apply,
                                peptide class, /shop, the three parent indexes
                                (/peptides, /compounds, /supplies) and the QR
                                pay page. /shop and the parents joined with the
                                shop structure run: they render the class
                                page's shell and card, so they sit where the
                                class pages sit, through the same body class.
                                The original six are byte-identical in light
                                this run and their dark is byte-identical to
                                run 3.

     .rs-feat / .rs-card /     REMOVED FROM THIS LIST. They were here on the
     .rs-box                   reasoning that the Research blog's cards and
                               sidebar boxes are furniture, dark in all three
                               states like the bar and the footer. Measured, they
                               are not furniture: they cover the page, so with no
                               cookie /research painted 99% dark and a post 97%,
                               against 46% on a product page, and the blog read as
                               a dark site whatever its body background said.
                               Their --rsc-* / --rsb-* families are THEMED now,
                               light on the bare .rs-body and dark under
                               :root[data-theme="dark"] .rs-body, so they follow
                               the page like the rest of it. Resetting their
                               tokens to light inside dark mode is exactly what
                               this list does, which is why they had to leave it.
                               The share rail was always absent from this list:
                               its tiles are brand literals and its "Share" label
                               sits on the page background.

   .cf-stage and .cf-dots ARE GONE FROM THE LIST because .hero now covers them:
   both live inside .hero-featured inside <section class="hero"> on home and
   nowhere else. Custom properties inherit, so naming the ancestor names them.

   RATHER THAN PIN RULE BY RULE, this resets every token that has a different
   dark value back to its LIGHT value inside those shells. Custom properties
   inherit, so one declaration covers every descendant, every rule that reads a
   token today, every rule that inherits one (the global `a` colour is the
   biggest of those), and every rule a later run adds. There is nothing to
   remember and nothing to miss.

   IT CANNOT AFFECT LIGHT MODE. Every selector is under
   :root[data-theme="dark"], which light never matches, and every value is
   copied from :root, so the chrome renders in dark exactly what it renders in
   light. Generated from the two token blocks above by scratch/chrome-block.php,
   which also checks the block in this file against them: run 4 reports 336
   light tokens, 319 dark, 307 that differ, 307 here, EXACT.
   ========================================================================= */
:root[data-theme="dark"] body.catalog-dark:not(.home-body):not(.f-body),
:root[data-theme="dark"] .topbar,
:root[data-theme="dark"] .navbar,
:root[data-theme="dark"] .mnav,
:root[data-theme="dark"] .mnav-noscript,
:root[data-theme="dark"] .site-footer,
:root[data-theme="dark"] .hero,
:root[data-theme="dark"] .login-visual,
:root[data-theme="dark"] .splash,
:root[data-theme="dark"] .announce,
:root[data-theme="dark"] .pcd-chat,
:root[data-theme="dark"] .topbar-flash,
:root[data-theme="dark"] .avat-dark {
    --bg: #fafafa;
    --border: #e4e4e4;
    --border-2: #d4d4d4;
    --btn-ink: #ffffff;
    --edge-01: #efdfa8;
    --edge-02: #e3e6ec;
    --edge-03: #e8732a;
    --edge-04: #eef0f4;
    --edge-05: #f0f0f3;
    --edge-06: #9aa2ac;
    --edge-07: #bfe6d1;
    --edge-08: #c9ecd7;
    --edge-09: #ffe2b8;
    --edge-10: #999;
    --edge-11: #c8c8ce;
    --edge-12: #ccd9fb;
    --edge-13: #cfdcfa;
    --edge-14: #cfe6d9;
    --edge-15: #d5dae0;
    --edge-16: #dcdcdc;
    --edge-17: #e0e0e4;
    --edge-18: #e4e8ec;
    --edge-19: #e8a9a9;
    --edge-20: #eeeef0;
    --edge-21: #f0c4c4;
    --edge-213: #fff;
    --edge-22: #f0d2d2;
    --edge-23: #f2f3f6;
    --edge-24: #f4f4f6;
    --edge-25: #1a9457;
    --edge-26: #1fa863;
    --edge-27: #b6bdc6;
    --edge-28: #b79ce8;
    --edge-29: #bbb;
    --edge-30: #c06a15;
    --edge-31: #c3cff5;
    --edge-32: #c8ccd2;
    --edge-33: #d3e2fc;
    --edge-34: #d7dbe4;
    --edge-35: #dcdce2;
    --edge-36: #dcdfe3;
    --edge-37: #ddd;
    --edge-38: #dfb100;
    --edge-39: #e3e3e3;
    --edge-40: #e6e2f2;
    --edge-41: #ececed;
    --edge-42: #eceef1;
    --edge-43: #eef1f4;
    --edge-44: #eef1f5;
    --edge-45: #f0d38a;
    --edge-46: #f0d9a8;
    --edge-47: #f0e3bd;
    --edge-48: #f0f0f0;
    --edge-49: #f2f2f4;
    --edge-50: #f2f4f7;
    --edge-51: #f6cfe0;
    --edge-52: #f6dcc0;
    --edge-err: #c0392b;
    --fill-01: #e7f6ee;
    --fill-02: #fafbfc;
    --fill-03: #fdecec;
    --fill-04: #fdf0f0;
    --fill-05: #e8f7ee;
    --fill-06: #eceef2;
    --fill-07: #f0eafb;
    --fill-08: #f0f2f5;
    --fill-09: #f2fbf6;
    --fill-10: #fafafb;
    --fill-11: #e4e4e8;
    --fill-12: #e7f5ec;
    --fill-13: #ededed;
    --fill-14: #f2f2f4;
    --fill-15: #f4f4f6;
    --fill-16: #f4f5f8;
    --fill-17: #f4f6f9;
    --fill-18: #f6f6f7;
    --fill-19: #f7f8fa;
    --fill-20: #fafafa;
    --fill-21: #fbfcfd;
    --fill-214: #fff;
    --fill-22: #fdf1dc;
    --fill-23: #fdf1f1;
    --fill-24: #fdf3e0;
    --fill-25: #b9c6ea;
    --fill-26: #dbe7fd;
    --fill-27: #e2e2ea;
    --fill-28: #e7f6ed;
    --fill-29: #e8eefc;
    --fill-30: #e8effd;
    --fill-31: #eaf1fe;
    --fill-32: #eaf1ff;
    --fill-33: #eceef1;
    --fill-34: #eeeef0;
    --fill-35: #eef0f5;
    --fill-36: #eef2fe;
    --fill-37: #eef3ff;
    --fill-38: #efe6fb;
    --fill-39: #efe9fb;
    --fill-40: #f0f1f4;
    --fill-41: #f1f1f3;
    --fill-42: #f1f3f6;
    --fill-43: #f4f5f7;
    --fill-44: #f4f6fa;
    --fill-45: #f4f6fb;
    --fill-46: #f4f7ff;
    --fill-47: #f4f8ff;
    --fill-48: #f5f6f8;
    --fill-49: #f6f6f8;
    --fill-50: #f6f7f9;
    --fill-51: #f6f9ff;
    --fill-52: #f7f7f8;
    --fill-53: #f7f7f9;
    --fill-54: #f7f8fb;
    --fill-55: #f7faf8;
    --fill-56: #f7faff;
    --fill-57: #faf9fd;
    --fill-58: #fbdada;
    --fill-59: #fbdcdc;
    --fill-60: #fbefe2;
    --fill-61: #fbfbfc;
    --fill-62: #fdeaf3;
    --fill-63: #fdf0e3;
    --fill-64: #fdf6e3;
    --fill-65: #fdf6e6;
    --fill-66: #fdf6f3;
    --fill-67: #fdf8e6;
    --fill-68: #fdf9ec;
    --fill-69: #ffe2b8;
    --fill-70: #fff4d6;
    --fill-71: #fff4e0;
    --fill-72: #fff8ec;
    --fill-73: #fff9ec;
    --fill-btnhover: #262626;
    --fill-sale: #e23744;
    --focus-ring: #0a0a0a;
    --ink: #141414;
    --ink-01: #a9b0bc;
    --ink-02: #1c1c1e;
    --ink-03: #1fa863;
    --ink-04: #96201f;
    --ink-05: #12703f;
    --ink-06: #3ddc97;
    --ink-07: #7ea0ff;
    --ink-08: #8f1d1d;
    --ink-09: #0f6b3d;
    --ink-10: #8b8f9a;
    --ink-100: #e0a63a;
    --ink-101: #f87171;
    --ink-102: #fca5a5;
    --ink-103: #ff7a7a;
    --ink-104: #ff8a74;
    --ink-105: #ff8f8f;
    --ink-106: #ff9d8a;
    --ink-11: #8b919c;
    --ink-12: #9aa6bc;
    --ink-13: #d83b3b;
    --ink-14: #3d4553;
    --ink-15: #6b5200;
    --ink-16: #6b7480;
    --ink-17: #6f92ff;
    --ink-18: #7a4d05;
    --ink-19: #8fadf5;
    --ink-20: #9a6a00;
    --ink-21: #b42318;
    --ink-210: #5a6478;
    --ink-22: #c5311f;
    --ink-23: #ff8b8b;
    --ink-24: #1a7f4b;
    --ink-25: #3a3a40;
    --ink-26: #3d3000;
    --ink-27: #6b6b70;
    --ink-28: #7d3ec0;
    --ink-29: #8a5c00;
    --ink-30: #8d959d;
    --ink-31: #a9c1ff;
    --ink-32: #c0392b;
    --ink-33: #d8443c;
    --ink-34: #04310f;
    --ink-35: #116635;
    --ink-36: #12151c;
    --ink-37: #1a4fa8;
    --ink-38: #54545a;
    --ink-39: #555;
    --ink-40: #55555f;
    --ink-41: #7fa0f0;
    --ink-42: #8a94a0;
    --ink-43: #96b0ff;
    --ink-44: #b7791f;
    --ink-45: #b8860b;
    --ink-46: #04130c;
    --ink-47: #0f7a4d;
    --ink-48: #12805a;
    --ink-49: #19733f;
    --ink-50: #19b877;
    --ink-51: #1a1a1a;
    --ink-52: #1b3fa8;
    --ink-53: #1c3f9e;
    --ink-54: #20408f;
    --ink-55: #2044ad;
    --ink-56: #2743c7;
    --ink-57: #2e9e5b;
    --ink-58: #3d2b00;
    --ink-59: #3d4149;
    --ink-60: #4a3900;
    --ink-61: #4a4a55;
    --ink-62: #4bbf7a;
    --ink-63: #4c5462;
    --ink-64: #4fd08a;
    --ink-65: #555555;
    --ink-66: #5b2fa8;
    --ink-67: #5b3fa8;
    --ink-68: #5b87ff;
    --ink-69: #5c4a10;
    --ink-70: #5d636e;
    --ink-71: #666;
    --ink-72: #6b6b74;
    --ink-73: #6b727d;
    --ink-74: #7a5a12;
    --ink-75: #7a5b00;
    --ink-76: #7a5c00;
    --ink-77: #7a7a7a;
    --ink-78: #8a4b12;
    --ink-79: #8a6508;
    --ink-80: #8a6d1f;
    --ink-81: #8a8a8a;
    --ink-82: #8aa9ff;
    --ink-83: #8f1d55;
    --ink-84: #9a6212;
    --ink-85: #9aa2b1;
    --ink-86: #9aa2b4;
    --ink-87: #9ca3af;
    --ink-88: #a0a0a0;
    --ink-89: #b06a00;
    --ink-90: #b07d00;
    --ink-91: #b07f10;
    --ink-92: #b4b4ba;
    --ink-93: #b6becd;
    --ink-94: #c0322b;
    --ink-95: #c04747;
    --ink-96: #c05a12;
    --ink-97: #c22f2f;
    --ink-98: #d9635c;
    --ink-99: #d99b3a;
    --ink-err: #c0392b;
    --ink-placeholder: #cdced3;
    --input-bg: #ffffff;
    --input-line: #d4d4d4;
    --input-text: #141414;
    --line: #e4e4e4;
    --line-cool: #e4e4e8;
    --line-pale: #dedede;
    --line-soft: #ececec;
    --line-strong: #d4d4d4;
    --line-warm: #e6e6e9;
    --link: #2e5be0;
    --link-hover: #264fc4;
    --muted: #6b6b6b;
    --muted-2: #9a9a9a;
    --muted-3: #b8b8b8;
    --muted-ash: #5a6470;
    --muted-blue: #6b7280;
    --muted-cool: #6b7382;
    --muted-grey: #8a8a90;
    --muted-mid: #6b6b6b;
    --muted-plum: #4a4a55;
    --muted-slate: #4a5560;
    --muted-slate-2: #4a5260;
    --muted-steel: #5c6880;
    --muted-warm: #6d717c;
    --muted-warm-2: #6b6b70;
    --off-white: #fafafa;
    --pill-amber: #f5c518;
    --pill-amber-bg: #fdf8e6;
    --pill-amber-ink: #8a5a00;
    --pill-green: #1fa863;
    --pill-green-2: #1b8a4e;
    --pill-grey: #54545a;
    --pill-ink: #ffffff;
    --pill-ink-amber-a: #3d3000;
    --pill-ink-amber-b: #1c1c1e;
    --pill-part: #d97a1f;
    --pill-red: #d83b3b;
    --pill-red-bg: #fdeaea;
    --placeholder: #7d8494;
    --rsp-tint-b: #7a1d1f;
    --rsp-tint-cta: #b42318;
    --rsp-tint-cta2: #8c1a12;
    --rsp-tint-ink: #6d4a4a;
    --scrim: rgba(0, 0, 0, 0.45);
    --selected-bg: #f7f9ff;
    --selected-line: #9aa2ac;
    --shadow: 0 3px 10px rgba(0, 0, 0, 0.10);
    --shadow-soft: rgba(20, 30, 60, .08);
    --surface: #ffffff;
    --surface-2: #eef0f4;
    --surface-3: #ececf1;
    --surface-4: #f1f1f3;
    --text: #141414;
    --text-1: #111418;
    --text-2: #111111;
    --text-3: #1c1c1e;
    --text-body: #3a3a3a;
    --text-link: #0a0a0a;
    --tint-amber: #fdf3e0;
    --tint-amber-2: #fdf9f0;
    --tint-amber-ln: #f0e0c0;
    --tint-blue: #eaf1fe;
    /* THE UA'S OWN DEFAULTS, TOO. A <button> with no background, an <input> with
       no colour and an <svg> inheriting `canvastext` all take their value from
       color-scheme, which the page sets to dark. Inside chrome that flips a
       button's buttonface from rgb(239,239,239) to rgb(107,107,107) and every
       unstyled colour from black to white — differences no rule in this file can
       reach. Declaring the scheme light for these subtrees closes the whole class
       at once. Light never matches this block, so light cannot move. */
    color-scheme: light;
}
/* THE INHERITED COLOUR, for the two shells that do not declare one of their own.
   The drawer and the chat launcher take `color` from <body>, which is outside the
   shell and has already resolved --ink to its dark value, so the reset above
   could not reach it. Naming --ink here re-resolves it against the shell's own
   light value. Scoped OFF a catalog-dark page,
   because there the drawer correctly inherits that page's own #c7cbd2 and
   pinning it to --ink would have been a new difference rather than a fix: the
   measurement went 0 -> 9 on home and forum when it was not scoped.

   .pcd-chat IS NEW IN RUN 4, and it is a leak run 3's chrome check did not
   look at: the launcher and its white panel set no colour of their own, so on
   every light storefront page their four elements read #141414 in light and
   #ececee in dark. Measured: 4 differing chrome elements on product, cart,
   checkout, confirmation, coa-library, faq, contact and eleven more, 160 in
   total across the sweep, all of them this one cause.

   .login-page and .login-single LEFT this rule: the login panel is a themed
   surface now and is supposed to take the page's ink. */
:root[data-theme="dark"] body:not(.catalog-dark) .mnav,
:root[data-theme="dark"] body:not(.catalog-dark) .pcd-chat {
    color: var(--ink);
}

/* =========================================================================
   FOCUS / MOTION
   ========================================================================= */

:focus-visible {
    outline: 2px solid var(--black);
    outline-offset: 2px;
}
.topbar :focus-visible,
.login-visual :focus-visible,
.login-auth :focus-visible {
    outline-color: var(--white);
}

@media (prefers-reduced-motion: reduce) {
    * { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
}


/* =========================================================================
   RESPONSIVE
   ========================================================================= */

/* Between 768px and ~1040px the logo + three nav items + three actions get
   tight in the floating bar. Trim the item padding rather than dropping items. */
@media (max-width: 1040px) {
    .navbar { gap: 14px; }
    .nav-item { padding: 0 10px; letter-spacing: 0.06em; }
}


@media (max-width: 1050px) {
    .hero-h1 { font-size: 30px; }
}



@media (max-width: 900px) {
    /* Hero stacks (text, then image); glows still cover. Horizontal padding stays
       on --gutter from the base rule, never restated per breakpoint. The
       min-height:auto that used to sit here is gone with the 100svh it existed to
       undo: the base rule is content-height now at every width. */
    .hero-inner, .hero.no-media .hero-inner { grid-template-columns: 1fr; }
    .hero-col { padding-bottom: 8px; }   /* top padding stays on the base clamp */
    .hero-h1 { font-size: 26px; }
    .hero-media { padding-bottom: 28px; }

    .catalog { padding: 40px var(--gutter); }
    .pgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }

    /* Stack the login split: image becomes a shorter banner on top, auth card full-width. */
    .login-split { grid-template-columns: 1fr; }
    .login-seam { display: none; }
    .login-visual { order: -1; padding: 36px 32px; }
    .login-visual .login-logo-img { height: 84px; }
    .login-features { width: 100%; margin: 26px 0; }
    .login-visual-foot { width: 100%; }
    .login-auth { padding: 36px 32px; }
    .login-auth::before { display: none; }
    /* The three nav rules that used to sit here (12px bar padding, 12px gap, a
       26px logo) are gone: the desktop bar never renders under 1200 now, so
       they matched nothing, and the logo is 48px everywhere the bar shows. */
}


@media (max-width: 640px) {
    body { font-size: 14.5px; }

    .login-visual { padding: 28px 18px; }
    .login-visual .login-logo-img { height: 70px; }
    .login-feature { padding: 13px 18px; }
    .login-feature-title { font-size: 18px; }
    .login-auth { padding: 28px 18px; }
    .trust-row { gap: 10px 16px; }

    .field-row { grid-template-columns: 1fr; gap: 0; }
    .hero-col { padding-bottom: 4px; }   /* top padding stays on the base clamp */
    /* No .hero-h1 override here any more. It read 40px, which was smaller than the
       old 76px desktop and is LARGER than the 34px one, so the heading grew as the
       viewport shrank. The 900 rule's 26px carries down instead. */
    .hero-subhead { font-size: 17px; }

    /* Hero CTAs: two equal columns on one row rather than stacked. A grid, not a
       wrapping flex row, so the two cells stay exactly equal however long the
       labels get, and the short labels take over so nothing has to be clipped. */
    .hero-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
    .hero-btn {
        justify-content: center;
        min-width: 0;
        height: 36px;
        padding: 0 10px;
        gap: 4px;
        border-radius: 8px;
        font-size: 10.5px;
        font-weight: 500;
        white-space: nowrap;
    }
    .hero-btn svg { width: 10px; height: 10px; flex: 0 0 auto; }
    .hero-btn-full { display: none; }
    .hero-btn-short { display: inline; }
    .hero-btn-primary { color: #fff; }
    .hero-btn-secondary {
        background: rgba(255, 255, 255, 0.05);
        border: 1px solid rgba(255, 255, 255, 0.12);
        color: #fff;
    }
    /* Three stats on ONE row, same bar as the desktop rule and the same inset
       hairline between them, just tighter. It used to be a grid whose dividers were
       the middle cell's left and right borders; those were the last single-side
       borders in the hero and they are gone with it. Nothing here needs to clear the
       desktop divider any more, because that divider is a ::before which this rule
       inherits rather than fights. */
    .hero-stats {
        margin-top: 28px;
        max-width: none;
    }
    .hero-stat {
        min-width: 0;
        padding: 11px 4px;
        margin: 0;
        text-align: center;
    }
    .hero-stat + .hero-stat::before { top: 9px; bottom: 9px; }
    .hero-stat-value { font-size: 18px; letter-spacing: -0.01em; }
    .hero-stat-label { margin-top: 3px; font-size: 8px; line-height: 1.3; }
    .hero-stat-full { display: none; }
    .hero-stat-short { display: inline; }
    .catalog { padding: 32px var(--gutter); }
    .pd-related-grid { grid-template-columns: 1fr; }
    .page-inner { padding: 36px 16px 56px; }
    .contact-form { padding: 20px; }
}






/* ---- Phone catalog: TWO cards per row, scaled down proportionally --------
   Same structure as desktop (image, quick-add, name + price row, size, add).
   Nothing here may shrink a tap target below 44px. */
@media (max-width: 640px) {
    .pgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
    .pcard-media { height: 150px; }
    /* The hit area stays 44px; only the white disc shrinks, so the quick-add
       does not swallow a 143px-wide card. */
    .pcard-cart { top: 6px; right: 6px; background: transparent; box-shadow: none; }
    .pcard-cart::before {
        content: "";
        position: absolute;
        inset: 4px;
        border-radius: 50%;
        background: var(--accent-blue);
        box-shadow: 0 3px 10px rgba(0, 0, 0, 0.14);
        transition: background-color .16s ease;
    }
    /* Below 640 the element itself is transparent and the ::before IS the disc,
       so the hover colour has to move with it or the phone quick-add never
       changes on press. */
    .pcard-cart:hover::before { background: var(--surface-dark); }
    .pcard-cart svg { position: relative; width: 18px; height: 18px; }
    .pcard-sale { top: 8px; left: 8px; font-size: 9.5px; padding: 5px 8px; }
    .pcard-body { padding: 11px 11px 12px; }
    .pcard-head { gap: 8px; }
    .pcard-name { font-size: 13px; line-height: 1.28; }
    .pcard-amount { font-size: 13px; }
    .pcard-compare { font-size: 10.5px; }
    .pcard-restock { font-size: 10.5px; }
    .pcard-buy { padding-top: 10px; }
    .pcard-size { font-size: 13px; padding: 0 30px 0 10px; background-position: right 9px center; background-size: 13px; }
    .pcard-add { font-size: 13px; gap: 6px; padding: 11px 8px; }
    .pcard-add svg { width: 15px; height: 15px; }
}


/* 414 / 375 / 320: tighten the gutters and the gap so two tracks still fit
   without the name or the price being clipped. */
/* Phones: the vial fills the media box. The export is a square with the vial
   76% of its height (11.8% clear above the cap, 12.2% below, measured on the
   1000px PNG's alpha; the BAC water photo 12.8% / 11.2%), so an image box the
   size of the media box drew a 99px vial in the 130px box at 390. The image
   box is now 118% of the media box, centred, the box's overflow:hidden
   trimming 9% of transparent margin each side: 117px of vial in the same
   130px box with 3.7px of the export's own headroom still inside it, and the
   catalogue's 1.04 lift (pointer devices only) still inside it (11.4% against
   the 11.8%). The card, the box, its padding, the price, the select and the
   button are not touched; the certificate sheet has its own img rule. */
@media (max-width: 600px) {
    .pcard-media-link img { width: 118%; height: 118%; max-width: none; max-height: none; inset: -9%; }
}

@media (max-width: 400px) {
    .catalog { padding: 28px var(--gutter); }
    .pgrid { gap: 10px; }
    .pcard-media { height: 130px; }
    .pcard-body { padding: 10px 9px 11px; }
    .pcard-head { gap: 6px; }
    .pcard-name { font-size: 12.5px; }
    .pcard-amount { font-size: 12.5px; }
    /* The option text is "10 mg — $45.00"; at 320px it has to fit a ~125px box
       or the native select clips it mid-price. */
    .pcard-size { font-size: 11.5px; padding: 0 24px 0 8px; background-position: right 7px center; background-size: 12px; }
    .pcard-add { font-size: 12px; gap: 5px; padding: 11px 4px; }
    .pcard-oos-badge { font-size: 9.5px; padding: 6px 11px; }
    .pcard-sale { font-size: 9px; padding: 4px 7px; }
}


/* =========================================================================
   PHASE 2 — logo helper + admin center
   ========================================================================= */

/* ---- Logo helper (shared) --------------------------------------------- */
.logo-wordmark { font-weight: 600; font-size: 16px; letter-spacing: -0.01em; }
.nav-brand { color: #fff; }
.nav-brand .logo-wordmark,
.login-logo .logo-wordmark,
.admin-brand .logo-wordmark { color: #fff; }
.login-logo { color: #fff; }
.login-logo-img { height: 72px; width: auto; }
.admin-logo-img { height: 24px; width: auto; }

/* ---- Admin shell ------------------------------------------------------- */

.admin-shell { display: flex; min-height: 100vh; align-items: stretch; }

/* =========================================================================
   THE SIDEBAR, OPTION A (docs/mockups/admin-cleanup-mockup.html, section 1)
   #1c1c1c throughout. Four links on top, five collapsible groups, a footer.
   Icons are monochrome (#9a9ea6): orange marks the current item, red is a
   count that needs somebody. Group labels are bright white 11px caps; item
   text is #d8d9dd; the current item sits on a #2a2a2a plate. NO LEFT ACCENT
   BORDER on the active item, which is a house rule everywhere in the admin.
   ========================================================================= */
.admin-sidebar {
    --sb-bg: #1c1c1c;
    --sb-plate: #2a2a2a;
    --sb-line: #2c2c2c;
    --sb-ink: #d8d9dd;
    --sb-ink-soft: #b3b7bf;
    --sb-icon: #9a9ea6;
    flex: 0 0 250px;
    width: 250px;
    background: var(--sb-bg);
    color: var(--sb-ink);
    display: flex;
    flex-direction: column;
    padding: 18px 12px 0;
    position: sticky;
    top: 0;
    height: 100vh;
    align-self: flex-start;
    /* The box is exactly one viewport tall, so its content must be CONTAINED
       rather than allowed to spill: the nav below scrolls on its own and the
       footer stays put. */
    overflow: hidden;
}
.admin-brand-row { display: flex; align-items: center; justify-content: space-between; padding: 2px 10px 0; }
.admin-brand { display: block; }
.admin-brand:hover { text-decoration: none; }
.admin-logo-img { height: 26px; width: auto; display: block; }
.admin-brand-tag {
    color: #8a8e96;
    font-size: 10px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    padding: 0 10px;
    margin: 8px 0 14px;
}
.admin-drawer-close { display: none; }

/* Takes the space between the search and the footer, and scrolls on its own
   when the groups are all expanded. min-height:0 is what lets a flex item
   shrink below its content height; without it the nav refuses to scroll. */
.admin-nav {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding-bottom: 8px;
}
.admin-nav-top { display: flex; flex-direction: column; }
.admin-nav-link {
    display: flex;
    align-items: center;
    gap: 11px;
    min-height: 40px;
    padding: 8px 10px;
    border-radius: 8px;
    color: var(--sb-ink);
    font-size: 14.5px;
    line-height: 1.25;
    border: 0;
    transition: background .12s ease, color .12s ease;
}
.admin-nav-link:hover { color: #fff; background: rgba(255, 255, 255, .05); text-decoration: none; }
.admin-nav-link:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: -2px; }
.admin-nav-link.is-active { background: var(--sb-plate); color: #fff; font-weight: 500; }
.admin-nav-icon { width: 16px; height: 16px; flex: none; color: var(--sb-icon); }
.admin-nav-link:hover .admin-nav-icon { color: #c9ccd3; }
.admin-nav-link.is-active .admin-nav-icon { color: var(--pcd-orange); }
.admin-nav-text { min-width: 0; }
.admin-nav-external { display: flex; align-items: center; }
.admin-nav-ext-icon { width: 13px; height: 13px; flex: none; opacity: .7; margin-left: 2px; color: var(--sb-icon); }
.admin-nav-external:hover .admin-nav-ext-icon { opacity: 1; }
.admin-nav-ext-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ---- The red counts ------------------------------------------------------
   Pushed to the right edge of the link by margin-left:auto. RED, and the only
   red in the sidebar: every other signal here is navigation; this one stands
   for "somebody is waiting". min-width plus centred text keeps a single digit
   round and lets it grow to a pill at two or three digits. [hidden] is
   respected explicitly because the base display:inline-flex would otherwise
   beat it. */
.admin-nav-badge,
.admin-navgroup-count {
    margin-left: auto;
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 19px; height: 19px; padding: 0 6px;
    border-radius: 999px;
    background: var(--pill-red); color: #fff;
    font-family: var(--font-ui);
    font-size: 11px; font-weight: 700; line-height: 1;
    letter-spacing: 0;
    text-transform: none;
    flex: none;
}
.admin-nav-badge[hidden],
.admin-navgroup-count[hidden] { display: none; }

/* ---- Groups --------------------------------------------------------------
   Real <details> elements, rendered open by the server so a no-JS admin can
   still reach every section. The inline script in admin_nav.php applies the
   remembered state before paint; admin.js keeps it. */
.admin-navgroup { border: 0; }
.admin-navgroup-head {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 8px 0 2px;
    padding: 6px 10px;
    min-height: 32px;
    cursor: pointer;
    font-size: 11px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: #ffffff;
    font-weight: 700;
    list-style: none;
    user-select: none;
    border-radius: 6px;
}
.admin-navgroup-head::-webkit-details-marker { display: none; }
.admin-navgroup-head::marker { content: ''; }
.admin-navgroup-head:hover { background: rgba(255, 255, 255, .04); }
.admin-navgroup-head:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: -2px; }
.admin-navgroup-chevron {
    width: 14px; height: 14px; flex: none;
    margin-left: auto;
    color: #666;
    transform: rotate(-90deg);   /* closed: points right */
    transition: transform .15s ease;
}
/* The summed count sits before the chevron, and only while the group is
   closed: open, the item badges themselves are on screen. */
.admin-navgroup-count { margin-left: auto; }
.admin-navgroup-count + .admin-navgroup-chevron { margin-left: 0; }
.admin-navgroup[open] > .admin-navgroup-head .admin-navgroup-count { display: none; }
.admin-navgroup[open] > .admin-navgroup-head .admin-navgroup-chevron { transform: rotate(0deg); }
.admin-navgroup-items { display: flex; flex-direction: column; padding-bottom: 2px; }
.admin-navgroup-items .admin-nav-link { font-size: 14px; }

/* Jump-to-only rows: hidden until the box has text in them. */
.admin-nav-jump { display: flex; flex-direction: column; }
.admin-nav-jumprow .admin-nav-icon { opacity: .6; }

/* ---- "Jump to" ----------------------------------------------------------- */
.admin-navsearch { position: relative; margin: 0 4px 14px; }
.admin-navsearch-icon {
    position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
    width: 15px; height: 15px; color: var(--sb-icon); pointer-events: none;
}
.admin-navsearch-input {
    width: 100%;
    height: 38px;
    background: #262626;
    border: 1px solid #333;
    border-radius: 9px;
    padding: 0 10px 0 34px;
    color: #fff;
    font-family: inherit;
    font-size: 13px;
    -webkit-appearance: none;
    appearance: none;
}
.admin-navsearch-input::placeholder { color: var(--sb-icon); font-size: 13px; }
.admin-navsearch-input:focus { outline: none; border-color: var(--accent-blue); background: #2a2a2a; }
.admin-navsearch-input::-webkit-search-cancel-button { -webkit-appearance: none; }
.admin-nav-empty { color: var(--sb-icon); font-size: 12.5px; padding: 6px 10px; margin: 0; }
.admin-nav [hidden] { display: none !important; }
.admin-sidebar-foot [hidden] { display: none !important; }

/* ---- Footer ----------------------------------------------------------------
   Pinned to the bottom and never shrinks: the nav above absorbs the spare space
   and scrolls behind it. */
.admin-sidebar-foot {
    flex: 0 0 auto;
    margin-top: auto;
    padding: 10px 0 14px;
    display: flex;
    flex-direction: column;
    border-top: 1px solid var(--sb-line);
}
.admin-sidebar-foot .admin-nav-link { font-size: 13.5px; color: var(--sb-ink-soft); }
.admin-sidebar-foot .admin-nav-link:hover,
.admin-sidebar-foot .admin-nav-link.is-active { color: #fff; }

.admin-main { flex: 1 1 auto; min-width: 0; }
.admin-main-inner { max-width: 1600px; margin: 0 auto; padding: 40px; }

/* ---- The top strip and the drawer, under 900px ---------------------------
   Above 900 the strip does not exist and the sidebar is the column above. */
.admin-topstrip { display: none; }
.admin-scrim { display: none; }
.admin-menu-toggle { display: none; }

/* ---- Admin headings / sections ---------------------------------------- */

.muted { color: var(--muted); }
.small { font-size: 12.5px; }
.btn-sm { padding: 9px 16px; font-size: 13px; }






/* Two columns down to 768, one below it. minmax(0, 1fr) rather than 1fr: a grid
   track's default min-width is auto, so a long email would push the field wider
   than the panel instead of the input scrolling inside it. */

.ordc-contact-acts .small { flex: 1 1 210px; min-width: 0; line-height: 1.5; }

@media (max-width: 767px) {
    /* Stack the row so the button is not squeezed to a sliver beside the label. */
    
    .ordc-form .btn { width: 100%; }
    .ordc-contact-acts .btn { width: 100%; }
}


/* ---- Stat grid --------------------------------------------------------- */
.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
    gap: 1px;
    background: var(--border);
    border: 1px solid var(--border);
    margin-bottom: 32px;
}
.stat { background: var(--surface); padding: 20px; display: flex; flex-direction: column; gap: 6px; }
a.stat:hover { background: var(--off-white); text-decoration: none; }
.stat-num { font-size: 26px; font-weight: 600; line-height: 1; }
.stat-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.07em; }

/* ---- Dashboard: site traffic panel ------------------------------------- */
.traffic-grid { margin-bottom: 20px; }
.traffic-lists { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
.traffic-list { min-width: 0; }
.traffic-rows { list-style: none; margin: 0; padding: 0; }
.traffic-row {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 13px;
}
.traffic-row:last-child { border-bottom: none; }
.traffic-row-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); }
.traffic-row-count { flex: 0 0 auto; color: var(--muted); }
@media (max-width: 640px) {
    .traffic-lists { grid-template-columns: 1fr; gap: 16px; }
}


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

.truncate { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.thumb {
    display: inline-flex;
    width: 40px; height: 40px;
    border: 1px solid var(--border);
    align-items: center; justify-content: center;
    background: var(--off-white);
    overflow: hidden;
}
.thumb img { width: 100%; height: 100%; object-fit: cover; filter: grayscale(1); }
.thumb-empty { color: var(--muted-3); }

/* ---- Badges ------------------------------------------------------------ */
.badge {
    display: inline-block;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 3px 8px;
    border: 1px solid var(--border-2);
    color: var(--muted);
}
.badge-pending { color: var(--black); border-color: var(--black); background: var(--off-white); }
.badge-admin { background: var(--black); color: #fff; border-color: var(--black); }

/* ---- Row actions / link buttons --------------------------------------- */
/* Action cells stay real table cells (never display:flex on a <td>, which pulls
   the cell out of the table layout and pushes the links past the right border).
   Lay the links/forms out inline instead, right-aligned within the cell. */

.inline-form { display: inline-block; margin: 0; vertical-align: middle; }
.link-action {
    background: none; border: none; padding: 0;
    font-size: 13px; color: var(--text-link);
    cursor: pointer; font-family: inherit;
    text-decoration: underline;
}
.link-action:hover { color: var(--muted); }
.link-danger { color: var(--text-link); }

/* Messages: bulk-delete bar + checkbox column */

.msg-check-col { width: 1%; white-space: nowrap; text-align: center; vertical-align: middle; }
.msg-check-col input[type="checkbox"] { margin: 0; vertical-align: middle; }

/* A disabled checkbox still has to read as a control, just an inert one. */
.check.is-disabled { opacity: .55; cursor: default; }
.check.is-disabled input { cursor: default; }

.check-standalone { margin-top: 28px; }
/* The product form's three-way "View in 3D" control: a fieldset without the
   UA's box, its three radios stacked close, the legend as a field label. */

.pf-3d .check-standalone { margin-top: 8px; }

/* Disclaimer editor rows */

.disc-row .field { margin-bottom: 0; }

/* Product cert checkboxes */

.cert-checks .check { margin-bottom: 0; }

/* Logo manager */

.logo-preview-light { background: var(--plate); }
.current-image { min-height: auto; padding: 12px; display: inline-flex; }
.current-image img { max-height: 80px; filter: grayscale(1); }

/* Users: accepted disclaimers */
.disc-cell { max-width: 260px; }
.disc-accepted { margin: 0; padding-left: 16px; font-size: 12px; color: var(--muted); }
.disc-accepted li { margin-bottom: 2px; }
.disc-gone { color: var(--muted-3); text-decoration: line-through; }

/* ---- Admin list filters (products, FAQs) -------------------------------
   Client-side only: the rows are already in the DOM, nothing reloads. */

[data-row][hidden] { display: none !important; }
[data-filter-section][hidden] { display: none !important; }


/* ---- Admin responsive -------------------------------------------------- */
@media (max-width: 900px) {
    /* Filters stack full width; nothing overflows at 320px. */
    

    .admin-shell { flex-direction: column; }
    /* THE STRIP: sticky, the hamburger at 44px and the logo beside it. */
    .admin-topstrip {
        display: flex; align-items: center; gap: 10px;
        position: sticky; top: 0; z-index: 40;
        height: 56px; padding: 0 10px;
        background: #1c1c1c;
    }
    .admin-menu-toggle {
        display: inline-flex; align-items: center; justify-content: center;
        width: 44px; height: 44px; min-width: 44px; min-height: 44px;
        padding: 0; border: 0; border-radius: 10px;
        background: transparent; color: #fff; cursor: pointer;
    }
    .admin-menu-toggle:hover { background: rgba(255, 255, 255, .06); }
    .admin-menu-toggle-ico { width: 22px; height: 22px; }
    .admin-topstrip-brand { display: flex; align-items: center; min-height: 44px; }
    .admin-topstrip-brand:hover { text-decoration: none; }
    .admin-topstrip-logo { height: 24px; width: auto; display: block; }
    /* THE DRAWER: the same sidebar, off the left edge until the hamburger.
       Same groups, same memory; admin.js closes it on any navigation. */
    .admin-sidebar {
        position: fixed; top: 0; left: 0; bottom: 0; z-index: 61;
        width: 290px; max-width: 86vw; height: 100dvh;
        transform: translateX(-102%);
        visibility: hidden;
        transition: transform .2s ease, visibility 0s linear .2s;
        box-shadow: none;
    }
    .admin-sidebar.is-open { transform: none; visibility: visible; transition: transform .2s ease, visibility 0s; box-shadow: 0 0 40px rgba(0, 0, 0, .45); }
    /* Everything a finger lands on in the drawer is 44 tall: group heads, the
       brand link, the rows. */
    .admin-navgroup-head { min-height: 44px; }
    .admin-brand { display: flex; align-items: center; min-height: 44px; }
    .admin-nav-link { min-height: 44px; }
    .admin-drawer-close {
        display: inline-flex; align-items: center; justify-content: center;
        width: 44px; height: 44px; padding: 0; border: 0; border-radius: 10px;
        background: transparent; color: #d8d9dd; cursor: pointer;
    }
    .admin-drawer-close:hover { background: rgba(255, 255, 255, .06); color: #fff; }
    .admin-drawer-close-ico { width: 20px; height: 20px; }
    .admin-brand-row { padding-right: 0; }
    .admin-scrim { display: block; position: fixed; inset: 0; z-index: 60; background: rgba(0, 0, 0, .5); }
    .admin-scrim[hidden] { display: none; }
    .admin-main-inner { padding: 24px 16px; }
    .disc-required, .check-standalone { margin-top: 0; }
}


/* Admin: monospace body textarea (login.body) + inline code in helper notes */
.mono-body { font-family: var(--font-mono); font-size: 13px; line-height: 1.55; }

/* =========================================================================
   Legal page rendered HTML (light background)
   ========================================================================= */
.legal-body-copy > :first-child { margin-top: 0; }
.legal-body-copy h1 { font-size: 26px; margin: 28px 0 12px; }
.legal-body-copy h2 { font-size: 21px; margin: 24px 0 10px; }
.legal-body-copy h3 { font-size: 17px; margin: 20px 0 8px; }
.legal-body-copy p { margin: 0 0 14px; }
.legal-body-copy ul, .legal-body-copy ol { margin: 0 0 16px; padding-left: 22px; }
.legal-body-copy li { margin-bottom: 6px; }
.legal-body-copy strong { font-weight: 600; }
.legal-body-copy a { color: var(--black); text-decoration: underline; }

/* =========================================================================
   WYSIWYG editor (Quill Snow) — monochrome admin overrides
   (style.css loads AFTER quill.snow.css, so these win)
   ========================================================================= */
.rich-field { margin-top: 2px; }
.ql-toolbar.ql-snow,
.ql-container.ql-snow { border-color: var(--border-2); border-radius: 0; }
.ql-toolbar.ql-snow { background: var(--off-white); }
.ql-container.ql-snow { background: var(--surface); }
.ql-editor {
    min-height: 220px;
    font-family: var(--font-ui);
    font-size: 15px;
    line-height: 1.6;
    color: var(--ink);
}
.ql-editor.ql-blank::before { color: var(--muted-2); font-style: normal; left: 14px; }

/* THE GHOST: what the page renders when this field is left empty, drawn greyed
   over the editing area. admin-editor.js moves it inside .ql-container (already
   position:relative) after Quill boots, so it lands over the text area without
   any measuring, and hides it as soon as anything is typed.

   Quill's own placeholder is a CSS content string and can only be one run of
   plain text. This is real HTML, so a shared block with headings and paragraphs
   reads as the page will render it rather than as one grey wall of words.

   inert and unselectable: it is not content, it is a preview of the absence. */
.rich-field.has-ghost .ql-editor.ql-blank::before { content: none; }
.rich-ghost {
    position: absolute;
    inset: 0;
    z-index: 1;
    overflow: auto;
    padding: 12px 15px;
    color: var(--muted-2);
    font-family: var(--font-ui);
    font-size: 14px;
    line-height: 1.55;
    pointer-events: none;
    user-select: none;
}
.rich-field.is-filled .rich-ghost { display: none; }
/* Taller than the box it is drawn in, and inert, so it cannot be scrolled to.
   The fade says there is more rather than ending mid-sentence as if that were
   the end of it. Set by admin-editor.js, which is the only thing that can
   measure it. */
.rich-field.is-clipped .rich-ghost {
    -webkit-mask-image: linear-gradient(180deg, #000 72%, transparent 100%);
    mask-image: linear-gradient(180deg, #000 72%, transparent 100%);
}
.rich-ghost h4 { margin: 0 0 4px; font-size: 14px; font-weight: 600; }
.rich-ghost h4 ~ h4 { margin-top: 12px; }
.rich-ghost p { margin: 0 0 9px; }
.rich-ghost p:last-child { margin-bottom: 0; }
.rich-ghost em { font-style: italic; }


/* Icons: muted by default, black on hover/active (kills Quill's blue accent) */
.ql-snow .ql-stroke { stroke: var(--muted); }
.ql-snow .ql-fill { fill: var(--muted); }
.ql-snow .ql-picker { color: var(--muted); }
.ql-snow.ql-toolbar button:hover,
.ql-snow.ql-toolbar button.ql-active,
.ql-snow .ql-picker-label:hover,
.ql-snow .ql-picker-item:hover,
.ql-snow .ql-picker-item.ql-selected { color: var(--black); }
.ql-snow.ql-toolbar button:hover .ql-stroke,
.ql-snow.ql-toolbar button.ql-active .ql-stroke,
.ql-snow .ql-picker-label:hover .ql-stroke,
.ql-snow .ql-picker-label.ql-active .ql-stroke,
.ql-snow .ql-picker-item:hover .ql-stroke,
.ql-snow .ql-picker-item.ql-selected .ql-stroke { stroke: var(--black); }
.ql-snow.ql-toolbar button:hover .ql-fill,
.ql-snow.ql-toolbar button.ql-active .ql-fill { fill: var(--black); }
.ql-snow .ql-picker.ql-expanded .ql-picker-label { border-color: var(--border-2); }
.ql-snow .ql-picker-options { border-color: var(--border-2); border-radius: 0; }
.ql-snow a { color: var(--black); }
.ql-snow .ql-tooltip {
    border-color: var(--border-2);
    border-radius: 0;
    box-shadow: 0 6px 18px rgba(10, 10, 10, 0.10);
    color: var(--ink);
}
.ql-snow .ql-tooltip input[type=text] { border-color: var(--border-2); border-radius: 0; }
.ql-snow .ql-tooltip a.ql-action,
.ql-snow .ql-tooltip a.ql-remove { color: var(--black); }


/* Category manager rows: name + colour + order + preview + usage */

.cat-fields .field { margin-bottom: 0; }

/* =========================================================================
   COMMERCE — cart / checkout / confirmation / account / admin orders
   ========================================================================= */

/* Status pills */
.st-pill { display: inline-block; font-size: 11px; font-weight: 600; letter-spacing: .03em; padding: 4px 10px; border-radius: 999px; white-space: nowrap; }
.st-amber  { background: var(--fill-24); color: var(--ink-44); }
.st-green  { background: var(--fill-01); color: #146b45; }   /* was #1b8a4e, 3.93:1 on its own tint; this is 5.85:1 in both modes */
.st-purple { background: var(--fill-07); color: var(--ink-28); }
.st-gray   { background: var(--fill-13); color: var(--ink-39); }
/* An order number is a single token and must never break across lines. */
.order-link { color: var(--text-2); font-weight: 700; white-space: nowrap; }

/* Orders list: internal payment-note flag + row quick-actions */
/* white-space: nowrap so the label can never break inside its own pill — in the
   118px Payment track it was rendering as a two-line lozenge 35px tall. */
.pay-note-flag { display: inline-block; margin-left: 6px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .03em; color: var(--ink-28); background: var(--fill-07); padding: 2px 7px; border-radius: 999px; vertical-align: middle; cursor: help; white-space: nowrap; }
.order-status-form { margin: 0; }
.status-select { font-size: 12.5px; padding: 6px 8px; border: 1px solid var(--border-2); background: var(--surface); color: var(--ink); border-radius: 7px; max-width: 160px; }
.status-select:focus { outline: none; border-color: var(--black); }
.order-status-form noscript button { margin-left: 8px; }

/* Admin danger zone (destructive actions) */

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

/* Cart */
.cart { display: grid; grid-template-columns: 1fr 320px; gap: 32px; align-items: start; }
.cart-line { display: grid; grid-template-columns: 76px 1fr auto; gap: 16px; align-items: center; padding: 18px 0; border-bottom: 1px solid var(--border); }
.cart-line-media { width: 76px; height: 76px; background: none; border-radius: 12px; display: flex; align-items: center; justify-content: center; overflow: hidden; }
/* The SKU fallback keeps a tile behind it; a cut-out vial does not. */
.cart-line-media:has(.cart-line-ph) { background: linear-gradient(170deg, var(--plate-a), var(--plate-b)); }
.cart-line-media picture { display: contents; }
.cart-line-media img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; border-radius: 12px; }
.cart-line-ph { color: #c9c9cd; font-size: 11px; }
.cart-line-name { margin: 0; font-weight: 600; font-size: 16px; color: var(--text-1); }
.cart-line-meta { margin: 3px 0 0; font-size: 12px; color: var(--muted-slate); }
.cart-line-unit { margin: 4px 0 0; font-size: 14px; color: var(--text-1); }
.cart-line-unit .muted { color: var(--muted-slate); }
/* Which discount is live on this line — quantity tier or code, never both. */
.cart-line-was { color: var(--ink-16); font-size: 13px; margin-right: 6px; }
.cart-line-save { margin: 3px 0 0; font-size: 12.5px; color: var(--ink-05); }
.cart-line-save .muted { color: var(--muted-slate); }
.cart-line-actions { display: flex; align-items: center; gap: 0 16px; margin-top: 0; flex-wrap: wrap; }
/* Update and Remove are the only two controls on a cart line and they measured
   16px tall, which is the text and nothing more. They stay text links; the box
   around them is the tap target. The margin above the row is folded into this
   height so the line does not grow by 36px. */
.cart-line-actions .link-action { display: inline-flex; align-items: center; min-height: 44px; }
.cart-qty-form { display: flex; align-items: center; gap: 8px; }
.cart-qty-label { font-size: 12px; color: var(--muted); margin: 0; }
.cart-qty { width: 64px; text-align: center; border: 1px solid var(--border-2); border-radius: 8px; padding: 7px; }
.cart-line-total { font-weight: 700; font-size: 16px; color: var(--text-1); }

/* BAC water offer on the cart: under the line items on desktop, above the summary
   when the grid stacks (the mobile rule below resets this placement). The "added"
   confirmation is row 3, the strip row 4: they shared row 3 and overprinted. */
.cart-bac-added { grid-column: 1; grid-row: 3; }
.cart-bac { grid-column: 1; grid-row: 4; margin-top: 20px; }
.cart-lines { grid-column: 1; grid-row: 1; }
/* Row 2 is the backorder notice. It is in the DOM between the lines and the offer
   already, but every other child of .cart is explicitly placed, so an auto-placed
   notice was pushed past them all and landed under "Frequently added". Naming its
   row is what puts it where the markup already says it goes. */
.cart-backorder-note { grid-column: 1; grid-row: 2; }
.cart-summary { grid-column: 2; grid-row: 1 / span 4; align-self: start; }
.cart-bac-form { margin: 0; }
.cart-bac-add {
    flex: 0 0 auto; border: none; border-radius: 7px; padding: 9px 16px;
    background: var(--btn-orange, var(--pcd-orange)); color: var(--btn-ink); font-size: 14px; font-weight: 600;
    font-family: inherit; cursor: pointer; white-space: nowrap;
    transition: background-color .16s ease;
}
.cart-bac-add:hover { background: var(--accent-blue); }
.cart-bac-added { margin: 20px 0 0; font-size: 13.5px; color: var(--ink-05); }

/* Admin settings: the BAC bundle size picker. A tick list rather than a dropdown,
   so more than one size can be offered. Scrolls once the catalogue outgrows the
   panel instead of pushing the rest of the tab down the page. */

.cart-summary { border: 1px solid var(--border); border-radius: 14px; padding: 22px; background: var(--surface); position: sticky; top: 100px; }
.cart-summary-row { display: flex; justify-content: space-between; font-size: 15px; }
.cart-summary-total { font-weight: 700; font-size: 17px; margin-bottom: 6px; }
.cart-summary-note { margin: 10px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--muted); }
/* THE LABEL, NOT THE BOX. .btn-auth pads 16px all round and the anchor's line box
   sits flush against the top padding, so the visible band of "Proceed to checkout"
   (cap height down to the baseline, no descenders in it) measured 22px from the top
   edge and 24px from the bottom: 1px high. The two paddings still add to 32px, so
   the button is exactly as tall as it was and nothing below it moves. Scoped to the
   cart's link. The login and checkout .btn-auth buttons are <button>s and are not
   touched.

   text-align, same reason. A <button> gets text-align:center from the UA sheet; this
   is an <a>, so it inherited start and the label sat hard against the left padding,
   36px off centre at desktop and 236px at 768. The other .btn-auth callers are real
   buttons and already centre themselves, so this belongs here and not on .btn-auth. */
.cart-checkout { margin-top: 16px; padding-top: 17px; padding-bottom: 15px; text-align: center; }
.cart-continue { display: block; text-align: center; margin-top: 12px; font-size: 14px; color: var(--muted); }

/* =========================================================================
   SHIPPING METHOD PICKER (checkout)
   ========================================================================= */
.ship-block { margin-top: 4px; }
.ship-label { display: block; margin-bottom: 10px; font-size: 11px; text-transform: uppercase; letter-spacing: .09em; color: var(--muted-ash); }
.ship-methods { display: flex; flex-direction: column; gap: 9px; }
.ship-row {
    display: flex; align-items: center; gap: 12px;
    min-height: 44px; padding: 14px; box-sizing: border-box;
    border: 1px solid var(--edge-15); border-radius: 11px; background: var(--surface);
    cursor: pointer; transition: border-color .12s ease, background .12s ease, box-shadow .12s ease;
}
.ship-row:hover { border-color: var(--selected-line); }
.ship-row input[type="radio"] { position: absolute; opacity: 0; width: 0; height: 0; }
.ship-row:has(input:checked) { border-color: var(--accent-blue); box-shadow: inset 0 0 0 1px var(--accent-blue); background: var(--selected-bg); }
.ship-row:has(input:focus-visible) { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
/* Only one method configured: no choice to make, so no radio is drawn. */
.ship-row.is-only { cursor: default; }
.ship-row.is-only:hover { border-color: var(--edge-15); }

.ship-dot {
    flex: 0 0 auto; width: 19px; height: 19px; border-radius: 50%;
    border: 2px solid var(--edge-27); background: var(--surface); position: relative;
}
.ship-row:has(input:checked) .ship-dot { border-color: var(--accent-blue); }
.ship-row:has(input:checked) .ship-dot::after {
    content: ""; position: absolute; inset: 3px; border-radius: 50%; background: #2E5BE0;
}

/* THE CARRIER PLATE ON EACH METHOD ROW (docs/reports/2026-09-18-cart-nav-fixes.md):
   the same white plate with the carrier's logo the tracking card and the shipping
   emails draw (public/assets/img/carriers/, carrier_logo_path). White in both
   themes on purpose: the logo file is transparent purple and orange, made for a
   light ground, and the plate is what the site already puts under it on dark. Its
   height is the two-line text block beside it (name over detail, 40px), centred
   on the row like the radio. */
.ship-plate {
    flex: 0 0 auto; align-self: center; width: 76px; height: 40px; border-radius: 6px;
    background: #fff; border: 1px solid #e3e5ea;
    display: inline-flex; align-items: center; justify-content: center; overflow: hidden;
}
.ship-plate-img { max-width: 60px; max-height: 24px; width: auto; height: auto; display: block; }
.ship-info { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ship-name { font-size: 15px; font-weight: 500; color: var(--text-1); }
.ship-detail { font-size: 13px; color: var(--muted-slate); }
.ship-price { flex: 0 0 auto; font-size: 16px; color: var(--text-1); white-space: nowrap; text-align: right; }
.ship-was { display: block; font-size: 12px; color: var(--ink-16); }
.ship-free { color: var(--ink-05); font-weight: 600; }

@media (max-width: 420px) {
    .ship-row { gap: 10px; padding: 12px; }
    .ship-plate { width: 62px; height: 36px; }
    .ship-plate-img { max-width: 50px; max-height: 20px; }
    .ship-name { font-size: 14px; }
    .ship-detail { font-size: 12px; }
    .ship-price { font-size: 15px; }
}

/* THE LIVE OVERNIGHT ROW (docs/mockups/fedex-storefront-mockup.html). Same row
   as the flat options; the "Live rate" chip on the name and the one-line note
   under the picker are the only additions. The row is [hidden] until
   checkout-overnight.js has a price for the zip, and [hidden] again on any
   failure, so a dead FedEx leaves the picker exactly as it was. */
.ship-row[hidden] { display: none; }
.ship-chip {
    display: inline-block; vertical-align: 2px; margin-left: 6px;
    font-size: 10.5px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
    background: var(--fill-08); color: var(--muted-slate); border-radius: 999px; padding: 3px 8px;
}
.ship-note { margin: 10px 0 0; font-size: 13px; line-height: 1.5; color: var(--muted-ash); }

/* =========================================================================
   ON-PAGE PAYMENT STEP (confirmation + account order)
   Brand COLOURS only — no provider logo, wordmark, or icon anywhere.
   ========================================================================= */
.orderpay-panel { display: block; }
.orderpay-due-label { margin: 0; font-size: 13px; color: var(--muted-ash); }
.orderpay-due-amount { margin: 2px 0 0; font-family: var(--font-mono); font-size: 28px; font-weight: 500; color: var(--text-1); }
.orderpay-ships-note { margin: 8px 0 0; font-size: 14px; color: var(--muted-slate); }

.orderpay-now-label { display: block; margin: 22px 0 10px; font-size: 11px; text-transform: uppercase; letter-spacing: .09em; color: var(--muted-ash); }
.orderpay-buttons { display: flex; flex-direction: column; gap: 9px; }
.orderpay-btn {
    display: flex; align-items: center; justify-content: center;
    width: 100%; box-sizing: border-box; min-height: 52px; padding: 0 18px;
    border: none; border-radius: 10px; text-decoration: none;
    font-size: 16px; font-weight: 600; font-family: inherit;
    cursor: pointer; transition: filter .12s ease;
}
.orderpay-btn:hover { filter: brightness(1.07); }
.orderpay-btn:focus-visible { outline: 3px solid var(--text-1); outline-offset: 2px; }
/* Configured but not live yet: visible, obviously inert, not clickable. */
.orderpay-btn.is-coming {
    background: var(--fill-33); color: var(--ink-16); cursor: not-allowed;
    font-weight: 500; filter: none;
}

/* "Pay now" in the order history list. A real tappable button, not a text link:
   44px minimum on a phone, where it is the full width of the card. */
.pay-now-link {
    display: inline-flex; align-items: center; justify-content: center;
    box-sizing: border-box; min-height: 44px; padding: 0 18px;
    border-radius: 10px; background: #2E5BE0; color: #fff;
    font-size: 15px; font-weight: 600; text-decoration: none;
}
.pay-now-link:hover { text-decoration: none; filter: brightness(1.07); }
.pay-now-link:focus-visible { outline: 3px solid var(--text-1); outline-offset: 2px; }

.orderpay-zelle { margin-top: 14px; border: 1px solid var(--edge-36); border-radius: 10px; padding: 14px 16px; }
.orderpay-zelle.is-coming { color: var(--ink-16); background: var(--fill-48); font-size: 14px; }
.orderpay-zelle-head { margin: 0 0 6px; font-size: 15px; font-weight: 600; color: var(--text-1); }
.orderpay-zelle-addr { margin: 0 0 8px; font-size: 16px; color: var(--text-1); word-break: break-all; }
.orderpay-zelle-note { margin: 0; font-size: 13px; line-height: 1.55; color: var(--muted-slate); }

/* Zelle carries neither an amount nor a reference, so the ID, the exact amount, and
   the memo are each shown with their own copy control rather than left to be typed
   from memory. Label left, value + Copy right; stacks on a narrow phone. */
.orderpay-zelle-dl { margin: 10px 0 12px; }
.orderpay-zelle-row {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: 6px 10px; padding: 9px 0; border-bottom: 1px solid var(--edge-42);
}
.orderpay-zelle-row:first-child { padding-top: 0; }
.orderpay-zelle-row:last-child { padding-bottom: 0; border-bottom: none; }
.orderpay-zelle-row dt { flex: 0 0 auto; font-size: 13px; color: var(--muted-ash); }
.orderpay-zelle-row dd {
    flex: 1 1 auto; min-width: 0; margin: 0;
    display: flex; align-items: center; justify-content: flex-end; gap: 10px;
}
.orderpay-zelle-val { min-width: 0; font-size: 15px; color: var(--text-1); word-break: break-all; text-align: right; }
.orderpay-copy {
    flex: 0 0 auto; min-height: 36px; padding: 0 12px;
    background: var(--surface); color: var(--text-1); border: 1px solid var(--edge-06); border-radius: 8px;
    font-size: 13px; font-family: inherit; cursor: pointer;
}
.orderpay-copy:hover { border-color: var(--text-1); }
.orderpay-copy:focus-visible { outline: 3px solid var(--text-1); outline-offset: 2px; }
.orderpay-copy.is-copied { border-color: #12703f; color: var(--ink-05); }
/* The Zelle account is registered to another legal entity, so the name the customer
   is about to see is stated plainly. Informational, not a warning. */
.orderpay-zelle-recipient { margin: 8px 0 0; font-size: 13px; line-height: 1.55; color: var(--muted-slate); }

@media (max-width: 480px) {
    .orderpay-zelle-row { flex-direction: column; align-items: stretch; gap: 4px; }
    .orderpay-zelle-row dd { justify-content: space-between; }
    .orderpay-zelle-val { text-align: left; }
}


.orderpay-reported-btn {
    display: block; width: 100%; min-height: 48px; padding: 0 18px;
    background: var(--surface); color: var(--text-1); border: 1px solid var(--edge-06); border-radius: 10px;
    font-size: 15px; font-weight: 600; font-family: inherit; cursor: pointer;
}
.orderpay-reported-btn:hover { border-color: var(--text-1); }
/* The "tell us which app" prompt. Replaces the optional <select> that let an order
   be reported with no method. .is-lead is the confirmation page, which is the first
   thing a customer sees after checkout and so states the requirement outright. */
.paystep-report { margin: 18px 0 0; }
.paystep-report-top { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; margin: 0 0 4px; }
.paystep-req {
    display: inline-flex; align-items: center; padding: 3px 9px; border-radius: 7px;
    background: #b4441f; color: #fff; font-size: 10.5px; font-weight: 700;
    text-transform: uppercase; letter-spacing: .06em;
}
.paystep-report-h { font-size: 15px; font-weight: 650; color: var(--text-1); }
.paystep-report-s { margin: 0 0 14px; font-size: 13.5px; line-height: 1.55; color: var(--muted-slate); }
/* Confirmation page: a bordered panel rather than a line of text, so it reads as a
   step that is still outstanding instead of an afterthought under the buttons. */
.paystep-report.is-lead {
    border: 2px solid #b4441f; border-radius: 12px; padding: 16px; background: var(--fill-66);
}
.paystep-report.is-lead .paystep-report-h { font-size: 16px; }
.paystep-report.is-lead .orderpay-reported-btn { background: #111418; color: #fff; border-color: var(--text-1); }
.paystep-report.is-lead .orderpay-reported-btn:hover { background: #000; }
/* A refused report. Solid, no tint games, and it sits above the prompt it belongs to. */
.paystep-err, .opay-err {
    margin: 0 0 14px; padding: 12px 14px; border-radius: 10px;
    background: #b4441f; color: #fff; font-size: 13.5px; line-height: 1.55; font-weight: 500;
}
.orderpay-reported { margin: 18px 0 0; font-size: 14px; font-weight: 500; color: var(--ink-05); }
.orderpay-status-note { margin: 12px 0 0; font-size: 13px; color: var(--muted-ash); }

/* ---- Cash App order-number reminder dialog ---- */
.orderpay-dialog[hidden] { display: none; }
.orderpay-dialog { position: fixed; inset: 0; z-index: 9500; display: flex; align-items: center; justify-content: center; padding: 16px; }
.orderpay-dialog-backdrop { position: absolute; inset: 0; background: rgba(17, 20, 24, .55); }
.orderpay-dialog-panel {
    position: relative; width: 100%; max-width: 380px; box-sizing: border-box;
    background: var(--surface); border-radius: 14px; padding: 22px;
    box-shadow: 0 18px 50px rgba(0, 0, 0, .28);
}
.orderpay-dialog-h { margin: 0 0 10px; font-size: 19px; font-weight: 700; color: var(--text-1); }
.orderpay-dialog-body { margin: 0 0 14px; font-size: 14px; line-height: 1.55; color: var(--muted-slate); }
.orderpay-dialog-order {
    margin: 0 0 18px; padding: 12px; text-align: center;
    border: 1px solid var(--edge-32); border-radius: 10px;
    font-size: 20px; font-weight: 600; color: var(--text-1); word-break: break-all;
}
.orderpay-dialog-actions { display: flex; gap: 9px; }
.orderpay-dialog-back, .orderpay-dialog-go {
    flex: 1 1 0; min-width: 0; min-height: 44px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 10px; font-size: 15px; font-weight: 600; font-family: inherit;
    text-decoration: none; cursor: pointer; box-sizing: border-box;
}
.orderpay-dialog-back { background: var(--surface); color: var(--text-1); border: 1px solid var(--edge-06); }
/* Fallback only: the real pair is set inline from data-pay-go-bg / -fg, which
   come from payment_buttons() and so from PaymentMethods::ALL. */
.orderpay-dialog-go   { background: #00D54B; color: var(--ink-34); border: none; }
body.orderpay-dialog-lock { overflow: hidden; }

@media (max-width: 400px) {
    .orderpay-dialog-panel { padding: 18px; }
    .orderpay-dialog-order { font-size: 18px; }
    .orderpay-dialog-back, .orderpay-dialog-go { font-size: 14px; }
}


/* Discount code UI (cart + checkout) */
.discount-row { color: #1b8a4e; }

/* =========================================================================
   FREE-GIFT THRESHOLD (cart only)
   Green line + progress bar, per docs/mockups/loyalty-subscribe-mockups.html.
   Every rule here is inert unless the offer is switched on: the view emits none
   of these classes when it is off.
   ========================================================================= */
/* The earned line. Green because it is a gain, and it sits in the same summary
   column as the discount rows so the eye reads one list of what came off. */
.cart-summary-row.gift-row { color: var(--ink-03); font-weight: 600; }
.cart-summary-row.gift-row .gift-ico { margin-right: 6px; }

/* The line in the item list, when the gift has been added. */
.cart-line.is-gift .cart-line-total { color: var(--ink-03); }
.cart-gift-tag {
    display: inline-block; font-size: 12px; font-weight: 700; letter-spacing: .04em;
    text-transform: uppercase; color: var(--ink-03);
    background: rgba(31, 168, 99, .10); border-radius: 999px; padding: 4px 10px;
}

/* The nudge, below the threshold. Quiet: it is an offer, not an alert. */
.cart-gift-nudge { margin: 14px 0 4px; }
.cart-gift-nudge-t { margin: 0 0 8px; font-size: 13.5px; color: var(--muted); }
.cart-gift-bar {
    height: 8px; background: rgba(0, 0, 0, .08); border-radius: 4px; overflow: hidden;
}
.cart-gift-bar i {
    display: block; height: 100%; border-radius: 4px;
    background: linear-gradient(90deg, var(--bar-blue), var(--pill-green));
}
/* Qualified but nothing to give. Same muted register as the nudge: it is not the
   customer's problem to solve and must not read as an error. */
.cart-gift-note { margin: 14px 0 4px; font-size: 13.5px; color: var(--muted); }

/* Admin: the same gift, on the order view and the packing slip (the order
   editor's own rule is with .ordedit-line). */

.packing-slip .gift, .nm .gift { font-weight: 700; }
.discount-row span { color: #1b8a4e; }
.discount-form { margin: 14px 0 6px; padding: 14px 0 4px; border-top: 1px solid var(--border); }
.checkout-discount { margin-top: 8px; }
.discount-label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 7px; color: var(--ink); }
.discount-input-row { display: flex; gap: 8px; }
.discount-input { flex: 1; min-width: 0; background: var(--surface); border: 1px solid var(--border-2); padding: 10px 12px; color: var(--ink); text-transform: uppercase; }
.discount-input:focus { outline: none; border-color: var(--black); }
.discount-apply { flex: none; }
.discount-applied { color: #1b8a4e; margin: 8px 0 0; }
.discount-error { margin: 8px 0 0; }
.discount-remove { margin-top: 6px; }

/* Referral discount. The pill marks an applied code as the one the visitor's link
   carried; the offer line appears only when swapping would genuinely save more.
   Neither ever names the owner. */
.ref-pill {
    display: inline-block; margin-left: 6px; padding: 2px 8px; border-radius: 7px;
    background: #2E5BE0; color: #fff; font-size: 10.5px; font-weight: 700;
    text-transform: uppercase; letter-spacing: .05em; vertical-align: 1px;
}
.ref-offer {
    display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
    margin: 14px 0 0; padding: 12px 14px; border: 1px solid var(--edge-13); border-radius: 10px;
    background: var(--fill-47);
}
.ref-offer-t { margin: 0; font-size: 13px; line-height: 1.5; color: var(--ink-02); min-width: 0; }
.ref-offer-amt { color: #1b8a4e; font-weight: 650; }
.ref-offer-btn {
    flex: 0 0 auto; min-height: 40px; padding: 0 16px; border: none; border-radius: 9px;
    background: #2E5BE0; color: #fff; font-family: inherit; font-size: 13px; font-weight: 650;
    cursor: pointer;
}
.ref-offer-btn:hover { background: #264fc4; }
.ref-offer-btn:focus-visible { outline: 2px solid #2E5BE0; outline-offset: 2px; }
.ref-offer .inline-form { margin: 0; flex: 0 0 auto; }

@media (max-width: 480px) {
    /* Stacked, with the button full width, so the tap target does not shrink to a
       sliver beside two lines of wrapped text. */
    .ref-offer { flex-direction: column; align-items: stretch; gap: 10px; }
    .ref-offer-btn { width: 100%; }
    .ref-offer .inline-form { width: 100%; }
}


/* Checkout */
.checkout { display: grid; grid-template-columns: 1fr 360px; gap: 32px; align-items: start; }
.checkout-block { margin-bottom: 24px; }
.checkout-h2 { font-size: 16px; margin: 0 0 14px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
.checkout-billing.is-hidden { display: none; }
.checkout-summary { border: 1px solid var(--border); border-radius: 14px; padding: 22px; background: var(--surface); position: sticky; top: 100px; }
.summary-lines { margin-bottom: 12px; }
.summary-line { display: flex; justify-content: space-between; font-size: 13.5px; padding: 5px 0; color: var(--muted); gap: 12px; }
.summary-line-name { color: var(--ink); }
.summary-row { display: flex; justify-content: space-between; font-size: 14.5px; padding: 6px 0; }
.summary-grand { font-weight: 700; font-size: 17px; border-top: 1px solid var(--border); margin-top: 6px; padding-top: 12px; }
.checkout-place { margin-top: 16px; }

/* PCD Credit at checkout. The tick box sits UNDER the totals rather than among
   them: the rows above are what the order costs, this is a decision about how to
   pay for it, and mixing a control into a column of figures is how a customer
   reads a checkbox as another charge. The green matches the discount rows, which
   is the existing language for "this took money off". */
.ck-credit { margin-top: 14px; padding: 12px 14px; border: 1px solid var(--border);
    border-radius: 10px; background: var(--fill-55); }
.ck-credit-lab { display: flex; gap: 10px; align-items: flex-start; cursor: pointer; margin: 0; }
.ck-credit-lab input { margin-top: 3px; width: 17px; height: 17px; accent-color: var(--accent-green); flex: 0 0 auto; }
.ck-credit-lab > span { display: block; font-size: 14px; line-height: 1.45; }
.ck-credit-note { display: block; color: var(--muted); font-size: 12.5px; font-weight: 400; margin-top: 2px; }

/* Google Places autocomplete dropdown (classic widget). Restyled to match the site:
   the widget attaches .pac-container to <body>, so it is never clipped by container
   overflow; a high z-index keeps it above the sticky summary. The "powered by Google"
   row is required attribution, so it is kept and styled around, not hidden. */
.pac-container {
    z-index: 4000;
    margin-top: 6px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 10px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.10);
    font-family: 'Inter', system-ui, sans-serif;
    overflow: hidden;
}
.pac-item {
    padding: 10px 14px;
    font-size: 14px;
    line-height: 1.4;
    color: var(--ink);
    border-top: 1px solid var(--edge-48);
    cursor: pointer;
}
.pac-item:first-child { border-top: none; }
.pac-item:hover,
.pac-item-selected,
.pac-item-selected:hover { background: var(--fill-37); }
.pac-item-query { font-size: 14px; color: var(--ink); }
.pac-matched { font-weight: 600; }
.pac-icon { margin-top: 2px; }
/* Attribution row spacing (kept per Google terms). */
.pac-logo:after { margin: 8px 14px; opacity: 0.7; }

/* =========================================================================
   ORDER CONFIRMATION, desktop two column (approved mockup).
   Left: pay, then what was bought. Right: invoice, address, email note.
   The payment block leads because it is the only thing this page is for; it
   used to sit under the invoice preview and the summary, about a screen down.
   ========================================================================= */
.conf-page { max-width: 1200px; }

.conf-head { display: flex; align-items: center; gap: 13px; margin-bottom: 7px; }
.conf-tick { width: 30px; height: 30px; border-radius: 50%; background: var(--pill-green); color: var(--pill-ink); display: flex; align-items: center; justify-content: center; flex: 0 0 auto; }
.conf-tick svg { width: 17px; height: 17px; }
.conf-h1 { font-size: 28px; font-weight: 600; margin: 0; letter-spacing: -0.01em; }
.conf-sub { font-size: 14px; color: var(--muted); margin: 0 0 24px; }
.conf-sub b { color: var(--ink); font-weight: 600; }
.conf-sub code { font-size: 13.5px; }

/* 1.25fr / 1fr: the payment block and the summary need the width, the invoice
   preview and the address do not. minmax(0,…) so a long product name cannot
   push its track wider than its share. */
.conf-cols { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: 20px; align-items: start; }
.conf-col { min-width: 0; }

/* ---- The payment block --------------------------------------------------- */
.conf-paybox { background: #101319; border-radius: 12px; padding: 22px 24px; margin-bottom: 16px; }
.conf-paytop { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 6px; flex-wrap: wrap; }
.conf-lbl { font-size: 12px; color: var(--ink-10); letter-spacing: 0.1em; text-transform: uppercase; }
.conf-due { font-size: 40px; font-weight: 600; color: #fff; line-height: 1.12; margin: 0 0 6px; }
.conf-note { font-size: 13px; color: var(--ink-10); margin: 0 0 20px; line-height: 1.6; }

/* The 48 hour clock. Ticked by the shared assets/js/countdown.js, which sets the
   state class on this element; the four colourways below match the pay page. */
.conf-cd { display: inline-flex; align-items: center; gap: 7px; background: rgba(245, 197, 24, 0.15); color: #f5c518; font-size: 12px; padding: 6px 12px; border-radius: 999px; white-space: nowrap; }
.conf-cd svg { width: 13px; height: 13px; flex: 0 0 auto; }
.conf-cd-t { font-variant-numeric: tabular-nums; }
.conf-cd.is-warn { background: rgba(245, 197, 24, 0.15); color: #f5c518; }
.conf-cd.is-urgent { background: rgba(224, 90, 74, 0.18); color: var(--ink-106); }
.conf-cd.is-past { background: rgba(224, 90, 74, 0.22); color: var(--ink-104); }

/* Two by two. Only configured methods render, so this is a 3-up or 2-up when a
   provider is switched off rather than a hole where a button should be. */
.conf-paygrid { display: grid; grid-template-columns: 1fr 1fr; gap: 11px; }
.conf-pay { font-size: 14px; font-weight: 600; text-align: center; padding: 14px; border-radius: 8px; text-decoration: none; display: block; }
.conf-pay:hover { filter: brightness(1.06); }
/* NO PER-PROVIDER COLOUR RULES HERE ON PURPOSE. Each button's background and
   foreground are set inline from PaymentMethods::ALL, the same table the pay page
   paints from. Duplicating the palette in this stylesheet is what let the two
   pages disagree about Cash App's green in the first place. */
.conf-pay.is-coming { background: #23262f; color: var(--muted-warm); cursor: default; }
.conf-zelle { display: flex; align-items: baseline; gap: 10px; margin: 16px 0 0; color: #fff; font-size: 13px; flex-wrap: wrap; }

/* ---- Panels -------------------------------------------------------------- */
.conf-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 18px 20px; }
.conf-panel + .conf-panel, .conf-paybox + .conf-panel { margin-top: 16px; }
.conf-panel-t { font-size: 14px; font-weight: 600; margin: 0 0 14px; }

/* The rule under a line item is an inset positioned hairline, not a
   border-bottom: a border paints one side of the box, which the house rules
   forbid. left/right/bottom pinned, so it spans the row exactly. */
/* SCOPED TO .conf-page. A plain .conf-item rule already exists further down for
   the account order and admin order pages, and it sits LATER in the file, so at
   equal specificity it would win over this one. The page scope outranks it. */
.conf-page .conf-item { position: relative; display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding: 0 0 12px; margin-bottom: 12px; font-size: 13px; }
.conf-page .conf-item::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--border); }
.conf-page .conf-item-n span { color: var(--muted); }
.conf-page .conf-item-v { font-size: 13px; }

.conf-line { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; color: var(--ink-2, var(--ink-59)); padding: 5px 0; }
.conf-line span:last-child { font-family: var(--font-mono); }
.conf-line.is-discount { color: var(--ink-03); }
/* Same reasoning as .conf-item: an inset hairline, not a border-top. */
.conf-line.is-total { position: relative; font-size: 15px; font-weight: 600; color: var(--ink); padding-top: 12px; margin-top: 8px; }
.conf-line.is-total::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 1px; background: var(--border); }

.conf-ref { font-size: 13px; color: var(--muted); margin: 0 0 12px; }
.conf-paylist { margin: 0; }
.conf-payrow { position: relative; display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 12px; padding: 9px 0; font-size: 13px; }
.conf-payrow + .conf-payrow::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 1px; background: var(--border); }
.conf-payrow dt { color: var(--muted); margin: 0; }
.conf-payrow dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; }

.conf-invhead { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.conf-invhead .conf-panel-t { margin: 0; }
.conf-dl { background: var(--accent-blue); color: #fff; font-size: 12px; font-weight: 600; padding: 9px 14px; border-radius: 7px; text-decoration: none; white-space: nowrap; }
.conf-dl:hover { background: #244cc0; }
.conf-invprev { display: block; width: 100%; height: 230px; border: 0; border-radius: 8px; background: var(--fill-40); }
.conf-invfallback { padding: 16px; font-size: 13px; color: var(--muted); }

.conf-addr { font-style: normal; font-size: 13px; color: var(--ink); line-height: 1.75; }
.conf-emailnote { background: var(--fill-19); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; margin: 16px 0 0; font-size: 13px; color: var(--ink); line-height: 1.65; }
.conf-account { display: inline-block; margin-top: 16px; font-size: 13.5px; font-weight: 600; color: var(--link); }

/* Desktop only, per the brief. Below 900 the two columns become one and the
   blocks stack in document order, which already leads with payment. This block
   sits later in the file than the rules above, which is what lets it win: a
   media query adds no specificity of its own. */
@media (max-width: 900px) {
    .conf-cols { grid-template-columns: 1fr; gap: 16px; }
    .conf-paygrid { grid-template-columns: 1fr; }
    .conf-h1 { font-size: 22px; }
    .conf-due { font-size: 32px; }
    .conf-payrow { grid-template-columns: 1fr; gap: 2px; }
}


/* Confirmation + order detail */
.conf-check { display: inline-flex; align-items: center; justify-content: center; width: 52px; height: 52px; border-radius: 50%; background: var(--fill-01); color: #1b8a4e; font-size: 26px; margin-bottom: 14px; }
.conf-number { font-size: 16px; margin: 0 0 8px; }
.conf-section { margin-top: 28px; }
.conf-h2 { font-size: 16px; margin: 0 0 14px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
.conf-item { display: flex; justify-content: space-between; padding: 7px 0; font-size: 14.5px; gap: 16px; }
.conf-totals { margin-top: 12px; max-width: 360px; }
.conf-address { font-style: normal; font-size: 14.5px; line-height: 1.6; color: var(--ink); }
.pay-list { margin: 14px 0 0; }
.pay-row { display: grid; grid-template-columns: 170px 1fr; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--border); font-size: 14.5px; }
.pay-row dt { color: var(--muted); font-weight: 500; margin: 0; }
.pay-row dd { margin: 0; color: var(--ink); }

/* Account order detail: timeline + tracking */
.order-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.order-head-num { font-size: 0.62em; color: var(--muted); }
.timeline { list-style: none; display: flex; margin: 24px 0; padding: 0; }
.timeline-step { flex: 1; text-align: center; position: relative; }
.timeline-step::before { content: ""; position: absolute; top: 8px; left: -50%; width: 100%; height: 2px; background: var(--border); z-index: 0; }
.timeline-step:first-child::before { display: none; }
.timeline-dot { position: relative; z-index: 1; display: block; width: 18px; height: 18px; border-radius: 50%; background: var(--surface); border: 2px solid var(--border-2); margin: 0 auto 8px; }
.timeline-step.is-done .timeline-dot { background: var(--accent-blue); border-color: var(--accent-blue); }
.timeline-step.is-done::before { background: var(--accent-blue); }
.timeline-label { display: block; font-size: 13px; font-weight: 600; color: var(--muted-2); }
.timeline-step.is-done .timeline-label { color: var(--ink); }
.timeline-date { display: block; color: var(--muted-2); }
/* THE SHIPPING PLATE (docs/mockups/admin-cleanup-mockup.html, section 4).
   Logo on a white plate, carrier and number beside it, Track package on the
   right. Shared by the customer order page, the pay page and the admin order
   page's shipment history; the emails carry a table-built copy of the same
   plate (EmailLayout::carrierPlate). The tracking box is now just the plate's
   outer margin. */
.tracking-box { margin: 8px 0 20px; }
.tracking-box p { margin: 0; }
.carrier-plate {
    display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
    background: #f6f7f9; border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px;
    color: var(--ink);
}
.carrier-logo {
    width: 92px; height: 36px; flex: none; border-radius: 6px; background: #fff;
    border: 1px solid #e3e5ea; display: inline-flex; align-items: center; justify-content: center; overflow: hidden;
}
.carrier-logo-img { max-width: 76px; max-height: 26px; width: auto; height: auto; display: block; }
.carrier-logo-txt { font-size: 12px; font-weight: 700; color: #54545a; }
.carrier-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 200px; }
.carrier-service { font-size: 14px; font-weight: 700; color: var(--ink); }
.carrier-number { font-size: 13px; color: var(--muted); overflow-wrap: anywhere; }
.carrier-extra { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; }
.carrier-track {
    margin-left: auto; flex: none; display: inline-flex; align-items: center; justify-content: center;
    min-height: 44px; padding: 0 16px; border-radius: 8px;
    background: #2e5be0; color: #ffffff; font-weight: 700; font-size: 13.5px; text-decoration: none;
}
.carrier-track:hover { background: #2450c4; color: #fff; text-decoration: none; }
/* On the dark catalogue pages the plate keeps its light surface: it is the
   mockup's plate on both the light account page and the dark email. */
.catalog-dark .carrier-plate { background: #181d30; border-color: #2a3047; color: #ececee; }
.catalog-dark .carrier-service { color: #fff; }
.catalog-dark .carrier-number { color: #b3b7bf; }
.catalog-dark .carrier-logo { border-color: #3a3f52; }
/* The admin's despatch history: one plate per shipment under its line. */
.ful-hist-i .carrier-plate { margin-top: 8px; }
.admin-body .carrier-track { min-height: 40px; }
@media (pointer: coarse) { .admin-body .carrier-track { min-height: 44px; } }

/* Keeps the carrier/number/status lines together as one block so the button
   stays on the right of the row rather than being spread by justify-content.
   The 260px basis is what makes the button drop to its own line on a narrow
   phone instead of squeezing the status into a four-line column: the status can
   now be the carrier's own sentence, not just "In transit". */
.tracking-info { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 260px; }
.tracking-ident { overflow-wrap: anywhere; }
.tracking-status { font-weight: 600; color: var(--ink); line-height: 1.45; overflow-wrap: anywhere; }
/* "No detailed status yet." Deliberately quieter than a real status: it is the
   absence of news, not a state to read as progress. */
.tracking-note { line-height: 1.5; }

/* =========================================================================
   FEDEX TRACKING CARD (account order page, partials/fedex_track_card.php)
   docs/mockups/fedex-storefront-mockup.html. The plate is the carrier logo on
   the same white tile the shipping plate uses; the pill is one of the site's
   four tinted pills (green moving or delivered, amber label created, red
   exception, grey unknown); the timeline is the scan history newest first.
   Every colour is a theme token, so the card follows the theme toggle.
   ========================================================================= */
.fxt { margin: 8px 0 20px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); overflow: hidden; }
.fxt-h { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 14px 18px; border-bottom: 1px solid var(--border); }
.fxt-plate {
    width: 92px; height: 36px; flex: none; border-radius: 6px; background: #fff; border: 1px solid #e3e5ea;
    display: inline-flex; align-items: center; justify-content: center; overflow: hidden;
}
.fxt-logo { max-width: 76px; max-height: 26px; width: auto; height: auto; display: block; }
.fxt-plate-txt { font-size: 12px; font-weight: 700; color: #54545a; }
.fxt-num { font-size: 14px; color: var(--muted-slate); overflow-wrap: anywhere; min-width: 0; }
.fxt-num a { color: var(--link); }
.fxt-pill { margin-left: auto; font-size: 13px; font-weight: 600; line-height: 1.2; border-radius: 999px; padding: 6px 14px; white-space: nowrap; }
.fxt-pill.is-green { background: var(--fill-12); color: var(--ink-05); }
.fxt-pill.is-amber { background: var(--fill-24); color: var(--pill-amber-ink); }
.fxt-pill.is-red   { background: var(--pill-red-bg); color: var(--ink-32); }
.fxt-pill.is-grey  { background: var(--fill-08); color: var(--muted-slate); }
.fxt-eta { padding: 12px 18px; border-bottom: 1px solid var(--border); font-size: 14.5px; line-height: 1.5; color: var(--muted-slate); }
.fxt-eta b { color: var(--text-1); font-weight: 600; }
.fxt-tl { list-style: none; margin: 0; padding: 18px 18px 20px; }
.fxt-tl li { position: relative; padding: 0 0 18px 30px; }
.fxt-tl li:last-child { padding-bottom: 0; }
.fxt-tl li::before { content: ""; position: absolute; left: 7px; top: 18px; bottom: -2px; width: 2px; background: var(--border); }
.fxt-tl li:last-child::before { display: none; }
.fxt-dot { position: absolute; left: 0; top: 3px; width: 16px; height: 16px; border-radius: 50%; background: var(--surface); border: 2px solid var(--edge-27); }
.fxt-tl li.is-now .fxt-dot::after { content: ""; position: absolute; inset: 4px; border-radius: 50%; background: var(--surface); }
.fxt-tl b { display: block; font-size: 14.5px; font-weight: 600; line-height: 1.35; color: var(--text-1); overflow-wrap: anywhere; }
.fxt-tl small { display: block; font-size: 13px; line-height: 1.4; color: var(--muted-ash); margin-top: 1px; overflow-wrap: anywhere; }
.fxt-tl .fxt-when { font-size: 12.5px; font-weight: 500; color: var(--muted-slate); }
/* The newest scan takes the pill's colour, so the card reads as one state. */
.fxt.t-green .fxt-tl li.is-now .fxt-dot { border-color: var(--ink-05); background: var(--ink-05); }
.fxt.t-green .fxt-tl li.is-now b { color: var(--ink-05); }
.fxt.t-amber .fxt-tl li.is-now .fxt-dot { border-color: var(--pill-amber-ink); background: var(--pill-amber-ink); }
.fxt.t-amber .fxt-tl li.is-now b { color: var(--pill-amber-ink); }
.fxt.t-red .fxt-tl li.is-now .fxt-dot { border-color: var(--ink-32); background: var(--ink-32); }
.fxt.t-red .fxt-tl li.is-now b { color: var(--ink-32); }
.fxt.t-grey .fxt-tl li.is-now .fxt-dot { border-color: var(--muted-slate); background: var(--muted-slate); }
.fxt-empty { margin: 0; padding: 16px 18px; font-size: 14px; color: var(--muted-slate); }
.fxt-f { padding: 12px 18px; border-top: 1px solid var(--border); font-size: 12.5px; line-height: 1.4; color: var(--muted-ash); }
@media (max-width: 480px) {
    .fxt-h { gap: 10px; padding: 12px 14px; }
    .fxt-pill { margin-left: 0; order: 5; }
    .fxt-eta, .fxt-f, .fxt-empty { padding-left: 14px; padding-right: 14px; }
    .fxt-tl { padding: 16px 14px 18px; }
}

.btn-sm-blue { display: inline-flex; align-items: center; gap: 6px; background: var(--accent-blue); color: #fff; border: none; border-radius: 9px; padding: 10px 18px; font-size: 13.5px; font-weight: 600; }
.btn-sm-blue:hover { background: #264fc4; text-decoration: none; }
.order-cols { display: grid; grid-template-columns: 1.4fr 1fr; gap: 32px; align-items: start; }

/* Admin orders */
.order-filters { margin-top: 6px; }
.order-cust-email { display: block; color: var(--muted-2); }
.pager { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 18px; flex-wrap: wrap; }



/* ---- THE ORDER PAGE GRID (2026-09-15) ---------------------------------------
   3:2, editor left (it carries a five-column grid of its own and needs the
   room), fulfilment right. The split is a CONTAINER query on the grid's own
   content area, the same way the editor card measures itself: the admin
   sidebar takes 250px, so the viewport says nothing useful. Under 900px of
   content width the two column wrappers dissolve (display: contents) and every
   card spans the grid in a stated order, editor, fulfilment, refunds, customer,
   then the rest, rather than the left column's whole stack ahead of the
   right's. The wrappers are still there in the markup and still the flex
   stacks above 900, so no form moved and no block was rewritten. */

@container (max-width: 899.98px) {
    .admin-order-col { display: contents; }
    .admin-order-col > * { grid-column: 1 / -1; min-width: 0; }
    /* The one-field outcome form is empty on screen; as a grid item it would be
       an empty row and a gap. Its hidden inputs still post. */
    .admin-order-col > .ful-outcome-form { display: contents; }
    .admin-order-col > .ordedit { order: 1; }
    .admin-order-col > .admin-order-formcol { order: 2; }
    .admin-order-col:first-child > :nth-child(2) { order: 3; }   /* Refunds */
    .admin-order-col > #order-customer { order: 4; }
    .admin-order-col > #order-merge { order: 5; }
    .admin-order-col:first-child > :nth-child(3) { order: 6; }   /* Ship to */
    .admin-order-col:first-child > :nth-child(4) { order: 7; }   /* Documents and email */
    .admin-order-col:last-child > :nth-last-child(2) { order: 8; } /* History */
    .admin-order-col > .admin-danger { order: 9; }
}





/* One line per fulfilment event under the slip buttons, for reprinting an
   earlier box's slip. */
/* The order editor's per-line notes under the line total: the stored price
   against the catalogue's, and a line the order's code was left off. */
/* Plain muted text: the generic `.ordedit-total small` rule above strikes the
   "List $x" figure through, which is right for a superseded list price and
   wrong for a note stating what the line sold at. */

.btn.btn-blue { background: var(--accent-blue); border-color: var(--accent-blue); color: #fff; }
.btn.btn-blue:hover { background: #264fc4; }


/* Admin users: tax-exempt cell */
.tax-form { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.tax-check { margin-bottom: 0; font-size: 12px; white-space: nowrap; gap: 7px; }
.tax-cert { width: 120px; padding: 7px 9px; border: 1px solid var(--border-2); border-radius: 8px; font-size: 12px; }

/* Commerce responsive */
@media (max-width: 900px) {
    .cart, .checkout, .order-cols { grid-template-columns: 1fr; }   /* .admin-order-cols measures its own width above */
    /* Status and carrier stack too; two dropdowns side by side at 360 is unusable. */
    .cart-summary, .checkout-summary { position: static; }
    /* Single column: drop the explicit placement so DOM order rules, which puts the
       offer above the summary. */
    .cart-lines, .cart-backorder-note, .cart-bac-added, .cart-bac, .cart-summary { grid-column: 1; grid-row: auto; }
}


/* =========================================================================
   PRODUCT DETAIL PAGE
   ========================================================================= */
.pd { max-width: 1360px; margin: 0 auto; padding: 36px 40px 60px; }
.pd-crumb { font-size: 13px; color: var(--muted); margin-bottom: 22px; }
.pd-crumb a { color: var(--muted); }
.pd-crumb a:hover { color: var(--text-2); }
.pd-crumb span { color: var(--muted-3); margin: 0 2px; }
.pd-crumb-current { color: var(--text-2); }

/* Two equal columns, per the approved mockup. minmax(0,1fr) rather than 1fr so a
   long spec value or a wide certificate row cannot push its track wider than half
   the page: a grid track's default min-width is auto, not zero. */
.pd-top { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 24px; align-items: start; }

/* The two column area holds ONLY the image and the buy box now, so the two
   columns end at roughly the same height and no empty column is left behind.
   Everything that used to stack under the image runs full width below .pd-top. */
.pd-gallery { min-width: 0; }
.pd-top { margin-bottom: 18px; }

/* ---- Full width below the buy box --------------------------------------- */

/* The certificate row, when the selected size has one. */
.pd-coa-row { margin-bottom: 16px; }

/* Trust row. Three cards: purity testing, free shipping, support. Absorbs the
   two standalone cards that used to sit under the image. */
.pd-trustrow { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
.pd-trustcard {
    display: flex; align-items: center; gap: 12px; min-width: 0;
    background: var(--surface); border: 1px solid var(--line-soft); border-radius: 10px;
    padding: 15px 17px;
}
.pd-trustcard > svg { width: 20px; height: 20px; color: var(--link); flex: 0 0 auto; }
.pd-trustcard-t { margin: 0; font-size: 13.5px; font-weight: 600; color: var(--text-2); }
.pd-trustcard-s { margin: 2px 0 0; font-size: 12.5px; color: var(--muted); }

/* Tabs and accordions side by side. Half the vertical space of stacking them. */
.pd-info { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; margin-bottom: 16px; align-items: start; }
.pd-info > .pd-tabs-panel, .pd-info > .pd-accordions { min-width: 0; }

/* Gallery */
/* TRANSPARENT VIALS (2026-09-12-vial-v2.md, framed again in -image-frame.md).
   Product images are cut-outs now: the generator exports a transparent square
   and the catalogue's photographs already are. The main box IS the card: the
   studio charcoal (2026-09-12-charcoal.md: the generator's own backdrop, a
   radial #24272d to #0b0d10, painted by the container in BOTH themes because
   the background belongs to the page and never to the file), the hairline,
   the radius and the shadow the cards carry, and the image fills its content
   box edge to edge, so nothing of the page shows inside the frame. Square,
   because the export is 1:1. A legacy photo that is not square is contained,
   never cropped: centred, its short axis letterboxed by the charcoal. The old
   gallery card wrapper paints nothing. --charcoal is declared once here and
   read by the thumbs and the certificate hover below, so the three stay one
   surface. */
.pd-gallery { --charcoal: radial-gradient(circle at 50% 42%, #24272d 6%, #0b0d10 78%); }
.pd-gallery-card { background: transparent; border: 0; border-radius: 0; padding: 0; }
.pd-main { height: auto; aspect-ratio: 1 / 1; background: var(--charcoal); border: 1px solid var(--line-soft); border-radius: 15px; box-shadow: var(--shadow); display: flex; align-items: center; justify-content: center; overflow: hidden; }
/* picture_tag() wraps the image; the wrapper steps out of the layout so the
   img's percentage max-height measures the box, not an auto-height wrapper
   (measured before: the img laid out 505 x 900 from its width/height
   attributes and object-fit hid the excess). */
.pd-main picture { display: contents; }
/* The img box IS the square; object-fit places the bitmap inside it. A 1000
   px export fills it; a legacy 600 px square scales up a little; a legacy
   photo that is not square is contained, centred, with its short axis
   letterboxed by the page itself. Nothing is ever cropped. */
.pd-main img { width: 100%; height: 100%; max-width: 100%; max-height: 100%; object-fit: contain; }
/* THE PICTURE'S OWN BUTTON (product page, 3D products only). A real <button>
   wrapping the <picture>, so it is the square itself that is pressed rather
   than a control parked on it. Every button default is taken back off -- box,
   border, padding, font, appearance -- and it takes the img's place as the flex
   item, which is why it carries the 100%/100% the img used to resolve against
   .pd-main directly. z-index 0 is the same floor the image had: the badge sits
   at 3 and the certificate panel at 2, both over it, which is what lets the
   panel decide who gets a click (see the pointer-events rules below).

   NOT display: contents, which would have kept every existing `.pd-main > img`
   selector working untouched: a display:contents button is dropped from the
   accessibility tree in shipping browsers, and an invitation nobody can hear is
   not an invitation. The four child-combinator rules carry the extra level
   instead. */
.pd-img-btn {
    display: block;
    width: 100%;
    height: 100%;
    position: relative;
    z-index: 0;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    -webkit-appearance: none;
    appearance: none;
    cursor: pointer;
}
/* Disabled is the state the whole card is in while the viewer is up (the same
   setBusy() that disables the corner button), so it must not read as a broken
   control: no dimming, no pointer. */
.pd-img-btn[disabled] { cursor: default; }
.pd-img-btn:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: -3px; border-radius: 14px; }
.pd-main .pcard-placeholder svg { width: 110px; height: 110px; color: var(--ink-placeholder); }
/* =========================================================================
   Certificate on the product page. Three surfaces, one certificate:
     .pd-coa-badge   always visible when the selected size has one
     .pd-coa-hov     the hover panel, the grid's treatment at a larger size
     .pd-coa-row     under the trust list, above the size chips

   The ROW is the one that matters. It needs no pointer, it is the same on
   every device, and it carries a real button. The badge announces that a
   certificate exists without any interaction; the hover panel is a desktop
   enhancement on top of both. All three are hidden by JS when the selected
   size has no certificate: no placeholder, no pending state.
   ========================================================================= */
/* An explicit stacking context, so the order inside is decided here rather than
   inherited from wherever .pd-main happens to sit. Without it the product image
   is a static element with z-index auto and the panel is positioned at z-index 2,
   which SHOULD order them, but the image also carries a filter while the panel is
   open, and a filtered element forms its own stacking context. Pinning the parent
   removes the argument: everything inside sorts by z-index against this box. */
.pd-main { position: relative; z-index: 0; }

.pd-coa-badge {
    position: absolute;
    left: 14px;
    top: 14px;
    z-index: 3;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    min-height: 44px;
    padding: 8px 14px;
    background: var(--surface);
    color: var(--ink-48);
    border: 1px solid var(--edge-14);
    border-radius: 999px;
    box-shadow: 0 2px 8px rgba(20, 23, 29, 0.10);
    font: inherit;
    font-size: 11.5px;
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
    transition: opacity 250ms ease, transform 250ms cubic-bezier(.2, .8, .25, 1);
}
.pd-coa-badge i { width: 7px; height: 7px; border-radius: 999px; background: var(--pill-green); display: block; }
.pd-coa-badge:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
/* Tucks away as the panel it announces opens, so it is not left sitting on
   top of the document. */
@media (hover: hover) and (pointer: fine) {
    .pd-main.pd-coa-ready:not(.pd-3d-on):hover .pd-coa-badge { opacity: 0; transform: translateY(-6px); pointer-events: none; }
}

.pd-main.coa-open .pd-coa-badge { opacity: 0; transform: translateY(-6px); pointer-events: none; }

/* =========================================================================
   The 3D viewer (docs/reports/2026-09-13-vial-3d-viewer.md). Per product
   (products.viewer_3d) and only in browsers that answered a WebGL 2 probe.
     .pd-3d-btn    the rotate cue, bottom right, the badge's round twin:
                   the same 44px box, the same 14px inset, the same surface,
                   border and shadow, in the mirrored corner. Glyph only --
                   the words moved to aria-label, because a caption on the
                   image reads as the name of a feature and the picture
                   itself is the invitation now (.pd-img-btn above)
     .pd-3d-on     on .pd-main while the viewer is up: the certificate hover
                   (the :not() on every hover rule above and below), the
                   badge, the panel and the button all stand down
     .pd-3d-stage  the <three-d-stage>, filling the card between the picture
                   (z 0) and the controls (z 3); its canvas is transparent
                   around the vial, so the card's own charcoal is the
                   background and nothing flashes on the way in
     .pd-3d-live   added once the first full frame is drawn: the picture
                   underneath steps back (kept in the DOM for app.js)
     .pd-3d-close  top right while the viewer is up
     .pd-3d-note   one line when the viewer could not load; the picture stays
   ========================================================================= */
.pd-3d-btn {
    position: absolute;
    right: 14px;
    bottom: 14px;
    z-index: 3;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    min-height: 44px;
    padding: 0;
    background: var(--surface);
    color: var(--ink-48);
    border: 1px solid var(--edge-14);
    border-radius: 999px;
    box-shadow: 0 2px 8px rgba(20, 23, 29, 0.10);
    font: inherit;
    cursor: pointer;
    transition: opacity 250ms ease, transform 250ms cubic-bezier(.2, .8, .25, 1);
}
.pd-3d-btn svg { width: 18px; height: 18px; display: block; }
.pd-3d-btn:hover { color: var(--ink); }
.pd-3d-btn:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
.pd-3d-btn[disabled] { opacity: .6; cursor: default; }
.pd-main.pd-3d-on .pd-3d-btn,
.pd-main.pd-3d-on .pd-3d-note,
.pd-main.pd-3d-on .pd-coa-badge,
.pd-main.pd-3d-on .pd-coa-hov { display: none; }
/* While the certificate panel is showing (pointer hover, or opened from the
   badge by touch or keyboard) the button stands down: at z 3 it painted over
   the panel's own View COA link, which sits in the same corner. display, not
   opacity, so it also leaves the tab order under an open panel; it is back
   the moment the panel goes. The hover half lives under the same media guard
   as the panel itself, so a tap on the picture on a phone does not take it. */
.pd-main.coa-open .pd-3d-btn { display: none; }
@media (hover: hover) and (pointer: fine) {
    .pd-main.pd-coa-ready:not(.pd-3d-on):hover .pd-3d-btn { display: none; }
}

.pd-3d-stage { position: absolute; inset: 0; z-index: 1; }
.pd-main.pd-3d-live > img,
.pd-main.pd-3d-live > picture > img,
.pd-main.pd-3d-live > .pd-img-btn > picture > img { visibility: hidden; }
.pd-3d-close {
    position: absolute;
    right: 14px;
    top: 14px;
    z-index: 3;
    width: 44px;
    height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: var(--surface);
    color: var(--ink-48);
    border: 1px solid var(--edge-14);
    border-radius: 999px;
    box-shadow: 0 2px 8px rgba(20, 23, 29, 0.10);
    cursor: pointer;
}
.pd-3d-close svg { width: 18px; height: 18px; display: block; }
.pd-3d-close:hover { color: var(--ink); }
.pd-3d-close:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
.pd-3d-note {
    position: absolute;
    left: 14px;
    right: 14px;
    bottom: 68px;
    z-index: 3;
    margin: 0;
    text-align: center;
    font-size: 12.5px;
    line-height: 1.45;
    color: #fff;
    text-shadow: 0 1px 2px rgba(0, 0, 0, .6);
}

/* The hover panel. Same construction as the grid's, sized for a 470px box. */
.pd-coa-hov {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: grid;
    grid-template-rows: 1fr auto;
    overflow: hidden;             /* the tilt swings a corner out; clip it */
    background: var(--charcoal);  /* the card's own charcoal: one surface, not a navy plate */
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 280ms ease, visibility 280ms;
}
@media (hover: hover) and (pointer: fine) {
    .pd-main.pd-coa-ready:not(.pd-3d-on):hover .pd-coa-hov { opacity: 1; visibility: visible; }
    .pd-main.pd-coa-ready:not(.pd-3d-on):hover > img,
    .pd-main.pd-coa-ready:not(.pd-3d-on):hover > picture > img,
    .pd-main.pd-coa-ready:not(.pd-3d-on):hover > .pd-img-btn > picture > img { filter: brightness(0.12); }
}

.pd-main.coa-open .pd-coa-hov { opacity: 1; visibility: visible; pointer-events: auto; }
/* WHO GETS THE CLICK, now that the picture is a control.
   The panel has always been pointer-events: none until .coa-open, so on a mouse
   hover it is a picture of a panel: nothing in it can be pressed, including its
   own View COA. That was survivable while the only 3D control was a button in
   the corner. It is not survivable now, because "hover shows the certificate,
   clicking the picture turns the vial" needs BOTH halves reachable at once.

   So on hover the BAR is live and the rest of the panel is not: View COA and
   Rotate in 3D take their clicks, and a click anywhere else -- the dimmed
   photograph, the certificate sheet, the gap around it -- falls straight
   through to the picture's button underneath and enters 3D. One surface, two
   answers, decided by where the pointer is rather than by a mode.

   Touch is untouched by this rule and keeps the badge: no hover, so the panel
   only ever appears under .coa-open, where it is fully live and sits over the
   picture, and a tap on the photograph enters 3D exactly when the panel is
   closed -- which is every moment the visitor has not asked for the
   certificate. */
@media (hover: hover) and (pointer: fine) {
    .pd-main.pd-coa-ready:not(.pd-3d-on):hover .pd-coa-hbar { pointer-events: auto; }
}

.pd-main.coa-open > img,
.pd-main.coa-open > picture > img,
.pd-main.coa-open > .pd-img-btn > picture > img { filter: brightness(0.12); }
/* Explicitly the bottom layer. It was static with z-index auto, which leaves its
   order against the positioned panel above it decided by painting rules rather
   than by a number, and it gains a filter while the panel is open, which makes it
   a stacking context of its own. Stating 0 here and 2 on the panel removes the
   ambiguity: the product image is always under the certificate, never over the
   one control in it. */
.pd-main > img,
.pd-main > picture > img,
.pd-main > .pd-img-btn > picture > img { position: relative; z-index: 0; transition: filter 340ms ease; }

.pd-coa-hdoc { position: relative; display: grid; place-items: center; padding: 26px 26px 0; min-height: 0; }
/* Shown in place of the sheet when the lot has no preview image. Sits in the same
   grid cell, centred, so the panel keeps its shape instead of collapsing. Quiet
   copy on the panel ink: nothing is wrong, the picture simply does not exist, and
   the button underneath still reaches the certificate. */
.pd-coa-hnone {
    margin: 0; max-width: 30ch; text-align: center;
    font-size: 13.5px; line-height: 1.55; color: var(--ink-93);
}
.pd-coa-hnone[hidden], .pd-coa-hsheet[hidden] { display: none; }
/* The glow: one solid colour under a blur, no gradient. */
.pd-coa-hdoc::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 46%;
    width: 74%;
    height: 70%;
    border-radius: 50%;
    background: #4c74e8;
    filter: blur(54px);
    opacity: 0;
    transform: translate(-50%, -50%) scale(.85);
    transition: opacity 420ms ease 40ms, transform 500ms cubic-bezier(.2, .8, .25, 1) 40ms;
}
@media (hover: hover) and (pointer: fine) {
    .pd-main.pd-coa-ready:not(.pd-3d-on):hover .pd-coa-hdoc::before { opacity: .34; transform: translate(-50%, -50%) scale(1); }
}

.pd-main.coa-open .pd-coa-hdoc::before { opacity: .34; transform: translate(-50%, -50%) scale(1); }

/* THE SHEET IS SIZED BY RATIO, NOT BY ITS CONTENT.

   It used to have no width of its own and shrink-wrapped around the image. That
   works only when every link in the chain behaves: the image must have resolved
   its intrinsic size, the grid item must not stretch, and nothing may contribute
   extra inline width. When any one of those failed the box came out wider than
   the page and the panel colour showed down one side.

   aspect-ratio removes the dependency. The height is a share of the box, the
   width is computed FROM that height and the ratio, and the image then fills
   100% of both. The container cannot be wider than the page because the page is
   the container. --coa-ar is set by JS from the stored preview dimensions, and
   re-set from the image's own natural size once it loads, so a certificate that
   is not US Letter still lands exactly. The literal fallback only applies for the
   instant before either of those runs. */
.pd-coa-hsheet {
    position: relative;
    height: 78%;
    width: auto;
    aspect-ratio: var(--coa-ar, 420 / 544);
    max-width: 100%;
    /* Stated rather than inherited from the parent's place-items. A grid item
       that stretches would take the full column width and win over width:auto,
       which is the one remaining way the box could end up wider than the page. */
    justify-self: center;
    align-self: center;
    background: #141b28;          /* the loading panel, never white: see the grid */
    line-height: 0;
    font-size: 0;
    border-radius: 4px;
    overflow: hidden;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.22), 0 18px 44px rgba(0, 0, 0, 0.62);
    opacity: 0;
    transform: translateY(16px) scale(.94) rotate(-1.2deg);
    transition: opacity 320ms ease, transform 420ms cubic-bezier(.2, .8, .25, 1);
}
@media (hover: hover) and (pointer: fine) {
    .pd-main.pd-coa-ready:not(.pd-3d-on):hover .pd-coa-hsheet { opacity: 1; transform: none; }
}

.pd-main.coa-open .pd-coa-hsheet { opacity: 1; transform: none; }
/* Fills the sheet exactly. The box already carries the page's own ratio, so
   width and height at 100% leave nothing of the container visible, and there is
   no letterboxing for object-fit to do. max-* are reset because .pd-main img caps
   every image in the gallery at 88%, which would inset the page inside its sheet. */
.pd-coa-hsheet img {
    position: relative;
    z-index: 1;
    display: block;
    height: 100%;
    width: 100%;
    max-width: none;
    max-height: none;
    object-fit: fill;
    filter: brightness(1.10) contrast(1.14);
    opacity: 0;
    transition: opacity 340ms ease;
}
.pd-coa-hsheet img.is-ready { opacity: 1; }

/* The loading placeholder, scoped to THIS page. The markup reuses the grid's
   .pcard-coa-skel class, but every rule for it is scoped to .catalog-dark .pgrid,
   so on the product page it was an unstyled inline span: measured at 0x0,
   position:static, display:inline. The panel had no placeholder at all and simply
   opened onto the panel colour until the image arrived. */
.pd-coa-hsheet .pcard-coa-skel {
    position: absolute;
    inset: 0;
    z-index: 0;
    display: block;
    background: #141b28;
    overflow: hidden;
    opacity: 1;
    transition: opacity 300ms ease;
}
.pd-coa-hsheet .pcard-coa-skel i {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 42%;
    background: #243049;
    filter: blur(22px);
    opacity: .85;
    animation: pcdCoaSweep 1.15s ease-in-out infinite;
}
.pd-coa-hsheet.is-ready .pcard-coa-skel { opacity: 0; }

/* THE BAR TAKES POINTER EVENTS BACK.
   The panel above is pointer-events:none so it never swallows a click meant for
   the product image, and the hover rule only ever lifted opacity and visibility.
   That left the View COA button unreachable on a pointer device: measured, the
   element at the button's centre was img#pd-main-img, not the button.
   pointer-events is inherited but a descendant may re-enable it, so re-enabling
   it here fixes BOTH paths at once rather than duplicating the state. While the
   panel is closed it inherits visibility:hidden and hit-tests nothing, so this
   cannot make a hidden button clickable. */
.pd-coa-hbar {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 16px 20px;
    background: transparent;      /* the panel's charcoal shows through; a fill here was the navy seam */
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    pointer-events: auto;
    /* Above the sheet and above anything the panel overlays, so the one control
       in here is never sitting under the document it captions. */
    position: relative;
    z-index: 2;
}
.pd-coa-ht { flex: 1; min-width: 0; }
.pd-coa-ht b { display: block; color: #fff; font-size: 14px; font-weight: 700; }
.pd-coa-ht span { display: block; margin-top: 2px; color: var(--ink-86); font-size: 12px; font-family: var(--font-mono); overflow-wrap: anywhere; }
.pd-coa-hbtn {
    background: var(--accent-blue);
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    text-decoration: none;
    padding: 12px 20px;
    border-radius: 9px;
    white-space: nowrap;
    transition: background 200ms ease;
}
.pd-coa-hbtn:hover { background: #1B3FA8; }
/* "View in 3D" in the bar, a <button> wearing the link's clothes: the UA's
   font, border and cursor put back to the link's. Rendered only when the
   product's viewer is on, and shown only once the WebGL probe has passed. */
.pd-coa-h3d { font: inherit; font-size: 14px; font-weight: 700; line-height: inherit; border: 0; cursor: pointer; }
.pd-coa-h3d[disabled] { opacity: .6; cursor: default; }

/* The row. */
.pd-coa-row {
    margin-top: 18px;
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 14px 16px;
}
.pd-coa-thumb {
    width: 44px;
    height: 57px;
    flex: none;
    border-radius: 4px;
    overflow: hidden;
    background: var(--plate);
    box-shadow: 0 2px 8px rgba(20, 23, 29, 0.14);
}
.pd-coa-thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }
.pd-coa-rowtext { flex: 1; min-width: 150px; }
.pd-coa-rowtext b { display: block; font-size: 14.5px; font-weight: 700; color: var(--ink); }
.pd-coa-rowtext span { display: block; margin-top: 2px; font-size: 12.5px; color: var(--muted-2); font-family: var(--font-mono); overflow-wrap: anywhere; }
.pd-coa-rowbtn {
    background: var(--ink);
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    text-decoration: none;
    padding: 11px 18px;
    border-radius: 10px;
    white-space: nowrap;
}
.pd-coa-rowbtn:hover { background: #000; }

/* Phones: the button takes the full width rather than being squeezed beside the
   text, which is what the mockup does at 390. */
@media (max-width: 560px) {
    .pd-coa-row { padding: 12px 14px; }
    .pd-coa-thumb { width: 38px; height: 49px; }
    .pd-coa-rowbtn { width: 100%; text-align: center; }
    .pd-coa-badge { left: 10px; top: 10px; padding: 7px 12px; font-size: 11px; }
    /* The circle keeps its 44px: it is a touch target before it is a cue. */
    .pd-3d-btn { right: 10px; bottom: 10px; }
    /* Two controls in the panel's bar leave the title 73px at 390; with the
       3D control present the title takes the first line and the two buttons
       the second. A bar with View COA alone is as it was. */
    .pd-coa-hbar:has(.pd-coa-h3d) { flex-wrap: wrap; row-gap: 12px; }
    .pd-coa-hbar:has(.pd-coa-h3d) .pd-coa-ht { flex-basis: 100%; }
    .pd-3d-close { right: 10px; top: 10px; }
}


@media (prefers-reduced-motion: reduce) {
    .pd-coa-badge,
    .pd-3d-btn,
    .pd-coa-hov,
    .pd-coa-hdoc::before,
    .pd-coa-hsheet,
    .pd-coa-hsheet img,
    .pd-main > img,
    .pd-main > picture > img,
    .pd-main > .pd-img-btn > picture > img {
        transition-duration: .01ms;
        transition-delay: 0s;
    }
    .pd-coa-hsheet { transform: none; }
    .pd-coa-hdoc::before { transform: translate(-50%, -50%) scale(1); }
    .pd-coa-hsheet .pcard-coa-skel i { animation: none; }
}


.pd-thumbs { display: flex; gap: 11px; margin-top: 16px; flex-wrap: wrap; }
.pd-thumb { width: 82px; height: 82px; border-radius: 15px; border: 1px solid var(--line-soft); background: var(--charcoal); box-shadow: var(--shadow); overflow: hidden; padding: 0; cursor: pointer; }
.pd-thumb picture { display: contents; }
.pd-thumb img { width: 100%; height: 100%; object-fit: contain; }
.pd-thumb.is-active { border: 2px solid var(--accent-blue); }

/* Buy box */
.pd-buy .cat-badge { margin-bottom: 14px; }
.pd-title { font-size: 41px; font-weight: 800; letter-spacing: -.03em; line-height: 1.05; margin: 0; color: var(--text-2); }
.pd-subtitle { font-size: 17px; color: var(--ink-77); margin: 8px 0 0; }
/* align-items: center, not baseline: the stock pill sits on this line now and a
   pill aligned on its text baseline hangs below a 35px price. */
.pd-price-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 20px 0 18px; }
.pd-price-current { font-size: 35px; font-weight: 800; color: var(--link); }
.pd-price-compare { font-size: 20px; color: var(--ink-88); text-decoration: line-through; }
.pd-save { display: inline-flex; align-items: center; gap: 6px; background: var(--accent-blue); color: #fff; font-size: 12.5px; font-weight: 700; letter-spacing: .03em; padding: 5px 12px; border-radius: 999px; }
.pd-save svg { width: 14px; height: 14px; }
/* No top margin: it lives on the price line now, and .pd-price-row spaces it. */
.pd-stock { display: inline-flex; align-items: center; gap: 8px; background: #111; color: #fff; font-size: 12.5px; font-weight: 600; padding: 7px 14px; border-radius: 999px; margin: 0; }
.pd-stock-dot { width: 8px; height: 8px; border-radius: 50%; background: #3ED67F; }
.pd-stock-dot.pd-stock-low { background: #f0b429; }
.pd-stock-dot.pd-stock-out { background: #e05a4a; }
/* Backorder is not out of stock: the size can be ordered today. Amber, the same
   colour the backorder pill and the catalog strip already use for this state. */
.pd-stock-dot.pd-stock-backorder { background: #e0a63a; }

/* Trust list. A card in the left column now, matching the mockup, which carries no
   rules between the rows. The previous border-top on the list and border-bottom on
   each row are gone: they were single side borders, and the card edge plus the row
   spacing does the same separating job without them. */
.pd-trust { list-style: none; margin: 0; padding: 14px 18px; background: var(--surface); border: 1px solid var(--line-soft); border-radius: 14px; }
.pd-trust li { display: flex; align-items: center; gap: 11px; padding: 8px 0; font-size: 14.5px; color: var(--ink); }
.pd-trust svg { width: 19px; height: 19px; color: var(--link); flex: 0 0 auto; }

.pd-size { margin: 4px 0 20px; }
.pd-size-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px; }
.pd-size-label { font-size: 13px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--muted-2); }
.pd-size-selected { font-size: 13.5px; color: var(--muted); }
.pd-chips { display: flex; flex-wrap: wrap; gap: 10px; }
/* Compact rounded pill: label + small inline price, single wrapping row. */
.pd-chip { position: relative; display: inline-flex; align-items: center; gap: 7px; padding: 12px 26px; border: 1px solid var(--edge-16); border-radius: 999px; background: var(--surface); cursor: pointer; transition: border-color .12s ease, background .12s ease; }
.pd-chip:hover { border-color: var(--edge-10); }
.pd-chip input { position: absolute; opacity: 0; width: 0; height: 0; }
.pd-chip-size { font-size: 15px; font-weight: 600; color: var(--text-2); }
.pd-chip-price { font-size: 11px; color: var(--muted); }
.pd-chip:has(input:checked) { background: #111; border-color: var(--text-2); }
.pd-chip:has(input:checked) .pd-chip-size { color: #fff; }
.pd-chip:has(input:checked) .pd-chip-price { color: rgba(255,255,255,.7); }
.pd-chip:has(input:focus-visible) { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
.pd-chip.is-out { opacity: .45; cursor: not-allowed; text-decoration: line-through; }

/* Stepper + Add to cart: two matching rounded pills, sized to content, side by
   side, left-aligned within a capped-width row (not stretched full column). */
.pd-buyrow { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; max-width: 440px; }

.pd-stepper { display: inline-flex; align-items: center; gap: 12px; min-height: 56px; padding: 0 18px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface); flex: 0 0 auto; }
.pd-step { min-width: 20px; background: none; border: none; font-size: 20px; line-height: 1; color: var(--muted-mid); cursor: pointer; padding: 0; transition: color .12s ease; }
.pd-step:hover { color: var(--text-2); }
/* 56px, not 40: it is a number box a finger has to land in, and 40px was under
   the floor at every viewport including the phone. The stepper around it is
   already 56px tall, so nothing else in the buy row moves. */
.pd-qty { width: 56px; text-align: center; border: none; background: none; font-size: 15px; font-weight: 700; color: var(--ink-51); -moz-appearance: textfield; appearance: textfield; }
.pd-qty::-webkit-outer-spin-button, .pd-qty::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.pd-qty:focus { outline: none; }

.pd-add { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; min-height: 56px; padding: 0 44px; background: var(--btn-orange, var(--pcd-orange)); color: var(--btn-ink); border: none; border-radius: 999px; font-size: 17px; font-weight: 700; box-shadow: 0 7px 20px rgba(0, 0, 0, .16); cursor: pointer; transition: background-color .16s ease; }
.pd-add:hover { background: var(--accent-blue); }

/* ===== Pack selector (product page) =====
   Replaces the stepper entirely when quantity tiers are on: one row of four packs,
   the running total, and the saving. Each pack shows its own per-vial price rather
   than a discount percentage, so the packs compare directly. */
.pd-tiers { margin: 4px 0 16px; max-width: 620px; }
.pd-pack-label {
    display: block;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.11em;
    color: var(--ink-81);
    margin-bottom: 14px;
}

/* The top padding is what the badges hang into (they sit at top: -8px). */
.pd-pack-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; padding-top: 8px; }

.pd-pack {
    position: relative;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    padding: 11px 7px;
    border: 1px solid var(--edge-39);
    border-radius: 11px;
    background: var(--surface);
    cursor: pointer;
    text-align: center;
    transition: border-color .12s ease, background .12s ease;
}
.pd-pack input { position: absolute; opacity: 0; width: 0; height: 0; }
.pd-pack-qty  { font-size: 13px; font-weight: 500; color: var(--text-2); line-height: 1.25; }
.pd-pack-unit { margin-top: 2px; font-family: var(--font-mono); font-size: 10px; color: var(--ink-71); line-height: 1.25; }

/* Flagged packs, unselected. Both keep the plain white fill; only the rim and the
   per-vial price carry the signal. */
.pd-pack.is-popular { border: 1.5px solid #2E5BE0; }
.pd-pack.is-popular .pd-pack-unit { color: var(--link); }
.pd-pack.is-best .pd-pack-unit { color: var(--ink-47); }

.pd-pack:hover { border-color: var(--edge-10); }
.pd-pack.is-popular:hover { border-color: #2450c4; }

/* Selected. Black card whatever the badge, so the choice reads at a glance. */
.pd-pack:has(input:checked) { background: #111; border: 1px solid var(--text-2); }
.pd-pack:has(input:checked) .pd-pack-qty { color: #fff; }
.pd-pack:has(input:checked) .pd-pack-unit { color: rgba(255, 255, 255, 0.5); }
.pd-pack:has(input:focus-visible) { outline: 2px solid var(--accent-blue); outline-offset: 2px; }

/* Badges ride above the card edge and stay put when the card is selected. */
.pd-pack-badge {
    position: absolute;
    top: -8px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 2;
    font-size: 8px;
    font-weight: 600;
    line-height: 1.4;
    letter-spacing: 0.02em;
    white-space: nowrap;
    color: #fff;
    padding: 2px 9px;
    border-radius: 8px;
}
.pd-pack-badge-popular { background: #2E5BE0; }
.pd-pack-badge-best    { background: #0f7a4d; }

/* ----- "Frequently added" BAC water row -----
   One line at every width: the checkbox, thumbnail and price stack are fixed, and
   the name/subtitle column is the only thing that flexes (min-width:0 + ellipsis),
   so nothing wraps and nothing overflows at 320px. */
/* One box per offered size, stacked with a 9px gap. The gap is on the container as
   `row-gap`, not a margin on the row, so a single configured size has no trailing
   space and the two-size case needs no :last-child exception. */
.pd-bac { margin: 16px 0 0; display: flex; flex-direction: column; gap: 9px; }
.pd-bac-label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .09em; color: var(--muted-ash); margin-bottom: -1px; }
/* The cart renders each row inside its own <form>, so the gap has to come from the
   container there too rather than from the rows. */
.cart-bac { display: flex; flex-direction: column; gap: 9px; }
.cart-bac-form { margin: 0; }

/* Dark offer panel, shared by the product page and the cart. On the product page the
   row is a <label> wrapping a checkbox; on the cart it holds an Add button instead. */
.pd-bac-row {
    display: flex; align-items: center; gap: 10px;
    background: #1c1c1c; border: none; border-radius: 11px; padding: 12px;
    transition: box-shadow .12s ease;
}
label.pd-bac-row { cursor: pointer; }
.pd-bac-row:has(input:checked) { box-shadow: inset 0 0 0 2px #2E5BE0; }
.pd-bac-row:has(input:focus-visible) { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
.pd-bac-check { flex: 0 0 auto; width: 19px; height: 19px; margin: 0; accent-color: #2E5BE0; cursor: pointer; }
/* Light backing so the vial photography still reads against the dark panel.
   36px, not 44. Dropping the size line off the row halved the TEXT block (44px to
   23px) and changed the row height by nothing at all, because the thumbnail was
   taller than the text and was setting the height on its own. The row is a single
   line now, so the picture is what has to come down for the box to actually be
   shorter: 68px becomes 60px, and 64px becomes 56px on a phone. */
.pd-bac-thumb { flex: 0 0 auto; width: 36px; height: 36px; border-radius: 8px; background: var(--fill-08); object-fit: contain; }
.pd-bac-thumb-empty { display: block; }
 /* ONE LINE. The size used to sit under the name on its own row; now the name
    carries it, so a second line would print it twice. The box is shorter for it. */
.pd-bac-info { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; }
.pd-bac-name { font-size: 15px; font-weight: 500; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pd-bac-prices { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; line-height: 1.35; }
.pd-bac-was { font-size: 12px; color: var(--ink-30); }
.pd-bac-now { font-size: 16px; font-weight: 500; color: #4ade80; }

.pd-tier-totals { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-top: 16px; }
.pd-tier-total { font-family: var(--font-mono); font-size: 28px; font-weight: 500; color: var(--text-2); }
.pd-tier-each  { font-size: 13px; color: var(--muted); }

/* Height is reserved so the Add to cart button never shifts as the tier changes. */
.pd-tier-saved { margin: 6px 0 0; min-height: 19px; font-size: 13px; line-height: 19px; color: var(--ink-24); }

.pd-add-full { display: flex; width: 100%; max-width: 620px; min-height: 48px; padding: 0 24px; font-size: 16px; }
.pd-buyrow-coa { margin-top: 12px; }

/* Phones: 2x2, same card styling. The row gap is larger than the column gap on
   purpose — the second row's badges hang 8px above their cards and would otherwise
   sit on the bottom edge of the row above. Never one column: four stacked
   full-width cards push Add to cart below the fold. */
@media (max-width: 640px) {
    .pd-pack-row { grid-template-columns: 1fr 1fr; gap: 16px 8px; }
}


@media (max-width: 480px) {
    .pd-tier-total { font-size: 25px; }
    /* The BAC row stays one line: tighten the gaps, keep every box the same size. */
    .pd-bac-row { gap: 8px; padding: 10px; }
    .pd-bac-name { font-size: 14px; }
    .pd-bac-now  { font-size: 15px; }
    .cart-bac-add { padding: 8px 12px; font-size: 13px; }
}


.pd-info-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 22px; }
.pd-info-card { display: flex; gap: 12px; align-items: center; background: var(--surface); border: 1px solid var(--line-soft); border-radius: 14px; padding: 16px 18px; }
.pd-info-card svg { width: 26px; height: 26px; flex: 0 0 auto; }
.pd-flag { width: 30px; height: 20px; border: 1px solid var(--edge-37); border-radius: 2px; }
.pd-info-title { margin: 0; font-size: 14px; font-weight: 600; color: var(--text-2); }
.pd-info-sub { margin: 3px 0 0; font-size: 13px; color: var(--muted); }

.pd-disclaimer { background: var(--accent-blue); color: #fff; border-radius: 16px; padding: 22px 26px; font-size: 15.5px; line-height: 1.6; margin: 32px 0; }
.pd-disclaimer a { color: #fff; text-decoration: underline; }

/* .pd-lower is gone. It was the full width row under the buy box holding the tabs
   and the accordions; both now sit in the left column of .pd-top. */
.pd-tabs-panel { background: var(--surface); border: 1px solid var(--line-soft); border-radius: 16px; overflow: hidden; }
/* Tab bar. The rule under the bar and the accent bar under the active tab are inset
   positioned hairlines, not border-bottom: a border paints one side of the box and
   is exactly what the house rule forbids. Both are pseudo-elements pinned with
   left/right/bottom, so they span the full width of their box and nothing shifts by
   a pixel when a tab becomes active. */
.pd-tabs { display: flex; position: relative; }
.pd-tabs::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--border); }
.pd-tab { position: relative; background: none; border: none; padding: 16px 22px; font-size: 15px; font-weight: 600; color: var(--muted); }
.pd-tab.is-active { color: var(--text-2); }
.pd-tab.is-active::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: var(--accent-blue); z-index: 1; }
.pd-tab-body { padding: 24px 26px; font-size: 15px; line-height: 1.7; color: var(--ink); }
.pd-tab-body.is-hidden { display: none; }
.pd-tab-body p { margin: 0 0 14px; }
.pd-tab-body h1, .pd-tab-body h2, .pd-tab-body h3 { margin: 20px 0 10px; }
.pd-tab-body ul, .pd-tab-body ol { margin: 0 0 14px; padding-left: 22px; }
.pd-specs-h { font-size: 16px; margin: 24px 0 12px; }
.pd-specs { margin: 0; }
.pd-spec-row { display: flex; align-items: baseline; gap: 8px; padding: 8px 0; }
.pd-spec-row dt { color: var(--muted); font-size: 14px; white-space: nowrap; }
.pd-spec-row dd { margin: 0; font-size: 14px; color: var(--text-2); text-align: right; flex: 1; }
.pd-spec-row::after { content: ""; }
.pd-spec-row dt { order: 0; }
.pd-spec-row { border-bottom: 1px dotted var(--border-2); }

/* ---- Product specification tables --------------------------------------
   Written into description_general by the chemical-data build script. Distinct
   from .pd-specs / .pd-spec-row above, which are the older admin-entered
   definition list. */
.pd-spec-head,
.pd-spec { width: 100%; border-collapse: collapse; margin: 0 0 14px; }
.pd-spec-head th,
.pd-spec-head td { border-bottom: 1px solid var(--edge-43); padding: 8px 0; text-align: left; }
.pd-spec-head th {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--muted-ash);
    width: 130px;
    vertical-align: top;
    font-weight: 600;
}
.pd-spec-head td { font-size: 14px; color: var(--text-1); }

.pd-compound { margin: 0 0 18px; }
.pd-compound h4 { font-size: 15px; font-weight: 600; color: var(--text-1); margin: 0 0 8px; }

.pd-spec th,
.pd-spec td { border-bottom: 1px solid var(--edge-50); padding: 6px 0; text-align: left; }
.pd-spec th { font-size: 12px; color: var(--muted-ash); width: 150px; font-weight: 500; }
.pd-spec td { font-size: 13px; font-family: var(--font-mono); color: var(--text-1); }

/* Shared FDA notice, rendered from the product.fda_notice content block. */
.pd-fda-notice {
    border-top: 1px solid var(--edge-18);
    margin: 20px 0 0;
    padding-top: 14px;
    font-size: 12px;
    color: var(--muted-ash);
    line-height: 1.55;
}
/* ".pd-tab-body p" above sets margin:0 0 14px at the same specificity as a single
   class, so it would win on source order and flatten the 20px gap. Naming the
   container lifts this rule above it without !important. */
.pd-tab-body .pd-fda-notice { margin: 20px 0 0; }

/* Narrow screens: a 130/150px fixed label column plus a long formula would
   otherwise force the page sideways. */
@media (max-width: 560px) {
    .pd-spec-head th, .pd-spec th { width: 108px; }
    .pd-spec td { word-break: break-word; }
}


/* ONE panel with rows inside, not four separate cards with gaps between them.
   It sits beside the tabs panel now and has to read as its twin. */
.pd-accordions { background: var(--surface); border: 1px solid var(--line-soft); border-radius: 16px; overflow: hidden; }
.pd-acc { position: relative; background: none; border: none; border-radius: 0; }
/* The rule between rows is an inset positioned hairline, not a border-bottom.
   On the row above rather than below, so the last row carries none without
   needing a :last-child exception. */
.pd-acc + .pd-acc::before {
    content: ""; position: absolute; left: 18px; right: 18px; top: 0;
    height: 1px; background: var(--border);
}
.pd-acc-head { width: 100%; display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 15px 18px; background: none; border: none; font-size: 14px; font-weight: 600; color: var(--text-2); text-align: left; }
.pd-acc-chev { width: 16px; height: 16px; color: var(--muted); transition: transform .2s ease; flex: 0 0 auto; }
.pd-acc-head[aria-expanded="true"] .pd-acc-chev { transform: rotate(180deg); }
.pd-acc-inner { padding: 0 18px 16px; font-size: 13.5px; line-height: 1.65; color: var(--ink); }
.pd-acc-inner p { margin: 0 0 12px; }
.pd-acc-inner p:last-child { margin-bottom: 0; }

/* "You may also like" — reuses the catalog .pcard component */
.pd-related { margin-top: 56px; }
.pd-related-h { text-align: center; font-size: 34px; font-weight: 800; letter-spacing: -0.02em; color: var(--text-2); margin: 0 0 30px; }
.pd-related-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 22px; }

/* Admin: variant / spec / image rows.
   Header and rows share ONE column template so every label sits above its field.
   minmax(0,…) + width:100% on the fields stops inputs from blowing out their track
   (which previously squashed the Stock select into a thin vertical sliver). */
/* NINE COLUMNS, NINE CELLS. The row carried nine controls in one stock mode and ten
   in the other against an eight column template, so the surplus wrapped into implicit
   rows: Remove sat under the row above, the On box moved a line on backordered sizes
   only, and the expected date painted over the sort order. Every floor below is what
   the widest thing that control can hold actually needs, so nothing truncates either.
   Nine of these only fit on one line above about 1400px; below that the row is
   deliberately more than one line rather than accidentally so. */

.var-cell .check { margin-bottom: 0; font-size: 12px; justify-content: center; }


/* Below the one-line tier the row is DELIBERATELY more than one line: four cells
   across, grouped identity / availability / flags, each variant on its own panel and
   every cell carrying its own name because the header row is no longer above it. */
@media (max-width: 1399px) {
    .var-cell .check { justify-content: flex-start; }

}


/* Compare-at cell holds the field plus the per-row SALE flag (Compare-at > Price). */


#add-variant, #add-spec { margin-top: 10px; }
.img-row .field input { min-width: 0; width: 100%; box-sizing: border-box; }
.img-row .field { margin-bottom: 0; }

/* Product detail responsive */
@media (max-width: 960px) {
    .pd { padding: 24px 18px 48px; }
    /* Single column below 960. The image and the buy box are the only things in
       .pd-top now, and they are already in the right reading order, so no
       display:contents reordering is needed any more. Everything below .pd-top
       is full width at every size and simply stacks.
       These sit later in the file than the base rules they override, which is
       what makes them win: a media query adds no specificity of its own. */
    .pd-top { grid-template-columns: 1fr; gap: 20px; }
    .pd-trustrow { grid-template-columns: 1fr; gap: 10px; }
    .pd-info { grid-template-columns: 1fr; gap: 12px; }
    .pd-title { font-size: 32px; }
    /* Square at every width now (aspect-ratio above); nothing to restate. */
    .pd-related-h { font-size: 27px; }
    .pd-related-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
    /* TWO COLUMNS, AND EVERY FIELD IN THE SECOND ONE. Narrowing the template
       alone left the five children to auto-flow, so "Shows for" landed in the
       64px thumbnail column: measured at 834 that select was 64px wide and cut
       its own longest option. The thumbnail spans the rows instead and the
       fields stack down the wide column. */
    
    .img-row > .field, .img-row > .img-del { grid-column: 2; }
}



/* =========================================================================
   INVOICE (confirmation embed + download)
   ========================================================================= */
.invoice-block { border: 1px solid var(--border); border-radius: 14px; background: var(--surface); padding: 20px 22px; }
.invoice-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.invoice-head .conf-h2 { margin: 0; }
.invoice-dl { white-space: nowrap; }
.invoice-embed { display: block; width: 100%; height: 560px; margin-top: 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--fill-17); }
.invoice-fallback { padding: 24px; text-align: center; color: var(--muted); }
@media (max-width: 640px) {
    .invoice-embed { height: 420px; }
    .invoice-head { justify-content: flex-start; }
}


/* =========================================================================
   PRE-LAUNCH CLEANUP — forgot link + one-time success flash
   ========================================================================= */
.auth-forgot { margin: 12px 0 0; text-align: center; font-size: 13.5px; }
.auth-forgot a { color: var(--link); text-decoration: none; }
.auth-forgot a:hover { text-decoration: underline; }

.topbar-flash {
    background: var(--fill-01);
    color: #1b7a44;
    border-bottom: 1px solid #cde9d8;
    text-align: center;
    padding: 11px 16px;
    font-size: 14px;
    font-weight: 600;
}

/* =========================================================================
   CONTACT FORM — select, helper text, compliance notice; admin msg detail
   ========================================================================= */
/* THE SELECT CHEVRON IS A DATA URI, so its stroke cannot read a custom property.
   A dark-only override swaps the whole image rather than tinting it; light never
   sees this rule, so the light chevron is byte-identical. */
:root[data-theme="dark"] .field select,
:root[data-theme="dark"] .contact-form select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a3a7ae' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

.field select,
.contact-form select {
    width: 100%;
    background: var(--input-bg);
    border: 1px solid var(--input-line);
    padding: 12px 40px 12px 14px;
    color: var(--input-text);
    font-family: inherit;
    font-size: inherit;
    transition: border-color .15s ease;
    -webkit-appearance: none;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 13px center;
    background-size: 16px;
}
.field select:focus,
.contact-form select:focus { outline: none; border-color: var(--black); }

.field-help { color: var(--muted-2); font-size: 12.5px; margin: 6px 0 0; line-height: 1.45; }

/* Compliance notice: a calm neutral panel (reads as a notice, not an error).
   Full hairline border on all four sides, neutral grey fill, body text colour. */
.contact-disclaimer {
    margin: 22px 0;
    padding: 15px 18px;
    background: var(--off-white);
    border: 1px solid var(--border);
    border-radius: 12px;
    color: var(--ink);
    font-size: 13.5px;
    line-height: 1.55;
}


/* =========================================================================
   ADMIN — user management column
   ========================================================================= */
.user-manage { display: flex; flex-direction: column; align-items: flex-start; gap: 7px; }
.user-manage .inline-form { margin: 0; }
.user-setpass { display: inline-flex; align-items: center; gap: 6px; margin: 0; }
.user-setpass input { width: 128px; padding: 6px 9px; font-size: 12px; border: 1px solid var(--border-2); border-radius: 6px; }

/* Admin email editor: same shape as the password setter, plus room for the hint. */
.user-setemail { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0; }
.user-setemail input { width: 190px; padding: 6px 9px; font-size: 12px; border: 1px solid var(--border-2); border-radius: 6px; }
.user-setemail .email-hint { flex: 1 0 100%; margin: 2px 0 0; }

/* Customer says they paid; nobody has verified it yet. A queue marker, not a status. */
.pay-reported-flag {
    display: inline-block; margin-top: 4px; padding: 2px 7px; border-radius: 4px;
    background: var(--fill-70); color: var(--ink-75); border: 1px solid var(--edge-45);
    font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
    white-space: nowrap; cursor: help;
}


/* ===== QR sales + inventory (v50) ===================================== */
.st-red { background: var(--fill-03); color: var(--ink-22); }
.stat-warn { color: var(--ink-22); }
/* Visually hidden but still announced. clip-path replaces the legacy clip:
   rect() because clip() does NOT stop the element contributing to an ancestor's
   scrollable area — that is what pushed the admin orders page 619px wide when an
   .sr-only label sat inside a horizontally scrolling tab row. */
.sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
.break { word-break: break-all; }

/* Admin inventory table */

.inv-in { width: 66px; padding: 6px 8px; border: 1px solid var(--border-2); background: var(--surface); color: var(--ink); }
.inv-qr { margin-top: 8px; }
.inv-qr img { border: 1px solid var(--border); background: var(--plate); }

/* Salesperson area (mobile-first) */
.sales-body { background: var(--fill-17); }
.sales-topbar { background: #1c1c1c; color: #fff; }
.sales-topbar-inner { max-width: 1240px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; padding: 14px 18px; }
.sales-brand { font-weight: 700; font-size: 16px; display: flex; align-items: center; }
.sales-logo-link { display: block; }
.sales-logo { height: 26px; width: auto; display: block; }
.sales-tag { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--ink-07); margin-left: 6px; }
.sales-signout { color: rgba(255,255,255,0.7); font-size: 14px; }
.sales-signout:hover { color: #fff; }
.sales-main { max-width: 1240px; margin: 0 auto; padding: 18px; }
.sales-head { margin: 6px 0 16px; }
.sales-h1 { font-size: 22px; margin: 0 0 4px; }
.sales-list { display: grid; gap: 14px; }
/* Salespeople may use a laptop: single column on phones, two columns >900px,
   three >1400px. Cards stay full-width below 768px as before. */
@media (min-width: 900px)  { .sales-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

@media (min-width: 1400px) { .sales-list { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.sales-card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 16px; }
.sales-card-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.sales-prod { font-weight: 600; margin: 0; }
.sales-size { color: var(--muted); margin: 2px 0 0; font-size: 13px; }
.sales-price { font-size: 18px; font-weight: 700; }
.sales-card-meta { margin: 10px 0; }
.sales-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.sales-qr { margin-top: 12px; text-align: center; }
.sales-qr img { width: 180px; height: 180px; border: 1px solid var(--border); background: var(--plate); }
.sales-confirm { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 14px; }
.sales-confirm-row { display: flex; gap: 10px; margin-bottom: 10px; }
.sales-qty { width: 80px; padding: 12px; border: 1px solid var(--border-2); text-align: center; }
.sales-method { flex: 1; padding: 12px; border: 1px solid var(--border-2); background: var(--surface); }
.sales-complete { width: 100%; }

/* Public pay page (matte black, mobile-first) */
/* BOTH CLASSES IN THE SELECTOR ON PURPOSE. This page carries catalog-dark so the
   nav inverts to its dark variant (see the note in views/pay.php). That class also
   brings body.catalog-dark { background:#070910; color:#c7cbd2 }, which is declared
   later in this file and would otherwise repaint the page and change the colour
   every uncoloured element here inherits. Two classes beats one, so the surface
   stays exactly what it was before the nav landed. */
/* BACKGROUND ONLY ON THE BODY, colour on the page's own wrapper below.
   Setting color:#fff here leaked into the nav's mobile overlay, whose containers
   inherit it: on home they inherit catalog-dark's #c7cbd2, so the two bars were
   not identical after all. The body now keeps catalog-dark's colour exactly as
   home does, and the pay card gets its white from .pay-wrap. */
body.pay-page.catalog-dark,
body.pay-page { background: #0e0e10; }

/* The card is centred in the screen BELOW THE NAV, not in the whole viewport, so
   the page does not gain a scrollbar for the height of the bar alone. */
.pay-wrap { min-height: calc(100vh - var(--nav-h)); color: #fff; display: flex; align-items: center; justify-content: center; padding: 20px; }
.pay-card { position: relative; width: 100%; max-width: 420px; background: #1c1c1c; border: 1px solid rgba(255,255,255,0.12);
    border-radius: 15px; padding: 28px 24px; overflow: hidden; box-shadow: 0 24px 60px rgba(0,0,0,0.5); }
.pay-glow { position: absolute; top: -80px; right: -80px; width: 220px; height: 220px; pointer-events: none;
    background: radial-gradient(circle, rgba(46,91,224,0.5), transparent 66%); }
.pay-eyebrow { position: relative; font-family: 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: 2px;
    text-transform: uppercase; color: var(--ink-07); margin: 0 0 10px; }
.pay-product { position: relative; font-size: 26px; font-weight: 800; margin: 0 0 4px; line-height: 1.2; }
.pay-size { position: relative; color: rgba(255,255,255,0.6); margin: 0 0 18px; }
.pay-price-row { position: relative; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.pay-unit { font-size: 19px; font-weight: 700; }
.pay-each { color: rgba(255,255,255,0.5); font-size: 13px; font-weight: 400; }
.pay-qty { position: relative; display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.pay-qty-label { color: rgba(255,255,255,0.7); font-size: 14px; }
.pay-stepper { display: flex; align-items: center; gap: 4px; }
.pay-step { width: 42px; height: 42px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06);
    color: #fff; border-radius: 10px; font-size: 20px; cursor: pointer; }
.pay-step:hover { background: rgba(255,255,255,0.12); }
.pay-qty-input { width: 54px; height: 42px; text-align: center; background: transparent; border: none; color: #fff; font-size: 18px; font-weight: 700; }
.pay-total-row { position: relative; display: flex; justify-content: space-between; align-items: center; font-size: 18px;
    font-weight: 700; padding: 14px 0; border-top: 1px solid rgba(255,255,255,0.12); margin-bottom: 18px; }
.pay-total { color: var(--ink-07); }
.pay-methods { position: relative; display: grid; gap: 10px; }
.pay-btn { display: block; text-align: center; padding: 15px; border-radius: 11px; font-size: 16px; font-weight: 700; cursor: pointer; border: none; font-family: inherit; }
.pay-venmo { background: #3d95ce; color: #fff; }
.pay-venmo:hover { background: #3484ba; text-decoration: none; }
.pay-zelle { background: #6d1ed4; color: #fff; }
.pay-zelle:hover { background: #5f19bb; }
.pay-zelle-panel { position: relative; margin-top: 14px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12); border-radius: 11px; padding: 16px; }
.pay-zelle-h { font-weight: 700; margin: 0 0 10px; }
.pay-zelle-dl { margin: 0 0 12px; }
.pay-zelle-dl > div { display: flex; justify-content: space-between; gap: 12px; padding: 5px 0; font-size: 14px; }
.pay-zelle-dl dt { color: rgba(255,255,255,0.55); }
.pay-zelle-dl dd { margin: 0; text-align: right; color: #fff; }
.pay-copy { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.18); color: #fff; border-radius: 9px; padding: 10px 14px; font-size: 13px; cursor: pointer; }
.pay-foot, .pay-oos { position: relative; color: rgba(255,255,255,0.55); font-size: 13px; margin-top: 16px; }
.pay-page .muted { color: rgba(255,255,255,0.55); }

/* =========================================================================
   Inventory manager rework + internal-name chip (admin/inventory + /sales)
   Monochrome, #2E5BE0 accent, #1c1c1c save bar, Inter labels, mono numbers.
   ========================================================================= */

.inv-search-row, .sales-search-row { margin: 0 0 14px; }
.inv-search, .sales-search {
    width: 100%; max-width: 420px; padding: 9px 12px;
    border: 1px solid var(--border-2); background: var(--surface); color: var(--ink);
    font-family: var(--font-ui); font-size: 13.5px;
}
.sales-search { max-width: none; }
.inv-search:focus, .sales-search:focus { outline: none; border-color: var(--accent-blue); }

.inv-table td { vertical-align: middle; }
.inv-prod strong { font-family: var(--font-ui); font-weight: 600; }

/* The internal-name chip that used to sit here is gone with its last user: the sales
   page now shows the compound name the same way every other admin screen does, so
   there is one treatment for the field instead of two. See .iname-sub. */

/* Stock stepper (one field, minus/plus either side). */

.inv-dot {
    display: inline-block; margin-left: 8px; font-size: 10px; font-weight: 600;
    text-transform: uppercase; letter-spacing: .04em; padding: 3px 7px; border-radius: 999px;
    vertical-align: middle;
}

/* Icon buttons (QR / threshold edit). */

/* Detail rows: QR panel + threshold editor. */

.qr-panel { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; }
.qr-panel img { border: 1px solid var(--border); background: var(--plate); }
.qr-panel-body { display: flex; flex-direction: column; gap: 10px; }
.qr-url { font-size: 12px; color: var(--muted); max-width: 360px; }
.qr-panel-actions { display: flex; gap: 8px; }


/* The page-bottom save bar is gone: every inventory line saves itself. */

/* =========================================================================
   Responsive: inventory + sales QR thumbnail, and mobile cards (<768px).
   No horizontal scroll at any width down to 320px.
   ========================================================================= */

/* Shared QR thumbnail (inventory desktop, inventory mobile, sales). */
.qr-wrap { display: inline-block; vertical-align: top; }
.qr-thumb {
    display: block; width: 46px; height: 46px; max-width: 100%;
    border: 1px solid var(--border); background: var(--plate); cursor: pointer;
}
.qr-thumb-url { display: none; margin: 6px 0 0; font-size: 11px; color: var(--muted); word-break: break-all; }

/* Desktop-first visibility helpers. */
.inv-mobile-only { display: none; }
.inv-cost-mobile, .inv-stockline { display: none; }
.inv-actions .qr-wrap { margin-right: 8px; }

/* Stat cards: two-up on small screens instead of one very tall column. */
@media (max-width: 767px) {
    .stat-grid { grid-template-columns: 1fr 1fr; }
    .stat { padding: 14px; }
    .stat-num { font-size: 22px; }
}


/* ---- Sales cards: QR always visible on the right, tap to expand. ---- */
.sales-card-top { flex-wrap: wrap; }
.sales-card-main { min-width: 0; flex: 1 1 auto; }
.sales-price { margin-top: 6px; font-weight: 600; }
.sales-size { overflow-wrap: anywhere; }
.sales-card .qr-thumb { width: 74px; height: 74px; }
.sales-card .qr-wrap { flex: 0 0 auto; text-align: center; }
.sales-card .qr-wrap.qr-open { flex-basis: 100%; }
.sales-card .qr-wrap.qr-open .qr-thumb { width: 220px; max-width: 100%; height: auto; margin: 8px auto 0; }
.sales-card .qr-wrap.qr-open .qr-thumb-url { display: block; text-align: center; }
.sales-actions .btn { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }

/* ---- Inventory: table becomes one card per variant below 768px. ---- */
@media (max-width: 767px) {
    .inv-mobile-only { display: block; }
    .inv-desktop-only { display: none !important; }

    /* Drop the scrolling shell for the INVENTORY table only (its content reflows to
       fit). The in-person sales-log table keeps its own .table-wrap scroll so its
       columns never push the page wide. */
    .inv-table-wrap { overflow: visible; border: none; background: transparent; }
    .inv-table, .inv-table > tbody { display: block; width: 100%; }
    .inv-table > thead { display: none; }

    /* Product name (leave room for the QR thumbnail top-right). */
    .inv-prod { padding-right: 86px; }
    .inv-prod strong { font-size: 16px; }

    /* Size wraps, never widens the card. */
    .inv-size-cell { margin-top: 3px; color: var(--muted); overflow-wrap: anywhere; }

    /* Price line, then the combined muted "cost $X · NN%" line. */
    
    .inv-cost-desktop, .inv-margin-cell { display: none; }
    .inv-cost-cell { margin-top: 4px; }
    .inv-cost-mobile { display: inline; font-size: 12px; color: var(--muted); }

    /* Stepper: 44px touch targets, input flexes and never overflows. */
    .inv-stock-cell { margin-top: 12px; }
    .inv-dot-desktop { display: none; }
    .inv-stockline { display: block; margin-top: 6px; font-size: 12px; color: var(--muted); }
    .inv-stockline.is-out { color: var(--ink-22); }
    .inv-stockline.is-low { color: var(--ink-44); }

    /* Actions: QR pinned top-right; Share/Edit full-width buttons below. */
    .inv-actions { margin-top: 12px; }
    .inv-actions .qr-wrap { position: absolute; top: 14px; right: 14px; width: 74px; margin: 0; }
    .inv-actions .qr-thumb { width: 74px; height: 74px; }
    .inv-cardbtns { display: flex; gap: 8px; }
    .inv-cardbtns .btn { flex: 1 1 0; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }

    /* Expanded QR: full-width block, ~220px, pay URL beneath. */
    .inv-row.qr-open { padding-right: 14px; }
    .inv-row.qr-open .inv-prod { padding-right: 0; }
    .inv-row.qr-open .inv-actions .qr-wrap { position: static; width: 100%; margin-top: 12px; text-align: center; }
    .inv-row.qr-open .inv-actions .qr-thumb { width: 220px; max-width: 100%; height: auto; margin: 0 auto; }
    .inv-row.qr-open .qr-thumb-url { display: block; text-align: center; }


    /* Edit panel: attached card-style block, stacked fields. */
    
    .inv-edit-print { margin-top: 12px; }
    .inv-edit-print .btn { min-height: 44px; }

    .inv-savebar { flex-wrap: wrap; }
}













/* The code and rate boxes: the mockup's .codebox, 42px, mono, a soft focus ring. */

.as-disc-in .btn { flex: 0 0 auto; }
.as-disc-ovrnote { margin: 8px 0 0; max-width: 46ch; }

/* One line, three states. Colour carries the same meaning it does everywhere
   else in the admin, and the warn state is the one that says a rule was
   deliberately stepped over, so it must not read as a plain success. */

.as-disc-msg.is-ok    { color: var(--ink-24); font-weight: 600; }
.as-disc-msg.is-warn  { color: var(--ink-84); font-weight: 600; }


/* Two-up when the control is narrow: four segments in a row need about 380px,
   so under 420 the group becomes a 2 x 2 grid (the tax control's two segments
   fill one row; a three-way picker's last segment spans the second row). The
   control measures ITSELF (container-type on .as-disc), because the same
   partial sits in an 800px form on Add sale and in a 340px column on the order
   editor at a 1024 viewport, and no viewport width tells those two apart. */

@container (max-width: 420px) {
    .as-disc-kinds { display: grid; grid-template-columns: 1fr 1fr; width: 100%; }
    .as-disc-k span { border-right: 0; border-bottom: 1px solid var(--border); text-align: center; white-space: normal; }
    .as-disc-k:nth-child(odd) span { border-right: 1px solid var(--border); }
    .as-disc-k:nth-last-child(-n+2) span { border-bottom: 0; }
    .as-disc-k:last-child:nth-child(odd) span { grid-column: 1 / -1; border-right: 0; }
}





/* On a phone the three numeric fields stop fitting side by side, so they go two
   up and the remove button takes a full-width row of its own rather than
   staying a 34px square stranded beside nothing. */

.pricing-btnrow .btn { flex: 1 1 0; min-height: 44px; }

/* =========================================================================
   Login redesign — single centered column on the dark bg, animated blue glows
   behind the WHOLE page, translucent card, Google button. All under .login-page
   so nothing here touches other screens. (The old .login-split/.login-visual
   rules go unused.)
   ========================================================================= */

/* =========================================================================
   RUN 4 -- THE LIGHT LOGIN
   =========================================================================
   The login page was chrome: .login-page, .login-auth and .login-single were in
   the isolation list, so it rendered the same near-black card in both modes. It
   is a themed surface now.

   ONE FAMILY, DECLARED ON THE BODY, because everything the page draws lives
   inside body.login-body and nothing outside it does: .tabs, .tab and
   .auth-fineprint are unscoped selectors, and login.php is the only view in the
   tree that renders any of them.

   THREE STATES (run 5). THIS BLOCK IS THE ORIGINAL and it is the near-black
   card the login page shipped with, character for character: no cookie, no
   attribute, neither block below matches, and the page is what it always was.
   :root[data-theme="light"] body.login-body is an explicit light choice;
   :root[data-theme="dark"] body.login-body is an explicit dark one and restates
   these same values so the explicit state does not depend on this block.

   The light values are the storefront's own light surface -- #fafafa, #ffffff,
   #e4e4e4, #141414, #6b6b6b, #d4d4d4 -- so the card is the same card the cart and
   the product page are made of. NOT TOUCHED, because they are brand:
   --accent-blue under .btn-auth and the active tab pill, the Google button's
   white fill and its four-colour G, and the two blue glows (dimmed in explicit
   light, not removed -- see --lg-glow-op).

   .btn-auth IS DELIBERATELY ABSENT. Cart and checkout render it too, and it is
   blue with white type on all three, in both modes. Giving it a token here would
   have put a light-mode change on two pages Fence 4 keeps byte-identical. The
   .checkout-* rules below are handled the same way: only the .login-single- and
   body.login-body-scoped ones move, so /checkout itself cannot. */
body.login-body {
    --lg-bg: #0d0d0f;
    --lg-card: rgba(18, 21, 29, 0.72);
    --lg-card-line: rgba(255, 255, 255, 0.10);
    --lg-wordmark: #fff;
    --lg-label: rgba(255, 255, 255, 0.80);
    --lg-hint: rgba(255, 255, 255, 0.38);
    --lg-field-bg: rgba(255, 255, 255, 0.04);
    --lg-field-line: rgba(255, 255, 255, 0.12);
    --lg-field-ink: #fff;
    --lg-placeholder: rgba(255, 255, 255, 0.30);
    --lg-field-bg-foc: rgba(255, 255, 255, 0.06);
    --lg-err: #f0a58f;
    --lg-google-line: rgba(255, 255, 255, 0.85);
    --lg-divider-ink: rgba(255, 255, 255, 0.35);
    --lg-divider-line: rgba(255, 255, 255, 0.10);
    --lg-disc-line: rgba(255, 255, 255, 0.10);
    --lg-disc-bg: rgba(255, 255, 255, 0.02);
    --lg-legend: rgba(255, 255, 255, 0.40);
    --lg-check: rgba(255, 255, 255, 0.70);
    --lg-req: rgba(255, 255, 255, 0.34);
    --lg-opt: rgba(255, 255, 255, 0.48);
    --lg-forgot: rgba(255, 255, 255, 0.60);
    --lg-forgot-h: #fff;
    --lg-trust: rgba(255, 255, 255, 0.42);
    /* The separator between the three trust phrases. #d4d4d4 measured 1.42:1 on
       #fafafa, the only under-gate text the light login sweep found that was not
       already under gate somewhere else on the site. A middot carrying the same
       ink as the phrases it separates is ordinary typography and clears 5.31:1.
       Dark keeps its dimmer dot: there it sits on #0d0d0f, not on white. */
    --lg-trust-dot: rgba(255, 255, 255, 0.22);
    --lg-tabs-bg: rgba(255, 255, 255, 0.05);
    --lg-tabs-line: rgba(255, 255, 255, 0.09);
    --lg-tab-ink: rgba(255, 255, 255, 0.52);
    --lg-tab-ink-h: #fff;
    --lg-fine: rgba(255, 255, 255, 0.45);
    --lg-fine-a: rgba(255, 255, 255, 0.75);
    --lg-fine-a-h: #fff;
    --lg-rule: rgba(255, 255, 255, 0.08);
    --lg-steps: rgba(255, 255, 255, 0.5);
    --lg-steps-rule: rgba(255, 255, 255, 0.18);
    --lg-strong: #fff;
    --lg-sum-bg: rgba(255, 255, 255, 0.05);
    --lg-sum-line: rgba(255, 255, 255, 0.12);
    --lg-sum-ink: rgba(255, 255, 255, 0.8);
    --lg-sub: rgba(255, 255, 255, 0.6);
    /* The two brand glows are 90px-blurred blue blooms designed for a near-black
       ground. At full strength on #fafafa they read as a blue cast over the whole
       page rather than a light behind the card, so the light block dims them; it
       does not remove them, because they are the page's one piece of brand
       movement. Full strength here, which is where they were designed. */
    --lg-glow-op: 1;
}
/* AND THE LIGHT HALF, run 5, moved behind an explicit choice for the same
   reason as the forum: the login card's original is the near-black one. Every
   value is run 4's. */
:root[data-theme="light"] body.login-body {
    --lg-bg: #fafafa;
    --lg-card: #ffffff;
    --lg-card-line: #e4e4e4;
    --lg-wordmark: #111111;
    --lg-label: #141414;
    --lg-hint: #6b6b6b;
    --lg-field-bg: #ffffff;
    --lg-field-line: #d4d4d4;
    --lg-field-ink: #141414;
    --lg-placeholder: #7d8494;
    --lg-field-bg-foc: #ffffff;
    --lg-err: #b42318;
    --lg-google-line: #d4d4d4;
    --lg-divider-ink: #6b6b6b;
    --lg-divider-line: #e4e4e4;
    --lg-disc-line: #e4e4e4;
    --lg-disc-bg: #fafafa;
    --lg-legend: #6b6b6b;
    --lg-check: #3a3a3a;
    --lg-req: #6b6b6b;
    --lg-opt: #3a3a3a;
    --lg-forgot: #2e5be0;
    --lg-forgot-h: #111111;
    --lg-trust: #6b6b6b;
    --lg-trust-dot: #6b6b6b;
    --lg-tabs-bg: #eef0f4;
    --lg-tabs-line: #e4e4e4;
    --lg-tab-ink: #6b6b6b;
    --lg-tab-ink-h: #111111;
    --lg-fine: #6b6b6b;
    --lg-fine-a: #3a3a3a;
    --lg-fine-a-h: #111111;
    --lg-rule: #e4e4e4;
    --lg-steps: #6b6b6b;
    --lg-steps-rule: #d4d4d4;
    --lg-strong: #111111;
    --lg-sum-bg: #ffffff;
    --lg-sum-line: #e4e4e4;
    --lg-sum-ink: #141414;
    --lg-sub: #3a3a3a;
    --lg-glow-op: 0.30;
}


.login-page {
    position: relative;
    display: flex;
    min-height: 100vh;
    overflow: hidden;              /* clip the off-canvas glow blobs -> no scroll */
    background: var(--lg-bg);
}
/* Reuse the existing .login-glow-1/2 blobs, but sit them behind the whole page. */
.login-page .login-glow { position: absolute; z-index: 0; opacity: var(--lg-glow-op); }

.login-single {
    position: relative;
    z-index: 1;
    width: 100%;
    max-width: 400px;
    margin: auto;
    padding: 48px 20px;
    display: flex;
    flex-direction: column;
    gap: 22px;
}
.login-single .login-logo { align-self: center; display: inline-block; }
.login-single .login-logo-img { height: 62px; width: auto; }
/* THE MARK FOLLOWS THE CARD. The white lettering is the login page's original
   and is therefore the bare rule; the dark lettering is reached only by an
   explicit light choice. Both faces are in the markup; neither is ever fetched
   twice and the logo cannot flash.

   STACKED, NOT display:none, AND THAT IS THE WHOLE REASON THIS IS A GRID. The
   two files are not the same shape -- 205.56 x 52 against 226.08 x 52 at 1440 --
   so hiding one and showing the other resized the anchor and run 4's toggle
   check measured five rects moving on /login and eighteen on the age gate. Both
   faces sit in one grid cell, so the cell is the larger of the two in every
   state and visibility picks which one paints. Nothing reflows.

   AND THE ORIGINAL FACE IS THE ONE THAT SIZES THE BOX. Stacking them in a grid
   cell made the cell the LARGER of the two, 226.08 where run 3's <a> was 205.56,
   so the wrapper measured 20.52px wider than it ever had even with no choice
   made. The run 5 original gate caught it. Taking the light face out of flow
   puts the box back to exactly the mark the page has always shown; in explicit
   light the wider mark overflows 10.26px each side of a box that is centred in a
   centred 400px column, so it still lands on the middle of the card. Nothing
   reflows on the toggle, because an out-of-flow box cannot resize its parent. */
.login-single .login-logo { display: grid; place-items: center; position: relative; }
.login-single .login-logo-face { grid-area: 1 / 1; }
.login-single .login-logo-face[data-logo="light"] {
    position: absolute; inset: 0;
    display: grid; place-items: center;
    visibility: hidden;
}
.login-single .login-logo-face[data-logo="dark"] { visibility: visible; }
:root[data-theme="light"] .login-single .login-logo-face[data-logo="light"] { visibility: visible; }
:root[data-theme="light"] .login-single .login-logo-face[data-logo="dark"] { visibility: hidden; }
.login-single .logo-wordmark { color: var(--lg-wordmark); font-size: 26px; letter-spacing: .01em; }

/* Translucent card so the glows show through. */
.login-page .auth-card {
    position: relative;
    z-index: 1;
    width: 100%;
    max-width: none;
    background: var(--lg-card);
    border: 1px solid var(--lg-card-line);
    border-radius: 18px;
    padding: 26px 24px;
    -webkit-backdrop-filter: blur(16px) saturate(140%);
    backdrop-filter: blur(16px) saturate(140%);
}

/* Fields on the dark card (the old dark styles were scoped to .login-auth). */
.login-page .field { margin-bottom: 16px; }
.login-page .field label { display: block; font-size: 14px; font-weight: 500; color: var(--lg-label); margin-bottom: 8px; }
.login-page .field label .hint { color: var(--lg-hint); font-weight: 400; }
.login-page .input-icon input {
    width: 100%;
    background: var(--lg-field-bg);
    border: 1px solid var(--lg-field-line);
    border-radius: 12px;
    padding: 13px 44px;
    color: var(--lg-field-ink);
    transition: border-color .15s ease, background .15s ease;
}
.login-page .input-icon input::placeholder { color: var(--lg-placeholder); }
.login-page .input-icon input:focus { outline: none; border-color: rgba(46, 91, 224, 0.70); background: var(--lg-field-bg-foc); }
.login-page .field-error { color: var(--lg-err); }
.login-single :focus-visible { outline: 2px solid rgba(46, 91, 224, 0.75); outline-offset: 2px; }

/* Google button (white, brand G). Disabled state on the Create-account tab. */
.login-page .btn-google {
    display: flex; align-items: center; justify-content: center; gap: 12px;
    width: 100%;
    background: var(--chrome-surface); color: #1f2328;
    border: 1px solid var(--lg-google-line);
    border-radius: 12px;
    padding: 13px 16px;
    font-size: 15.5px; font-weight: 600;
    cursor: pointer; text-decoration: none;
    transition: background .15s ease, opacity .15s ease;
}
.login-page .btn-google:hover { background: #f2f3f5; text-decoration: none; }
.login-page .btn-google[disabled], .login-page .btn-google:disabled { opacity: 0.45; cursor: not-allowed; }
.login-page .google-g { width: 18px; height: 18px; flex: 0 0 auto; }
.login-page .google-hint { margin: 9px 2px 0; font-size: 12px; line-height: 1.45; color: var(--lg-hint); text-align: center; }

.login-page .or-divider { display: flex; align-items: center; gap: 12px; margin: 16px 0; color: var(--lg-divider-ink); font-size: 12px; }
.login-page .or-divider::before, .login-page .or-divider::after { content: ""; flex: 1; height: 1px; background: var(--lg-divider-line); }

/* Agreements on the dark card. */
.login-page .disclaimers {
    border: 1px solid var(--lg-disc-line);
    border-radius: 12px;
    padding: 16px;
    margin: 4px 0 18px;
    background: var(--lg-disc-bg);
}
.login-page .disclaimers legend {
    font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
    color: var(--lg-legend); padding: 0; margin-bottom: 6px;
}
.login-page .check { display: flex; gap: 11px; align-items: flex-start; font-size: 13.5px; color: var(--lg-check); line-height: 1.5; margin-bottom: 11px; }
.login-page .check:last-of-type { margin-bottom: 0; }
.login-page .check input { width: 18px; height: 18px; margin-top: 1px; flex: 0 0 auto; accent-color: var(--accent-blue); }
.login-page .req { font-family: var(--font-mono); font-size: 10px; color: var(--lg-req); text-transform: uppercase; margin-left: 4px; }

/* Optional agreements: dimmer full-width row + a small "Optional" tag after the text. */
.login-page .ag-optional { color: var(--lg-opt); }
.login-page .ag-optlabel {
    font-style: normal; margin-left: 6px; font-family: var(--font-mono);
    font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--lg-req);
}

.login-page .auth-forgot { text-align: center; margin: 14px 0 0; font-size: 13.5px; }
.login-page .auth-forgot a { color: var(--lg-forgot); text-decoration: underline; text-underline-offset: 2px; }
.login-page .auth-forgot a:hover { color: var(--lg-forgot-h); }

.login-page .login-trust {
    margin: 0; display: flex; flex-wrap: wrap; justify-content: center; gap: 8px;
    text-align: center; font-size: 12.5px; color: var(--lg-trust);
}
.login-page .login-trust-dot { color: var(--lg-trust-dot); }

/* Mobile: full width, taller touch targets, optional agreements collapse. */
@media (max-width: 640px) {
    .login-single { max-width: none; padding: 32px 18px; gap: 18px; }
    .login-single .login-logo-img { height: 52px; }
    .login-page .auth-card { padding: 22px 18px; border-radius: 16px; }
    .login-page .input-icon input { padding-top: 14px; padding-bottom: 14px; min-height: 48px; }
    .login-page .btn-auth { min-height: 50px; }
    .login-page .btn-google { min-height: 48px; }
}


/* Desktop card width. The column carries 20px side padding, so the card lands at
   max-width - 40px: 560 -> ~520px, 660 -> ~620px. (Below 640px stays full width.) */
@media (min-width: 641px) {
    .login-single { max-width: 560px; }
    .login-single .login-logo-img { height: 76px; }
    .login-single .logo-wordmark { font-size: 30px; }
}

@media (min-width: 1025px) {
    .login-single { max-width: 660px; }
    .login-single .login-logo-img { height: 90px; }
    .login-single .logo-wordmark { font-size: 34px; }
    .login-page .auth-card { padding: 32px; }
    /* Agreements stay a single full-width column at every width (no two-column grid). */
}


/* =========================================================================
   Safari toolbar tint sampler. See the note where it is emitted, in
   views/partials/head.php.

   1px tall and pinned to the very top edge, so it takes no space and moves
   nothing. z-index 0 puts it under the ticker (51), the nav (50) and every
   interactive layer, and over the fixed .catalog-bg (-1). With the ticker on it
   is covered by 34-38px of orange and is invisible at every width; that is
   measured, not assumed. pointer-events:none so it can never intercept a tap.
   Deliberately NOT given backdrop-filter (blocks sampling), opacity:0 or
   display:none (pointless or fatal for the same reason).
   ========================================================================= */
.tint-sampler {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 1px;
    background: var(--pcd-orange);
    z-index: 0;
    pointer-events: none;
}

/* =========================================================================
   Announcement bar (signed-in storefront) + cart lock UI. Brand-orange bar
   (--pcd-orange) with near-black text; the cart lock note keeps the blue accent.
   Inter labels, JetBrains Mono numbers. Nothing here affects login/pay/admin
   layout beyond the opt-in classes.
   ========================================================================= */

.announce { width: 100%; height: 38px; overflow: hidden; display: flex; align-items: center; background: var(--pcd-orange); color: var(--black); }
.announce-still { width: 100%; text-align: center; padding: 0 16px; }
.announce-track { display: flex; align-items: center; white-space: nowrap; will-change: transform; animation-name: announce-scroll; animation-timing-function: linear; animation-iteration-count: infinite;
    /* HOW FAR ONE LOOP TRAVELS: exactly one copy of the message strip, expressed
       as a percentage of the whole track. Two copies means 50%, four means 25%.
       app.js sets this after it has measured how many copies it took to cover the
       viewport; 50% is the two-copy default, which is what this did before and is
       what a browser with no JS still gets. */
    --ann-shift: 50%; }
.announce-set { display: flex; align-items: center; flex: 0 0 auto; }
.announce-msg { font-size: 13.5px; font-weight: 700; line-height: 1; white-space: nowrap; padding: 0 28px; }
/* Translating by one strip lands the second copy exactly where the first began,
   so the loop point is invisible. It is only seamless while the track is wider
   than the viewport PLUS one strip -- otherwise the tail of the strip clears the
   right edge before the next copy reaches it, which is the gap this fixes. app.js
   clones until that holds. */
@keyframes announce-scroll { from { transform: translateX(0); } to { transform: translateX(calc(-1 * var(--ann-shift))); } }

/* Reduced motion: no scroll. Drop the duplicate set and centre the messages. */
@media (prefers-reduced-motion: reduce) {
    /* No scroll at all: the messages sit still and centred. Every copy after the
       first is hidden, including the ones app.js adds, so the bar reads once. The
       bar is also allowed to grow to fit rather than clipping a wrapped second
       line, which the fixed 38px height would otherwise cut in half. */
    .announce-track { animation: none !important; transform: none !important; flex-wrap: wrap; justify-content: center; }
    .announce-set ~ .announce-set { display: none; }
    .announce { height: auto; min-height: 38px; padding: 6px 0; }
}

@media (max-width: 640px) {
    .announce { height: 34px; }
    .announce-msg { font-size: 12px; padding: 0 18px; }
}


/* THE TWO SETTINGS OPTIONS (2026-09-18). Nothing below matches a bar with both
   off: .is-static, .has-pin and the two pin elements exist only when Settings
   asks for them, so the scroll path above is untouched.

   STATIC: one centred line, a middle dot between messages, the messages at
   10px of side padding rather than the scroll's 28 (which was spacing for a
   strip, not for a sentence). app.js sets .is-first-only on the line when it
   is wider than the bar: every message but the first is hidden, the pinned
   one (see below) kept, and as the last resort the line clips with an
   ellipsis rather than growing the bar. */
.announce.is-static .announce-still { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.announce.is-static .announce-msg { display: inline; padding: 0 10px; }
.announce-sep { display: inline; font-weight: 700; opacity: 0.7; }
.announce-still.is-first-only .announce-msg:not(:first-child):not(.announce-msg-pin),
.announce-still.is-first-only .announce-sep:not(.announce-sep-pin) { display: none; }

/* PINNED RIGHT: the bar splits into the scroll region and the slot. The slot
   is the same type as the messages (13.5px / 700, 12px under 640) in the bar's
   own colour, the truck inheriting it, so the light theme's white and the
   orange bar's near-black both carry it with no rule of their own. The scroll
   region takes what is left and clips, and app.js measures IT for the loop.
   Under 640 the slot goes and its text shows as the last message in the set
   (or on the still line) instead; above 640 that copy is hidden. */
.announce-scroll { flex: 1 1 auto; min-width: 0; height: 100%; display: flex; align-items: center; overflow: hidden; }
.announce-pin { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px; padding: 0 16px 0 14px; font-size: 13.5px; font-weight: 700; line-height: 1; white-space: nowrap; }
.announce-pin svg { width: 18px; height: 18px; flex: none; }
.announce-msg-pin,
.announce-sep-pin,
.announce.is-static .announce-msg-pin,
.announce.is-static .announce-sep-pin { display: none; }   /* the static selectors outrank the still line's own display */
@media (max-width: 640px) {
    .announce-pin { display: none; }
    .announce-msg-pin { display: inline-block; }
    /* THE STILL LINE ON A PHONE, WITH THE PIN. The line becomes a flex row so
       the pinned message can go FIRST and stay whole (flex: none), and it is
       the announcement beside it that gives way: min-width 0 and an ellipsis
       on the message itself, since a flex container cannot carry one. So the
       pinned message is never lost, and what a 360px bar cannot hold is the
       tail of the announcement, not the promise. */
    .announce.is-static .announce-still { display: flex; justify-content: center; align-items: center; }
    .announce.is-static .announce-msg { display: block; flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
    .announce.is-static .announce-sep { flex: none; }
    .announce.is-static .announce-msg-pin { display: block; flex: none; order: -2; }
    .announce.is-static .announce-sep-pin { display: inline; order: -1; }
}


/* Stick the bar + nav together as one unit. The storefront bar (NOT the admin
   preview) pins to the very top; the nav — already position:sticky top:0 — is
   offset down by the bar's exact height so it sits flush beneath it, no gap/overlap.
   The `.announce + .topbar` offset only applies when the bar is actually present
   (adjacent sibling), so with the bar off the nav keeps top:0. z-index sits at/above
   the nav's 50 so page content never scrolls over either. */
.announce:not(.ann-preview) { position: sticky; top: 0; z-index: 51; }
.announce:not(.ann-preview) + .topbar { top: 38px; }

/* Anchor jumps (e.g. #catalog) clear the sticky nav, plus the bar when it's shown,
   so nothing lands hidden behind the taller sticky block. */
/* 107 = the 97px bar plus 10; 145 adds the 38px announcement. Every other
   anchor margin in this file that clears the desktop bar uses the same 107. */
html { scroll-padding-top: 107px; }
html:has(.announce:not(.ann-preview)) { scroll-padding-top: 145px; }

@media (max-width: 640px) {
    .announce:not(.ann-preview) + .topbar { top: 34px; }
    html { scroll-padding-top: 116px; }
    html:has(.announce:not(.ann-preview)) { scroll-padding-top: 150px; }
}


/* Cart-lock inline note (fades in/out beside the Add-to-cart button). */
.cart-lock-note { display: inline-block; margin-left: 10px; font-size: 12.5px; font-weight: 600; color: var(--link); opacity: 0; transition: opacity .25s ease; }
.cart-lock-note.is-show { opacity: 1; }

/* Catalog notice (e.g. the cart-lock redirect message). */
.catalog-flash { max-width: 760px; margin: 16px auto 0; padding: 12px 18px; background: var(--off-white); border: 1px solid var(--border); border-radius: 10px; color: var(--ink); font-size: 14px; text-align: center; }




/* The input still has to be focusable and reachable by keyboard, so it is made
   invisible in place rather than removed with display:none. */

.ann-preview.is-empty .announce-track { padding: 0 14px; font-size: 12.5px; }

/* =========================================================================
   Settings: tabbed layout + search + save bar. Presentation only; the single
   form and every field are unchanged. Monochrome, #2E5BE0 active-tab underline.
   ========================================================================= */


.settings-tab.is-active { color: var(--ink); border-bottom-color: var(--accent-blue); font-weight: 600; }
.settings-panel.is-active { display: block; }

/* Search mode: reveal every panel, drop the chrome, show only matches + a tab badge. */

.search-tab-badge { display: none; }

@media (max-width: 767px) {
    .field-row { grid-template-columns: 1fr; }
}





/* Solid fill, no single-side border. Same treatment as the legal page sidebar:
   the whole item carries the state rather than a lone left edge. */

.content-group.is-active { color: #fff; font-weight: 600; background: var(--accent-blue); }
.content-group.is-active:hover { color: #fff; background: var(--accent-blue); }
/* The count chip has to invert too, or it stays grey-on-blue and disappears. */
.content-group.is-active .content-group-count { background: rgba(255, 255, 255, 0.18); border-color: transparent; color: #fff; }
.content-group.is-active .content-group-count { color: var(--link); border-color: rgba(46, 91, 224, 0.30); background: var(--surface); }
.content-panel.is-active { display: block; }

/* Search mode: reveal all panels, filter to matches, tag each with its group. */

.content-searching .field.search-hidden { display: none; }
.search-group-badge { display: none; }

@media (max-width: 900px) {

    /* Scrolling pill row. The fill carries the state here too, so the mobile
       fallback no longer swaps one single-side border for another. */
    
    .content-group.is-active { background: var(--accent-blue); color: #fff; }
}


/* ===== Certificate of Analysis (product page) ===== */
/* Widen the buy row so the secondary "View COA" button sits beside Add to cart. */
.pd-buyrow { max-width: 620px; }

.pd-coa-btn {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center; gap: 9px;
    min-height: 56px; padding: 0 22px;
    background: var(--surface); color: var(--ink);
    border: 1px solid var(--border-2); border-radius: 999px;
    font-size: 15px; font-weight: 600; font-family: inherit;
    cursor: pointer; transition: border-color .15s ease, color .15s ease, background .15s ease;
}
.pd-coa-btn svg { width: 19px; height: 19px; color: var(--link); flex: 0 0 auto; }
.pd-coa-btn:hover { border-color: var(--accent-blue); color: var(--link); }
.pd-coa-btn.is-open { border-color: var(--accent-blue); background: rgba(46, 91, 224, .05); }

.pd-coa-panel {
    margin-top: 16px; max-width: 620px;
    border: 1px solid var(--border); border-radius: 14px;
    background: var(--fill-20); padding: 18px 20px;
}
.pd-coa-title { margin: 0 0 12px; font-size: 15px; font-weight: 700; color: var(--ink); }
.pd-coa-meta { display: flex; flex-wrap: wrap; gap: 8px 26px; margin-bottom: 14px; }
.pd-coa-k { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted-2); margin-bottom: 2px; font-family: 'Inter', system-ui, sans-serif; }
.pd-coa-lot .mono, .pd-coa-tested .mono { font-size: 14px; color: var(--ink); }
.pd-coa-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.pd-coa-open, .pd-coa-dl, .pd-coa-contact {
    display: inline-flex; align-items: center; justify-content: center;
    min-height: 40px; padding: 0 20px; border-radius: 999px;
    font-size: 14px; font-weight: 600; text-decoration: none;
}
.pd-coa-open, .pd-coa-contact { background: var(--accent-blue); color: #fff; }
.pd-coa-open:hover, .pd-coa-contact:hover { background: #264fc4; }
.pd-coa-dl { background: var(--surface); color: var(--ink); border: 1px solid var(--border-2); }
.pd-coa-dl:hover { border-color: var(--accent-blue); color: var(--link); }

.pd-coa-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.pd-coa-item { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 10px; padding: 12px 0 0; border-top: 1px solid var(--border); }
.pd-coa-item:first-child { border-top: none; padding-top: 0; }
.pd-coa-item .pd-coa-meta { margin-bottom: 0; }

.pd-coa-soon .pd-coa-title { margin-bottom: 6px; }
.pd-coa-soon-body { margin: 0 0 14px; font-size: 14px; color: var(--muted); line-height: 1.5; }

@media (max-width: 640px) {
    .pd-coa-btn { flex: 1 1 100%; }
    .pd-coa-panel { max-width: 100%; }
}



/* ===== Checkout account step (login page in checkout mode) ===== */
/* Account step width: full below 640, 560 up to 1024, 720 above 1024. The card
   and all three chrome blocks (progress row, summary, heading) share one width. */
.login-body.checkout-account .login-single,
.login-body.checkout-account .checkout-steps,
.login-body.checkout-account .checkout-summary,
.login-body.checkout-account .checkout-acct-head { max-width: none; }

.login-single .checkout-steps {
    display: flex; align-items: center; justify-content: center; gap: 6px;
    list-style: none; margin: 0 auto 18px; padding: 0; width: 100%; max-width: none;
    font-size: 12.5px; color: var(--lg-steps);
}
.checkout-steps li { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
.checkout-steps li:not(:last-child)::after { content: ""; width: 18px; height: 1px; background: var(--lg-steps-rule); margin-left: 6px; }
.checkout-step-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--lg-steps-rule); flex: 0 0 auto; }
.checkout-steps li.is-done { color: var(--lg-check); }
.checkout-steps li.is-done .checkout-step-dot { background: #2E5BE0; }
.checkout-steps li.is-current { color: var(--lg-strong); font-weight: 600; }
.checkout-steps li.is-current .checkout-step-dot { background: #2E5BE0; box-shadow: 0 0 0 4px rgba(46, 91, 224, 0.25); }

.login-single .checkout-summary {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    width: 100%; max-width: none; margin: 0 auto 16px; padding: 12px 16px;
    background: var(--lg-sum-bg); border: 1px solid var(--lg-sum-line);
    border-radius: 10px; font-size: 13.5px; color: var(--lg-sum-ink);
}
.checkout-summary-items { font-weight: 600; color: var(--lg-strong); }
.checkout-summary-label { color: var(--lg-fine); margin-right: 6px; }
.checkout-summary-total .mono { color: var(--lg-strong); font-weight: 600; }

.login-single .checkout-acct-head { width: 100%; max-width: none; margin: 0 auto 14px; text-align: center; }
.checkout-acct-h1 { margin: 0 0 6px; font-size: 20px; font-weight: 800; color: var(--lg-strong); line-height: 1.25; }
.checkout-acct-sub { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--lg-sub); }

@media (max-width: 420px) {
    .login-single .checkout-steps { font-size: 11.5px; gap: 4px; }
    .checkout-steps li:not(:last-child)::after { width: 12px; margin-left: 4px; }
    .login-single .checkout-summary { flex-direction: column; align-items: flex-start; gap: 4px; }
}


/* 640–1024px: card + chrome at 560px. */
@media (min-width: 640px) {
    .login-body.checkout-account .login-single,
    .login-body.checkout-account .checkout-steps,
    .login-body.checkout-account .checkout-summary,
    .login-body.checkout-account .checkout-acct-head { max-width: 560px; }
}

/* Above 1024px: 720px, with roomier card padding so it does not look empty. */
@media (min-width: 1025px) {
    .login-body.checkout-account .login-single,
    .login-body.checkout-account .checkout-steps,
    .login-body.checkout-account .checkout-summary,
    .login-body.checkout-account .checkout-acct-head { max-width: 720px; }
    .login-body.checkout-account .auth-card { padding: 32px; }
}


/* =========================================================================
   Admin — product reorder arrows (light admin shell)
   ========================================================================= */

.reorder .inline-form { margin: 0; }


/* =========================================================================
   RUN 4 -- THE HOME PAGE'S OWN THEME
   =========================================================================
   WHAT CHANGED, AND WHY THERE ARE THREE BLOCKS RATHER THAN ONE.

   Until this run the whole home page was chrome: body.catalog-dark was in the
   isolation list, so every token inside it was pinned to its light value and the
   page rendered identically in both modes. The owner wants light mode to mean
   light, so home came out of that list, the hero went in on its own, and the
   dark catalog's literals moved behind the names below.

   The catalog rules did not change shape. Each one reads a token now, and the
   token is declared three times:

     body.catalog-dark            TODAY'S DARK LITERALS, exactly as they were.
                                  This is what wholesale, affiliates, first
                                  responders, responder apply, peptide class and
                                  the QR pay page keep reading, so their light
                                  mode is byte-identical to run 3 by
                                  construction: a var() that resolves to the same
                                  hex paints the same pixel.

     body.home-body               THE LIGHT VALUES. Same specificity as the block
                                  above and later in the file, so it wins on
                                  home. Every value is an existing light token
                                  (--surface, --line, --text-2, --ink-39,
                                  --line-soft, --line-pale, --plate-a/b) rather
                                  than a new hex: the product page and the cart
                                  already say what a light card, a light chip and
                                  a muted label look like on this site, and the
                                  mockup's palette IS those tokens. The two
                                  exceptions are noted where they are declared.

     :root[data-theme=dark]       THE DARK LITERALS AGAIN, for home. Wins over
     body.home-body               both, so pressing the moon on home puts the
                                  dark catalog back exactly as run 3 shipped it.

   NOT TOKENISED, ON PURPOSE. The photo overlays -- .pcard-fx-*, .pcard-specbox
   and .pcard-coa-* -- are drawn ON the product photograph, not on the card surface,
   and a photograph is the same photograph in both modes. They follow the hero's
   cover-flow plates: dark in both, and measured identical. The .cat-glow blooms
   are unchanged too; in light .catalog paints an opaque --bg over them, so they
   are only ever seen through the transparent hero, which is where they belong.
   ========================================================================= */
body.catalog-dark {
    --cat-page: transparent;
    --cat-ink: #c7cbd2;
    --cat-chip-bg: rgba(255, 255, 255, 0.06);
    --cat-chip-line: rgba(255, 255, 255, 0.12);
    --cat-chip-ink: #a9b0bc;
    --cat-chip-line-h: rgba(255, 255, 255, 0.28);
    --cat-chip-ink-h: #fff;
    --cat-chip-on-bg: #fff;
    --cat-chip-on-ink: #111;
    --cat-chip-on-line: #fff;
    --cat-count: #7d8494;
    --cat-empty-ink: #a9b0bc;
    --cat-outline-ink: #fff;
    --cat-outline-line: rgba(255, 255, 255, 0.30);
    --cat-flash-bg: rgba(255, 255, 255, 0.06);
    --cat-flash-ink: #fff;
    --cat-flash-line: rgba(255, 255, 255, 0.12);
    --cat-card-bg: rgba(255, 255, 255, 0.035);
    --cat-card-line: rgba(255, 255, 255, 0.08);
    --cat-card-line-h: rgba(255, 255, 255, 0.16);
    --cat-card-shadow: 0 3px 10px rgba(0, 0, 0, 0.30);
    --cat-card-shadow-h: 0 10px 26px rgba(0, 0, 0, 0.3);
    --cat-media-bg: transparent;   /* cut-out vials sit on the card itself */
    --cat-media-line: transparent;
    --cat-name: #fff;
    --cat-amount: #fff;
    --cat-compare: #8b919c;
    --cat-size-ink: #c7cbd2;
    --cat-size-bg: rgba(255, 255, 255, 0.04);
    --cat-size-line: rgba(255, 255, 255, 0.11);
    --cat-size-caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23c7cbd2' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    --cat-add-gloss: inset 0 1px 0 rgba(255, 255, 255, 0.22);
    --cat-oos-shadow: 0 3px 12px rgba(0, 0, 0, 0.24), inset 0 0 0 1px rgba(255, 255, 255, 0.18);
    --cbc-trig-bg: #161a22;
    --cbc-trig-line: rgba(255, 255, 255, 0.14);
    --cbc-trig-ink: #fff;
    --cbc-trig-line-h: rgba(255, 255, 255, 0.28);
    --cbc-trig-bg-m: #12151c;
    --cbc-trig-line-m: rgba(255, 255, 255, 0.09);
    --cbc-trig-ink-m: #c9ccd3;
    --cbc-chev: #8b8f9a;
    --cbc-panel-bg: #12151c;
    --cbc-panel-line: rgba(255, 255, 255, 0.09);
    --cbc-divider: rgba(255, 255, 255, 0.06);
    --cbc-group: #6d717c;
    --cbc-label: #c9ccd3;
    --cbc-row-hover: rgba(255, 255, 255, 0.05);
    --cbc-label-hover: #fff;
    --cbc-foot-bg: #0f1219;
    --cbc-foot-bg-h: #131720;
    --cat-panel-bg: rgba(18, 21, 29, 0.72);
    --cat-panel-bg-glass: rgba(255, 255, 255, 0.075);
    --cat-panel-edge: inset 0 1px 0 rgba(255, 255, 255, 0.31), inset 0 0 0 1px rgba(255, 255, 255, 0.17), inset 0 -1px 0 rgba(255, 255, 255, 0.068);
    --cat-h: #fff;
    --cat-body: #c7cbd2;
    --cat-body-2: #a9b0bc;
    --cat-link: #8fadf5;
    --cat-link-h: #fff;
    --cat-chev: #8b919c;
    --cat-seo-rail: linear-gradient(165deg, #131a2c, #0f1524);
    --cat-seo-rail-edge: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
    --cat-seo-rule: rgba(255, 255, 255, 0.07);
    --cat-seo-tab: #e9edf6;
    --cat-panel-lift: 0 8px 28px rgba(0, 0, 0, 0.35);
    --cat-backorder: #e0a63a;
    --cat-restock: #8b919c;
}

/* THE LIGHT HALF, AND IT TAKES AN EXPLICIT CHOICE TO REACH IT (run 5). It used
   to be a bare body.home-body, which meant it was what a visitor who had never
   touched the toggle saw. Home's original is the dark catalog, so with no
   attribute this block does not match and body.catalog-dark above answers with
   the dark literals instead.

   --cat-count and --cat-compare are the two values that are NOT the light rule's
   own: the base .catalog-count and .pcard-compare read --muted-2 (#9a9a9a),
   which measures 2.32:1 on --bg and 2.54:1 on --surface. Reproducing that on a
   page run 4 repainted would be shipping a new contrast failure on purpose.
   --muted (#6b6b6b) is the mockup's own muted value, 4.83:1 and 5.31:1, and it
   is an existing token, not an invention.

   THE SELECTOR WIDENED FROM body.home-body TO body.catalog-dark (light-theme
   run, 2026-09-13). The light catalog existed and only home could reach it: the
   nine other catalog-dark pages -- /shop, the three parent indexes, the class
   pages, wholesale, affiliates, first responders, responder apply -- stayed dark
   under an explicit light choice, so choosing light and then pressing Shop put
   the visitor back on a black page. The approved mockup's grid IS this token
   set on those pages, so they reach it by widening one selector rather than by
   a second copy of sixty values. home-body carries catalog-dark, so home is
   unchanged by the widening, and the explicit-dark restatement below still
   wins on source order. */
:root[data-theme="light"] body.catalog-dark {
    --cat-page: var(--bg);
    --cat-ink: var(--ink);
    --cat-chip-bg: var(--surface);
    --cat-chip-line: var(--line);
    --cat-chip-ink: var(--ink-39);
    --cat-chip-line-h: var(--edge-29);
    --cat-chip-ink-h: var(--text-2);
    --cat-chip-on-bg: var(--text-2);
    --cat-chip-on-ink: var(--surface);
    --cat-chip-on-line: var(--text-2);
    --cat-count: var(--muted);
    --cat-empty-ink: var(--ink);
    --cat-outline-ink: var(--ink);
    --cat-outline-line: var(--border-2);
    --cat-flash-bg: var(--off-white);
    --cat-flash-ink: var(--ink);
    --cat-flash-line: var(--border);
    --cat-card-bg: var(--surface);
    --cat-card-line: var(--line-soft);
    --cat-card-line-h: var(--edge-16);
    --cat-card-shadow: var(--shadow);
    --cat-card-shadow-h: 0 16px 34px rgba(10, 10, 10, 0.09);
    --cat-media-bg: transparent;   /* cut-out vials sit on the card itself */
    --cat-media-line: transparent;
    --cat-name: var(--text-2);
    --cat-amount: var(--text-2);
    --cat-compare: var(--muted);
    --cat-size-ink: var(--text-2);
    --cat-size-bg: var(--surface);
    --cat-size-line: var(--line-pale);
    --cat-size-caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23111111' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    --cat-add-gloss: 0 6px 18px rgba(0, 0, 0, 0.14);
    --cat-oos-shadow: 0 3px 12px rgba(0, 0, 0, 0.24);
    --cbc-trig-bg: var(--surface);
    --cbc-trig-line: var(--line);
    --cbc-trig-ink: var(--ink-39);
    --cbc-trig-line-h: var(--edge-29);
    --cbc-trig-bg-m: var(--surface);
    --cbc-trig-line-m: var(--line);
    --cbc-trig-ink-m: var(--ink-39);
    --cbc-chev: var(--muted);
    --cbc-panel-bg: var(--surface);
    --cbc-panel-line: var(--line);
    --cbc-divider: var(--line);
    --cbc-group: var(--muted);
    --cbc-label: var(--text-2);
    --cbc-row-hover: var(--off-white);
    --cbc-label-hover: var(--text-2);
    --cbc-foot-bg: var(--off-white);
    --cbc-foot-bg-h: var(--surface-2);
    --cat-panel-bg: var(--surface);
    --cat-panel-bg-glass: var(--surface);
    --cat-panel-edge: inset 0 0 0 1px var(--line);
    --cat-h: var(--text-2);
    --cat-body: var(--text-body);
    --cat-body-2: var(--text-body);
    --cat-link: var(--link);
    --cat-link-h: var(--text-2);
    --cat-chev: var(--muted);
    --cat-seo-rail: var(--surface);
    --cat-seo-rail-edge: inset 0 0 0 1px var(--line);
    --cat-seo-rule: var(--line);
    --cat-seo-tab: var(--text-2);
    --cat-panel-lift: 0 8px 28px rgba(10, 10, 10, 0.07);
    --cat-backorder: #8a5a00;
    --cat-restock: #6b6b6b;
}

/* THE EXPLICIT-DARK RESTATEMENTS ARE GONE (2026-09-13-light-default.md).
   Three blocks restated the dark literals word for word under
   :root[data-theme="dark"] -- body.home-body, .f-body and body.login-body -- so
   that "nobody has chosen" and "chose dark" stayed two separate facts in the
   file. There is no longer a "nobody has chosen": light is the site and the
   attribute is always printed, so the bare blocks above are reached only under
   dark and the restatements said, at 132 declarations, exactly what the block
   above each of them already said. The bare block is the dark state now. */


/* =========================================================================
   Dark catalog (/ and /home only). Scoped to body.catalog-dark so product
   detail, cart, checkout, account, contact, and legal stay light.
   ========================================================================= */
body.catalog-dark { background: #070910; color: #c7cbd2; }

/* Blue glow layer behind all content (upper-left, right third-down, lower-centre). */
.catalog-dark .catalog-bg { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; }
/* Static, unfiltered gradients — same treatment the mnav glows got.
   These three blobs used to carry filter: blur(90px) AND a 19-24s drift each, on a
   position:fixed layer covering the whole viewport behind the entire catalog. That
   is three full-layer blur passes recomposited every frame, for the life of the
   page. The radial-gradient is already soft on its own, so the stops are pulled in
   and the alphas dropped to match what the blur was producing, and nothing moves. */
.catalog-dark .cat-glow { position: absolute; border-radius: 50%; pointer-events: none; }
.catalog-dark .cat-glow-1 {
    width: 620px; height: 620px; top: -150px; left: -130px;
    background: radial-gradient(circle, rgba(46, 91, 224, 0.30) 0%, rgba(46, 91, 224, 0.14) 40%, transparent 74%);
}
.catalog-dark .cat-glow-2 {
    width: 540px; height: 540px; top: 32%; right: -160px;
    background: radial-gradient(circle, rgba(46, 91, 224, 0.22) 0%, rgba(46, 91, 224, 0.10) 42%, transparent 76%);
}
.catalog-dark .cat-glow-3 {
    width: 660px; height: 660px; bottom: -200px; left: 50%; margin-left: -330px;
    background: radial-gradient(circle, rgba(46, 91, 224, 0.18) 0%, rgba(46, 91, 224, 0.08) 42%, transparent 76%);
}

/* Hero: transparent so the page bg + glows show; its own glows give way to the 3 page glows. */
.catalog-dark .hero { background: transparent; }

/* The catalog <main> normally paints a light #fafafa fill that would cover the
   dark body + glows. Make it transparent so #070910 and the glows show through.
   (.catalog-dark .catalog = 0,2,0 beats the base .catalog = 0,1,0 — no !important.) */
.catalog-dark .catalog { background: var(--cat-page); color: var(--cat-ink); }

/* The header band drops away here so the floating glass bar sits directly on the
   dark page background + glows. Light storefront pages keep the dark band. */
.catalog-dark .topbar { background: transparent; }

/* Filter pills + count. */
.catalog-dark .catalog-filter { background: var(--cat-chip-bg); border: 1px solid var(--cat-chip-line); color: var(--cat-chip-ink); }
.catalog-dark .catalog-filter:hover { border-color: var(--cat-chip-line-h); color: var(--cat-chip-ink-h); }
.catalog-dark .catalog-filter.is-active { background: var(--cat-chip-on-bg); color: var(--cat-chip-on-ink); border-color: var(--cat-chip-on-line); }
.catalog-dark .catalog-count { color: var(--cat-count); }
.catalog-dark .empty { color: var(--cat-empty-ink); }
.catalog-dark .empty .btn-outline { color: var(--cat-outline-ink); border-color: var(--cat-outline-line); }
.catalog-dark .catalog-flash { background: var(--cat-flash-bg); color: var(--cat-flash-ink); border-color: var(--cat-flash-line); }

/* Cards on dark. Flat translucent fill + a hairline rim — no backdrop-filter,
   deliberately (it was a measured cost on iPhone and is not coming back). */
.catalog-dark .pcard {
    background: var(--cat-card-bg);
    border: 1px solid var(--cat-card-line);
    border-radius: 15px;
    box-shadow: var(--cat-card-shadow);   /* the subtle rest shadow, restated for the dark rim */
}
.catalog-dark .pcard:hover { border-color: var(--cat-card-line-h); box-shadow: var(--cat-card-shadow-h); }

/* Card contents on dark. */
.catalog-dark .pcard-media { background: var(--cat-media-bg); border-bottom: 0; }
/* The no-image placeholder keeps its faint tile on the dark catalogue too. */
.catalog-dark .pcard-media:has(.pcard-placeholder) { background: rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.08); }
.catalog-dark .pcard-name-link,
.catalog-dark .pcard-name-link:hover { color: var(--cat-name); }
.catalog-dark .pcard-amount { color: var(--cat-amount); }
.catalog-dark .pcard-compare { color: var(--cat-compare); }
.catalog-dark .pcard-size {
    color: var(--cat-size-ink);
    background-color: var(--cat-size-bg);
    border-color: var(--cat-size-line);
    background-image: var(--cat-size-caret);
}
.catalog-dark .pcard-size:focus { border-color: var(--accent-blue); }
.catalog-dark .pcard-add { box-shadow: var(--cat-add-gloss); }
/* Both badges must read on the dark cards: OOS pill gains a light rim. */
.catalog-dark .pcard-oos-badge { box-shadow: var(--cat-oos-shadow); }

/* =========================================================================
   Catalog card motion — "Instrument" handoff. Catalog grid only
   (.catalog-dark .pgrid), so the shared card partial renders unchanged in
   the product page's "You may also like" row.

   Every animated property here is transform or opacity. No filter, no
   backdrop-filter, no animated box-shadow, no width/height/letter-spacing —
   nothing that repaints the whole card or triggers layout, which is what a
   39-card grid cannot afford on iPhone. The one exception is the select's
   border-color (paint-only, no layout); noted at that rule.

   Driven by a single --on: 0|1 on the card root, per the handoff's
   implementation pattern: each child computes its own transform/opacity
   from the inherited value, so hover is one property write on one element.
   --k is the reduced-motion kill switch: it zeroes movement while leaving
   the state changes intact.
   ========================================================================= */
.pcard-fx { display: none; }   /* off everywhere except the catalog grid */

.catalog-dark .pgrid .pcard {
    --on: 0;
    --k: 1;
    --fx-acc: 46, 91, 224;     /* brand blue #2E5BE0, not the Nocturne purple */
    position: relative;        /* anchors the accent rim below */
    transform: translateY(calc(var(--on) * var(--k) * -4px));
    transition: transform 170ms cubic-bezier(.2, .9, .3, 1);
}
/* Accent rim as a faded overlay rather than a border-color transition: same
   result as color-mix(accent 45%, border) at opacity 1, but it animates
   opacity instead of paint-recolouring the card edge. */
.catalog-dark .pgrid .pcard::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 4;
    border: 1px solid rgba(var(--fx-acc), 0.45);
    border-radius: inherit;
    opacity: var(--on);
    pointer-events: none;
    transition: opacity 160ms linear;
}
/* The handoff specifies no elevation ("the card lift is 4px of translate and
   a border brightening, no shadow"), which also drops the animated
   box-shadow the base card had. */
.catalog-dark .pgrid .pcard:hover { box-shadow: var(--cat-card-shadow); border-color: var(--cat-card-line); }

/* Hover only where hover really exists. Touch gets .is-active from app.js,
   so a tap can never leave a card stuck in the hover state. */
@media (hover: hover) and (pointer: fine) {
    .catalog-dark .pgrid .pcard:hover { --on: 1; }
}

.catalog-dark .pgrid .pcard.is-active { --on: 1; }

/* Image area layers. The whole overlay is inert; it sits above the vial but
   below the quick-add (z2), SALE (z2) and out-of-stock (z3) badges. */
.catalog-dark .pgrid .pcard-fx {
    display: block;
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    overflow: hidden;
}
.catalog-dark .pgrid .pcard-fx-bloom {
    position: absolute;
    inset: 0;
    background: radial-gradient(70% 55% at 50% 45%, rgba(var(--fx-acc), 0.15), transparent 70%);
    opacity: var(--on);
    transition: opacity 200ms linear;
}
/* Full-size layer, not a 2px element, so the sweep is a plain translateY(100%)
   and works at any card height. 2px accent rule + 22px glow beneath it. */
.catalog-dark .pgrid .pcard-fx-scan {
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(90deg, transparent, rgba(var(--fx-acc), 1), transparent),
        linear-gradient(180deg, rgba(var(--fx-acc), 0.3), transparent);
    background-size: 100% 2px, 100% 22px;
    background-repeat: no-repeat, no-repeat;
    background-position: 0 0, 0 0;
    transform: translateY(calc(var(--on) * var(--k) * 100%));
    opacity: calc(var(--on) * 0.95);
    transition: transform 720ms cubic-bezier(.35, .9, .2, 1), opacity 140ms linear;
}
.catalog-dark .pgrid .pcard-fx-br {
    position: absolute;
    width: 13px;
    height: 13px;
    opacity: var(--on);
    transition: opacity 150ms linear;
}
.catalog-dark .pgrid .pcard-fx-tl {
    top: 10px; left: 10px;
    border-top: 1px solid rgb(var(--fx-acc));
    border-left: 1px solid rgb(var(--fx-acc));
    transition-delay: 20ms;
}
.catalog-dark .pgrid .pcard-fx-tr {
    top: 10px; right: 10px;
    border-top: 1px solid rgb(var(--fx-acc));
    border-right: 1px solid rgb(var(--fx-acc));
    transition-delay: 60ms;
}
.catalog-dark .pgrid .pcard-fx-bl {
    bottom: 10px; left: 10px;
    border-bottom: 1px solid rgb(var(--fx-acc));
    border-left: 1px solid rgb(var(--fx-acc));
    transition-delay: 100ms;
}
.catalog-dark .pgrid .pcard-fx-brr {
    bottom: 10px; right: 10px;
    border-bottom: 1px solid rgb(var(--fx-acc));
    border-right: 1px solid rgb(var(--fx-acc));
    transition-delay: 140ms;
}

/* =========================================================================
   Spec box, docs/mockups/spec-box-and-badges-mockup-v3.html.

   THE MOCKUP'S BOX, SCALED. The box is laid out at the mockup's own size
   (198 x 196, its type, its padding) and --sb scales the whole thing, so
   the proportions never drift from the drawing and a card holding three
   rows is the same box, to the pixel, as one holding none. --sb is chosen
   per media height: 0.93 puts the 196px box in the 210px media with the
   mockup's margins; the phone value is set with its media below.

   ONE COMPOSITION, CENTRED. Box and vial are placed against the media's
   centre line with fixed offsets rather than against its edges, because a
   card is 242px wide in the tightest four-up band and 422px wide at the
   top of the two-up band, and the mockup's pair (box left, vial tucked
   over its right edge) has to hold together at both. The vial's square
   export carries 34.9% of transparent margin each side (measured on the
   1000px PNGs), so the img box is offset by that margin to land the glass
   where the mockup's vial stands: its right edge 236 mockup units from
   the box's left edge, its left edge 18 clear of the box's text column.

   NARROWER SINCE 2026-09-18 (docs/mockups/spec-box-narrow-orange-mockup.html):
   the box is 168 units wide, not 198, and the vial no longer stands over
   its edge: the composition grew 8 units to 244 so the glass clears the
   box by 8px on the desktop, the mockup's gap, and the freed width is the
   vial's. The right padding that tucked the text under the glass went with
   it (14, the mockup's), so the text column is 139 units against the old
   135 and nothing that fitted before is cut now. The phone keeps its 236
   composition (the box flush to a 360px media's edge was the alternative),
   where the glass clears the narrower box by 2px. The line above the
   purity footer is the logo orange, 2px, the mockup's; the ink is not.

   PAINT ORDER IS SOURCE ORDER. The box is in the markup before the photo
   link and both sit at the auto/0 level, so the vial paints over the box's
   right edge and nothing else moves: the fx layer (1), the quick add and
   sale (2), the badges and the certificate (3) keep their places above.

   WHITE IN BOTH THEMES, like the certificate sheet: on dark it is a white
   card on the dark card, on light it stands on the matte plate the vials
   are lit for. Its ink is the mockup's, measured on white, so no theme
   override is needed. Off everywhere but the catalog grids, like .pcard-fx,
   so the product page's related rail renders as before.
   ========================================================================= */
.pcard-specbox { display: none; }
/* THE TALLER CARD (2026-09-18, card polish): 234px of media instead of 210,
   four more pixels between the size select and the Add to cart and four
   below the button, 373.3 to 405.3 at every desktop width (+8.6%); 170px
   of media instead of 150 on the phone, three and three, 298.9 to 324.9
   (+8.7%). Catalog grids only: the
   product page's related rail keeps the base card's 210/150/130. The box
   does not grow with the media; it keeps its size and sits centred in the
   new height (26px in, 20 on the phone), and the vial's square, 90% of
   the media, is back to the 160px glass the 210px box had before the
   quick-add clearance took 10% off it: cap top 62, disc bottom 56. */
.catalog-dark .pgrid .pcard-media { height: 234px; --sb: 0.93; --sb-w: calc(244px * 0.93); --sb-sq: calc(0.9 * 234px); }
.catalog-dark .pgrid .pcard-body { padding-bottom: 19px; }
.catalog-dark .pgrid .pcard-add { margin-top: 13px; }
.catalog-dark .pgrid .pcard-specbox {
    display: block;
    position: absolute;
    top: 26px;
    left: calc(50% - var(--sb-w) / 2);
    z-index: 0;
    box-sizing: border-box;
    width: 168px;
    height: 196px;
    margin: 0;
    padding: 14px 14px 12px 15px;
    transform: scale(var(--sb));
    transform-origin: 0 0;
    background: #fff;
    border: 0;
    border-radius: 15px;
    box-shadow: 0 10px 26px rgba(16, 24, 40, 0.12);
    color: #14181f;
    text-align: left;
    overflow: hidden;
    pointer-events: none;
}
/* Every line is one line. A long name or value is cut with an ellipsis (the
   value's title carries the whole) rather than wrapping into the row below,
   which is what keeps a fixed-height box from spilling. */
.catalog-dark .pgrid .pcard-specbox-name,
.catalog-dark .pgrid .pcard-specbox-rows dt,
.catalog-dark .pgrid .pcard-specbox-rows dd {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.catalog-dark .pgrid .pcard-specbox-name {
    margin: 0 0 7px;
    font-size: 14px;
    font-weight: 800;
    line-height: 1.2;
    color: #14181f;
}
.catalog-dark .pgrid .pcard-specbox-rows { margin: 0; }
.catalog-dark .pgrid .pcard-specbox-rows dt {
    margin-top: 6px;
    font-size: 9.5px;
    font-weight: 700;
    line-height: 1.3;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: #5b616b;
}
.catalog-dark .pgrid .pcard-specbox-rows dd {
    margin: 0;
    font-size: 12px;
    font-weight: 600;
    line-height: 1.35;   /* the mockup's 1.25 plus room for the subscripts below */
    color: #14181f;
}
/* The counts, set as subscripts. Shifted with position rather than
   vertical-align: a true subscript box hangs 2px below the line, and a line
   that overflows its row is what a nowrap box with overflow hidden clips.
   This keeps the line box as it is and moves only the glyphs, 0.18em, the
   most the 16px line holds with the digits' own descent inside it. */
.catalog-dark .pgrid .pcard-specbox-rows dd sub {
    position: relative;
    top: 0.18em;
    vertical-align: baseline;
    font-size: 0.68em;
    line-height: 0;
}
/* A BLEND'S BOX (2026-09-18). The rows are the box's own dt/dd type; what is
   added is the component line. Two components: the name as a dt in its own
   case at 10px (in capitals with tracking "CJC-1295 WITHOUT DAC" is wider
   than the column), the CAS as the dd with the share after it in the label
   grey. Three or more: one dd a component, the name giving way with an
   ellipsis (min-width 0) and the CAS kept whole (flex: none) at the right,
   at 11px so five of them sit inside the fixed box with room over the foot. */
.catalog-dark .pgrid dt.pcard-specbox-comp {
    text-transform: none;
    letter-spacing: 0;
    font-size: 10px;
    color: #14181f;
}
.catalog-dark .pgrid dd.pcard-specbox-comp {
    display: flex;
    align-items: baseline;
    gap: 6px;
}
.catalog-dark .pgrid dd.pcard-specbox-comp-line {
    justify-content: space-between;
    font-size: 11px;
    line-height: 1.35;
    margin-top: 1px;
}
.catalog-dark .pgrid .pcard-specbox-comp-name {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.catalog-dark .pgrid .pcard-specbox-comp-cas { flex: none; }
.catalog-dark .pgrid .pcard-specbox-comp-mg {
    flex: none;
    font-weight: 700;
    color: #5b616b;
}

.catalog-dark .pgrid .pcard-specbox-foot {
    position: absolute;
    left: 15px;
    right: 15px;
    bottom: 11px;
    padding-top: 7px;
    border-top: 2px solid var(--pcd-orange);
}
.catalog-dark .pgrid .pcard-specbox-purity {
    display: block;
    font-size: 11.5px;
    font-weight: 800;
    line-height: 1.25;
    color: #2456d6;
}
.catalog-dark .pgrid .pcard-specbox-ruo {
    display: block;
    font-size: 10px;
    line-height: 1.3;
    color: #5b616b;
}

/* THE VIAL, MOVED TO THE RIGHT OF THE COMPOSITION. The base rule fills the
   media box with the square export and lets object-fit centre the vial; here
   the square is 90% of the media's height, gives up the width, and is pinned
   so the glass stands where the mockup's vial does (see the note above).
   BOTTOM-ANCHORED, AND 90 RATHER THAN 100, FOR THE QUICK ADD. The mockup has
   nothing in the media's top right corner; the card has the 44px quick-add
   disc there, and a full-height vial standing at the composition's right
   put its cap under that disc on every four-up card narrower than 289px.
   At 90% the glass is 143px tall; hung 14px below the media's edge (its
   own 23px of transparent margin absorbs it) the cap's top lands at 58px,
   two below the disc's bottom edge at 56, so the two never meet. The 1.04
   hover lift and the certificate dim are untouched: both are transforms and
   filters on this same element, and neither reads its position. */
.catalog-dark .pgrid .pcard-media-link img {
    top: auto;
    bottom: -14px;
    left: auto;
    right: calc(50% - var(--sb-w) / 2 - 0.349 * var(--sb-sq));
    width: auto;
    height: 90%;
    max-width: none;
}

/* A SUPPLY CARD HAS NO BOX (product_is_supply(): bacteriostatic water, every
   size, and whatever else the class map files under /supplies), and its
   photo gets the plain card's own rule back, centred and filling the media,
   because a lone vial standing at the composition's right edge reads as a
   gap where the box should be. The water export is also the one card photo
   that is not the 30%-wide vial cut, so centring is the only placement that
   shows all of it. Both phone restatements follow the base rule at 600. */
.catalog-dark .pgrid .pcard-nobox .pcard-media-link img {
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
}

/* Phones, two cards a row. The catalog grid's media box is 170px tall from
   640 down (the base card's 150 and 130 stay for the related rail): a 130px
   box with the quick add in its corner leaves a 74px vial beside the box,
   and the taller card buys a 107px one. The box scales to 0.66, a 129px box
   centred in the 170px media, and its type is the mockup's at that scale, 6
   to 9px: a two-up phone card cannot hold the drawing larger, which the 390
   capture in the report shows rather than assumes. The vial follows the
   desktop rule with the phone quick add (44px at 6px, bottom edge 50): an
   83% square hung 11px below the media puts the cap's top at 56. The 118%
   square the phone rule at 600 gives the vial is overruled here: it was
   sized for a vial standing alone in the box, and the supply card, which
   does stand alone, gets it back below. */
@media (max-width: 640px) {
    .catalog-dark .pgrid .pcard-media { height: 170px; --sb: 0.66; --sb-w: calc(236px * 0.66); --sb-sq: calc(0.83 * 170px); }
    .catalog-dark .pgrid .pcard-body { padding-bottom: 15px; }
    .catalog-dark .pgrid .pcard-add { margin-top: 12px; }
    .catalog-dark .pgrid .pcard-specbox { top: 20px; }
    .catalog-dark .pgrid .pcard-media-link img {
        bottom: -11px;
        height: 83%;
        max-height: none;
        right: calc(50% - var(--sb-w) / 2 - 0.349 * var(--sb-sq));
    }
}

@media (max-width: 600px) {
    .catalog-dark .pgrid .pcard-nobox .pcard-media-link img {
        top: -9%;
        right: -9%;
        bottom: -9%;
        left: -9%;
        width: 118%;
        height: 118%;
        max-width: none;
        max-height: none;
    }
}

@media (max-width: 400px) {
    .catalog-dark .pgrid .pcard-media { height: 170px; }
    .catalog-dark .pgrid .pcard-body { padding-bottom: 14px; }
}


/* Vial lift. 1.04 stays inside the export's own headroom, so it never clips. */
.catalog-dark .pgrid .pcard-media img {
    transform: scale(calc(1 + var(--on) * var(--k) * 0.04));
    transition: transform 170ms cubic-bezier(.2, .9, .3, 1);
}
.catalog-dark .pgrid .pcard-price {
    transform: translateY(calc(var(--on) * var(--k) * -1px));
    transition: transform 160ms cubic-bezier(.2, .9, .3, 1);
}
/* The one paint-only property animated here. A <select> cannot carry a
   pseudo-element overlay, so the accent edge has to be the border itself;
   border-color repaints the control and nothing else — no layout, no blur. */
.catalog-dark .pgrid .pcard.is-active .pcard-size {
    border-color: rgba(46, 91, 224, 0.38);
    border-color: color-mix(in srgb, #2E5BE0 28%, var(--cat-size-line));
}
/* Gated with the rest of the hover state, so a sticky :hover after a tap can
   never strand the select with an accent border. */
@media (hover: hover) and (pointer: fine) {
    .catalog-dark .pgrid .pcard:hover .pcard-size {
        border-color: rgba(46, 91, 224, 0.38);
        border-color: color-mix(in srgb, #2E5BE0 28%, var(--cat-size-line));
    }
}

.catalog-dark .pgrid .pcard-size { transition: border-color 160ms linear; }

/* Reduced motion: state still changes (bloom, brackets, rim, scan all still
   appear), movement does not. Transitions collapse to .01ms per the handoff. */
@media (prefers-reduced-motion: reduce) {
    .catalog-dark .pgrid .pcard { --k: 0; }
    .catalog-dark .pgrid .pcard,
    .catalog-dark .pgrid .pcard::after,
    .catalog-dark .pgrid .pcard-fx-bloom,
    .catalog-dark .pgrid .pcard-fx-scan,
    .catalog-dark .pgrid .pcard-fx-br,
    .catalog-dark .pgrid .pcard-media img,
    .catalog-dark .pgrid .pcard-price,
    .catalog-dark .pgrid .pcard-size {
        transition-duration: .01ms;
        transition-delay: 0s;
    }
}


/* Touch / no-hover devices: neutralise the base card's :hover entirely so a
   tap cannot leave a lifted, shadowed card behind after the finger lifts. */
@media (hover: none), (pointer: coarse) {
    .catalog-dark .pgrid .pcard:hover {
        transform: translateY(calc(var(--on) * var(--k) * -4px));
        box-shadow: none;
        border-color: var(--cat-card-line);
    }
}


/* =========================================================================
   Certificate on hover (treatment A). The product image dims and page one of
   the certificate rises and straightens into place over it.

   --coa: 0|1 IS ITS OWN STATE AND NOT --on, deliberately. The card already has
   a touch activation that sets .is-active and clears itself after 2600ms. If
   the certificate hung off that, every tap on a phone would slap a document
   over the product image and then snatch it away mid-read. So --coa is set by
   hover on a pointer device, and on touch only by the badge, with no timeout.

   --k is the shared reduced-motion kill switch. It multiplies the rise, the
   scale and the tilt, so at --k:0 the panel simply appears, already square and
   in place, with no movement and no stagger. The state change survives; the
   choreography does not.

   Only ever rendered for a signed-in visitor, and only for a certificate that
   has a cached preview. Both decisions are made in PHP; there is no CSS here
   that could reveal a certificate the markup did not carry.
   ========================================================================= */
.catalog-dark .pgrid .pcard { --coa: 0; }

@media (hover: hover) and (pointer: fine) {
    .catalog-dark .pgrid .pcard.has-coa:hover { --coa: 1; }
}

/* Touch, and keyboard on any device. Set by app.js on the badge, cleared by a
   tap outside, a second tap, or Escape. */
.catalog-dark .pgrid .pcard.has-coa.coa-open { --coa: 1; }

.catalog-dark .pgrid .pcard-coa {
    position: absolute;
    inset: 0;
    z-index: 3;                      /* over the vial and the fx, under the badges */
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: var(--coa);
    /* THE CLIP. The sheet rotates 1.2 degrees as it rises, which swings its
       corners outside the box it is centred in, and for a few frames a bright
       sliver of that corner showed at the edge. Clipping at the container ends
       it. The sheet settles square and well inside these bounds, so nothing is
       cut once it has arrived: measured, not assumed. This also keeps the glow
       below from bleeding past the media box. */
    overflow: hidden;
    /* Inert while closed AND while open on a pointer device: nothing in here is
       interactive, and swallowing clicks would break the card link and the
       quick-add button underneath it. The touch rule below re-enables it only
       when the panel is open, so a tap on the document dismisses it instead of
       navigating away. */
    pointer-events: none;
    transition: opacity 300ms ease;
}

/* The glow. A single SOLID colour, blurred, sitting behind the page, so it is a
   light source rather than a gradient fill. It blooms outward as the sheet rises,
   which is what makes the document read as lit rather than pasted on.
   The scale is multiplied by --k, so under reduced motion it arrives already at
   full size: the panel lands lit but static. */
.catalog-dark .pgrid .pcard-coa::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 46%;
    width: 78%;
    height: 74%;
    border-radius: 50%;
    background: #4c74e8;
    filter: blur(46px);
    opacity: calc(var(--coa) * 0.34);
    transform: translate(-50%, -50%) scale(calc(1 - (1 - var(--coa)) * var(--k) * 0.15));
    transition:
        opacity 420ms ease 40ms,
        transform 500ms cubic-bezier(.2, .8, .25, 1) 40ms;
}

/* The sheet. Sized off the media box HEIGHT, not its width: the box is a fixed
   210/150/130px and a portrait page is height-constrained in it, so a width
   percentage (as the mockup used on a square card) would overflow the top and
   bottom here. width:auto keeps whatever ratio the stored page actually has,
   so an A4 or a landscape scan still fits rather than being stretched. */
.catalog-dark .pgrid .pcard-coa-sheet {
    height: 76%;
    /* NO WHITE FILL, EVER. This box used to paint #fff behind the page, and
       during the tilt the image and that fill rasterised a fraction of a pixel
       apart, which is what put a bright line down the leading edge.
       #141b28 is the placeholder panel the page loads over, and being DARK it
       cannot bring the flash back: a sliver of it against the card is invisible
       where a sliver of white was not. line-height and font-size zeroed so no
       inline descender adds a sliver either. */
    background: #141b28;
    line-height: 0;
    font-size: 0;
    border-radius: 4px;
    overflow: hidden;
    /* The hairline. A 1px light edge lifts the page off the dark card, which is
       what stops a bright document reading as a floating rectangle. Deliberate,
       symmetric and static, unlike the flash it replaces. */
    box-shadow:
        0 0 0 1px rgba(255, 255, 255, 0.22),
        0 18px 44px rgba(0, 0, 0, 0.62);
    opacity: var(--coa);
    transform:
        translateY(calc((1 - var(--coa)) * var(--k) * 16px))
        scale(calc(1 - (1 - var(--coa)) * var(--k) * 0.06))
        rotate(calc((1 - var(--coa)) * var(--k) * -1.2deg));
    transition:
        opacity 320ms ease,
        transform 420ms cubic-bezier(.2, .8, .25, 1);
}
/* The loading placeholder.
   The panel opens at 0ms over this, rather than waiting for the image, so the
   animation never stalls on the network. A solid bar under a blur, swept across
   a solid panel: no gradient anywhere, which is why it survives the solid
   colours rule that a normal shimmer would not.
   It sits UNDER the image (z-index 0 against the image's 1), so the moment the
   paper fades in it is covered rather than needing to be removed. */
.catalog-dark .pgrid .pcard-coa-skel {
    position: absolute;
    inset: 0;
    z-index: 0;
    background: #141b28;
    overflow: hidden;
    opacity: 1;
    transition: opacity 300ms ease;
}
.catalog-dark .pgrid .pcard-coa-skel i {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 42%;
    background: #243049;
    filter: blur(22px);
    opacity: .85;
    animation: pcdCoaSweep 1.15s ease-in-out infinite;
}
@keyframes pcdCoaSweep {
    0%   { transform: translateX(-60%); }
    100% { transform: translateX(240%); }
}
/* Once the paper has arrived the placeholder is redundant. Fading it rather than
   removing it keeps the sheet's box height, which the image depends on. */
.catalog-dark .pgrid .pcard-coa-sheet.is-ready .pcard-coa-skel { opacity: 0; }

/* Four classes deep so this beats .pcard-media img, which owns the vial's hover
   scale and would otherwise grow the certificate by 5 percent along with it. The
   sheet has its own rise; it must not also inherit the product's lift. */
.catalog-dark .pgrid .pcard-coa .pcard-coa-sheet img {
    display: block;
    height: 100%;
    width: auto;
    transform: none;
    /* .pcard-media img centres the vial absolutely at 82%. The sheet image is
       inside that same box but is laid out by its own container, so every one of
       those declarations has to be given back. */
    max-width: none;
    max-height: none;
    position: static;
    margin: 0;
    inset: auto;
    object-fit: contain;
    /* Lift the paper. A filter on the IMAGE, not a pale layer over it: raising
       brightness and contrast together takes the page toward white while pushing
       the type the other way, so it stays black and legible. An overlay would
       have washed both to grey. */
    filter: brightness(1.10) contrast(1.14);
    /* Above the placeholder, and invisible until it has actually decoded. The
       width and height attributes give the box its size before the bytes land,
       so the sheet is never the wrong shape and nothing reflows on arrival. */
    position: relative;
    z-index: 1;
    opacity: 0;
    transition: opacity 340ms ease;
}
.catalog-dark .pgrid .pcard-coa .pcard-coa-sheet img.is-ready { opacity: 1; }

/* THE NOTE, shown in place of the sheet. Two different facts share this element and
   app.js picks the sentence: the selected size has no certificate, or the lot has one
   whose preview will not render. Either way the panel opens and says so, which is the
   whole change: it used to open onto another size's document.

   It rides the same --coa fade and the same rise as the sheet it replaces, so the
   panel behaves identically whichever of the two it is showing. Centred by the flex
   on .pcard-coa above, and sitting above the ::before glow on its own z-index so the
   blur cannot wash the text out. */
.catalog-dark .pgrid .pcard-coa-none {
    position: relative;
    z-index: 1;
    max-width: 80%;
    margin: 0;
    padding: 10px 13px;
    background: #141b28;
    border: 1px solid rgba(255, 255, 255, 0.16);
    border-radius: 8px;
    color: #c3cad8;
    font-size: 11px;
    line-height: 1.45;
    letter-spacing: 0.01em;
    text-align: center;
    opacity: var(--coa);
    transform: translateY(calc((1 - var(--coa)) * var(--k) * 12px));
    transition:
        opacity 320ms ease,
        transform 420ms cubic-bezier(.2, .8, .25, 1);
}

/* Caption. A SOLID bar, not the mockup's gradient scrim: the rule here is solid
   colours, and a solid bar with a defined top edge reads as a caption rather
   than as a fade that failed to load. */
.catalog-dark .pgrid .pcard-coa-cap {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    padding: 9px 12px 10px;
    background: #0d1220;
    color: #fff;
    opacity: var(--coa);
    transform: translateY(calc((1 - var(--coa)) * var(--k) * 10px));
    transition:
        opacity 300ms ease 60ms,
        transform 400ms cubic-bezier(.2, .8, .25, 1) 60ms;
}
.catalog-dark .pgrid .pcard-coa-cap b {
    display: block;
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: -0.01em;
}
.catalog-dark .pgrid .pcard-coa-cap span {
    display: block;
    margin-top: 1px;
    font-family: var(--font-mono);
    font-size: 10.5px;
    line-height: 1.35;
    color: #a9b1c4;
    overflow-wrap: anywhere;
}

/* THE DIM. The photo and the spec box fade to 0.05 while the certificate is
   up, on the same --coa the panel rides, so the page reads without the busy
   ground: what shows through is the card's own surface with a ghost of the
   vial and the box in it. This was filter: brightness(0.12) on the photo
   alone; that left a white spec box at full strength under the sheet, and on
   the light plate a darkened vial is not a quiet one. opacity is the same
   kind of property for the compositor (no layout, no paint of the card) and
   it treats a white box and a dark vial alike. 240ms, a touch under the
   panel's own 300, so the ground is gone by the time the sheet lands.

   TARGETED AT .pcard-media-link img, NOT .pcard-media img. The certificate is
   also an <img> inside the media box, so the broader selector would fade the
   document by exactly as much as the product. Scoping to the product's own
   link element separates the two for good. Cards with no certificate never
   set --coa, so their hover is exactly what it was. */
.catalog-dark .pgrid .pcard.has-coa .pcard-media-link img {
    opacity: calc(1 - var(--coa) * 0.95);
    transition: transform 170ms cubic-bezier(.2, .9, .3, 1), opacity 240ms ease;
}
.catalog-dark .pgrid .pcard.has-coa .pcard-specbox {
    opacity: calc(1 - var(--coa) * 0.95);
    transition: opacity 240ms ease;
}

/* ---- the badge ----
   One control, two presentations. On a pointer device it is invisible and inert
   at rest, so the card looks exactly as treatment A was drawn, but it is still a
   real focusable button: Tab reaches it and :focus-visible brings it into view,
   which is the keyboard route into a design that is otherwise hover-only. On
   touch it is always visible, because a phone has no hover at all. */
.catalog-dark .pgrid .pcard-coa-badge {
    position: absolute;
    left: 8px;
    top: 8px;
    z-index: 4;                      /* above the panel it opens */
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 44px;                /* the hit area, never smaller than this */
    padding: 6px 11px;
    background: #ffffff;
    color: #12603f;
    border: 1px solid var(--edge-14);
    border-radius: 999px;
    font-family: inherit;
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    transition: opacity 200ms ease;
}
/* [hidden] HAS TO WIN HERE. app.js takes the badge away when the selected size has no
   certificate, and the UA's [hidden] { display: none } is a lone attribute selector
   that loses to the three classes above it. Every other piece of the panel is left at
   its default display for exactly this reason; this is the one that is not. */
.catalog-dark .pgrid .pcard-coa-badge[hidden] { display: none; }

.catalog-dark .pgrid .pcard-coa-tick { display: inline-flex; width: 11px; height: 11px; color: var(--ink-03); }
.catalog-dark .pgrid .pcard-coa-tick svg { width: 100%; height: 100%; }

/* Keyboard: focus makes it real on every device. */
.catalog-dark .pgrid .pcard-coa-badge:focus-visible {
    opacity: 1;
    pointer-events: auto;
    outline: 2px solid #7d9bf0;
    outline-offset: 2px;
}

@media (hover: none), (pointer: coarse) {
    .catalog-dark .pgrid .pcard-coa-badge { opacity: 1; pointer-events: auto; }
    /* Open on touch: the panel takes the taps, so tapping the document closes it
       rather than following the card link underneath. */
    .catalog-dark .pgrid .pcard.coa-open .pcard-coa { pointer-events: auto; }
    /* The badge tucks away as the panel opens, so it is not left sitting on top
       of the certificate it just revealed. */
    .catalog-dark .pgrid .pcard.coa-open .pcard-coa-badge { opacity: 0; pointer-events: none; }
}


/* Smaller media boxes: 150px at 640, 130px at 400. The sheet is a percentage of
   that height so it follows automatically; only the caption needs to give back
   some padding, and below 400 it drops the mono line rather than wrapping a lot
   number across three lines inside a 130px-tall box. */
@media (max-width: 640px) {
    .catalog-dark .pgrid .pcard-coa-cap { padding: 7px 10px 8px; }
    .catalog-dark .pgrid .pcard-coa-cap b { font-size: 10.5px; }
    .catalog-dark .pgrid .pcard-coa-cap span { font-size: 9.5px; }
}

/* Under 400px the media box is only 130px tall and the card about 160px wide, so
   the caption has room for one line of two. It keeps the LOT and drops the title:
   the picture of a certificate already says "certificate", and the lot number is
   the fact that cannot be inferred from the image. Dropping the lot instead would
   leave a caption that names nothing. */

/* THE BADGE IS GONE BELOW 641, NOT SHRUNK AND NOT HIDDEN.
   Every attempt to make it fit a 160px card failed on its own terms: the full
   pill covered the vial, and the 28px chip that cleared the vial was a tick with
   no label, which says nothing to anyone who has not already seen the desktop
   version.

   display: none is the removal, not a dodge. It takes the button out of the box
   tree, out of the tab order and out of the accessibility tree in one move, so
   it occupies no space and no screen reader or keyboard can reach it. visibility
   or opacity would have left a control that is invisible but still focusable,
   which is worse than either state.

   THIS BLOCK MUST STAY BELOW THE (hover: none) RULE ABOVE, which sets opacity 1
   on coarse pointers. Media queries add no specificity, so source order is the
   only thing deciding the winner.

   WHAT THIS COSTS: the badge is the only way to open the certificate panel on a
   touch device, because the panel's other trigger is hover. Below 641 there is
   now no route from a product card to the certificate. The routes that remain
   are the COAs link in the nav, which reaches /coa-library on every page, and
   the certificate line on each product page. Do not treat the panel below as
   reachable on a phone; it is not. */
@media (max-width: 640px) {
    .catalog-dark .pgrid .pcard-coa-badge { display: none; }
}


@media (max-width: 400px) {
    .catalog-dark .pgrid .pcard-coa-sheet { height: 70%; }
    .catalog-dark .pgrid .pcard-coa-cap { padding: 6px 9px 7px; }
    .catalog-dark .pgrid .pcard-coa-cap b { display: none; }
    .catalog-dark .pgrid .pcard-coa-cap span {
        margin-top: 0;
        font-size: 10px;
        color: #ffffff;
    }
}


/* Reduced motion: the panel still appears, the document still dims the vial,
   nothing moves. --k already zeroes the rise, the scale and the tilt; these
   collapse the durations and the caption's stagger so the whole thing lands as
   one state change. */
@media (prefers-reduced-motion: reduce) {
    .catalog-dark .pgrid .pcard-coa,
    .catalog-dark .pgrid .pcard-coa::before,
    .catalog-dark .pgrid .pcard-coa-sheet,
    .catalog-dark .pgrid .pcard-coa-none,
    .catalog-dark .pgrid .pcard-coa-cap,
    .catalog-dark .pgrid .pcard-coa-badge,
    .catalog-dark .pgrid .pcard-coa-skel,
    .catalog-dark .pgrid .pcard.has-coa .pcard-media-link img,
    .catalog-dark .pgrid .pcard.has-coa .pcard-specbox {
        transition-duration: .01ms;
        transition-delay: 0s;
    }
    /* The shimmer stops. The placeholder stays as a flat panel, which is still
       the right thing to look at while the page loads, and the paper appears
       rather than fading in. */
    .catalog-dark .pgrid .pcard-coa-skel i { animation: none; }
    .catalog-dark .pgrid .pcard-coa .pcard-coa-sheet img { transition: none; }
}


/* =========================================================================
   FAQ page (/faq) — light, consistent with contact/legal. <details> accordions.
   ========================================================================= */
/* ONE COLUMN, CAPPED AT 900px, NOT THE FULL SHELL.
   This used to run to --content-max (1440px) and then split the questions into a
   two-column grid to fill it. The grid is what made the page read ragged: the two
   sides are independent, so opening an answer on the left grew that column and left
   a hole beside it, and the taller the answer the worse the hole. A single column
   fixes it structurally rather than by tuning heights.

   900 rather than the full width because this is body copy. At 1440px a single
   column runs past 100 characters a line, which is where prose stops being
   comfortable to read. The answer measure inside the card is tighter still. */
/* width:100% IS LOAD-BEARING. body.app is a flex column, so this <main> is a flex
   item, and `margin: 0 auto` on the cross axis suppresses the default stretch: the
   item then sizes to its own content and the max-width below never becomes the
   actual width. It measured 842px at a 1440px viewport, i.e. whatever the longest
   question happened to be, so the cap was decorative. Declaring the width makes it
   fill first and the cap then does the capping. */
.faq-page {
    width: 100%;
    max-width: calc(900px + 2 * var(--gutter));
    margin: 0 auto;
    padding: 48px var(--gutter) 72px;
}
.faq-head { margin-bottom: 26px; }
.faq-h1 { margin: 0 0 10px; font-size: 34px; font-weight: 800; letter-spacing: -0.02em; color: var(--ink); }
.faq-intro { margin: 0; font-size: 16px; line-height: 1.6; color: var(--muted); max-width: 60ch; }

.faq-jumpnav { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 34px; }
.faq-jump { font-size: 13px; color: var(--ink); background: var(--surface); border: 1px solid var(--border-2); border-radius: 999px; padding: 8px 15px; text-decoration: none; transition: border-color .15s ease, color .15s ease; }
.faq-jump:hover { border-color: var(--accent-blue); color: var(--link); }
/* AFTER :hover ON PURPOSE. `.faq-jump.is-active` and `.faq-jump:hover` are both
   one class plus one simple selector, so they carry identical specificity and the
   later rule is the one that wins. Written above :hover, the active pill would
   turn white-on-blue-text the moment the pointer crossed it and stop looking
   active exactly when the user was pointing at it. */
.faq-jump.is-active {
    /* AN INVERTED CHIP, and it has to invert in both directions: the label takes
       --surface rather than a literal white, so on a dark page it is the dark
       surface colour on the light --ink fill instead of white on white. */
    background: var(--ink); border-color: var(--ink); color: var(--surface); font-weight: 500;
}

.faq-cat { margin-bottom: 34px; scroll-margin-top: 107px; }
.faq-cat-h { margin: 0 0 14px; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--link); }
/* ONE CARD PER SECTION, questions are rows inside it. Not one card per question:
   nine floating cards is the thing that left holes in the layout. At every width
   this is a single panel and the rows simply stack. */
.faq-list { border: 1px solid var(--border); border-radius: 12px; overflow: hidden; background: var(--surface); }

/* The row divider is a POSITIONED HAIRLINE, not a border, so it belongs to the gap
   between two rows rather than to either row. A border-top would also be a single
   side border, which this codebase does not use, and it would have needed undoing
   again on the first row. The adjacent sibling selector means the first row never
   gets one, so there is nothing to reset. */
.faq-item + .faq-item { position: relative; }
.faq-item + .faq-item::before {
    content: ""; position: absolute; left: 0; right: 0; top: 0;
    height: 1px; background: var(--border);
}
.faq-q {
    display: flex; align-items: center; justify-content: space-between; gap: 18px;
    min-height: 44px; padding: 19px 22px; cursor: pointer; list-style: none;
    font-size: 16px; font-weight: 600; color: var(--ink);
}
.faq-q::-webkit-details-marker { display: none; }
.faq-q:hover { background: var(--off-white); }
.faq-chev { width: 17px; height: 17px; flex: 0 0 auto; color: var(--muted-2); transition: transform .2s ease; }
.faq-item[open] > .faq-q .faq-chev { transform: rotate(180deg); }
/* The answer measure is tighter than the card. The card is sized for the question
   rows, which are one line of heavier type; a paragraph wants a shorter line than
   that, so it is capped independently rather than filling the panel. */
.faq-a { padding: 0 22px 19px; max-width: 720px; font-size: 15px; line-height: 1.7; color: var(--text-body); }
.faq-a p { margin: 0 0 10px; }
.faq-a p:last-child { margin-bottom: 0; }
.faq-a a { color: var(--link); }

@media (max-width: 768px) {
    .faq-h1 { font-size: 26px; }
    .faq-q { padding: 16px 17px; font-size: 15px; }
    .faq-a { padding: 0 17px 16px; }
}

@media (max-width: 640px) {
    .faq-page { padding: 32px var(--gutter) 56px; }
}


/* =========================================================================
   Homepage FAQ block — dark, glass-consistent with the catalog cards.
   ========================================================================= */
.catalog-dark .home-faq { max-width: 1440px; margin: 44px auto 0; }
.home-faq-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
.home-faq-h { margin: 0; font-size: 22px; font-weight: 800; letter-spacing: -0.01em; color: var(--cat-h); }
.home-faq-all { font-size: 14px; color: var(--cat-link); text-decoration: none; }
.home-faq-all:hover { color: var(--cat-link-h); }
.home-faq-list { display: grid; gap: 10px; }
.home-faq-item {
    border-radius: 14px; overflow: hidden;
    background: var(--cat-panel-bg);
    box-shadow: var(--cat-panel-edge);
}
/* NO backdrop-filter ON THE FAQ ITEMS, at any width.
 *
 * The @supports block above swapped the solid --cat-panel-bg for a 7.5% white
 * wash and blurred whatever was behind it, and the 767 block then took both back
 * off on a phone. So the same component had two different grounds depending on
 * the width, and on the desktop one the text's contrast depended on the hero
 * gradient that happened to be behind it, which is not a fixed number at all.
 *
 * --cat-panel-bg is what a phone already got, it is opaque enough to measure
 * against, and it is the token the rest of the dark catalog panels use. What the
 * blur was doing visually is done by the existing box-shadow rim instead, which
 * is unchanged. Measured: the text contrast on this component is now one number
 * at every width, and it is the number the phone already had. */
.home-faq-q {
    display: flex; align-items: center; justify-content: space-between; gap: 14px;
    min-height: 44px; padding: 15px 20px; cursor: pointer; list-style: none;
    font-size: 15.5px; font-weight: 600; color: var(--cat-h);
}
.home-faq-q::-webkit-details-marker { display: none; }
.home-faq-chev { width: 18px; height: 18px; flex: 0 0 auto; color: var(--cat-chev); transition: transform .2s ease; }
.home-faq-item[open] > .home-faq-q .home-faq-chev { transform: rotate(180deg); }
.home-faq-a { padding: 2px 20px 16px; font-size: 14.5px; line-height: 1.65; color: var(--cat-body); }
.home-faq-a p { margin: 0 0 8px; }
.home-faq-a p:last-child { margin-bottom: 0; }
.home-faq-a a { color: var(--cat-link); }
@media (max-width: 640px) {
    .home-faq-q { padding: 14px 16px; font-size: 14.5px; }
    .home-faq-a { padding: 2px 16px 14px; }
}


/* =========================================================================
   Hamburger menu: under 1200px, OR any touch-primary device (pointer: coarse),
   OR a device with no pointer. The horizontal bar renders only on a fine
   pointer at 1200 and up. An iPad Pro 13 reports 1366px and a coarse pointer
   and gets the drawer; a laptop at 1366 gets the bar. The same split the admin
   uses for its tap floor, so the two chromes agree on what a tablet is.

   The 900 that stood here before was measured, not chosen, and the note is kept
   for the record: the widths it named are all inside the drawer band now.

   WHY 900 AND NOT 767, measured rather than chosen.
   The bar is one row of a fixed width brand, the link list, and the actions.
   Signed OUT it is four links and fits everywhere: 158px of slack at 768.
   Signed IN it is seven, and once the nav pills went to 44px for touch it
   needed 725px inside a 704px bar at 768: 21px over, and the row spilled past
   its own strip. At 834 the same set fits with 45px to spare, which is 5.8% of
   the bar and one unread badge away from spilling too.

   So both iPad portrait widths hand over to the drawer, and 1024 up keeps the
   bar, where the signed in row still has 70px of room at its tightest. The
   drawer is not a downgrade at these widths: it is 22 to 28 links at 44px each
   instead of seven pills competing for a row.

   900 is also the width the admin sidebar collapses at, so the two chromes
   change at the same place.
   ========================================================================= */
.topbar-mobile { display: none; }
.mnav, .mnav-noscript { display: none; }

@media (max-width: 1199px), (pointer: coarse), (pointer: none) {
    .topbar-inner { display: none; }
    /* Same two-state treatment as the desktop bar: see the .navbar rules above for
       why only the colours transition and the blur is a fixed, class-toggled value. */
    .topbar-mobile {
        display: flex; align-items: center; justify-content: space-between; gap: 12px;
        height: 76px; padding: 0 var(--gutter);
        /* THE STRIP CARRIES THE DARK, not the container. At this width the bar is
           a full-width strip rather than a floating pill, so once the container
           stopped painting on light pages the strip had to hold the colour itself
           or the mobile bar would have gone light. Same matte panel the container
           uses on the dark catalog, so the two widths agree. */
        background-color: var(--surface-dark);
        border-bottom: 1px solid rgba(255, 255, 255, 0.07);
        transition: background-color 0.25s ease, border-color 0.25s ease;
    }
    .topbar.is-scrolled .topbar-mobile {
        background-color: rgba(16, 18, 22, 0.72);
        border-bottom-color: rgba(255, 255, 255, 0.12);
        -webkit-backdrop-filter: blur(16px) saturate(1.3);
        backdrop-filter: blur(16px) saturate(1.3);
    }

    /* Light pages: same treatment mirrored, exactly as the desktop bar. */
    /* .tm-cart keeps #2E5BE0 + white text on both surfaces. */
}

@media (min-width: 1200px) and (pointer: fine) {
    /* Never let the overlay/fallback interfere with the desktop nav. */
    .mnav, .mnav-noscript { display: none !important; }
}


/* Collapsed bar */
.tm-brand { display: inline-flex; align-items: center; flex: 0 0 auto; }
.tm-brand:hover { text-decoration: none; }
.tm-logo { height: 40px; width: auto; display: block; }
.tm-brand .logo-wordmark { color: #fff; font-size: 18px; font-weight: 800; letter-spacing: -0.01em; }
.tm-actions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
/* REVERTED FROM ORANGE with the desktop .order-pill above, back to the blue and
   to bare white count text with no bubble behind it. Note that the hover here
   only ever restated the colour: the blue .tm-cart had no hover background of its
   own and does not get one back, because "exactly as it was" is the ask. */
.tm-cart {
    display: inline-flex; align-items: center; justify-content: center; gap: 7px;
    min-height: 44px;   /* 44px minimum tap target */
    background: #2E5BE0; color: #fff; border-radius: 999px; padding: 0 14px;
    font-size: 13px; font-weight: 600; text-decoration: none;
}
.tm-cart:hover { text-decoration: none; color: #fff; }
.tm-cart-ico { display: inline-flex; flex: 0 0 auto; }
.tm-cart-ico svg { width: 17px; height: 17px; display: block; }
.tm-cart-count { font-family: var(--font-mono); font-weight: 700; line-height: 1; }
.tm-burger {
    width: 44px; height: 44px; padding: 0; border: none; background: none; color: #fff;
    display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
}
.tm-burger svg { width: 27px; height: 27px; }

/* Full-screen overlay — SOLID fill.
   No backdrop-filter and no animated glow layers, deliberately. This overlay only
   ever renders below 768px (it is display:none !important above that), so either
   one is a phone-only cost by definition. The blur was blur(24px) saturate(140%)
   over the whole viewport, and because the overlay fades in via opacity, iOS
   Safari had to re-rasterize a blurred copy of the entire page behind it on EVERY
   frame of the 180ms fade. That was the stutter. The fill is opaque instead:
   nothing shows through, so there is nothing worth blurring. */
.mnav {
    position: fixed; inset: 0; z-index: 10050;
    /* CLOSED MEANS display:none, NOT merely invisible. Safari picks the toolbar
       tint by sampling fixed elements, and a fixed full-viewport element that is
       only opacity:0 / visibility:hidden is still sampled -- closed, this overlay
       was handing Safari #0b0e17 at phone widths and beating .tint-sampler, which
       sits 10000 below it in z-order. display:none is the one state that takes it
       out of the running, and it also keeps the closed overlay out of the tab
       order, which is what visibility:hidden used to be doing.

       THE 180ms FADE SURVIVES. display is transitioned as a discrete property:
       allow-discrete flips it to flex at the START of the open, so the opacity
       animation has something to run on, and back to none at the END of the
       close. @starting-style gives the opening frame an opacity to animate from.
       A browser without allow-discrete gets an instant open/close, no fade, still
       fully functional. */
    display: none; overflow: hidden;
    opacity: 0;
    background: #0b0e17;
    transition: opacity .18s ease, display .18s allow-discrete;
    font-family: 'Inter', system-ui, sans-serif;
}
.mnav.is-open { display: flex; opacity: 1; }
@starting-style {
    .mnav.is-open { opacity: 0; }
}

.mnav-inner { position: relative; z-index: 1; display: flex; flex-direction: column; width: 100%; height: 100%; }

.mnav-head { flex: 0 0 auto; height: 76px; display: flex; align-items: center; justify-content: space-between; padding: 0 16px; }
.mnav-close { width: 44px; height: 44px; padding: 0; border: none; background: none; color: #fff; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
.mnav-close svg { width: 27px; height: 27px; }

.mnav-search { flex: 0 0 auto; position: relative; padding: 0 18px; margin-bottom: 6px; }
.mnav-search-ico { position: absolute; left: 34px; top: 50%; transform: translateY(-50%); display: inline-flex; pointer-events: none; }
.mnav-search-ico svg { width: 18px; height: 18px; color: #7d8494; }
.mnav-search input {
    width: 100%; box-sizing: border-box; height: 48px;
    background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 11px; color: #fff; font-size: 15px; padding: 0 14px 0 44px;
}
.mnav-search input::placeholder { color: #7d8494; }
.mnav-search input:focus { outline: none; border-color: rgba(46, 91, 224, 0.7); }
/* The drawer field's clear control, inside the field at the right; see .nav-search-clear. */
.mnav-search-clear {
    position: absolute; right: 26px; top: 50%; transform: translateY(-50%);
    width: 32px; height: 32px; padding: 0; border: none; border-radius: 8px;
    background: transparent; color: #7d8494;
    display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
}
.mnav-search-clear[hidden] { display: none; }
.mnav-search-clear:hover { color: #fff; }
.mnav-search-clear svg { width: 17px; height: 17px; display: block; }
.mnav-search[data-live] input { padding-right: 48px; }
.mnav-search[data-live] input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

.mnav-links { flex: 1 1 auto; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 8px 18px 12px; }
/* Hairlines sit on the TOP of each row, not the bottom, so the group divider
   below never doubles up with a row's own rule. */
.mnav-link {
    display: flex; align-items: center; gap: 14px;
    min-height: 52px;   /* comfortably past the 44px minimum */
    border-top: 1px solid rgba(255, 255, 255, 0.06);
    color: #fff; font-size: 17px; text-decoration: none;
}
.mnav-link:first-child { border-top: none; }
.mnav-link:hover { text-decoration: none; }
.mnav-ico { width: 21px; height: 21px; flex: 0 0 auto; color: var(--chrome-link); }
/* THE DRAWER'S PRIMARY ROWS ONLY. They carry 'nav-item-ico' as well, which the
   category rows and the account group do not, so those keep the 21px blue they
   have always had and only the seven that match the desktop bar change. Those
   seven are white like their labels since Sept 18, 2026 (they were orange). */
.mnav-link .nav-item-ico { width: 17px; height: 17px; color: currentColor; }   /* white, the row's own colour; was orange */

/* Divider between the site links and the account group: a spacer, with the
   heavier rule drawn on the first account row after it. */
.mnav-sep { display: block; height: 14px; }
.mnav-sep + .mnav-link { border-top-color: rgba(255, 255, 255, 0.14); }
/* ---- Theme toggle ---------------------------------------------------------
   THREE RULES, AND NOT ONE COLOUR AMONG THEM. The desktop button wears
   .nav-icon-btn and the drawer row wears .mnav-link, so both inherit the colour,
   the hover, the transition and the focus ring that already exist for the
   controls beside them. Nothing here restates a colour, which is why light mode
   is byte-identical: these rules only exist on an element that is not rendered
   at all unless the admin switches the setting on.

   [hidden] needs saying out loud because .nav-icon-btn svg sets display:block and
   would otherwise win over the UA sheet's [hidden]{display:none} and show both
   icons at once. */
.theme-toggle-ico { width: 18px; height: 18px; flex: 0 0 auto; }
.theme-toggle-ico[hidden] { display: none; }
/* A <button> in a list of <a>s. Only the differences a button brings with it are
   reset; the border-TOP is left alone because .mnav-link draws the row's hairline
   with it, and :first-child removes it for this row anyway. */
.mnav-theme {
    width: 100%;
    padding: 0;
    background: none;
    border-right: 0;
    border-bottom: 0;
    border-left: 0;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
}

.mnav-link-acct { font-size: 16px; min-height: 48px; }
.mnav-link-acct .mnav-ico { color: #7d8494; }
.mnav-link-acct .mnav-label { color: var(--ink-01); }

.mnav-foot { flex: 0 0 auto; border-top: 1px solid rgba(255, 255, 255, 0.08); padding: 8px 18px 12px; display: flex; flex-wrap: wrap; gap: 4px 14px; }
/* 44px tall, like everything else in the drawer. They measured 18.6px, which is
   the text and nothing else; the type size is unchanged and the height comes
   from the line box. */
.mnav-foot a { font-size: 12px; color: var(--chrome-muted-2); text-decoration: none;
    display: inline-flex; align-items: center; justify-content: center;
    min-height: 44px; min-width: 44px; }
.mnav-foot a:hover { color: var(--ink-01); }

/* Body scroll lock while the menu is open (position preserved by JS). */
body.mnav-lock { position: fixed; left: 0; right: 0; width: 100%; overflow: hidden; }

/* No-JS fallback nav (revealed by the <noscript> style on mobile). */
.mnav-noscript { background: #1c1c1c; padding: 6px 18px 16px; border-bottom: 1px solid var(--border-dark); }
.mnav-noscript a { display: block; padding: 12px 0; color: #fff; font-size: 16px; text-decoration: none; border-bottom: 1px solid rgba(255, 255, 255, 0.06); }
.mnav-noscript-legal { display: flex; flex-wrap: wrap; gap: 14px; padding-top: 12px; }
.mnav-noscript-legal a { display: inline; padding: 0; border: none; font-size: 12px; color: var(--chrome-muted-2); }









/* =========================================================================
   Homepage SEO copy (dark catalog), public COA library (light), admin SEO counter.
   ========================================================================= */
/* Same width wrapper as .pgrid / .home-faq (1440px, inside .catalog padding). */
.catalog-dark .home-seo { max-width: 1440px; margin: 48px auto 0; }
.home-seo-inner { display: grid; grid-template-columns: 1fr; gap: 14px; }


/* NO BACKDROP-FILTER. It used to blur the page behind these panels from 768 up,
   which is the one rule this site does not bend outside the nav strip, and it cost a
   compositing layer on the longest page.

   THE REPLACEMENT IS THE SITE'S OWN FLAT FALLBACK, unchanged, not a new colour. That
   value already shipped at 767 and below, so phones stay pixel-identical and only the
   768 to 1024 band, the only place the blur was ever active, changes. A first attempt
   used a more opaque gradient here and moved every width; the pixel diff caught it. */
.home-seo-block {
    border-radius: 16px; padding: 22px;
    background: var(--cat-panel-bg);
    box-shadow: var(--cat-panel-edge), var(--cat-panel-lift);
}

.home-seo-head { display: flex; align-items: center; gap: 11px; margin-bottom: 12px; }
.home-seo-titles { min-width: 0; }

/* ---- Hue per card ---------------------------------------------------------
   THE PALETTE LIVES HERE, not in home.php, which carries a name rather than a
   colour. Held as an "r, g, b" triple so the icon well can mix its own alphas with
   plain rgba(): color-mix() would express this in one step, but it only reaches
   Chrome 111, Safari 16.2 and Firefox 113, and this is a public storefront. Same
   values, same result, no support question to answer. */
/* Scoped to the section, not to the card: the rail buttons carry the same attribute
   and need the same variable. When this only matched .home-seo-block the buttons had
   no --seo-hue at all, so every rgba(var(--seo-hue), ...) was invalid and the rail
   icons fell back to the initial black on a black panel. */
/* THREE MAPS, SAME THREE HUES. The shipped triplets are picked for a near-black
   panel and are the ORIGINAL, so they are the bare rules: with no choice made,
   the SEO block is on the dark panel it has always been on.

   On the white panel an explicit light choice reaches they measured 2.14, 1.77
   and 2.72:1 on the kicker labels, the only new under-gate text run 4's light
   sweep found. The light map is the same hues taken down until each clears
   4.5:1 there (5.4, 6.1, 6.5:1), and they are the values the tints, the icon
   ring and the rail bar read too, so the whole family moves together.

   .ws-tile has its OWN map further down and is not touched: wholesale is a
   permanently dark page in all three states. */
.home-seo [data-seo-hue]                { --seo-hue: 56, 189, 248; }
.home-seo [data-seo-hue="cyan"]         { --seo-hue: 56, 189, 248; }
.home-seo [data-seo-hue="green"]        { --seo-hue: 61, 220, 151; }
.home-seo [data-seo-hue="violet"]       { --seo-hue: 167, 139, 250; }
:root[data-theme="light"] .home-seo [data-seo-hue]          { --seo-hue: 14, 116, 144; }
:root[data-theme="light"] .home-seo [data-seo-hue="cyan"]   { --seo-hue: 14, 116, 144; }
:root[data-theme="light"] .home-seo [data-seo-hue="green"]  { --seo-hue: 18, 112, 63; }
:root[data-theme="light"] .home-seo [data-seo-hue="violet"] { --seo-hue: 91, 70, 201; }
:root[data-theme="dark"] .home-seo [data-seo-hue]          { --seo-hue: 56, 189, 248; }
:root[data-theme="dark"] .home-seo [data-seo-hue="cyan"]   { --seo-hue: 56, 189, 248; }
:root[data-theme="dark"] .home-seo [data-seo-hue="green"]  { --seo-hue: 61, 220, 151; }
:root[data-theme="dark"] .home-seo [data-seo-hue="violet"] { --seo-hue: 167, 139, 250; }

/* The well: a faint wash of the hue lit from the top left, an inset hairline ring in
   the hue, and a soft glow of it below. Replaces the solid amber block, which read as
   a filled chip rather than an engraved one. */
.home-seo-icon {
    flex: 0 0 auto; width: 46px; height: 46px; border-radius: 12px;
    display: inline-flex; align-items: center; justify-content: center;
    position: relative;
    background:
        radial-gradient(120% 120% at 20% 15%, rgba(var(--seo-hue), 0.22), transparent 70%),
        linear-gradient(160deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0));
}
.home-seo-icon::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 12px;
    pointer-events: none;
    box-shadow:
        inset 0 0 0 1px rgba(var(--seo-hue), 0.42),
        0 8px 22px -10px rgba(var(--seo-hue), 0.55);
}
.home-seo-icon svg {
    width: 22px; height: 22px; display: block;
    fill: none;
    stroke: rgb(var(--seo-hue));
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.home-seo-kicker {
    margin: 0 0 6px;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: rgb(var(--seo-hue));
}
.home-seo-h2 { margin: 0; font-size: 17px; font-weight: 800; letter-spacing: -0.01em; color: var(--cat-h); }
/* =========================================================================
   WHOLESALE APPLICATION  /wholesale

   The icon wells reuse .home-seo-icon and the .home-seo [data-seo-hue] map that
   already ship, so the treatment is defined once. The hue map is scoped to
   .home-seo, so this page opts in by the same attribute on a .ws-tile below.
   ========================================================================= */
/* .page paints the light off-white ground for every ordinary page, and it wins over
   body.catalog-dark because it is an element background rather than the body's. The
   dark ground is opted into here rather than by adding another global rule. */
.catalog-dark .page.ws { background: transparent; }
.ws-inner { max-width: 1100px; }
.ws-eyebrow {
    margin: 0 0 6px;
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ink-17);
}
.ws-h1 { margin: 0 0 14px; font-size: clamp(30px, 4vw, 44px); font-weight: 800; letter-spacing: -0.015em; color: #fff; }
.ws-lede { margin: 0 0 44px; max-width: 62ch; font-size: 1.08rem; line-height: 1.65; color: var(--ink-01); }

.ws-tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; margin-bottom: 64px; }
.ws-tile {
    position: relative;
    padding: 24px 24px 26px;
    border-radius: 16px;
    background: linear-gradient(165deg, #131a2c, #0f1524);
}
/* Inset ring rather than a border, so the radius stays true. */
.ws-tile::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 16px;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
    pointer-events: none;
}
/* The shipped hue map is scoped to .home-seo; this page uses the same tokens. */
.ws-tile[data-seo-hue]                { --seo-hue: 56, 189, 248; }
.ws-tile[data-seo-hue="cyan"]         { --seo-hue: 56, 189, 248; }
.ws-tile[data-seo-hue="green"]        { --seo-hue: 61, 220, 151; }
.ws-tile[data-seo-hue="violet"]       { --seo-hue: 167, 139, 250; }
.ws-tile[data-seo-hue="amber"]        { --seo-hue: 245, 182, 78; }
.ws-tile .home-seo-icon { margin-bottom: 16px; }
.ws-tile-h { margin: 0 0 6px; font-size: 1.02rem; font-weight: 700; color: #fff; }
.ws-tile-p { margin: 0; font-size: 0.9rem; line-height: 1.6; color: var(--ink-01); }

.ws-formhead { margin-bottom: 22px; }
.ws-eyebrow-form { color: rgb(61, 220, 151); }
.ws-h2 { margin: 0 0 10px; font-size: clamp(22px, 2.4vw, 30px); font-weight: 800; letter-spacing: -0.01em; color: #fff; }
.ws-formhead-p { margin: 0; max-width: 68ch; font-size: 0.95rem; line-height: 1.65; color: var(--ink-01); }

.ws-panel {
    position: relative;
    padding: 28px 28px 30px;
    border-radius: 18px;
    background: linear-gradient(165deg, #131a2c, #0f1524);
}
.ws-panel::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 18px;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
    pointer-events: none;
}
.ws-fset { border: 0; margin: 0 0 26px; padding: 0; min-width: 0; }
.ws-fset:last-of-type { margin-bottom: 22px; }
.ws-legend {
    display: block;
    padding: 0;
    margin-bottom: 14px;
    font-size: 0.74rem;
    font-weight: 800;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-17);
}
.ws-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-bottom: 16px; }
.ws-row:last-child { margin-bottom: 0; }
.ws-field { margin: 0; min-width: 0; }
.ws-field label { display: block; margin-bottom: 6px; font-size: 0.86rem; color: var(--ink-01); }
.ws-input {
    width: 100%;
    box-sizing: border-box;
    padding: 11px 13px;
    border: 0;
    border-radius: 9px;
    background: rgba(255, 255, 255, 0.045);
    color: #fff;
    font: inherit;
    font-size: 0.95rem;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
.ws-input:focus { outline: 2px solid var(--accent-blue); outline-offset: -2px; }
.ws-input.is-bad { box-shadow: inset 0 0 0 1px #f87171; }
.ws-input::placeholder { color: var(--muted-steel); }
select.ws-input { appearance: none; background-image: none; }
.ws-opt { display: block; margin-top: 5px; font-size: 0.76rem; color: var(--muted-steel); }
.ws-err { margin: 5px 0 0; font-size: 0.78rem; color: var(--ink-101); }
/* The shared .alert-error is drawn for the light pages: white ground, dark border.
   On the dark application pages that renders as a white slab with grey text in it,
   so the summary is restated here for the ground it actually sits on. */
.ws .alert-error {
    border: 0;
    border-radius: 10px;
    background: rgba(248, 113, 113, 0.12);
    box-shadow: inset 0 0 0 1px rgba(248, 113, 113, 0.35);
    color: var(--ink-102);
}
/* The shared .btn-primary is drawn for the light pages, where it sits on white.
   On this dark ground it needs the brand blue stated, the same blue the hero
   buttons use, or it reads as a black rectangle. */
.ws-submit,
.ws-success .btn-primary,
/* The responder page's CTAs take the SAME treatment /wholesale and /affiliates
   already give theirs. Bare .btn is the near-black storefront button, which is
   invisible against catalog-dark; this is the established fix, not a new one. */
.rsp .btn-primary {
    background: var(--accent-blue);
    color: #fff;
    border: 0;
    border-radius: 10px;
    padding: 12px 22px;
    font-size: 0.95rem;
    font-weight: 600;
}
.ws-submit:hover,
.ws-success .btn-primary:hover { background: #2449b8; color: #fff; }
.ws-submit { margin-top: 4px; }
.ws-after { margin: 12px 0 0; font-size: 0.82rem; color: var(--muted-steel); }
.ws-ruo { margin: 34px 0 0; max-width: 90ch; font-size: 0.82rem; line-height: 1.7; color: var(--muted-steel); }

.ws-success { max-width: 62ch; padding: 40px 0 20px; }
.ws-success-h { margin: 0 0 12px; font-size: clamp(26px, 3.4vw, 38px); font-weight: 800; letter-spacing: -0.015em; color: #fff; }
.ws-success-p { margin: 0 0 24px; font-size: 1.02rem; line-height: 1.7; color: var(--ink-01); }

@media (max-width: 1000px) { .ws-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

@media (max-width: 768px) {
    .ws-row { grid-template-columns: minmax(0, 1fr); }
    .ws-panel { padding: 22px 18px 24px; }
    .ws-lede { margin-bottom: 34px; }
    .ws-tiles { margin-bottom: 44px; }
}

@media (max-width: 560px) { .ws-tiles { grid-template-columns: minmax(0, 1fr); } }


/* =========================================================================
   AFFILIATE PROGRAM  /affiliates

   The form is the /wholesale panel, unchanged: same .ws-panel, .ws-fset,
   .ws-input, .ws-submit, so the two application pages cannot drift apart. Only
   what Option A adds on top of that panel is defined here.

   NO COMMISSION FIGURE APPEARS IN THIS FILE. The rate is rendered by PHP from
   the admin setting; CSS only decides how big it is.
   ========================================================================= */
.ws-row-single { grid-template-columns: minmax(0, 1fr); }
.ws-textarea { min-height: 96px; resize: vertical; line-height: 1.6; }

/* Sits under the lede and above the stats: the one trust line, dimmer than the
   lede so it reads as a footnote to it rather than a second headline. */
.af-trust {
    margin: -30px 0 40px;
    max-width: 68ch;
    font-size: 0.9rem;
    line-height: 1.65;
    color: var(--muted-steel);
}

.af-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; margin-bottom: 40px; }
.af-stat {
    position: relative;
    padding: 22px 24px 24px;
    border-radius: 16px;
    background: linear-gradient(165deg, #131a2c, #0f1524);
}
.af-stat::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 16px;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
    pointer-events: none;
}
/* Same three hues the info cards and the wholesale tiles use, stated here because
   the shipped map is scoped to .home-seo. */
.af-stat[data-af-hue]           { --af-hue: 56, 189, 248; }
.af-stat[data-af-hue="green"]   { --af-hue: 61, 220, 151; }
.af-stat[data-af-hue="cyan"]    { --af-hue: 56, 189, 248; }
.af-stat[data-af-hue="violet"]  { --af-hue: 167, 139, 250; }
.af-stat-big {
    margin: 0;
    font-size: clamp(26px, 2.6vw, 34px);
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.1;
    color: rgb(var(--af-hue));
}
.af-stat-p { margin: 6px 0 0; font-size: 0.88rem; line-height: 1.6; color: var(--ink-01); }
/* Figures that change under a slider or sit beside each other in a column line up
   only on tabular figures; the proportional default makes them jitter. */
.af-money { font-variant-numeric: tabular-nums; }

/* --- calculator ------------------------------------------------------- */
.af-calc {
    position: relative;
    display: grid;
    grid-template-columns: 1.3fr 1fr;
    gap: 34px;
    align-items: center;
    padding: 30px 32px;
    margin-bottom: 48px;
    border-radius: 18px;
    background: linear-gradient(165deg, #131a2c, #0f1524);
}
.af-calc::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 18px;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
    pointer-events: none;
}
.af-calc-controls { min-width: 0; }
.af-calc-h { margin: 0 0 4px; font-size: 1.15rem; font-weight: 700; color: #fff; }
.af-calc-sub { margin: 0 0 22px; font-size: 0.88rem; line-height: 1.6; color: var(--ink-01); }
.af-slider { margin: 0 0 20px; }
.af-slider:last-child { margin-bottom: 0; }
.af-slider label {
    display: flex;
    justify-content: space-between;
    gap: 14px;
    margin-bottom: 8px;
    font-size: 0.85rem;
    color: var(--ink-01);
}
.af-slider label b { color: #fff; font-variant-numeric: tabular-nums; }
.af-slider input[type="range"] { width: 100%; height: 4px; accent-color: var(--accent-blue); }
.af-slider input[type="range"]:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 4px; }

.af-payout {
    position: relative;
    padding: 24px 26px;
    border-radius: 14px;
    text-align: center;
    background: linear-gradient(160deg, rgba(61, 220, 151, 0.16), rgba(61, 220, 151, 0.05));
}
.af-payout::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 14px;
    box-shadow: inset 0 0 0 1px rgba(61, 220, 151, 0.35);
    pointer-events: none;
}
.af-payout-lbl {
    margin: 0;
    font-size: 0.74rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-01);
}
.af-payout-amt {
    margin: 4px 0 2px;
    font-size: clamp(30px, 3.4vw, 42px);
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.1;
    color: rgb(61, 220, 151);
}
/* The sign is set smaller and lifted off the baseline so the digits stay the
   headline; the same treatment the dashboard ledger uses. */
.af-cur { font-size: 0.58em; opacity: 0.9; vertical-align: 14%; margin-right: 2px; color: #5df2ae; }
.af-payout-per { margin: 0; font-size: 0.8rem; color: var(--muted-steel); }
/* Spans both columns: the disclaimer qualifies the whole panel, not the number. */
.af-calc-note {
    grid-column: 1 / -1;
    margin: 4px 0 0;
    font-size: 0.78rem;
    line-height: 1.6;
    color: var(--muted-steel);
}

/* --- steps ------------------------------------------------------------ */
.af-steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; margin-bottom: 56px; }
.af-step { min-width: 0; padding: 0 2px; }
.af-step-n {
    margin: 0;
    font: 800 2.4rem/1 "SF Mono", ui-monospace, Menlo, monospace;
    letter-spacing: -0.04em;
    background: linear-gradient(180deg, #6f92ff, rgba(111, 146, 255, 0.15));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}
.af-step-h { margin: 8px 0 6px; font-size: 1.05rem; font-weight: 700; color: #fff; }
.af-step-p { margin: 0; font-size: 0.9rem; line-height: 1.6; color: var(--ink-01); }

@media (max-width: 900px) {
    .af-stats { grid-template-columns: minmax(0, 1fr); gap: 14px; }
    .af-steps { grid-template-columns: minmax(0, 1fr); gap: 22px; margin-bottom: 44px; }
    .af-calc { grid-template-columns: minmax(0, 1fr); gap: 26px; padding: 24px 20px; }
    .af-trust { margin: -22px 0 32px; }
}

@media (max-width: 560px) {
    .af-payout { padding: 20px 18px; }
    .af-step-n { font-size: 2.1rem; }
}


.home-seo-rail { display: none; }
.home-seo-tabtitle { display: none; }

.home-seo-body { margin: 0; font-size: 14px; line-height: 1.65; color: var(--cat-body-2); }

/* =========================================================================
   INFO SECTION, RAIL AND REVEAL, DESKTOP ONLY

   GATED ON THE SECTION'S OWN EXISTING BREAKPOINT. 1025px is where it already
   switched from one stacked column to three, so that is "desktop" here and
   nothing below it is touched: the cards, their icon treatment, their spacing
   and their order are exactly what shipped.
   ========================================================================= */
@media (min-width: 1025px) {
    .home-seo-inner {
        grid-template-columns: 360px minmax(0, 1fr);
        gap: 0;
        position: relative;
        border-radius: 18px;
        overflow: hidden;
        min-height: 380px;
        background: var(--cat-seo-rail);
    }
    /* Inset hairline ring rather than a border, so the radius stays true and no
       single side carries an edge of its own. */
    .home-seo-inner::after {
        content: "";
        position: absolute;
        inset: 0;
        border-radius: 18px;
        box-shadow: var(--cat-seo-rail-edge);
        pointer-events: none;
    }

    .home-seo-rail {
        display: block;
        grid-column: 1;
        grid-row: 1;
        align-self: stretch;
        padding: 22px 0;
        position: relative;
    }
    .home-seo-rail::after {
        content: "";
        position: absolute;
        top: 0; bottom: 0; right: 0;
        width: 1px;
        background: var(--cat-seo-rule);
    }
    .home-seo-tab {
        display: flex;
        gap: 14px;
        align-items: center;
        width: 100%;
        text-align: left;
        background: none;
        border: 0;
        color: var(--cat-seo-tab);
        font: inherit;
        padding: 18px 26px;
        cursor: pointer;
        position: relative;
        opacity: .65;
        transition: opacity .18s ease, background .18s ease;
    }
    .home-seo-tab.is-on { opacity: 1; background: linear-gradient(90deg, rgba(var(--seo-hue), .14), transparent 80%); }
    /* Inset positioned bar, not a border-left. */
    .home-seo-tab.is-on::before {
        content: "";
        position: absolute;
        left: 0; top: 10px; bottom: 10px;
        width: 3px;
        border-radius: 3px;
        background: rgb(var(--seo-hue));
    }
    .home-seo-tab:focus-visible { outline: 2px solid rgb(var(--seo-hue)); outline-offset: -3px; }
    .home-seo-tab .home-seo-titles { min-width: 0; }
    .home-seo-tabtitle { display: block; font-size: 1.12rem; font-weight: 700; color: var(--cat-h); }
    .home-seo-tab .home-seo-kicker { display: block; margin-bottom: 2px; }

    /* The cards become the body column. Each keeps its own row until JS takes over,
       so with scripting off all three read in sequence instead of stacking on top of
       one another in a single cell. */
    .home-seo-block {
        grid-column: 2;
        border-radius: 0;
        background: none;
        box-shadow: none;
        padding: 40px 44px;
    }
    /* The rail already carries the icon and the kicker for the active topic, so the
       body shows the heading and the copy, at the larger type the panel calls for. */
    .home-seo-block .home-seo-icon,
    .home-seo-block .home-seo-kicker { display: none; }
    .home-seo-head { margin-bottom: 14px; }
    .home-seo-h2 { font-size: 1.3rem; }
    .home-seo-body { font-size: 1rem; max-width: 64ch; }

    /* Only once JavaScript is running do the three bodies share one cell and the
       inactive ones go away. Without it, the rule below never applies. */
    [data-seo-rail-ready] .home-seo-block { grid-row: 1; }
    [data-seo-rail-ready] .home-seo-block:not(.is-on) { display: none; }
}


/* =========================================================================
   THE INFO PANEL IN MATTE BLACK, BOTH THEMES
   (2026-09-14, docs/mockups/black-cards-mockup-2.html)

   ONE SET OF RULES. Nothing here reads --seo-hue or a --cat-* token, which is
   what the light theme used to steer, so the panel on the white page is the
   panel on the dark page. Ground #141518, a hairline of white at .09 (drawn as
   the inset ring this sheet already used, so the radius stays true), 20px
   corners, white heading, #a7abb3 copy.

   THE ACTIVE ROW IS LIFTED, NOT MARKED. A faint fill and an inner top
   highlight, and NO left accent bar: the is-on::before bar is removed below.
   That rule is permanent and this is the component it is about.

   The icon chip rules are scoped to .home-seo: .ws-tile reuses .home-seo-icon
   on the wholesale page and keeps its own hue treatment.

   Stacking is what it was: the rail exists from 1025 up; below that the three
   bodies are the stacked cards, in the same black. The rail's divider is the
   right-hand rule; there is no stacked state in which the rail is drawn, so
   there is no bottom-border form of it. */
.home-seo .home-seo-icon {
    width: 44px; height: 44px; border-radius: 12px;
    background: rgba(255, 255, 255, 0.05);
}
.home-seo .home-seo-icon::after { border-radius: 12px; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.09); }
.home-seo .home-seo-icon svg { width: 20px; height: 20px; stroke: #d7dae0; stroke-width: 1.3; }
.home-seo .home-seo-kicker { color: #7f848d; }
.home-seo .home-seo-h2 { color: #ffffff; }
.home-seo .home-seo-body { color: #a7abb3; }
@media (max-width: 1024px) {
    .home-seo-block {
        border-radius: 20px;
        background: #141518;
        box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.09);
    }
}

@media (min-width: 1025px) {
    .home-seo-inner { border-radius: 20px; background: #141518; }
    .home-seo-inner::after { border-radius: 20px; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.09); }
    .home-seo-rail { padding: 14px; }
    .home-seo-rail::after { background: rgba(255, 255, 255, 0.09); }
    .home-seo-tab { padding: 16px; border-radius: 14px; opacity: 1; color: #e9eaec; }
    .home-seo-tab.is-on { background: rgba(255, 255, 255, 0.055); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.09); }
    .home-seo-tab.is-on::before { content: none; }
    .home-seo-tab:focus-visible { outline: 2px solid var(--pcd-orange); }
    .home-seo-tab.is-on .home-seo-icon svg { stroke: var(--pcd-orange); }
    .home-seo-tabtitle { color: #e9eaec; font-size: 17px; font-weight: 600; }
    .home-seo-tab.is-on .home-seo-tabtitle { color: #ffffff; }
}

.seo-counter.over { color: var(--ink-22); }

/* ---- Backorder ("more on the way") -------------------------------------
   A buyable state that is neither in stock nor out of stock. */

/* Catalog card: a full-width strip in the normal flow between the image and the
   name. Deliberately NOT absolutely positioned over the image, so it can never
   sit off-centre over artwork of a different size. */
/* The card's row is now a holder for the shared pill rather than a band of its
   own: no fill, no radius, no letter-spacing, just the centring and the gap. */
.pcard-backorder {
    margin: 10px 12px 0;
    text-align: center;
}
.pcard-bo-pill {
    display: inline-block;
    background: var(--pill-red);
    color: var(--pill-ink);
    border-radius: 999px;
    padding: 3px 10px;
    font-size: 11.5px;
    font-weight: 650;
}
.pcard-restock { font-size: 11.5px; color: var(--cat-restock, var(--ink-11)); margin: 2px 0 0; }

/* Product page: amber pill beside the price, matching the in-stock pill pattern. */
/* THE CART'S PILL. Same fill, same ink, same radius as .cart-bo-pill, which is
   the point: one fact, one colour, wherever it is said. The amber it replaced was
   the same tint the "expected" strip underneath uses, so the pill and the strip
   read as one soft block rather than as a state and its explanation. */
.pd-backorder-pill {
    display: inline-block;
    background: var(--pill-red);
    color: var(--pill-ink);
    border-radius: 999px;
    padding: 4px 12px;
    font-size: 12.5px;
    font-weight: 600;
    margin-left: 8px;
}
.pd-backorder-pill[hidden] { display: none; }

.pd-backorder-note {
    background: var(--tint-amber-2);
    border: 1px solid var(--tint-amber-ln);
    border-radius: 10px;
    padding: 13px;
    margin: 12px 0 0;
}
.pd-backorder-note[hidden] { display: none; }
.pd-backorder-h    { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: var(--text-1); }
.pd-backorder-body { margin: 0; font-size: 13.5px; color: var(--muted-slate-2); line-height: 1.5; }
.pd-backorder-date { margin: 6px 0 0; font-size: 13.5px; color: var(--muted-slate-2); }
.pd-backorder-date[hidden] { display: none; }

/* Cart line + the order-level notice under the lines. */
.cart-line-backorder { margin: 4px 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
/* SOLID, NOT TINTED. The same red pill the account page already uses for a bad
   order state (.acct-recent-pill.t-bad): #d83b3b on white, 999px, 3px 10px, 11.5px.
   The amber tint it replaced read as a note; a line the customer cannot have today
   is a fact about their order and is said in the site's existing solid red.
   Cart only. .cart-bo-pill is rendered by views/cart.php and by nothing else, so
   the product page's amber .pd-backorder-pill and the card's .pcard-backorder are
   untouched. */
.cart-bo-pill {
    display: inline-block;
    background: var(--pill-red);
    color: var(--pill-ink);
    border-radius: 999px;
    padding: 3px 10px;
    font-size: 11.5px;
    font-weight: 650;
}
.cart-bo-date { font-size: 11.5px; color: var(--ink-11); }
.cart-backorder-note {
    background: var(--tint-amber-2);
    border: 1px solid var(--tint-amber-ln);
    border-radius: 10px;
    padding: 13px;
    margin: 14px 0 0;
}
.cart-bo-head { margin: 0 0 5px; font-size: 13.5px; color: var(--text-1); display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
/* The name and size are the answer to "which one", so they carry the weight. The
   pill beside them is already the loudest thing in the row and needs no more. */
.cart-bo-item { font-weight: 700; min-width: 0; }
.cart-bo-body { margin: 0; font-size: 13px; color: var(--muted-slate-2); line-height: 1.5; }
.cart-bo-body strong { font-weight: 700; color: var(--text-1); }

/* Admin inventory: a negative count means units OWED, not a broken row. */

.inv-stockline.is-backorder { color: var(--pill-amber-ink); }
/* The min-widths that used to sit here are gone. They were added when these two
   controls were bolted onto an eight column grid with no column of their own, and the
   date's 130px floor inside a 48px track is what painted it over the sort order. The
   grid template carries the floors now. */

/* Nothing here may push the page sideways on a small screen. */
@media (max-width: 560px) {
    .pcard-backorder { margin: 8px 8px 0; font-size: 10.5px; }
    .cart-line-backorder, .cart-bo-head { gap: 6px; }
    .pd-backorder-pill { margin-left: 0; margin-top: 6px; }
}


/* A grid item defaults to min-width:auto, so its content can force the track
   wider than the container — that is what pushed the cart and checkout columns
   to 336px inside a 320px viewport and produced a sideways scroll. Letting the
   columns shrink fixes it without changing the desktop layout. */
.cart-lines,
.cart > *,
.checkout-main,
.checkout > * { min-width: 0; }

/* At 320px the cart line's 76px thumbnail + 16px gaps + the total leave the
   middle column too little room, and the total pushed ~6px past the viewport.
   Tightening the thumbnail and gap keeps the same three-column layout. */
@media (max-width: 360px) {
    .cart-line { grid-template-columns: 56px 1fr auto; gap: 10px; }
    .cart-line-media { width: 56px; height: 56px; }
    .cart-line-total { font-size: 15px; }
    .cart-line-info, .cart-line-total { min-width: 0; }
    .cart-line-name { overflow-wrap: anywhere; }
}


/* ---- Users list: inline name editing ----------------------------------- */
.uname-cell { min-width: 180px; }

/* Rows with no name on file are meant to stand out in a long list. */

.uname-flag {
    display: inline-block;
    margin-left: 6px;
    padding: 2px 7px;
    border-radius: 999px;
    background: var(--tint-amber);
    color: var(--pill-amber-ink);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: .03em;
    text-transform: uppercase;
    vertical-align: middle;
}
.uname-flag[hidden] { display: none; }

/* ---- Admin orders list: two status columns ------------------------------
   The row is a grid, not table layout, so the customer column can be given a
   real minimum and everything else a fixed track. The old table let the
   channel pill squeeze the customer name until it was clipped. */
@media (min-width: 901px) {
    /* Both of these are DESKTOP-only. Left global, the 962px floor applied on
       phones too: the card became 962px wide inside a 328px window and the order
       number was laid out ~595px from the left, i.e. entirely off-screen. */
    .ord-table-scroll { overflow-x: auto; }
    /* 984 = the real minimum this grid can occupy: 900px of tracks (with the
       customer column at its 220px floor) plus 6 gaps of 14px. Was 962 when the
       Payment and Fulfilment tracks were 118 and 132; widening them to 136 each
       moves the floor by the same 22px. The scroll behaviour is unchanged — this
       number only has to keep matching the tracks above, or they get squeezed. */
    .ord-table-scroll > .orders-table { min-width: 984px; width: 100%; }
}


@media (min-width: 901px) {
    .orders-table thead tr,
    .orders-table tbody tr {
        display: grid;
        /* Order column is 150px, not 110: it also carries the bulk-select checkbox
           and the item count, which the original width could not hold.
           Date is 84px, not 78: that is what "Jul 24, 2026" measures, so it sits in
           its own track instead of spilling into the gap. */
        /* Payment 118 -> 136 and Fulfilment 132 -> 136. Each cell carries 11px of
           padding a side (from .admin-cards), so the old Payment track gave only
           96px of content — exactly the width of the "payment note" pill, i.e. no
           slack at all. 136 leaves ~18px on both. */
        grid-template-columns: 150px minmax(220px, 1.6fr) 92px 84px 136px 136px 82px;
        gap: 14px;
        align-items: start;
    }
    .orders-table th, .orders-table td { display: block; border-bottom: 0; padding: 12px 0; }
    .orders-table tbody tr { border-bottom: 1px solid var(--border); }

    /* The customer cell must be allowed to shrink, or the grid track refuses to
       go below its content width and the name gets cut off. */
    .orders-table td[data-label="Customer"] { min-width: 0; overflow: hidden; }
    .orders-table td[data-label="Customer"] strong,
    .orders-table td[data-label="Customer"] .order-cust-email {
        display: block;
        min-width: 0;
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* Payment cell: one item per line.
       The note flag used to be an inline-block sitting BESIDE the status pill.
       Neither fits alongside the other in the track, so the flag dropped to a
       second line and then broke inside its own pill — a 35px two-line lozenge
       level with the Fulfilment column's Shipped pill, which is the collision.
       Kept as block layout rather than a flex column on purpose: the select has
       to fill the track, and `width: 100%` on a flex item is a cross-axis size
       that did not resolve against the grid track here. In normal block flow it
       just works. Making the flag and the form block-level also puts the status
       pill on a line of its own, with or without a note. */
    .orders-table td[data-label="Payment"] { min-width: 0; }
    .orders-table td[data-label="Payment"] .pay-note-flag {
        display: block;
        width: max-content;
        max-width: 100%;
        margin: 6px 0 0;
    }
    .orders-table td[data-label="Payment"] .order-status-form {
        display: block;
        margin-top: 6px;
        width: 100%;
    }
    .orders-table td[data-label="Payment"] .status-select {
        display: block;
        width: 100%;
        max-width: none;
    }
}


.ord-items { display: block; font-size: 11px; color: var(--muted); margin-top: 3px; }
.orders-table .order-status-form { margin-top: 6px; }
.orders-table .status-select { max-width: 118px; font-size: 12px; }
.ord-cancelled { opacity: .72; }

/* Status pills. Payment always carries a dot; fulfilment only when shipped. */
.ord-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border-radius: 20px;
    padding: 4px 10px;
    font-size: 12px;
    font-weight: 500;
    white-space: nowrap;
}
.ord-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }

.ord-pay-unpaid   { background: var(--fill-24); color: var(--pill-amber-ink); }
.ord-pay-reported { background: var(--fill-30); color: var(--ink-53); }
.ord-pay-paid     { background: var(--fill-12); color: var(--ink-05); }
.ord-pay-cancelled { background: var(--fill-08); color: var(--ink-73); }
/* Carrier status line under the fulfilment pill on the orders list. Text-only so
   it reads as secondary to the fulfilment pill above it. */
.ord-track {
    display: block;
    margin-top: 5px;
    font-size: 11px;
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ord-track.st-green { color: var(--ink-62); }
.ord-track.st-amber { color: var(--ink-99); }
.ord-track.st-red   { color: var(--ink-98); }
.ord-track.st-gray  { color: var(--ink-42); }
.ord-ful-ready   { background: var(--fill-08); color: var(--muted-slate); }
.ord-ful-shipped { background: var(--fill-39); color: var(--ink-67); }
/* Hand-set delivery. Same settled green as ord-pay-paid, because it carries the
   same meaning: this one is closed out. Solid, no dot. */
.ord-ful-delivered { background: var(--fill-12); color: var(--ink-05); }

/* Filter bar */
.ord-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: center;
    margin: 0 0 12px;
}
.ord-filter-search { position: relative; flex: 1 1 320px; min-width: 0; }
.ord-filter-icon {
    position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
    width: 16px; height: 16px; color: var(--muted-3); pointer-events: none;
}
.ord-filters input[type="search"] {
    width: 100%; height: 40px; border: 1px solid var(--line); border-radius: 9px;
    padding: 0 12px 0 38px; font-family: inherit; font-size: 14px; background: var(--surface);
    -webkit-appearance: none; appearance: none;
}
.ord-filters input[type="search"]:focus { outline: none; border-color: var(--black); }
.ord-filters select {
    height: 40px; border: 1px solid var(--line); border-radius: 9px;
    padding: 0 12px; font-family: inherit; font-size: 14px; background: var(--surface);
    flex: 0 1 175px; min-width: 0;
}

.ord-needs { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 0 0 16px; }
.ord-needs-label {
    font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
}
.ord-needs-btn {
    display: inline-block;
    border: 1px solid var(--line);
    border-radius: 20px;
    padding: 6px 14px;
    font-size: 13px;
    color: var(--ink);
    background: var(--surface);
}
.ord-needs-btn:hover { border-color: var(--black); text-decoration: none; }
.ord-needs-btn.is-active { background: var(--black); border-color: var(--black); color: #fff; }

@media (max-width: 900px) {
    .ord-filters select { flex: 1 1 100%; }
    .ord-filter-search { flex: 1 1 100%; }
}


/* ---- Orders list: mobile card (below 901px) ----------------------------
   The shared .admin-cards pattern turns every cell into a "label: value" row,
   which is wrong for orders: the order number and total belong together on a
   top line, and the two status pills belong side by side. This overrides the
   shared pattern for this one table. */
@media (max-width: 900px) {
    table.orders-table > tbody > tr {
        display: grid;
        grid-template-areas:
            "ord total"
            "cust cust"
            "pills pills"
            "meta meta";
        grid-template-columns: 1fr auto;
        gap: 8px 12px;
        background: var(--surface);
        border: 1px solid var(--edge-18);
        border-radius: 11px;
        padding: 14px;
        margin-bottom: 10px;
        position: relative;   /* anchors the whole-card tap target */
    }
    /* The shared pattern's label/value rows and separators do not apply here. */
    table.orders-table > tbody > tr > td {
        display: block;
        padding: 0;
        border: none;
        text-align: left;
        min-width: 0;
    }
    table.orders-table > tbody > tr > td::before { content: none !important; }

    table.orders-table > tbody > tr > td[data-label="Order"] {
        grid-area: ord;
        display: flex;
        align-items: center;
        /* The shared card pattern sets justify-content:space-between, which is what
           pushed the order number to the middle of the card. Cancel it explicitly. */
        justify-content: flex-start;
        gap: 9px;
        min-width: 0;
    }
    .orders-table td[data-label="Order"] .order-link {
        font-family: var(--font-mono);
        font-size: 15px;
        white-space: nowrap;
    }
    /* Item count is desktop detail; the card has no room for it. */
    .orders-table td[data-label="Order"] .ord-items { display: none; }
    /* Above the card's tap layer so it stays clickable. */
    .orders-table td[data-label="Order"] input[type="checkbox"] {
        position: relative; z-index: 2; flex: none; margin: 0;
    }

    table.orders-table > tbody > tr > td[data-label="Total"] {
        grid-area: total;
        font-family: var(--font-mono);
        font-size: 15px;
        text-align: right;
        white-space: nowrap;
    }

    table.orders-table > tbody > tr > td[data-label="Customer"] { grid-area: cust; overflow: hidden; }
    .orders-table td[data-label="Customer"] strong {
        display: block; font-size: 14px; font-weight: 500;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    .orders-table td[data-label="Customer"] .order-cust-email {
        display: block; font-size: 11px; font-family: var(--font-mono); color: var(--ink-42);
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }

    /* Payment + fulfilment share one row and wrap if the pills are long. */
    table.orders-table > tbody > tr > td[data-label="Payment"] {
        grid-area: pills; display: flex; flex-wrap: wrap; align-items: center;
        justify-content: flex-start; gap: 8px;
    }
    table.orders-table > tbody > tr > td[data-label="Fulfilment"] {
        grid-area: pills; display: flex; align-items: center;
        justify-content: flex-end; gap: 8px;
    }
    /* The quick status select is a desktop affordance; the card opens the order. */
    .orders-table td[data-label="Payment"] .order-status-form { display: none; }

    table.orders-table > tbody > tr > td[data-label="Channel"],
    table.orders-table > tbody > tr > td[data-label="Date"] {
        grid-area: meta; font-size: 12px; color: var(--muted-slate);
    }
    table.orders-table > tbody > tr > td[data-label="Channel"] { justify-self: start; }
    table.orders-table > tbody > tr > td[data-label="Date"]    { justify-self: end; }
    .orders-table td[data-label="Channel"] .st-pill {
        background: none; padding: 0; font-size: 12px; font-weight: 400; color: var(--muted-slate);
    }

    /* Whole card is tappable: the link's ::after stretches over the card. It must
       resolve against the tr (position:relative), so the link itself must NOT be
       positioned — making it its own containing block would shrink the overlay to
       the size of the text. The checkbox is lifted above it so it stays clickable. */
    table.orders-table > tbody > tr > td[data-label="Order"] .order-link::after {
        content: ""; position: absolute; inset: 0; z-index: 1;
    }

    /* Filters: everything full width, needs-action buttons wrap. */
    .ord-filters { flex-direction: column; align-items: stretch; }
    .ord-filter-search, .ord-filters select, .ord-filters .btn { width: 100%; flex: 1 1 auto; }
    .ord-needs { flex-wrap: wrap; }
    .ord-needs-btn { flex: 0 1 auto; }
}


/* =========================================================================
   CUSTOMER ACCOUNT: order history (/account)

   This table used to carry .orders-table, which is the ADMIN orders grid. Its
   mobile rules place cells by grid-area keyed off data-label values this table
   never had, so on a phone the row became a two-column grid with every cell
   auto-placed on top of its neighbour: order number printed over the date,
   total printed over the Download link, while the six-column header row stayed
   visible above the wreckage. It has its own class and its own card now.
   ========================================================================= */
.account-orders .order-link { color: var(--ink); font-weight: 600; }
.account-orders .order-link:hover { text-decoration: underline; }

@media (max-width: 767px) {
    /* The wrapper's frame and scrollbar belong to the desktop table only: the
       cards carry their own borders, and nothing may scroll sideways. */
    .account-orders-wrap {
        overflow: visible;
        border: none;
        background: transparent;
    }

    /* Labels move inline with the values, so the header row has no job left. */
    table.account-orders > thead { display: none; }
    table.account-orders,
    table.account-orders > tbody { display: block; width: 100%; }

    table.account-orders > tbody > tr {
        display: grid;
        grid-template-areas:
            "num    status"
            "date   date"
            "items  total"
            "pay    pay"
            "inv    inv";
        grid-template-columns: minmax(0, 1fr) auto;
        gap: 6px 12px;
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: 11px;
        padding: 14px;
        margin-bottom: 10px;
    }
    table.account-orders > tbody > tr:last-child { margin-bottom: 0; }

    table.account-orders > tbody > tr > td {
        display: block;
        padding: 0;
        border: none;
        text-align: left;
        min-width: 0;
    }

    /* Row 1: order number bold on the left, status pills right. */
    table.account-orders > tbody > tr > td[data-label="Order"] { grid-area: num; }
    .account-orders td[data-label="Order"] .order-link {
        font-family: var(--font-mono);
        font-size: 15px;
        font-weight: 600;
        overflow-wrap: anywhere;   /* a long order number wraps, never overflows */
    }
    table.account-orders > tbody > tr > td[data-label="Status"] {
        grid-area: status;
        display: flex;
        flex-wrap: wrap;
        justify-content: flex-end;
        align-items: flex-start;
        gap: 5px;
    }

    /* Row 2: date, muted. */
    table.account-orders > tbody > tr > td[data-label="Date"] {
        grid-area: date;
        font-size: 12.5px;
        color: var(--muted);
        white-space: normal;
    }

    /* Row 3: item count and total, labelled inline, pushed to opposite edges. */
    table.account-orders > tbody > tr > td[data-label="Items"] {
        grid-area: items;
        justify-self: start;
    }
    table.account-orders > tbody > tr > td[data-label="Total"] {
        grid-area: total;
        justify-self: end;
        text-align: right;
        font-size: 15px;
        font-weight: 600;
    }

    /* Row 4: Pay now, full width and above the invoice link — paying is the
       primary action. A paid row's cell is :empty and takes no space. */
    table.account-orders > tbody > tr > td[data-label="Pay"] {
        grid-area: pay;
        margin-top: 4px;
    }
    table.account-orders > tbody > tr > td[data-label="Pay"]:empty { display: none; }
    .account-orders td[data-label="Pay"] .pay-now-link { width: 100%; }

    /* Row 5: the invoice link, on its own line under a hairline. */
    table.account-orders > tbody > tr > td[data-label="Invoice"] {
        grid-area: inv;
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: 12px;
        margin-top: 4px;
        padding-top: 10px;
        border-top: 1px solid var(--border);
    }
    .account-orders td[data-label="Invoice"] .link-action { white-space: nowrap; }

    /* The inline labels the hidden header row used to provide. */
    table.account-orders > tbody > tr > td[data-label="Items"]::before,
    table.account-orders > tbody > tr > td[data-label="Total"]::before,
    table.account-orders > tbody > tr > td[data-label="Invoice"]::before {
        content: attr(data-label);
        margin-right: 8px;
        font-family: var(--font-ui);
        font-size: 11px;
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: .06em;
        color: var(--muted-2);
    }
}


/* ---- Order pages: payment above the invoice on mobile -------------------
   Paying is the primary action on an unpaid order, so on a phone the payment
   panel is lifted above the invoice download on both the confirmation page and
   the account order page. Desktop order is unchanged. Only the page header
   elements are pinned above it; everything else keeps its document order. */
@media (max-width: 767px) {
    .order-page { display: flex; flex-direction: column; }
    .order-page > .conf-check,
    .order-page > .page-h1,
    .order-page > .page-intro,
    .order-page > .conf-number,
    .order-page > .order-head,
    .order-page > .link-action:first-child { order: -2; }
    .order-page > .orderpay-panel { order: -1; }
}






/* Tabs are <a> on the orders page and <button> on products/inventory. The rule
   was written for anchors and never reset the button UA appearance, so the
   <button> ones inherited the browser's grey buttonface + outset border and read
   as solid grey buttons with a shadow. Resetting appearance/background/border
   here makes both elements render identically; .is-on then re-applies the white
   fill and ring. No !important, and nothing outside .ordv-tab is touched. */

.ordv-tab.is-on { background: var(--surface); color: var(--ink-02); font-weight: 600; box-shadow: 0 0 0 1px #e0e0e4, 0 1px 2px rgba(0,0,0,.05); }
.ordv-tcount.warn { background: var(--fill-69); color: var(--pill-amber-ink); }
.ordv-tcount.info { background: var(--fill-26); color: var(--ink-37); }
.ordv-tab.is-on .ordv-tcount { background: #2E5BE0; color: #fff; }

/* Collapsible search + filters (same controls as before) */

.ordv-filters .btn { min-height: 44px; }





/* Pills */
.ordv-pill {
    display: inline-flex; align-items: center; gap: 7px; padding: 5px 11px 5px 9px; border-radius: 9px;
    font-size: 12px; font-weight: 600; white-space: nowrap; border: 1px solid transparent;
}
.ordv-pill.t-neutral { background: var(--fill-41); color: var(--ink-38); border-color: var(--line-cool); }
.ordv-pill.t-neutral .ordv-dot { background: #9a9aa2; }
.ordv-pill.t-warn { background: var(--fill-71); color: var(--pill-amber-ink); border-color: var(--edge-09); }
.ordv-pill.t-warn .ordv-dot { background: #e39400; }
.ordv-pill.t-info { background: var(--fill-31); color: var(--ink-37); border-color: var(--edge-33); }
.ordv-pill.t-info .ordv-dot { background: #2E5BE0; }
.ordv-pill.t-ok { background: var(--fill-05); color: var(--ink-09); border-color: var(--edge-08); }
.ordv-pill.t-ok .ordv-dot { background: var(--pill-green); }
/* Balance states. part = money owed to us, due = money owed BY us — deliberately
   different hues so the two directions are never confused at a glance. */
.ordv-pill.t-part { background: var(--fill-63); color: var(--ink-78); border-color: var(--edge-52); }
.ordv-pill.t-part .ordv-dot { background: #d97a1f; }
.ordv-pill.t-due { background: var(--fill-62); color: var(--ink-83); border-color: var(--edge-51); }
.ordv-pill.t-due .ordv-dot { background: #c2286f; }

/* Unchanged: still used by the customer order history in views/account.php. */
.ordv-carrier { display: flex; align-items: center; gap: 7px; margin-top: 7px; font-size: 11.5px; color: var(--muted-grey); }

/* Payment-method chip. Same shape, size and weight as the carrier badge above so
   the payment and fulfilment columns read as one family. Colour comes from the
   brand map inline (payment_method_brand), because PayPal needs black text and a
   per-brand class per provider would drift from the pay buttons. */
.ordv-method { display: inline-flex; margin-top: 5px; }


/* Darker than the pill fills on purpose. These are 11px text on a white row, not
   a solid pill, and the pale reds that work on the dark countdown panel wash out
   here. The urgent one has to read as MORE urgent than the amber above it, which
   the pale #ff8b8b did not. */
.ordv-clock.is-warn   { color: var(--ink-20); }
.ordv-clock.is-urgent { color: var(--ink-97); }

/* ---- payment deadline, order detail ------------------------------------
   Sits under the balance in the totals card, because how long the order has and
   how much is owed are the two halves of deciding whether to chase somebody.

   Tinted rather than solid: this is information about an order that is still
   perfectly alive, and a solid red block next to a balance reads as an error that
   has already happened. The bar is the only filled element. */


.ordcd.is-warn { background: var(--pill-amber-bg); border-color: var(--edge-01); }
.ordcd.is-warn .ordcd-l { color: var(--ink-29); }
.ordcd.is-warn .ordcd-bar { background: rgba(245, 197, 24, 0.22); }
.ordcd.is-warn .ordcd-bar i { background: var(--pill-amber); }
.ordcd.is-warn .ordcd-sub { color: var(--ink-15); }

.ordcd.is-urgent { background: var(--fill-04); border-color: var(--edge-21); }
.ordcd.is-urgent .ordcd-l { color: var(--ink-04); }
.ordcd.is-urgent .ordcd-clock { color: var(--ink-04); }
.ordcd.is-urgent .ordcd-bar { background: rgba(216, 59, 59, 0.18); }
.ordcd.is-urgent .ordcd-bar i { background: var(--pill-red); }
.ordcd.is-urgent .ordcd-sub { color: var(--ink-04); }

/* PAST DUE. Its own state, not the bottom of the urgent one: there is no time
   left to be running short of. Solid red on the bar because the deadline has
   actually gone, and the clock counts up to say how long ago. Nothing here
   claims the order is cancelled: the sweep decides that on its own schedule,
   which is what the sentence underneath explains. */
.ordcd.is-past { background: var(--fill-04); border-color: var(--edge-19); }
.ordcd.is-past .ordcd-l { color: var(--ink-04); font-weight: 700; }
.ordcd.is-past .ordcd-clock { color: var(--ink-04); }
.ordcd.is-past .ordcd-bar { background: rgba(216, 59, 59, 0.18); }
.ordcd.is-past .ordcd-bar i { background: var(--pill-red); }
.ordcd.is-past .ordcd-sub { color: var(--ink-04); }






/* Why a cancelled order is cancelled, in the fulfilment column, which is empty
   for a cancelled order anyway. Two solid tones and no border: dark grey when a
   person cancelled it, red when the unpaid-order expiry sweep did, because those
   two need telling apart at a glance now that the sweep runs on its own. The full
   sentence, the reason and the timestamp are on the title attribute. */

.ordv-cxl.is-auto { background: var(--pill-red); }

/* Order line editing. Wraps down the page rather than scrolling sideways, so every
   control stays reachable at 320px. */
/* ---- Backorder note on the admin order view -------------------------------
   Amber, not red: this is not an error and nothing has gone wrong. It is a fact
   about the shelf that changes what goes in the box today, and a red panel on an
   otherwise healthy order teaches people to dismiss it.

   Sits directly above the lines it describes and takes the full row width, so it
   cannot be mistaken for a property of any single line. */

.ordbo-list { list-style: none; margin: 8px 0 0; padding: 0; }
.ordbo-list li { padding: 3px 0; }
.ordbo-list li + li { border-top: 1px solid rgba(180, 130, 20, .18); }
/* The count leads the line, because "2x" is the number being picked. */
.ordbo-qty { font-weight: 800; color: #4a3608; margin-right: 4px; }
/* The gap is real margin, not a space in the markup: the size and the arrival
   phrase are separate spans and ran together as "5mg Expected 4 Sep 2026". */
.ordbo-when { color: #8a6b23; margin-left: 8px; }
@media (max-width: 560px) {
    /* The expected-arrival phrase drops under the name rather than squeezing the
       product name into two characters a line. On its own line the horizontal
       offset would read as an indent, so it goes. */
    .ordbo-when { display: block; margin-left: 0; }
}



/* The backorder banner: the amber .ordbo-note surface with the mockup's icon
   and one detail line per affected line. */

.ordbo-d .mono { font-weight: 700; color: #4a3608; }

/* The list: phone cards first, the grid arrives with the container. */

.ordedit-line { position: relative; border: 1px solid var(--border); border-radius: 12px; padding: 14px; margin-top: 12px; }
.ordedit-line.is-gift .ordedit-pname { color: #1b8a4e; }
.ordedit-line.is-removed .ordedit-pname { text-decoration: line-through; color: var(--muted); }
.ordedit-line.is-removed .ordedit-total .mono:first-child { color: var(--muted); }
.admin-shell .ordedit-pmeta .ordv-pill { font-size: 11px; padding: 3px 8px; }










/* The save bar: the card's foot, the note leading and the two buttons trailing. */

.ordedit-savebar .btn { flex: 1 1 auto; }

/* ---- 480 and up (a 530px card): the tightened grid -----------------------
   A 547px card is what the 1440 desktop actually gives this column, so this
   tier is the everyday desktop, not a tablet case. */
@container (min-width: 480px) {
    .ordedit-head, .ordedit-line {
        display: grid; grid-template-columns: minmax(0, 1fr) 104px 100px 84px 34px;
        gap: 8px; align-items: center;
    }
    /* Touch: the stepper's 40px buttons and the 40px trigger need their columns. */
    @media (pointer: coarse) {
        .ordedit-head, .ordedit-line { grid-template-columns: minmax(0, 1fr) 116px 104px 84px 40px; }
    }
    .ordedit-head { padding: 6px 0 8px; border-bottom: 1px solid var(--border); }
    .ordedit-head span { font-size: 10.5px; font-weight: 600; letter-spacing: .13em; text-transform: uppercase; color: var(--muted-2); }
    .ordedit-head .r { text-align: right; }
    .ordedit-line { border: 0; border-bottom: 1px solid var(--border); border-radius: 0; padding: 14px 0; margin: 0; }
    .ordedit-line:last-child { border-bottom: 0; }
    .ordedit-prod { padding-right: 0; }
    .ordedit-addrow select { flex: 1 1 220px; }
    .ordedit-savebar { flex-wrap: nowrap; }
    .ordedit-savenote { flex: 1 1 auto; margin: 0 auto 0 0; }
    .ordedit-savebar .btn { flex: 0 0 auto; }
}

/* ---- 700 and up (a 750px card): the mockup's columns and the two-up split -- */
@container (min-width: 700px) {
    .ordedit-head, .ordedit-line { grid-template-columns: minmax(0, 1fr) 122px 132px 110px 40px; gap: 14px; }
    .ordedit-split { grid-template-columns: minmax(0, 1fr) 300px; gap: 26px; }
    .ordedit-ledger { order: 0; }
}

/* Phone cards: what the row's pieces do under 480. */
@container (max-width: 479.98px) {
    .ordedit-menu { position: absolute; top: 12px; right: 12px; }
    .ordedit-qty { display: inline-flex; margin-top: 12px; width: 118px; vertical-align: top; }
    .ordedit-price { display: inline-flex; margin-top: 12px; width: calc(100% - 128px); vertical-align: top; }
    .ordedit-ro.ordedit-qty, .ordedit-ro.ordedit-price { width: auto; margin-right: 12px; }
    .ordedit-total { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-top: 12px; }
    .ordedit-total::before { content: "Line total"; font-size: 12px; font-weight: 500; color: var(--muted-2); }
    .ordedit-total > .mono { order: 2; }
    .ordedit-total > small.mono { display: inline; order: 1; margin-right: auto; }
}

/* Payments-by-method strip. Wraps rather than scrolls, so every method stays
   visible at any width without a hidden overflow. */

.ordv-mbadge {
    display: inline-block;
    font-size: 9.5px; font-weight: 700; padding: 2.5px 7px; border-radius: 5px;
    letter-spacing: .03em; white-space: nowrap;
}
/* A customer's unverified claim: same chip, dimmed and outlined so it never reads
   as a confirmed fact. */
.ordv-mbadge.is-claim { opacity: .55; box-shadow: inset 0 0 0 1px rgba(255,255,255,.35); }

/* Customer actions */
.ordv-actions { white-space: nowrap; }
.ordv-link { color: var(--link); font-weight: 600; margin-right: 18px; display: inline-flex; align-items: center; min-height: 44px; }
.ordv-link:last-child { margin-right: 0; }
.ordv-link:hover { text-decoration: underline; }
.ordv-paynow {
    display: inline-flex; align-items: center; justify-content: center; min-height: 44px;
    background: #2E5BE0; color: #fff; font-size: 13px; font-weight: 600; padding: 8px 16px;
    border-radius: 9px; box-shadow: 0 1px 2px rgba(46, 91, 224, .3);
}
.ordv-paynow:hover { background: #264fc4; color: #fff; text-decoration: none; }

/* Footer */
/* One plain sentence stating the basis of the money figures above it, rather than a
   qualifier bolted onto every stat label. */

.ordv-pgb.is-off { opacity: .4; }

/* Admin shell paints a dark surface; these two pages are light cards on it. */
.admin-shell .ordv-title { color: var(--ink-02); }

/* -------- Mobile: rows become stacked cards -------- */
@media (max-width: 780px) {






    /* Option A keeps the badge beside the pill at every width; only the tracking
       text drops to its own line when the card is narrow. account.php keeps its
       own stacked treatment. */
    
    .ordv-carrier { flex-basis: 100%; margin-top: 2px; }
    .ordv-actions { grid-column: 1 / -1; margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; }
    .ordv-link { margin-right: 0; }
    /* Customer table has no checkbox column, so everything shifts left one track. */
    .ordv-table-account tbody tr { grid-template-columns: minmax(0,auto) minmax(0,auto) minmax(0,1fr); }
    .ordv-table-account tbody td[data-label] { grid-column: auto; }
    .ordv-table-account tbody tr > td:nth-child(1) { grid-column: 1 / 3; grid-row: 1; }
    .ordv-table-account tbody tr > td:nth-child(3) { grid-column: 3;     grid-row: 1; text-align: right; }
    .ordv-table-account tbody tr > td:nth-child(2) { grid-column: 1;     grid-row: 2; }
    .ordv-table-account tbody tr > td:nth-child(5) { grid-column: 2 / -1; grid-row: 2; }
    .ordv-table-account tbody tr > td:nth-child(4) { grid-column: 1 / -1; grid-row: 3; }
    .ordv-table-account tbody tr > td:nth-child(6) { grid-column: 1 / -1; grid-row: 4; }
}


/* The dashboard gained a Billed tile beside Received, so it is 8 across now. */
.dash-stats { grid-template-columns: repeat(8, 1fr); }

/* Eight or nine cells do not fit across a laptop content column: at 1024 the money
   values ran into each other. Between tablet and full desktop the bar wraps to four
   per row instead of squeezing, so every figure stays readable and none is hidden. */
/* AT EVERY WIDTH FROM 761 UP, not to a ceiling: a single row of nine put
   "$9,999.99" flush against the cell edge at 1920 (167px cells, value right
   1822.0 on an inner edge of 1822.0, measured), and the content column is
   capped at 1600 so no width ever has the room. Four figures per row, two
   rows, the range selector on its own row above them. */
@media (min-width: 761px) {
    .ordv-stats-9, .dash-stats { grid-template-columns: repeat(4, 1fr); }

}

.dash-stat-sub { display: block; margin-top: 5px; font-size: 11.5px; color: rgba(255,255,255,.45); }
.dash-kv-v.is-warn { color: var(--ink-90); }
.dash-kv-v.is-bad { color: var(--ink-13); }

/* Solid status pills (shared design language). Overrides the pastel tones for
   the admin only; the customer-facing pages keep the softer set. */
.admin-shell .ordv-pill { font-weight: 650; padding: 5px 11px; }
.admin-shell .ordv-pill .ordv-dot { display: none; }
.admin-shell .ordv-pill.t-ok      { background: var(--pill-green); color: var(--pill-ink); border-color: var(--edge-25); }
.admin-shell .ordv-pill.t-warn    { background: var(--pill-amber); color: var(--ink-02); border-color: var(--edge-38); }
.admin-shell .ordv-pill.t-info    { background: #2E5BE0; color: #fff; border-color: #2549b8; }
.admin-shell .ordv-pill.t-neutral { background: var(--pill-grey); color: var(--pill-ink); border-color: #45454a; }
.admin-shell .ordv-pill.t-bad     { background: var(--pill-red); color: var(--pill-ink); border-color: #c22f2f; }
.admin-shell .ordv-pill.t-part    { background: #d97a1f; color: #fff; border-color: var(--edge-30); }
.admin-shell .ordv-pill.t-due     { background: #c2286f; color: #fff; border-color: #a81f5e; }
@media (max-width: 780px) {
    .dash-stats, .ordv-stats-9 { grid-template-columns: 1fr 1fr; }
    }





/* The staff-only compound name, under whatever product name it belongs to.
   DELIBERATELY NOT THE SKU TREATMENT. It sat in the same quiet grey as .prodv-sku
   and read as reference detail, when it is the thing actually being scanned for.
   Bold, in the admin blue, so the three tiers separate at a glance: product name in
   ink, compound name in blue, SKU staying quiet underneath. .prodv-sku is untouched
   on purpose; the contrast between them is the point.
   Admin only: no storefront template renders either of these classes. */
.iname-sub {
    display: block;
    margin-top: 2px;
    font-size: 12px;
    font-weight: 700;
    color: var(--link);
}
/* Same treatment where the name has to sit inside a run of text rather than on its
   own line, which is where the parenthesised form is used. */
.iname-inline { font-weight: 700; color: var(--link); }








/* Movement history inside a row panel. Reuses the order page's .ordv-hist
   treatment rather than inventing a second one. */

.invv-delta.is-out { color: var(--ink-13); }



/* Cart: a line asking for more than is available, and the one-shot notice after a
   line has been capped. Both are plain solid blocks, no tint filters. */
.cart-line-short {
    margin: 4px 0 0; font-size: 12.5px; line-height: 1.5; font-weight: 600; color: var(--ink-13);
}
.cart-short-line { margin: 0; }
.cart-short-line + .cart-short-line { margin-top: 6px; }
.alert-notice {
    background: var(--pill-amber-bg); border: 1px solid var(--edge-01); color: var(--ink-15);
    padding: 12px 14px; margin: 0 0 16px; font-size: 13.5px; line-height: 1.55;
}




/* Status pill menus on the orders list + inline price correction. */

.pillmenu-item.is-current { font-weight: 650; color: var(--link); }
.pillmenu-item.is-current::after { content: "✓"; margin-left: auto; }
.ordprice-wrap { display: inline-flex; align-items: center; gap: 1px; }
.ordprice-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px; }





/* The row menu holds small forms, so it is wider and its fields stack. */
.usrv-menu { min-width: 236px; padding: 8px; gap: 2px; }
.usrv-mform { margin: 0; display: flex; flex-direction: column; gap: 4px; padding: 6px 2px; border-bottom: 1px solid var(--edge-24); }
.usrv-mform:last-child { border-bottom: none; }
.usrv-mlabel { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .03em; color: var(--muted-grey); }
.usrv-minput { width: 100%; min-height: 36px; border: 1px solid var(--edge-17); border-radius: 7px; padding: 0 8px; font-size: 12.5px; background: var(--surface); }
.usrv-minput:focus { outline: none; border-color: #2E5BE0; box-shadow: 0 0 0 3px rgba(46,91,224,.12); }
.usrv-mcheck { font-size: 12.5px; }

@media (max-width: 780px) {

    /* SCOPED TO .usrv-table, and that scope is the whole point.
       Written bare, this rule reached every table that reuses the .usrv-act-col
       class for its action cell, which is the users list, referrals AND abandoned
       carts. Only this table pairs it with `position: relative` on the row above,
       so only here does "absolute" mean "the top right of this card". On the
       abandoned carts list the rows are not positioned, so all of its Delete cells
       resolved against .ordv-tablewrap instead, stacked on the same coordinates,
       and painted over the FIRST card's customer name. Referrals never had the
       problem because it already carries its own scoped copy of this rule. */
    
    .usrv-menu { min-width: 210px; }

    }


/* Collapsed: exactly one line of body. */

.msgv-card.is-open .msgv-excerpt { display: none; }
/* The tinted panel, no left accent (house rule). */

.msgv-actions .btn { min-height: 44px; }

/* =========================================================================
   Contact page (design_mockup_admin/pcd-contact-premium.html). Form + sidebar.
   ========================================================================= */
.ctv { max-width: 1180px; }
.ctv-cols { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr); gap: 22px; align-items: start; margin-top: 22px; }
.ctv-main { min-width: 0; }
.ctv-side { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.ctv-card { background: var(--surface); border: 1px solid var(--line-warm); border-radius: 14px; box-shadow: 0 1px 3px rgba(0,0,0,.04); overflow: hidden; }
.ctv-cardhead { padding: 18px 20px; border-bottom: 1px solid var(--edge-20); }
.ctv-cardtitle { margin: 0; font-size: 17px; font-weight: 680; letter-spacing: -0.01em; color: var(--text-3); }
.ctv-cardsub { margin: 4px 0 0; font-size: 12.5px; line-height: 1.55; color: var(--muted-warm-2); }
.ctv-form { padding: 20px; }
/* Focus state on every control in the form. */
.ctv-form input[type="text"], .ctv-form input[type="email"], .ctv-form input[type="tel"],
.ctv-form select, .ctv-form textarea { min-height: 44px; }
.ctv-form input:focus, .ctv-form select:focus, .ctv-form textarea:focus {
    outline: none; border-color: #2E5BE0; box-shadow: 0 0 0 3px rgba(46, 91, 224, .12);
}
.ctv-submit { background: #2E5BE0; color: #fff; border: none; border-radius: 10px; min-height: 48px; font-weight: 600; }
.ctv-submit:hover { background: #264fc4; color: #fff; }

/* Research-use notice: same dark ground as the admin stat bar. */
.ctv-notice {
    display: flex; gap: 12px; align-items: flex-start; margin: 18px 0;
    background: linear-gradient(180deg, #16181d, #101216);
    border: 1px solid #23252b; border-radius: 11px; padding: 14px 16px;
}
.ctv-notice-ico { flex: 0 0 auto; color: #f5c518; font-size: 15px; line-height: 1.4; }
.ctv-notice-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ctv-notice-h { font-size: 12.5px; font-weight: 650; color: #fff; }
.ctv-notice-t { font-size: 12.5px; line-height: 1.6; color: rgba(255,255,255,.62); }

/* Sidebar: dark chat card. */
.ctv-dark { background: linear-gradient(180deg, #16181d, #101216); border: 1px solid #23252b; border-radius: 14px; padding: 18px 20px; }
.ctv-badge { display: inline-flex; align-items: center; gap: 7px; font-size: 11px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; border-radius: 999px; padding: 5px 11px; }
.ctv-badge.is-open { background: rgba(31,168,99,.16); color: var(--ink-64); }
.ctv-badge.is-closed { background: rgba(255,255,255,.08); color: rgba(255,255,255,.62); }
.ctv-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.ctv-dark-h { margin: 12px 0 6px; font-size: 16px; font-weight: 680; color: #fff; letter-spacing: -0.01em; }
.ctv-dark-t { margin: 0; font-size: 12.5px; line-height: 1.6; color: rgba(255,255,255,.62); }
.ctv-dark-hours { margin: 8px 0 0; font-size: 11.5px; color: rgba(255,255,255,.45); }
.ctv-chatbtn {
    -webkit-appearance: none; appearance: none; margin-top: 14px; width: 100%; min-height: 44px;
    background: var(--surface); color: var(--text-3); border: none; border-radius: 9px;
    font-family: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer;
}
.ctv-chatbtn:hover { background: var(--surface-4); }

/* Sidebar: details + FAQ. */
.ctv-details, .ctv-faq { padding: 16px 18px; }
/* The contact card on /account/messages/new only, where it sits in the single
   column above the message form and would otherwise butt straight against the
   "New message" heading. 28px is not a guess: it is the measured gap below the
   card, .sup-form's 16px margin-top plus .sup-label's 12px, so the air above and
   below the card matches. Scoped to .sup-narrow, which /contact does not have:
   there the card lives in .ctv-side, whose flex gap already spaces it. */
.sup-narrow .ctv-details { margin-top: 28px; }
.ctv-sideh { margin: 0 0 12px; font-size: 14px; font-weight: 680; color: var(--text-3); }
.ctv-row { display: flex; align-items: center; gap: 11px; padding: 8px 0; min-height: 44px; color: inherit; }
a.ctv-row:hover { text-decoration: none; }
a.ctv-row:hover .ctv-rowv { color: var(--link); }
.ctv-ico { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 36px; height: 36px; border-radius: 10px; background: var(--tint-blue); color: var(--link); font-size: 15px; }
.ctv-rowbody { display: flex; flex-direction: column; min-width: 0; }
.ctv-rowl { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--muted-grey); }
.ctv-rowv { font-size: 13px; color: var(--text-3); overflow-wrap: anywhere; }
.ctv-faqlink { display: flex; align-items: center; min-height: 44px; padding: 6px 0; font-size: 13px; color: var(--link); font-weight: 500; border-bottom: 1px solid var(--edge-24); }
.ctv-faqlink:last-child { border-bottom: none; }
.ctv-faqlink:hover { text-decoration: underline; }

@media (max-width: 900px) {
    .ctv-cols { grid-template-columns: 1fr; }
    .ctv-side { order: 2; }
}


/* =========================================================================
   PUBLIC PAY PAGE  (/order-pay/{token})  — .opay-*
   Mobile first, matching design_mockup_admin/pcd-pay-page-redesign.html and
   pcd-pay-page-desktop.html. Scoped entirely to .opay so it cannot reach the
   confirmation or account order pages, which still use payment_step.php.
   No backdrop-filter anywhere: the chooser backdrop is a flat rgba fill.
   ========================================================================= */
/* The grey panel fills the screen BELOW THE NAV, so a short order (paid, cancelled)
   still paints a full screen of page instead of letting the dark footer ride up,
   and a full one does not gain a scrollbar for the height of the bar alone. */
.opay { background: var(--fill-14); min-height: calc(100vh - var(--nav-h)); padding: 22px 18px 40px; }
.opay-inner { max-width: 1180px; margin: 0 auto; }

.opay-head { margin-bottom: 18px; }
.opay-eyebrow { display: flex; align-items: center; gap: 6px; margin: 0 0 7px;
    font-size: 11px; font-weight: 650; letter-spacing: .09em; text-transform: uppercase; color: var(--ink-03); }
.opay-tick { width: 15px; height: 15px; border-radius: 50%; background: var(--pill-green); color: var(--pill-ink);
    font-size: 9px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.opay-h1 { font-size: 21px; font-weight: 680; letter-spacing: -0.02em; margin: 0 0 5px; color: var(--ink-02); }
.opay-ordn { font-size: 12.5px; color: var(--muted-grey); margin: 0; }
.opay-note { font-size: 13.5px; color: var(--ink-25); line-height: 1.6; }

/* How long the order is held. Quiet by design: it is information, not a warning,
   so it reads at the weight of a footnote and carries no colour of its own. */
.opay-hold {
    margin: 12px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--ink-72);
}

/* 1. Amount due. First thing on screen. */
.opay-due { background: linear-gradient(180deg, #16181d, #101216); border-radius: 14px;
    padding: 18px 20px; margin-bottom: 18px; }
.opay-due-l { font-size: 11.5px; color: rgba(255,255,255,.5); font-weight: 500; margin: 0 0 6px; }
.opay-due-v { font-size: 31px; font-weight: 680; color: #fff; letter-spacing: -0.025em; margin: 0; line-height: 1; }
.opay-due-r { font-size: 11.5px; color: rgba(255,255,255,.42); margin: 8px 0 0; line-height: 1.6; }
.opay-due-r b { color: #fff; display: block; font-size: 12.5px; font-weight: 650; margin-bottom: 2px; }

/* ---- payment countdown -------------------------------------------------
   Lives INSIDE the amount due panel, directly under the number, so it is the
   second thing read and cannot be scrolled past.

   Three states: blue over 12 hours, yellow under 12, red under 3. The colours
   are tints of the palette reds and yellows rather than the flat pills, because
   this sits on the dark panel where a solid #d83b3b block would read as an error
   rather than as a clock. The bar underneath is the only solid fill.

   Server-rendered first, then JS ticks it. Every rule here applies equally to the
   no-JS render, so a page with scripting off shows the same block, just static. */
.opay-due.has-cd { padding-bottom: 20px; }
.opay-cd { margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(255, 255, 255, 0.12); }
.opay-cd-l {
    margin: 0; font-size: 11.5px; font-weight: 700; letter-spacing: 0.09em;
    text-transform: uppercase; color: rgba(255, 255, 255, 0.55);
}
.opay-cd-t { display: flex; gap: 8px; align-items: flex-end; margin-top: 9px; }
.opay-cd-u {
    background: rgba(255, 255, 255, 0.09); border-radius: 9px; padding: 8px 11px;
    text-align: center; min-width: 56px;
}
.opay-cd-n {
    display: block; font-family: var(--font-mono); font-size: 23px; font-weight: 700;
    line-height: 1; color: #fff; font-variant-numeric: tabular-nums;
}
.opay-cd-x {
    display: block; font-size: 10px; font-weight: 700; letter-spacing: 0.07em;
    text-transform: uppercase; color: rgba(255, 255, 255, 0.45); margin-top: 5px;
}
.opay-cd-bar {
    margin-top: 14px; height: 5px; border-radius: 99px;
    background: rgba(255, 255, 255, 0.12); overflow: hidden;
}
.opay-cd-bar i {
    display: block; height: 100%; border-radius: 99px; background: #2E5BE0;
    transition: width 1s linear;
}
.opay-cd-sub {
    margin: 11px 0 0; font-size: 12.5px; line-height: 1.55; color: rgba(255, 255, 255, 0.55);
}

.opay-cd.is-warn .opay-cd-l { color: #ffd75e; }
.opay-cd.is-warn .opay-cd-u { background: rgba(245, 197, 24, 0.16); }
.opay-cd.is-warn .opay-cd-n { color: #ffd75e; }
.opay-cd.is-warn .opay-cd-bar i { background: var(--pill-amber); }

.opay-cd.is-urgent .opay-cd-l { color: var(--ink-23); }
.opay-cd.is-urgent .opay-cd-u { background: rgba(216, 59, 59, 0.2); }
.opay-cd.is-urgent .opay-cd-n { color: var(--ink-23); }
.opay-cd.is-urgent .opay-cd-bar i { background: var(--pill-red); }

/* PAST DUE, on the dark panel. Same reasoning as the admin clock: a distinct
   state rather than the far end of "urgent", with the digits counting up. The
   customer can still pay and save the order until the sweep runs, so this is
   firm without being final. */
.opay-cd.is-past .opay-cd-l { color: var(--ink-23); font-weight: 700; }
.opay-cd.is-past .opay-cd-u { background: rgba(216, 59, 59, 0.28); }
.opay-cd.is-past .opay-cd-n { color: var(--ink-23); }
.opay-cd.is-past .opay-cd-bar i { background: var(--pill-red); }
.opay-cd.is-past .opay-cd-sub { color: #ffb3b3; }

@media (prefers-reduced-motion: reduce) {
    .opay-cd-bar i { transition: none; }
}

@media (max-width: 390px) {
    /* Three units plus gaps need 56px each at minimum; below this they share the
       row evenly instead of overflowing it. */
    .opay-cd-u { min-width: 0; flex: 1 1 0; padding: 8px 6px; }
    .opay-cd-n { font-size: 21px; }
}


/* Steps */
.opay-step { display: flex; align-items: center; gap: 9px; margin: 0 0 12px; }
.opay-snum { width: 21px; height: 21px; border-radius: 50%; background: #2E5BE0; color: #fff;
    font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.opay-stxt { font-size: 13.5px; font-weight: 650; color: var(--ink-02); }
/* Desktop-only, per the mockups: the mobile step row is just "Send payment". */
.opay-shint { display: none; font-size: 12.5px; color: var(--muted-grey); margin-left: auto; }

/* 2. Payment buttons */
.opay-pgrid { display: grid; grid-template-columns: 1fr; gap: 9px; margin-bottom: 9px; }
.opay-pay { border: none; border-radius: 11px; height: 50px; display: flex; align-items: center;
    justify-content: center; font-size: 14px; font-weight: 650; letter-spacing: -0.01em;
    text-decoration: none; cursor: pointer; font-family: inherit; width: 100%; }
.opay-pay:hover { text-decoration: none; filter: brightness(.96); }
.opay-pay.is-soon { background: var(--fill-11); color: var(--muted-grey); cursor: default; }

/* Zelle detail block. Content unchanged from the previous version. */
.opay-zdrop { background: var(--fill-57); border: 1px solid var(--edge-40); border-radius: 11px; padding: 13px 14px; margin-bottom: 11px; }
.opay-zdrop-h { font-size: 12.5px; font-weight: 650; margin: 0 0 9px; display: flex; align-items: center; gap: 7px; }
.opay-zdrop-h span { width: 6px; height: 6px; border-radius: 50%; background: #6D1ED4; }
.opay-zrow { display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 5px 0; font-size: 12.5px; }
.opay-zl { color: var(--muted-grey); flex-shrink: 0; }
.opay-zv { font-weight: 650; font-family: ui-monospace, Menlo, monospace; word-break: break-all; }
.opay-cpy { font-size: 10.5px; color: var(--link); font-weight: 650; border: 1px solid var(--edge-13);
    border-radius: 6px; padding: 3px 8px; margin-left: 8px; background: var(--surface); cursor: pointer; font-family: inherit; }
.opay-zfoot { font-size: 11px; color: var(--muted-grey); margin: 8px 0 0; line-height: 1.5; }

.opay-memo { background: var(--fill-73); border: 1px solid var(--edge-09); border-radius: 10px; padding: 13px 16px;
    font-size: 12.5px; color: var(--pill-amber-ink); line-height: 1.6; margin: 0 0 4px; }
.opay-memo b { font-weight: 700; }

.opay-sep { display: flex; align-items: center; gap: 11px; margin: 20px 0 16px; color: var(--ink-92); font-size: 11.5px; }
.opay-sep::before, .opay-sep::after { content: ""; height: 1px; background: var(--fill-11); flex: 1; }

/* Step 2 trigger */
.opay-sent { background: var(--surface); border: 2px solid #1c1c1e; border-radius: 12px; padding: 15px;
    width: 100%; text-align: center; font-size: 14px; font-weight: 650; color: var(--ink-02);
    cursor: pointer; font-family: inherit; }
.opay-sentnote { font-size: 11.5px; color: var(--muted-grey); text-align: center; line-height: 1.55; margin: 9px 0 0; }
.opay-reported { background: var(--fill-05); border: 1px solid var(--edge-08); border-radius: 11px; padding: 14px;
    font-size: 13px; color: var(--ink-09); line-height: 1.55; margin: 0; }

/* 3. Sidebar: summary + the invoice button. Below the fold on mobile, on purpose. */
.opay-side { background: var(--fill-61); border: 1px solid var(--edge-20); border-radius: 14px; padding: 20px; margin-top: 26px; }
.opay-side-t { font-size: 11.5px; font-weight: 650; letter-spacing: .06em; text-transform: uppercase;
    color: var(--muted-grey); margin: 0 0 14px; }
.opay-li { display: flex; justify-content: space-between; gap: 12px; padding: 8px 0;
    border-bottom: 1px solid var(--edge-49); font-size: 13px; color: var(--ink-25); }
.opay-li-v { font-weight: 600; font-variant-numeric: tabular-nums; }
.opay-tot { display: flex; justify-content: space-between; gap: 12px; padding: 13px 0 0; margin-top: 8px;
    border-top: 1.5px solid #1c1c1e; font-size: 15px; font-weight: 680; }
/* Mobile: the actions sit as a compact row at the foot of the page, matching the
   mini button row in pcd-pay-page-redesign.html. The order summary lines are
   desktop-only there, so they are hidden here rather than invented. */
.opay-acts { margin-top: 18px; display: flex; gap: 8px; }
.opay-side .opay-li, .opay-side .opay-tot, .opay-side .opay-side-t { display: none; }
.opay-side { padding: 14px; }
.opay-abtn { flex: 1; display: flex; align-items: center; justify-content: center; border: 1px solid var(--edge-17);
    border-radius: 10px; padding: 11px 8px; font-size: 12px; font-weight: 600; color: var(--ink-25);
    background: var(--surface); text-decoration: none; text-align: center; }
.opay-abtn:hover { border-color: var(--edge-11); background: var(--fill-10); text-decoration: none; }

/* 4. The chooser. Full screen on mobile, modal on desktop. Same markup. */
.opay-ovl { position: fixed; inset: 0; z-index: 200; background: var(--fill-14);
    display: flex; align-items: flex-start; justify-content: center; padding: 16px; overflow-y: auto; }
.opay-ovl[hidden] { display: none; }
body.opay-lock { overflow: hidden; }
.opay-modal { background: var(--surface); border-radius: 16px; padding: 22px 20px 26px; width: 100%; max-width: 520px; }
.opay-mh { font-size: 19px; font-weight: 680; letter-spacing: -0.02em; margin: 0 0 5px; color: var(--ink-02); }
.opay-ms { font-size: 13px; color: var(--ink-27); line-height: 1.55; margin: 0 0 18px; }
.opay-mgrid { display: grid; grid-template-columns: 1fr; gap: 9px; }
.opay-mcard { display: flex; align-items: center; gap: 13px; border: 1.5px solid var(--line-cool); border-radius: 12px;
    padding: 14px; background: var(--surface); width: 100%; text-align: left; cursor: pointer; font-family: inherit; }
.opay-mcard.is-on { border-color: #2E5BE0; background: var(--fill-56); box-shadow: 0 0 0 3px rgba(46,91,224,.1); }
.opay-mswatch { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center;
    justify-content: center; font-size: 11px; font-weight: 700; flex-shrink: 0; }
.opay-mtext { min-width: 0; }
.opay-mname { display: block; font-size: 14px; font-weight: 650; color: var(--ink-02); }
.opay-mhint { display: block; font-size: 11.5px; color: var(--muted-grey); margin-top: 1px; word-break: break-all; }
.opay-mradio { width: 20px; height: 20px; border-radius: 50%; border: 1.5px solid var(--edge-11);
    margin-left: auto; flex-shrink: 0; }
.opay-mcard.is-on .opay-mradio { border-color: #2E5BE0; border-width: 6px; }
/* Optional sender handle. Sits between the cards and the confirm button, where it
   reads as part of the same answer rather than a separate form. */
.opay-sender { margin-top: 16px; }
.opay-sender-l { display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
    font-size: 12.5px; font-weight: 650; color: var(--ink-02); margin-bottom: 6px; }
.opay-opt { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
    color: var(--ink-27); background: var(--fill-34); border-radius: 6px; padding: 2px 7px; }
.opay-sender-in { width: 100%; min-height: 46px; border: 1.5px solid var(--line-cool); border-radius: 11px;
    padding: 0 13px; font-size: 14px; font-family: inherit; color: var(--ink-02); background: var(--surface); }
.opay-sender-in:focus { outline: none; border-color: #2E5BE0; box-shadow: 0 0 0 3px rgba(46,91,224,.1); }
.opay-sender-h { margin: 6px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--muted-grey); }
.opay-confirm { background: #2E5BE0; color: #fff; border: none; border-radius: 11px; height: 50px;
    width: 100%; display: flex; align-items: center; justify-content: center; font-size: 14.5px;
    font-weight: 650; margin-top: 16px; cursor: pointer; font-family: inherit; }
.opay-confirm:disabled { background: var(--fill-25); cursor: not-allowed; }
.opay-mfoot { display: flex; align-items: center; justify-content: space-between; gap: 12px;
    margin-top: 14px; font-size: 12.5px; color: var(--muted-grey); flex-wrap: wrap; }
.opay-back { background: none; border: none; color: var(--ink-27); font-size: 13px; font-family: inherit;
    cursor: pointer; text-decoration: underline; padding: 0; }

/* -------- Desktop: two columns, buttons in pairs, chooser becomes a modal -------- */
@media (min-width: 900px) {
    .opay { padding: 30px; }
    .opay-h1 { font-size: 28px; letter-spacing: -0.025em; }
    .opay-ordn { font-size: 13px; }
    .opay-head { margin-bottom: 26px; }
    .opay-cols { display: grid; grid-template-columns: 1fr 340px; gap: 32px; align-items: start; }
    .opay-side { margin-top: 0; padding: 22px; }
    .opay-side .opay-li, .opay-side .opay-tot { display: flex; }
    .opay-side .opay-side-t { display: block; }
    .opay-acts { display: block; }
    .opay-abtn { padding: 12px 14px; font-size: 13px; margin-bottom: 8px; }
    .opay-shint { display: inline; }
    .opay-due { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 22px 26px; margin-bottom: 26px; }
    .opay-due-v { font-size: 36px; }
    .opay-due-r { text-align: right; margin: 0; }
    .opay-pgrid { grid-template-columns: 1fr 1fr; gap: 11px; }
    .opay-pay { height: 54px; font-size: 14.5px; }
    .opay-sent { width: auto; display: inline-flex; padding: 15px 32px; font-size: 15px; }
    .opay-sentnote { text-align: left; font-size: 12.5px; }
    /* Now a true overlay: dimmed backdrop, centred panel. Flat fill, no blur. */
    .opay-ovl { background: rgba(16,18,22,.55); align-items: center; padding: 44px; }
    .opay-modal { padding: 30px 32px; box-shadow: 0 20px 60px rgba(0,0,0,.25); }
    .opay-mh { font-size: 22px; }
    .opay-ms { font-size: 13.5px; margin-bottom: 22px; }
    .opay-mgrid { grid-template-columns: 1fr 1fr; gap: 10px; }
    .opay-mcard .opay-mradio { display: none; }
}



/* ---- inline edit panel --------------------------------------------------
   Lives in a second <tr> under the row it edits, so the form is attached to the
   link it changes rather than floating at the top of the page. The cell is a
   containing block with its own width, which stops the form's fields from
   widening the table they sit inside. */

.refv-edit-grid .field { min-width: 0; margin: 0; }


/* Owner cell. Avatar colours follow the kind, not the account role, because the
   Type column is what a reader is matching them against. */
.refv-av-rep      { background: #6D1ED4; }
.refv-av-customer { background: #2E5BE0; }




/* Detail page: the id line beside the title, and section heads reusing the tab bar. */

.refv-table tbody td.num { font-variant-numeric: tabular-nums; }
.refv-table thead th.r, .refv-table tbody td.r { text-align: right; }







/* Left out of both the value beside it and the email. Said plainly, as a note
   rather than a badge: the only pills on this page are the two status columns,
   and a third tinted chip here would read as a status it is not. */

.acv-table tbody td.num { font-variant-numeric: tabular-nums; }
.acv-table thead th.r, .acv-table tbody td.r { text-align: right; }



/* =========================================================================
   STOREFRONT PAGE SHELL (design_mockup_admin/pcd-storefront-pages.html)

   THE one container for storefront page content. It is the SAME contract the
   nav bar, hero, catalog grid and footer already use, written out here so a
   page can opt in by adding one class instead of copying the calc():

       max-width: calc(--content-max + 2 * --gutter)   cap, matches the grid
       margin: 0 auto                                  centre
       padding: 0 --gutter                             gutter, matches the nav

   Before this there were seven different width contracts on the storefront and
   the pages drifted from the nav by between 64px and 768px depending on the
   viewport. Anything that must line up with the nav uses .page-shell. Nothing
   else should invent its own container.

   Body copy does NOT run the full 1440. .prose caps the measure at 72ch inside
   the shell, which is what the mockup does and what makes the wider shell
   readable rather than just wider.
   ========================================================================= */
.page-shell {
    max-width: calc(var(--content-max) + 2 * var(--gutter));
    margin: 0 auto;
    padding: 0 var(--gutter);
}
.prose { max-width: 72ch; }

/* Shared storefront page furniture, straight from the mockup's type scale. */
.sf-eyebrow {
    font-family: var(--font-mono);
    font-size: 11px; font-weight: 600; letter-spacing: 0.18em;
    text-transform: uppercase; color: var(--link); margin: 0;
}
.sf-h1 {
    margin: 12px 0 0; font-size: clamp(28px, 3.4vw, 40px); font-weight: 800;
    letter-spacing: -0.035em; line-height: 1.08; color: var(--ink);
}
.sf-lede { margin: 14px 0 0; font-size: 16.5px; line-height: 1.65; color: var(--muted-plum); max-width: 66ch; }

/* Solid pills only. No translucency, no blur: these are the mockup's exact
   values and they are the only pill colours these pages use. */
.sf-pill {
    display: inline-flex; align-items: center; white-space: nowrap;
    font-size: 12px; font-weight: 700; line-height: 1;
    padding: 6px 11px; border-radius: 7px; color: var(--pill-ink);
}
.sf-pill.g { background: var(--pill-green); }
.sf-pill.y { background: var(--pill-amber); color: var(--pill-ink-amber-a); }
.sf-pill.r { background: var(--pill-red); }
.sf-pill.d { background: var(--pill-grey); }
.sf-pill.b { background: var(--accent-blue); color: var(--white); }

/* =========================================================================
   LEGAL TEMPLATE (Terms / Privacy / Waiver / Refund)
   Sticky derived section nav, numbered sections, capped prose column.
   ========================================================================= */
.lg-page { background: var(--off-white); }
.lg-head { padding: 44px 0 30px; border-bottom: 1px solid var(--border); }
.lg-meta { margin-top: 18px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.lg-chip {
    background: var(--surface); border: 1px solid var(--border); border-radius: 999px;
    padding: 5px 12px; font-size: 12.5px; color: var(--muted);
}

.lg-grid { display: grid; grid-template-columns: 236px minmax(0, 1fr); gap: 44px; padding: 34px 0 0; align-items: start; }
.lg-grid.is-solo { grid-template-columns: minmax(0, 1fr); }   /* no headings -> no nav column */

.lg-toc { position: sticky; top: 20px; }
.lg-toc-t { font-size: 11px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted-2); margin: 0 0 12px; }
.lg-toc-nav { display: flex; flex-direction: column; gap: 2px; }

/* Option B, solid fill. The whole item carries the state: a solid blue fill with
   white text when active, a light grey fill on hover. No border on any single
   side of any item, which is what the old left-edge treatment was.

   Hover deliberately does NOT use blue. It used to set the same
   var(--accent-blue) as the active item, so hovering any entry made it the same
   colour as the active one and two items read as selected at once. Grey fill plus
   dark text keeps blue meaning exactly one thing on this list. */
.lg-toc-a {
    display: block; font-size: 13.5px; color: var(--ink-40); text-decoration: none;
    padding: 9px 13px; border-radius: 9px; line-height: 1.45;
}
.lg-toc-a:hover { background: var(--surface-3); color: var(--ink); text-decoration: none; }
.lg-toc-a.on { background: var(--accent-blue); color: #fff; font-weight: 600; }
/* Hovering the active item must not repaint it grey. */
.lg-toc-a.on:hover { background: var(--accent-blue); color: #fff; }
.lg-toc-side { margin-top: 22px; padding: 16px; border-radius: 12px; background: var(--surface); border: 1px solid var(--border); }
.lg-toc-side b { font-size: 13px; display: block; color: var(--ink); }
.lg-toc-side p { font-size: 12.5px; color: var(--muted); margin: 5px 0 8px; line-height: 1.55; }
.lg-toc-side a { font-size: 12.5px; color: var(--link); }

.lg-sec { padding-bottom: 34px; margin-bottom: 34px; border-bottom: 1px solid var(--border); scroll-margin-top: 107px; }
.lg-sec:last-of-type { border-bottom: 0; }
.lg-sec-n { font-family: var(--font-mono); font-size: 11.5px; font-weight: 600; color: var(--link); letter-spacing: 0.06em; margin: 0; }
.lg-sec-h { font-size: 22px; font-weight: 800; letter-spacing: -0.025em; margin: 7px 0 0; color: var(--ink); }
.lg-sec .legal-body-copy { margin-top: 13px; }
.lg-sec-intro { border-bottom: 1px solid var(--border); }

/* Body copy inside the sections. .prose caps the measure at 72ch, so the wider
   shell gives the page air without giving it an unreadable line length. */
.legal-body-copy p { margin: 0 0 13px; }
.legal-body-copy p:last-child { margin-bottom: 0; }
.legal-body-copy ul, .legal-body-copy ol { margin: 14px 0 0; padding: 0; list-style: none; }
.legal-body-copy li { position: relative; padding-left: 24px; margin-top: 9px; line-height: 1.7; }
.legal-body-copy li::before {
    content: ''; position: absolute; left: 6px; top: 11px;
    width: 6px; height: 6px; border-radius: 2px; background: var(--accent-blue);
}
.legal-body-copy a { color: var(--link); }

/* Critical-items callout. Solid tint, flat border, no blur. */
.legal-callout {
    margin-top: 18px; border: 1px solid var(--edge-01); background: var(--fill-68);
    border-radius: 12px; padding: 20px 22px;
}
.legal-callout-t {
    font-size: 11.5px; font-weight: 700; letter-spacing: 0.11em;
    text-transform: uppercase; color: var(--ink-76); margin: 0;
}
.legal-callout ul { margin-top: 12px; }
.legal-callout li { color: var(--ink-69); }
.legal-callout li::before { background: var(--pill-amber); }

/* Related documents.
   Light cards on the page background. This replaces a dark panel that sat flush
   against the dark footer, where the two merged into a single black mass and the
   grey-on-dark buttons read as disabled. A rule and real space above it, and a
   deliberate gap below so the footer starts clear of it. */
.lg-rel { margin-top: 44px; padding-top: 34px; border-top: 1px solid var(--border); }
.lg-rel-t {
    margin: 0; font-size: 11px; font-weight: 700; letter-spacing: 0.13em;
    text-transform: uppercase; color: var(--muted-2);
}
.lg-rel-grid { margin-top: 16px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.lg-rel-card {
    display: block; text-decoration: none; background: var(--surface);
    border: 1px solid var(--border); border-radius: 13px; padding: 18px 19px;
    transition: border-color 0.15s ease, transform 0.15s ease;
}
.lg-rel-card:hover { border-color: var(--edge-31); transform: translateY(-1px); text-decoration: none; }
.lg-rel-card b { display: block; font-size: 15px; color: var(--ink); letter-spacing: -0.01em; }
/* Direct child only. Unscoped, this also matched the arrow <span> inside the
   Read link and turned it into a block, so the arrow dropped onto its own line. */
.lg-rel-card > span { display: block; margin-top: 5px; font-size: 12.5px; color: var(--muted); line-height: 1.5; }
.lg-rel-card i { display: block; margin-top: 12px; font-size: 12.5px; font-style: normal; font-weight: 700; color: var(--link); }
.lg-rel-card i span { display: inline; }

/* Space before the footer, so the page does not end on the cards. */
.lg-page .page-shell { padding-bottom: 56px; }

@media (prefers-reduced-motion: reduce) {
    .lg-rel-card { transition: none; }
    .lg-rel-card:hover { transform: none; }
}


@media (max-width: 900px) {
    .lg-grid { grid-template-columns: minmax(0, 1fr); gap: 26px; }
    .lg-toc { position: static; }
    /* Same solid-fill logic, laid out as a wrapping pill row. The full 1px border
       here is an all-round outline on a pill, not a single-side edge. */
    .lg-toc-nav { flex-direction: row; flex-wrap: wrap; gap: 6px; }
    .lg-toc-a { border: 1px solid var(--border); background: var(--surface); border-radius: 999px; padding: 8px 14px; font-size: 12.5px; }
    .lg-toc-a:hover { background: var(--surface-3); border-color: var(--border); }
    .lg-toc-a.on { background: var(--accent-blue); border-color: var(--accent-blue); color: #fff; }
    .lg-toc-a.on:hover { background: var(--accent-blue); border-color: var(--accent-blue); }
}

@media (max-width: 900px) {
    /* Two up on a tablet, one up on a phone. Three 1fr cards at 768 leaves the
       descriptions too narrow to read. */
    .lg-rel-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 640px) {
    .lg-head { padding: 30px 0 22px; }
    .lg-sec-h { font-size: 19px; }
    .lg-rel { margin-top: 34px; padding-top: 26px; }
    .lg-rel-grid { grid-template-columns: minmax(0, 1fr); }
    .lg-page .page-shell { padding-bottom: 40px; }
}


/* =========================================================================
   COA LIBRARY (/coa-library)
   Dark stat bar, client-side search + product chips, grid list.
   No purity column: purity is documented inside each PDF and is not stored
   per lot, so a column would be hand-maintained and would go stale.
   ========================================================================= */
.coalib-page { background: var(--off-white); padding: 44px 0 72px; }
.coalib-head { margin-bottom: 0; }

/* ---- Certificate library stat band (approved mockup) ---------------------
   Two tiers inside one rounded panel, spanning the full content width so its
   edges line up with the search field and the table beneath it. The old version
   was a four column grid holding three cells, so the band stopped three quarters
   of the way across and left a dead column. There is also no blue accent on the
   first cell any more: that was an inset box-shadow faking a border-left, which
   is a single side border by another name. */
.coalib-band {
    margin-top: 26px;
    background: #0b0e14;
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 16px; overflow: hidden;
}

/* Top tier: the claim, and the link out to how testing works. */
.coalib-band-top {
    display: flex; align-items: center; justify-content: space-between; gap: 20px;
    padding: 16px 30px; background: #12161e;
}
.coalib-band-claim { font-size: 14px; color: #e8eaee; }
.coalib-band-link {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: 13px; color: var(--ink-41); white-space: nowrap; text-decoration: none;
}
.coalib-band-link:hover { color: var(--ink-31); }
.coalib-band-link svg { width: 13px; height: 13px; flex: 0 0 auto; }

/* Lower tier: three equal columns. */
.coalib-band-figures { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.coalib-figure { position: relative; padding: 28px 30px 30px; min-width: 0; }
/* Column dividers are inset hairline elements inside the cell, not borders. */
.coalib-figure + .coalib-figure::before {
    content: ""; position: absolute; left: 0; top: 24px; bottom: 26px;
    width: 1px; background: rgba(255, 255, 255, 0.09);
}
.coalib-figure-l {
    font-size: 11px; color: var(--muted-warm); letter-spacing: 0.13em;
    text-transform: uppercase; margin-bottom: 15px;
}
.coalib-figure-v { font-size: 38px; font-weight: 600; line-height: 1; letter-spacing: -0.01em; }
.coalib-figure-v.is-green { color: var(--ink-03); }
.coalib-figure-v.is-blue { color: var(--ink-41); }
/* A date is a timestamp, not a count, so it stays neutral and steps down a size
   to keep its column the same width as the other two. Never wraps. */
.coalib-figure-v.is-date { font-size: 27px; color: #e8eaee; line-height: 1.35; white-space: nowrap; }

.coalib-signin { font-size: 13px; color: var(--muted); margin: 16px 0 0; }

.coalib-toolbar { margin-top: 22px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.coalib-search { position: relative; flex: 1 1 240px; min-width: 0; }
.coalib-search svg {
    position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
    width: 16px; height: 16px; color: var(--muted-2); pointer-events: none;
}
.coalib-field {
    width: 100%; padding: 12px 15px 12px 40px; min-height: 44px;
    border: 1px solid var(--border); border-radius: 10px;
    font: inherit; font-size: 14.5px; background: var(--surface); color: var(--ink);
}
.coalib-field:focus { outline: 2px solid var(--accent-blue); outline-offset: -1px; }
.coalib-chips { display: flex; gap: 7px; flex-wrap: wrap; }
.coalib-chip {
    font: inherit; font-size: 13px; font-weight: 600; min-height: 44px;
    padding: 9px 15px; border-radius: 999px;
    background: var(--surface); border: 1px solid var(--border); color: var(--ink-61); cursor: pointer;
}
.coalib-chip:hover { border-color: var(--accent-blue); color: var(--link); }
.coalib-chip.is-on { background: var(--surface-dark); border-color: var(--surface-dark); color: #fff; }

.coalib-list { margin-top: 18px; border: 1px solid var(--border); border-radius: 14px; overflow: hidden; background: var(--surface); }
.coalib-lh, .coalib-row { display: grid; grid-template-columns: 1.9fr 0.9fr 1fr 150px; gap: 16px; align-items: center; }
.coalib-lh {
    padding: 13px 20px; background: var(--fill-10); border-bottom: 1px solid var(--border);
    font-size: 11px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted-2);
}
.coalib-row { padding: 15px 20px; border-bottom: 1px solid var(--border); }
.coalib-row:last-of-type { border-bottom: 0; }
.coalib-cell { min-width: 0; overflow-wrap: anywhere; }
/* LOT AND TESTED, READABLE. Both were already #141414, so this was never a
   colour bug in the usual sense: the monospace face at 400 lays down about a
   third of the ink the 700-weight product name beside it does, and next to that
   name it reads as pale grey. Measured on the rendered page: same darkest pixel
   (20), mean ink 65 vs 52, a third of the covered pixels.

   So the fix is weight as much as colour. 600 is a real cut of JetBrains Mono
   (the font link loads 400;500;600), so this is a proper face and not a
   synthetic bold, and the monospace alignment the lot column depends on is
   untouched. */
.coalib-cell.mono { color: var(--ink-36); font-weight: 600; }
/* The lab's own report number, a quieter pill beside the lot. Solid, no border. */
.coalib-ref { display: inline-block; margin-left: 8px; padding: 2px 8px; border-radius: 999px; background: var(--fill-20); color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .02em; vertical-align: middle; }
/* The certificate's size on the product form: a select with its own Save, and
   the orphan note in the site's solid red ink, no border. */

.coalib-product { color: var(--ink); font-weight: 700; font-size: 15px; text-decoration: none; }
.coalib-product:hover { color: var(--link); }
.coalib-size { font-size: 12.5px; color: var(--muted-2); margin-top: 2px; }
.coalib-act { display: flex; justify-content: flex-end; }
.coalib-dl {
    display: inline-flex; align-items: center; gap: 7px; min-height: 44px;
    font-size: 13.5px; font-weight: 700; color: var(--link); text-decoration: none;
    padding: 8px 13px; border: 1px solid var(--edge-12); border-radius: 9px; background: var(--fill-51);
}
.coalib-dl svg { width: 15px; height: 15px; flex: none; }
.coalib-dl:hover { background: var(--fill-32); text-decoration: none; }
.coalib-none { padding: 28px 20px; margin: 0; text-align: center; color: var(--muted); font-size: 14.5px; }
.coalib-count { margin: 16px 0 0; font-size: 13px; color: var(--muted); }
.coalib-empty { margin-top: 26px; }

/* At 768 and below the top tier stacks to two lines and the three figures go one
   per row, with the dividers turning from vertical rules between columns into
   horizontal rules between rows. Later in the file than the base rules above,
   which is what makes it win: a media query adds no specificity of its own. */
@media (max-width: 768px) {
    .coalib-band-top { flex-direction: column; align-items: flex-start; gap: 9px; padding: 16px 20px; }
    .coalib-band-figures { grid-template-columns: 1fr; }
    .coalib-figure { padding: 20px; }
    .coalib-figure + .coalib-figure::before {
        left: 20px; right: 20px; top: 0; bottom: auto; width: auto; height: 1px;
    }
    .coalib-figure-v { font-size: 32px; }
    .coalib-figure-v.is-date { font-size: 24px; }
}

@media (max-width: 767px) {
    /* Header row cannot survive four columns on a phone; rows become cards and
       each cell carries its own label. */
    .coalib-lh { display: none; }
    .coalib-row { grid-template-columns: minmax(0, 1fr); gap: 8px; padding: 16px; }
    .coalib-cell[data-label]:not([data-label=""])::before {
        content: attr(data-label) '  ';
        display: block;
        font-size: 11px; font-weight: 600; letter-spacing: 0.06em;
        text-transform: uppercase; color: var(--muted-2); margin-bottom: 2px;
    }
    .coalib-cell[data-label="Product"]::before { content: none; }
    .coalib-act { justify-content: flex-start; margin-top: 4px; padding-top: 12px; border-top: 1px solid var(--border); }
}

@media (max-width: 640px) {
    .coalib-page { padding: 30px 0 56px; }
}


/* =========================================================================
   YOUR ORDERS (/account)
   Order cards, tabs with live counts, improved empty state.
   Deliberately its own namespace: this page used to borrow the admin .ordv-*
   classes, which is how an admin-only max-width rule ran it edge to edge.
   ========================================================================= */
.acct-page { background: var(--off-white); padding: 44px 0 72px; }

.acct-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 20px; flex-wrap: wrap; }
.acct-dl {
    display: inline-flex; flex-direction: column; align-items: flex-start; justify-content: center;
    min-height: 44px; padding: 9px 15px; border-radius: 10px;
    background: var(--surface); border: 1px solid var(--border); color: var(--ink);
    font-size: 13px; font-weight: 700; text-decoration: none; line-height: 1.3;
}
.acct-dl:hover { border-color: var(--accent-blue); color: var(--link); text-decoration: none; }
.acct-dl-sub { font-size: 11.5px; font-weight: 500; color: var(--muted-2); }

/* ---- PCD Credit card on the account page (mockup section 1) --------------
   Two columns on a wide screen (the balance and the rule on the left, the recent
   ledger on the right), stacking under 760 where a two-column ledger would be
   unreadable. The dark panel is deliberate and is the only dark block on this
   page: it is a balance, and it should read as the one number on the screen that
   is money the customer already has. */
.acct-credit {
    margin-top: 24px; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 0; border-radius: 14px; overflow: hidden; background: var(--surface-dark); color: #fff;
}
.acct-credit-main { padding: 22px 24px; }
.acct-credit-led { padding: 22px 24px; border-left: 1px solid rgba(255, 255, 255, 0.08); min-width: 0; }
.acct-credit-k {
    margin: 0 0 8px; font-size: 11.5px; font-weight: 800; letter-spacing: 0.14em;
    text-transform: uppercase; color: var(--muted-3);
}
.acct-credit-bal { margin: 0; font-size: 34px; font-weight: 800; line-height: 1.1; color: var(--ink-06); }
.acct-credit-note { margin: 8px 0 0; font-size: 13px; line-height: 1.55; color: var(--muted-3); }
.acct-credit-bar { height: 8px; border-radius: 4px; background: rgba(255, 255, 255, 0.10); overflow: hidden; margin: 16px 0 6px; }
.acct-credit-bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--accent-blue), #3ddc97); }
.acct-credit-prog { margin: 0; font-size: 12.5px; color: var(--muted-2); }
.acct-cled { list-style: none; margin: 0; padding: 0; }
.acct-cled-row {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px; align-items: baseline;
    padding: 7px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.06); font-size: 13px;
}
.acct-cled-row:last-child { border-bottom: 0; }
.acct-cled-when { color: var(--muted-2); font-size: 12px; white-space: nowrap; }
.acct-cled-what { color: #fff; min-width: 0; }
.acct-cled-ord { color: var(--muted-2); font-size: 11.5px; }
.acct-cled-amt { font-weight: 700; white-space: nowrap; }
.acct-cled-amt.is-plus { color: var(--ink-06); }
.acct-cled-amt.is-minus { color: var(--muted-3); }
@media (max-width: 760px) {
    .acct-credit { grid-template-columns: minmax(0, 1fr); }
    .acct-credit-led { border-left: 0; border-top: 1px solid rgba(255, 255, 255, 0.08); }
    .acct-credit-bal { font-size: 30px; }
}


.acct-tabs { margin-top: 24px; display: flex; gap: 4px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.acct-tab {
    font-size: 13.5px; font-weight: 600; color: var(--muted); text-decoration: none;
    padding: 11px 15px; border-bottom: 2px solid transparent; margin-bottom: -1px;
    display: inline-flex; align-items: center; gap: 7px; min-height: 44px;
}
.acct-tab:hover { color: var(--ink); text-decoration: none; }
.acct-tab.is-on { color: var(--ink); border-bottom-color: var(--accent-blue); }
.acct-tct { font-size: 11.5px; font-weight: 700; background: var(--surface-3); color: var(--ink-40); border-radius: 999px; padding: 2px 8px; }
.acct-tab.is-on .acct-tct { background: var(--accent-blue); color: #fff; }

.acct-card {
    margin-top: 14px; padding: 20px 22px;
    background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
    display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 18px; align-items: start;
}
.acct-main { min-width: 0; }
.acct-top { display: flex; gap: 11px; align-items: center; flex-wrap: wrap; }
.acct-no { font-family: var(--font-mono); font-size: 14px; font-weight: 600; color: var(--ink); }
.acct-date { font-size: 13px; color: var(--muted-2); }

.acct-items { margin-top: 14px; display: flex; gap: 9px; align-items: center; flex-wrap: wrap; }
.acct-thumb {
    width: 42px; height: 42px; flex: none; border-radius: 9px; overflow: hidden;
    display: grid; place-items: center;
}
/* The fill and hairline only behind the initials fallback; a cut-out vial
   sits on the card with nothing behind it. */
.acct-thumb:has(.acct-thumb-t) { background: var(--fill-35); border: 1px solid var(--border); }
.acct-thumb picture { display: contents; }
.acct-thumb img { width: 100%; height: 100%; object-fit: contain; }
.acct-thumb-t { font-size: 10px; color: var(--muted-2); font-family: var(--font-mono); }
.acct-names { font-size: 13px; color: var(--muted); min-width: 0; overflow-wrap: anywhere; }

.acct-side { text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: 11px; }
.acct-total { font-size: 19px; font-weight: 800; letter-spacing: -0.02em; color: var(--ink); }
.acct-owe { font-size: 13px; font-weight: 700; color: var(--ink-89); }

/* Card buttons are local rather than the site .btn, which is black. The mockup
   wants the brand blue here and a bordered white secondary. */
.acct-btn {
    display: inline-flex; align-items: center; justify-content: center;
    min-height: 44px; padding: 10px 18px; border-radius: 10px;
    background: var(--accent-blue); color: #fff;
    font-size: 13.5px; font-weight: 700; text-decoration: none; white-space: nowrap;
}
.acct-btn:hover { background: #264fc4; color: #fff; text-decoration: none; }
.acct-btn.is-ghost { background: var(--surface); color: var(--ink); border: 1px solid var(--border); }
.acct-btn.is-ghost:hover { background: var(--off-white); color: var(--ink); border-color: var(--accent-blue); }

.acct-foot {
    grid-column: 1 / -1;
    margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border);
    display: flex; gap: 9px; align-items: center; flex-wrap: wrap;
    font-size: 13px; color: var(--muted);
}
.acct-carrier { font-size: 10.5px; font-weight: 600; padding: 4px 7px; border-radius: 5px; color: var(--pill-ink); background: var(--pill-grey); }
/* THE CARRIER CHIPS ARE BRAND COLOURS AND THE LABEL HAS TO BE READ ON THEM.
   --pill-ink is #0a0a0a, which is 1.69:1 on FedEx purple and 2.22:1 on USPS
   navy: the darkest label on two of the darkest brand colours on the site. The
   chips are the same in both modes, so the label is white in both: 11.70:1 and
   8.91:1. Every other carrier chip keeps --pill-ink on --pill-grey. */
.acct-carrier.c-fedex { background: #4D148C; color: #fff; }
.acct-carrier.c-usps  { background: #004B87; color: #fff; }
.acct-trk { font-size: 12.5px; color: var(--ink); }

/* ---- order history deadline line ---------------------------------------
   The quiet counterpart to the pay page clock. One sentence and a button, no
   ticking units: this is read at a glance beside several other orders, and a row
   of clocks down the page would be noise. Tinted rather than solid, because it is
   information about an order that is still perfectly fine. */
.acct-cd {
    border-top: 0; margin-top: 13px; padding: 11px 14px; border-radius: 10px;
    background: var(--pill-amber-bg); border: 1px solid var(--edge-01); color: var(--ink-29);
    font-size: 13.5px; line-height: 1.5;
}
.acct-cd b { font-family: var(--font-mono); font-weight: 700; }
.acct-cd.is-urgent { background: var(--fill-04); border-color: var(--edge-21); color: var(--ink-04); }
/* Past due but not yet swept. Red, because it is still actionable: paying now
   still saves the order. Distinct from is-released below, which is history. */
.acct-cd.is-past { background: var(--fill-04); border-color: var(--edge-19); color: var(--ink-04); font-weight: 600; }
/* Already released. Grey, because it is history rather than something to act on. */
.acct-cd.is-released { background: var(--fill-16); border-color: var(--border); color: var(--muted); }
.acct-cd-btn {
    margin-left: auto; font-size: 13px; font-weight: 700; padding: 9px 14px;
    border-radius: 9px; background: #2E5BE0; color: #fff; text-decoration: none;
    min-height: 40px; display: inline-flex; align-items: center; white-space: nowrap;
}
.acct-cd-btn:hover { background: #264fc4; color: #fff; text-decoration: none; }

@media (max-width: 640px) {
    /* The button goes full width rather than sitting in a 90px column beside a
       sentence that has wrapped to three lines. */
    .acct-cd { flex-direction: column; align-items: stretch; gap: 10px; }
    .acct-cd-btn { margin-left: 0; justify-content: center; }
}


.acct-empty { margin-top: 16px; padding: 64px 24px; text-align: center; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; }
.acct-empty.is-tab { padding: 40px 24px; }
.acct-empty-ic {
    width: 54px; height: 54px; border-radius: 14px; background: var(--fill-36);
    margin: 0 auto 18px; display: grid; place-items: center; color: var(--link);
}
.acct-empty-ic svg { width: 24px; height: 24px; }
.acct-empty-h { font-size: 19px; font-weight: 800; letter-spacing: -0.02em; margin: 0; color: var(--ink); }
.acct-empty-p { margin: 8px 0 0; color: var(--muted); font-size: 14.5px; }
.acct-empty .btn { margin-top: 20px; }

@media (max-width: 640px) {
    .acct-page { padding: 30px 0 56px; }
    /* Card goes single column: the money and the action move under the contents
       rather than being squeezed into a second column on a 360px screen. */
    .acct-card { grid-template-columns: minmax(0, 1fr); padding: 16px; gap: 14px; }
    .acct-side { text-align: left; align-items: stretch; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 10px; }
    .acct-total { flex: 1 1 auto; }
    .acct-btn { width: 100%; }
    .acct-head { align-items: stretch; }
    .acct-dl { width: 100%; align-items: center; }
    .acct-empty { padding: 44px 18px; }
}




/* Tabs. Solid fill when active, no single-side border. */

.lcv-tab {
    display: inline-flex; align-items: center; gap: 7px; min-height: 44px;
    font-size: 13.5px; font-weight: 600; color: var(--muted); text-decoration: none;
    padding: 9px 14px; border-radius: 9px;
}
.lcv-tab:hover { background: var(--surface-3); color: var(--ink); text-decoration: none; }
.lcv-tab.is-on { background: var(--accent-blue); color: #fff; }
.lcv-tab.is-on:hover { background: var(--accent-blue); color: #fff; }
.lcv-tab.is-on .lcv-tct { background: rgba(255, 255, 255, 0.24); color: #fff; }

/* List */

.lcv-pill.r { background: var(--pill-red); }



/* THE PICK LISTS on the lab invoice form. A select in front of the text field
   it fills, with the text field hidden until "Other..." is chosen or there is
   no list to choose from. Both halves sit in one column, so the row grid is
   unchanged and a revealed input pushes nothing sideways. */

.lcv-pick-txt.is-hidden { display: none; }

/* Assign */

.lcv-asg-opt .mono { display: block; font-size: 12px; color: var(--muted); }


/* =========================================================================
   DASHBOARD TOP: hero, attention strip, money flow

   Replaces the two eight-and-four cell bars. The old grid gave the number that
   runs the business the same size as "Needs shipping: 0"; here Received leads at
   roughly twice the rest, colour is semantic (green in, red out, amber held for
   the state) and the dollar sign sits back so the digits lead. Every divider is
   an inset hairline, never a border on one side of a box.
   ========================================================================= */

.dash-hero {
    display: grid;
    grid-template-columns: 1.2fr 1fr 0.9fr 0.8fr 0.9fr;
    gap: 0;
    padding: 18px 24px;
    border-radius: 16px;
    position: relative;
    overflow: hidden;
    color: #f2f4f8;
    background: linear-gradient(170deg, #1c1f27, #16181d);
}
/* Soft green wash from the top left, behind everything. */
.dash-hero::before {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(560px 260px at 8% -30%, rgba(61, 220, 151, 0.14), transparent 65%);
    pointer-events: none;
}
.dash-hcell { padding: 2px 24px; position: relative; min-width: 0; }
.dash-hcell:first-child { padding-left: 0; }
.dash-hcell + .dash-hcell::before {
    content: "";
    position: absolute;
    left: 0; top: 6px; bottom: 6px;
    width: 1px;
    background: rgba(255, 255, 255, 0.08);
}


/* =========================================================================
   DASHBOARD CARDS: the two black bars as individual matte cards
   (docs/mockups/dashboard-cards-mockup.html), made COMPACT on 2026-09-18 to
   the approved compact spec: 13px padding, 21px mono figures, 28px chips at
   radius 8, radius-13 cards with a lighter shadow, 26px operators, and a
   tonal wash per card over #141518 that carries the figure's meaning (green
   received, red refunded, amber lab cost, orange tax held, blue average;
   billed and product cost stay plain; the result card keeps its green
   gradient). Phone: the stat row is a 2-up grid with 18px figures and the
   money row stacks, the money row still measured against its own wrapper
   (container query) so the admin sidebar cannot make the viewport lie. The
   old .dash-hero and .dash-flow rules above are left for the record; nothing
   renders them now.

   Colours were measured on each wash, not assumed from the matte ground
   (docs/reports/2026-09-18-onerate-search-cards-coa.md). The washes are the
   figure colour at 10 percent over #141518: green rgb(24,41,35), red
   rgb(44,30,32), amber rgb(42,39,32), orange rgb(43,33,23), blue
   rgb(30,35,47). The 21px figures are not "large text" (600 weight), so they
   are held to 4.5:1: the lowest is red on its wash at 5.74:1. The sub-line
   is #8a8f97, lifted from #82878f, which read 4.12:1 on the amber wash; #8a8f97
   is the nearest grey passing 4.5:1 on every wash and both ends of the result
   gradient (lowest 4.58:1 on amber). Label #9ba0a8 lowest 5.67:1 on amber.
   ========================================================================= */

.dcs .dash-hlbl .stat-chip { width: 28px; height: 28px; border-radius: 8px; }
.dcs .dash-hlbl .stat-chip svg { width: 15px; height: 15px; }
.dcs .dash-hlbl .stat-chip-txt { font-size: 14px; }

/* Tints: the wash is the figure's colour at 10 percent over the matte ground,
   the chip the same colour at 12 percent, the figure the colour itself. */

.dcs.t-green .stat-chip  { background: rgba(61, 220, 132, .12);  color: #3ddc84; }
.dcs.t-red .stat-chip    { background: rgba(255, 107, 100, .12);  color: #ff6b64; }
.dcs.t-amber .stat-chip  { background: rgba(244, 199, 106, .12);  color: #f4c76a; }
.dcs.t-orange .stat-chip { background: rgba(247, 137, 15, .12);   color: #f7890f; }
.dcs.t-blue .stat-chip   { background: rgba(122, 162, 255, .12);  color: #7aa2ff; }
.dcs.t-plain .stat-chip  { background: rgba(255, 255, 255, .08);  color: #d7dae0; }


/* A negative result keeps its red: a large test run before the sales it covers. */

@container (max-width: 760px) {
    .dcs-money { flex-direction: column; gap: 8px; }
    .dcs-money .dcs { flex: none; width: 100%; }
    .dcs-op { margin: 0 auto; }
}


/* ---- attention strip ---- */

.dash-chip.is-ok    .dash-cdot { background: rgba(25, 184, 119, 0.13); color: var(--ink-50); }
.dash-chip.is-warn  .dash-cdot { background: rgba(214, 158, 32, 0.15); color: var(--ink-91); }
.dash-chip.is-bad   .dash-cdot { background: rgba(220, 80, 80, 0.13);  color: var(--ink-95); }

/* ---- money flow ---- */
.dash-flow {
    background: linear-gradient(170deg, #1c1f27, #16181d);
    border-radius: 14px;
    padding: 14px 22px;
    color: #f2f4f8;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0;
}
.dash-fstep { display: flex; flex-direction: column; gap: 2px; padding: 0 20px; min-width: 0; }
.dash-fstep:first-child { padding-left: 0; }
.dash-fop { font-size: 1.3rem; color: #6a7280; font-weight: 400; padding: 0 4px; align-self: center; }
.dash-fresult {
    margin-left: auto;
    padding: 9px 18px;
    border-radius: 12px;
    position: relative;
    background: linear-gradient(160deg, rgba(61, 220, 151, 0.16), rgba(61, 220, 151, 0.05));
}
.dash-fresult::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 12px;
    box-shadow: inset 0 0 0 1px rgba(61, 220, 151, 0.35);
    pointer-events: none;
}

/* Five money cells stop fitting one row well before the admin gets narrow, so they
   fold rather than squeeze. The dividers follow, or a wrapped cell would carry a
   hairline into open space. */
@media (max-width: 1180px) {
    .dash-hero { grid-template-columns: repeat(3, minmax(0, 1fr)); row-gap: 16px; }
    .dash-hcell:nth-child(3n+1) { padding-left: 0; }
    .dash-hcell:nth-child(3n+1)::before { display: none; }
}

/* =========================================================================
   DASHBOARD TOP, NARROW: ledger, stacked chips, vertical arithmetic

   BELOW 820, the ladder point already in this file. Above it nothing here
   applies, so the approved desktop layout at 1024 and up is untouched. The
   old rules for this band folded the five hero cells into two and then one
   column, which turned each figure into a giant stacked block with dead space
   beside it, and left the flow wrapping into a jumble where the green result
   box overlapped its own subtext and ran past the panel edge.

   This block sits AFTER the base .dash-* rules on purpose: a media query adds
   no specificity of its own, so the only thing making these win is that they
   come later.
   ========================================================================= */
@media (max-width: 820px) {
    /* ---- hero: Received leads, the other four become ledger rows ---- */
    .dash-hero {
        grid-template-columns: minmax(0, 1fr);
        row-gap: 0;
        padding: 16px 16px 6px;
    }
    .dash-hcell { padding: 0; }
    /* The vertical dividers belonged to columns that no longer exist. */
    .dash-hcell + .dash-hcell::before { display: none; }

    .dash-lead { padding-bottom: 12px; }

    .dash-hcell:not(.dash-lead) {
        display: flex;
        align-items: baseline;
        gap: 9px;
        padding: 9px 0;
        position: relative;
    }
    /* Inset hairline above every ledger row, including the first, which is what
       separates the block from the lead figure. Positioned, not a border. */
    .dash-hcell:not(.dash-lead)::after {
        content: "";
        position: absolute;
        top: 0; left: 0; right: 0;
        height: 1px;
        background: rgba(255, 255, 255, 0.08);
    }



    /* ---- flow: the arithmetic read down the page ----
       Two columns: the operators in their own narrow column on the left, each
       one on the row of the step it applies to, and the step itself beside it.
       The operators are siblings of the steps in the markup, so they are placed
       explicitly rather than left to auto-flow. div = step, span = operator. */
    .dash-flow {
        display: grid;
        grid-template-columns: 16px minmax(0, 1fr);
        column-gap: 10px;
        align-items: stretch;
        padding: 6px 16px 12px;
    }
    .dash-flow > div:nth-of-type(1)  { grid-column: 2; grid-row: 1; }
    .dash-flow > span:nth-of-type(1) { grid-column: 1; grid-row: 2; }
    .dash-flow > div:nth-of-type(2)  { grid-column: 2; grid-row: 2; }
    .dash-flow > span:nth-of-type(2) { grid-column: 1; grid-row: 3; }
    .dash-flow > div:nth-of-type(3)  { grid-column: 2; grid-row: 3; }
    .dash-flow > span:nth-of-type(3) { grid-column: 1; grid-row: 4; }
    .dash-flow > div:nth-of-type(4)  { grid-column: 1 / -1; grid-row: 4; }

    .dash-fop {
        width: 16px;
        padding: 0;
        text-align: center;
        align-self: center;
        font-size: 1.05rem;
    }
    /* The "=" sits inside the result box rather than outside it, which is what
       the approved layout shows. Both occupy row 4; the operator is lifted above
       the box and the box carries the left padding that makes room for it. */
    .dash-flow > span:nth-of-type(3) { z-index: 1; justify-self: start; }

    /* Same shape as a ledger row: label, its note inline after it, amount hard
       right, and only the note may give way. */
    .dash-fstep {
        display: flex;
        /* The base rule stacks these in a column; without resetting the direction the
           row read as three stacked lines and every check for one line failed. */
        flex-direction: row;
        align-items: baseline;
        gap: 8px;
        padding: 10px 0;
        position: relative;
    }

    /* Hairline above every step except the first and the result, which has its
       own box and its own spacing. */
    .dash-flow > div:nth-of-type(2)::before,
    .dash-flow > div:nth-of-type(3)::before {
        content: "";
        position: absolute;
        top: 0; left: 0; right: 0;
        height: 1px;
        background: rgba(255, 255, 255, 0.08);
    }

    .dash-fresult {
        margin: 8px 0 0;
        padding: 10px 12px 10px 30px;   /* room on the left for the "=" */
        border-radius: 10px;
    }
}






/* The global `a { color: var(--black) }` renders black on these dark tiles, which
   is all but invisible. Links inside the cost bar get the lit blue instead. */
.dash-cost .dash-stat-sub a { color: var(--ink-68); text-decoration: underline; }
.dash-cost .dash-stat-sub a:hover { color: var(--ink-82); }



/* Rotate is not undoable, so it is worded and coloured as a real action, not a
   danger button that invites a double take. Text only, no red fill. */


.cvl-note .sf-pill { margin-right: 6px; vertical-align: middle; }



/* Source tier pills. Solid, same palette as the public page's badges. */

.srcpill.fda { background: var(--pill-green); }
.srcpill.trial { background: #2E5BE0; }
.srcpill.conv { background: var(--pill-grey); }
.srcpill.none { background: var(--pill-amber); color: var(--ink-26); }




/* Tabs. Solid fill when active, no single-side border. */

.vnd-tab.is-on { background: var(--accent-blue); color: #fff; }
.vnd-tab.is-on:hover { background: var(--accent-blue); color: #fff; }
.vnd-tab.is-on .vnd-tct { background: rgba(255, 255, 255, 0.24); color: #fff; }

/* Solid pills only. */

.vnd-pill.r { background: var(--pill-red); }

/* ---- vendor cards ---- */

.vnd-v.mono { font-family: var(--font-mono); font-size: 13px; }

/* ---- panels ---- */

.vnd-hint { font-size: 12.5px; color: var(--muted); margin: 6px 0 0; max-width: 90ch; line-height: 1.6; }

/* ---- tier picker + search ---- */

.vnd-tier {
    display: inline-flex; align-items: center; min-height: 40px; padding: 8px 14px;
    border-radius: 9px; font-size: 13px; font-weight: 700; text-decoration: none;
    color: var(--ink); background: var(--surface-3);
}
.vnd-tier:hover { background: var(--fill-27); color: var(--ink); text-decoration: none; }
.vnd-tier.is-on { background: var(--accent-blue); color: #fff; }
.vnd-tier.is-on:hover { background: var(--accent-blue); color: #fff; }

/* ---- product table ---- */

.vnd-tr.is-off { background: var(--fill-10); color: var(--muted); }

/* Tier prices, shown together with the one in force filled solid. */

.vnd-price.is-on { background: var(--accent-blue); border-color: var(--accent-blue); }
.vnd-price.is-on b { color: rgba(255, 255, 255, 0.78); }
.vnd-price.is-on i { color: #fff; }
.vnd-price.is-on em { color: rgba(255, 255, 255, 0.85); }

/* ---- forms ---- */

.vnd-fld { width: 100%; padding: 10px 13px; min-height: 44px; border: 1px solid var(--border); border-radius: 9px; font: inherit; font-size: 14px; background: var(--surface); color: var(--ink); }
.vnd-fld:focus { outline: 2px solid var(--accent-blue); outline-offset: -1px; }
/* The lab select's Add a lab row, under the select in the same field. */

.vnd-fld.is-readonly { background: var(--fill-53); color: var(--muted); }
.vnd-num { font-family: var(--font-mono); text-align: right; }


/* ---- import preview ---- */

.vnd-sumtile.is-warn { background: var(--fill-64); border-color: var(--edge-47); }
.vnd-readout li.is-warn { color: var(--ink-20); }
@media (max-width: 640px) {


    /* Full-width controls on a phone, so nothing is a 30px tap target. */
    
    .vnd-tier { flex: 1 1 auto; justify-content: center; }
}







/* An Amount being worked out from Qty x Unit. Tinted, but with full-strength
   text and the ordinary border, because the field IS editable and muted text
   would read as disabled. Typing clears the tint, which is the line saying the
   figure is now yours and nothing will overwrite it. Focus goes white so typing
   feels like any other field. */
.exp-frow .vnd-fld.is-calc { background: var(--fill-45); }
.exp-frow .vnd-fld.is-calc:focus { background: var(--surface); }
/* minmax(0, 1fr), not 1fr: a native date input has a wide intrinsic minimum and
   an auto track would let it push out past the panel instead of shrinking. */

.exp-dates .vnd-fld { min-width: 0; }
.exp-export .vnd-hint { max-width: 90ch; }

/* Five tiles into three columns, then into two, leaves the last one stranded on
   a row of its own. It spans instead, the same way the batches bar does. */
@media (max-width: 1200px) {
    .exp .vnd-stats > .vnd-stat:last-child { grid-column: span 2; }
}



/* Floored at 641px for the same reason as the export grid above: the explicit
   cell placement below is more specific than the stacked-card rules at 640, so
   it has to stop rather than be argued with. */


@media (max-width: 640px) {

    /* The table becomes stacked cards: the header row goes and each cell sits on
       its own line, which is what the mockup does at its own breakpoint. */
    
    .exp .vnd-stats > .vnd-stat:last-child { grid-column: auto; }
}


/* =========================================================================
   ADMIN: RESEARCH CATALOGUE  (/admin/catalogue)
   Extends the .vnd-* vocabulary the vendors and lab costs screens use, so the
   three read as one system. Only the pieces with no equivalent there are new:
   the waiting notice, the product row layout, and the category reorder list.
   ========================================================================= */

/* The notice that names what is being held back. Amber because it is a gap to
   fill rather than something broken, and tinted rather than a solid pill because
   it is a paragraph, not a status. */
.cat-notice {
    margin-top: 16px; padding: 13px 16px; border-radius: 10px;
    background: var(--pill-amber-bg); border: 1px solid var(--edge-01); color: var(--ink-15);
    font-size: 13.5px; line-height: 1.6;
}
.cat-notice b { font-weight: 700; }
.cat-notice-list { display: block; margin-top: 6px; }
.cat-notice-list a { color: var(--ink-15); font-weight: 600; text-decoration: underline; }
.cat-notice-list a:hover { color: var(--ink-60); }





/* =========================================================================
   ADMIN: BATCHES  (/admin/batches)
   Extends the .vnd-* vocabulary the vendors, lab costs and catalogue screens
   use, so the four read as one system. Only the pieces with no equivalent
   there are new: the number preview, the labelling run list, and the two
   states a COA cell can be in that a vendor row has no equivalent for.
   ========================================================================= */

/* The awaiting-a-COA tile. Amber on the figure only, because the tile is
   counting something that still needs doing rather than reporting a failure. */
.vnd-stat.is-warn .vnd-stat-n { color: #ffd75e; }







/* The size label that failed to parse, quoted inside the notice. Monospace so a
   stray character in the label is visible, which is usually what is wrong. */
/* Label-and-value rows in the summary panels. The lab costs screen has the same
   pattern as .lcv-kv; this is the .vnd-* spelling of it, so the batch panels sit
   in the same vocabulary as the rest of these screens. */

.vnd-kv b.mono { font-family: var(--font-mono); font-size: 13.5px; }

.bat-badlabel { font-family: var(--font-mono); font-size: 12px; }
/* Each reason is its own line inside the notice rather than one running
   paragraph, since they are separate jobs with separate fixes. */
.cat-notice .cat-notice-list + .cat-notice-list { margin-top: 6px; }



/* =========================================================================
   PUBLIC: THE CATEGORY TIER
   The ten class pages, the homepage section that links them, the mobile menu
   group, and the product page's link back up. Built on the existing storefront
   vocabulary: the grid is .pgrid, the same one the catalog uses.
   ========================================================================= */

/* ---- Browse by class, on the shop page ----
   Replaced the .hclass tile grid that used to sit above the catalog. Colours,
   radii, spacing and type sizes below are the approved shop mockup's, and they
   match the nav dropdown, which is the same component in a different place. */

/* The pill row and the trigger. The pills themselves are untouched: this only
   gives them a parent that can hold something at the other end. */
/* 26px, not the mockup's 10px. The mockup was rendered at 680px wide, where 10px
   reads correctly; at 1440 it is too tight against the count line below. */
.cbc-bar {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    margin-bottom: 26px;
}
.cbc-bar .catalog-filters { margin-bottom: 0; min-width: 0; }
.cbc-trigger {
    display: inline-flex; align-items: center; gap: 8px; flex: 0 0 auto;
    padding: 9px 15px; border-radius: 999px; cursor: pointer;
    background: var(--cbc-trig-bg); border: 0.5px solid var(--cbc-trig-line);
    color: var(--cbc-trig-ink); font-size: 13px; line-height: 1.2;
}
.cbc-trigger:hover { border-color: var(--cbc-trig-line-h); }
.cbc-chev { width: 12px; height: 12px; flex: 0 0 auto; color: var(--cbc-chev); transition: transform .15s ease; }
.cbc-trigger[aria-expanded="true"] .cbc-chev { transform: rotate(180deg); }

/* IN NORMAL FLOW. Opening pushes the grid down instead of covering it, so this
   never competes with the cards' certificate hover panels for a stacking
   context. Do not make it absolute.

   THE GAP ABOVE IT IS 18px, THE SAME 18 AS EVERYTHING ELSE HERE. It was 4px at
   1440: the panel opened hard against the row of buttons that opens it, so the
   two read as one object with a hairline through it. 18 is not a new number,
   it is this panel's own internal padding and the margin it already carries
   below, so the panel now sits with the same air on both sides. */
.cbc-panel {
    background: var(--cbc-panel-bg); border: 0.5px solid var(--cbc-panel-line);
    border-radius: 12px; overflow: hidden; margin: 18px 0;
}
.cbc-cols {
    display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
    padding: 18px 18px 16px;
}
.cbc-col { padding-right: 16px; min-width: 0; }
/* The second half of Peptides. No heading of its own, so it needs the height of
   one to line its first row up with the first column's. */
.cbc-col-continued { padding-top: 24px; }
/* THE COLUMN DIVIDER IS AN INSET POSITIONED HAIRLINE, NOT A border-left.
   No single side borders anywhere, structural or otherwise. A positioned element
   also insets cleanly from the column's top and bottom, which a border cannot:
   a border runs the full box height whether or not there is content beside it. */
.cbc-col-divided { position: relative; padding-left: 18px; }
.cbc-col-divided::before {
    content: "";
    position: absolute; left: 0; top: 0; bottom: 0;
    width: 1px; background: var(--cbc-divider);
}
.cbc-group {
    display: block; margin-bottom: 10px;
    font-size: 11px; letter-spacing: .09em; text-transform: uppercase; color: var(--cbc-group);
}
.cbc-row {
    display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
    padding: 7px 10px; border-radius: 6px; text-decoration: none;
}
.cbc-label { font-size: 13px; color: var(--cbc-label); overflow-wrap: anywhere; }
.cbc-count { font-size: 12px; color: var(--cbc-group); flex: 0 0 auto; }
.cbc-row:hover, .cbc-row:focus-visible { background: var(--cbc-row-hover); }
.cbc-row:hover .cbc-label, .cbc-row:focus-visible .cbc-label { color: var(--cbc-label-hover); }
.cbc-foot {
    position: relative;
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 15px 28px; text-decoration: none;
    background: var(--cbc-foot-bg);
}
.cbc-foot::before {
    content: "";
    position: absolute; left: 0; right: 0; top: 0;
    height: 1px; background: var(--cbc-divider);
}
.cbc-foot .cbc-label { font-size: 13px; font-weight: 600; color: var(--link); }
.cbc-foot:hover { background: var(--cbc-foot-bg-h); }

/* Two columns. Peptides still splits across the first two, so its halves stay
   side by side; Compounds and Supplies drop to the second row, where a vertical
   divider would draw a stray line down the container edge. */
@media (max-width: 1024px) {
    .cbc-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .cbc-col-divided { padding-left: 0; padding-top: 18px; }
    .cbc-col-divided::before { display: none; }
}


/* One stacked full width list. The trigger becomes a full width row above it,
   because a pill at the end of a row that already scrolls sideways is a control
   nobody finds. */
@media (max-width: 640px) {
    .cbc-bar { display: block; margin-bottom: 0; }
    .cbc-bar .catalog-filters { margin-bottom: 10px; }
    .cbc-trigger {
        display: flex; width: 100%; justify-content: space-between;
        padding: 12px 14px; border-radius: 8px;
        background: var(--cbc-trig-bg-m); border-color: var(--cbc-trig-line-m);
        color: var(--cbc-trig-ink-m); margin-bottom: 14px;
    }
    .cbc-chev { width: 14px; height: 14px; }
    /* WAS margin: -6px 0 14px, A NEGATIVE TOP. It pulled the panel up under the
       trigger until the two touched; 12px is the gap the rest of this block
       works in (10, 12, 14) and it is the first value above the trigger's own
       14px bottom margin that still reads as one group. */
    .cbc-panel { border-radius: 8px; margin: 12px 0 14px; }
    /* WAS padding: 8px 0. With no horizontal padding the heading boxes and the
       rows ran to the panel's border: the label was inset 14px by the row's own
       padding, but the row's hover fill and the heading's box touched the edge.
       8px here and 6px on the row keeps the label at the same 14px from the
       panel and lifts the boxes off the border. */
    .cbc-cols { grid-template-columns: 1fr; padding: 8px; }
    .cbc-col, .cbc-col-divided, .cbc-col-continued { padding: 0; }
    .cbc-col-divided::before { display: none; }
    .cbc-group { padding: 6px; margin-bottom: 0; }
    .cbc-row { padding: 10px 6px; border-radius: 6px; }
    .cbc-foot { margin-top: 7px; padding: 12px 14px; }
}

@media (prefers-reduced-motion: reduce) {
    .cbc-chev, .pclass-morechev { transition: none; }
}


/* ---- the class page ---- */
/* THE CAP IS ON THE CONTAINER, NOT ON EACH CHILD, AND THAT IS THE WHOLE POINT.
   This used to be `.pclass > * { max-width: var(--content-max); margin-inline:
   auto }`, which centres every child independently. Any child that then set a
   margin SHORTHAND silently opted itself out, because `margin: 18px 0 12px`
   writes margin-left: 0 over the auto. .pclass-count did exactly that, so past
   the cap the count row sat hard against the gutter while the breadcrumb, title
   and grid were centred: one row on a different left edge from everything above
   and below it, for no visible reason.

   With the cap here, children are ordinary block boxes that fill the padding box
   and all start at the same x. There is no auto margin left for a shorthand to
   overwrite, so the failure cannot come back.

   width: 100% is load bearing. body is a flex COLUMN for the sticky footer, and
   auto inline margins on a flex child override the stretch, so without it this
   would shrink to its content instead of filling the row. */
.pclass {
    width: 100%;
    max-width: calc(var(--content-max) + var(--gutter) * 2);
    margin-left: auto;
    margin-right: auto;
    padding: 26px var(--gutter) 60px;
}
.pclass-crumb { display: flex; flex-wrap: wrap; gap: 7px; font-size: 13px; color: var(--ink-10); }
.pclass-crumb a { color: inherit; text-decoration: none; }
.pclass-crumb a:hover { color: #fff; text-decoration: underline; }
.pclass-crumb-sec { color: var(--muted-warm); }
.pclass-crumb-current { color: #fff; }

/* Compressed head: breadcrumb, title, one paragraph, then the grid. The rest of
   the prose is behind the toggle below, so the products start near the top of
   the page rather than under four paragraphs. */
.pclass-head { margin-top: 14px; }
.pclass-h1 { margin: 0; font-size: clamp(26px, 4.4vw, 38px); font-weight: 800; letter-spacing: -0.02em; color: #fff; }
.pclass-intro { margin-top: 10px; max-width: 78ch; }
.pclass-intro p { margin: 0 0 12px; font-size: 15px; line-height: 1.7; color: #c9ccd3; }
.pclass-intro a { color: var(--ink-07); text-decoration: underline; text-underline-offset: 2px; }
.pclass-intro a:hover { color: var(--ink-31); }
/* Matches .pclass-intro p at 0,2,0 and sits after it, so it wins on order. */
.pclass-intro .pclass-lead { margin-bottom: 8px; }
.pclass-siblings, .pclass-coalink { font-size: 14px; }
.pclass-siblings a + a { margin-left: 4px; }
.pclass-more { padding-top: 4px; }
/* A text control, not a button shape: it sits in a paragraph flow and a filled
   button here would read as the page's primary action, which it is not. */
.pclass-morebtn {
    display: inline-flex; align-items: center; gap: 6px;
    margin: 0; padding: 0; border: 0; background: none; cursor: pointer;
    font-family: inherit; font-size: 14px; color: var(--ink-07);
}
.pclass-morebtn:hover { color: var(--ink-31); text-decoration: underline; }
.pclass-morechev { width: 13px; height: 13px; flex: 0 0 auto; transition: transform .15s ease; }
.pclass-morebtn[aria-expanded="true"] .pclass-morechev { transform: rotate(180deg); }

/* The count row, and on the right the way out to another class. The hairline is
   a positioned element, not a border-top: no single side borders anywhere. */
.pclass-countbar {
    position: relative;
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    margin-top: 22px; padding-top: 16px; margin-bottom: 14px;
}
.pclass-countbar::before {
    content: "";
    position: absolute; left: 0; right: 0; top: 0;
    height: 1px; background: rgba(255, 255, 255, 0.09);
}
.pclass-count { margin: 0; min-width: 0; font-size: 13px; color: var(--muted-warm); }

/* THIS BLOCK MUST STAY BELOW THE RULE IT OVERRIDES, AND THAT IS THE WHOLE POINT.
   A media query adds no specificity. These rules and the .pclass-countbar rule
   above are both 0,1,0, so the only thing deciding the winner is source order.
   This block used to live several hundred lines earlier, next to the other .cbc
   mobile rules, where it lost: the bar stayed display:flex on a phone, the
   full-width trigger sat beside the count in a row that could not hold both, and
   the page grew wider than the viewport.

   That one overflow caused three separate visible faults, because Safari widens
   the layout viewport to fit overflowing content and then evaluates media
   queries against the WIDER width. So the nav strip, sized to the real viewport,
   stopped short of the right edge with black beyond it, and narrow breakpoints
   stopped matching on a phone even though they measured correctly in every
   desktop browser. Keep related breakpoints beside the rule they modify. */
@media (max-width: 640px) {
    /* The trigger takes its own full width line under the count. Nothing shares
       a line, so nothing can wrap into anything else. */
    .pclass-countbar { display: block; margin-bottom: 12px; }
    .pclass-count { margin-bottom: 10px; }
}

.pclass-empty { margin-top: 26px; padding: 26px; border-radius: 14px; background: #12151c; border: 0.5px solid rgba(255, 255, 255, 0.09); }
.pclass-empty p { margin: 0 0 14px; color: #c9ccd3; }
.pclass-back { color: var(--ink-07); font-weight: 600; text-decoration: none; }
.pclass-back:hover { text-decoration: underline; }

/* ---- the product page's link up ---- */
/* The certificate source beside a purity figure, and the neutral pending line
   that replaces a figure when no document records one. Both link to the library
   so the claim, or its absence, is checkable in one click. */
.pd-purity-src {
    margin-left: 8px; font-family: var(--font-ui); font-size: 12px;
    color: var(--muted); text-decoration: underline; text-underline-offset: 2px;
}
.pd-purity-src:hover { color: var(--link); }
.pd-coa-pending { font-size: 13.5px; color: var(--muted); text-decoration: underline; text-underline-offset: 2px; }
.pd-coa-pending:hover { color: var(--link); }
.pd-class { margin: 10px 0 0; font-size: 13.5px; line-height: 1.6; color: var(--muted); }
.pd-class a { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }
.pd-class a:hover { color: var(--ink-52); }

/* ---- the mobile menu category group ----
   NOT .mnav-head. That name belongs to the overlay's 76px header bar at the top
   of this file, which holds the logo and the close button. This block used it
   too, sits later in the file, and so was overriding that bar from flex to
   block and replacing its padding. Do not rename this back. */
.mnav-group {
    display: block; padding: 12px 4px 4px;
    font-size: 11px; letter-spacing: .09em; text-transform: uppercase;
    color: var(--chrome-muted);
}
.mnav-link-cat { padding-left: 12px; justify-content: space-between; gap: 12px; }
.mnav-link-cat .mnav-label { font-size: 15px; color: #c9ccd3; }
.mnav-count { font-size: 12px; color: var(--chrome-muted); flex: 0 0 auto; }
.mnav-link-all .mnav-label { font-weight: 600; color: var(--chrome-link); }

/* ---- the admin gap report ---- */


@media (max-width: 900px) {
    .pclass { padding: 22px var(--gutter) 48px; }
}


/* ---- /shop and the parent indexes ----
   Both share the class page's shell (.pclass, its crumb, title, intro and count
   row) and the class page's body class, so these pages are dark in all three
   states like the class pages, through the same chrome list. Every control is
   44px tall, per the touch target block further down.

   GLASS, per docs/mockups/shop-toolbar-mockup.html: a gradient, a hairline edge
   and an inner highlight. No backdrop-filter anywhere outside the nav, and no
   drop shadow: the mockup's outer shadow is left off because the brief's
   definition of glass is those three things only. .glass-ctl is the one class,
   worn by the segment, the sort, the class links, the mobile Browse control and
   the home band's link, so the surfaces cannot drift. Literals rather than
   tokens: these controls only ever sit on the always-dark catalog ground. */
.glass-ctl {
    background: linear-gradient(160deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.04));
    border: 1px solid rgba(255, 255, 255, 0.14);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14);
    border-radius: 999px;
}

/* The toolbar row: segment left, sort right, 900 and up. */
.shop-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 22px; }

/* The stock segment: three links in one glass pill, the current one white on
   dark. The container is 52 (4px of padding round 44px items). */
.shop-seg { display: inline-flex; align-items: center; padding: 4px; }
.shop-seg-item {
    display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0 18px;
    border-radius: 999px; text-decoration: none;
    color: #d7d9de; font-size: 15px; font-weight: 600; line-height: 1.2;
    transition: color .15s ease, background .15s ease;
}
.shop-seg-item:hover, .shop-seg-item:focus-visible { color: #fff; text-decoration: none; }
.shop-seg-item.is-on { background: #fff; color: #111; }
.shop-seg-item.is-on:hover { color: #111; }

/* The sort: the glass pill is the form; the select sits inside it with no
   chrome of its own, and the chevron is a data URI because a select cannot
   hold an <svg>. 52 outside, 44 inside, matching the segment. */
.shop-sort { display: inline-flex; align-items: center; gap: 8px; padding: 4px 4px 4px 16px; }
.shop-sort-label { font-size: 15px; font-weight: 500; color: #9a9ea6; }
.shop-sort-select {
    appearance: none; -webkit-appearance: none;
    min-height: 44px; padding: 0 36px 0 6px; border: 0; border-radius: 999px; cursor: pointer;
    background: transparent url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23cfd3da' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>") no-repeat right 14px center / 13px 13px;
    color: #ececee; font-family: inherit; font-size: 15px; font-weight: 600; line-height: 1.2;
}
.shop-sort-select option { color: #111; background: #fff; }
.shop-sort-select:focus-visible { outline: 2px solid var(--ink-07); outline-offset: 2px; }
/* Apply is for the no-script visitor. With script the select submits itself
   and app.js stamps the form so the button leaves the row. */
.shop-sort-go {
    min-height: 44px; padding: 0 16px; border-radius: 999px; cursor: pointer;
    background: rgba(255, 255, 255, 0.10); border: 1px solid rgba(255, 255, 255, 0.14); color: #ececee;
    font-family: inherit; font-size: 14px; font-weight: 600;
}
.shop-sort[data-shop-sort-ready] .shop-sort-go { display: none; }

/* The mobile Browse by class control: hidden from 900 up, where the strip is. */
.shop-classes-btn {
    display: none; align-items: center; justify-content: space-between; gap: 10px;
    min-height: 52px; padding: 0 16px 0 20px; cursor: pointer;
    color: #ececee; font-family: inherit; font-size: 15px; font-weight: 600;
}
.shop-classes-btn .cbc-chev { width: 14px; height: 14px; color: #cfd3da; }
.shop-classes-btn[aria-expanded="true"] .cbc-chev { transform: rotate(180deg); }

/* The class strip, 900 and up: the glass links WRAP, two rows at most at 1024
   and 1440, every link fully visible. It shipped as one sideways-scrolling row
   with the scrollbar hidden, which on a desktop without a trackpad has no
   scrollbar and no arrows, so the links past the edge were unreachable except
   by Tab. No overflow, no hidden scrollbar. Links out, never filters. */
.shop-strip {
    display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
    margin-top: 14px; padding: 2px 0 6px;
}
.shop-strip-label {
    flex: none; padding-right: 6px;
    font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: #9a9ea6;
}
.shop-class {
    flex: none; display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 14px;
    text-decoration: none; white-space: nowrap;
    color: #d7d9de; font-size: 14px; font-weight: 600;
    transition: color .15s ease, border-color .15s ease;
}
.shop-class:hover, .shop-class:focus-visible { color: #fff; border-color: rgba(255, 255, 255, 0.30); text-decoration: none; }
.shop-class-count { font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: #9a9ea6; }
/* The shared panel is the phone's list; from 900 up the strip is, so the panel
   stays out of the way even when noscript would show it.
   12, the same gap the phone block gives it, so the two page-specific overrides
   and the base all work in one scale rather than three. */
.shop-body .cbc-panel { margin-top: 12px; }
@media (min-width: 900px) {
    .shop-body .cbc-panel { display: none !important; }
}


/* Under 900: segment, sort and Browse by class stack, each full width. */
@media (max-width: 899px) {
    .shop-bar { flex-direction: column; align-items: stretch; }
    .shop-seg { display: flex; }
    .shop-seg-item { flex: 1 1 0; padding: 0 8px; }
    .shop-sort { display: flex; }
    .shop-sort-select { flex: 1 1 auto; min-width: 0; }
    .shop-classes-btn { display: flex; }
    .shop-strip { display: none; }
}


/* The pager. Links and one current marker, 44px each. */
.shop-pager { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 28px; }
.shop-page {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 44px; min-height: 44px; padding: 0 14px; border-radius: 10px; text-decoration: none;
    background: var(--cat-chip-bg); border: 1px solid var(--cat-chip-line); color: var(--cat-chip-ink);
    font-size: 14px; font-weight: 500;
}
a.shop-page:hover, a.shop-page:focus-visible { border-color: var(--cat-chip-line-h); color: var(--cat-chip-ink-h); text-decoration: none; }
.shop-page.is-on { background: var(--cat-chip-on-bg); color: var(--cat-chip-on-ink); border-color: var(--cat-chip-on-line); }

/* ---- the home All products band ----
   Directly above the content sections: the glass link to /shop with the live
   count on the left, the Browse by class trigger as before on the right. The
   gap above and below is the 44px between the content sections, so the band
   reads as one of them rather than as a leftover of the old filter row. */
.home-body .catalog { padding-top: 44px; }
.home-body .home-seo { margin-top: 44px; }
.home-band {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    max-width: var(--content-max); margin: 0 auto;
}
.home-band-shop {
    display: inline-flex; align-items: center; min-height: 52px; padding: 0 22px;
    text-decoration: none; color: #ececee; font-size: 15px; font-weight: 600;
    transition: color .15s ease, border-color .15s ease;
}
.home-band-shop:hover, .home-band-shop:focus-visible { color: #fff; border-color: rgba(255, 255, 255, 0.30); text-decoration: none; }
.home-band-count { margin: 0 .35em; font-family: var(--font-mono); font-weight: 600; color: #fff; }
.home-body .cbc-trigger { min-height: 44px; }
@media (max-width: 640px) {
    .home-band { display: block; }
    .home-band-shop { display: flex; justify-content: center; margin-bottom: 10px; }
    .home-body .cbc-trigger { margin-bottom: 0; }
    /* 8px was under the trigger it opens from; 12 is the phone block's gap. */
    .home-body .cbc-panel { margin-top: 12px; }
}


/* ---- size-specific badges ----
   One badge over the card image says Out of Stock or On backorder for the size
   the card has selected (card_badge_state() in PHP, cardBadgeState() in app.js).
   The backorder form is the cart's solid red, the same fact in the same colour.
   Add to cart follows the size on the card and the product page. */
.pcard-oos-badge.is-backorder { background: var(--pill-red); color: var(--pill-ink); }
.pcard-add:disabled, .pcard-cart:disabled, .pd-add:disabled {
    opacity: .45; cursor: not-allowed; box-shadow: none;
}
.pd-add:disabled:hover { background: var(--btn-orange, var(--pcd-orange)); }
.pcard-add:disabled:hover { background: var(--accent-blue); }
.pcard-cart:disabled:hover { transform: none; background: var(--accent-blue); }
/* An out size on the product page is selectable now: the pill says Out of
   Stock about it and Add to cart goes off. Still marked, no longer forbidden. */
.pd-chip.is-out { cursor: pointer; }
/* The featured rail's badge: top right, opposite the Lot tested pill. Solid,
   in the markup on every card that needs it; fades in on the centre card like
   its neighbour so the swap never changes a card's height. */
.cf-stock {
    position: absolute; top: 11px; right: 11px;
    display: inline-flex; align-items: center;
    background: #1c1c1c; color: #fff;
    font-size: 10px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
    padding: 5px 10px; border-radius: 999px;
    opacity: 0; transition: opacity .35s ease .1s;
}
.cf-stock.is-backorder { background: var(--pill-red); color: var(--pill-ink); }

/* The parent index: one card per class, the whole card a link. The panel
   family gives it the class panel's surface; the lift on hover is the same
   two pixels the mockup drew. */
.pparent-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 18px;
    margin-top: 22px;
}
.pparent-card {
    display: block; padding: 20px 22px; border-radius: 16px; text-decoration: none; color: inherit;
    background: var(--cbc-panel-bg); border: 0.5px solid var(--cbc-panel-line);
    transition: transform .2s ease, border-color .2s ease;
}
.pparent-card:hover, .pparent-card:focus-visible { transform: translateY(-2px); border-color: var(--cbc-trig-line-h); text-decoration: none; }
.pparent-name {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
    margin: 0 0 6px; font-size: 18px; font-weight: 700; letter-spacing: -0.01em; color: #fff;
}
.pparent-count { font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--muted-warm); }
.pparent-blurb { margin: 0 0 10px; font-size: 14.5px; line-height: 1.6; color: #c9ccd3; }
.pparent-examples { margin: 0; font-size: 13px; line-height: 1.6; color: var(--muted-warm); }
.pparent-examples b { font-weight: 600; color: #fff; }
.pparent-all { margin: 26px 0 0; font-size: 14px; }
.pparent-all .pclass-back { display: inline-flex; align-items: center; min-height: 44px; }
@media (max-width: 640px) {
    .pparent-grid { grid-template-columns: 1fr; }
}



@media (prefers-reduced-motion: reduce) {
    .pparent-card { transition: none; }
    .pparent-card:hover, .pparent-card:focus-visible { transform: none; }
}










/* A lot with no certificate. Muted and in the body face rather than the mono
   used for a real figure, so it does not read as a value. */
.lbl-purity-none { font-family: var(--font-ui); font-weight: 500; color: var(--muted); }

/* The purity claim disagreeing with this lot's certificate.

   99% PURITY prints under a checkbox now rather than being read off a COA, so
   nothing stops it being printed on a lot whose certificate says something
   else. This is not a block and it does not change the canvas: the operator
   asked for the line and gets the line. It is amber rather than red for
   exactly that reason, and it uses the same amber the order screens use. */
.lbl-purity-warn { font-family: var(--font-ui); font-weight: 600; color: var(--ink-29); }

/* The CAS note reuses .lbl-purity-none above: a product nobody has typed a
   registry number for is the same kind of blank as a lot with no certificate,
   and it must read as a statement rather than as a value.

   The LYOPHILIZED POWDER box. Same shape as the bac water generator's mode rows,
   because it is the same kind of control: a decision about the run that carries a
   sentence of explanation, not a bare tick in a form grid. */
.lbl-chk { display: flex; gap: 10px; align-items: flex-start; margin-top: 14px; padding: 11px 0 2px; font-size: 13.5px; line-height: 1.55; color: var(--ink); cursor: pointer; }
.lbl-chk input { margin: 3px 0 0; flex: none; width: 17px; height: 17px; accent-color: var(--accent-blue); cursor: pointer; }

/* The label sits on a neutral grey so a pure white print is visible against it. */

#lbl-canvas { display: block; max-width: 100%; height: auto; }
.lbl-dims { margin: 12px 0 0; font-family: var(--font-mono); font-size: 12px; color: var(--muted); text-align: center; }

/* ---- The drag editor overlay --------------------------------------------
   Handles sit over the canvas, one per element, positioned in CSS pixels from
   the box list the renderer publishes. The wrap is the positioning context, so
   the overlay tracks the canvas at any zoom or label size without a second
   measurement. */

.lbl-edit { position: absolute; inset: 0; }
.lbl-h {
    position: absolute; appearance: none; -webkit-appearance: none; margin: 0; padding: 0;
    background: rgba(46, 91, 224, .10); border: 1px solid rgba(46, 91, 224, .55);
    border-radius: 3px; cursor: grab; font: inherit; color: transparent;
    /* The handle IS the drag target on a phone, so it must not be hijacked by
       the browser's own scroll and pinch gestures mid drag. */
    touch-action: none; -webkit-user-select: none; user-select: none;
    min-width: 10px; min-height: 10px;
}
.lbl-h:hover  { background: rgba(46, 91, 224, .18); border-color: rgba(46, 91, 224, .85); }
/* A HANDLE OVER A FULL BLEED BLACK FIELD, which on the vial label is the
   compound band and nothing else. The handle's wash and border are read as part
   of the artwork when what is under them is solid black running to both edges:
   measured on the preview, the band's outer columns came back at luminance 55
   and its field at 9, against 0 for the info block below it, which has no handle
   over its field. That reads as a band inset from the label edge with white
   stock either side, and the exported PNG has no such margin. So it draws
   nothing until it is pointed at, focused or selected, and stays just as
   draggable. */
.lbl-h.is-bleed { background: transparent; border-color: transparent; border-radius: 0; }
.lbl-h.is-bleed:hover, .lbl-h.is-bleed:focus-visible {
    background: rgba(46, 91, 224, .18); border-color: rgba(46, 91, 224, .85);
}
.lbl-h.is-bleed.is-sel { background: rgba(232, 115, 42, .20); border-color: var(--edge-03); }
/* The exported-size line under the download button, when the file is
   deliberately larger than the stock. It is the one thing on this screen that
   would otherwise read as a mistake. */
.lbl-export-bleed { color: var(--ink); background: var(--fill-14, #f4f6fb); border-radius: 8px; padding: 8px 10px; }
.lbl-h:active { cursor: grabbing; }
.lbl-h.is-sel { background: rgba(232, 115, 42, .20); border-color: var(--edge-03); border-width: 2px; }
.lbl-h:focus-visible { outline: 2px solid var(--edge-03); outline-offset: 2px; }
/* The name rides above its handle, and only when it is the one being moved, so
   eight overlapping captions do not bury the artwork. */
.lbl-h-tag {
    position: absolute; left: 0; bottom: 100%; margin-bottom: 3px; display: none;
    padding: 2px 6px; border-radius: 5px; background: #e8732a; color: #fff;
    font-family: var(--font-ui); font-size: 11px; font-weight: 650; line-height: 1.5;
    white-space: nowrap; pointer-events: none;
}
.lbl-h.is-sel .lbl-h-tag, .lbl-h:hover .lbl-h-tag { display: block; }
.lbl-edit-readout {
    margin: 0; flex: 1 1 260px; min-width: 0;
    font-family: var(--font-ui); font-size: 12.5px; color: var(--muted);
}
.lbl-edit-readout.is-on { color: var(--ink); }

/* ---- Type controls, one row per text block ------------------------------ */
.lbl-type, .lbl-logo { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--border); }
.lbl-type-grid { display: flex; flex-direction: column; gap: 14px; margin-top: 12px; }
.lbl-type-row + .lbl-type-row { padding-top: 12px; border-top: 1px solid var(--border); }
/* THE LABEL SITS ABOVE ITS CONTROLS AT EVERY WIDTH, not beside them.
   Four columns on one line measured 92px for the face select at 1440, because
   this panel is the narrow right hand column of the page and 1440 does not make
   it wider, it makes the OTHER column wider. "Helvetica Neue Bold" does not fit
   in 92px at any screen size, so the row stacks everywhere and the select gets
   the whole width instead of a share of it. */
.lbl-type-row { display: grid; grid-template-columns: minmax(0, 1fr) 92px;
                gap: 6px 8px; align-items: center; }
.lbl-type-l { grid-column: 1 / -1; font-size: 12.5px; font-weight: 650;
              color: var(--ink); font-family: var(--font-ui); }
.lbl-type-stem { grid-column: 1 / -1; text-align: left; }
.lbl-type-row .vnd-fld { margin: 0; min-height: 36px; }
.lbl-type-stem { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); text-align: right; }
.lbl-type-stem.is-ok  { color: var(--ink-24); }
.lbl-type-stem.is-bad { color: var(--ink-33); font-weight: 700; }
.lbl-type-warn { margin: 12px 0 0; padding: 9px 12px; border: 1px solid var(--edge-46);
                 background: var(--fill-65); border-radius: 8px; font-size: 12.5px; color: var(--ink-74); }

/* ---- The label mark upload --------------------------------------------- */

.lbl-logo-prev { margin-top: 14px; }
.lbl-logo-shots img, .lbl-logo-out { max-width: 100%; background: var(--fill-214); border: 1px solid var(--border);
                                     border-radius: 8px; padding: 10px; display: block; }
.lbl-logo-out { min-height: 44px; display: flex; align-items: center; }
.lbl-logo-canvas { display: block; image-rendering: pixelated; }
.lbl-logo-facts { margin: 12px 0 0; font-size: 12.5px; color: var(--muted); }
.lbl-logo-facts.is-ok  { color: var(--ink); }
.lbl-logo-facts.is-bad { color: var(--ink-33); }

.lbl-edit-readout .muted { color: var(--muted); }
/* Solid fill for the active state, never a single-side border. */
.lbl-sz.is-on { background: var(--surface-dark); border-color: var(--surface-dark); color: #fff; }
.lbl-sz.is-on:hover { background: var(--surface-dark); color: #fff; }
.lbl-sz:focus-visible, .lbl-zb:focus-visible { outline: 3px solid var(--accent-blue); outline-offset: 2px; }
.lbl-zb {
    font: inherit; font-size: 12.5px; font-weight: 700; color: var(--muted);
    background: var(--fill-214); border: 1px solid var(--border); border-radius: 8px;
    padding: 8px 12px; cursor: pointer; min-height: 38px;
}
.lbl-zb:hover { border-color: var(--accent-blue); color: var(--ink); }
.lbl-zb.is-on { background: var(--accent-blue); border-color: var(--accent-blue); color: #fff; }
.lbl-zb.is-on:hover { background: var(--accent-blue); color: #fff; }
@media (max-width: 640px) {
    .lbl-sz, .lbl-zb { flex: 1 1 auto; justify-content: center; text-align: center; }
}


/* =========================================================================
   PUBLIC: BATCH VERIFICATION  (/b/{batch_number})
   Storefront standard, not an admin table. For some customers this is the only
   page of ours they look at closely, so it reads as a document rather than a
   data dump. Nothing commercial appears here: no price, no buy button.
   ========================================================================= */

.vfy { padding: 40px 20px 72px; }
.vfy-wrap { max-width: 680px; margin: 0 auto; min-width: 0; }

.vfy-head { text-align: center; margin-bottom: 26px; }
.vfy-eyebrow { font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted-2); margin: 0; }
.vfy-num {
    font-family: var(--font-mono); font-size: clamp(26px, 6.4vw, 40px); font-weight: 700;
    letter-spacing: -0.02em; margin: 8px 0 0; color: var(--ink); overflow-wrap: anywhere;
}
.vfy-compound { margin: 8px 0 0; font-size: 17px; color: var(--ink); font-weight: 600; }
.vfy-strength { color: var(--muted); font-weight: 500; }
.vfy-pill {
    display: inline-flex; align-items: center; margin-top: 14px;
    font-size: 12.5px; font-weight: 700; line-height: 1; letter-spacing: 0.02em;
    padding: 8px 14px; border-radius: 7px; color: #fff; white-space: nowrap;
}
.vfy-pill.g { background: var(--pill-green); }
.vfy-pill.y { background: var(--pill-amber); color: var(--ink-26); }
.vfy-pill.d { background: var(--pill-grey); }

.vfy-card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 26px 28px; margin-top: 16px; min-width: 0; }
.vfy-card-h { font-size: 19px; font-weight: 700; letter-spacing: -0.02em; margin: 0 0 10px; color: var(--ink); }
.vfy-lede { margin: 0 0 12px; font-size: 15px; line-height: 1.7; color: var(--muted); }
.vfy-lede:last-of-type { margin-bottom: 0; }
/* The state cards are tinted rather than outlined on one side, so the three
   read as different at a glance without a directional border. */
.vfy-card.is-verified { background: var(--fill-09); border-color: var(--edge-07); }
.vfy-card.is-pending { background: var(--pill-amber-bg); border-color: var(--edge-01); }
.vfy-card.is-unavailable { background: var(--fill-49); border-color: var(--edge-35); }

.vfy-act { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.vfy-btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    font-size: 15px; font-weight: 700; padding: 13px 22px; min-height: 48px;
    border-radius: 10px; background: var(--accent-blue); color: #fff; text-decoration: none; border: 0;
}
.vfy-btn:hover { background: #264fc4; color: #fff; text-decoration: none; }
.vfy-btn.ghost { background: var(--surface); color: var(--ink); border: 1px solid var(--border); }
.vfy-btn.ghost:hover { background: var(--off-white); color: var(--ink); border-color: var(--accent-blue); }
.vfy-btn:focus-visible { outline: 3px solid var(--accent-blue); outline-offset: 2px; }

.vfy-rows { margin: 0; }
.vfy-row { display: flex; justify-content: space-between; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--border-soft, var(--edge-23)); }
.vfy-row:last-child { border-bottom: 0; }
.vfy-row dt { font-size: 14.5px; color: var(--muted); margin: 0; }
.vfy-row dd { font-size: 15px; font-weight: 600; color: var(--ink); margin: 0; text-align: right; overflow-wrap: anywhere; }
.vfy-row dd.mono { font-family: var(--font-mono); font-size: 14.5px; }

.vfy-note { margin-top: 24px; padding: 0 4px; }
.vfy-note-h { font-size: 13px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted-2); margin: 0 0 10px; }
.vfy-note p { font-size: 14px; line-height: 1.75; color: var(--muted); margin: 0 0 12px; }
.vfy-ruo { font-size: 13px; color: var(--muted-2); margin: 0; }

.vfy-back { margin: 26px 0 0; text-align: center; font-size: 14px; }
.vfy-back a { color: var(--link); text-decoration: none; font-weight: 600; }
.vfy-back a:hover { text-decoration: underline; }
.vfy-dot { color: var(--muted-2); margin: 0 8px; }

@media (max-width: 640px) {
    .vfy { padding: 28px 16px 56px; }
    .vfy-card { padding: 20px 18px; }
    .vfy-row { flex-direction: column; gap: 3px; }
    .vfy-row dd { text-align: left; }
    .vfy-act .vfy-btn { width: 100%; }
}



/* =========================================================================
   ADMIN: QR SCAN TEST SHEET  (/admin/batches/{id}/label-test)
   Sizes are in MILLIMETRES on purpose. This page exists to be printed at 100
   percent and held against a vial, so a comparison rendered at the wrong size
   would prove nothing.
   ========================================================================= */


.qrt-meta { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); margin-top: 8px; }
/* A printed reference, so a printer that silently scaled can be caught. */


@media print {
    /* Everything but the sheet itself goes, so the codes print at true size on
       a clean page. Backgrounds are forced white: a tinted panel behind a QR
       lowers contrast and is exactly what a marginal scan cannot afford. */
    .admin-sidebar, .admin-menu-toggle, .vnd-top, .qrt-noprint, .admin-sidebar-foot { display: none !important; }
    .admin-main, .admin-main-inner { margin: 0 !important; padding: 0 !important; max-width: none !important; }
    body, .admin-body { background: var(--surface) !important; }
}


/* Scan test sheet: three variants per footprint, so the row wraps rather than
   forcing a horizontal scroll on a narrow screen. */

.qrt-meta.is-ok   { color: var(--ink-03); }
.qrt-meta.is-warn { color: var(--ink-20); }
.qrt-meta.is-bad  { color: var(--ink-13); }

/* ---- label generator: the live readout beside the QR controls ---- */
.lbl-readout {
    margin-top: 14px; padding: 12px 14px; border-radius: 10px;
    font-family: var(--font-mono); font-size: 12.5px; line-height: 1.7;
    background: var(--off-white); border: 1px solid var(--border); color: var(--ink);
}
.lbl-readout.is-ok   { background: var(--fill-09); border-color: var(--edge-07); }
.lbl-readout.is-warn { background: var(--fill-67); border-color: var(--edge-01); }
.lbl-readout.is-bad  { background: var(--fill-23); border-color: var(--edge-22); }

/* ---- label generator: the Zebra panel ----
   SOLID PILLS ONLY. The status here is a print job that either reached a
   printer or did not, and a translucent chip over whatever happens to be
   behind it is a status somebody has to squint at. One flat fill, white text,
   no backdrop-filter, no left accent border on the panel: it is a panel like
   every other panel on this screen and the pill is the thing that changes. */

.lbl-copies { width: 76px; flex: 0 0 auto; }
.lbl-pill.is-ok   { background: #1c7a49; }
.lbl-pill.is-bad  { background: #c0322b; }
/* The one-bit preview. image-rendering: pixelated because every dot in it is a
   dot on the stock: a browser smoothing it would show a stem that is not there
   and hide one that is. */

#lbl-zpl-canvas { display: block; image-rendering: pixelated; background: var(--surface); }
.lbl-zpl-off { color: var(--ink-94); }

/* =========================================================================
   ADMIN: CONTACT SHEET  (/admin/batches/{id}/contact-sheet)
   Printed at 100 percent and wrapped round a vial, so the cells are sized in
   millimetres and the grid wraps rather than scrolling.
   ========================================================================= */

.cs-meta { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); margin-top: 2px; }
.cs-meta.is-ok { color: var(--ink-03); }
.cs-meta.is-warn { color: var(--ink-20); }
.cs-meta.is-bad { color: var(--ink-13); }


/* The label sits on a neutral grey so a pure white print is visible against it. */

#bwl-canvas { display: block; max-width: 100%; height: auto; }
.bwl-dims { margin: 12px 0 0; font-family: var(--font-mono); font-size: 12px; color: var(--muted); text-align: center; }
.bwl-zb {
    font: inherit; font-size: 12.5px; font-weight: 700; color: var(--muted);
    background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
    padding: 8px 12px; cursor: pointer; min-height: 38px;
}
.bwl-zb:hover { border-color: var(--accent-blue); color: var(--ink); }
.bwl-zb.is-on { background: var(--accent-blue); border-color: var(--accent-blue); color: #fff; }
.bwl-zb.is-on:hover { background: var(--accent-blue); color: #fff; }
.bwl-zb:focus-visible { outline: 3px solid var(--accent-blue); outline-offset: 2px; }

/* The two QR destinations. Each radio owns the field under it, and the field
   dims rather than disappearing so the choice reads as a comparison. */
.bwl-mode { display: flex; gap: 10px; align-items: flex-start; padding: 11px 0 4px; font-size: 13.5px; line-height: 1.55; color: var(--ink); cursor: pointer; }
.bwl-mode input { margin: 3px 0 0; flex: none; width: 17px; height: 17px; accent-color: var(--accent-blue); cursor: pointer; }
.bwl-mode + .bwl-modefld { margin: 0 0 6px 27px; }
.bwl-modefld.is-off { opacity: 0.45; }
.bwl-modefld.is-off .vnd-fld { background: var(--off-white); }

/* The live readout: module size, quiet zone, what is actually encoded. */
.bwl-readout {
    margin-top: 14px; padding: 12px 14px; border-radius: 10px;
    font-family: var(--font-mono); font-size: 12.5px; line-height: 1.7;
    background: var(--off-white); border: 1px solid var(--border); color: var(--ink);
    overflow-wrap: anywhere;
}
.bwl-readout.is-ok   { background: var(--fill-09); border-color: var(--edge-07); }
.bwl-readout.is-warn { background: var(--pill-amber-bg); border-color: var(--edge-01); }
.bwl-readout.is-bad  { background: var(--fill-23); border-color: var(--edge-22); }
.bwl-save { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border-soft, var(--edge-23)); }
@media (max-width: 640px) {
    .bwl-zb { flex: 1 1 auto; text-align: center; }
    .bwl-mode + .bwl-modefld { margin-left: 0; }
}


/* =============================================================================
   PROFILE PICTURES
   One control, two themes. The storefront is dark and the admin is light, so the
   surfaces differ and the geometry does not.
   ========================================================================== */
.avat { display: flex; align-items: center; gap: 16px; }
.avat-now { flex: none; }
.avat-img { width: 72px; height: 72px; border-radius: 50%; object-fit: cover; display: block; }
/* The initials fallback is a span, so it needs the circle drawing on it. */
span.avat-img { display: flex; align-items: center; justify-content: center;
                font-weight: 800; font-size: 24px; letter-spacing: .02em; }
.avat-h { font-weight: 700; font-size: 15px; margin: 0; }
.avat-sub { font-size: 13px; margin: 2px 0 8px; }
.avat-btns { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.avat-form { display: inline-flex; gap: 8px; align-items: center; margin: 0; }
/* Visually hidden, not display:none: a hidden input cannot be opened by its
   label in some browsers, and keyboard users need it focusable. */
.avat-file { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
             overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.avat-file:focus-visible + .avat-btn { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
.avat-btn { display: inline-flex; align-items: center; justify-content: center;
            min-height: 38px; padding: 0 16px; border-radius: 9px; border: 0;
            font: inherit; font-size: 14px; font-weight: 700; cursor: pointer; }
/* The explicit Upload button only exists for a browser with no JavaScript. With
   script, picking a file submits and this would be a second way to do one thing. */
.avat-go { display: none; }
.avat.no-js .avat-go { display: inline-flex; }

.avat-dark span.avat-img { background: #2E5BE0; color: #fff; }
.avat-dark .avat-h { color: #e9edf6; }
.avat-dark .avat-sub { color: var(--ink-12); }
.avat-dark .avat-btn { background: #2E5BE0; color: #fff; }
.avat-dark .avat-btn.ghost { background: rgba(255,255,255,.08); color: #cfd6e6; }

.avat-light span.avat-img { background: #2E5BE0; color: #fff; }
.avat-light .avat-h { color: var(--ink); }
.avat-light .avat-sub { color: var(--muted); }
.avat-light .avat-btn { background: var(--accent-blue); color: #fff; }
.avat-light .avat-btn.ghost { background: var(--surface-2); color: var(--ink); }

/* Inside a row menu there is no room for a preview or help text. */
.avat.is-compact { display: block; }
.avat.is-compact .avat-btns { flex-direction: column; align-items: stretch; }
.avat.is-compact .avat-form { display: block; }
.avat.is-compact .avat-btn { width: 100%; min-height: 34px; font-size: 13px; }

/* Photos wherever the initials circle already draws one. */
.usrv-avatar.has-photo { object-fit: cover; padding: 0; }
.sales-acct { margin: 0 0 18px; padding: 14px 16px; border-radius: 12px; background: var(--surface);
              box-shadow: inset 0 0 0 1px rgba(20,30,60,.10); }

/* =============================================================================
   SUPPORT MESSAGES, customer side (storefront, light)

   Structure follows docs/mockups/support-messages-mockup.html. The PALETTE does
   not: the mockup draws this half on a dark panel because the mockup's own page
   is dark, and the real account area is light on every other screen. One black
   page in the middle of an account reads as a broken stylesheet, not as a
   design, so the surfaces are the storefront's and the layout is the mockup's.
   ========================================================================== */
.sup-narrow { max-width: 860px; }
.sup-new { display: inline-flex; align-items: center; min-height: 42px; padding: 0 22px;
           border-radius: 10px; background: var(--accent-blue, #2E5BE0); color: #fff;
           font-weight: 700; font-size: 15px; text-decoration: none; }
.sup-new:hover { text-decoration: none; filter: brightness(1.06); }

.sup-list { list-style: none; margin: 18px 0 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.sup-row { position: relative; border-radius: 12px; background: var(--surface);
           box-shadow: inset 0 0 0 1px rgba(20,30,60,.10); }
/* The unread edge, the same device the admin inbox uses. */
.sup-row.is-unread { box-shadow: inset 0 0 0 1px rgba(20,30,60,.10), inset 3px 0 0 var(--accent-blue, #2E5BE0); }
.sup-rlink { display: flex; align-items: center; gap: 14px; padding: 14px 18px;
             text-decoration: none; color: inherit; }
.sup-rlink:hover { text-decoration: none; }
.sup-pill { flex: none; font-size: 11px; font-weight: 800; letter-spacing: .06em;
            padding: 3px 10px; border-radius: 10px; }
.sup-pill.t-open     { background: rgba(214,158,32,.15); color: var(--ink-80); }
.sup-pill.t-answered { background: rgba(25,184,119,.13); color: var(--ink-49); }
.sup-pill.t-closed   { background: rgba(90,100,120,.12); color: var(--ink-210); }
.sup-rmain { min-width: 0; }
.sup-rsub { display: block; font-weight: 700; font-size: 15px; }
.sup-rmeta { display: block; color: var(--muted); font-size: 13px; }
.sup-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent-blue, #2E5BE0); flex: none; }
.sup-rwhen { margin-left: auto; color: var(--muted); font-size: 13px; flex: none; }

.sup-chead { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.sup-cref { color: var(--muted); font-size: 14px; margin: 2px 0 0; }
.sup-back { text-decoration: none; }

/* ---- The conversation, as a conversation ---------------------------------
   These were boxy full-width cards with a heading inside each one, which read as
   a list of records rather than as two people talking. They are bubbles now.

   THE VIEWER IS ALWAYS ON THE RIGHT, in brand blue, in both views. That is the
   convention every chat client uses and it is what makes a screenshot from one
   side legible to the other: whoever took it is on the right.

   RUNS ARE GROUPED IN PHP, not here. A CSS sibling selector cannot see who sent
   the previous message, so the view marks the first and last bubble of each run
   and this styles them. Inside a run the gap closes to 3px and the corners on
   the speaking side tighten, which is what makes three bubbles read as one turn. */
/* The send error, for the enhanced composer only. With no JavaScript a failed
   reply still redirects to the page's own .alert, so this is additive: it is the
   same information in the place the reply was actually typed. */
.sup-liveerr, .supa-liveerr {
    margin: 8px 0 0; font-size: 13px; font-weight: 600; color: var(--ink-33);
}

.sup-conv { margin: 18px 0 0; display: flex; flex-direction: column; }

.sup-lbl { font-size: 11px; font-weight: 700; letter-spacing: .04em; color: var(--muted);
           margin: 14px 0 4px; }
.sup-lbl:first-child { margin-top: 0; }
.sup-lbl.is-mine { text-align: right; }

.sup-row-msg { display: flex; margin-top: 3px; }
/* The gap BETWEEN runs. Inside a run the 3px above is what applies. */
.sup-row-msg.is-first { margin-top: 0; }
.sup-row-msg.is-mine { justify-content: flex-end; }

.sup-bub {
    /* HUGS ITS CONTENT. max-width caps it; there is no width, so a two word
       reply is a two word bubble instead of a full width card. */
    max-width: 66%;
    padding: 9px 14px;
    border-radius: 18px;
    font-size: 15px;
    line-height: 1.45;
    /* A pasted tracking number or a URL with no spaces has to break rather than
       push the bubble past its max-width. */
    overflow-wrap: anywhere;
}
.sup-row-msg.is-them .sup-bub { background: var(--fill-06); color: var(--ink); }
.sup-row-msg.is-mine .sup-bub { background: var(--accent-blue, #2E5BE0); color: #fff; }
/* The tail corner: square off the side the run is speaking from, except on the
   last bubble, which keeps the full radius and reads as the end of the turn. */
.sup-row-msg.is-them:not(.is-last) .sup-bub { border-bottom-left-radius: 6px; }
.sup-row-msg.is-them:not(.is-first) .sup-bub { border-top-left-radius: 6px; }
.sup-row-msg.is-mine:not(.is-last) .sup-bub { border-bottom-right-radius: 6px; }
.sup-row-msg.is-mine:not(.is-first) .sup-bub { border-top-right-radius: 6px; }

.sup-when { font-size: 11px; color: var(--muted); margin: 4px 0 0; }
.sup-when.is-mine { text-align: right; }

/* Photos: rounded thumbnails inside the bubble, no border. The radius is the
   bubble's minus the padding, so the two curves sit concentric. */
.sup-photo { display: block; margin-top: 7px; }
.sup-txt + .sup-photo { margin-top: 8px; }
/* A BUBBLE WITH A PHOTO IS A PHOTO-WIDTH COLUMN. Left to the normal cap the
   bubble is as wide as its longest text line, which either strands the
   thumbnail in a band of empty colour or, if the photo stretches to fill,
   blows a snapshot up to half the page. Narrowing the bubble instead wraps
   the words to the picture's width, which is the shape a caption wants. */
.sup-bub.has-photo { max-width: 320px; }
.sup-photo img { display: block; width: 100%; height: auto; border-radius: 12px; }
/* A photo with no words needs no padding around it: the image is the bubble. */
.sup-bub.is-photo-only { padding: 4px; border-radius: 16px; }
.sup-bub.is-photo-only .sup-photo { margin-top: 0; }
.sup-bub.is-photo-only .sup-photo img { border-radius: 13px; }

.sup-form { margin-top: 16px; display: flex; flex-direction: column; }
.sup-label { font-size: 13px; font-weight: 700; margin: 12px 0 5px; }
.sup-opt { font-weight: 500; color: var(--muted); }
.sup-fld { width: 100%; padding: 11px 14px; min-height: 44px; border: 1px solid var(--border);
           border-radius: 10px; font: inherit; font-size: 15px; background: var(--surface); color: var(--ink); }
.sup-fld:focus { outline: 2px solid var(--accent-blue, #2E5BE0); outline-offset: -1px; }
.sup-area { min-height: 150px; resize: vertical; }
.sup-attachrow { display: flex; align-items: center; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
/* Visually hidden rather than display:none, so the label can open it and it
   stays reachable by keyboard. */
.sup-file { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
            overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.sup-attach { display: inline-flex; align-items: center; gap: 8px; min-height: 42px; padding: 0 16px;
              border-radius: 10px; background: var(--surface-2); color: var(--ink); font-size: 14px;
              font-weight: 700; cursor: pointer; }
.sup-file:focus-visible + .sup-attach, .sup-attach:focus-visible { outline: 2px solid var(--accent-blue, #2E5BE0); outline-offset: 2px; }
.sup-attach-sq { width: 46px; height: 46px; padding: 0; justify-content: center; flex: none; }
.sup-fname { font-size: 13px; color: var(--muted); }
.sup-hint { font-size: 13px; color: var(--muted); }
.sup-reopen { margin-top: 8px; }
.sup-actions { display: flex; align-items: center; gap: 12px; margin-top: 18px; }
.sup-send { min-height: 46px; padding: 0 24px; border: 0; border-radius: 10px;
            background: var(--accent-blue, #2E5BE0); color: #fff; font: inherit;
            font-size: 15px; font-weight: 700; cursor: pointer; flex: none; }
.sup-cancel { color: var(--muted); }

/* POLISH ONLY, no behaviour change: the same controls in the same order, given
   the bubble radius language so the composer belongs to the thread above it.
   Every rule is scoped INSIDE .sup-composer, because .sup-fld, .sup-attach and
   .sup-send are shared with the new-message form, which is a page-sized form and
   should keep its own proportions. */
.sup-composer { display: flex; gap: 8px; margin-top: 16px; align-items: flex-end; flex-wrap: wrap;
                background: var(--surface); border-radius: 24px; padding: 8px;
                box-shadow: inset 0 0 0 1px rgba(20,30,60,.12); }
.sup-cinput { flex: 1; min-width: 220px; min-height: 42px; resize: vertical; }
.sup-composer .sup-fld { border: 0; border-radius: 18px; background: var(--fill-16); padding: 10px 14px;
                         box-shadow: inset 0 0 0 1px rgba(20,30,60,.10); }
.sup-composer .sup-attach-sq { width: 42px; height: 42px; border-radius: 50%; background: var(--fill-06); }
.sup-composer .sup-send { min-height: 42px; border-radius: 21px; padding: 0 20px; }
.sup-fname-wide { flex-basis: 100%; }

/* ===========================================================================
   SUPPORT: THE ACKNOWLEDGEMENT, AND THE AUTOMATIC REPLY.
   Built to docs/mockups/support-autoreply-mockup.html.
   ========================================================================= */

/* The required tick box, above the send button on both the new-thread form and
   the composer. It is a real sentence of legal wording, not a one word label, so
   it is set at reading size and given room rather than squeezed onto the button
   row. */
.sup-ack {
    display: flex; gap: 10px; align-items: flex-start;
    margin: 16px 0 4px; padding: 12px 14px;
    background: var(--fill-54); border: 1px solid var(--edge-02); border-radius: 10px;
    font-size: 13px; line-height: 1.5; color: var(--ink-14); cursor: pointer;
}
.sup-ack input { margin: 2px 0 0; flex: none; width: 17px; height: 17px; accent-color: #2E5BE0; cursor: pointer; }
.sup-ack span { min-width: 0; }
/* THERE IS NO TICK BOX IN THE COMPOSER any more: the acknowledgement is a
   condition of opening a thread, not of every reply in it, so .sup-ack renders on
   the new-message form alone. The composer variant of this rule went with it. */

/* THE AUTOMATIC REPLY IS VISUALLY DISTINCT FROM A HUMAN ONE, which is the whole
   requirement: a template that looked like a person answering would be worse
   than no acknowledgement at all. Quiet grey ground and a dashed edge against
   the solid bubbles either side of it, plus the tag on the label. */
.sup-autotag, .supa-autotag {
    display: inline-block; margin-left: 6px; padding: 2px 7px; border-radius: 5px;
    background: var(--surface-2); color: var(--ink-63);
    font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
}
/* THE SELECTOR HAS TO OUT-SPECIFY ".is-mine .bub", which paints the sender's own
   bubble solid blue. On the admin side the acknowledgement IS from the desk, so
   it matched that rule and came out as a wall of blue: distinct from the
   customer, identical to a human reply from us, which is the one distinction
   that had to hold.

   ROW-PLUS-BUBBLE ALONE WAS NOT ENOUGH: it TIES with ".is-mine .bub" at three
   classes, and that rule is further down the file, so it won on order. The
   conversation container is added to break the tie on specificity rather than on
   position, because position is what a future edit moves by accident. */
.sup-conv .sup-row-msg.is-auto .sup-bub, .supa-conv .supa-row-msg.is-auto .supa-bub,
.sup-row-msg.is-auto .sup-bub, .supa-row-msg.is-auto .supa-bub,
.sup-bub.is-auto, .supa-bub.is-auto {
    background: var(--fill-21); color: var(--ink-14);
    border: 1px dashed var(--edge-34); box-shadow: none;
}
.sup-conv .sup-row-msg.is-auto .sup-bub .sup-txt, .supa-conv .supa-row-msg.is-auto .supa-bub .supa-txt,
.sup-row-msg.is-auto .sup-bub .sup-txt, .supa-row-msg.is-auto .supa-bub .supa-txt,
.sup-bub.is-auto .sup-txt, .supa-bub.is-auto .supa-txt { color: var(--ink-14); }
/* An acknowledgement is a wall of text next to a one line question, so it is
   allowed to be wider than an ordinary bubble rather than becoming a column. */
.sup-bub.is-auto { max-width: 88%; font-size: 14px; }
.supa-bub.is-auto { max-width: 88%; }

/* WHAT THE CUSTOMER TICKED, on the admin side of the thread. Read off the
   message row, so it shows the wording that was on screen at the time even after
   the setting has been edited. */

.supa-ack.is-mine { margin-left: auto; }

@media (max-width: 520px) {
    .sup-ack { font-size: 12.5px; padding: 10px 12px; }
    .sup-bub.is-auto, .supa-bub.is-auto, .supa-ack { max-width: 100%; }
}


/* The unread badge on the account menu, and the dot on the avatar. */
/* ===========================================================================
   MESSAGES IN THE PRIMARY NAV.

   The row existed before this only in the account group, which on mobile renders
   after the whole category tree and on desktop lives inside the avatar dropdown.
   Measured at 390px on this build, it was the nineteenth row in the overlay. It
   is a primary row on both now, and it carries the unread count.
   ========================================================================= */

/* The desktop bar's items are a fixed-height uppercase row, so the count sits
   inline beside the label rather than pushed to a margin the way it is in the
   dropdown. margin-left:auto from .nav-badge would shove it to the far edge of
   the whole bar. */
.nav-item-msgs { display: inline-flex; align-items: center; gap: 7px; }

/* THE TIGHT DESKTOP BAND, 768 to 860. Adding Messages made the signed-in bar one
   item wider, and at exactly 768 (the desktop breakpoint) that pushed
   .nav-actions past the viewport and gave the whole page a 21px horizontal
   scroll, 48px with the unread badge showing. Measured on the element rects, not
   on scrollWidth: .nav-actions ended at 789 on a 768 viewport.

   The padding is what gives: six items at 13px a side is 156px of air, and
   halving it buys back more than the overflow. Nothing is hidden and nothing is
   dropped, because a nav item somebody cannot see is the problem this whole
   change exists to fix. */
/* THE TIGHT DESKTOP BAND. The signed-in bar is seven items now (Shop, Account,
   Orders, Messages, COAs, Forum, Contact) plus the search, cart and avatar, and
   at the 768 breakpoint that pushed .nav-actions 121px past the viewport.
   Measured on element rects, not scrollWidth.

   Two bands, because the shortfall is not the same at both ends: 1024 was 25px
   over and only needs the padding back, 768 needs the type down as well. Nothing
   is hidden at either: a nav item somebody cannot see is the problem this whole
   change exists to fix. */
@media (min-width: 1200px) and (max-width: 1300px) {
    /* 1200 IS THE BAR'S OWN FLOOR NOW (the drawer takes everything under it),
       so this band covers 1200 to 1300 only. 1300, NOT 1180, SINCE THE 80px BAR. The taller bar's 48px logo and 30px
       padding cost 110px, and the signed-in bar without icons overflowed at
       1181 by 28px and at 1200 by 9px with the old ceiling; with a two-digit
       Messages badge on the bar it had 0px of slack at 1241. At 1301 the same
       bar with the badge has 60px, measured. The bar's padding and gap step
       back to their old 14 here as well, because the mockup's 30px is a width
       the bar only has from 1301 up. */
    .navbar { padding: 0 14px; gap: 14px; }
    .nav-item { padding: 0 6px; letter-spacing: .03em; }
    .nav-item-msgs { gap: 4px; }
    /* Sized for a two digit count, which is the widest this ever gets: the
       badge is a per-thread count and the list itself is the place for more. */
    .nav-item-badge { min-width: 15px; height: 15px; padding: 0 4px; font-size: 9.5px; }
}

/* The 768 to 1100 band (2px item padding, 10px type, 10px bar padding) and the
   768 to 1000 band that shrank the logo to 36px are gone: the bar renders from
   1200 up only, so neither matched anything, and the logo is 48px everywhere
   the bar shows. */
.nav-item-badge { margin-left: 0; min-width: 17px; height: 17px; padding: 0 5px;
                  border-radius: 9px; font-size: 10px; }

/* In the overlay the row IS a flex line with the icon and label, so auto margin
   is right here: the count goes to the trailing edge of the row. */
.mnav-badge { margin-left: auto; margin-right: 2px; flex: 0 0 auto; }

/* The notice on /contact for somebody who is already signed in. */
.ctv-signedin { margin-bottom: 18px; }

/* ===========================================================================
   QUOTES. Admin list, admin builder, and the customer's page.
   Built to docs/mockups/quote-system-mockup.html.

   The admin status chip is .ordv-pill with its existing five solid tones, so a
   quote reads the same colour language as an order. Nothing new is invented for
   it. Every table that can outgrow its container scrolls INSIDE its own box, so
   no screen here can give the page a horizontal scrollbar.
   ========================================================================= */

/* ---- Admin list ------------------------------------------------------- */

.qtv-list .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ---- Admin builder ---------------------------------------------------- */

.qtf-lines .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.qtf-pname { font-weight: 600; }
.qtf-pmeta { color: var(--muted-cool); font-size: 12px; margin-top: 1px; }
.qtf-qty  { width: 68px; padding: 6px 8px; border: 1px solid var(--line-cool); border-radius: 7px; font: inherit; text-align: right; }
.qtf-unit { width: 92px; padding: 6px 8px; border: 1px solid var(--line-cool); border-radius: 7px; font: inherit; text-align: right; }

.qtf-stk { font-size: 11.5px; font-weight: 650; white-space: nowrap; padding: 2px 8px; border-radius: 999px; }
.qtf-stk.is-ok  { background: var(--fill-28); color: var(--ink-35); }
.qtf-stk.is-low { background: var(--fill-22); color: var(--ink-18); }
.qtf-stk.is-out { background: var(--pill-red-bg); color: var(--ink-08); }
/* A LOOKUP THAT FAILED IS NOT ZERO STOCK, so it is not painted like zero stock.
   Pending is quiet grey; unknown is the striped amber that means "we do not know",
   deliberately different from the red that means "there are none". */
.qtf-stk.is-pending { background: var(--fill-42); color: var(--muted-cool); }
.qtf-stk.is-unknown { background: var(--fill-38); color: var(--ink-66); border: 1px dashed var(--edge-28); }
.qtf-results {
    position: absolute; left: 0; right: 0; z-index: 20; margin-top: 4px; max-height: 320px; overflow-y: auto;
    background: #fff; border: 1px solid #e4e4e8; border-radius: 10px; box-shadow: 0 10px 28px rgba(16,24,40,.12);
}
.qtf-res {
    display: flex; width: 100%; gap: 10px; align-items: center; flex-wrap: wrap;
    padding: 10px 12px; border: 0; border-bottom: 1px solid var(--edge-05);
    background: var(--surface); text-align: left; font: inherit; cursor: pointer;
}
.qtf-res:last-child { border-bottom: 0; }
.qtf-res:hover { background: var(--fill-02); }
.qtf-res-n { font-weight: 600; }
.qtf-res-s { color: var(--muted-cool); font-size: 12.5px; }
.qtf-res-p { margin-left: auto; font-weight: 650; }
.qtf-none.is-bad { color: var(--ink-08); }
.qtf-none.is-bad .mono { background: var(--pill-red-bg); padding: 1px 5px; border-radius: 4px; }

/* A hint that is reporting a failure rather than explaining a field. */
.vnd-hint.is-bad { color: var(--ink-08); }
.vnd-hint.is-bad .mono { background: var(--pill-red-bg); padding: 1px 5px; border-radius: 4px; }
.vnd-empty-p.is-bad { color: var(--ink-08); }
.vnd-empty-p.is-bad .mono { background: var(--pill-red-bg); padding: 1px 5px; border-radius: 4px; }
.qtf-tot .qtf-r.is-disc span:last-child { color: var(--ink-09); }
.qtf-tot .qtf-r.is-grand { border-top: 1px solid var(--line-cool); margin-top: 6px; padding-top: 9px; font-weight: 700; font-size: 15px; }
@media (max-width: 480px) {
        .qtf-res-p { margin-left: 0; }
}


/* ---- The customer's page ---------------------------------------------- */
.qp-page { padding: 26px 14px 64px; background: var(--fill-50); min-height: 60vh; }
.qp-doc {
    max-width: 760px; margin: 0 auto; background: var(--surface);
    border: 1px solid var(--edge-02); border-radius: 14px; overflow: hidden;
}
.qp-doc-narrow { padding: 26px 22px; }
.qp-brand { background: #12151c; padding: 20px; text-align: center; }
.qp-logo { color: #fff; font-weight: 800; letter-spacing: .18em; font-size: 15px; }
.qp-logo small { display: block; font-size: 9px; letter-spacing: .3em; font-weight: 600; color: var(--ink-85); margin-top: 3px; }
.qp-meta {
    display: flex; gap: 20px; flex-wrap: wrap; justify-content: space-between;
    padding: 18px 22px; border-bottom: 1px solid var(--edge-04);
}
.qp-meta dl { margin: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 14px; font-size: 13px; }
.qp-meta dt { color: var(--muted-cool); }
.qp-meta dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.qp-pill { display: inline-block; padding: 2px 9px; border-radius: 999px; font-size: 11.5px; font-weight: 650; }
.qp-pill.is-amber { background: var(--fill-22); color: var(--ink-18); }
.qp-pill.is-bad   { background: var(--pill-red-bg); color: var(--ink-08); }

.qp-body { padding: 18px 22px 22px; }
.qp-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.qp-lines { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.qp-lines th {
    text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em;
    color: var(--muted-cool); font-weight: 650; padding: 8px; border-bottom: 1px solid #1c2530; white-space: nowrap;
}
.qp-lines td { padding: 10px 8px; border-bottom: 1px solid var(--edge-44); vertical-align: top; }
.qp-lines .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.qp-pname { font-weight: 600; }
.qp-pmeta { color: var(--muted-cool); font-size: 12px; margin-top: 1px; }
.qp-tot { max-width: 300px; margin-left: auto; margin-top: 16px; }
.qp-r { display: flex; justify-content: space-between; gap: 18px; padding: 5px 0; font-size: 13.5px; }
.qp-r.is-disc span:last-child { color: var(--ink-35); }
.qp-r.is-grand { border-top: 1px solid #1c2530; margin-top: 6px; padding-top: 9px; font-weight: 700; font-size: 16px; }

.qp-foot { padding: 18px 22px; border-top: 1px solid var(--edge-04); background: var(--fill-21); }
.qp-actions { display: flex; gap: 9px; flex-wrap: wrap; align-items: center; }
.qp-acceptform { margin: 0; }
.qp-btn {
    display: inline-block; border: 1px solid var(--edge-02); background: var(--surface); color: var(--ink-14);
    border-radius: 9px; padding: 10px 16px; font-size: 13.5px; font-weight: 600;
    cursor: pointer; text-decoration: none; font-family: inherit;
}
.qp-btn:hover { background: var(--surface-2); text-decoration: none; }
.qp-btn.is-pri { background: #2E5BE0; border-color: #2E5BE0; color: #fff; }
.qp-btn.is-pri:hover { background: #1f43ad; }
.qp-btn.is-lnk { border-color: transparent; background: transparent; color: var(--muted-cool); }
.qp-note { margin: 12px 0 0; color: var(--muted-cool); font-size: 12.5px; line-height: 1.55; }
.qp-note-bad { color: var(--ink-08); }
.qp-rue { padding: 13px 22px; border-top: 1px solid var(--edge-04); font-size: 11.5px; color: var(--muted-cool); text-align: center; line-height: 1.5; }

@media (max-width: 480px) {
    .qp-page { padding: 14px 8px 48px; }
    .qp-meta, .qp-body, .qp-foot, .qp-rue { padding-left: 14px; padding-right: 14px; }
    .qp-tot { max-width: 100%; }
    .qp-actions .qp-btn, .qp-acceptform { width: 100%; }
    .qp-acceptform .qp-btn { width: 100%; }
}


/* ===========================================================================
   THE ACCOUNT LANDING PAGE, at /account. The order list moved to
   /account/orders and this is what took its place.
   ========================================================================= */
.acct-you, .acct-msgs, .acct-recent {
    background: var(--surface); border: 1px solid var(--border, var(--edge-02)); border-radius: 14px;
    padding: 18px 20px; margin-bottom: 16px;
}
.acct-you-dl { margin: 10px 0 0; display: grid; grid-template-columns: auto minmax(0, 1fr);
               gap: 7px 18px; font-size: 14px; }
.acct-you-dl dt { color: var(--muted, var(--muted-cool)); }
.acct-you-dl dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.acct-you-mail { font-family: var(--font-mono, ui-monospace, Menlo, monospace); font-size: 13px; }

.acct-msgs-top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.acct-more { font-size: 13px; font-weight: 600; text-decoration: none; }
.acct-msgs-badge { display: inline-block; margin-left: 7px; min-width: 19px; padding: 1px 6px;
                   border-radius: 10px; background: var(--pill-red); color: var(--pill-ink);
                   font-size: 11px; font-weight: 800; text-align: center; }
.acct-msgs-none { margin: 10px 0 12px; color: var(--muted, var(--muted-cool)); font-size: 14px; }
.acct-msgs-list, .acct-recent-list { list-style: none; margin: 12px 0; padding: 0; }
.acct-msgs-row + .acct-msgs-row, .acct-recent-row + .acct-recent-row {
    border-top: 1px solid var(--line-2, var(--edge-04)); }
.acct-msgs-row a { display: flex; justify-content: space-between; gap: 14px; align-items: baseline;
                   padding: 10px 0; text-decoration: none; }
.acct-msgs-subj { font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.acct-msgs-when { color: var(--muted, var(--muted-cool)); font-size: 12.5px; white-space: nowrap; flex: none; }
.acct-msgs-row.is-unread .acct-msgs-subj::before {
    content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 50%;
    background: var(--pill-red); margin-right: 7px; vertical-align: middle;
}

/* The recent-order strip. A row wraps rather than scrolling: four short values on
   a 360px screen fit on two lines and nothing has to be cut. */
.acct-recent-row a { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px;
                     padding: 10px 0 4px; text-decoration: none; }
.acct-recent-no { font-weight: 650; }
.acct-recent-when { color: var(--muted, var(--muted-cool)); font-size: 13px; }
.acct-recent-tot { margin-left: auto; font-weight: 650; white-space: nowrap; }
.acct-recent-pill { padding: 3px 10px; border-radius: 999px; font-size: 11.5px; font-weight: 650;
                    white-space: nowrap; color: var(--pill-ink); }
.acct-recent-pill.t-ok      { background: var(--pill-green); }
.acct-recent-pill.t-warn    { background: var(--pill-amber); color: var(--pill-ink-amber-b); }
.acct-recent-pill.t-info    { background: var(--accent-blue); color: var(--white); }
.acct-recent-pill.t-neutral { background: var(--pill-grey); }
.acct-recent-pill.t-bad     { background: var(--pill-red); }
.acct-recent-pill.t-part    { background: var(--pill-part); }
.acct-recent-pill.t-due     { background: #c2286f; }
.acct-recent-msg { display: inline-block; padding: 0 0 10px; font-size: 12.5px;
                   color: var(--muted, var(--muted-cool)); text-decoration: underline; }

/* The offline chat panel when the visitor is signed in: a sentence and a link
   where the name/email/message form would be. */
.pcd-chat-signedin { padding: 16px 18px; font-size: 13.5px; line-height: 1.55; }
.pcd-chat-signedin p { margin: 0 0 12px; }
.pcd-chat-signedin .pcd-chat-submit { display: block; text-align: center; text-decoration: none; }

@media (max-width: 420px) {
    .acct-you-dl { grid-template-columns: 1fr; gap: 2px; }
    .acct-you-dl dt { margin-top: 8px; }
    .acct-recent-tot { margin-left: 0; }
}


.nav-badge { margin-left: auto; min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px;
             background: var(--accent-blue, #2E5BE0); color: #fff; font-size: 11px; font-weight: 800;
             display: inline-flex; align-items: center; justify-content: center; }
.nav-avatar { position: relative; }
.nav-avatar-img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block; }
.nav-avatar-dot { position: absolute; top: -1px; right: -1px; width: 10px; height: 10px;
                  border-radius: 50%; background: var(--accent-blue, #2E5BE0);
                  box-shadow: 0 0 0 2px #fff; }

@media (max-width: 640px) {
    .sup-bub { max-width: 85%; }
    .sup-rmeta { display: none; }
    .sup-composer { gap: 8px; }
    .sup-cinput { min-width: 0; }
}


/* =============================================================================
   SUPPORT INBOX, admin side (light). Straight from the mockup's admin half.
   ========================================================================== */
.supa { max-width: 1000px; }
.supa-tab { position: relative; background: var(--surface); border: 0; font: inherit; font-size: 14px;
            font-weight: 600; padding: 8px 16px; border-radius: 18px; color: var(--ink);
            text-decoration: none; box-shadow: inset 0 0 0 1px rgba(20,30,60,.12); }
.supa-tab:hover { text-decoration: none; }
.supa-tab.is-on { background: #16181d; color: #fff; box-shadow: none; }
.supa-n { background: rgba(59,100,244,.12); color: var(--ink-56); font-size: 11px; font-weight: 800;
          border-radius: 9px; padding: 1px 7px; margin-left: 6px; }
.supa-tab.is-on .supa-n { background: rgba(255,255,255,.18); color: #fff; }

.supa-row { position: relative; display: flex; align-items: center; gap: 14px;
            background: var(--surface); border-radius: 12px; padding: 13px 18px; margin-bottom: 8px;
            text-decoration: none; color: var(--ink);
            box-shadow: inset 0 0 0 1px rgba(20,30,60,.12); }
.supa-row:hover { text-decoration: none; }
/* Unread: a tinted row, not a stripe down its left edge (house rule). */
.supa-row.is-unread { box-shadow: inset 0 0 0 1px rgba(59,100,244,.35); background: #f7f9ff; }
.supa-av.lg { width: 48px; height: 48px; font-size: 16px; }
.supa-row.is-unread .supa-prev { color: var(--ink); font-weight: 600; }

/* The desk's side of the same conversation. Mirror of the customer view: the
   viewer is on the right in blue, the other party on the left in neutral grey.
   See the note on .sup-conv for why the runs are grouped in PHP. */

.supa-lbl.is-mine { text-align: right; }
.supa-row-msg.is-first { margin-top: 0; }
.supa-row-msg.is-mine { justify-content: flex-end; }

.supa-bub {
    max-width: 66%;
    padding: 9px 14px;
    border-radius: 18px;
    font-size: 15px;
    line-height: 1.45;
    overflow-wrap: anywhere;
}
.supa-row-msg.is-them .supa-bub { background: var(--fill-06); color: var(--ink); }
.supa-row-msg.is-mine .supa-bub { background: var(--accent-blue, #2E5BE0); color: #fff; }
.supa-row-msg.is-them:not(.is-last) .supa-bub { border-bottom-left-radius: 6px; }
.supa-row-msg.is-them:not(.is-first) .supa-bub { border-top-left-radius: 6px; }
.supa-row-msg.is-mine:not(.is-last) .supa-bub { border-bottom-right-radius: 6px; }
.supa-row-msg.is-mine:not(.is-first) .supa-bub { border-top-right-radius: 6px; }
.supa-when.is-mine { text-align: right; }
/* A BUBBLE WITH A PHOTO IS A PHOTO-WIDTH COLUMN. Left to the normal cap the
   bubble is as wide as its longest text line, which either strands the
   thumbnail in a band of empty colour or, if the photo stretches to fill,
   blows a snapshot up to half the page. Narrowing the bubble instead wraps
   the words to the picture's width, which is the shape a caption wants. */
.supa-bub.has-photo { max-width: 320px; }
.supa-bub.is-photo-only { padding: 4px; border-radius: 16px; }
.supa-bub.is-photo-only .supa-photo { margin-top: 0; }
.supa-bub.is-photo-only .supa-photo img { border-radius: 13px; }

/* POLISH ONLY, no behaviour change: the same three controls in the same order,
   given the bubble radius language so the composer belongs to the thread above
   it rather than sitting under it as a form. */


@media (max-width: 780px) {
    .supa-bub { max-width: 85%; }
}


/* =========================================================================
   THE FORUM
   Built to docs/mockups/pcd-forum-mockup.html. Every selector starts with f-,
   which is what keeps this section from ever colliding with a storefront rule.

   THE PALETTE IS THE MOCKUP'S, declared here rather than reusing the
   storefront tokens: the forum is a darker, denser surface than the shop and
   the mockup's panel gradients are the approved look. Where a value matches a
   storefront token it is still written out, because the two are allowed to
   drift and a shared token would drag one when the other moved.
   ========================================================================= */
.f-body {
    /* THE FORUM'S THREE STATES. Run 4 made the forum a themed surface; run 5
       decided which of its two palettes a visitor who has chosen nothing sees.

       THIS BLOCK IS THE ORIGINAL, and it is the palette the forum shipped with,
       character for character. No cookie means no attribute on <html>, no
       attribute means neither block below matches, and the forum renders the
       dark room it has always rendered.

       :root[data-theme="light"] .f-body   an explicit light choice
       :root[data-theme="dark"]  .f-body   an explicit dark choice, same values
                                           as here, restated so the explicit
                                           state does not depend on this block
                                           staying what it is.

       THE SHAPE DID NOT CHANGE, in run 4 or in run 5. Every f- rule below reads
       one of these twenty-five names, so the three states are three value sets
       and not one selector moved. */
    --f-bg: #070b14;
    --f-bgtop: #0d1730;
    --f-panel: #0f1524;
    --f-panel2: #131a2c;
    --f-ink: #e9edf6;
    --f-dim: #9aa6bc;
    --f-faint: #5c6880;
    --f-blue: #3b64f4;
    --f-bluehi: #6f92ff;
    --f-green: #3ddc97;
    --f-amber: #f5b64e;
    --f-violet: #8f7df0;
    --f-teal: #39c6d8;
    --f-hair: rgba(255, 255, 255, .07);
    --f-red: #ff7a7a;
    --f-rowline: rgba(255, 255, 255, .05);
    --f-rowhover: rgba(255, 255, 255, .02);
    --f-soft-a: rgba(255, 255, 255, .06);
    --f-soft-b: rgba(255, 255, 255, .03);
    --f-soft-c: rgba(255, 255, 255, .05);
    --f-fieldbg: #0a0f1c;
    --f-fieldring: rgba(255, 255, 255, .1);
    --f-strong: #fff;
    --f-alert-err: #ffdcdc;
    --f-alert-ok: #cdeede;
    background: radial-gradient(1100px 600px at 70% -10%, var(--f-bgtop) 0%, var(--f-bg) 55%);
    min-height: 100vh;
}
/* AND THE LIGHT HALF, run 5. It used to be the block above, which is to say it
   used to be what a visitor who had never touched the toggle saw. The forum's
   original is dark, so the bare block carries the palette the forum shipped with
   and these values are reached only by an explicit choice. Every value here is
   run 4's, unchanged: this is a move, not a redesign. */
:root[data-theme="light"] .f-body {
    --f-bg: #fafafa;
    --f-bgtop: #ffffff;
    --f-panel: #ffffff;
    --f-panel2: #f7f8fa;
    --f-ink: #141414;
    --f-dim: #3a3a3a;
    --f-faint: #6b6b6b;
    --f-blue: #3b64f4;
    --f-bluehi: #2e5be0;
    --f-green: #12703f;
    --f-amber: #8a5a00;
    --f-violet: #5b46c9;
    --f-teal: #0e7490;
    --f-hair: #e4e4e4;
    --f-red: #b42318;
    --f-rowline: #e4e4e4;
    --f-rowhover: #fafafa;
    --f-soft-a: #eef0f4;
    --f-soft-b: #eef0f4;
    --f-soft-c: #eef0f4;
    --f-fieldbg: #ffffff;
    --f-fieldring: rgba(16, 24, 40, .18);
    --f-strong: #141414;
    --f-alert-err: #b42318;
    --f-alert-ok: #12703f;
}

/* TWO CLASSES IN THE SELECTOR ON PURPOSE. Forum pages carry catalog-dark so the
   site topbar renders its dark variant, and body.catalog-dark also sets its own
   background, at the same specificity as a bare .f-body. This wins and keeps the
   forum's gradient. */
body.f-body { background: radial-gradient(1100px 600px at 70% -10%, var(--f-bgtop) 0%, var(--f-bg) 55%); }

/* INK ON THE SHELL, NOT THE BODY. --f-ink used to be the body's colour, which the
   site topbar's mobile overlay inherits: it would have read #e9edf6 here against
   #c7cbd2 everywhere else, so the forum's bar would not have been the same bar.
   Every forum view puts its content inside .f-shell, so setting it here covers
   the same ground and stops at the nav. */
.f-shell { color: var(--f-ink); }
/* width:100% IS LOAD-BEARING, for the same reason it is on .faq-page above.
   body.app is a flex column, so this <main> is a flex item, and `margin: 0 auto`
   on the cross axis suppresses the default stretch: the item sizes to its own
   content and the max-width never becomes the actual width. The home page
   happened to look right because its .f-layout grid declares a 300px sidebar
   track and forced the width from the inside; category and thread had no such
   grid and measured 670px at 1440, 1920 and 2560 alike, so the 1180 cap was
   decorative. Declaring the width makes it fill first and the cap then caps. */
.f-shell { width: 100%; max-width: 1180px; margin: 0 auto; padding: 0 20px 90px; }
/* LINK COLOUR ON THE SHELL, NOT THE BODY, for the same reason --f-ink moved: a
   descendant selector off the body repaints EVERY anchor on the page, and that
   included the site topbar's. Its items rendered #6f92ff here against white
   everywhere else, so the forum's bar was not the same bar. It also reached the
   site footer, which now renders on forum pages exactly as it does elsewhere.
   Scoped to .f-shell, which is what every forum view wraps its content in. */
.f-shell a { color: var(--f-bluehi); text-decoration: none; }
.f-shell a:hover { text-decoration: underline; }
.f-muted { color: var(--f-faint); font-size: .84rem; }

/* ---- Flashes ----------------------------------------------------------- */
/* THE STOREFRONT .alert IS A LIGHT-THEME COMPONENT. Both variants set
   `background: var(--white)` and neither declares a colour, so on a forum page
   the text inherited --f-ink (#e9edf6) and rendered near-white on white: the
   message was present, correct, and invisible, which is worse than no message
   at all. The username validation notice is the one people actually hit.

   Scoped to .f-body so the storefront and admin alerts are untouched, and it
   overrides background AND colour together because fixing only one of them
   just moves which half is unreadable. */
.f-body .alert {
    background: var(--f-panel2);
    border: 1px solid var(--f-hair);
    border-left: 3px solid var(--f-dim);
    border-radius: 10px;
    color: var(--f-ink);
    font-size: .9rem;
    line-height: 1.45;
    padding: 12px 16px;
    margin: 0 0 18px;
}
.f-body .alert-error   { border-left-color: var(--f-red);   color: var(--f-alert-err); }
.f-body .alert-success { border-left-color: var(--f-green); color: var(--f-alert-ok); }
.f-body .alert a { color: inherit; text-decoration: underline; }

/* ---- Masthead ---------------------------------------------------------- */
/* The forum's own controls, in a slim strip directly under the site topbar. The
   brand block that used to open this row is gone: the logo lives in the topbar
   now, so .f-brand / .f-brand-logo / .f-logo went with it. */
.f-top { display: flex; align-items: center; gap: 18px; padding: 18px 0 14px; flex-wrap: wrap; }
.f-sep { color: var(--f-faint); }   /* still used by .f-crumbs below */
.f-mark { font-weight: 800; color: var(--f-dim); font-size: .8rem; letter-spacing: .18em; text-transform: uppercase; }
.f-search { flex: 1; max-width: 360px; margin-left: auto; min-width: 160px; }
.f-search input {
    width: 100%; background: var(--f-panel); border: 0; border-radius: 10px; padding: 10px 14px;
    color: var(--f-ink); font-size: .88rem; font-family: inherit;
    box-shadow: inset 0 0 0 1px var(--f-hair);
}
.f-search input::placeholder { color: var(--f-faint); }
.f-search input:focus { outline: 2px solid var(--f-blue); outline-offset: 1px; }
.f-new {
    background: var(--f-blue); color: #fff; font-weight: 700; font-size: .9rem; padding: 10px 18px;
    border: 0; border-radius: 9px; cursor: pointer; font-family: inherit; flex: none;
}
/* WHITE LABEL, AND THE SELECTOR HAS TO EARN IT. .f-new is an <a>, so the forum's
   link colour (.f-shell a, one class + one type) outranked a bare .f-new and the
   label rendered #6f92ff on the #3b64f4 fill: 1.66:1, unreadable, and it had been
   that way since the button was built. .f-shell .f-new matches the link rule's
   specificity and wins on order, so the label is #fff on #3b64f4 = 4.87:1, which
   clears AA for the 14.4px bold text it carries. */
.f-shell .f-new, .f-shell .f-new:visited { color: #fff; }
.f-new:hover { background: #2b50cf; color: #fff; text-decoration: none; }
.f-signin { color: var(--f-dim); font-size: .88rem; }
.f-chip { display: flex; align-items: center; }
.f-av {
    width: 32px; height: 32px; border-radius: 50%; flex: none; object-fit: cover;
    background: linear-gradient(135deg, #3b64f4, #8f7df0);
    display: inline-flex; align-items: center; justify-content: center;
    font-weight: 800; font-size: .78rem; color: #fff;
}
.f-av.sm { width: 30px; height: 30px; font-size: .7rem; }
.f-crumbs { color: var(--f-faint); font-size: .8rem; margin-bottom: 10px; display: flex; gap: 7px; flex-wrap: wrap; }

/* ---- Layout ------------------------------------------------------------ */
/* minmax(0, 1fr), NOT 1fr. A bare 1fr track takes its automatic minimum from its
   content's min-content, so this column refused to shrink below the widest row it
   contained and pushed the 300px rail past the grid: .f-layout measured 1140 wide
   with a 1222 scrollWidth even on the launch copy, which only stayed invisible
   because the centred shell had slack either side. The longer category
   descriptions took it to 1327 and it reached the viewport edge. Zeroing the
   automatic minimum makes the track obey its share, and .f-cdesc already
   ellipsises, so nothing is lost, the descriptions just truncate as designed. */
.f-layout { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 22px; align-items: start; }
.f-group { margin-bottom: 22px; }
/* line-height is pinned rather than inherited because .f-rail's offset below is
   derived from this box's height. 1.1 is what it already computed to, so nothing
   moves; it just stops the rail drifting if the inherited value ever changes. */
.f-group-h { font-size: .78rem; line-height: 1.1; font-weight: 800; letter-spacing: .16em;
             text-transform: uppercase; color: var(--f-dim); padding: 0 4px 8px; margin: 0; }

/* ---- Category cards ---------------------------------------------------- */
.f-cat { background: linear-gradient(165deg, var(--f-panel2), var(--f-panel)); border-radius: 14px;
         overflow: hidden; position: relative; }
.f-cat::after { content: ""; position: absolute; inset: 0; border-radius: 14px;
                box-shadow: inset 0 0 0 1px var(--f-hair); pointer-events: none; }
/* .f-body a wins on specificity, so the row anchors have to say ink again or
   every category name and thread title renders as link blue. The mockup has
   them as ordinary text in a clickable row, which is what a list of rooms
   should read as: the row is the link, not the words. */
.f-body .f-crow, .f-body .f-trow { color: var(--f-ink); }
.f-crow { display: flex; align-items: center; gap: 16px; padding: 15px 18px;
          border-bottom: 1px solid var(--f-rowline); }
.f-crow:last-child { border-bottom: 0; }
.f-crow:hover { background: var(--f-rowhover); text-decoration: none; }
.f-cicon { width: 38px; height: 38px; border-radius: 10px; display: flex; align-items: center;
           justify-content: center; flex: none; font-size: 1.05rem; }
.f-cicon.lg { width: 46px; height: 46px; font-size: 1.3rem; }
.f-cicon.hue-blue   { background: rgba(59, 100, 244, .14); color: var(--f-bluehi); }
.f-cicon.hue-green  { background: rgba(61, 220, 151, .12); color: var(--f-green); }
.f-cicon.hue-amber  { background: rgba(245, 182, 78, .13); color: var(--f-amber); }
.f-cicon.hue-violet { background: rgba(143, 125, 240, .14); color: var(--f-violet); }
.f-cicon.hue-teal   { background: rgba(57, 198, 216, .13); color: var(--f-teal); }
.f-cmain { min-width: 0; flex: 1; display: block; }
.f-cname { font-weight: 700; font-size: 1rem; display: block; }
.f-lock { font-size: .68rem; font-weight: 800; color: var(--f-amber); background: rgba(245, 182, 78, .12);
          border-radius: 8px; padding: 2px 8px; margin-left: 8px; vertical-align: 2px; letter-spacing: .04em; }
.f-lock-shut { color: var(--f-faint); background: var(--f-soft-a); }
.f-cdesc { color: var(--f-faint); font-size: .82rem; display: block;
           white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The mobile summary line. Hidden on desktop, where the stats and last-post
   columns say the same thing with more room. */
.f-cmob { display: none; color: var(--f-faint); font-size: .8rem; }
.f-cstats { display: flex; gap: 18px; flex: none; color: var(--f-dim); font-size: .8rem; text-align: center; }
.f-cstats b { display: block; color: var(--f-ink); font-size: .95rem; }
.f-clast { flex: none; width: 230px; display: flex; gap: 10px; align-items: center; min-width: 0; }
.f-clast-t { min-width: 0; display: block; }
.f-clast-title { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
                 font-size: .86rem; color: var(--f-bluehi); }
.f-clast-meta { color: var(--f-faint); font-size: .75rem; display: block; }

/* ---- Rail -------------------------------------------------------------- */
.f-rail { display: flex; flex-direction: column; gap: 18px; }

/* RAIL ALIGNMENT. The two columns already start on the same line: .f-layout is
   align-items:start and both grid items measure the same top. What looked
   staggered is that the two columns label themselves differently. The left column
   puts its section label ABOVE its card (.f-group-h, a 22px box: 13.73px line +
   8px padding), so its first .f-cat starts 22px down; the rail puts its label
   INSIDE the card (.f-railhead), so its first .f-railbox starts at 0. The cards,
   which are what the eye lines up, were 22px out.
   Offsetting the rail by that same box makes the first card of each column start
   on the same line. Derived from the heading's own values, not a magic 22px.
   .has-grouphead is set by the view and only when the first group actually
   renders a label: a group with an empty name draws no heading, and then there is
   nothing to offset against. Single column below 1000px, where the rail sits under
   the list and any offset would just be a stray gap. */
@media (min-width: 1001px) {
    .f-layout.has-grouphead > .f-rail { padding-top: calc(.78rem * 1.1 + 8px); }
}

.f-railbox { background: linear-gradient(165deg, var(--f-panel2), var(--f-panel)); border-radius: 14px;
             padding: 16px 18px; position: relative; }
.f-railbox::after { content: ""; position: absolute; inset: 0; border-radius: 14px;
                    box-shadow: inset 0 0 0 1px var(--f-hair); pointer-events: none; }
.f-railhead { font-size: .78rem; font-weight: 800; letter-spacing: .16em; text-transform: uppercase;
              color: var(--f-dim); margin: 0 0 12px; }
.f-trend { display: flex; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--f-rowline); }
.f-trend:last-child { border-bottom: 0; }
.f-trend-n { color: var(--f-faint); font: 700 .8rem/1.4 ui-monospace, Menlo, monospace; flex: none; padding-top: 2px; }
.f-trend-t { min-width: 0; }
.f-trend-t a { font-size: .88rem; line-height: 1.35; display: block; }
.f-trend-t span { color: var(--f-faint); font-size: .74rem; }
.f-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; text-align: center; }
.f-stats div { background: var(--f-soft-b); border-radius: 10px; padding: 10px 6px; }
.f-stats b { display: block; font-size: 1.05rem; }
.f-stats span { color: var(--f-faint); font-size: .72rem; }
.f-rules-short { color: var(--f-dim); font-size: .84rem; line-height: 1.6; margin: 0; }

/* ---- Research policy strip (every forum page) -------------------------- */
/* Soft blue tint panel from the approved mockup, same 14px radius and inset
   hairline the category and rail cards use so it reads as part of the set. */
.f-policy {
    display: flex; gap: 10px; align-items: flex-start;
    background: linear-gradient(165deg, rgba(59, 100, 244, .12), rgba(59, 100, 244, .05));
    border-radius: 14px; padding: 14px 18px; margin: 0 0 20px;
    position: relative; font-size: .9rem; line-height: 1.55; color: var(--f-dim);
}
.f-policy::after {
    content: ""; position: absolute; inset: 0; border-radius: 14px;
    box-shadow: inset 0 0 0 1px rgba(111, 146, 255, .3); pointer-events: none;
}
.f-policy-ico { flex: none; font-size: 1rem; line-height: 1.45; }
.f-policy-t { min-width: 0; }
.f-policy b { color: var(--f-strong); font-weight: 700; }

/* ---- Seeded-handle notice (only for a member the backfill named) -------- */
/* Amber rather than the policy strip's blue, and directly under it: two notices
   in the same colour read as one block and the second gets skipped. Same 14px
   radius and inset hairline as everything else in the set, so it is a different
   voice and not a different design. */
.f-handle-note {
    display: flex; gap: 10px; align-items: flex-start;
    background: linear-gradient(165deg, rgba(245, 182, 78, .13), rgba(245, 182, 78, .05));
    border-radius: 14px; padding: 14px 18px; margin: 0 0 20px;
    position: relative; font-size: .9rem; line-height: 1.55; color: var(--f-dim);
}
.f-handle-note::after {
    content: ""; position: absolute; inset: 0; border-radius: 14px;
    box-shadow: inset 0 0 0 1px rgba(245, 182, 78, .32); pointer-events: none;
}
.f-handle-ico { flex: none; font-size: 1rem; line-height: 1.45; }
.f-handle-t { min-width: 0; flex: 1 1 auto; }
.f-handle-note b { color: var(--f-strong); font-weight: 700; }
/* The handle itself in mono: it is a name to be copied exactly, and the digits
   in it are the part somebody has to read back. */
.f-handle-name {
    color: var(--f-amber); font-weight: 700;
    font-family: ui-monospace, Menlo, monospace; font-size: .92em;
}
.f-handle-x { flex: none; margin: -2px -4px 0 0; }
.f-handle-x button {
    background: none; border: 0; cursor: pointer; padding: 0 6px;
    color: var(--f-faint); font-size: 1.15rem; line-height: 1.2;
}
.f-handle-x button:hover { color: var(--f-ink); }
.f-handle-x button:focus-visible { outline: 2px solid var(--f-amber); outline-offset: 2px; border-radius: 4px; }
@media (max-width: 520px) {
    .f-handle-note { padding: 12px 14px; font-size: .86rem; }
}


/* ---- DO / DON'T ruleset (rail card + rules page) ----------------------- */
.f-rules-list { list-style: none; margin: 0; padding: 0; }
.f-rules-list li {
    margin: 0 0 9px; padding: 0 0 0 2px;
    font-size: .85rem; line-height: 1.5; color: var(--f-dim);
}
.f-rules-list li:last-child { margin-bottom: 0; }
.f-rules-list strong { color: var(--f-ink); font-weight: 700; }
/* The lead words carry the whole scan-read of this list, so they are the only
   colour in it: green for what belongs here, red for what gets removed. */
.f-ok { color: var(--f-green); font-weight: 800; }
.f-no { color: var(--f-red);   font-weight: 800; }
.f-enforce { color: var(--f-faint); font-size: .8rem; line-height: 1.55; margin: 12px 0 0; }
/* On the rules page the summary is followed by the numbered long version, and the
   enforcement line ran straight into the first heading. The rule closes the
   summary and opens the detail, the same separator the mockup uses for its note.
   Scoped to .f-card so the rail card, where this line is last, is untouched. */
.f-card .f-enforce {
    margin-bottom: 26px; padding-bottom: 18px; border-bottom: 1px solid var(--f-hair);
}

/* ---- Category page ----------------------------------------------------- */
.f-cathead { display: flex; align-items: center; gap: 16px; margin: 4px 0 18px; flex-wrap: wrap; }
.f-h1 { font-size: 1.5rem; font-weight: 800; margin: 0; }
.f-cathead .f-new { margin-left: auto; }
.f-cathead .f-cdesc { white-space: normal; }
.f-empty { background: linear-gradient(165deg, var(--f-panel2), var(--f-panel)); border-radius: 14px;
           padding: 40px 24px; text-align: center; color: var(--f-dim); }
.f-empty h2 { font-size: 1.05rem; margin: 0 0 6px; color: var(--f-ink); }

.f-tlist { background: linear-gradient(165deg, var(--f-panel2), var(--f-panel)); border-radius: 14px;
           overflow: hidden; position: relative; }
.f-tlist::after { content: ""; position: absolute; inset: 0; border-radius: 14px;
                  box-shadow: inset 0 0 0 1px var(--f-hair); pointer-events: none; }
.f-trow { display: flex; align-items: center; gap: 14px; padding: 14px 18px;
          border-bottom: 1px solid var(--f-rowline); }
.f-trow:last-child { border-bottom: 0; }
.f-trow:hover { background: var(--f-rowhover); text-decoration: none; }
/* Unread carries a blue left edge, the same signal the support inbox uses. */
.f-trow.is-unread { box-shadow: inset 3px 0 0 var(--f-blue); }
.f-trow.is-unread .f-ttitle { font-weight: 700; }
.f-tmain { min-width: 0; flex: 1; }
.f-ttitle { display: block; font-size: .95rem; }
.f-tmeta { display: block; color: var(--f-faint); font-size: .76rem; }
.f-tstats { display: flex; gap: 16px; flex: none; color: var(--f-dim); font-size: .78rem; text-align: center; }
.f-tstats b { display: block; color: var(--f-ink); font-size: .9rem; }
.f-tlast { flex: none; width: 120px; text-align: right; }
.f-pin, .f-tlock { margin-right: 4px; }

.f-pager { display: flex; gap: 6px; flex-wrap: wrap; margin: 18px 0 0; }
.f-page { padding: 7px 12px; border-radius: 8px; background: var(--f-panel); color: var(--f-dim);
          font-size: .85rem; box-shadow: inset 0 0 0 1px var(--f-hair); }
.f-page.is-on { background: var(--f-blue); color: #fff; }

/* ---- Thread view ------------------------------------------------------- */
.f-tv-title { font-size: 1.35rem; font-weight: 800; margin: 6px 0 2px; }
.f-tv-meta { color: var(--f-faint); font-size: .82rem; margin: 0 0 16px; }
.f-pinned { color: var(--f-amber); font-weight: 700; }
.f-lockedtag { color: var(--f-dim); font-weight: 700; }

.f-post { background: linear-gradient(165deg, var(--f-panel2), var(--f-panel)); border-radius: 14px;
          margin-bottom: 14px; position: relative; overflow: hidden; }
.f-post::after { content: ""; position: absolute; inset: 0; border-radius: 14px;
                 box-shadow: inset 0 0 0 1px var(--f-hair); pointer-events: none; }
/* A held post reads as provisional to its author: the amber edge is the same
   language the MEMBERS pill uses for "this is gated". */
.f-post.is-held { box-shadow: inset 3px 0 0 var(--f-amber); }
.f-post.is-deleted { opacity: .55; box-shadow: inset 3px 0 0 #e0565b; }
.f-phead { display: flex; align-items: center; gap: 12px; padding: 12px 18px;
           border-bottom: 1px solid var(--f-rowline); }
.f-who { min-width: 0; }
.f-who b { font-size: .92rem; display: block; font-weight: 700; }
.f-who b a { color: var(--f-ink); }
.f-who span { color: var(--f-faint); font-size: .74rem; }
.f-role { font-size: .64rem; font-weight: 800; letter-spacing: .05em; border-radius: 8px;
          padding: 2px 8px; margin-left: 8px; vertical-align: 1px; }
.f-role.admin { background: rgba(59, 100, 244, .18); color: var(--f-bluehi); }
.f-role.mod   { background: rgba(61, 220, 151, .14); color: var(--f-green); }
.f-when { margin-left: auto; color: var(--f-faint); font-size: .76rem; flex: none; }
.f-pbody { padding: 16px 18px; font-size: .94rem; line-height: 1.65; overflow-wrap: anywhere; }
.f-pbody p { margin: 0 0 10px; }
.f-pbody p:last-child { margin-bottom: 0; }
.f-pbody blockquote { border-left: 3px solid var(--f-blue); background: rgba(59, 100, 244, .07);
                      padding: 8px 14px; border-radius: 6px; margin: 0 0 10px; color: var(--f-dim);
                      font-size: .88rem; }
.f-pimg { display: block; padding: 0 18px 14px; }
.f-pimg img { max-width: 100%; height: auto; border-radius: 10px; display: block; }
.f-heldnote { margin: 0; padding: 10px 18px 0; color: var(--f-amber); font-size: .8rem; }
.f-edited { margin: 0; padding: 0 18px; color: var(--f-faint); font-size: .74rem; }
.f-pfoot { display: flex; gap: 16px; padding: 10px 18px 12px; color: var(--f-faint); font-size: .78rem; flex-wrap: wrap; }
.f-pfoot a { color: var(--f-faint); }
.f-pfoot a:hover { color: var(--f-dim); }
.f-modlink { margin-left: auto; }

/* ---- Composer ---------------------------------------------------------- */
.f-reply { background: var(--f-panel); border-radius: 14px; padding: 14px 18px; position: relative; }
.f-reply::after { content: ""; position: absolute; inset: 0; border-radius: 14px;
                  box-shadow: inset 0 0 0 1px var(--f-hair); pointer-events: none; }
.f-fld { width: 100%; background: var(--f-fieldbg); border: 0; border-radius: 10px; min-height: 76px;
         padding: 11px 14px; color: var(--f-ink); font-size: .9rem; font-family: inherit;
         box-shadow: inset 0 0 0 1px var(--f-fieldring); resize: vertical; }
.f-fld::placeholder { color: var(--f-faint); }
.f-fld:focus { outline: 2px solid var(--f-blue); outline-offset: 1px; }
.f-reply-foot { display: flex; align-items: center; gap: 12px; margin-top: 10px; flex-wrap: wrap; }
.f-reply-foot .f-new { margin-left: auto; }
.f-attach { width: 40px; height: 40px; border-radius: 10px; background: var(--f-soft-c);
            display: flex; align-items: center; justify-content: center; cursor: pointer; flex: none; }
.f-file { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.f-fname { color: var(--f-faint); font-size: .78rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.f-signedout { color: var(--f-dim); font-size: .9rem; background: var(--f-panel); border-radius: 14px;
               padding: 16px 18px; box-shadow: inset 0 0 0 1px var(--f-hair); }

/* ---- Tablet: the rail drops under the list ----------------------------- */
@media (max-width: 1000px) {
    .f-layout { grid-template-columns: 1fr; }
    .f-clast { width: 180px; }
}


/* ---- Phone: the mockup's third view -----------------------------------
   The stats and the last-post column go, the one-line summary takes their
   place, and the masthead loses the search field so brand, New and avatar fit
   one row at 360. */
@media (max-width: 860px) {
    .f-shell { padding: 0 14px 70px; }
    .f-crow { padding: 12px 10px; gap: 12px; }
    .f-cstats, .f-clast, .f-tstats, .f-tlast { display: none; }
    .f-cdesc { display: none; }
    .f-cmob { display: block; }
    .f-cname { font-size: .92rem; }
    .f-search { order: 4; max-width: none; flex-basis: 100%; margin-left: 0; }
    .f-new { padding: 8px 14px; font-size: .82rem; }
    /* THE MASTHEAD IS ONE WRAPPING ROW and the brand now carries the real logo
       instead of the 30px hex mark it used to. At full size the logo is 113px
       wide, which pushed "Sign in" onto a line of its own and grew the header by
       40px. These three values buy that row back: brand + New thread + Sign in
       measure 174 + 102 + 45 against 362px of usable width. */
    .f-top { gap: 14px; }
    .f-mark { font-size: .74rem; letter-spacing: .14em; }
    .f-tv-title { font-size: 1.15rem; }
    .f-phead { padding: 10px 14px; }
    .f-pbody { padding: 14px; }
    .f-pfoot { padding: 8px 14px 12px; gap: 12px; }
    .f-cathead .f-new { margin-left: 0; }
}


/* ---- Forum: forms, cards, profile (phase 2) ---------------------------- */
.f-narrow { max-width: 760px; }
.f-card { background: linear-gradient(165deg, var(--f-panel2), var(--f-panel)); border-radius: 14px;
          padding: 24px 26px; position: relative; margin-bottom: 18px; }
.f-card::after { content: ""; position: absolute; inset: 0; border-radius: 14px;
                 box-shadow: inset 0 0 0 1px var(--f-hair); pointer-events: none; }
.f-lede { color: var(--f-dim); font-size: .9rem; line-height: 1.6; margin: 6px 0 18px; }
.f-label { display: block; font-size: .78rem; font-weight: 700; letter-spacing: .06em;
           text-transform: uppercase; color: var(--f-dim); margin: 16px 0 6px; }
.f-input { width: 100%; background: var(--f-fieldbg); border: 0; border-radius: 10px; padding: 11px 14px;
           color: var(--f-ink); font-size: .95rem; font-family: inherit;
           box-shadow: inset 0 0 0 1px var(--f-fieldring); }
.f-input::placeholder { color: var(--f-faint); }
.f-input:focus { outline: 2px solid var(--f-blue); outline-offset: 1px; }
select.f-input { appearance: none; background-image:
    linear-gradient(45deg, transparent 50%, var(--f-dim) 50%),
    linear-gradient(135deg, var(--f-dim) 50%, transparent 50%);
    background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
    background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; padding-right: 34px; }
.f-ta { min-height: 180px; resize: vertical; line-height: 1.6; }
.f-hint { color: var(--f-faint); font-size: .78rem; margin: 6px 0 0; }
.f-check { display: flex; align-items: center; gap: 10px; margin: 16px 0 0;
           color: var(--f-dim); font-size: .88rem; cursor: pointer; }
.f-check input { accent-color: var(--f-blue); }
.f-wide { width: 100%; margin-top: 20px; display: block; text-align: center; }
.f-rule { margin: 0 0 20px; }
.f-rule h2 { font-size: 1rem; font-weight: 700; margin: 0 0 6px; }
.f-rule p { color: var(--f-dim); font-size: .9rem; line-height: 1.65; margin: 0; }
.f-accept { border-top: 1px solid var(--f-hair); padding-top: 18px; margin-top: 6px; }
.f-quotebox { border-left: 3px solid var(--f-blue); background: rgba(59, 100, 244, .07);
              padding: 10px 14px; border-radius: 6px; margin: 0 0 16px; color: var(--f-dim); font-size: .88rem; }
.f-quotebox p { margin: 0; }
.f-profile { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.f-av.xl { width: 64px; height: 64px; font-size: 1.3rem; }
.f-recent { padding: 12px 0; border-bottom: 1px solid var(--f-rowline); }
.f-recent:last-child { border-bottom: 0; }
.f-recent a { font-size: .92rem; }
.f-recent span { color: var(--f-faint); font-size: .74rem; margin-left: 8px; }
.f-recent p { color: var(--f-dim); font-size: .84rem; margin: 4px 0 0; line-height: 1.5; }
/* The honeypot FormGuard renders. Off-screen rather than display:none, which
   some bots treat as a signal to skip the field. */
.f-body .fg-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ---- Forum admin: the wide inline rows the rooms editor needs ---------- */

.admin-pager .is-on { background: var(--accent-blue); color: #fff; border-color: var(--accent-blue); }









/* =========================================================================
   SUBSCRIBE & SAVE  (product page chooser, account manager, admin list)

   Three surfaces, one feature, built to docs/mockups/loyalty-subscribe-mockups.html
   sections 4 and 5. Nothing here invents a colour: the product chooser reuses the
   dark offer panel the BAC bundle row already uses on that column, the account
   manager reuses the dark card the PCD Credit section uses on that page, and the
   admin list adds only its column set to the vnd-* system.
   ========================================================================= */

/* ---- 1. Product page: the one-time / subscribe chooser ------------------ */
.pd-sub { margin: 16px 0 0; display: flex; flex-direction: column; gap: 9px; }

/* Same dark panel as .pd-bac-row, and for the same reason: this column is dark
   from the price down, and a white card here would cut it in half. */
.pd-sub-opt {
    display: flex; align-items: flex-start; gap: 11px; cursor: pointer;
    /* The positioning context for the SAVE ribbon below. No ancestor between here
       and the ribbon is positioned, and nothing on this column clips, so the
       ribbon can hang above the card's top edge without being cut off. */
    position: relative;
    background: #1c1c1c; border-radius: 11px; padding: 13px 14px;
    transition: box-shadow .12s ease;
}
.pd-sub-opt:has(input:checked) { box-shadow: inset 0 0 0 2px #2E5BE0; }
.pd-sub-opt:has(input:focus-visible) { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
/* A guest sees the offer and cannot take it. Dimmed, not hidden: the price is the
   reason to sign in, so it has to be legible. */
.pd-sub-opt.is-guest { cursor: default; }
.pd-sub-opt input[type="radio"] {
    flex: 0 0 auto; width: 18px; height: 18px; margin: 2px 0 0;
    accent-color: #2E5BE0; cursor: pointer;
}
.pd-sub-opt.is-guest input[type="radio"] { cursor: not-allowed; opacity: .45; }

.pd-sub-body { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.pd-sub-name { font-size: 15px; font-weight: 600; color: #fff; }
/* The one-time price is a plain figure; the subscribe price is the offer, so it
   gets the green pill the mockup gives it. */
.pd-sub-was  { font-size: 14px; color: var(--ink-30); }
.pd-sub-pill {
    font-size: 13px; font-weight: 700; color: var(--ink-06);
    background: rgba(61, 220, 151, .14); border-radius: 9px; padding: 3px 9px;
}
.pd-sub-every { flex: 1 1 100%; display: flex; align-items: center; gap: 8px; font-size: 13.5px; color: #c8ced6; }
.pd-sub-int {
    background: #0f0f0f; color: #fff; border: 1px solid rgba(255, 255, 255, .14);
    border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 13.5px; cursor: pointer;
}
.pd-sub-int:disabled { opacity: .5; cursor: not-allowed; }
.pd-sub-int:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
/* The honest paragraph. Small and quiet, but never hidden behind a disclosure:
   "nothing is charged automatically" is the deal, not the small print. */
.pd-sub-how { flex: 1 1 100%; font-size: 12.5px; line-height: 1.55; color: var(--ink-30); }
.pd-sub-signin { color: var(--ink-17); font-weight: 600; }
.pd-sub-signin:hover { color: var(--ink-43); }

/* ---- The SAVE ribbon (approved mockup, variant C) -----------------------
   Two behaviours, one element, split at 768px.

   DESKTOP: out of flow and hung off the card's top-left corner. Because it is
   absolutely positioned it takes no part in the body's flex layout, so nothing
   below it moves and selecting or deselecting the card shifts nothing (the
   selection ring is an INSET box-shadow, which costs no space either).

   The card carries the clearance rather than the ribbon carrying a negative
   margin: 16px on top of the flex column's 9px gap leaves 25px between the two
   cards, of which the ribbon eats 11px, so it never touches the one-time card
   above it however that card grows.

   left is 14px, not the mockup's 18px, and that is the faithful translation
   rather than a drift: the mockup set left to its own card's padding-left so the
   ribbon sits flush with the card's left edge inset, and this card's padding is
   14px. Copying the number instead of the intent would put it 4px out. */
.pd-sub-opt.is-sub { margin-top: 16px; }
.pd-sub-ribbon {
    position: absolute; top: -11px; left: 14px;
    flex: 0 0 auto;
    background: #19b877; color: var(--ink-46);
    font-size: 11px; font-weight: 800; line-height: 1.25; letter-spacing: .06em;
    border-radius: 8px; padding: 4px 12px; white-space: nowrap;
}
/* Inert on desktop: the ribbon is out of flow, so there is no line to break. */
.pd-sub-break { display: none; }

@media (max-width: 768px) {
    /* MOBILE: no overhang, no absolute positioning. The ribbon drops back into the
       card's own flex flow as a plain badge on the first line, ABOVE the radio, so
       there is nothing to clip against, nothing to overlap, and the card no longer
       needs clearance above it. The card wraps only at this width; above it the
       ribbon is out of flow and the row is the two items it always was. */
    /* row-gap goes to 0 and the ribbon carries its own 11px instead. The break
       below occupies a flex line of its own, so leaving the card's 11px row-gap in
       place would put TWO gaps between the badge and the radio. A negative margin
       on the break does not fix that (a flex line's cross size clamps at zero), so
       the spacing is moved onto the one element that should own it. column-gap is
       untouched: the radio and the text column still sit 11px apart. */
    .pd-sub-opt.is-sub { margin-top: 0; flex-wrap: wrap; row-gap: 0; }
    .pd-sub-ribbon { position: static; margin-bottom: 11px; }
    /* The break that gives the badge its own line. */
    .pd-sub-break { display: block; flex: 0 0 100%; height: 0; }
}


/* ---- 2. Account page: the subscription manager -------------------------- */
.acct-subs { margin-top: 24px; border-radius: 14px; background: var(--surface-dark); color: #fff; padding: 22px 24px; }
.acct-subs-k {
    margin: 0 0 12px; font-size: 11.5px; font-weight: 800; letter-spacing: 0.14em;
    text-transform: uppercase; color: var(--muted-3);
}
.acct-sub { padding: 12px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.07); }
.acct-sub:last-of-type { border-bottom: 0; }
.acct-sub-top, .acct-sub-bot { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.acct-sub-bot { margin-top: 6px; }
.acct-sub-what { font-size: 14.5px; color: #fff; min-width: 0; overflow-wrap: anywhere; }
.acct-sub-sep { color: var(--muted-2); padding: 0 2px; }
.acct-sub-price { color: var(--ink-06); font-weight: 700; }
.acct-sub-qty { color: var(--muted-2); font-size: 13px; }
.acct-sub-next { margin: 0; font-size: 13px; color: var(--muted-3); }
.acct-sub-next b { color: #fff; font-weight: 600; }

/* The controls are links in the mockup and stay links here: they sit inside their
   own tiny forms so each one is a real POST with a CSRF token, and a <button> that
   looks like a link is the honest way to say "this does something". */
.acct-sub-acts { display: flex; align-items: center; gap: 14px; flex: 0 0 auto; }
.acct-sub-f { margin: 0; }
.acct-sub-a {
    background: none; border: 0; padding: 0; font: inherit; font-size: 13.5px;
    cursor: pointer; text-decoration: none;
}
.acct-sub-a.is-go  { color: var(--ink-17); font-weight: 600; }
.acct-sub-a.is-go:hover { color: var(--ink-43); text-decoration: underline; }
.acct-sub-a.is-off { color: var(--muted-2); }
.acct-sub-a.is-off:hover { color: var(--ink-105); text-decoration: underline; }
.acct-sub-a:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 3px; border-radius: 3px; }
/* A paused row is still a row, not a warning. Dimmed by a hair, and the pill says it. */
.acct-sub.is-paused .acct-sub-what { color: var(--muted-3); }
.acct-subs-note { margin: 12px 0 0; font-size: 12.5px; line-height: 1.55; color: var(--muted-2); }

@media (max-width: 640px) {
    .acct-sub-top, .acct-sub-bot { flex-direction: column; align-items: flex-start; gap: 8px; }
    .acct-sub-acts { gap: 18px; }
}




/* The real radio is hidden, not removed: it stays focusable and keyboard-
   operable, and the card below shows the focus ring on its behalf. */
.camp-audcard input[type="radio"] { position: absolute; opacity: 0; width: 0; height: 0; }
.camp-audcard {
    position: relative; display: block; cursor: pointer;
    border-radius: 12px; padding: 13px 15px; background: var(--surface);
    box-shadow: inset 0 0 0 1px rgba(16, 24, 40, .12);
    transition: box-shadow .12s ease, background .12s ease;
}
.camp-audcard:hover { box-shadow: inset 0 0 0 1px rgba(16, 24, 40, .28); }
.camp-audcard.is-on { box-shadow: inset 0 0 0 2px var(--accent-blue); background: rgba(59, 100, 244, .05); }
.camp-audcard:focus-within { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
.camp-audcard b { display: block; font-size: 14.5px; color: var(--ink); }
.camp-audcard span { display: block; margin-top: 2px; font-size: 12.5px; color: var(--muted); }
/* Pushed to its own line under the buttons at every width: it is a sentence, not
   a control, and squeezing it beside three buttons is what made it unreadable. */

.camp-actions .btn[disabled] { opacity: .5; cursor: not-allowed; }



/* ---- The public unsubscribe page ---------------------------------------- */
/* ITS OWN SHELL, not the storefront one: views/unsubscribe.php renders outside
   partials/head.php so the page is not behind the age gate, which means it also
   has no .page wrapper, no topbar and no footer to inherit spacing from. These
   few rules are the whole layout. Storefront tokens (--ink, --muted, --border)
   still apply, because the same stylesheet is linked. */
.unsub-body { background: var(--off-white); }
.unsub-page {
    /* svh second so iOS Safari does not resize the page when the address bar
       collapses; the vh line is the fallback for engines without svh. */
    min-height: 100vh;
    min-height: 100svh;
    display: flex; align-items: center; justify-content: center;
    padding: 32px 20px;
}
.unsub-card { width: 100%; max-width: 560px; }
.unsub-logo { display: inline-block; margin-bottom: 26px; }
.unsub-logo-img { height: 34px; width: auto; display: block; }
.unsub-eyebrow {
    margin: 0 0 6px; font-size: 11.5px; font-weight: 700;
    letter-spacing: .14em; text-transform: uppercase; color: var(--muted);
}
.unsub-h1 { margin: 0; font-size: clamp(26px, 4vw, 34px); font-weight: 800; letter-spacing: -0.02em; color: var(--ink); }
.unsub-lede { margin: 12px 0 0; font-size: 15.5px; line-height: 1.6; color: var(--muted); }
.unsub-lede strong { color: var(--ink); }
.unsub-fine { margin: 26px 0 0; font-size: 12px; line-height: 1.6; color: var(--muted-2); }

/* A real panel, not a white-alpha wash: the page is light, and the sentence that
   has to stand out would otherwise read as ordinary paragraph text. */
.unsub-note {
    margin: 22px 0; padding: 16px 18px; border-radius: 12px;
    background: var(--fill-44); border: 1px solid var(--border);
}
.unsub-note p { margin: 0; font-size: 14px; line-height: 1.65; }
.unsub-form { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; margin-top: 6px; }
.unsub-back { font-size: 14px; }

/* =========================================================================
   FIRST RESPONDER
   Storefront and admin, together, because the badge is one element rendered by
   one helper in five places and splitting its rules by side is how five
   slightly different badges start.

   SOLID RED SURFACE, BLACK BADGE, to the approved mockup
   docs/mockups/responder-red-solid-v3.html. A verified account is not a
   footnote on a row, it is the whole row: the surface goes red (#d92c2f), the
   name goes white, the email goes pale red, the avatar goes deep red, and the
   badge sits on top of it in near-black with white text.

   WHY THE BADGE IS BLACK RATHER THAN RED. It has to read as a distinct object
   ON the red surface. A red badge on a red card is invisible, and a white one
   competes with the name. Black is the only value with enough separation from
   #d92c2f to stay legible without out-shouting the name beside it, and it
   matches the black .usrv-chip already in that row.

   RED HERE IS NOT AN ERROR STATE, which is the one risk this palette carries.
   It is used because the mockup calls for the loudest possible "this account is
   different, do not price it normally", and it is always accompanied by the
   words First Responder so it can never be read as a failure on its own.

   THE THREE VALUES ARE STATED ONCE, as custom properties, because four
   different screens paint the same treatment and the mockup gives one set of
   hexes for all of them.
   ========================================================================= */
:root {
    --rsp-red:      #d92c2f;   /* the card surface */
    --rsp-red-deep: #8f1416;   /* the avatar on that surface */
    --rsp-red-pale: #ffd4d4;   /* secondary text on that surface */
    --rsp-ink:      #0c0e13;   /* the badge */
}

/* The shield, wherever it appears on its own (the cart/checkout money line).
   Storefront money lines keep the discount green they inherit from
   .discount-row; the red treatment is about identifying an ACCOUNT, not about
   marking a saving. */
.rsp-ico {
    width: 14px; height: 14px; flex: none;
    vertical-align: -2px; margin-right: 6px;
    color: var(--link);
}

/* THE BADGE. Black pill, white text, 20px shield, per the mockup. */
.rsp-badge {
    display: inline-flex; align-items: center; gap: 8px;
    vertical-align: middle;
    padding: 7px 14px;
    border: 0;
    border-radius: 999px;
    background: var(--rsp-ink);
    color: #fff;
    font-size: 12.5px; font-weight: 700; letter-spacing: .01em;
    line-height: 1.25;
    /* Never wraps mid-badge: it drops to its own line whole (see the mobile
       block at the end of this section) rather than breaking across two. */
    white-space: nowrap; flex: none;
}
/* 22px, the approved size (docs/mockups/responder-badge-image.html panel 1).
   border-radius is belt and braces: the source is already a circle on a
   transparent ground, so this only matters if the asset is ever replaced with a
   square one. */
.rsp-badge-ico { width: 22px; height: 22px; flex: none; border-radius: 50%; }
.rsp-badge-t   { display: inline-block; }
/* The header variant, beside an <h1> rather than inside a table cell. */

.rsp-badge-lg .rsp-badge-ico { width: 22px; height: 22px; }

/* ---- THE RED ROW: admin users list ---------------------------------------
   One hook (.is-responder on the <tr>) paints the whole row. */
.ordv-table tbody tr.is-responder,
.ordv-table tbody tr.is-responder:hover { background: var(--rsp-red); }
.ordv-table tbody tr.is-responder td { border-bottom-color: rgba(0,0,0,.14); }

/* The inline-editable name is an <input>, so it needs the colour set on the
   control itself: a white <tr> rule does not reach into a form field. It keeps
   its transparent background until hovered/focused, where it takes a translucent
   white so the edit affordance still reads on red. */
tr.is-responder .usrv-nameinput {
    color: #fff; background: transparent; border-color: transparent;
}
tr.is-responder .usrv-nameinput::placeholder { color: rgba(255,255,255,.72); }
tr.is-responder .usrv-nameinput:hover  { border-color: rgba(255,255,255,.45); }
tr.is-responder .usrv-nameinput:focus  {
    background: rgba(255,255,255,.16); border-color: var(--edge-213);
    box-shadow: 0 0 0 3px rgba(255,255,255,.25); color: #fff;
}
tr.is-responder .usrv-email,
tr.is-responder .usrv-email .cell-clip { color: var(--rsp-red-pale); }
tr.is-responder .usrv-avatar { background: var(--rsp-red-deep); color: #fff; }
/* Everything else in the row that was grey ink on white. */
tr.is-responder td,
tr.is-responder .ordv-muted,
tr.is-responder .uname-state { color: #fff; }
/* The "You" chip and the row-actions button, which were tinted for white. */
tr.is-responder .usrv-you {
    color: #fff; background: rgba(0,0,0,.28);
}
/* The typo warning triangle, which is amber on white and vanishes on red. */
tr.is-responder .email-typo-flag { color: #ffe9a8; }


/* Pushes the badge to the far end on a wide band, and lets it drop below on a
   narrow one because the band is flex-wrap. */
.ucrd-rsp-band .rsp-badge { margin-left: auto; }

/* Admin order view, customer panel. */

.ordc-rsp-card .rsp-badge { margin-left: auto; }

/* The customer's own account card. */
.acct-rsp-card {
    background: var(--rsp-red); color: #fff;
    border-radius: 14px; padding: 16px 18px; margin: 14px 0 0;
    display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
}
.acct-rsp-card .acct-rsp-t { font-weight: 800; font-size: 15px; margin: 0; }
.acct-rsp-card .acct-rsp-s { margin: 2px 0 0; font-size: 13px; color: var(--rsp-red-pale); }
.acct-rsp-card > div { min-width: 0; }

/* THE MARK ON THE CUSTOMER'S OWN CARD. It stands alone at the leading edge and
   the black pill is gone, so there is no .acct-rsp-card .rsp-badge rule here any
   more; there is no pill on this card to push right.

   A 13px .rsp-badge-lg .rsp-badge-ico rule also lived on this line. It was a
   second definition of a selector already set to 22px 76 lines above, so it won
   on source order and made the HEADER badge's icon smaller than the table one it
   is supposed to outrank. Removed rather than reconciled: 22px is the approved
   size and it is already stated once. */
.acct-rsp-mark { flex: none; border-radius: 50%; }
.acct-rsp-when { white-space: nowrap; }

/* The customer's own account page. */
.acct-rsp { margin: 8px 0 0; }

/* ---- The money line on the cart and at checkout ----------------------------
   .discount-row already paints this row green; the shield inherits the row's
   colour rather than fighting it, so the line stays one colour and the icon
   just says which discount it is. */
.responder-row .rsp-ico { color: currentColor; }

/* The "which discount applied" sentence at checkout. The cart has its own
   .cart-summary-note; this is that rule for the checkout summary, which had no
   note style of its own. */
.summary-note {
    margin: 8px 0 0;
    font-size: 12.5px; line-height: 1.5;
    color: var(--muted);
}

/* ---- The admin grant panel (user credit page) --------------------------- */

.rsp-panel.is-on .rsp-panel-ico { color: var(--ink-55); }

.rsp-form { margin-top: 14px; }



/* ---- MOBILE: the badge gets its own line -------------------------------
   780px, NOT 768px, and the four pixels matter. 780 is where .usrv-table
   already turns each row into a stacked card (see the users-view block
   above). Setting this to 768 would leave a 780-769 window where the row is
   stacked but the badge is still trying to sit inline beside the name, which
   is exactly the overflow this rule exists to prevent.

   flex-basis:100% is what drops it: .usrv-nwrap wraps, the badge takes a full
   line of its own under the name, and because .rsp-badge is white-space:nowrap
   it moves as one piece instead of breaking across two lines. The email above
   it keeps its own ellipsis, so a long address truncates rather than pushing
   the card wider than the screen. */
@media (max-width: 780px) {
    .usrv-nwrap .rsp-badge {
        flex: 0 0 100%;
        margin-top: 10px;
        /* Own line, but still only as wide as its own text. */
        width: -moz-fit-content; width: fit-content;
        font-size: 11.5px; padding: 6px 12px;
    }
    .usrv-nwrap .rsp-badge-ico { width: 17px; height: 17px; }
    /* The name column is the full width of the card at this size, so the email
       has a real box to truncate inside. */
    
    tr.is-responder .usrv-email { min-width: 0; }
    tr.is-responder .usrv-email .cell-clip {
        display: block; min-width: 0;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
}


@media (max-width: 600px) {

    /* The customer-facing cards stack their badge under the words too. */
    .acct-rsp-card, .ordc-rsp-card, .ucrd-rsp-band { align-items: flex-start; }
}


/* =========================================================================
   AFFILIATE APPLICATIONS (admin)
   The list reuses the shared .ordv shell wholesale, so these are only the few
   things that shell has no rule for: the applicant cell, the untrusted outbound
   link, the decided-by stamp, and the wider row menu the note field needs.
   ========================================================================= */
.affv-name { font-weight: 600; }
.affv-empty { text-align: center; padding: 28px 10px; color: var(--muted); }

/* An applicant-supplied URL. Clipped rather than wrapped: some are very long and
   a wrapping URL pushes every other column out of shape. */
.affv-link {
    display: inline-block; max-width: 230px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    vertical-align: bottom;
    font-size: 11.5px; color: var(--link);
}
.affv-decided { display: block; margin-top: 4px; font-size: 11px; line-height: 1.5; color: var(--muted); }

/* Wider than the default row menu because it holds a text field, not just links. */
.affv-menu { min-width: 268px; gap: 2px; }

.affv-promo { margin-top: 6px; border-top: 1px solid var(--edge-41); padding-top: 6px; }
.affv-promo > summary { cursor: pointer; font-size: 12px; color: var(--muted); padding: 4px 8px; }
.affv-promo p { margin: 4px 8px 6px; font-size: 12.5px; line-height: 1.6; }

/* Shared row-menu extras: a label and a static note inside a menu that until now
   only ever held links and buttons. */
.rowmenu-lab {
    display: block; padding: 6px 8px 3px;
    font-size: 11px; font-weight: 600; color: var(--muted);
}
.rowmenu-note {
    margin: 0; padding: 6px 8px 8px;
    font-size: 12px; line-height: 1.55; color: var(--muted);
}
.affv-menu .input { width: calc(100% - 16px); margin: 0 8px 6px; font-size: 13px; }

@media (max-width: 700px) {
    .affv-link { max-width: 100%; }
    .affv-menu { min-width: 0; width: 100%; }
}




/* =========================================================================
   THE USER DRAWER — admin Users
   docs/mockups/user-drawer-redesign.html. Replaces the per-row menu, which
   stacked fourteen controls into a 236px dropdown with no room to say what
   any of them did.

   THE HEADER HAS TWO SKINS. Dark navy is the default, matching the admin
   sidebar; a verified first responder gets the red one, so the drawer agrees
   with the red row it was opened from and staff never have to check twice.
   ========================================================================= */

@keyframes usrd-fade { from { opacity: 0; } to { opacity: 1; } }

.usrd-panel {
    position: absolute; top: 0; right: 0; bottom: 0;
    width: 560px; max-width: 100%;
    background: var(--surface);
    box-shadow: -16px 0 40px rgba(10, 14, 22, .18);
    display: flex; flex-direction: column;
    animation: usrd-slide .18s cubic-bezier(.2, .7, .3, 1);
    outline: none;
}
@keyframes usrd-slide { from { transform: translateX(24px); opacity: .6; } to { transform: none; opacity: 1; } }

/* Stops the list behind the drawer scrolling under the finger on mobile. */
body.usrd-open { overflow: hidden; }

/* ---- Header ------------------------------------------------------------- */

.usrd-head.is-responder { background: var(--rsp-red); }
.usrd-head.is-responder .usrd-av { background: var(--rsp-red-deep); }
.usrd-who .mono {
    display: block; font-size: 12.5px; color: rgba(255,255,255,.62);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.usrd-head.is-responder .usrd-who .mono { color: var(--rsp-red-pale); }

/* THE MARK IN THE RED BAND, on its own, per the mockup's panel 3. It replaced a
   black "First Responder" pill: at 44px the mark says it without the words, and
   the header is already painted responder red, so the pill was saying the same
   thing a third time.

   THE AUTO MARGIN IS ON THE WRAPPER, NOT ON THE IMAGE. [data-usrd-badge] is the
   flex item; the mark is built inside it by users-drawer.js. margin-left:auto on
   the image does nothing, because its parent is an inline span rather than the
   flex container, which is why the old pill also sat against the email address
   instead of against the right edge.

   margin-right clears the close button. .usrd-x is positioned at right:14px and
   is about 28px wide, so it reaches 42px in from the edge while the header's own
   padding stops at 26px; without the extra 20px the mark would sit under it. */

.usrd-mark { width: 44px; height: 44px; flex: none; border-radius: 50%; }

/* ---- Stat strip: each figure is a link to the screen behind it ----------- */

.usrd-stat {
    background: var(--surface); padding: 14px 10px; text-align: center;
    text-decoration: none; color: inherit;
}
.usrd-stat:hover { background: var(--fill-19); }
.usrd-stat b {
    display: block; font-weight: 800; font-size: 16px; line-height: 1.2;
    font-family: ui-monospace, Menlo, monospace; color: var(--ink-36);
}
.usrd-stat span {
    color: var(--muted-blue); font-size: 10.5px; text-transform: uppercase; letter-spacing: .08em;
}

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

.usrd-sec { margin-bottom: 22px; }
.usrd-sec:last-child { margin-bottom: 0; }
.usrd-sec h3 {
    font-size: 11px; font-weight: 800; letter-spacing: .12em;
    text-transform: uppercase; color: var(--muted-blue); margin: 0 0 10px;
}

.usrd-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.usrd-f { display: block; min-width: 0; }
.usrd-f > span { display: block; font-size: 11.5px; color: var(--muted-blue); margin-bottom: 4px; }
.usrd-f input, .usrd-f select {
    width: 100%; padding: 10px 12px; border-radius: 9px; border: 0;
    font: inherit; font-size: 13.5px; background: var(--surface); color: var(--ink-02);
    box-shadow: inset 0 0 0 1px rgba(16, 24, 40, .14);
}
.usrd-f input:focus, .usrd-f select:focus {
    outline: none; box-shadow: inset 0 0 0 1px #2E5BE0, 0 0 0 3px rgba(46,91,224,.12);
}
.usrd-f select[disabled] { background: var(--fill-15); color: var(--muted-grey); }
.usrd-note { margin: 8px 0 0; font-size: 12px; color: var(--muted-grey); }
.usrd-save { margin-top: 12px; }
.usrd-save .usrd-abtn { width: 100%; }

/* ---- Buttons: forms and links share one look ---------------------------- */
.usrd-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.usrd-aform { margin: 0; display: contents; }
.usrd-abtn {
    display: block; width: 100%;
    padding: 12px 14px; border: 0; border-radius: 10px;
    font: inherit; font-weight: 700; font-size: 13px; text-align: center;
    cursor: pointer; background: var(--surface); color: var(--ink-02); text-decoration: none;
    box-shadow: inset 0 0 0 1px rgba(16, 24, 40, .16);
}
.usrd-abtn:hover { background: var(--fill-18); text-decoration: none; }
/* The line under Send account invite. Spans the two-column action grid, because
   it describes the button above it and not the one beside it. */
.usrd-anote {
    grid-column: 1 / -1; margin: -2px 0 0;
    font-size: 12px; color: var(--muted-grey); line-height: 1.5;
}
.usrd-abtn.blue { background: #3b64f4; color: #fff; box-shadow: none; }
.usrd-abtn.blue:hover { background: #2f56dd; }
.usrd-abtn.red  { background: var(--fill-03); color: var(--ink-21); box-shadow: none; }
.usrd-abtn.red:hover { background: var(--fill-59); }

/* Rows that put a field and its button side by side (password, tax, photo). */
.usrd-inline { margin: 0; display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }
.usrd-inline .usrd-abtn { width: auto; flex: 0 0 auto; }
.usrd-grow { flex: 1 1 160px; }
.usrd-photo { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.usrd-file { font-size: 12.5px; max-width: 100%; }


/* ---- Mobile: the same drawer, full width ------------------------------- */
@media (max-width: 780px) {
    .usrd-panel { width: 100%; box-shadow: none; }

    /* The pill used to drop to a line of its own here because it was as wide as a
       sentence. The mark is 44px and fits beside the name at any width, so it
       keeps its place on the right and only loses most of the close-button
       offset, the header having stopped being one line. */
    
    .usrd-fields, .usrd-actions { grid-template-columns: 1fr; }
    .usrd-inline { flex-direction: column; align-items: stretch; }
    .usrd-inline .usrd-abtn { width: 100%; }
}


/* ===========================================================================
   ORDER HISTORY IN THE USER DRAWER.
   Built to docs/mockups/customer-order-history-mockup.html.

   NO LEFT ACCENT BORDERS anywhere in here, and no new pill colours: the status
   chip is the admin's existing .ordv-pill with its existing five solid tones, so
   an order reads the same colour in this drawer as it does in the orders table.

   The drawer is 560px wide and the mockup's grid was drawn for a 1120px page, so
   the collapsed row goes to the mockup's own narrow layout early rather than
   being squeezed: number and total on the first line, date, items and the pill
   under them. That is the same rearrangement, at the width this container
   actually has.
   ========================================================================= */
.usrd-olist { border: 1px solid var(--line-cool); border-radius: 10px; overflow: hidden; }
.usrd-orow { border-bottom: 1px solid var(--edge-05); }
.usrd-orow:last-child { border-bottom: 0; }

.usrd-osum {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    grid-template-areas: "num pill total" "date pill total" "items pill total";
    gap: 2px 10px; align-items: center;
    width: 100%; padding: 11px 12px; border: 0; cursor: pointer;
    background: var(--surface); text-align: left; font: inherit; color: inherit;
}
.usrd-osum:hover { background: var(--fill-02); }
.usrd-orow.is-open .usrd-osum { background: var(--fill-02); }
.usrd-osum:focus-visible { outline: 2px solid #2E5BE0; outline-offset: -2px; }
.usrd-onum   { grid-area: num;   font-weight: 650; font-variant-numeric: tabular-nums; }
.usrd-odate  { grid-area: date;  color: var(--ink-38); font-size: 13px; }
.usrd-oitems { grid-area: items; color: var(--muted-cool); font-size: 12.5px; }
.usrd-osum .ordv-pill { grid-area: pill; justify-self: end; }
.usrd-ototal { grid-area: total; justify-self: end; font-weight: 650;
    font-variant-numeric: tabular-nums; white-space: nowrap; }
.usrd-ochev  { display: none; }

/* ---- the expanded half ---------------------------------------------- */
.usrd-odet { display: none; padding: 2px 12px 14px; background: var(--fill-02); border-top: 1px solid var(--edge-05); }
.usrd-orow.is-open .usrd-odet { display: block; }

/* The line table can be wider than a 360px drawer no matter how it is set, so it
   scrolls INSIDE its own box. Measured on the element rect, not the page: the
   drawer itself must never gain a horizontal scrollbar. */
.usrd-otwrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.usrd-lines { width: 100%; border-collapse: collapse; margin: 10px 0 0; font-size: 13px; }
.usrd-lines th {
    text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em;
    color: var(--muted-cool); font-weight: 650; padding: 7px 7px; border-bottom: 1px solid var(--line-cool);
    white-space: nowrap;
}
.usrd-lines td { padding: 8px 7px; border-bottom: 1px solid var(--edge-05); vertical-align: top; }
.usrd-lines tr:last-child td { border-bottom: 0; }
.usrd-lines .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.usrd-pname { font-weight: 600; }
.usrd-pmeta { color: var(--muted-cool); font-size: 12px; margin-top: 1px; }
.usrd-batch { font-family: var(--font-mono, ui-monospace, Menlo, monospace); font-size: 12px; color: var(--muted-cool); white-space: nowrap; }

.usrd-ofoot { display: flex; gap: 18px; justify-content: space-between; flex-wrap: wrap; margin-top: 12px; }
.usrd-ometa { flex: 1 1 240px; min-width: 0; }
.usrd-ometa dl { margin: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 5px 12px; font-size: 13px; }
.usrd-ometa dt { color: var(--muted-cool); }
.usrd-ometa dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.usrd-ometa dd .ordv-pill { vertical-align: middle; }
.usrd-otot { flex: 0 1 220px; min-width: 0; }
.usrd-or { display: flex; justify-content: space-between; gap: 16px; padding: 4px 0; font-size: 13px; }
.usrd-or span:last-child { font-variant-numeric: tabular-nums; white-space: nowrap; }
.usrd-or.is-disc span:last-child { color: var(--ink-09); }
.usrd-or.is-grand { border-top: 1px solid var(--line-cool); margin-top: 5px; padding-top: 8px; font-weight: 700; font-size: 14.5px; }

/* The three per-order actions sit ON ONE LINE like the mockup, which means
   overriding .usrd-abtn's full-width block: that rule is for the account actions
   in the two-column grid below, where full width is right. Here they are three
   small buttons under a detail panel. */
.usrd-oact { margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; }
.usrd-oact .usrd-abtn {
    display: inline-block; width: auto;
    padding: 7px 12px; border-radius: 8px; font-weight: 550; font-size: 12.5px;
}
.usrd-none { margin: 0; padding: 12px 2px; color: var(--muted-cool); font-size: 13px; }
.usrd-guestnote {
    margin: 10px 0 0; padding: 10px 12px; border: 1px solid var(--edge-09); background: var(--fill-72);
    border-radius: 8px; color: var(--ink-18); font-size: 12.5px; line-height: 1.5;
}
.usrd-guestnote a { color: var(--ink-18); text-decoration: underline; }

/* The narrowest phones. The batch column is the one thing that can be dropped
   without losing a fact the row cannot show elsewhere: it is repeated on the
   packing slip and on the order screen, both one tap away. */
@media (max-width: 420px) {
    .usrd-lines th:nth-child(2), .usrd-lines td:nth-child(2) { display: none; }
    .usrd-otot { flex: 1 1 100%; }
    .usrd-oact .usrd-abtn { width: 100%; text-align: center; }
}


/* ===========================================================================
   ADMIN: REVIEW QUEUES  (.revc / .revh)
   docs/mockups/affiliates-review-redesign.html
   docs/mockups/responder-application-mockup.html (panel 3)

   ONE CARD LANGUAGE FOR EVERY QUEUE. Affiliates and the responder verification
   queue both ask the operator the same question, so they are rendered by the
   same classes rather than by two lookalike sets that drift apart on the next
   change. A third queue (wholesale) can use these as they stand.

   A PENDING APPLICATION IS A CARD, A DECIDED ONE IS A ROW. That split is the
   whole layout: work is worth vertical space, history is not, and burying two
   decisions under twenty records is the failure the old table had.
   =========================================================================== */

/* ---- Tabs. Pills that carry their own counts, so no separate stat bar. ---- */

.revc-tab.is-on { background: #12151c; color: #fff; }
.revc-tab.is-on i { background: rgba(255, 255, 255, .18); color: #fff; }

/* The navy header is the one dark element on a light screen, which is what makes
   a queue of cards scannable: the eye lands on who is asking before anything else. */

.revc-pill.pend { background: #f5b64e; color: var(--ink-58); }





/* Approved rows lead with the mark; rejected rows lead with an empty box the
   same size, so the names stay in one column down the list. */

.revh-dec.ok { color: var(--ink-57); }
.revh-dec.no { color: var(--ink-21); }






/* ===========================================================================
   STOREFRONT: FIRST RESPONDER PAGE + APPLICATION  (/first-responders)
   docs/mockups/responder-application-mockup.html
   docs/mockups/responder-form-states.html  (authoritative for the form)

   Namespaced under .rsp-* alongside the badge and shield that already live in
   this file. Every colour is drawn against catalog-dark, the same ground
   /wholesale and /affiliates use.
   =========================================================================== */
.rsp-inner { max-width: 820px; }
.rsp-narrow { max-width: 680px; }

/* The tinted 54px rounded box is gone. It existed to give a 30px line glyph
   enough presence to open a page with; the mark is a solid roundel and carries
   itself, so it is drawn at the 54px the box used to occupy. The hero's rhythm is
   unchanged: same 54px of height, same 16px under it. line-height:0 keeps that
   16px exact by stopping the inline image from adding descender space. */
.rsp-mark { margin: 0 0 16px; line-height: 0; }
.rsp-mark-ico { width: 54px; height: 54px; border-radius: 50%; }

.rsp-h1 { margin-bottom: 10px; }
/* The rate is the promise, so it is the one coloured word in the headline.
   nowrap because "10% off" is one idea and one phrase.

   NAMED rsp-rate AND NOT rsp-pct. .rsp-pct was already taken, by the percentage
   OVERRIDE INPUT on the admin users screen: it carries width:92px and
   padding-right:26px so the % sign can sit inside the box. Both live in this one
   stylesheet, so reusing the name put 26px of padding after "10% off" and the
   headline rendered as "10% off , always." The storefront and the admin share a
   namespace here; check before adding to it. */
.rsp-rate { color: var(--ink-06); font-weight: 800; white-space: nowrap; }
.rsp-lede { max-width: 560px; }

.rsp-steps {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
    margin: 24px 0; padding: 0; list-style: none;
}
.rsp-step {
    background: linear-gradient(165deg, #131a2c, #0f1524);
    border-radius: 12px; padding: 16px; position: relative;
}
.rsp-step::after {
    content: ""; position: absolute; inset: 0; border-radius: 12px;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .07); pointer-events: none;
}
.rsp-step b { display: block; margin: 6px 0; color: #e9edf6; }
.rsp-step > span:last-child { color: var(--muted-steel); font-size: .82rem; line-height: 1.5; display: block; }
.rsp-n {
    width: 24px; height: 24px; border-radius: 50%; background: #3b64f4; color: #fff;
    font-weight: 800; font-size: .78rem; display: flex; align-items: center; justify-content: center;
}

/* The promise, in the green that means "this is the good news" on this site. */
.rsp-privacy {
    display: flex; gap: 10px; align-items: flex-start;
    background: rgba(61, 220, 151, .07); border-radius: 12px;
    padding: 14px 16px; margin: 0 0 22px;
    font-size: .86rem; line-height: 1.6; color: #bfe9d6; position: relative;
}
.rsp-privacy::after {
    content: ""; position: absolute; inset: 0; border-radius: 12px;
    box-shadow: inset 0 0 0 1px rgba(61, 220, 151, .25); pointer-events: none;
}
.rsp-privacy b { color: #fff; }

.rsp-already { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; color: var(--ink-12); margin: 0 0 16px; }
.rsp-signin-note { color: var(--muted-steel); font-size: .84rem; margin: 10px 0 0; }

/* ---- The application form ------------------------------------------------ */
.rsp-h2 { font-size: 1.25rem; margin-bottom: 4px; color: #e9edf6; }
.rsp-sub { color: var(--muted-steel); font-size: .86rem; margin: 0 0 18px; }
.rsp-submit-note { margin: 8px 0 0; }

.rsp-label { font-size: .8rem; font-weight: 700; color: var(--ink-12); margin: 14px 0 6px; display: block; }
.rsp-label-top { margin-top: 0; }
.rsp-in {
    width: 100%; background: #0a0f1c; border: 0; border-radius: 10px;
    padding: 12px 14px; color: #e9edf6; font: inherit;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .12);
}
.rsp-in:focus { outline: 2px solid #3b64f4; outline-offset: 1px; }

.rsp-two { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 14px; }
.rsp-box { min-width: 0; }

/* THE FILE INPUT IS INSIDE THE DROP AREA AND COVERS IT, rather than being hidden
   behind a label. That is what makes the box work with no JavaScript at all: it
   is a real file input with a large hit area, and the script only decorates it. */
.rsp-drop {
    position: relative; border: 2px dashed rgba(255, 255, 255, .18); border-radius: 12px;
    padding: 18px 14px; text-align: center; color: var(--muted-steel); font-size: .84rem;
    min-height: 110px; display: flex; flex-direction: column; justify-content: center; gap: 4px;
}
.rsp-drop b { color: var(--ink-12); overflow-wrap: anywhere; }
.rsp-drop input[type="file"] {
    position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: 0; cursor: pointer;
}
.rsp-drop:focus-within { outline: 2px solid #3b64f4; outline-offset: 2px; }
.rsp-drop.is-filled { border-style: solid; border-color: rgba(61, 220, 151, .45); background: rgba(61, 220, 151, .06); }
.rsp-drop.is-err    { border-color: rgba(255, 122, 122, .55); background: rgba(255, 122, 122, .05); }
.rsp-thumb {
    height: 56px; width: 100%; object-fit: contain; border-radius: 8px;
    background: #0a0f1c; margin-bottom: 6px;
}
.rsp-fname  { color: var(--ink-06); font-size: .8rem; font-weight: 700; overflow-wrap: anywhere; }
.rsp-replace { color: var(--muted-steel); font-size: .76rem; text-decoration: underline; }
.rsp-errline { color: var(--ink-103); font-size: .8rem; margin: 6px 0 0; line-height: 1.5; }

.rsp-consent { display: flex; gap: 10px; align-items: flex-start; font-size: .84rem; color: var(--ink-12); margin: 16px 0; }
.rsp-consent input { margin-top: 3px; accent-color: #3b64f4; flex: none; }
.rsp-submit[disabled] { opacity: .45; cursor: not-allowed; }

/* ---- The three returning states ----------------------------------------- */
.rsp-state {
    border-radius: 12px; padding: 16px 18px; position: relative;
    font-size: .92rem; line-height: 1.6; margin: 0 0 12px;
}
.rsp-state::after { content: ""; position: absolute; inset: 0; border-radius: 12px; pointer-events: none; }
.rsp-state b { display: block; margin-bottom: 4px; color: #fff; }
.rsp-state.pend { background: rgba(245, 182, 78, .08); color: #f0d9a8; }
.rsp-state.pend::after { box-shadow: inset 0 0 0 1px rgba(245, 182, 78, .3); }
.rsp-state.ok   { background: rgba(61, 220, 151, .08); color: #bfe9d6; }
.rsp-state.ok::after   { box-shadow: inset 0 0 0 1px rgba(61, 220, 151, .3); }
.rsp-state.no   { background: rgba(255, 122, 122, .07); color: #f3c1c1; }
.rsp-state.no::after   { box-shadow: inset 0 0 0 1px rgba(255, 122, 122, .3); }
.rsp-after { margin: 12px 0 0; }

@media (max-width: 720px) {
    /* The mockup's mobile note: steps stack, uploads stack, same content. */
    .rsp-steps { grid-template-columns: 1fr; }
    .rsp-two   { grid-template-columns: 1fr; }
}


/* ---- First-responder placements (product strip + footer link) -------------
   docs/mockups/responder-link-placements.html, panels A and B.

   THE RED FAMILY IS THE PROGRAMME'S IDENTITY, and it is the same red the shield,
   the account card and the admin queue's link already wear. Somebody who has
   seen the badge once should recognise the strip without reading it.

   NAMED pd-rsp AND footer-rsp, NOT rsp-*. The rsp-* namespace in this stylesheet
   is shared by the storefront responder page AND the admin grant form, and the
   last collision there put 26px of padding into a headline. These two live on
   surfaces that already have their own prefixes; they use them. */
/* THE MOCKUP DRAWS THIS ON A DARK FRAME AND THE PRODUCT PAGE IS LIGHT. Carrying
   the mockup's colours across literally (white bold text, #ff9a8f link) put
   near-white text on a pale pink panel and the strip could not be read at all.
   The tint, the hairline and the red family are the mockup's; the ink is
   re-derived for the surface it actually lands on, which is what the mockup
   would have specified had it been drawn there. */
.pd-rsp {
    display: flex; gap: 10px; align-items: center;
    background: rgba(217, 44, 47, .07); border-radius: 11px;
    padding: 12px 15px; margin: 10px 0 0;
    font-size: .88rem; line-height: 1.5; color: var(--rsp-tint-ink);
    position: relative;
}
.pd-rsp::after {
    content: ""; position: absolute; inset: 0; border-radius: 11px;
    box-shadow: inset 0 0 0 1px rgba(217, 44, 47, .22); pointer-events: none;
}
/* 28px, up from an 18px glyph. The strip grows by about 8px because the mark is
   now the tallest thing in the row; nothing else moved and nothing wraps that did
   not wrap before. */
.pd-rsp-ico { width: 28px; height: 28px; flex: none; border-radius: 50%; }
.pd-rsp-t { min-width: 0; }
.pd-rsp b { color: var(--rsp-tint-b); font-weight: 700; }
/* margin-left:auto is what pins the link to the right edge on desktop and is
   also what has to go on mobile, where the mockup wraps it under the text. */
.pd-rsp-cta { color: var(--rsp-tint-cta); font-weight: 700; margin-left: auto; white-space: nowrap; }
.pd-rsp-cta:hover { color: var(--rsp-tint-cta2); text-decoration: underline; }
.pd-rsp-cta:focus-visible { outline: 2px solid var(--rsp-tint-cta); outline-offset: 2px; border-radius: 4px; }

@media (max-width: 520px) {
    /* The mockup's mobile note: the link wraps under the text. Wrapping the flex
       row and dropping the auto margin is the whole change; nothing is hidden and
       nothing overflows, because the only nowrap element now starts a line of its
       own. */
    .pd-rsp { flex-wrap: wrap; }
    .pd-rsp-t { flex: 1 1 100%; }
    /* LEFT AT 28px. At this width .pd-rsp-t takes a full line, so the mark is alone
       on the line above and this indent is not clearing it; it is the mockup's
       hanging indent for the wrapped link. Growing the mark to 28px therefore
       changes nothing here, and re-deriving the number from the new icon width
       would have moved the link 10px for no reason. */
    .pd-rsp-cta { margin-left: 28px; }
}






/* =========================================================================
   RESEARCH BLOG
   =========================================================================
   Two public pages, /research and /research/{slug}, and nothing else on the
   site uses a rule in this section. Every selector is under .rs-body or is an
   .rs- class that only views/research_index.php and views/research_post.php
   print, so this block cannot reach the storefront or the admin.

   WHAT IS THEMED AND WHAT IS NOT, which is the whole of the theme decision here:

     THE PAGE is themed. The background, the body ink, the muted text, the
     hairlines and the article's own typography all read the storefront role
     tokens, so light, dark and original each render correctly with no rule of
     their own in this file.

     THE CARDS ARE NOT. The black glass index cards, the matte black sidebar
     cards and the share rail are CHROME: dark by design in all three states,
     exactly like the top bar and the footer. Each declares its own literal
     token family on itself, the way views/partials/agegate.php does, so it does
     not inherit a page token that a theme could move underneath it. They are
     also named in the CHROME ISOLATION selector list above, which resets every
     token that differs in dark back to its light value inside them, so nothing
     a later run adds can leak in either.

   NO backdrop-filter ANYWHERE. "Black glass" here is a solid near black with a
   single low-opacity sheen gradient over it and a hairline. That is a house
   rule, it was measured off iPhone once already for the nav bar, and a blur is
   not worth paying for a second time. The sheen is a background-image, so the
   card composites to the same colour on a client that never supported filters.

   NO LEFT ACCENT BORDERS, and the pills are solid fills, not tinted outlines.
   ========================================================================= */

/* ---- The self-hosted family ---------------------------------------------
   Five static weights, woff2, served from this domain. There is NO Google
   Fonts request at runtime: nothing on this site links fonts.googleapis.com
   for Montserrat and nothing fetches fonts.gstatic.com for it.

   AN @font-face RULE DOWNLOADS NOTHING BY ITSELF. The browser fetches a face
   only when a rule that matches a rendered element asks for the family, and the
   only rules that do are under .rs-body below. Declaring the faces in the shared
   stylesheet therefore costs the rest of the site nothing, and keeps them in the
   one file build-css.php generates the storefront copy from.

   font-display: swap on every face: the fallback stack paints immediately and
   the real face swaps in, which is the right trade for an article somebody is
   about to read. */
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('/assets/fonts/montserrat/montserrat-400.woff2') format('woff2');
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('/assets/fonts/montserrat/montserrat-500.woff2') format('woff2');
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('/assets/fonts/montserrat/montserrat-600.woff2') format('woff2');
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('/assets/fonts/montserrat/montserrat-700.woff2') format('woff2');
}
@font-face {
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 800;
    font-display: swap;
    src: url('/assets/fonts/montserrat/montserrat-800.woff2') format('woff2');
}

.rs-body {
    /* The stack the whole feature reads. The fallbacks are real fonts with real
       metrics, so the pre-swap paint is not Times. */
    --rs-sans: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
               Helvetica, Arial, sans-serif;

    /* THE RADIUS, stated once. Every image on both pages wears it through
       .rs-img, which is ResearchHtml::IMG_CLASS, so the cover, the card
       pictures, the related thumbnails and an image an editor pasted into the
       body are all the same corner and there is one number to change. */
    --rs-radius: 15px;

    /* THE CARD AND BOX FAMILIES, AND THEY ARE THEMED NOW, NOT CHROME.
     *
     * They were dark literals in all three states, on the reasoning that the
     * index cards and the sidebar boxes are furniture like the bar and the
     * footer. Measured, that reasoning does not survive contact with the page:
     * .rs-body's own background is the light --bg and always was, but the cards
     * cover almost all of it, so with no cookie set the painted surface of
     * /research was 99% dark and a post 97%, against 46% on a product page. A
     * reader does not see a light page with dark cards on it; they see a dark
     * page. "The research pages are dark by default" was a true description of
     * what was on screen even though every page-level token said light.
     *
     * So these are the LIGHT values, on the bare selector, which is what an
     * unchosen page and an explicit light page both get; the dark family is
     * under :root[data-theme="dark"] .rs-body below. The bar, the footer, the
     * drawer and the announcement are untouched and are still chrome.
     *
     * The shadows are not the dark ones dimmed: a 0.30 black lift under a white
     * card on a near-white page is a smudge, so light gets its own smaller pair.
     */
    --rsc-bg:     #ffffff;   /* the index cards */
    --rsc-sheen:  rgba(0, 0, 0, 0.015);
    --rsc-line:   rgba(0, 0, 0, 0.10);
    --rsc-line-2: rgba(0, 0, 0, 0.20);
    --rsc-ink:    #141414;   /* the light --ink */
    --rsc-muted:  #5f5f5f;   /* darker than the light --muted: this is body copy
                                on a card, and it has to clear 4.5:1 on #ffffff */
    --rsc-lift:   0 10px 26px rgba(0, 0, 0, 0.07);
    --rsc-lift-2: 0 16px 36px rgba(0, 0, 0, 0.11);

    --rsb-bg:     #ffffff;   /* the sidebar cards */
    --rsb-line:   #e5e5e5;
    --rsb-ink:    #141414;
    --rsb-muted:  #5f5f5f;

    /* The no-cover placeholder. Neutral, so a post without a picture reads as a
       gap rather than as a black tile on a white card. */
    --rs-noimg:   linear-gradient(135deg, #eef0f4, #e2e6ec);

    /* Share rail, brand colours. Icon tiles, so the bar to clear is the 3:1 that
       applies to a graphical object, not the 4.5:1 that applies to text. Every
       one of these clears it against both the white glyph on it and the page
       behind it; the measured numbers are in the run report. */
    --rs-x:        #0f0f0f;
    --rs-fb:       #1877f2;
    --rs-li:       #0a66c2;
    --rs-rd:       #ff4500;
    --rs-mail:     #e8590c;
    --rs-copy:     #2e5be0;
}

/* THE DARK HALF OF THE TWO FAMILIES ABOVE. These are the literals that used to
   sit on the bare selector, moved here unchanged, so an explicit dark cookie
   renders exactly the cards and boxes it rendered before this run: black glass
   index cards, matte black sidebar boxes, the same sheen, lines and lifts.

   ONLY .rs-body. Nothing else on the site declares --rsc-* or --rsb-*, so this
   cannot reach another page, and the research pages' own chrome (the bar, the
   footer) is unaffected because it never read these names. */
:root[data-theme="dark"] .rs-body {
    --rsc-bg:     #101216;   /* black glass, the index cards */
    --rsc-sheen:  rgba(255, 255, 255, 0.07);
    --rsc-line:   rgba(255, 255, 255, 0.10);
    --rsc-line-2: rgba(255, 255, 255, 0.18);
    --rsc-ink:    #ffffff;
    --rsc-muted:  rgba(255, 255, 255, 0.68);
    --rsc-lift:   0 18px 40px rgba(0, 0, 0, 0.30);
    --rsc-lift-2: 0 24px 52px rgba(0, 0, 0, 0.38);

    --rsb-bg:     #1c1c1c;   /* matte black, the sidebar cards. The same value
                                --surface-dark carries. */
    --rsb-line:   #3a3a3a;
    --rsb-ink:    #f2f2f2;
    --rsb-muted:  #b3b3b3;

    /* The placeholder this run replaced with a token, at its original value. */
    --rs-noimg:   linear-gradient(135deg, #1b2340, #0d1220);
}

/* THE PAGE, on storefront tokens, so all three theme states are correct with no
   rule of their own. */
.rs-body {
    background: var(--bg);
    color: var(--ink);
}

.rs-wrap {
    max-width: 1180px;
    margin: 0 auto;
    padding: 44px 24px 80px;
    font-family: var(--rs-sans);
}

/* EVERY IMAGE, 15px. One class, one value, and it is the class
   ResearchHtml::prepare() stamps on body images as well as the one the cover and
   the cards print, so there is no image on either page outside this rule. */
.rs-img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: var(--rs-radius);
}

/* The stand-in for a post with no cover. Same corner, same box, so a post
   without a picture does not change the shape of a card. */
/* THE STAND-IN FOR A MISSING COVER, and it is themed like the card it sits in.
   It was a hard-coded near-black gradient, which was right while the cards were
   black glass and is a hole punched in a white card now. It hid from the first
   survey of this page because it paints with background-IMAGE, and a luminance
   check that only reads background-color sees a gradient as transparent; the
   survey reads both now. */
.rs-noimg {
    display: block;
    width: 100%;
    border-radius: var(--rs-radius);
    background: var(--rs-noimg);
}

/* ---- Index head ---------------------------------------------------------- */
.rs-head { margin: 0 0 26px; }
.rs-kicker {
    margin: 0 0 10px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--muted);
}
.rs-h1 {
    margin: 0 0 12px;
    font-family: var(--rs-sans);
    font-size: 54px;
    font-weight: 800;
    line-height: 1.05;
    letter-spacing: -0.02em;
    color: var(--ink);
}
.rs-lede {
    margin: 0;
    max-width: 60ch;
    font-size: 19px;
    line-height: 1.55;
    color: var(--muted);
}

/* ---- Category filter pills ----------------------------------------------- */
.rs-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 26px 0 34px;
}
/* SOLID, both states. The resting pill is a solid surface and the selected one
   is a solid near black; neither is an outline and neither carries a tint. */
.rs-filter {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 8px 14px;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--ink);
    font-family: var(--rs-sans);
    font-size: 13.5px;
    font-weight: 600;
    text-decoration: none;
    transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.rs-filter:hover { border-color: var(--border-2); }
.rs-filter.is-on {
    background: #151517;
    border-color: #151517;
    color: #ffffff;
}
.rs-filter-n {
    font-size: 11.5px;
    font-weight: 700;
    opacity: 0.7;
}

/* ---- Category pill, on cards and at the top of a post -------------------- */
/* SOLID FILLS, white type. The colour is picked from the category id, so the
   same category wears the same colour everywhere it appears. */
.rs-pill {
    display: inline-block;
    padding: 5px 12px;
    border-radius: 999px;
    font-family: var(--rs-sans);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.02em;
    color: #ffffff;
    text-decoration: none;
}
.rs-p0 { background: #3a3a3a; }
.rs-p1 { background: #2e5be0; }
.rs-p2 { background: #1f7a3e; }
.rs-p3 { background: #5b6068; }
.rs-p4 { background: #b0450f; }
.rs-p5 { background: #6b3fa0; }

/* ---- The featured post, black glass -------------------------------------- */
.rs-feat,
.rs-card {
    /* One paint for both, so the feature and a card are the same material.
       Solid colour, then ONE sheen gradient on top of it. No backdrop-filter. */
    background-color: var(--rsc-bg);
    background-image: linear-gradient(160deg, var(--rsc-sheen), rgba(255, 255, 255, 0) 46%);
    border: 1px solid var(--rsc-line);
    color: var(--rsc-ink);
    box-shadow: var(--rsc-lift);
}
.rs-feat {
    display: grid;
    grid-template-columns: 1.25fr 1fr;
    gap: 30px;
    align-items: center;
    border-radius: 22px;
    padding: 18px;
    margin: 0 0 40px;
}
.rs-feat-media { display: block; }
.rs-feat-media .rs-noimg { aspect-ratio: 16 / 10; }
.rs-feat-body { padding: 6px 12px 6px 0; }
.rs-feat-h {
    margin: 14px 0 12px;
    font-family: var(--rs-sans);
    font-size: 34px;
    font-weight: 700;
    line-height: 1.14;
    letter-spacing: -0.015em;
}
.rs-feat-h a { color: var(--rsc-ink); text-decoration: none; }
.rs-feat-h a:hover { text-decoration: underline; text-underline-offset: 3px; }
.rs-feat-p {
    margin: 0 0 18px;
    font-size: 17px;
    line-height: 1.55;
    color: var(--rsc-muted);
}

/* ---- The card grid ------------------------------------------------------- */
.rs-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 24px;
}
.rs-card {
    border-radius: 18px;
    padding: 14px;
    display: flex;
    flex-direction: column;
    transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
}
.rs-card:hover {
    transform: translateY(-3px);
    box-shadow: var(--rsc-lift-2);
    border-color: var(--rsc-line-2);
}
.rs-card-media { display: block; margin-bottom: 14px; }
.rs-card-media .rs-noimg { aspect-ratio: 16 / 10; }
.rs-card .rs-pill { align-self: flex-start; }
.rs-card-h {
    margin: 10px 0 8px;
    font-family: var(--rs-sans);
    font-size: 20px;
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.01em;
}
.rs-card-h a { color: var(--rsc-ink); text-decoration: none; }
.rs-card-h a:hover { text-decoration: underline; text-underline-offset: 3px; }
.rs-card-p {
    margin: 0 0 14px;
    font-size: 14.5px;
    line-height: 1.5;
    color: var(--rsc-muted);
    /* Three lines, then stop. Keeps a long excerpt from making one card in a row
       twice the height of its neighbours. */
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.rs-card .rs-meta { margin-top: auto; }

/* The dated line under a card and under the feature. */
.rs-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: center;
    font-size: 13.5px;
    color: var(--rsc-muted);
}
.rs-meta i {
    width: 4px;
    height: 4px;
    border-radius: 999px;
    background: currentColor;
    opacity: 0.6;
    display: inline-block;
}

.rs-empty {
    margin: 40px 0;
    font-size: 16px;
    color: var(--muted);
}

/* ---- Pagination ---------------------------------------------------------- */
.rs-pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 18px;
    margin: 48px 0 0;
    font-family: var(--rs-sans);
    font-size: 14px;
}
.rs-pg {
    padding: 10px 18px;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--ink);
    font-weight: 600;
    text-decoration: none;
}
.rs-pg:hover { border-color: var(--border-2); }
.rs-pg.is-off { opacity: 0.45; }
.rs-pg-n { color: var(--muted); font-weight: 600; }

/* ---- The post page ------------------------------------------------------- */
/* THE TITLE IS FLUSH WITH THE COVER'S LEFT EDGE, and this is where that is
   decided: .rs-post-top is the only element on the post page that sets a
   horizontal inset, and the heading and the cover are both its direct children
   with no margin of their own. They share an edge by construction, so the two
   cannot drift apart when one of them is restyled. */
.rs-post-top {
    max-width: 100%;
    margin: 0 0 34px;
    padding: 0;
}
.rs-post-h1 {
    margin: 14px 0 14px;
    font-family: var(--rs-sans);
    font-size: 46px;
    font-weight: 800;
    line-height: 1.08;
    letter-spacing: -0.02em;
    color: var(--ink);
}
.rs-dek {
    margin: 0 0 20px;
    max-width: 68ch;
    font-size: 19.5px;
    line-height: 1.5;
    color: var(--muted);
}
.rs-author {
    display: flex;
    align-items: center;
    gap: 12px;
    font-size: 14px;
    color: var(--muted);
}
/* The initials disc. Near black in every state, like the rest of the chrome. */
.rs-av {
    width: 38px;
    height: 38px;
    flex: 0 0 38px;
    border-radius: 999px;
    background: #151517;
    color: #ffffff;
    display: grid;
    place-items: center;
    font-weight: 700;
    font-size: 13px;
    letter-spacing: 0.02em;
}
.rs-author-t { line-height: 1.4; }
.rs-dot { opacity: 0.6; padding: 0 2px; }

.rs-cover { margin: 30px 0 0; }

.rs-preview-note {
    margin: 0 0 24px;
    padding: 12px 16px;
    border-radius: 12px;
    border: 1px solid var(--border);
    background: var(--surface-2);
    color: var(--ink);
    font-size: 14px;
}

/* ---- The three column layout --------------------------------------------- */
.rs-layout {
    display: grid;
    grid-template-columns: 64px minmax(0, 1fr) 300px;
    gap: 40px;
    align-items: start;
}

/* ---- The share rail, brand coloured -------------------------------------- */
.rs-rail {
    position: sticky;
    top: 107px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.rs-rail-l {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--muted);
    text-align: center;
    margin-bottom: 2px;
}
.rs-share {
    position: relative;
    width: 44px;
    height: 44px;
    border: 0;
    border-radius: 12px;
    display: grid;
    place-items: center;
    cursor: pointer;
    padding: 0;
    /* SOLID BRAND FILL, white glyph. Set per network below; this is the shared
       shape and the default for anything without one. */
    background: #151517;
    color: #ffffff;
    transition: transform 0.16s ease, filter 0.16s ease;
}
.rs-share:hover { transform: translateY(-2px); filter: brightness(1.12); }
.rs-share:focus-visible { outline: 2px solid var(--rs-copy); outline-offset: 2px; }
.rs-share svg { width: 18px; height: 18px; fill: currentColor; }
.rs-s-x        { background: var(--rs-x); }
.rs-s-facebook { background: var(--rs-fb); }
.rs-s-linkedin { background: var(--rs-li); }
.rs-s-reddit   { background: var(--rs-rd); }
.rs-s-email    { background: var(--rs-mail); }
.rs-s-copy     { background: var(--rs-copy); }

/* The "Copied" flag. Absolutely positioned so showing it moves nothing. */
.rs-copied {
    position: absolute;
    left: calc(100% + 8px);
    top: 50%;
    transform: translateY(-50%);
    white-space: nowrap;
    background: #151517;
    color: #ffffff;
    font-family: var(--rs-sans);
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: 0.04em;
    padding: 5px 9px;
    border-radius: 7px;
}

/* ---- The article --------------------------------------------------------- */
/* ON PAGE TOKENS, deliberately, unlike the cards. This is the reading surface
   and it should be the page: light in light, dark in dark. */
.rs-article {
    font-family: var(--rs-sans);
    font-size: 18px;
    font-weight: 400;
    line-height: 1.72;
    color: var(--ink);
    max-width: 70ch;
    /* The one rule that stops a long URL or a wide table pushing the whole
       document sideways at 360px. */
    min-width: 0;
}
.rs-article h2 {
    margin: 40px 0 12px;
    font-family: var(--rs-sans);
    font-size: 28px;
    font-weight: 700;
    line-height: 1.22;
    letter-spacing: -0.015em;
    color: var(--ink);
    /* The TOC links here, so a heading must not land under the sticky top bar. */
    scroll-margin-top: 107px;
}
.rs-article h3 {
    margin: 30px 0 10px;
    font-size: 21px;
    font-weight: 700;
    line-height: 1.3;
    color: var(--ink);
}
.rs-article p { margin: 0 0 20px; }
.rs-article a { color: var(--link, #2e5be0); text-decoration: underline; text-underline-offset: 2px; }
.rs-article ul,
.rs-article ol { margin: 0 0 20px; padding-left: 24px; }
.rs-article li { margin-bottom: 8px; }
.rs-article figure { margin: 30px 0; }
.rs-article figcaption {
    margin-top: 10px;
    font-size: 14px;
    line-height: 1.5;
    color: var(--muted);
}
/* NO LEFT ACCENT BORDER. A quotation is a filled block with the same corner
   every image on the page has, which is the house treatment. */
.rs-article blockquote {
    margin: 28px 0;
    padding: 18px 22px;
    background: var(--surface-2);
    border-radius: var(--rs-radius);
    font-size: 21px;
    font-weight: 500;
    line-height: 1.45;
    color: var(--ink);
}
.rs-article blockquote p:last-child { margin-bottom: 0; }
.rs-article sup a {
    font-size: 12px;
    font-weight: 700;
    padding: 0 2px;
    text-decoration: none;
}
.rs-article sup a:hover { text-decoration: underline; }
.rs-article code {
    font-family: var(--font-mono);
    font-size: 0.9em;
    background: var(--surface-2);
    padding: 2px 5px;
    border-radius: 5px;
}
.rs-article pre {
    background: var(--surface-2);
    border-radius: var(--rs-radius);
    padding: 16px 18px;
    overflow-x: auto;
}
.rs-article pre code { background: none; padding: 0; }
.rs-article hr { border: 0; border-top: 1px solid var(--border); margin: 34px 0; }
/* A wide table scrolls inside itself rather than widening the document. */
.rs-article table { width: 100%; border-collapse: collapse; font-size: 15px; display: block; overflow-x: auto; }
.rs-article th,
.rs-article td { border: 1px solid var(--border); padding: 8px 10px; text-align: left; }

/* The research-use line at the foot of every post. */
.rs-ruo {
    margin: 36px 0 0;
    padding: 16px 18px;
    border: 1px solid var(--border);
    border-radius: var(--rs-radius);
    font-size: 14px;
    line-height: 1.55;
    color: var(--muted);
    background: var(--surface);
}

/* ---- The sidebar, matte black -------------------------------------------- */
.rs-side {
    position: sticky;
    top: 107px;
    display: grid;
    gap: 20px;
}
.rs-box {
    background: var(--rsb-bg);
    border: 1px solid var(--rsb-line);
    border-radius: 18px;
    padding: 18px 20px;
    color: var(--rsb-ink);
    font-family: var(--rs-sans);
}
.rs-box-h {
    margin: 0 0 12px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--rsb-muted);
}
.rs-toc a {
    display: block;
    padding: 7px 0;
    font-size: 14.5px;
    font-weight: 500;
    line-height: 1.35;
    color: var(--rsb-ink);
    text-decoration: none;
    border-bottom: 1px solid var(--rsb-line);
}
.rs-toc a:last-child { border-bottom: 0; }
.rs-toc a:hover { text-decoration: underline; text-underline-offset: 2px; }
.rs-refs ol {
    margin: 0;
    padding-left: 20px;
    font-size: 13.5px;
    line-height: 1.45;
    color: var(--rsb-ink);
}
.rs-refs li { margin-bottom: 10px; scroll-margin-top: 107px; }
.rs-refs li:last-child { margin-bottom: 0; }
.rs-refs a { color: #9db4ff; word-break: break-word; }
.rs-rel { display: grid; gap: 14px; }
.rs-rel a {
    display: grid;
    grid-template-columns: 72px minmax(0, 1fr);
    gap: 12px;
    align-items: center;
    color: var(--rsb-ink);
    text-decoration: none;
}
.rs-rel-img { border-radius: var(--rs-radius); }
.rs-rel span.rs-noimg { height: 50px; }
.rs-rel-t { min-width: 0; }
.rs-rel-t b {
    display: block;
    font-size: 15px;
    font-weight: 600;
    line-height: 1.28;
}
.rs-rel-t small { display: block; margin-top: 3px; font-size: 12px; color: var(--rsb-muted); }
.rs-rel a:hover .rs-rel-t b { text-decoration: underline; text-underline-offset: 2px; }

/* ---- Widths -------------------------------------------------------------- */
@media (max-width: 1100px) {
    .rs-layout { grid-template-columns: 64px minmax(0, 1fr) 260px; gap: 28px; }
}


/* THE BREAKPOINT THE RAIL DROPS AT. Below this the three columns become one and
   the rail stops being a rail: it unsticks and lays out as a horizontal row
   directly under the cover, which is the same markup with two properties
   changed rather than a second copy of it. */
@media (max-width: 960px) {
    .rs-layout { grid-template-columns: minmax(0, 1fr); gap: 30px; }
    .rs-rail {
        position: static;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 9px;
    }
    .rs-rail-l { margin: 0 6px 0 0; text-align: left; }
    .rs-side { position: static; }
    .rs-article { max-width: none; }
    .rs-copied { left: 50%; top: calc(100% + 6px); transform: translateX(-50%); }
}


@media (max-width: 900px) {
    .rs-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}


@media (max-width: 860px) {
    .rs-feat { grid-template-columns: minmax(0, 1fr); gap: 18px; }
    .rs-feat-body { padding: 0 4px 6px; }
    .rs-feat-h { font-size: 28px; }
}


@media (max-width: 700px) {
    .rs-h1 { font-size: 38px; }
    .rs-post-h1 { font-size: 32px; }
    .rs-lede { font-size: 17px; }
    .rs-dek { font-size: 17px; }
    .rs-article { font-size: 17px; }
    .rs-article h2 { font-size: 24px; }
}


@media (max-width: 600px) {
    .rs-wrap { padding: 30px 18px 60px; }
    .rs-grid { grid-template-columns: minmax(0, 1fr); }
    .rs-h1 { font-size: 32px; }
    .rs-post-h1 { font-size: 27px; }
    .rs-feat { padding: 14px; }
}


@media (max-width: 400px) {
    .rs-wrap { padding: 26px 14px 52px; }
    .rs-h1 { font-size: 28px; }
    .rs-post-h1 { font-size: 24px; }
    .rs-share { width: 40px; height: 40px; }
}


@media (prefers-reduced-motion: reduce) {
    .rs-card,
    .rs-share { transition: none; }
    .rs-card:hover,
    .rs-share:hover { transform: none; }
}





/* The voice reminder above the body box. A filled note, not an outlined one,
   and no left accent border: it is the same treatment every other note in this
   admin uses. */

.rsf-tool {
    font: inherit;
    font-size: 12.5px;
    font-weight: 600;
    padding: 6px 11px;
    border: 1px solid var(--border-2);
    border-radius: 7px;
    background: #ffffff;
    color: var(--ink);
    cursor: pointer;
}
.rsf-tool:hover:not([disabled]) { background: #f2f4f7; }
.rsf-tool[disabled] { opacity: 0.5; cursor: not-allowed; }
.rsf-tool:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 1px; }






/* =========================================================================
   ORDERS: INLINE PAYMENT METHOD, and the shared method button
   =========================================================================
   Built to docs/mockups/orders-partial-payment-mockup.html, Part 1.

   .pm-btn is the SHARED button, rendered by
   views/partials/payment_method_button.php and used by BOTH the orders list row
   and the order page, which is what makes them the same thing rather than two
   things that currently look alike. Its colours are inline styles from
   payment_method_brand(), which reads PaymentMethods::ALL, so the palette is not
   repeated here. Never assume white text: Cash App and PayPal are light grounds
   and carry dark foregrounds, which is why there is no `color` in this rule.

   Admin is never themed, so every value here is a literal or a light token.
   ========================================================================= */
.pm-btn {
    display: inline-flex;
    align-items: center;
    height: 26px;
    padding: 0 10px;
    border-radius: 7px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.01em;
    white-space: nowrap;
    text-decoration: none;
    /* No background or color: both arrive as an inline style from the brand table. */
}
a.pm-btn:hover { filter: brightness(1.08); text-decoration: none; }
a.pm-btn:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
/* The placeholder. Not a button and not styled as one: there is nothing to press
   until a method is recorded. */
.pm-btn.is-none {
    background: none;
    color: var(--muted-2);
    font-weight: 600;
    font-style: italic;
    padding: 0;
    height: auto;
}






/* SOLID pills, both states, per the house rule. Green says saved, red says why
   not; neither is an outline and neither carries a tint. */

.ordv-pmflash.is-ok  { background: var(--pill-green); }
.ordv-pmflash.is-bad { background: var(--pill-red); white-space: normal; height: auto; padding: 3px 9px; }



/* =========================================================================
   PARTIAL FULFILMENT
   =========================================================================
   Built to docs/mockups/orders-partial-payment-mockup.html, Part 2.

   THE PILL NEEDED NO NEW RULE. order_fulfilment_pill() returns the 'info' tone
   for partially_shipped, and .admin-shell .ordv-pill.t-info is already a solid
   #2E5BE0 on white, which is the mockup's blue exactly. The customer's account
   list already maps 'info' to its solid blue 'b'. The only thing missing on
   either side was the sub-line, which is what .acct-partline and .ord-partline
   are. Everything else here is the order page's Fulfilment card.
   ========================================================================= */

/* ---- Storefront: the "2 of 3 lines shipped" sub-line --------------------- */
.acct-partline,
.ord-partline {
    margin: 4px 0 0;
    font-size: 13px;
    line-height: 1.4;
    color: var(--muted);
}
.ord-partline { margin: 8px 0 0; }

/* The customer's own pills for the two partial states. Solid blue, matching the
   tone the admin list uses, so one order reads the same on both sides of the
   site. The SAME blue for both: the label carries whether it was a carrier or a
   collection, the colour carries the state, and a second blue would suggest a
   difference in status that does not exist. */
.ord-ful-partially_shipped,
.ord-ful-partially_picked_up,
.ord-ful-partially_fulfilled { background: #2e5be0; color: #ffffff; }



/* One line. Tick, name, quantity, state, exactly the mockup's four columns. */
.ful-line {
    display: grid;
    grid-template-columns: 24px minmax(0, 1fr) auto auto;
    gap: 12px;
    align-items: center;
    padding: 11px 0;
    border-bottom: 1px solid var(--border);
}
.ful-line:last-of-type { border-bottom: 0; }
.ful-line input[type="checkbox"] { width: 19px; height: 19px; margin: 0; cursor: pointer; }
.ful-line input[type="checkbox"]:disabled { cursor: default; }










/* ---- Carrier status: the shared FedEx cache row, admin presentation -------
   (views/admin/partials/fedex_track_admin.php). The same fedex_tracking row the
   customer's card draws, in the admin's own vocabulary: the .st-pill the orders
   list uses for the state, the delivery line, plain ruled rows for the scans
   newest first, the age footer. No accent border, no timeline connector. */


@media (max-width: 700px) {

    /* The quantity and the state pill drop under the name rather than squeezing
       it to nothing on a phone. */
    .ful-line { grid-template-columns: 24px minmax(0, 1fr); }
    .ful-line-q, .ful-line .ordv-pill { grid-column: 2; }
}















/* =========================================================================
   CUSTOMER ORDER CHANGES, inside the one-hour window
   views/account_order.php, docs/mockups/customer-order-edit-mockup.html

   TWO BANNERS AND A STEPPER, and nothing else new. The lines, the totals and
   the payment panel below them are the page's existing markup; only the
   controls in front of them are added here.

   NO LEFT ACCENT BORDER anywhere in this block. Both banners are tinted
   surfaces with their own border on all four sides, which is the house rule.
   ========================================================================= */

/* ---- The blue window banner --------------------------------------------- */
.aoe-window {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 16px;
    align-items: center;
    background: #eef2ff;
    border: 1px solid #cdd6fb;
    border-radius: 14px;
    padding: 16px 20px;
    margin: 0 0 18px;
}
.aoe-window-clock {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 26px;
    font-weight: 700;
    color: var(--accent-blue);
    white-space: nowrap;
    line-height: 1.15;
}
.aoe-window-clock small {
    display: block;
    font-family: var(--font-ui);
    font-size: 11px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--muted);
    font-weight: 600;
}
.aoe-window-copy { margin: 0; font-size: 15px; line-height: 1.5; }
.aoe-window-copy b { display: block; margin-bottom: 2px; }

/* countdown.js sets is-urgent on any window under three hours, which a one-hour
   window always is, and is-past when the hour is up. The first is deliberately
   not styled: a banner that is red for its whole life is not a warning. The
   second is, because by then the controls under it have gone dead. */
.aoe-window.is-past { background: var(--off-white); border-color: var(--border); }
.aoe-window.is-past .aoe-window-clock { color: var(--muted); }

/* ---- The grey lock banner ----------------------------------------------- */
.aoe-locked {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 16px;
    align-items: center;
    background: var(--off-white);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 16px 20px;
    margin: 0 0 18px;
}
.aoe-locked-ic {
    width: 40px;
    height: 40px;
    border-radius: 999px;
    background: var(--surface-dark);
    color: #fff;
    display: grid;
    place-items: center;
    font-size: 18px;
}
.aoe-locked-copy { margin: 0; font-size: 15px; line-height: 1.5; }
.aoe-locked-copy b { display: block; }

/* ---- The editable lines -------------------------------------------------- */
.aoe-form { margin: 0; }
/* The line keeps .conf-item's flex row and gains a middle column for the
   stepper, so a locked order and an open one sit on the same grid. */
.aoe-line { align-items: center; gap: 14px; flex-wrap: wrap; }
.aoe-line-n { flex: 1 1 220px; min-width: 0; }
.aoe-line-t { min-width: 90px; text-align: right; }
.aoe-line.is-removed .aoe-line-n,
.aoe-line.is-removed .aoe-line-t { text-decoration: line-through; color: var(--muted-2); }
.aoe-line-cap { margin: 0 0 8px; }

.aoe-rm { display: inline-flex; align-items: center; gap: 6px; margin: 4px 0 0; font-size: 13px; color: var(--muted); cursor: pointer; }
.aoe-rm input { margin: 0; }

.aoe-step { display: inline-flex; align-items: center; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: var(--white); }
/* 44px, the touch floor. The row it sits in is sized by this control, so the
   line grows with it rather than the button being cropped. */
.aoe-step-b {
    width: 44px;
    height: 44px;
    border: 0;
    background: var(--white);
    font-size: 18px;
    line-height: 1;
    color: var(--ink);
    cursor: pointer;
}
.aoe-step-b:hover:not(:disabled) { background: var(--off-white); }
.aoe-step-b:disabled { color: var(--muted-3); cursor: default; }
/* THE TWO HAIRLINES ARE BOTH border-right, on the minus button and on the box.
   The same two lines a border-left on the value would have drawn, and no rule in
   this run puts a border on any element's left edge. */
.aoe-step-b:first-child { border-right: 1px solid var(--border); }
.aoe-step-v {
    width: 56px;
    height: 44px;
    border: 0;
    border-right: 1px solid var(--border);
    text-align: center;
    font-weight: 600;
    font-size: 15px;
    background: var(--white);
    color: var(--ink);
    padding: 0;
}
.aoe-step-v:disabled { color: var(--muted-3); background: var(--off-white); }

/* ---- The action row ------------------------------------------------------ */
.aoe-pending { margin: 12px 0 0; }
.aoe-acts { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 16px; }
/* Pushed to the right edge, as the mockup lays it out. On a narrow screen the
   row wraps and the margin collapses to nothing, so the button falls in line
   with the others rather than hanging off on its own. */
.aoe-pay { margin-left: auto; }

/* .btn-ghost had no rule of its own and fell through to the solid black .btn.
   The mockup's secondary buttons are white with a hairline, and this page is
   the only place the class is used. */
.btn.btn-ghost { background: var(--white); color: var(--ink); border-color: var(--border-2); }
.btn.btn-ghost:hover { background: var(--off-white); }
/* The one orange button on the storefront that is not an add-to-cart, reading
   --pcd-orange like every other one rather than repeating the hex. */
.btn.btn-orange { background: var(--btn-orange, var(--pcd-orange)); color: #0a0a0a; border-color: var(--btn-orange, var(--pcd-orange)); }
.btn.btn-orange:hover { background: #e07c0a; }
.btn:disabled { opacity: 0.45; cursor: default; }

@media (max-width: 640px) {
    .aoe-window, .aoe-locked { grid-template-columns: minmax(0, 1fr); gap: 10px; }
    .aoe-window-clock { font-size: 22px; }
    .aoe-line-t { text-align: left; }
    .aoe-pay { margin-left: 0; }
}


/* =========================================================================
   THE PROFILE CARDS on /account
   views/account_home.php, docs/mockups/customer-order-edit-mockup.html

   They replace the read-only .acct-you list, so they take that block's card
   treatment (surface, hairline, 14px radius) rather than inventing a second
   one. Two side by side above 760px, stacked below, which is the mockup's
   own breakpoint.

   WHY THE CLASS IS .acct-pcard AND NOT .acct-card.
   It shipped as .acct-card, which is ALREADY TAKEN, 6,800 lines above this
   block, by the ORDER card on /account/orders:

       .acct-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; }

   This block never set `display`, so that rule won and the profile card became
   a two column grid with three children. The <h2> took column one, the
   subtitle took the `auto` column beside it, and the whole <form> was left the
   1fr remainder: measured in WebKit at 768 the details form was 71.5px wide
   and its inputs 29.8px, and on the address card the longer subtitle ate the
   whole row so the form column computed to 0 and every input sat at its 26px
   minimum. Chromium measured the same, so it was never a WebKit bug and never
   about the inputs: they already carried width:100% and min-width:0, and they
   carry no size attribute. The container was the defect.

   TWO GUARDS, because renaming alone only fixes today's collision:
   1. `display: block` is stated here, so nothing matching this element on
      another class can hand it a different formatting context again.
   2. The :has() rule below does the same for ANY future card built around one
      of these forms, whatever it ends up being called.
   ========================================================================= */
.acct-prof { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }
.acct-pcard {
    display: block;
    background: var(--surface); border: 1px solid var(--border, var(--edge-02)); border-radius: 14px;
    padding: 18px 20px;
    min-width: 0;
}
.acct-pcard-h { margin: 0 0 4px; font-size: 19px; }
.acct-pcard-s { margin: 0 0 4px; color: var(--muted, var(--muted-cool)); font-size: 13.5px; }

/* THE SHARED GUARD. Whatever element wraps one of these forms, it lays its
   children out in normal flow, so a heading can never be pulled up beside its
   subtitle and a form can never be handed a leftover column. Scoped to a
   direct child so it cannot reach a form nested deep inside some other
   component. */
:is(section, article, div, li, aside):has(> .acct-form) { display: block; }

.acct-form { margin: 0; min-width: 0; }
.acct-f { margin-top: 12px; min-width: 0; }
.acct-f label { display: block; font-size: 12.5px; color: var(--muted, var(--muted-cool)); margin: 0 0 6px; letter-spacing: 0.03em; }
.acct-f input, .acct-f select {
    width: 100%;
    height: 44px;
    border: 1px solid var(--border-2);
    border-radius: 10px;
    padding: 0 12px;
    font-size: 15px;
    font-family: inherit;
    background: var(--white);
    color: var(--ink);
}
/* THE FIELD ROWS CARRY THEIR OWN FLOOR. minmax(0, ...) on every track is what
   lets a column shrink under its content instead of forcing the card wide, and
   min-width:0 on the row keeps the row itself shrinkable inside .acct-prof. */
.acct-row2 { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; min-width: 0; }
/* ZIP IS THE WIDER OF THE TWO SMALL TRACKS, by 5%. At 768 the card is 302px and
   an even 2/1/1 split gave the ZIP box 69.5px: 44.0px of room for a value that
   measures 44.1px, so a five digit ZIP sat flush against both edges with
   nowhere for the caret. State holds two letters and does not need the space.
   Measured after: ZIP 77.8px, 51.8px of room for the same 44.1px value. */
.acct-row3 { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr) minmax(0, 1.05fr); gap: 12px; min-width: 0; }
.acct-check { display: flex; align-items: center; gap: 8px; margin-top: 14px; font-size: 14px; cursor: pointer; }
.acct-check input { width: 18px; height: 18px; margin: 0; }
.acct-acts { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 16px; }
.acct-since { white-space: nowrap; }

@media (max-width: 760px) {
    .acct-prof { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 420px) {
    /* Below this the three-up city / state / ZIP row squeezes the ZIP box past
       the point where a five digit number fits, so it becomes two rows. */
    .acct-row2, .acct-row3 { grid-template-columns: minmax(0, 1fr); }
}



/* A line whose quantity was just summed into, so the merge is visible rather
   than a number that silently changed. Fades on its own; no left border. */
.qtf-lines tr.is-merged td { background: var(--fill-02); }


/* =========================================================================
   TOUCH TARGETS, 44 x 44
   docs/reports/2026-09-10-ipad-sweep.md

   WHY THIS BLOCK EXISTS AND WHY IT IS NOT INSIDE A MEDIA QUERY.
   Every control below measured under 44 x 44 in WebKit on the iPad viewports:
   the nav pills at 34px high, the inventory stepper at 30 x 28, the admin's
   small buttons at 33px. There is no width that separates "touch" from
   "mouse": an iPad Pro in landscape reports 1366 CSS pixels, the same as a
   laptop, so a min-width query would leave the largest iPad failing. The size
   is therefore unconditional, which also means one admin rather than two.

   The nav strip is 58px tall and the mobile strip 76px, so a 44px control fits
   inside both without either bar changing height. .nav-avatar keeps its 34px
   picture: the button grows by padding, not the image, so the avatar itself
   looks the same and only its hit area changes.
   ========================================================================= */

/* ---- The storefront nav ------------------------------------------------ */
.nav-item,
.nav-icon-btn,
.nav-avatar { height: 44px; min-width: 44px; }
.nav-avatar { padding: 5px; }
.order-pill { height: 44px; }
/* OPTION B OF THE NAV HEIGHT MOCKUP: the cart pill and the avatar go to 48 with
   the 80px bar. The nav items, the search button and the icons stay at their
   sizes; only the bar, the logo, the pill and the avatar grew. The avatar's
   picture grows with its button (38px inside 5px of padding) so the circle
   reads as the 48px disc in the mockup rather than a 34px picture floating in a
   48px hit area. */
.order-pill { height: 48px; }
.nav-avatar { height: 48px; min-width: 48px; width: 48px; }
.nav-brand,
.tm-brand,
.tm-cart,
.tm-burger { min-height: 44px; min-width: 44px; }

/* ---- Checkout and cart ------------------------------------------------- */
/* .btn-sm is the small button both sides of the site use; the discount "Apply"
   at checkout measured 70.2 x 40. Padding is untouched, so nothing that was
   already 44 or taller moves. */
.btn-sm { min-height: 44px; }

/* ---- The admin's action buttons ----------------------------------------
   44 ON TOUCH, 40 ON A MOUSE. The Sept 10 sweep made every admin control 44px
   unconditionally because no viewport width separates an iPad from a laptop.
   The pointer media feature does: an iPad reports pointer: coarse at every
   width and a Mac reports fine. So the rule is 40px by default and 44px under
   (pointer: coarse), which is the same admin on both, one rule apart. */
.admin-body button,
.admin-body .btn,
.admin-body a.btn,
.admin-menu-toggle { min-height: 40px; min-width: 40px; }


/* An <input type=file> is styled through its label, so the label is the target
   the finger actually lands on. */
.admin-body label.btn { min-height: 40px; }

@media (pointer: coarse) {
    .admin-body button,
.admin-body .btn,
.admin-body a.btn,
.admin-menu-toggle { min-height: 44px; min-width: 44px; }
    .admin-body label.btn { min-height: 44px; }
}


/* =========================================================================
   THE ADMIN BUTTON SYSTEM (docs/mockups/admin-buttons-mockup.html, section 1)
   Four kinds and that is all:
     .b-primary    blue, soft shadow, at most one per screen
     .b-secondary  white gradient, hairline edge, lifts on hover; the everyday one
     .b-quiet      text with an underline, for low-stakes links
     .b-danger     white with a red edge
   .b-icon is a square secondary at 40x40 (44 on touch). .b-group is several
   .btn in one pill so a row's actions read as one control.

   Scoped to .admin-body so the storefront's .btn is untouched: the storefront
   never carries these classes and its base .btn rule is not edited. Every
   admin-only button class that existed before (vnd-btn, lcv-btn, cvl-btn,
   link-action, icon-btn, dash-btn, ordv-add, usrv-rowact, rowmenu-item and the
   rest, listed in the report) is retired in favour of these.
   ========================================================================= */
.admin-body .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    height: 40px;
    min-height: 40px;
    padding: 0 16px;
    border-radius: 10px;
    border: 1px solid #d5d8de;
    background: linear-gradient(180deg, #ffffff, #f3f4f7);
    color: var(--ink);
    box-shadow: inset 0 1px 0 #fff, 0 1px 2px rgba(0, 0, 0, .06);
    font-family: inherit;
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0;
    line-height: 1;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    transition: transform .15s ease, box-shadow .15s ease, background .15s ease, border-color .15s ease, color .15s ease;
}
.admin-body .btn:hover { transform: translateY(-1px); text-decoration: none; }
.admin-body .btn:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; }
.admin-body .btn:disabled,
.admin-body .btn[aria-disabled="true"] { opacity: .55; cursor: default; transform: none; box-shadow: none; }
.admin-body .btn svg { width: 16px; height: 16px; flex: none; stroke: currentColor; fill: none; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }

.admin-body .b-danger { background: #fff; color: var(--red, #c0322b); border-color: #e6c6c3; box-shadow: none; }
.admin-body .b-danger:hover { background: #fff4f3; color: #c0322b; border-color: #d9a9a4; }

/* Square icon buttons: secondary at 40x40, the glyph in the muted ink. */
.admin-body .b-icon { width: 40px; min-width: 40px; padding: 0; color: var(--muted); }
.admin-body .b-icon svg { width: 17px; height: 17px; stroke-width: 1.8; }
.admin-body .b-icon:hover { color: var(--ink); }
.admin-body .b-danger.b-icon { color: #c0322b; }

/* Grouped row actions: one pill, hairline dividers, no lift on hover. */

.admin-body .b-group .btn {
    border-radius: 0;
    border: 0;
    border-right: 1px solid #e3e5ea;
    background: transparent;
    box-shadow: none;
    color: var(--ink);
    text-decoration: none;
    padding: 0 14px;
}
.admin-body .b-group .btn:last-child { border-right: 0; }
.admin-body .b-group .btn:hover { transform: none; background: #f3f4f7; }
.admin-body .b-group .b-danger { color: #c0322b; }
.admin-body .b-group .b-danger:hover { background: #fff4f3; }

/* A block button in a form column, and the full-width phone case. */
.admin-body .btn.b-block { width: 100%; }

/* Two transient states the screens toggle by class: a pressed segment (the
   label size and zoom pickers, is-on) and a copy button that has just copied
   (is-copied). Solid fills, the way every pill on the site is solid. */
.admin-body .btn.is-on { background: var(--surface-dark); border-color: var(--surface-dark); color: #fff; box-shadow: none; }
.admin-body .btn.is-on:hover { background: var(--surface-dark); color: #fff; transform: none; }
.admin-body .btn.is-copied { background: var(--pill-green); border-color: var(--pill-green); color: var(--pill-ink); }

@media (pointer: coarse) {
    .admin-body .btn { height: 44px; min-height: 44px; }
    .admin-body .b-icon { width: 44px; min-width: 44px; }
}


/* ---- Row controls on a mouse: 34px inside a table row ------------------
   The orders list's payment-method select and its "Message customer" button,
   and the Message / Edit / View account pill on the customers list, are
   controls that live INSIDE a table row, and at 44 (the select) and 40 (the
   buttons) they were taller than the row's own text by a wide margin and read
   as page controls dropped into a cell. The quotes list has no equivalent: its
   rows carry a status pill and nothing to press, so nothing here reaches it.

   ONLY ON A FINE POINTER AT 1200 AND UP. The same two conditions the nav uses
   to tell a laptop from an iPad: an iPad reports pointer: coarse at every
   width, so it keeps the 44px targets from the touch sweep, and under 1200 the
   admin is treated as a tablet whatever the pointer says. Font size is not
   touched; only the box and its side padding shrink.

   Written AFTER the button system and the (pointer: coarse) rule above on
   purpose: these selectors tie with .admin-body .b-group .btn (0,3,0) and with
   .admin-body .ordv-pmsel (0,2,0), so they win on source order, and the touch
   rule never applies at the same time as this one. */
@media (min-width: 1200px) and (pointer: fine) {
    /* 8px before the text, 26px after it: the chevron is drawn in the right 4
       to 14px of the box, so 26 leaves 12px between the value and the arrow.
       The two gradients move up with the box: 5px tall, centred in 34. */
    
    .admin-body .ordv-cust .btn,
    .admin-body .usrv-rowacts .btn {
        height: 34px;
        min-height: 34px;
        padding: 0 12px;
    }
}


/* Message customer on the orders list: solid green, the Paid pill's green
   (--pill-green, #1fa863) with the pill's ink (white; near-black in dark mode,
   where the pill inverts its label for the same contrast reason), a step
   darker on hover. Measured in docs/reports/2026-09-18-tracking-fold-coa-resend.md.
   Only the colours: the box, the 34px fine-pointer height above and the touch
   height are untouched. Written after .admin-body .b-secondary(:hover) and
   wins on specificity (0,3,0 / 0,4,0 against 0,2,0 / 0,3,0). */
.admin-body .ordv-cust .btn {
    background: var(--pill-green);
    border-color: var(--pill-green);
    color: var(--pill-ink);
    box-shadow: none;
}
.admin-body .ordv-cust .btn:hover {
    background: var(--btn-green-hover, #1a9457);
    border-color: var(--btn-green-hover, #1a9457);
    color: var(--pill-ink);
    box-shadow: 0 4px 10px rgba(0, 0, 0, .08);
}

/* =========================================================================
   STAT TILES: THE CHIP (docs/mockups/admin-buttons-mockup.html, section 2)
   Every admin stat bar (orders, users, inventory, products, referrals,
   abandoned carts, messages, the vnd/lcv/cvl bars on vendors, shipments,
   expenses, lab costs, research, subscriptions, catalogue, private links,
   recon, campaigns, the customer page strip, the dashboard hero) loses its
   coloured left stripe and gains a tinted icon chip beside the number. Two
   columns under 760px, every bar. The stripe rules (.ordv-accent, .*-stat.is-acc)
   are gone; the tone the stripe carried is on the chip, through
   admin_stat_tone() in the templates.
   ========================================================================= */
.stat-chip {
    width: 40px; height: 40px; border-radius: 10px; flex: none;
    display: inline-grid; place-items: center;
}
.stat-chip svg { width: 18px; height: 18px; display: block; }
/* The one text glyph: a real "$" where the tile's figure is money. */
.stat-chip-txt { font-family: var(--font-ui); font-size: 19px; font-weight: 700; line-height: 1; }
.ordv-stats-9 .stat-chip-txt { font-size: 16px; }
.dash-hlbl .stat-chip-txt { font-size: 15px; }
.stat-chip.c-blue   { background: rgba(46, 91, 224, .22);  color: #8ea2ff; }
.stat-chip.c-amber  { background: rgba(184, 119, 11, .22); color: #f0b84d; }
.stat-chip.c-red    { background: rgba(192, 50, 43, .22);  color: #ff8b83; }
.stat-chip.c-green  { background: rgba(31, 122, 62, .22);  color: #5fd38b; }
.stat-chip.c-grey   { background: rgba(255, 255, 255, .10); color: #b9bec8; }
.stat-chip.c-violet { background: rgba(124, 92, 214, .24); color: #c2b0ff; }
.stat-chip.c-teal   { background: rgba(58, 168, 184, .22); color: #7fdbe6; }
.stat-chip.c-pink   { background: rgba(194, 40, 111, .22); color: #ff8fc0; }
/* The orders bar carries nine cells: a 32px chip and a tighter gap keep the
   money figures on one line at 1024. */
.ordv-stats-9 .stat-chip { width: 32px; height: 32px; border-radius: 8px; }
.ordv-stats-9 .stat-chip svg { width: 16px; height: 16px; }
.dash-hlbl .stat-chip { width: 30px; height: 30px; border-radius: 8px; }
.dash-hlbl .stat-chip svg { width: 15px; height: 15px; }

@media (max-width: 760px) {
    .ordv-stats, .ordv-stats-9, .dash-stats,
    .prodv-stats, .invv-stats, .usrv-stats, .msgv-stats, .refv-stats, .acv-stats,
    .vnd-stats, .vnd-stats.is-four, .lcv-stats, .cvl-stats, .cpg-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}


/* ---- Inventory: the line's actions and its own Save ----------------------
   docs/mockups/admin-cleanup-mockup.html section 3 and admin-buttons-mockup
   section 3. Receive and pencil are square secondaries on the row's centreline,
   then Save: grey "Saved" until the line changes, blue "Save" once it has, a
   two-second "Saved" tick after a write, then grey again. The dirty wash is the
   mockup's warm tint. */

.admin-body .invv-save.is-done:disabled { color: var(--pill-green); }
.invv-save-note.is-bad { color: #c0322b; }





/* The user drawer's Yes/No selects sat at 67px. */
.usrd-f select { min-width: 120px; }






/* The support thread's status select held "Answered" in 105px. */
#supa-status { min-width: 132px; }

/* A row pill's segments never shrink into each other (WebKit at 390 squeezed
   "View account" under "Edit" by 3.7px). */
.admin-body .b-group .btn { flex: 0 0 auto; }
/* A hint that carries a URL breaks it rather than pushing the panel wide. */
.vnd-hint, .vnd-panel { overflow-wrap: anywhere; min-width: 0; }

/* ---- Vial mockups (/admin/vial-generator) ----
   The generator is an iframe (its own document, with the product dropdown and
   Save in its own panel). The tool fills the frame and lays itself out by the
   frame's width (docs/mockups/vial3d/vial-3d-pro.html, built into
   views/admin/vial_generator_frame.html), so this card is the one card: it
   owns the border, the radius and the clipping, and the frame's height follows
   the viewport rather than a fixed 572. min(78vh, 860px) with a 560px floor;
   on a phone or a portrait tablet, the viewport less 200px, same floor. This
   page adds a tinted status line for the frame's no-WebGL message, no accent
   edge. */

.vgn-status.is-bad { background: var(--pill-red-bg); border-color: #f0c9c9; }


/* =========================================================================
   THE LIGHT THEME, REBUILT (2026-09-13, docs/reports/2026-09-13-light-theme.md)
   Approved mockup: docs/mockups/light-storefront-mockup.html.

   SCOPE, AND WHY IT IS THE WHOLE OF IT. Every rule in this block is under
   :root[data-theme="light"], which matches only when the visitor has explicitly
   chosen light. Two things follow, and both were the brief:

     DARK IS UNTOUCHED. Not "carefully kept the same" -- unreachable. A selector
     that begins :root[data-theme="light"] cannot match a document carrying
     data-theme="dark", so there is no value here dark could pick up, and the
     dark computed styles are compared against HEAD surface by surface to say so
     rather than to assume it.

     THE ORIGINAL MIXED STATE IS UNTOUCHED TOO, for now. A visitor who has never
     pressed the toggle still gets exactly the page they got yesterday. The
     report recommends retiring that state and says what it costs; until the
     owner says so, it stays, and this block does not reach it.

   THE ONE TOKEN OUTSIDE THIS BLOCK is --btn-orange, declared at :root as
   var(--pcd-orange) so every other state resolves to the colour it already had.

   NO backdrop-filter AND NO LEFT ACCENT BORDER is added by anything here, and
   the two the storefront already had on the scrolled bar are switched off in
   light rather than left to blur a matte panel that does not need it.
   ========================================================================= */

:root[data-theme="light"] {
    /* THE PAGE. White, not --off-white: the mockup's ground is #ffffff and the
       wash below supplies every tint the page has. */
    --lt-page:   #ffffff;
    /* THE FIELD: TWO POOLS, AND WHAT EACH ONE PEAKS AT.
       The ground is white and stays white; the blue is a wide diffuse field
       across the top fading to pure white, and a softer one low in the viewport.

       THE PEAKS ARE ARITHMETIC. --accent-blue is #2E5BE0, so a pool at alpha a
       over white lands red at 255 - 209a, green at 255 - 164a and blue at
       255 - 31a. Pool A at 0.200 is (213, 222, 249), forty-two off white on the
       channel that moves most; pool B at 0.105 is (233, 238, 252), twenty-two.

       THEY HAVE BEEN 0.028 AND 0.086 AND BOTH WERE TOO FAINT. Six of 255 is
       under the perceptual floor; eighteen is visible but reads as a tint on a
       corner rather than as light in the page, because the pools were also too
       small: each sat off one corner at under a viewport wide, so the middle of
       the screen never saw either of them. The size is half of this change and
       the alpha is the other half.

       BLUE ONLY. Every stop is --accent-blue at an alpha, so the field cannot
       drift toward purple or pink at any point in the ramp: there is one hue and
       the fade is to transparency, not to another colour. */
    --lt-pool-a: rgba(46, 91, 224, 0.200);
    --lt-pool-b: rgba(46, 91, 224, 0.105);
    --lt-pool-0: rgba(46, 91, 224, 0);

    /* Matte black. Already the site's --surface-dark; named again here so the
       four surfaces the brief lists read as one decision. */
    --lt-matte:  #1c1c1c;

    /* THE BUTTON ORANGE, DARKENED, AND BY EXACTLY HOW MUCH.
       White on --pcd-orange #f7890f is 2.46:1. Scaling the channels toward
       black in the same proportion -- which moves lightness and leaves the hue
       angle and the saturation ratio where they were -- reaches 4.5:1 at a
       factor of 0.718 (#b1620b, 4.54:1). This is 0.698, #ac600a, 4.74:1: the
       same margin for antialiasing the footer glow took, one step further down
       than the bare pass. In words, the button's orange gives up about 30% of
       its light and keeps its colour. --pcd-orange itself does not move, so the
       hero figures, the nav icons, the footer glow and the footer hairline are
       the brand orange on every page in every state. */
    --btn-orange: #ac600a;
}

/* ---- The page: WHITE, and it stays white ------------------------------
   The ground is one colour and it is #ffffff. body is the flex column that
   holds the ticker, the main region and the footer, so its box runs from the top
   of the first to the bottom of the last, and it is the only thing painting a
   ground: --cat-page goes transparent below and the catalog's fixed glow layer
   is switched off, so no section paints its own colour inside that run and there
   is no edge for a seam to live on.

   position: relative is the only structural line here, and it is what the two
   pools below are positioned against. body is a flex column and that does not
   change: the pools are position: absolute, so they are out of flow and are not
   flex items. */
:root[data-theme="light"] body,
:root[data-theme="light"] body.catalog-dark {
    background: var(--lt-page);
    color: var(--ink);
    position: relative;
}

/* ---- The two pools ------------------------------------------------------
   Two soft ellipses of --lt-pool on the white, widely separated: one off the top
   left, one off the lower right, so the space between them is the page's own
   white and not a lighter blue. They are body's own pseudo-elements, so this is
   still CSS only and there is no wrapper to keep in step with the markup.

   z-index -1 puts them behind every flow child of body and in front of body's
   own background, which is what makes them a layer on the white rather than a
   replacement for it. The footer is a flow child and opaque, so it paints over
   them: it stays flat matte black and takes no wash, by construction.

   POSITION: FIXED, WHICH IS THE PATTERN THIS SITE ALREADY USES for a page glow
   (.catalog-dark .catalog-bg is fixed, inset 0, z-index -1) and which is also
   the only way to hang a pool past the edge of the page without creating one.
   Absolutely positioned, they were scrollable overflow: pulled 26% past the
   right edge they took a 1440 page to 1728 and a 390 page to 468, which is a
   horizontal scrollbar on every storefront page. Fixed elements contribute no
   scrollable overflow at all, so the pools can sit half off the viewport, which
   is what keeps a pool's boundary from ever being visible.

   SIZED IN vw AND vh so they scale with the viewport instead of being a
   phone-sized blob at 390 and a smear at 1440.

   THE GRADIENT FADES TO NOTHING AT 72% of the closest side, not at the box edge.
   That is what keeps the tint inside the middle of each box and leaves the rest
   of the page at 255,255,255: the box is the drift envelope, not the pool.

   ONLY opacity AND transform ARE ANIMATED. Both are compositor properties: the
   browser paints each pool once and re-blends it. background-position would
   repaint the layer every frame for the life of the page, and a filter would
   re-rasterize it; neither is used, here or anywhere in this block. */
:root[data-theme="light"] body::before,
:root[data-theme="light"] body::after {
    content: "";
    position: fixed;
    z-index: -1;
    pointer-events: none;
    will-change: opacity, transform;
}
/* POOL A: THE FIELD ACROSS THE TOP, and the reason it is not a circle.
   The box is 160vw wide and hangs 30vw off each side, and the ellipse inside it
   has a 60% horizontal radius, so the tint runs from about -16vw to 141vw: there
   is no edge of it anywhere on screen at any width, which is what makes it read
   as light in the page rather than as a shape on it.

   THE CREST SITS BELOW THE CHROME, not at the top of the box. Centred at 33% of
   a 72vh box that starts 10vh above the viewport puts it near 14vh, which is
   just under the nav pill: a crest hidden behind the opaque ticker and bar would
   be a peak nobody could see and a measurement nobody could take.

   FOUR STOPS, so the ramp is long. Two would reach white in a hard shoulder; the
   0.115 and 0.045 steps stretch the falloff so it arrives at nothing around 58%
   down the first viewport, which is the brief's "roughly 60 percent". */
:root[data-theme="light"] body::before {
    top: -10vh;
    left: -30vw;
    width: 160vw;
    height: 68vh;
    background: radial-gradient(ellipse 60% 82% at 58% 33%,
        var(--lt-pool-a) 0%, rgba(46, 91, 224, 0.115) 34%, rgba(46, 91, 224, 0.045) 60%, var(--lt-pool-0) 82%);
    opacity: 0.85;
    animation: lt-pool-a 23s ease-in-out infinite;
}
/* POOL B: SMALLER AND SOFTER, LOW IN THE VIEWPORT. Half the peak, a shorter
   ramp, and placed left of centre so it does not sit under pool A's own crest
   and add to it: the strongest point on the page is pool A's and only pool A's. */
:root[data-theme="light"] body::after {
    bottom: -18vh;
    left: -18vw;
    width: 96vw;
    height: 62vh;
    background: radial-gradient(ellipse 62% 74% at 38% 58%,
        var(--lt-pool-b) 0%, rgba(46, 91, 224, 0.05) 42%, var(--lt-pool-0) 78%);
    opacity: 0.8;
    animation: lt-pool-b 29s ease-in-out infinite;
}
/* 23 and 29 seconds: both inside the brief's 20 to 30, and coprime, so the two
   crests are never in the same place twice inside an hour. They cross-fade,
   which is why one starts at its trough and the other at its crest. */
@keyframes lt-pool-a {
    0%, 100% { opacity: 0.55; transform: translate3d(-3%, -2%, 0) scale(1); }
    50%      { opacity: 1;    transform: translate3d(4%, 3%, 0) scale(1.12); }
}
@keyframes lt-pool-b {
    0%, 100% { opacity: 1;    transform: translate3d(3%, 2%, 0) scale(1.1); }
    50%      { opacity: 0.5;  transform: translate3d(-4%, -3%, 0) scale(0.95); }
}
/* Not optional. The pools keep the opacity their own rule declares and simply
   stop moving, which is what this setting is for: the page is still the page. */
@media (prefers-reduced-motion: reduce) {
    :root[data-theme="light"] body::before,
    :root[data-theme="light"] body::after { animation: none; }
}

/* The catalog's three blue blooms are a dark-page device: on white they are a
   second, competing wash. The mockup has one. */
:root[data-theme="light"] .catalog-dark .catalog-bg { display: none; }
/* Same reason, on the hero: its drift blooms were painted for #0d0d0f. */
:root[data-theme="light"] .hero { background: transparent; }
:root[data-theme="light"] .hero-drift { display: none; }
/* The catalog region stops painting a ground so the wash is the ground. */
:root[data-theme="light"] body.catalog-dark {
    --cat-page: transparent;
    /* THE PLATE. The exports are cut-out vials tuned against dark, so on a white
       card they need the dark behind them the photograph was lit for. It is the
       card's own media box, flush to the card edge, so it takes the card's inner
       radius from the card's overflow:hidden and adds no geometry of its own.
       The mockup insets the plate by the card's padding; that is not adopted,
       because the media box is flush in the real markup and insetting it in
       light only would make a light card and a dark card different sizes and
       shift the whole grid on every toggle. */
    --cat-media-bg: var(--lt-matte);
    --cat-media-line: transparent;
}
/* The same plate on the cards that are NOT inside a catalog page: the product
   page's related rail is a .pcard on a plain light body. */
:root[data-theme="light"] .pcard-media { background: var(--lt-matte); }

/* ---- The announcement ticker ------------------------------------------
   Matte black with bright white bold text, replacing the brand-orange bar with
   near-black on it. 15.44:1 measured, against the orange bar's 8.05:1. */
:root[data-theme="light"] .announce {
    background: var(--lt-matte);
    color: #ffffff;
}
:root[data-theme="light"] .announce-msg,
:root[data-theme="light"] .announce-still { font-weight: 700; }

/* ---- The nav ------------------------------------------------------------
   Matte black floating on the wash, at the 69px height and 45px logo the bar
   already has: nothing here touches geometry. On the catalog pages the pill was
   a 3% white wash designed to sit on a black page; on white it is invisible.

   THE SCROLLED STATE STOPS BLURRING. Its whole job was to fill the pill in once
   content passed under it, which a matte panel already does, and backdrop-filter
   on an opaque surface is a per-frame rasterize that buys nothing. Off here, in
   light, on both the desktop pill and the mobile strip. */
:root[data-theme="light"] .navbar,
:root[data-theme="light"] .topbar.is-scrolled .navbar {
    background-color: var(--lt-matte);
    border-color: rgba(255, 255, 255, 0.07);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}
:root[data-theme="light"] .topbar.is-scrolled .topbar-mobile {
    background-color: var(--lt-matte);
    border-bottom-color: rgba(255, 255, 255, 0.07);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}
/* The pill sits on a white page now, so it needs the lift the mockup gives it. */
:root[data-theme="light"] .navbar { box-shadow: 0 10px 30px rgba(20, 30, 60, 0.10); }

/* ---- The hero carousel cards -------------------------------------------
   Matte black with white text. The plate behind the vial was a blue-black
   gradient (#1b2233 to #151a25, and a lighter pair on the centre card); flat
   matte in light, one surface with the card, as the mockup draws it. The centre
   card keeps its scale, its blue rim and its shadow, and the Lot tested chip and
   the View button keep the green and the blue they already carry. */
:root[data-theme="light"] .cf-card { background: var(--lt-matte); }
:root[data-theme="light"] .cf-top { background: transparent; }

/* ---- The footer has NO light override any more -------------------------
   It had three: a matte background, a lifted body colour and a rule hiding the
   animated glow and the orange top hairline. v4 is one footer in both themes, it
   declares its own --ft-* palette on .site-footer itself, and the two things the
   light rules used to hide no longer exist. Overriding any of it here would be
   the light theme reaching into a band that is deliberately the same on both
   sides of the toggle. */

/* ---- The Safari toolbar sampler ----------------------------------------
   THE MECHANISM IS UNTOUCHED: still a 1px position:fixed strip at the top edge,
   still the only element Safari can sample, still rendered only when the ticker
   is on. Only the colour it holds up moves. It was painted --pcd-orange because
   the announcement ticker was orange and the toolbar's whole job is to match it;
   in light the ticker is the matte panel, so the sampler is too, and Safari's
   toolbar goes on matching the bar directly beneath it instead of a colour that
   left the page. Dark keeps the orange, because there the ticker still is. */
:root[data-theme="light"] .tint-sampler { background: var(--lt-matte); }

/* ---- Orange buttons -----------------------------------------------------
   --btn-orange carries the darkened colour; this is the one rule that wrote a
   label colour by hand instead of reading --btn-ink. The .pcard-cart override
   that sat here went with the orange: the quick-add is the site blue now and
   its base rule already says white. */
:root[data-theme="light"] .btn.btn-orange { color: #ffffff; }

/* ---- The copy that was written for a black page -------------------------
   The nine catalog pages and the home hero were dark-only when they were built,
   so their headings, leads, crumbs and chip labels are hardcoded #fff and
   white-alpha rather than tokens. On the rebuilt light page every one of them is
   white on white. They are listed here one by one, mapped onto tokens this
   stylesheet already has, and every value was chosen by measuring it: the report
   prints the ratio for each.

   --lt-muted is the ONE new value, and it is the mockup's own --mute. The
   site's --muted (#6b6b6b) measures 4.58:1 against the darkest point of the
   wash, which passes and leaves nothing for antialiasing; #5b6068 measures
   5.45:1 there and 6.33:1 on white. */
:root[data-theme="light"] { --lt-muted: #5b6068; }

/* The shared glass pill: the band's Shop-all, the stock segment and the class
   chips all wear .glass-ctl. One override, three surfaces, and it is the
   mockup's white pill with a hairline and a soft shadow. */
:root[data-theme="light"] .glass-ctl {
    background: var(--surface);
    border-color: var(--line);
    box-shadow: 0 2px 8px rgba(20, 30, 60, 0.05);
}

/* Home hero */
:root[data-theme="light"] .hero-badge { background: #eef3ff; border-color: #c9d6f5; color: #2a4bb8; }
:root[data-theme="light"] .hero-h1 { color: var(--text-2); }
:root[data-theme="light"] .hero-h1-line2 { color: var(--link); }
:root[data-theme="light"] .hero-subhead { color: var(--lt-muted); }
:root[data-theme="light"] .hero-btn-secondary {
    background: var(--surface);
    border-color: var(--line);
    color: var(--text-2);
    box-shadow: 0 2px 8px rgba(20, 30, 60, 0.06);
}
:root[data-theme="light"] .hero-btn-secondary:hover { background: var(--surface-4); }
/* The hero stat bar has NO light override any more: it is matte black in both
   themes (2026-09-14, the base .hero-stats rules), so the four rules that used
   to sit here would only have put a white card back on the white page. */
:root[data-theme="light"] .home-band-shop { color: var(--text-2); }
:root[data-theme="light"] .home-band-shop:hover,
:root[data-theme="light"] .home-band-shop:focus-visible { color: var(--text-2); border-color: var(--edge-16); }
:root[data-theme="light"] .home-band-count { color: var(--text-2); }

/* The class, parent and shop page heads */
:root[data-theme="light"] .pclass-crumb { color: var(--lt-muted); }
:root[data-theme="light"] .pclass-crumb a:hover { color: var(--text-2); }
:root[data-theme="light"] .pclass-crumb-current { color: var(--text-2); }
:root[data-theme="light"] .pclass-h1 { color: var(--text-2); }
:root[data-theme="light"] .pclass-lead { color: var(--text-body); }
:root[data-theme="light"] .pclass-back,
:root[data-theme="light"] .pclass-morebtn { color: var(--link); }
:root[data-theme="light"] .pparent-name { color: var(--text-2); }
:root[data-theme="light"] .pparent-blurb { color: var(--text-body); }
:root[data-theme="light"] .pparent-count,
:root[data-theme="light"] .pparent-examples { color: var(--lt-muted); }
:root[data-theme="light"] .pparent-examples b { color: var(--text-2); }

/* The shop toolbar and the class strip */
:root[data-theme="light"] .shop-seg-item { color: var(--lt-muted); }
:root[data-theme="light"] .shop-seg-item:hover,
:root[data-theme="light"] .shop-seg-item:focus-visible { color: var(--text-2); }
/* The chosen one has to be the filled pill, not a white pill on a white pill:
   the same inversion --cat-chip-on-bg already makes on the catalog's own chips. */
:root[data-theme="light"] .shop-seg-item.is-on { background: var(--text-2); color: var(--surface); }
:root[data-theme="light"] .shop-sort-label,
:root[data-theme="light"] .shop-strip-label,
:root[data-theme="light"] .shop-class-count { color: var(--lt-muted); }
:root[data-theme="light"] .shop-class,
/* The phone's collapsed "Browse by class" button, which is the strip at 900 up. */
:root[data-theme="light"] .shop-classes-btn { color: var(--text-2); }
:root[data-theme="light"] .shop-class:hover,
:root[data-theme="light"] .shop-class:focus-visible { color: var(--text-2); border-color: var(--edge-16); }

/* The footer's column headings were rgba(255,255,255,.35), which is 3.20:1 on
   the matte panel. .55 is 5.98:1. Light only: the footer elsewhere keeps the
   value it has. */
/* .footer-heading moved to the base rule: one value clears both footers. */

/* Two on the product page that were already under the floor on the light pages
   this site has always had (--muted-3 is 1.98:1 and --muted-2 is 2.81:1 on
   --surface). They are not on the brief's list of surfaces; they are on the
   page the brief makes primary, so they are fixed here and named in the report
   rather than left for someone to find in the new look. */
:root[data-theme="light"] .pd-crumb span { color: var(--lt-muted); }
:root[data-theme="light"] .pd-crumb-current { color: var(--text-2); }
:root[data-theme="light"] .pd-size-label { color: var(--lt-muted); }

/* ---- The hero carousel cards, above 1201 --------------------------------
   THE CARDS WERE ALREADY LIGHT UP HERE, in every theme: the desktop block at
   @media (min-width: 1201px) turns them into off-white plates with dark text,
   from docs/mockups/carousel-light-cards.html, because an off-white card reads
   as an object on a near-black page. On a white page it is the page, and the
   approved mockup draws them matte black with white text. Below 1201 the cards
   were always dark, so only the surface and two muted values move there.

   Everything the brief says to keep is kept: the centre card stays scaled and
   raised, the Lot tested chip stays green and the View button stays blue.

   The chip and the shadow take the MOCKUP's values rather than the dark page's.
   The dark card's chip is #4cc98a on a 31,168,99 tint and the centre card's lift
   is a blue bloom; both were tuned to belong to a near-black page, and on white
   the bloom reads as a smudge under the card. #69d693 measures 9.45:1 on the
   matte and 6.25:1 on its own tint over it; the dark page's #4cc98a would also
   have passed, and this is the colour that was approved. */
:root[data-theme="light"] .cf-card {
    color: #ffffff;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
/* ONE SHADOW FOR EVERY CARD NOW. There were three, keyed to the three
   positions that no longer exist; the ring gives depth through scale, Y
   rotation and the fade, and the mockup draws the same shadow on every card
   whatever its offset. */
:root[data-theme="light"] .cf-card {
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08), 0 26px 60px rgba(15, 22, 45, 0.28);
}
:root[data-theme="light"] .cf-name,
:root[data-theme="light"] .cf-price { color: #ffffff; }
/* The site's --chrome-muted is 3.49:1 on the matte; the mockup's own size grey
   is 6.34:1 there. */
:root[data-theme="light"] .cf-size { color: #9a9ea6; }
:root[data-theme="light"] .cf-noimg { color: #9a9ea6; }
:root[data-theme="light"] .cf-tested { background: rgba(31, 122, 62, 0.22); color: #69d693; }
/* The rail's own eyebrow sits on the PAGE, not on a card, and it was white at 42
   per cent for a near-black page. It is the muted label the rest of the light
   page uses. */
:root[data-theme="light"] .cf-head span { color: var(--lt-muted); }

/* ---- The page wrappers that painted their own ground --------------------
   .page (cart, contact, wholesale, affiliates, first responders, legal) and
   .coalib-page both fill the viewport width with --off-white. On the rebuilt
   light page that is an opaque #fafafa slab laid over the wash: the wash stops
   at the top of it and starts again under the footer, which is exactly the seam
   the brief rules out. Transparent, so the ONE layer on body is the ground on
   every storefront page.

   .login-page is NOT in this list and is deliberate. It is a full-viewport auth
   surface with its own light, dark and original blocks (--lg-bg and the glow
   blobs), it carries neither the ticker nor the footer, and it is not one of the
   surfaces the brief lists. The wash does not run there; the report says so
   rather than leaving it to be found. */
:root[data-theme="light"] .page,
:root[data-theme="light"] .coalib-page { background: transparent; }

/* =========================================================================
   THE WHOLE-SITE LIGHT SWEEP (2026-09-13-light-sweep.md)

   The light rebuild's contrast pass covered seven pages. The sweep that should
   have been run covers all 36 storefront surfaces, and it found 40 elements
   below AA on 13 of them. They are two families and nothing else.

   FAMILY ONE, 23 elements on the four marketing pages. wholesale, affiliates,
   first responders and responder apply carry body.catalog-dark, so widening the
   light catalog token block to body.catalog-dark turned them light; their own
   copy was written for a black page and hardcodes #fff and the dark palette's
   --ink-01, --ink-12, #6f92ff, #3ddc97, #e9edf6, #bfe9d6. Every one of them was
   white or near-white on white, including the first responders H1, which is the
   page's whole headline.

   FAMILY TWO, 17 elements on pages that were light before any of this existed.
   --muted-2 (#9a9a9a, 2.81:1), --muted-3 (#b8b8b8, 1.98:1), --muted-grey
   (#8a8a90, 3.43:1), --ink-89 (#b06a00, 4.28:1) and two greens on their own
   tints have always been under the floor on the contact, legal, account, verify,
   pay and order-pay pages. They were not introduced here and they are not fixed
   in dark, where those tokens carry different values that pass; they are fixed
   in light, because light is now what everybody sees.

   EVERY VALUE BELOW IS A TOKEN THIS STYLESHEET ALREADY HAS, and the two hue
   families copy the light values --seo-hue was already given for the same job.
   Nothing here is a new colour.
   ========================================================================= */

/* ---- Headings that were #fff ------------------------------------------- */
:root[data-theme="light"] .ws-h1,
:root[data-theme="light"] .ws-h2,
:root[data-theme="light"] .ws-tile-h,
:root[data-theme="light"] .af-calc-h,
:root[data-theme="light"] .af-step-h,
:root[data-theme="light"] .rsp-h2,
:root[data-theme="light"] .rsp-step b,
:root[data-theme="light"] .af-slider label b,
:root[data-theme="light"] .rsp-privacy b { color: var(--text-2); }

/* ---- Body copy and field labels that were --ink-01 / --ink-12 ---------- */
:root[data-theme="light"] .ws-lede,
:root[data-theme="light"] .ws-tile-p,
:root[data-theme="light"] .ws-formhead-p,
:root[data-theme="light"] .ws-field label,
:root[data-theme="light"] .af-stat-p,
:root[data-theme="light"] .af-calc-sub,
:root[data-theme="light"] .af-step-p,
:root[data-theme="light"] .af-slider label,
:root[data-theme="light"] .af-payout-lbl,
:root[data-theme="light"] .rsp-label,
:root[data-theme="light"] .rsp-drop b,
:root[data-theme="light"] .rsp-consent,
:root[data-theme="light"] .rsp-privacy { color: var(--lt-muted); }

/* ---- The blue eyebrows and legends ------------------------------------- */
:root[data-theme="light"] .ws-eyebrow,
:root[data-theme="light"] .ws-legend { color: var(--link); }

/* ---- The green figures and eyebrows ------------------------------------
   --ink-05 is the light theme's own green and is already what this stylesheet
   reaches for when a green has to be read rather than glow. */
:root[data-theme="light"] .ws-eyebrow-form,
:root[data-theme="light"] .rsp-rate,
:root[data-theme="light"] .af-payout-amt,
:root[data-theme="light"] .af-cur { color: var(--ink-05); }

/* ---- The affiliate stat hues ------------------------------------------
   Same shape and the same three values as the light --seo-hue block already
   two hundred lines above: a triplet per data attribute, swapped for the mode.
   5.36:1, 6.15:1 and 6.64:1 on white against the neon originals' 2.14, 1.77
   and 2.72. */
:root[data-theme="light"] .af-stat[data-af-hue]          { --af-hue: 14, 116, 144; }
:root[data-theme="light"] .af-stat[data-af-hue="green"]  { --af-hue: 18, 112, 63; }
:root[data-theme="light"] .af-stat[data-af-hue="cyan"]   { --af-hue: 14, 116, 144; }
:root[data-theme="light"] .af-stat[data-af-hue="violet"] { --af-hue: 91, 70, 201; }

/* ---- The step number, which is painted BY a gradient --------------------
   background-clip:text with color:transparent, so the ink is the gradient and
   the bottom of it fades to 15% alpha: on black that reads as a number falling
   away, on white it is a number that is not there. One solid colour in light,
   and the clip has nothing left to clip. */
:root[data-theme="light"] .af-step-n { background: none; color: var(--link); }

/* ---- The greys that were always under the floor on the light pages ----- */
:root[data-theme="light"] .hint,
:root[data-theme="light"] .field-help,
:root[data-theme="light"] .vfy-eyebrow,
:root[data-theme="light"] .lg-toc-t,
:root[data-theme="light"] .lg-rel-t,
:root[data-theme="light"] .acct-credit-k,
:root[data-theme="light"] .acct-dl-sub,
:root[data-theme="light"] .acct-date,
:root[data-theme="light"] .ctv-rowl,
:root[data-theme="light"] .opay-ordn { color: var(--lt-muted); }

/* The amount owed was --ink-89 (#b06a00, 4.28:1). #8a5a00 is 5.93:1 and is the
   value the catalog's own backorder label already uses in light. */
:root[data-theme="light"] .acct-owe { color: #8a5a00; }

/* Two greens on their own pale tints: the stock pill at 3.93:1 and the paid
   eyebrow at 2.75:1. #146b45 is the hero card chip's measured light green and
   --ink-05 is the light green everywhere else here. */
/* .st-green moved to the base rule: the pill is the same in both modes. */
:root[data-theme="light"] .opay-eyebrow { color: var(--ink-05); }
/* White on --pill-green measured 3.07:1; on --ink-05 it is 6.15:1, and the tick
   is a 15px disc where the glyph is the whole content. */
:root[data-theme="light"] .opay-tick { background: var(--ink-05); }

/* =========================================================================
   THE DARK HALF OF THE SWEEP (2026-09-13-light-sweep.md)

   The same whole-site sweep in dark found 15 selectors below AA, every one of
   them there before this run. Three were the same value failing in both modes
   and are fixed in their base rules above. These five are dark's alone.

   THIS GIVES UP THE BYTE-IDENTICAL-TO-HEAD PROPERTY dark has held through the
   light rebuild, deliberately and on the owner's instruction to take both modes
   to zero. What moves in dark is exactly what is below plus the three base rules:
   nothing else.
   ========================================================================= */

/* THE CHROME ISOLATION BLOCK REACHES TOO FAR ON THESE NINE PAGES, and this is
   the actual cause of five of the fifteen. That block resets every token that
   differs to its LIGHT value inside the chrome shells so the nav and the footer
   render identically in both modes; its first selector is the catalog page's
   BODY, so on wholesale, affiliates, first responders and responder apply the
   whole page, not just its chrome, gets light-mode greys on a #070910 ground.
   --muted-steel lands at #5c6880 (3.55:1) and --muted-warm at #6d717c (4.08:1).

   Put back on the body only, after that block. The chrome elements declare these
   tokens on themselves in the block above, and a token declared on the element
   beats one inherited from an ancestor, so .topbar, .navbar and .site-footer are
   untouched and still render identically in both modes. */
:root[data-theme="dark"] body.catalog-dark:not(.home-body):not(.f-body) {
    --muted-steel: #a3a7ae;
    --muted-warm:  #a3a7ae;
}

/* The hero rail's eyebrow and its View all link, on the dark home page. The
   eyebrow was white at .42 (4.04:1) and is .50 (5.33:1); the link was
   --chrome-link #2e5be0 (3.49:1) and is #8fadf5, which is the blue the dark
   catalog already uses for a link on this exact ground (--cat-link), at 8.97:1.
   Light is unaffected: it has its own values for both, measured earlier. */
:root[data-theme="dark"] .cf-head span { color: rgba(255, 255, 255, 0.50); }
:root[data-theme="dark"] .cf-head a { color: #8fadf5; }

/* The login card's trust line and the dots between its items: white at .42
   (4.10:1) and at .22 (1.94:1) on the card's near-black. .50 and .45 measure
   5.34:1 and 4.72:1. The light login card declares both as #6b6b6b and is not
   touched. */
:root[data-theme="dark"] body.login-body {
    --lg-trust: rgba(255, 255, 255, 0.50);
    --lg-trust-dot: rgba(255, 255, 255, 0.45);
}

/* =========================================================================
   THE CALCULATION ON AN ORDER (2026-09-14,
   docs/reports/2026-09-14-discount-slot-and-order-detail.md)

   Layout only. The words come from OrderBreakdown and the type is the page's
   own (.muted .small, .mono, .cpg-sz, .usrd-pmeta); the cause chip is the
   admin's existing .ordv-pill in its five solid tones, made small enough to
   sit inside a line of 12px text. No new component, no accent border.
   ========================================================================= */
.cpg-money-why, .usrd-or-why { padding-top: 0 !important; }
/* The sentence under a money row wraps; the row rule keeps its last span on one line for figures. */
.usrd-or-why span:last-child { white-space: normal; text-align: left; }
.cpg-sz .ordv-pill,
.usrd-pmeta .ordv-pill { font-size: 11px; padding: 2px 8px; vertical-align: middle; }