/* ============================================================
   primitives.css — the shared primitive layer
   ------------------------------------------------------------
   docs/plans/primitive-layer-design-2026-09-21.md.

   Imported by base.css and by admin-shell.css, after tokens.css
   and aliases.css, so it reaches all three bundles and sits
   below every bespoke sheet in the cascade.

   Three rules, and they are why this is not the layer that was
   retired on 2026-09-07 (design-system/README.md has the record):

   1. Nothing lands here without the commit also deleting every
      rule it replaces and moving the markup.
   2. Tokens only. No raw px font size, no raw px radius, no
      literal colour — scripts/css-duplication.mjs fails the
      build on one.
   3. Only custom properties that resolve in all three bundles:
      tokens.css, aliases.css, and --font-body. (Since 2026-09-30 the
      --fw-* weights live in tokens.css, so every bundle has them, and
      the type primitives read their --type-* role.)
   4. Everything here is in `@layer primitives`. Bespoke sheets are
      unlayered and win without a specificity contest; the element
      resets are in `@layer reset`, below this, so they cannot take
      a primitive's margin, border or background away.
   ============================================================ */

@layer primitives {
.shell-card,

  .directory-tile,

  .result-list,

  .shortlist-list,

  .att-results,

  .att-preview-card,

  .att-preview-detail,

  .ws-queue,

  .publish-item,

  .accepting-row,

  .blocked-item,

  .verify-row,

  .verify-maint-drift,

  .pipeline-list,

  .pipeline-side,

  .pipeline-note,

  .pipeline-status-card,

  .lc-event-card,

  .trust-hero,

  .logo-row,

  .holiday-card {
    --shell-card-pad: 0;
    --shell-card-radius: 0;
    --shell-card-shadow: none;
    --shell-card-face: transparent;
    --shell-card-line: transparent;
    --shell-card-border: 0;
    background: var(--shell-card-face);
    border: var(--shell-card-border, var(--border-width) solid var(--shell-card-line));
    border-radius: var(--shell-card-radius);
    padding: var(--shell-card-pad);
    box-shadow: var(--shell-card-shadow);
  }
  .shell-card {
    --shell-card-pad: var(--inset-2xl);
    --shell-card-radius: var(--card-radius);
    --shell-card-shadow: var(--shadow-sm);
    --shell-card-face: var(--surface);
    --shell-card-line: var(--border);
    --shell-card-border: var(--border-width) solid var(--shell-card-line);
  }
  .shell-card--flat { box-shadow: none; }
  /* A larger font or enlarged text must still fit a narrow shared card. */
  .shell-card-title { overflow-wrap: anywhere; }

  /* --- Eyebrow --------------------------------------------------------
     The small uppercase line above a heading. 14px at 0.06em, not 12px
     at 0.14em: wide tracking breaks word shapes, and the audience
     includes low vision (measured 2026-09-19 on the homepage hero).
     --fg-accent-sm and not --fg-accent, because small uppercase is
     small text by WCAG's measure and --fg-accent reads 4.22:1 on the
     page wash. It rebinds in dark mode, so there is no dark rule here.

     Margin is the context's business. The default suits a section
     head; a hero asks for --hero, and anything else overrides
     margin in its own sheet, one line. */
  .eyebrow {
    margin: 0 0 var(--gap-close);
    font-family: var(--type-eyebrow-family);
    font-size: var(--type-eyebrow-size);
    font-weight: var(--type-eyebrow-weight);
    letter-spacing: var(--type-eyebrow-tracking);
    text-transform: var(--type-eyebrow-transform);
    color: var(--fg-accent-sm);
  }
  .eyebrow--sm      { font-size: var(--type-overline-size); }
  .eyebrow--hero    { margin-bottom: var(--gap-group-md); }
  /* At --text-xs on a portal card surface this ink is the open
     color-contrast finding recorded in docs/audit/portal-editor.json
     (4 nodes) — inherited from the classes this replaced and not yet
     fixed. Check the reading before using it on a new surface. */
  .eyebrow--muted   { color: var(--text-muted); }
  .eyebrow--warn    { color: var(--warn-ink); }
  .eyebrow--danger  { color: var(--danger-text); }

  /* A leading hairline, in the text's own colour so a tone modifier
     carries it. A pseudo-element, not a span: it is decoration, and
     markup that exists only to be styled is how this had two
     implementations. The 1.5px thickness below is a literal because the
     scale has no step for it and the rules it replaced used that value;
     `.pill`, `.chip` and `.toast` carry the only other literals in this
     file, `1px solid` and the toast's 3px edge. */
  .eyebrow--rule {
    display: inline-flex;
    align-items: center;
    gap: var(--gap-related);
  }
  .eyebrow--rule::before {
    content: '';
    flex: none;
    width: var(--space-6);
    height: 1.5px;
    background: currentColor;
    border-radius: var(--radius-pill);
  }

  /* --- Buttons ----------------------------------------------------------
     One family. The variant classes work with or without .btn beside them:
     the portal and sign-in markup never carried it, the admin console always
     does. Sizes are the --btn-* tokens in aliases.css; every target is at
     least --target-min. Layout (margin, width, flex) is never set here. A
     context that needs it says so in its own sheet, unlayered, and may not
     restate the box. */
  .btn,
  .btn-primary,
  .btn-secondary,
  .btn-tertiary,
  .btn-danger {
    appearance: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--btn-icon-gap);
    min-height: var(--target-min);
    padding: var(--btn-pad-sm);
    /* The label role (aliases.css). */
    font-family: var(--type-label-family);
    font-size: var(--type-label-size);
    font-weight: var(--type-label-weight);
    letter-spacing: var(--type-label-tracking);
    line-height: var(--type-label-leading);
    text-decoration: none;
    border: var(--btn-border) solid transparent;
    border-radius: var(--button-radius);
    cursor: pointer;
    user-select: none;
    transition: background-color var(--motion-interaction-duration) var(--motion-interaction-ease),
                border-color     var(--motion-interaction-duration) var(--motion-interaction-ease),
                color            var(--motion-interaction-duration) var(--motion-interaction-ease),
                transform        var(--motion-interaction-duration) var(--motion-interaction-ease),
                box-shadow       var(--motion-interaction-duration) var(--motion-interaction-ease),
                opacity          var(--motion-interaction-duration) var(--motion-interaction-ease);
  }
  .btn svg,
  .btn-primary svg,
  .btn-secondary svg,
  .btn-tertiary svg,
  .btn-danger svg { flex-shrink: 0; }

  /* A button whose request is open is aria-disabled rather than disabled
     (lib/busy-button.js): a disabled button cannot keep the focus it was
     pressed with, and a request that failed left the keyboard on <body>. It is
     drawn exactly as :disabled is, so each :disabled selector in this family
     has an [aria-disabled="true"] twin, and each hover and active state
     excludes both. */
  .btn:disabled,
  .btn-primary:disabled,
  .btn-secondary:disabled,
  .btn-tertiary:disabled,
  .btn-danger:disabled,
  .btn[aria-disabled="true"],
  .btn-primary[aria-disabled="true"],
  .btn-secondary[aria-disabled="true"],
  .btn-tertiary[aria-disabled="true"],
  .btn-danger[aria-disabled="true"] { cursor: not-allowed; }

  /* The one filled call to action in a view — the nav's own .btn-primary is
     chrome, not a view, so it does not count against this (phase 3A review,
     docs/plans/deferred.md). */
  .btn-primary {
    min-height: var(--btn-h-md);
    padding: var(--btn-pad-lg);
    font-size: var(--text-base);
    font-weight: var(--type-label-weight);
    background: var(--cta);
    color: var(--cta-ink);
    box-shadow: var(--btn-shadow-cta);
  }
  .btn-primary:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--cta-hover);
  }
  .btn-primary:disabled,
  .btn-primary[aria-disabled="true"] {
    background: var(--surface-3);
    color: var(--text-muted);
    box-shadow: none;
  }
  /* Busy is said with aria-busy, so the style cannot disagree with what a
     screen reader is told. The spinner itself is the admin console's. */
  .btn-primary[aria-busy="true"] { opacity: var(--opacity-busy); cursor: progress; }

  /* A compact size for dense rows (the admin invites row's Send/Preview
     pair): still meets --target-min. Placed after .btn-primary so it wins
     at equal specificity. */
  .btn--sm,
  .btn-primary.btn--sm {
    min-height: var(--target-min);
    padding: var(--btn-pad-sm);
    font-size: var(--type-label-size);
  }

  .btn-secondary {
    background: var(--surface);
    color: var(--primary);
    border-color: var(--border);
  }
  .btn-secondary:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--primary-tint);
    border-color: var(--primary-tint);
    color: var(--primary-hover);
  }
  .btn-secondary:disabled,
  .btn-secondary[aria-disabled="true"] {
    background: var(--surface);
    color: var(--text-muted);
    border-color: var(--border);
  }

  /* A text-weight action. The hairline is what makes it read as pressable:
     "Resend code" was dim text with nothing around it until it had one. */
  .btn-tertiary {
    background: transparent;
    color: var(--primary);
    border-color: var(--border);
  }
  .btn-tertiary:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--primary-tint);
    color: var(--primary);
  }
  /* Without it a disabled tertiary read exactly as it does at rest (the TOTP
     dialog's Cancel while a code is checked, the portal's Resend code while
     it sends). Opacity, as .btn-danger does: --text-muted reads brighter
     than the dark theme's --primary, so a disabled control looked more live. */
  .btn-tertiary:disabled,
  .btn-tertiary[aria-disabled="true"] { opacity: var(--opacity-disabled); }

  /* Destructive confirmation. The fill is darkened in aliases.css to clear
     4.5:1 under white text. */
  .btn-danger {
    background: var(--danger-fill);
    color: var(--ink-inverse);
    border-color: var(--danger-fill-border);
    box-shadow: 0 1px 2px color-mix(in srgb, var(--danger) 30%, transparent);
  }
  .btn-danger:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--danger-fill-hover);
  }
  .btn-danger:active:not(:disabled):not([aria-disabled="true"]) {
    transform: translateY(0);
    box-shadow: 0 1px 2px color-mix(in srgb, var(--danger) 30%, transparent);
  }
  .btn-danger:disabled,
  .btn-danger[aria-disabled="true"] { opacity: var(--opacity-disabled); transform: none; box-shadow: none; }

  /* --- Field ------------------------------------------------------------
     One text-control family. .field wraps, .field-label names, .field-hint
     explains, .field-input takes input, select and textarea alike, and
     .field-error carries the message. Invalid state is [aria-invalid="true"]
     and not a class, so the style cannot disagree with what a screen reader
     is told. The shape is the one commit 08b302b established:

       <div class="field">
         <label class="field-label" for="x">Name <span class="field-optional">optional</span></label>
         <input class="field-input" id="x" aria-describedby="x-hint">
         <p class="field-hint" id="x-hint">…</p>
         <p class="field-error" id="x-error" hidden></p>
       </div>

     The control's aria-describedby names the hint at rest and the error
     FIRST and the hint second while an error shows — the error is the news,
     the hint is usually how to fix it. That is portal/sections/login.js's
     rule, and it is the shape that ships; §5.3's snippet shows both ids
     named statically, which browsers also accept.

     Values are the portal editor's family, the design doc's reference, moved
     onto the token scale. Layout is the context's business except for the
     two declarations marked below. */
  .field {
    display: flex;
    flex-direction: column;
    gap: var(--gap-close);
    /* Three sheets wrote this so a field in a grid track can shrink. */
    min-width: 0;
  }
  /* The one layout declaration the wrapper carries: .ed-field--wide,
     .verify-field--wide and .verify-edit-field--wide were the same line. */
  .field--wide { grid-column: 1 / -1; }

  .field-label {
    margin: 0;
    /* The two names a context may override. The admin console binds both in
       its own :root — mono (design-system/README.md, 2026-09-04) at --text-xs
       — because it is an operator console, not a form a provider fills in.
       The default is what the 2026-09-20 rule asks for: anything a provider
       reads to fill a form in is --text-sm (D1.1, 2026-09-22). Both carry a
       fallback, so they resolve in every bundle with neither one set. The
       defaults are the eyebrow role (aliases.css). */
    font-family: var(--field-label-font, var(--type-eyebrow-family));
    font-size: var(--field-label-size, var(--type-eyebrow-size));
    font-weight: var(--type-eyebrow-weight);
    letter-spacing: var(--type-eyebrow-tracking);
    text-transform: var(--type-eyebrow-transform);
    color: var(--text-muted);
  }
  .field-optional {
    margin-inline-start: var(--gap-tight);
    font-family: var(--field-label-font, var(--font-body));
    /* Not --field-label-size: the 2026-09-20 rule keeps the small tags at the
       --text-xs floor while the label itself is --text-sm. It takes the
       console's FACE, so a mono console tag still reads as one. */
    font-size: var(--type-overline-size);
    font-weight: var(--type-overline-weight);
    letter-spacing: var(--tracking-wide);
    text-transform: var(--type-overline-transform);
    color: var(--text-muted);
  }
  .field-hint {
    margin: 0;
    font-family: var(--type-body-sm-family);
    font-size: var(--type-body-sm-size);
    line-height: var(--type-body-sm-leading);
    color: var(--text-muted);
  }
  /* Hidden until something writes to it. [hidden] is unlayered in base.css
     and carries !important, so nothing here can un-hide it by accident. */
  .field-error {
    margin: 0;
    font-family: var(--type-label-family);
    font-size: var(--type-label-size);
    font-weight: var(--type-label-weight);
    line-height: var(--leading-normal);
    color: var(--danger-ink);
  }

  .field-input {
    /* Width is the one box declaration that is really layout: every one of
       the rules this replaces set it, and a bare input's own width is a UA
       size attribute, not a design. A context that needs a different one
       says so in its own sheet. */
    width: 100%;
    min-height: var(--target-min);
    padding: var(--field-pad);
    font-family: var(--type-body-sm-family);
    font-size: var(--type-body-sm-size);
    line-height: var(--leading-snug);
    color: var(--text);
    background: var(--surface);
    border: var(--field-border-width) solid var(--control-border);
    border-radius: var(--field-radius);
    transition: border-color var(--motion-interaction-duration) var(--motion-interaction-ease),
                background   var(--motion-interaction-duration) var(--motion-interaction-ease);
  }
  .field-input::placeholder {
    color: var(--text-muted);
    /* Firefox dims a placeholder by default, which turns a measured colour
       into an unmeasured one. */
    opacity: 1;
  }
  /* ── State precedence, written once ──────────────────────────────────
     disabled  >  [aria-invalid="true"]  >  focus  >  hover  >  rest
     and a --seam control draws no border in any of them (its box is the
     wrapper's). Read the four rules below as that order and nothing else.

     It is spelled out rather than left to source order because a `:not()`
     chain is specificity: `.field-input:hover:not(…):not(…)` climbs a point
     per exclusion, so the hover rule out-ranks every plain state selector
     underneath it and silently wins. Each state a control can be in AT THE
     SAME TIME as hover is therefore excluded from the hover selector by
     name, and each rule that must beat another is both at least as specific
     and written after it. tests/convention/css-field-states.test.js
     evaluates all nine pairs against this sheet rather than reading it. */
  .field-input:hover:not(:focus):not([aria-invalid="true"]):not(:disabled):not([readonly]) {
    /* The hover boundary still identifies a control: the decorative border
       made it lighter than rest and fell to 2.88:1 on console grounds. */
    border-color: color-mix(in srgb, var(--primary) 35%, var(--control-border));
  }
  /* One focus indicator, and it is not this rule's (decision 4, D1.2). The
     site's :focus-visible outline in base.css and admin-shell.css is
     unlayered, so it wins whatever is written here; a ring drawn here would
     show on top of it rather than instead of it. So the field says only that
     its border goes primary, and the outline does the announcing — the same
     way every button and link on the site announces focus. :focus-visible's
     default 8px radius is in @layer reset, below this, so the outline follows
     the field's own 12px. To go back to a ring, see the plan's decision 4:
     restore two rules here and add one unlayered line per entry sheet. */
  .field-input:focus { border-color: var(--primary); }
  /* Above focus in the order: an error stays visible while it is being fixed,
     so the :focus pair is named here rather than left to lose by a point. */
  .field-input[aria-invalid="true"],
  .field-input[aria-invalid="true"]:focus { border-color: var(--danger); }
  /* Top of the order. The border is stated rather than left to fall through,
     because a control that was marked invalid and then disabled should stop
     shouting about its value — falling through would leave --danger showing. */
  .field-input:disabled,
  .field-input:disabled[aria-invalid="true"] {
    opacity: var(--opacity-disabled);
    cursor: not-allowed;
    border-color: var(--control-border);
  }

  textarea.field-input { resize: vertical; min-height: var(--space-12); }
  select.field-input {
    appearance: none;
    cursor: pointer;
    background-image: var(--field-select-arrow);
    background-repeat: no-repeat;
    background-position: right var(--space-3) center;
    background-size: var(--space-4);
    padding-inline-end: var(--control-icon-room);
  }
  /* The one type="number" in the product (the invite batch size). Its UA
     spin buttons are a pair of filled triangles in a console whose icons are
     all Lucide strokes. min/max validation, arrow-key stepping and the
     numeric keypad all survive appearance:textfield; only the painted
     triangles go. Delete these two rules to bring the click affordance back. */
  .field-input[type="number"] { appearance: textfield; }
  .field-input[type="number"]::-webkit-outer-spin-button,
  .field-input[type="number"]::-webkit-inner-spin-button { appearance: none; margin: 0; }

  /* Digits, codes, ABNs, times and counts — what README.md reserves the mono
     face for. Not labels. */
  .field-input--mono {
    font-family: var(--type-mono-family);
    font-variant-numeric: var(--type-mono-numeric);
    letter-spacing: var(--type-mono-tracking);
  }
  /* One-time code entry, at the size a six-digit code is read back at.
     Written by Task 1.2, the first task with markup for it — sign-in's
     #codeInput and the admin sign-in's #totp — and never edited afterwards;
     Task 1.7's TOTP dialog adds the class and nothing else. 24px rather than
     the 26px the two rules it replaces used: --text-xl is the scale's step,
     and the tracking is what makes a code read as six separate digits. */
  .field-input--code {
    font-family: var(--font-mono);
    font-size: var(--text-xl);
    font-weight: var(--fw-semi);
    text-align: center;
    letter-spacing: 0.36em;
    font-variant-numeric: tabular-nums;
    padding: var(--control-pad-lg) var(--control-pad-xl);
  }
  /* A control that sits inside a shared bordered shell (the phone group, an
     hours pill) and must not draw a second box inside the first. */
  .field-input--seam {
    background: transparent;
    border-color: transparent;
  }
  .field-input--seam:focus { background: var(--surface); }
  /* …and it draws no border in ANY state. One selector here per state rule
     in the block above, each at least as specific as the rule it answers and
     written after it, so plain source order decides rather than an arms race
     of `:not()`s. Without the hover line a 1.5px border appeared inside the
     phone group's pill and inside the hours pill the moment the pointer
     crossed them — a second box inside the first, which is the one thing
     this modifier exists to prevent. */
  .field-input--seam:hover:not(:focus):not([aria-invalid="true"]):not(:disabled):not([readonly]),
  .field-input--seam:focus,
  .field-input--seam[aria-invalid="true"],
  .field-input--seam[aria-invalid="true"]:focus,
  .field-input--seam:disabled,
  .field-input--seam:disabled[aria-invalid="true"] { border-color: transparent; }
  /* A seam select draws no chevron. `background: transparent` above cannot
     clear it on its own: select.field-input is (0,1,1) and --seam is (0,1,0),
     so the image wins on specificity whatever the source order. The phone
     group's area-code and 1300/1800 selects and both hours selects had no
     chevron before this primitive and have none after it; the end gutter goes
     with it, since there is no longer anything in it. */
  select.field-input.field-input--seam {
    background-image: none;
    /* Back to the inline half of --field-pad, which is what a control with
       nothing in its end gutter should have. */
    padding-inline-end: var(--control-pad-lg);
  }

  /* A filter box with an icon in its gutter: the console's five toolbar
     searches (#analyticsSearch, #pipelineSearch, #wsSearch, #lcSearch,
     #attSearchInput). NOT the home or directory search, whose box is drawn by
     the wrapper and whose focus lives on an ancestor — those stay bespoke
     (the plan's decision 5). Written by Task 1.4, the first task with markup
     for it, and never edited afterwards.

     Two declarations, because only two things are this modifier's: the softer
     radius the five shared (16px, already on the scale as --radius-lg) and
     the start gutter the absolutely-positioned magnifier sits in. The gutter
     is --space-8 (40px) rather than the 32/38/40 the five drifted to: it is
     the same token the select chevron's end gutter uses, so a console row
     with a search and a select has one inset, not two. `padding-inline-start`
     alone, after the base rule's `padding` shorthand, so the other three
     sides stay --field-pad. */
  .field-input--search {
    border-radius: var(--shape-lg);
    padding-inline-start: var(--control-icon-room);
  }

  /* Forced colours. base.css gives every `<button>` a boundary there, but a
     good part of this family is `<a>` — Call on a provider page, the nav CTA,
     the for-providers pair — and a filled anchor with no border dissolves
     into the system Canvas. Said once for the family instead of once per
     bespoke name: style.css's forced-colours block listed four of those names
     before this phase folded them in, and each fold would otherwise have had
     to remember to leave one behind. */
  @media (forced-colors: active) {
    .btn,
    .btn-primary,
    .btn-secondary,
    .btn-tertiary,
    .btn-danger { border: 1px solid ButtonText; }
  }

  /* --- Form errors ------------------------------------------------------
     The two form-level boxes lib/field-error.js writes; one field's own
     message is .field-error, above. [data-form-error] is a form's single
     role="alert" region for a failure that is no field's fault (network, 429,
     5xx, 409). .error-summary is what a submit with three or more errors, or
     errors across tab panels, focuses: a heading and one link per field.

     Both are the sign-in banner's pair (style.css:.login-error), --danger-ink
     on --danger-tint, measured 2026-10-01: 7.30:1 light, 11.01:1 dark over
     --surface, 8.21:1 on the console's dark block. The edge is decoration —
     the words carry the meaning — and still clears 3:1 against the ground
     outside the box (light 5.43:1, dark 3.63:1, console dark 5.92:1). The
     banner keeps the sign-in banner's 3px start edge; the summary is boxed
     all round, so the two never read as the same thing. Forced colours drop
     the tint and keep both borders. */
  .notice,
  [data-form-error],
  .error-summary {
    margin: 0;
    padding: var(--inset-md) var(--inset-lg);
    border-radius: var(--notice-radius);
    background: var(--notice-face, var(--danger-tint));
    color: var(--notice-ink, var(--danger-ink));
    font-family: var(--type-body-sm-family);
    font-size: var(--type-body-sm-size);
    line-height: var(--type-body-sm-leading);
  }
  .notice,
  [data-form-error] { border-left: var(--border-width-accent) solid var(--notice-line, var(--danger)); }
  .error-summary { border: var(--border-width-strong) solid var(--danger); }
  .error-summary-title {
    margin: 0 0 var(--gap-close);
    font-family: var(--type-label-family);
    font-size: var(--type-label-size);
    font-weight: var(--type-label-weight);
    line-height: var(--type-label-leading);
    letter-spacing: var(--type-label-tracking);
  }
  .error-summary-list {
    margin: 0;
    padding-inline-start: var(--inset-lg);
  }
  /* Underlined and in the box's own ink: a list of links that are the whole
     content still says "link" by more than colour. */
  .error-summary-list a {
    color: inherit;
    text-decoration: underline;
    /* Each link is its own 24px target (2.5.8): at the text's own 18px line
       two links on consecutive rows failed on size and on spacing at once,
       which the error-state audit measured at 320 and 390. */
    display: inline-flex;
    align-items: center;
    min-height: var(--target-aa);
  }
  /* The third thing lib/field-error.js writes, and the only one nobody sees:
     a form's assertive region for an error on the field that ALREADY has
     focus, where focus() fires no event and a screen reader would say nothing
     (review C1, 2026-10-01). Its words are the visible .field-error's, so it
     is hidden from the eye and from nothing else. Here and not only .sr-only
     (which the helper also sets, for a page still holding last deploy's copy
     of this sheet): the console links neither base.css nor style.css, and its
     drawer's edit form is one of the forms this is for. Out of flow and
     clipped to nothing, as .toast-kind is, but in a 1px box where that one
     keeps its natural size: this holds a sentence that may not wrap, and at
     its natural 565px it made a 320px page 765px wide (measured in Chromium;
     out of flow is not out of the scrollable area, WCAG 1.4.10). One pixel is
     still not the zero-size box some screen readers skip. Never [hidden]:
     that would silence it. */
  [data-field-error-announce] {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    white-space: nowrap;
    clip-path: inset(50%);
  }

  /* --- Pill ------------------------------------------------------------
     A STATIC status label: a word in a coloured box, on a <span>, <li> or
     <div>. Never a <button> — the interactive control is .chip. No state
     rules and no mono: a pill is not focusable, and README.md's mono
     reservation belongs to the console's .pill--mono.

     TONE IS AN ATTRIBUTE, so it cannot disagree with itself the way
     class="pill pill--good pill--warn" can, and the class name stays a
     literal the census can see. data-tone="neutral" needs no rule: the
     default IS the quiet tone.

     A TONE SETS THREE CUSTOM PROPERTIES AND NOTHING ELSE — the extension
     point for a colour the tokens cannot name (portal.css's seven
     visibility contexts, the trust tiers).
     tests/convention/css-layers-chips.test.js fails a context rule that
     restates the box and passes one that only sets those three. It checks
     the box, not the colour: a --pill-tint that fails AA passes it.

     14% and the *-ink step are MEASURED, not chosen: --success/--warn/
     --danger as text on their own tint read 2.88-2.95:1, and 16% left
     `live` at 4.48:1 where its siblings cleared 4.58:1. */
  .pill {
    --pill-tint: color-mix(in srgb, var(--text) 6%, var(--surface));
    --pill-ink:  var(--text-dim);
    --pill-line: var(--border);
    display: inline-flex;
    align-items: center;
    gap: var(--gap-tight);
    padding: var(--inset-xs) var(--inset-sm);
    border: var(--border-width) solid var(--pill-line);
    border-radius: var(--radius-pill);
    background: var(--pill-tint);
    color: var(--pill-ink);
    /* The badge role (aliases.css), less its tracking: a pill has always
       inherited its letter-spacing, and setting the role's 0 would move a
       pill that sits in tracked text. */
    font-family: var(--type-badge-family);
    font-size: var(--type-badge-size);
    font-weight: var(--type-badge-weight);
    line-height: var(--type-badge-leading);
    white-space: nowrap;
  }
  .pill[data-tone="good"] {
    --pill-tint: color-mix(in srgb, var(--success) 14%, var(--surface));
    --pill-ink:  var(--success-ink);
    --pill-line: color-mix(in srgb, var(--success) 32%, transparent);
  }
  .pill[data-tone="warn"] {
    --pill-tint: color-mix(in srgb, var(--warn) 14%, var(--surface));
    --pill-ink:  var(--warn-ink);
    --pill-line: color-mix(in srgb, var(--warn) 32%, transparent);
  }
  .pill[data-tone="bad"] {
    --pill-tint: color-mix(in srgb, var(--danger) 14%, var(--surface));
    --pill-ink:  var(--danger-ink);
    --pill-line: color-mix(in srgb, var(--danger) 32%, transparent);
  }
  /* --primary-tint-ink, not --primary-ink: in dark the tint is 14% teal on
     a near-black surface and --primary-ink is --deep-900 (~1.48:1). The
     alias binds --primary-ink in light, --text in dark (aliases.css). */
  .pill[data-tone="accent"] {
    --pill-tint: var(--primary-tint);
    --pill-ink:  var(--primary-tint-ink);
    --pill-line: color-mix(in srgb, var(--primary) 18%, transparent);
  }
  /* Not the default: uppercase at --text-xs is small text by WCAG's
     measure, so every pill that takes it is one whose ink was read at that
     size. --tracking-wide (0.06em), not the 0.1-0.16em the three families
     drifted to — wide tracking breaks word shapes (the eyebrow's note). */
  .pill--caps {
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    font-weight: var(--fw-bold);
  }
  /* README.md reserves the mono face for the ADMIN CONSOLE — the telegraphic
     voice of a tool, against the public site's body face. This modifier is
     what keeps it there now that the box is shared. It must follow .pill in
     this file: both are (0,1,0), so source order is the whole of the cascade
     between them. */
  .pill--mono {
    font-family: var(--font-mono);
  }

  /* For a pill carrying a SENTENCE rather than a label, where the base's
     `white-space: nowrap` would let an ancestor clip it with no ellipsis.
     The workspace record summary's chip is the example: at 320 "Not in
     search — ABN active on the ABR" lost its last eighteen pixels. Must
     follow .pill, for the same source-order reason as .pill--mono. */
  .pill--wrap {
    white-space: normal;
  }

  /* --- Pill, removable -------------------------------------------------
     A pill whose last child is a .pill-remove. The right padding closes up
     to --space-1 because the button brings its own visual weight to that
     edge; without it the dismiss control sits in a gutter twice as wide as
     the one on the label side. */
  .pill--removable {
    padding-right: var(--inset-xs);
  }

  /* --- Pill dismiss control --------------------------------------------
     The one interactive part of a static pill: a real <button> with its own
     accessible name ("Remove <value>"), inside a <span> that is not a
     control. It is NOT .chip — a chip is the whole control; this is a
     control inside a label.

     THE TARGET ARITHMETIC. --target-aa (24px, aliases.css) is WCAG 2.2 AA's
     2.5.8 floor, which the box meets on its own; the ::after extends the hit
     area --space-2 (8px) past every edge, taking it to 40px. The box reads
     the role and not --space-6, the step it equals, so a change to the floor
     reaches it (css-spacing-roles.test.js holds that). The pattern is
     style.css:.trust-chip's, written in tokens rather than as that rule's
     literal `-8px -6px`.

     THE CONTAINER RULE. Two dismiss controls spaced closer than --space-4
     (16px) have overlapping extended areas — 8px + 8px is exactly 16px — and
     in the overlap the later element in the DOM takes the click, which
     removes the WRONG pill. So every container holding two or more of a
     .pill-remove or a .chip--compact is set to `gap: var(--space-4)`, where
     two extensions abut and never overlap.

     The background is a mix of currentColor, not of a named token, so the
     control darkens whatever tone the pill carries and the ink stays the
     pill's own — the ratio that tone was chosen for. */
  .pill-remove {
    position: relative;
    display: inline-grid;
    place-items: center;
    width: var(--target-aa);
    height: var(--target-aa);
    padding: 0;
    border: 0;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, currentColor 14%, transparent);
    color: inherit;
    font: inherit;
    font-size: var(--text-xs);
    line-height: var(--leading-none);
    cursor: pointer;
    transition: background-color var(--motion-interaction-duration) var(--motion-interaction-ease);
  }
  .pill-remove:hover {
    background: color-mix(in srgb, currentColor 28%, transparent);
  }
  .pill-remove::after {
    content: '';
    position: absolute;
    inset: calc(var(--space-2) * -1);
  }

  /* --- Chip ------------------------------------------------------------
     The interactive sibling of .pill: always a real <button> or <a>
     (HANDOFF.md §9), never a span. Its selected state is said with ARIA and
     read back from it — aria-pressed, aria-checked or aria-selected — so the
     style cannot disagree with what a screen reader is told, and there is no
     .is-selected class. min-height is --target-min, the 44px HANDOFF.md §8
     asks for. */
  .chip {
    --chip-face: var(--surface);
    --chip-ink:  var(--text);
    --chip-line: var(--border-strong);
    appearance: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--gap-tight);
    min-height: var(--target-min);
    padding: 0 var(--control-pad-lg);
    border: var(--border-width) solid var(--chip-line);
    border-radius: var(--chip-radius);
    background: var(--chip-face);
    color: var(--chip-ink);
    /* The label role (aliases.css), less its tracking, which the chip has
       never set: the role's -0.005em is a change, left to a restyle. */
    font-family: var(--type-label-family);
    font-size: var(--type-label-size);
    font-weight: var(--type-label-weight);
    line-height: var(--type-label-leading);
    white-space: nowrap;
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--motion-interaction-duration) var(--motion-interaction-ease),
                border-color var(--motion-interaction-duration) var(--motion-interaction-ease),
                color var(--motion-interaction-duration) var(--motion-interaction-ease);
  }
  /* Optional wrapping for constrained filter groups and enlarged text.
     Ordinary single-line chips keep their existing recipe. */
  .chip--wrap { white-space: normal; overflow-wrap: anywhere; }

  /* Hover excludes the selected states explicitly, because an unlayered
     bespoke rest rule out-ranks a layered hover in every state (Task 0.2's
     finding) and the precedence here is disabled > selected > hover > rest.
     A working chip is aria-disabled (lib/busy-button.js): drawn as disabled. */
  .chip:hover:not(:disabled):not([aria-disabled="true"]):not([aria-pressed="true"]):not([aria-checked="true"]):not([aria-selected="true"]) {
    --chip-face: var(--primary-tint);
    --chip-line: var(--primary);
  }
  .chip[aria-pressed="true"],
  .chip[aria-checked="true"],
  .chip[aria-selected="true"] {
    --chip-face: var(--cta);
    --chip-ink:  var(--cta-ink);
    --chip-line: var(--cta);
    font-weight: var(--fw-bold);
  }
  .chip:disabled,
  .chip[aria-disabled="true"] {
    cursor: not-allowed;
    opacity: var(--opacity-disabled);
  }
  /* --- Chip, compact ---------------------------------------------------
     A 24px visual box (--target-aa, the 2.5.8 floor) whose hit area is
     extended --space-2 (8px) past every edge, so the target is 40px:
     .pill-remove's arithmetic, on a whole chip.
     For the console's dense rows, where 44px filters would push the queue
     rail's first record down the rail. Its container is set to
     `gap: var(--space-4)`, where two 8px extensions abut and never overlap. */
  .chip--compact {
    position: relative;
    min-height: var(--target-aa);
    padding: 0 var(--control-pad-md);
    font-size: var(--type-badge-size);
  }
  .chip--compact::after {
    content: '';
    position: absolute;
    inset: calc(var(--space-2) * -1);
  }

  /* --- Dialog ----------------------------------------------------------
     The surface of every <dialog> opened with showModal(). No closed-state
     rule: a native <dialog> is display:none until [open]. `margin: auto` is
     written, not left to the UA, because @layer reset's `margin: 0` beats
     the UA's centring. A dialog retunes its width with --dialog-max. */
  .dialog {
    margin: auto;
    border: 0;
    padding: 0;
    max-width: var(--dialog-max, 520px);
    width: calc(100% - var(--space-7));
    border-radius: var(--dialog-radius);
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-xl);
    font-family: var(--font-body);
  }
  /* ::backdrop inherits from its dialog, so the token resolves here. The
     scale has no step for a filter length. */
  .dialog::backdrop {
    background: color-mix(in srgb, var(--brand-depth-strong) 55%, transparent);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
  }
  .dialog-content { padding: var(--inset-xl); --stack-gap: var(--gap-related); }
  .dialog-title {
    font-family: var(--type-card-title-family);
    font-size: var(--type-card-title-size);
    font-weight: var(--type-card-title-weight);
    line-height: var(--type-card-title-leading);
  }
  .dialog-description { color: var(--text-muted); white-space: pre-line; overflow-wrap: anywhere; }
  .dialog-actions { justify-content: flex-end; }
  /* Forced colours drop the shadow, which is this box's only edge: the
     console's TOTP dialog measured 0px of border there. */
  @media (forced-colors: active) {
    .dialog { border: 1px solid CanvasText; }
  }
  /* --ease-out, not --ease-entrance, which does not resolve in admin. Opacity
     only: any transform, even mid-entrance, makes the dialog the containing
     block for the fixed toast region it carries (lib/toast.js), and a toast
     raised with the TOTP dialog sat 421px left of and 199px above its corner
     for the first 205ms, then jumped. */
  @media (prefers-reduced-motion: no-preference) {
    .dialog[open] { animation: dialog-in var(--dur-med) var(--motion-interaction-ease); }
  }
  @keyframes dialog-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  /* --- Toast region -----------------------------------------------------
     lib/toast.js's #toast and #toast-alert, one corner on every surface:
     bottom-right (D5.2 row 20). column-reverse keeps the oldest of a console
     stack nearest the corner and grows it upward. max-content, because a
     fixed box shrink-fits and wrapped the location toast into six lines.
     The two regions share the corner; while #toast-alert holds an error on
     the same edge, lib/toast.js sets --toast-clear on #toast (that region's
     height and the gap) as an inline style, and the margin on whichever
     edge the region is anchored to stacks it clear. Otherwise it is 0. */
  .toast-region {
    --toast-clear: 0;
    position: fixed;
    right: var(--space-6);
    bottom: calc(var(--space-6) + env(safe-area-inset-bottom, 0));
    margin-block: var(--toast-clear);
    z-index: var(--z-toast);
    display: flex;
    flex-direction: column-reverse;
    gap: var(--gap-close);
    width: max-content;
    max-width: calc(100vw - 2 * var(--space-4));
    pointer-events: none;
  }
  /* The bottom lift clears the public and portal phone theme control (16px +
     44px + 24px); the console has no such control, so it keeps its corner. */
  @media (max-width: 640px) {
    .toast-region { right: var(--space-4); }
    :root:not([data-surface="console"]) .toast-region:not([data-edge="top"]) {
      bottom: calc(var(--space-11) + var(--space-1) + env(safe-area-inset-bottom, 0));
    }
  }
  /* lib/toast.js moves the region here while the control with focus would be
     entirely hidden under it (WCAG 2.4.11). */
  .toast-region[data-edge="top"] {
    top: calc(var(--space-4) + env(safe-area-inset-top, 0));
    bottom: auto;
    flex-direction: column;
  }

  /* --- Toast --------------------------------------------------------------
     lib/toast.js's `.toast.toast--<kind>`, one box on every surface: an opaque
     fill (aliases.css --toast-bg-*), the kind's ink, and a 3px left edge in its
     strong token, at 4.5:1 text and 3:1 edge in both themes. A surface sets
     layout, type and motion only. No pointer events: on a phone a toast sits
     over the controls it is about (#geoBtn at 320), and a tap must reach them. */
  .toast {
    pointer-events: none;
    padding: var(--inset-md) var(--inset-lg);
    border: var(--border-width) solid var(--border);
    border-left: var(--border-width-accent) solid var(--primary);
    border-radius: var(--toast-radius);
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-md);
  }
  /* border-left-color after border-color: the shorthand resets all four sides,
     and in the other order it erased the edge. */
  .toast--success { background: var(--toast-bg-success); color: var(--success-ink); border-color: transparent; border-left-color: var(--success); }
  .toast--error   { background: var(--toast-bg-danger);  color: var(--danger-ink);  border-color: transparent; border-left-color: var(--danger); }
  .toast--warn    { background: var(--toast-bg-warn);    color: var(--warn-ink);    border-color: transparent; border-left-color: var(--warn); }
  /* The kind's word — "Error: ", "Warning: " — ahead of the message, so the
     kind is in the text and not only the colour (WCAG 1.4.1). Read, never
     seen: the fill and the edge already say it to the eye. The console does
     not load base.css's .sr-only, so the toast hides its own: out of flow,
     at its natural size (a zero-size box is one some screen readers skip),
     and clipped to nothing. */
  .toast-kind {
    position: absolute;
    overflow: hidden;
    white-space: nowrap;
    clip-path: inset(50%);
  }
  /* The dismiss control only a sticky toast carries — every error in
     #toast-alert — and the only part of a toast that takes the pointer. Its
     ring is each surface's own :focus-visible ring, which reads 3.85:1 or
     better on every toast fill (design-system/README.md, .toast). */
  .toast-close {
    pointer-events: auto;
    font: inherit;
    color: inherit;
    background: none;
    border: none;
    min-width: var(--target-min);
    min-height: var(--target-min);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    margin-left: var(--gap-close);
  }
}

@layer primitives { .notice[data-tone="danger"] { --notice-face: var(--danger-tint); --notice-ink: var(--danger-ink); --notice-line: var(--danger); } }

@layer primitives { .notice[data-tone="warn"] { --notice-face: var(--warn-tint); --notice-ink: var(--warn-ink); --notice-line: var(--warn); } }

@layer primitives { .notice[data-tone="success"] { --notice-face: var(--success-tint); --notice-ink: var(--success-ink); --notice-line: var(--success); } }

@layer primitives { .notice[data-tone="info"] { --notice-face: var(--primary-tint); --notice-ink: var(--primary-tint-ink); --notice-line: var(--primary); } }

@layer primitives { .notice[data-tone="neutral"] { --notice-face: var(--surface-2); --notice-ink: var(--text); --notice-line: var(--border); } }

@layer primitives {
  .pipeline-degraded,
  .ds-action--warn,
  .lc-source-warning,
  .att-opt-out,
  .verify-decline-note,
  .trust-prompt,
  .accepting-bulk,
  .outlet-remote-toggle,
  .login-note,
  .ingest-alarm,
  .scheduler-alarm,
  .ops-alarm {
    background: var(--notice-face, transparent);
    color: var(--notice-ink, inherit);
    padding: var(--notice-pad, 0);
    border: var(--notice-border, 0);
    border-radius: var(--notice-box-radius, 0);
    box-shadow: var(--notice-shadow, none);
  }
}
