/* ============================================================================
   tokens.css — semantic token layer for Reddit Pro
   Sits on top of FlyonUI v2 / DaisyUI v5. Everything FlyonUI does NOT cover:
   paired dark/light semantics, density, the overlay panel primitive,
   z-index scale, motion, data-table geometry.

   Rules:
   - No hardcoded color may appear in markup. Every color is a token,
     and every token is defined for BOTH themes.
   - Light is not an inversion of dark: tints mix with white, shadows get
     shorter, borders do more work.
   - Scale: spacing 4px steps · radius 6 / 8 / 999 · type 11 / 12 / 13 / 14 / 20
   ==========================================================================*/

/* ── 1. Theme-independent scales ───────────────────────────────────────── */
:root {
  --r-sm: 6px;
  --r-md: 8px;
  --r-full: 9999px;

  --fs-label: 11px;   /* uppercase column labels, section labels */
  --fs-xs: 12px;      /* compact table body, meta */
  --fs-sm: 13px;      /* default body, overlay items, controls */
  --fs-md: 14px;      /* emphasis */
  --fs-h1: 20px;      /* page title */

  /* z-index scale — nothing in the app may use a raw z-index above these */
  /* 20 was --z-sticky-col, deleted with horizontal column pinning */
  --z-sticky-head: 30;
  --z-page-header: 40;
  --z-dropdown: 50;
  --z-popover: 60;
  --z-tooltip: 70;
  --z-modal: 80;
  --z-toast: 90;

  /* motion */
  --motion-overlay: 120ms;
  --motion-ui: 150ms;
  --motion-layout: 200ms;
  --ease-overlay: cubic-bezier(.2, .8, .2, 1);
  --ease-ui: cubic-bezier(.4, 0, .2, 1);

  /* overlay panel geometry (identical in both themes) */
  --overlay-pad: 4px;
  --overlay-gap: 6px;
  --overlay-min-w: 200px;
  --overlay-max-w: 360px;
  --overlay-max-h: 320px;
  --overlay-radius: var(--r-md);
}

/* ── 2. Geometry ───────────────────────────────────────────────────────────
   One density. The compact variant is gone: two sets of row heights, cell
   padding and column widths meant every width measurement had to state which
   density it was taken at, and half of them silently didn't. The kept values are
   the comfortable ones. */
:root {
  --row-h: 48px;
  --cell-px: 16px;
  --table-fs: var(--fs-sm);
  --control-h: 36px;
  --item-h: 36px;
  --avatar-size: 20px;
  --col-check: 40px;
  --col-status: 148px;
  --col-age: 80px;
  --col-karma: 124px;   /* content needs 95; the 16px went to CQS */
  --col-cqs: 104px;     /* the select renders "Moderate" + the caret: 101 */
  --col-bb: 88px;
  --col-geo: 88px;
  --col-project: 120px;
  --col-kebab: 44px;
}


/* ── 3. Colour tokens — DARK ───────────────────────────────────────────── */
[data-theme="dark"] {
  color-scheme: dark;

  --bg-app: #15161a;
  --bg-surface: #1b1c22;
  --bg-inset: #121317;

  --overlay-bg: #23252e;
  --overlay-border: #333644;
  --shadow-overlay: 0 8px 24px rgba(0, 0, 0, .45);
  --shadow-card: 0 2px 8px rgba(0, 0, 0, .30);

  --text: #e7e7ea;            /* ≥7:1 on --bg-app */
  --text-muted: #a2a6b4;      /* ≥4.5:1 — was e7e7ea/50 = 3.1:1 (fixed) */
  --text-subtle: #8b8f9c;     /* ≥4.5:1, for de-emphasised meta only */

  --border-hairline: rgba(255, 255, 255, .08);
  --border-strong: #333644;

  --row-hover: rgba(255, 255, 255, .04);   /* light goes UP in dark */
  --row-selected: rgba(124, 92, 255, .08);
  --row-focus: rgba(255, 255, 255, .06);

  --primary: #7c5cff;
  --primary-hover: #8f74ff;
  --primary-fg: #ffffff;
  --primary-tint: rgba(124, 92, 255, .14);
  --primary-tint-strong: rgba(124, 92, 255, .22);
  --primary-text-on-tint: #b6a2ff;

  --focus-ring: #9b83ff;      /* lighter than the surface in dark */

  /* status pairs — tint bg + full-strength fg, no border needed in dark */
  --ok-bg: rgba(74, 222, 128, .12);
  --ok-fg: #86c9a0;
  --ok-bd: transparent;
  --err-bg: rgba(248, 113, 113, .12);
  --err-fg: #e09b9b;
  --err-bd: transparent;
  --warn-bg: rgba(251, 191, 36, .12);
  --warn-fg: #fbbf24;
  --warn-bd: transparent;
  --info-bg: rgba(56, 189, 248, .12);
  --info-fg: #38bdf8;
  --info-bd: transparent;
  --neutral-bg: rgba(231, 231, 234, .10);
  --neutral-fg: #a2a6b4;
  --neutral-bd: transparent;

  --skeleton-base: rgba(255, 255, 255, .05);
  --skeleton-shine: rgba(255, 255, 255, .11);

  --scroll-thumb: oklch(40% .01 265);
}

/* ── 4. Colour tokens — LIGHT ──────────────────────────────────────────── */
[data-theme="light"] {
  color-scheme: light;

  --bg-app: #fafafa;                /* never pure #fff */
  --bg-surface: #ffffff;            /* white panel lifts off the app bg */
  --bg-inset: #f4f4f6;

  --overlay-bg: #ffffff;
  --overlay-border: #d8d9e0;        /* border does the lifting in light */
  --shadow-overlay: 0 4px 12px rgba(16, 18, 27, .10);   /* shorter, softer */
  --shadow-card: 0 1px 3px rgba(16, 18, 27, .07);

  --text: #1a1c23;                  /* ≥7:1 on white */
  --text-muted: #5c6070;            /* 5.9:1 */
  --text-subtle: #6e7280;           /* 4.8:1 */

  --border-hairline: rgba(16, 18, 27, .10);
  --border-strong: #d8d9e0;

  --row-hover: rgba(16, 18, 27, .03);   /* light goes DOWN in light */
  --row-selected: rgba(109, 74, 255, .06);
  --row-focus: rgba(16, 18, 27, .05);

  --primary: #6d4aff;
  --primary-hover: #5b36f5;
  --primary-fg: #ffffff;
  --primary-tint: color-mix(in oklch, #6d4aff 12%, white);
  --primary-tint-strong: color-mix(in oklch, #6d4aff 20%, white);
  --primary-text-on-tint: #5b36f5;

  --focus-ring: #5b36f5;            /* darker than the surface in light */

  /* status pairs — tint mixes with WHITE (transparency would vanish),
     text is a dark shade of the same hue, 1px border keeps the shape */
  --ok-bg: color-mix(in oklch, #16a34a 9%, white);
  /* Softened per the 2026-08-15 conformance spec. designs/tokens.css still has
     the raw Tailwind #15803d / #b91c1c here — the softening landed in the dark
     theme only and left light half-updated (mixes moved, foregrounds did not).
     The spec is the newer document; do not "restore" these from the copy. */
  --ok-fg: #3f7d5c;
  --ok-bd: color-mix(in oklch, #16a34a 22%, white);
  --err-bg: color-mix(in oklch, #dc2626 9%, white);
  --err-fg: #a2504f;
  --err-bd: color-mix(in oklch, #dc2626 22%, white);
  --warn-bg: color-mix(in oklch, #d97706 12%, white);
  --warn-fg: #a45309;
  --warn-bd: color-mix(in oklch, #d97706 28%, white);
  --info-bg: color-mix(in oklch, #0284c7 10%, white);
  --info-fg: #036394;
  --info-bd: color-mix(in oklch, #0284c7 26%, white);
  --neutral-bg: #f1f1f4;
  --neutral-fg: #5c6070;
  --neutral-bd: #e2e3e9;

  --skeleton-base: rgba(16, 18, 27, .06);
  --skeleton-shine: rgba(16, 18, 27, .11);

  --scroll-thumb: oklch(78% .005 265);
}

/* DaisyUI bridge — keep FlyonUI components on the same semantics */
[data-theme="dark"], [data-theme="light"] {
  --color-base-100: var(--bg-app);
  --color-base-200: var(--bg-surface);
  --color-base-300: var(--border-strong);
  --color-base-content: var(--text);
  --color-primary: var(--primary);
  --color-primary-content: var(--primary-fg);
  --color-success: var(--ok-fg);
  --color-error: var(--err-fg);
  --color-warning: var(--warn-fg);
  --color-info: var(--info-fg);
}

/* ── 5. Base ───────────────────────────────────────────────────────────── */
html, body {
  margin: 0;
  background: var(--bg-app);
  color: var(--text);
}
body {
  font-family: 'Inter', system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  transition: background-color var(--motion-ui) var(--ease-ui),
              color var(--motion-ui) var(--ease-ui);
}
a { color: var(--primary); text-decoration: none; }
a:hover { color: var(--primary-hover); }
* { scrollbar-color: var(--scroll-thumb) transparent; scrollbar-width: thin; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: var(--r-full); }

.material-symbols-outlined {
  font-family: 'Material Symbols Outlined';
  font-weight: normal; font-style: normal; line-height: 1;
  letter-spacing: normal; text-transform: none; white-space: nowrap; direction: ltr;
  font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 20;
  font-size: 18px;
}
.mono { font-family: 'IBM Plex Mono', ui-monospace, monospace; }
.num { font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }

:where(a, button, input, select, textarea, [tabindex]):focus-visible,
.dt-row:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
  border-radius: var(--r-sm);
}

/* ── 6. Badges ─────────────────────────────────────────────────────────── */
.b {
  display: inline-flex; align-items: center; gap: 4px;
  width: fit-content;                     /* badges hug content, never fixed */
  padding: 3px 8px;
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  font-size: var(--fs-label);
  /* 16px, not the design copy's 1.3. At --fs-label 11px that ratio is 14.3px, so
     the badge computes to 22.297px and its centre never lands on a whole pixel —
     which is half of why badges read as sitting slightly high against their
     neighbours. A deliberate divergence from designs/tokens.css: do not "restore"
     it during the §4.4 sync. */
  font-weight: 600; line-height: 16px;
  white-space: nowrap;
}
.b-ok      { background: var(--ok-bg);      color: var(--ok-fg);      border-color: var(--ok-bd); }
.b-err     { background: var(--err-bg);     color: var(--err-fg);     border-color: var(--err-bd); }
.b-warn    { background: var(--warn-bg);    color: var(--warn-fg);    border-color: var(--warn-bd); }
.b-info    { background: var(--info-bg);    color: var(--info-fg);    border-color: var(--info-bd); }
.b-neutral { background: var(--neutral-bg); color: var(--neutral-fg); border-color: var(--neutral-bd); }
.b-primary {
  background: var(--primary-tint); color: var(--primary-text-on-tint);
  border-radius: var(--r-full); padding: 2px 8px;
}
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 2px 8px; border-radius: var(--r-full);
  background: var(--neutral-bg); color: var(--neutral-fg);
  border: 1px solid var(--neutral-bd);
  font-size: var(--fs-xs); font-weight: 600; line-height: 16px;   /* even height, see .b */
}
.empty { color: var(--text-subtle); }      /* the ONLY empty marker is an em dash */

/* Media capability icons: on and off are the same hue, so the row reads as
   "this one yes, that one no" rather than "coloured vs grey". */
.media-on  { color: var(--info-fg); }
.media-off { color: color-mix(in oklch, var(--info-fg) 30%, transparent); }

/* ── 7. Controls ───────────────────────────────────────────────────────── */
.ctl {
  height: var(--control-h);
  display: inline-flex; align-items: center; gap: 6px;
  padding: 0 12px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  background: transparent; color: var(--text);
  font-family: inherit; font-size: var(--fs-sm); font-weight: 500;
  cursor: pointer;
  transition: background-color var(--motion-ui) var(--ease-ui),
              border-color var(--motion-ui) var(--ease-ui);
}
.ctl:hover { background: var(--row-focus); }
.ctl-primary {
  background: var(--primary); border-color: var(--primary);
  color: var(--primary-fg); font-weight: 600;
}
.ctl-primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
.ctl-quiet { border-color: transparent; color: var(--text-muted); }
.ctl-quiet:hover { background: var(--row-focus); color: var(--text); }
.ctl-danger { border-color: transparent; background: var(--err-bg); color: var(--err-fg); }
.ctl[aria-disabled="true"], .ctl:disabled {
  opacity: .4; cursor: not-allowed; pointer-events: none;
}
.ctl-icon { width: var(--control-h); padding: 0; justify-content: center; }

.field {
  height: var(--control-h);
  display: flex; align-items: center; gap: 8px;
  padding: 0 10px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  background: var(--bg-inset);
}
.field:focus-within { border-color: var(--focus-ring); box-shadow: 0 0 0 1px var(--focus-ring); }
.field input {
  flex: 1; min-width: 0;
  border: 0; outline: 0; background: transparent;
  color: var(--text); font-family: inherit; font-size: var(--fs-sm);
}
.field input::placeholder { color: var(--text-subtle); }

/* segmented control — used for Density and Theme */
.seg {
  display: inline-flex; padding: 2px; gap: 2px;
  background: var(--bg-inset);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
}
.seg > button {
  height: calc(var(--control-h) - 8px);
  padding: 0 10px; border: 0; border-radius: 4px;
  background: transparent; color: var(--text-muted);
  font-family: inherit; font-size: var(--fs-xs); font-weight: 600; cursor: pointer;
  display: inline-flex; align-items: center; gap: 5px;
  /* Метка сегмента не переносится никогда. flex:1 уравнивает кнопки по ширине, и
     та, чья метка длиннее, получала ширину соседа: «Single post» на /downloader
     складывался в две строки внутри кнопки фиксированной высоты 28px и вылезал из
     рамки (замерено: текст 30px в клиентской области 28px). Без переноса контейнер
     вместо этого растёт до самой длинной метки. */
  white-space: nowrap;
}
.seg > button:hover { color: var(--text); }
/* Two-row variant, used by the sidebar theme switcher. It has to declare
   grid-auto-flow and grid-auto-columns, not just the template: the switcher is a
   direct child of a sidebar <li>, and FlyonUI's .menu styles every such child
   with `grid-auto-columns: minmax(auto,max-content) auto max-content;
   grid-auto-flow: column`. That flattened the two rows into one, squeezed the
   declared 1fr 1fr into two ~14px tracks and left "Auto" — which spans the full
   row — clipped on top of Light and Dark. */
.seg-stack {
  display: grid; width: 100%;
  grid-template-columns: 1fr 1fr;
  grid-auto-flow: row;
  grid-auto-columns: auto;
  gap: 4px;
}
.seg-stack > .seg-wide { grid-column: 1 / -1; }
.seg > button[aria-checked="true"] {
  background: var(--bg-surface); color: var(--text);
  box-shadow: var(--shadow-card);
}

/* ── 8. THE overlay panel primitive ────────────────────────────────────────
   Every overlay in the product is this panel with different content:
   dropdown, select, multi-select, row-action menu, columns, filters popover,
   saved views, user menu, date-range picker. Same tokens, same behaviour.
   ------------------------------------------------------------------------*/
.panel {
  position: absolute;
  min-width: var(--overlay-min-w);
  max-width: var(--overlay-max-w);
  padding: var(--overlay-pad);
  background: var(--overlay-bg);
  border: 1px solid var(--overlay-border);
  border-radius: var(--overlay-radius);
  box-shadow: var(--shadow-overlay);
  z-index: var(--z-dropdown);
  animation: panel-in var(--motion-overlay) var(--ease-overlay);
}
.panel-popover { z-index: var(--z-popover); }
/* Form pickers are overlays too. They were carrying a raw Tailwind z-50 to clear
   the form around them; the layer belongs to the scale, where it can be reasoned
   about against the kebab, the popovers and the drawer. */
.picker-panel { z-index: var(--z-dropdown); }
.panel-below  { top: calc(100% + var(--overlay-gap)); }
.panel-above  { bottom: calc(100% + var(--overlay-gap)); }  /* flip */
.panel-start  { left: 0; }
.panel-end    { right: 0; }                                  /* horizontal shift */
.panel-fit    { min-width: 100%; }                           /* select = trigger width */

@keyframes panel-in {
  from { opacity: 0; transform: translateY(-2px); }
  to   { opacity: 1; transform: none; }
}
.panel-above { animation-name: panel-in-up; }
@keyframes panel-in-up {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: none; }
}

.panel-scroll {
  max-height: var(--overlay-max-h);
  overflow-y: auto;
  overscroll-behavior: contain;      /* overlay must not steal page scroll */
}
.panel-fade-bottom { position: relative; }
.panel-fade-bottom::after {
  content: ''; position: absolute; inset: auto 0 0 0; height: 20px;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent, var(--overlay-bg));
  border-radius: 0 0 var(--overlay-radius) var(--overlay-radius);
}

.panel-label {
  position: sticky; top: 0; z-index: 1;
  padding: 6px 10px 4px;
  background: var(--overlay-bg);
  font-size: var(--fs-label); font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-subtle);
}
.panel-divider {
  height: 1px; margin: 4px calc(var(--overlay-pad) * -1);
  background: var(--border-hairline);
}
.panel-search {
  position: sticky; top: 0; z-index: 2;
  padding: 4px; background: var(--overlay-bg);
  border-bottom: 1px solid var(--border-hairline);
  margin: 0 calc(var(--overlay-pad) * -1) 4px;
}

.panel-item {
  width: 100%; box-sizing: border-box;
  min-height: var(--item-h);
  display: flex; align-items: center; gap: 8px;
  padding: 6px 10px;
  border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--text);
  font-family: inherit; font-size: var(--fs-sm); font-weight: 500;
  text-align: left; cursor: pointer;
}
.panel-item:hover { background: var(--row-hover); }
.panel-item[aria-current="true"], .panel-item[aria-selected="true"] { color: var(--primary); }
.panel-item-trail { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; }
.panel-item-shortcut {
  font-family: 'IBM Plex Mono', monospace; font-size: var(--fs-label);
  color: var(--text-subtle);
}
.panel-item-danger { color: var(--err-fg); }
.panel-item-danger:hover { background: var(--err-bg); }
.panel-item[aria-disabled="true"] {
  opacity: .4; cursor: not-allowed; pointer-events: none;   /* stays visible */
}

/* tooltip — same primitive, no interaction */
.tt {
  position: absolute;
  z-index: var(--z-tooltip);
  max-width: 280px;
  padding: 6px 8px;
  background: var(--overlay-bg);
  border: 1px solid var(--overlay-border);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-overlay);
  font-size: var(--fs-xs); font-weight: 500; line-height: 1.45;
  color: var(--text); white-space: normal; text-align: left;
  animation: panel-in var(--motion-overlay) var(--ease-overlay);
}

/* modal / confirm */
.scrim {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: rgba(0, 0, 0, .5);
  display: grid; place-items: center;
}
[data-theme="light"] .scrim { background: rgba(16, 18, 27, .35); }
.modal-panel {
  width: 100%; max-width: 420px;
  padding: 20px;
  background: var(--overlay-bg);
  border: 1px solid var(--overlay-border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-overlay);
}

/* toast */
.toast-stack {
  position: fixed; top: 16px; right: 16px; z-index: var(--z-toast);
  display: flex; flex-direction: column; gap: 8px;
}
.toast {
  display: flex; align-items: center; gap: 8px;
  min-width: 260px; max-width: 360px;
  padding: 10px 12px;
  background: var(--overlay-bg);
  border: 1px solid var(--overlay-border);
  border-left: 2px solid var(--border-strong);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-overlay);
  font-size: var(--fs-sm);
}
.toast-ok  { border-left-color: var(--ok-fg); }
.toast-err { border-left-color: var(--err-fg); }

@media (prefers-reduced-motion: reduce) {
  .panel, .tt { animation: none; }
  * { transition-duration: 1ms !important; }
}

/* ── 9. Data table ─────────────────────────────────────────────────────── */
.dt-wrap {
  container-type: inline-size;   /* breakpoints follow the CONTAINER, not the viewport */
  min-width: 0;                  /* without this a table blows out its flex parent */
  overflow-x: auto;
  background: var(--bg-surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-card);
}
.dt {
  width: 100%;
  min-width: 1120px;             /* target width — fits both sidebar states */
  table-layout: fixed;           /* colgroup owns the widths, browser doesn't guess */
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--table-fs);
}
.dt thead th {
  position: sticky; top: 0; z-index: var(--z-sticky-head);
  height: var(--row-h);
  padding: 0 var(--cell-px);
  background: var(--bg-surface);
  border-bottom: 1px solid var(--border-strong);
  font-size: var(--fs-label); font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-muted); text-align: left; white-space: nowrap;
}
.dt tbody td {
  height: var(--row-h);
  padding: 0 var(--cell-px);
  border-bottom: 1px solid var(--border-hairline);
  vertical-align: middle;
  /* vertical-align centres the cell's LINE BOX, and at the default ratio that box
     carries the 13px font's descender space below the baseline. Every inline-level
     child then aligns to that baseline instead of the cell's middle: badges rode
     ~1.3px high, the subreddit flags differently again (3.3px), while the cells
     that wrap their content in a flex box were genuinely centred and so sat lower
     than their neighbours. Two centring mechanisms in one row, which is what the
     "cells sit at different heights" report was. line-height:1 removes the
     descender from the box; the rule below puts every child on the same
     mechanism. .panel is excluded — an absolutely positioned overlay must not be
     realigned by its host cell. */
  line-height: 1;
  /* Clipped, and deliberately so. The previous rule here was "a column that
     cannot hold its content is a width bug, so don't clip" — true for columns
     whose content is a known label (Status is sized off its longest badge,
     see --col-status), false for columns holding arbitrary user data. A
     subreddit name or a username has no upper bound, so without this the text
     ran straight out of its cell and collided with the next column's text.
     Rows are one line at a fixed --row-h, so nowrap is the existing contract;
     cells that need to show the untruncated value carry title="…". */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A cell that hosts an overlay must be declared as one, and must not clip.
   Both the kebab wrapper (.tt-wrap) and the counter wrapper are position:relative,
   so the panel's containing block sits INSIDE the cell — which is exactly the case
   overflow:hidden clips. Measured: with the cell hidden, elementFromPoint over the
   open panel returns a cell of the row below; with it visible, it returns the
   panel, and the panel paints over the following rows with no z-index help.

   An earlier note here claimed the opposite, from an A/B that had not loaded
   base.html — where .tt-wrap gets its position:relative — so the panels' containing
   block was above the clipped cell and nothing appeared to change. Do not trust
   that reading; the harness must load both stylesheets.

   An explicit class, not :has(): it is greppable, and it cannot silently start
   matching a cell that merely looks like a host. */
.dt tbody td > *:not(.panel) { vertical-align: middle; }

/* Relation counters are centred on BOTH sides — header and cell. A single digit
   right-aligned next to its right-aligned dropdown arrow reads ragged, and the
   design centres them. .num pulls them right, so this has to outrank it; it is
   scoped to the counter columns, since karma, traffic and the rest stay right. */
.dt thead th[data-col="subreddits"],
.dt thead th[data-col="accounts"],
.dt tbody td[data-col="subreddits"],
.dt tbody td[data-col="accounts"] { text-align: center; }
.dt[data-page="accounts"] thead th[data-col="posts"],
.dt[data-page="accounts"] tbody td[data-col="posts"],
.dt[data-page="keywords"] thead th[data-col="posts"],
.dt[data-page="keywords"] tbody td[data-col="posts"],
.dt[data-page="subreddits"] thead th[data-col="posts"],
.dt[data-page="subreddits"] tbody td[data-col="posts"] { text-align: center; }
/* Karma is read DOWN the column, so the separator has to be the axis. The cell
   used to centre the whole "535 / 7" group, which centres its combined width —
   so the slash landed at a different x in every row and there was no line to
   read down. That is what "karma is not in one line" meant.

   Three tracks with the slash in the middle one: post karma grows leftwards from
   the centre, comment karma rightwards. tabular-nums because .mono alone does not
   carry it — .num does, and this cell never had it, so even the monospace
   fallback shifted digit widths. The gap replaces the old padding on the slash:
   the newlines between the spans rendered as spaces on top of it, making the gap
   ~8px where the style said 4. Whitespace-only text nodes are not grid items. */
.karma-cell {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 4px;
  font-variant-numeric: tabular-nums;
}
.karma-cell > :nth-child(1) { text-align: right; }
.karma-cell > :nth-child(3) { text-align: left; }
.karma-sep { color: var(--text-subtle); }

/* One checkbox across all four tables. Accounts styled its inputs inline
   (accent-color, 15px, vertical-align) while Keywords, Posts and Subreddits used
   DaisyUI's .checkbox .checkbox-sm — a different box at a different height, and
   only one of the two carried an alignment. */
.dt-check {
  accent-color: var(--primary);
  width: 15px; height: 15px;
  vertical-align: middle;
  margin: 0;
}

/* A <select> wearing badge classes still carries the UA's own box, which is
   taller than .b and is part of why cells sat at different heights. Pin it to
   the badge's height. Two classes, so it beats .b-primary's padding. */
.b.cqs-select {
  height: 24px;
  padding-top: 0; padding-bottom: 0;
  line-height: 1;
  /* .cqs-select sets the mono family, so these three read as mono next to the
     Inter badges beside them in the same row. A badge is a badge. */
  font-family: inherit;
  /* Sized by the SELECTED option, not the longest one. A select's intrinsic
     width comes from its widest option: Posts' Status rendered 102px for "DONE"
     because ON_MODERATION exists in the list, and Keywords' Project 115px
     because a project is called seo-campaign-DE. Chrome 123+; where it is not
     supported the control stays as wide as it is today. */
  field-sizing: content;
  max-width: 100%;
}

.dt tbody td.dt-cell-overlay { overflow: visible; }
/* The checkbox column holds one centred control and nothing else. With --cell-px
   on both sides its 40px became an 8px content box, so the 15px checkbox
   overflowed and the clip drew an ellipsis beside it — the "dots at the start of
   the row". Measured: contentBox 8px vs checkbox 15px. */
.dt thead th[data-col="check"],
.dt tbody td[data-col="check"] { padding: 0; text-align: center; }
/* Centred here rather than inline per template. Accounts carried
   style="text-align:center" on both its <th> and its <td>; the other three had it
   on the header only, so their row checkboxes sat 13px to the left of the header
   checkbox above them — measured, identical on all three. */

/* Headers clip too, otherwise a label wider than its column paints over the
   neighbouring header and reads as two overlapping words. */
.dt thead th { overflow: hidden; }
.dt-row { cursor: pointer; }
.dt-row:hover > td { background: var(--row-hover); }
.dt-row[aria-selected="true"] > td { background: var(--row-selected); }
.dt-row[aria-selected="true"] > td:first-child { box-shadow: inset 2px 0 0 var(--primary); }

/* sort affordance — identical on every sortable column */
.th-sort {
  display: inline-flex; align-items: center; gap: 4px;
  background: 0; border: 0; padding: 0; cursor: pointer;
  font: inherit; letter-spacing: inherit; text-transform: inherit;
  color: inherit;
}
.th-sort .material-symbols-outlined { font-size: 14px; opacity: .4; }
.th-sort:hover .material-symbols-outlined { opacity: .7; }
.th-sort[aria-sort="ascending"], .th-sort[aria-sort="descending"] { color: var(--primary); }
.th-sort[aria-sort="ascending"] .material-symbols-outlined,
.th-sort[aria-sort="descending"] .material-symbols-outlined { opacity: 1; }
.th-help {
  font-size: 13px; opacity: .45; cursor: help;
  margin-left: 2px; vertical-align: middle;
}
.th-help:hover { opacity: .9; }

/* sticky edge columns — only meaningful when the table actually scrolls */
/* Horizontal pinning is off: with columns hidden the freed space made the
   table narrower than its min-width floor, the wrapper still scrolled, and the
   pinned name column painted over the cells beside it. Vertical thead pinning
   stays — that is the one that matters when scanning 100 rows. */
.dt-sticky-l { position: static; }
/* The name column's 200px floor is held by the .dt min-width values in the
   container queries below (container − Σ visible fixed columns ≥ 200px).
   A min-width here would be silently ignored under table-layout: fixed. */
.dt-sticky-l2 { position: static; }

/* The RIGHT edge is pinned again. The reasoning above is about the left: a wide
   flex name column painted over its neighbours once hidden columns made the
   table narrower than its floor. A 44px fixed column whose whole content is one
   button has neither property — and unpinned it is the first column to leave the
   screen on every table that scrolls, which is exactly the "there is something
   on the right I cannot click" report. Posts scrolls at any window under 1750px
   (floor 1502 vs 1480 of content at 1728), so this is not an edge case. */
.dt-sticky-r {
  position: sticky; right: 0;
  /* Opaque. The cell now sits OVER other cells, and --row-hover / --row-selected
     are translucent — used alone the scrolled content shows through them. The
     row tint is therefore composited on top of the surface, not instead of it. */
  background: var(--bg-surface);
  border-left: 1px solid var(--border-hairline);
  /* Above unpositioned body cells, below --z-sticky-head (30) so the header still
     wins where the two meet. */
  z-index: 1;
}
.dt-row:hover > td.dt-sticky-r {
  background: linear-gradient(var(--row-hover), var(--row-hover)), var(--bg-surface);
}
.dt-row[aria-selected="true"] > td.dt-sticky-r {
  background: linear-gradient(var(--row-selected), var(--row-selected)), var(--bg-surface);
}
/* The header cell pins on both axes, or the corner tears on horizontal scroll:
   the body's pinned cell would slide under a header cell that is only pinned
   vertically. */
.dt thead th.dt-sticky-r { right: 0; z-index: calc(var(--z-sticky-head) + 1); }

/* Visible at rest. Hover-to-reveal is a legitimate pattern, but combined with an
   unpinned column it produced "I never saw it and cannot open it" — and a column
   that reads as empty on a screenshot, before the mouse has moved, is the same
   thing for anyone scanning. .icon-btn carries only size and transition, so the
   button also had no shape to aim at; the hovered state now uses the same quiet
   background as the other icon controls. */
.dt-kebab {
  opacity: .55;
  transition: opacity var(--motion-ui) var(--ease-ui), background-color var(--motion-ui) var(--ease-ui);
}
.dt-row:hover .dt-kebab,
.dt-row:focus-within .dt-kebab,
.dt-kebab:hover,
.dt-kebab:focus-visible,
.dt-kebab[aria-expanded="true"] { opacity: 1; }
.dt-kebab:hover, .dt-kebab[aria-expanded="true"] { background: var(--row-focus); }
@media (hover: none) { .dt-kebab { opacity: 1; } }   /* touch: always visible */

.avatar {
  width: var(--avatar-size); height: var(--avatar-size);
  border-radius: var(--r-full); flex: 0 0 auto;
  border: 1px solid var(--border-hairline);   /* keeps colour icons off bare white */
  background: var(--neutral-bg);
  display: inline-grid; place-items: center; overflow: hidden;
}
.uname { color: var(--primary); font-weight: 500; }
/* The row's identity when it is not a link. .uname carries the same weight but
   also the primary colour, which only belongs on something clickable. Four
   tables had four different treatments of this one role: Inter 500, Inter 400,
   14px Inter 500 and 14px mono 500. */
.dt-title { font-weight: 500; }
.dt-row:hover .uname { text-decoration: underline; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* container-driven column hiding — see checklist.md
   Three generic slots, dropped in a fixed order as the CONTAINER narrows:
   opt-3 first (nice to have), then opt-1, then opt-2. Page-specific columns
   opt in by adding a slot class, so the flex column always keeps a usable
   minimum instead of collapsing to zero. Anything hidden here stays
   reachable in the Columns menu. */
/* Actual hiding is done in JS (see rpApplyColHiding in base.html), not here.
   table-layout:fixed takes column widths from the <colgroup> once; neither
   display:none nor visibility:collapse on a <col> reliably makes the browser
   give that width back to the flex column across engines (Chromium in
   particular keeps the column's width reserved even with visibility:collapse
   on fixed layout) — the only mechanism every browser respects is setting
   the <col>'s own width to 0, which requires JS to read/restore the original
   value. These container queries only adjust the floor width. */
/* ── 10. Bulk actions bar ──────────────────────────────────────────────── */
.bulkbar {
  position: fixed; bottom: 20px; left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-dropdown);   /* below popovers: it tied with them at --z-popover and,
                                 being later in the document, covered a popover opened
                                 on a bottom row. Still clears the sticky header. */
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px;
  background: var(--overlay-bg);
  border: 1px solid var(--overlay-border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-overlay);
  animation: bulk-in var(--motion-ui) var(--ease-overlay);
}
@keyframes bulk-in {
  from { opacity: 0; transform: translate(-50%, 8px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
.bulkbar-sep { width: 1px; align-self: stretch; background: var(--border-hairline); }

/* ── 11. Skeleton / states ─────────────────────────────────────────────── */
/* Skeleton rows must be exactly one row tall, or the table's height changes
   between the placeholder and the real content and everything shifts. */
.rp-sk-row > td { height: var(--row-h); padding: 0 var(--cell-px); }
.dt-wrap[aria-busy="true"] { pointer-events: none; }
.sk {
  border-radius: 4px;
  background: linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 37%, var(--skeleton-base) 63%);
  background-size: 400% 100%;
  animation: sk 1.4s ease infinite;
}
@keyframes sk { from { background-position: 100% 0; } to { background-position: 0 0; } }
@keyframes spin { to { transform: rotate(360deg); } }
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
[aria-busy="true"] { pointer-events: none; }

/* ── 12. Sidebar geometry (expanded 244 / collapsed 64) ────────────────── */
.shell {
  display: grid;
  grid-template-columns: 200px minmax(0, 1fr);
  min-height: 100vh;
}
/* No width transition on purpose: animating grid-template-columns reflowed the
   sticky header and the fixed-layout table on every frame. The collapse snaps,
   and only the rail handle animates. */
[data-sidebar="collapsed"] .shell { grid-template-columns: 64px minmax(0, 1fr); }

/* ── 13. Dashboard ─────────────────────────────────────────────────────────
   Unused so far — the Dashboard is still on Tailwind cards. Kept deliberately:
   this file mirrors the design's token layer, and these are the vocabulary the
   port will use. Same reasoning for .panel-fit / .panel-fade-bottom /
   .panel-search above.
   Explicit 12-column grid with named spans instead of auto-fit: an auto-fit
   track count changes with content width and was leaving orphan rows.
   Every child carries min-width:0 and every number nowraps, so nothing
   spills out of its tile the way the ApexCharts cards used to.
   ------------------------------------------------------------------------*/
.card {
  background: var(--bg-surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-card);
  min-width: 0;
  overflow: hidden;
}
.card-head {
  display: flex; align-items: center; gap: 8px;
  padding: 11px 16px;
  border-bottom: 1px solid var(--border-hairline);
}
.card-title {
  margin: 0;
  font-size: var(--fs-label); font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-muted);
}
.card-body { padding: 14px 16px; min-width: 0; }
/* card-head links were 17px tall — below the 24px minimum for a pointer target */
.card-head a {
  min-height: 24px; padding: 0 2px;
  display: inline-flex; align-items: center;
}

.dash {
  display: grid; gap: 14px;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  align-items: start;
}
.span-4, .span-5, .span-6, .span-7, .span-8, .span-12 { grid-column: span 12; min-width: 0; }
@media (min-width: 1040px) {
  .span-4 { grid-column: span 4; }
  .span-5 { grid-column: span 5; }
  .span-6 { grid-column: span 6; }
  .span-7 { grid-column: span 7; }
  .span-8 { grid-column: span 8; }
}

.kpis {
  display: grid; gap: 1px;
  background: var(--border-hairline);
  /* auto-fit here on purpose: 5 tiles never divide evenly into a fixed 3-track
     grid, and the orphan cell reads as a broken tile. 148px is enough for a
     3-digit tabular number plus an ellipsised label. */
  grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
}
.kpi {
  background: var(--bg-surface);
  padding: 13px 16px 14px;
  min-width: 0;
  position: relative;                 /* for .kpi-figure */
  display: flex; flex-direction: column; gap: 2px;
}
/* Not in designs/tokens.css: its KPI tiles carry no icon, ours do. Absolute so
   the icon cannot push the number around when a label wraps. */
.kpi-figure {
  position: absolute; top: 50%; right: 16px; transform: translateY(-50%);
  opacity: .75; pointer-events: none;
}
.kpi-label {
  font-size: var(--fs-label); font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.kpi-value {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 25px; font-weight: 700; line-height: 1.2;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.kpi-note {
  font-size: var(--fs-label); color: var(--text-subtle); line-height: 1.4;
  overflow: hidden; text-overflow: ellipsis;
}

.stack { display: flex; height: 8px; border-radius: var(--r-full); overflow: hidden; background: var(--neutral-bg); }
.stack > span { min-width: 2px; }

.legend { display: flex; flex-direction: column; }
.legend-row {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 0;
  border-top: 1px solid var(--border-hairline);
  font-size: var(--fs-xs); color: var(--text);
  min-width: 0;
}
.legend-row:first-child { border-top: 0; }
a.legend-row:hover .legend-name { color: var(--primary); }
.legend-dot { width: 8px; height: 8px; border-radius: var(--r-full); flex: 0 0 auto; }
.legend-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legend-val {
  font-family: 'IBM Plex Mono', monospace;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.legend-share { width: 44px; text-align: right; color: var(--text-muted); }

.spark { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 6px; align-items: end; height: 92px; }
.spark-bar { background: var(--primary); border-radius: 3px 3px 0 0; min-height: 3px; }
.spark-bar:last-child { background: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); }
.spark-x {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 6px;
  margin-top: 6px;
  font-family: 'IBM Plex Mono', monospace; font-size: 10px;
  color: var(--text-subtle); text-align: center;
}

/* ── 15. Detail drawer ─────────────────────────────────────────────────── */
.sheet-scrim {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: rgba(0, 0, 0, .5);
  animation: fade-in var(--motion-ui) var(--ease-overlay);
}
[data-theme="light"] .sheet-scrim { background: rgba(16, 18, 27, .32); }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.sheet {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: var(--z-modal);
  width: min(440px, 100vw);
  display: flex; flex-direction: column;
  background: var(--bg-surface);
  border-left: 1px solid var(--border-strong);
  box-shadow: var(--shadow-overlay);
  animation: sheet-in var(--motion-layout) var(--ease-overlay);
}
@keyframes sheet-in { from { transform: translateX(16px); opacity: .6; } to { transform: none; opacity: 1; } }
.sheet-head {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border-hairline);
}
.sheet-body { flex: 1; overflow-y: auto; padding: 4px 16px 16px; }
.sheet-foot {
  /* wrap, not overflow: the drawer is min(440px, 100vw) and Posts puts three
     check actions down here (Check ranking / Check keyword / Check Reddit),
     which do not fit on one line at that width. */
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid var(--border-hairline);
}
.sheet-section { padding: 14px 0 4px; border-top: 1px solid var(--border-hairline); }
.sheet-section:first-child { border-top: 0; }
.sheet-label {
  margin: 0 0 8px;
  font-size: var(--fs-label); font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-muted);
}
.meta-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 14px; }
.meta-label { display: block; font-size: var(--fs-label); color: var(--text-subtle); margin-bottom: 1px; }
.meta-value {
  display: block;
  font-family: 'IBM Plex Mono', monospace; font-size: var(--fs-xs);
  color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── 16. Command palette ───────────────────────────────────────────────── */
.palette-scrim {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: rgba(0, 0, 0, .5);
  display: flex; justify-content: center; align-items: flex-start;
  padding: 12vh 16px 16px;
  animation: fade-in var(--motion-ui) var(--ease-overlay);
}
[data-theme="light"] .palette-scrim { background: rgba(16, 18, 27, .32); }
.palette {
  width: 100%; max-width: 560px;
  background: var(--overlay-bg);
  border: 1px solid var(--overlay-border);
  border-radius: var(--overlay-radius);
  box-shadow: var(--shadow-overlay);
  overflow: hidden;
  animation: panel-in var(--motion-overlay) var(--ease-overlay);
}
.palette-field {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border-hairline);
}
.palette-field input {
  flex: 1; min-width: 0;
  border: 0; outline: 0; background: transparent;
  color: var(--text); font-family: inherit; font-size: 15px;
}
.palette-field input::placeholder { color: var(--text-subtle); }
.palette-list { max-height: 340px; overflow-y: auto; padding: 4px; overscroll-behavior: contain; }
.palette-foot {
  display: flex; align-items: center; gap: 12px;
  padding: 8px 14px;
  border-top: 1px solid var(--border-hairline);
  font-size: var(--fs-label); color: var(--text-subtle);
}
.kbd {
  font-family: 'IBM Plex Mono', monospace; font-size: 10px;
  padding: 1px 5px; border-radius: 4px;
  border: 1px solid var(--border-strong); background: var(--bg-inset);
  color: var(--text-muted);
}

/* ── 17. Sidebar rail handle ───────────────────────────────────────────────
   The collapse control used to sit inside the sidebar header, where it fought
   the logo for 64px in the collapsed state. It now lives on the sidebar's own
   edge: out of the content's way in both states, and reachable with "[".
   ------------------------------------------------------------------------*/
.rail {
  position: absolute; top: 72px; right: -11px;
  z-index: calc(var(--z-page-header) + 1);
  width: 22px; height: 40px;
  display: grid; place-items: center;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-full);
  background: var(--bg-surface);
  color: var(--text-muted);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--motion-ui) var(--ease-ui),
              color var(--motion-ui) var(--ease-ui);
}
aside:hover > .rail, .rail:focus-visible { opacity: 1; }
.rail:hover { color: var(--text); border-color: var(--text-subtle); }
@media (hover: none) { .rail { opacity: 1; } }

/* ── Размеры иконок ───────────────────────────────────────────────────────
   Раньше это были 217 инлайновых style="font-size:NNpx" на span.material-
   symbols-outlined — по одному на каждую иконку в разметке. Поменять размер
   иконок разом было нельзя: пришлось бы править каждую строку шаблона. */
.ico-10 { font-size: 10px; }
.ico-11 { font-size: 11px; }
.ico-12 { font-size: 12px; }
.ico-13 { font-size: 13px; }
.ico-14 { font-size: 14px; }
.ico-15 { font-size: 15px; }
.ico-16 { font-size: 16px; }
.ico-18 { font-size: 18px; }
.ico-20 { font-size: 20px; }
.ico-22 { font-size: 22px; }
.ico-32 { font-size: 32px; }
.ico-40 { font-size: 40px; }

/* Частые точечные правила, которые повторялись десятками в разметке. */
.t-muted  { color: var(--text-muted); }
.t-subtle { color: var(--text-subtle); }

/* ── 18. Shop — listings and orders ────────────────────────────────────────
   The one table in the app whose row is an editing surface rather than a link
   to one. Everything here follows from that, and lives here rather than inline
   in the template for the same reason as the rest of the file: the previous
   version of this page carried its layout as inline styles on a single cell
   with colspan="5", and eleven headers stood over seven cells. */

/* The row opens nothing, so it must not offer a pointer. */
.dt[data-page="shop"] .dt-row,
.dt[data-page="shop-orders"] .dt-row { cursor: default; }

/* A boolean column holds one control, and the box belongs under its own word.
   Centred here, not per template: that is how the four other tables ended up
   with their row checkbox 13px to the left of the header's. */
.dt[data-page="shop"] thead th[data-col="email"],
.dt[data-page="shop"] tbody td[data-col="email"] { text-align: center; }

/* Editing controls are 30px, not the app's 36px: a row carries three of them
   side by side and --row-h stays 48px across every table in the app. */
.dt[data-page="shop"] .field { height: 30px; padding: 0 10px; gap: 6px; }
.dt[data-page="shop"] .ctl   { height: 30px; padding: 0 12px; }

/* The action column is narrower than a button plus two full gutters. */
.dt[data-page="shop"] thead th[data-col="kebab"],
.dt[data-page="shop"] tbody td[data-col="kebab"] { padding: 0 12px; }

/* A row that was edited but not saved says so. The whole row posts on Save, so
   until then the change lives in the browser only — and a quiet button next to
   a changed field reads as "already saved". The swapped-in row comes back from
   the server without the flag, so it clears itself. */
.dt[data-page="shop"] .dt-row[data-dirty] .shop-save {
  background: var(--primary); border-color: var(--primary);
  color: var(--primary-fg); font-weight: 600;
}
.dt[data-page="shop"] .dt-row[data-dirty] .shop-save:hover {
  background: var(--primary-hover); border-color: var(--primary-hover);
}

/* A disabled control still has to read as a control, not as a hole in the row:
   the proxy type and country wait for the proxy box, and come back the moment
   it is ticked. */
.dt[data-page="shop"] :disabled { opacity: .45; cursor: not-allowed; }
.dt[data-page="shop"] .field:has(:disabled) { opacity: .45; }
