/* ══════════════════════════════════════════════════════════════════
   UI — design-system layer   (loads AFTER app.css)

   app.css is the app's history: ~8k lines grown page by page, with
   every card, modal and chip carrying its own hand-tuned duration,
   shadow and focus treatment. Rewriting it wholesale would risk the
   thing it styles. So this file sits on top and does four jobs:

     1. TOKENS      — one scale each for motion, elevation, radius and
                      type. Custom properties resolve at use time, so
                      rules already written in app.css pick these up
                      even though app.css loads first.
     2. NORMALISE   — one transition curve, one focus ring, one
                      scrollbar, applied across the classes that were
                      each doing their own version.
     3. COMPONENTS  — .ui-* primitives the duplicated blocks now share
                      (overlay, modal, field, chip, skeleton, toast).
     4. MOTION      — start states for JS-driven reveals, and a single
                      reduced-motion kill switch for the whole app.

   Aesthetic brief: dark fantasy, but read as a premium SaaS dashboard
   — restraint, hierarchy and air. Crimson and gold stay as accents on
   a neutral dark ground; they are never the surface.
   ══════════════════════════════════════════════════════════════════ */

:root {
  /* ── Motion ─────────────────────────────────────────────────────
     Mirrored in motion.js (D / E) so a CSS transition and a JS
     animation on the same element move at the same speed. */
  --dur-xs:  120ms;   /* hover, focus, colour shifts        */
  --dur-sm:  180ms;   /* small state changes, chips, tabs   */
  --dur-md:  260ms;   /* entrances: cards, rows, panels     */
  --dur-lg:  420ms;   /* page-level, drawers                */
  --dur-xl:  620ms;   /* number count-ups                   */

  --ease-out:   cubic-bezier(.22,.61,.36,1);   /* default */
  --ease-emph:  cubic-bezier(.2,.8,.3,1);      /* entrances */
  --ease-in-out:cubic-bezier(.4,0,.2,1);
  --ease-back:  cubic-bezier(.2,.9,.3,1.15);   /* small overshoot */

  /* Composite shorthands — what most rules actually want. */
  --t-hover: var(--dur-xs) var(--ease-out);
  --t-base:  var(--dur-sm) var(--ease-out);
  --t-enter: var(--dur-md) var(--ease-emph);

  /* ── Elevation ──────────────────────────────────────────────────
     Five steps, each a genuine step up in blur and distance. On a
     near-black ground shadow alone is nearly invisible, so the upper
     levels pair it with a hairline highlight to read as "lifted". */
  --e0: none;
  --e1: 0 1px 2px rgba(0,0,0,.40);
  --e2: 0 4px 14px rgba(0,0,0,.42);
  --e3: 0 10px 28px rgba(0,0,0,.50), 0 1px 0 rgba(255,255,255,.03) inset;
  --e4: 0 22px 60px rgba(0,0,0,.62), 0 1px 0 rgba(255,255,255,.04) inset;
  --e-pop: 0 14px 34px rgba(0,0,0,.45);   /* hover lift on cards */

  /* Focus ring — gold, double-stroked so it survives any background.
     Accent crimson is too close to the error state to mean "focus". */
  --ring: 0 0 0 2px var(--void), 0 0 0 4px rgba(200,150,60,.60);
  --ring-soft: 0 0 0 3px rgba(200,150,60,.22);

  /* ── Type ───────────────────────────────────────────────────────
     Line heights and tracking as tokens: display type needs wide
     tracking and tight leading, body copy the opposite, and every
     page was picking those by hand. */
  --lh-tight: 1.15;
  --lh-snug:  1.35;
  --lh-body:  1.6;
  --tr-tight: -.01em;
  --tr-wide:  .08em;
  --tr-wider: .14em;
  --tr-caps:  .18em;   /* Cinzel small caps / eyebrow labels */

  /* Comfortable measure for prose. Long lines are the single most
     common readability failure on wide dashboards. */
  --measure: 68ch;

  /* One geometry for the app's primary controls. A tab rail, button and
     dropdown all occupy the same 36px row; nested tab triggers use the
     inner radius so the shared rail keeps a precise 3px inset. */
  --control-radius: 8px;
  --control-radius-inner: 6px;
  --control-pad-x: 12px;

  /* Scrollbars — one width and colour for the fifteen places that
     previously each declared their own. */
  --sb-size: 8px;
  --sb-thumb: rgba(255,255,255,.13);
  --sb-thumb-hover: rgba(200,150,60,.42);
}

/* ══════════════════════════════════════════════════════════════════
   1 · NORMALISE
   ══════════════════════════════════════════════════════════════════ */

/* One curve for every interactive surface. These classes previously
   declared eleven different durations between them (.12s to .25s,
   `all` vs enumerated properties). Enumerating the properties also
   stops `transition: all` from animating layout on reflow. */
.btn, .btn-sm, .btn-crimson, .btn-gold,
.search-btn, .nav-item, .nav-icon,
.sr-btn, .sidebar-collapse-btn,
.cmdk-item, .notif-item,
.mkt-tab, .mkt-card, .mkt-menu-btn,
.sp-filter, .sp-offset-chip, .sp-star,
.im-tile, .im-tile-img-wrap,
.wx-ccy-card, .nft-sort-btn, .nft-dd-btn,
.fav-btn, .auth-modal-tab, .auth-pill-link,
.lookup-suggest-item, .ui-btn, .ui-chip, .ui-card {
  transition:
    color var(--t-hover),
    background-color var(--t-hover),
    border-color var(--t-hover),
    box-shadow var(--t-base),
    transform var(--t-base),
    opacity var(--t-hover);
}

/* ── Touch targets ────────────────────────────────────────────────
   Measured under 32px on every device: the favourite stars in the
   checklist/boss tables (22×18), the refresh buttons (32×25) and the
   clan-name links in table cells (17px tall). WCAG 2.5.8 puts the floor
   at 24×24; these bring the frequently-hit ones to 32. */
.fav-btn { min-width: 32px; min-height: 32px; padding: 6px; }
.war-meta-refresh { min-height: 32px; padding: 6px 12px; }
.cr-clan-link, .gl-player-name { display: inline-block; padding: 6px 0; }

/* Pressed state: a 1px settle. Tiny, but it's the difference between
   a button that responds and a button that just recolours. */
.btn:active, .ui-btn:active, .search-btn:active,
.sp-filter:active, .sp-offset-chip:active, .nft-sort-btn:active {
  transform: translateY(1px);
}

/* ── Focus ────────────────────────────────────────────────────────
   The app had exactly one :focus-visible rule. Keyboard users got
   the browser default (or, on the many elements with `outline:none`,
   nothing at all). This restores a visible, consistent focus state
   everywhere without putting rings on mouse clicks. */
:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible,
.nav-item:focus-visible, .cmdk-item:focus-visible,
.mkt-tab:focus-visible, .sp-filter:focus-visible {
  outline: none;
  /* No border-radius here on purpose: box-shadow already follows the
     element's own radius, so pills stay pills when focused. */
  box-shadow: var(--ring);
}
/* Text fields get a softer treatment than buttons: a gold halo plus a
   crimson inner hairline, so a focused field reads as "editable here"
   rather than wearing the same hard ring as a pressed control. */
input:focus-visible, textarea:focus-visible, select:focus-visible {
  box-shadow: var(--ring-soft), 0 0 0 1px var(--accent) inset;
}
/* Clickable divs that carry onclick but no tab stop still need a
   pointer affordance — flagged here rather than left implicit. */
.nav-item, .cmdk-item, .notif-item[onclick], .sr-item[onclick] { cursor: pointer; }

/* Nav items are anchors now (real URLs, keyboard-reachable, openable in a
   new tab). Strip the link defaults so they look exactly as they did.
   text-decoration ONLY — the colours that used to be restated here are
   deliberately gone. app.css already sets .nav-item / .nav-item.active
   colour, and this file loads after it, so the duplicates silently won on
   source order at equal specificity: the sidebar redesign's dark-on-gold
   active pill came out gold-on-gold, contrast 1.17, and no amount of
   specificity in app.css could fix it from the other side. One owner for
   nav colour now, and it isn't this file. */
a.nav-item { text-decoration: none; }
a.nav-item:hover { text-decoration: none; }

/* ── Tab bars ─────────────────────────────────────────────────────
   Seven implementations exist (.tab, .plr-tab, .checklist-tab,
   .mkt-tab, .ai-tab, .nftp-inv-tab, .nftp-spirit-tab, .admin-tab).
   They now share their keyboard behaviour (initTablists) and, here,
   their base appearance. What each one keeps is its own ACTIVE
   decoration — an underline on the ranking pages, a filled pill on
   Boss Schedule — because those distinctions are deliberate.

   The reset also matters functionally: these are <button> elements
   now, so without it they'd inherit UA chrome the divs never had. */
.tab, .plr-tab, .checklist-tab, .nftp-inv-tab, .nftp-spirit-tab, .admin-tab {
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  text-align: inherit;
}
/* Shared typography for the four display-font families only. The
   Marketplace and AI Studio bars are deliberately sentence-case in the
   UI font — "Community gallery" set in uppercase Cinzel would read as a
   different product — so they share behaviour but not letterforms. */
.tab, .plr-tab, .checklist-tab, .admin-tab {
  font-family: var(--font-display);
  letter-spacing: var(--tr-wider);
  text-transform: uppercase;
}
.tab, .plr-tab, .checklist-tab, .mkt-tab, .ai-tab, .admin-tab {
  cursor: pointer;
  white-space: nowrap;
}
.tab:hover, .plr-tab:hover, .checklist-tab:hover,
.mkt-tab:hover, .ai-tab:hover, .admin-tab:hover { color: var(--text); }

/* ── Shared sliding tabs ──────────────────────────────────────────
   Boss Schedule established the app's tab language: one quiet glass
   rail and one highlight that travels to the selected item. Keep that
   interaction everywhere a horizontal text tab is used. The vertical
   NFT equipment switcher is deliberately excluded by motion.js because
   it is an icon tool rail, not section navigation. */
.tabs,
.plr-tabs,
.checklist-tabs,
.mkt-tabs,
body .ai-tabs,
body .admin-tabs {
  position: relative;
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 2px;
  width: fit-content;
  max-width: 100%;
  height: var(--control-h);
  min-height: var(--control-h);
  box-sizing: border-box;
  padding: 3px;
  border: 0;
  border-radius: 8px;
  background: rgba(17,21,29,.68);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.055), 0 10px 24px rgba(0,0,0,.16);
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-backdrop-filter: blur(16px) saturate(110%);
  backdrop-filter: blur(16px) saturate(110%);
}
.tabs::-webkit-scrollbar,
.plr-tabs::-webkit-scrollbar,
.checklist-tabs::-webkit-scrollbar,
.mkt-tabs::-webkit-scrollbar,
.ai-tabs::-webkit-scrollbar,
.admin-tabs::-webkit-scrollbar { display: none; }

.tabs .tab,
.plr-tabs .plr-tab,
.checklist-tabs .checklist-tab,
.mkt-tabs .mkt-tab,
body .ai-tabs .ai-tab,
body .admin-tabs .admin-tab {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  height: 30px;
  min-height: 30px;
  box-sizing: border-box;
  margin: 0;
  padding: 0 11px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: #bac1cc;
  box-shadow: none;
  text-shadow: none;
  transition: color var(--t-base), opacity var(--t-hover);
}
.tabs .tab:hover,
.plr-tabs .plr-tab:hover,
.checklist-tabs .checklist-tab:hover,
.mkt-tabs .mkt-tab:hover,
body .ai-tabs .ai-tab:hover,
body .admin-tabs .admin-tab:hover {
  border-color: transparent;
  background: none;
  color: var(--text-strong);
}
.tabs .tab.active,
.plr-tabs .plr-tab.active,
.checklist-tabs .checklist-tab.active,
.mkt-tabs .mkt-tab.active,
body .ai-tabs .ai-tab.active,
body .admin-tabs .admin-tab.active {
  border-color: transparent;
  background: rgba(200,150,60,.14);
  color: var(--gold-bright);
  box-shadow: none;
  text-shadow: none;
}
/* Marketplace used a second active underline; the shared travelling
   surface is now the sole selection signal. */
.mkt-tabs .mkt-tab.active::after { display: none; }

.ui-tab-ind {
  position: absolute;
  top: 3px;
  left: 0;
  z-index: 0;
  height: calc(100% - 6px);
  border: 0;
  border-radius: 6px;
  background: rgba(200,150,60,.19);
  box-shadow: inset 0 1px 0 rgba(255,226,154,.09), 0 1px 4px rgba(0,0,0,.16);
  pointer-events: none;
}
.ui-tabs-slide .tab.active,
.ui-tabs-slide .plr-tab.active,
.ui-tabs-slide .checklist-tab.active,
.ui-tabs-slide .mkt-tab.active,
.ui-tabs-slide .ai-tab.active,
.ui-tabs-slide .admin-tab.active { background: none; }

/* Boss Schedule's content handoff, now shared by every tab switch through
   Motion.tabSwap(). The named snapshot is only the panel/result surface, so
   persistent page chrome never flashes. */
::view-transition-group(ui-tab-content) {
  animation-duration: 420ms;
  animation-timing-function: cubic-bezier(.22,.61,.36,1);
}
::view-transition-old(ui-tab-content),
::view-transition-new(ui-tab-content) {
  width: 100%;
  height: 100%;
  mix-blend-mode: normal;
}
html.ui-tab-forward::view-transition-old(ui-tab-content) {
  animation: ui-tab-old-forward 220ms cubic-bezier(.4,0,1,1) both;
}
html.ui-tab-forward::view-transition-new(ui-tab-content) {
  animation: ui-tab-new-forward 360ms 35ms cubic-bezier(.22,.61,.36,1) both;
}
html.ui-tab-backward::view-transition-old(ui-tab-content) {
  animation: ui-tab-old-backward 220ms cubic-bezier(.4,0,1,1) both;
}
html.ui-tab-backward::view-transition-new(ui-tab-content) {
  animation: ui-tab-new-backward 360ms 35ms cubic-bezier(.22,.61,.36,1) both;
}
@keyframes ui-tab-old-forward  { to { opacity: 0; transform: translateX(-9px); } }
@keyframes ui-tab-new-forward  { from { opacity: 0; transform: translateX(14px); } }
@keyframes ui-tab-old-backward { to { opacity: 0; transform: translateX(9px); } }
@keyframes ui-tab-new-backward { from { opacity: 0; transform: translateX(-14px); } }

/* Action feedback is the same everywhere: a one-pixel hover lift and a
   one-pixel pressed settle. Colours remain owned by each button variant. */
@media (hover: hover) {
  :where(.btn, .ui-btn, .search-btn, .sr-btn, .mkt-menu-btn,
         .nft-sort-btn, .sidebar-collapse-btn):not(:disabled):not([disabled]):hover {
    transform: translateY(-1px);
  }
}
:where(.btn, .ui-btn, .search-btn, .sr-btn, .mkt-menu-btn,
       .nft-sort-btn, .sidebar-collapse-btn):not(:disabled):not([disabled]):active {
  transform: translateY(1px);
  transition-duration: var(--dur-xs);
}

/* ── Standard control geometry ───────────────────────────────────
   Page-specific styles still own semantic colour (gold primary,
   crimson destructive), but no page gets to invent another height,
   radius or nested border. */
:where(.btn, .btn-sm, .ui-btn, .sr-btn, .mkt-menu-btn,
       .nft-dd-btn, .nft-sort-btn) {
  height: var(--control-h) !important;
  min-height: var(--control-h) !important;
  padding-block: 0 !important;
  border: 0 !important;
  border-radius: var(--control-radius) !important;
  box-sizing: border-box;
}
body .admin-wrap .btn {
  height: var(--control-h) !important;
  min-height: var(--control-h) !important;
  border-radius: var(--control-radius) !important;
}

/* The native select is retained as the source of truth, while .aui-dropdown
   is its visible shell. Resetting the shell is essential: older inline
   select chrome used to be copied onto it, creating the double boxes seen
   on Rankings and War filters. */
.aui-dropdown.aui-dropdown {
  padding: 0 !important;
  border: 0 !important;
  border-radius: var(--control-radius) !important;
  background: transparent !important;
  box-shadow: none !important;
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
}
.aui-dropdown.aui-dropdown:not(.bs-filter-select) {
  height: var(--control-h) !important;
  min-height: var(--control-h) !important;
}
.aui-dropdown:not(.bs-filter-select) .aui-dropdown-trigger {
  height: var(--control-h);
  min-height: var(--control-h);
  padding: 0 var(--control-pad-x);
  border: 0;
  border-radius: var(--control-radius);
  background: rgba(255,255,255,.055);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 8px 20px rgba(0,0,0,.12);
}
.aui-dropdown:not(.bs-filter-select) .aui-dropdown-trigger:hover:not(:disabled) {
  border: 0;
  background: rgba(255,255,255,.085);
}
.aui-dropdown:not(.bs-filter-select).is-open .aui-dropdown-trigger,
.aui-dropdown:not(.bs-filter-select) .aui-dropdown-trigger:focus-visible {
  border: 0;
  background: rgba(255,255,255,.075);
  box-shadow: var(--ring-soft), inset 0 1px 0 rgba(255,255,255,.06);
}
.aui-dropdown-panel {
  padding: 6px;
  border: 0;
  border-radius: 10px;
  background: rgba(16,21,29,.96);
  box-shadow: 0 22px 52px rgba(0,0,0,.46), inset 0 1px 0 rgba(255,255,255,.055);
}
.aui-dropdown-option {
  min-height: var(--control-h);
  border-radius: 7px;
}

/* Non-enhanced fallback and bespoke market menus resolve to the same
   dimensions and borderless glass surface. */
select:not(.aui-select-native),
.nft-dd-btn,
.nft-sort-btn {
  height: var(--control-h) !important;
  min-height: var(--control-h) !important;
  border: 0 !important;
  border-radius: var(--control-radius) !important;
  background-color: rgba(255,255,255,.055) !important;
}

/* Do not reserve a decorative tray before the user has selected a ranking
   scope. It reappears automatically as soon as updateFilterTag adds content. */
#page-rankings #rk-filter-tags:empty {
  display: none !important;
  min-height: 0 !important;
  margin: 0 !important;
  padding: 0 !important;
}

/* ── Skip link ────────────────────────────────────────────────────
   Off-screen until focused. Without it, a keyboard user tabs through
   all 18 nav items before reaching content — on every page. */
.skip-link {
  position: fixed;
  top: -100px;
  left: var(--sp-2);
  z-index: 10000;
  padding: 10px var(--sp-2);
  background: var(--surface-2);
  border: 1px solid var(--bdr-gold);
  border-radius: var(--r);
  color: var(--gold-bright);
  font-size: var(--fs-sm);
  font-weight: 600;
  text-decoration: none;
  transition: top var(--t-base);
}
.skip-link:focus { top: var(--sp-2); box-shadow: var(--ring); }

/* ── Scrollbars ───────────────────────────────────────────────────
   One rule replaces fifteen near-identical ::-webkit-scrollbar
   blocks scattered through app.css (widths ranged 4–8px, thumbs
   ranged .08–.20 alpha). Firefox gets the standard properties. */
* { scrollbar-width: thin; scrollbar-color: var(--sb-thumb) transparent; }
::-webkit-scrollbar { width: var(--sb-size); height: var(--sb-size); }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--sb-thumb);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: padding-box;
  transition: background-color var(--t-hover);
}
::-webkit-scrollbar-thumb:hover { background: var(--sb-thumb-hover); background-clip: padding-box; }
::-webkit-scrollbar-corner { background: transparent; }

/* Selection in the app's own accent rather than the browser blue. */
::selection { background: rgba(200,150,60,.28); color: var(--text-strong); }

/* ══════════════════════════════════════════════════════════════════
   2 · TYPOGRAPHY & HIERARCHY
   ══════════════════════════════════════════════════════════════════ */

/* Page header: the strongest hierarchy break on any screen. More air
   above than below so the title binds to its own content, and the
   rune underline gets a touch more room to breathe. */
.page-header {
  margin: var(--sp-3) 0 var(--sp-4);
  padding-bottom: var(--sp-3);
}
/* Page and card titles are real <h1>/<h2> now, so the document has an
   outline a screen reader can navigate. They're styled entirely by class,
   so the only thing the tag swap changes is the UA margin — zeroed here
   to keep the layout pixel-identical. */
h1.page-title, h2.card-title { margin: 0; }
h1.page-title { font-size: var(--fs-2xl); }
h2.card-title { font-size: var(--fs-sm); }
.page-title {
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-wider);
  text-wrap: balance;
}
/* Sub-heads are supporting copy, so they get body leading, a muted
   tone and a hard measure cap — several were running the full 1600px
   width of the viewport. */
.page-sub {
  line-height: var(--lh-body);
  max-width: var(--measure);
  margin-top: var(--sp-1);
  color: var(--text-muted);
}
.breadcrumb { line-height: 1; opacity: .85; }

/* Card headings and eyebrow labels — one treatment, three sizes. */
.card-title, .sp-section-label {
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-wider);
}
.stat-label, .input-label, .nav-label {
  line-height: 1.2;
  letter-spacing: var(--tr-caps);
}
/* Stat values: tabular figures and tight tracking so columns of
   numbers line up and read as data, not as prose. */
.stat-value, .wx-ccy-val, .mkt-card-price, .sp-v, .admin-list-count {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum';
  letter-spacing: var(--tr-tight);
  line-height: var(--lh-tight);
}

/* Body copy inside dense surfaces was inheriting 1.55 from body but
   being overridden ad hoc; pin the comfortable value. */
.notif-msg, .im-item .im-reply, .ai-hint, .admin-muted { line-height: var(--lh-body); }

/* ══════════════════════════════════════════════════════════════════
   3 · SPACING & SURFACES
   ══════════════════════════════════════════════════════════════════ */

/* Main column: fluid gutters instead of a fixed 28px, and a wider
   floor of breathing room at the bottom of long pages. */
.main { padding: 0 clamp(16px, 2.4vw, 32px) var(--sp-6); }

/* Cards get the elevation scale and a hairline that reads on black.
   Previously flat (no shadow at all), which made stacked cards on the
   Home dashboard fuse into one grey mass. */
.card {
  box-shadow: var(--e2);
  border-radius: var(--rl);
}
.card-header { border-radius: var(--rl) var(--rl) 0 0; }

/* Raised, interactive surfaces — floating panels sit two steps up so
   the layering is legible rather than uniform. */
.cmdk-modal, .notif-center, .lookup-suggest, .ui-modal { box-shadow: var(--e4); }
.mkt-card:hover, .sp-floor-card:hover { box-shadow: var(--e-pop); }

/* Table rhythm: header row slightly tighter than body rows so the
   head reads as a rail, and a subtle zebra that survives the hover. */
.tbl th { padding-top: var(--sp-1); padding-bottom: var(--sp-1); }
.tbl tbody tr { transition: background-color var(--t-hover); }
.tbl tbody tr:nth-child(even) td { background: rgba(255,255,255,.012); }
.tbl tbody tr:hover td { background: rgba(255,255,255,.04); }

/* ══════════════════════════════════════════════════════════════════
   4 · REUSABLE COMPONENTS

   Each of these replaces a block that had been copy-pasted across
   index.html's inline <style> islands. The originals are deleted
   there; the elements now carry these classes.
   ══════════════════════════════════════════════════════════════════ */

/* ── Overlay + modal ─────────────────────────────────────────────
   Shared form controls appear first; modal surfaces follow. */
/* Animated checkbox: Animate UI's Checkbox/Indicator structure with the real
   input retained for keyboard, form and browser semantics. */
.aui-checkbox-host { position: relative; align-items: center; cursor: pointer; }
.aui-checkbox-host.aui-checkbox-only {
  display: inline-flex;
  justify-content: center;
  vertical-align: middle;
}
.aui-checkbox-native {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  min-width: 1px !important;
  min-height: 1px !important;
  margin: 0 !important;
  padding: 0 !important;
  opacity: 0 !important;
  pointer-events: none;
}
.aui-checkbox-box {
  position: relative;
  display: inline-grid;
  place-items: center;
  flex: 0 0 18px;
  width: 18px;
  height: 18px;
  box-sizing: border-box;
  border: 0;
  border-radius: 5px;
  color: #111318;
  background: rgba(255,255,255,.085);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.16), 0 1px 3px rgba(0,0,0,.2);
  transition: background-color var(--t-base), color var(--t-base),
              box-shadow var(--t-base), transform var(--t-hover);
}
.aui-checkbox-host:hover .aui-checkbox-box {
  background: rgba(255,255,255,.13);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.24), 0 1px 4px rgba(0,0,0,.25);
}
.aui-checkbox-host.is-checked .aui-checkbox-box,
.aui-checkbox-host.is-indeterminate .aui-checkbox-box,
.checklist-td-cell.done .aui-checkbox-box {
  background: var(--gold);
  color: #111318;
  box-shadow: inset 0 1px 0 rgba(255,241,198,.38), 0 2px 6px rgba(200,150,60,.2);
}
.aui-checkbox-box svg {
  width: 13px;
  height: 13px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.35;
  stroke-linecap: round;
  stroke-linejoin: round;
  overflow: visible;
}
.aui-checkbox-box .aui-check,
.aui-checkbox-box .aui-minus {
  transform-origin: center;
  opacity: 0;
  transform: scale(.62);
  transition: opacity var(--t-hover), transform var(--t-base);
}
.aui-checkbox-host.is-checked .aui-check,
.checklist-td-cell.done .aui-check,
.aui-checkbox-host.is-indeterminate .aui-minus {
  opacity: 1;
  transform: scale(1);
}
.aui-checkbox-host:has(.aui-checkbox-native:focus-visible) .aui-checkbox-box,
[role="checkbox"]:focus-visible .aui-checkbox-box {
  outline: 2px solid color-mix(in srgb, var(--gold) 72%, white);
  outline-offset: 2px;
}
.aui-checkbox-host.is-disabled { cursor: not-allowed; opacity: .45; }
.checklist-td-cell .checklist-checkbox.aui-checkbox-box {
  width: 18px;
  height: 18px;
  border: 0;
  border-radius: 5px;
  color: #111318;
  background: rgba(255,255,255,.085);
}
.checklist-td-cell.done .checklist-checkbox.aui-checkbox-box {
  background: var(--gold);
  color: #111318;
}

/* Overlay and modal surfaces.
   Was duplicated five times (#feedback-overlay, #inmail-overlay,
   #mkt-new-overlay, #mkt-detail-overlay, #mkt-lightbox-overlay) with
   drifting z-indexes and blur radii. `display:none` is the resting
   state; the app's open functions set `display:flex` inline, and
   motion.js animates on that mutation. */
.ui-overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 9990;
  align-items: center;
  justify-content: center;
  padding: var(--sp-2);
  background: rgba(6,7,9,.74);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  overscroll-behavior: contain;
}
.ui-overlay.open { display: flex; }
/* Lightboxes want the image, not the room it's in — and they opt out of
   the mobile bottom-sheet treatment below, since a full-bleed image has
   nowhere to put a sheet edge. */
.ui-overlay--lightbox { background: rgba(3,4,6,.88); }

.ui-modal {
  width: 100%;
  max-width: 520px;
  max-height: min(88vh, 900px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: linear-gradient(180deg, var(--surface-1) 0%, var(--surface-0) 100%);
  border: 1px solid var(--bdr2);
  border-radius: var(--cut);
  box-shadow: var(--e4);
}
/* Gold hairline along the top edge — the same signature the cards
   carry, so modals read as part of the same system. */
.ui-modal::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--gold), transparent);
  opacity: .4;
  pointer-events: none;
}
.ui-modal { position: relative; }
.ui-modal--wide   { max-width: 720px; }
.ui-modal--narrow { max-width: 440px; }
/* For dialogs whose internals aren't head/body/foot — the whole panel
   scrolls as one flow instead of a pinned header over a scroll area. */
.ui-modal--flow { display: block; overflow-y: auto; }
/* Lightboxes: the image *is* the dialog, so the chrome comes off. */
.ui-modal--bare {
  background: none;
  border: 0;
  box-shadow: none;
  overflow: visible;
  max-width: 92vw;
  max-height: 88vh;
  width: auto;
}
.ui-modal--bare::before { display: none; }

.ui-modal-head {
  position: relative;
  flex: 0 0 auto;
  padding: var(--sp-3) var(--sp-3) var(--sp-2);
  border-bottom: 1px solid var(--bdr);
}
.ui-modal-head h3 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 700;
  letter-spacing: var(--tr-wide);
  line-height: var(--lh-snug);
  color: var(--gold-bright);
}
/* Two header washes: gold for informational dialogs, crimson for the
   ones that ask something of the player. */
.ui-modal-head--center  { text-align: center; background: radial-gradient(120% 90% at 50% 0%, rgba(200,150,60,.16), transparent 70%); }
.ui-modal-head--crimson { background: radial-gradient(120% 90% at 50% 0%, rgba(139,0,0,.28), transparent 70%); }
.ui-modal-sub { margin-top: 4px; font-size: var(--fs-sm); color: var(--text-muted); line-height: var(--lh-body); }

.ui-modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--sp-3);
}
.ui-modal-foot {
  flex: 0 0 auto;
  display: flex;
  gap: var(--sp-1);
  justify-content: flex-end;
  padding: var(--sp-2) var(--sp-3);
  border-top: 1px solid var(--bdr);
}

.ui-modal-close {
  position: absolute;
  top: 10px;
  right: 12px;
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r);
  color: var(--text-dim);
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
  transition: color var(--t-hover), background-color var(--t-hover), border-color var(--t-hover);
}
.ui-modal-close:hover {
  color: var(--text-strong);
  background: rgba(255,255,255,.05);
  border-color: var(--bdr2);
}

/* ── Field ───────────────────────────────────────────────────────
   The label + control pair, previously re-declared as .mkt-field,
   .ai-composer textarea, .fb-field and the auth inputs. */
.ui-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: var(--sp-2); }
.ui-field > label,
.cs-field > label,
.mkt-field > label {
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
  color: var(--text-muted);
}
/* One control appearance for every field in the app. The AI Studio,
   Character Studio and Marketplace composers each shipped their own
   copy of these five declarations; they now point here. */
.ui-field input,
.ui-field textarea,
.ui-field select,
.ai-composer textarea,
.ai-controls select,
.ai-code-row input,
.cs-field input[type=text],
.cs-field select,
.cs-field textarea {
  width: 100%;
  box-sizing: border-box;
  background: var(--surface-2);
  border: 1px solid var(--bdr2);
  border-radius: var(--r);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  padding: 9px 11px;
  outline: none;
  transition: border-color var(--t-hover), box-shadow var(--t-hover), background-color var(--t-hover);
}
.ui-field textarea,
.ai-composer textarea,
.cs-field textarea { min-height: 84px; resize: vertical; }

.ui-field input:hover, .ui-field textarea:hover,
.ai-composer textarea:hover, .cs-field textarea:hover { border-color: var(--bdr3); }

.ui-field input:focus, .ui-field textarea:focus, .ui-field select:focus,
.ai-composer textarea:focus, .ai-controls select:focus, .ai-code-row input:focus,
.cs-field input[type=text]:focus, .cs-field select:focus, .cs-field textarea:focus {
  border-color: var(--gold-soft);
  box-shadow: var(--ring-soft);
}
.ui-field-hint { font-size: var(--fs-xs); color: var(--text-dim); line-height: var(--lh-snug); }

/* ── Chip / pill ─────────────────────────────────────────────────
   Filter pills, quota badges and status tags were three separate
   implementations of the same object. */
/* .ai-quota is built by app.js as a plain string, so it's aliased onto
   the chip here rather than having the class added at every call site. */
.ui-chip,
.ai-quota {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border: 1px solid var(--bdr2);
  border-radius: 999px;
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: .04em;
  white-space: nowrap;
  cursor: default;
}
.ui-chip--action { cursor: pointer; }
.ui-chip--action:hover { color: var(--text); border-color: var(--bdr3); background: rgba(255,255,255,.03); }
.ui-chip.is-on {
  color: var(--gold-bright);
  border-color: var(--bdr-gold);
  background: rgba(200,150,60,.10);
}
.ui-chip--danger,
.ai-quota.spent { color: #ff8a9b; border-color: var(--bdr-red); background: rgba(139,0,0,.12); }
.ui-chip--good   { color: var(--jade); border-color: rgba(0,184,148,.35); background: rgba(0,184,148,.10); }

/* ── Button ──────────────────────────────────────────────────────
   A neutral primitive for new markup. Existing .btn is untouched —
   .ui-btn simply gives the same geometry a single source of truth. */
.ui-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  height: 34px;
  padding: 0 var(--sp-2);
  border: 1px solid var(--bdr2);
  border-radius: var(--r);
  background: var(--surface-2);
  color: var(--text-muted);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: var(--tr-wide);
  text-transform: uppercase;
  cursor: pointer;
  white-space: nowrap;
}
.ui-btn:hover { background: var(--surface-3); color: var(--text); border-color: var(--bdr3); }
.ui-btn[disabled], .ui-btn.is-busy { opacity: .5; pointer-events: none; }
.ui-btn--primary {
  background: linear-gradient(135deg, rgba(139,0,0,.55), rgba(139,0,0,.28));
  border-color: rgba(139,0,0,.55);
  color: #fff;
}
.ui-btn--primary:hover {
  background: linear-gradient(135deg, rgba(160,0,0,.75), rgba(139,0,0,.40));
  border-color: rgba(200,60,60,.6);
  box-shadow: var(--glow-red);
  color: #fff;
}
.ui-btn--gold {
  background: linear-gradient(135deg, rgba(200,150,60,.20), rgba(200,150,60,.07));
  border-color: var(--bdr-gold);
  color: var(--gold-bright);
}
.ui-btn--gold:hover { background: linear-gradient(135deg, rgba(200,150,60,.32), rgba(200,150,60,.12)); box-shadow: var(--glow-gold-sm); }
.ui-btn--ghost { background: transparent; border-color: transparent; }
.ui-btn--ghost:hover { background: rgba(255,255,255,.04); border-color: var(--bdr2); }

/* ── Skeleton ────────────────────────────────────────────────────
   Replaces the Marketplace-only shimmer with a shared loading
   surface, so every page can show structure instead of a spinner. */
.ui-skeleton,
.skel {
  position: relative;
  overflow: hidden;
  border-radius: var(--r);
  background: var(--surface-2);
}
.ui-skeleton::after,
.skel::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 30%, rgba(255,255,255,.055) 50%, transparent 70%);
  background-size: 200% 100%;
  animation: uiShimmer 1.4s ease-in-out infinite;
}
.ui-skeleton--text { height: 11px; margin: 8px 0; }
.ui-skeleton--title { height: 16px; width: 45%; margin: 10px 0; }
.ui-skeleton--block { height: 100%; width: 100%; }
@keyframes uiShimmer { from { background-position: 140% 0; } to { background-position: -40% 0; } }

/* ── Toast ───────────────────────────────────────────────────────
   app.js's toast() previously referenced `animation: fadeIn .2s` —
   a keyframe that does not exist anywhere in the stylesheet, so the
   toast simply appeared. It's now a class, animated by motion.js. */
.ui-toast {
  position: fixed;
  right: var(--sp-3);
  bottom: var(--sp-3);
  z-index: 99999;
  max-width: min(380px, calc(100vw - 32px));
  padding: 11px var(--sp-2);
  background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
  border: 1px solid var(--bdr2);
  border-left: 2px solid var(--gold);
  border-radius: var(--r);
  box-shadow: var(--e3);
  color: var(--text);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
}

/* ── Empty states ────────────────────────────────────────────────
   Seven "nothing here" treatments had grown up independently, each
   picking its own size and tone. The audit counted ten, but three of
   those (.nftp-equip-empty, .nftp-spirit-portrait--empty) are empty
   *slot frames* in the NFT inventory, which are a different thing and
   are left alone.

   Aliased rather than replaced: these classes are emitted from a dozen
   render functions as plain strings, so pointing them at one rule here
   dedupes the styling without a sweep through app.js. Each keeps its
   own padding — a dropdown row, a table cell and a full panel need
   different amounts of room. */
.empty,
.cmdk-empty,
.notif-empty,
.lookup-suggest-empty,
.checklist-empty,
#inmail-empty {
  color: var(--text-dim);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  letter-spacing: .02em;
  text-align: center;
}
.home-row-empty, .hv-empty-cell { color: var(--text-dim); font-style: italic; }

/* ── Divider / section rule ──────────────────────────────────────— */
.ui-divider {
  height: 1px;
  border: 0;
  margin: var(--sp-3) 0;
  background: linear-gradient(90deg, var(--bdr2), transparent);
}

/* ══════════════════════════════════════════════════════════════════
   5 · MOTION START STATES

   Scoped to html.motion-ready — the class motion.js adds once it has
   booted. If the script fails, 404s, or is blocked, none of these
   rules apply and every element renders visible. Content is never
   hidden by CSS that depends on JS to reveal it.
   ══════════════════════════════════════════════════════════════════ */
.motion-ready:not(.motion-reduced) [data-reveal]:not(.is-revealed),
.motion-ready:not(.motion-reduced) [data-reveal-stagger]:not(.is-revealed) > * {
  opacity: 0;
}

/* Hover lift for card-like surfaces — declared here so the whole
   family shares one distance and one curve. */
.ui-lift { transition: transform var(--t-base), box-shadow var(--t-base), border-color var(--t-hover); }
.ui-lift:hover { transform: translateY(-2px); box-shadow: var(--e-pop); }

/* app.css's pageReveal keyframe is superseded by motion.js's staggered
   entrance — running both means the page fades twice. Scoped to
   .motion-ready so the CSS fade still covers the no-JS case. */
.motion-ready .page.active { animation: none; }

/* ── Shared entrance keyframes ───────────────────────────────────
   For elements built by innerHTML, where a CSS animation is simpler
   than reaching for the JS layer. These three replace the six near
   identical one-off keyframes that were declared across index.html's
   inline <style> islands (fbPop, mktCardIn, mktCommentIn,
   mktLightboxIn, nftpStatsSlide, nftpStatsFadeIn). */
@keyframes uiPop  { from { opacity: 0; transform: translateY(8px) scale(.97); } to { opacity: 1; transform: none; } }
@keyframes uiRise { from { opacity: 0; transform: translateY(6px); }            to { opacity: 1; transform: none; } }
@keyframes uiZoom { from { opacity: 0; transform: scale(.94); }                 to { opacity: 1; transform: none; } }

.ui-anim-pop  { animation: uiPop  var(--dur-md) var(--ease-emph) both; }
.ui-anim-rise { animation: uiRise var(--dur-sm) var(--ease-out)  both; }
.ui-anim-zoom { animation: uiZoom var(--dur-sm) var(--ease-out)  both; }

/* ══════════════════════════════════════════════════════════════════
   6 · REDUCED MOTION

   One switch for the entire app, including the decorative loops
   (fbGlow, shimmer) that JS never touches. bs-pulse and srPulse used to
   be on this list; both idled forever and neither earned it, so they
   were removed outright rather than only for users who ask for calm.
   Durations go to ~0 rather than `none` so transitionend/animationend
   handlers still fire and no state machine stalls.
   ══════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  /* Purely decorative pulses have nothing to communicate at 0ms —
     drop them entirely so nothing flickers on a single frame. */
  #feedback-fab, .sr-notif-badge, .ui-skeleton::after, .skel::after,
  .bs-pulse, .mkt-skel .mkt-skel-img, .mkt-skel .mkt-skel-line {
    animation: none !important;
  }
  .ui-lift:hover, .mkt-card:hover { transform: none; }
}

/* ══════════════════════════════════════════════════════════════════
   7 · RESPONSIVE POLISH
   ══════════════════════════════════════════════════════════════════ */
@media (max-width: 720px) {
  .page-header { margin: var(--sp-2) 0 var(--sp-3); padding-bottom: var(--sp-2); }
  .page-title { font-size: var(--fs-xl); }
  .page-sub { font-size: var(--fs-xs); }

  /* ── Tables ───────────────────────────────────────────────────
     Global Rankings and Clan Rankings were CLIPPING at 375px: a
     420px table inside a 343px card with no overflow, so the
     right-hand columns simply couldn't be reached. Only 2 of the 23
     tables in the app had a scroll wrapper. Rather than wrap the
     other 21 by hand, the table itself becomes the scroll container.

     display:block on the table alone is what makes this work: thead and
     tbody keep their default table-row-group display, so the browser
     wraps them in ONE anonymous table box and the columns stay aligned.
     (Setting display:table on them individually splits the table in two
     and the header stops lining up with the rows.) */
  .tbl {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  /* Data cells shouldn't wrap mid-value when there's somewhere to scroll. */
  .tbl td, .tbl th { white-space: nowrap; }

  /* The status rail overflowed its own width at 375px, hiding the clock
     and the WEMIX price entirely. Let it scroll instead of truncate. */
  .status-rail {
    overflow-x: auto;
    scrollbar-width: none;
  }
  .status-rail::-webkit-scrollbar { display: none; }
  .ui-overlay { padding: 0; align-items: flex-end; }
  /* On phones a centred dialog fights the keyboard; a bottom sheet
     with a square lower edge sits where the thumb already is. */
  .ui-modal {
    max-width: 100%;
    max-height: 92vh;
    border-radius: var(--cut) var(--cut) 0 0;
    border-bottom: 0;
  }
  /* …except lightboxes, which stay centred with room for their
     floating close button. */
  .ui-overlay--lightbox { align-items: center; padding: var(--sp-2); }
  .ui-overlay--lightbox .ui-modal--bare { border-radius: 0; max-height: 88vh; }
  .ui-toast { right: var(--sp-2); left: var(--sp-2); bottom: var(--sp-2); max-width: none; }
}

/* Keep the primary control system geometrically stable on touch devices.
   Thirty-six pixels remains above the 24px minimum; icon-only dismiss
   controls retain the larger 40px hit area because they have no label. */
@media (pointer: coarse) {
  .ui-btn, .btn, .btn-sm { height: var(--control-h); min-height: var(--control-h); }
  .ui-modal-close { width: 40px; height: 40px; }
  .tab, .plr-tab, .checklist-tab, .mkt-tab, .ai-tab,
  .admin-tab { height: 30px; min-height: 30px; }
  .sp-filter, .sp-offset-chip, .ui-chip--action { min-height: 34px; }
  /* Native controls the browser sizes from the font */
  input[type="checkbox"], input[type="radio"] { min-width: 22px; min-height: 22px; }
}
