/* Alert banner — persistent inline message. Use role="alert" for urgent/assertive
 * content, role="status" for informational.
 *   <div class="alert alert--solid" role="alert">
 *     <svg class="alert__icon icon" aria-hidden="true"><use href="#icon-info"/></svg>
 *     <div><p class="alert__title">Margin call</p>
 *       <p class="alert__text">Deposit by 16:00 EST.</p></div>
 *     <button class="alert__close btn btn--icon btn--sm" aria-label="Dismiss">
 *       <svg class="icon" aria-hidden="true"><use href="#icon-cross"/></svg>
 *     </button>
 *   </div>
 * Outlined on paper by default; add alert--solid to fill instead. Text
 * always present. */

@layer bleed.components {
  .alert {
    --_border: var(--alert-border, var(--ink));

    position: relative;
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);

    color: var(--alert-fg, var(--ink));
    background-color: var(--alert-bg, var(--paper));
    border: var(--border-w) var(--border-style) var(--_border);
    border-radius: var(--radius);
  }

  .alert__icon {
    inline-size: calc(var(--space-4) + var(--space-1));
    block-size: calc(var(--space-4) + var(--space-1));
    margin-block-start: 1px; /* px-ok: optical alignment */
    color: currentColor;
  }

  .alert__title {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--font-size-1);
  }

  .alert__text {
    margin: 0;
    font-size: var(--font-size-1);
  }

  /* Close is a plain .btn (btn--icon btn--sm) — the alert only pushes it to the
   * end; everything else about it is the button contract. */
  .alert__close {
    margin-inline-start: auto;
    margin-block-start: calc(-1 * var(--space-1));
  }

  /* fill — solid replaces the outlined-on-paper default */
  .alert--solid {
    --alert-bg: var(--_border);
    --alert-fg: var(--paper);
    /* the close button now sits ON the fill, where its ink border and ink
     * shadow would disappear — flip both to paper. The shadow has to be restated
     * rather than recoloured: --shadow already substituted its var(--ink) back
     * at :root, so remapping ink here would not reach it. */
    --btn-fg: var(--paper);
    --btn-border: var(--paper);
    --btn-shadow: var(--space-1) var(--space-1) 0 var(--paper);
    --btn-shadow-hover: calc(var(--space-1) / 2) calc(var(--space-1) / 2) 0 var(--paper);
    --btn-shadow-active: 0 0 0 var(--paper);
  }
}
/* Badge / Tag — <span class="badge">. Tone sets border + text color and is
 * outlined on paper by default; add badge--solid to fill with the tone
 * instead, independent of which tone is used — same split as alert.
 * Uses foundational (:root) tokens so it works in bleed-core.css too.
 * A badge always carries a text label, so tone is never the sole signal. */

@layer bleed.components {
  .badge {
    --_border: var(--badge-border, var(--ink));
    --_bg: var(--badge-bg, transparent);
    --_fg: var(--badge-fg, var(--ink));

    display: inline-flex;
    align-items: center;
    gap: var(--space-1);

    padding-block: var(--space-1);
    padding-inline: var(--space-2);

    font-size: var(--font-size-1);
    font-family: var(--font-body);
    font-weight: var(--font-weight-normal);
    line-height: var(--line-height-tight);
    letter-spacing: var(--letter-spacing-tight);
    white-space: nowrap;

    color: var(--_fg);
    background-color: var(--_bg);
    border: var(--border-w) var(--border-style) var(--_border);
    border-radius: var(--radius);
  }

  /* tone — border + text color only; fill stays whatever .badge/--solid set */
  .badge--warning  { --badge-border: var(--warning-strong);  --badge-fg: var(--warning-strong); }
  .badge--negative { --badge-border: var(--negative-strong); --badge-fg: var(--negative-strong); }
  .badge--positive { --badge-border: var(--positive-strong); --badge-fg: var(--positive-strong); }
  .badge--accent   { --badge-border: var(--accent);   --badge-fg: var(--accent); }

  /* fill — solid is a separate axis from tone, combine e.g. badge--negative badge--solid */
  .badge--solid {
    --badge-bg: var(--_border);
    --badge-fg: var(--paper);
  }

  /* a small leading dot, e.g. status pills */
  .badge__dot {
    inline-size: var(--space-2);
    block-size: var(--space-2);
		margin-inline-end: var(--space-1);
    border-radius: 50%;
    background-color: currentColor;
  }
}
/* Button — <button class="btn"> (also fine on <a>/<input type=submit>).
 *
 * Interaction contract (shared by every pressable component: btn, interactive card,
 * tab, nav link). The rest shadow is the interactivity signal across the whole kit —
 * static surfaces carry none. All of it is token-driven so a theme can change OR remove it:
 *   rest    → box-shadow: --shadow
 *   :hover / :focus-visible → translate by --hover-shift-*, box-shadow: --shadow-hover
 *   :active → translate 2×,               box-shadow: --shadow-active
 * Bleed moves the element INTO its offset shadow (press). Soft sets --hover-shift 0
 * and swaps blur via the shadow tokens (lift), proving the motion lives in tokens.
 *
 * Public override tokens: --btn-bg --btn-fg --btn-border. Internals are --_*.
 *
 * Outlined on paper by default; add btn--solid to fill with ink instead. */

@layer bleed.components {
  .btn {
    --_bg: var(--btn-bg, transparent);
    --_fg: var(--btn-fg, var(--ink));
    --_bd: var(--btn-border, var(--ink));
    --_shadow: var(--btn-shadow, var(--shadow));
    --_shadow-hover: var(--btn-shadow-hover, var(--shadow-hover));
    --_shadow-active: var(--btn-shadow-active, var(--shadow-active));

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);

    padding-block: var(--space-2);
    padding-inline: var(--space-4);
    min-block-size: calc(var(--space-6) + var(--space-3));

    font-family: var(--font-display);
    font-size: var(--font-size-1);
    line-height: var(--line-height-tight);
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;

    color: var(--_fg);
    background-color: var(--_bg);
    border: var(--border-w) var(--border-style) var(--_bd);
    border-radius: var(--radius);
    box-shadow: var(--_shadow);

    transition:
      translate var(--transition),
      box-shadow var(--transition),
      background-color var(--transition),
      color var(--transition);
  }

  /* Keyboard focus is the pointer-free equivalent of hover, so it lands on the
   * same beat. :focus-visible, not :focus — a mouse click focuses the button too,
   * and plain :focus would leave it stuck mid-press once the pointer moves away. */
  .btn:hover,
  .btn:focus-visible {
    translate: var(--hover-shift-x) var(--hover-shift-y);
    box-shadow: var(--_shadow-hover);
  }

  .btn:active {
    translate: calc(var(--hover-shift-x) * 2) calc(var(--hover-shift-y) * 2);
    box-shadow: var(--_shadow-active);
  }

  .btn:disabled,
  .btn[aria-disabled="true"] {
    cursor: not-allowed;
    opacity: 0.5;
    box-shadow: var(--_shadow-active);
    translate: none;
    pointer-events: none;
  }

  /* --- fill: solid is a separate axis from the outlined default ----------- */
  .btn--solid {
    --btn-bg: var(--ink);
    --btn-fg: var(--paper);
    --btn-border: var(--ink);
    --btn-shadow: var(--space-1) var(--space-1) 0 var(--neutral);
    --btn-shadow-hover: calc(var(--space-1) / 2) calc(var(--space-1) / 2) 0 var(--neutral);
    --btn-shadow-active: 0 0 0 var(--neutral);
  }

  /* --- sizes: scale via space + font, geometry stays tokenised ------------- */
  .btn--sm {
    padding-block: var(--space-1);
    padding-inline: var(--space-3);
    min-block-size: var(--space-6);
    font-size: var(--font-size-1);
  }

  .btn--lg {
    padding-block: var(--space-3);
    padding-inline: var(--space-5);
    min-block-size: calc(var(--space-8) + var(--space-1));
    font-size: var(--font-size-2);
  }

  /* --- icon-only: square, centres an .icon (or a raw SVG) ----------------- */
  .btn--icon {
    padding: var(--space-2);
    min-block-size: calc(var(--space-6) + var(--space-3));
    inline-size: calc(var(--space-6) + var(--space-3));
    aspect-ratio: 1;
    font-size: var(--font-size-3);
  }
  .btn--icon.btn--sm {
    min-block-size: var(--space-6);
    inline-size: var(--space-6);
    padding: var(--space-1);
  }
  .btn--icon.btn--lg {
    min-block-size: calc(var(--space-8) + var(--space-1));
    inline-size: calc(var(--space-8) + var(--space-1));
  }
  .btn--icon > :is(svg, .icon),
  .btn__icon {
    inline-size: calc(var(--space-4) + var(--space-1));
    block-size: calc(var(--space-4) + var(--space-1));
  }

  .btn--full {
    display: flex;
    inline-size: 100%;
  }

  /* horizontal cluster of related buttons */
  .btn-group {
    display: inline-flex;
    flex-wrap: wrap;
    gap: var(--space-3);
  }
}
/* Card / Panel — <article class="card">. Optional structure:
 *   <article class="card">
 *     <header class="card-header"><h3 class="card-title">…</h3></header>
 *     <div class="card-body"> …arbitrary content… </div>
 *     <footer class="card-footer">…</footer>
 *   </article>
 * Or the simple form: <article class="card card--pad">…</article>. */

@layer bleed.components {
  .card {
    --_bg: var(--card-bg, var(--paper));
    --_bd: var(--card-border, var(--ink));
    --_shadow: var(--card-shadow, var(--shadow));
    /* set --card-pad on the card or an ancestor to retune the padding */
    --_pad: var(--card-pad, var(--space-5));

    display: flex;
    flex-direction: column;
    min-inline-size: 0;

    color: var(--ink);
    background-color: var(--_bg);
    border: var(--border-w) var(--border-style) var(--_bd);
    border-radius: var(--radius);
  }

  /* simple card: content sits directly inside with padding */
  .card--pad {
    padding: var(--_pad);
    gap: var(--space-3);
  }

  .card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-4) var(--_pad);
    border-block-end: var(--border-w) var(--border-style) var(--_bd);
  }

  .card-title {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--font-size-2);
    line-height: var(--line-height-tight);
  }

  .card-body {
    padding: var(--_pad);
    min-inline-size: 0;
  }
  /* let an embedded viz go edge-to-edge */
  .card-body--flush {
    padding: 0;
  }

  .card-footer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-3);
    padding: var(--space-4) var(--_pad);
    border-block-start: var(--border-w) var(--border-style) var(--_bd);
  }

  /* interactive card (wrap in <a>/<button> or add role) — same press contract.
   * The shadow lives here, not on .card: a plain card is a flat surface, the
   * shadow is what marks the card as pressable. */
  .card--interactive {
    cursor: pointer;
    box-shadow: var(--_shadow);
    transition:
      translate var(--transition),
      box-shadow var(--transition);
  }
  /* keyboard focus rides the same beat as hover — see .btn for why it is
   * :focus-visible and not :focus */
  .card--interactive:hover,
  .card--interactive:focus-visible {
    translate: var(--hover-shift-x) var(--hover-shift-y);
    box-shadow: var(--shadow-hover);
  }
  .card--interactive:active {
    translate: calc(var(--hover-shift-x) * 2) calc(var(--hover-shift-y) * 2);
    box-shadow: var(--shadow-active);
  }
}
/* Checkbox / radio / range — all three are fully custom-drawn (appearance: none) so
 * they carry the theme's alt border and ink/paper fill.
 *   <label class="control"><input type="checkbox">Reinvest dividends</label>
 *   <input class="range" type="range" min="0" max="100">
 *
 * checkbox — square (0 radius), border + "check" both ink on paper. The check is an
 * X whose ends touch the four corners; indeterminate draws just one diagonal of it.
 * radio — circular border, check is a centred ink dot inset to clear the border.
 * range — bordered paper track, square ink thumb inset from the border by the same
 * gap the radio dot uses. */

@layer bleed.components {
  .control {
    --_control-w: calc((var(--space-6) - var(--space-5)) / 2 + var(--space-5));
    --_control-h: calc((var(--space-6) - var(--space-5)) / 2 + var(--space-5));
    display: inline-flex;
    align-items: center;
    gap: calc(var(--space-2) + var(--space-1));
    padding-block-start: calc(var(--space-1) / 2);
    cursor: pointer;
  }

  .control input[type="checkbox"],
  .control input[type="radio"],
  .field input[type="checkbox"],
  .field input[type="radio"] {
    appearance: none;
    inline-size: var(--_control-w);
    block-size: var(--_control-h);
    margin: calc(-1 * var(--space-1) / 2) 0 0 0;
    background-color: var(--paper);
    background-repeat: no-repeat;
    border: var(--border-w) var(--border-style) var(--ink);
    box-shadow: var(--shadow);
    cursor: pointer;
    transition:
      translate var(--transition),
      box-shadow var(--transition);
  }

  /* focus/press — the .btn contract, same as .input */
  .control :is(input[type="checkbox"], input[type="radio"]):focus,
  .field :is(input[type="checkbox"], input[type="radio"]):focus {
    translate: var(--hover-shift-x) var(--hover-shift-y);
    box-shadow: var(--shadow-hover);
  }
  .control :is(input[type="checkbox"], input[type="radio"]):active,
  .field :is(input[type="checkbox"], input[type="radio"]):active {
    translate: calc(var(--hover-shift-x) * 2) calc(var(--hover-shift-y) * 2);
    box-shadow: var(--shadow-active);
  }

  /* checkbox: square box, X drawn corner-to-corner. Each diagonal is a stripe
   * (thickness = border width) laid perpendicular to the gradient axis, so the two
   * gradients meet the four corners. */
  .control input[type="checkbox"],
  .field input[type="checkbox"] {
    border-radius: 0;
  }

  .control input[type="checkbox"]:checked,
  .field input[type="checkbox"]:checked {
    background-image:
      linear-gradient(
        to top right,
        transparent calc(50% - var(--border-w) / 2),
        var(--ink) calc(50% - var(--border-w) / 2),
        var(--ink) calc(50% + var(--border-w) / 2),
        transparent calc(50% + var(--border-w) / 2)
      ),
      linear-gradient(
        to bottom right,
        transparent calc(50% - var(--border-w) / 2),
        var(--ink) calc(50% - var(--border-w) / 2),
        var(--ink) calc(50% + var(--border-w) / 2),
        transparent calc(50% + var(--border-w) / 2)
      );
  }

  /* indeterminate: a single diagonal stroke of the X (overrides :checked) */
  .control input[type="checkbox"]:indeterminate,
  .field input[type="checkbox"]:indeterminate {
    background-image: linear-gradient(
      to top right,
      transparent calc(50% - var(--border-w) / 2),
      var(--ink) calc(50% - var(--border-w) / 2),
      var(--ink) calc(50% + var(--border-w) / 2),
      transparent calc(50% + var(--border-w) / 2)
    );
  }

  /* radio: circular box, filled ink dot inset to clear the border */
  .control input[type="radio"],
  .field input[type="radio"] {
    border-radius: 50%;
  }

  .control input[type="radio"]:checked,
  .field input[type="radio"]:checked {
    background-image: radial-gradient(circle calc(var(--_control-w) / 2 - var(--border-w) * 2) at center, var(--ink) 98%, transparent 100%);
  }

  .control:has(:disabled) {
    opacity: 0.5;
    cursor: not-allowed;
  }
  /* disabled drops to the pressed shadow, matching .btn:disabled */
  .control :is(input[type="checkbox"], input[type="radio"]):disabled,
  .field :is(input[type="checkbox"], input[type="radio"]):disabled {
    box-shadow: var(--shadow-active);
    translate: none;
  }

  /* range: the thumb is sized to sit inside the track's border with a gap on every
   * side, so track height = 2 borders + 2 gaps + thumb; inline padding keeps the same
   * gap at both ends of its travel. WebKit lays the thumb out from the track's
   * content-box top, hence the matching margin; Firefox centres it. */
  .range,
  .field input[type="range"] {
    --range-track-h: calc((var(--space-6) - var(--space-5)) / 2 + var(--space-5));
    --range-thumb-gap: var(--border-w);
    --range-thumb-size: calc(var(--range-track-h) - 2 * var(--border-w) - 2 * var(--range-thumb-gap));

    appearance: none;
    inline-size: 100%;
    block-size: var(--range-track-h);
    margin: 0;
    background-color: transparent;
    cursor: pointer;
    transition: translate var(--transition);
  }

  /* The press is split across two elements: the translate has to sit on the input
   * (the whole control moves, thumb included), while the shadow belongs to the
   * track pseudo that actually draws the box. */
  .range:focus,
  .field input[type="range"]:focus {
    translate: var(--hover-shift-x) var(--hover-shift-y);
  }
  .range:active,
  .field input[type="range"]:active {
    translate: calc(var(--hover-shift-x) * 2) calc(var(--hover-shift-y) * 2);
  }
  .range:disabled,
  .field input[type="range"]:disabled {
    translate: none;
  }

  .range::-webkit-slider-runnable-track,
  .field input[type="range"]::-webkit-slider-runnable-track {
    block-size: var(--range-track-h);
    padding: 0 var(--range-thumb-gap);
    background-color: var(--paper);
    border: var(--border-w) var(--border-style) var(--ink);
    border-radius: 0;
    box-shadow: var(--shadow);
    transition: box-shadow var(--transition);
  }
  .range:focus::-webkit-slider-runnable-track,
  .field input[type="range"]:focus::-webkit-slider-runnable-track {
    box-shadow: var(--shadow-hover);
  }
  .range:active::-webkit-slider-runnable-track,
  .field input[type="range"]:active::-webkit-slider-runnable-track {
    box-shadow: var(--shadow-active);
  }
  .range:disabled::-webkit-slider-runnable-track,
  .field input[type="range"]:disabled::-webkit-slider-runnable-track {
    box-shadow: var(--shadow-active);
  }

  .range::-moz-range-track,
  .field input[type="range"]::-moz-range-track {
    block-size: var(--range-track-h);
    padding: 0 var(--range-thumb-gap);
    background-color: var(--paper);
    border: var(--border-w) var(--border-style) var(--ink);
    border-radius: 0;
    box-shadow: var(--shadow);
    transition: box-shadow var(--transition);
  }
  .range:focus::-moz-range-track,
  .field input[type="range"]:focus::-moz-range-track {
    box-shadow: var(--shadow-hover);
  }
  .range:active::-moz-range-track,
  .field input[type="range"]:active::-moz-range-track {
    box-shadow: var(--shadow-active);
  }
  .range:disabled::-moz-range-track,
  .field input[type="range"]:disabled::-moz-range-track {
    box-shadow: var(--shadow-active);
  }

  .range::-webkit-slider-thumb,
  .field input[type="range"]::-webkit-slider-thumb {
    appearance: none;
    inline-size: var(--range-thumb-size);
    block-size: var(--range-thumb-size);
    margin-block-start: var(--range-thumb-gap);
    background-color: var(--ink);
    border: 0;
    border-radius: 0;
  }

  .range::-moz-range-thumb,
  .field input[type="range"]::-moz-range-thumb {
    inline-size: var(--range-thumb-size);
    block-size: var(--range-thumb-size);
    background-color: var(--ink);
    border: 0;
    border-radius: 0;
  }

  /* vertical stack of options */
  .control-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
  }
}
/* Definition-list stats — semantic key/value pairs. Wrap each pair in a <div> so
 * flex/grid can group dt+dd:
 *   <dl class="dl-stats">
 *     <div><dt>Market cap</dt><dd>$2.9T</dd></div>
 *     <div><dt>P/E</dt><dd>31.4</dd></div>
 *   </dl> */

@layer bleed.components {
  .dl-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: var(--border-w);
    margin: 0;

    border: var(--border-w) var(--border-style) var(--ink);
    background-color: var(--ink); /* shows through the gap as gridlines */
    border-radius: var(--radius);
    overflow: hidden;
  }

  .dl-stats > div {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    background-color: var(--paper);
  }

  .dl-stats dt {
    font-size: var(--font-size-1);
    font-family: var(--font-body);
    line-height: var(--line-height-normal);
    letter-spacing: var(--letter-spacing-normal);
    font-weight: var(--font-weight-normal);
    color: var(--neutral-strong);
  }

  .dl-stats dd {
    margin: 0;
    font-size: var(--font-size-3);
    font-family: var(--font-body);
    line-height: var(--line-height-normal);
    letter-spacing: var(--letter-spacing-normal);
    font-weight: var(--font-weight-normal);
    font-variant-numeric: tabular-nums;
  }

  /* inline variant: label left, value right, one per row */
  .dl-inline {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-2) var(--space-4);
    margin: 0;
  }
  .dl-inline dt {
    color: var(--neutral-strong);
  }
  .dl-inline dd {
    margin: 0;
    text-align: end;
    font-size: var(--font-size-1);
    font-family: var(--font-body);
    line-height: var(--line-height-normal);
    letter-spacing: var(--letter-spacing-normal);
    font-weight: var(--font-weight-normal);
    font-variant-numeric: tabular-nums;
  }
}
/* Empty state — <div class="empty"> with an optional icon, title, text, action.
 *   <svg class="empty__icon icon" aria-hidden="true"><use href="#icon-inbox"/></svg> */

@layer bleed.components {
  .empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    padding: var(--space-6);
    text-align: center;

    border: var(--border-w) var(--border-style) var(--neutral);
    border-radius: var(--radius);
    color: var(--ink);
  }

  .empty__icon {
    inline-size: var(--space-6);
    block-size: var(--space-6);
  }

  .empty__title {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--font-size-3);
  }

  .empty__text {
    margin: 0;
    max-inline-size: 40ch;
    font-size: var(--font-size-2);
  }

  .empty__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-block-start: var(--space-3);
  }
}
/* Form fields — wrap label + control + hint/error in .field.
 *   <div class="field">
 *     <label for="amt">Amount</label>
 *     <input id="amt" class="input" type="number" required>
 *     <p class="field__hint">Max $10,000</p>
 *     <p class="field__error">Enter a valid amount</p>
 *   </div>
 * Validation is native + CSS: :user-invalid fires only AFTER interaction (not on
 * first paint), and .field:has(:user-invalid) reveals the error + tints the label.
 *
 * For a group of checkboxes/radios, use <fieldset class="field"> + <legend
 * class="field__label"> instead of a bare div + span — same visuals, correct
 * group semantics:
 *   <fieldset class="field">
 *     <legend class="field__label">Notifications</legend>
 *     <div class="control-list">…</div>
 *   </fieldset> */

@layer bleed.components {
  .field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-inline-size: 0;
    width: 100%;
    margin: 0;
    padding: 0;
    border: none;
  }

  .field > label,
  .field__label {
    font-size: var(--font-size-1);
    line-height: var(--line-height-snug);
    padding: 0;
  }

  /* <legend> never participates in a fieldset's flex/grid layout in any
   * browser — the rest of the children get wrapped in an implicit box that
   * the fieldset's `gap` applies within, so it never reaches the legend.
   * Margin is the only way to space a legend from what follows it. */
  fieldset.field > legend {
    margin-block-end: var(--space-2);
  }

  /* The control's shadow is painted outside its border box, so text sitting one
   * `gap` below the box only clears the shadow by `gap - shadow-offset`. Hint and
   * error add that offset back, restoring a full gap below the shadow itself. */
  .field__hint,
  .field__error {
    margin-block-start: var(--space-1);
    font-size: var(--font-size-1);
    line-height: var(--line-height-snug);
  }
  /* …but only to clear a control. Text casts no shadow, so a hint and an error
   * stacked together are already a full gap apart. */
  :is(.field__hint, .field__error) + :is(.field__hint, .field__error) {
    margin-block-start: 0;
  }

  .field__hint {
    color: var(--neutral-strong);
  }

  .field__error {
    color: var(--negative-strong);
  }
  /* Error is hidden until the control is invalid post-interaction (:user-invalid),
   * OR forced open via [aria-invalid="true"] — the latter is how server-side
   * validation surfaces an error immediately, without waiting for interaction. */
  .field:not(:has(:user-invalid)):not(:has([aria-invalid="true"])) .field__error {
    display: none;
  }
  .field:has(:user-invalid) > label,
  .field:has(:user-invalid) .field__label,
  .field:has([aria-invalid="true"]) > label,
  .field:has([aria-invalid="true"]) .field__label {
    color: var(--negative-strong);
  }
  /* Error remaps the field's neutrals onto the negative ramp, so the control
   * text, border, and addons all shift together without per-element overrides. */
  .field:has(:user-invalid),
  .field:has([aria-invalid="true"]) {
    --ink: var(--negative-strong);
    --paper: var(--negative-subtle);
    --neutral-subtle: var(--negative-subtle);
  }

  /* The shadow does NOT follow that remap: a custom property substitutes its own
   * var()s where it is declared, so --shadow baked in the theme's --ink back at
   * :root and children inherit the finished value. Recolouring it means restating
   * the triple — on the field (so .input-group, which casts the shadow for an
   * adorned control, picks it up) and on a bare .input outside any .field. */
  .field:has(:user-invalid),
  .field:has([aria-invalid="true"]),
  .input:is(:user-invalid, [aria-invalid="true"]) {
    --shadow: var(--space-1) var(--space-1) 0 var(--negative-strong);
    --shadow-hover: calc(var(--space-1) / 2) calc(var(--space-1) / 2) 0 var(--negative-strong);
    --shadow-active: 0 0 0 var(--negative-strong);
  }

  /* --- text-like controls ------------------------------------------------- */
  .input,
  .field :is(input, textarea):not(:where([type="checkbox"], [type="radio"], [type="range"])) {
    inline-size: 100%;
    column-gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    font: inherit;
    color: var(--ink);
    background-color: var(--paper);
    border: var(--border-w) var(--border-style) var(--ink);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    transition:
      border-color var(--transition),
      translate var(--transition),
      box-shadow var(--transition);
  }

  .input::placeholder,
  .field :is(input, textarea)::placeholder {
    color: var(--neutral);
  }

  /* Date-like controls (date, datetime-local, month, time, week) lay their
   * segments out in a shadow wrapper that the UA sheet pads by 1px block-wise —
   * 2px of extra inner height that pushes them a hair taller than every other
   * control on the same row. Zeroing it lands them back on the shared height. */
  .input::-webkit-datetime-edit-fields-wrapper,
  .field input::-webkit-datetime-edit-fields-wrapper {
    padding-block: 0;
  }

  .field textarea {
    min-block-size: calc(var(--space-9) + var(--space-4));
    resize: vertical;
  }

  /* focus/press — the .btn contract, with :focus standing in for :hover: focus
   * moves the control into its shadow, :active presses it the rest of the way. */
  :is(.input, .field input, .field textarea):focus {
    translate: var(--hover-shift-x) var(--hover-shift-y);
    box-shadow: var(--shadow-hover);
  }
  :is(.input, .field input, .field textarea):active {
    translate: calc(var(--hover-shift-x) * 2) calc(var(--hover-shift-y) * 2);
    box-shadow: var(--shadow-active);
  }

  :is(.input, .field input, .field textarea):user-invalid,
  :is(.input, .field input, .field textarea)[aria-invalid="true"] {
    border-color: var(--negative-strong);
    background-color: var(--negative-subtle);
  }

  /* disabled drops to the pressed shadow, matching .btn:disabled */
  :is(.input, .field input, .field textarea):disabled {
    opacity: 0.5;
    cursor: not-allowed;
    background-color: var(--neutral-subtle);
    box-shadow: var(--shadow-active);
    translate: none;
  }

  /* Custom select — no wrapper. appearance:none + a two-tone linear-gradient
   * chevron drawn straight on the select's own background is the universal
   * baseline (works in Safari, which has no base-select support yet); unlike
   * a background-image url(), a gradient is native CSS so var(--ink) still
   * follows the theme. Where appearance:base-select IS supported (Chromium
   * 135+), it takes over: the native ::picker-icon gets the same text-glyph
   * treatment, and the gradient is switched off.
   *   <select class="input">…</select> */
  select.input {
    --_chevron-arm: 5px;

    appearance: none;
    align-items: center;
    column-gap: var(--space-3);
    cursor: pointer;
    padding-inline-end: var(--space-4);
    background-image: linear-gradient(45deg, transparent 50%, var(--ink) 50%), linear-gradient(135deg, var(--ink) 50%, transparent 50%);
    background-position:
      calc(100% - var(--space-4) - var(--_chevron-arm)) center,
      calc(100% - var(--space-4)) center;
    background-size: var(--_chevron-arm) var(--_chevron-arm);
    background-repeat: no-repeat;
  }

  /* open flips the chevron to point at the list — mirrored gradients here, and a
   * scale on the glyph below; both say the same thing the trigger button does. */
  @supports not (appearance: base-select) {
    select.input:open {
      background-image: linear-gradient(135deg, transparent 50%, var(--ink) 50%), linear-gradient(45deg, var(--ink) 50%, transparent 50%);
    }
  }

  @supports (appearance: base-select) {
    select.input {
      appearance: base-select;
      background-image: none;
    }
    select.input::picker-icon {
      content: "";
      flex-shrink: 0;
      inline-size: calc(var(--space-4) + var(--space-1));
      block-size: calc(var(--space-4) + var(--space-1));
      background-color: var(--ink);
      mask: var(--icon-chevron-down) center / contain no-repeat;
      transition: scale var(--transition);
    }
    select.input:open::picker-icon {
      scale: 1 -1;
    }
  }

  select.input:user-invalid,
  select.input[aria-invalid="true"] {
    border-color: var(--negative-strong);
    background-color: var(--negative-subtle);
  }

  select.input:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    background-color: var(--neutral-subtle);
    box-shadow: var(--shadow-active);
    translate: none;
  }

  /* Input adornments. Leading and/or trailing addons flank the control — a
   * static <span>, a <select>, or a pressable <button class="btn">/icon
   * button, any of them tagged .input-group__addon:
   *   <div class="input-group"><span class="input-group__addon">$</span><input class="input"></div>
   *   <div class="input-group"><input class="input"><span class="input-group__addon">USD</span></div>
   *   <div class="input-group"><button class="btn input-group__addon" type="button">−</button><input class="input"><button class="btn input-group__addon" type="button">+</button></div> */
  /* The joined control + addon(s) cast one shadow as a unit — a shadow on the
   * input alone would fall across the addon it is butted up against. */
  .input-group {
    display: flex;
    align-items: stretch;
    min-inline-size: 0;
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    transition:
      translate var(--transition),
      box-shadow var(--transition);
  }
  /* the group owns the press, so focusing any control inside moves the whole
   * unit — control plus addons — instead of sliding the input off its addon */
  .input-group:has(.input:focus),
  .input-group:has(.btn:is(:hover, :focus-visible)) {
    translate: var(--hover-shift-x) var(--hover-shift-y);
    box-shadow: var(--shadow-hover);
  }
  .input-group:has(.input:active),
  .input-group:has(.btn:active) {
    translate: calc(var(--hover-shift-x) * 2) calc(var(--hover-shift-y) * 2);
    box-shadow: var(--shadow-active);
  }
  /* the inner control stays put and casts nothing — stated for the focus/press
   * states too, so it never depends on source order */
  .input-group .input,
  .input-group .input:is(:focus, :active),
  .input-group .btn,
  .input-group .btn:is(:hover, :focus-visible, :active) {
    translate: none;
    box-shadow: none;
  }
  .input-group > .input:not(.input-group__addon) {
    inline-size: 100%;
    min-inline-size: 0;
  }
  .input-group > .input-group__addon {
    display: inline-flex;
    flex-shrink: 0;
    align-items: center;
    padding-inline: var(--space-3);
    color: var(--ink);
    background-color: var(--paper);
    border: var(--border-w) var(--border-style) var(--ink);
  }
  .input-group > .input-group__addon:is(select.input, .btn):is(:hover, :focus) {
    background-color: var(--neutral-subtle);
  }
  .input-group > .input-group__addon,
  .input-group > .input:not(.input-group__addon) {
    border-radius: 0;
    border-inline-end: none;
  }
  /* leading addon: joins the input's start edge */
  .input-group > .input-group__addon:first-child,
  .input-group > .input:not(.input-group__addon):first-child {
    border-start-start-radius: var(--radius);
    border-end-start-radius: var(--radius);
  }
  /* trailing addon: joins the input's end edge */
  .input-group > .input-group__addon:last-child,
  .input-group > .input:not(.input-group__addon):last-child {
    border-start-end-radius: var(--radius);
    border-end-end-radius: var(--radius);
    border-inline-end: var(--border-w) var(--border-style) var(--ink);
  }
}
/* Grid — the layout primitive: auto-fit tiles with opt-in spans.
 *   <div class="grid">
 *     <section class="card" data-span="2">…</section>
 *     <section class="card" data-span="full">…</section>
 *   </div>
 * The grid sizes itself from its own width, never the viewport, so one nested
 * inside a narrow panel collapses the same way the page does. */

@layer bleed.components {
  .grid {
    --_min: var(--grid-min, 24rem);
    /* --grid-gap sets both axes; --grid-row-gap / --grid-col-gap override it per axis */
    --_gap: var(--grid-gap, var(--space-4));

    container-type: inline-size;
    display: grid;
    /* min() keeps a track from overflowing a container narrower than --_min */
    grid-template-columns: repeat(auto-fit, minmax(min(var(--_min), 100%), 1fr));
    row-gap: var(--grid-row-gap, var(--_gap));
    column-gap: var(--grid-col-gap, var(--_gap));
    align-items: start;
  }

  /* A span is a ceiling, not a promise. Spanning more tracks than the grid has
   * generates implicit columns and overflows, so a tile starts full-width and
   * only narrows to its span once that many tracks actually fit. Thresholds are
   * n × 24rem + gaps — override them alongside --grid-min. */
  .grid > [data-span] {
    grid-column: 1 / -1;
  }

  @container (min-width: 49rem) {
    .grid > [data-span="2"] {
      grid-column: span 2;
    }
  }

  @container (min-width: 74rem) {
    .grid > [data-span="3"] {
      grid-column: span 3;
    }
  }
}
/* Modal — native <dialog class="modal">, opened with showModal() (see runtime.js,
 * ~5 lines: wire [data-open-modal] / [data-close-modal]). Native gives focus trap,
 * Esc-to-close, inert background, and ::backdrop for free.
 *   <dialog class="modal">
 *     <form method="dialog" class="modal__wrap">
 *       <header class="modal__header"><h2 class="modal__title">…</h2>
 *         <button class="modal__close btn btn--icon btn--sm" data-close-modal aria-label="Close">
 *           <svg class="icon" aria-hidden="true"><use href="#icon-cross"/></svg>
 *         </button>
 *       </header>
 *       <div class="modal__body">…</div>
 *       <footer class="modal__footer"><button class="btn">Confirm</button></footer>
 *     </form>
 *   </dialog>
 * Add closedby="any" for light dismiss (click the backdrop to close).
 * Entry/exit animate via @starting-style + transition-behavior:allow-discrete.
 * Entry is two-beat: the shell fades/rises, then __body unfurls 0 → auto (one
 * --transition-duration later). Exit is a single fade — no reverse unfurl. */

@layer bleed.components {
  .modal {
    margin: auto auto calc(-1 * var(--border-w));
    padding: 0;
    inline-size: min(92vw, 34rem);
    overflow: hidden;
    color: var(--ink);
    background-color: var(--paper);
    border: var(--border-w) var(--border-style) var(--ink);
    border-radius: var(--radius);

    opacity: 0;
    translate: 0 var(--space-3);
    transition:
      opacity var(--transition),
      translate var(--transition),
      overlay var(--transition) allow-discrete,
      display var(--transition) allow-discrete;
  }

  .modal[open] {
    opacity: 1;
    translate: 0 0;
  }

  @starting-style {
    .modal[open] {
      opacity: 0;
      translate: 0 var(--space-3);
    }
  }

  .modal::backdrop {
    background-color: color-mix(in oklch, var(--ink) 50%, transparent);
    backdrop-filter: blur(2px); /* px-ok: optical blur radius, not a layout size */
  }

  .modal__wrap {
    display: flex;
    flex-direction: column;
    max-block-size: 85svh;
  }

  .modal__header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-5);
    border-block-end: var(--border-w) var(--border-style) var(--ink);
  }

  .modal__title {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--font-size-3);
  }

  /* Close is a plain .btn (btn--icon btn--sm) — the header only pushes it to the
   * end and nudges it up onto the title's first line; everything else about it is
   * the button contract. Identical to .alert__close and .toast__close. */
  .modal__close {
    margin-inline-start: auto;
    margin-inline-end: calc(-1 * var(--space-2));
    margin-block-start: calc(-1 * var(--space-1));
  }

  .modal__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    block-size: auto;
    max-block-size: 85svh;
    padding: var(--space-5);
    overflow: auto;
    /* the gutter is reserved up front, so the text doesn't jolt sideways the
     * moment the content outgrows the unfurling body and a scrollbar lands.
     * No-op where scrollbars overlay (macOS, touch) — nothing to reserve. */
    scrollbar-gutter: stable;
  }

  /* Animating to the `auto` keyword needs interpolate-size; where it's missing
   * block-size would snap to full while the padding still crawled, so those
   * browsers get the plain fade and no unfurl at all. */
  @supports (interpolate-size: allow-keywords) {
    .modal__body {
      --unfurl-delay: var(--transition-duration);
      --unfurl-duration: calc(var(--transition-duration) * 6);

      interpolate-size: allow-keywords;
      /* delayed one duration so the unfurl starts as the shell finishes fading;
       * padding rides along, else a collapsed body still shows 2×--space-5 */
      transition:
        block-size var(--unfurl-duration) var(--unfurl-delay) var(--transition-ease),
        max-block-size var(--unfurl-duration) var(--unfurl-delay) var(--transition-ease),
        padding-block calc(var(--unfurl-duration) / 2) var(--unfurl-delay) var(--transition-ease);
    }

    /* the dialog's subtree first renders when it opens, so descendants get their
     * own @starting-style pass — this is the collapsed beat */
    @starting-style {
      .modal[open] .modal__body {
        block-size: 0;
        max-block-size: 0;
        padding-block: 0;
      }
    }
  }

  .modal__footer {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-5);
    border-block-start: var(--border-w) var(--border-style) var(--ink);
  }
}
/* Popover / dropdown menu — the popover attribute (zero JS for open/close/light-
 * dismiss) + CSS anchor positioning. Primary use: a row actions menu.
 *   <button class="btn btn--icon" popovertarget="row-menu" aria-label="Row actions"
 *           style="anchor-name:--row1">
 *     <svg class="icon" aria-hidden="true"><use href="#icon-menu-meatball"/></svg></button>
 *   <div id="row-menu" popover class="menu" style="position-anchor:--row1">
 *     <button class="menu__item">Edit</button>
 *     <button class="menu__item">Duplicate</button>
 *     <button class="menu__item" aria-current="true">Pin</button>
 *     <hr>
 *     <button class="menu__item menu__item--danger">Delete</button>
 *   </div> */

@layer bleed.components {
  .menu {
    margin: 0;
    padding: var(--space-1) 0;
    min-inline-size: 11rem;
    color: var(--ink);
    background-color: var(--paper);
    border: var(--border-w) var(--border-style) var(--ink);
    border-radius: var(--radius);

    /* anchored below the trigger, aligned to its inline-end, flipping if clipped */
    position: fixed;
    position-area: block-end span-inline-start;
    margin-block-start: var(--space-2);
    position-try-fallbacks: flip-block, flip-inline;
  }

  /* entry animation */
  .menu {
    opacity: 0;
    transition:
      opacity var(--transition),
      translate var(--transition),
      overlay var(--transition) allow-discrete,
      display var(--transition) allow-discrete;
    translate: 0 calc(-1 * var(--space-2));
  }
  .menu:popover-open {
    opacity: 1;
    translate: 0 0;
  }
  @starting-style {
    .menu:popover-open {
      opacity: 0;
      translate: 0 calc(-1 * var(--space-2));
    }
  }

  /* An open menu holds its trigger in the hover beat. Needs the popover to be the
   * trigger's next sibling; :where() keeps :hover/:active winning. */
  .btn:where(:has(+ .menu:popover-open)) {
    translate: var(--hover-shift-x) var(--hover-shift-y);
    box-shadow: var(--_shadow-hover);
  }

  /* a chevron-down trigger points at the menu's state, like an open select */
  .btn[popovertarget] svg:has(use[href="#icon-chevron-down"]) {
    transition: scale var(--transition);
  }
  .btn:where(:has(+ .menu:popover-open)) svg:has(use[href="#icon-chevron-down"]) {
    scale: 1 -1;
  }

  .menu__item {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    inline-size: 100%;
    padding: var(--space-2) var(--space-3);

    font: inherit;
    text-align: start;
    text-decoration: none;
    color: var(--ink);
    background-color: transparent;
    border: none;
    border-radius: var(--radius);
    cursor: pointer;
  }
  .menu__item:hover,
  .menu__item:focus-visible {
    background-color: var(--neutral-subtle);
  }
  /* selected/current item — inverted so it reads at a glance, and stays inverted
   * on hover rather than falling back to the neutral hover fill */
  .menu__item[aria-current] {
    color: var(--paper);
    background-color: var(--ink);
  }
  .menu__item--danger {
    color: var(--negative-strong);

    &:hover,
    &:focus-visible {
      background-color: var(--negative-subtle);
    }
  }

  .menu hr {
    margin-block: var(--space-1);
    border: none;
    border-block-start: var(--border-w) var(--border-style) var(--neutral);
  }
}
/* Progress & capacity — native <progress> and <meter>.
 *   <progress class="progress" value="0.6"></progress>
 *   <meter class="meter meter--tone" value="82" min="0" max="100" low="50" high="80" optimum="30">
 * A plain <meter> fills with --ink. Add .meter--tone to colour by threshold:
 * the browser picks optimum / sub-optimum / sub-sub-optimum and we map those to
 * positive/warning/negative. Pseudo-elements are vendor-specific, hence the
 * duplicated blocks. */

@layer bleed.components {
  .progress,
  .meter {
    -webkit-appearance: none;
    appearance: none;
    inline-size: 100%;
    block-size: var(--space-5);
    border-block-end: var(--border-w) var(--border-style) var(--ink);
    border-inline-end: calc(var(--border-w) * 4) var(--border-style) var(--ink);
    border-radius: var(--radius);
    overflow: hidden;
  }

  /* --- <progress> --------------------------------------------------------- */
  .progress::-webkit-progress-bar {
    background: transparent;
  }
  .progress::-webkit-progress-value {
    background: var(--progress-color, var(--ink));
    transition: inline-size var(--transition);
  }
  .progress::-moz-progress-bar {
    background: var(--progress-color, var(--ink));
  }

  .progress--positive {
    --progress-color: var(--positive);
  }
  .progress--negative {
    --progress-color: var(--negative);
  }
  .progress--warning {
    --progress-color: var(--warning);
  }
  .progress--accent {
    --progress-color: var(--accent);
  }

  /* --- <meter> ------------------------------------------------------------ */
  .meter::-webkit-meter-inner-element {
    block-size: 100%;
  }
  .meter::-webkit-meter-bar {
    background: transparent;
    border: none;
  }
  /* Default: value bar is ink regardless of threshold. */
  .meter::-webkit-meter-optimum-value,
  .meter::-webkit-meter-suboptimum-value,
  .meter::-webkit-meter-even-less-good-value {
    background: var(--ink);
  }
  .meter::-moz-meter-bar {
    background: var(--ink);
  }

  /* .meter--tone: colour by optimum / low / high threshold. */
  .meter--tone::-webkit-meter-optimum-value {
    background: var(--positive);
  }
  .meter--tone::-webkit-meter-suboptimum-value {
    background: var(--warning);
  }
  .meter--tone::-webkit-meter-even-less-good-value {
    background: var(--negative);
  }

  .meter--tone:-moz-meter-optimum::-moz-meter-bar {
    background: var(--positive);
  }
  .meter--tone:-moz-meter-sub-optimum::-moz-meter-bar {
    background: var(--warning);
  }
  .meter--tone:-moz-meter-sub-sub-optimum::-moz-meter-bar {
    background: var(--negative);
  }

  /* labelled capacity row */
  .capacity {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: var(--space-2) var(--space-3);
  }
  .capacity > :is(.progress, .meter) {
    grid-column: 1 / -1;
  }
  .capacity__label {
    font-size: var(--font-size-1);
    line-height: var(--line-height-snug);
  }
  .capacity__value {
    font-size: var(--font-size-1);
    line-height: var(--line-height-snug);
    font-variant-numeric: tabular-nums;
    text-align: end;
  }
}
/* Skeleton loader — <div class="skeleton"> for a block, or add .skeleton--text on a
 * line, .skeleton--circle for an avatar. The loading cue is jittering film-noise
 * static: an oversized ::before carrying the feTurbulence noise is flung around in
 * steps via translate3d, and the block's overflow:hidden clips it back so the jumps
 * never expose an edge. Honoured by prefers-reduced-motion via base.css (it
 * neutralises animation-duration, leaving a still noise). */

@layer bleed.components {
  .skeleton {
    --noise: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.25' numOctaves='3' stitchTiles='stitch' /%3E%3CfeColorMatrix type='saturate' values='0' /%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' /%3E%3C/svg%3E");

    position: relative;
    display: block;
    min-block-size: calc(var(--space-4) + var(--space-1));
    border-radius: var(--radius);
    background-color: var(--neutral-subtle);
    overflow: hidden;
  }

  /* Inset past every edge by more than the animation's peak travel (9rem), so a
   * translate never drags a bare corner into view; the parent clips it back. */
  .skeleton::before {
    content: "";
    position: absolute;
    inset: -10rem;
    background-image: var(--noise);
    background-repeat: repeat;
    background-size: 130px 130px;
    opacity: 0.5;
    pointer-events: none;
    animation: skeleton-noise 1s steps(2) infinite;
  }

  .skeleton--text {
    border-radius: var(--radius);
  }
  .skeleton--text:last-child { inline-size: 70%; }

  .skeleton--circle {
    border-radius: 50%;
    aspect-ratio: 1;
    inline-size: calc(var(--space-6) + var(--space-3));
  }

  @keyframes skeleton-noise {
    0%   { transform: translate3d(0, 9rem, 0); }
    10%  { transform: translate3d(-1rem, -4rem, 0); }
    20%  { transform: translate3d(-8rem, 2rem, 0); }
    30%  { transform: translate3d(9rem, -9rem, 0); }
    40%  { transform: translate3d(-2rem, 7rem, 0); }
    50%  { transform: translate3d(-9rem, -4rem, 0); }
    60%  { transform: translate3d(2rem, 6rem, 0); }
    70%  { transform: translate3d(7rem, -8rem, 0); }
    80%  { transform: translate3d(-9rem, 1rem, 0); }
    90%  { transform: translate3d(6rem, -5rem, 0); }
    to   { transform: translate3d(-7rem, 0, 0); }
  }
}
/* Data table — real <table>. Sticky header, tabular numerics, zebra, :has() row
 * states. Wrap in .table-scroll for a bounded scroll region that scrolls both
 * axes — sticky header down, columns across.
 *   <div class="table-scroll"><table class="table table--zebra"> … </table></div>
 * Mark numeric columns with class="num" on the <th>/<td> to right-align + tabular.
 * Add table--sticky-foot to pin the totals row, and class="solid" on a cell or
 * row to fill it with ink. */

@layer bleed.components {
  .table-scroll {
    overflow: auto;
    min-inline-size: 0;
    max-inline-size: 100%;
    overscroll-behavior-inline: contain;
    /* set --table-scroll-max-h: none to scroll across only and let the table
     * run its full height */
    max-block-size: var(--table-scroll-max-h, 70svh);
    border-radius: var(--radius);
  }

  .table-scroll > .table {
    min-inline-size: max-content;
  }

  .table {
    inline-size: 100%;
    /* separate, not collapse: a collapsed border belongs to the table rather
     * than the cell, so the sticky head's rule scrolls away without it */
    border-collapse: separate;
    border-spacing: 0;
    font-variant-numeric: tabular-nums;
  }

  .table :is(th, td) {
    padding: var(--space-2) var(--space-3);
    text-align: start;
    font-size: var(--font-size-1);
    font-family: var(--font-body);
    line-height: var(--line-height-normal);
    letter-spacing: var(--letter-spacing-normal);
    font-weight: var(--font-weight-normal);
  }

  .table thead th {
    position: sticky;
    inset-block-start: 0;
    z-index: 1;
    background-color: var(--paper);
    border-block-end: var(--border-w) var(--border-style) var(--ink);
  }

  .table tfoot :is(th, td) {
    background-color: var(--paper);
    border-block-start: var(--border-w) var(--border-style) var(--ink);
    border-block-end: none;
  }

  /* sticky footer row */
  .table--sticky-foot tfoot :is(th, td) {
    position: sticky;
    inset-block-end: 0;
    z-index: 1;
  }

  /* solid cell */
  .table :is(th, td).solid,
  .table tr.solid :is(th, td) {
    color: var(--paper);
    background-color: var(--ink);
    border-color: var(--ink);
  }

  /* numeric column: right-align + align digits */
  .table :is(th, td).num,
  .table :is(th, td)[data-type="number"] {
    text-align: end;
    font-variant-numeric: tabular-nums;
  }

  /* zebra */
  .table--zebra tbody tr:nth-child(even) {
    background-color: color-mix(in oklch, var(--ink) 6%, var(--paper));
  }

  /* interactive rows (:has powers hover + selection without JS classes) */
  .table--rows tbody tr:hover {
    background-color: color-mix(in oklch, var(--ink) 12%, var(--paper));
  }
  .table tbody tr:has(:checked) {
    background-color: color-mix(in oklch, var(--ink) 18%, var(--paper));
  }

  .table caption {
    padding-block-end: var(--space-2);
    font-family: var(--font-display);
    text-align: start;
  }
}
/* Tabs & segmented control — the ONE JS component (see runtime.js: role=tablist +
 * arrow-key roving focus). No radio hack — that breaks the a11y model.
 *
 * Segmented (boxed) — primary use is a period switcher:
 *   <div class="segmented" role="tablist" aria-label="Range">
 *     <button role="tab" aria-selected="true">1D</button>
 *     <button role="tab" aria-selected="false" tabindex="-1">1W</button> …
 *   </div>
 *
 * Tabs (standard) — .tabs__list[role=tablist] + .tabs__tab, with
 *   <section role="tabpanel">…</section> panels (add hidden to inactive ones).
 *   A tab may lead with .tabs__icon (an .icon or any raw SVG, aria-hidden). */

@layer bleed.components {
  /* --- segmented ---------------------------------------------------------- */
  /* The shadow sits on the group, not the individual tabs — the segmented control
   * reads as one pressable unit. No overflow:hidden: it would crop the focus outline. */
  .segmented {
    /* the tabs sit inside the group's border, so their corner is one border-width
     * tighter than the group's; floored at 0 for the square themes */
    --_tab-radius: max(0px, calc(var(--radius) - var(--border-w)));

    width: fit-content;
    display: flex;
    border: var(--border-w) var(--border-style) var(--ink);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    transition:
      translate var(--transition),
      box-shadow var(--transition);
  }

  /* The group is the pressable unit, so a tab's hover/press plays out on the whole
   * segmented — the .btn contract one level up. */
  .segmented:has(> [role="tab"]:is(:hover, :focus-visible)) {
    translate: var(--hover-shift-x) var(--hover-shift-y);
    box-shadow: var(--shadow-hover);
  }
  .segmented:has(> [role="tab"]:active) {
    translate: calc(var(--hover-shift-x) * 2) calc(var(--hover-shift-y) * 2);
    box-shadow: var(--shadow-active);
  }

  .segmented > [role="tab"] {
    appearance: none;
    padding: var(--space-2) var(--space-4);
    font: inherit;
		text-decoration: none;
    color: var(--ink);
    background-color: var(--paper);
    border: none;
    border-inline-end: var(--border-w) var(--border-style) var(--ink);
    cursor: pointer;
    transition:
      background-color var(--transition),
      color var(--transition);
  }
  /* A single tab matches both, so it rounds on all four corners. */
  .segmented > [role="tab"]:first-child {
    border-start-start-radius: var(--_tab-radius);
    border-end-start-radius: var(--_tab-radius);
  }
  .segmented > [role="tab"]:last-child {
    border-inline-end: none;
    border-start-end-radius: var(--_tab-radius);
    border-end-end-radius: var(--_tab-radius);
  }
  .segmented > [role="tab"]:hover {
    background-color: var(--neutral-subtle);
  }
  .segmented > [role="tab"][aria-selected="true"] {
    color: var(--paper);
    background-color: var(--ink);
  }

  /* --- standard ----------------------------------------------------------- */
  .tabs__list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding-inline-start: var(--space-2);
    border-block-end: var(--border-w) var(--border-style) var(--ink);
  }

  .tabs__tab {
    appearance: none;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    font: inherit;
		text-decoration: none;
    color: var(--ink);
    background-color: transparent;
    border: var(--border-w) var(--border-style) transparent;
    margin-block-end: calc(-1 * var(--border-w));
    cursor: pointer;
    transition:
      margin-block-start var(--transition),
      box-shadow var(--transition),
      padding-block-end var(--transition);
  }
  .tabs__icon {
    inline-size: calc(var(--space-4) + var(--space-1));
    block-size: calc(var(--space-4) + var(--space-1));
  }

  .tabs__tab:not([aria-selected="true"], [aria-current]):hover {
    color: var(--ink);
    border: var(--border-w) var(--border-style) var(--ink);
    box-shadow: var(--shadow-hover);
    margin-block-start: calc(-1 * var(--space-2));
  }
  .tabs__tab:is([aria-selected="true"], [aria-current]):not([aria-current="false"]) {
    position: relative;
    color: var(--ink);
    background: var(--paper);
    border-color: var(--ink);
    box-shadow: var(--shadow);

    /* blend tab button with panel with a colored block instead of changing button border color for cleaner look */
    &::after {
      content: "";
      position: absolute;
      inset-block-end: calc(-1 * var(--border-w));
      inset-inline-start: 0px;
      inset-inline-end: 0px;
      display: block;
      width: 100%;
      height: var(--border-w);
      background-color: var(--paper);
    }
  }

  .tabs__panel {
    position: relative;
    overflow: auto;
    background: var(--paper);
    padding: var(--space-5);
    border: var(--border-w) var(--border-style) var(--ink);
    border-block-start: none;
  }
}
/* Toast — transient status. Each toast is an <output role="status"> (polite live
 * region) inside a fixed region; runtime.js appends one and auto-removes it.
 *   <div class="toast-region" id="toasts"></div>
 *   bleed.toast('Trade executed', { title: 'Filled', icon: 'tick', solid: true })
 * `icon` names a file in src/icons/, rendered as a #icon-<name> sprite reference.
 * Outlined on paper by default; add toast--solid to fill instead. Title
 * optional, message always present — mirrors the alert banner. */

@layer bleed.components {
  .toast-region {
    position: fixed;
    inset-block-end: var(--space-5);
    inset-inline-end: var(--space-5);
    z-index: 1000;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    inline-size: min(24rem, calc(100vw - var(--space-6)));
    pointer-events: none;
  }

  .toast {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);
    color: var(--toast-fg, var(--ink));
    background-color: var(--toast-bg, var(--paper));
    border: var(--border-w) var(--border-style) var(--ink);
    border-radius: var(--radius);

    /* entry; JS adds .toast--leaving to reverse before removal */
    animation: toast-in var(--transition) both;
  }

  /* fill — solid replaces the outlined-on-paper default */
  .toast--solid {
    --toast-bg: var(--ink);
    --toast-fg: var(--paper);
    /* the dismiss button now sits ON the fill, where its ink border and ink
     * shadow would disappear — flip both to paper. The shadow has to be restated
     * rather than recoloured: --shadow already substituted its var(--ink) back
     * at :root, so remapping ink here would not reach it. */
    --btn-fg: var(--paper);
    --btn-border: var(--paper);
    --btn-shadow: var(--space-1) var(--space-1) 0 var(--paper);
    --btn-shadow-hover: calc(var(--space-1) / 2) calc(var(--space-1) / 2) 0 var(--paper);
    --btn-shadow-active: 0 0 0 var(--paper);
  }

  .toast__icon {
    inline-size: calc(var(--space-4) + var(--space-1));
    block-size: calc(var(--space-4) + var(--space-1));
    margin-block-start: 1px; /* px-ok: optical alignment */
    color: currentColor;
  }

  .toast__title {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--font-size-1);
  }

  .toast__msg {
    margin: 0;
    font-size: var(--font-size-1);
  }

  /* Close is a plain .btn (btn--icon btn--sm) — the toast only pushes it to the
   * end and nudges it up onto the title's baseline, the way .alert__close does;
   * everything else about it is the button contract. */
  .toast__close {
    margin-inline-start: auto;
    margin-block-start: calc(-1 * var(--space-1));
  }

  .toast--leaving {
    animation: toast-out var(--transition) both;
  }

  @keyframes toast-in {
    from {
      opacity: 0;
      translate: var(--space-4) 0;
    }
    to {
      opacity: 1;
      translate: 0 0;
    }
  }
  @keyframes toast-out {
    from {
      opacity: 1;
      translate: 0 0;
    }
    to {
      opacity: 0;
      translate: var(--space-4) 0;
    }
  }
}
/* Tooltip — popover="hint" (non-modal, non-light-dismissing) + anchor positioning.
 *   <button popovertarget="tt" popovertargetaction="show"
 *           style="anchor-name:--tt-btn" aria-describedby="tt">?</button>
 *   <div id="tt" popover="hint" class="tooltip" style="position-anchor:--tt-btn">
 *     Annualised, net of fees
 *   </div>
 * Outlined on paper by default; add tooltip--solid to fill instead.
 * Opens above the anchor by default; set position-area inline to pick a side.
 * Keep tooltip content non-essential — hint popovers are supplementary. */

@layer bleed.components {
  .tooltip {
    margin: var(--space-2);
    max-inline-size: 22ch;
    padding: var(--space-1) var(--space-2);

    font-size: var(--font-size-1);
    color: var(--tooltip-fg, var(--ink));
    background-color: var(--tooltip-bg, var(--paper));
    border: var(--border-w) var(--border-style) var(--ink);
    border-radius: var(--radius);

    /* opens above the anchor by default; override position-area inline to move it */
    position: fixed;
    position-area: block-start;
    position-try-fallbacks: flip-block;

    opacity: 0;
    transition:
      opacity var(--transition),
      overlay var(--transition) allow-discrete,
      display var(--transition) allow-discrete;
  }

  /* fill — solid replaces the outlined-on-paper default */
  .tooltip--solid {
    --tooltip-bg: var(--ink);
    --tooltip-fg: var(--paper);
  }

  .tooltip:popover-open {
    opacity: 1;
  }
  @starting-style {
    .tooltip:popover-open {
      opacity: 0;
    }
  }
}
