/*
 * Mailbox MCP control panel: design tokens
 *
 * PORTED VERBATIM from the marketing site on 2026-08-26, which is UPSTREAM.
 *   E:/Development/Websites/mailbox-mcp.com/website/assets/css/main.css
 *
 * The two halves of this product must look like one product, so this file is a
 * COPY rather than a fork. Do not edit a value here to suit a panel screen. If
 * the panel genuinely needs a token the site does not have, add it in the
 * clearly marked panel block at the BOTTOM of this file, never by changing a
 * value above it - otherwise the day somebody diffs the two files, every real
 * divergence is buried in noise.
 *
 * Colour values are generated and contrast-verified by tools/palette.py.
 * A hex edited straight into this file is a colour nobody measured.
 *
 * The panel needs a denser type tier than a marketing page does. It is in the
 * PANEL BLOCK at the bottom of this file, and its reasoning is in
 * docs/design-plan.md section 2. Unlike the site's, it is FLAT rather than
 * fluid, which is what makes every line box an exact 4px multiple at every
 * width by construction rather than by derivation. If a later session makes a
 * panel size fluid, that leading has to be re-derived with the procedure at
 *   E:/Development/Websites/_playbook/SPEC-2026-07-31-baseline-grid-design.md
 * section 4.2. Never hand-tune one.
 */

/* ==========================================================================
   1. Design tokens
   ========================================================================== */

:root {
  /* ---- Grid units -------------------------------------------------------
     Two units. 8 is a multiple of 4, which is the entire arithmetic argument
     for why they do not fight: every accumulated offset down the page stays a
     4px multiple.
       --mbx-grid        line boxes. Every line-height is a multiple.
       --mbx-grid-block  block flow. Every margin-block, padding-block and flow
                        gap inside a .rhythm scope is a multiple.
     The 4px sub-grid survives for spacing INSIDE a line: icon gaps, border
     offsets, chip padding, a form label to its field. Those do not move
     baselines. A 4px value on a block-level margin is a bug. */
  --mbx-grid: 0.25rem;         /* 4px */
  --mbx-grid-block: 0.5rem;    /* 8px */

  /* ---- Type scale -------------------------------------------------------
     font-size stays fluid. line-height is a stepped absolute rem equal to an
     exact 4px multiple at EVERY width, not at two pinned ends. A fluid 16.87px
     glyph inside a fixed 24px line box is completely fine, and it is what lets
     the grid exist at 768px instead of only at 390px and 1600px.

     Tier breakpoints 640px and 960px. Every clamp is derived to top out at
     960px so the top tier is static and trivially verifiable. The coefficients
     carry three decimals, so the true ceilings land between 957.3px and
     961.8px rather than on 960px exactly; the worst residue is 0.0089px of
     font-size, which is far below a device pixel and cannot reach the grid at
     all, because line-height is an absolute rem and never derived from the
     font-size. Not worth chasing to more decimal places. Display and h1 step in BOTH
     axes, because a narrow ratio band cannot survive a wide fluid range under
     one leading value, and because large headings are where wrapping and orphan
     control matter most.

     Re-derive with the procedure in
     _playbook/SPEC-2026-07-31-baseline-grid-design.md section 4.2 whenever a
     font-size clamp changes. Never hand-tune. */
  /* Mono and label sizes. TWO of them, and the rule is what they are FOR
     rather than how big they are, because "pick something small" is what
     produced ten different literals across the components.

       micro  UPPERCASE labels and eyebrows. Never a sentence. Always tracked.
       mono   mono used as CONTENT: measured values, code, numbers, quoted
              error strings. Sentence case or verbatim.

     Anything larger than these is not mono, it is text, and it takes the
     ordinary scale. Adding a third size here needs a reason written beside it. */
  --mbx-text-micro:   0.6875rem;                                     /* 11 flat */
  --mbx-text-mono:    0.8125rem;                                     /* 13 flat */
  --mbx-lh-micro:     1rem;                                          /* 16 */
  --mbx-lh-mono:      1.25rem;                                       /* 20 */

  --mbx-text-sm:      0.875rem;                                      /* 14 flat */
  --mbx-text-base:    clamp(1rem, 0.914rem + 0.351vw, 1.125rem);     /* 16 -> 18 */
  --mbx-text-md:      1.125rem;                                      /* 18 flat, h5/h6 */
  --mbx-text-lg:      clamp(1.125rem, 1.040rem + 0.351vw, 1.25rem);  /* 18 -> 20, h4 */
  --mbx-text-xl:      clamp(1.25rem, 1.079rem + 0.702vw, 1.5rem);    /* 20 -> 24, h3 */
  --mbx-text-2xl:     clamp(1.5rem, 1.243rem + 1.053vw, 1.875rem);   /* 24 -> 30, h2 */
  --mbx-text-3xl:     2rem;                                          /* 32, h1, steps below */
  --mbx-text-display: 2.25rem;                                       /* 36, steps below */

  /* Leading, tier 1 (below 640px). Steps live in the media queries below the
     :root block. Every value here is an exact 4px multiple. */
  --mbx-lh-sm:      1.25rem;   /* 20 */
  --mbx-lh-base:    1.5rem;    /* 24 */
  --mbx-lh-md:      1.5rem;    /* 24 */
  --mbx-lh-lg:      1.5rem;    /* 24 */
  --mbx-lh-xl:      1.75rem;   /* 28 */
  --mbx-lh-2xl:     1.75rem;   /* 28 */
  --mbx-lh-3xl:     2.25rem;   /* 36 */
  --mbx-lh-display: 2.5rem;    /* 40 */
  --mbx-lh-prose:   1.5rem;    /* 24 */

  /* Spacing scale: 4px base unit.
     Every margin, padding, gap and offset comes from here. Steps at and
     above space-4 are multiples of 8; space-1 to space-3 are the 4px
     sub-grid for tight internal spacing, icons and borders. A literal
     length on a spacing property is a bug unless it is a hairline. */
  --mbx-space-1: 0.25rem;
  --mbx-space-2: 0.5rem;
  --mbx-space-3: 0.75rem;
  --mbx-space-4: 1rem;
  --mbx-space-5: 1.5rem;
  --mbx-space-6: 2rem;
  --mbx-space-7: 3rem;
  --mbx-space-8: 4rem;
  --mbx-space-9: 6rem;

  /* ======================================================================
     TIER 1: PRIMITIVES
     Raw ramps, named for what they are. THIS IS THE ONLY PLACE IN THIS
     PROJECT A COLOUR VALUE IS WRITTEN. Nothing below this block, and no page
     stylesheet or component, may introduce one.

     Every value here is generated and contrast-verified by tools/palette.py.
     Change a colour THERE, re-run it to exit 0, then copy the result here.
     Editing a hex in this file without re-running the gate is how a site ends
     up with unmeasured contrast.

     ONE hue, deliberately. Owner ruling 2026-08-25: the accent is #2162e0,
     which measures oklch(53.4% 0.203 261.8). That is 2.2 degrees off the navy
     already in use, so it is not a second hue. It is the same hue taken to
     high chroma at mid lightness.
        262  navy/blue   ink is the quiet end, accent-500 the loud end
     Anchored on, and both present here exactly:
        Ink #11224A = oklch(26.3% 0.078 264)   Accent #2262E0 = oklch(53.4% 0.203 262)

     Warmth comes only from the paper now, which is why the surfaces keep hue
     85 and must not be neutralised. Warm paper against a cool blue is what
     keeps this reading as a document rather than as another blue SaaS page.
     ====================================================================== */

  /* surfaces: warm document stock, cool ink. Paper is the MIDDLE step. A
     raised surface has to differ from its ground in LIGHTNESS, not only in
     shadow, and there is nowhere above 99.8% to go, so the ladder runs
     sunken -> paper -> surface, about 3.3% apart at each step. */
  --mbx-c-sunken:        #EBE8E3;
  --mbx-c-paper:         #F5F3EF;
  --mbx-c-surface:       #FFFEFE;

  /* navy */
  --mbx-c-ink:           #11224A;
  --mbx-c-ink-muted:     #495772;
  --mbx-c-ink-soft:      #5F697D;
  --mbx-c-line:          #D7DBE3;
  --mbx-c-line-strong:   #7D8697;
  --mbx-c-line-warm:     #DED7C8;

  /* blue. Unlike the amber this replaced, accent-500 is dark enough to be
     BOTH a fill and text: 4.88:1 on paper as text, 5.41:1 carrying white as a
     fill. The amber could only ever do one of those.

     accent-300 is not decoration and is not optional. The band is navy at hue
     264 and the accent is hue 262, so accent-500 inside the band measures
     3.46:1 and vanishes. Darkening the band does not rescue it: solving for
     4.5:1 puts the band at pure black. Anything accent-coloured on band or
     band-raised uses accent-300, which measures 9.77:1 there.

     Putting accent-500 in the band is the one error the gate cannot catch for
     you at a glance, because on a bright monitor it looks fine. */
  --mbx-c-accent-050:    #F3F8FF;   /* callout ground */
  --mbx-c-accent-100:    #E2EFFF;
  --mbx-c-accent-200:    #B6D5FF;
  --mbx-c-accent-300:    #91BDFF;   /* DARK GROUNDS ONLY */
  --mbx-c-accent-500:    #2262E0;   /* the signal, as given */
  --mbx-c-accent-600:    #114EC6;
  --mbx-c-accent-700:    #0D45B0;
  --mbx-c-white:         #FFFFFF;   /* label ON the accent fill; true white,
                                       because warm reads as dirt on blue */

  /* the inverted band, declared in full before anything needs it. A band with
     a background token and nothing else is exactly where white-at-a-dozen-
     alphas starts. */
  --mbx-c-band:          #06112B;
  --mbx-c-band-raised:   #12203F;
  --mbx-c-on-band:       #F7F5F1;
  --mbx-c-on-band-muted: #A9B1C2;
  --mbx-c-on-band-line:  #2A354D;

  /* state: a triad each, 100 fill / 200 border / 700 text. The border is the
     one that gets forgotten, and then every callout invents its own.
     THERE IS STILL NO SEPARATE WARNING HUE, now for a different reason. Amber
     used to be this brand's "pay attention" so a warning hue would have
     collided with it; amber is gone and the slot is free, and it is still not
     being filled. A note is informational and takes the accent triad; a real
     "this will fail" caution takes the danger triad. The setup guides need
     exactly those two registers and no more. */
  --mbx-c-success-100:   #E3F8E9;
  --mbx-c-success-200:   #A7E1BA;
  --mbx-c-success-700:   #0B703F;
  --mbx-c-danger-050:    #FFF7F5;   /* callout ground */
  --mbx-c-danger-100:    #FFEBE7;
  --mbx-c-danger-200:    #FFC2B9;
  --mbx-c-danger-700:    #A9231E;

  /* ======================================================================
     TIER 2: SEMANTIC
     Named for the role, pointing only at tier 1. Components read THIS tier
     and only this tier.
     ====================================================================== */

  --mbx-bg:              var(--mbx-c-paper);
  --mbx-bg-sunken:       var(--mbx-c-sunken);
  --mbx-bg-raised:       var(--mbx-c-surface);

  --mbx-ink:             var(--mbx-c-ink);
  --mbx-ink-muted:       var(--mbx-c-ink-muted);
  --mbx-ink-soft:        var(--mbx-c-ink-soft);

  --mbx-border:          var(--mbx-c-line);          /* decorative hairline */
  --mbx-border-warm:     var(--mbx-c-line-warm);     /* hairline on a warm surface */
  --mbx-border-control:  var(--mbx-c-line-strong);   /* input and control edge, 3:1 */

  /* --mbx-accent both fills and reads, which the amber could not. --mbx-accent
     -ink still exists and is still the one to reach for at SMALL sizes and on
     the sunken well, where accent-500 drops to 4.42:1. Display sizes and body
     text on paper or a card take --mbx-accent directly. */
  --mbx-accent:          var(--mbx-c-accent-500);
  --mbx-accent-hover:    var(--mbx-c-accent-600);
  --mbx-accent-ink:      var(--mbx-c-accent-700);
  --mbx-accent-soft:     var(--mbx-c-accent-100);    /* tint fill */
  --mbx-accent-soft-border: var(--mbx-c-accent-200);
  --mbx-on-accent:       var(--mbx-c-white);         /* label ON the accent fill */

  /* Links are navy and carry an accent underline, so the accent is the
     interaction rather than the thing being read. That keeps WCAG 1.4.1
     satisfied by the underline, not by the colour, and it means links need no
     second token per background. */
  --mbx-link:            var(--mbx-c-ink);
  --mbx-link-underline:  var(--mbx-c-accent-500);
  --mbx-link-hover:      var(--mbx-c-accent-700);

  /* the inverted band */
  --mbx-band:            var(--mbx-c-band);
  --mbx-band-raised:     var(--mbx-c-band-raised);
  --mbx-on-band:         var(--mbx-c-on-band);
  --mbx-on-band-muted:   var(--mbx-c-on-band-muted);
  --mbx-on-band-border:  var(--mbx-c-on-band-line);
  --mbx-on-band-accent:  var(--mbx-c-accent-300);    /* 9.77:1 on the band.
                                                        NOT accent-500: 3.46:1 */

  /* state */
  /* A CALLOUT ground and an inline FILL are different jobs and now have
     different tokens. The 100 steps stay for inline fills, where a small patch
     of colour is the point; the 050 steps are for callouts, where a large
     block of it is not. The BAR is what carries the meaning. */
  --mbx-note-bg:         var(--mbx-c-accent-050);
  --mbx-note-fill:       var(--mbx-c-accent-100);
  --mbx-note-border:     var(--mbx-c-accent-200);
  --mbx-note-bar:        var(--mbx-c-accent-500);
  --mbx-note-ink:        var(--mbx-c-accent-700);
  --mbx-success-bg:      var(--mbx-c-success-100);
  --mbx-success-border:  var(--mbx-c-success-200);
  --mbx-success-ink:     var(--mbx-c-success-700);
  --mbx-danger-bg:       var(--mbx-c-danger-100);
  --mbx-caution-bg:      var(--mbx-c-danger-050);
  --mbx-danger-border:   var(--mbx-c-danger-200);
  --mbx-caution-bar:     var(--mbx-c-danger-700);
  --mbx-danger-ink:      var(--mbx-c-danger-700);

  /* focus, and the scrim. Both are slots that get retrofitted to a live site
     and are cheap to declare now. The focus ring is accent-700 because it must
     clear 3:1 on paper, on a card AND in a sunken well. Inside the band it
     flips to accent-300, for the same reason everything else does. */
  --mbx-focus:           var(--mbx-c-accent-700);
  --mbx-focus-on-band:   var(--mbx-c-accent-300);
  --mbx-focus-width:     2px;
  --mbx-focus-offset:    2px;
  --mbx-scrim:           color-mix(in oklch, var(--mbx-c-band) 88%, transparent);

  /* ======================================================================
     TIER 3: COMPONENT
     One knob for one component that no semantic token covers. Reaching for
     this tier repeatedly means tier 2 is too thin. There are two, and both
     belong to the ledger, which is this site's signature element.
     ====================================================================== */

  /* The ledger: a claim on the left, the evidence for it on the right, joined
     by a dotted leader. It carries the hero, the proof section, the pricing
     table and the "what you will need" block in each setup guide. The leader
     is the only dotted rule on the site, so it gets its own token rather than
     being reinvented per page. */
  --mbx-ledger-leader:   var(--mbx-c-line-strong);
  --mbx-ledger-bg:       var(--mbx-c-sunken);
  /* The leader is drawn ON --mbx-ledger-bg, where the decorative --mbx-border
     measures 1.1:1 and is simply not there. It points at the strong line
     instead. A dotted rule reads far lighter than its own colour suggests,
     so this lands as a delicate leader rather than a heavy rule.
     The leader IS the row divider. Do not also give ledger rows a border:
     two rules doing one job is what made the first render look like a table
     with a decorative artefact in it. */

  /* ---- Typography ------------------------------------------------------
     Three families, four files, 85 KB total, all self-hosted latin subsets.
     The weight list is a BUDGET, not a starting point: every weight is a
     separate download on first visit.

       Bricolage Grotesque  display only, 700. Utilitarian signage with
                            opinions. Carries an optical-size axis (12-96),
                            so font-optical-sizing does real work here.
       Hanken Grotesk       body and UI, 400 and 600. Variable 100-900 in one
                            file, so both weights cost one download.
       Martian Mono         data and eyebrows only, 400. This is the ledger's
                            face and the site's character.

     Mono is a spice everywhere except the ledger, where it IS the content.
     It must never reach buttons, nav or body links: that is the difference
     between a page that looks technical and one that looks unfinished. */
  --mbx-font-display: "Bricolage Grotesque", "Hanken Grotesk", ui-sans-serif,
                      system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mbx-font-body:    "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system,
                      "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mbx-font-mono:    "Martian Mono", ui-monospace, SFMono-Regular, Menlo,
                      Consolas, "Liberation Mono", monospace;

  /* Tracking. letter-spacing does not touch the baseline grid, so these are
     free parameters in a way the size and leading tokens are not. Display
     type tightens: the measured range across sites that look designed is
     -0.02em to -0.04em, and letter-spacing:normal on a 56px heading is the
     tell that nobody set it. */
  --mbx-track-display: -0.035em;
  --mbx-track-heading: -0.022em;
  --mbx-track-body:     0;
  --mbx-track-eyebrow:  0.08em;
  --mbx-track-mono:    -0.02em;   /* Martian Mono is wide by design */

  /* ---- Shape -----------------------------------------------------------
     A scale, not one global radius. Radius is a decision per element: the
     same corner on a chip, a card and an image is the tell of a template. */
  --mbx-radius-xs:   0.25rem;   /* 4  chips, ledger leader caps, small marks */
  --mbx-radius-sm:   0.375rem;  /* 6  buttons, inputs */
  --mbx-radius-md:   0.625rem;  /* 10 cards */
  --mbx-radius-lg:   1rem;      /* 16 panels, the ledger, the logo tile */
  --mbx-radius-xl:   1.5rem;    /* 24 THE FLOATING NAV, hero cards, big surfaces.
                                   Added 2026-08-25 from the visa-ltd review: our
                                   top end was too timid against their 40-56. This
                                   is NOT a raise of the whole scale. Radius stays
                                   a decision per element; a 24 button would look
                                   like a mistake. */
  --mbx-radius-pill: 62.5rem;   /* badges only */
  --mbx-radius:      var(--mbx-radius-sm);   /* scaffold alias, buttons/inputs */

  /* ---- Elevation -------------------------------------------------------
     Layered and tinted toward the navy, never pure black: a black shadow over
     a warm paper reads muddy. One flat shadow reads as a sticker; two reads
     as a physical surface. Elevation is semantic: 1 rests, 2 lifts, 3 floats.
     Every component picks one rather than inventing its own. */
  --mbx-shadow-1: 0 1px 2px rgb(17 34 74 / 0.05),
                  0 1px 1px rgb(17 34 74 / 0.04);
  --mbx-shadow-2: 0 2px 4px rgb(17 34 74 / 0.05),
                  0 4px 12px rgb(17 34 74 / 0.07);
  --mbx-shadow-3: 0 4px 8px rgb(17 34 74 / 0.05),
                  0 12px 32px rgb(17 34 74 / 0.10);
  --mbx-edge-light: inset 0 1px 0 rgb(255 255 255 / 0.9);

  /* ---- Containers ------------------------------------------------------
     More than one width, declared now. One --container looks sufficient right
     up until a page wants a middle measure and someone types 1000px.
     Wide by house convention: the container stays wide, the PROSE inside it
     is what gets a measure. A wide page is fine; a wide paragraph is not. */
  /* ONE content width, used by the nav, every section on every page, and the
     footer. Every vertical edge on the site therefore lines up, which is the
     whole point: a width that changes down a page reads as accidental unless
     the reader can infer why, and on a marketing site they cannot.

     Owner feedback 2026-08-25: three widths were in use (1380 / 1000 / 780),
     mixed within single pages. 1240 is the value that serves all of it: the
     hero's 7fr/5fr split still has room for the ledger, three provider cards
     still clear a readable measure, and prose is capped by --mbx-measure
     rather than by the container.

     The two below are kept as TOKENS because a future page may genuinely need
     one, but nothing uses them today and using one needs a reason written next
     to it. The measure goes on the TEXT, never on the container. */
  --mbx-container:        77.5rem;   /* 1240, everything */
  --mbx-container-mid:    62.5rem;   /* 1000, unused: needs a stated reason */
  --mbx-container-narrow: 48.75rem;  /* 780,  unused: needs a stated reason */
  --mbx-gutter:           var(--mbx-space-5);

  /* ---- Motion ----------------------------------------------------------
     Two durations and one easing. Anything animated uses transform/opacity
     only. prefers-reduced-motion is honoured in the reset below. */
  --mbx-ease:      cubic-bezier(0.22, 0.61, 0.36, 1);
  --mbx-dur-fast:  120ms;
  --mbx-dur-base:  260ms;

  /* Readable line length. ONE measure, and it is an ABSOLUTE length rather
     than ch.

     OWNER 2026-08-25: "I don't understand why the content width is different
     for some things, leaving a blank area on the right... I just don't get the
     widths of the text content." Measured on the page, he was looking at two
     different right edges 56px apart, and there was no reason for it.

     THE CAUSE WAS THE UNIT, not the numbers. `ch` resolves against the
     ELEMENT'S OWN font size, so one token per role gave one edge per role:
     58ch on a 20px lead landed at 650px and 70ch on 18px body landed at 706px.
     Two caps that were meant to read as one system arrived 56px apart, which
     is the worst of both: too close to look deliberate, too far apart to look
     identical. Any future ch-based measure reintroduces exactly this.

     44rem is 704px, which is 70 characters at 18px body and 63 at the 20px
     lead. Both sit inside the checker's 45-85 window, so one number serves
     every size of running text AND every block ends on the same vertical line.

     WHY TEXT IS NARROWER THAN THE CONTAINER AT ALL: line length. At the full
     1176px a paragraph runs about 120 characters and the eye loses the start
     of the next line on the return sweep. Cards, grids and the hub diagram
     have no return sweep, so they take the full width. That is the difference
     the reader is actually seeing, and it is the only width difference on the
     site that is meant to be there. */
  --mbx-measure: 44rem;

  /* The step marker column, as tokens rather than literals, because the step
     body derives its cap from them: a block indented past a marker has to lose
     exactly the indent from its measure or it ends further right than every
     other paragraph on the page. */
  --mbx-step-marker: 4rem;
  --mbx-step-gap:    2rem;
  --mbx-step-indent: calc(var(--mbx-step-marker) + var(--mbx-step-gap));
}

/* ---- Leading tiers ------------------------------------------------------
   Only body, h2, h1 and display need a step. h3, h4, h5, h6 and small text
   hold one 4px multiple across their whole fluid range and stay inside band,
   so they are absent here on purpose rather than by omission. */

@media (min-width: 40em) {          /* 640px */
  :root {
    --mbx-text-3xl:     2.375rem;    /* 38 */
    --mbx-text-display: 2.75rem;     /* 44 */
    --mbx-lh-base:      1.75rem;     /* 28 */
    --mbx-lh-2xl:       2rem;        /* 32 */
    --mbx-lh-3xl:       2.75rem;     /* 44 */
    --mbx-lh-display:   3rem;        /* 48 */
    --mbx-lh-prose:     1.75rem;     /* 28 */
  }
}

@media (min-width: 60em) {          /* 960px */
  :root {
    --mbx-text-3xl:     2.75rem;     /* 44 */
    --mbx-text-display: 3.5rem;      /* 56 */
    --mbx-lh-3xl:       3rem;        /* 48 */
    --mbx-lh-display:   3.75rem;     /* 60 */
    --mbx-lh-prose:     2rem;        /* 32 */
  }
}

@media (min-width: 80em) {          /* 1280px */
  :root {
    --mbx-text-display: 4rem;        /* 64 */
    --mbx-lh-display:   4.25rem;     /* 68 */
  }
}

/* ==========================================================================
   1b. Fonts
   Self-hosted latin subsets in assets/fonts/. Never loaded from the Google
   CDN: that costs an extra connection and a render-blocking round trip, and
   hands visitor IP addresses to a third party, which is a GDPR problem on a
   UK site.
   ========================================================================== */

/* Variable on the optical-size axis only; the weight is baked in. Browsers
   drive opsz from the rendered font-size on their own, which is exactly what
   a display face wants: tighter fit at 64px, more open at 20px. */
@font-face {
  font-family: "Bricolage Grotesque";
  src: url("/assets/fonts/bricolage-grotesque-var.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* One file covers 400 and 600, so the second weight is free. Declared across
   the full axis because that is what the file actually contains; the brief
   still commits the design to 400 and 600 only. */
@font-face {
  font-family: "Hanken Grotesk";
  src: url("/assets/fonts/hanken-grotesk-var.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Martian Mono";
  src: url("/assets/fonts/martian-mono-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ==========================================================================
   PANEL BLOCK
   Everything below this line belongs to the control panel and does NOT exist
   on the marketing site. Nothing above this line may be edited to suit a panel
   screen: the file above is a verbatim port, and keeping it that way is what
   makes a diff between the two projects show real divergence instead of noise.

   Reasoning for every value here is in docs/design-plan.md section 2.
   ========================================================================== */

:root {
  /* ---- The panel type scale ---------------------------------------------
     FLAT at every width, which is the one real deviation from the site.

     A control panel has no measure argument for growing its chrome on a big
     monitor: more room means more ROWS, not bigger ones, and a table row that
     changes height between 1280px and 1600px makes a baseline grid harder to
     hold rather than easier. The payoff is that every line box is an exact 4px
     multiple at EVERY width by construction. There is no clamp to solve and no
     tier to re-derive.

     Leading ratio falls as size rises, which is the rule: a heading is scanned
     in one pass, a paragraph is read line after line.

     Prose is the exception and keeps the site's fluid --mbx-text-base /
     --mbx-lh-base, because prose is read in long runs. Use --pnl-text-body for
     panel prose, which is the flat 16/24 step; use the inherited fluid tokens
     inside a .rhythm scope for anything genuinely long-form.

       11/16  1.45  micro    UPPERCASE labels, column heads, eyebrows
       13/20  1.54  mono     mono AS CONTENT: values, IDs, code, error strings
       13/20  1.54  sm       secondary UI: hints, timestamps, row metadata
       15/20  1.33  ui       THE DOMINANT SIZE: labels, cells, buttons, nav
       16/24  1.50  body     prose: standfirsts, help, empty states
       18/24  1.33  lg       card and section titles, h3
       22/28  1.27  xl       section headings, h2
       28/32  1.14  2xl      page title, h1
       36/40  1.11  display  sign-in, and the one big number on a stat

     TWO MONO SIZES AND NO MORE, same rule as the site: micro is for uppercase
     labels, mono is for mono used as content. Anything larger than 13px is not
     mono, it is text, and it takes the ordinary scale. A third needs a reason
     written beside it. */
  --pnl-text-micro:   0.6875rem;   /* 11 */
  --pnl-text-mono:    0.8125rem;   /* 13 */
  --pnl-text-sm:      0.8125rem;   /* 13 */
  --pnl-text-ui:      0.9375rem;   /* 15 */
  --pnl-text-body:    1rem;        /* 16 */
  --pnl-text-lg:      1.125rem;    /* 18 */
  --pnl-text-xl:      1.375rem;    /* 22 */
  --pnl-text-2xl:     1.75rem;     /* 28 */
  --pnl-text-display: 2.25rem;     /* 36 */

  --pnl-lh-micro:     1rem;        /* 16 */
  --pnl-lh-mono:      1.25rem;     /* 20 */
  --pnl-lh-sm:        1.25rem;     /* 20 */
  --pnl-lh-ui:        1.25rem;     /* 20 */
  --pnl-lh-body:      1.5rem;      /* 24 */
  --pnl-lh-lg:        1.5rem;      /* 24 */
  --pnl-lh-xl:        1.75rem;     /* 28 */
  --pnl-lh-2xl:       2rem;        /* 32 */
  --pnl-lh-display:   2.5rem;      /* 40 */

  /* ---- Element heights --------------------------------------------------
     All 8px multiples. CENTRE THE LINE BOX WITH FLEX ALIGNMENT, NEVER WITH
     COMPUTED PADDING: a 20px line box inside a 40px control wants 10px above
     and below, and 10 is not on the 4px sub-grid. Set the height, use
     align-items:center, and pad horizontally only. That keeps every element
     height an 8px multiple and every line box a 4px multiple with no
     arithmetic left to get wrong. */
  --pnl-control-h:    2.5rem;      /* 40  input, select, button */
  --pnl-control-sm-h: 2rem;        /* 32  small button, chip. Clears the WCAG
                                          2.5.8 24px target minimum. */
  --pnl-row-h:        3rem;        /* 48  table row, comfortable */
  --pnl-row-sm-h:     2.5rem;      /* 40  table row, compact */
  --pnl-nav-h:        2.5rem;      /* 40  sidebar nav item */
  --pnl-tab-h:        3rem;        /* 48  tab */
  --pnl-header-h:     6rem;        /* 96  page header block */

  /* ---- Layout -----------------------------------------------------------
     The sidebar is FIXED and does not collapse to an icon rail. An icon rail
     is a memory test, and 264px is not a width worth reclaiming.

     THE CONTENT COLUMN IS NOT CAPPED. Prose inside it is capped by the site's
     --mbx-measure (44rem), never by the container: cap what is READ in long
     runs, never what is SCANNED. A contents list, a table, a settings block
     and a data list have no return sweep to lose, and a cap only strands them
     in a container they were meant to fill.

     This carried a 67.5rem cap until the owner saw the mailbox list on a
     desktop monitor with a third of the screen empty beside it. The cap
     contradicted the paragraph above it: every page in this panel is a table,
     a ledger list or a chart, and all three want the width. Anything that is
     genuinely read in a long run already carries its own measure. */
  --pnl-sidebar:      16.5rem;     /* 264 */
  --pnl-gutter:       2rem;        /* 32  content column inset */
  --pnl-card-pad:     1.5rem;      /* 24 */
  /* A CEILING, NOT A COLUMN, AND THE DISTINCTION IS THE WHOLE HISTORY OF THIS
     LINE. Owner 2026-08-27: *"This looks awful on full width in my desktop
     (very wide) ... it needs to continue to look nice whatever the size of the
     viewport. I think that means it should have a maximum size for its
     container box."*

     That is not a reversal of his 2026-08-26 ruling, it is the other end of
     the same one. He objected then to a 67.5rem cap that left a third of a
     1440 monitor empty beside the mailbox list. `none` fixed that and, on a
     2560 monitor, produced the opposite fault: a 2,232px content column where
     the h1 and the standfirst run the full width, a chart card is twice the
     width of the chart in it, and a `.hint` capped at its own measure sits in
     a card with 1,500px of nothing to its right.

     100rem is chosen so BOTH complaints stay fixed. It is above every common
     desktop content width, so a 1440 or a 1920 is exactly as it was and the
     ruling that produced `none` is untouched; it only bites above that. It
     also matches the cap already on the chart SVG (pages/charts.css), which
     means a full-width chart card now ends where its drawing ends instead of
     leaving the empty half he screenshotted.

     There is a data argument for the same number: the activity chart holds at
     most 90 days, so past roughly this width the bars only get fatter and no
     more is shown. A chart is not more readable for being bigger. */
  --pnl-content-max:  100rem;      /* 1600 */

  /* ---- Charts -----------------------------------------------------------
     There is deliberately NO categorical palette. Categories are expressed by
     tint steps within the one hue, by position in a ranked list, or by small
     multiples. The single exception is pass/fail, which takes the state triads
     the system already has. See docs/design-plan.md section 3.1.

     THE CEILING LINE APPEARS ONLY AT MAILBOX LEVEL. There is no account-level
     ceiling, because the plan attaches to the mailbox, so an account has no
     limit of its own and drawing one would be inventing a number. */
  --pnl-chart-bar:      var(--mbx-c-accent-500);
  --pnl-chart-bar-soft: var(--mbx-c-accent-200);   /* read tools */
  --pnl-chart-bar-hard: var(--mbx-c-accent-700);   /* destructive tools */
  --pnl-chart-ceiling:  var(--mbx-c-line-strong);  /* 1px dashed, a reference
                                                      and not data. Must not
                                                      compete with the bars. */
  --pnl-chart-grid:     var(--mbx-c-line);
  --pnl-chart-label:    var(--mbx-c-ink-soft);

  /* ---- A third band step ------------------------------------------------
     A TIER-1 COLOUR VALUE, LIVING DOWN HERE, and the exception needs its
     reason written next to it. Everything above the PANEL BLOCK line is a
     verbatim port of the marketing site, so adding a primitive up there in the
     ramp where it belongs would put a real divergence into a file whose whole
     value is that a diff against upstream shows only real divergences. It goes
     here instead, and it is a candidate to push upstream rather than a
     permanent home.

     It is generated and gated exactly like every value above it: oklch(34%
     0.066 264), produced by tools/palette.py, which measures it against all
     three text tokens that appear on the band. It is not a hex somebody liked.

     WHAT IT IS FOR: the lit end of the soft radial behind the character on the
     sign-in screen, which the owner asked to be lighter than band-raised. A
     gradient is the one surface palette.py cannot check after the fact, so the
     construction carries the argument instead: the brightest pixel anywhere in
     that gradient is exactly this value, and this value is measured.

     L stops at 0.34 rather than the 0.38 the ratios would just allow. At 0.38
     on-band-muted lands on 4.50:1 with no headroom, and a colour sitting
     exactly on its threshold fails the first time anything near it moves. */
  --mbx-c-band-lift:    #26375A;
  --mbx-band-lift:      var(--mbx-c-band-lift);
}

/* ==========================================================================
   PANEL BASE
   Reset, element defaults, and the primitives every screen is built from.
   Component CSS goes in assets/css/pages/ and loads via $page_styles.
   ========================================================================== */

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

/* THE ONE !important IN THIS PROJECT, AND IT EARNS IT. The `hidden` attribute
   is implemented by the user agent as `[hidden] { display: none }`, which any
   author rule setting `display` beats on specificity. So `.btn { display:
   inline-flex }` silently defeats `<button hidden>`, and every button this
   project hides in markup and shows from script was visible the whole time.

   That is not cosmetic here. The copy buttons and the confirm dialog's opener
   are hidden precisely SO THAT they only appear once the script giving them
   meaning has loaded: a button that silently does nothing tells a customer the
   feature is broken rather than absent. Without this rule the entire
   progressive-enhancement argument in copy.js and confirm.js was a comment
   describing something that was not happening. */
[hidden] { display: none !important; }

body, h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd, ul, ol { margin: 0; }
ul[class], ol[class] { padding: 0; list-style: none; }

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

body {
  background: var(--mbx-bg);
  color: var(--mbx-ink);
  font-family: var(--mbx-font-body);
  font-size: var(--pnl-text-ui);
  line-height: var(--pnl-lh-ui);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  min-block-size: 100vh;
}

/* Under text-box-trim a MARGIN IS THE ENTIRE VISIBLE GAP: the half-leading
   that used to pad both sides of a text block is exactly what the trim
   removes, so a value carried over from untrimmed CSS renders far tighter
   than it used to. Roughly, 8px trimmed looks like 0px untrimmed. Choose
   spacing in a trimmed scope BY LOOKING AT IT: every numeric check passes a
   value that is visibly wrong. */
@supports (text-box-trim: trim-both) {
  h1, h2, h3, h4, p, .trim {
    text-box-trim: trim-both;
    text-box-edge: cap alphabetic;
  }
}

img, svg { display: block; max-inline-size: 100%; }

/* A HAIRLINE UNDERLINE IN THE ACCENT, not the browser's default rule in ink.
   This is what --mbx-link-underline was declared for and it had never actually
   been wired up, so every link was drawing a full-weight navy rule at the
   default thickness: the same colour and nearly the same weight as the text
   itself, which reads as emphasis rather than as a link.

   The system's rule, from the token block above: links are navy and carry an
   ACCENT underline, so the accent is the interaction and never the thing being
   read. WCAG 1.4.1 is satisfied by the underline existing at all rather than by
   its colour, so it is free to be quiet.

   DOTTED AT REST, SOLID ON HOVER, and the dotted rule is not a novelty: it is
   the ledger's leader. The dotted line joining a claim to its evidence is this
   brand's one signature rule, so making a link wear the same rule puts the two
   in the same visual language rather than giving the page two unrelated dotted
   things. It is also about as quiet as an underline can be while still being
   an underline, which is the point.

   1px flat rather than from-font: dotted at a face-supplied thickness renders
   as a row of squares rather than as a fine rule. A little more offset than
   the default so the dots sit clear of the descenders instead of through them.

   Hover goes solid AND takes currentColor, so the state change is a change of
   texture and of colour at once and does not depend on either alone. */
:where(a) {
  color: var(--mbx-link);
  text-decoration-line: underline;
  text-decoration-style: dotted;
  text-decoration-color: var(--mbx-link-underline);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.25em;
}
:where(a):hover {
  color: var(--mbx-link-hover);
  text-decoration-style: solid;
  text-decoration-color: currentColor;
}

/* Inside the band the accent underline would be accent-500 at 3.46:1, which is
   the one thing that is simply not present on this navy. Links there already
   take accent-300 for their colour, so the underline takes it too. */
.band :where(a) { text-decoration-color: currentColor; }

/* One focus style, everywhere, and it must clear 3:1 on paper, on a card AND
   in a sunken well. Inside the band it flips, for the same reason everything
   else does: accent-500 measures 3.46:1 there and simply is not present. */
:focus-visible {
  outline: var(--mbx-focus-width) solid var(--mbx-focus);
  outline-offset: var(--mbx-focus-offset);
  border-radius: var(--mbx-radius-xs);
}
.band :focus-visible { outline-color: var(--mbx-focus-on-band); }

.skip {
  position: absolute;
  inset-block-start: var(--mbx-space-2);
  inset-inline-start: var(--mbx-space-2);
  z-index: 100;
  padding: var(--mbx-space-2) var(--mbx-space-4);
  background: var(--mbx-bg-raised);
  border-radius: var(--mbx-radius-sm);
  box-shadow: var(--mbx-shadow-2);
  transform: translateY(-200%);
}
.skip:focus { transform: none; }

/* ---- Type primitives ---------------------------------------------------- */

/* A FLAT SCALE, IN THE BODY FACE. Owner 2026-08-26, on the page header:
   "This does not need to be a heading, normal bold text would be fine and the
   text below it could be smaller."

   The old scale was the MARKETING SITE'S, inherited wholesale: 28px display
   for the H1 and 22px display for a card title. That is right above a hero
   and wrong above a table. A panel is read by somebody who already knows what
   page they asked for, so the title's job is to confirm it, not to announce
   it, and 28px of Bricolage over a 15px data column is the single loudest
   thing on a screen whose content is the quiet part.

   THE DISPLAY FACE STAYS LOADED AND STAYS USED: the wordmark, the sign-in
   screen and the tile figures. It is no longer the default for every heading
   in the panel, which is the change. Bricolage at 18px is not a display face
   doing display work, it is a display face doing a label's job.

   The steps stay 4px multiples, so the grid gate is unaffected:
     h1  28/32 bold       the page title, IN THE HEADER BAND
     h2  16/24 semibold   a card title
     h3  15/20 semibold   a group inside a card

   H1 PUT BACK UP TO 28 ON 2026-08-27, and the two rulings do not conflict
   once you separate the size from the face and the surface. The owner sent
   the 365i panel that day: "Intuitive and clear. Nice design elements. Simple
   and beautiful." Its page title is large and heavy, and the page reads
   better for it, because it lives in a white masthead with a hard edge under
   it (see .page-band in app.css). Ours had no such zone, so the argument
   above -- that 28px over a 15px data column is the loudest thing on the
   screen -- was true of OUR page and not of theirs. The band is what settles
   it: a title in its own zone is not competing with the table.

   WHAT SURVIVES FROM 2026-08-26 IS THE PART THAT WAS ACTUALLY RIGHT. The
   owner objected to a marketing-site heading in the panel, and this is not
   one: 28px in the BODY FACE at 700, not 28px of Bricolage. The display face
   still does not set page titles. And the standfirst stays 15/20, smaller
   than the title, which is the other half of what he asked for.

   18 was an over-correction. It left the h1 smaller than the tile figures
   below it and no heavier than the mono in the table, so no page had an
   anchor at all. */
h1, .h1 {
  font-weight: 700;
  font-size: var(--pnl-text-2xl);
  line-height: var(--pnl-lh-2xl);
  letter-spacing: var(--mbx-track-heading);
}
h2, .h2 {
  font-weight: 600;
  font-size: var(--pnl-text-body);
  line-height: var(--pnl-lh-body);
  letter-spacing: var(--mbx-track-heading);
}
h3, .h3 {
  font-weight: 600;
  font-size: var(--pnl-text-ui);
  line-height: var(--pnl-lh-ui);
  letter-spacing: var(--mbx-track-heading);
}

.display {
  font-family: var(--mbx-font-display);
  font-weight: 700;
  font-size: var(--pnl-text-display);
  line-height: var(--pnl-lh-display);
  letter-spacing: var(--mbx-track-display);
  font-optical-sizing: auto;
  text-wrap: balance;              /* no single-word last line on a headline */
}

/* UPPERCASE labels and eyebrows. Never a sentence. Always tracked, because
   uppercase without tracking is the tell that nobody set it. */
.eyebrow {
  font-family: var(--mbx-font-mono);
  font-size: var(--pnl-text-micro);
  line-height: var(--pnl-lh-micro);
  letter-spacing: var(--mbx-track-eyebrow);
  text-transform: uppercase;
  color: var(--mbx-ink-soft);
}

/* Mono AS CONTENT: measured values, IDs, code, quoted error strings. Never
   buttons, nav or body links - that is the line between a page that looks
   technical and one that looks unfinished. */
.mono {
  font-family: var(--mbx-font-mono);
  font-size: var(--pnl-text-mono);
  line-height: var(--pnl-lh-mono);
  letter-spacing: var(--mbx-track-mono);
  font-variant-numeric: tabular-nums;
}

/* THE MEASURE GOES ON THE ELEMENT AND ON ITS CHILDREN, because .prose is used
   both ways: as a wrapper round several blocks, and directly on a single
   paragraph. Capping only the children left a lone <p class="prose"> running
   the full 1080 of the content column, which is about 150 characters and well
   past the point where the eye loses the start of the next line. */
.prose {
  font-size: var(--pnl-text-body);
  line-height: var(--pnl-lh-body);
  max-inline-size: var(--mbx-measure);
}
.prose > * { max-inline-size: var(--mbx-measure); }
.prose > * + * { margin-block-start: var(--mbx-space-4); }
.muted { color: var(--mbx-ink-muted); }
.soft  { color: var(--mbx-ink-soft); }

/* ---- Controls -----------------------------------------------------------
   CENTRE THE LINE BOX WITH FLEX, NEVER WITH COMPUTED PADDING. A 20px line box
   in a 40px control wants 10px above and below, and 10 is not on the 4px
   sub-grid. Set the height, align-items:center, pad horizontally only, and
   there is no arithmetic left to get wrong. */

.field { display: flex; flex-direction: column; gap: var(--mbx-space-2); }

/* ---- Two columns, where the fields are short -----------------------------
   A form of full-width inputs in a full-width card is a 1300px text box for a
   port number, which is the most obvious way a form says nobody laid it out.
   Every form on the reference panel is two columns and it is most of why
   theirs looks considered.

   THE PAIRING IS EDITORIAL, NOT AUTOMATIC. Reading server and sending server
   belong side by side because they are the same question asked twice; a
   password does not belong beside a username it is checked against. So this
   is a class a template opts into, and .field--wide takes a row back for
   anything long: a URL, a signature, a block of help.

   One breakpoint at 768. Below it a two-column form is two columns of nothing,
   and the fields are the full width of a phone. */
.fieldgrid { display: grid; gap: var(--mbx-space-5) var(--mbx-space-6); }

@media (min-width: 48em) {
  .fieldgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .fieldgrid > .field--wide,
  .fieldgrid > .fieldgrid__full { grid-column: 1 / -1; }
}

.label {
  font-weight: 600;
  font-size: var(--pnl-text-ui);
  line-height: var(--pnl-lh-ui);
}

/* Persistent, always visible. NEVER a placeholder: placeholder text disappears
   exactly when it is needed, fails contrast in most implementations, and is
   not a label. */
.hint {
  font-size: var(--pnl-text-sm);
  line-height: var(--pnl-lh-sm);
  color: var(--mbx-ink-muted);
  max-inline-size: var(--mbx-measure);
}

.input {
  block-size: var(--pnl-control-h);
  inline-size: 100%;
  padding-inline: var(--mbx-space-3);
  font: inherit;
  color: var(--mbx-ink);
  background: var(--mbx-bg-raised);
  border: 1px solid var(--mbx-border-control);
  border-radius: var(--mbx-radius-sm);
  transition: border-color var(--mbx-dur-fast) var(--mbx-ease);
}
.input:hover { border-color: var(--mbx-c-line-strong); }
.input:focus-visible { border-color: var(--mbx-accent); }

/* ---- Select --------------------------------------------------------------
   Everything .input has, plus the one thing a select needs and an input does
   not: room for the arrow, and an arrow worth looking at.

   THE ARROW IS A BACKGROUND IMAGE, NOT AN APPENDED ELEMENT. A <select> may not
   contain anything but <option>, so the usual trick is a wrapper with an
   absolutely positioned chevron, which then has to be told not to swallow the
   click. An inline SVG in the background needs no wrapper, no extra node and no
   pointer-events rule.

   IT IS URL-ENCODED, NOT BASE64. `data:image/svg+xml,` with percent-encoding
   stays readable in the diff, so the colour in it can be seen to match the
   token beside it. It is the one place in this stylesheet a colour is written
   as a literal, because a CSS custom property cannot be interpolated into a
   data URI; #5F697D is --mbx-c-ink-soft and the two must be changed together.

   appearance:none on all three prefixes, because Safari keeps its own control
   otherwise and the field ends up 4px shorter than every input beside it. */
.select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  block-size: var(--pnl-control-h);
  inline-size: 100%;
  padding-inline: var(--mbx-space-3) var(--mbx-space-7);
  font: inherit;
  color: var(--mbx-ink);
  background-color: var(--mbx-bg-raised);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' stroke='%235F697D' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--mbx-space-3) center;
  border: 1px solid var(--mbx-border-control);
  border-radius: var(--mbx-radius-sm);
  transition: border-color var(--mbx-dur-fast) var(--mbx-ease);
}
.select:hover { border-color: var(--mbx-c-line-strong); }
.select:focus-visible { border-color: var(--mbx-accent); }

/* ---- Textarea ------------------------------------------------------------
   NOT block-size, because a textarea is sized by `rows` and the caller knows
   how much room the answer needs. What it does take is a MINIMUM, so a template
   that forgets `rows` still gets a box somebody can write in rather than one
   line with a scrollbar.

   resize:vertical, never both. Horizontal resizing lets a customer drag the
   field wider than the column it sits in, which on this form breaks a two
   column grid on a page they are halfway through filling in. */
.textarea {
  display: block;
  inline-size: 100%;
  min-block-size: calc(var(--pnl-control-h) * 2);
  padding: var(--mbx-space-3);
  font: inherit;
  line-height: var(--pnl-lh-body);
  color: var(--mbx-ink);
  background: var(--mbx-bg-raised);
  border: 1px solid var(--mbx-border-control);
  border-radius: var(--mbx-radius-sm);
  resize: vertical;
  transition: border-color var(--mbx-dur-fast) var(--mbx-ease);
}
.textarea:hover { border-color: var(--mbx-c-line-strong); }
.textarea:focus-visible { border-color: var(--mbx-accent); }

.field--invalid .select,
.field--invalid .textarea { border-color: var(--mbx-danger-ink); }

.btn {
  block-size: var(--pnl-control-h);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--mbx-space-2);
  padding-inline: var(--mbx-space-5);
  font: inherit;
  font-weight: 600;
  border: 1px solid transparent;
  border-radius: var(--mbx-radius-sm);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--mbx-dur-fast) var(--mbx-ease),
              border-color var(--mbx-dur-fast) var(--mbx-ease);
}
.btn--primary {
  background: var(--mbx-accent);
  color: var(--mbx-on-accent);
}
.btn--primary:hover { background: var(--mbx-accent-hover); color: var(--mbx-on-accent); }

.btn--secondary {
  background: var(--mbx-bg-raised);
  color: var(--mbx-ink);
  border-color: var(--mbx-border-control);
}
.btn--secondary:hover { border-color: var(--mbx-c-line-strong); background: var(--mbx-bg); }

/* The third weight: an action that is available but is not the one being
   recommended. No fill and no border, so it reads as a way out rather than as
   an alternative of equal standing. It keeps the full 40px height, because a
   quiet button is still a target. */
.btn--quiet {
  background: none;
  color: var(--mbx-ink-muted);
  border-color: transparent;
}
.btn--quiet:hover { color: var(--mbx-ink); background: var(--mbx-bg-sunken); }

.btn--block { inline-size: 100%; }

/* ==========================================================================
   Waiting, which is a state and not an animation

   AN IMAP CONNECTION TEST TAKES TWO OR THREE SECONDS against a real server,
   and a button that looks unchanged for three seconds is a button somebody
   presses twice. The second press is a second connection to their mail
   provider, and on one that throttles failed logins that has a real cost. So
   the button is disabled, its label changes to the verb in the present tense,
   and it carries a mark that is visibly moving.

   THE SPINNER GOES IN THE GAP THE FLEX BOX ALREADY HAS. `.btn` is an
   inline-flex with `gap: 8px`, so a pseudo-element is laid out as a second
   flex item and the label simply shifts along - no absolute positioning, no
   reserved slot sitting empty the rest of the time, and no width change to
   make the row it sits in jump. The button does get wider by 24px while it
   works, which is honest: something is happening.

   A CONIC GRADIENT WITH A HOLE, not a border trick. `border-radius` plus a
   transparent border segment is the usual approach and it renders visibly
   lumpy at 14px in Chrome. This is one element, perfectly round at any size,
   and it inherits the button's own text colour so it works on the accent fill
   and on paper without a second rule.

   A FADING TAIL RATHER THAN A SHORT ARC. The first version drew a 60-degree
   segment, which at 14px is a dot: it turns, but a dot going round reads as a
   dot, not as motion, and the eye needs a moment to work out it is a spinner
   at all. A full sweep from transparent to solid gives the whole ring a
   direction, so it reads as turning from the first frame.
   ========================================================================== */

.btn.is-busy {
  /* Not `cursor: wait`: the pointer is not what is waiting, this button is,
     and a wait cursor over the whole button reads as the page having hung. */
  cursor: default;
}

.btn.is-busy::before {
  content: "";
  inline-size: 14px;
  block-size: 14px;
  flex: none;
  border-radius: 50%;
  background: conic-gradient(from 0deg, transparent 8%, currentColor);
  /* The hole. `mask` is safe here: every browser this panel supports has it
     unprefixed, and without it this is a filled disc rather than a ring.
     2px of stroke at 14px: thinner disappears against the label, thicker reads
     as a loading donut rather than as a control's own busy mark. */
  mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  animation: mbx-spin 720ms linear infinite;
}

@keyframes mbx-spin {
  to { transform: rotate(1turn); }
}

/* THE REGION UNDER IT GOES QUIET WHILE THE ANSWER IS FETCHED. Not hidden and
   not replaced with a skeleton: what is on screen is still true, it is simply
   about to be re-measured, and blanking it would lose the figure the customer
   is comparing against. Dimmed and made inert instead, so nothing there can be
   clicked in the second before it is swapped.

   `pointer-events: none` WITHOUT `inert` ON PURPOSE. `inert` also removes it
   from the accessibility tree, which would take the region out from under a
   screen reader mid-sentence; the live region announces the change instead. */
.is-waiting {
  opacity: 0.55;
  pointer-events: none;
  transition: opacity var(--mbx-dur-fast) var(--mbx-ease);
}

/* NOBODY NEEDS THE RING TO TURN. Reduced motion gets a mark that pulses in
   opacity instead: still visibly a state, with no rotation. The pulse is slow
   enough not to read as a flash, which is the accessibility failure a naive
   "just fade it" fix introduces. */
@media (prefers-reduced-motion: reduce) {
  .btn.is-busy::before { animation: mbx-pulse 1.4s ease-in-out infinite; }
  .is-waiting { transition: none; }
  @keyframes mbx-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.35; }
  }
}

/* A "Sign in with" button carrying the provider's own mark.
 *
 * THE MARK IS PINNED LEFT AND THE LABEL TAKES WHAT IS LEFT. Two stacked
 * provider buttons want their logos on one vertical line, so the pair scans as
 * a set rather than as two buttons that happen to be adjacent; that is what the
 * mark being first and `flex: none` gets, and it is why the label rather than
 * the group is what centres.
 *
 * IT USED TO PIN THE MARK WITH `position: absolute`, WHICH BROKE THE MOMENT THE
 * BUTTON WAS NOT FULL WIDTH. An absolutely positioned child is out of flow and
 * contributes no width, so on a shrink-to-fit button the label starts at the
 * same 24px inset the mark was pinned to and the logo lands on top of the first
 * two letters. It survived because both buttons that used it carried
 * `btn--block`; the add-mailbox step does not, and the owner found the Microsoft
 * mark sitting on "Co" in "Continue". Owner 2026-08-30: "This isn't right."
 *
 * In flow it cannot happen at any width: at full width the label still centres
 * in the space beside the mark, and on an auto-width button the two simply sit
 * next to each other with the button's own gap between them, which is what
 * every "Sign in with" button in the world looks like anyway. */
.btn--provider .provider-mark { flex: none; }
.btn--provider > span { flex: 1 1 auto; text-align: center; }

/* ---- The ledger ---------------------------------------------------------
   The signature: a claim on the left, the evidence that proves it on the
   right, joined by a dotted leader.

   THE LEADER IS THE ROW DIVIDER. Do not also give rows a border: two rules
   doing one job is what made the marketing site's first render look like a
   table with a decorative artefact in it.

   It only ever holds CHECKABLE FACTS. A ledger row containing an adjective has
   broken the device. */
/* CAPPED, AND THAT IS WHAT MAKES THE DEVICE WORK. A leader's job is to carry
   the eye from a claim to its evidence, which it does over a few centimetres
   and stops doing over twenty: on a wide window it became a dotted line across
   an empty field with a word at each end. 44rem is the site's measure and the
   claim and the value stay related. */
.ledger { display: grid; gap: var(--mbx-space-3); max-inline-size: 44rem; }

.ledger__row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: var(--mbx-space-3);
}
.ledger__claim { font-size: var(--pnl-text-ui); line-height: var(--pnl-lh-ui); }
.ledger__leader {
  border-block-end: 1px dotted var(--mbx-ledger-leader);
  transform: translateY(-0.25em);
}
.ledger__value {
  font-family: var(--mbx-font-mono);
  font-size: var(--pnl-text-mono);
  line-height: var(--pnl-lh-mono);
  letter-spacing: var(--mbx-track-mono);
}

/* Verified at 390px on the marketing site: the claim wraps and the leader then
   strands on the first line. Below md the ledger stacks and the leader is
   dropped entirely. Do not try to keep it on a narrow screen. */
@media (max-width: 39.99em) {
  .ledger__row { grid-template-columns: 1fr; gap: var(--mbx-space-1); }
  .ledger__leader { display: none; }
}

/* ---- The band ----------------------------------------------------------
   ANYTHING ACCENT-COLOURED IN HERE USES --mbx-on-band-accent. The band is navy
   at hue 264 and the accent is hue 262, so accent-500 measures 3.46:1 inside
   it and disappears. Darkening the band does not rescue it: solving for 4.5:1
   puts the band at pure black. On a bright monitor the wrong one looks fine,
   which is exactly why this is written down. */
.band {
  background: var(--mbx-band);
  color: var(--mbx-on-band);
}
.band .soft,
.band .muted,
.band .eyebrow { color: var(--mbx-on-band-muted); }
.band .ledger__leader { border-block-end-color: var(--mbx-on-band-border); }
.band .ledger__value  { color: var(--mbx-on-band-accent); }
.band a { color: var(--mbx-on-band-accent); }

/* ---- Motion -------------------------------------------------------------
   An app is not a marketing page. NO SCROLL REVEALS: animation on arrival is
   latency nobody asked for. Motion confirms an action, explains where a thing
   came from, or makes a change noticeable. transform and opacity only. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* The danger variant. The one action weight that is genuinely a warning: it
   takes the danger triad, which is the only place in this system a second hue
   appears, and it appears because something is about to stop working. */
.btn--danger {
  background: var(--mbx-danger-bg);
  color: var(--mbx-danger-ink);
  border-color: var(--mbx-danger-border);
}
.btn--danger:hover {
  background: var(--mbx-danger-ink);
  color: var(--mbx-c-white);
  border-color: var(--mbx-danger-ink);
}

/* A smaller control for a button that sits INSIDE a field row rather than
   beside a paragraph. 32 clears the WCAG 2.5.8 target minimum. */
.btn--small {
  block-size: var(--pnl-control-sm-h);
  padding-inline: var(--mbx-space-3);
  font-size: var(--pnl-text-sm);
}
