/* OATR master components.
   Loaded last as a low-specificity base layer: route styles may own layout,
   density, and explicit states without accidentally forking shared material. */

/* ── Visually hidden ─────────────────────────────────────────────────────────
   Text for assistive technology only: a label on an icon-only control, a
   heading that orders a region, the word a screen reader needs and the layout
   does not have room for.

   The component set had no such utility, so the forum experiment reinvented it
   and needed it on twelve surfaces. That is the failure worth naming: without a
   shared one, every author writes their own, and the obvious attempts —
   display:none, visibility:hidden, width:0, font-size:0 — all remove the text
   from the accessibility tree, which is the exact opposite of the intent. This
   keeps the element rendered and reachable while occupying no visible space.

   clip-path over the legacy clip:rect(); both are honoured, only one is not
   deprecated. white-space:nowrap stops a long label being collapsed into a
   1px-wide column and read one character per line by some engines.

   No margin:-1px. The classic recipe carries it to stop the 1px box nudging
   layout, but that is only needed when the element stays in flow — this one is
   absolutely positioned, so the negative margin does nothing except bypass the
   spacing roles, which check-structure correctly rejects. */
.ui-sr-only:not(:focus):not(:active){
  position:absolute;
  inline-size:1px;block-size:1px;
  padding:0;
  overflow:hidden;
  clip-path:inset(50%);
  white-space:nowrap;
  border:0;
}

/* ── Timestamp ───────────────────────────────────────────────────────────────
   A moment in a metadata line: "3 ชม.", "2 วัน", "1 ปี".

   Its whole job is to not wrap. A relative time is a single indivisible fact —
   broken across two lines it reads as two facts, and in a meta row separated by
   middots the second half orphans onto a new line under a bullet that belongs
   to the item after it. The forum experiment needed this on eight surfaces
   including the shared card renderer, which is what made it a component rather
   than a page's business.

   Deliberately not a colour or a size: those belong to whatever row it sits in
   (.ui-meta already sets both), and fixing them here would stop it composing. */
.ui-time{white-space:nowrap}

/* Fixed Order Book Button master. Context styles may place or size a button,
   but material, type, spacing, radius, and interaction live here. */
:where(.btn,.public-button,.fc-btn,.button){
  min-block-size:var(--ct-floor);
  min-inline-size:var(--ct-floor);
  padding:var(--button-pad-block) var(--button-pad-inline);
  border:var(--hairline) solid transparent;
  border-radius:var(--button-radius);
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:var(--button-gap);
  font:var(--fw-medium) var(--ob-t--1)/var(--lh-solid) var(--font);
  white-space:nowrap;
  cursor:pointer;
  transition:
    transform var(--button-motion-duration) var(--ease-enter),
    background var(--button-motion-duration) var(--ease-enter),
    border-color var(--button-motion-duration) var(--ease-enter),
    box-shadow var(--button-motion-duration) var(--ease-enter),
    color var(--button-motion-duration) var(--ease-enter);
}
:where(.btn,.public-button,.fc-btn,.button):hover{
  transform:translateY(var(--button-lift-distance));
}
:where(.btn,.public-button,.fc-btn,.button):active{
  transform:translateY(0);
}
:where(.btn.pri,.public-button-primary,.fc-btn-primary,.button.primary){
  background:var(--button-primary-bg);
  color:var(--button-primary-fg);
  border-color:var(--button-primary-border);
  box-shadow:var(--button-primary-shadow);
  font-weight:var(--fw-semibold);
}
:where(.btn.pri,.public-button-primary,.fc-btn-primary,.button.primary):hover{
  box-shadow:var(--button-primary-shadow-hover);
}
:where(.btn.ghost,.public-button-secondary,.fc-btn-ghost,.button.ghost){
  background:var(--button-secondary-bg);
  color:var(--button-secondary-fg);
  border-color:var(--button-secondary-border);
}
:where(.btn.ghost,.public-button-secondary,.fc-btn-ghost,.button.ghost):hover{
  color:var(--button-secondary-fg-hover);
  border-color:var(--button-secondary-border-hover);
}
:where(.btn,.public-button,.fc-btn,.button).on{
  color:var(--accent);
  border-color:var(--accent);
  background:var(--accent-soft);
}
:where(.btn,.public-button,.fc-btn,.button):focus-visible{
  outline:var(--focus-width) solid var(--focus);
  outline-offset:var(--focus-offset);
}
:where(.btn,.public-button,.fc-btn,.button):disabled{
  opacity:var(--component-disabled-opacity);
  color:var(--component-disabled-fg);
  background:var(--component-disabled-bg);
  border-color:var(--component-disabled-border);
  box-shadow:none;
  cursor:not-allowed;
  transform:none;
}

/* Compact filter master. JavaScript progressively wraps every list/table
   filter in this disclosure so closed filters occupy one control row and the
   result surface remains visible. */
.compact-filter-host{
  position:relative;
  display:block;
  min-width:0;
  padding:0;
  border:0;
  background:transparent;
}
/* The open panel has z-index:var(--z-overlay-menu) below, but z-index only ever
   competes INSIDE a stacking context - and these hosts sit on toolbars that open
   one of their own (backdrop-filter does it on its own; the .rise entrance adds
   opacity and transform while it plays). Boxed in like that, the panel could not
   outrank anything outside the toolbar, so it opened UNDERNEATH the very table
   it filters. Lifting the host is what actually puts the panel on top, because
   the host is the box the page ranks.

   --z-popover, not --z-overlay-menu: this is a popover over page content, so it
   must clear the table and the cards while still passing beneath the sticky
   header (--z-sticky/--z-header) when the page scrolls. It lifts only while a
   panel is open, so a closed toolbar keeps its normal place in the stack. */
.compact-filter-host:has(.filter-disclosure[open]){
  z-index:var(--z-popover);
}
.content-toolbar:has(.filter-disclosure){
  gap:var(--sp-8);
  padding:var(--sp-8);
}
.filter-disclosure{
  position:relative;
  z-index:var(--z-content);
  display:block;
  inline-size:max-content;
  max-inline-size:100%;
}
.filter-disclosure[open]{z-index:var(--z-overlay-menu)}
.filter-disclosure-summary{
  min-block-size:var(--filter-disclosure-height);
  max-inline-size:100%;
  display:inline-flex;
  align-items:center;
  gap:var(--sp-8);
  padding-inline:var(--sp-12);
  border:var(--hairline) solid var(--oasis-filter-border);
  border-radius:var(--oasis-filter-radius);
  background:var(--oasis-filter-bg);
  color:var(--text);
  font:var(--fw-semibold) var(--oasis-filter-label-size)/var(--lh-solid) var(--font);
  list-style:none;
  white-space:nowrap;
  cursor:pointer;
}
.filter-disclosure-summary::-webkit-details-marker{display:none}
.filter-disclosure-summary:hover,
.filter-disclosure-summary:focus-visible,
.filter-disclosure[open]>.filter-disclosure-summary{
  border-color:var(--accent);
  background:var(--oasis-panel-bg-hover);
}
.filter-disclosure-summary:focus-visible{
  outline:var(--focus-width) solid var(--focus);
  outline-offset:var(--focus-offset);
}
.filter-disclosure-icon{
  inline-size:var(--sp-16);
  block-size:var(--sp-16);
  flex:0 0 var(--sp-16);
  fill:none;
  stroke:currentColor;
  stroke-width:1.75;
  stroke-linecap:round;
  stroke-linejoin:round;
}
.filter-disclosure-icon svg{inline-size:100%;block-size:100%}
.filter-disclosure-label{color:var(--text)}
.filter-disclosure-value{
  max-inline-size:16rem;
  overflow:hidden;
  color:var(--text-2);
  font-weight:var(--fw-regular);
  text-overflow:ellipsis;
  white-space:nowrap;
}
.filter-disclosure-value::before{
  content:"";
  display:inline-block;
  block-size:1em;
  margin-inline-end:var(--sp-8);
  border-inline-start:var(--hairline) solid var(--border);
  vertical-align:-.12em;
}
.filter-disclosure-chevron{
  inline-size:var(--sp-8);
  block-size:var(--sp-8);
  flex:0 0 var(--sp-8);
  border-inline-end:var(--hairline) solid currentColor;
  border-block-end:var(--hairline) solid currentColor;
  transform:rotate(45deg) translateY(calc(var(--sp-2) * -1));
  transition:transform var(--dur-fast) var(--ease-enter);
}
.filter-disclosure[open] .filter-disclosure-chevron{
  transform:rotate(225deg) translate(calc(var(--sp-2) * -1),calc(var(--sp-2) * -1));
}
.filter-disclosure-panel{
  position:absolute;
  inset-block-start:calc(100% + var(--sp-6));
  inset-inline-start:0;
  inline-size:var(--filter-disclosure-width);
  display:flex;
  flex-wrap:wrap;
  gap:var(--filter-disclosure-gap);
  max-inline-size:calc(100vw - (var(--density-page-inline) * 2));
  max-block-size:min(28rem,calc(100vh - var(--header-h) - var(--sp-32)));
  overflow:auto;
  padding:var(--filter-disclosure-panel-pad);
  border:var(--hairline) solid var(--border);
  border-radius:var(--radius-ctl);
  /* Solid, not glass. This panel opens ON TOP of a dense table, and --surface is
     72% white: the rows read straight through it. The blur that normally rescues
     a translucent surface cannot help here either - the toolbar behind it sets
     backdrop-filter, which makes it a backdrop root, so this panel's own blur
     only ever sees the toolbar and never the table it actually covers. A popover
     over content has to bring its own opaque ground. */
  background:var(--surface-solid);
  box-shadow:var(--filter-disclosure-shadow);
}
.filter-disclosure-panel .filter-chip,
.community-page .community-tabs .filter-disclosure-panel button,
.groups-page .groups-filters .filter-disclosure-panel button{
  width:auto;
  min-width:0;
  min-height:var(--filter-disclosure-height);
  flex:0 1 auto;
  padding:0 var(--oasis-filter-pad-inline);
  border:var(--hairline) solid var(--oasis-filter-border);
  border-radius:var(--oasis-filter-radius);
  background:var(--oasis-filter-bg);
  color:var(--oasis-filter-fg);
  font-size:var(--oasis-filter-label-size);
  white-space:nowrap;
}
.filter-disclosure-panel .filter-chip.on,
.filter-disclosure-panel .filter-chip[aria-pressed="true"],
.community-page .community-tabs .filter-disclosure-panel button.is-active,
.groups-page .groups-filters .filter-disclosure-panel button.is-active{
  border-color:transparent;
  background:var(--oasis-filter-active-bg);
  color:var(--oasis-filter-active-fg);
}
.copytrade-filter-panel.compact-filter-host{
  display:block;
  padding:0;
  border:0;
  background:transparent;
}
.copytrade-filter-panel .filter-disclosure-panel{
  display:grid;
  gap:var(--sp-8);
}
.filter-disclosure-panel .copytrade-filter-row{
  gap:var(--filter-disclosure-gap);
  padding:0;
  border:0;
}
.filter-disclosure-panel .copytrade-filter-meta{
  width:100%;
  margin:0;
  padding-top:var(--sp-6);
  border-top:var(--hairline) solid var(--border-soft);
}
.leaderboard-page .leaderboard-metrics.compact-filter-host{
  display:block;
  border:0;
  background:transparent;
}
.leaderboard-page .leaderboard-workspace{padding-block:var(--sp-24) var(--sp-40)}
.leaderboard-page .leaderboard-section-head{margin-bottom:var(--sp-8)}
.leaderboard-page .leaderboard-controls{min-height:0;padding-block:var(--sp-8)}
.leaderboard-page .leaderboard-metrics .filter-disclosure-panel{
  display:grid;
  grid-template-columns:repeat(2,minmax(0,1fr));
  gap:var(--filter-disclosure-gap);
}
.leaderboard-page .leaderboard-metrics .filter-disclosure-panel button{
  min-height:var(--filter-disclosure-height);
  gap:var(--sp-8);
  padding:0 var(--sp-10);
  border:var(--hairline) solid var(--oasis-filter-border);
  border-radius:var(--oasis-filter-radius);
  background:var(--oasis-filter-bg);
}
.leaderboard-page .leaderboard-metrics .filter-disclosure-panel button[aria-pressed="true"]{
  border-color:transparent;
  background:var(--oasis-filter-active-bg);
  color:var(--oasis-filter-active-fg);
}
.leaderboard-page .leaderboard-metrics .filter-disclosure-panel small{display:none}
.leaderboard-page .leaderboard-metrics .filter-disclosure-panel .metric-code{
  width:var(--sp-24);
  height:var(--sp-24);
  flex-basis:var(--sp-24);
}
.groups-page .compact-filter-surface .groups-filter-head{display:none}
.groups-page .compact-filter-surface.groups-filter-panel{padding-block:var(--sp-8)}
.groups-page .groups-filters.compact-filter-host{display:block}
.groups-page .groups-filters .filter-disclosure-panel{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}
.groups-page .groups-filters .filter-disclosure-panel .groups-filter-meta{grid-column:1/-1;min-height:var(--touch-target);margin-top:var(--sp-4)}
.groups-page .groups-filters .filter-disclosure-panel button{
  justify-content:flex-start;
  flex-direction:row;
  gap:var(--sp-8);
  text-align:start;
}
.community-page .community-tabs.compact-filter-host{display:block;padding-block:var(--sp-8)}
.news-page .category-board-heading{
  display:block;
  padding:var(--sp-12) var(--sp-16);
  border-block-end:var(--hairline) solid var(--news-line);
  color:var(--accent);
  font-family:var(--brand);
  font-size:var(--fs-sm);
  font-weight:var(--fw-semibold);
  line-height:var(--lh-tight);
}
@media(max-width:32rem){
  .filter-disclosure{inline-size:100%}
  .filter-disclosure-summary{inline-size:100%;justify-content:flex-start}
  .filter-disclosure-value{margin-inline-start:auto}
  .filter-disclosure-panel{
    position:fixed;
    inset-inline:var(--sp-8);
    inset-block-start:auto;
    inset-block-end:calc(var(--sp-8) + env(safe-area-inset-bottom));
    inline-size:auto;
    max-inline-size:none;
    max-block-size:min(70vh,32rem);
  }
  .leaderboard-page .leaderboard-metrics .filter-disclosure-panel,
  .groups-page .groups-filters .filter-disclosure-panel{
    grid-template-columns:1fr;
  }
}
@media(prefers-reduced-motion:reduce){
  .filter-disclosure-chevron{transition:none}
}

/* Fixed Order Book Card / Proof-panel master. Layout remains contextual, while
   the paper, edge, radius, elevation, and honest hover affordance propagate. */
:where(.ob-card,.component-card,.widget,.fc-cardmsg,.public-product){
  --card-pad-block:var(--component-card-pad-block);
  --card-pad-inline:var(--component-card-pad-inline);
  --card-gap:var(--component-card-gap);
  background:var(--component-card-bg);
  border:var(--hairline) solid var(--component-card-border);
  border-radius:var(--component-card-radius);
  box-shadow:var(--component-card-shadow);
}
:where(.ob-card,.component-card){
  padding:var(--card-pad-block) var(--card-pad-inline);
}
:where(.ob-card--proof,.component-card[data-presentation="proof"],.ob-book-panel){
  background:var(--component-proof-bg);
  border-radius:var(--component-proof-radius);
  box-shadow:var(--shadow-card),var(--shadow-float);
}
:where(.ob-card,.component-card,.widget)[data-interactive="true"]{
  transition:
    transform var(--dur-slow) var(--ease-enter),
    border-color var(--dur-slow) var(--ease-enter),
    box-shadow var(--dur-slow) var(--ease-enter);
}
:where(.ob-card,.component-card,.widget)[data-interactive="true"]:not(.sel):hover{
  transform:translateY(var(--card-hover-lift));
  border-color:var(--border);
  box-shadow:var(--shadow-hover);
}

/* Fixed Field/Input master. Product surfaces opt in with .ob-field; the named
   legacy selectors are retained while their markup is migrated. */
:where(.ob-field,.field-control,.d-search,.sym-search input,.cat-search,.mk-search,.tr-search,.silo-search,.m-filter-select select,
    .yr-note-field textarea,.cp-text,.cr-input,.cr-editor,.edit-row select,.mk-search input,.tr-search input,
    .auth-field input,.otp input,.dj-search input,.dj-detail-note textarea,
    .review-search input,.evidence-search input,.setup-form input,.setup-form textarea,.setup-form select,
    .studio-disclosures input,.studio-disclosures textarea,.friends-redeem input,.direction-reason input,
    .fc-pasteinput,.fc-composer textarea){
  color:var(--text);
  background:var(--component-field-bg);
  border:var(--hairline) solid var(--component-field-border);
  border-radius:var(--component-field-radius);
  font-family:var(--font);
}
:where(.ob-field,.field-control,.sym-search input,.silo-search,.yr-note-field textarea,.cp-text,.cr-input,.cr-editor,
    .mk-search input,.tr-search input,.auth-field input,.otp input,.dj-search input,.dj-detail-note textarea,
    .review-search input,.evidence-search input,.setup-form input,.setup-form textarea,
    .studio-disclosures input,.studio-disclosures textarea,.friends-redeem input,.direction-reason input,
    .fc-pasteinput,.fc-composer textarea)::placeholder{
  color:var(--text-2);
  opacity:var(--component-placeholder-opacity);
}
:where(.ob-field,.field-control,.d-search,.sym-search input,.cat-search,.mk-search,.tr-search,.silo-search,.m-filter-select select,
    .yr-note-field textarea,.cp-text,.cr-input,.cr-editor,.edit-row select,.mk-search input,.tr-search input,
    .auth-field input,.otp input,.dj-search input,.dj-detail-note textarea,
    .review-search input,.evidence-search input,.setup-form input,.setup-form textarea,.setup-form select,
    .studio-disclosures input,.studio-disclosures textarea,.friends-redeem input,.direction-reason input,
    .fc-pasteinput,.fc-composer textarea):focus-visible{
  outline:var(--focus-width) solid var(--focus);
  outline-offset:var(--focus-offset);
  border-color:var(--accent);
}
:where(.d-search,.cat-search,.mk-search,.tr-search):focus-within{
  outline:var(--focus-width) solid var(--focus);
  outline-offset:var(--focus-offset);
  border-color:var(--accent);
}

/* Ledger label / badge master. These are compact identifiers, never large
   rounded pills competing with the data. */
:where(.ob-badge,.ob-sim,.reason-sig,.evidence-level,.w-grp,.fc-sigpill){
  display:inline-flex;
  align-items:center;
  gap:var(--sp-4);
  padding:var(--sp-4) var(--sp-8);
  color:var(--text-2);
  background:transparent;
  border:var(--hairline) solid var(--border);
  border-radius:var(--radius-label);
  font:var(--fw-semibold) var(--ob-t--2)/var(--lh-solid) var(--mono);
  letter-spacing:var(--ls-status);
}
:where(.ob-badge,.ob-sim)[data-tone="gold"],
:where(.ob-sim){
  color:var(--accent);
  border-color:var(--accent-line);
}

/* Native data-table master. Visual canvases may duplicate this data, but the
   ruled table remains the accessible source and uses the same ledger grammar. */
:where(.ob-ledger-table,.ob-book-table,.portfolio-table){
  inline-size:100%;
  border-collapse:collapse;
  color:var(--text);
  background:var(--surface);
}
:where(.ob-ledger-table,.ob-book-table,.portfolio-table) :is(th,td){
  border-color:var(--border-soft);
}
:where(.ob-ledger-table,.ob-book-table,.portfolio-table) thead th{
  color:var(--text-2);
  background:var(--surface-2);
  font-weight:var(--fw-semibold);
}
:where(.ob-ledger-table,.ob-book-table,.portfolio-table) td{
  font-family:var(--font-numeric);
  font-variant-numeric:tabular-nums;
}

/* Header and mobile navigation share the reference's fine underline instead
   of filled tab pills. */
:where(.ob-tab,.d-tab,.m-tab){
  position:relative;
  border:0;
  background:transparent;
  color:var(--text-2);
  text-decoration:none;
}
:where(.ob-tab,.d-tab,.m-tab)::after{
  content:"";
  position:absolute;
  inset-inline:var(--nav-underline-inset);
  inset-block-end:0;
  block-size:var(--hairline);
  background:var(--order-mid);
  transform:scaleX(0);
  transform-origin:center;
  transition:transform var(--dur-slow) var(--ease-enter);
}
:where(.ob-tab,.d-tab,.m-tab):hover,
:where(.ob-tab,.d-tab,.m-tab).on{
  color:var(--text);
}
:where(.ob-tab,.d-tab,.m-tab):hover::after,
:where(.ob-tab,.d-tab,.m-tab).on::after{
  transform:scaleX(1);
}
:where(.ob-tab,.d-tab,.m-tab):focus-visible{
  outline:var(--focus-width) solid var(--focus);
  outline-offset:var(--focus-inset-tight);
}

/* Modal, sheet, menu, toast, and avatar masters. Context styles own only
   placement, scrolling, and responsive dimensions. */
:where(.modal-card,.fc-sheet){
  background:var(--component-overlay-bg);
  border:var(--hairline) solid var(--component-overlay-border);
  border-radius:var(--component-overlay-radius);
  box-shadow:var(--shadow-float);
  padding:var(--density-panel-pad);
}
:where(.sheet){
  background:var(--component-overlay-bg);
  border:var(--hairline) solid var(--component-overlay-border);
  border-block-end:0;
  border-radius:var(--component-overlay-radius) var(--component-overlay-radius) 0 0;
  box-shadow:var(--shadow-sheet);
}
:where(.board-menu,.fc-menu){
  background:var(--component-overlay-bg);
  border:var(--hairline) solid var(--component-overlay-border);
  border-radius:var(--component-overlay-radius);
  box-shadow:var(--shadow-menu);
  padding:var(--sp-6);
}
:where(.toast,.fc-toast){
  color:var(--order-inverse-text);
  background:var(--order-inverse-surface);
  border:var(--hairline) solid var(--component-toast-border);
  border-radius:var(--radius-sm);
  box-shadow:var(--shadow-float);
}
:where(.m-av,.fc-avatar){
  border-radius:var(--radius-avatar);
  background:var(--component-avatar-bg);
  color:var(--component-avatar-fg);
  border:var(--hairline) solid var(--component-avatar-border);
}

/* The global reset removes the user-agent `margin:auto` that normally centers
   native modal dialogs. Restore an explicit viewport-centered contract here so
   every generated page gets the same placement and safe edge clearance. */
:where(dialog.site-dialog,dialog.auth-dialog){
  position:fixed;
  inset:0;
  margin:auto;
  max-inline-size:calc(100vw - (var(--dialog-viewport-gap) * 2));
  max-block-size:calc(100dvh - (var(--dialog-viewport-gap) * 2));
}

/* Shared Oasis Mission language. The menu and every page consume the same
   headline rhythm, image-like sand wash, and compact filter contract. */
:where(h1,h2,h3){
  font-family:var(--brand);
  text-wrap:balance;
}
:where(.page-kicker,.section-kicker,.eyebrow,.oasis-kicker){
  color:var(--editorial-eyebrow-color);
  font:var(--editorial-eyebrow-font);
  letter-spacing:var(--ls-status);
}
:where(.oasis-hero,.page-hero,.copytrade-hero){
  color:var(--oasis-hero-fg);
  background:var(--oasis-hero-bg);
}
:where(.oasis-panel){
  border:0;
  border-radius:var(--component-card-radius);
  background:var(--oasis-panel-bg);
  box-shadow:var(--shadow-card);
}
:where(.oasis-filter,.filter-chip){
  min-block-size:var(--oasis-filter-height);
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:var(--oasis-filter-gap);
  padding-inline:var(--oasis-filter-pad-inline);
  border:var(--hairline) solid var(--oasis-filter-border);
  border-radius:var(--oasis-filter-radius);
  background:var(--oasis-filter-bg);
  color:var(--oasis-filter-fg);
  font:var(--fw-semibold) var(--oasis-filter-label-size)/var(--lh-solid) var(--font);
  white-space:nowrap;
}
:where(.oasis-filter,.filter-chip):hover{
  border-color:var(--accent);
  background:var(--oasis-panel-bg-hover);
  color:var(--text);
}
:where(.oasis-filter,.filter-chip).on,
:where(.oasis-filter,.filter-chip).is-active,
:where(.oasis-filter,.filter-chip)[aria-pressed="true"]{
  border-color:transparent;
  background:var(--oasis-filter-active-bg);
  color:var(--oasis-filter-active-fg);
  box-shadow:var(--shadow-gold);
}

@media (pointer:coarse){
  :where(.oasis-filter,.filter-chip){min-block-size:var(--touch-target)}
}

@media (prefers-reduced-motion:reduce){
  :where(.btn,.public-button,.fc-btn,.button),
  :where(.ob-card,.component-card,.widget),
  :where(.ob-card,.component-card,.widget)[data-interactive="true"],
  :where(.ob-tab,.d-tab,.m-tab)::after,
  :where(.toast,.fc-toast){transition:none}
  :where(.btn,.public-button,.fc-btn,.button):hover,
  :where(.ob-card,.component-card,.widget):hover,
  :where(.ob-card,.component-card,.widget)[data-interactive="true"]:not(.sel):hover{transform:none}
}


/* ── Theme switcher ───────────────────────────────────────────────────────────
   Three segments, because the page follows the OS preference by default and a
   two-way toggle would leave no way back to that once touched. Each segment
   carries a text label as well as an icon: the selected state is a fill AND a
   word, never colour alone. Built from semantic roles, so it themes itself. */
.theme-switch{
  display:inline-flex;align-items:center;gap:var(--sp-2);
  padding:var(--sp-2);
  border:var(--hairline) solid var(--border);
  border-radius:var(--radius-ctl);
  background:var(--surface-2);
}
.theme-switch button{
  min-block-size:var(--sp-32);
  display:inline-flex;align-items:center;gap:var(--sp-6);
  padding-inline:var(--sp-8);
  border:0;border-radius:var(--radius-sm);
  background:transparent;color:var(--text-2);
  font:var(--fw-medium) var(--fs-caption)/var(--lh-solid) var(--font);
  cursor:pointer;
  transition:background var(--dur-base) var(--ease-enter),color var(--dur-base) var(--ease-enter);
}
.theme-switch button svg{
  inline-size:var(--sp-16);block-size:var(--sp-16);flex:0 0 var(--sp-16);
  fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;
}
.theme-switch button:hover{background:var(--surface);color:var(--text)}
.theme-switch button[aria-pressed="true"]{background:var(--accent-fill-solid);color:var(--on-accent-fill)}
.theme-switch button:focus-visible{outline:var(--focus-width) solid var(--focus);outline-offset:var(--focus-offset)}
/* Below the auth breakpoint the words go and the icons carry it, so the header
   keeps one row. The accessible name survives on title/aria-pressed. */
@media (max-width:900px){
  .theme-switch button span{display:none}
  .theme-switch button{padding-inline:var(--sp-6)}
}

/* ═══════════════════════════════════════════════════════════════════════════
   PRODUCT COMPONENTS — the half the theme-06 reference does not cover
   ═══════════════════════════════════════════════════════════════════════════
   input/references/theme-06-components.html is an editorial page. It documents
   cards, chips, badges, a search box and a footer, and stops there - it never
   needed a data table, a dialog, a form field, a tab strip or an empty state.

   Our product needs all of those and grew each one per page: five separate
   empty states, four separate tab strips, four separate table treatments, five
   avatars. None of it wrong; none of it inheritable either, so page sixteen
   would have invented a sixth empty state.

   Everything here is built ONLY from tokens.css semantic roles, so it follows a
   re-pointed role and needs no dark-mode rules of its own. Nothing is wired into
   a shipped page yet - this is the vocabulary a new page can adopt, and the
   existing per-page implementations can migrate onto it as each is touched.

   Deliberately absent: anything that manufactures urgency. No countdown, no
   streak, no "N people watching". The tones label state; they do not push.
   That is the product rule in CLAUDE.md, not a styling preference. */

/* ── Button ───────────────────────────────────────────────────────────────── */
.ui-btn{
  min-block-size:var(--touch-target);
  display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-8);
  padding-inline:var(--sp-16);
  /* --control-border, not --border. --border resolves to --glass-brd, which in
     the LIGHT tier is rgba(255,255,255,.70) — a white rim designed to catch the
     edge of a glass panel over the coloured ground. Put it on a button sitting
     on a near-white surface and it is white on white: every default button
     rendered as bare text in light mode while looking correct in dark, because
     there the same token is a pale hairline on a dark field. --control-border is
     the role that already exists for exactly this, and it is defined in both
     tiers. Found by screenshotting light for the first time. */
  border:var(--hairline) solid var(--control-border);
  border-radius:var(--radius-ctl);
  background:transparent;
  color:var(--text);
  font:var(--fw-semibold) var(--fs-sm)/var(--lh-solid) var(--font);
  cursor:pointer;
  transition:background var(--dur-button) var(--ease-enter),
             border-color var(--dur-button) var(--ease-enter),
             transform var(--dur-button) var(--ease-enter);
}
.ui-btn:hover{border-color:var(--text-2);transform:translateY(-1px)}
.ui-btn:active{transform:translateY(0)}
.ui-btn:focus-visible{outline:var(--focus-width) solid var(--focus);outline-offset:var(--focus-offset)}
.ui-btn[disabled],.ui-btn[aria-disabled="true"]{opacity:.5;cursor:not-allowed;transform:none}
.ui-btn--primary{border-color:transparent;background:var(--accent-fill-solid);color:var(--on-accent-fill)}
.ui-btn--primary:hover{border-color:transparent;background:var(--accent-fill-hover)}
/* Quiet until intent is clear: a destructive action should be reachable, not
   eye-catching. It takes the critical hue on hover and focus only. */
.ui-btn--danger{color:var(--down);border-color:var(--down-soft)}
.ui-btn--danger:hover{background:var(--down-soft);border-color:var(--down)}
.ui-btn--ghost{border-color:transparent}
.ui-btn--ghost:hover{background:var(--surface-2);border-color:transparent}
.ui-btn--sm{min-block-size:var(--sp-32);padding-inline:var(--sp-12);font-size:var(--fs-caption)}
.ui-btn--block{display:flex;inline-size:100%}

/* ── Field ────────────────────────────────────────────────────────────────────
   Label, control, hint and error as one unit. The invalid state sets a border
   AND a text message: colour alone is no signal to a colour-blind reader. */
.ui-field{display:grid;gap:var(--sp-6)}
.ui-field>label{color:var(--text-2);font-size:var(--fs-caption);font-weight:var(--fw-medium)}
.ui-field input,.ui-field select,.ui-field textarea{
  min-block-size:var(--touch-target);
  inline-size:100%;
  padding:var(--sp-8) var(--sp-12);
  border:var(--hairline) solid var(--control-border);
  border-radius:var(--radius-ctl);
  background:var(--surface-2);
  color:var(--text);
  font:var(--fs-sm)/var(--lh-snug) var(--font);
}
.ui-field textarea{min-block-size:calc(var(--touch-target) * 2.4);resize:vertical}
.ui-field input::placeholder,.ui-field textarea::placeholder{color:var(--text-3)}
.ui-field input:focus-visible,.ui-field select:focus-visible,.ui-field textarea:focus-visible{
  outline:var(--focus-width) solid var(--focus);outline-offset:var(--focus-offset);border-color:var(--accent);
}
.ui-field-hint{color:var(--text-3);font-size:var(--fs-caption)}
.ui-field-error{color:var(--down);font-size:var(--fs-caption);font-weight:var(--fw-medium)}
.ui-field[data-invalid] input,.ui-field[data-invalid] select,.ui-field[data-invalid] textarea{border-color:var(--down)}

/* ── Choice ─────────────────────────────────────────────────────────────────── */
.ui-choice{display:inline-flex;align-items:center;gap:var(--sp-8);min-block-size:var(--touch-target);cursor:pointer}
.ui-choice input{inline-size:18px;block-size:18px;accent-color:var(--accent);cursor:pointer}
.ui-choice input:focus-visible{outline:var(--focus-width) solid var(--focus);outline-offset:var(--focus-offset)}
.ui-choice>span{color:var(--text);font-size:var(--fs-sm)}
.ui-switch{
  inline-size:40px;block-size:22px;flex:0 0 40px;position:relative;
  border:0;border-radius:var(--radius-pill);background:var(--table-header-bg);cursor:pointer;
  transition:background var(--dur-base) var(--ease-enter);
}
.ui-switch::after{
  content:"";position:absolute;inset-block-start:3px;inset-inline-start:3px;
  inline-size:16px;block-size:16px;border-radius:var(--radius-avatar);background:var(--surface-solid);
  transition:translate var(--dur-base) var(--ease-enter);
}
.ui-switch:focus-visible{outline:var(--focus-width) solid var(--focus);outline-offset:var(--focus-offset)}
.ui-switch[aria-checked="true"]{background:var(--accent-fill-solid)}
.ui-switch[aria-checked="true"]::after{translate:18px 0}

/* ── Table ────────────────────────────────────────────────────────────────────
   Four pages each grew their own. The shared parts: a scroll container so the
   page never overflows, a header that survives scrolling, zebra rows for
   tracking across wide rows, and tabular numerals so digits line up. */
.ui-table-wrap{overflow-x:auto;border-radius:var(--radius-card)}
.ui-table{inline-size:100%;border-collapse:collapse;font-size:var(--fs-sm)}
.ui-table th,.ui-table td{padding:var(--sp-10) var(--sp-12);text-align:start;vertical-align:middle}
.ui-table thead th{
  position:sticky;inset-block-start:0;z-index:var(--z-raised);
  background:var(--table-header-bg);color:var(--text-2);
  font-size:var(--fs-caption);font-weight:var(--fw-semibold);
  letter-spacing:var(--ls-caps);text-transform:uppercase;
  /* Inset shadow, not a border: a sticky header's own border scrolls away with
     the cell box in some engines and the rule silently goes missing. */
  box-shadow:inset 0 -1px var(--border);
}
.ui-table tbody tr{border-block-end:var(--hairline) solid var(--border-soft)}
.ui-table tbody tr:last-child{border-block-end:0}
.ui-table tbody tr:nth-child(even){background:var(--table-stripe)}
.ui-table tbody tr:hover{background:var(--table-row-hover)}
.ui-table [data-numeric]{text-align:end;font-family:var(--mono);font-variant-numeric:tabular-nums}
.ui-table caption{padding:var(--sp-10) var(--sp-12);color:var(--text-3);font-size:var(--fs-caption);text-align:start}

/* ── Dialog ───────────────────────────────────────────────────────────────────
   Solid, never glass. A dialog lands on arbitrary page content, and a
   translucent surface lets that content read straight through it - the same
   trap the filter popover fell into. */
.ui-dialog{
  inline-size:min(560px,calc(100vw - var(--sp-24) * 2));
  padding:var(--sp-20);
  border:var(--hairline) solid var(--border);
  border-radius:var(--radius-card);
  background:var(--surface-solid);
  color:var(--text);
  box-shadow:var(--shadow-menu);
}
.ui-dialog-head{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-12)}
.ui-dialog-body{margin-block:var(--sp-12);color:var(--text-2);font-size:var(--fs-sm)}
.ui-dialog-actions{display:flex;justify-content:flex-end;gap:var(--sp-8);margin-block-start:var(--sp-16)}
.ui-scrim{position:fixed;inset:0;z-index:var(--z-overlay-scrim);background:rgba(18,80,127,.38)}

/* ── Tabs / segmented control ─────────────────────────────────────────────────
   One shape, two readings: tabs switch a view, a segmented control filters one.
   The difference lives in the ARIA, not the CSS. */
.ui-tabs{
  display:inline-flex;gap:var(--sp-4);padding:var(--sp-4);
  border:var(--hairline) solid var(--control-border);
  border-radius:var(--radius-ctl);
  background:var(--surface-2);
}
.ui-tabs>button,.ui-tabs>a{
  min-block-size:var(--sp-32);
  display:inline-flex;align-items:center;gap:var(--sp-6);
  padding-inline:var(--sp-12);
  border:0;border-radius:var(--radius-sm);
  background:transparent;color:var(--text-2);
  font:var(--fw-medium) var(--fs-caption)/var(--lh-solid) var(--font);
  cursor:pointer;
  transition:background var(--dur-base) var(--ease-enter),color var(--dur-base) var(--ease-enter);
}
.ui-tabs>button:focus-visible,.ui-tabs>a:focus-visible{outline:var(--focus-width) solid var(--focus);outline-offset:var(--focus-offset)}
/* Hover applies to UNSELECTED tabs only. `:hover` adds a pseudo-class, so
   `.ui-tabs>button:hover` (0,2,1) outranks `.ui-tabs>[aria-selected="true"]`
   (0,2,0) — a tab that had just been clicked lost its fill for as long as the
   pointer stayed on it, which is always, immediately after clicking it. Found by
   driving a billing-cycle switch, not by a gate. */
.ui-tabs>button:hover:not([aria-selected="true"]):not([aria-pressed="true"]):not([aria-current="page"]):not([aria-checked="true"]),
.ui-tabs>a:hover:not([aria-selected="true"]):not([aria-pressed="true"]):not([aria-current="page"]):not([aria-checked="true"]){
  background:var(--surface);color:var(--text);
}
/* aria-checked is here because a radiogroup uses it, and a segmented control IS
   a radiogroup as often as it is a tablist. Without it the selected option
   rendered identically to the unselected ones — found by building a settings
   surface, which is the first archetype to use the control that way. */
.ui-tabs>[aria-selected="true"],.ui-tabs>[aria-pressed="true"],
.ui-tabs>[aria-current="page"],.ui-tabs>[aria-checked="true"]{
  background:var(--accent-fill-solid);color:var(--on-accent-fill);
}

/* ── Breadcrumb — the separator is generated, so it is never read as content. */
.ui-crumbs{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-6);list-style:none;font-size:var(--fs-caption)}
.ui-crumbs a{color:var(--text-2)}
.ui-crumbs a:hover{color:var(--accent);text-decoration:underline}
.ui-crumbs li+li::before{content:"/";margin-inline-end:var(--sp-6);color:var(--text-3)}
.ui-crumbs [aria-current="page"]{color:var(--text);font-weight:var(--fw-medium)}

/* ── Avatar ───────────────────────────────────────────────────────────────────
   Initials are the fallback, not an afterthought: most personas here have no
   uploaded image, so the imageless case IS the common case. */
.ui-avatar{
  inline-size:36px;block-size:36px;flex:0 0 36px;
  display:inline-grid;place-items:center;overflow:hidden;
  border-radius:var(--radius-avatar);background:var(--accent-fill-solid);color:var(--on-accent-fill);
  font:var(--fw-semibold) var(--fs-caption)/var(--lh-solid) var(--font);text-transform:uppercase;
}
.ui-avatar img{inline-size:100%;block-size:100%;object-fit:cover}
.ui-avatar--sm{inline-size:26px;block-size:26px;flex-basis:26px}
.ui-avatar--lg{inline-size:56px;block-size:56px;flex-basis:56px;font-size:var(--fs-sm)}
.ui-avatar-stack{display:inline-flex}
.ui-avatar-stack .ui-avatar+.ui-avatar{margin-inline-start:calc(var(--sp-12) * -1);box-shadow:0 0 0 2px var(--surface-solid)}

/* ── Note — callout / alert ───────────────────────────────────────────────────
   Tone is a data attribute, so a note can change severity without changing its
   markup. The leading rule carries the tone as well as the text colour, because
   severity must never ride on hue alone. */
/* BLOCK, not grid. A grid container makes every child its own row — including an
   inline <a> and each bare text node — so a note whose body contains a link mid
   sentence was split into three stacked lines. The grid only ever existed to put
   the <strong> lead on its own line, and a block-level <strong> does that
   without destroying inline flow around it. */
.ui-note{
  display:block;
  padding:var(--sp-12) var(--sp-16);
  border-inline-start:var(--stroke-accent) solid var(--border);
  border-radius:var(--radius-sm);
  background:var(--surface-2);color:var(--text-2);font-size:var(--fs-sm);
  line-height:var(--lh-snug);
}
.ui-note>strong{
  display:block;margin-block-end:var(--sp-6);
  color:var(--text);font-weight:var(--fw-semibold);
}
/* Block children keep the rhythm the grid gap used to provide. */
.ui-note>p{margin:0}
.ui-note>p+p,.ui-note>ul,.ui-note>div{margin-block-start:var(--sp-6)}
.ui-note[data-tone="info"]{border-inline-start-color:var(--info);background:var(--info-soft)}
.ui-note[data-tone="info"]>strong{color:var(--info)}
.ui-note[data-tone="caution"]{border-inline-start-color:var(--warn);background:var(--warn-soft)}
.ui-note[data-tone="caution"]>strong{color:var(--warn)}
.ui-note[data-tone="critical"]{border-inline-start-color:var(--down);background:var(--down-soft)}
.ui-note[data-tone="critical"]>strong{color:var(--down)}
.ui-note[data-tone="positive"]{border-inline-start-color:var(--up);background:var(--up-soft)}
.ui-note[data-tone="positive"]>strong{color:var(--up)}

/* ── Empty state ──────────────────────────────────────────────────────────────
   Five pages wrote their own. An empty state says what is missing AND what to
   do about it, so the action slot is part of the component and stops being
   forgotten. */
.ui-empty{
  display:grid;justify-items:center;gap:var(--sp-8);
  padding:var(--sp-32) var(--sp-16);
  border:var(--hairline) dashed var(--border);
  border-radius:var(--radius-card);
  color:var(--text-2);text-align:center;
}
.ui-empty-mark{
  inline-size:var(--touch-target);block-size:var(--touch-target);
  display:grid;place-items:center;border-radius:var(--radius-avatar);
  background:var(--table-header-bg);color:var(--text-2);
}
.ui-empty h3{color:var(--text);font-size:var(--fs-body);font-weight:var(--fw-semibold)}
.ui-empty p{max-inline-size:44ch;font-size:var(--fs-sm)}

/* ── Meter — a proportion, not a race. No fill animation: a bar that grows on
   load dramatises a number that is not changing. */
.ui-meter{block-size:6px;overflow:hidden;border-radius:var(--radius-pill);background:var(--table-header-bg)}
.ui-meter>span{display:block;block-size:100%;border-radius:inherit;background:var(--accent-fill-solid)}
/* Both tones get a texture, and the two textures differ in ANGLE. Striping only
   the critical one still left positive indistinguishable from the untoned
   default, which is also plain — the comparison that matters is each tone
   against every other state, not against one chosen sibling. */
.ui-meter[data-tone="positive"]>span{
  background:var(--up-vivid);
  background-image:repeating-linear-gradient(45deg,
    rgba(255,255,255,.28) 0 2px, transparent 2px 5px);
}
/* Critical carries a texture as well as a hue. A meter's length already encodes
   its value, so two meters at the SAME length differ only by colour — which is
   precisely the colour-alone signal the toned label and note variants are
   forbidden from using. The stripe is the second channel; it survives greyscale
   and every form of colour blindness. */
.ui-meter[data-tone="critical"]>span{
  background:var(--down-vivid);
  background-image:repeating-linear-gradient(135deg,
    rgba(255,255,255,.32) 0 2px, transparent 2px 5px);
}

/* ── Stat — label above, figure below. Tabular so a row does not jitter. */
.ui-stat{display:grid;gap:var(--sp-4)}
.ui-stat-label{color:var(--text-3);font-size:var(--fs-caption);font-weight:var(--fw-medium);letter-spacing:var(--ls-caps);text-transform:uppercase}
.ui-stat-value{color:var(--text);font-family:var(--mono);font-size:var(--fs-h3,1.3rem);font-weight:var(--fw-semibold);font-variant-numeric:tabular-nums;line-height:var(--lh-tight)}
.ui-stat-note{color:var(--text-3);font-size:var(--fs-caption)}
.ui-stat-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:var(--sp-16)}

/* ── Toast — confirms something that already happened. It never carries the
   only copy of an outcome the reader needs. */
.ui-toast{
  inline-size:min(380px,calc(100vw - var(--sp-24) * 2));
  display:flex;align-items:flex-start;gap:var(--sp-12);
  padding:var(--sp-12) var(--sp-16);
  border:var(--hairline) solid var(--border);
  border-radius:var(--radius-ctl);
  background:var(--surface-solid);color:var(--text);
  box-shadow:var(--shadow-menu);font-size:var(--fs-sm);
}
.ui-toast-stack{position:fixed;z-index:var(--z-overlay);inset-block-end:var(--sp-16);inset-inline-end:var(--sp-16);display:grid;gap:var(--sp-8)}

/* ── Tooltip — supplementary only. Anything a reader must have belongs in the
   page: a tooltip is unreachable on touch and invisible in print. */
.ui-tooltip{
  position:absolute;z-index:var(--z-overlay-menu);max-inline-size:28ch;
  padding:var(--sp-6) var(--sp-10);border-radius:var(--radius-sm);
  background:var(--text);color:var(--bg);
  font-size:var(--fs-caption);line-height:var(--lh-snug);box-shadow:var(--shadow-menu);
}

/* ── Skeleton — a placeholder while content loads. It carries no digits and no
   units, so it can never be mistaken for a value. */
.ui-skeleton{
  border-radius:var(--radius-sm);
  background:linear-gradient(90deg,var(--table-header-bg),var(--surface-2),var(--table-header-bg));
  background-size:200% 100%;
  animation:ui-skeleton-sweep 1.2s linear infinite;
}
.ui-skeleton--text{block-size:1em}
.ui-skeleton--line{block-size:6px}
@keyframes ui-skeleton-sweep{from{background-position:200% 0}to{background-position:-200% 0}}

/* ── Pagination — numbered, because "load more" loses the reader's place on a
   back navigation. */
.ui-pager{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-6)}
.ui-pager a,.ui-pager button{
  min-inline-size:var(--sp-32);min-block-size:var(--sp-32);
  display:inline-grid;place-items:center;padding-inline:var(--sp-8);
  border:var(--hairline) solid transparent;border-radius:var(--radius-sm);
  background:transparent;color:var(--text-2);
  font:var(--fw-medium) var(--fs-caption)/var(--lh-solid) var(--mono);cursor:pointer;
  transition:background var(--dur-base) var(--ease-enter),color var(--dur-base) var(--ease-enter);
}
.ui-pager a:focus-visible,.ui-pager button:focus-visible{outline:var(--focus-width) solid var(--focus);outline-offset:var(--focus-offset)}
.ui-pager a:hover,.ui-pager button:hover{background:var(--surface-2);color:var(--text)}
.ui-pager [aria-current="page"]{background:var(--accent-fill-solid);color:var(--on-accent-fill)}

/* ── Label — inline, tone-carrying ───────────────────────────────────────────
   Promoted from experiments/forum-clone, where it was needed by all seven
   archetypes and was the single cheapest gap on the list. `.ui-note` is a BLOCK
   callout; this is one word attached to a title. theme.css has `.tag`, but no
   page loads theme.css, so the shipped system had no small inline label at all.
   Tone is a data attribute so a label can change severity without changing its
   markup. */
.ui-label{
  display:inline-flex;align-items:center;
  padding-inline:var(--sp-6);
  border-radius:var(--radius-pill);
  background:var(--surface-2);color:var(--text-2);
  font-size:var(--fs-caption);font-weight:var(--fw-medium);
  white-space:nowrap;
}
.ui-label[data-tone="info"]{background:var(--info-soft);color:var(--info)}
.ui-label[data-tone="caution"]{background:var(--warn-soft);color:var(--warn)}
.ui-label[data-tone="critical"]{background:var(--down-soft);color:var(--down)}
.ui-label[data-tone="positive"]{background:var(--up-soft);color:var(--up)}

/* ── Meta line ───────────────────────────────────────────────────────────────
   Author, place, time and counters on one wrapping line with GENERATED
   separators. Needed by all seven archetypes, and its absence is why the shipped
   pages each spell this differently with ad-hoc spans and literal middots.
   The separator is generated so a screen reader never reads it as content, and
   it is suppressed before a label so a leading tag does not get a stray dot. */
.ui-meta{
  display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-6);
  color:var(--text-3);font-size:var(--fs-caption);
}
.ui-meta a{color:var(--text-2);font-weight:var(--fw-medium)}
.ui-meta a:hover{color:var(--accent-strong);text-decoration:underline}
.ui-meta>*+*:not(.ui-label)::before{
  content:"·";margin-inline-end:var(--sp-6);color:var(--text-3);
}
/* Times and counters must not break mid-value. */
.ui-meta time,.ui-meta [data-numeric]{white-space:nowrap}
.ui-meta [data-numeric]{font-family:var(--mono);font-variant-numeric:tabular-nums}

/* ── Module — a titled rail block ────────────────────────────────────────────
   Trivial, and its absence is exactly how six near-identical panel rules ended
   up in the shipped CSS. */
/* Cards and panels pair the border with an ELEVATION. In the light tier the
   border resolves to a white glass rim, which vanishes on a light surface — so a
   card carrying a border and nothing else had no boundary at all. Every panel in
   this set was edgeless in light while looking correct in dark. order-book-page
   already pairs border + --shadow-card on each of its cards; the component set
   never did. The shadow is the channel that reads in both tiers. */
.ui-module{
  padding:var(--sp-12);
  border:var(--hairline) solid var(--border);
  border-radius:var(--radius-card);
  background:var(--surface);
  box-shadow:var(--shadow-card);
}
.ui-module>h2,.ui-module>h3{
  margin:0 0 var(--sp-8);
  color:var(--text-3);
  font-size:var(--fs-caption);font-weight:var(--fw-semibold);
  letter-spacing:var(--ls-caps);text-transform:uppercase;
}

/* ── Modal host ──────────────────────────────────────────────────────────────
   The largest gap the experiment found. `.ui-dialog` above is the SURFACE only:
   it had no host to centre it, no scrim placement, no scroll lock, and no focus
   handling. The app has openOverlay() in board-app.js, but that is app-scoped
   and unreachable from anything else, so every new surface started from zero.

   CSS covers placement, layering and scroll lock. Focus trapping and focus
   restore are behaviour and belong to whatever opens it - documented here so the
   next implementer does not assume the CSS is the whole component. */
.ui-modal{
  position:fixed;inset:0;z-index:var(--z-overlay-dialog);
  display:grid;place-items:center;
  padding:var(--sp-16);
}
/* No z-index on either: both are positioned and share one stacking context, so
   the dialog paints above the scrim purely by coming after it in the DOM. Two
   raw z-indexes here would also have bypassed the --z-* ladder for values that
   never needed to exist. */
.ui-modal>.ui-scrim{position:absolute}
.ui-modal>.ui-dialog{
  position:relative;
  max-block-size:calc(100dvh - var(--sp-32));
  overflow:auto;
}
/* Set on <body> while a modal is open so the page behind cannot scroll. */
.ui-modal-open{overflow:hidden}

/* ── Ownership ───────────────────────────────────────────────────────────────
   A row the reader owns. Carried by an edge rail, never by colour alone - the
   owner-only action set beside it is the other half of the signal. */
[data-owned="true"]{border-inline-start:var(--stroke-accent) solid var(--accent-fill-solid)}

/* ── Listing row ─────────────────────────────────────────────────────────────
   A card with a fixed ANATOMY: leading control, body, title, meta, actions.
   `.ui-card` is a surface — it has no anatomy, which is precisely why the
   leaderboard, groups, trending and feed surfaces each rebuilt this same shape
   independently. Promoted from the forum-clone, where all seven archetypes
   needed it.

   overflow-wrap:anywhere is load-bearing: a 350-character title has to wrap, and
   an unbroken 100-character string must not widen the grid column. Both cases
   are real — they were in the stress dataset for exactly this reason. */
.ui-listing{
  display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--sp-12);
  padding:var(--sp-12);
  border:var(--hairline) solid var(--border);
  border-radius:var(--radius-card);
  background:var(--surface);
  box-shadow:var(--shadow-card);
  transition:border-color var(--dur-fast) var(--ease-enter);
}
.ui-listing:hover{border-color:var(--text-3)}
.ui-listing-body{min-inline-size:0;display:grid;gap:var(--sp-6);align-content:start}
/* A card in a list is a PREVIEW. Unclamped, one post with a 240-character title
   takes five lines and pushes every other post out of the viewport — the list
   stops being a list. Several pages already carried an inline
   `-webkit-line-clamp:none`, anticipating a clamp that had never been written. */
.ui-listing-title{
  margin:0;color:var(--text);
  font-size:var(--fs-body);font-weight:var(--fw-semibold);line-height:var(--lh-tight);
  overflow-wrap:anywhere;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;
}
.ui-listing-title a{color:inherit}
.ui-listing-title a:hover{color:var(--accent-strong)}
.ui-listing-excerpt{
  color:var(--text-2);font-size:var(--fs-sm);line-height:var(--lh-snug);
  overflow-wrap:anywhere;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;
}

/* The DETAIL view is not a preview — clamping there would hide the thing the
   reader navigated to. Any surface showing a post in full opts out by carrying
   data-post, rather than each page re-declaring it inline. */
[data-post] .ui-listing-title,
[data-post] .ui-listing-excerpt,
.ui-listing-title[data-clamp="none"],
.ui-listing-excerpt[data-clamp="none"]{
  display:block;-webkit-line-clamp:none;overflow:visible;
}

/* ── Vote ────────────────────────────────────────────────────────────────────
   Two directional toggles sharing one count, where the middle value IS the
   state. `.ui-btn` is a single action and `.ui-tabs` is single-select; neither
   models "two toggles, one shared value".

   It ships BOTH orientations because the experiment proved it had to: the
   vertical control looked finished until it went into a comment's action row,
   where it stacked into three lines. That only appeared on the second context,
   never from reading the CSS. */
.ui-vote{display:grid;justify-items:center;gap:var(--sp-2);min-inline-size:var(--sp-40)}
.ui-vote--inline{grid-auto-flow:column;align-items:center;gap:var(--sp-4);min-inline-size:0}
.ui-vote button{
  inline-size:var(--sp-24);block-size:var(--sp-24);
  display:grid;place-items:center;
  border:0;border-radius:var(--radius-sm);
  background:transparent;color:var(--text-3);cursor:pointer;
  transition:color var(--dur-fast) var(--ease-enter),background var(--dur-fast) var(--ease-enter);
}
.ui-vote button:hover{background:var(--surface-2);color:var(--text)}
.ui-vote button svg{inline-size:var(--sp-16);block-size:var(--sp-16);fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.ui-vote [aria-pressed="true"][data-dir="up"]{color:var(--up);background:var(--up-soft)}
.ui-vote [aria-pressed="true"][data-dir="down"]{color:var(--down);background:var(--down-soft)}
/* Tabular, or the column jitters every time a score changes. */
.ui-vote-score{
  color:var(--text);font-family:var(--mono);
  font-size:var(--fs-caption);font-weight:var(--fw-semibold);
  font-variant-numeric:tabular-nums;
}
.ui-vote[data-state="up"] .ui-vote-score{color:var(--up)}
.ui-vote[data-state="down"] .ui-vote-score{color:var(--down)}

/* ── Media frame ─────────────────────────────────────────────────────────────
   Fixed-ratio, cropped, rounded, with a placeholder when there is no asset.
   Every media surface in the product sets aspect-ratio and object-fit by hand. */
.ui-media{
  position:relative;overflow:hidden;
  border-radius:var(--radius-sm);
  background:var(--table-header-bg);
}
.ui-media[data-aspect="16:9"]{aspect-ratio:16/9}
.ui-media[data-aspect="4:3"]{aspect-ratio:4/3}
.ui-media[data-aspect="1:1"]{aspect-ratio:1/1}
.ui-media[data-aspect="portrait"]{aspect-ratio:3/4}
.ui-media img{inline-size:100%;block-size:100%;object-fit:cover;display:block}
.ui-media-placeholder{
  position:absolute;inset:0;display:grid;place-items:center;
  padding:var(--sp-12);
  color:var(--text-3);font-size:var(--fs-caption);
  letter-spacing:var(--ls-caps);
  text-align:center;text-wrap:balance;
}

/* Three states, and the frame has to survive all three. Previously the
   placeholder showed unconditionally and sat UNDER a loaded image — which
   worked only because no image ever loaded. */
.ui-media[data-state] .ui-media-placeholder{display:none}
.ui-media[data-state="failed"] .ui-media-placeholder{display:grid}
.ui-media[data-state="failed"] img{display:none}

/* While loading, the reserved box shows a quiet shimmer instead of a hole —
   and holds its aspect ratio, so an arriving image never shoves the page. */
.ui-media[data-state="loading"]{
  background:linear-gradient(100deg,
    var(--table-header-bg) 40%, var(--surface-2) 50%, var(--table-header-bg) 60%);
  background-size:300% 100%;
  animation:ui-media-wait var(--dur-slow, 1.2s) linear infinite;
}
@keyframes ui-media-wait{from{background-position:150% 0}to{background-position:-50% 0}}
@media(prefers-reduced-motion:reduce){
  .ui-media[data-state="loading"]{animation:none}
}

/* ── Spoiler — content withheld until asked for ──────────────────────────────
   The system had no way to say "this is here, and you have not asked to see it
   yet". A spoiler flag rendered as a badge next to content that was fully
   visible, which is a label that describes the opposite of what it does.

   Withheld by CLIPPING, not by blur: a blur is reversible by anyone who
   screenshots and sharpens, and it still shows shapes — a photograph is often
   readable through it. The content is genuinely not rendered until revealed. */
/* Hidden by the `hidden` ATTRIBUTE, not by a class the stylesheet has to be
   present to apply. A spoiler that depends on CSS loading is a spoiler that
   leaks the moment the stylesheet 404s or is slow — the failure mode has to be
   "stays hidden", never "shows everything". */
.ui-spoiler{position:relative;display:grid}
.ui-spoiler-cover{
  display:grid;place-items:center;gap:var(--sp-8);
  min-block-size:var(--sp-56, 4rem);
  padding:var(--sp-16);
  border:var(--hairline) dashed var(--control-border);
  border-radius:var(--radius-card);
  background:var(--surface-2);
  color:var(--text-2);font-size:var(--fs-sm);text-align:center;
}
.ui-spoiler-cover strong{color:var(--text);font-weight:var(--fw-semibold)}

/* ── Labelled divider ────────────────────────────────────────────────────────
   A titled separator inside a list ("today", "this week"). Every grouped list
   needs one; every grouped list in the product currently draws its own. */
.ui-divider{
  display:flex;align-items:center;gap:var(--sp-8);
  margin-block:var(--sp-16) var(--sp-8);
  color:var(--text-3);
  font-size:var(--fs-caption);font-weight:var(--fw-semibold);
  letter-spacing:var(--ls-caps);text-transform:uppercase;
}
.ui-divider::after{content:"";flex:1;block-size:var(--hairline);background:var(--border)}

/* ── Facet list ──────────────────────────────────────────────────────────────
   A vertical, MULTI-select filter list where every row carries a count.
   `.ui-tabs` is horizontal and single-select, so it is the wrong primitive
   despite looking similar. A zero count must still read as "none here" rather
   than as disabled noise. */
.ui-facet{display:grid;gap:var(--sp-2)}
.ui-facet button{
  min-block-size:var(--sp-32);
  display:flex;align-items:center;gap:var(--sp-8);
  padding-inline:var(--sp-8);
  border:0;border-radius:var(--radius-sm);
  background:transparent;color:var(--text-2);cursor:pointer;
  font:var(--fw-medium) var(--fs-sm)/var(--lh-solid) var(--font);
  text-align:start;
}
.ui-facet button:hover{background:var(--surface-2);color:var(--text)}
.ui-facet button[aria-pressed="true"]{background:var(--accent-soft);color:var(--accent-strong)}
.ui-facet-count{
  margin-inline-start:auto;
  color:var(--text-3);font-family:var(--mono);
  font-size:var(--fs-caption);font-variant-numeric:tabular-nums;
}
.ui-facet button[aria-pressed="true"] .ui-facet-count{color:var(--accent-strong)}

/* ── Read / unread ───────────────────────────────────────────────────────────
   Unread is carried by a leading marker AND a weight change — never by colour
   alone, and never by background alone: a read/unread background on a
   translucent surface is nearly invisible in dark mode, which is what the first
   attempt at this actually did. */
.ui-unread-dot{
  inline-size:var(--sp-8);block-size:var(--sp-8);
  border-radius:var(--radius-avatar);background:transparent;
}
/* DESCENDANT, not direct child. The dot is a marker placed wherever the row's
   layout has room for it — inside a meta line, beside a title, at the end of a
   cell — and a `>` combinator silently left it transparent everywhere except one
   exact nesting. An 8px invisible box is a worse failure than no dot at all,
   because the markup looks correct. */
[data-unread="true"] .ui-unread-dot{background:var(--accent-fill-solid)}

/* SCOPED. These were bare attribute selectors in a shared sheet, so every
   element anywhere that used data-unread — a mail row, a chat entry, a table
   cell — was painted by a rule written for a listing. A component sheet may not
   claim an attribute name globally. */
.ui-listing[data-unread="true"]{background:var(--surface-2)}
.ui-listing[data-unread="true"] .ui-listing-excerpt{color:var(--text);font-weight:var(--fw-medium)}

/* ── Thread — nested discussion ──────────────────────────────────────────────
   The deepest gap the experiment found: nothing in the system nests at all.

   Indentation is carried by a CLICKABLE rail that collapses the subtree, so the
   rail must be a real hit target rather than a border — that is the whole
   affordance. Collapsed hides the body, the actions and the children, but never
   the author line, so a collapsed comment can still be identified and reopened.
   Deleted keeps the row rather than removing it, because a hole in a thread
   destroys the reply structure below it. */
.ui-thread{display:grid;gap:var(--sp-10)}
.ui-comment{display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--sp-8)}
.ui-comment-rail{
  inline-size:var(--sp-12);padding:0;border:0;
  background:transparent;cursor:pointer;
  display:grid;justify-items:center;
}
.ui-comment-rail::after{
  content:"";inline-size:var(--hairline);block-size:100%;
  background:var(--border);
  transition:background var(--dur-fast) var(--ease-enter);
}
.ui-comment-rail:hover::after{background:var(--accent)}
.ui-comment-main{min-inline-size:0;display:grid;gap:var(--sp-4)}
.ui-comment-body{
  color:var(--text);font-size:var(--fs-sm);line-height:var(--lh-snug);
  overflow-wrap:anywhere;
}
.ui-comment[data-collapsed="true"] .ui-comment-body,
.ui-comment[data-collapsed="true"] .ui-comment-actions,
.ui-comment[data-collapsed="true"] .ui-thread{display:none}
.ui-comment[data-deleted="true"] .ui-comment-body{color:var(--text-3);font-style:italic}
.ui-comment-actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-8)}
.ui-comment-actions button{
  padding:var(--sp-2) var(--sp-4);
  border:0;border-radius:var(--radius-sm);
  background:transparent;color:var(--text-3);cursor:pointer;
  font:var(--fw-medium) var(--fs-caption)/var(--lh-solid) var(--font);
}
.ui-comment-actions button:hover{background:var(--surface-2);color:var(--text)}

/* ── Poll ────────────────────────────────────────────────────────────────────
   A result bar per option where the fill IS the proportion and the label sits
   ON the row. `.ui-meter` is a bare track with no label, no option list and no
   closed state. A 0% option must still render as a readable row, not collapse
   to nothing — that case is what makes the component worth having. */
.ui-poll{display:grid;gap:var(--sp-6)}
/* Works as a <div> (results only) or a <button> (answerable). As a button it
   needs the properties a button does not inherit — full width, start-aligned
   text, the page font — or the label centres itself and wraps against the
   percentage. Found by making polls votable and watching the label jump. */
.ui-poll-option{
  position:relative;overflow:hidden;
  display:flex;align-items:center;justify-content:space-between;gap:var(--sp-8);
  inline-size:100%;
  min-block-size:var(--touch-target);
  padding:var(--sp-6) var(--sp-8);
  border:var(--hairline) solid var(--control-border);
  border-radius:var(--radius-sm);
  background:var(--surface-2);
  color:var(--text);font:var(--fs-sm)/var(--lh-snug) var(--font);
  text-align:start;
}
.ui-poll-option:not([disabled]){cursor:pointer}
.ui-poll-option:hover:not([disabled]){border-color:var(--text-2)}
.ui-poll-option[aria-pressed="true"]{border-color:var(--accent-fill-solid)}
.ui-poll-option:focus-visible{outline:var(--focus-width) solid var(--focus);outline-offset:var(--focus-offset)}
.ui-poll-fill{position:absolute;inset-block:0;inset-inline-start:0;background:var(--accent-soft)}
/* Everything EXCEPT the fill. `.ui-poll-option>*` sat after the fill rule at
   equal specificity, so it overrode position:absolute — the fill became a flex
   item as wide as its percentage and shoved the label sideways. Invisible at 0%,
   which is why it survived: the placeholder data had no votes to show. */
.ui-poll-option>:not(.ui-poll-fill){position:relative}
.ui-poll-pct{
  color:var(--text-2);font-family:var(--mono);font-variant-numeric:tabular-nums;
}

/* ── Signed-out placeholder ──────────────────────────────────────────────────
   Says what signing in unlocks, instead of an empty box or a disabled control
   with no reason attached. */
.ui-signedout{
  display:grid;justify-items:center;gap:var(--sp-8);
  padding:var(--sp-20);
  border:var(--hairline) dashed var(--border);
  border-radius:var(--radius-card);
  color:var(--text-2);text-align:center;
}

/* ── App bar ─────────────────────────────────────────────────────────────────
   The product's header is generated per-page inside build-public-pages.js and is
   not reusable anywhere else, so every non-generated surface builds its own.
   This is the reusable shape. */
.ui-appbar{
  position:sticky;inset-block-start:0;z-index:var(--z-header);
  display:flex;align-items:center;gap:var(--sp-12);
  padding:var(--sp-10) var(--sp-16);
  border-block-end:var(--hairline) solid var(--border);
  background:var(--surface-solid);
}
.ui-appbar-brand{
  display:flex;align-items:center;gap:var(--sp-8);
  font-weight:var(--fw-semibold);white-space:nowrap;
}
.ui-appbar-actions{margin-inline-start:auto;display:flex;align-items:center;gap:var(--sp-8)}

/* ── Setting row ─────────────────────────────────────────────────────────────
   Label plus description on the left, control on the right, repeating down a
   list. `.ui-field` stacks its label ABOVE the control, so it is the wrong shape
   for preferences — the description has to sit under the label and carry the
   explanation, while the control stays on the opposite edge.

   A disabled row keeps its description visible and says WHY, rather than greying
   out with no reason: an unexplained disabled control is a dead end. */
.ui-setting{
  display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--sp-12);
  align-items:center;
  padding-block:var(--sp-12);
  border-block-end:var(--hairline) solid var(--border-soft);
}
.ui-setting:last-child{border-block-end:0}
.ui-setting-label{display:grid;gap:var(--sp-2);min-inline-size:0}
.ui-setting-label strong{
  color:var(--text);font-size:var(--fs-sm);font-weight:var(--fw-medium);
}
.ui-setting-label span{color:var(--text-3);font-size:var(--fs-caption)}
.ui-setting[data-disabled="true"] .ui-setting-label strong{color:var(--text-3)}

/* ── Destructive zone ────────────────────────────────────────────────────────
   `.ui-btn--danger` marks a single action. This marks a REGION as irreversible,
   which is a different job: it separates the actions that cannot be undone from
   the ones that can, before the reader reaches them. */
.ui-danger-zone{
  padding:var(--sp-12);
  border:var(--hairline) solid var(--down);
  border-radius:var(--radius-card);
  background:var(--down-soft);
}
.ui-danger-zone>h2,.ui-danger-zone>h3{
  margin:0 0 var(--sp-8);
  color:var(--down);font-size:var(--fs-sm);font-weight:var(--fw-semibold);
}

/* ── Selectable row ──────────────────────────────────────────────────────────
   A listing row that can be picked for a batch operation. Selection is carried
   by border AND fill, never by the checkbox alone: before an irreversible batch
   action the reader has to see at a glance which rows are in the batch, and an
   18px control is too small a signal to carry that.

   Pairs with the tri-state select-all below — `indeterminate` is a JS-only
   property with no HTML attribute, which is part of why partial selection so
   often renders identically to an empty one. */
.ui-pick{
  display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--sp-12);
  padding:var(--sp-12);
  border:var(--hairline) solid var(--control-border);
  border-radius:var(--radius-card);
  background:var(--surface);
  transition:border-color var(--dur-fast) var(--ease-enter),
             background var(--dur-fast) var(--ease-enter);
}
.ui-pick[data-selected="true"]{border-color:var(--accent);background:var(--accent-soft)}
.ui-pick input[type="checkbox"]{
  inline-size:18px;block-size:18px;accent-color:var(--accent);cursor:pointer;
}

/* ── Batch bar ───────────────────────────────────────────────────────────────
   Appears only when a selection exists, states the count, and scopes its actions
   to it. Sticky, because on a long queue the actions otherwise scroll away from
   the rows they act on.

   The destructive action must name the count: "delete 4 items" is a different
   decision from "delete", and a batch delete that does not say how many is a
   trap. */
.ui-batchbar{
  position:sticky;inset-block-start:var(--sp-40);z-index:var(--z-sticky);
  display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-8);
  margin-block-end:var(--sp-12);
  padding:var(--sp-10) var(--sp-12);
  border:var(--hairline) solid var(--accent);
  border-radius:var(--radius-ctl);
  background:var(--surface-solid);
  box-shadow:var(--shadow-menu);
}
.ui-batchbar-count{
  color:var(--text);font-size:var(--fs-sm);font-weight:var(--fw-semibold);
}

/* ── Conversation ────────────────────────────────────────────────────────────
   A pane that scrolls its OWN history with a composer pinned to the bottom.
   Every other surface here scrolls the page; a conversation must not, or the
   composer walks off screen while you type. Set a block-size on `.ui-convo` for
   the space it should fill. */
.ui-convo{
  display:grid;grid-template-rows:minmax(0,1fr) auto;
  overflow:hidden;
  border:var(--hairline) solid var(--border);
  border-radius:var(--radius-card);
  background:var(--surface);
}
.ui-convo-history{
  overflow-y:auto;overscroll-behavior:contain;
  padding:var(--sp-12);
  display:grid;gap:var(--sp-8);align-content:start;
}
.ui-convo-composer{
  display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--sp-8);
  align-items:end;
  padding:var(--sp-10) var(--sp-12);
  border-block-start:var(--hairline) solid var(--border);
  background:var(--surface-solid);
}

/* ── Message ─────────────────────────────────────────────────────────────────
   Side-aligned by author, tail corner squared toward the speaker. Mine and
   theirs differ by POSITION and SHAPE, not only colour, so alignment still
   carries the distinction without it.

   The incoming bubble carries a BORDER, and that is the whole lesson: it first
   used a surface token on top of another surface token, and translucent over
   translucent composites to nearly the same value — "theirs" rendered as bare
   text while "mine" read as a solid object. An edge survives both themes; a
   darker fill would need retuning per theme. */
.ui-msg{max-inline-size:min(70ch,78%);display:grid;gap:var(--sp-2)}
.ui-msg-body{
  padding:var(--sp-8) var(--sp-10);
  border-radius:var(--radius-card);
  background:var(--surface-2);color:var(--text);
  font-size:var(--fs-sm);line-height:var(--lh-snug);
  overflow-wrap:anywhere;
}
.ui-msg[data-mine="false"] .ui-msg-body{
  border:var(--hairline) solid var(--border);
  border-end-start-radius:var(--radius-rule);
}
.ui-msg[data-mine="true"]{justify-self:end;text-align:end}
.ui-msg[data-mine="true"] .ui-msg-body{
  background:var(--accent-fill-solid);color:var(--on-accent-fill);
  border-end-end-radius:var(--radius-rule);
}
.ui-msg-meta{color:var(--text-3);font-size:var(--fs-caption)}

/* A day marker INSIDE a scrolling history — a marker in the flow, not a section
   head, so it carries no outer margin of its own. */
.ui-divider--inline{margin-block:var(--sp-8)}
.ui-divider--inline::before{
  content:"";flex:1;block-size:var(--hairline);background:var(--border);
}

/* ── Multi-step task ────────────────────────────────────────────────────────
   Three shapes a branching form needs and a flat panel never does. Promoted
   together because they are useless apart: a spine with no read-back leaves the
   reader committing answers they cannot re-check.                             */

/* Ordered progress. Distinct from .ui-tabs (peer views, any order) and
   .ui-pager (page N of M) — this one asserts that step 3 is not yet reachable.
   Done is marked with a glyph as well as colour; the rail alone would be a
   colour-only signal. Author it as <ol>. */
.ui-steps{
  display:flex;gap:var(--sp-4);list-style:none;margin:0;padding:0;
}
.ui-steps>li{
  flex:1;display:grid;gap:var(--sp-4);
  padding-block-start:var(--sp-8);
  border-block-start:var(--sp-2) solid var(--border);
  color:var(--text-3);font-size:var(--fs-caption);
}
.ui-steps>li>b{display:block;font-size:var(--fs-sm);font-weight:var(--fw-medium)}
.ui-steps>li[data-state="done"]{border-block-start-color:var(--accent-fill-solid);color:var(--text-2)}
.ui-steps>li[data-state="done"]>b::after{content:" ✓";color:var(--accent-fill-solid)}
.ui-steps>li[data-state="current"]{border-block-start-color:var(--accent-fill-solid);color:var(--text)}
@media(max-width:640px){
  .ui-steps{flex-direction:column;gap:var(--sp-8)}
  .ui-steps>li{padding-block-start:var(--sp-4)}
}

/* A choice whose label is a title PLUS the sentence that makes it meaningful,
   with the whole row as the target. .ui-choice stays the single-line form. */
.ui-reason{display:grid;gap:var(--sp-8)}
.ui-reason>label{
  display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--sp-10);
  align-items:start;padding:var(--sp-12);
  border:var(--hairline) solid var(--control-border);border-radius:var(--radius-card);
  background:var(--surface);cursor:pointer;
}
.ui-reason>label:hover{border-color:var(--border-strong)}
/* Selected carries a border change as well as a wash — the wash alone is close
   enough to the surface to be missed. */
.ui-reason>label:has(input:checked){border-color:var(--accent-fill-solid);background:var(--accent-soft)}
.ui-reason>label:has(input:focus-visible){outline:var(--focus-width) solid var(--focus);outline-offset:var(--sp-2)}
.ui-reason input{margin-block-start:var(--sp-2)}
.ui-reason strong{display:block;font-size:var(--fs-sm);font-weight:var(--fw-medium);color:var(--text)}
.ui-reason strong+span,.ui-reason label>span>span{
  display:block;color:var(--text-2);font-size:var(--fs-caption);line-height:var(--lh-snug);
}

/* What is about to be submitted, read back term / value / edit. Distinct from
   .ui-stat-row (a metric readout) because the third column routes back to the
   step that set the value. Author it as <dl> with one <div> per row. */
.ui-readback{display:grid;gap:0;margin:0}
.ui-readback>div{
  display:grid;grid-template-columns:minmax(0,10rem) minmax(0,1fr) auto;
  gap:var(--sp-12);align-items:baseline;
  padding-block:var(--sp-10);
  border-block-end:var(--hairline) solid var(--border);
}
.ui-readback>div:last-child{border-block-end:0}
.ui-readback dt{color:var(--text-2);font-size:var(--fs-caption)}
.ui-readback dd{margin:0;color:var(--text);font-size:var(--fs-sm);overflow-wrap:anywhere}
@media(max-width:640px){
  .ui-readback>div{grid-template-columns:minmax(0,1fr) auto;row-gap:var(--sp-2)}
  .ui-readback dt{grid-column:1/-1}
}

/* The skeleton loops, so reduced motion has to silence it explicitly rather
   than merely shorten it. */
@media(prefers-reduced-motion:reduce){
  .ui-skeleton{animation:none}
  .ui-btn,.ui-tabs>button,.ui-tabs>a,.ui-pager a,.ui-pager button,.ui-switch,.ui-switch::after{transition:none}
  .ui-btn:hover{transform:none}
}
