/* Aktib shared web components.
 *
 * Load order on every surface: theme.css → (vendor CSS) → this file → the
 * page's own <style>. Everything here lives inside a cascade layer and the
 * page's styles do not, which is the whole trick: unlayered author styles beat
 * layered ones no matter the specificity, so a page overrides a shared
 * component with a plain `.btn { … }` — no !important, no selector inflation,
 * no ordering puzzle. That property is what lets the 3,950-line dashboard
 * stylesheet adopt this file without a single visual regression on day one.
 *
 * Internals use :where() so every rule here sits at specificity (0,1,0) and a
 * page-level `.thing` always wins.
 *
 * Requires the token contract in theme.css (guarded by `npm run brand:check`).
 */

@layer aktib.base, aktib.components, aktib.utilities;

@layer aktib.base {
  /* A button does not inherit typography by default, so every surface that
     styles buttons needs this. It belongs in the BASE layer specifically: an
     unlayered `button { font: inherit }` on the page beats every layered
     component rule regardless of specificity, which silently reverted
     .btn/.btn--mini typography to the inherited 16px/400 while their padding
     and radius applied normally. Here, aktib.components legitimately wins. */
  :where(button) {
    font: inherit;
    cursor: pointer;
  }

  /* Form controls, lifted from the admin console — it was the only surface in
     the product with one coherent control system covering hover, focus,
     placeholder and disabled, while the dashboard had grown three competing
     styles. Re-pointed onto the shared tokens. */
  :where(input, textarea, select) {
    color-scheme: inherit;
  }

  :where(
    input[type="text"], input[type="email"], input[type="password"],
    input[type="search"], input[type="tel"], input[type="url"],
    input[type="number"], input[type="date"], input[type="time"],
    input[type="datetime-local"], select, textarea
  ) {
    appearance: none;
    -webkit-appearance: none;
    min-height: 34px;
    padding: 0 var(--space-2);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--paper);
    color: var(--ink);
    font: inherit;
    font-size: var(--text-md);
    line-height: 1.2;
    transition:
      border-color var(--dur-fast) var(--ease-out),
      box-shadow var(--dur-fast) var(--ease-out);
  }

  :where(textarea) {
    min-height: 80px;
    padding: var(--space-1) var(--space-2);
    line-height: 1.45;
    resize: vertical;
  }

  /* Two gradients rather than an SVG data URI: it inherits currentColor, so
     the chevron follows the theme without a second asset. */
  :where(select) {
    padding-right: var(--space-5);
    background-image:
      linear-gradient(45deg, transparent 50%, currentColor 50%),
      linear-gradient(-45deg, transparent 50%, currentColor 50%);
    background-position: right 15px center, right 10px center;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat, no-repeat;
    cursor: pointer;
  }

  :where(input, select, textarea):hover:not(:disabled) {
    border-color: var(--ink-soft);
  }

  :where(input, select, textarea):focus {
    outline: none;
    border-color: var(--focus-color);
    box-shadow: var(--focus-ring);
  }

  :where(input, textarea)::placeholder {
    color: var(--ink-mute);
  }

  :where(input, select, textarea):disabled {
    opacity: 0.55;
    cursor: not-allowed;
  }

  /* One visible focus ring for everything else. The dashboard ringed focus in
     --lime, which measured ~1.4:1 on a paper card. */
  :where(a, button, [tabindex], [role="button"]):focus-visible {
    outline: 2px solid var(--focus-color);
    outline-offset: 2px;
    border-radius: var(--r-xs);
  }
}

@layer aktib.components {
  /* ---- Button ----
     One system. The dashboard ran two: `.btn` at pill radius and
     `.owner-mini-btn` at 8px, same height, different type — which read as two
     unrelated products on one screen. `.btn--mini` is the dense table-row
     size; the shape is shared. */
  :where(.btn) {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-1);
    min-height: 36px;
    padding: 0 var(--space-3);
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    background: var(--paper);
    color: var(--ink);
    font: inherit;
    font-size: var(--text-sm);
    font-weight: var(--w-bold);
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
    transition:
      border-color var(--dur-fast) var(--ease-out),
      background var(--dur-fast) var(--ease-out),
      transform var(--dur-fast) var(--ease-out);
  }

  :where(.btn):hover:not(:disabled) {
    border-color: color-mix(in srgb, var(--green) 35%, transparent);
    background: var(--card);
  }

  :where(.btn):active:not(:disabled) {
    transform: translateY(1px);
  }

  :where(.btn--primary) {
    border-color: transparent;
    background: var(--green);
    color: var(--paper-50);
  }

  :where(.btn--primary):hover:not(:disabled) {
    background: var(--green-2);
    border-color: transparent;
  }

  :where(.btn--quiet) {
    border-color: transparent;
    background: transparent;
  }

  :where(.btn--danger) {
    border-color: var(--tone-danger-line);
    color: var(--danger);
  }

  :where(.btn--danger):hover:not(:disabled) {
    background: var(--tone-danger-bg);
    border-color: var(--tone-danger-line);
  }

  /* Dense variant for table rows and ticket cards. */
  :where(.btn--mini) {
    min-height: 32px;
    padding: 0 var(--space-2);
    border-radius: var(--r-sm);
    font-size: var(--text-xs);
    font-weight: var(--w-heavy);
  }

  /* Icon-only. Square rather than pill so it reads as a control, not a label
     with the text missing; the accessible name lives in aria-label. */
  :where(.btn--icon) {
    display: inline-grid;
    place-items: center;
    width: 34px;
    height: 34px;
    min-height: 0;
    padding: 0;
    color: var(--green);
    font-weight: var(--w-black);
  }

  /* Disabled has to beat the danger variant, which is declared after it in
     source and at equal specificity — the exact bug that once left a disabled
     destructive button in full red beside a live one. Listing both selectors
     settles it by specificity instead of by source order. */
  :where(.btn):disabled,
  :where(.btn--danger):disabled {
    color: var(--ink-mute);
    border-color: var(--line-soft);
    background: transparent;
    cursor: not-allowed;
    transform: none;
  }

  /* ---- Card ----
     Elevation level 1: a background step and the lightest shadow, no border.
     A hairline around every card plus a heavy shadow is the generic-dashboard
     tell; a resting card is not floating, and if it needs an outline to read
     as its own thing then the spacing around it is wrong. Borders come back
     at level 3 (overlays), where an edge against arbitrary content is real. */
  :where(.card) {
    border-radius: var(--r-md);
    background: var(--card);
    box-shadow: var(--shadow-sm);
    padding: var(--space-3);
  }

  /* Roomy variant for cards that are read rather than scanned. */
  :where(.card--roomy) {
    padding: var(--space-4);
  }

  /* Level 2. Interactive cards lift on hover — a background and shadow shift,
     never a border appearing out of nowhere (which shifts layout by 1px and
     reads as a glitch). */
  :where(.card--interactive) {
    cursor: pointer;
    transition:
      box-shadow var(--dur-fast) var(--ease-out),
      transform var(--dur-fast) var(--ease-out);
  }

  :where(.card--interactive):hover {
    box-shadow: var(--shadow-md);
    transform: translateY(-1px);
  }

  /* ---- Stat tile ----
     Replaces seven independent definitions of one idea (.kpi,
     .frontdesk-stat, .owner-stat, .ops-tile, .queue-metric, .modal-stat,
     .estimate-number) whose min-heights ran 58–104px and whose number sizes
     disagreed by 2px for no reason. */
  :where(.stat) {
    display: grid;
    align-content: center;
    gap: var(--space-0);
    min-height: 82px;
    /* 16px, not the 10px the dashboard used. Density comes from a consistent
       rhythm and from information per row, not from small numbers. */
    padding: var(--space-3);
    border-radius: var(--r-md);
    background: var(--card);
    /* Level 1. The tile is used standalone as well as composed with .card, so
       it carries its own resting elevation rather than depending on a wrapper.
       The definitions it replaces used a 1px border instead; surface, not
       stroke. */
    box-shadow: var(--shadow-sm);
  }

  /* first-child, not every span: a tile's third slot is often a delta or a
     note that happens to be a span ("Serving now"), and blanket-uppercasing
     it turns a sentence into a second competing heading. The label is always
     the first child. */
  :where(.stat) > :where(span:first-child, .stat__label) {
    font-size: var(--text-xs);
    font-weight: var(--w-heavy);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--muted);
  }

  :where(.stat) > :where(strong, .stat__value) {
    font-size: var(--text-3xl);
    font-weight: var(--w-heavy);
    line-height: 1.05;
    letter-spacing: -0.02em;
    color: var(--ink-display);
    /* Digits in a row of tiles must line up. */
    font-variant-numeric: tabular-nums;
  }

  :where(.stat--compact) {
    min-height: 58px;
    padding: var(--space-2);
  }

  :where(.stat--compact) > :where(strong, .stat__value) {
    font-size: var(--text-xl);
  }

  /* The note under the value. Sentence case on purpose: it is a phrase
     ("Payment proof and holds"), not a label, and uppercasing it makes the
     tile read as two competing headings. */
  :where(.stat) > :where(small, .stat__note) {
    font-size: var(--text-xs);
    font-weight: var(--w-bold);
    line-height: 1.2;
    color: var(--muted);
    text-transform: none;
    letter-spacing: 0;
  }

  :where(.stat--hero) {
    min-height: 104px;
  }

  :where(.stat--hero) > :where(strong, .stat__value) {
    font-size: var(--text-display);
  }

  /* Tone is carried by the surface. No rail, no outline. */
  :where(.stat[data-tone="positive"]) { background: var(--tone-positive-bg); }
  :where(.stat[data-tone="info"])     { background: var(--tone-info-bg); }
  :where(.stat[data-tone="warn"])     { background: var(--tone-warn-bg); }
  :where(.stat[data-tone="danger"])   { background: var(--tone-danger-bg); }

  /* Count-up target. The tile animates its number on first paint so a live
     figure reads as live; the final value is in the DOM from the start, so a
     reduced-motion or no-JS reader sees the real number immediately. */
  :where(.stat__value[data-countup]) {
    font-variant-numeric: tabular-nums;
  }

  /* ---- Status chip ----
     The ::before glyph is not decoration: it is what makes state readable
     without color, which is the single strongest accessibility decision on
     the dashboard. Any restyle keeps it. */
  :where(.chip) {
    display: inline-flex;
    align-items: center;
    gap: var(--space-0);
    padding: 3px var(--space-1);
    border-radius: var(--r-pill);
    background: var(--tone-neutral-bg);
    color: var(--tone-neutral-ink);
    font-size: var(--text-xs);
    font-weight: var(--w-heavy);
    line-height: 1.2;
    white-space: nowrap;
  }

  :where(.chip)::before {
    content: "\25CF";
    font-size: 0.8em;
    line-height: 1;
  }

  :where(.chip[data-tone="positive"]) { background: var(--tone-positive-bg); color: var(--tone-positive-ink); }
  :where(.chip[data-tone="info"])     { background: var(--tone-info-bg);     color: var(--tone-info-ink); }
  :where(.chip[data-tone="warn"])     { background: var(--tone-warn-bg);     color: var(--tone-warn-ink); }
  :where(.chip[data-tone="danger"])   { background: var(--tone-danger-bg);   color: var(--tone-danger-ink); }

  /* Distinct glyph per tone so the states differ in shape, not only in hue. */
  :where(.chip[data-tone="info"])::before   { content: "\25D1"; }
  :where(.chip[data-tone="warn"])::before   { content: "\25B2"; }
  :where(.chip[data-tone="danger"])::before { content: "\25C6"; }

  /* ---- Table scroller ----
     A six-column table pushed the whole page sideways on a phone because the
     wrapper looked like a scroll container but only carried padding. */
  :where(.table-wrap) {
    overflow-x: auto;
    min-width: 0;
  }

  :where(.table-wrap) table {
    min-width: 520px;
    width: 100%;
    border-collapse: collapse;
  }

  /* Level 0: rows are separated by one line BETWEEN them, never boxed. The
     last row has no trailing rule — a table should not look like it was cut
     off mid-thought. */
  :where(.table-wrap) :where(th, td) {
    padding: var(--space-2) var(--space-3);
    text-align: left;
    border-bottom: 1px solid var(--line-soft);
  }

  :where(.table-wrap) :where(th) {
    font-size: var(--text-xs);
    font-weight: var(--w-heavy);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--muted);
  }

  :where(.table-wrap) tbody tr:last-child :where(td) {
    border-bottom: 0;
  }

  /* Hover is a background wash. The old treatment added an inset 3px rail on
     the leading cell, which is the accent-rail tell in miniature. */
  :where(.table-wrap) tbody tr:hover :where(td) {
    background: var(--card);
  }

  /* ---- Toast ---- */
  :where(.toast) {
    position: sticky;
    top: var(--space-1);
    z-index: var(--z-toast);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--r-md);
    background: var(--paper);
    /* Level 3 on the ladder: a toast floats over page content, so it gets the
       overlay shadow rather than the resting one. */
    box-shadow: var(--shadow-lg);
    font-size: var(--text-sm);
    font-weight: var(--w-bold);
  }

  /* Routine status is the common case, so it stays calm — a filled tint on
     every "action applied" message would make the page shout constantly. */
  :where(.toast--info)    { background: var(--tone-neutral-bg);  color: var(--tone-neutral-ink); }
  :where(.toast--success) { background: var(--tone-positive-bg); color: var(--tone-positive-ink); }
  /* Error text takes the tone's INK, never its tint — a pale rose used as a
     foreground measured under 2:1 on paper. */
  :where(.toast--error)   { background: var(--tone-danger-bg);   color: var(--tone-danger-ink); }

  /* ---- Empty state ---- */
  :where(.empty-state) {
    display: grid;
    gap: var(--space-0);
    padding: var(--space-4) var(--space-3);
    /* The one place a border survives on a resting element: dashed means
       "nothing here yet", which is information, not decoration. */
    border: 1px dashed var(--line);
    border-radius: var(--r-md);
    color: var(--muted);
    font-size: var(--text-md);
    text-align: center;
  }

  :where(.empty-state) strong {
    display: block;
    color: var(--ink);
    font-size: var(--text-base);
  }

  /* ---- Popover ----
     Level 3, and one of the three places a border is still correct: an
     overlay lands on arbitrary content and needs a real edge. */
  :where(.popover) {
    z-index: var(--z-popover);
    padding: var(--space-3);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--paper);
    box-shadow: var(--shadow-lg);
  }

/* ---- Menu / select ----
     Anatomy from shadcn's Select: a trigger that states the current value with
     a low-contrast chevron, and a panel that fades and scales in from the side
     it opens on. The item's hover and keyboard focus share one treatment — an
     accent fill — so the pointer and the arrow keys highlight the same way. */
  :where(.menu-trigger) {
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-1);
    min-height: 36px;
    padding: 0 var(--space-2);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--paper);
    color: var(--ink);
    font: inherit;
    font-size: var(--text-md);
    font-weight: var(--w-bold);
    white-space: nowrap;
    cursor: pointer;
    transition:
      border-color var(--dur-fast) var(--ease-out),
      box-shadow var(--dur-fast) var(--ease-out);
  }

  :where(.menu-trigger):hover { border-color: var(--ink-soft); }

  :where(.menu-trigger)[aria-expanded="true"] {
    border-color: var(--focus-color);
    box-shadow: var(--focus-ring);
  }

  /* Chevron rotates to point at the open panel. */
  :where(.menu-trigger)::after {
    content: "";
    width: 7px;
    height: 7px;
    margin-left: var(--space-0);
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    opacity: 0.5;
    transform: translateY(-2px) rotate(45deg);
    transition: transform var(--dur-fast) var(--ease-out);
  }

  :where(.menu-trigger)[aria-expanded="true"]::after {
    transform: translateY(1px) rotate(-135deg);
  }

  :where(.menu-panel) {
    position: absolute;
    z-index: var(--z-popover);
    min-width: 200px;
    max-height: min(60vh, 420px);
    overflow-y: auto;
    padding: var(--space-0);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--paper);
    box-shadow: var(--shadow-lg);
    animation: aktib-menu-in var(--dur-fast) var(--ease-out);
  }

  @keyframes aktib-menu-in {
    from { opacity: 0; transform: translateY(-4px) scale(0.97); }
    to   { opacity: 1; transform: none; }
  }

  :where(.menu-group-label) {
    padding: var(--space-1) var(--space-1) var(--space-0);
    font-size: var(--text-xs);
    font-weight: var(--w-heavy);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    /* --muted, not --ink-mute: theme.css marks that one decorative-only and
       below AA on purpose, and a group heading is read. */
    color: var(--muted);
  }

  :where(.menu-item) {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    width: 100%;
    min-height: 34px;
    padding: var(--space-0) var(--space-1);
    border: 0;
    border-radius: var(--r-xs);
    background: transparent;
    color: var(--ink);
    font: inherit;
    font-size: var(--text-md);
    font-weight: var(--w-semibold);
    text-align: left;
    cursor: pointer;
  }

  /* Hover and keyboard focus are the same state on purpose: a menu you arrow
     through should highlight exactly as it does under the pointer. */
  :where(.menu-item):hover,
  :where(.menu-item):focus-visible {
    outline: none;
    background: var(--soft);
  }

  :where(.menu-item)[aria-current="true"] {
    background: var(--tone-positive-bg);
    color: var(--tone-positive-ink);
  }

  /* Checkmark occupies its gutter whether or not it is shown, so labels do not
     shift by 14px as the selection moves. */
  :where(.menu-item)::before {
    content: "";
    flex: none;
    width: 14px;
    text-align: center;
    font-weight: var(--w-heavy);
  }

  :where(.menu-item)[aria-current="true"]::before { content: "\2713"; }

  :where(.menu-item__badge) {
    margin-left: auto;
    font-size: var(--text-xs);
    font-weight: var(--w-heavy);
    color: var(--muted);
    font-variant-numeric: tabular-nums;
  }

  /* An item that carries a description stacks label over description. The
     wrapper is what makes the two lines a single flex child, so the checkmark
     gutter stays aligned to the label rather than centring on both lines. */
  :where(.menu-item__text) {
    display: grid;
    gap: 1px;
    min-width: 0;
  }

  :where(.menu-item__desc) {
    font-size: var(--text-xs);
    font-weight: var(--w-medium);
    line-height: 1.25;
    color: var(--muted);
    white-space: normal;
  }

  /* Keyboard hint, right-aligned like the badge. */
  :where(.menu-item__kbd) {
    margin-left: auto;
    padding: 1px var(--space-0);
    border-radius: var(--r-xs);
    background: var(--soft);
    color: var(--muted);
    font-size: var(--text-2xs);
    font-weight: var(--w-heavy);
  }

  /* A destructive item reads red at rest and fills on hover, so it cannot be
     mistaken for the row above it while arrowing through. */
  :where(.menu-item--danger) { color: var(--danger); }

  :where(.menu-item--danger):hover,
  :where(.menu-item--danger):focus-visible {
    background: var(--tone-danger-bg);
    color: var(--tone-danger-ink);
  }

  /* Listed after every variant so it wins by source order at equal
     specificity — same reason .btn:disabled repeats its selectors. */
  :where(.menu-item):disabled,
  :where(.menu-item--danger):disabled {
    color: var(--ink-mute);
    background: transparent;
    cursor: not-allowed;
  }

  :where(.menu-separator) {
    height: 1px;
    margin: var(--space-0) var(--space-1);
    background: var(--line-soft);
  }

  /* A trigger used as a form control fills its field rather than hugging its
     label, so a column of them lines up. There is no separate `.select`
     component: a select IS a menu trigger whose panel holds one choice set. */
  :where(.menu-trigger--block) {
    width: 100%;
    justify-content: space-between;
  }

  :where(.menu-trigger__value) {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* ---- Switch ----
     A binary state rendered as a real checkbox with role="switch": one tap
     instead of two, and the state is legible without opening anything. The
     dashboard modelled two of these as two-option <select>s ("Open now:
     Closed/Open"), which is a menu asking a yes/no question. */
  :where(.switch) {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    cursor: pointer;
  }

  :where(.switch__control) {
    appearance: none;
    -webkit-appearance: none;
    flex: none;
    position: relative;
    width: 40px;
    height: 24px;
    margin: 0;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    background: var(--sunk);
    cursor: pointer;
    transition:
      background var(--dur-fast) var(--ease-out),
      border-color var(--dur-fast) var(--ease-out);
  }

  /* The knob. Its travel is the whole affordance, so it animates even though
     the track colour change would be enough on its own. */
  :where(.switch__control)::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--paper);
    box-shadow: var(--shadow-sm);
    transition: transform var(--dur-fast) var(--ease-out);
  }

  :where(.switch__control):checked {
    border-color: transparent;
    background: var(--green);
  }

  :where(.switch__control):checked::after { transform: translateX(16px); }

  :where(.switch__control):disabled {
    cursor: not-allowed;
    opacity: 0.5;
  }

  /* The word next to the switch states the CURRENT value, not the action, so
     it never contradicts the knob. */
  :where(.switch__state) {
    font-size: var(--text-md);
    font-weight: var(--w-bold);
    color: var(--ink);
    font-variant-numeric: tabular-nums;
  }

  /* ---- Date picker ----
     Square day cells on a 7-column grid. Today is a quiet accent fill and the
     selected day is the solid one — the reverse reads as two selections. */
  :where(.datepicker) {
    display: grid;
    gap: var(--space-1);
    min-width: 248px;
    padding: var(--space-2);
  }

  :where(.datepicker__head) {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-1);
  }

  :where(.datepicker__month) {
    font-size: var(--text-md);
    font-weight: var(--w-bold);
  }

  :where(.datepicker__nav) {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border: 0;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--ink);
    font: inherit;
    cursor: pointer;
  }

  :where(.datepicker__nav):hover { background: var(--soft); }

  :where(.datepicker__grid) {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 2px;
  }

  :where(.datepicker__weekday) {
    padding-bottom: var(--space-0);
    font-size: var(--text-xs);
    font-weight: var(--w-medium);
    text-align: center;
    color: var(--muted);
  }

  :where(.datepicker__day) {
    aspect-ratio: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--ink);
    font: inherit;
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    cursor: pointer;
  }

  :where(.datepicker__day):hover { background: var(--soft); }

  :where(.datepicker__day)[data-outside="true"] {
    color: var(--ink-mute);
  }

  :where(.datepicker__day)[data-today="true"] {
    background: var(--soft);
    font-weight: var(--w-heavy);
  }

  :where(.datepicker__day)[aria-selected="true"] {
    background: var(--green);
    color: var(--paper-50);
    font-weight: var(--w-heavy);
  }

  :where(.datepicker__day)[data-has-events="true"]::after {
    content: "";
    position: absolute;
    transform: translateY(11px);
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: currentColor;
    opacity: 0.6;
  }

  :where(.datepicker__day) { position: relative; }

  /* ---- Skeleton ----
     The dashboard could not tell "your venue is empty" from "we have not
     loaded yet"; both rendered as an empty state. */
  :where(.skeleton) {
    border-radius: var(--r-sm);
    background:
      linear-gradient(90deg, var(--soft) 25%, var(--sunk) 37%, var(--soft) 63%)
      0 0 / 400% 100%;
    animation: aktib-skeleton 1.4s ease-in-out infinite;
  }

  @keyframes aktib-skeleton {
    to { background-position: -400% 0; }
  }
}

@layer aktib.utilities {
  .u-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  .u-tabular { font-variant-numeric: tabular-nums; }
  .u-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* The global reduced-motion contract. Outside a layer on purpose: this must
   beat any page rule that reintroduces animation. */
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; }
}
