﻿/* =============================================================================
   Matrix Portal design tokens and base styles.

   SOURCE OF TRUTH, AND WHY THIS FILE IS A FORK.
   The tokens below come from Nastasja's Portal prototype
   (investigations/Portal/Matrix Portal.html). Their colour values are identical
   to the Vision handoff at
   Vision/design/design_handoff_matrix_vision/css/matrix-vision.css.

   Decision #438 (2026-08-20) settled that brand tokens become ONE versioned
   package shared across apps, not a copy per repo. This file is a deliberate
   interim copy so the Portal can be styled before that package exists. It WILL
   drift: 04-portal-ux.md measured 82 tokens in the handoff against 101 in
   Vision's app, and vision.css now declares 159. Do not treat this file as the
   place a brand colour gets decided. When the package lands, delete this block
   of tokens and consume it instead.

   DELIBERATE DIVERGENCES FROM THE PROTOTYPE. All are marked below:
     1. The font stack keeps Gilroy at the head, and self-hosts Manrope.
     2. danger / success / warning use Vision's contrast-corrected values.
     3. .btn-primary and .avatar stay FLAT instead of taking --brand-gradient.
        Same reason as 2, measured the same way. See the block above .btn-primary.
        Open on PRODUCT-2983 (PRODUCT-3092, 2026-09-21).
     4. Contrast, same rule as 2 (PRODUCT-2806 parity pass 2, 2026-09-23):
        the admin eyebrow is --page-pink-ink, not --brand-pink (2.6:1); sidebar
        labels are 55% white, not 36% (about 3:1); and the module page's button
        no longer takes the module accent (white on pink is 4.08:1).
        Parity pass 3: --text-muted is Vision's darker value, not neutral-500.
   ============================================================================= */

:root {
    /* ---- brand ---- */
    --brand-pink: #FA5CDC;
    --brand-magenta: #C13AE0;
    --brand-purple: #9342DC;
    --brand-violet: #7A2FD8;
    --brand-indigo: #4A20CA;
    --brand-indigo-deep: #3A18A8;
    --brand-ink: #1E1A34;
    --brand-ink-2: #1C1735;

    --brand-gradient: linear-gradient(115deg, #FA5CDC 0%, #9342DC 48%, #4A20CA 100%);
    --brand-gradient-vivid: linear-gradient(115deg, #FF6FE0 0%, #A24BE6 45%, #5A2BE0 100%);
    --brand-gradient-ink: linear-gradient(150deg, #2A2350 0%, #1E1A34 60%, #17122B 100%);
    --brand-gradient-lilac: linear-gradient(135deg, #D8C8F4 0%, #BCA6EE 100%);

    /* ---- neutrals ---- */
    --neutral-0: #FFFFFF;
    --neutral-50: #F7F6FB;
    --neutral-100: #EFEDF5;
    --neutral-200: #E2DEEC;
    --neutral-300: #CEC9DD;
    --neutral-400: #A9A2BE;
    --neutral-500: #7C7691;
    --neutral-600: #574F6E;
    --neutral-700: #3B3452;
    --neutral-800: #262038;
    --neutral-900: #1E1A34;

    --violet-50: #F5F0FE;
    --violet-100: #ECE2FC;
    --violet-200: #D9C6F6;
    --pink-50: #FEF0FB;
    --pink-100: #FBDDF4;

    /* ---- semantic status ----
       DIVERGENCE 2. The prototype ships --danger #E23B5D (4.19:1 on white),
       --success #1F9D6B (3.45:1) and --warning #E39A2B (2.35:1). All three fail
       WCAG AA for text. Vision measured and darkened them; the ratios and the
       reasoning are at Vision/src/Vision.Web/wwwroot/css/vision.css:78-86.
       Matching Vision matters more than matching the mockup here, because these
       are the same brand rendering the same words to the same people.

       The foregrounds below are the corrected values, safe as text on white and
       on their own tinted background. The backgrounds are the handoff values
       unchanged. Where the raw hue is a FILL behind white text, darker only
       raises contrast, so the corrected value is right there too. */
    --danger: #C21D41;
    --danger-bg: #FBE4E9;
    --success: #12744C;
    --success-bg: #E4F5EE;
    --warning: #8A5A06;
    --warning-bg: #FBF0DC;
    --info: #4A20CA;
    --info-bg: #ECE2FC;

    /* ---- page accents ----
       Four families for colouring one module, one page or one section. Each is
       a base, an ink, a bright, a soft tint and an on-colour that were chosen
       together, which is why a module names a FAMILY rather than a colour.

       Added in Phase 6 with the module accents. They were left out of the
       original token copy in Phase 1 because nothing used them yet, and the
       omission is not visible until something does: var() on an undefined
       property is invalid at computed-value time, so the declaration is simply
       dropped and the element renders untouched rather than erroring. */
    --page-purple: #7A2FD8;
    --page-purple-ink: #3A18A8;
    --page-purple-bright: #9342DC;
    --page-purple-soft: #ECE2FC;
    --page-purple-on: #FFFFFF;
    --page-purple-gradient: linear-gradient(115deg, #9342DC 0%, #7A2FD8 50%, #3A18A8 100%);

    --page-blue: #2563C9;
    --page-blue-ink: #12306B;
    --page-blue-bright: #3F86EE;
    --page-blue-soft: #E9F0FC;
    --page-blue-on: #FFFFFF;
    --page-blue-gradient: linear-gradient(115deg, #3F86EE 0%, #2563C9 52%, #12306B 100%);

    --page-pink: #D83AB0;
    --page-pink-ink: #9E1580;
    --page-pink-bright: #FA5CDC;
    --page-pink-soft: #FBDDF4;
    --page-pink-on: #FFFFFF;
    --page-pink-gradient: linear-gradient(115deg, #FA5CDC 0%, #D83AB0 52%, #9E1580 100%);

    --page-black: #1E1A34;
    --page-black-ink: #0C0A16;
    --page-black-bright: #3B3452;
    --page-black-soft: #E7E5EF;
    --page-black-on: #FFFFFF;
    --page-black-gradient: linear-gradient(150deg, #3B3452 0%, #1E1A34 60%, #0C0A16 100%);

    /* ---- text ---- */
    --text-strong: var(--neutral-900);
    --text-body: var(--neutral-700);
    /* Divergence 2 again: neutral-500 is 4.3:1 on white and 3.7:1 on a pill, under AA
       for small text. Vision's value (vision.css:66), 5.9:1 at worst. */
    --text-muted: #5C5772;
    --text-faint: var(--neutral-400);
    --text-on-brand: #FFFFFF;
    --text-link: var(--brand-violet);

    /* ---- surfaces ---- */
    --surface-page: var(--neutral-50);
    --surface-card: var(--neutral-0);
    --surface-sunken: var(--neutral-100);
    --surface-ink: var(--neutral-900);
    --surface-brand-soft: var(--violet-50);

    /* ---- borders ---- */
    --border-subtle: var(--neutral-200);
    --border-default: var(--neutral-300);
    --border-strong: var(--neutral-400);
    --border-width: 1px;
    --border-width-strong: 1.5px;

    /* ---- interactive ---- */
    --brand-primary: var(--brand-violet);
    --brand-primary-hover: #6A28C6;
    --brand-primary-active: #591FB0;
    --focus-ring: rgba(122, 47, 216, 0.45);

    /* ---- type ----
       DIVERGENCE 1. The prototype names Gilroy, which the design system
       self-hosts. Gilroy's web licence is unconfirmed here (Vision hit the same
       wall, PRODUCT-2617), so Manrope is self-hosted below as a geometric
       humanist stand-in under the SIL Open Font Licence.

       Gilroy stays at the HEAD of the stack on purpose: dropping licensed
       Gilroy woff2 files into /fonts/ and adding their @font-face picks them up
       with no other change to this file or any page. Same arrangement as
       vision.css:99-103. Nastasja is designing in Gilroy, so mockups will not
       match the built article exactly until that swap happens. */
    --font-display: 'Gilroy', 'Manrope', 'Century Gothic', system-ui, sans-serif;
    --font-body: 'Gilroy', 'Manrope', 'Segoe UI', system-ui, -apple-system, sans-serif;
    --font-mono: ui-monospace, 'SF Mono', Menlo, monospace;

    --fw-light: 300;
    --fw-regular: 400;
    --fw-medium: 500;
    --fw-semibold: 600;
    --fw-bold: 700;

    --text-xs: 12px;
    --text-sm: 14px;
    --text-base: 16px;
    --text-md: 18px;
    --text-lg: 20px;
    --text-xl: 24px;
    --text-2xl: 30px;
    --text-3xl: 38px;
    --text-4xl: 48px;
    --text-5xl: 64px;
    --text-6xl: 84px;

    --leading-none: 1;
    --leading-tight: 1.12;
    --leading-snug: 1.28;
    --leading-normal: 1.5;
    --leading-relaxed: 1.65;

    --tracking-tight: -0.02em;
    --tracking-normal: 0;
    --tracking-wide: 0.04em;
    --tracking-label: 0.14em;

    /* Composite type styles, straight from the prototype. */
    --type-display: var(--fw-light) var(--text-5xl)/var(--leading-tight) var(--font-display);
    --type-h1: var(--fw-regular) var(--text-4xl)/var(--leading-tight) var(--font-display);
    --type-h2: var(--fw-regular) var(--text-3xl)/var(--leading-snug) var(--font-display);
    --type-h3: var(--fw-medium) var(--text-2xl)/var(--leading-snug) var(--font-display);
    --type-h4: var(--fw-medium) var(--text-xl)/var(--leading-snug) var(--font-display);
    --type-body: var(--fw-regular) var(--text-base)/var(--leading-relaxed) var(--font-body);
    --type-body-sm: var(--fw-regular) var(--text-sm)/var(--leading-normal) var(--font-body);
    --type-label: var(--fw-semibold) var(--text-sm)/var(--leading-normal) var(--font-body);

    /* ---- space, on a 4px grid ---- */
    --space-0: 0;
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-8: 32px;
    --space-10: 40px;
    --space-12: 48px;
    --space-16: 64px;
    --space-20: 80px;
    --space-24: 96px;
    --space-32: 128px;

    --container-max: 1200px;
    --gutter: var(--space-6);
    --sidebar-w: 264px;

    /* ---- shape ---- */
    --radius-xs: 4px;
    --radius-sm: 6px;
    --radius-md: 10px;
    --radius-lg: 14px;
    --radius-xl: 20px;
    --radius-2xl: 28px;
    --radius-pill: 999px;

    /* ---- depth ---- */
    --shadow-xs: 0 1px 2px rgba(30, 26, 52, 0.06);
    --shadow-sm: 0 2px 6px rgba(30, 26, 52, 0.08);
    --shadow-md: 0 8px 24px rgba(30, 26, 52, 0.10);
    --shadow-lg: 0 18px 48px rgba(30, 26, 52, 0.14);
    --shadow-xl: 0 30px 70px rgba(30, 26, 52, 0.18);
    --shadow-brand: 0 12px 30px rgba(122, 47, 216, 0.30);
    --shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.6);

    /* ---- motion ---- */
    --ease-standard: cubic-bezier(0.22, 0.61, 0.36, 1);
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
    --dur-fast: 120ms;
    --dur-base: 200ms;
    --dur-slow: 320ms;

    color-scheme: light;
}

/* Self-hosted Manrope. No CDN: the Portal is the front door to every module and
   must not depend on a third party being up to render its own sign-in page. */
@font-face { font-family: 'Manrope'; src: url('../fonts/manrope/manrope-400.woff2') format('woff2'); font-weight: 400; font-display: swap; font-style: normal; }
@font-face { font-family: 'Manrope'; src: url('../fonts/manrope/manrope-500.woff2') format('woff2'); font-weight: 500; font-display: swap; font-style: normal; }
@font-face { font-family: 'Manrope'; src: url('../fonts/manrope/manrope-600.woff2') format('woff2'); font-weight: 600; font-display: swap; font-style: normal; }
@font-face { font-family: 'Manrope'; src: url('../fonts/manrope/manrope-700.woff2') format('woff2'); font-weight: 700; font-display: swap; font-style: normal; }
@font-face { font-family: 'Manrope'; src: url('../fonts/manrope/manrope-800.woff2') format('woff2'); font-weight: 800; font-display: swap; font-style: normal; }


/* =============================================================================
   Reset and base
   ============================================================================= */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

/* Room for the 60px app bar, which is sticky only above 900px. */
@media (min-width: 901px) { html { scroll-padding-top: 76px; } }

body {
    margin: 0;
    background: var(--surface-page);
    color: var(--text-body);
    font: var(--type-body);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4 { margin: 0 0 var(--space-4); color: var(--text-strong); }
/* Page headings follow the design's SCREENS, not its token sheet. --type-h1 is
   48px, but every Portal screen in the prototype sets its page title at
   --text-2xl regular and its section titles at --text-lg / --text-base medium
   (parity pass 2). The tokens stay as copied; these rules are what a page uses. */
h1 { margin-bottom: 6px; font: var(--fw-regular) var(--text-2xl)/1.2 var(--font-display); letter-spacing: var(--tracking-tight); }
h2 { font: var(--fw-medium) var(--text-lg)/1.25 var(--font-display); }
h3 { font: var(--fw-medium) var(--text-base)/1.25 var(--font-display); }
h4 { font: var(--fw-semibold) var(--text-sm)/1.3 var(--font-body); }

/* The small uppercase line above a page title: "Matrix Portal", "Administration". */
.eyebrow {
    margin: 0 0 var(--space-2);
    font: var(--fw-semibold) 10.5px/1 var(--font-body);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--brand-primary);
}

/* The design draws the admin eyebrow in --brand-pink, which is 2.6:1 on the page
   background and fails WCAG AA as text. The ink of the same family keeps the hue
   and passes. Same call as divergence 2. */
.eyebrow-admin { color: var(--page-pink-ink); }

/* "Portal home" with an arrow, at the top of a page. */
.back-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: 18px;
    font: var(--fw-medium) var(--text-xs)/1 var(--font-body);
    color: var(--brand-primary);
    text-decoration: none;
}
.back-link:hover { text-decoration: underline; }
.back-link svg { width: 14px; height: 14px; }

p { margin: 0 0 var(--space-4); }

a { color: var(--text-link); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--brand-primary-hover); }

code { font-family: var(--font-mono); font-size: 0.9em; }

/* One focus treatment everywhere. :focus-visible so a mouse click on a button
   does not draw a ring, but a keyboard tab always does. */
:focus-visible {
    outline: 2px solid var(--brand-primary);
    outline-offset: 2px;
    border-radius: var(--radius-xs);
}

/* Honour a reader who has asked the OS for less movement. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* Off-screen but still read aloud and still focusable. Used where the design
   shows only a lockup but the page still needs its heading. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}


/* =============================================================================
   Form primitives
   ============================================================================= */

.field { margin: 0 0 var(--space-5); }

/* The label WRAPS its input rather than pointing at an id. That keeps the pair
   associated with no id to keep unique across a page, and clicking the text
   still focuses the field. It does mean the label is a block containing both,
   so the gap between the words and the box is a margin on the input. */
.field > label {
    display: block;
    font: var(--type-label);
    color: var(--text-strong);
}

.field > label > input,
.field > label > select {
    display: block;
    margin-top: var(--space-2);
}

.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="tel"],
.field textarea,
.field select {
    width: 100%;
    padding: var(--space-3) var(--space-4);
    font: var(--type-body);
    color: var(--text-strong);
    background: var(--surface-card);
    border: var(--border-width) solid var(--border-default);
    border-radius: var(--radius-md);
    transition: border-color var(--dur-fast) var(--ease-standard),
                box-shadow var(--dur-fast) var(--ease-standard);
}

.field textarea { display: block; margin-top: var(--space-2); resize: vertical; min-height: 6rem; }

/* The design's 14px, 44px field. Desktop only: iOS Safari zooms the page into any
   field set under 16px, so phones keep the rule above. */
@media (min-width: 901px) {
    .field input[type="text"],
    .field input[type="email"],
    .field input[type="password"],
    .field input[type="tel"],
    .field select { height: 44px; padding: 0 14px; font: var(--type-body-sm); }
    .field textarea { padding: var(--space-3) 14px; font: var(--type-body-sm); }
}

/* A group of tick boxes reads as one question, so the fieldset carries the box
   and the legend is the question. */
.checkbox-set {
    padding: var(--space-4);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-md);
}

.checkbox-set legend { padding: 0 var(--space-2); font: var(--type-body-sm); color: var(--text-muted); }

/* The whole row is the target, not just the box. A 16px hit area is a miss on a
   phone, and the label text is the part people aim at anyway. */
.checkbox-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) 0;
    cursor: pointer;
}

/* Three overrides, all undoing rules meant for text boxes.
   ".field > label > input { display: block }" puts the tick box on its own line
   above the words, and the width:100% stretches it across the form. Both are
   right for a text field and wrong for a checkbox. */
.checkbox-row > input[type="checkbox"] {
    display: inline-block;
    width: auto;
    flex: none;
    margin: 0;
}

/* The row itself is a label inside .field, so it inherits display:block from
   the rule above. Say flex again, after it. */
.field > label.checkbox-row { display: flex; }

.field input:hover, .field select:hover { border-color: var(--border-strong); }

.field input:focus, .field select:focus {
    outline: none;
    border-color: var(--brand-primary);
    box-shadow: 0 0 0 3px var(--focus-ring);
}

.field .hint { display: block; margin-top: var(--space-2); font: var(--type-body-sm); color: var(--text-muted); }


/* =============================================================================
   Buttons
   ============================================================================= */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: 40px;
    padding: 0 var(--space-5);
    font: var(--type-label);
    font-size: var(--text-base);
    border: var(--border-width) solid transparent;
    border-radius: var(--radius-md);
    cursor: pointer;
    text-decoration: none;
    transition: background var(--dur-fast) var(--ease-standard),
                border-color var(--dur-fast) var(--ease-standard),
                box-shadow var(--dur-fast) var(--ease-standard);
}

/* DIVERGENCE 3. STAYS FLAT, AND THAT IS A MEASUREMENT, NOT AN OVERSIGHT.

   The designs put --brand-gradient on every primary action, and it was applied here
   under PRODUCT-3092 and then reverted the same day. White text on that gradient
   fails WCAG 2.2 SC 1.4.3 over roughly its first third:

     #FA5CDC  2.75:1     #9342DC  5.18:1     #4A20CA  8.93:1

   The fill only reaches 4.5:1 at about the 37% mark. Labels are centred, so a WIDER
   label spans MORE of the gradient, and measured in a browser on this stylesheet at
   115deg with a 49.6px button the glyphs land at:

     "Sign in" on the 400px block button   43% .. 57%   passes, and is the exception
     "Add client"                          16% .. 84%   ~3.4:1 at the leading edge
     "Request a demo"                      13% .. 87%   ~3.3:1
     "Start using Vision Demo now"          9% .. 91%   ~3.0:1

   So nearly every primary button in the Portal fails. The flat --brand-primary it
   keeps is a uniform 6.51:1.

   Divergence 2 at the top of this file is the same situation and settled the rule:
   the team measured --danger, --success and --warning against the prototype, called
   them failures, and diverged, because matching Vision matters more than matching the
   mockup. What is NOT settled is what a contrast-safe brand gradient should look like,
   and that is Nastasja's call rather than one to make in a stylesheet. It is on
   PRODUCT-2983 with these numbers.

   A hue-preserving darkening of the pink stop reaches 4.5:1 at #DA07B2, which is a
   visibly different colour and clears the bar by 0.01. Recorded so the next person
   does not redo the arithmetic. */
.btn-primary {
    background: var(--brand-primary);
    color: var(--text-on-brand);
    box-shadow: var(--shadow-brand);
}
.btn-primary:hover { background: var(--brand-primary-hover); color: var(--text-on-brand); }
.btn-primary:active { background: var(--brand-primary-active); }

.btn-secondary {
    background: var(--surface-card);
    color: var(--text-strong);
    border-color: var(--border-default);
    box-shadow: var(--shadow-xs);
}
.btn-secondary:hover { border-color: var(--brand-primary); color: var(--text-strong); }

.btn-block { width: 100%; }

.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; box-shadow: none; }


/* =============================================================================
   Messages
   ============================================================================= */

.alert {
    padding: var(--space-3) var(--space-4);
    margin: 0 0 var(--space-5);
    border: var(--border-width) solid transparent;
    border-radius: var(--radius-md);
    font: var(--type-body-sm);
}

.alert-error { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }
.alert-success { background: var(--success-bg); border-color: var(--success); color: var(--success); }
.alert-info { background: var(--info-bg); border-color: var(--info); color: var(--info); }

/* Blazor's own validation classes, retokened. These names come from
   InputText and the EditForm pipeline, so they cannot be renamed. */
.valid.modified:not([type=checkbox]) { outline: 1px solid var(--success); }
.invalid { outline: 1px solid var(--danger); }
.validation-message { display: block; margin-top: var(--space-2); font: var(--type-body-sm); color: var(--danger); }


/* =============================================================================
   The signed-in shell. Used by MainLayout.razor.

   A dark sidebar and a light content column. The sidebar collapses to an icon
   rail, driven entirely by the #shell-collapse checkbox that sits just before
   .shell in the markup, so there is no script in the path and no render mode.
   ============================================================================= */

.shell {
    display: grid;
    grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
    min-height: 100vh;
    min-height: 100dvh;
}

/* Collapsed. Only the width changes and the labels go; the icons, the order and
   the tab order all stay exactly as they were. */
#shell-collapse:checked ~ .shell { grid-template-columns: 96px minmax(0, 1fr); }
#shell-collapse:checked ~ .shell .nav-label,
#shell-collapse:checked ~ .shell .sidebar-heading,
#shell-collapse:checked ~ .shell .sidebar-subheading,
#shell-collapse:checked ~ .shell .sidebar-brand img { display: none; }
#shell-collapse:checked ~ .shell .sidebar { padding-inline: 10px; }
#shell-collapse:checked ~ .shell .sidebar-head { justify-content: center; }
#shell-collapse:checked ~ .shell .nav-item { justify-content: center; }
#shell-collapse:checked ~ .shell .sidebar-badge { justify-content: center; }

/* Keyboard focus on an off-screen checkbox has to show somewhere the eye can
   find, so the ring is drawn on the label it controls. */
#shell-collapse:focus-visible ~ .shell .sidebar-toggle {
    outline: 2px solid var(--neutral-0);
    outline-offset: 2px;
}

.sidebar {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: 14px 14px 12px;
    background: var(--brand-gradient-ink);
    color: var(--neutral-0);
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100dvh;
    overflow-y: auto;
}

.sidebar-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    min-height: 29px;
}

.sidebar-brand { display: block; line-height: 0; }
.sidebar-brand img { display: block; width: 168px; height: 29px; }

.sidebar-toggle {
    flex: none;
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease-standard);
}
.sidebar-toggle:hover { background: rgba(255, 255, 255, 0.12); }

/* Three bars, drawn rather than fetched. One icon is not worth a sprite. */
.sidebar-toggle-glyph,
.sidebar-toggle-glyph::before,
.sidebar-toggle-glyph::after {
    display: block;
    width: 16px;
    height: 2px;
    border-radius: 1px;
    background: var(--neutral-0);
}
.sidebar-toggle-glyph { position: relative; }
.sidebar-toggle-glyph::before { content: ""; position: absolute; top: -5px; }
.sidebar-toggle-glyph::after { content: ""; position: absolute; top: 5px; }

.sidebar-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; }

/* The design sets these at 36% white, about 3:1 on the sidebar, which fails AA
   for text this small. 55% keeps them quiet and readable. */
.sidebar-heading,
.sidebar-subheading {
    margin: var(--space-4) 11px var(--space-1);
    font: var(--fw-semibold) 10px/1 var(--font-body);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.55);
}
.sidebar-subheading { margin-top: var(--space-3); }

/* The staff badge at the top of the sidebar. Pink on the ink sidebar is 5.5:1. */
.sidebar-badge {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    padding: 8px 11px;
    border: var(--border-width) solid rgba(250, 92, 220, 0.32);
    border-radius: var(--radius-md);
    background: rgba(250, 92, 220, 0.14);
    font: var(--fw-semibold) 10.5px/1.3 var(--font-body);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--brand-pink);
}
.sidebar-badge svg { width: 14px; height: 14px; flex: none; }

.nav-icon { width: 16px; height: 16px; flex: none; }

/* The sidebar's sign-out is a button in a form, dressed as a nav item. */
.sidebar-signout {
    width: 100%;
    border: 0;
    background: rgba(255, 255, 255, 0.05);
    cursor: pointer;
    text-align: left;
}

.nav-item {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 8px 11px;
    border-radius: var(--radius-md);
    font: var(--fw-regular) var(--text-xs)/1.3 var(--font-body);
    color: rgba(255, 255, 255, 0.72);
    text-decoration: none;
    white-space: nowrap;
    transition: background var(--dur-fast) var(--ease-standard),
                color var(--dur-fast) var(--ease-standard);
}
.nav-item:hover { background: rgba(255, 255, 255, 0.10); color: var(--neutral-0); }
.nav-item.sidebar-signout { padding-block: 9px; font-weight: var(--fw-semibold); }

/* ---- the module rail ----
   Every module the person can see, solid when held and DASHED when not. That
   is the design's whole language for this rail.

   NO PADLOCK, NO GREYING, NO DIMMING, and both not-held states share ONE class
   here exactly as they do on a tile (AzDO #437). A dashed outline says "there
   is more here", which is an invitation. A padlock says "you are not allowed",
   which is a wall, and that is the design that was rejected.

   THE DASHES ARE NOT THE ONLY SIGNAL. Each item also carries its state in an
   off-screen span, so a screen reader is told what the border says. A border
   alone would fail WCAG 2.2 SC 1.4.1. */
.module-rail { display: flex; flex-direction: column; gap: var(--space-1); }

.module-rail-item { border: var(--border-width) solid transparent; }

/* Held: a solid tint in the module's own accent. */
.module-rail-item.is-held {
    background: rgba(255, 255, 255, 0.07);
    border-color: rgba(255, 255, 255, 0.14);
    color: var(--neutral-0);
}

/* Not held: an outline you can see through. */
.module-rail-item.is-not-held {
    border-style: dashed;
    border-color: rgba(255, 255, 255, 0.22);
}

.module-rail-icon { width: 18px; height: 18px; flex: none; }

/* The accent goes on the ICON only. The label stays the sidebar's own colour,
   because two of the four families are dark and a coloured text run would go
   unreadable against this background.

   The BRIGHT member of each family, not the base, for the same reason: these
   sit on a near-black sidebar, where --page-black would be invisible.

   PER MODULE, NEVER PER STATE. No [data-held] or .is-held in these four
   selectors, and there must not be. Colouring by state is the dimming that
   AzDO #437 rejected. */
.module-rail-item { --rail-accent: var(--violet-200); }
.module-rail-item[data-accent="purple"] { --rail-accent: var(--page-purple-bright); }
.module-rail-item[data-accent="blue"]   { --rail-accent: var(--page-blue-bright); }
.module-rail-item[data-accent="pink"]   { --rail-accent: var(--page-pink-bright); }
.module-rail-item[data-accent="ink"]    { --rail-accent: var(--neutral-400); }

.module-rail-icon { color: var(--rail-accent); }

/* Collapsed to the icon rail, the dashes are the only thing left saying which
   modules are not held, so they have to survive the label going away. */
#shell-collapse:checked ~ .shell .module-rail-item { padding-inline: var(--space-2); }

/* NavLink adds "active" itself, server-side, from the current URL. */
.nav-item.active {
    background: rgba(255, 255, 255, 0.13);
    color: var(--neutral-0);
    font-weight: var(--fw-semibold);
}

.sidebar-foot { margin-top: auto; }

/* The app bar across the top of the content column. The account menu is the
   only thing in it; the design's bell is left out, there are no notifications. */
.app-bar {
    position: sticky;
    top: 0;
    z-index: 25;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 10px;
    height: 60px;
    padding: 0 34px;
    background: var(--surface-card);
    border-bottom: var(--border-width) solid var(--border-subtle);
}

.profile { position: relative; }

.profile-summary {
    display: block;
    border-radius: var(--radius-pill);
    cursor: pointer;
    list-style: none;
}
.profile-summary::-webkit-details-marker { display: none; }
.profile-summary:hover .avatar { background: var(--brand-primary-hover); }

.avatar {
    flex: none;
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    border-radius: var(--radius-pill);
    /* Flat, for the same measured reason as .btn-primary above. The prototype's own
       avatar is the gradient, and on a 32px circle the initials span roughly 30% to
       70% of it, bottoming out at 4.07:1 against white. Marginal rather than badly
       wrong, and not worth carrying one contrast exception while the button question
       is open on PRODUCT-2983 (PRODUCT-3092). */
    background: var(--brand-primary);
    color: var(--text-on-brand);
    font: var(--fw-semibold) var(--text-xs)/1 var(--font-body);
    box-shadow: var(--shadow-xs);
}

.profile-menu {
    position: absolute;
    top: calc(100% + var(--space-2));
    right: 0;
    width: 268px;
    background: var(--surface-card);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-xl);
    z-index: 60;
    overflow: hidden;
}

.profile-email {
    margin: 0;
    padding: 12px 14px;
    border-bottom: var(--border-width) solid var(--border-subtle);
    font: var(--fw-semibold) var(--text-sm)/1.3 var(--font-body);
    color: var(--text-strong);
    overflow-wrap: anywhere;
}

/* Block, so the whole row is the target and not just the words. */
.profile-link,
.profile-signout button {
    display: flex;
    align-items: center;
    gap: 10px;
    width: calc(100% - 12px);
    margin: 0 6px;
    padding: 9px 10px;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    font: var(--fw-medium) var(--text-xs)/1 var(--font-body);
    color: var(--text-body);
    text-align: left;
    text-decoration: none;
    cursor: pointer;
}
.profile-link:first-of-type { margin-top: 6px; }
.profile-link svg,
.profile-signout svg { width: 15px; height: 15px; flex: none; }

.profile-link:hover,
.profile-link:focus-visible { background: var(--surface-sunken); color: var(--text-strong); }

.profile-signout {
    margin: 6px 0 0;
    padding: 6px 0;
    border-top: var(--border-width) solid var(--border-subtle);
}
.profile-signout button { font-weight: var(--fw-semibold); color: var(--danger); }
.profile-signout button:hover { background: var(--danger-bg); }

/* The account pages' cards, as the design lays out profile and password: a
   card per concern, two columns on a wide screen, one on a narrow one. */
.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 330px), 1fr));
    gap: 18px;
    align-items: start;
    margin-top: var(--space-6);
}

.card-stack { display: grid; gap: 18px; }

.card {
    padding: 22px;
    background: var(--surface-card);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-lg);
}

.card-title {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0 0 var(--space-4);
    font: var(--fw-semibold) var(--text-sm)/1.3 var(--font-body);
    color: var(--text-strong);
}

.card-note { margin: var(--space-4) 0; font: var(--fw-regular) var(--text-xs)/1.5 var(--font-body); color: var(--text-muted); }
.card > .btn:last-child { margin-top: var(--space-1); }

.card .stack-form { max-width: none; gap: var(--space-4); }
.card .stack-form .field { margin-bottom: 0; }

/* Label and value in one row, as the design's "My client & access". */
.fact-list {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0;
    margin: 0;
    font: var(--type-body-sm);
}
.fact-list dt,
.fact-list dd { padding: var(--space-3) 0; border-bottom: var(--border-width) solid var(--border-subtle); }
.fact-list dt { color: var(--text-muted); padding-right: var(--space-4); }
.fact-list dd { margin: 0; text-align: right; color: var(--text-strong); font-weight: var(--fw-semibold); overflow-wrap: anywhere; }
/* With the space between pills, this makes the design's 6px gap. */
.fact-list dd .pill { margin-left: 3px; }

/* The sign-in address: drawn like a locked field, but text, not an input. */
.readonly-label { margin: 0; font: var(--type-label); color: var(--text-strong); }
.readonly-value {
    margin: var(--space-2) 0 0;
    padding: var(--space-3) var(--space-4);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: var(--surface-sunken);
    font: var(--type-body-sm);
    color: var(--text-muted);
    overflow-wrap: anywhere;
}

.button-row { display: flex; flex-wrap: wrap; gap: var(--space-3); }

.pill.pill-brand { background: var(--info-bg); color: var(--info); }
.pill.pill-success { background: var(--success-bg); color: var(--success); }

/* An ordinary vertical form. Capped, because a name field the width of a
   1200px page reads as a mistake and invites a paragraph. */
.stack-form {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
    align-items: flex-start;
    max-width: 32rem;
}

.stack-form .field { width: 100%; }

.field-hint {
    margin: var(--space-1) 0 0;
    font: var(--type-body-sm);
    color: var(--text-muted);
}

/* Facts you can read but not edit. Two columns on a wide screen, stacked on a
   narrow one, so the label never gets squeezed to one word per line. */
.detail-list {
    display: grid;
    grid-template-columns: minmax(9rem, 12rem) 1fr;
    gap: var(--space-3) var(--space-6);
    max-width: 42rem;
    margin: 0;
}

.detail-list dt { font-weight: var(--fw-semibold); color: var(--text-muted); }
.detail-list dd { margin: 0; overflow-wrap: anywhere; }
.detail-list dd .admin-sub { display: block; margin: var(--space-1) 0 0; }

@media (max-width: 640px) {
    .detail-list { grid-template-columns: 1fr; gap: var(--space-1) 0; }
    .detail-list dd { margin-bottom: var(--space-3); }
}

.shell-main { background: var(--surface-page); min-width: 0; }

/* The design: 28px under the app bar, 34px either side, a 1180px column. */
.shell-content {
    max-width: calc(1180px + 68px);
    margin: 0 auto;
    padding: 28px 34px 56px;
}

/* The sidebar becomes a top bar. Below this width a 264px rail eats most of a
   phone screen, and the content is the job. */
@media (max-width: 900px) {
    .shell, #shell-collapse:checked ~ .shell { grid-template-columns: 1fr; }

    .sidebar,
    #shell-collapse:checked ~ .shell .sidebar {
        position: static;
        height: auto;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-3);
        padding: var(--space-4) var(--space-5);
    }

    /* Collapsing is a desktop idea. On a phone the labels are the only way to
       tell the items apart, so the toggle goes and the labels always show. */
    .sidebar-toggle { display: none; }
    #shell-collapse:checked ~ .shell .nav-label,
    #shell-collapse:checked ~ .shell .sidebar-heading,
    #shell-collapse:checked ~ .shell .sidebar-brand img { display: revert; }

    .sidebar-head { flex: 1 0 100%; }
    .sidebar-nav { flex-direction: row; flex-wrap: wrap; flex-basis: auto; }
    .sidebar-heading, .sidebar-subheading { display: none; }
    .sidebar-foot { margin-top: 0; margin-left: auto; }

    .app-bar { position: static; height: 52px; padding: 0 var(--space-5); }

    /* No overflow here any more. Phase 3 scrolled the whole content column as a
       stop-gap so the document would not scroll sideways; each table now owns a
       .table-scroll of its own, which keeps the page heading still as well as
       the sidebar. */
    .shell-content { padding: var(--space-8) var(--space-5); }
}


/* =============================================================================
   The tile grid on the home page.

   READ THIS BEFORE ADDING A RULE HERE.

   There is no .tile-entitled rule and no .tile-not-held rule below, and that is
   deliberate. Both classes exist in the markup, and styling them differently is
   the one thing this grid must not do.

   The rule, from AzDO #437 and repeated at the top of Home.razor: a tile you do
   not hold looks IDENTICAL to one you do, minus the tick. No padlock, no
   greying, no dimming, no muted border. The two not-held states share one class
   precisely so nobody can give them two treatments, and the held and not-held
   cards differ only in the state line, which is separate markup.

   test/flow/tile-grid.py check 5 normalises the two not-held tiles and asserts
   their markup is identical, so it catches a second CLASS. It cannot catch a
   .tile-not-held { opacity: .6 } added here. That one is on you.

   The only colour difference anywhere is on the tick and the word Active, which
   IS the tick. Never colour alone: the word rides with it (WCAG 2.2 SC 1.4.1).
   ============================================================================= */

/* The sentence under a page heading: what this screen is for, or why it is
   empty. Kept narrow, because a line of text the full width of a 1200px column
   is hard to read back to the start of. */
.page-lede {
    max-width: 62ch;
    font: var(--type-body-sm);
    color: var(--text-muted);
}

/* Two wide cards to a row, as the design lays out the modules you hold. */
.tile-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 420px), 1fr));
    gap: var(--space-5);
    margin: var(--space-6) 0 0;
    padding: 0;
    list-style: none;
}

.tile-item { display: flex; }

/* The sprite itself. Present on every page, drawing nothing. */
.icon-sprite { display: none; }

/* THE STROKE RULES GO ON THE CONSUMING <svg>, NOT ON THE SYMBOL.
   A <use> clones its symbol into a shadow tree under the element doing the
   using, so a selector like ".icon-sprite symbol" never reaches the copy that
   is actually drawn. Styling it there does nothing at all, silently: the paths
   just fall back to fill:black, stroke:none and render as filled blobs.

   fill and stroke are inherited SVG presentation properties, so setting them
   here does cross into the shadow tree, which is why this works and that
   did not. */
.icon {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* ---- per-module accent ----
   data-accent names one of the design system's four page-accent families and
   resolves it into two local custom properties. Everything below uses those,
   so adding a fifth family means one more block here and nothing else.

   PER MODULE, NEVER PER STATE. There is no [data-state] in any selector in
   this block, and there must not be: a Vision tile is the same purple whether
   the person holds it or not. Colouring by state is the dimming AzDO #437
   rejected, and check 5 of tile-grid.py normalises data-accent away precisely
   because it is allowed to differ per module and not per state. */
.tile[data-accent="purple"] { --tile-accent: var(--page-purple); --tile-accent-soft: var(--page-purple-soft); }
.tile[data-accent="blue"]   { --tile-accent: var(--page-blue);   --tile-accent-soft: var(--page-blue-soft); }
.tile[data-accent="pink"]   { --tile-accent: var(--page-pink);   --tile-accent-soft: var(--page-pink-soft); }
.tile[data-accent="ink"]    { --tile-accent: var(--page-black);  --tile-accent-soft: var(--page-black-soft); }

/* A module with no accent attribute at all still gets the brand colour rather
   than an unstyled box. */
.tile { --tile-accent: var(--brand-primary); --tile-accent-soft: var(--violet-100); }

.tile-icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    border-radius: var(--radius-md);
    background: var(--tile-accent-soft);
    color: var(--tile-accent);
}

.tile-icon svg { width: 18px; height: 18px; }

/* The design's module card: icon, then the words, then the way in, in one row,
   with the brand stripe down the left edge. The stripe is on EVERY tile, both
   sections and every state, for the same reason as everything else here. */
.tile {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-4);
    width: 100%;
    min-height: 84px;
    padding: 18px 20px 18px 23px;
    overflow: hidden;
    background: var(--surface-card);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    color: var(--text-strong);
    text-decoration: none;
    transition: border-color var(--dur-fast) var(--ease-standard),
                box-shadow var(--dur-fast) var(--ease-standard),
                transform var(--dur-fast) var(--ease-standard);
}

/* Applies to every tile, whichever state. A not-held tile is not a dead link:
   it goes to the module information page, so it must feel as clickable as the
   rest. */
.tile:hover {
    border-color: var(--tile-accent);
    box-shadow: var(--shadow-md);
    transform: translateY(-2px);
    color: var(--text-strong);
}

.tile:focus-visible {
    outline: 2px solid var(--tile-accent);
    outline-offset: 2px;
}

.tile::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 3px;
    background: var(--brand-gradient);
}

.tile-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); }

.tile-go { flex: none; width: 18px; height: 18px; color: var(--tile-accent); }

.tile-name {
    font: var(--fw-medium) var(--text-lg)/1.25 var(--font-display);
    color: var(--text-strong);
    overflow-wrap: anywhere;
}

.tile-state {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font: var(--fw-semibold) var(--text-xs)/1.3 var(--font-body);
    color: var(--text-muted);
}

.tile-state-active { color: var(--success); }

.tile-tick { font-size: var(--text-base); line-height: 1; }

/* The upsell section: modules the client does not license.

   THERE IS NOTHING HERE THAT CHANGES A TILE. No smaller card, no muted
   background, no reduced opacity. The heading and the sentence under it are
   what mark this block as different; the cards inside are the same cards.

   That is deliberate and it is the same rule as above. AzDO #437 rejected
   dimming a tile somebody does not hold, and moving these tiles into their own
   section did not un-reject it. What Phase 5 changed is WHERE they sit, which
   is AzDO #427, and nothing else. A rule like
   .upsell .tile { opacity: .7 } would quietly undo the part still standing. */
.upsell {
    margin-top: var(--space-10);
    padding-top: var(--space-6);
    border-top: var(--border-width) solid var(--border-subtle);
}

.upsell-heading {
    color: var(--text-strong);
    margin-bottom: var(--space-1);
}

.upsell .tile-grid { margin-top: var(--space-6); }


/* =============================================================================
   The admin screens.

   These are dense working screens, not marketing. They stay plain: a card, a
   table, and buttons that look like buttons.

   THE BUTTONS ARE STYLED BY DESCENDANT RULE, NOT BY CLASS. A grant grid renders
   one form and one button per user per module, so a class on each would be
   dozens of identical attributes and one of them would eventually be missed.
   Every control in an admin table is the same control, so it is styled by where
   it is. That is the exception, not the pattern: elsewhere use .btn.
   ============================================================================= */

/* Each table gets its OWN horizontal scroller, so a wide grant grid scrolls
   under a heading and a sidebar that stay put. Phase 3 put overflow on the
   whole content column as a stop-gap; this replaces it. */
.table-scroll {
    overflow-x: auto;

    /* POSITIONED, AND THAT IS THE FIX FOR A REAL BUG, not a habit.
       overflow only clips an absolutely positioned descendant when the
       overflowing element is that descendant's containing block, and a static
       element never is. So the .visually-hidden labels inside these tables
       were laid out against the PAGE instead: off-screen at x=538 on a 430px
       phone, unclipped, and setting the document's scroll width. The symptom
       was the whole admin page scrolling sideways by 108px with every visible
       box measuring 430 or less, which is why it looked like nothing at all
       was overflowing. (A guess that the sticky table header caused it was
       tested and disproved first.) */
    position: relative;
    margin: var(--space-6) 0;
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-lg);
    background: var(--surface-card);
}

.admin-table {
    width: 100%;
    border-collapse: collapse;
    font: var(--type-body-sm);
}

.admin-table caption {
    padding: var(--space-4) var(--space-5);
    text-align: left;
    font: var(--type-label);
    color: var(--text-muted);
    border-bottom: var(--border-width) solid var(--border-subtle);
}

.admin-table th,
.admin-table td {
    padding: var(--space-3) var(--space-4);
    text-align: left;
    vertical-align: top;
    border-bottom: var(--border-width) solid var(--border-subtle);
}

.admin-table thead th {
    position: sticky;
    top: 0;
    background: var(--surface-sunken);
    font: var(--type-label);
    font-size: 10.5px;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
    white-space: nowrap;
}

.admin-table tbody th { font: var(--type-label); color: var(--text-strong); }
.admin-table td { font: var(--fw-medium) var(--text-xs)/1.3 var(--font-body); color: var(--text-body); }
.admin-table tbody tr:last-child th,
.admin-table tbody tr:last-child td { border-bottom: 0; }
.admin-table tbody tr:hover td,
.admin-table tbody tr:hover th { background: var(--surface-page); }

/* One form per action means one form per cell, and a form is a block. Without
   this every button sits on its own line and the rows grow to three times the
   height they need. */
.admin-table form { display: inline; margin: 0; }

.admin-table button,
.admin-table select,
.admin-table input[type="text"] {
    height: 32px;
    font: var(--type-body-sm);
    font-weight: var(--fw-semibold);
    padding: 0 14px;
    border: var(--border-width) solid var(--border-default);
    border-radius: var(--radius-md);
    background: var(--surface-card);
    color: var(--text-strong);
    cursor: pointer;
}
/* A rename box is typed into, so it reads as text rather than a button. */
.admin-table input[type="text"] { padding: 0 var(--space-3); font-weight: var(--fw-regular); cursor: text; }
.admin-table input[type="text"]:focus { outline: none; border-color: var(--brand-primary); box-shadow: 0 0 0 3px var(--focus-ring); }

.admin-table button:hover:not(:disabled),
.admin-table select:hover { border-color: var(--brand-primary); color: var(--brand-primary); }

/* "Make admin" wrapped to two lines in a narrow column. A button label is one line. */
.admin-table button { white-space: nowrap; }

/* A .btn link inside a table (Invite, on the enquiries queue) is sized like the
   buttons beside it rather than like a page-level button. */
.admin-table .btn { min-height: 32px; padding: 0 14px; font: var(--type-body-sm); font-weight: var(--fw-semibold); border-radius: var(--radius-md); }

/* The one action per row the design fills: Approve. Named by class because it is
   the exception to the descendant rule above, not a second pattern. */
.admin-table button.is-primary { background: var(--brand-primary); border-color: var(--brand-primary); color: var(--text-on-brand); }
.admin-table button.is-primary:hover:not(:disabled) { background: var(--brand-primary-hover); color: var(--text-on-brand); }

/* When something happened reads first, as the design sets it. */
.admin-table td time { font-weight: var(--fw-semibold); color: var(--text-strong); }

/* Your own row on the users screen, said once. */
.admin-you {
    display: inline-block;
    margin-left: var(--space-2);
    padding: 2px var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--violet-100);
    color: var(--brand-primary);
    font: var(--fw-semibold) var(--text-xs)/1.3 var(--font-body);
}

/* The count and the add form in one row above a table. */
.admin-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    margin: var(--space-6) 0 0;
}
.admin-count { margin: 0; font: var(--type-body-sm); color: var(--text-muted); }
.inline-form { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0 0 0 auto; }
.inline-form input[type="text"] {
    min-width: 220px;
    height: 40px;
    padding: 0 var(--space-3);
    font: var(--type-body-sm);
    border: var(--border-width) solid var(--border-default);
    border-radius: var(--radius-md);
}
.inline-form input[type="text"]:focus { outline: none; border-color: var(--brand-primary); box-shadow: 0 0 0 3px var(--focus-ring); }

/* A client's state as the design's coloured pill, beside the select that sets it. */
.pill-state { margin-right: var(--space-2); }
.pill-state[data-pill="Active"]  { background: var(--success-bg); color: var(--success); }
.pill-state[data-pill="Paused"]  { background: var(--warning-bg); color: var(--warning); }
.pill-state[data-pill="Revoked"] { background: var(--danger-bg);  color: var(--danger); }

/* Disabled here means "you cannot suspend yourself". The form is still
   rendered on purpose, so the server-side refusal sits on a path that exists;
   see the comment in Users.razor. */
.admin-table button:disabled { opacity: 0.5; cursor: not-allowed; }

/* The sentence under an admin heading. Same job as .page-lede on the tile
   grid, so it gets the same treatment rather than a second look. */
.admin-sub {
    max-width: 62ch;
    font: var(--type-body-sm);
    color: var(--text-muted);
}

/* Inside a table cell it is a second line under the name, as the design stacks
   name over email. */
.admin-table .admin-sub { display: block; font-size: 11.5px; font-weight: var(--fw-regular); }

/* "suspended", next to a person's address. A word, not a colour: the row is
   not tinted, because a tinted row in a long table reads as an error. */
.admin-flag {
    display: inline-block;
    margin-left: var(--space-2);
    padding: 2px var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--danger-bg);
    color: var(--danger);
    font: var(--type-label);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
}

/* The second stage of a suspend, shown in the row rather than in a browser
   dialog, because it names who loses access and says what happens. */
.admin-confirm {
    margin: 0 0 var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-left: 3px solid var(--warning);
    background: var(--warning-bg);
    color: var(--text-strong);
    font: var(--type-body-sm);
}

.admin-error {
    margin: var(--space-2) 0 0;
    color: var(--danger);
    font: var(--type-body-sm);
}

.admin-note {
    margin: var(--space-2) 0 0;
    color: var(--text-muted);
    font: var(--type-body-sm);
}

/* The invite result: an address was created and here is the one-time link. */
.admin-banner {
    margin: 0 0 var(--space-6);
    padding: var(--space-5);
    border: var(--border-width) solid var(--border-subtle);
    border-left: 3px solid var(--success);
    border-radius: var(--radius-lg);
    background: var(--surface-card);
    box-shadow: var(--shadow-xs);
}

.admin-banner label { display: block; margin-bottom: var(--space-2); font: var(--type-label); }

/* Readonly, not disabled. A disabled input cannot be focused, so its contents
   cannot be copied by keyboard, which is the one thing this field is for. */
.admin-banner input[type="text"] {
    width: 100%;
    padding: var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--text-strong);
    background: var(--surface-sunken);
    border: var(--border-width) solid var(--border-default);
    border-radius: var(--radius-md);
}

/* A section on the email screen. */
.admin-block { margin-top: var(--space-10); }

/* Recovery codes. Monospace and roomy: these get read aloud down a phone and
   typed back in, so 0 against O and 1 against l have to be different shapes. */
.recovery-codes {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: var(--space-2);
    margin: var(--space-5) 0;
    padding: var(--space-5);
    list-style: none;
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-lg);
    background: var(--surface-sunken);
}

.recovery-codes code {
    font-family: var(--font-mono);
    font-size: var(--text-base);
    letter-spacing: 0.05em;
    color: var(--text-strong);
}

/* Two-step enrolment. */
.steps {
    max-width: 62ch;
    margin: 0 0 var(--space-6);
    padding-left: var(--space-5);
    color: var(--text-body);
}

.steps li { margin-bottom: var(--space-2); }

.qr-code {
    display: block;
    padding: var(--space-4);
    background: var(--neutral-0);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-lg);
}

/* The typed-by-hand fallback. Same reasoning as the recovery codes: this gets
   read off a screen and typed into a phone, so the shapes have to be distinct. */
.manual-key { margin-top: var(--space-5); font: var(--type-body-sm); color: var(--text-muted); }

.manual-key code {
    display: inline-block;
    margin-top: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background: var(--surface-sunken);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-sm);
    font-size: var(--text-base);
    letter-spacing: 0.1em;
    color: var(--text-strong);
    overflow-wrap: anywhere;
}

/* Admin forms that are not tables: invite, and the email test-send. A column,
   not a grid, because each is three or four fields at most. */
.admin-form { max-width: 480px; }

/* ---- the overview screen ---- */

.stat-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: var(--space-4);
    margin: var(--space-8) 0;
    padding: 0;
    list-style: none;
}

.stat {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-5);
    background: var(--surface-card);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-xs);
}

/* The design's tile: a small uppercase label, then the figure, then a note. */
.stat-value {
    font: var(--fw-medium) var(--text-2xl)/1.1 var(--font-display);
    color: var(--text-strong);
    letter-spacing: var(--tracking-tight);
    /* Tabular figures, so a column of numbers lines up and a 1 is as wide as
       a 9. Without it the tiles jitter as counts change. */
    font-variant-numeric: tabular-nums;
}

.stat-label {
    margin-bottom: var(--space-2);
    font: var(--fw-semibold) 10.5px/1 var(--font-body);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--text-muted);
}
.stat-note { font: var(--type-body-sm); font-size: var(--text-xs); color: var(--text-muted); }

.stat-heading { margin-top: var(--space-10); }

/* The mix bar. Decoration on top of a number that is already in the row, never
   instead of it, so it carries aria-hidden and nothing is lost without it. */
.mix-bar {
    display: block;
    width: 120px;
    height: 8px;
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    overflow: hidden;
}

.mix-fill {
    display: block;
    height: 100%;
    border-radius: var(--radius-pill);
    background: var(--mix-accent, var(--brand-primary));
}

.mix-bar[data-accent="purple"] { --mix-accent: var(--page-purple); }
.mix-bar[data-accent="blue"]   { --mix-accent: var(--page-blue); }
.mix-bar[data-accent="pink"]   { --mix-accent: var(--page-pink); }
.mix-bar[data-accent="ink"]    { --mix-accent: var(--page-black); }


/* =============================================================================
   The module information page, where a not-held tile leads.

   Wears the module's own accent, resolved the same way the tile does, so a
   person who clicked a blue tile lands on a blue page.
   ============================================================================= */

.module-page {
    max-width: 920px;
    --tile-accent: var(--brand-primary);
    --tile-accent-soft: var(--violet-100);
}

.module-page[data-accent="purple"] { --tile-accent: var(--page-purple); --tile-accent-soft: var(--page-purple-soft); }
.module-page[data-accent="blue"]   { --tile-accent: var(--page-blue);   --tile-accent-soft: var(--page-blue-soft); }
.module-page[data-accent="pink"]   { --tile-accent: var(--page-pink);   --tile-accent-soft: var(--page-pink-soft); }
.module-page[data-accent="ink"]    { --tile-accent: var(--page-black);  --tile-accent-soft: var(--page-black-soft); }

.module-head {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin-bottom: var(--space-6);
}

.module-head h1 { margin: 0; }

/* "Not on your account", above the headline. NotLicensed branch only. */
.module-status { margin: calc(var(--space-6) * -1) 0 var(--space-6); }
.module-status + .module-headline { margin-top: calc(var(--space-4) * -1); }

.pill {
    display: inline-flex;
    align-items: center;
    padding: 4px 10px;
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    font: var(--fw-semibold) 11px/1.4 var(--font-body);
    color: var(--text-muted);
}
/* The one pill the design sets in capitals. */
.module-status .pill { padding: 3px 9px; font-size: 10px; letter-spacing: var(--tracking-wide); text-transform: uppercase; }

/* The question the module answers, under the heading block. Not inside it: see the
   note in ModuleInfo.razor for why it renders on one branch only (PRODUCT-3092). */
.module-headline {
    /* COUPLED TO .module-head's margin-bottom above, which this cancels exactly so the headline
       sits against the name rather than a heading-sized gap below it. Change one and change both,
       or the two either overlap or grow a double gap. */
    margin: calc(var(--space-6) * -1) 0 var(--space-4);
    font: var(--type-h4);
    font-weight: var(--fw-regular);
    color: var(--text-muted);
}

/* What you would get. A card, because the design groups the bullets into one
   (PRODUCT-3092). */
.benefits {
    margin: var(--space-6) 0;
    padding: 28px;
    background: var(--surface-card);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
}
@media (max-width: 900px) { .benefits { padding: var(--space-5) var(--space-5) var(--space-6); } }

/* The card's header row: icon, heading, and the action on the right. */
.benefits-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin: 0 0 var(--space-5);
    padding-bottom: var(--space-4);
    border-bottom: var(--border-width) solid var(--border-subtle);
}

.benefits-icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    border-radius: var(--radius-md);
    background: var(--tile-accent-soft);
    color: var(--tile-accent);
}
.benefits-icon svg { width: 18px; height: 18px; }

/* The header row WRAPS. On a phone the icon, the heading and "Start using
   Vision Demo now" do not fit on one line, and a row that cannot wrap pushed
   the whole page sideways (code review, parity pass 2). The button drops to
   its own line instead. */
.benefits-head { flex-wrap: wrap; }
.benefits-heading { flex: 1 1 10rem; margin: 0; font: var(--fw-medium) var(--text-base)/1.25 var(--font-display); }

.demo-action { flex: 0 1 auto; max-width: 100%; }
.demo-action .btn { max-width: 100%; white-space: normal; }
.demo-action form { margin: 0; }

.benefits-list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: var(--space-3);
}

/* Two columns so a long line wraps under itself and not under the tick. */
.benefits-list li {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-3);
    align-items: start;
    font: var(--type-body-sm);
}

/* Decorative and aria-hidden, with the meaning carried by the text beside it, so the
   3:1 non-text rule applies rather than 4.5:1.

   --tile-accent resolves to one of four colours by data-accent, and --surface-card is
   --neutral-0, which IS #FFFFFF - there is no theme override in this file. So all four
   are measurable and all four clear 3:1:

     ink  #1E1A34  16.77:1     purple #7A2FD8  6.51:1
     blue #2563C9   5.67:1     pink   #D83AB0  4.08:1

   PINK is the one to check if a fifth accent is added: it is the LIGHTEST, so it is the
   closest to the floor. An earlier version of this comment said the card was not white
   and named blue as "the darkest", which was wrong twice - ink is darkest, and darkest
   is the safe end anyway. */
.benefits-tick {
    color: var(--tile-accent);
    font-weight: var(--fw-bold);
    line-height: inherit;
}

.module-icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 56px;
    height: 56px;
    border-radius: var(--radius-lg);
    background: var(--tile-accent-soft);
    color: var(--tile-accent);
}

.module-icon svg { width: 30px; height: 30px; }

/* The primary action here USED to take the module's accent. Removed in parity
   pass 2: white on --page-pink is 4.08:1, under AA for a 16px label, and the
   design gives this button the same treatment as every other primary. It is
   flat --brand-primary now, for the reason in divergence 3. */


/* =============================================================================
   Message pages: /not-allowed, /not-found, /Error, and the NotAuthorized block
   in Routes.razor.

   These are NOT auth pages. A person reading /not-allowed is signed in, and the
   page says so, so putting them behind the sign-in brand panel would be a lie.
   They keep MainLayout and bring their own container, which is why this is a
   plain class and not a second layout. MainLayout itself is Phase 3.
   ============================================================================= */

.message-page {
    max-width: 560px;
    margin: 0 auto;
    padding: var(--space-16) var(--space-6);
}



.message-page code {
    padding: 2px var(--space-2);
    background: var(--surface-sunken);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-xs);
    color: var(--text-strong);
    overflow-wrap: anywhere;
}

@media (max-width: 900px) {
    .message-page { padding: var(--space-10) var(--space-5); }
}


/* =============================================================================
   The unauthenticated shell. Used by AuthLayout.razor.

   The design (parity pass 2): one dark ink page with faint slanted shapes, the
   wordmark centred above a white card, and a contact line under it. Sign-in is
   the one page every user reaches, so it has to work on a phone even though the
   prototype was drawn desktop only; under 900px the card simply goes full width.
   ============================================================================= */

.auth {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-6);
    min-height: 100vh;
    min-height: 100dvh;
    padding: var(--space-12) var(--space-6) var(--space-8);
    background: var(--brand-gradient-ink);
    overflow: hidden;
}

/* Decoration only, and drawn rather than fetched: an image here would sit
   behind the MFA gate's allow-list, and a missed prefix redirects it. */
.auth-shapes span {
    position: absolute;
    width: 150px;
    height: 72px;
    border-radius: 14px;
    background: rgba(147, 66, 220, 0.22);
    transform: skewX(-24deg);
    pointer-events: none;
}
.auth-shapes span:nth-child(1) { top: 6%;  right: 6%;  width: 220px; height: 110px; background: rgba(122, 47, 216, 0.30); }
.auth-shapes span:nth-child(2) { top: 20%; right: 20%; width: 120px; height: 60px; }
.auth-shapes span:nth-child(3) { top: 34%; right: 2%;  width: 260px; height: 120px; background: rgba(74, 32, 202, 0.26); }
.auth-shapes span:nth-child(4) { top: 58%; left: -2%;  width: 110px; height: 60px; }
.auth-shapes span:nth-child(5) { top: 72%; left: 5%;   width: 90px;  height: 58px; }
.auth-shapes span:nth-child(6) { top: 88%; left: 12%;  width: 60px;  height: 34px; }

.auth-brand,
.auth-panel,
.auth-footer { position: relative; z-index: 1; }

/* The wordmark is the PNG the branded emails already use: white on transparent,
   336x58, shown here at 208x36 so it is still scaled down. A proper SVG is still
   outstanding from Nastasja, and when it lands this is the only line that changes. */
.auth-brand img { display: block; width: 208px; height: 36px; }

.auth-panel { display: flex; justify-content: center; width: 100%; }

.auth-card {
    width: 100%;
    max-width: 420px;
    padding: 30px;
    background: var(--surface-card);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-xl);
}

/* The request form is two columns, so its card is wider. */
.auth-card:has(.field-grid) { max-width: 620px; padding: var(--space-8); }

.auth-card h1 { margin-bottom: var(--space-1); font: var(--fw-medium) var(--text-xl)/1.2 var(--font-display); }

/* The card's one main action is the design's large button. */
.auth-card .btn-primary { min-height: 48px; padding: 0 28px; font-size: var(--text-md); border-radius: var(--radius-lg); }

.auth-lede { font: var(--type-body-sm); color: var(--text-muted); margin-bottom: var(--space-6); }

.auth-foot { margin: var(--space-5) 0 0; font: var(--type-body-sm); }

.auth-footer {
    font: var(--fw-regular) var(--text-xs)/1.5 var(--font-body);
    color: rgba(255, 255, 255, 0.62);
    text-align: center;
}
.auth-footer a { color: inherit; }

/* "Forgot your password?" sits under the password box, right-aligned. */
.field-aside { margin: calc(var(--space-2) * -1) 0 var(--space-5); text-align: right; font: var(--fw-medium) var(--text-xs)/1.3 var(--font-body); }
.field-aside a { text-decoration: none; }
.field-aside a:hover { text-decoration: underline; }

/* The rule with words in it: "No account yet?" */
.auth-divider {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin: var(--space-5) 0 var(--space-4);
    font: var(--fw-regular) var(--text-xs)/1 var(--font-body);
    /* Not --text-faint (2.4:1 on the card): 12px text needs 4.5:1. --neutral-600 is 7.6:1. */
    color: var(--neutral-600);
}
.auth-divider::before,
.auth-divider::after { content: ""; flex: 1; height: 1px; background: var(--border-subtle); }

.auth-note { margin: var(--space-3) 0 0; font: var(--fw-regular) var(--text-xs)/1.5 var(--font-body); color: var(--neutral-600); }

/* An alert with its icon, as the design draws the sign-in error. */
.alert-icon { display: flex; align-items: flex-start; gap: var(--space-2); }
.alert-icon svg { width: 16px; height: 16px; flex: none; margin-top: 1px; }

/* The "done" states: request received, check your email, password set. */
.auth-done { text-align: center; }
.auth-done .auth-lede { margin-bottom: var(--space-5); }
.auth-done-icon {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    margin: 0 auto var(--space-4);
    border-radius: var(--radius-pill);
    background: var(--violet-50);
    color: var(--brand-primary);
}
.auth-done-icon svg { width: 20px; height: 20px; }
.auth-done-icon.is-success { background: var(--success-bg); color: var(--success); }

/* Two fields side by side on the request form, one column on a phone. */
.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--space-4); }

/* Module interest as chips. Still real tick boxes: the box is off-screen, the
   label is the chip, and :has() draws the ticked and focused states. */
.chip-set { margin: 0 0 var(--space-5); padding: 0; border: 0; }
.chip-set legend { margin-bottom: var(--space-2); padding: 0; font: var(--type-label); color: var(--text-strong); }
.chip-row { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.chip {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 8px 10px;
    border: var(--border-width) solid var(--border-default);
    border-radius: var(--radius-md);
    background: var(--surface-card);
    font: var(--fw-semibold) var(--text-xs)/1 var(--font-body);
    color: var(--text-body);
    cursor: pointer;
}
.chip:hover { border-color: var(--brand-primary); }
.chip:has(input:checked) { border-color: var(--brand-primary); background: var(--violet-50); color: var(--brand-primary); }
.chip:has(input:focus-visible) { outline: 2px solid var(--brand-primary); outline-offset: 2px; }
.chip-tick { display: none; width: 14px; height: 14px; }
.chip:has(input:checked) .chip-tick { display: block; }

@media (max-width: 900px) {
    .auth { padding: var(--space-8) var(--space-4) var(--space-6); }
    .auth-card, .auth-card:has(.field-grid) { padding: var(--space-6) var(--space-5); }
    .auth-shapes { display: none; }
}

@media (max-width: 640px) {
    .field-grid { grid-template-columns: 1fr; }
}

/* ===== Parity pass 4: login ===== */

/* Fix 1. The design's sign-in card is 396px wide (334px inside the padding), not 420.
   Scoped to the login page through the class on its form; Login.razor carries it. */
.auth-card:has(.auth-login) { max-width: 396px; }

/* Fix 2 (shared auth frame). The design centres the wordmark, card and footer as one block on
   the page; the Portal pinned them to the top. Two causes:
   - .auth-shapes was an empty static flex item, so it took a gap slot above the wordmark (24px
     of phantom space). It is taken out of the flow here; its spans still position against .auth.
   - nothing absorbed the free height. Auto margins on the first and last item do it, and they
     resolve to 0 when the content is taller than the window, so nothing clips (a plain
     justify-content: center would push the wordmark off the top of a short window).
   Desktop only: the design has no phone layout and the phone rules above stay as they were. */
.auth-shapes { position: absolute; inset: 0; }

@media (min-width: 901px) {
    .auth-brand { margin-top: auto; }
    .auth-footer { margin-bottom: auto; }
}

/* Fix 3 (shared auth frame). Gap between wordmark, card and footer is 34px in the design (24px
   here), and the page keeps 48px below as well as above (32px here). Desktop only. */
@media (min-width: 901px) {
    .auth { gap: 34px; padding-bottom: 48px; }
}

/* Fix 4 (shared auth frame). The design's backdrop is one picture of tilted tiles, shown twice:
   620px wide at 16% opacity off the top right, and 520px wide at 10% opacity, turned half a turn,
   off the bottom left. The Portal drew six CSS parallelograms instead, which look nothing like it.
   The picture is the prototype's own asset, so it is copied to /images/auth. The old comment said
   an image would sit behind the MFA gate, but /images is already on its allow-list (the wordmark
   lives there), so the reason no longer holds. The six spans are hidden, not deleted, so
   AuthLayout.razor stays untouched for the other two pages that share it. */
.auth-shapes { display: none; }

.auth::before,
.auth::after {
    content: "";
    position: absolute;
    background: url("../images/auth/backdrop-shapes.png") center / 100% 100% no-repeat;
    pointer-events: none;
}
.auth::before { top: -120px; right: -140px; width: 620px; height: 497.4px; opacity: 0.16; }
.auth::after { bottom: -160px; left: -180px; width: 520px; height: 417.1px; opacity: 0.1; transform: rotate(180deg); }

@media (max-width: 900px) {
    .auth::before, .auth::after { display: none; }
}

/* Fix 5. Label to box is 6px in the design, 8px here. Scoped to the sign-in form: the shared
   .field rules also serve the request form, the set-password form and the account pages. */
.auth-login .field > label > input { margin-top: 6px; }

/* Fix 6. Between one field and the next the design leaves 16px (box bottom to next label), the
   Portal 20px. "Forgot your password?" then sits 10px under the password box in the design; the
   pull-up on .field-aside is shortened so it still lands there (16 - 6 = 10). */
.auth-login .field { margin-bottom: 16px; }
.auth-login .field-aside { margin-top: -6px; }

/* Fix 7. The design's "Forgot your password?" row is one 12px line (line-height 12px), so the
   gap above the button is a clean 20px. The Portal's row was 15.6px tall, which pushed the
   button 3.6px lower. */
.auth-login .field-aside { line-height: 12px; }

/* Fix 8. The design's "Sign in" heading has no letter spacing. The Portal inherited the global
   h1 tightening of -0.02em (-0.48px at 24px), which makes the word narrower than the design's. */
.auth-card:has(.auth-login) h1 { letter-spacing: normal; }

/* Fix 9. Heading to sub-line is 6px in the design, 4px here. */
.auth-card:has(.auth-login) h1 { margin-bottom: 6px; }

/* Fix 10. Sub-line to first label is 22px in the design, 24px here. */
.auth-card:has(.auth-login) .auth-lede { margin-bottom: 22px; }

/* Fix 11. "No account yet?" sits 22px under the Sign in button and 18px above Request access
   in the design; the Portal had 20px and 16px. */
.auth-card:has(.auth-login) .auth-divider { margin: 22px 0 18px; }

/* Fix 12. The divider words are medium weight (500) in the design, regular (400) here. Colour is
   left alone on purpose: the design's #A9A2BE is 2.44:1 on white, under the 4.5:1 floor. */
.auth-card:has(.auth-login) .auth-divider { font-weight: var(--fw-medium); }

/* Fix 13. The note under Request access is 11.5px on a 17.825px line in the design, 12px on 18px
   here. Manrope runs wider than Gilroy, and at 12px the note broke one word early ("before an /
   account opens." against the design's "...before an account / opens."). Colour is left alone:
   the design's #A9A2BE is 2.44:1 on white. */
.auth-card:has(.auth-login) .auth-note { font-size: 11.5px; line-height: 1.55; }

/* ===== Parity pass 4: request-access ===== */

/* The design's example text in the five fields. Its colour is --text-faint, 2.4:1 on white,
   which fails 1.4.3, so the Portal draws the same text in a darker ink (5.2:1). */
.auth-card:has(.field-grid) .field input::placeholder,
.auth-card:has(.field-grid) .field textarea::placeholder { color: #6F6987; opacity: 1; }

/* Chip height. The design's chip text sits on a 1.3 line (15.6px), so the chip is 34px tall. */
.auth-card:has(.field-grid) .chip { line-height: 1.3; }

/* Note box height. The shared 6rem minimum made three rows 96px; the design's is 89px. */
.auth-card:has(.field-grid) .field textarea { min-height: 0; }

/* Label to box gap: 6px in the design, 8px in the shared rule. Desktop only, as the 14px fields are. */
@media (min-width: 901px) {
    .auth-card:has(.field-grid) .field > label > input,
    .auth-card:has(.field-grid) .field > label > textarea { margin-top: 6px; }
}

/* Row gap in the two-column grid: 16px in the design, 20px in the shared .field rule. */
@media (min-width: 901px) {
    .auth-card:has(.field-grid) .field-grid > .field { margin-bottom: 16px; }
}

/* Gap under "What are you interested in?": 10px in the design, 8px in the shared rule. */
@media (min-width: 901px) {
    .auth-card:has(.field-grid) .chip-set legend { margin-bottom: 10px; }
}

/* Note box to button: 22px in the design, 20px in the shared rule. Only the note field is a direct child of the form. */
@media (min-width: 901px) {
    .auth-card:has(.field-grid) form > .field { margin-bottom: 22px; }
}

/* Space between module chips: 10px in the design, 8px in the shared rule. */
@media (min-width: 901px) {
    .auth-card:has(.field-grid) .chip-row { gap: 10px; }
}

/* Heading rhythm: the design puts 16px under the back link and 6px under the title; the shared rules have 18 and 4.
   The two move together, so the lede and everything below it stay where they were. */
@media (min-width: 901px) {
    .auth-card:has(.field-grid) .back-link { margin-bottom: 16px; }
    .auth-card:has(.field-grid) h1 { margin-bottom: 6px; }
}

/* Each chip carries its module's icon, as the design draws it (15px). A ticked chip swaps the icon for
   the tick, so "chosen" is not told by colour alone. */
.auth-card:has(.field-grid) .chip-mod,
.auth-card:has(.field-grid) .chip-tick { width: 15px; height: 15px; flex: none; }
.auth-card:has(.field-grid) .chip:has(input:checked) .chip-mod { display: none; }

/* ===== Parity pass 4: forgot-password ===== */

/* Fixes 1 to 3 of this page (centring the stack, taking the empty shapes layer out of the
   flow, the picture backdrop) were made twice: the login page's block above carries the same
   three changes against the same design values, so this block keeps only the rules that are this
   page's own. The one picture lives at /images/auth/backdrop-shapes.png. (Merge, parity pass 4.) */

/* Fix 4. The design's wordmark is the tile mark plus live text: 52 x 42 mark, 16px gap,
   38px semibold "Matrix", then "Portal" in regular weight, tracked out and set in a light
   purple. The design sweeps pink to white across "Portal"; here it is one static colour,
   4.45:1 on the page (3:1 is the floor at 38px). The text is aria-hidden, as the old PNG's alt="" was. */
.auth-wordmark { display: flex; align-items: center; gap: 16px; }
.auth-wordmark-mark {
    flex: none;
    width: 52px;
    height: 42px;
    background: url("/images/auth/mark.png") center / contain no-repeat;
}
.auth-wordmark-text {
    font: var(--fw-semibold) 38px/1 var(--font-display);
    letter-spacing: -0.02em;
    color: #FFFFFF;
}
.auth-wordmark-portal {
    margin-left: 0.34em;
    font-weight: var(--fw-regular);
    letter-spacing: 0.04em;
    color: #A56EDC;
}

/* Fix 5. The design sets the reset title at 20px / 24px with a 6px gap under it, and
   the sign-in and request titles at 24px. The 24px card title is shared with those two
   pages, so this is a class on this page's own h1s. Manrope at 20px is as wide as Gilroy
   at 20px was, so the title no longer reads as a size bigger than the design's. */
.auth-card h1.auth-reset-title { margin-bottom: 6px; font-size: 20px; line-height: 24px; letter-spacing: normal; }
.auth-done h1.auth-reset-title { margin-bottom: var(--space-2); }

/* Fix 6. Design gap under the lede is 20px; the shared .auth-lede leaves 24px. */
.auth-card:has(.auth-reset-title) .auth-lede { margin-bottom: var(--space-5); }

/* Fix 7. The design's back link sits 16px above the title with a 7px icon gap; the shared
   .back-link uses 18px and 8px. Scoped to this page so the other back links stay as they are. */
.auth-card:has(.auth-reset-title) .back-link { gap: 7px; margin-bottom: var(--space-4); }

/* Fix 8. Label to box is 6px in the design; the shared .field rule leaves 8px. */
.auth-card:has(.auth-reset-title) .field > label > input { margin-top: 6px; }

/* Fix 9. The design's footer row is 30px tall because the "Start from the website" pill
   sits in it (the pill itself stays out, G14). With the row at 18px the centred stack sat
   about 6px low. Desktop only: on a phone the line can wrap and 30px lines would double up. */
@media (min-width: 901px) {
    .auth-footer { line-height: 30px; }
}

/* Fix 10. The "sent" card: the design's mail badge is 52px with a 24px icon; the shared
   .auth-done-icon is 44px with 20px. The other done cards keep theirs. */
.auth-done:has(.auth-reset-title) .auth-done-icon { width: 52px; height: 52px; }
.auth-done:has(.auth-reset-title) .auth-done-icon svg { width: 24px; height: 24px; }

/* Fix 11. Phone guard for fix 4. The 305px wordmark is wider than the 288px a 320px phone
   leaves inside the page padding, so it ran 9px into the margin on each side. Scaled down
   under 400px; nothing was clipped or scrolling, so this is tidiness, not a repair. */
@media (max-width: 400px) {
    .auth-wordmark { gap: 12px; }
    .auth-wordmark-mark { width: 41px; height: 33px; }
    .auth-wordmark-text { font-size: 30px; }
}

/* ===== Parity pass 4: home ===== */

/* Fix 1. The design draws a hairline above the pinned sign-out (1px at 12% white,
   12px of air under it). Desktop only: under 900px the foot is a flex item in a
   row and a top border would be a stray line. The foot grows upward from the
   bottom, so the sign-out button does not move. */
@media (min-width: 901px) {
    .sidebar-foot {
        border-top: var(--border-width) solid rgba(255, 255, 255, 0.12);
        padding-top: 12px;
    }
}

/* Fix 2. The design card pads 20px on every side and sets 12px between the icon
   and the words, so the icon sits at x+21 and the name 48px after it. The Portal
   padded 23px on the left and 16px between. State-agnostic: every tile gets it. */
.tile {
    padding-left: 20px;
    gap: var(--space-3);
}

/* Fix 3. The design leaves 30px between the greeting's sentence and the first
   card. The Portal collapsed that to the grid's 24px. data-lede marks the
   greeting's paragraph, so the upsell section's sentence is untouched. */
.page-lede[data-lede="greeting"] { margin-bottom: 30px; }

/* Fix 4. The design sets "Your modules" 10px under Portal home (8px margin plus
   the nav's 2px gap). The Portal left 18px. Only the heading that directly
   follows a nav item is touched, which is this one: the Administration heading
   follows the module rail, not an item. */
@media (min-width: 901px) {
    .sidebar-nav > .nav-item + .sidebar-heading { margin-top: 8px; }
}

/* Fix 5. The design's collapse button is 30px square with an 8% white tint, set
   4px lower so it lines up with the wordmark. The Portal drew 32px with no tint.
   Desktop only: on a phone the toggle is hidden and the head must not grow. */
@media (min-width: 901px) {
    .sidebar-head { padding-top: 4px; }
    .sidebar-toggle {
        width: 30px;
        height: 30px;
        background: rgba(255, 255, 255, 0.08);
        color: rgba(255, 255, 255, 0.7);
    }
    .sidebar-toggle:hover { background: rgba(255, 255, 255, 0.12); color: var(--neutral-0); }
}

/* Fix 6. The design's collapse icon is a panel outline: a 17px rounded square, a
   divider near the left edge and a chevron in the wider pane. The Portal drew
   three bars (a menu icon, which says "menu", not "side panel"). Drawn here with
   borders, as before: one icon is not worth a sprite. Desktop only, since the
   toggle is hidden on a phone. */
@media (min-width: 901px) {
    .sidebar-toggle-glyph {
        width: 17px;
        height: 17px;
        border: 1.5px solid currentColor;
        border-radius: 3px;
        background: transparent;
    }
    .sidebar-toggle-glyph::before {
        top: 0;
        bottom: 0;
        left: 4.5px;
        width: 1.5px;
        height: auto;
        border-radius: 0;
        background: currentColor;
    }
    .sidebar-toggle-glyph::after {
        top: 4px;
        left: 8.5px;
        width: 5px;
        height: 5px;
        border-radius: 0;
        background: transparent;
        border-left: 1.5px solid currentColor;
        border-bottom: 1.5px solid currentColor;
        transform: rotate(45deg);
    }
}

/* ===== Parity pass 4: module-info ===== */

/* 1. The design centres a 900px column; the page was left-aligned at 920px. */
.module-page { max-width: 900px; margin-inline: auto; }

/* 2. The blurb under the question: 16px on a 26.4px line, 28px above the card, 625px measure. */
.module-page .page-lede { font-size: 16px; line-height: 26.4px; max-width: 625px; margin-bottom: 28px; }

/* 3. The card header: a 46px icon tile (22px glyph), 14px to the title, 20px above the divider and
   18px below it, so the first bullet sits 114px under the card's top edge as in the design. */
.module-page .benefits-head { column-gap: 14px; padding-bottom: 20px; margin-bottom: 18px; }
.module-page .benefits-icon { width: 46px; height: 46px; }
.module-page .benefits-icon svg { width: 22px; height: 22px; }

/* 4. The "Not on your account" pill sits on the name's row, as the design draws it beside the
   module name, instead of on a line of its own pulled up with negative margins. */
.module-headrow { display: flex; flex-wrap: wrap; align-items: center; column-gap: 14px; row-gap: 8px; }
.module-headrow .module-head { margin-bottom: 0; }
.module-headrow .module-status { margin: 0; }
.module-headrow + .module-headline { margin: 14px 0 10px; }

/* 5. The bullet text starts 27px in: a 15px tick column and the 12px gap. The glyph stays a text
   character (see ModuleInfo.razor); only the column it sits in is the design's width. */
.module-page .benefits-list li { grid-template-columns: 15px 1fr; }

/* ===== Parity pass 4: account-profile ===== */

/* The lede is one line in the design (617px wide at 14px). .admin-sub caps at 62ch, which wraps this
   sentence and pushes both cards 22px down. Scoped to this page: .admin-sub is shared. */
.profile-lede { max-width: 620px; }

/* The read-only note is a tinted box with an info icon in the design (13px pad, radius 6, 12px text at
   #3B3452 on #EFEDF5, 10.06:1). Plain muted text before. No bottom margin: the card pads it. */
.profile-grid .card-note {
    display: flex;
    gap: 9px;
    margin: var(--space-4) 0 0;
    padding: 13px;
    background: var(--surface-sunken);
    border-radius: 6px;
    color: var(--text-body);
    font: var(--fw-regular) var(--text-xs)/1.55 var(--font-body);
}
.profile-grid .card-note .note-icon { flex: none; width: 15px; height: 15px; margin-top: 1px; color: var(--text-muted); }

/* Card title: 14px line box in the design, 18.2px here. Four px per card, and every row below moves. */
.profile-grid .card-title { line-height: 1; }

/* Label to box: 6px in the design, 8px here. Same for the read-only email box. */
.profile-grid .field > label > input,
.profile-grid .readonly-value { margin-top: 6px; }

/* Space between fields is 14px in the design (16px here), and the buttons sit 20px under the last field
   (14 + 6). */
.profile-grid .stack-form { gap: 14px; }
.profile-grid .stack-form .button-row { margin-top: 6px; }

/* The read-only email is a 44px box with 14px side padding, like the design's locked field (47px and
   16px before). Text stays --text-muted (5.91:1 on the sunken fill; the design's #7C7691 is 3.73:1). */
.profile-grid .readonly-value {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 0 14px;
}

/* Field hints are 11.5px with 6px above in the design (14px and 4px here). Colour stays --text-muted:
   the design's #A9A2BE is 2.44:1 on white, under the 4.5:1 floor. */
.profile-grid .field-hint {
    margin-top: 6px;
    font-size: 11.5px;
    line-height: 1.4;
}

/* Space between Save and Change password: 10px in the design, 12px here. */
.profile-grid .button-row { gap: 10px; }

/* Fact rows: 19.6px line box (1.4) in the design, 21px here, so each row is 45px not 46px. */
.profile-grid .fact-list { line-height: 1.4; }

/* Module pills on this page are 12px/600, 22px tall in the design. Pass 3 kept ONE 11px pill for the
   whole Portal because the admin pills are 11px in the design; the design itself uses 12px on the
   account pages, so the size is set here only and the admin pills are left alone. Colours unchanged
   (--info on --info-bg, 7.17:1). */
.profile-grid .pill {
    padding: 5px 10px;
    font-size: 12px;
    line-height: 12px;
    letter-spacing: 0.01em;
}

/* The last row of a list has no rule under it in the design. */
.profile-grid .fact-list dt:nth-last-of-type(1),
.profile-grid .fact-list dd:nth-last-of-type(1) { border-bottom: 0; }

/* ===== Parity pass 4: account-password ===== */

/* The sentence under the heading is one line in the design, so the cards start at y=211.
   The 62ch cap on .admin-sub wrapped it and pushed both cards down by a line. */
.password-lede { max-width: none; }

/* The two-factor card, Off state. The design always shows the authenticator method, and when
   two-factor is off it adds a warning row with the way in. The warning text takes --warning
   (5.25:1 on --warning-bg), not the design's #9A6410 (4.42:1): same call as divergence 2. */
.cards-password .tfa-method {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 13px 14px;
    margin-bottom: 18px;
    background: var(--surface-card);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-md);
}
.cards-password .tfa-method-on { background: var(--violet-50); border-color: var(--brand-primary); }
.cards-password .tfa-method-icon { flex: none; width: 17px; height: 17px; color: var(--text-faint); }
.cards-password .tfa-method-on .tfa-method-icon { color: var(--brand-primary); }
.cards-password .tfa-method-text { flex: 1; min-width: 0; }
.cards-password .tfa-method-title { display: block; font: var(--fw-semibold) var(--text-xs)/1.35 var(--font-body); color: var(--text-strong); }
.cards-password .tfa-method-sub { display: block; margin-top: 2px; font: var(--fw-regular) 11.5px/1.45 var(--font-body); color: var(--text-muted); }

.cards-password .tfa-warning {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    background: var(--warning-bg);
    border: var(--border-width) solid rgba(255, 193, 77, 0.55);
    border-radius: var(--radius-md);
}
.cards-password .tfa-warning-text { flex: 1; min-width: 200px; font: var(--fw-regular) var(--text-xs)/1.55 var(--font-body); color: var(--warning); }

/* The design's "sm" button: 32px tall, 14px text. */
.cards-password .btn-sm { min-height: 32px; padding: 0 14px; font-size: var(--text-sm); letter-spacing: 0.01em; }

/* The On/Off pill beside the card title: 12px text, 22px tall, and Off in the warning tone
   rather than grey, as the design draws it. --warning on --warning-bg is 5.25:1; the design's
   #B4741A on the same fill is 3.41:1. */
.cards-password .card-title .pill { padding: 5px 10px; font-size: var(--text-xs); line-height: 12px; letter-spacing: 0.01em; }
.cards-password .pill.pill-warning { background: var(--warning-bg); color: var(--warning); }

/* Two-factor card head: the design's title row is 10px between title and pill, 6px above the
   note, and the note is 16px above the next block. Margins collapse, so the note's own 16px
   top margin was winning over the title's 6px. */
.cards-password [data-panel="two-factor"] .card-title { gap: 10px; margin-bottom: 6px; line-height: 1; }
.cards-password .card-note { margin: 0 0 16px; line-height: 1.55; }

/* Change-password card title: the design's is 14px on a line-height of 1 (14px tall), 16px above
   the first field. The shared .card-title is 1.3, which put the fields 4px low. */
.cards-password .card-title { line-height: 1; }

/* Field rhythm: 6px between a label and its box, 14px between fields (design pitch 85px, was 89px). */
.cards-password .stack-form { gap: 14px; }
.cards-password .field > label > input { margin-top: 6px; }

/* The design puts 18px between the last field and what follows. The strength meter is not built
   (U1), so the button takes that gap itself. */
.cards-password .stack-form > .btn { margin-top: 4px; }

/* The back link sat 1px low, and the heading and both cards followed it. It is inline-flex inside
   a 26px line box, so where it lands depends on the font's ascent: Manrope puts it at 93, the
   design at 92. As a block it lands where the stylesheet says (4px under the content edge), whatever
   the font, and the heading is at the design's y=124. */
.password-back { display: flex; width: fit-content; margin-top: 4px; }

/* ===== Parity pass 4: admin-overview ===== */

/* 1. The module table frame takes the same soft shadow as the design panels. Scoped by the
   .stat-row sibling, which only the overview renders, so the shared .table-scroll is untouched. */
.stat-row ~ .table-scroll { box-shadow: var(--shadow-xs); }

/* 2. Lede to tile row is 28px in the design (the Portal collapsed to 32). */
.stat-row { margin-top: 28px; }

/* 3. Tile figure: the design is 30px on a 30px line with no tracking (the Portal had 33px and -0.6px). */
.stat-value { line-height: 30px; letter-spacing: normal; }

/* 4. Label to figure is 10px in the design. The tile is a flex column with a 4px gap, so 8px of margin made 12. */
.stat-label { margin-bottom: 6px; }

/* 5. Tile note: 11.5px on a 16.1px line in the design (the Portal had 12px on 18px). */
.stat-note { font-size: 11.5px; line-height: 16.1px; }

/* ===== Parity pass 4: admin-users ===== */
/* Gap 1. The design's panel opens straight onto the column headings. The Portal's
   <caption> was a 54px text band above them. It stays in the page for screen readers
   (and for the flow checks that read it), drawn off the visual flow the same way
   .visually-hidden does it. Scoped to this page: three other admin pages share the rule. */
.users-scroll .admin-table caption {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* Gap 2. Column headings: the design sets them at the label tracking (0.14em, the same
   as the eyebrow), on a 14px/20px inset, with the line box the size of the type. */
.users-scroll .admin-table thead th {
    padding: 14px var(--space-4);
    line-height: 1;
    letter-spacing: var(--tracking-label);
}
.users-scroll .admin-table thead th:first-child { padding-left: var(--space-5); }

/* Gap 3. The first column starts 20px in, level with its heading and with the design.
   (The design's 14px row padding is not taken: the rows here are taller than its
   already, because two text buttons stack where it has icons, and 4px more per row
   moved the page further from the design, measured.) */
.users-scroll .admin-table tbody th:first-child { padding-left: var(--space-5); }

/* Gap 4. Row controls: the design's are 28px tall with a 6px radius. Text labels stay
   (one form per action, G7); the box around them takes the design's size and corner. */
.users-scroll .admin-table button,
.users-scroll .admin-table select {
    height: 28px;
    border-radius: var(--radius-sm);
}

/* ===== Parity pass 4: admin-clients ===== */

/* Fix 1. The design's panel starts at the column headers. The table caption is kept for screen
   readers and hidden from sight, so it no longer draws a 54px title strip above the header. */
.admin-clients .admin-table caption {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* Fix 2. Column headers: the design tracks them at 0.14em (1.47px) on a 10.5px line, not 0.04em. */
.admin-clients .admin-table thead th {
    letter-spacing: var(--tracking-label);
    line-height: 1;
}

/* Fix 3. Vertical cell padding is 14px in the design, and a body row is 64px tall (header 39.5px).
   The side padding stays at 16px for now: the row forms (input + button, pill + select + button)
   already fill the 1106px panel at 1440, and wider sides make them wrap. */
.admin-clients .admin-table th,
.admin-clients .admin-table td {
    padding-top: 14px;
    padding-bottom: 14px;
    vertical-align: middle;
}
.admin-clients .admin-table tbody tr { height: 64px; }

/* Fix 4. Row controls. The design's row buttons are 28px tall with a 6px radius; the Portal's were
   32px with 10px. Labels drop to 12px, the size of the design's cell text, which also gives the
   row forms room: header tracking from fix 2 made the module columns wider and the forms wrapped.
   The rename box keeps its 14px text, since it is typed into. */
.admin-clients .admin-table button,
.admin-clients .admin-table select,
.admin-clients .admin-table input[type="text"] {
    height: 28px;
    border-radius: 6px;
    vertical-align: middle;
}
.admin-clients .admin-table button,
.admin-clients .admin-table select {
    font-size: var(--text-xs);
    padding: 0 12px;
}

/* Fix 5. The eyebrow is 12px on a 12px line in the design (tracking 0.14em, so 1.68px), with 6px
   under it. The Portal's was 10.5px with 8px under it. Colour stays: D4. */
.admin-clients .eyebrow {
    font-size: 12px;
    margin-bottom: 6px;
}

/* Fix 6. The design sets the toolbar 28px under the lede; the Portal had 24px. */
.admin-clients .admin-toolbar { margin-top: 28px; }

/* Fix 7. The panel sits 16px under the toolbar in the design; the Portal had 24px. The bottom margin
   becomes 16px too (design: 16px between the panel and the footnote), so this is one rule. */
.admin-clients .table-scroll { margin: 16px 0; }

/* Fix 8. The footnote under the panel is 11.5px on 17.25px in the design; the Portal's notes were
   14px on 21px. 12px on 18px is the nearest size token. The design's colour (neutral-400) is 2.3:1
   on the page and stays out of reach: the notes keep --text-muted. */
.admin-clients .admin-note {
    font-size: var(--text-xs);
    line-height: 1.5;
}

/* Fix 9. The first and last cells sit 20px in from the panel edge in the design (16px before). */
.admin-clients .admin-table th:first-child,
.admin-clients .admin-table td:first-child { padding-left: 20px; }
.admin-clients .admin-table th:last-child,
.admin-clients .admin-table td:last-child { padding-right: 20px; }

/* Fix 10. Phones. iOS Safari zooms into any field set under 16px (F2). The new-client box and the
   row rename box and state select were 14px and 12px at 390. */
@media (max-width: 900px) {
    .admin-clients .inline-form input[type="text"],
    .admin-clients .admin-table input[type="text"],
    .admin-clients .admin-table select { font-size: 16px; }
}

/* ===== Parity pass 4: admin-requests ===== */

/* Scoped by the row attributes only this page renders (tr[data-request], tr[data-enquiry]),
   so the shared .admin-table rules above stay as they are for the other admin screens. */

/* The design sets the name strong and everything under it as a quiet detail line:
   12px, regular, muted. The shared table cell is 500 in body ink. */
tr[data-request] td,
tr[data-enquiry] td { font-weight: var(--fw-regular); color: var(--text-muted); }

/* When it was asked is a quiet note in the design, not the strongest thing in the row:
   11.5px, regular. The design's own colour (#A9A2BE) is 2.3:1 on white, under the 4.5:1
   floor, so it stays --text-muted here. The shared rule makes it 600 in strong ink. */
tr[data-request] td time,
tr[data-enquiry] td time { font-size: 11.5px; font-weight: var(--fw-regular); color: var(--text-muted); }

/* The design sets a row's actions flush right, 8px apart. The shared table leaves them in
   the left of their column with only the inline whitespace between them. The wrapper is
   a flex row so the gap is exact, and the last column (header and cells) reads right. */
.row-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-1) var(--space-2); }
tr[data-request] td:last-child,
tr[data-enquiry] td:last-child,
.admin-table:has(tr[data-request]) thead th:last-child,
.admin-table:has(tr[data-enquiry]) thead th:last-child { text-align: right; }

/* The design's request row is a roomy 82px with 22px side padding, its text and its
   buttons on one centre line. The shared cell is 12px/16px and top-aligned, which makes
   a 57px row with the button hanging below the text. */
tr[data-request] > th,
tr[data-request] > td { padding-top: var(--space-6); padding-bottom: var(--space-6); vertical-align: middle; }
tr[data-request] > :first-child,
.admin-table:has(tr[data-request]) thead th:first-child { padding-left: 22px; }
tr[data-request] > :last-child,
.admin-table:has(tr[data-request]) thead th:last-child { padding-right: 22px; }

/* The design lifts its row buttons off the card: a hairline shadow on Decline and a
   slightly deeper one under the filled Approve. The shared admin button has none. */
tr[data-request] button,
tr[data-enquiry] button { box-shadow: var(--shadow-xs); }
tr[data-request] button.is-primary { box-shadow: var(--shadow-sm); }

/* The design's cards start 28px under the lede and carry a hairline shadow. The shared
   wrapper starts 24px under and has none. */
.table-scroll:has(tr[data-request]),
.table-scroll:has(tr[data-enquiry]) { margin-top: 28px; box-shadow: var(--shadow-xs); }
