/* ==========================================================================
   ModHost — Twenty-One child theme
   Custom stylesheet. Auto-loaded last by the parent's includes/head.tpl,
   so equal-specificity overrides win on source order alone. See CLAUDE.md
   for the rules this file has to follow before touching anything below.
   ========================================================================== */


/* ==========================================================================
   1. TOKENS
   ========================================================================== */

/* --------------------------------------------------------------------------
   1a. Design tokens — ported from brand/tokens.css (source of truth).
   Do not hand-edit values here; update brand/tokens.css and re-port.
   Twenty-One's theme.css is compiled with hardcoded hex (see CLAUDE.md),
   so these variables paint nothing on their own — every component
   override below still needs an explicit rule against the real selector.
   -------------------------------------------------------------------------- */

:root {

  /* --- 1. Colour — raw ramps ------------------------------------------- */

  /* Brand green. 400 is the ModHost accent mark (logo, dots, highlights).
     It is too light to carry white text, so interactive surfaces use 600. */
  --mh-green-100: #E9F6EE;   /* tint fill (alert/badge backgrounds)       */
  --mh-green-200: #C9EADA;   /* tint border                               */
  --mh-green-300: #8FDCA9;
  --mh-green-400: #46C36D;   /* ACCENT — brand mark, highlights           */
  --mh-green-500: #2E9E55;
  --mh-green-600: #1E7A44;   /* ACCENT INK — primary buttons, links       */
  --mh-green-700: #1A6B3C;   /* primary hover                             */
  --mh-green-800: #14532D;   /* primary active, link hover                */
  --mh-green-900: #0F2C1B;   /* text on a green tint                      */

  /* Warm neutral ("stone"). The site is built on warm paper, not pure
     white/grey — always take neutrals from this ramp.                    */
  --mh-stone-50:  #FFFFFF;   /* raised surface: cards, tables, modals     */
  --mh-stone-100: #FAF9F6;   /* PAPER — page background, input fills      */
  --mh-stone-150: #F3F1EC;   /* hover fill on paper                       */
  --mh-stone-200: #EFEDE8;   /* SUNKEN — table heads, panel heads, wells  */
  --mh-stone-300: #E3E0D9;   /* divider on a sunken surface               */
  --mh-stone-400: #C9C6BE;   /* disabled border, empty-state art          */
  --mh-stone-500: #9AA0A6;   /* muted text on dark surfaces               */
  --mh-stone-600: #8A8E94;   /* placeholder text                          */
  --mh-stone-700: #5A5E64;   /* secondary text, table labels              */
  --mh-stone-800: #3A3D42;   /* body text                                 */
  --mh-stone-900: #1A1D21;   /* INK — headings, footer background         */

  /* Semantic hues. Each role carries six values:
       <role>          solid fill / strong border  (white text on it)
       <role>-hover    solid fill, hovered
       <role>-active   solid fill, pressed  (added 4S)
       <role>-ink      text + icon colour on a tint
       <role>-tint     tinted background
       <role>-edge     border for a tinted surface

     -active derivation (4S): --mh-primary-active is simply green-800, the
     next rung of the hand-built brand ramp. The semantic roles have no
     ramp, so -active continues each role's own solid -> hover step by the
     same HSL lightness delta, hue and saturation held constant. Measured
     deltas were 5.6-6.7 points, so the ladders stay comparable in feel:
       success 40.0 -> 34.1 -> 28.2    warning 39.4 -> 33.3 -> 27.3
       danger  33.3 -> 26.7 -> 20.0    info    33.1 -> 27.5 -> 21.8
     Note the roles are NOT uniform about where -ink sits: for success and
     info, -active is lighter than -ink; for warning and danger it is
     darker. Check before assuming a single ordering.

     COLLISION: --mh-success-ink and --mh-primary-active are the same
     value, #14532D (both resolve through green-800, which --mh-link-hover
     also uses). Any rule that fills a surface with success-ink therefore
     renders it identically to a pressed primary button. .btn-success hit
     exactly this in 4S and now rests on --mh-success-active instead.
     Success-ink is still correct as TEXT on a success tint; it is fills
     that collide.                     */

  /* success — deliberately the BRIGHT accent so it reads differently from
     primary (deep green). "Active", "Paid", "Completed".                 */
  --mh-success:        #2E9E55;
  --mh-success-hover:  #26884A;
  --mh-success-active: #1F713D;
  --mh-success-ink:    #14532D;
  --mh-success-tint:   #E9F6EE;
  --mh-success-edge:   #B9DFC7;

  /* warning — ochre, sits inside the warm palette. "Pending", "Due".     */
  --mh-warning:        #B57614;
  --mh-warning-hover:  #9A6410;
  --mh-warning-active: #7E520D;
  --mh-warning-ink:    #8A5A0C;
  --mh-warning-tint:   #F7EEDD;
  --mh-warning-edge:   #E5D2B2;

  /* danger — muted brick, already used by the site's form errors.
     "Suspended", "Overdue", "Cancelled", destructive actions.            */
  --mh-danger:         #8A2320;
  --mh-danger-hover:   #6E1C1A;
  --mh-danger-active:  #521514;
  --mh-danger-ink:     #8A2320;
  --mh-danger-tint:    #F7EBEA;
  --mh-danger-edge:    #DEBFBD;

  /* info — desaturated slate blue; the only non-warm hue, kept quiet.    */
  --mh-info:           #2B5C7E;
  --mh-info-hover:     #234C69;
  --mh-info-active:    #1C3C53;
  --mh-info-ink:       #234C69;
  --mh-info-tint:      #E8EFF4;
  --mh-info-edge:      #BFD2DE;

  /* --- 2. Colour — semantic roles --------------------------------------- */

  --mh-bg:              var(--mh-stone-100);  /* page                     */
  --mh-bg-sunken:       var(--mh-stone-200);  /* wells, table heads       */
  --mh-surface:         var(--mh-stone-50);   /* cards, modals, dropdowns */
  --mh-surface-hover:   var(--mh-stone-150);
  --mh-surface-inverse: var(--mh-stone-900);  /* footer, dark bars        */

  --mh-text:            var(--mh-stone-900);
  --mh-text-body:       var(--mh-stone-800);
  --mh-text-muted:      var(--mh-stone-700);
  --mh-text-subtle:     var(--mh-stone-600);
  --mh-text-inverse:    var(--mh-stone-100);
  --mh-text-inverse-muted: var(--mh-stone-500);

  --mh-primary:         var(--mh-green-600);
  --mh-primary-hover:   var(--mh-green-700);
  --mh-primary-active:  var(--mh-green-800);
  --mh-primary-tint:    var(--mh-green-100);
  --mh-primary-edge:    var(--mh-green-200);
  --mh-on-primary:      var(--mh-stone-100);
  --mh-accent:          var(--mh-green-400);  /* decorative mark only     */

  --mh-link:            var(--mh-green-600);
  --mh-link-hover:      var(--mh-green-800);

  /* Borders are alpha so they sit correctly on paper, white and stone.   */
  --mh-border:          rgba(26, 29, 33, 0.08);  /* card / row hairline   */
  --mh-border-strong:   rgba(26, 29, 33, 0.12);  /* table outer, dividers */
  --mh-border-control:  rgba(26, 29, 33, 0.18);  /* inputs                */
  --mh-border-button:   rgba(26, 29, 33, 0.22);  /* secondary buttons     */
  --mh-border-inverse:  rgba(255, 255, 255, 0.10);

  --mh-overlay:         rgba(20, 22, 25, 0.50);  /* modal backdrop        */
  --mh-selection:       rgba(70, 195, 109, 0.26);

  /* --- 3. Typography ------------------------------------------------------ */

  /* Hanken Grotesk carries ALL interface text — buttons, labels, tables,
     nav, headings. Source Serif 4 is reserved for long-form prose
     (knowledgebase articles, announcements, invoice notes). Do not set
     serif on UI chrome; the client area is denser than the marketing site.
     Load: Hanken Grotesk 400,500,600,700 · Source Serif 4 400,500,600     */
  --mh-font-ui:    'Hanken Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  --mh-font-prose: 'Source Serif 4', Georgia, 'Times New Roman', serif;
  --mh-font-mono:  ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;

  --mh-weight-regular:  400;
  --mh-weight-medium:   500;
  --mh-weight-semibold: 600;   /* buttons, labels, table heads, h2–h6     */
  --mh-weight-bold:     700;   /* h1, prices, badges                      */

  /* Type scale — 15px base (client-area density, one step under the
     16.5px marketing body).                                              */
  --mh-text-2xs:  11.5px;  /* badges, table micro-labels                  */
  --mh-text-xs:   13px;    /* help text, captions — NOT breadcrumbs:
                              they are persistent navigation, not fine
                              print, and take --mh-text-sm (4T)         */
  --mh-text-sm:   14px;    /* small buttons, dense table cells            */
  --mh-text-base: 15px;    /* BODY — inputs, cells, nav, buttons          */
  --mh-text-md:   16px;    /* large buttons, lead-in text                 */
  --mh-text-lg:   18px;    /* card titles                                 */
  --mh-text-xl:   21px;    /* section headings (h3)                       */
  --mh-text-2xl:  24px;    /* page sub-headings (h2)                      */
  --mh-text-3xl:  30px;    /* page title (h1)                             */
  --mh-text-4xl:  38px;    /* stat figures, prices                        */
  --mh-text-5xl:  48px;    /* hero numerals — rare in the client area     */

  --mh-leading-none:    1;
  --mh-leading-tight:   1.15;  /* display headings                        */
  --mh-leading-snug:    1.3;   /* headings, table cells                   */
  --mh-leading-normal:  1.5;   /* UI text                                 */
  --mh-leading-relaxed: 1.62;  /* prose                                   */

  --mh-tracking-display: -0.032em;  /* 38px+                              */
  --mh-tracking-heading: -0.02em;   /* 24–30px                            */
  --mh-tracking-tight:   -0.01em;   /* 18–21px                            */
  --mh-tracking-normal:  0;
  --mh-tracking-caps:    0.09em;    /* uppercase eyebrows, table heads    */
  --mh-tracking-badge:   0.06em;    /* uppercase badges                   */

  /* --- 4. Spacing — 2px grid, named by px so handoff stays unambiguous --- */

  --mh-space-2:   2px;
  --mh-space-4:   4px;
  --mh-space-6:   6px;    /* label → control                             */
  --mh-space-8:   8px;
  --mh-space-10:  10px;
  --mh-space-12:  12px;   /* icon → text, inline button gap              */
  --mh-space-14:  14px;   /* stacked form fields                         */
  --mh-space-16:  16px;
  --mh-space-20:  20px;   /* grid gutter                                 */
  --mh-space-24:  24px;   /* page gutter, card padding (dense)           */
  --mh-space-28:  28px;   /* card padding                                */
  --mh-space-32:  32px;   /* card padding (roomy)                        */
  --mh-space-40:  40px;
  --mh-space-48:  48px;
  --mh-space-64:  64px;   /* section rhythm                              */
  --mh-space-80:  80px;

  --mh-container:      1120px;  /* marketing max-width                   */
  --mh-container-app:  1320px;  /* client area is wider — tables need it */
  --mh-gutter:         24px;

  /* Control geometry — keeps buttons, inputs and selects the same height */
  --mh-control-height-sm: 34px;
  --mh-control-height:    42px;
  --mh-control-height-lg: 50px;
  --mh-control-pad-x:     14px;   /* inputs, selects, input-group addons  */
  --mh-control-pad-y:     11px;
  --mh-button-pad-x:      18px;   /* buttons are wider than inputs        */

  /* --- 5. Radii ------------------------------------------------------------ */

  --mh-radius-xs:   2px;    /* logo dots, tiny marks                     */
  --mh-radius-sm:   6px;    /* badges, inline chips                      */
  --mh-radius-md:   10px;   /* DEFAULT — buttons, inputs, selects, pills */
  --mh-radius-lg:   14px;   /* cards, panels, alerts                     */
  --mh-radius-xl:   18px;   /* modals, feature panels                    */
  --mh-radius-pill: 999px;

  /* --- 6. Elevation --------------------------------------------------------- */

  --mh-shadow-none:    none;
  --mh-shadow-sm:      0 1px 2px rgba(26, 29, 33, 0.04);
  --mh-shadow-card:    0 1px 3px rgba(26, 29, 33, 0.05);
  --mh-shadow-raised:  0 8px 24px rgba(26, 29, 33, 0.07);   /* featured   */
  --mh-shadow-overlay: 0 24px 60px rgba(26, 29, 33, 0.18);  /* modal/menu */
  --mh-shadow-focus:   0 0 0 3px rgba(70, 195, 109, 0.28);
  --mh-shadow-focus-danger: 0 0 0 3px rgba(138, 35, 32, 0.22);

  /* Keyboard focus is an outline, not a shadow — it must survive on
     transparent and dark surfaces alike.                                 */
  --mh-focus-ring:        2px solid var(--mh-green-600);
  --mh-focus-ring-offset: 3px;

  /* --- 7. Motion ------------------------------------------------------------ */

  --mh-duration-fast:  120ms;  /* colour/opacity on hover                 */
  --mh-duration-base:  180ms;  /* most state changes                      */
  --mh-duration-slow:  220ms;  /* panels, modals entering                 */
  --mh-duration-panel: 450ms;  /* large cross-fades                       */

  --mh-ease:        cubic-bezier(0.22, 0.61, 0.36, 1);  /* default        */
  --mh-ease-in-out: cubic-bezier(0.45, 0, 0.55, 1);
  --mh-ease-out:    ease-out;

  --mh-transition-colors: color var(--mh-duration-fast) var(--mh-ease-out),
                          background-color var(--mh-duration-fast) var(--mh-ease-out),
                          border-color var(--mh-duration-fast) var(--mh-ease-out);
  --mh-transition-all:    all var(--mh-duration-base) var(--mh-ease);

  /* --- 8. Z-index ------------------------------------------------------------ */

  --mh-z-dropdown: 100;
  --mh-z-sticky:   60;
  --mh-z-overlay:  200;
  --mh-z-modal:    210;
  --mh-z-toast:    300;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --mh-duration-fast:  0ms;
    --mh-duration-base:  0ms;
    --mh-duration-slow:  0ms;
    --mh-duration-panel: 0ms;
  }
}

/* --------------------------------------------------------------------------
   1b. Nexus Cart :root — branded copy of the block from the parent's own
   ../twenty-one-reference/css/custom.css.

   This block styles NEXUS CART ONLY. It has no effect on Twenty-One (see
   CLAUDE.md, "The Nexus :root trap") — Twenty-One is not using Nexus Cart.
   Kept here purely for future-proofing in case that ever changes; nothing
   below should be assumed to be live on the current site.

   Structure preserved from the source file: raw ramps first, semantic
   roles alias through them, exactly as the parent wrote it — only the
   raw/leaf values are replaced with brand tokens, so the alias chain
   still does the same work it did in the original.
   -------------------------------------------------------------------------- */

html {
    font-size: 14px;
}

:root {
    --white: var(--mh-stone-50);

    /* Neutral shades */
    --neutral-50:  var(--mh-stone-50);
    --neutral-100: var(--mh-stone-100);
    --neutral-200: var(--mh-stone-150);
    --neutral-300: var(--mh-stone-200);
    --neutral-400: var(--mh-stone-300);
    --neutral-500: var(--mh-stone-400);
    --neutral-600: var(--mh-stone-500);
    --neutral-700: var(--mh-stone-600);
    --neutral-800: var(--mh-stone-700);
    --neutral-900: var(--mh-stone-800);
    --neutral-950: var(--mh-stone-900);

    /* Primary colors — the source aliased this ramp to --neutral-* as a
       monochrome placeholder ("define own pallet with brand colors"); we
       have a real brand ramp, so it points at --mh-green instead. */
    --primary-50:  var(--mh-green-100);
    --primary-100: var(--mh-green-100);
    --primary-200: var(--mh-green-200);
    --primary-300: var(--mh-green-300);
    --primary-400: var(--mh-green-400);
    --primary-500: var(--mh-green-500);
    --primary-600: var(--mh-green-600);
    --primary-700: var(--mh-green-700);
    --primary-800: var(--mh-green-800);
    --primary-900: var(--mh-green-900);
    --primary-950: var(--mh-green-900);

    /* Primary colors */
    --primary: var(--primary-600);
    --primary-lifted: var(--primary-700);
    --primary-accented: var(--primary-800);

    /* Secondary colors */
    --secondary: var(--neutral-500);
    --secondary-lifted: var(--neutral-600);
    --secondary-accented: var(--neutral-700);

    /* Success colors */
    --success: var(--mh-success);
    --success-lifted: var(--mh-success-hover);
    --success-accented: var(--mh-success-ink);

    /* Info colors */
    --info: var(--mh-info);
    --info-lifted: var(--mh-info-hover);
    --info-accented: var(--mh-info-ink);

    /* Notice colors — brand palette has no purple/notice hue; repurposing
       info, since the brand palette doesn't distinguish "notice" from
       "info" either. */
    --notice: var(--mh-info);
    --notice-lifted: var(--mh-info-hover);
    --notice-accented: var(--mh-info-ink);

    /* Warning colors */
    --warning: var(--mh-warning);
    --warning-lifted: var(--mh-warning-hover);
    --warning-accented: var(--mh-warning-ink);

    /* Error colors */
    --error: var(--mh-danger);
    --error-lifted: var(--mh-danger-hover);
    --error-accented: var(--mh-danger-ink);

    /* Grayscale colors */
    --grayscale: var(--neutral-900);
    --grayscale-lifted: var(--neutral-800);
    --grayscale-accented: var(--neutral-700);

    /* Neutral colors */
    --neutral: var(--neutral-500);
    --neutral-lifted: var(--neutral-600);
    --neutral-accented: var(--neutral-700);

    /* Text neutral colors */
    --text-inverted: var(--white);
    --text-muted: var(--neutral-400);
    --text-lifted: var(--neutral-500);
    --text-accented: var(--neutral-600);
    --text: var(--neutral-900);

    /* Border neutral colors */
    --border-muted: var(--neutral-200);
    --border: var(--neutral-300);
    --border-lifted: var(--neutral-400);
    --border-accented: var(--neutral-600);

    /* Background neutral colors */
    --bg: var(--white);
    --bg-muted: var(--neutral-50);
    --bg-lifted: var(--neutral-100);
    --bg-accented: var(--neutral-200);
    --bg-inverted: var(--neutral-900);

    /* Additional colors — dropped (yellow/teal/emerald/pink swatches from
       the source have no brand equivalent and aren't referenced by any
       role above). */

    /* Additional custom properties */
    /* Font sizes */
    --text-xs: 0.625rem;
    --text-sm: 0.75rem;
    --text-md: 0.875rem;
    --text-lg: 1rem;

    /* Spacing */
    --outline-sm: 1px;
    --outline-md: 2px;
    --outline-lg: 3px;

    /* Rounding */
    --rounding-sm: 0.25rem;
    --rounding-md: 0.5rem;
    --rounding-lg: 0.75rem;

    /* Other */
    --letter-spacing: 0em;
    --disabled-opacity: 25%;
}

/* --------------------------------------------------------------------------
   1c. Derived tints — WHMCS-specific. Mirrored in brand/tokens.css
   (section 2, "Bootstrap-compat derived tints") since 2026-09-08 so
   check-tokens.sh can hold the two files in lockstep; edit there first.
   Bootstrap generates several primary-family colours via Sass mix()/
   lighten(), not the plain primary hex. These reproduce those exact
   ratios (verified against the parent's compiled output) with our
   primary substituted in, so they regenerate correctly if --mh-primary
   ever changes.
   -------------------------------------------------------------------------- */

:root {
    /* mix(white, primary, 80%) — theme.css .alert-primary background. */
    --mh-primary-mix-bg: color-mix(in srgb, white 80%, var(--mh-primary) 20%);
    /* mix(white, primary, 72%) — theme.css .alert-primary border-color
       AND .table-primary background-color (Bootstrap uses the same level
       for both). */
    --mh-primary-mix-surface: color-mix(in srgb, white 72%, var(--mh-primary) 28%);
    /* mix(white, primary, 48%) — theme.css .table-primary border-color. */
    --mh-primary-mix-border: color-mix(in srgb, white 48%, var(--mh-primary) 52%);
    /* mix(black, primary, 48%) — theme.css .alert-primary /
       .list-group-item-primary text colour. */
    --mh-primary-mix-text: color-mix(in srgb, black 48%, var(--mh-primary) 52%);

    /* lighten(primary, 25%) in HSL — theme.css's #79a6d2 focus-border
       tint. Static, not color-mix(): color-mix() interpolates toward
       white in whatever space you pick, which also pulls saturation down
       as it lightens. Sass's lighten() only raises HSL lightness and
       leaves hue/saturation alone, so no color-mix() percentage
       reproduces it exactly. Regenerate by converting --mh-primary to
       HSL, adding 25 to L (clamped 100), converting back — this is that
       result for #1E7A44. */
    --mh-primary-mix-focus: #46D280;
    /* lighten(primary, 35%) in HSL — theme.css's #9fbfdf active-fill
       tint. Same reasoning and same regeneration method, +35 to L. */
    --mh-primary-mix-active: #6FDC9C;
}



/* ==========================================================================
   FONTS
   Self-hosted so no template override is needed to load them. The parent's
   head.tpl loads Open Sans via
   {\WHMCS\View\Asset::fontCssInclude('open-sans-family.css')} on line 2;
   we can't remove that, so we override the family instead. Open Sans stays
   as a download but goes unused.

   Upright only — no italic faces. Confirmed against production:
   zero <em>/<i> in tblannouncements and tblknowledgebase. If authored
   content later introduces italics, the browser will synthesise an oblique;
   add real italic faces at that point rather than living with it.

   Source Serif 4 is deliberately NOT loaded. components.html scopes it to
   .mh-prose, which is a design-system class with no counterpart in WHMCS
   markup — the real containers are .announcement, .kb-article-item and the
   ticket message body. Revisit as its own decision.
   ========================================================================== */

@font-face {
  font-family: 'Hanken Grotesk';
  src: url('../fonts/hanken-grotesk-v12-latin_latin-ext-regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Hanken Grotesk';
  src: url('../fonts/hanken-grotesk-v12-latin_latin-ext-500.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Hanken Grotesk';
  src: url('../fonts/hanken-grotesk-v12-latin_latin-ext-600.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Hanken Grotesk';
  src: url('../fonts/hanken-grotesk-v12-latin_latin-ext-700.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* Parent sets body { font-family: "Open Sans", sans-serif } at
   theme.css:9200. Equal specificity (0,0,1), so custom.css wins on source
   order — no compound selector needed here, unlike the background-color
   case where .primary-bg-color out-specifies a bare body rule. */
/* Size and leading join the family swap here. The root stays at the
   browser's 16px: theme.css uses rem for 972 values but only 56 of them
   are font-size, so a 15px root would shift ~916 spacing, radius and
   dimension values by 6.25% — including 56 rem radii that would then
   fragment against the px radii set in Session 4B. Type is therefore set
   per component and rem stays anchored to 16. */
body {
  font-family: var(--mh-font-ui);
  font-size: var(--mh-text-base);
  line-height: var(--mh-leading-normal);
}

/* These two declare their own stack rather than inheriting from body
   (theme.css lines ~1010 and ~1090), so they need naming explicitly. */
.tooltip,
.popover {
  font-family: var(--mh-font-ui);
}




/* ==========================================================================
   2. BASE / TYPOGRAPHY
   ========================================================================== */

/* Links — theme.css:162-169. #204060 is a third primary derivative (the
   brief named only #29537c and #264d73); included because it's the
   text-link hover colour, and links were explicitly in scope. */
a {
    color: var(--mh-link);
}
a:hover {
    color: var(--mh-link-hover);
}

/* ---- Headings — theme.css:363-392. The parent sets weight 500 and
   line-height 1.2 on the shared h1-h6 rule at 365, then a Bootstrap rem
   scale (2.5 / 2 / 1.75 / 1.5 / 1.25 / 1rem) that is not proportional to
   ours, so every level needs an explicit size regardless of root.
   h1-h3 come straight from the token comments, which name them; lg
   (18px, "card titles") continues the run at h4, and h5/h6 step down
   through md and base rather than inventing values.
   Tracking is matched to the px bands named in the token comments:
   heading (-0.02em) covers 24-30px, tight (-0.01em) covers 18-21px, and
   h5/h6 fall below both so they take none. Leading moves 1.2 -> snug
   (1.3), which is what --mh-leading-snug's comment specifies for
   headings. */
h1, h2, h3, h4, h5, h6,
.h1, .h2, .h3, .h4, .h5, .h6 {
    font-weight: var(--mh-weight-semibold);
    line-height: var(--mh-leading-snug);
}
h1, .h1 {
    font-size: var(--mh-text-3xl);
    font-weight: var(--mh-weight-bold);
    letter-spacing: var(--mh-tracking-heading);
}
h2, .h2 {
    font-size: var(--mh-text-2xl);
    letter-spacing: var(--mh-tracking-heading);
}
h3, .h3 {
    font-size: var(--mh-text-xl);
    letter-spacing: var(--mh-tracking-tight);
}
h4, .h4 {
    font-size: var(--mh-text-lg);
    letter-spacing: var(--mh-tracking-tight);
}
h5, .h5 {
    font-size: var(--mh-text-md);
}
h6, .h6 {
    font-size: var(--mh-text-base);
}

/* .display-1 (theme.css:398-402, 6rem) is the only element in the theme
   that reaches the 38px+ band --mh-tracking-display is scoped to. Used
   on the 404 and rate-limit pages only (error/page-not-found.tpl:4-5,
   error/rate-limit-exceeded.tpl:4-5). Size deliberately left alone —
   resizing display classes wasn't in this brief. */
.display-1 {
    letter-spacing: var(--mh-tracking-display);
}

/* ---- Small text — the floor raise. Twenty-One treats small text as a
   default rather than an exception; `small` at 80% of a 16px body was
   12.8px on prose, helper text, table metadata and fine print alike.
   Body drops 16 -> 15px and small rises to 14px, so the gap between
   body and its supporting text narrows from 3.2px to 1px.

   `small` gets a single tier because it can't be subdivided: the 38
   usages across 24 templates cover four different roles — knowledgebase
   excerpts and category descriptions (prose), form helper text, table
   metadata, and legal fine print — with no class distinguishing them,
   and template overrides are barred. 14px ("dense table cells" in the
   token comment) serves all four better than 12.8px served any.
   theme.css:429-433 also sets font-weight 400 here, left alone. */
small,
.small {
    font-size: var(--mh-text-sm);
}

/* Genuine fine print, where a class does exist to target it. xs (13px)
   is the token's "help text, captions" tier. */
footer.footer .copyright,
footer.footer .dropdown-item,
.view-ticket .posted-by {
    font-size: var(--mh-text-xs);
}

/* Invoice/quote/billing-note addresses and dates — invoice.css:9353-9355,
   0.9em. Document metadata, not fine print. */
.invoice-container .small-text {
    font-size: var(--mh-text-sm);
}

/* Blockquote attribution — theme.css:468-472, 80%. */
.blockquote-footer {
    font-size: var(--mh-text-sm);
}

/* Client-area page background — theme.css:9560. header.tpl:10 renders
   <body class="primary-bg-color">, and a bare `body` rule loses to this
   on specificity regardless of source order (the specificity trap in
   CLAUDE.md, confirmed on staging in Session 1) — the selector must
   include the class. */
body.primary-bg-color {
    background-color: var(--mh-bg);
}

/* Breadcrumb bar — theme.css:9660, 0.9rem (14.4px). Sized to sm rather
   than the xs the token comment originally listed breadcrumbs under:
   breadcrumbs are persistent navigation, not captions, and xs would have
   lowered them against the point of this pass. The token comment in
   brand/tokens.css was amended to match. */
.master-breadcrumb {
    background-color: var(--mh-bg-sunken);
    font-size: var(--mh-text-sm);
}
/* The <ol> inside that bar was missed when the bar itself was rebranded,
   leaving two tones in one band: .master-breadcrumb took --mh-bg-sunken
   (#EFEDE8, warm) while the nested .breadcrumb kept Bootstrap's stock
   #e9ecef (cool grey, theme.css:4086-4094) — and since the <ol> sits
   inside .container, the parent's colour only showed in the gutters
   either side of it. In stock both are #e9ecef, which is why nothing
   looked wrong until one of the two moved.
   Transparent rather than restating the token: the band's colour then
   has exactly one source, the parent, instead of two declarations to
   keep in step. Safe to scope this way — includes/breadcrumb.tpl is the
   only thing that renders <ol class="breadcrumb"> and header.tpl:145 is
   its only include site, always inside .master-breadcrumb — and (0,2,0)
   clears the stock (0,1,0) rule outright. The stock rule's
   border-radius: 0.25rem goes inert with no fill behind it, so it's
   left alone rather than zeroed for the sake of it. */
.master-breadcrumb .breadcrumb {
    background-color: transparent;
}

/* Inline code, code blocks, keyboard keys — theme.css:505-533. `code`
   is Bootstrap's own inline-code pink (#e83e8c), visible anywhere WHMCS
   emits <code> — confirmed in the password-tips box
   (includes/pwstrength.tpl's passwordtips language string, core-
   rendered so the exact markup isn't visible here, but the element and
   the rule are global regardless of where it's used) and in any
   markdown/help text. No dedicated "code" role exists in the token set,
   and constraint 4 says stop and ask rather than invent one — mapped to
   --mh-text-body instead, a neutral ink distinct from --mh-link (also
   green): inline code sitting the same green as a link would read as
   clickable when it isn't. `pre` (528-531, the second `pre` rule — the
   first, 189-194, is layout-only, no colour) gets the same treatment,
   it has no background of its own so it sits on whatever ambient
   surface surrounds it. `kbd` (514-520) is a solid dark badge —
   matched to the surface-inverse/text-inverse pair already used for
   the footer and topbar, the same role. `a > code { color: inherit }`
   (510) already reads correctly with no change: it inherits the link
   colour rather than pink, which is the right behaviour, not a gap.
   `samp` (184-187) sets no colour, font-family/size only — nothing to
   map. */
code {
    color: var(--mh-text-body);
}
pre {
    color: var(--mh-text-body);
}
kbd {
    color: var(--mh-text-inverse);
    background-color: var(--mh-surface-inverse);
}


/* ==========================================================================
   3. BUTTONS
   ========================================================================== */

/* Generic focus ring shared by every .btn variant — theme.css:2235-2237.
   .btn-default (theme.css:9948, hand-written by WHMCS, not part of
   compiled Bootstrap) has no focus box-shadow of its own and relies on
   this rule, so it's the one place a single override covers a class the
   colour sweep would otherwise miss entirely. */
.btn:focus,
.btn.focus {
    box-shadow: var(--mh-shadow-focus);
}

/* ---- Global button shape — set on .btn so primary and secondary stay
   consistent. theme.css:2209-2223 (.btn), 2707-2712 (.btn-lg),
   2714-2718 (.btn-sm), 10007-10012 (.btn-xs, hand-written).

   Radius: tokens.css designates --mh-radius-md (10px) as the default for
   buttons; the brief's "closer to 8px" has no token, and 10px is the
   system's own answer rather than an approximation of 8. Size variants
   scale down/up the radius ramp: sm 6px, md 10px, lg 14px.

   Padding: --mh-control-pad-y and --mh-button-pad-x (18px — buttons are
   wider than inputs in the design; inputs keep --mh-control-pad-x 14px,
   see tokens.css). The size variants set their own
   padding in the parent at the same (0,1,0) specificity but earlier in
   the cascade, so without re-declaring them here .btn-sm/.btn-lg/.btn-xs
   would all inherit the full-size padding and stop being compact — they
   are re-declared on the parent's exact selectors with the nearest
   spacing tokens (sm 4/8, lg 8/16 are exact matches to Bootstrap's own
   values; xs 2/6 replaces the parent's 1px/5px, which has no token).

   Height: buttons must match inputs (they sit adjacent in the domain
   search and the cart), so each size targets its --mh-control-height*
   token exactly. Font-size is set explicitly per size (base/sm/md text
   tokens) and line-height is derived from the height token rather than
   picked from the leading ramp — none of 1.15/1.3/1.5 lands on 42px.
   `2px` in each calc is Bootstrap's 1px border × 2 (theme.css:2219), a
   structural value, not a brand one. Resulting heights: .btn 42px,
   .btn-sm 34px, .btn-lg 50px. .btn-xs has no height token and no exact
   font token (parent 12px; --mh-text-xs 13px is the nearest), so it
   keeps a plain 1.5 leading and comes out at 25.5px. */
.btn {
    font-weight: var(--mh-weight-semibold);
    padding: var(--mh-control-pad-y) var(--mh-button-pad-x);
    font-size: var(--mh-text-base);
    line-height: calc(var(--mh-control-height) - 2 * var(--mh-control-pad-y) - 2px);
    border-radius: var(--mh-radius-md);
}
.btn-lg,
.btn-group-lg > .btn {
    padding: var(--mh-space-8) var(--mh-space-16);
    font-size: var(--mh-text-md);
    line-height: calc(var(--mh-control-height-lg) - 2 * var(--mh-space-8) - 2px);
    border-radius: var(--mh-radius-lg);
}
.btn-sm,
.btn-group-sm > .btn {
    padding: var(--mh-space-4) var(--mh-space-8);
    font-size: var(--mh-text-sm);
    line-height: calc(var(--mh-control-height-sm) - 2 * var(--mh-space-4) - 2px);
    border-radius: var(--mh-radius-sm);
}
.btn-group-xs > .btn,
.btn-xs {
    padding: var(--mh-space-2) var(--mh-space-6);
    font-size: var(--mh-text-xs);
    line-height: var(--mh-leading-normal);
    border-radius: var(--mh-radius-sm);
}

/* ---- .btn-default — theme.css:9948-10012. Hand-written by WHMCS, not
   compiled Bootstrap; the most-used button class in the templates (159
   uses vs .btn-primary's 102) and the one a colour sweep misses because
   it carries no brand hex. Secondary button: white fill, ink text,
   quiet border at rest; brand-green tint, border and text on hover.

   Every selector carries :not([class*="bg-color-"]). clientareahome.tpl:81
   renders dashboard card buttons as `btn btn-default bg-color-{color}`
   and .client-home-cards .card-header .btn (theme.css:10597) forces
   color:#fff; border:0 on them at (0,3,0). Any resting background rule
   here beats .bg-color-* on source order, and the (0,3,0) rule keeps the
   text white — a white button with white text and no border, invisible
   until hover flips the text to #000 (10601). Excluding them leaves those
   buttons fully stock (coloured fill, and the parent's own #e6e6e6 grey
   on hover, which is inventory row 10's problem, not this one). The
   alternative — re-declaring all 19 .bg-color-* after this block — only
   fixes the resting fill: the hover tint below is (0,3,0) and would still
   win over a re-declared (0,1,0) .bg-color-*, and the forced white text
   would sit on a pale green tint. Hence :not(). */
.btn-default:not([class*="bg-color-"]) {
    color: var(--mh-text);
    background-color: var(--mh-surface);
    border-color: var(--mh-border-button);
}

/* Hover, and focus brought in line with it (parent focus was a
   different grey pair, #e6e6e6/#8c8c8c, at 9954-9959). Focus also
   restates the ring: the generic .btn:focus above already supplies it,
   but the parent's own .btn-default:focus is where a future upstream
   change would most likely add a competing box-shadow.

   CONTRAST: --mh-primary (#1E7A44) on --mh-primary-tint (#E9F6EE) is
   4.8:1 — passes WCAG AA (4.5:1) but by the thinnest margin anywhere in
   the theme, on the most-used button. Any lightening of --mh-primary-tint
   (green-100) or of --mh-primary (green-600) in tokens.css must be
   rechecked against this pair before shipping. */
.btn-default:not([class*="bg-color-"]):hover,
.btn-default:not([class*="bg-color-"]):focus,
.btn-default:not([class*="bg-color-"]).focus {
    color: var(--mh-primary);
    background-color: var(--mh-primary-tint);
    border-color: var(--mh-primary);
}
.btn-default:not([class*="bg-color-"]):focus,
.btn-default:not([class*="bg-color-"]).focus {
    box-shadow: var(--mh-shadow-focus);
}

/* Active / pressed — theme.css:9967-9988, two rules (base and
   :hover/:focus-while-active, #e6e6e6/#adadad then #d4d4d4/#8c8c8c). The
   design shows resting and hover only; active is derived as one rung
   down the primary ladder on the same tint, so a press reads as a
   deepening of hover rather than a new colour. The parent's `.open >`
   selectors are Bootstrap 3 and never match in Twenty-One's markup
   (Bootstrap 4 toggles `.show` on the parent .btn-group); both are
   listed so the override is complete against the parent and against
   what actually renders. */
.btn-default:not([class*="bg-color-"]):active,
.btn-default:not([class*="bg-color-"]).active,
.open > .dropdown-toggle.btn-default:not([class*="bg-color-"]),
.show > .dropdown-toggle.btn-default:not([class*="bg-color-"]),
.btn-default:not([class*="bg-color-"]):active:hover,
.btn-default:not([class*="bg-color-"]).active:hover,
.open > .dropdown-toggle.btn-default:not([class*="bg-color-"]):hover,
.show > .dropdown-toggle.btn-default:not([class*="bg-color-"]):hover,
.btn-default:not([class*="bg-color-"]):active:focus,
.btn-default:not([class*="bg-color-"]).active:focus,
.open > .dropdown-toggle.btn-default:not([class*="bg-color-"]):focus,
.show > .dropdown-toggle.btn-default:not([class*="bg-color-"]):focus,
.btn-default:not([class*="bg-color-"]):active.focus,
.btn-default:not([class*="bg-color-"]).active.focus,
.open > .dropdown-toggle.btn-default:not([class*="bg-color-"]).focus,
.show > .dropdown-toggle.btn-default:not([class*="bg-color-"]).focus {
    color: var(--mh-primary-active);
    background-color: var(--mh-primary-tint);
    border-color: var(--mh-primary-active);
}

/* Disabled, resting — theme.css:9990-9992. No !important in the parent,
   handled normally. Fill stays the resting white; only the border is
   set, as in the parent. Bootstrap's .btn:disabled opacity .65 applies
   on top. */
.btn-default:not([class*="bg-color-"]).disabled {
    border-color: var(--mh-stone-400);
}

/* Disabled hover/focus — theme.css:9994-10012. The parent sets
   background-color and border-color with !important across these nine
   selectors, so a normal override cannot win at any specificity;
   matching it is the only way to win at all, not an escalation for a
   specificity problem (CLAUDE.md hard constraint 6). Only those two
   properties carry it. `color` is set here without !important — the
   hover rule above would otherwise turn a disabled button's text green
   — and wins on specificity alone ((0,4,0)+ vs the hover rule's
   (0,3,0)). */
.btn-default:not([class*="bg-color-"]).disabled:hover,
.btn-default:not([class*="bg-color-"])[disabled]:hover,
fieldset[disabled] .btn-default:not([class*="bg-color-"]):hover,
.btn-default:not([class*="bg-color-"]).disabled:focus,
.btn-default:not([class*="bg-color-"])[disabled]:focus,
fieldset[disabled] .btn-default:not([class*="bg-color-"]):focus,
.btn-default:not([class*="bg-color-"]).disabled.focus,
.btn-default:not([class*="bg-color-"])[disabled].focus,
fieldset[disabled] .btn-default:not([class*="bg-color-"]).focus {
    color: var(--mh-text);
    background-color: var(--mh-surface) !important;
    border-color: var(--mh-stone-400) !important;
}

/* .btn-success and .btn-outline-success are ALIASED onto the .btn-primary
   and .btn-outline-primary rules below — joined to the same selector
   lists rather than given their own values, so the two cannot drift.

   WHMCS does not use btn-success as a semantic role. Of 35 occurrences
   across 20 templates, 12 are in the excluded store/ MarketConnect
   templates; the in-scope labels are "Manage" x2, enable-2FA,
   accept-quote, leave-feedback and assorted save/submit buttons. Every
   one is a primary action wearing a green class — no success STATE is
   being signalled anywhere.

   This also retires the success/primary contrast problem documented
   through 4S: the palette could not produce a success fill that both
   cleared AA with light text and read as distinct from primary, and
   nothing needs it to any more.

   BUTTONS ONLY. .alert-success, .badge-success, .label-success,
   .list-group-item-success, .text-success, .bg-success and
   .border-success keep the semantic role below — those do signal state,
   and they pass at 8.19 on tint.

   .btn-primary — theme.css:2250-2277.
   Bootstrap uses two distinct darkened shades here: #29537c (lighter, the
   :hover background only) and #264d73 (darker — :hover border, :focus
   fill, badge/bg-utility hover). Those map onto our 3-rung ladder as
   primary-hover and primary-active respectively. The :active state's
   border in the source is a still-darker #234669 with no third rung
   available in our tokens; collapsed onto primary-active rather than
   reaching for a token (e.g. --mh-green-900) documented for a different
   role. */
.btn-primary,
.btn-success {
    background-color: var(--mh-primary);
    border-color: var(--mh-primary);
}
.btn-primary:hover,
.btn-success:hover {
    background-color: var(--mh-primary-hover);
    border-color: var(--mh-primary-active);
}
.btn-primary:focus,
.btn-primary.focus,
.btn-success:focus,
.btn-success.focus {
    background-color: var(--mh-primary-active);
    border-color: var(--mh-primary-active);
    box-shadow: var(--mh-shadow-focus);
}
.btn-primary.disabled,
.btn-primary:disabled,
.btn-success.disabled,
.btn-success:disabled {
    background-color: var(--mh-primary);
    border-color: var(--mh-primary);
}
.btn-primary:not(:disabled):not(.disabled):active,
.btn-primary:not(:disabled):not(.disabled).active,
.show > .btn-primary.dropdown-toggle,
.btn-success:not(:disabled):not(.disabled):active,
.btn-success:not(:disabled):not(.disabled).active,
.show > .btn-success.dropdown-toggle {
    background-color: var(--mh-primary-active);
    border-color: var(--mh-primary-active);
}
.btn-primary:not(:disabled):not(.disabled):active:focus,
.btn-primary:not(:disabled):not(.disabled).active:focus,
.show > .btn-primary.dropdown-toggle:focus,
.btn-success:not(:disabled):not(.disabled):active:focus,
.btn-success:not(:disabled):not(.disabled).active:focus,
.show > .btn-success.dropdown-toggle:focus {
    box-shadow: var(--mh-shadow-focus);
}

/* .btn-outline-primary — theme.css:2490-2512. Source only ever uses the
   base primary hex here (no separate hover/active shade), so this does
   too. */
.btn-outline-primary,
.btn-outline-success {
    color: var(--mh-primary);
    border-color: var(--mh-primary);
}
.btn-outline-primary:hover,
.btn-outline-success:hover {
    background-color: var(--mh-primary);
    border-color: var(--mh-primary);
}
.btn-outline-primary:focus,
.btn-outline-primary.focus,
.btn-outline-success:focus,
.btn-outline-success.focus {
    box-shadow: var(--mh-shadow-focus);
}
.btn-outline-primary.disabled,
.btn-outline-primary:disabled,
.btn-outline-success.disabled,
.btn-outline-success:disabled {
    color: var(--mh-primary);
}
.btn-outline-primary:not(:disabled):not(.disabled):active,
.btn-outline-primary:not(:disabled):not(.disabled).active,
.show > .btn-outline-primary.dropdown-toggle,
.btn-outline-success:not(:disabled):not(.disabled):active,
.btn-outline-success:not(:disabled):not(.disabled).active,
.show > .btn-outline-success.dropdown-toggle {
    background-color: var(--mh-primary);
    border-color: var(--mh-primary);
}
.btn-outline-primary:not(:disabled):not(.disabled):active:focus,
.btn-outline-primary:not(:disabled):not(.disabled).active:focus,
.show > .btn-outline-primary.dropdown-toggle:focus,
.btn-outline-success:not(:disabled):not(.disabled):active:focus,
.btn-outline-success:not(:disabled):not(.disabled).active:focus,
.show > .btn-outline-success.dropdown-toggle:focus {
    box-shadow: var(--mh-shadow-focus);
}

/* .btn-link — theme.css:2690-2705. :focus has no colour/shadow rule of
   its own in the source; it inherits the generic .btn:focus ring above. */
.btn-link {
    color: var(--mh-link);
}
.btn-link:hover {
    color: var(--mh-link-hover);
}



/* ---- Semantic buttons — theme.css:2310-2429 (.btn-*) and 2540-2659
   (.btn-outline-*). Pass A swept #336699 and its derivatives, so it never
   reached these; all four roles were still stock Bootstrap.

   Three roles here, not four: success is aliased to primary further up
   and is not a semantic button.

   Fill choice is contrast-driven, not stylistic. --mh-text-inverse on
   --mh-warning is 3.77:1, under AA, and buttons are 15px/600 so no
   large-text exemption applies — warning therefore fills with -ink
   (5.62). Danger and info keep their solid (8.52 and 6.80). Bootstrap's
   warning-only dark text
   (#212529, theme.css:2371) is dropped — all four carry
   --mh-text-inverse, and every state re-states it because the parent
   sets colour on each one separately.

   The success/primary fill collision that ran through 4S is resolved by
   the alias above rather than by a token change: --mh-success-ink and
   --mh-primary-active are the same #14532D, and no success rung was both
   AA-safe with light text and visually distinct from primary. Buttons no
   longer need one. The collision note stays in the token headers because
   it still applies to any future fill use of success-ink.

   Focus rings are not set here. Bootstrap gives each role its own rgba
   shadow (e.g. theme.css:2324), but the generic .btn:focus above is
   equal specificity (0,2,0) and later in the cascade, so every role
   already takes --mh-shadow-focus. */
.btn-warning {
    color: var(--mh-text-inverse);
    background-color: var(--mh-warning-ink);
    border-color: var(--mh-warning-ink);
}
.btn-warning:hover,
.btn-warning:focus,
.btn-warning.focus,
.btn-warning:not(:disabled):not(.disabled):active,
.btn-warning:not(:disabled):not(.disabled).active,
.show > .btn-warning.dropdown-toggle {
    color: var(--mh-text-inverse);
    background-color: var(--mh-warning-active);
    border-color: var(--mh-warning-active);
}
.btn-warning.disabled,
.btn-warning:disabled {
    color: var(--mh-text-inverse);
    background-color: var(--mh-warning-ink);
    border-color: var(--mh-warning-ink);
}

.btn-danger {
    color: var(--mh-text-inverse);
    background-color: var(--mh-danger);
    border-color: var(--mh-danger);
}
.btn-danger:hover,
.btn-danger:focus,
.btn-danger.focus {
    color: var(--mh-text-inverse);
    background-color: var(--mh-danger-hover);
    border-color: var(--mh-danger-hover);
}
.btn-danger:not(:disabled):not(.disabled):active,
.btn-danger:not(:disabled):not(.disabled).active,
.show > .btn-danger.dropdown-toggle {
    color: var(--mh-text-inverse);
    background-color: var(--mh-danger-active);
    border-color: var(--mh-danger-active);
}
.btn-danger.disabled,
.btn-danger:disabled {
    color: var(--mh-text-inverse);
    background-color: var(--mh-danger);
    border-color: var(--mh-danger);
}

.btn-info {
    color: var(--mh-text-inverse);
    background-color: var(--mh-info);
    border-color: var(--mh-info);
}
.btn-info:hover,
.btn-info:focus,
.btn-info.focus {
    color: var(--mh-text-inverse);
    background-color: var(--mh-info-hover);
    border-color: var(--mh-info-hover);
}
.btn-info:not(:disabled):not(.disabled):active,
.btn-info:not(:disabled):not(.disabled).active,
.show > .btn-info.dropdown-toggle {
    color: var(--mh-text-inverse);
    background-color: var(--mh-info-active);
    border-color: var(--mh-info-active);
}
.btn-info.disabled,
.btn-info:disabled {
    color: var(--mh-text-inverse);
    background-color: var(--mh-info);
    border-color: var(--mh-info);
}

/* Outline buttons, three roles (success is aliased to primary above).
   Rest state uses -ink rather than the solid, which fails AA as a
   foreground for warning (3.77 on white). Applied to all three rather
   than only the one that fails, so the family behaves predictably — a
   deliberate deviation from solid-to-solid, logged in the README. Hover and active fill with the
   same value the matching .btn-* uses at rest, so the two families agree.
   :focus deliberately does not fill — Bootstrap's outline focus is
   shadow-only (theme.css:2549) and that is preserved. */
.btn-outline-warning {
    color: var(--mh-warning-ink);
    border-color: var(--mh-warning-ink);
}
.btn-outline-warning:hover,
.btn-outline-warning:not(:disabled):not(.disabled):active,
.btn-outline-warning:not(:disabled):not(.disabled).active,
.show > .btn-outline-warning.dropdown-toggle {
    color: var(--mh-text-inverse);
    background-color: var(--mh-warning-ink);
    border-color: var(--mh-warning-ink);
}
.btn-outline-warning.disabled,
.btn-outline-warning:disabled {
    color: var(--mh-warning-ink);
}

.btn-outline-danger {
    color: var(--mh-danger-ink);
    border-color: var(--mh-danger-ink);
}
.btn-outline-danger:hover,
.btn-outline-danger:not(:disabled):not(.disabled):active,
.btn-outline-danger:not(:disabled):not(.disabled).active,
.show > .btn-outline-danger.dropdown-toggle {
    color: var(--mh-text-inverse);
    background-color: var(--mh-danger);
    border-color: var(--mh-danger);
}
.btn-outline-danger.disabled,
.btn-outline-danger:disabled {
    color: var(--mh-danger-ink);
}

.btn-outline-info {
    color: var(--mh-info-ink);
    border-color: var(--mh-info-ink);
}
.btn-outline-info:hover,
.btn-outline-info:not(:disabled):not(.disabled):active,
.btn-outline-info:not(:disabled):not(.disabled).active,
.show > .btn-outline-info.dropdown-toggle {
    color: var(--mh-text-inverse);
    background-color: var(--mh-info);
    border-color: var(--mh-info);
}
.btn-outline-info.disabled,
.btn-outline-info:disabled {
    color: var(--mh-info-ink);
}

/* ==========================================================================
   4. FORMS
   ========================================================================== */

/* ---- Control height — inputs, selects and input-group addons brought to
   the same --mh-control-height* tokens as .btn (BUTTONS above), by the
   same derivation: font-size set explicitly, line-height =
   height − 2·padding − 2px border (Bootstrap's 1px × 2, theme.css:1793).
   Bootstrap sets an explicit `height` on .form-control and .custom-select
   (theme.css:1785, 3407) from its own 1.5em/.75rem constants, so height
   has to be overridden as well or the control stays 38px whatever the
   padding does. Buttons and inputs sit adjacent in the homepage domain
   search (includes/domain-search.tpl:8-36) and throughout the cart; a
   4px mismatch there reads as a layout bug. They were only aligned
   before because both were Bootstrap defaults.
   Parent rules: theme.css:1782-1796 (.form-control), 1889-1903
   (-sm/-lg), 1905-1911 (textarea/select height:auto), 3404-3416
   (.custom-select), 3447-3461 (-sm/-lg), 3190-3200 (.input-group-text),
   3210-3245 (.input-group-lg/-sm children). Radius is not touched here —
   that is the forms row's call. */
.form-control,
.custom-select,
.input-group-text {
    padding: var(--mh-control-pad-y) var(--mh-control-pad-x);
    font-size: var(--mh-text-base);
    line-height: calc(var(--mh-control-height) - 2 * var(--mh-control-pad-y) - 2px);
}
.form-control,
.custom-select {
    height: var(--mh-control-height);
}
/* .custom-file — theme.css:3463-3480, three rules each at
   calc(1.5em + 0.75rem + 2px). Missed by Session 4B, so it sat at 38px
   beside the 42px controls; the `em` term also meant it would drift
   again the moment body font-size moved (to 36.5px). It sits next to
   inputs and buttons on the ticket form (12 template uses), so it joins
   the set. The wrapper, the transparent input and the visible label all
   need the height or the click target and the painted box come apart;
   ::after is the rendered "Browse" segment and takes the inner height
   (42px less the 2px border). */
.custom-file,
.custom-file-input,
.custom-file-label {
    height: var(--mh-control-height);
}
.custom-file-label {
    padding: var(--mh-control-pad-y) var(--mh-control-pad-x);
    font-size: var(--mh-text-base);
    line-height: calc(var(--mh-control-height) - 2 * var(--mh-control-pad-y) - 2px);
}
.custom-file-label::after {
    height: calc(var(--mh-control-height) - 2px);
    padding: var(--mh-control-pad-y) var(--mh-control-pad-x);
    line-height: calc(var(--mh-control-height) - 2 * var(--mh-control-pad-y) - 2px);
}
/* Radius — the follow-up flagged in the block below (search "Radius —
   deferred by 4B"). .custom-file-label stays stock 0.25rem (theme.css:
   3493-3507) and its ::after ("Browse", theme.css:3508-3523) stock
   `0 0.25rem 0.25rem 0` — worked out here rather than assumed:

   ::after is always the true right edge of the control (it's absolutely
   positioned over the label's right portion, content:"Browse"), so its
   LEFT corners are the seam against the label's own text and are
   hard-zeroed in the source regardless of grouping — that never changes,
   so it stays 0 here too. Its RIGHT corners are the real outer corner in
   the ungrouped case, so they take --mh-radius-md.

   .custom-file-label's own right corners sit underneath ::after and are
   never visible on their own, so giving it a full --mh-radius-md is safe
   either way.

   Grouping (attachment-group in supportticketsubmit-steptwo.tpl, where
   .custom-file sits beside an .input-group-append "Add" button) is
   handled by Bootstrap's own position rules, already in the source at
   (0,4,0) — theme.css:3149-3156:
     .input-group > .custom-file:not(:last-child) .custom-file-label,
     .input-group > .custom-file:not(:last-child) .custom-file-label::after
       { border-top-right-radius: 0; border-bottom-right-radius: 0; }
     .input-group > .custom-file:not(:first-child) .custom-file-label
       { border-top-left-radius: 0; border-bottom-left-radius: 0; }
   Those outrank the (0,1,0) rules below and zero only the two longhand
   corner properties they name, so the untouched corner on each element
   keeps whatever this rule set it to — the cascade does the "outer
   rounded, interior seam square" split without any :not()/group
   selectors of our own. Confirmed against the actual markup: the visible
   attachment field has a sibling append button (interior seam on its
   right, correctly squared); the hidden clone-template instance
   (file-upload w-hidden) has no group siblings, so nothing here gets
   zeroed and it renders fully rounded. */
.custom-file-label {
    border-radius: var(--mh-radius-md);
}
.custom-file-label::after {
    border-radius: 0 var(--mh-radius-md) var(--mh-radius-md) 0;
}
/* .custom-select keeps Bootstrap's 1.75rem right padding for its chevron
   (theme.css:3408); --mh-space-28 is that exact value. Re-declared after
   every rule that sets the padding shorthand on a select. */
.custom-select {
    padding-right: var(--mh-space-28);
}
/* Multi-line controls: Bootstrap's textarea.form-control and
   select.form-control[size]/[multiple] height:auto (theme.css:1905-1911)
   are (0,1,1) and already outrank the (0,1,0) height above, so those
   stay auto-sized — but the 18px line-height would still apply, and is
   too tight for prose (ticket replies, notes). Restore normal leading. */
textarea.form-control {
    line-height: var(--mh-leading-normal);
}
/* Small — matched to .btn-sm at 34px. Unused in the templates
   (.form-control-sm 0, .custom-select-sm 0, .input-group-sm 0) but
   mirrored so the size pair stays complete against the parent. */
.form-control-sm,
.custom-select-sm,
.input-group-sm > .form-control,
.input-group-sm > .custom-select,
.input-group-sm > .input-group-prepend > .input-group-text,
.input-group-sm > .input-group-append > .input-group-text,
.input-group-sm > .input-group-prepend > .btn,
.input-group-sm > .input-group-append > .btn {
    padding: var(--mh-space-4) var(--mh-space-8);
    font-size: var(--mh-text-sm);
    line-height: calc(var(--mh-control-height-sm) - 2 * var(--mh-space-4) - 2px);
}
.form-control-sm,
.custom-select-sm,
.input-group-sm > .form-control:not(textarea),
.input-group-sm > .custom-select {
    height: var(--mh-control-height-sm);
}
/* Large — matched to .btn-lg at 50px. .input-group-lg is the
   knowledgebase and downloads search (4 templates) plus one SSL upgrade
   form; .form-control-lg is the 2FA backup-code field. The parent's
   .input-group-lg child rules (theme.css:3210-3225) are (0,2,0)/(0,3,0)
   and would otherwise hold those at Bootstrap's 48px; listed here on
   the same selectors so they win on source order. */
.form-control-lg,
.custom-select-lg,
.input-group-lg > .form-control,
.input-group-lg > .custom-select,
.input-group-lg > .input-group-prepend > .input-group-text,
.input-group-lg > .input-group-append > .input-group-text,
.input-group-lg > .input-group-prepend > .btn,
.input-group-lg > .input-group-append > .btn {
    padding: var(--mh-space-8) var(--mh-space-16);
    font-size: var(--mh-text-md);
    line-height: calc(var(--mh-control-height-lg) - 2 * var(--mh-space-8) - 2px);
}
.form-control-lg,
.custom-select-lg,
.input-group-lg > .form-control:not(textarea),
.input-group-lg > .custom-select {
    height: var(--mh-control-height-lg);
}
.custom-select-sm,
.custom-select-lg,
.input-group-sm > .custom-select,
.input-group-lg > .custom-select {
    padding-right: var(--mh-space-28);
}

/* Radius — deferred by 4B ("that is the forms row's call"), completed
   here. .form-control's stock 0.25rem (4px, theme.css:1794) meant
   inputs and buttons rendered at different corner radii everywhere
   they sit adjacent, even though 4B already brought .btn to
   --mh-radius-md — the mismatch this row exists to close.

   .form-control and .custom-select move together, not .form-control
   alone: they've been a matched pair for height/padding since 4B
   (the shared rule just above this one), and Bootstrap gives them the
   same stock 0.25rem base radius (theme.css:1794, 3416) — no reason to
   split them now. -sm/-lg follow the exact three-tier pattern already
   set for buttons (.btn-sm/--mh-radius-sm, .btn/--mh-radius-md,
   .btn-lg/--mh-radius-lg): Bootstrap's own .form-control-sm/-lg already
   use a SMALLER/LARGER stock radius than the base (0.2rem/0.3rem,
   theme.css:1894/1902) rather than inheriting it unchanged, so the
   three-tier shape was already there in the source, just not on our
   scale. Extended to the input-group-sm/-lg children for the same
   reason 4B extended the height/padding rules to them: an addon or a
   button inside a sized input-group needs to match its siblings, not
   just the bare input.

   Follow-up completed: .input-group-text was flagged here as "more
   interdependent than a straight token swap" and deferred. Read the
   actual zeroing rules (theme.css:3249-3266) rather than assuming —
   .input-group-text turns out to zero exactly like .btn already does
   inside a group:
     .input-group > .input-group-prepend > .input-group-text
       { border-top-right-radius: 0; border-bottom-right-radius: 0; }
     .input-group > .input-group-append > .input-group-text
       { border-top-left-radius: 0; border-bottom-left-radius: 0; }
   Both are (0,3,0), well above the (0,1,0) rule below, and each names
   only the two longhand corners on its interior (seam) side — a prepend
   addon's right edge touches the input, so that's the side zeroed; an
   append addon's left edge does. The outer two corners are left alone by
   Bootstrap and simply keep whatever --mh-radius-md sets here, the same
   "one rule, cascade does the split" shape already used for .btn and now
   for .custom-file-label/::after just above. So it composes cleanly as a
   plain addition to this rule — no group-specific selectors of our own
   needed. .custom-file-label is handled separately just above (search
   "the follow-up flagged in the block below") since its ::after
   pseudo-element needs its own non-uniform base value.

   Every other stock 0.25rem user surveyed and left alone as genuinely
   out of scope — not inputs: .custom-checkbox .custom-control-label
   ::before, .nav-pills .nav-link, .promo-container. */
.form-control,
.custom-select,
.input-group-text {
    border-radius: var(--mh-radius-md);
}
.form-control-sm,
.custom-select-sm,
.input-group-sm > .form-control,
.input-group-sm > .custom-select {
    border-radius: var(--mh-radius-sm);
}
.form-control-lg,
.custom-select-lg,
.input-group-lg > .form-control,
.input-group-lg > .custom-select {
    border-radius: var(--mh-radius-lg);
}

/* Bug found on the KB search box (knowledgebase.tpl:2-8,
   input-group-lg + append btn) and confirmed as the same class of bug
   on every other -sm/-lg prepend/append .btn or .input-group-text in
   the theme: the blanket "one rule, cascade does the split" shape used
   above for .form-control/.custom-select and for the base-tier
   .input-group-text (search "Follow-up completed" above) does NOT
   survive at the -sm/-lg tier for .btn/.input-group-text specifically.

   Why the base tier is fine but -sm/-lg isn't: .input-group-lg >
   .input-group-append > .btn is a 3-class selector — (0,3,0) — and so
   is Bootstrap's own zeroing rule for the same element,
   .input-group > .input-group-append > .btn (theme.css:3259-3266,
   also (0,3,0)). Equal specificity means SOURCE ORDER decides, and
   custom.css loads after theme.css — so the old blanket
   "border-radius: token" here won the tie and re-rounded the interior
   corner Bootstrap had just zeroed. .form-control/.custom-select don't
   have this problem: .input-group-lg > .form-control is only 2 classes,
   (0,2,0), genuinely below Bootstrap's (0,3,0) zeroing rule for it —
   no tie, Bootstrap wins outright regardless of source order, which is
   exactly why the KB search INPUT's edge was already square and only
   the button's was wrong.

   Fixed by writing the true shape directly instead of relying on a
   competing rule to trim it — same technique as .custom-file-label::after
   above, not a new one: prepend is always structurally first in an
   input-group and append always last (Bootstrap's own model, not a
   guess), so prepend's outer edge is its LEFT corners and append's is
   its RIGHT — stated explicitly per side, including the interior
   corners as explicit 0 rather than omitted, so correctness no longer
   depends on which rule happens to load second.

   Checked every input-group in the theme for shape, not just this one
   (prepend-only, append-only, both, and which size tier each uses) —
   full inventory in README. -xs (store/ox/manage.tpl, 1 use) is a
   separate, pre-existing gap: no rule here touches it at all yet, so it
   stays on WHMCS's own stock 0.2rem — not broken by this fix, just
   never migrated; flagged there rather than folded into this one. */
.input-group-sm > .input-group-prepend > .input-group-text,
.input-group-sm > .input-group-prepend > .btn {
    border-radius: var(--mh-radius-sm) 0 0 var(--mh-radius-sm);
}
.input-group-sm > .input-group-append > .input-group-text,
.input-group-sm > .input-group-append > .btn {
    border-radius: 0 var(--mh-radius-sm) var(--mh-radius-sm) 0;
}
.input-group-lg > .input-group-prepend > .input-group-text,
.input-group-lg > .input-group-prepend > .btn {
    border-radius: var(--mh-radius-lg) 0 0 var(--mh-radius-lg);
}
.input-group-lg > .input-group-append > .input-group-text,
.input-group-lg > .input-group-append > .btn {
    border-radius: 0 var(--mh-radius-lg) var(--mh-radius-lg) 0;
}

/* Text input focus ring — theme.css:1810-1815. The companion border-color
   (#79a6d2, a lighter primary tint) is deliberately left untouched: it
   isn't one of the brief's three values and has no brand token — flagged
   separately for you. */
.form-control:focus {
    box-shadow: var(--mh-shadow-focus);
}

/* Checkboxes / radios / switches — theme.css:3292-3401. */
.custom-control-input:checked ~ .custom-control-label::before {
    border-color: var(--mh-primary);
    background-color: var(--mh-primary);
}
.custom-control-input:focus ~ .custom-control-label::before {
    box-shadow: var(--mh-shadow-focus);
}
.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {
    border-color: var(--mh-primary);
    background-color: var(--mh-primary);
}
/* Disabled checked/indeterminate fills — source uses primary at 50% alpha
   (theme.css:3357, 3360, 3370, 3401). Custom properties can't be
   interpolated inside rgba() colour channels, so this is the literal
   rgb() of --mh-primary (#1E7A44 = 30, 122, 68) at that same alpha. */
.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before,
.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before,
.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {
    background-color: rgba(30, 122, 68, 0.5);
}

/* Select / file-input focus rings — theme.css:3421-3424, 3479-3481. */
.custom-select:focus {
    box-shadow: var(--mh-shadow-focus);
}
.custom-file-input:focus ~ .custom-file-label {
    box-shadow: var(--mh-shadow-focus);
}

/* Range slider — theme.css:3537-3614. */
.custom-range:focus::-webkit-slider-thumb {
    box-shadow: 0 0 0 1px #fff, var(--mh-shadow-focus);
}
.custom-range:focus::-moz-range-thumb {
    box-shadow: 0 0 0 1px #fff, var(--mh-shadow-focus);
}
.custom-range:focus::-ms-thumb {
    box-shadow: 0 0 0 1px #fff, var(--mh-shadow-focus);
}
.custom-range::-webkit-slider-thumb {
    background-color: var(--mh-primary);
}
.custom-range::-moz-range-thumb {
    background-color: var(--mh-primary);
}
.custom-range::-ms-thumb {
    background-color: var(--mh-primary);
}

/* Password-reveal button hover/active — theme.css:9815-9821. */
.input-group .btn-reveal-pw:hover,
.input-group .btn-reveal-pw:active {
    color: var(--mh-primary);
}

/* Focus-state border tint (#79a6d2 in the source) — theme.css:1813, 3301,
   3422, 3480. Pairs with the box-shadow rings above so focus states read
   as one colour instead of blue-border/green-glow. */
.form-control:focus,
.custom-select:focus,
.custom-file-input:focus ~ .custom-file-label {
    border-color: var(--mh-primary-mix-focus);
}
.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
    border-color: var(--mh-primary-mix-focus);
}

/* Active-state fill tint (#9fbfdf in the source) — theme.css:3305-3306,
   3568, 3597, 3628. */
.custom-control-input:not(:disabled):active ~ .custom-control-label::before {
    background-color: var(--mh-primary-mix-active);
    border-color: var(--mh-primary-mix-active);
}
.custom-range::-webkit-slider-thumb:active {
    background-color: var(--mh-primary-mix-active);
}
.custom-range::-moz-range-thumb:active {
    background-color: var(--mh-primary-mix-active);
}
.custom-range::-ms-thumb:active {
    background-color: var(--mh-primary-mix-active);
}

/* Message editor (submitticket step two, viewticket reply) — square
   corners while every other control on the page is rounded. Traced, not
   guessed: the textarea carries class "markdown-editor"
   (supportticketsubmit-steptwo.tpl:70); js/whmcs.js:468 finds every
   `.markdown-editor` on load and calls jQuery(this).markdown({...}),
   which is the bootstrap-markdown plugin (toopay/bootstrap-markdown),
   bundled inside js/scripts.js (and its minified js/scripts.min.js) —
   not a separate library file, but not our JS either, so nothing here is
   ours to touch. At init it wraps the textarea in a generated
   `.md-editor` container with its own `.md-header` toolbar and
   `.md-footer` status bar.

   That wrapper's CSS is split across two files, not one — checked both
   rather than stopping at the first hit:
     css/all.css: `.md-editor{display:block;border:1px solid #ddd}` —
       the plugin's own shipped default, no radius set (so 0).
     theme.css:11094-11097 (the hand-written WHMCS tail, already
       inventoried as README row 26 for its .md-preview/.markdown-content
       colour rules — radius wasn't part of that pass): `.md-editor{
       border:1px solid #ddd;border-radius:3px}` — WHMCS's own override
       of the plugin default.
   Both are (0,1,0). head.tpl loads all.min.css at line 3 and
   theme.min.css at line 4, so theme.css's copy already wins over
   all.css's on source order — the rendered radius before this change
   was 3px, not 0, close enough to square next to --mh-radius-md (10px)
   elsewhere on the page to read as the square corners flagged. Both are
   stylesheets already in our load-order inventory (CLAUDE.md, "Which
   pages load our CSS"), not uninventoried ones. Reachable from here
   either way: this template renders through the standard header.tpl /
   includes/head.tpl chain (it doesn't build its own <head>, unlike the
   six templates listed in CLAUDE.md), custom.css loads last on that
   chain — the normal case, not the cart's load-order inversion — so a
   plain same-specificity override beats both on source order.

   overflow:hidden is required alongside the radius, not just the
   radius alone: .md-header/.md-footer paint their own #f5f5f5
   background edge-to-edge with no radius of their own, and the
   textarea its own #eee/#fff background — without clipping, those
   square backgrounds would still poke past the wrapper's now-rounded
   border at each corner. */
.md-editor {
    border-radius: var(--mh-radius-md);
    overflow: hidden;
}


/* ==========================================================================
   5. CARDS & PANELS
   ========================================================================== */

/* Invoice/quote/billing-note "paper" — invoice.css:9268-9274. White fill
   and a light border are already the right neutral tone for the page
   background; mapped to tokens rather than left as hardcoded hex. */
.invoice-container {
    background-color: var(--mh-surface);
    border-color: var(--mh-border-strong);
}

/* Document logo, swapped to the SVG — same content: url() approach as
   the header mark, and for the same reason: WHMCS's logo upload won't
   take an SVG, so {$logo} always resolves to a raster file.
   Applies to the HTML invoice AND the billing note: both wrap in
   .container-fluid.invoice-container and render the identical
   `<p><img src="{$logo}">` inside .invoice-header (viewinvoice.tpl:38,
   viewbillingnote.tpl:31). viewquote.tpl renders no logo at all —
   checked, there is no {$logo} block in it — so nothing to do there.
   Keyed on .invoice-container .invoice-header img because the <img>
   itself carries no class; this is invoice.css's own selector for it
   (9366, where it sets max-width:100%), so it's the established handle
   for this element rather than one invented here. custom.css loads
   after invoice.min.css on these pages — that's what the three
   {assetExists} template overrides exist to guarantee — and `content`
   is a property nothing else sets, so there's no contest either way.
   Points at the same logo.svg the header uses (190x61), not the
   300x100 invoice variant in brand/. The first attempt used that
   variant and it rendered too large: the only sizing rule in play is
   invoice.css's max-width:100%, so the file's intrinsic size is the
   rendered size, and 300x100 was a step up from what the page actually
   had. WHMCS serves a single logo.png for every surface and it is the
   190x61 header mark (confirmed against the live file), so the invoice
   has always worn that size — pointing here at the same asset keeps it
   a pure format swap with no change in footprint, and leaves one logo
   file to maintain rather than two that must be kept in step.
   brand/modhost-logo-whmcs-invoice.svg stays as the master if a
   document-sized mark is ever wanted here; it just isn't deployed.
   NOT the PDF invoice: invoicepdf.tpl is a separate rendering system
   with no CSS, out of scope per CLAUDE.md, and unaffected by this. */
.invoice-container .invoice-header img {
    content: url('../img/logo.svg');
}

/* "Ledger" and "Notes" section headers on the invoice/quote/billing-note
   pages, and "Proposal" on the quote page. includes/panel.tpl renders a
   plain white-text card-title on bg-{$type} (bg-info for Ledger/Notes,
   bg-warning for Proposal); viewinvoice.tpl:234 and
   viewbillingnote.tpl:124 hardcode the same bg-info pattern outside
   panel.tpl for the Ledger transactions table. These three are section
   headings, not alerts — teal/amber carry no meaning here.

   The same three templates also use panel.tpl for genuine payment-status
   alerts (viewinvoice.tpl:81-91: "Payment Initiated", "Payment Pending
   Review", offline-paid confirmation, all type="info") that must keep
   their colour. The two are structurally distinguishable: every alert
   call passes bodyTextCenter=true, which panel.tpl turns into a
   .text-center class on the sibling .card-text — Ledger/Notes/Proposal
   never pass it. :has() lets the selector key off that instead of the
   heading text, which no selector can read.

   Scoped to .invoice-container so panel.tpl's bg-info/bg-warning keep
   their real alert meaning everywhere else panel.tpl is used in the
   theme. .text-white ships !important (theme.css:9007) — matched here
   per CLAUDE.md hard constraint 6, same precedent as the UTILITIES
   section below. */
.invoice-container .card:not(:has(.card-text.text-center)) > .card-title.bg-info,
.invoice-container .card:not(:has(.card-text.text-center)) > .card-title.bg-warning {
    background-color: var(--mh-bg-sunken) !important;
    color: var(--mh-text) !important;
}

/* Square-cornered title inside a rounded card — same shape as the
   list-group overflow bug, same fix. Verified before touching anything:
   includes/panel.tpl renders `<div class="card w-100"><div
   class="card-title … bg-{$type}">`, a coloured header-like block as
   .card's own DIRECT child — not .card-header, so none of the
   .card-header radius rules above reach it, and .card-title itself
   (theme.css:4165) is `margin-bottom: 0.75rem` and nothing else in
   either stylesheet — no radius, no clipping, checked in both files.
   With .card rounding at --mh-radius-lg and .card-title flat, the title
   fill's own square top corners paint past the card's curve exactly
   like the list-group item did. Same two "Ledger" instances noted just
   above (viewinvoice.tpl:234, viewbillingnote.tpl:124) hand-write the
   identical `.card > .card-title` shape outside panel.tpl, so they get
   the same bug and need the same fix — checked, not assumed: grepped
   every panel.tpl include (14 calls across viewinvoice/viewquote/
   viewbillingnote, confirmed panel.tpl is used nowhere else in the
   theme) plus both hardcoded duplicates, and every one shares this
   exact shape.

   overflow: hidden on .card itself was rejected for the list-group case
   because .card is used far too broadly theme-wide to safely clip
   everything inside it. That risk doesn't disappear here just because
   the selector's scoped to .invoice-container — this container likely
   holds other, unrelated cards too. :has(> .card-title) narrows it to
   the actual shape in question: a card whose direct child is a
   .card-title, the thing panel.tpl and the Ledger duplicates both
   produce and nothing else in this theme does (.card-title normally
   nests inside .card-body, not directly under .card). Any other card
   inside .invoice-container — one with a dropdown or popover that
   needs to escape it — doesn't match this and is untouched. */
.invoice-container .card:has(> .card-title) {
    overflow: hidden;
}



/* ---- Dashboard stat underlines — clientareahome.tpl:10-54 hardcodes
   .highlight.bg-color-{blue|green|red|gold} on the four tiles. The second
   tile switches between Domains, Affiliate Signups and Quotes depending
   on configuration (lines 19, 27, 35); all three already share green, so
   one rule covers every branch.

   The four colours carry no meaning — blue for Services and red for
   Tickets is arbitrary variety — and being hardcoded in the template they
   cannot be configured per panel anyway. Unified on a stone neutral:
   this is a 2px rounded rule under a label (theme.css:10670-10674), not
   an accent. Scoped with .highlight (0,2,0) so the .bg-color-* names stay
   free for the card buttons, which reuse the same classes for a different
   job. */
.highlight.bg-color-blue,
.highlight.bg-color-green,
.highlight.bg-color-red,
.highlight.bg-color-gold {
    background-color: var(--mh-stone-400);
}

/* ---- Dashboard card accents — clientareahome.tpl:76 renders
   class="card card-accent-{$item->getExtra('color')}", and line 81 the
   header button as "btn btn-default bg-color-{the same value}".

   Targeted by menuItemName, not by colour name. The value comes from a
   core panel definition, so cards can share one name — remapping
   .card-accent-blue would hit every card using blue, which is the
   arbitrary-variety problem in a new form. Panel identity gives one card
   per rule, states the intent directly, survives a core colour change,
   and leaves all 19 .card-accent-* / .bg-color-* names untouched so an
   unconfigured panel or a third-party addon keeps its stock accent. The
   parent uses the same hook (theme.css:10713, 11627).

   Specificity: .client-home-cards div[menuitemname="…"] is (0,2,1) and
   beats .card-accent-* at (0,1,0). border-top-color is the longhand, so
   the parent's 3px solid width and style survive.

   Mapping — brand for actions, semantic only where a real state is
   signalled, neutral otherwise:
     Active Products/Services  gold          -> primary
     Recent Support Tickets    blue          -> primary
     Register a New Domain     emerald       -> primary  (border only)
     Overdue Invoices          red           -> danger
     Services Renewing Soon    midnight-blue -> warning
     Recent News               asbestos      -> neutral                */
.client-home-cards div[menuitemname="Active Products/Services"],
.client-home-cards div[menuitemname="Recent Support Tickets"],
.client-home-cards div[menuitemname="Register a New Domain"] {
    border-top-color: var(--mh-primary);
}
.client-home-cards div[menuitemname="Overdue Invoices"] {
    border-top-color: var(--mh-danger);
}
.client-home-cards div[menuitemname="Services Renewing Soon"] {
    border-top-color: var(--mh-warning);
}
.client-home-cards div[menuitemname="Recent News"] {
    border-top-color: var(--mh-stone-400);
}

/* Re-check against the corrected .card > .list-group rules (NAVIGATION &
   SIDEBAR) — this file's own .client-home-cards .list-group rule
   (theme.css hand-written tail, (0,2,0)) unconditionally zeroes every
   corner of a list-group nested here, deliberately, so the dashboard's
   lists sit flush with no radius of their own. That held automatically
   against the flat, blanket version of our rule (0,1,0) with no help
   needed. It does NOT hold on its own against the corrected version:
   .card > .list-group:last-child is (0,3,0), genuinely higher than this
   rule's (0,2,0), and DOES fire here — every "Active Products/Services"
   /"Recent Support Tickets"-style widget puts its list-group as the
   card's last child (header, no separate card-footer) — so without this
   override the dashboard's bottom corners would round again, exactly
   the regression this file exists to prevent. (:first-child never fires
   here — a .card-header always precedes the list — so only :last-child
   needs restating; :first-child is listed anyway for symmetry in case a
   future widget config ever omits the header.) */
.client-home-cards .card > .list-group:first-child,
.client-home-cards .card > .list-group:last-child {
    border-radius: 0;
}

/* Card header buttons. The parent forces color:#fff at theme.css:10597
   (0,3,0) and flips it to #000 on hover at 10601 (0,4,0). No fill can
   serve both: light text needs a luminance at or below 0.1833 and dark
   text needs 0.175 or above, a band 0.008 wide that no brand colour
   occupies — every candidate passed at rest and failed on hover. So the
   text stays light in both states and the fill darkens instead, which is
   what a button normally does. Both rules match the parent's specificity
   exactly and win on source order.

   These buttons are excluded from the .btn-default treatment in BUTTONS
   above (the :not([class*="bg-color-"]) guard), so the fill has to be
   stated here rather than inherited. Register a New Domain has an accent
   but no header button — it uses the register/transfer input group — so
   it gets no button rule. */
.client-home-cards .card-header .btn,
.client-home-cards .card-header .btn:hover {
    color: var(--mh-text-inverse);
}
.client-home-cards div[menuitemname="Active Products/Services"] .card-header .btn,
.client-home-cards div[menuitemname="Recent Support Tickets"] .card-header .btn {
    background-color: var(--mh-primary);
}
.client-home-cards div[menuitemname="Active Products/Services"] .card-header .btn:hover,
.client-home-cards div[menuitemname="Recent Support Tickets"] .card-header .btn:hover {
    background-color: var(--mh-primary-hover);
}
.client-home-cards div[menuitemname="Overdue Invoices"] .card-header .btn {
    background-color: var(--mh-danger);
}
.client-home-cards div[menuitemname="Overdue Invoices"] .card-header .btn:hover {
    background-color: var(--mh-danger-hover);
}
/* Warning fills with -ink, not the solid: --mh-text-inverse on
   --mh-warning is 3.77, under AA. Same choice as the semantic buttons. */
.client-home-cards div[menuitemname="Services Renewing Soon"] .card-header .btn {
    background-color: var(--mh-warning-ink);
}
.client-home-cards div[menuitemname="Services Renewing Soon"] .card-header .btn:hover {
    background-color: var(--mh-warning-active);
}
.client-home-cards div[menuitemname="Recent News"] .card-header .btn {
    background-color: var(--mh-stone-700);
}
.client-home-cards div[menuitemname="Recent News"] .card-header .btn:hover {
    background-color: var(--mh-stone-800);
}

/* ---- Recent Support Tickets card — the one dashboard badge no selector
   can reach on fill alone. The card renders through the children path
   (clientareahome.tpl:97-115), so it has no .card-body, and each child's
   label is raw HTML from core:

       <label class="label" style="background-color: #8A5A0C">Answered</label>

   Bare .label — no .status-* and no .label-{role} — with the fill written
   inline, where specificity cannot win. The colours are
   tblticketstatuses.color, the admin-editable field, and all six statuses
   carry one, so every badge on this card is inline-filled.

   Two-layer approach. The admin colours were reset to token values that
   pass AA with .label's white text on their own (Answered #8A5A0C,
   In Progress #2B5C7E, Open and Customer-Reply #3A3D42, On Hold and
   Closed #5A5E64), which fixes three pre-existing AA failures — Open
   3.45, Customer-Reply 2.94, Closed 3.54 — without any CSS. This block
   then puts the 4TS reading on top: everything recedes to the neutral
   tint, and Answered alone lifts to the warning tint, because it is the
   only ticket state where the CLIENT has to act.

   !important is on background-color only, and only because the value is
   an inline style — the one thing specificity cannot beat, so matching it
   is not an escalation for a specificity problem (CLAUDE.md constraint
   6). `color` needs none: this selector is (0,3,1) against .label's
   (0,1,0) at theme.css:9492.

   COUPLING: the lift is keyed to Answered's configured hex via an
   attribute substring match, because the badge carries nothing else to
   distinguish it — all six are identical .label elements differing only
   by inline colour and text. If Answered's colour is changed in
   Setup -> Support -> Ticket Statuses, update the selector to match.
   The failure mode is graceful: an unmatched Answered falls back to the
   neutral floor, which is legible, rather than breaking. The `i` flag
   makes the match case-insensitive so the stored casing does not
   matter. */
.client-home-cards div[menuitemname="Recent Support Tickets"] .label {
    background-color: var(--mh-bg-sunken) !important;
    color: var(--mh-text-body);
}
.client-home-cards div[menuitemname="Recent Support Tickets"] .label[style*="8a5a0c" i] {
    background-color: var(--mh-warning-tint) !important;
    color: var(--mh-warning-ink);
}

/* Markdown editor — theme.css:11094-11192 (Pass B row 26). Two greys
   throughout, kept at their relative weights rather than flattened to
   one token: #ddd (medium) -> --mh-border-strong, #eee (lighter, the
   blockquote rule only) -> --mh-border. #fff carries !important in the
   parent (11099, 11108) — matched per constraint 6. Headings inside
   markdown content take --mh-text, matching how headings read
   elsewhere; body-weight text (the status line) takes --mh-text-subtle
   where the stock value already cleared AA, --mh-text-muted where it
   didn't — theme.css's own #959694 was 2.97:1 against white, already
   failing before this pass touched it; --mh-text-muted reaches 6.52.
   .md-preview/.markdown-content pre (11158) gets the same dark
   surface-inverse/text-inverse pair as kbd/pre above — a code block is
   the same role wherever it appears. */
.md-editor,
div.md-editor.active {
    border-color: var(--mh-border-strong);
}
.md-editor > .btn-toolbar {
    background-color: var(--mh-surface) !important;
}
.md-editor > textarea.markdown-editor,
.md-editor > .md-preview {
    border-top-color: var(--mh-border-strong) !important;
    border-bottom-color: var(--mh-border-strong) !important;
    background-color: var(--mh-surface) !important;
}
.markdown-editor-status {
    color: var(--mh-text-muted);
}
.md-preview h1, .markdown-content h1,
.md-preview h2, .markdown-content h2,
.md-preview h3, .markdown-content h3,
.md-preview h4, .markdown-content h4 {
    color: var(--mh-text);
}
.md-preview pre, .markdown-content pre {
    background-color: var(--mh-surface-inverse);
    color: var(--mh-text-inverse);
}
.md-preview blockquote, .markdown-content blockquote {
    border-left-color: var(--mh-border);
}
.md-preview table, .markdown-content table {
    background-color: var(--mh-surface);
    border-color: var(--mh-border-strong);
}
.md-preview table > thead > tr > th, .markdown-content table > thead > tr > th {
    background-color: var(--mh-bg-sunken);
    border-color: var(--mh-border-strong);
}
.md-preview table > tbody > tr > td, .markdown-content table > tbody > tr > td {
    border-color: var(--mh-border-strong);
}

/* View-ticket panel — theme.css:12011-12055 (Pass B row 27). #ddd ->
   border-strong throughout; #f6f6f6 (the posted-by header and the
   attachment figure background) -> bg-sunken, the same wells/table-head
   role it plays elsewhere. */
.view-ticket .card-body {
    border-bottom-color: var(--mh-border-strong);
}
.view-ticket .posted-by {
    color: var(--mh-text-muted);
    background-color: var(--mh-bg-sunken);
}
.view-ticket .attachments {
    border-top-color: var(--mh-border-strong);
}
.view-ticket .attachment-list li span {
    border-color: var(--mh-border-strong);
}
.view-ticket .attachment-list li figure {
    background-color: var(--mh-bg-sunken);
}

/* Product details tabs — theme.css:12529 (Pass B row 32). */
div.product-details-tab-container {
    background-color: var(--mh-surface);
    border-color: var(--mh-border-strong);
}

/* Select-account / invite-box / continue-container — theme.css:9301-9334
   (Pass B row 33). Same two-grey weighting as the markdown editor
   above: #ddd -> border-strong, #eee -> border (lighter). The two
   static callout panels (.continue-container, .invite-box) take
   bg-sunken for their resting fill; the :hover state on a select-account
   row takes surface-hover, the token's own intended role, rather than
   the static-panel token. */
.select-account {
    border-top-color: var(--mh-border-strong);
}
.select-account a {
    border-bottom-color: var(--mh-border-strong);
}
.select-account a:hover {
    background-color: var(--mh-surface-hover);
}
.continue-container,
.invite-box {
    background-color: var(--mh-bg-sunken);
    border-color: var(--mh-border);
}

/* Radius audit follow-up (from the ticket-form corner-radius work): .card,
   .modal-content, .dropdown-menu, .popover, .alert, .progress and
   .list-group were still on Bootstrap's stock two-tier scale — 0.25rem
   for the first five of those seven, 0.3rem for the other two — never
   touched by 4B/4C's control-radius rollout. Same split reproduced here
   that "form controls all render the same" already closed for inputs and
   buttons. .card and .modal-content are done in this section; .alert and
   .progress are with their existing colour rules in ALERTS & BADGES;
   .dropdown-menu, .popover and .list-group are with theirs in
   NAVIGATION & SIDEBAR — each placed where its sibling rules already
   live rather than gathered in one block.

   .list-group's first pass here applied the token flat and shipped a
   real bug: a .list-group nested in a .card needs Bootstrap's own
   position-scoped treatment (round the true outer edge only, square
   everywhere else), not a uniform radius on all four corners — see the
   correction in NAVIGATION & SIDEBAR (search ".list-group as a direct
   child of .card") and its own regression fix a few rules above this
   comment (search "Re-check against the corrected"). The token value
   (lg) didn't change, only how it's applied.

   Tier assignment: tokens.css:262-267 names three of the seven by role
   in its own comments — "buttons, inputs, selects, pills" (md, already
   spoken for), "cards, panels, alerts" (lg) and "modals, feature panels"
   (xl) — which settles .card, .alert and .modal-content directly: lg,
   lg, xl. The other four aren't named. Rather than pick a bucket by eye,
   used Bootstrap's OWN grouping as the tie-breaker, since it's a real
   fact about the source and not a guess: stock Bootstrap already splits
   these seven into the same two tiers our token comments use —
   .card/.alert/.dropdown-menu/.progress/.list-group all sit at 0.25rem,
   .modal-content/.popover both at 0.3rem (theme.css, confirmed by direct
   read of each rule). card/alert anchor the 0.25rem tier at lg;
   modal-content anchors the 0.3rem tier at xl. dropdown-menu, progress
   and list-group take lg as 0.25rem-tier members; popover takes xl as a
   0.3rem-tier member, alongside modal rather than reassigned to a
   different bucket than the one its own source relationship puts it in.

   .card-header:first-child / .card-footer:last-child (theme.css:4191,
   4200) and .card-img(-top/-bottom) / .card-img-overlay (theme.css:4216-
   4234) all compute calc(<card radius> - 1px) so the inner corner sits
   1px inside the 1px border — kept as calc() against the token, not
   hardcoded, so it keeps tracking if the token ever changes.
   .modal-header / .modal-footer (theme.css:5253-5320) follow the same
   calc(<modal radius> - 1px) shape. None of these four is redefined at
   higher specificity anywhere in theme.css's hand-written tail (checked,
   not assumed, per CLAUDE.md's specificity-regression note) — all four
   are plain (0,1,0)/(0,2,0) overrides of the single compiled Bootstrap
   rule, same as every other component in this file.

   .mc-promo-manage / .mc-promo-login deliberately NOT included, even
   though theme.css:4123 lists them in the same compiled rule as .card
   (`.card, .mc-promo-manage, .mc-promo-login { ...border-radius:
   0.25rem }`) — checked, they don't appear in any .tpl in
   ../twenty-one-reference/ at all, meaning they're MarketConnect
   markup injected outside the versioned theme, the same posture as
   store.css/dynamic-store.css (CLAUDE.md, "Out of scope"). Leaving them
   off this selector list leaves them on Bootstrap's stock radius,
   untouched, same as those two files. */
.card {
    border-radius: var(--mh-radius-lg);
}
.card-header:first-child {
    border-radius: calc(var(--mh-radius-lg) - 1px) calc(var(--mh-radius-lg) - 1px) 0 0;
}
.card-footer:last-child {
    border-radius: 0 0 calc(var(--mh-radius-lg) - 1px) calc(var(--mh-radius-lg) - 1px);
}
.card-img,
.card-img-top {
    border-top-left-radius: calc(var(--mh-radius-lg) - 1px);
    border-top-right-radius: calc(var(--mh-radius-lg) - 1px);
}
.card-img,
.card-img-bottom {
    border-bottom-right-radius: calc(var(--mh-radius-lg) - 1px);
    border-bottom-left-radius: calc(var(--mh-radius-lg) - 1px);
}
.card-img-overlay {
    border-radius: calc(var(--mh-radius-lg) - 1px);
}
.modal-content {
    border-radius: var(--mh-radius-xl);
}
.modal-header {
    border-top-left-radius: calc(var(--mh-radius-xl) - 1px);
    border-top-right-radius: calc(var(--mh-radius-xl) - 1px);
}
.modal-footer {
    border-bottom-right-radius: calc(var(--mh-radius-xl) - 1px);
    border-bottom-left-radius: calc(var(--mh-radius-xl) - 1px);
}

/* Support department chooser, given the knowledgebase category card's
   shape. supportticketsubmit-stepone.tpl renders each department as a
   bare `<p class="h5"><a href="…deptid=N"><i class="fas fa-envelope">`,
   with nothing to hang a card on — so the link itself becomes the card
   rather than the paragraph. That is also what makes the whole box
   clickable, which is how the KB cards behave: there the anchor IS the
   .card-body (knowledgebase.tpl:17).
   Values taken from what those KB cards actually render rather than
   picked to look similar: --mh-radius-lg is what .card carries, 20px is
   the padding .primary-content .card-body gives them, 24px matches
   their mb-4, and text-decoration:none mirrors .kb-category a
   (theme.css:11941).
   Border is --mh-border-strong, not --mh-border. The KB cards keep
   Bootstrap's stock rgba(0,0,0,.125) — we only ever set radius on .card
   — and -strong (0.12) is that value on our ramp. The 0.08 hairline
   would be near-invisible here for a reason that does not apply on the
   KB page: these sit INSIDE the white page card, not on paper, so the
   border is the only thing separating card from background.
   Deliberately not copied from the KB cards: the article-count badge
   (departments have no equivalent figure) and text-transform — the
   uppercase in those categories is how the names are typed, not a rule
   ("MySQL DATABASES" keeps its mixed case), so forcing it here would be
   reading the screenshot wrong.
   Known limit: WHMCS renders an optional department description as a
   SIBLING paragraph after the link, not inside it. None are set today
   (confirmed on the live page), but if one is added it will sit below
   the card rather than within it, unlike KB. Merging them needs the
   markup changed, not more CSS. */
.card-body p.h5:has(> a[href*="deptid="]) {
    margin-bottom: var(--mh-space-24);
}
.card-body p.h5 > a[href*="deptid="] {
    display: block;
    padding: var(--mh-space-20);
    background-color: var(--mh-surface);
    border: 1px solid var(--mh-border-strong);
    border-radius: var(--mh-radius-lg);
    text-decoration: none;
}
.card-body p.h5 > a[href*="deptid="]:hover {
    background-color: var(--mh-surface-hover);
}


/* ==========================================================================
   6. TABLES
   ========================================================================== */

/* .table-primary — theme.css:1523-1533. Same tint ratios Bootstrap used
   for .alert-primary's background/border (see --mh-primary-mix-* in
   TOKENS). */
.table-primary,
.table-primary > th,
.table-primary > td {
    background-color: var(--mh-primary-mix-surface);
}
.table-primary th,
.table-primary td,
.table-primary thead th,
.table-primary tbody + tbody {
    border-color: var(--mh-primary-mix-border);
}

/* Table type — theme.css:1471-1481. Cells inherit body (now 15px) and
   take snug leading for density. Headers get semibold; `th` is bold (700)
   from the UA stylesheet, and 600 is the token for table heads.
   --mh-tracking-caps is deliberately NOT applied: the brief made it
   conditional on the headers being uppercase, and they aren't — .table th
   sets no text-transform anywhere in the parent, and the only uppercase
   rules in the theme are the .text-uppercase utility, tile titles,
   .initialism, .product-status-text and .tld-sale-group. */
.table th,
.table td {
    line-height: var(--mh-leading-snug);
}
.table th,
.table thead th {
    font-weight: var(--mh-weight-semibold);
}

/* Invoice/quote/billing-note line-item totals — invoice.css:9358, #f8f8f8.
   A different selector and file from the payment-summary partial's
   .invoice-summary-table td.total-row (theme.css:12340) — that one is
   untouched by this rule and needs its own. */
.invoice-container td.total-row {
    background-color: var(--mh-bg-sunken);
}

/* Remaining #efefef wells — theme.css:12340 (.invoice-summary-table,
   the payment-summary partial, distinct from the invoice.css rule just
   above), 12473 (masspay), 12478 (product icon well), 12068
   (ticket-feedback banner) — Pass B row 31, the last four members of
   this group. Same token as every other #efefef well already mapped. */
.invoice-summary-table td.total-row,
tr.masspay-total td,
div.product-details div.product-icon,
.ticketfeedbackstaffcont {
    background-color: var(--mh-bg-sunken);
}

/* Striped rows — theme.css:1514-1516, rgba(0, 0, 0, 0.05). A cool grey
   wash on warm paper; --mh-stone-100 is the paper tone itself, so the
   stripe now reads as the same family as the surface it sits on. Body
   text over it is 10.36:1. */
.table-striped tbody tr:nth-of-type(odd) {
    background-color: var(--mh-stone-100);
}

/* Listing-page table overflow at md — 4T measured the products table's
   max-content width at ~749px against ~690px available at the md
   breakpoint, pre-existing and unrelated to any of our type or colour
   work. domain-pricing.tpl already ships
   `<div class="table-container clearfix overflow-auto">`; every other
   listing page — clientareaproducts/invoices/quotes/domains/emails,
   supportticketslist, affiliates, managessl — renders the identical
   bare `<div class="table-container clearfix">`, no page-specific
   class or ID, confirmed across all eight templates. No template
   override needed: .table-container carries no CSS rule anywhere in
   the parent theme (checked — it's a bare, previously unstyled
   structural class), so a plain global rule reaches every one of these
   pages safely and identically, the same way domain-pricing.tpl's own
   markup already behaves. overflow-x only shows a scrollbar when
   content actually overflows, so this changes nothing on a page whose
   table already fits. */
.table-container {
    overflow-x: auto;
}

/* ==========================================================================
   7. ALERTS & BADGES
   ========================================================================== */

/* Radius — theme.css:4622-4627, stock 0.25rem. tokens.css:265 names
   "cards, panels, alerts" for --mh-radius-lg directly — see the
   radius-audit comment in CARDS & PANELS for the full reasoning shared
   across all seven components touched in that pass. */
.alert {
    border-radius: var(--mh-radius-lg);
}

/* Badge and label type — theme.css:4464-4473 (.badge) and 9483-9492
   (.label). Both are 75% of their parent and weight 700; .label is
   WHMCS's Bootstrap 3 holdover and carries the status pills (26 template
   uses vs .badge's 17). They are the same visual component at two
   different sizes, which reads as a bug — given identical treatment
   here. 2xs (11.5px) is the token's "badges, table micro-labels" tier,
   and is the one place in this pass that goes smaller rather than larger.

   Not touched: header.header .toolbar .nav-link .badge (theme.css:9618,
   10px) is the absolutely-positioned circular cart counter, which
   genuinely warrants micro size — and out-specifies this rule anyway. */
.badge,
.label {
    font-size: var(--mh-text-2xs);
    font-weight: var(--mh-weight-bold);
    letter-spacing: var(--mh-tracking-badge);
}

/* .badge-primary — theme.css:4500-4510. */
.badge-primary {
    background-color: var(--mh-primary);
}
a.badge-primary:hover,
a.badge-primary:focus {
    background-color: var(--mh-primary-active);
}
a.badge-primary:focus,
a.badge-primary.focus {
    box-shadow: var(--mh-shadow-focus);
}

/* Progress bar fill — theme.css:4772. */
.progress-bar {
    background-color: var(--mh-primary);
}

/* Radius — theme.css:4754-4761, stock 0.25rem, same Bootstrap tier as
   .card/.alert/.dropdown-menu/.list-group; see the radius-audit comment
   in CARDS & PANELS for the full tier reasoning. Assigned lg, not md,
   for that reason.
   .progress-bar itself (theme.css:4764+) carries no radius of its own —
   it relies on .progress's own overflow:hidden (also stock, unchanged)
   to clip its square corners to the track's rounded shape, same
   mechanism as every other "one token, cascade clips the rest"
   component in this file. Flagging rather than asserting a render
   result per CLAUDE.md constraint 7: the track is only 1rem (16px)
   tall, so a 14px radius is close to half the box and the browser will
   clamp it — expect the corners to render fully round (pill-like ends)
   rather than a shallow curve, and a very-low-percentage bar to show
   mostly rounded cap with little or no flat fill visible. Worth a look
   on staging specifically at a low fill percentage. */
.progress {
    border-radius: var(--mh-radius-lg);
}

/* .alert-primary — theme.css:4650-4653. The nested `.alert-primary hr`
   border (#b5c7d9) and `.alert-primary .alert-link` colour (#0e1c2a) are
   further shades on top of this that don't match any of the ratios
   given for this pass — not touched, flagged separately. */
.alert-primary {
    color: var(--mh-primary-mix-text);
    background-color: var(--mh-primary-mix-bg);
    border-color: var(--mh-primary-mix-surface);
}

/* Full-page loading overlay — defined identically at theme.css:9428
   (site-wide, via footer.tpl) and invoice.css:9371 (duplicated there
   because the three invoice-family pages don't include footer.tpl). One
   ID-selector rule covers both copies. */
#fullpage-overlay {
    background-color: var(--mh-overlay);
    color: var(--mh-text-inverse);
}

/* Popup backdrop — theme.css:9239, #2E363F (Pass B row 34, the second
   half; #fullpage-overlay above was the first). Same overlay role as
   #fullpage-overlay, same token. */
#popup-backdrop {
    background-color: var(--mh-overlay);
}



/* ---- Semantic alerts — theme.css:4674-4721. ink text, tint fill, edge
   border. Ratios 5.14 (warning) to 8.19 (success). */
.alert-success {
    color: var(--mh-success-ink);
    background-color: var(--mh-success-tint);
    border-color: var(--mh-success-edge);
}
.alert-warning {
    color: var(--mh-warning-ink);
    background-color: var(--mh-warning-tint);
    border-color: var(--mh-warning-edge);
}
.alert-danger {
    color: var(--mh-danger-ink);
    background-color: var(--mh-danger-tint);
    border-color: var(--mh-danger-edge);
}
.alert-info {
    color: var(--mh-info-ink);
    background-color: var(--mh-info-tint);
    border-color: var(--mh-info-edge);
}

/* .alert-{role} hr and .alert-{role} .alert-link are deliberately left
   stock, following the Pass A precedent set for .alert-primary. Bootstrap
   carries five values per role: text, fill, border, an hr rule between
   -edge and the solid, and an .alert-link darker than the text. We have
   three. Neither missing shade has a token, and inventing two more per
   role buys very little — the hr is a hairline and alert-link inherits a
   colour that still clears AA on the tint. Logged in the README. */

/* ---- Badges and labels move from solid fill to tint. components.html
   defines the default badge as tint background + edge border + ink text,
   with solid a separate opt-in (.mh-badge-solid) reserved for badges on
   coloured or dark surfaces.

   This is a contrast fix as much as an aesthetic one: --mh-text-inverse
   on --mh-success is 3.42:1 and on --mh-warning 3.77:1, and badge text is
   11.5px, so the stock solid fills failed AA outright with no large-text
   exemption available. Tint + ink clears it for every role (5.14-8.19).

   .badge and .label were unified at 11.5px in 4T and take the same
   treatment here. They need different handling underneath, though:
   .badge-* sets colour and background in the parent (theme.css:4526-4578)
   while .label-* sets background only (10183-10201) and inherits
   color:#fff from .label at 9492 — so colour has to be set for both.
   Bootstrap badges carry no border at all, so border-width and style are
   declared here alongside the token colour.

   SPECIFICITY — the selectors are compound (.label.label-success, not
   .label-success) and must stay that way. theme.css:10743-10749 sets
   `.div-service-status .label { color: white }` at (0,2,0), which beats a
   plain .label-{role} at (0,1,0). The first 4S implementation used the
   plain form, so the dashboard status badges on
   includes/active-products-services-item.tpl:6 kept white text over the
   new pale tint — 1.11:1 for Active, 1.15 for Pending, 1.16 for
   Suspended. Illegible, and fixed here.
   Compound .label.label-{role} is (0,2,0): it ties that rule and wins on
   source order, and unlike scoping to .div-service-status it holds in
   any container. .badge is given the same form for symmetry — .badge
   declares no base colour of its own, so it had no equivalent bug.
   Terminated was unaffected throughout: it renders .label-default, which
   4S never touched (white on #666, 5.74:1). */
.badge.badge-success,
.label.label-success {
    color: var(--mh-success-ink);
    background-color: var(--mh-success-tint);
    border: 1px solid var(--mh-success-edge);
}
.badge.badge-warning,
.label.label-warning {
    color: var(--mh-warning-ink);
    background-color: var(--mh-warning-tint);
    border: 1px solid var(--mh-warning-edge);
}
.badge.badge-danger,
.label.label-danger {
    color: var(--mh-danger-ink);
    background-color: var(--mh-danger-tint);
    border: 1px solid var(--mh-danger-edge);
}
.badge.badge-info,
.label.label-info {
    color: var(--mh-info-ink);
    background-color: var(--mh-info-tint);
    border: 1px solid var(--mh-info-edge);
}

/* Linked badges — theme.css:4530-4581. Hover steps the fill from -tint to
   -edge, except warning: ink on warning-edge is 4.00, a fail, so warning
   holds at -tint (5.14) and signals hover through the border alone. */
a.badge-success:hover,
a.badge-success:focus {
    color: var(--mh-success-ink);
    background-color: var(--mh-success-edge);
}
a.badge-danger:hover,
a.badge-danger:focus {
    color: var(--mh-danger-ink);
    background-color: var(--mh-danger-edge);
}
a.badge-info:hover,
a.badge-info:focus {
    color: var(--mh-info-ink);
    background-color: var(--mh-info-edge);
}
a.badge-warning:hover,
a.badge-warning:focus {
    color: var(--mh-warning-ink);
    background-color: var(--mh-warning-tint);
    border-color: var(--mh-warning-ink);
}

/* ==========================================================================
   8. NAVIGATION & SIDEBAR
   ========================================================================== */

/* Radius — .dropdown-menu (theme.css:2787-2805, stock 0.25rem) and
   .popover (theme.css:5463-5490, stock 0.3rem, distinct from the plain
   .popover font-family rule already near the top of TOKENS). Neither is
   named in tokens.css:262-267's own component comments; see the
   radius-audit comment in CARDS & PANELS for the full reasoning — both
   take whichever tier Bootstrap's own stock value already groups them
   with (.dropdown-menu alongside .card/.alert at lg, .popover alongside
   .modal-content at xl), not a freshly invented bucket.
   .popover-header (theme.css:5590-5599) computes calc(<popover radius> -
   1px) for its top two corners, same 1px-inset-from-border shape as
   .card-header/.modal-header above; kept as calc() against the token. */
.dropdown-menu {
    border-radius: var(--mh-radius-lg);
}
.popover {
    border-radius: var(--mh-radius-xl);
}
.popover-header {
    border-top-left-radius: calc(var(--mh-radius-xl) - 1px);
    border-top-right-radius: calc(var(--mh-radius-xl) - 1px);
}

/* Dropdown active item — theme.css:2962-2965. */
.dropdown-item.active,
.dropdown-item:active {
    background-color: var(--mh-primary);
}

/* Header dropdown active item — parent is
   header.header .dropdown-item.active, header.header .dropdown-item:active
   at theme.css:9656, specificity (0,3,1) — header element + .header class +
   .dropdown-item class + .active class/pseudo-class. The general
   .dropdown-item.active override above is only (0,2,0) and loses to this
   hand-written header rule, so header nav dropdowns still render the
   parent's grey (#ccc) instead of brand. Match the parent's specificity
   here rather than escalating with !important. */
header.header .dropdown-item.active,
header.header .dropdown-item:active {
    background-color: var(--mh-primary);
}

/* Header dropdown plain hover/focus — parent is
   .dropdown-item:hover, .dropdown-item:focus at theme.css:2957-2961
   (Bootstrap layer, global, specificity (0,2,0): .dropdown-item class +
   :hover pseudo-class). Not touched by the active-item fix above, which
   only covers .active/:active. Scoped to header.header so it wins
   ((0,3,1), header element + .header class + .dropdown-item class +
   :hover) without changing dropdown hover anywhere else in the theme —
   this class is used throughout, e.g. footer.footer .dropdown-item at
   theme.css:9724. Tinted rather than solid so hover reads as a lighter
   preview of the active state fixed above, not a duplicate of it. */
header.header .dropdown-item:hover,
header.header .dropdown-item:focus {
    background-color: var(--mh-primary-tint);
    color: var(--mh-primary);
}

/* Pill nav active state — theme.css:3732-3735. */
.nav-pills .nav-link.active,
.nav-pills .show > .nav-link {
    background-color: var(--mh-primary);
}

/* Pagination — theme.css:4390-4426. */
.page-link {
    color: var(--mh-link);
}
.page-link:hover {
    color: var(--mh-link-hover);
}
.page-link:focus {
    box-shadow: var(--mh-shadow-focus);
}
.page-item.active .page-link {
    background-color: var(--mh-primary);
    border-color: var(--mh-primary);
}

/* Pagination link colour — parent is .pagination > li > a,
   .pagination > li > span at theme.css:12873, specificity (0,1,2) —
   .pagination class + li element + a element. The .page-link override
   above is only (0,1,0) and loses to this hand-written rule, so link text
   still renders the parent's #444 instead of brand. Match the parent's
   specificity here. The active-page background already wins on source
   order via .page-item.active .page-link above, so only the resting
   link colour needs this fix. */
.pagination > li > a,
.pagination > li > span {
    color: var(--mh-link);
}

/* Radius — theme.css:4804-4809, stock 0.25rem, same Bootstrap tier as
   .card/.alert/.dropdown-menu; see the radius-audit comment in CARDS &
   PANELS for the full reasoning behind the token choice.

   CORRECTED — the flat version of this rule (border-radius: lg,
   unconditional) shipped a real bug: Bootstrap never rounds a
   .list-group nested in a .card that way, it rounds only the true outer
   edge (.card > .list-group:first-child / :last-child, both (0,3,0),
   calc(0.25rem - 1px)) and leaves every other corner at 0. At
   Bootstrap's stock 4px that distinction was nearly invisible; at our
   14px it produced two confirmed, live bugs — a bottom curve that
   didn't match the card's own 14px (the old flat rule left .list-group
   at 14px while :last-child, still on the OLD stock calc(0.25rem-1px)
   value, forced the bottom corners to 3px — a visible gap against the
   card), and a top corner that rounded under a flat card-header/card-body
   edge because nothing had ever needed to zero it before (nothing in
   this theme puts .list-group first in a .card — a header or body
   always precedes it — so Bootstrap's :first-child rule never fires and
   the corner was left to whatever the base rule said).

   Restricted here rather than left to rely on the position rules always
   outranking it: excludes anything inside a .card (any depth, not just
   direct children — deeply-nested lists like bulkdomainmanagement.tpl's
   and configuressl-steptwo.tpl's fall back to Bootstrap's own stock 4px
   rather than picking up the token, a deliberate, minor trade for not
   having this rule's reach depend on every card-position rule below
   staying complete) and anything inside .mc-promo-manage/.mc-promo-login
   (MarketConnect, same hands-off posture as .card there). The actual
   card-nested corner logic lives in the rules below and in
   .client-home-cards's own correction in CARDS & PANELS. */
.list-group:not(.card *):not(.mc-promo-manage *):not(.mc-promo-login *) {
    border-radius: var(--mh-radius-lg);
}

/* Active item's fill overflowing the card's curve at the true top/bottom
   edge — same underlying shape as the bug above, one layer down.
   .list-group-item:first-child/:last-child (theme.css:4835-4839) get
   their OWN corner via border-top-left-radius/border-top-right-radius/
   etc.: inherit, which is correct in principle — Bootstrap wrote it
   that way on purpose so the end item always matches whatever radius
   the list around it ends up with, rather than needing every
   consuming component to also override the item. But it makes the
   item's visible corner load-bearing on getting .list-group's OWN
   corner right in EVERY markup shape it can appear in, including ones
   this theme adds that Bootstrap never had to think about — confirmed
   real via the storefront's "Categories" sidebar (fetched live,
   #order-standard_cart's markup isn't in ../twenty-one-reference/):
   .cart-sidebar's panels put .card-header directly, then
   `<div class="list-group collapsable-card-body">` — .list-group and
   .collapsable-card-body are the SAME element there, not the two nested
   elements includes/sidebar.tpl uses for its own left-nav panels
   (search "a case the rules above cannot reach at all" below) — a
   THIRD structural shape, distinct from both cases already handled,
   that a inherit-only approach would need yet another selector for
   every time a future page introduces a fourth.

   Decided against giving .list-group-item:first-child/:last-child their
   own explicit (non-inherit) radius instead — that would mean mirroring
   the ENTIRE first/last/collapsable-card-body selector matrix above a
   second time, one DOM level deeper, and staying in sync with it
   forever after. overflow: hidden here is one rule, fixes the current
   bug and every future markup shape at once, and costs nothing visible
   when .list-group's own radius is already correct (which it is, once
   the rules above are right) — it only ever matters as a backstop.
   The .card itself was the other candidate raised and rejected: .card
   is used far too broadly (dashboard panels, ticket cards, invoice
   panels) to safely clip everything inside it — a dropdown menu or a
   focus glow opening from a .card-header button, for instance, would be
   cut off. .list-group is the right element to clip precisely because
   it's also the element whose own radius is what's supposed to be
   showing through — clipping it to itself is a no-op when correct and
   a fix when it isn't. */
.list-group {
    overflow: hidden;
}

/* .list-group as a direct child of .card — theme.css:4138-4156. Mirrors
   Bootstrap's own :first-child/:last-child split on our token (real in
   this theme: the KB/downloads "related articles"/"popular" widgets and
   the dashboard's .client-home-cards panels all put .list-group as a
   card's last child, after a .card-body acting as a plain heading — so
   :last-child fires in practice, :first-child structurally can't, since
   something always precedes the list). Bootstrap needs no rule for the
   corner that ISN'T the true edge — at 4px nobody could see it was still
   rounded — so :not(:first-child)/:not(:last-child) are new here, not
   copied from the source; without them the un-zeroed corner would still
   show 14px from the base rule once it stopped being excluded by
   position (it isn't excluded — .card > .list-group is a real, matched
   selector, just not the FIRST-matching one; explicit zero, not
   omission, is what keeps it square). */
.card > .list-group:first-child {
    border-top-left-radius: calc(var(--mh-radius-lg) - 1px);
    border-top-right-radius: calc(var(--mh-radius-lg) - 1px);
}
.card > .list-group:last-child {
    border-bottom-right-radius: calc(var(--mh-radius-lg) - 1px);
    border-bottom-left-radius: calc(var(--mh-radius-lg) - 1px);
}
.card > .list-group:not(:first-child) {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}
.card > .list-group:not(:last-child) {
    border-bottom-right-radius: 0;
    border-bottom-left-radius: 0;
}

/* .list-group inside a sidebar card (includes/sidebar.tpl) — a case the
   rules above cannot reach at all, checked against the actual template
   rather than assumed. Sidebar panels wrap their list-group in an extra
   .collapsable-card-body div, between .card-header and .list-group (it
   exists for the panel-collapse JS, not for us), which breaks the
   .card > .list-group child combinator outright: neither Bootstrap's own
   position rules nor the four just above can match through it. Before
   this fix the sidebar's list-group — including the current-page
   ("active") item, styled green via .sidebar .list-group-item.active —
   got the (now-excluded) blanket rule uncontested on every corner: not
   the 3px/14px mismatch above, but the same top-corner problem, since
   .card-header unconditionally precedes .collapsable-card-body in this
   template — the list is never at the card's true top edge, so the top
   is always an interior seam and needs no first-child conditional.
   The bottom genuinely varies: $item->hasFooterHtml() is a live
   per-panel condition. No footer -> .collapsable-card-body is .card's
   last child and the list sits at the true bottom edge, rounds like the
   direct-child case above; a footer -> that edge is an interior seam
   too and stays square. */
.card > .collapsable-card-body > .list-group {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}
.card > .collapsable-card-body:last-child > .list-group:last-child {
    border-bottom-right-radius: calc(var(--mh-radius-lg) - 1px);
    border-bottom-left-radius: calc(var(--mh-radius-lg) - 1px);
}
.card > .collapsable-card-body:not(:last-child) > .list-group {
    border-bottom-right-radius: 0;
    border-bottom-left-radius: 0;
}

/* Collapsed sidebar panel — stray border line inside the card's rounded
   bottom corner. Traced, not guessed: .card-header's own border-bottom
   (theme.css:4185-4190, stock, correct when the panel is open — that's
   the seam between the title and the list below it) doesn't go away
   when the panel is minimised. js/whmcs.js:95-104 handles the collapse
   entirely in JS: clicking .card-minimise calls jQuery .slideUp() on
   .collapsable-card-body (an inline style, not a class — nothing to
   select there directly) and toggles a class called "minimised", but
   that class lands on .card-minimise itself (the chevron, for its own
   rotation animation), never on .card-header, .card, or the body. With
   the body gone, the card shrinks to wrap just the header, and the
   header's flat bottom border is now sitting right at the card's own
   --mh-radius-lg curve (CARDS & PANELS) instead of the straight seam it
   was drawn for.

   .card-minimise IS a real, live-toggled class, and it lives inside
   .card-header (nested in .card-title) — :has() reaches it from there,
   same technique already used for the invoice-panel case in CARDS &
   PANELS. (0,3,0): .card-header contributes 1, :has(.card-minimise
   .minimised) contributes the specificity of its own argument, 2
   classes — comfortably past the stock (0,1,0) rule, no source-order
   dependency needed. Also covers the "minimise on load below 720px"
   branch in the same JS block, which sets the identical class through a
   different trigger.

   Scoped to :has(), not to .card-sidebar generally, so a panel that's
   open keeps its border exactly as before — this is the collapsed state
   only, not a blanket removal. */
.card-header:has(.card-minimise.minimised) {
    border-bottom: 0;
}

/* Active list-group item — theme.css:4848-4852. */
.list-group-item.active {
    background-color: var(--mh-primary);
    border-color: var(--mh-primary);
}

/* Sidebar current-page marker — parent is .sidebar .list-group-item.active,
   .sidebar .list-group-item.active:hover, .sidebar .list-group-item.active:focus
   at theme.css:10439, specificity (0,3,0) — three classes (.sidebar,
   .list-group-item, .active). The general .list-group-item.active override
   above is only (0,2,0) and loses to this hand-written sidebar rule, so the
   sidebar's current-page marker still renders the parent's #666 instead of
   brand (confirmed on staging). Match the parent's specificity here; equal
   specificity plus later source order (custom.css loads last) wins, per the
   verified .btn-primary case above. */
.sidebar .list-group-item.active,
.sidebar .list-group-item.active:hover,
.sidebar .list-group-item.active:focus {
    background-color: var(--mh-primary);
    border-color: var(--mh-primary);
}

/* .list-group-item-primary — theme.css:4991-5001. The plain :hover/:focus
   background (#b5c7d9) is a further shade not covered by this pass's
   ratios — left untouched, same as the matching .alert-primary hr note
   in ALERTS & BADGES. */
.list-group-item-primary {
    color: var(--mh-primary-mix-text);
    background-color: var(--mh-primary-mix-surface);
}
.list-group-item-primary.list-group-item-action.active {
    background-color: var(--mh-primary-mix-text);
}

/* ---- Header chrome — theme.css:9564-9656. Every page. -------------- */

/* Header bar itself. Moved from --mh-surface (#FFFFFF, which is what the
   stock #fff tokenised to) to --mh-bg, the brand's page background —
   tokens.css calls stone-100 "PAPER", and the header is chrome rather
   than a raised surface like a card or a modal, so surface was the
   wrong role for it.
   Nothing separates the header from the page by border or shadow (stock
   sets background-color and nothing else), so it was relying on
   white-against-paper to define its edge. That still holds: header.tpl
   renders <nav class="master-breadcrumb"> unconditionally directly
   beneath it, and the breadcrumb band is --mh-bg-sunken (#EFEDE8) — a
   distinctly warmer tone — so the boundary stays legible even though
   the header now matches the body colour below that band.
   Text contrast is effectively unchanged: --mh-text-body (#3A3D42) goes
   from 10.90:1 on white to 10.36:1 on paper, both far above AA. */
header.header {
    background-color: var(--mh-bg);
}

/* Logo, swapped to the SVG. WHMCS's logo upload doesn't accept SVG, so
   {$assetLogoPath} (header.tpl:83-84, `<img src="{$assetLogoPath}"
   class="logo-img">`) always resolves to the PNG — CSS content: url()
   replaces the rendered image without touching the template, keeping
   the zero-override discipline (CLAUDE.md hard constraint 1).
   header.header .logo-img already carries WHMCS's own sizing
   (max-width:400px, max-height:60px, theme.css:9593-9596); content:
   url() replaces what the <img> renders, not the element's own box
   model, so that constraint still applies to the SVG untouched — no
   extra sizing needed here.
   No specificity fight to win: nothing else in either stylesheet sets
   `content` on .logo-img, so this doesn't need to out-rank anything,
   just state a new property. Matched the exact selector for locality
   with the rule above, not because winning required it.
   Path is theme-relative: logo.svg lives in theme/img/ (part of this
   repo, deploys with everything else — confirmed present locally,
   15688 bytes, matching the file already served from the earlier,
   separately-uploaded copy this replaces), one directory up from
   theme/css/custom.css, hence ../img/. Known, accepted trade-off: the
   <img> tag's own `src` still names the PNG, so some browsers may fetch
   it even though it's never painted — not worth a template change to
   avoid one small unused request. */
header.header .logo-img {
    content: url('../img/logo.svg');
}

/* Topbar (logged-in only — header.tpl:15 wraps it in {if $loggedin}) is a
   dark surface, mapped to surface-inverse. The source used three tiers of
   white-on-#404040 opacity (.65 base, .75 for the "logged in as" label,
   .65 dashed underline); preserved as two tiers against the new, darker
   background rather than carried over as literal alpha, since alpha alone
   doesn't guarantee AA on a different base colour — checked directly:
   --mh-text-inverse-muted (#9AA0A6) on --mh-surface-inverse (#1A1D21) is
   6.4:1, --mh-text-inverse (#FAF9F6) on the same is 16.1:1. Both clear
   WCAG AA's 4.5:1 with room to spare. */
header.header .topbar {
    background-color: var(--mh-surface-inverse);
    color: var(--mh-text-inverse-muted);
}
header.header .topbar .btn,
header.header .topbar .active-client .btn {
    color: var(--mh-text-inverse-muted);
}
/* "Logged in as:" sitting above the client name rather than beside it.
   Cause is structural, in stock markup: header.tpl:49-50 renders
   <div class="input-group-prepend d-none d-md-inline">, and .d-md-inline
   is `display: inline !important`, which overrides the
   .input-group-prepend { display: flex } that would normally centre the
   label. Being a flex item of .input-group, that inline display is then
   blockified to `block` (CSS Display 3 §2.7), so the prepend is a plain
   block stretched to the row height with its label parked at the TOP,
   while the .btn-group beside it centres its own text against the
   button padding — hence two different vertical positions.
   The two boxes are also unequal heights: stock keeps this label at
   0.25rem padding / 0.8rem type via a (0,4,1) rule, but sets no
   line-height, so 4B's control-height line-height (18px, sized for a
   42px form control) reaches it through the generic (0,1,0)
   .input-group-text rule and applies — a directly-set value beating the
   container's inherited line-height: 1.
   Centring the group on the cross axis fixes it at the level the
   layout is actually decided, rather than trying to match the two boxes
   by hand: each item then shrinks to its own content and sits on a
   shared centre line whatever its height. Same selector stock uses for
   this group, (0,3,1), and a property it doesn't set. */
header.header .topbar .active-client {
    align-items: center;
}
header.header .topbar .active-client .input-group-text {
    color: var(--mh-text-inverse);
}
header.header .topbar .active-client .btn.btn-active-client span {
    border-bottom-color: var(--mh-text-inverse-muted);
}

/* Toolbar icon buttons (cart, mobile menu) sit on the light header, not
   the topbar — theme.css's own #e5e5e5/#666 here is already a
   deliberately quiet default-vs-hover pair, not a contrast problem;
   tokenised at the same weight rather than redesigned.

   Brought into line with the search box beside it, which is the same
   visual weight and the same kind of control but was carrying a
   different neutral for its border and a different grey for its icon —
   two controls sitting side by side in one header, styled from two
   different places. Border moves from the raw --mh-stone-300 ramp value
   to --mh-border-control, the role token the search box (and every
   other bordered control in the theme) already uses; icon colour moves
   to --mh-text-subtle to match the magnifier. Both are small increases
   in contrast, not decreases: the icon was #E3E0D9 on a white header
   (~1.3:1, below any threshold) and is now 3.29:1, which clears WCAG
   2.1 SC 1.4.11's 3:1 bar for a non-text UI component — the same figure
   the search icon has always rendered at, so this is parity, not a new
   compromise. Say if you'd rather both went darker (--mh-text-muted is
   6.5:1); that's a weight change, not a consistency one, so it isn't
   made here.

   Radius: both this and .search below were still on Bootstrap's stock
   0.25rem. The radius reconciliation never reached them — these are
   hand-written header-chrome rules at (0,3,1)/(0,2,1) that out-specify
   the general .btn/.form-control token rules, so the controls kept 4px
   while everything else on the page moved to 10px. Same token, stated
   on the selectors that actually win. */
header.header .toolbar .nav-link {
    border-color: var(--mh-border-control);
    color: var(--mh-text-subtle);
    border-radius: var(--mh-radius-md);
}
header.header .toolbar .nav-link:hover {
    color: var(--mh-text-muted);
}

/* Knowledgebase search box. */
header.header .search {
    border-color: var(--mh-border-control);
    border-radius: var(--mh-radius-md);
}
header.header .search .btn {
    color: var(--mh-text-subtle);
}
/* Exposed by the header moving off white: both children inside .search
   carry a white fill — the input from Bootstrap's own .form-control
   {background-color:#fff}, the magnifier from .btn-default's
   --mh-surface — and until now that was invisible because it matched
   the header exactly. On paper it reads as a white pill, with square
   corners poking past the wrapper's --mh-radius-md curve at all four
   corners: the same clipping shape already fixed for .list-group and
   the invoice panel.
   Made transparent rather than clipped. overflow:hidden on .search
   would work for the corners but would also cut off the input's own
   green focus glow (--mh-shadow-focus, painted outside its box), which
   is a worse trade. Transparent removes the rectangle instead of
   hiding its edges, and it matches the control standing next to it:
   the cart button is already transparent-with-a-border, so the two
   header controls now read as one system. The wrapper keeps the border
   and the :focus-within green below, so the field is still clearly a
   field and focus is still clearly signalled.
   Same selector pair the parent uses for its own border reset
   (theme.css:9635), (0,3,1) — clears stock .form-control (0,1,0) and
   our .btn-default rule (0,2,0) outright. */
header.header .search .btn,
header.header .search .form-control {
    background-color: transparent;
}
/* :focus-within border was grey (#a5a5a5), independent of the green
   focus ring Pass A already puts on the inner .form-control (FORMS
   above) — two different colours doing the same job. Matched to
   --mh-primary so the wrapper border and the input's own focus glow
   read as one system instead of clashing. */
header.header .search:focus-within {
    border-color: var(--mh-primary);
}

/* Main nav links. */
header.header .navbar a {
    color: var(--mh-text-body);
}

/* ---- Footer — theme.css:9702-9712. Every page. Dark surface, same
   token pair as the topbar above and the same AA result: 16.1:1. -------- */
footer.footer {
    background-color: var(--mh-surface-inverse);
    color: var(--mh-text-inverse);
}
footer.footer .nav-link {
    color: var(--mh-text-inverse);
}
/* "Contact Us" removed — sales enquiries go to the main site's form and
   client-area users have ticketing, so the footer link is a third door
   to neither. Hardcoded in footer.tpl:34-38, not a menu item, so there
   is no admin setting to switch it off; done in CSS to stay inside
   hard constraint 1 rather than forking the template for one <li>.
   Keyed on the href rather than on position — it happens to be
   first-child today, but that's a fact about the current config (the
   sibling TOS item is behind {if $acceptTOS}, currently off), and a
   positional selector would silently start hiding the wrong link if
   that ever changes.
   Hides the <li>, not just the <a>, so no empty list item is left in
   the flex row. Nothing else is left behind either: checked the
   rendered page, this is currently the only item in that <ul>, and an
   emptied .nav collapses to zero height — it sets margin-bottom: 0 and
   padding-left: 0, and the mb-7 class beside it isn't defined in any
   stylesheet we load, so there's no residual gap to clean up.
   Note this hides the link, it does not retire the route: contact.php
   still resolves, and is still linked from error/page-not-found.tpl and
   two store/dynamic partials (the latter inside the excluded store
   area). Say the word if those should go too. */
footer.footer .nav-item:has(> a[href$="/contact.php"]) {
    display: none;
}
/* .btn background was rgba(255,255,255,.1) exactly — --mh-border-inverse
   is that identical value, already defined for dark-surface use. */
footer.footer .btn {
    background-color: var(--mh-border-inverse);
    color: var(--mh-text-inverse);
}


/* ---- Login page: minimal chrome, centred card ---------------------- */

/* Scoped on body:has(.login-form), which is exact. Verified by grepping
   every .tpl in the parent theme: `.login-form` and `#login` appear in
   login.tpl and nowhere else — not in two-factor-challenge.tpl, not in
   oauth/login-twofactorauth.tpl, and not in any of the four
   password-reset-*.tpl. Those pages keep the normal header, breadcrumb
   and footer, which is the right outcome: someone midway through a
   password reset or a 2FA challenge should still have navigation.
   There is no page identifier to key off otherwise — <body> renders as
   class="primary-bg-color" on every page including this one — so :has()
   is doing real work here rather than being a shortcut.
   Deliberately NOT hidden: WHMCS's own dev-license notice, which sits
   inside .primary-content on staging. It is a system message about the
   install's licensing state, not our chrome, and suppressing it would
   hide something that is meant to be seen. It won't render in
   production. */
body:has(.login-form) header.header,
body:has(.login-form) .master-breadcrumb,
body:has(.login-form) footer.footer {
    display: none;
}

/* #main-body is the section wrapping the whole content area, so it is
   what can actually fill the viewport once the chrome above and below
   is gone. min-height rather than height, and padding rather than a
   fixed box: with min-height, a card taller than the viewport makes the
   section grow and centring simply stops applying, instead of the top
   of the card being clipped out of reach — the usual failure mode of
   centring with a fixed height. */
body:has(.login-form) #main-body {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    padding-top: var(--mh-space-32);
    padding-bottom: var(--mh-space-32);
}

/* The logo went with the header, so it is reintroduced above the card —
   same content: url() approach as the header mark and the invoice, and
   the same theme-relative path. No width is set: the intrinsic 190x61
   is the asset's own, and repeating it here would be a second number to
   keep in step with the file.
   Non-clickable and with no alt text, which was agreed — it is
   decorative here rather than the site's home link, and the page it
   would link to is the one you are already on.

   Hung off #main-body, not off .login-form. The first attempt put it on
   the form with `display:block; margin:0 auto` and it rendered hard
   against the left edge instead of centring: auto margins only centre a
   block-level REPLACED box, and engines do not consistently treat a
   pseudo-element carrying content:url() as one — treat it as a block
   box with an inline image inside and the box centres while the image
   stays at its left. Making it a flex item of the column above sidesteps
   that entirely: align-items:center places it by the flex algorithm,
   which has no such ambiguity, and it lands above .container because a
   ::before is the container's first child. */
body:has(.login-form) #main-body::before {
    content: url('../img/logo.svg');
    display: block;
    margin-bottom: var(--mh-space-32);
}



/* ---- Semantic list-group items — theme.css:5019-5074. Bootstrap sits
   these at edge level rather than tint, and that is kept for three roles
   (ink on edge: 6.27 / 5.25 / 5.84). Warning is the exception: ink on
   warning-edge is 4.00, a fail, so warning uses -tint (5.14).

   :hover has no colour step. Bootstrap darkens to a fourth shade
   (e.g. #b1dfbb at 5023) that sits between -edge and -ink and has no
   token; rather than leave the stock green showing, hover holds the base
   fill. Logged in the README. .active inverts to an ink fill, which is
   how Bootstrap treats it too. */
.list-group-item-success,
.list-group-item-success.list-group-item-action:hover,
.list-group-item-success.list-group-item-action:focus {
    color: var(--mh-success-ink);
    background-color: var(--mh-success-edge);
}
.list-group-item-warning,
.list-group-item-warning.list-group-item-action:hover,
.list-group-item-warning.list-group-item-action:focus {
    color: var(--mh-warning-ink);
    background-color: var(--mh-warning-tint);
}
.list-group-item-danger,
.list-group-item-danger.list-group-item-action:hover,
.list-group-item-danger.list-group-item-action:focus {
    color: var(--mh-danger-ink);
    background-color: var(--mh-danger-edge);
}
.list-group-item-info,
.list-group-item-info.list-group-item-action:hover,
.list-group-item-info.list-group-item-action:focus {
    color: var(--mh-info-ink);
    background-color: var(--mh-info-edge);
}
.list-group-item-success.list-group-item-action.active {
    color: var(--mh-text-inverse);
    background-color: var(--mh-success-active);
    border-color: var(--mh-success-active);
}
.list-group-item-warning.list-group-item-action.active {
    color: var(--mh-text-inverse);
    background-color: var(--mh-warning-ink);
    border-color: var(--mh-warning-ink);
}
.list-group-item-danger.list-group-item-action.active {
    color: var(--mh-text-inverse);
    background-color: var(--mh-danger-ink);
    border-color: var(--mh-danger-ink);
}
.list-group-item-info.list-group-item-action.active {
    color: var(--mh-text-inverse);
    background-color: var(--mh-info-ink);
    border-color: var(--mh-info-ink);
}

/* Header notification popover — theme.css:10883-10941 (Pass B row 29,
   core-rendered). #ddd -> border-strong; the #555 link text ->
   text-body; the #f6f6f6 hover fill -> surface-hover, the token's own
   intended role. The severity icons (#5cb85c/#ecc317/#ec2d45) are
   semantic — notification severity — and stay untouched, same as the
   original Pass B classification. */
.popover-user-notifications {
    border-color: var(--mh-border-strong);
}
.popover-user-notifications ul li {
    border-bottom-color: var(--mh-border-strong);
}
.popover-user-notifications ul li a {
    color: var(--mh-text-body);
}
.popover-user-notifications ul li a:hover {
    background-color: var(--mh-surface-hover);
}

/* ==========================================================================
   9. CART & CHECKOUT
   ========================================================================== */

/* Billing/service status badges — theme.css:10203-10295. #336699 here is
   the neutral/informational slot in a semantic status palette (green
   #5cb85c success, red #d9534f attention, amber #f0ad4e warning, blue
   neutral/informational) — not the brand colour. Our primary is green,
   so mapping this to --mh-primary would make .status-payment-pending
   read as "success" (near-identical to .status-paid). Mapped to a
   neutral stone slate instead, matched to the weight of the untouched
   neutral greys already in this same block (.status-closed /
   .status-cancelled #87939f, .status-terminated #666). */
.status-payment-pending,
.status-delivered {
    background-color: var(--mh-stone-600);
}
/* .status-onhold was in the rule above until 4TS. It is a TICKET status
   as well as an invoice one, so it moved to the tint scheme below; the
   two invoice statuses keep the Pass A stone-600 solid. */


/* ---- Ticket statuses — theme.css:10211-10258. WHMCS picked these from
   the admin's point of view, where "Customer-Reply" means you must
   answer (loud orange) and "Answered" means you are done (muted purple).
   Both meanings invert in the client area and the palette was never
   re-picked for it. Reframed by who needs to act, and moved to the tint
   scheme so ticket badges match the ones 4S converted.

     answered        client acts      -> warning tint, the only ticket
                                         status that should draw the eye
     inprogress      support acts     -> info tint
     open            support acts     -> neutral, live
     customer-reply  support acts     -> neutral, live
     onhold          nobody           -> neutral, dormant
     closed          nobody           -> neutral, dormant + flat

   SELECTOR AUDIT (the 4S regression check): only two rules in any
   stylesheet we load set `color` on a status/label/badge selector above
   (0,1,0) — .div-service-status .label (theme.css:10743, 0,2,0) and
   .sidebar .list-group-item .badge (10432, 0,3,0). Neither is a ticket
   badge container: the first is the products dashboard, which renders
   .label-{modifier} and never .status-*, and the second is bare .badge.
   So the only colour these rules have to beat is .label { color:#fff }
   at theme.css:9492, which is (0,1,0) — a plain .status-* selector ties
   it and wins on source order. Plain selectors are used deliberately
   rather than compound .label.status-*, because clientareaproductdetails
   .tpl:467 renders .status-* without .status, and a compound form would
   miss it. Ink is set on every rule regardless, which is what 4S got
   wrong.

   TRAP 1: .status-open shares theme.css:10211 with .status-active and
   .status-completed, which are service and invoice statuses where green
   is correct. Only .status-open is named here; equal specificity plus
   later source order takes it out of that rule without touching the
   other two.
   TRAP 2: .status-onhold shared our own Pass A rule with
   .status-payment-pending and .status-delivered. Split above.

   THREE NEUTRALS: not available from the token set, reported rather than
   approximated. In the usable tint band the ramp steps are dE 1.4
   (stone-150 to stone-200), 4.3 and 4.7 — indistinguishable to marginal
   at badge size. Only stone-100 vs stone-300 clears dE 9, and stone-100
   is dE 2.6 from the white card behind it, so it barely reads as a badge
   at all. Two neutral levels are separated by INK instead, which the
   ramp does support cleanly: text-body and text-muted are dE 14.1 apart
   and both clear AA on the shared fill (9.32 and 5.57). Closed takes a
   third step structurally rather than chromatically — same fill and ink
   as onhold, but no border — so it sits flattest without inventing a
   colour. */
.status-answered {
    color: var(--mh-warning-ink);
    background-color: var(--mh-warning-tint);
    border: 1px solid var(--mh-warning-edge);
}
.status-inprogress {
    color: var(--mh-info-ink);
    background-color: var(--mh-info-tint);
    border: 1px solid var(--mh-info-edge);
}
.status-open,
.status-customer-reply {
    color: var(--mh-text-body);
    background-color: var(--mh-bg-sunken);
    border: 1px solid var(--mh-stone-300);
}
.status-onhold {
    color: var(--mh-text-muted);
    background-color: var(--mh-bg-sunken);
    border: 1px solid var(--mh-stone-300);
}
.status-closed {
    color: var(--mh-text-muted);
    background-color: var(--mh-bg-sunken);
    border: 1px solid transparent;
}

/* Quote-page status pill reuses invoice-status classes with a different
   meaning per page. viewquote.tpl:43 renders "Delivered" as
   <span class="unpaid"> (invoice.css:9322, red) and "On Hold" as
   <span class="refunded"> (invoice.css:9330, dark blue). Both are neutral
   quote stages, not money states, and both directly correspond to
   .status-delivered / .status-onhold just above — already stone-600 on
   the quotes list page — so this brings the individual quote view in
   line with that.

   Scoped to .col-sm-5, the quote page's own status wrapper
   (viewquote.tpl:41) — viewinvoice.tpl's equivalent wrapper is
   .invoice-col (viewinvoice.tpl:43), a different class, so the invoice
   page's own "Unpaid" (correctly red, invoice.css:9322) and "Refunded"
   are untouched by this rule.

   "Lost" and "Dead" reuse .cancelled/.collections the same way, but
   theme.css's own .status-lost/.status-dead render #000 on the quotes
   list page, not stone-600 — flattening them here would itself
   contradict "consistent with the theme.css status palette" as often as
   it would satisfy it. Left alone pending a decision; see README. */
.invoice-container .col-sm-5 .invoice-status .unpaid,
.invoice-container .col-sm-5 .invoice-status .refunded {
    color: var(--mh-stone-600);
}

/* Refunded / Collections — the invoice page's OWN direct use of these
   two statuses (not the quote-page reuse above), found during 4-I and
   left open at the time: invoice.css:9330/9338 render Refunded as dark
   blue (#224488) and Collections as bright yellow (#ffcc00), neither
   matching what theme.css's own status palette says these mean —
   .status-refunded is amber (theme.css:10276, #f0ad4e) and
   .status-collections is dark slate (10272, #2c3e50). Same reasoning as
   the Delivered fix: bring the invoice page in line with the palette
   rather than inventing a third answer.

   Invoice statuses stay SOLID here, not tints — unlike 4TS's ticket
   reframing, which was a deliberate change of convention specific to
   tickets (whose colours were picked from the admin's point of view and
   needed reframing by who acts). Nothing about Refunded/Collections
   asks for that; this is a straight value correction within the
   existing invoice-status convention, which has stayed solid
   throughout (Unpaid, Paid, Draft, Cancelled all remain solid text with
   no fill).

   List-page fills use -ink, not the raw hue: white text (inherited via
   .label) on raw --mh-warning is 3.77, under AA; --mh-warning-ink
   reaches 5.92. --mh-stone-900 needs no ink variant, already 16.91 with
   white. Invoice-page text (no fill of its own, same as every other
   invoice-status class) takes the matching ink: --mh-warning-ink at
   5.62 on paper, --mh-text (= stone-900) at 16.06.

   Scoped to .invoice-col, viewinvoice.tpl's own status wrapper — NOT
   .invoice-container alone — so quote's already-fixed "On Hold" (above)
   keeps winning there via its own higher-specificity .col-sm-5 rule,
   and so quote's "Dead" (which also reuses .collections, and is still
   the open Lost/Dead question flagged above) is not touched by this
   rule at all. That question stays exactly as open as it was. */
.status-refunded {
    background-color: var(--mh-warning-ink);
}
.status-collections {
    background-color: var(--mh-stone-900);
}
.invoice-container .invoice-col .invoice-status .refunded {
    color: var(--mh-warning-ink);
}
.invoice-container .invoice-col .invoice-status .collections {
    color: var(--mh-text);
}

/* ---- Domain pricing page — domain-pricing.tpl, includes/domain-search.tpl.

   Header bars ("New Price" / "Transfer" / "Renewal") are TWO overlapping
   rules, not one, and that overlap is what hid a third stock colour.
   theme.css:12752 sets border-bottom: 6px solid #97b5d2 and
   background-color: #efefef on every header cell (.col-xs-2/.col-sm-2/
   .col-xs-4/.col-sm-4). theme.css:12767, higher specificity and later in
   the file, then overrides border-color — all four sides, this same
   bottom edge included — and background-color, but only on odd-position
   cells. So odd cells were already on #336699/#f5f5f5 (Pass A mapped the
   border half; the background half was still stock) while even cells
   were sitting on #efefef AND a lighter navy, #97b5d2, that never turned
   up in any colour-value sweep because the odd-cell rule masks it.
   #97b5d2 is the same family as #336699, just lighter — mapped to
   --mh-primary-edge. This is the mechanism to watch for elsewhere: a
   later, higher-specificity partial override (border-color, not
   border-bottom-color) can hide a sibling rule's colour behind it on the
   selectors it doesn't reach. theme.css:12747-12770. */
.domain-pricing .tld-pricing-header .col-xs-2,
.domain-pricing .tld-pricing-header .col-sm-2,
.domain-pricing .tld-pricing-header .col-xs-4,
.domain-pricing .tld-pricing-header .col-sm-4 {
    background-color: var(--mh-bg-sunken);
    border-bottom-color: var(--mh-primary-edge);
}
.domain-pricing .tld-pricing-header div:nth-child(odd) {
    background-color: var(--mh-bg-sunken);
    border-color: var(--mh-primary);
}

/* Highlighted TLD row — theme.css:12743, #f3f9fd, a cool blue wash on
   warm paper. Same problem as .table-striped in 4S, same fix. */
.domain-pricing .tld-row.highlighted {
    background-color: var(--mh-stone-100);
}

/* Sale-group ribbons ("HOT" / "SALE" / "NEW") — theme.css:12787-12803.
   .tld-sale-group sets color:#fff directly (12789); measured against
   that literal value, not --mh-text-inverse, per the CLAUDE.md rule this
   page's ticket-badge work established. All three fill with -ink rather
   than the plain solid: white on the raw --mh-warning is 3.77 and on
   --mh-success is 3.42, both under AA. -ink passes for every role,
   danger's included (its -ink equals its solid, so hot is unchanged in
   value and just tokenised). Applying -ink uniformly means this reads as
   one rule, not an exception carved out for two roles that failed.

   Font-size stays at the parent's 10px, below the --mh-text-2xs floor 4T
   set for badges elsewhere. This is a corner ribbon on an
   already-space-constrained price tile, not a standalone badge, and
   raising it risks the wrap 4T measured on the neighbouring <small>
   price-year elements. Deliberate exception, not a miss.
   .tld-sale-group also sets text-transform:uppercase (confirmed in the
   4T uppercase inventory) — tracking-badge, not tracking-caps, which
   tokens.css documents for table heads and eyebrows; this is a badge. */
.tld-sale-group {
    letter-spacing: var(--mh-tracking-badge);
}
.tld-sale-group-hot {
    background-color: var(--mh-danger-ink);
}
.tld-sale-group-sale {
    background-color: var(--mh-warning-ink);
}
.tld-sale-group-new {
    background-color: var(--mh-success-ink);
}

/* Homepage domain search — theme.css:10469-10490. */
.home-domain-search .tld-logos {
    color: var(--mh-text-muted);
}
.home-domain-search .input-group-wrapper {
    background-color: var(--mh-surface);
}
/* Homepage domain search button — parent is
   .home-domain-search .input-group-wrapper .advanced-input .btn-success
   at theme.css:10501-10503, specificity (0,4,0) — four classes
   (.home-domain-search, .input-group-wrapper, .advanced-input,
   .btn-success). It sets #218739, WHMCS's own Bootstrap success green,
   not our brand green — the button currently renders green so it reads
   as correct at a glance, but it's the wrong green. Match the parent's
   specificity here.
   Redundant since .btn-success was aliased to .btn-primary (BUTTONS
   above) — it now sets what the button would inherit anyway. Kept
   because it still has to out-specify the parent's (0,4,0) rule, which
   would otherwise reassert #218739. */
.home-domain-search .input-group-wrapper .advanced-input .btn-success {
    background-color: var(--mh-primary);
    border-color: var(--mh-primary);
}

/* Featured TLD tiles — theme.css:12608-12643. Only the shell and the
   base .price fill/text; the 16 .price.{tld} identity colours are
   excluded, same category as the payment and social marks in
   CLAUDE.md's exclusion table. */
.featured-tld {
    background-color: var(--mh-surface);
}
.featured-tld .price {
    background-color: var(--mh-bg-sunken);
    color: var(--mh-text-body);
}

/* Upgrade page product panel — theme.css:11832, 11884. #2b5580 here isn't
   one of Bootstrap's formula-derived shades (it's a hand-picked panel
   colour, not generated from #336699 by any consistent ratio), so this
   maps to the nearest existing rung on the primary ladder rather than
   inventing a matching tint. */
.upgrade .products .product .header,
.upgrade .products .product .btn {
    background-color: var(--mh-primary-active);
}

/* Remaining .upgrade neutrals — theme.css:11779-11860 (Pass B row 24).
   .current/.recommended (#ffbc00/#25ac2f) and .fa-check are semantic —
   current vs. recommended plan — and stay untouched, per the original
   Pass B classification. #aaa on the plain feature-list text was
   already failing AA before this pass (2.32:1) — --mh-text-subtle would
   have too (3.29), so this uses --mh-text-muted (6.52), which both
   tokenises and clears the pre-existing failure. */
.upgrade .product-to-be-upgraded {
    background-color: var(--mh-bg-sunken);
}
.upgrade .products .product {
    background-color: var(--mh-surface);
}
.upgrade .products .product ul {
    border-left-color: var(--mh-border);
    border-right-color: var(--mh-border);
}
.upgrade .products .product .footer {
    border-color: var(--mh-border);
}
.upgrade .products .product ul li {
    color: var(--mh-text-muted);
    border-bottom-color: var(--mh-border);
}
.upgrade .products .product ul li span {
    color: var(--mh-text-body);
}

/* Store order container — theme.css:11216-11255 (Pass B row 25).
   .domain-validation.ok (green) is semantic and already handled; this
   is the neutral base state only. */
.store-order-container .store-domain-tabs li a {
    background-color: var(--mh-bg-sunken);
    border-bottom-color: var(--mh-border-strong);
}
.store-order-container .store-domain-tabs li.active a {
    background-color: var(--mh-surface);
}
.store-order-container .store-domain-tab-content {
    border-color: var(--mh-border-strong);
}
.store-order-container .domain-validation {
    color: var(--mh-text-muted);
}

/* Credit card panel — theme.css:12250-12270 (Pass B row 30). #ccc as a
   background (the card-number strip) takes a solid stone rung, not a
   border alpha — --mh-stone-400 is the closest weight match. */
div.credit-card {
    background-color: var(--mh-bg-sunken);
    border-color: var(--mh-border-strong);
}
div.credit-card div.card-number {
    background-color: var(--mh-stone-400);
}


/* ---- The checkout — cart.php, domainchecker.php, the whole order flow.
   Session 4C found this page loads
   templates/orderforms/standard_cart/css/all.min.css at position 16 of
   17, after custom.css at position 9 — confirmed on the cart.php network
   tab. Everywhere else in this file, equal specificity is enough because
   custom.css loads last (the verified .btn-primary case in CLAUDE.md).
   ON THIS PAGE THAT INVERTS: we are the EARLIER file, so an
   equal-specificity tie now goes to standard_cart's rule, not ours.
   MATCHING the #order-standard_cart ID is therefore not sufficient on
   its own where the parent rule already carries that ID — every such
   rule below EXCEEDS it, by repeating the class that follows the ID
   (`.form-control.form-control`), a deliberate, conventional
   specificity boost, not a typo. Where the parent rule carries NO ID at
   all (the domain-pricing block), prefixing #order-standard_cart is
   sufficient on its own, since the ID tier alone then exceeds the
   parent's class-only specificity regardless of load order.
   Selectors and values pulled from the live file over SSH
   (deploy-staging.sh credentials, read-only — standing permission,
   CLAUDE.md), 2026-09-10; full inventory in README.md. */

/* Focus ring. Parent (standard_cart's own rule, not theme.css) is
   #order-standard_cart .form-control:focus{border-color:#ddd;
   box-shadow:none} at (1,2,0). This removes the focus indicator
   entirely on every text input in checkout — a WCAG 2.4.7 Focus
   Visible failure, pre-existing, and unrelated to our work; see
   README. Restored at (1,3,0) via class repetition. */
#order-standard_cart .form-control.form-control:focus {
    box-shadow: var(--mh-shadow-focus);
}

/* Base field — #order-standard_cart .field,#order-standard_cart
   .form-control{border:1px solid #ddd;color:#626262;
   background-color:#fff;border-radius:3px} at (1,1,0), exceeded here at
   (1,2,0). Height, padding, font-size and line-height are NOT set by
   this parent rule (checked directly — the only cart-side rules that
   touch those are input-group-lg/-sm/-captcha variants, all separately
   scoped), so the 4B control-height tokens already apply uncontested on
   this page and are not re-declared here. Only the four properties the
   parent actually sets need overriding: border, text colour, fill,
   radius.
   Radius takes --mh-radius-md (10px), the value its own token comment
   in tokens.css already names for "inputs, selects" — an existing
   decision, not a new one made here. This creates a temporary
   inconsistency: the general, non-cart .form-control radius is still
   stock 4px (0.25rem), because 4B explicitly deferred that decision
   ("Radius is not touched here — that is the forms row's call"). Once
   the forms row sets a general radius, reconcile this rule to match
   rather than leaving the cart as the only place already on 10px.

   RECONCILED, but the reconciliation itself needed a follow-up: this
   rule sets border-radius uniformly on all four corners, which is fine
   for a bare field but wrong for one sitting inside an .input-group —
   the interior seam needs to stay square. That never showed up before
   because every .form-control here got the same uniform value
   regardless of position; it only became visible once a real, correctly
   zeroed neighbour (an .input-group-text or .btn) sat right next to it.
   Confirmed live on configureproductdomain.tpl's "www." prefix
   (register/transfer/owndomain domain rows — not in
   ../twenty-one-reference/, standard_cart's own template, pulled over
   SSH) and checkout.tpl's CVV help-button field — this ID rule is
   (1,2,0), which outranks Bootstrap's own interior-corner zeroing
   (.input-group > .form-control:not(:first-child)/:not(:last-child),
   (0,3,0), theme.css:3135-3143) unconditionally: an ID beats any number
   of classes regardless of count, so the general "let Bootstrap trim the
   interior corner" pattern used everywhere else in this file can't work
   here — it needs its own, higher-specificity statement of the true
   shape, same principle as the -sm/-lg button fix above. */
#order-standard_cart .field.field,
#order-standard_cart .form-control.form-control {
    border-color: var(--mh-border-control);
    color: var(--mh-text-body);
    background-color: var(--mh-surface);
    border-radius: var(--mh-radius-md);
}
/* Interior seam, stated directly rather than left to Bootstrap's zeroing
   (which can't win here — see above). One rule per side: a form-control
   that isn't the input-group's first child has its LEFT corners
   squared (something else, the prepend, is the true left edge); one
   that isn't the last child has its RIGHT corners squared. Where
   neither condition applies (a lone field, no group) nothing here
   matches and the uniform --mh-radius-md above stands untouched. */
#order-standard_cart .input-group > .form-control:not(:first-child) {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
}
#order-standard_cart .input-group > .form-control:not(:last-child) {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}

/* Domain-pricing block, re-applied under the ID prefix. Confirmed
   (Session 4C) that standard_cart/all.min.css duplicates the 4P rules
   verbatim, #369 (3-digit #336699) included, and that domainregister.tpl
   does render this markup — so without the ID prefix, the cart's own
   later-loading copy silently wins the tie and our 4P work reverts here.
   None of the parent rules below carry an ID, so #order-standard_cart
   alone is sufficient to exceed them; no class repetition needed. Same
   token mapping as 4P, restated for the exact selectors this page
   collides on. */
#order-standard_cart .domain-pricing .tld-pricing-header .col-xs-2,
#order-standard_cart .domain-pricing .tld-pricing-header .col-sm-2,
#order-standard_cart .domain-pricing .tld-pricing-header .col-xs-4,
#order-standard_cart .domain-pricing .tld-pricing-header .col-sm-4 {
    background-color: var(--mh-bg-sunken);
    border-bottom-color: var(--mh-primary-edge);
}
#order-standard_cart .domain-pricing .tld-pricing-header div:nth-child(odd) {
    background-color: var(--mh-bg-sunken);
    border-color: var(--mh-primary);
}
#order-standard_cart .domain-pricing .tld-row.highlighted {
    background-color: var(--mh-stone-100);
}

/* Hero — third attempt. First (4X-1) painted only the inner element and
   left the outer gradient showing through. Second (4X-3) painted the
   outer element --mh-surface-inverse instead — correct as far as it
   went, but at panel scale a full ink-dark fill reads as a black slab;
   that token suits thin bands (footer, topbar), not a large panel, and
   removing it was the user's call after looking at both attempts.

   Root cause, not just a colour pick: the panel is sized for the globe
   illustration it no longer has. Two elements nest —
   domainregister.tpl:19-20, div.domain-checker-container (OUTER) >
   div.domain-checker-bg.clearfix (INNER) > the search form — and the
   illustration-sized space is NOT where it looks like it should be.
   Stock values, read before changing anything:
     .domain-checker-container{margin:20px 0;padding:0;
       background-color:#ffce44;border-radius:10px;
       background:-webkit-gradient(...)/-webkit-/-moz-/-ms-/-o-linear-
       gradient(top,#ffd960,#ffb201)} — PADDING IS ZERO. All of this
       element's stock spacing is the 20px margin; it contributes no
       internal height.
     .domain-checker-bg{margin:0;padding:80px 0;
       background-image:url(../img/globe.png)} (top-level), overridden
       to padding:40px 0 inside @media(max-width:992px). ALL the
       illustration-sized space is here, on the INNER element, not the
       outer one — the user's instruction named the container as the
       thing to depad, but the container was never carrying any.
   Removed the panel fill entirely per the user's "try this first":
   both elements transparent, no image, no colour. Padding cut from the
   INNER element (where it actually lives) to --mh-space-32 (32px,
   tokens.css's own "card padding (roomy)") desktop and --mh-space-24
   (24px, "card padding (dense)") at the same ≤992px breakpoint the
   stock rule uses — not proportional to the 80/40 split, but both are
   existing tokens for exactly this kind of role, which reads more
   intentional than chasing an exact ratio. Combined with the
   container's unchanged 20px margin: total vertical gap around the
   search box drops from 100px (80+20) to 52px (32+20) desktop, 60px
   (40+20) to 44px (24+20) at ≤992px — a reduction, not a removal; some
   breathing room stays, since dropping straight to zero would sit the
   box flush against the paragraph above and the results area below
   with nothing to separate them.

   border-radius:10px on the container is neutralised (set to 0): with
   no fill, a rounded corner paints nothing, so leaving the stock value
   in place would be inert rather than meaningful. margin:20px 0 is
   LEFT ALONE (not overridden) — unlike radius, margin is layout
   spacing between this block and its siblings (the h1 above, the
   results area below) and keeps doing that with no fill at all, so it
   still does something and stays.

   Neither parent selector carries #order-standard_cart, so the ID
   prefix alone exceeds both (0,1,0) parents — no class repeat needed,
   consistent with 4X-1/4X-2/4X-3.

   FALLBACK, not applied — only if the search box reads as unanchored
   with no field behind it once this ships: give .domain-checker-bg
   background-color: var(--mh-bg-sunken) and a border on the search box
   itself (.input-group-box, already background: var(--mh-surface)) so
   it separates from a near-white field rather than blending into it.
   Padding should still come down either way; the illustration sizing
   isn't specific to which background choice wins. */
#order-standard_cart .domain-checker-container {
    background-color: transparent;
    background-image: none;
    border-radius: 0;
}
#order-standard_cart .domain-checker-bg {
    background-image: none;
    padding: var(--mh-space-32) 0;
}
@media (max-width: 992px) {
    #order-standard_cart .domain-checker-bg {
        padding: var(--mh-space-24) 0;
    }
}

/* ---- Checkout — WHMCS's own semantic classes. 4S mapped Bootstrap's
   semantic roles by targeting Bootstrap's own class names; these carry
   the same kind of state under WHMCS's own names, so that sweep never
   saw them. All in standard_cart/all.min.css (pulled again read-only
   under the standing SSH permission, confirmed unchanged since 4C), on
   domainregister.tpl and configureproductdomain.tpl.

   STATE MAPPING — checked against base.js's actual toggle logic and the
   template markup, not inferred from the class names:
     .domain-checker-available    domain.isAvailable true        -> success
     .domain-checker-unavailable  domain taken, OR preferred TLD
                                   not sold (preferredTLDNotAvailable)
                                                                    -> danger
     .domain-checker-invalid      malformed input / domain.error  -> danger

   "Invalid" and "unavailable" turned out NOT to be the two different
   severities their names suggest (the brief's own warning, confirmed):
   WHMCS's stock CSS already colours both identically
   (#cd322d/#d9534f), and configureproductdomain.tpl:243 goes further —
   its own "invalid domain" headline is marked up with
   class="domain-checker-unavailable", not domain-checker-invalid. WHMCS
   doesn't maintain the distinction itself, so this doesn't invent one:
   both fold onto danger. No warning-role candidate exists among these
   three; every state is a clean yes/no, not a caution.

   .transfer-eligible / .transfer-not-eligible confirm the same two
   roles directly: configureproductdomain.tpl:233 marks its "eligible"
   headline class="domain-checker-available", and :237 marks its
   "not-eligible" headline class="domain-checker-unavailable" — WHMCS's
   own markup equates transfer eligibility with domain availability, so
   no separate mapping decision was needed here.

   SOLID OR TINT — decided per component, not as one blanket answer,
   because the three items don't share a visual structure today:
     Item 1 is bare coloured TEXT with no fill of its own (checked —
       neither the ID-scoped nor the global rule sets a background).
       Adding a badge fill where none exists would be a bigger change
       than "recolour it"; ink-only keeps the existing structure.
     Item 2 already IS an alert-style box (.transfer-eligible's stock
       #dff0d8/#d6e9c6/#3c763d is a literal Bootstrap 3 .alert-success
       clone) — tint, matching the structure it already has and the 4S
       alert precedent.
     Item 3 replicates 4P's already-settled solid -ink ribbons per the
       brief — not re-decided here.
   Where solid vs tint might have mattered most — a coloured or dark
   field forcing solid, the .mh-badge-solid case — doesn't arise: item 1
   sits on the plain page background (#primaryLookupResult carries no
   background rule of its own, confirmed), and item 2's boxes are the
   field, not sitting on one. */

/* Availability text — item 1. Both the ID-scoped and the global (no-ID)
   copies of each rule exist in the parent file; both mapped so no
   residual stray copy is left stock. Both need the exceed treatment,
   not a match: the load-order inversion isn't specific to ID-scoped
   parents — ANY equal-specificity rule in this cart file, ID or not,
   loads after custom.css and wins a tie. The global rule is exceeded
   the same way, one tier down: (0,1,0) -> (0,2,0) via class repeat. */
#order-standard_cart .domain-checker-available.domain-checker-available {
    color: var(--mh-success-ink);
}
#order-standard_cart .domain-checker-invalid.domain-checker-invalid,
#order-standard_cart .domain-checker-unavailable.domain-checker-unavailable {
    color: var(--mh-danger-ink);
}
.domain-checker-available.domain-checker-available {
    color: var(--mh-success-ink);
}
.domain-checker-invalid.domain-checker-invalid,
.domain-checker-unavailable.domain-checker-unavailable {
    color: var(--mh-danger-ink);
}

/* Transfer eligibility boxes — item 2. .transfer-not-eligible's OWN
   background/text are neutral, not danger — confirmed from the markup:
   only its headline carries .domain-checker-unavailable (mapped above,
   inherits nothing extra needed here); the box's own three plain
   paragraphs use its own #666, unclassed. Setting color on the
   container cascades to them without overriding the headline's own
   explicit colour — a direct declaration always beats an inherited
   one regardless of specificity, so the danger-ink headline rule above
   is untouched by this. */
#order-standard_cart .transfer-eligible.transfer-eligible {
    background-color: var(--mh-success-tint);
    border-color: var(--mh-success-edge);
    color: var(--mh-success-ink);
}
#order-standard_cart .transfer-not-eligible.transfer-not-eligible {
    background-color: var(--mh-bg-sunken);
    color: var(--mh-text-muted);
}

/* Mirrored sale-group ribbons — item 3. Duplicates of 4P's
   .tld-sale-group-* under different names (.spotlight-tld-* on the
   inline suggestion tiles, .promo.hot/.sale/.new in the suggested-
   domains list) — same stock hex, same treatment: each role's -ink
   rung as a SOLID fill, uniformly, not just the two that fail white on
   their raw solid (warning 3.77, success 3.42). Both rule sets set
   color:#fff literally on themselves (not inherited from something
   .label-like), so the ratios below are measured against that literal
   value. Neither parent selector carries #order-standard_cart, so the
   ID prefix alone exceeds them — no class-repeat needed, unlike items
   1 and 2 above. */
#order-standard_cart .spotlight-tld-hot,
#order-standard_cart .suggested-domains .promo.hot {
    background-color: var(--mh-danger-ink);
}
#order-standard_cart .spotlight-tld-sale,
#order-standard_cart .suggested-domains .promo.sale {
    background-color: var(--mh-warning-ink);
}
#order-standard_cart .spotlight-tld-new,
#order-standard_cart .suggested-domains .promo.new {
    background-color: var(--mh-success-ink);
}

/* .spotlight-tld .invalid/.unavailable — correctly left outside 4X-2's
   named scope (neither is one of the six semantic classes that session
   covered). Already neutral (#eee/#ddd/#bbb), so token-only, not a
   contrast fix: the stock pairing is already 1.65:1, well under AA, but
   this is a disabled/unavailable-tile indicator, which WCAG doesn't
   require to meet contrast — a greyed-out affordance is supposed to
   read as de-emphasised. Mapped to the closest stone rungs at the same
   relative weights rather than pushed toward passing, which would make
   a "this TLD isn't available" tile look identical to a live one.
   #order-standard_cart alone exceeds the parent's (0,2,0), no class
   repeat needed — same as every other no-ID parent on this page. */
#order-standard_cart .spotlight-tld .invalid,
#order-standard_cart .spotlight-tld .unavailable {
    background-color: var(--mh-stone-200);
    border-color: var(--mh-stone-300);
    color: var(--mh-stone-400);
}

/* Search box alignment — confirmed genuinely centred, not drifting (the
   .col-xs-10/.col-xs-offset-1/.col-md-offset-2 classes on the column are
   dead Bootstrap 3 syntax, defined nowhere; the live BS4 pair,
   .col-10/.offset-1 base and .col-md-8/.offset-md-2 at >=768px, is
   symmetric — 1+10+1=12, 2+8+2=12). Changed anyway: every other element
   on this page (h1, the "find new domain" line, "Browse extensions by
   category") is a direct child of .cart-body with no column wrapper —
   full width, left-aligned by default — so a centred narrower box is
   the one outlier in an otherwise uniformly left-aligned, full-width
   grid.

   Tried first, per the user's own preference: keep the width
   (.col-md-8/.col-10 untouched), drop only the offset. theme.css:767-769
   (.offset-1, margin-left:8.3333333333%) and :1109-1111
   (.offset-md-2, margin-left:16.6666666667%, inside
   @media(min-width:768px)) — neither redefined anywhere in
   standard_cart's own CSS, so this is the ordinary (non-inverted) case:
   theme.css loads before custom.css. Scoped to .domain-checker-bg
   rather than matched to the full class fingerprint, so it stays
   correct if a future WHMCS update drops one of the dead BS3 classes —
   and because BOTH the search-box row and the CAPTCHA row (when
   enabled) carry this identical class list, scoping by container
   correctly left-aligns both together rather than fixing one and
   leaving the other centred.

   Left-aligning surfaced a second issue, reported back and fixed here:
   the box's left edge now matched the heading, but its right edge
   matched nothing — the pricing table below runs full width, and
   col-md-8/col-10 stopped 33%/17% short of it. Checked what else could
   be constraining width before assuming the column classes were the
   whole story: neither .domain-checker-container nor .domain-checker-bg
   carries a max-width anywhere, and their padding/margin (see the hero
   comment above) is vertical-only — zero horizontal, at every
   breakpoint including the 992px one. The pricing table's own wrapper,
   .domain-pricing > .bg-white, is equally unconstrained: .domain-pricing
   has no base rule at all in either stylesheet, and .bg-white
   (theme.css:5938) is a plain `background-color:#fff !important`
   utility with no box-model properties. Both the hero and the table
   sit as direct, unpadded children of .cart-body, so nothing wraps
   either one more tightly than the other — the gap was never a
   container issue, purely the col-md-8/col-10 width classes on the
   form's own column. */
#order-standard_cart .domain-checker-bg .offset-1,
#order-standard_cart .domain-checker-bg .offset-md-2 {
    margin-left: 0;
}
/* Bootstrap's grid columns aren't sized by `width` — the shared base
   rule (theme.css:596-601) already sets width:100% on every .col-*
   class uniformly; the actual constraint is `flex` and `max-width` on
   the SPECIFIC size class. .col-10 (theme.css:692-695) is
   flex:0 0 83.3333333333%/max-width:83.3333333333%; .col-md-8
   (theme.css:1039-1042, >=768px) is 66.6666666667% for both. Matched to
   what .col-12/.col-md-12 themselves set (100%/100%) rather than just
   zeroing max-width, so this reads as "become full width" and not as
   an unusual one-off value. Same container scoping as the offset fix
   above, for the same reason: the CAPTCHA row shares this exact class
   list, so it goes full width along with the search box rather than
   being left behind at the old 66.67%. */
#order-standard_cart .domain-checker-bg .col-10,
#order-standard_cart .domain-checker-bg .col-md-8 {
    flex: 0 0 100%;
    max-width: 100%;
}

/* AI domain search: Search button sitting inside the prompt box, where
   the same control on the cart's own domain step puts it alongside.
   Stock, not ours — standard_cart's sheet carries
   `.domain-checker-container.domain-checker-advanced #btnCheckAvailability
   {position:absolute;right:20px;top:74px}`, and since Bootstrap gives
   .input-group position:relative, that pins the button into the
   .input-group-box panel. The textarea beside it is 120px tall, so a
   74px top offset lands the button inside it at any width — this reads
   as "inside the textarea" in stock too, the widening above only moved
   it horizontally.
   Both pages run the same feature through different markup: the cart's
   domain step (configureproductdomain.tpl:48) puts the textarea and the
   button in separate grid columns, which is why it already looks right,
   while domainregister.tpl:25-38 makes them siblings inside one
   .input-group and positions the button out of flow. templates/
   orderforms/ is out of scope for forking (CLAUDE.md), so this is
   corrected in CSS by putting the button back in the flow and letting
   the textarea share the line with it.
   Scoped on the compound .domain-checker-container.domain-checker-
   advanced deliberately: BOTH pages carry .domain-checker-advanced
   (checked — configureproductdomain.tpl adds it too when AI search is
   on), and only domainregister.tpl also carries .domain-checker-
   container, so this reaches the broken page and cannot disturb the one
   already rendering correctly.
   Matched to the cart domain step's behaviour at BOTH widths, which is
   the part two earlier attempts missed. That page gets its layout from
   the grid — the textarea sits in .col-sm-8 and the button in
   .col-sm-2 with .btn-block (configureproductdomain.tpl:78-80) — so
   Bootstrap's sm breakpoint does the real work: above 576px the button
   sits beside the textarea, below it the column goes full width and
   btn-block makes the button a full-width block underneath. Reproduced
   here on the same 576px breakpoint rather than a number of our own, so
   the two pages change shape at the same point.
   Mobile first, per CLAUDE.md: the stacked arrangement is the base
   rule and the side-by-side one is the min-width override.
   The desktop half needs the textarea to CLAIM the first line, not just
   grow into it. A plain `flex: 1 1 auto` was the first attempt and was
   wrong: .input-group is flex-wrap:wrap, so the browser fills a line
   with every item whose BASE size fits before distributing free space —
   textarea, button, both .multiselect dropdowns and the Safe Search
   label all fit at their base sizes, so all five landed on one line and
   the textarea merely grew into the remainder. A flex-basis of nearly
   the full width leaves room for the button alone, and the option
   controls wrap beneath.
   160px is the one hand-set figure and it is a floor, not a
   measurement: enough for the button plus the gap. The textarea keeps
   flex-grow, so slack returns to it rather than showing as a hole, and
   a wider button shrinks the textarea instead of breaking the line.
   width:auto + white-space:nowrap on the desktop button because stock
   pins it to 100px, too narrow for "Search" plus its icon — that was
   stacking the label above the sparkle. Stock height:46px is left
   alone: it is what stops .input-group's default align-items:stretch
   from dragging the button to the textarea's full 120px, and it leaves
   it top-aligned with the textarea as on the other page.
   (2,2,0) on each, clearing the stock (1,2,0) button rule and the
   (1,1,1) textarea width rule; the ID prefix also keeps us ahead of
   standard_cart loading after custom.css. */
#order-standard_cart .domain-checker-container.domain-checker-advanced #message {
    width: 100%;
    flex: 0 0 100%;
}
#order-standard_cart .domain-checker-container.domain-checker-advanced #btnCheckAvailability {
    position: static;
    width: 100%;
    margin-top: var(--mh-space-12);
    white-space: nowrap;
}
@media (min-width: 576px) {
    #order-standard_cart .domain-checker-container.domain-checker-advanced #message {
        width: auto;
        flex: 1 1 calc(100% - 160px);
    }
    #order-standard_cart .domain-checker-container.domain-checker-advanced #btnCheckAvailability {
        width: auto;
        margin-top: 0;
        margin-left: var(--mh-space-12);
    }
}

/* "Explore packages now" hosting upsell — stock WHMCS bug, not ours.
   domainregister.tpl's two .domain-promo-box cards are a designed pair
   (confirmed: both collapse to col-md-6 when $domainTransferEnabled is
   true) doing the identical job, cross-selling the client into another
   product — but WHMCS hardcodes text-warning/btn-warning on the
   hosting card's headline and CTA while the sibling transfer card uses
   text-primary/btn-primary for the same role. Neither card signals an
   actual warning state; this reads as a copy-paste slip in WHMCS's own
   markup, not an intentional distinction. 4S's global .text-warning/
   .btn-warning mapping is correct and stays untouched — genuine
   warning contexts elsewhere depend on it — so this can only be a
   card-scoped fix, not a remap.

   Scoped by icon, not by any distinguishing class or ID, because
   neither exists: both cards are bare .domain-promo-box. fa-server
   (hosting) vs fa-globe (transfer) is the one structural difference in
   the markup — the same :has()-based technique 4S used to distinguish
   two otherwise-identical .label contexts by a sibling's class.
   :has() shipped in every evergreen engine well before this project
   started (Safari 15.4, Mar 2022; Chrome/Edge 105, Aug 2022; Firefox
   121, Dec 2023 — the last holdout, itself now nearly two years old),
   so current browser support is not a live concern; no prior "browser
   baseline" note exists in this repo to check against — this is that
   note.

   Rest/hover/focus only: the CTA is a plain <a class="btn">, not a
   form submit button, so a disabled state isn't reachable, and no
   :active pressed state exists in the source to mirror either — full
   .btn-primary parity isn't being claimed, only what this element can
   actually do.

   COUPLING — worth knowing, not a reason to avoid this approach: if
   WHMCS ever changes this card's icon away from fa-server, this
   selector stops matching and the card silently reverts to today's
   stock warning colour. Safe failure (back to the pre-fix state, not a
   crash or broken layout) but easy to miss since nothing announces it —
   re-check this rule specifically after any WHMCS order-form update.
   .text-warning ships !important (theme.css, matching 4S's own rule);
   matched here per CLAUDE.md constraint 6, same reason as every other
   surviving !important in this file. */
#order-standard_cart .domain-promo-box:has(.fa-server) .text-warning {
    color: var(--mh-primary) !important;
}
#order-standard_cart .domain-promo-box:has(.fa-server) .btn-warning {
    background-color: var(--mh-primary);
    border-color: var(--mh-primary);
}
#order-standard_cart .domain-promo-box:has(.fa-server) .btn-warning:hover {
    background-color: var(--mh-primary-hover);
    border-color: var(--mh-primary-active);
}
#order-standard_cart .domain-promo-box:has(.fa-server) .btn-warning:focus,
#order-standard_cart .domain-promo-box:has(.fa-server) .btn-warning.focus {
    background-color: var(--mh-primary-active);
    border-color: var(--mh-primary-active);
    box-shadow: var(--mh-shadow-focus);
}

/* Page type base. standard_cart's own `#order-standard_cart{font-size:
   14px}` re-bases every page in the order flow one step below the 15px
   --mh-text-base the rest of the site renders at, and because font-size
   inherits, everything under it follows: the same nav component renders
   at two sizes depending on which page you're on, and the stock em
   multipliers land on the wrong absolute values throughout.

   Raised to our own base. This was costed twice before being made, and
   the first pass deliberately did NOT make it — the blast radius is
   real and page-wide, so it was scoped to the sidebar alone while the
   ask was about the sidebar alone. Re-opened when the same problem
   surfaced in the product cards, where it can't be reached that way.
   Measured, not estimated, against the live stylesheet:
     41 em-keyed declarations under this ID scale +7.1% together —
     .products (header/qty/desc/footer), .panel-addon, .domain-checker-
     result-headline/.domain-suggestions, .product-info/.product-title,
     the .order-summary block down to .total-due-today .amt, .summary-
     container, .view-cart-items (item/title/group/price/cycle/qty),
     .view-cart-tabs .nav-tabs, .order-confirmation, .addon-promo-
     container, every .mc-promo-* rule, .checkout-security-msg, and
     .default-captcha. Because they are ALL em-relative to this one
     base, they scale in proportion and every size relationship the
     order form was designed with is preserved exactly — this is one
     change, not 41.
     18 absolute-px declarations under the same ID do NOT move, by
     design: the .font-size-{12,14,18,22,24,30,36} utilities are
     explicit by definition, and `label` (13px), .field-help-text
     (12px), .domain-checker-advanced label/.multiselect (12px),
     .cc-input-container .existing-cc-grid (13px) are deliberate fixed
     choices that already sit on legible values.
   The tier that actually prompted this: .8em x11 was computing 11.2px,
   under --mh-text-2xs (11.5px), our own floor — the product-card
   description text in particular. It lands at 12px now. Nothing drops
   anywhere: raising a base only ever pushes inherited sizes up.

   Specificity: the parent is a bare ID, (1,0,0), and on cart pages
   standard_cart/all.min.css loads at position 16 against custom.css at
   9 — so an exact tie would lose on source order (CLAUDE.md, "Which
   pages load our CSS"). The ID is repeated to reach (2,0,0), the same
   repetition technique already used for .field.field and
   .form-control.form-control just above.

   Supersedes a narrower `.sidebar.cart-sidebar .list-group` override
   that lived here and set the same 13.5px by hand: with the base
   correct, the stock `.sidebar .list-group{font-size:.9em}`
   (theme.css:10425) computes 0.9 x 15px = 13.5px on its own. Removed
   rather than left in place — it existed only to work around this
   base, and a dead workaround kept next to its own fix is the kind of
   thing that makes the next person doubt which one is load-bearing. */
#order-standard_cart#order-standard_cart {
    font-size: var(--mh-text-base);
}

/* Sidebar nav size, normalised across the order flow. WHMCS renders the
   cart sidebar wrapper with two different class sets depending on the
   page, confirmed by fetching three of them:
     index.php?rp=/store/*        <div class="cart-sidebar sidebar">
     cart.php?a=view              <div class="cart-sidebar">
     cart.php?a=add&domain=...    <div class="cart-sidebar">
   The stock `.sidebar .list-group{font-size:.9em}` (theme.css:10425) is
   keyed on .sidebar, so it fires on the storefront pages and not on the
   cart.php ones — the same nav component, two sizes, decided by a class
   that is present or absent for no reason we control. That split
   predates the base change above (it was 12.6px vs 14px before, same
   shape, just smaller and less obvious); raising the base made both
   ends bigger and the difference easier to see, which is how it
   surfaced. Pages affected: Renew Services, View Available Addons,
   Register Domain, Transfer Domain, View Cart.

   Keyed on .cart-sidebar instead, which is present on every one of
   them, and carrying the stock rule's own .9em rather than an absolute
   size — so it resolves against whatever --mh-text-base is (13.5px
   today) and both paths stay in step if that ever moves. (0,2,0), the
   same specificity as the stock rule; no repetition used to force a win
   because none is needed — where the stock rule also fires, both set
   the identical value, so the tie is immaterial, and where it doesn't,
   this is the only rule in play.
   Scoped to .list-group, not to .cart-sidebar as a whole: the panel
   headings have their own size (theme.css/standard_cart both set
   .cart-sidebar h3 to 16px) and are not part of this. The
   .secondary-cart-sidebar column on cart.php?a=view has no .list-group
   at all — checked — so it's untouched either way. */
.cart-sidebar .list-group {
    font-size: 0.9em;
}


/* ==========================================================================
   10. UTILITIES
   ========================================================================== */

/* Bootstrap utility classes ship with !important by design — matching it
   is required to win at all, not an escalation for a specificity problem
   (CLAUDE.md hard constraint 6). theme.css:5858-5865, 5986-5987,
   9011-9016. */
.bg-primary {
    background-color: var(--mh-primary) !important;
}
a.bg-primary:hover,
a.bg-primary:focus,
button.bg-primary:hover,
button.bg-primary:focus {
    background-color: var(--mh-primary-active) !important;
}
.border-primary {
    border-color: var(--mh-primary) !important;
}
.text-primary {
    color: var(--mh-primary) !important;
}
a.text-primary:hover,
a.text-primary:focus {
    color: var(--mh-link-hover) !important;
}

/* ---- Semantic utilities — theme.css:5878-5925 (.bg-*), 5994-6010
   (.border-*), 9027-9077 (.text-*). All three families ship !important
   by design, so matching it is required to win at all rather than an
   escalation for a specificity problem (CLAUDE.md constraint 6) — same
   precedent as .bg-primary above.

   .text-* uses -ink, not the solid: the solid fails AA as a foreground
   for success (3.42 on white) and warning (3.77). Applied uniformly
   across all four roles rather than only the two that fail, so the
   utility behaves predictably — a deliberate deviation from
   solid-to-solid, logged in the README. Hover takes -active throughout.
   .bg-* mirrors the .btn-* base fills so --mh-text-inverse stays legible
   on them. .border-* takes the plain solid: a border carries no text, so
   no ratio applies, and the solid is the role's identity colour. */
.text-success { color: var(--mh-success-ink) !important; }
.text-warning { color: var(--mh-warning-ink) !important; }
.text-danger  { color: var(--mh-danger-ink) !important; }
.text-info    { color: var(--mh-info-ink) !important; }
a.text-success:hover, a.text-success:focus { color: var(--mh-success-active) !important; }
a.text-warning:hover, a.text-warning:focus { color: var(--mh-warning-active) !important; }
a.text-danger:hover,  a.text-danger:focus  { color: var(--mh-danger-active) !important; }
a.text-info:hover,    a.text-info:focus    { color: var(--mh-info-active) !important; }

.bg-success { background-color: var(--mh-success-active) !important; }
.bg-warning { background-color: var(--mh-warning-ink) !important; }
.bg-danger  { background-color: var(--mh-danger) !important; }
.bg-info    { background-color: var(--mh-info) !important; }
a.bg-success:hover, a.bg-success:focus, button.bg-success:hover, button.bg-success:focus { background-color: var(--mh-success-ink) !important; }
a.bg-warning:hover, a.bg-warning:focus, button.bg-warning:hover, button.bg-warning:focus { background-color: var(--mh-warning-active) !important; }
a.bg-danger:hover,  a.bg-danger:focus,  button.bg-danger:hover,  button.bg-danger:focus  { background-color: var(--mh-danger-hover) !important; }
a.bg-info:hover,    a.bg-info:focus,    button.bg-info:hover,    button.bg-info:focus    { background-color: var(--mh-info-hover) !important; }

.border-success { border-color: var(--mh-success) !important; }
.border-warning { border-color: var(--mh-warning) !important; }
.border-danger  { border-color: var(--mh-danger) !important; }
.border-info    { border-color: var(--mh-info) !important; }

/* .text-muted — theme.css:9079, #6c757d. A cool grey on 37 elements
   across 31 templates, and brand/tokens.css warns explicitly that cool
   greys look dirty against the warm paper background. 6.19:1 on paper,
   6.52:1 on white. Parent carries !important, so this must too. */
.text-muted {
    color: var(--mh-text-muted) !important;
}
