/*
 * glass.css — Glassmorphism + accent-backlight design system
 *
 * Adopted by: admin/subscriptions.php
 *             admin/my_subscription_detail.php
 *             admin/dashboard.php
 *
 * Companion: assets/app.css
 *   Cascade order: <link app.css> FIRST, <link glass.css> SECOND.
 *   glass.css purposefully overrides .modal-overlay/.modal-box and a few
 *   table cell rules where they exist in app.css, so order matters.
 *
 * Token dependencies on app.css:
 *   --text, --bg, --accent, --success, --warn, --danger  (hex values, app.css owns these)
 *
 * This file declares its own RGB-triplet versions (--*-rgb) used for
 * rgba(var(--*-rgb), alpha) compositions. Triplets mirror the hex
 * values in app.css; if app.css ever changes, update them here too.
 *
 * Themes: html[data-theme="dark"] (default) and html[data-theme="light"].
 * Light overrides are placed ADJACENT to their dark counterparts, not
 * collected in a separate block — single-source maintenance.
 *
 * AUTO-DECISION: html[data-theme="light"] (not bare [data-theme="light"])
 *   for selector-form consistency with app.css. Specificity tie matters
 *   when extra root-level selectors get layered later.
 *
 * Naming:
 *   .glass-*       — surface containers (card, row, divider, …)
 *   .btn-glass     — buttons with backlight glow
 *   .badge-glass   — pill badges with status-coloured borders
 *   Other classes are subcomponents (kebab-case under their parent).
 */

/* ───────────────────────── 1. RGB triplet tokens ───────────────────────── */
/* RGB-triplet tokens (--accent-rgb / --success-rgb / --warn-rgb / --danger-rgb)
   are defined in app.css for both dark and light themes in comma-separated
   form, so every `rgba(var(--*-rgb), alpha)` call site in glass.css resolves
   correctly. Earlier versions of this file redefined those tokens with
   space-separated values, which broke legacy `rgba(...,a)` consumers. */

/* ───────────────────────── 1a. Theme tokens + a11y primitives ───────────── */
/* UI-0035: themed token for sticky table headers. Was a hardcoded RGBA literal
   in dashboard.php that painted opaquely over operator-set custom backgrounds.
   Default (and the dark theme) use the dark surface; light theme overrides. */
:root, html[data-theme="dark"] { --surface-thead-sticky: rgba(20,22,32,0.85); }
html[data-theme="light"]       { --surface-thead-sticky: rgba(255,255,255,0.88); }

/* UI-0036 (UI-tokens audit, R3-01/02/05/13/15 + R1-03/04/05/08 + R7-02/04):
   component-scoped neutral tokens, one name + two polarity bases each — the
   same shape as the sticky-header token above. Every value is the literal the
   guarded P-pair already painted in that polarity, so the merge is a no-op in
   both polarities; a system that re-maps polarity flows through untouched.
   Hairline fills intentionally sit below the WCAG 3:1 non-text floor, like
   --border / --border-soft (open decision,
   docs/audit/theme-non-text-contrast-20260923.md) — do not tighten locally.
   `--control-wash` (hover fill) and `--fill-recessed` (disabled/nested
   recessed fill) replace the 0.02–0.08 white/black literal band; the
   per-site alphas inside that band were one sub-step apart and are unified
   within the family. */
:root, html[data-theme="dark"] {
  --modal-scrim:        rgba(0,0,0,0.6);
  --modal-scrim-blur:   rgba(0,0,0,0.45);
  --modal-shadow:       0 24px 64px rgba(0,0,0,0.55);
  --hairline:           rgba(255,255,255,0.06);
  --hairline-strong:    rgba(255,255,255,0.12);
  --zebra-row:          rgba(255,255,255,0.02);
  --glass-panel:        rgba(20,22,32,0.80);
  --glass-panel-sm:     rgba(20,22,32,0.65);
  --tooltip-shadow:     0 8px 24px rgba(0,0,0,0.45);
  --sheet-grab:         rgba(255,255,255,0.2);
  --scroll-edge:        rgba(0,0,0,0.28);
  --nav-stuck-shadow:   0 8px 20px rgba(0,0,0,0.35);
  --form-section-bg:    rgba(20,22,32,0.55);
  --fill-recessed:      rgba(255,255,255,0.02);
  --control-wash:       rgba(255,255,255,0.07);
  /* R3-17: the horizon wash layer of the auth-redesign ::after field
     (login.php). The dark 0.02 white sheen and the light 0.45 white veil
     were the two values the page pinned per polarity; one owner now. */
  --auth-sheen:         rgba(255,255,255,0.02);
  /* UI-tokens audit R2-07: the "information"/"premium-tier" hue families that
     tx-badges and tier badges had pinned to literals. One value per polarity
     (not per system): they are a semantic role, not a brand choice. No system
    's accent collides hard — the badge's alpha-tinted background keeps them
     apart from an accent CTA. */
  --info-rgb:           96,165,250;   --info-text-rgb: 147,197,253;
  --purple-rgb:         168,85,247;   --purple-text-rgb: 216,180,254;
  /* R2-06: the slate neutrals the .adjustment / .muted badges had pinned to
     literals; the ink half is always the system's own --text-muted. */
  --neutral-rgb:        156,163,175;
}
html[data-theme="light"] {
  --modal-scrim:        rgba(15,18,30,0.30);
  --modal-scrim-blur:   rgba(15,18,30,0.30);
  --modal-shadow:       0 24px 64px rgba(0,0,0,0.18);
  --hairline:           rgba(0,0,0,0.05);
  --hairline-strong:    rgba(0,0,0,0.10);
  --zebra-row:          rgba(0,0,0,0.02);
  --glass-panel:        rgba(255,255,255,0.70);
  --glass-panel-sm:     rgba(255,255,255,0.65);
  --tooltip-shadow:     0 8px 24px rgba(0,0,0,0.14);
  --sheet-grab:         rgba(0,0,0,0.18);
  --scroll-edge:        rgba(0,0,0,0.14);
  --nav-stuck-shadow:   0 8px 20px rgba(0,0,0,0.12);
  --form-section-bg:    rgba(255,255,255,0.70);
  --fill-recessed:      rgba(0,0,0,0.025);
  --control-wash:       rgba(0,0,0,0.05);
  --auth-sheen:         rgba(255,255,255,0.45);
  --info-rgb:           29,78,216;    --info-text-rgb: 29,78,216;
  --purple-rgb:         126,34,206;  --purple-text-rgb: 126,34,206;
  --neutral-rgb:        156,163,175;
}

/* UI-0003: site-wide keyboard focus ring. Many controls set outline:none on
   :focus and replace it with a box-shadow ring; those keep their own ring
   (higher specificity). This adds a visible token-based outline to every other
   focusable surface (links, buttons, row-as-link, badges, menu items, anything
   with [tabindex]) so keyboard users never lose the focus indicator (WCAG
   2.4.7). Outline (not box-shadow) so it never fights existing shadow rings. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible,
[role="button"]:focus-visible,
[role="menuitem"]:focus-visible,
[role="radio"]:focus-visible,
.glass-row:focus-visible{
  outline: 2px solid var(--ring);       /* Phase 5 stage 2: focus ring token */
  outline-offset: 2px;
  border-radius: var(--r-1);
}

/* UI-0004: skip-to-content link. Visually hidden until focused, then pinned to
   the top so keyboard users can bypass the brand + nav + impersonate bar. */
.skip-link{
  position: absolute;
  inset-inline-start: 12px;
  top: -60px;
  z-index: 2000;
  padding: 10px var(--sp-4);
  border-radius: 0 0 12px 12px;
  background: rgba(var(--accent-rgb), 0.95);
  color: var(--accent-foreground);
  font-weight: 600;
  text-decoration: none;
  transition: top 0.15s ease;
}
.skip-link:focus{
  top: 0;
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* ───────────────────────── 1b. Form-control box-sizing reset ─────────────
   Every `.glass-*` form control declares `width: 100%` plus inner padding.
   Without box-sizing: border-box the padding adds to the 100% and the
   control overflows its parent — visible most cleanly on the wallet
   top-up modal's amount input (which carries 20 px horizontal padding +
   1.5rem font, so the overflow is ~40 px at 320 px viewport widths).
   Setting box-sizing here once covers every glass control without
   needing per-rule edits. */
.glass-amount-input,
.glass-textarea,
.glass-select,
.glass-input-group input,
.glass-input-group textarea,
.glass-stepper .stepper-input,
.form-control,
.btn-glass.block{
  box-sizing: border-box;
}

/* ───────────────────────── 2. Glass card ─────────────────────────────── */
.glass-card{
  background: var(--glass-card-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-2);
  box-shadow: var(--shadow);
  padding: var(--sp-5);
}
/* RULE-9 (Phase 6): .sm modifier — slim card variant for filter/control bars
   and inline notification cards. Smaller radius + tighter padding. */
.glass-card.sm{
  padding: var(--sp-3) var(--sp-3-5);
  border-radius: var(--r-2);
}

/* ── Commit 4 (#29/#36): `.list-shell` — no card-in-card on phone ──────────
   representatives.php and deposits.php both wrap their listing in a .glass-card.
   On desktop that shell IS the card around a real table — correct. But ≤768px
   the contents become cards in their own right (rep cards; table rows via the
   card-collapse), so the shell stacked a SECOND border + background + shadow
   around them: two nested surfaces that ate horizontal room and read heavy.
   On phone the shell drops to a layout-only wrapper — the item is the only
   visible card. Opt-in via the class, so other .glass-card users are untouched. */
@media (max-width: 1023px){
  .glass-card.list-shell,
  html[data-theme="light"] .glass-card.list-shell{
    background: none;
    border: none;
    border-radius: 0;
    box-shadow: none;
    padding: 0;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* ───────────────────────── Icon system (lib/Icons.php) ────────────────
   Inline SVG glyphs that replaced the emoji set. Sized in `em` so they
   scale with the surrounding text and inherit color via currentColor.
   Standalone glyphs inside fixed chips (nav/stat/action) size in px. */
.icon{
  width: 1.15em; height: 1.15em;
  display: inline-block;
  vertical-align: -0.18em;
  flex-shrink: 0;
}
.icon-sm{ width: 1em;   height: 1em; }
.icon-lg{ width: 1.5em; height: 1.5em; }
.icon-xl{ width: 2rem;  height: 2rem; }
/* Inline icon that leads a heading/label — small trailing gap, optical lift */
.icon-inline{ margin-inline-end: 6px; vertical-align: -0.16em; }
/* Fixed-size contexts */
.nav-icon .icon        { width: 24px; height: 24px; }
/* Stat-card glyphs take their tile's accent (falls back to text color for
   an unvariant card) — color stays on the icon, not the whole surface. */
.stat-icon .icon       { width: 22px; height: 22px; color: var(--stat-accent, currentColor); }
.rep-action-glyph .icon,
.rep-menu-glyph .icon,
.action-glyph .icon    { width: 20px; height: 20px; }
.empty-icon .icon      { width: 40px; height: 40px; }
.btn-glass .icon       { width: 18px; height: 18px; }
.btn-glass.icon-only   { padding: 10px; }
.theme-toggle .icon,
.auth-theme-toggle .icon { width: 20px; height: 20px; }
.glass-confirm-icon .icon { width: 30px; height: 30px; }
.toast-close .icon     { width: 16px; height: 16px; }
/* Semantic tints for standalone status icons (color used sparingly) */
.icon.text-success { color: var(--success); }
.icon.text-warn    { color: var(--warn); }
.icon.text-danger  { color: var(--danger); }
.icon.text-accent  { color: var(--accent); }
.icon.text-muted   { color: var(--text-muted); }

/* ───────────────────────── 3. Backlight button ───────────────────────── */
.btn-glass{
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  background: var(--glass-btn-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-1);            /* 8px — control radius (Phase 5 stage 2) */
  padding: var(--sp-3) var(--sp-5);
  font-weight: 600; font-size: 0.9rem; font-family: inherit;
  color: var(--text);
  cursor: pointer; text-decoration: none;
  /* Phase 5 stage 2: transition color/bg/border only (no transform lift) */
  transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
  z-index: 1; min-height: 44px;
}
.btn-glass:hover { border-color: rgba(var(--accent-rgb),0.35); background: var(--hover); }
.btn-glass:active { transform: translateY(0); }
.btn-glass:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }

/* Primary — solid accent, the one prominent action per view */
.btn-glass.primary{
  /* --accent-cta is the system's action fill and --primary-foreground its
     label. The old pairing (fill = brand, label = #fff) was 3.61:1 in dark
     and unreadable outright under a light brand; the pair is now chosen per
     design system and asserted at 4.5:1+ by bin/test_theme_systems.php. */
  background: var(--accent-cta);
  border-color: transparent;
  color: var(--primary-foreground);
}
/* Hover DARKENS rather than brightens. The old brightness(1.06) lightened the
   fill to 4.14:1 and pushed the button back under AA exactly while the pointer
   was on it; 0.94 gives the same "something changed" feedback at 4.98:1. */
.btn-glass.primary:hover{ background: var(--accent-cta); filter: brightness(0.94); border-color: transparent; }


.btn-glass.block { display: flex; width: 100%; }
.btn-glass.sm    { padding: var(--sp-2) var(--sp-3-5); min-height: 36px; font-size: var(--fs-1); }

/* Semantic tonal variants — success (approve / activate) + danger (reject / delete).
   Stage 3c fix: these classes were USED across the app (deposits تایید/رد, subscription
   activate/delete, sudo_admins, queue, confirm-modal OK, …) but had NO rule — every
   `.btn-glass.success`/`.danger` rendered as the neutral base, so positive vs
   destructive actions were indistinguishable. Recipe mirrors .badge-glass / .glass-alert
   (rgba semantic wash + colored text/border), reusing the per-theme tokens. Kept TONAL,
   never solid — `.primary` stays the single solid action per view (no conflict with the
   locked "one solid-primary per view" rule). Defined HERE (before .cta-deposit further
   down) on purpose: a button carrying both `success cta-deposit` (wallet top-up) keeps
   its solid-primary cta-deposit look because the later rule wins at equal specificity. */
.btn-glass.success{
  background: rgba(var(--success-rgb), 0.10);
  border-color: rgba(var(--success-rgb), 0.40);
  color: rgb(var(--success-text-rgb));
}
.btn-glass.danger{
  background: rgba(var(--danger-rgb), 0.10);
  border-color: rgba(var(--danger-rgb), 0.40);
  color: rgb(var(--danger-text-rgb));
}
/* PHASE5-WARN: `.warn` is the third tonal variant and had the identical bug —
   9 call sites, no rule, so every cautionary button read as a plain one. Same
   recipe/tokens as `.badge-glass.warn`; app.css redefines both per theme, so
   light needs no override. Same (0,2,0) and same block as its siblings, so the
   `.cta-*` protection above covers it too. */
.btn-glass.warn{
  background: rgba(var(--warn-rgb), 0.10);
  border-color: rgba(var(--warn-rgb), 0.40);
  color: rgb(var(--warn-text-rgb));
}

/* ───────────────────────── 4. Glass badge ────────────────────────────── */
.badge-glass{
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-full);
  font-size: 0.8rem; font-weight: 600; line-height: 1; /* Commit 1 (#11): 12px status-badge floor */
  background: var(--glass-card-bg);
  border: 1px solid var(--badge-border-color, rgba(255,255,255,0.12));
  color: var(--badge-text-color, var(--text));
}

/* Commit 1 (#13/#54): toman() wraps value+unit in .money-value so the amount and
   "تومان" never break across a line and stay a single readable group. */
.money-value{ white-space: nowrap; }
.badge-glass.active  { --badge-border-color: rgba(var(--success-rgb), 0.5); --badge-text-color: rgb(var(--success-text-rgb)); }
.badge-glass.warn    { --badge-border-color: rgba(var(--warn-rgb), 0.5);    --badge-text-color: rgb(var(--warn-text-rgb)); }
.badge-glass.danger  { --badge-border-color: rgba(var(--danger-rgb), 0.5);  --badge-text-color: rgb(var(--danger-text-rgb)); }
.badge-glass.deleted { --badge-border-color: var(--border-soft);           --badge-text-color: var(--text-muted); }
/* ── the accent FILL / accent INK pairing, stated once ──────────────────────
   Wherever a border or background reads `--accent-rgb`, the text beside it must
   read `--accent-text-rgb` (`--p-brand-ink-rgb` — the brand hue lightened for a
   dark fill and darkened for a light one, so it flips with polarity by itself).
   Rules that followed the fill token but hardcoded an indigo ink were written
   when `--accent` WAS indigo, and they survived the six-design-system work:
   under `saffron` they painted an indigo label on the golden accent, under
   `turquoise` an indigo label on teal. They are legible, so no contrast suite
   could see them — this is a wrong-theme bug, not a wrong-contrast one, and the
   fix at each site is a token swap.
   Components that pair a FIXED hue with itself (`.badge-glass.deleted/.muted`,
   `.l2`, `.l3`, `.crypto-method`, `.glass-tx-badge.refund/.adjustment`) are
   self-consistent and deliberately outside the accent vocabulary — leave them.
   The light-theme `color:` overrides those rules carried are now redundant,
   because the ink token resolves per polarity on its own; they were deleted
   rather than left as a second source of truth. */
.badge-glass.neutral { --badge-border-color: rgba(var(--accent-rgb), 0.4);  --badge-text-color: rgb(var(--accent-text-rgb)); }
/* `.muted` is the honest name for the grey the `.deleted` rule paints. A status
   meaning "finished / off / expired" is not a deletion, and the alternative
   spelling `.gray` — used by subscription_detail.php before sub_status_meta()
   replaced it — was never a real class, so that page's «منقضی» badge rendered
   unpainted. Same recipe as `.deleted`; consumers call the one that tells the
   truth and the paint cannot drift between them. */
.badge-glass.muted   { --badge-border-color: var(--border-soft);           --badge-text-color: var(--text-muted); }
/* Light-theme text colours — the dark-mode pales above fail WCAG AA contrast
   against the white card the badge sits on. Mirrors .glass-tx-badge's
   light-theme overrides (see §32). */
/* R2-05: the light-twin text overrides are gone — the *-text-rgb and
   --text-muted tokens carry the polarity. The neutral .deleted/.muted pair
   now follows the system's own border-soft/text-muted inks. */

/* RETIRED: pulse-glow (2.4s infinite, opacity 1 → 0.55 → 1) on .badge-glass.active.
   The earlier AUTO-DECISION traded readability for attention: because it animated
   the element's OPACITY, it dragged the badge TEXT down with it, and at the 0.55
   trough the label fell to ~2.5:1 in light theme — under half the AA floor — twice
   every 2.4 seconds, indefinitely, on a persistent status label rather than a
   transient alert. Reviewed with the operator against a live side-by-side; the
   static badge was chosen. The success-tinted ring below still distinguishes the
   live state, so the signal survives without the readability cost.
   If a pulse is ever wanted back, animate box-shadow/outline — never the opacity
   of an element whose own text has to stay legible. */
/* The 3px rest ring is scoped EXACTLY like the animation below it, and this is
   the part that was wrong: the exclusion list lived only on the animation, so
   inside a dense table the badge still painted a 3px halo while its neighbours
   did not. `.badge-glass.active` is the ONLY variant with a ring — `.warn`,
   `.danger`, `.neutral` and `.deleted` have none — so in feature_flags.php's
   status table a column of «خاموش»/«روشن» pills rendered at two different
   drawn sizes, and the haloed one read as taller than the row it sat in.
   A ring that only ever marks *presence* of the success tint is already carried
   by the badge's own border and text colour in a table, so it is dropped there
   — the same trade the animation note below already made. */
.badge-glass.active:not(.glass-tx-table *):not(.leaderboard *):not(.table-wrap *) {
  box-shadow: 0 0 0 3px rgba(var(--success-rgb), 0.25);
}
/* The note above named the replacement and it was never built: "If a pulse is
   ever wanted back, animate box-shadow/outline — never the opacity of an
   element whose own text has to stay legible." `pulse dot` is that pulse —
   "a halo that scales to 3x and fades out every 1.8s, layered behind the
   dot" — and it is buildable exactly as the note prescribed, because animating
   the SPREAD of a box-shadow changes no pixel of the label behind it. The 3px
   rest ring above stays as the static half, so the live state is still
   distinguishable with the animation off.
   Contrast note (why this is safe where the retired one was not): the halo is
   painted OUTSIDE the badge's border box, so at its widest it reaches 9px out
   — it never sits under the text, and the badge's own background, border and
   color are untouched at every frame. The retired version animated opacity on
   the element itself and dragged the glyphs with it.

   AUTO-DECISION: the halo is NOT applied inside the list tables. A `pulse dot`
   is a signal that something is live RIGHT NOW, and it only reads that way
   when it is rare — the technique's own perf note ("Medium", one dot) assumes
   one of them. `.badge-glass.active` in representatives.php is a status
   COLUMN: on a 50-row page ~45 rows carry it, so an infinite 1.8s box-shadow
   repaint would be 45 simultaneous animations that repaint every frame, and
   the operator would learn to read the motion as wallpaper — the exact
   "glow spam" Phase 5 removed, at real cost on a low-end phone. The halo
   therefore runs on the badge where it is a standalone status chip (page
   heads, stat cards, detail panels) and stays static in the dense tables,
   where the tinted ring alone already carries the state. */
.badge-glass.active:not(.glass-tx-table *):not(.leaderboard *):not(.table-wrap *){
  animation: badge-halo 1.8s var(--ease-out) infinite;
}
@keyframes badge-halo{
  0%   { box-shadow: 0 0 0 3px  rgba(var(--success-rgb), 0.25), 0 0 0 3px  rgba(var(--success-rgb), 0.22); }
  70%  { box-shadow: 0 0 0 3px  rgba(var(--success-rgb), 0.25), 0 0 0 9px  rgba(var(--success-rgb), 0); }
  100% { box-shadow: 0 0 0 3px  rgba(var(--success-rgb), 0.25), 0 0 0 3px  rgba(var(--success-rgb), 0.22); }
}

/* ───────────────────────── 5. Glass row (list) ───────────────────────── */
/* Commit 5 (#69): deterministic column widths.
   Every .glass-row is its OWN grid, so `auto` tracks were sized per row against
   that row's content — measured live, the status column ranged 51–67px and the
   volume column 52–61px across three rows. The list therefore looked like a
   table whose columns never lined up, and no header could align to it. Fixing
   the non-flexible tracks makes rows align with each other AND lets
   .glass-row-head (below) sit exactly above them. Widths are the measured
   content maxima plus headroom for the longest Persian status label. */
.glass-row{
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr) 92px 84px 188px 84px 24px;
  gap: var(--sp-4);
  align-items: center;
  padding: var(--sp-3-5) var(--sp-4-5);
  background: var(--glass-card-bg);
  /* R3-05: row border is the hairline-family token (0.06 dark / 0.05 light). */
  border: 1px solid var(--hairline);
  border-radius: var(--r-2);
  margin-bottom: 10px;
  transition: background 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
  cursor: pointer; text-decoration: none; color: inherit;
}
/* AUTO-DECISION: entire row clickable (anchor wrapper). Simpler than a
   per-row dropdown, friendlier on mobile where small action affordances
   regress. Actions live on the detail page. */
.glass-row:hover{
  background: var(--hover);
  border-color: var(--hairline-strong);
  /* AUTO-DECISION: small leftward nudge feels like a hover tick — physical
     coords (transform is direction-agnostic) chosen deliberately. */
  transform: translateX(-2px);
}
/* R3-05: the light hover/border twins are gone — var(--hover) and
   var(--hairline-strong) carry the polarity. */
/* The status track is fixed now, so the pill must not stretch to fill it.
   (The ≤720px rule further down re-pins it to the inline-end on mobile.) */
.glass-row > .badge-glass{ justify-self: start; }

/* Commit 5 (#69): column header for the desktop list. Same grid template and
   horizontal padding as .glass-row (+1px for the row's border) so the labels
   sit exactly above their values. aria-hidden: each row is a single <a> whose
   text already reads id → name → status → volume → date in order, so this is a
   visual aid, not an accessibility tree the SR needs to re-traverse. Hidden at
   ≤720px, where the row collapses to its stacked mobile layout with in-card
   labels. */
.glass-row-head{
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr) 92px 84px 188px 84px 24px;
  gap: var(--sp-4);
  align-items: center;
  padding: 0 calc(var(--sp-4-5) + 1px) var(--sp-2);
  font-size: 0.78rem;
  font-weight: 700;
  /* G10-assets: was 0.2px. The labels in this row are Persian (شناسه، ردیف ·
     اشتراک، وضعیت، حجم، مصرف، تاریخ) and DESIGN_RULES is unambiguous —
     «letter-spacing: 0 on Persian. Tracking breaks glyph joining.» The row
     header is where it mattered most: tracked Persian at 0.78rem pulled the
     joins apart on the six labels an operator reads on every list page. */
  letter-spacing: 0;
  opacity: 0.70;
}
@media (max-width: 1023px){ .glass-row-head{ display: none; } }

/* Commit 6 (#71): keep column context during long scrolls. With per-page set to
   50 the list runs several thousand pixels, and once the header scrolls away the
   values lose their meaning (is the left number volume or date?). The header
   pins to the top of the viewport; an opaque page-background is required so rows
   don't show through as they pass beneath. Desktop only — the mobile layout
   carries its labels inside each card. */
@media (min-width: 1024px){
  .glass-row-head{
    position: sticky;
    top: 0;
    z-index: 5;
    background: var(--bg);
    padding-top: var(--sp-2);
    /* `sticky tabs` + `scroll shadow`: pinned, the header is the same colour as
       the rows that slide under it, so the boundary between "column labels" and
       "first row's data" disappears exactly when the operator is mid-list and
       needs it most. The technique's rule is the strict one — "show edge
       shadows ... ONLY WHILE there is more content above or below" — so a
       permanent shadow is wrong: at scroll-top the header IS the top of the
       list and a shadow there would draw a line above the first row that means
       nothing.
       `animation-timeline: scroll()` gets that exactly right with no JS and no
       scroll listener: the keyframe is driven by the container's own scroll
       position, so the shadow is absent at 0 and present the moment there is
       content above. It also costs nothing — no per-frame JS, and a compositor-
       driven timeline.
       Browsers without scroll-driven animations ignore the whole declaration
       and simply get no edge shadow (never a wrong one). `.is-stuck`, toggled
       by a scroll listener, is the explicit fallback for those — see the block
       after this one. */
    animation: row-head-shadow linear both;
    animation-timeline: scroll(nearest block);
  }
}
/* Both the hairline and the shadow come from tokens, so the light theme needs
   no second keyframe — the animation is the same and only the ink changes. */
:root, html[data-theme="dark"]{
  --row-head-shadow-0: 0 1px 0 rgba(255,255,255,0.06);
  --row-head-shadow-1: 0 6px 12px -6px rgba(0,0,0,0.55), 0 1px 0 rgba(255,255,255,0.06);
}
html[data-theme="light"]{
  --row-head-shadow-0: 0 1px 0 rgba(0,0,0,0.04);
  --row-head-shadow-1: 0 6px 12px -6px rgba(30,40,70,0.14), 0 1px 0 rgba(0,0,0,0.05);
}
@keyframes row-head-shadow{
  0%   { box-shadow: var(--row-head-shadow-0); }
  100% { box-shadow: var(--row-head-shadow-1); }
}
/* Fallback path (`scroll shadow`): the same edge shadow, applied by a class
   instead of a scroll timeline, for browsers that do not implement
   `animation-timeline` — Firefox and Safari at the time of writing. The class
   is added only while the header is actually stuck, so it obeys the same
   "only while there is more content above" rule. */
@media (min-width: 1024px){
  .glass-row-head.is-stuck{ box-shadow: var(--row-head-shadow-1); }
}
/* The scroll-driven timeline and the JS class are two mechanisms for one
   effect; where the timeline works the class is redundant. Suppress the class
   inside the very @supports that enables the timeline so the two can never
   double up into a heavier shadow. */
@supports (animation-timeline: scroll()){
  @media (min-width: 1024px){
    .glass-row-head.is-stuck{ box-shadow: var(--row-head-shadow-0); }
  }
}

.glass-row .row-id   { font-family: ui-monospace, monospace; opacity: 0.70; font-size: var(--fs-1); }
/* Name truncation: the name cell is the minmax(0,1fr) track, so it must take
   the overflow instead of stretching the row past its siblings. The sequence
   chip keeps its fixed size; the badge and the «· username» fragment stay on
   one line and the name itself ellipsises where it runs out of room. */
.glass-row .row-name{
  min-width: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: var(--sp-1);
}
.glass-row .row-name > bdi{
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.glass-row .row-name > .row-pg{
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex-shrink: 1;
}
.glass-row .row-name > .badge-glass{ flex-shrink: 0; }
.glass-row .row-pg   { font-family: ui-monospace, monospace; font-size: 0.78rem; opacity: 0.7; direction: ltr; }
.glass-row .row-vol  { font-size: 0.85rem; opacity: 0.85; white-space: nowrap; }
.glass-row .row-date { font-size: 0.80rem; opacity: 0.70; white-space: nowrap; }
.glass-row .row-chev { opacity: 0.4; font-size: 1.1rem; }
/* «ردیف» — the subscription's ordinal for its own owner, rendered inside the
   name cell rather than as a new column: the grid template above is shared with
   .glass-row-head, which bots.php also uses, so an 8th track would misalign that
   page's header. Sized to sit beside the username without competing with it. */
.glass-row .row-seq{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  min-width: 22px;
  padding: 1px 6px;
  margin-inline-end: 6px;
  border-radius: var(--r-full);
  /* R3-06: chip fill/border move to the hairline family — 0.07→0.06 and
     0.10→0.12/0.10 are one alpha step within the family, not a re-tune. */
  background: var(--hairline);
  border: 1px solid var(--hairline-strong);
  font-size: 0.74rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
  vertical-align: 1px;
}
html[data-theme="light"] .glass-row .row-seq{
  opacity: 0.8;
}
@media (max-width: 1023px) {
  /* minmax(0,1fr) — not `auto` — on the name track: an `auto` track's min size
     is its min-content size, and a non-wrapping Pasarguard username would blow
     the card past the viewport (the .url-copy-glass blow-out fixed the same
     way in .detail-grid, see UI-0511). min-width:0 on .row-name lets the flex
     row inside it shrink and ellipsize instead. .row-pg is a flex child of
     .row-name, not a grid item, so it takes no grid placement here — the old
     `.row-pg` rule targeted the pre-flex markup and was inert. */
  .glass-row {
    grid-template-columns: auto minmax(0, 1fr) auto;
    row-gap: 8px;
    padding: var(--sp-3) var(--sp-3-5);
  }
  .glass-row .row-id      { grid-row: 1; grid-column: 1; }
  .glass-row .row-name    { grid-row: 1; grid-column: 2; min-width: 0; }
  /* The status badge shares column 3 with .row-date; the (wider) date sizes the
     column, so the auto-placed badge would stretch (default justify-self) and its
     centred text would drift to the inline-start edge. Pin it and hug its content
     so the pill stays content-width with the label centred. */
  .glass-row .badge-glass { grid-row: 1; grid-column: 3; justify-self: end; }
  .glass-row .row-vol     { grid-row: 2; grid-column: 1 / 3; }
  .glass-row .row-date    { grid-row: 2; grid-column: 3; justify-self: end; }
  .glass-row .row-meters  { grid-row: 3; grid-column: 1 / -1; }
  .glass-row .row-chev    { display: none; }
}

/* ───────────────────────── 6. Filter toolbar (shared) ────────────────────
   Phase 5 stage 3b: ONE filter pattern for representatives / deposits /
   subscriptions.
     • Desktop: a single inline toolbar row — search (icon inside) · selects ·
       result count · clear. Not a card.
     • Mobile: full-width search + one "فیلترها" button opening a bottom sheet
       that holds the selects; active filters render as chips under the search.
   It is ONE <form>: submit gathers every named field wherever it sits visually,
   so request handling / query params are unchanged (the selects only MOVE
   between an inline row and a fixed sheet — they never leave the form). */
.filters-glass{
  display: flex; gap: var(--sp-3);
  padding: 0; background: none; border: 0; border-radius: 0;
  margin-bottom: var(--sp-5);
  flex-wrap: wrap;
  /* Items in this bar have unequal heights: a labelled field is
     label(17px)+gap(4)+control, while the search box, the submit button and the
     result count are bare controls. Under `align-items: center` those unequal
     boxes were centred against each other, so the CONTROLS — the things the eye
     actually lines up on — landed on three different baselines (measured 336 /
     324 / 316 on deposits: a 20px spread), which read as the buttons sitting
     above/below the selects. `flex-end` puts every control on one bottom edge
     and lets the labels stack upward; this is the same fix search.php already
     uses for its own control bar. */
  align-items: flex-end;
  /* One height for every control in the bar, so the shared bottom edge is also
     a shared top edge for the unlabelled ones (select was 41px, button 38px). */
  --filter-ctrl-h: 42px;
}
.filters-glass .glass-input,
.filters-glass .glass-select,
.filters-glass .glass-scope-field,
.filters-glass .btn-glass{
  min-height: var(--filter-ctrl-h);
  height: var(--filter-ctrl-h);
  box-sizing: border-box;
}
/* The actions group holds a button + the result-count text; give it the control
   height so the text centres within the same band instead of floating. */
.filters-glass .filter-actions{
  min-height: var(--filter-ctrl-h);
  align-items: center;
}
.filters-glass label { display: block; font-size: 0.8rem; opacity: 0.7; margin-bottom: 4px; } /* Commit 1 (#11): 12px floor */
.filters-glass input,
.filters-glass select{
  /* §2a fix: background-COLOR (not the `background:` shorthand) so the canonical
     .glass-select chevron background-image survives — the shorthand was wiping it,
     leaving the selects looking like plain text inputs at rest. */
  background-color: var(--input-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: var(--sp-2) var(--sp-3);
  color: var(--text); font-size: 0.9rem; font-family: inherit;
  min-height: 40px;
}
/* §2a fix: keep the .glass-select dropdown chevron (+ its inline-end reserve,
   which the padding shorthand above resets) so a filter select reads as a
   dropdown, distinct from the search input. Tuned tighter for the 40px toolbar. */
.filters-glass select.glass-select{
  padding-inline-end: 34px;
  background-position: left 12px center;
}
.filters-glass input:focus,
.filters-glass select:focus{
  outline: none;
  border-color: rgba(var(--accent-rgb), 0.5);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.15);
}
/* R2-03: the light-twin `#fff` background is gone — var(--input-bg) and
   var(--border) carry the polarity themselves. */

/* ───────────────────────── 6z. Invalid field (error state) ─────────────
   G10-assets: NOT ONE form control in the panel had an error state. The
   technique's pattern is "an error border, a 380ms shake and an inline message
   linked via aria-describedby" — this is the first two; the third is a markup
   contract (an element with an id, referenced from the control's
   aria-describedby) and `.field-error` below is the class it should carry.
   `aria-invalid="true"` is the hook rather than a `.is-invalid` class on
   purpose: it is the state a screen reader already announces, so the visual
   and the announced state cannot drift apart. Nothing emits it yet — this
   styles the contract the moment anything does.
   The selector list is the shared control vocabulary (§2a filters, §24 select,
   §27 input-group, §26 amount, §25 method card's fields, .form-control) rather
   than a bare `[aria-invalid]`, so it cannot silently start painting chrome on
   an unrelated element that happens to carry the attribute.
   The shake is `both`-filled and one-shot: it runs when the attribute lands and
   does not repeat, because a field that keeps shaking is a field the user
   cannot read. 320ms, max 4px of travel — the technique's restraint ceiling —
   and it never fires under reduce-motion. */
.glass-input[aria-invalid="true"],
.glass-textarea[aria-invalid="true"],
.glass-select[aria-invalid="true"],
.glass-amount-input[aria-invalid="true"],
.glass-input-group input[aria-invalid="true"],
.glass-input-group textarea[aria-invalid="true"],
.filters-glass input[aria-invalid="true"],
.filters-glass select[aria-invalid="true"],
.form-control[aria-invalid="true"]{
  border-color: rgba(var(--danger-rgb), 0.65);
  animation: field-shake 320ms var(--ease-out) both;
}
.glass-input[aria-invalid="true"]:focus,
.glass-textarea[aria-invalid="true"]:focus,
.glass-select[aria-invalid="true"]:focus,
.glass-amount-input[aria-invalid="true"]:focus,
.glass-input-group input[aria-invalid="true"]:focus,
.filters-glass input[aria-invalid="true"]:focus,
.filters-glass select[aria-invalid="true"]:focus,
.form-control[aria-invalid="true"]:focus{
  /* Keep the danger border while focused — the focus ring and the error state
     are two different facts and the field is still wrong while you fix it. */
  border-color: rgba(var(--danger-rgb), 0.85);
  box-shadow: 0 0 0 3px rgba(var(--danger-rgb), 0.15);
}
html[data-theme="light"] .glass-input[aria-invalid="true"],
html[data-theme="light"] .glass-textarea[aria-invalid="true"],
html[data-theme="light"] .glass-select[aria-invalid="true"],
html[data-theme="light"] .glass-amount-input[aria-invalid="true"],
html[data-theme="light"] .glass-input-group input[aria-invalid="true"],
html[data-theme="light"] .filters-glass input[aria-invalid="true"],
html[data-theme="light"] .filters-glass select[aria-invalid="true"]{
  border-color: rgba(var(--danger-rgb), 0.75);
}
@keyframes field-shake{
  0%   { transform: translateX(0); }
  20%  { transform: translateX(-4px); }
  40%  { transform: translateX(4px); }
  60%  { transform: translateX(-3px); }
  80%  { transform: translateX(2px); }
  100% { transform: translateX(0); }
}
/* The inline message the control is described by. Sized and coloured like the
   rest of the panel's hints, so a form with one bad field does not grow a new
   visual language for it. */
.field-error{
  display: block;
  margin-top: 6px;
  font-size: 0.78rem;
  font-weight: 500;
  color: rgb(var(--danger-text-rgb));
  line-height: 1.5;
}
html[data-theme="light"] .field-error{ color: rgb(var(--danger-text-rgb)); }

/* search field — search icon sits inside the input */
.filter-search{ position: relative; flex: 1 1 240px; min-width: 0; display: flex; }
.filters-glass .filter-search input{ width: 100%; padding-inline-start: 38px; }
.filter-search-icon{
  position: absolute; inset-inline-start: 12px; top: 50%; transform: translateY(-50%);
  display: inline-flex; opacity: 0.5; pointer-events: none;
}
.filter-search-icon svg{ width: 18px; height: 18px; }

/* `clearable input`: the field had a leading search icon and no way back out.
   On mobile, clearing a search means opening the filter sheet and pressing
   «حذف فیلترها» — a control that is not in the field the user is looking at.
   The technique's rules are specific and all four are honoured here: the button
   exists only when there IS content (`.has-value`, toggled by the page's own
   script), it keeps the field focused after clearing (JS — `focus()` back to
   the input), it is reachable by keyboard (a real <button> in the tab order,
   not a click-target div), and visibility animates on opacity over ~160ms
   rather than appearing in one frame.
   Pointer-events are gated on `.has-value` too: an invisible button that still
   swallows taps in the empty field is worse than no button. */
.filter-clear{
  position: absolute;
  inset-inline-end: 4px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: var(--r-full);
  color: var(--text);
  opacity: 0;
  pointer-events: none;
  cursor: pointer;
  transition: opacity 160ms var(--ease-out), background 0.15s ease;
}
.filter-search.has-value .filter-clear{ opacity: 0.6; pointer-events: auto; }
.filter-search.has-value .filter-clear:hover{ opacity: 1; background: var(--hover); }
.filter-clear:focus-visible{
  opacity: 1;
  outline: 2px solid var(--ring);
  outline-offset: 1px;
}
/* Reserve the slot only while the button can actually appear, so an empty
   field keeps its full width for the placeholder text. */
.filter-search.has-value input{ padding-inline-end: 40px; }
.filter-clear svg{ width: 16px; height: 16px; }
/* 44px target wherever the pointer is coarse — the 34px box is a mouse size.
   44v (the inline-size variant) is the right unit here and not a typo for
   `vw`: it is the inline-axis form of the viewport unit, so the target scales
   with the touch dimension in either writing mode. */
@media (pointer: coarse){
  .filter-clear{ width: 44px; height: 44px; }
}

/* the select group: inline on desktop (display:contents flattens the wrappers
   into the toolbar row), a bottom sheet on mobile. */
.filter-fields{ display: contents; }
.filter-sheet{ display: contents; }
.filter-field{ display: flex; flex-direction: column; }
/* `.field-caption` is a <span> that LOOKS like a field label but labels no form
   control — used where the "field" is a read-only note (the L1 scope display on
   representatives.php). A bare <label> there is what Chrome reports as
   "label has neither for= nor a nested input", and it is honest markup: a
   <label> that labels nothing is a lie to a screen reader. Same declarations,
   so the swap is invisible. */
.filter-field > label,
.filter-field > .field-caption{ font-size: 0.72rem; opacity: 0.7; margin-bottom: 4px; }
.filter-sheet-head, .filter-sheet-foot{ display: none; }   /* mobile-only sheet chrome */

/* trailing actions: submit (where the backend needs it) · result count · clear */
/* Commit 5 (#35/#72): `margin-inline-start:auto` shoved this group to the far
   opposite end of the toolbar. On deposits — which has no growing search field
   to absorb the slack — the selects sat against one edge and the apply button +
   result count against the other, with a large empty gap between: three visually
   separate islands for what is one operation. The actions now follow the fields
   directly, forming a single coherent toolbar. Pages that DO have a search field
   still spread naturally, because .filter-search keeps its flex-grow. */
.filter-actions{ display: flex; align-items: center; gap: var(--sp-2); }
.filter-result-count{ font-size: 0.85rem; color: var(--muted-foreground); white-space: nowrap; }

/* Commit 5 (#74): role/scope is faint metadata beside the title — deliberately
   NOT a pill, so it stops competing with the result-count badge next to it. */
.head-sub .head-scope{ font-size: 0.85rem; color: var(--muted-foreground); }

/* Commit 6 (#57) put a 960px cap here: buy.php ships class="wrap wrap-narrow",
   and app.css's later `.wrap{ max-width: var(--container-max) }` was overriding
   .wrap-narrow's 720px, so the purchase form rendered at the full desktop shell.
   A compound selector outranked it — which worked, and is exactly why this rule
   had to be the one retired.

   M10 STAGE H (2026-09-21) — RETIRED, ONE MEASURE. The compound selector that
   beat app.css is what made `settings.php` (also `wrap wrap-narrow`) paint a
   960px column while `dashboard.php` painted 1200px, two clicks apart: the
   operator's "every tab is a different frame". Measured on the live DOM before
   the retirement: resolved max-widths across the four panel pages were
   ['1200px','960px']; after, ['1200px'].
   The rule could not be corrected from app.css. A grouped
   `.wrap,.wrap-narrow,.wrap.wide` there is (0,1,0) and loses to THIS rule's
   (0,2,0) at a later load position, so it compiles, reads as a fix, and changes
   nothing — verified by measuring, not by reading. Deleting the winner is the
   only edit that reaches the page; CLAUDE.md §0 "change the winner, leave the
   loser alone."
   What is deliberately NOT reverted: the reason the 960 existed. The form-heavy
   pages keep a narrow measure via the class they already carry — `wrap-narrow`
   alone is 720px (app.css:436) and is not overridden at ≥1024px any more, so
   buy.php/telegram_notify.php/bots.php narrow by one rule instead of by a
   compound special case. Net effect at the operator's 1440x900: 1200px on every
   panel page, settings.php included. */
@media (min-width: 1024px){
  /* M10 STAGE J (2026-09-21) — ONE MEASURE, and the losers are DELETED, not
     out-voted. Stage H made the three classes agree on `--container-max`
     (1200px); two rules still beat it, and both are gone in this diff rather
     than left compiling and losing:

       - `@media (min-width:1280px){ .wrap.wide{ max-width:1400px } }`, the
         eleven list pages' opt-in. At equal specificity it loaded later than
         the grouped rule above and won, so `audit.php` painted 1400px while
         `wallet.php` painted 1200px — the operator's "some pages are wider".
       - `.wrap.wrap-narrow{ max-width:960px }` (retired in stage H, kept named
         here so the next reader knows it was the other half of the same bug).

     A rule that compiles and loses is how this defect was created twice; the
     fix is one declaration, and every other statement of this measure in the
     shell now loses to it by LOAD ORDER (glass.css is last, CLAUDE.md §0) at
     equal specificity, which is a property `bin/test_one_measure.php` asserts
     by running the real mini-cascade rather than by grepping for a number.

     THE min() IS NOT DECORATION. 1400px alone would overflow the band left of
     the rail on a 1024px laptop (`100vw - 232px - 2*gutter` is 760px there).
     `--gutter` is the shell's own 16px token and `.wrap` already pads with it,
     so the frame is exactly the room it has and never wider. The subtraction
     is the EXPANDED rail reservation; the collapsed state reserves 64px and
     gets its own rule below — same measure, tracking the same state `body`'s
     padding tracks. A hard-coded 232 in this calc with a collapsed rail would
     leave a 168px hole on the rail's side.

     The frame is centred inside the body band (body carries the reservation),
     never in the full viewport: measured on the operator's 1846px screen, full
     viewport centring splits 214px/0px against the rail while the band splits
     107px/107px, which is the one that reads as balanced in RTL. */
  .wrap, .wrap-narrow, .wrap.wide{
    max-width: min(1400px, calc(100vw - 232px - 2 * var(--gutter)));
  }
  /* COLLAPSED RAIL: html[data-rail="collapsed"] narrows the rail to 64px, and
     nav.php's block narrows `body`'s reservation to match. This is the other
     half of that pair — a compound selector, so it outranks the grouped rule
     above without an !important and without a second load-order bet. */
  html[data-rail="collapsed"] .wrap,
  html[data-rail="collapsed"] .wrap-narrow,
  html[data-rail="collapsed"] .wrap.wide{
    max-width: min(1400px, calc(100vw - 64px - 2 * var(--gutter)));
  }
}

/* mobile-only affordances (hidden on desktop) */
.filter-toggle{ display: none; }
.filter-chips{ display: none; }
.filter-chip{
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px; border-radius: var(--r-full);
  background: rgba(var(--accent-rgb), 0.12);
  border: 1px solid rgba(var(--accent-rgb), 0.30);
  color: var(--text); font-size: 0.8rem; text-decoration: none; white-space: nowrap;
}
.filter-chip .filter-chip-x{ opacity: 0.7; font-weight: 700; line-height: 1; }
html[data-theme="light"] .filter-chip{ background: rgba(var(--accent-rgb), 0.10); }

@media (max-width: 768px){
  .filters-glass{ gap: var(--sp-2); }
  .filter-search{ flex: 1 1 auto; }         /* fills row 1 beside the compact toggle */
  .filter-toggle{ display: inline-flex; flex: 0 0 auto; align-items: center; gap: 6px; }
  .filter-toggle .nb{ margin-inline-start: 2px; }
  .filter-actions{ display: none; }         /* submit/clear move into the sheet on mobile */

  /* selects → bottom sheet */
  .filter-fields{ display: none; }
  .filter-fields.open{
    display: flex; position: fixed; inset: 0; z-index: 1150;
    align-items: flex-end; background: rgba(0,0,0,0.45);
  }
  .filter-fields.open .filter-sheet{
    /* Part A (3b + f4): controls 48px, field-to-field 16px (8px base gap + an 8px
       margin between adjacent .filter-field), but the CHROME gaps (head↔fields,
       fields↔foot) are the tighter 8px and the outer padding is trimmed, so a
       2-field sheet stays compact. Height is content-driven with an 80dvh cap. */
    display: flex; flex-direction: column; gap: var(--sp-2);
    width: 100%; background: var(--popover);
    border-top-left-radius: var(--r-3); border-top-right-radius: var(--r-3);
    box-shadow: 0 -12px 40px rgba(0,0,0,0.40);
    /* R6-04: floor clamp, not additive — a 34px home indicator must not
       push the padding to 44px; the inset is a lower bound on 10px. */
    padding: 8px var(--sp-4) max(10px, env(safe-area-inset-bottom, 10px));
    /* A-M: 80vh precedes 80dvh so iOS Safari <15.4 / Chrome <108 — which do
       not know `dvh` and drop the whole declaration — still cap the sheet
       instead of letting it run off the top of the screen. The rest of the
       file already pairs its dvh this way; this rule and the nav sheet below
       were the two that did not. */
    max-height: 80vh;
    max-height: 80dvh; overflow-y: auto;
    animation: nav-more-sheet-in 260ms cubic-bezier(0.32,0.72,0,1);
  }
  .filter-fields.open .filter-sheet-head{
    display: flex; align-items: center; gap: 4px;
    font-weight: 700; font-size: var(--fs-3);
  }
  /* title pushes the reset + close actions to the row end (RTL-aware). */
  .filter-fields.open .filter-sheet-head > span{ margin-inline-end: auto; }
  .filter-fields.open .filter-sheet-foot{ display: flex; gap: var(--sp-2); margin-top: 0; }
  .filter-fields.open .filter-sheet-foot > *{ flex: 1; justify-content: center; }
  .filter-fields.open .filter-field{ width: 100%; }
  /* restore the 16px field-to-field rhythm on top of the 8px base gap (f4). */
  .filter-fields.open .filter-field + .filter-field{ margin-top: var(--sp-2); }
  .filter-fields.open .filter-field select,
  .filter-fields.open .filter-field input{ width: 100%; min-height: 48px; }
  .filter-sheet-close{
    display: inline-flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; padding: 0; border: none; background: transparent;   /* f4: 44px tap target; the × glyph stays centered/small */
    color: var(--text); border-radius: var(--r-1); cursor: pointer;
  }
  /* Part B (3b): low-emphasis TEXT reset — JS-injected into the head, clears the
     sheet fields to their unfiltered defaults (no submit, no close). Deliberately
     subordinate to the solid "اعمال فیلتر": muted text, no fill, no border. */
  .filter-sheet-reset{
    border: 0; background: transparent;
    padding: 6px 10px; min-height: 44px;
    color: var(--muted-foreground);
    font-family: inherit; font-size: 0.85rem; font-weight: 500;
    cursor: pointer; border-radius: var(--r-1); white-space: nowrap;
  }
  .filter-sheet-reset:hover{ color: var(--text); }
  .filter-sheet-reset:focus-visible{ outline: 2px solid var(--ring); outline-offset: 2px; }
  .filter-chips:not(:empty){ display: flex; flex: 1 1 100%; flex-wrap: wrap; gap: 6px; }
}

.glass-pager{
  display: flex; align-items: center; justify-content: center;
  gap: var(--sp-3-5); flex-wrap: wrap;
  margin-top: 18px;
}
.glass-pager .pager-status{ font-size: 0.85rem; opacity: 0.75; }

/* ─────────────────── 6b. Glass filter bar (in-card) ───────────────────── */
/* Canonical filter/control ROW for forms that live INSIDE a .glass-card
   (the card already supplies the chrome — unlike .filters-glass above, which
   is the self-chromed standalone variant). Aligns labelled .glass-input-group
   controls and trailing .btn-glass actions onto a single baseline with a
   shared 44px height, so a label-less button never floats off the line and
   every control reads as one row.

   Why this exists: a .glass-input-group carries a default margin-bottom:12px;
   dropped into an align-items:flex-end row next to a bare button, that margin
   lifts the inputs 12px above the button's baseline (the exact misalignment
   this class neutralises). The selects/inputs and the button are also forced
   to the same 44px height. Layout-only — no markup semantics, names, ids, or
   behaviour change. Mirrors the page-scoped .search-form pattern. */
.glass-filter-bar{
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sp-3);
}
.glass-filter-bar .glass-input-group{
  margin-bottom: 0;
  flex: 1 1 160px;   /* equal-width fields that share the bar and fill it when wrapped */
  min-width: 0;      /* permit shrink below intrinsic width so wrapping is clean */
}
.glass-filter-bar .glass-input,
.glass-filter-bar .glass-select{ min-height: 44px; }
.glass-filter-bar .btn-glass{ flex: 0 0 auto; min-height: 44px; }
@media (max-width: 560px){
  /* Narrow screens: trailing action buttons take their own full-width row so
     they never wedge beside a half-width field. */
  .glass-filter-bar .btn-glass{ flex: 1 1 100%; width: 100%; }
}

/* ───────────────────────── 7. URL copy box ───────────────────────────── */
/* fixes-merged-02: drop the explicit copy button — the card number /
   crypto address are one-tap-selectable via user-select: all. Row
   stays a flex container only so the layout matches other elements
   that might still include a trailing meta chip. */
.url-copy-glass{
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 10px var(--sp-3-5);
  background: var(--glass-card-bg);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--r-2);
  /* A-L: no flex-wrap — children own `min-width: 0` + a scroll container,
     so the URL scrolls within its own box instead of pushing the copy
     button onto a second line on narrow viewports. */
  flex-wrap: nowrap;
  min-width: 0;
}
.url-copy-glass > .url-text,
.url-copy-glass > .card-number,
.url-copy-glass > .crypto-address{
  flex: 1 1 auto;
  min-width: 0;
  /* user-select: all — a single click/tap selects the entire string so
     the user can ⌘C / Ctrl-C / mobile-select it without an explicit
     copy affordance. The card-number / crypto-address already render
     with monospace + dir:ltr; this just adds the selection behaviour. */
  -webkit-user-select: all;
  -moz-user-select: all;
  user-select: all;
  cursor: text;
  /* Commit 5 (#65): was overflow:hidden + ellipsis — the tail of a subscription
     URL was simply unreadable, with no scroll and no tooltip; copy was the only
     way to see it. Horizontal scroll restores full inspection in place (the
     value stays on one line, and copy remains the primary action). Call sites
     also carry title="…" so a hover reveals it without scrolling. */
  overflow-x: auto;
  overflow-y: hidden;
  /* MUST stay `clip` — do NOT "restore" the ellipsis. #65 left
     `text-overflow: ellipsis` behind when it switched this box to
     overflow-x:auto, and the two cannot coexist: ellipsis paints only the
     first clientWidth of glyphs, so the ellipsized stub is narrower than
     the scroll range and scrolls straight out of the scrollport — past
     ~clientWidth the field renders EMPTY and the tail is unreachable by
     scrolling at all. Verified in Chromium at 390px: 223px visible /
     814px scrollWidth, blank at scrollLeft=581. `clip` is what makes the
     scroll #65 added actually usable. */
  text-overflow: clip;
  white-space: nowrap;
  scrollbar-width: thin;
  overscroll-behavior-inline: contain;
}
/* Keep the inline scrollbar unobtrusive on WebKit. Covers all three scroll
   children — .card-number / .crypto-address scroll too since the shadowing
   `overflow: hidden` was removed from the .glass-method-card rules below. */
.url-copy-glass > .url-text::-webkit-scrollbar,
.url-copy-glass > .card-number::-webkit-scrollbar,
.url-copy-glass > .crypto-address::-webkit-scrollbar{ height: 4px; }
.url-copy-glass > .url-text::-webkit-scrollbar-thumb,
.url-copy-glass > .card-number::-webkit-scrollbar-thumb,
.url-copy-glass > .crypto-address::-webkit-scrollbar-thumb{
  background: rgba(128,128,128,0.45); border-radius: 2px;
}
.url-copy-glass > button,
.url-copy-glass > .btn-glass{
  flex-shrink: 0;
}
.url-copy-glass .url-text{
  font-family: ui-monospace, monospace;
  font-size: var(--fs-1);
  direction: ltr; color: var(--text);
}
html[data-theme="light"] .url-copy-glass{
  border-color: rgba(0,0,0,0.08);
}

/* ───────────────────────── 7b. Usage progress bar ────────────────────── */
/* H4.3: per-row usage bar used in admin/my_subscriptions.php for the
   "مصرف" column. Width of the outer track is set inline by the caller
   (e.g. style="width:70px"); the inner <span> width is the percentage.
   Inline-axis is RTL-aware: anchoring the fill via `inset-inline-start`
   keeps it growing from the row's leading edge in both LTR and RTL. */
.usage-bar{
  position: relative;
  display: inline-block;
  height: 6px;
  max-width: 100%;
  min-width: 40px;
  border-radius: var(--r-full);
  background: rgba(255,255,255,0.08);
  overflow: hidden;
  vertical-align: middle;
}
.usage-bar > span{
  position: absolute;
  top: 0; bottom: 0;
  inset-inline-start: 0;
  display: block;
  /* UI-0112: fill width comes from a CSS custom property set in the markup
     (style="--usage-pct: NN%"), not an inline width declaration — keeps the
     bar working under a strict style-src CSP that forbids `width:%`. */
  width: var(--usage-pct, 0%);
  max-width: 100%;
  border-radius: var(--r-full);
  background: linear-gradient(
    90deg,
    rgba(var(--accent-rgb), 0.85),
    rgba(var(--accent-rgb), 0.55));
  /* `progress ease`: 250ms with an undeclared curve snapped rather than grew —
     the fallback `ease` is symmetric, so the bar decelerated into its final
     width as hard as it accelerated out of the old one. 600ms ease-out is the
     technique's value: one direction, arriving. */
  transition: width 600ms var(--ease-out);
}
/* The percentage that sits beside a meter is the number the meter is drawing,
   so it must not reflow as it changes — same reason as `.stat-val` below.
   `.ring-pct` is the LIVE one (§52's radial gauge, rendered by
   render_usage_ring()); the `<span>` inside `.usage-bar` that the finding
   names has no call site left, so nothing here targets it. */
.ring-pct,
.dot-meter .dm-label{ font-variant-numeric: tabular-nums; }
html[data-theme="light"] .usage-bar{
  background: rgba(0,0,0,0.08);
}

/* ───────────────────────── 8. QR card ────────────────────────────────── */
.qr-glass{
  display: flex; flex-direction: column; align-items: center;
  padding: var(--sp-6);
  border-radius: var(--r-2);
  background: var(--glass-card-bg);
  border: 1px solid rgba(255,255,255,0.10);
  box-shadow: 0 0 40px rgba(255,255,255,0.04);
}
/* QR image: must NOT be rounded — rounded corners clip the finder patterns
   and quiet zone, breaking scanner detection. White background and padding
   double as the QR quiet-zone (≥4 modules), so keep them. Size bumped from
   200 → 256 so modules land closer to whole-pixel boundaries at common DPRs
   (image is rendered as ~294–342px SVG natively). */
.qr-glass img{
  width: 288px; height: 288px;
  background: #fff; padding: 0;
  border-radius: 0;
  box-sizing: content-box;
  /* Native SVG (scale=10) is ~430–530px → display always downsamples.
     Combined with shape-rendering="auto" on the SVG, downsample produces
     uniform anti-aliased module edges. The hints below have no negative
     effect on SVG-in-img and stay for safety per scan-quality checklist. */
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
.qr-glass .qr-caption{ margin-top: 12px; font-size: 0.85rem; color: var(--text-muted); opacity: 1; }

/* QR zoom modal — desktop ≈ one-third of viewport, mobile full-screen.
   Same scanner-safety rules apply: image stays square-cornered and sits on
   a white tile so the quiet zone is preserved. */
.modal-overlay.glass-modal.qr-zoom-modal .modal-box{
  width: clamp(360px, 36vw, 560px);
  max-width: calc(100vw - 24px);
  padding: var(--sp-5);
}
.qr-zoom-body{
  display: flex; flex-direction: column; align-items: center;
  padding: var(--sp-2) 0 var(--sp-1);
}
.qr-zoom-img{
  width: 100%;
  max-width: 480px;
  height: auto;
  aspect-ratio: 1 / 1;
  /* UI-0371: in narrow landscape the 1:1 aspect made height follow width and
     overflow the short viewport. Clamp by viewport height (dvh + vh fallback)
     so the square shrinks to fit instead of spilling. */
  max-height: 80vh;
  max-height: 80dvh;
  background: #fff;
  padding: 0;
  border-radius: 0;
  box-sizing: content-box;
  /* See comment on .qr-glass img — native SVG is much larger than display,
     so downsampling + shape-rendering="auto" yields clean uniform modules. */
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
.qr-zoom-caption{ margin-top: 14px; font-size: 0.9rem; color: var(--text-muted); opacity: 1; }

/* Mobile = full-screen (opts out of the global bottom-sheet style applied
   to all .glass-modal at ≤768px further down in this file). */
@media (max-width: 768px){
  .modal-overlay.glass-modal.qr-zoom-modal{
    align-items: center;
    padding: 0;
  }
  .modal-overlay.glass-modal.qr-zoom-modal .modal-box{
    width: 100vw;
    max-width: 100vw;
    /* A-M: 100vh fallback for iOS Safari <15.4 / Chrome <108. */
    height: 100vh;
    height: 100dvh;
    max-height: 100vh;
    max-height: 100dvh;
    margin: 0;
    border-radius: 0;
    border-inline: 0;
  }
  .qr-zoom-body{
    flex: 1;
    justify-content: center;
    padding: var(--sp-4);
  }
  .qr-zoom-img{ max-width: min(90vmin, 540px); }
}

/* ───────────────────────── 9. Modal (glass) ──────────────────────────── */
/* fixes-merged-02 / Issue 1: the modal needs a single, reliable scroll
   container. Pre-fix the .modal-box inherited overflow-y:auto + max-height:
   90vh from app.css §20, but glass.css §9 redeclared the box without
   re-asserting those rules, and the mobile bottom-sheet at the bottom of
   this file capped to 92vh. On mobile, 1vh > visible vh while the URL bar
   is showing, so the bottom of the modal lived BELOW the visible area and
   no scroll affordance could reach it. The dvh unit handles the URL-bar
   case; flex + min-height:0 + overflow:hidden on the overlay restricts
   scrolling to the box; overscroll-behavior:contain prevents body-scroll
   leak when the touch reaches the box's edge. */
/* G10-assets: the entrance scaled from 0.96. `backdrop blur` names 0.9, and
   the gap is not cosmetic — at 0.96 the dialog is already 96% of its final
   size when it appears, so the motion reads as a flicker rather than as the
   dialog arriving from the backdrop. 0.9 with the same 220ms ease-out is the
   documented curve.
   `modal-out` is the matching exit, so a dialog leaves the way it came in.
   Nothing called it before this pass — the overlay simply went display:none —
   which is why the pair is asserted together. */
@keyframes modal-in {
  0%   { opacity: 0; transform: scale(0.9); }
  100% { opacity: 1; transform: scale(1); }
}
@keyframes modal-out {
  0%   { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.97); }
}
.modal-overlay.glass-modal{
  /* Closed by default — opened via .open / .active / [data-open="true"]
     which app.js's openModal() toggles. Pre-fix, this selector was
     display:flex (always-on) with no position:fixed/z-index, so every
     modal rendered permanently inline at the bottom of the document.
     Page-scoped workarounds existed on representatives.php and
     deposits.php; this rule supersedes them. */
  display: none;
  position: fixed;
  inset: 0;
  /* STACKING FIX: raised 1000→1100 so a dialog always outranks a menu.
     The portaled rep-menu panel is z-1050 (§ `body > .rep-menu-panel`), so at
     1000 every modal opened FROM that menu — the cost/reset-password modals
     and the glass confirm dialog — rendered underneath it, and on mobile was
     buried entirely by the sheet's 100vmax scrim shadow. app.js now also
     closes the menu on selection; this is the defence-in-depth half, so a
     lingering menu can never occlude a dialog. Stays below .nav-more (1200),
     which closes on selection of its own accord. */
  z-index: 1100;
  /* R3-01: polarity scrim token (§1a) — light systems get the softened
     rgba(15,18,30,0.30) base automatically. */
  background: var(--modal-scrim);
  /* `backdrop blur`: the technique asks for backdrop-filter: blur(4px) on the
     overlay, and the flat 0.6 black above is only the FALLBACK — where
     backdrop-filter is unsupported (or the user has asked for less
     transparency) the dim alone still separates the dialog from the page.
     @supports rather than a bare declaration so a browser that parses but
     cannot composite the filter does not end up with a half-painted overlay.
     The blur is generous (4px) and the dim is lightened in the same breath:
     blur without a lighter scrim reads as a muddy smear rather than depth. */
  -webkit-backdrop-filter: none;
}
@supports ((backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px))){
  .modal-overlay.glass-modal{
    /* R3-01: the blurred variant's lighter scrim — token carries both
       polarities (dark 0.45 black / light 0.30 slate). */
    background: var(--modal-scrim-blur);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
  }
}
.modal-overlay.glass-modal{
  /* Overlay itself never scrolls; the box does. Without overflow:hidden
     here the page can scroll behind the open modal on iOS Safari. */
  align-items: center;
  justify-content: center;
  overflow: hidden;
  /* dvh = dynamic viewport height: collapses by the URL bar's height on
     mobile, unlike vh which references the largest viewport. A-M:
     100vh precedes 100dvh so iOS Safari <15.4 / Chrome <108 still
     pin the overlay to the viewport. */
  height: 100vh;
  height: 100dvh;
  max-height: 100vh;
  max-height: 100dvh;
}
.modal-overlay.glass-modal.open,
.modal-overlay.glass-modal.active,
.modal-overlay.glass-modal[data-open="true"]{
  display: flex;
}
.modal-overlay.glass-modal.open .modal-box,
.modal-overlay.glass-modal.active .modal-box,
.modal-overlay.glass-modal[data-open="true"] .modal-box{
  animation: modal-in 220ms ease-out;
}
.modal-overlay.glass-modal .modal-box{
  background: var(--popover);           /* opaque elevated surface (shadcn) */
  border: 1px solid var(--border);
  border-radius: var(--r-3);            /* 16px — dialog radius */
  /* R3-02: dialog elevation as a polarity token (dark 0.55 / light 0.18). */
  box-shadow: var(--modal-shadow);
  /* fixes-merged-02 / Issue 1: scroll container. Flex column + min-height:0
     lets long-content children shrink so overflow-y:auto on the box itself
     actually scrolls. dvh handles the mobile URL-bar gap. */
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* UI-0044: canonical modal-box padding. Page modals override this with
     ID-scoped rules (higher specificity), so existing modals are unchanged;
     this gives any future generic .modal-box a sane default AND a real basis
     for the .modal-actions full-bleed `margin: 0 -24px` below (which used to
     cancel a padding that did not exist). */
  padding: 22px var(--sp-6);
  /* A-M: 100vh fallback before 100dvh for iOS Safari <15.4. */
  max-height: calc(100vh - 32px);
  max-height: calc(100dvh - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.modal-overlay.glass-modal .modal-header{
  /* Header never compresses when the body grows. */
  flex-shrink: 0;
  /* Canonical header layout: title on one side, the ✕ close control on the
     other. Page modals that ship an id-scoped .modal-header (deposits.php,
     subscription_detail.php) or an inline style (payment_methods.php) win on
     specificity and stay byte-for-byte unchanged; this gives the modals that
     DON'T (wallet top-up, representatives, my_subscription_detail) the same
     row instead of a stacked, default-flow header. */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin: 0;
}
.modal-overlay.glass-modal .modal-header h3{ margin: 0; }

/* Canonical glass close control — the icon-only "✕" button in every glass
   modal header. Without this the bare <button> fell back to raw UA chrome
   (grey box, default font) and clashed with the glass theme; that was the
   wallet top-up complaint. The same declarations were duplicated id-scoped
   in deposits.php / subscription_detail.php — hoisting them to the design
   system makes the wallet top-up, representatives and my_subscription_detail
   modals match too. Scoped :not(.btn-glass) so a "لغو/Cancel" button that
   only borrows .modal-close for its close BEHAVIOUR keeps its .btn-glass
   styling. Purely visual — the JS hook (app.js binds '.modal-close') is
   untouched, so every close button still works. */
.modal-close:not(.btn-glass){
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* `hit area + focus ring`: measured 32x32, under the 44px floor, and
     ui-craft-rules names the close "✕" as the usual offender. The glyph stays
     15px — it is the TARGET that grows: a 32px content box plus 6px of
     transparent padding is exactly 44x44 border-box, and the -6px margin pulls
     the layout footprint back to 32px so the header's optical edge does not
     move. (padding + content-box rather than min-width: §45's `.icon-only`
     uses min-width, which with content-box makes the border box 52px, not 44 —
     that overshoot is fine on a standalone chip but not in a dialog header.) */
  box-sizing: content-box;
  width: 32px;
  height: 32px;
  padding: 6px;
  margin: -6px;
  background: transparent;
  border: none;
  border-radius: var(--r-1);
  color: var(--text);
  font-family: inherit;
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s ease;
}
.modal-close:not(.btn-glass):hover{ background: var(--hover); }
/* The base ring in §1a covers button:focus-visible, but not every close control
   is a <button> — an <a class="modal-close"> is legitimate here and the anchor
   branch of §1a only covers a:focus-visible generically. Restated against the
   component so the ring is on the element the user actually aimed at.
   :focus-visible only — a bare :focus would ring the ✕ on a mouse click too,
   which is the "focus ring everywhere" noise the technique excludes. */
.modal-close:not(.btn-glass):focus-visible{
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
/* R3-03: light hover twin deleted — var(--hover) carries the polarity. The
   page-copied rules in subscription_detail.php ride the same token now. */
/* R3-02: the light-twin shadow rule is gone — var(--modal-shadow)'s light
   base (0 24px 64px rgba(0,0,0,0.18)) is the value it used to restate. */

/* ───────────────────────── 10. Layout utilities ──────────────────────── */
/* A-E: bare .page-head rules deleted — every admin page uses
   .glass-page-head (the canonical declaration is just below at §10b),
   and the duplicate here + app.css:365 combined kept producing
   inconsistent alignment depending on cascade order. */

.empty-glass{ text-align: center; padding: 56px var(--sp-6); }
.empty-glass .empty-icon { font-size: var(--fs-hero); opacity: 0.6; margin-bottom: 12px; }
.empty-glass h3 { margin: 0 0 6px; font-weight: 600; }
.empty-glass p  { margin: 0; opacity: 0.65; font-size: 0.88rem; }
/* `.sm` — for an empty state that replaces an INLINE block of a card that is
   otherwise populated (the 7-day sales chart, a ledger section) rather than
   standing in for a whole page. The 56px page padding above reads as a hole in
   the middle of a card that still has a title and a summary bar. */
.empty-glass.sm{ padding: 22px var(--sp-4); }
.empty-glass.sm .empty-icon{ font-size: var(--fs-5); margin-bottom: 8px; }
/* G10-assets: the state shipped icon + text and stopped there. Both the
   technique (`empty state`) and CRAFT_TIPS_POORIA §2 name the third part —
   «حتماً یک اکشن واضح» — one action, not a menu of them. `.empty-cta` is that
   slot; `.empty-actions` is the flex row it sits in when a page has a filter
   reset to offer beside the primary (e.g. subscriptions.php). The button
   itself is a stock `.btn-glass`, so this carries spacing and the entrance
   only — no new visual language. */
.empty-glass .empty-cta{ margin-top: 14px; }
.empty-glass .empty-actions{
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin-top: 14px;
}
.empty-glass .empty-actions .empty-cta{ margin-top: 0; }
/* Newly added items arrive with a spring; the empty state is the one surface
   that is ALWAYS newly added (it is the result of a full-page filter
   refresh), so it gets the same treatment as its contents. */
.empty-glass{ animation: empty-pop 280ms var(--ease-out) both; }
@keyframes empty-pop{
  0%   { opacity: 0; transform: scale(0.97) translateY(4px); }
  60%  { opacity: 1; transform: scale(1.005) translateY(0); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}

/* UI-0511: single-column mobile track MUST be minmax(0,1fr), not the implicit
   `auto`. With an `auto` track the column's min size = its min-content size,
   and the non-wrapping subscription URL in .url-copy-glass (white-space:nowrap)
   has a min-content as wide as the full URL (~800px) — blowing the cards far
   past the viewport on phones. The `html,body{overflow-x:hidden}` guard then
   CLIPS the spill instead of scrolling it, so on iPhone 12 Pro Max the page
   looked "zoomed-in / cut off". minmax(0,1fr) lets the track shrink to the
   viewport so the URL scrolls inside its own box (it already has min-width:0
   + overflow-x:auto). Note the containment comes from min-width:0 + this
   track, NOT from text-overflow — that property is paint-only and never
   affects layout, so §7 switching ellipsis→clip cannot regress this.
   The ≥900px two-column rule overrides this and is unaffected. */
.detail-grid{ display: grid; gap: var(--sp-4-5); grid-template-columns: minmax(0, 1fr); }
.detail-grid > * { min-width: 0; }
@media (min-width: 900px) {
  .detail-grid { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); align-items: start; }
}

/* Contrast: `.k` no longer leans on `opacity: 0.6` for its muted state — at
   0.78rem that lands under the AA ratio on both themes. The hairline/ink
   tokens (--text-muted carries polarity, see the badge rules above) do the
   job instead. Same 2-column grid, but `minmax(0, 1fr)` tracks +
   `min-width: 0` on the cells so a long monospace username breaks instead of
   stretching the card; `.v` gets word-break so it does the breaking. */
.info-kv{
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-3-5) 22px;
  font-size: 0.9rem;
}
.info-kv > div { min-width: 0; }
.info-kv .k { color: var(--text-muted); font-size: 0.78rem; margin-bottom: 2px; }
.info-kv .v { font-weight: 600; overflow-wrap: break-word; word-break: break-word; }
@media (max-width: 480px) { .info-kv { grid-template-columns: minmax(0, 1fr); } }

.action-stack{ display: flex; flex-direction: column; gap: 10px; }

.glass-divider{
  height: 1px;
  /* R3-04: the mid-point is the hairline-family token — one owner for both
     polarities (dark 0.12 / light 0.10 replaces the 0.10 / 0.08 split). */
  background: linear-gradient(to left, transparent, var(--hairline-strong), transparent);
  margin: 16px 0;
}

.refund-preview{
  background: rgba(var(--success-rgb), 0.08);
  border: 1px solid rgba(var(--success-rgb), 0.30);
  border-radius: var(--r-2);
  padding: var(--sp-3-5) var(--sp-4);
  margin-bottom: 12px;
  font-size: 0.9rem; line-height: 1.7;
}
/* R3-19: neutral modifier for zero-refund / expired window notices (replaces
   the inline style= with hardcoded slate literals in subscription_detail.php). */
.refund-preview.neutral{
  background: var(--hover);
  border-color: var(--hairline-strong);
}
.refund-preview .small { display: block; margin-top: 6px; font-size: 0.78rem; opacity: 0.65; }

.card-title{
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin: 0 0 14px; font-size: var(--fs-3); font-weight: 700;
}
.card-title .sub-id{ font-family: ui-monospace, monospace; opacity: 0.6; font-weight: 500; }

.wallet-balance{ font-size: 1.6rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.wallet-balance.low{ color: rgb(var(--warn-rgb)); }
.wallet-hint{ font-size: 0.8rem; opacity: 0.70; margin-top: 4px; }

/* `.bcrumb` lived here and `.breadcrumb` in subscription_detail.php's inline
   block: one component, two names, four pages — with a `/` separator on three
   of them and a chevron on the fourth. Both names are gone; the component is
   `.breadcrumb` below, emitted by `render_breadcrumb()` in config.php. */

/* ── §10c Skeleton (term: `skeleton shimmer`) ───────────────────────────
   G10-assets: the system had no loading placeholder at all. The list pages
   that matter here — representatives.php, deposits.php, wallet.php — are
   server-rendered behind `data-submit-once` with a full-page filter refresh,
   so between submit and paint the operator gets a frozen screen with no
   shape to it. A skeleton is the cheap half of that: it reserves the final
   geometry so nothing jumps when the data lands. (CRAFT_TIPS_POORIA §2 —
   «مثل آینه‌ی داخل آسانسور».)
   Token-driven: --surface-2 is the muted/table-header step in both themes,
   so the shimmer needs no light-theme twin. Sized by the consumer via
   --skeleton-h / width, because a skeleton that does not match its content
   is worse than no skeleton. */
.skeleton{
  --skeleton-h: 1em;
  display: block;
  height: var(--skeleton-h);
  border-radius: var(--r-1);
  background: var(--surface-2);
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    rgba(255,255,255,0.07) 50%,
    transparent 100%);
  background-repeat: no-repeat;
  background-size: 200% 100%;
  background-position: 200% 0;
  /* Duration is derived from --dur-3 rather than a magic number, so the
     shimmer moves with the rest of the motion system. It is multiplied because
     --dur-3 (380ms) is a TRANSITION duration — a state change the eye follows.
     A shimmer is an ambient loop, and at 380ms per sweep it strobes; 4x lands
     at 1.5s, which is the loop speed the technique implies. */
  animation: skeleton-shimmer calc(var(--dur-3) * 4) var(--ease) infinite;
  pointer-events: none;
  user-select: none;
}
html[data-theme="light"] .skeleton{
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    rgba(0,0,0,0.05) 50%,
    transparent 100%);
}
/* A skeleton line narrower than its container (the "this is a name, not a
   paragraph" hint). Inline-start so it shortens from the reading edge in RTL. */
.skeleton.w-40{ width: 40%; }
.skeleton.w-60{ width: 60%; }
.skeleton.w-80{ width: 80%; }
.skeleton.lg{ --skeleton-h: 2rem; }
.skeleton.sm{ --skeleton-h: 0.75rem; }

@keyframes skeleton-shimmer{
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* ═══════════════════════════════════════════════════════════════════════
   DASHBOARD EXTENSIONS — added in Phase 3 of the dashboard refactor session
   ═══════════════════════════════════════════════════════════════════════ */

/* ───────────────────────── 11. Glass stat card ───────────────────────── */
.glass-stat-card{
  position: relative;
  display: flex; flex-direction: column; gap: 6px;
  padding: var(--sp-5) 22px;
  background: var(--glass-card-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-2);
  box-shadow: var(--shadow);
  overflow: hidden;
  isolation: isolate;
}

/* AUTO-DECISION: accent strip uses inset-inline-start (NOT inset-inline-end).
   In RTL the inline-start direction is the right edge — i.e. the leading
   edge of the reading direction. The brief's wording said "leading edge
   (right side in RTL)" but specified inset-inline-end, which would put
   the strip on the trailing edge. We honor the stated visual intent. */
.glass-stat-card::before{
  content:"";
  position: absolute;
  inset-block: 12%;
  inset-inline-start: 0;
  width: 3px;
  border-radius: var(--r-full);
  background: var(--stat-accent, transparent);
  /* Phase 5 stage 2: dropped the colored box-shadow glow — no glow on cards.
     The 3px bar stays as a flat status/category accent (color for data). */
}

.glass-stat-card .stat-icon { font-size: 1.6rem; opacity: 0.85; line-height: 1; }
/* G10-assets: KPI values swapped with no transition and — the load-bearing
   half — no tabular figures, so a `number tween` (the JS half, dashboard.php)
   would shift each glyph's width mid-count and the card would visibly breathe.
   tabular-nums is declared on `body` via app.css, but `.stat-val` sets its own
   font-size and weight, and a proportional fallback here is exactly the case
   the technique names ("keep tabular numerals to avoid width shifts"). */
/* Two spellings, one recipe. dashboard.php hand-writes `.stat-val` / `.stat-lbl`;
   the shared `reports_stat_card()` helper in partials/reports_scope.php — used by
   profit.php and every reports tab — emits `.stat-value` / `.stat-label`. Neither
   of the helper's names was styled ANYWHERE, so those tiles' numbers rendered at
   the inherited body size (measured on profit.php: 16px/400) while the identical
   tile on the dashboard rendered at 2rem/800. The two groups are the same
   component and now share one declaration, so they cannot drift again. §5's
   tabular-nums is carried across with it. */
.glass-stat-card .stat-val,
.glass-stat-card .stat-value { font-size: 2rem; font-weight: 800; line-height: 1.1; font-variant-numeric: tabular-nums; }
.glass-stat-card .stat-lbl,
.glass-stat-card .stat-label { font-size: 0.85rem; opacity: 0.7; }
.glass-stat-card .stat-sub,
.glass-stat-card .stat-hint  { font-size: 0.78rem; opacity: 0.55; margin-top: 4px; }
.glass-stat-card a          { color: inherit; text-decoration: none; display: contents; }

/* AUTO-DECISION: 6 variants cover the dashboard's 6 source colors plus a
   neutral default. info ≈ blue/cyan, purple kept distinct from accent. */
.glass-stat-card.accent  { --stat-accent: rgb(var(--accent-rgb));  --stat-accent-soft: rgba(var(--accent-rgb), 0.12); }
.glass-stat-card.success { --stat-accent: rgb(var(--success-rgb)); --stat-accent-soft: rgba(var(--success-rgb), 0.12); }
.glass-stat-card.warn    { --stat-accent: rgb(var(--warn-rgb));    --stat-accent-soft: rgba(var(--warn-rgb), 0.12); }
.glass-stat-card.danger  { --stat-accent: rgb(var(--danger-rgb));  --stat-accent-soft: rgba(var(--danger-rgb), 0.12); }
/* AUTO-DECISION: .info + .purple folded into the single accent family —
   the old sky-blue/violet were part of the "inconsistent colors" issue. */
.glass-stat-card.info    { --stat-accent: rgb(var(--accent-rgb));   --stat-accent-soft: rgba(var(--accent-rgb), 0.12); }
/* CORRECTED 2026-09-22: the AUTO-DECISION above says `.info` and `.purple` were
   both folded into the accent family. `.info` was; `.purple` was not, and kept
   its own rgb(142,160,255) literal — a periwinkle one step off the accent indigo.
   On the dashboard stat grid that put `.purple` and `.accent` side by side as two
   near-identical blues, which is the "inconsistent colors" complaint the fold
   was written to fix, only quieter. Both are the accent family now. */
.glass-stat-card.purple  { --stat-accent: rgb(var(--accent-rgb));   --stat-accent-soft: rgba(var(--accent-rgb), 0.12); }

/* AUTO-DECISION: the .stat-grid container is page-scoped (dashboard/profit/
   reports_reps/vouchers/queue emit it); glass.css restates only the mobile
   gap for visual harmony with .actions-toolbar. */
.stat-grid { gap: var(--sp-3-5); }
/* UI-0023: status-filter tiles wrap an <a> around .glass-stat-card; surface a
   visible keyboard focus ring on the card when its anchor is focused. */
.stat-grid a:focus-visible { outline: none; }
.stat-grid a:focus-visible .glass-stat-card {
  outline: 2px solid rgba(var(--accent-rgb), 0.65);
  outline-offset: 2px;
}
/* UI-0116: mark the active status-filter tile (the link carries
   aria-current="page") so the current filter is visually distinguished. */
.stat-grid a[aria-current="page"] .glass-stat-card {
  outline: 2px solid rgba(var(--accent-rgb), 0.55);
  outline-offset: 2px;
}
@media (max-width: 480px) {
  .glass-stat-card { padding: var(--sp-4-5); }
  .glass-stat-card .stat-val { font-size: 1.7rem; }
}

/* Commit 6 (#8/#28) — SUPERSEDED by the breakpoint unification below.
   This used to hide the five `.lb-extra` leaderboard columns in the 769–1023px
   band, because that band rendered a full desktop table at tablet width. With
   the table→card collapse now extended to ≤1023px (see §"card collapse"), the
   leaderboard is a stacked card there instead, and every field is a labelled
   row — so hiding .lb-extra would DELETE data from the card rather than merely
   compress a column. The rule is therefore removed; the class stays in
   dashboard.php purely as a semantic marker of which columns are secondary. */

/* ───────────────────────── 12. Leaderboard table ─────────────────────── */
/* AUTO-DECISION: scoped to .glass-card descendants so it doesn't bleed
   into the legacy tables in other admin pages still using app.css. */
.glass-card .leaderboard{
  width: 100%;
  border-collapse: collapse;
  background: transparent;
}
.glass-card .leaderboard th{
  text-align: start; /* RTL leading edge (logical: right in RTL, left in LTR) */
  padding: 10px var(--sp-3);
  font-size: var(--fs-1); font-weight: 600;
  color: var(--muted-foreground); opacity: 1;   /* muted meta, token-driven */
  background: var(--muted);                       /* distinct table-header step */
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.glass-card .leaderboard td{
  padding: var(--sp-3);
  /* R3-09: td border steps down to the hairline token. */
  border-bottom: 1px solid var(--hairline);
  font-size: 0.9rem;
}
.glass-card .leaderboard tr:last-child td { border-bottom: none; }
.glass-card .leaderboard tr { transition: background 0.2s ease; }
.glass-card .leaderboard tbody tr:hover { background: var(--hover); }

/* R3-09: the th light twin is gone (var(--border) already owns that side);
   the td light twin merges into the --hairline token. */
/* leaderboard row hover uses the theme-independent --hover token (above) */

.glass-card .leaderboard .lb-rank{
  display: inline-flex;
  align-items: center; justify-content: center;
  width: 28px; height: 28px;
  border-radius: 50%;
  /* R3-09: the rank-4+ base wash was white 0.06 with no light twin — a
     white smear on white cards. The hairline token fixes both polarities. */
  background: var(--hairline);
  font-weight: 700; font-size: 0.85rem;
}
/* AUTO-DECISION: gold/silver/bronze hardcoded RGB rather than tokens —
   medal colors are universally recognized and need to stay consistent
   across themes; tying them to brand tokens would weaken the metaphor.
   gold = warn token, silver = neutral gray, bronze = amber-orange. */
.glass-card .leaderboard tbody tr:nth-child(1) .lb-rank { background: rgba(var(--warn-rgb), 0.25);     color: rgb(252, 211, 77); }
.glass-card .leaderboard tbody tr:nth-child(2) .lb-rank { background: rgba(156, 163, 175, 0.25);      color: rgb(229, 231, 235); }
.glass-card .leaderboard tbody tr:nth-child(3) .lb-rank { background: rgba(217, 119, 6, 0.25);        color: rgb(251, 146, 60); }
/* The three medal inks above are dark-theme values, and the comment's premise
   ("need to stay consistent across themes") does not survive contact with the
   light theme: on the white card they measured 1.10, 1.01 and 1.73 — the
   silver digit was literally invisible — while rank 4 onward, which uses the
   normal text token, sat at 15.49. The top three rows, the ones that matter
   most, were the only unreadable ones.
   Darkened per theme, hues preserved so the gold/silver/bronze metaphor still
   reads: 5.23, 6.16 and 5.58, all past AA. Note these are measured against the
   PILL's own tinted background (rgb(246,221,193) / rgb(230,232,235)), not the
   white card — against white they look ~1.3 stops better than they really are.
   The pill backgrounds are unchanged; only the digit is re-inked. Dark keeps
   the original values. */
html[data-theme="light"] .glass-card .leaderboard tbody tr:nth-child(1) .lb-rank { color: #854d0e; }
html[data-theme="light"] .glass-card .leaderboard tbody tr:nth-child(2) .lb-rank { color: #4b5563; }
html[data-theme="light"] .glass-card .leaderboard tbody tr:nth-child(3) .lb-rank { color: #9a3412; }

.glass-card .leaderboard .lb-num{ font-variant-numeric: tabular-nums; }

/* ───────────────────────── 13. Quick-actions toolbar ─────────────────── */
/* Commit 5 (#26): this was a full-width card holding 3 inline-start buttons, so
   most of its surface was empty — on a 1200px shell the emptiness read as a
   layout bug rather than a toolbar. Padding is trimmed to match the content, and
   from 769px up the surface shrinks to fit its buttons: a deliberate control
   cluster instead of a mostly-blank card. Mobile keeps full width, where the
   buttons wrap and actually use it. */
.actions-toolbar{
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: var(--sp-2) var(--sp-3);
}
@media (min-width: 769px){
  .actions-toolbar{ width: fit-content; max-width: 100%; }
}
.actions-toolbar .btn-glass{
  padding: 10px var(--sp-4);
  font-size: 0.9rem;
  min-height: 40px; /* tighter than the standard 44px since these are secondary actions */
}

/* ───────────────────────── 14. Glass page header ─────────────────────── */
.glass-page-head{
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-3);
  /* Phase 5 stage 3a: un-carded — a compact page header (title/count + primary
     action) separated by a bottom border, not wrapped in a big card. ~72-80px. */
  padding: var(--sp-4) 0;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  margin-bottom: var(--sp-5);
}
.glass-page-head .head-title { font-size: 1.4rem; font-weight: 700; margin: 0; }
.glass-page-head .head-sub   { font-size: 0.9rem; opacity: 0.7; margin-top: 4px; }
.glass-page-head .head-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* Compact variant — used on narrow-wrap pages where the standard height
   would dominate above-the-fold content. */
.glass-page-head.compact { padding: var(--sp-3) 0; margin-bottom: var(--sp-4); }
.glass-page-head.compact .head-title { font-size: 1.1rem; }

/* ═══════════════════════════════════════════════════════════════════════
   PURCHASE FLOW — components specific to admin/buy.php (and any future
   page that mirrors the same numbered-step + stepper + summary pattern).
   ═══════════════════════════════════════════════════════════════════════ */

/* ───────────────────────── 15. Glass info bar ────────────────────────── */
.glass-info-bar{
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4-5);
  background: var(--glass-card-bg);
  /* R3-07: hairline-family border, both polarities. */
  border: 1px solid var(--hairline);
  border-radius: var(--r-2);
  margin-bottom: 16px;
  flex-wrap: wrap;
}
.glass-info-bar .info-cell{ display: flex; align-items: center; gap: var(--sp-2); font-size: 0.88rem; }
.glass-info-bar .info-cell strong{ font-weight: 700; color: var(--text); }
.glass-info-bar .info-cell.faint{ opacity: 0.7; }
/* R3-07: light border twin deleted. */

/* ───────────────────────── 16. Glass alert ───────────────────────────── */
/* Inline page-level message. Composable with existing utility classes
   like .mt-2/.mb-2. Element can stay hidden via inline display:none or
   a co-class (e.g. .buy-vol-warn from app.css) and be toggled by JS. */
.glass-alert{
  position: relative;
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-2);
  border: 1px solid var(--alert-border, rgba(255,255,255,0.10));
  background: var(--alert-bg, var(--glass-card-bg));
  color: var(--text);
  margin-bottom: 12px;
  font-size: 0.9rem;
}
.glass-alert.error,
.glass-alert.danger{
  --alert-bg: rgba(var(--danger-rgb), 0.10);
  --alert-border: rgba(var(--danger-rgb), 0.35);
  color: rgb(var(--danger-text-rgb));
}
.glass-alert.warn{
  --alert-bg: rgba(var(--warn-rgb), 0.10);
  --alert-border: rgba(var(--warn-rgb), 0.35);
  color: rgb(var(--warn-text-rgb));
}
.glass-alert.info{
  --alert-bg: rgba(var(--info-rgb), 0.10);
  --alert-border: rgba(var(--info-rgb), 0.35);
  color: rgb(var(--info-text-rgb));
}
.glass-alert.success{
  --alert-bg: rgba(var(--success-rgb), 0.10);
  --alert-border: rgba(var(--success-rgb), 0.35);
  color: rgb(var(--success-text-rgb));
}
/* R1-01: the light-polarity ink twins are gone — the *-text-rgb tokens carry
   the polarity themselves (per-system values in app.css §2.1). */

/* ───────────────────────── 17. Glass form section ────────────────────── */
.glass-form-section{
  position: relative;
  padding: var(--sp-5) 22px;
  background: var(--form-section-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-3);
  margin-bottom: 14px;
  box-shadow: var(--shadow-lg);
}
.glass-form-section .section-title{
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 14px;
  font-size: 1rem;
  font-weight: 700;
}
/* AUTO-DECISION: numbered cue is a plain inline element rather than a CSS
   counter — counters drift if a section is hidden/reordered, and we want
   the number to be a stable, screen-reader-readable token in the DOM. */
.glass-form-section .section-num{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px; height: 28px;
  border-radius: 50%;
  background: rgba(var(--accent-rgb), 0.15);
  border: 1px solid rgba(var(--accent-rgb), 0.35);
  color: rgb(var(--accent-text-rgb));
  font-weight: 700; font-size: 0.85rem;
}
.glass-form-section .section-title-hint{
  margin-inline-start: auto;
  font-size: 0.78rem;
  font-weight: 400;
  opacity: 0.6;
}
/* R1-03: the light-twin block is gone — --form-section-bg / --border /
   --shadow-lg carry the polarity split; the per-polarity shadows soften the
   dark 0.25 black the fixed pair painted on light cards. */

/* ── Commit 3 (#41): inside a MODAL, steps share the dialog's single surface ──
   The charge modal stacked a bordered+shadowed card per step on top of the
   modal surface (and another for the nested crypto block) — three layers deep,
   off-pattern for the flat shadcn dialog. Inside .glass-modal a step is just a
   heading + spacing, separated from the next by a hairline divider. Scoped to
   modals ONLY, so the standalone buy/wallet page forms keep their cards.
   Light-theme selector is spelled out because html[data-theme="light"]
   .glass-form-section would otherwise out-rank a modal-scoped rule. */
.modal-overlay.glass-modal .glass-form-section,
html[data-theme="light"] .modal-overlay.glass-modal .glass-form-section{
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  padding: var(--sp-4) 0;
  margin-bottom: 0;
}
.modal-overlay.glass-modal .glass-form-section + .glass-form-section{
  border-top: 1px solid var(--border, rgba(255,255,255,0.10));
}
.modal-overlay.glass-modal .glass-form-section:first-of-type{ padding-top: 0; }

/* ───────────────────────── 18. Glass stepper ─────────────────────────── */
.glass-stepper{
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 10px;
  align-items: stretch;
  margin-bottom: 10px;
  width: 100%;
  max-width: 100%;
}
.glass-stepper .stepper-input{
  /* min-width:0 + width:100% lets the grid 1fr track shrink below the
     <input>'s intrinsic ~20ch min-content width — otherwise the stepper
     overflows its container on narrow viewports. */
  min-width: 0;
  width: 100%;
  background: var(--glass-card-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-2);
  padding: var(--sp-3-5) var(--sp-4-5);
  font-size: 1.4rem;
  font-weight: 700;
  text-align: center;
  color: var(--text);
  direction: ltr;
  font-family: inherit;
}
.glass-stepper .stepper-input:focus{
  outline: none;
  border-color: rgba(var(--accent-rgb), 0.5);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.15);
}
/* fixes-merged-02 / Section A: stepper buttons render bare "+" / "−" glyphs
   (no digit). The bare minus specifically reads anemic, so we bump font-size
   noticeably and keep weight at 600 — too heavy a weight makes the glyphs
   look stamped rather than confident. line-height:1 + display:inline-flex
   on .btn-glass already centres the glyph vertically. */
.glass-stepper .btn-glass{
  min-width: 56px;
  font-size: 1.7rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0;
  white-space: nowrap;
}
/* R1-04: light-twin border deleted — --border carries the polarity. */
@media (max-width: 480px){
  .glass-stepper{ gap: 6px; }
  .glass-stepper .stepper-input{ padding: var(--sp-3) 10px; font-size: var(--fs-4); }
  /* fixes-merged-02 / Section A: keep the glyph confident at narrow
     widths — the bare "−" reads anemic if it shrinks too aggressively.
     Merged from the original §18 mobile rule and my own override; the
     original's font-size:1rem was the bug (made the glyph 16px). */
  /* UI-0086: also pin a 44px min-height so the glyph buttons keep a full
     WCAG touch target on very narrow phones (≤360px), not just the width. */
  .glass-stepper .btn-glass{ min-width: 44px; min-height: 44px; padding: 10px var(--sp-3); font-size: 1.55rem; }
}

/* ─────────────── 18b. Quick-step chips (renew / add-volume modal) ───────
   subscription_detail.php + my_subscription_detail.php render the renew form
   as .stepper-wrap > (.stepper-input-row + .stepper-btns > .stepper-btn).
   These are DISTINCT from the §18 .glass-stepper component above (an inline
   3-column +/− stepper). Without these rules the quick-add chips (-1 -0.5
   +0.5 +1 +5 / -7 +7 +30) fall back to raw browser-default buttons — match
   the glass chip look already used by §19 .dur-btn. JS (initSteppers in
   app.js) keys off [data-stepper] / [data-step], NOT these class names, so
   this is purely cosmetic and never alters the renew behaviour. */
.stepper-wrap{
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}
.stepper-input-row{ width: 100%; }
.stepper-btns{
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.stepper-btn{
  flex: 1 1 auto;
  min-width: 52px;
  padding: var(--sp-2) 10px;
  background: var(--glass-card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  color: var(--text);
  font-weight: 600; font-size: 0.85rem;
  font-family: inherit;
  direction: ltr;            /* keep "+5" / "-0.5" glyph order stable in RTL */
  cursor: pointer;
  transition: background 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
}
.stepper-btn:hover{
  background: var(--control-wash);
  border-color: var(--border-strong);
  transform: translateY(-1px);
}
.stepper-btn:active{ transform: translateY(0); }
.stepper-btn:disabled{ opacity: 0.45; cursor: not-allowed; transform: none; }
/* R1-04: light-twin rules deleted — the border/control-wash/border-strong
   tokens carry the polarity split. */
@media (max-width: 480px){
  /* keep a full WCAG touch target on narrow phones */
  .stepper-btn{ min-width: 44px; min-height: 44px; }
}

/* ───────────────────────── 19. Glass duration toggle ─────────────────── */
.glass-duration-toggle{ display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.glass-duration-toggle .dur-btn{
  flex: 1 1 calc(33.333% - 6px);
  min-width: 80px;
  padding: 10px var(--sp-3-5);
  background: var(--glass-card-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-2);
  color: var(--text);
  font-weight: 600; font-size: 0.9rem;
  font-family: inherit;
  cursor: pointer;
  transition: background 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
}
.glass-duration-toggle .dur-btn:hover{
  background: var(--control-wash);
  border-color: var(--border-strong);
  transform: translateY(-1px);
}
.glass-duration-toggle .dur-btn.active{
  background: rgba(var(--accent-rgb), 0.18);
  border-color: rgba(var(--accent-rgb), 0.5);
  color: rgb(var(--accent-text-rgb));
  box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.2), 0 0 20px rgba(var(--accent-rgb), 0.25);
}
/* R2-09: the two non-active light twins are gone — --border / --control-wash /
   --border-strong carry the polarity. The .active restatement stays: it is
   token-correct (accent-rgb) and MUST restate border-color for specificity. */
html[data-theme="light"] .glass-duration-toggle .dur-btn.active{
  background: rgba(var(--accent-rgb), 0.12);
  /* border-color MUST be restated: `html[data-theme="light"] … .dur-btn`
     above is (0,3,1) and outranks `.dur-btn.active` (0,3,0), so without
     this the selected duration keeps the plain 0.06 black edge and loses
     its accent outline in light theme. */
  border-color: rgba(var(--accent-rgb), 0.45);
}

/* ───────────────────────── 20. Glass price line ──────────────────────── */
.glass-price-line{
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  margin-top: 10px;
  border-radius: var(--r-2);
  background: rgba(var(--accent-rgb), 0.06);
  border: 1px solid rgba(var(--accent-rgb), 0.18);
  font-size: 1rem;
  font-weight: 600;
  color: var(--text);
  transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}
.glass-price-line .price-label{ opacity: 0.7; font-weight: 500; font-size: 0.88rem; }
.glass-price-line .price-value{ font-weight: 800; color: rgb(var(--accent-text-rgb)); }
/* AUTO-DECISION: .flash class name preserved as the IIFE's toggle target;
   only the visual effect changes (old: opacity dim; new: accent glow). */
.glass-price-line.flash{
  background: rgba(var(--accent-rgb), 0.16);
  border-color: rgba(var(--accent-rgb), 0.45);
  box-shadow: 0 0 24px rgba(var(--accent-rgb), 0.30);
}

/* ───────────────────────── 21. Glass summary dl ──────────────────────── */
.glass-summary-dl{
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 10px var(--sp-4-5);
  padding: var(--sp-3-5) var(--sp-4);
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: var(--r-2);
  margin-bottom: 14px;
}
.glass-summary-dl dt{ font-size: 0.85rem; opacity: 0.7; align-self: center; }
/* R4-11: pin the live-tweening money figures here, not on the body
   inheritance alone — the same rule the .glass-stat-card .stat-val
   declaration carries (glass.css G10, §5). */
.glass-summary-dl dd{ margin: 0; text-align: end; font-weight: 600; font-size: var(--fs-2); font-variant-numeric: tabular-nums; }
.glass-summary-dl dd.total{ font-size: 1.15rem; font-weight: 800; color: rgb(var(--accent-text-rgb)); }
html[data-theme="light"] .glass-summary-dl{
  background: rgba(0,0,0,0.02);
  border-color: rgba(0,0,0,0.05);
}

/* ───────────────────────── 22. Glass buy CTA ─────────────────────────── */
.btn-glass.cta-buy{
  width: 100%;
  padding: var(--sp-4) var(--sp-6);
  font-size: var(--fs-3);
  font-weight: 800;
  /* Phase 5 stage 2: SOLID primary (was a decorative gradient) — this is the
     ONE main action of the buy page. */
  background: var(--primary);
  color: var(--primary-foreground);
  border-color: transparent;
  /* G10-assets: was 0.3px. The label is «ثبت درخواست» / «پرداخت و ساخت
     اشتراک» — Persian. */
  letter-spacing: 0;
  min-height: 52px;
}
.btn-glass.cta-buy:hover{ filter: brightness(1.06); }
.btn-glass.cta-buy:disabled,
.btn-glass.cta-buy[disabled]{
  /* UI-0065: 0.45 dropped the label below AA, especially in light theme.
     0.6 keeps it clearly "disabled" (no glow, not-allowed cursor, no lift)
     while leaving the text legible. */
  opacity: 0.6;
  cursor: not-allowed;
  transform: none;
}

/* ═══════════════════════════════════════════════════════════════════════
   WALLET / DEPOSIT FLOW — financial-UI components for admin/wallet.php.
   ═══════════════════════════════════════════════════════════════════════ */

/* ───────────────────────── 23. Glass balance card ────────────────────── */
/* Reshaped 2026-09-23 on `vibefarsi.ir/templates/wallet`. What changed and
   why, since three of the four are not cosmetic:

   - `text-align` moves from center to start. The reference card is a
     start-aligned block — a label row, then the figure, then the buttons, all
     flush to the inline-start edge — and centering is what made the old card
     read as a plaque rather than as the top of a column.
   - the label and a HIDE toggle now share one flex row (`.balance-head`). The
     toggle is the reference's, kept because a balance is the one figure on
     this panel that is read in public. It is deliberately NOT persisted: a
     `localStorage` key would be a second entry in the one place CLAUDE.md §10
     allows exactly one (the theme cookie), and the value is still in the DOM
     and in the HTML source either way — this hides a shoulder, not an
     attacker.
   - a brand-coloured wash in the inline-start top corner, which is the
     reference's `radial-gradient(60% 80% at 100% 0%, …)`. In a `dir="rtl"`
     layer `100% 0%` is the top-RIGHT corner, i.e. inline-start — the same
     corner, and it needs no flipping.
   - the hero figure, the reserved/credit chips and `.balance-actions` are
     ours and unchanged apart from the alignment they now inherit. */
.glass-balance-card{
  position: relative;
  text-align: start;
  padding: 22px var(--sp-5) var(--sp-5);
  /* R3-13: the panel surface + border are polarity tokens (§1a) — the
     light-twin below is gone. */
  background: var(--glass-panel);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-3);
  margin-bottom: 18px;
  overflow: hidden;
  isolation: isolate;
}
/* `z-index:-1` inside the card's own stacking context (`isolation: isolate`
   above): behind the content, in front of the card's background. `::after` is
   taken — that is the [data-tilt] glare — so the wash cannot live there. */
.glass-balance-card::before{
  content: "";
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: radial-gradient(60% 80% at 100% 0%, rgba(var(--accent-rgb), 0.22), transparent 70%);
}
.glass-balance-card .balance-head{
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  margin-bottom: 8px;
}
.glass-balance-card .balance-label{
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.88rem; opacity: 0.7; margin-bottom: 0;
}
.glass-balance-card .balance-hide{
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  color: var(--text-d);
  background: transparent;
  border: 1px solid transparent; border-radius: var(--r-full);
  cursor: pointer;
  transition: color 150ms ease, background-color 150ms ease, border-color 150ms ease;
}
.glass-balance-card .balance-hide:hover{
  color: var(--text);
  background: var(--hover);
  border-color: var(--hairline-strong);
}
.glass-balance-card .balance-hide:focus-visible{ outline: 2px solid var(--accent-2); outline-offset: 2px; }
/* Both glyphs ship and CSS picks one, so the swap costs no JS and cannot get
   out of step with aria-pressed — the same attribute drives both. */
.glass-balance-card .balance-hide .bh-alt,
.glass-balance-card .balance-hide[aria-pressed="true"] .bh-main{ display: none; }
.glass-balance-card .balance-hide[aria-pressed="true"] .bh-alt{ display: block; }
/* The mask replaces the figure rather than blurring it: a blur is a second
   render of the digits, and on the light theme it read as a rendering fault.
   Type metrics match .balance-value so the card does not change height when
   the state toggles. */
.glass-balance-card .balance-mask{
  font-size: var(--fs-hero);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: 0;
  color: var(--text-d);
  margin-bottom: 4px;
}
/* [hidden] is the state contract — the JS sets the attribute, never a class —
   and it is re-asserted here for the reason .rep-menu-panel[hidden] gives at
   §"rep menu": the UA rule `[hidden]{display:none}` loses to any author
   `display` declaration, so the pair is pinned by name. */
.glass-balance-card .balance-value[hidden],
.glass-balance-card .balance-mask[hidden]{ display: none; }
/* AUTO-DECISION: direction:ltr + unicode-bidi:isolate around the balance
   number so the ASCII digits render in their natural reading order inside
   the RTL flow — without this, large numbers can flip past surrounding
   punctuation. The label/currency siblings are RTL Persian and stay so. */
.glass-balance-card .balance-value{
  font-size: var(--fs-hero); font-weight: 800;
  /* G10-assets: was -0.5px — the one NEGATIVE tracking in the file. The value
     is a digit run («۵۰۰٬۰۰۰» via number_format + e_num) inside a <bdi>, so
     DESIGN_RULES' Persian rule does not bite; what a negative tracking does
     here is worse than tracking: at --fs-hero it pulls adjacent digit pairs
     into contact, and this is the number the operator checks before approving
     a deposit. Zero. Negative tracking on figures is a display-type idea that
     does not survive being read digit by digit. */
  letter-spacing: 0;
  line-height: 1.1;
  /* R3-14: the money-figure ink is the system's AA-asserted success text
     primitive — the polarity light-twin below is gone. */
  color: rgb(var(--success-text-rgb));
  /* UI-0457: a 24px/0.35 glow in the digits' own color haloed the numerals
     and softened the edges. A tighter, fainter glow keeps the accent without
     blurring readability. */
  text-shadow: 0 0 10px rgba(var(--success-rgb), 0.18);
  /* `direction: ltr; unicode-bidi: isolate` used to sit here. Both were
     dropped in the 2026-09-23 wallet pass, and for the reason the old comment
     gave in reverse: the isolation they were added for is already done twice
     over — `e_num()` emits the figure inside a `<bdi>`, and this line's own
     `body` is `dir="rtl"` with the unit in a sibling span. What the pin ACTUALLY
     did was lay the whole line out LTR, which put «تومان» to the RIGHT of its
     figure while every other money surface on the page — the two lists below,
     `toman_short()`'s scale words, the 20 `.glass-tx-table`s — follows the
     figure in reading order, i.e. to its left. One card, one convention. */
  margin-bottom: 4px;
}
/* The unit of a money figure. Promoted to a base rule 2026-09-22: it was scoped
   to `.glass-balance-card`, but 18 call sites emit it — profit.php, all four
   reports tabs, wallet.php, transfer.php, the dashboard stat grid — and 17 of
   them sit OUTSIDE that card. There the span matched nothing and inherited its
   parent, so on a reports tile «تومان» rendered at the same size and weight as
   the number it belongs to (measured on profit.php: 16px/400, opacity 1, zero
   inline-start margin — i.e. no styling at all).

   TWO SIZES, because the span lives in two type scales. In a table cell or a
   sentence it must match the text around it, so the base rule changes only
   weight and opacity. In a KPI tile or the balance card the number is 2rem, and
   a unit that inherits that is a wall of equal-weight digits, so those contexts
   pin it to a fixed 1rem. The gap is the literal space the call sites emit, not
   a margin: a margin-only gap is invisible to a screen reader, which then
   announces «۱۱۰٬۰۰۰تومان» as one word. */
.balance-currency{
  font-weight: 600; opacity: 0.75;
  white-space: nowrap;
}
.glass-stat-card .balance-currency,
.glass-balance-card .balance-currency{
  font-size: 1rem; opacity: 0.85;
}
.glass-balance-card .balance-reserved{
  margin-top: 14px;
  padding: var(--sp-2) var(--sp-3-5);
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-1);
  background: rgba(var(--warn-rgb), 0.10);
  border: 1px solid rgba(var(--warn-rgb), 0.30);
  border-radius: var(--r-full);
  /* R3-14: warn ink via the per-system text primitive. */
  color: rgb(var(--warn-text-rgb));
}
.glass-balance-card .balance-actions{
  display: flex; justify-content: flex-start; gap: 10px;
  margin-top: 22px;
  flex-wrap: wrap;
}
/* R3-13/14: the surface, status inks and hover hairlines are polarity tokens
   (--glass-panel / *-text-rgb / --hairline-strong), so the four light rules
   here are gone. Two intentional light-twins survive: on a white panel the
   0.22 accent wash reads as a coloured panel (drops to 0.13), and the card
   border steps down one hairline family (--hairline-strong → --hairline). */
html[data-theme="light"] .glass-balance-card{
  border-color: var(--hairline);
}
html[data-theme="light"] .glass-balance-card::before{
  background: radial-gradient(60% 80% at 100% 0%, rgba(var(--accent-rgb), 0.13), transparent 70%);
}

/* H5.3: negative-balance variant + wholesale credit-limit indicator.
   Triggered by `.is-negative` on the balance card (wallet.php sets it
   when Wallet::get()['available'] is < 0). The credit-limit line is
   rendered only when the wallet row carries a non-NULL negative_limit. */
.glass-balance-card.is-negative .balance-value{
  /* R3-14: danger ink via the per-system text primitive (light twin gone). */
  color: rgb(var(--danger-text-rgb));
  /* UI-0457: tighter glow (see positive variant). */
  text-shadow: 0 0 10px rgba(var(--danger-rgb), 0.18);
}
.glass-balance-card .balance-credit-limit{
  margin-top: 10px;
  padding: 6px var(--sp-3);
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.8rem;
  background: var(--fill-recessed);
  border: 1px solid var(--hairline);
  border-radius: var(--r-full);
  opacity: 0.9;
}
.glass-balance-card .balance-credit-limit.breached{
  background: rgba(var(--danger-rgb), 0.12);
  border-color: rgba(var(--danger-rgb), 0.35);
  /* R3-14: breached ink via the danger text primitive (light twin gone). */
  color: rgb(var(--danger-text-rgb));
}
.glass-balance-card .balance-credit-limit .credit-used{
  margin-inline-start: 4px;
  font-weight: 700;
}
/* R3-13/14: light twins deleted — --fill-recessed / --hairline /
   rgb(var(--danger-text-rgb)) carry the polarity. */

/* ───────────────────────── 24. Glass select ──────────────────────────── */
.glass-select{
  width: 100%;
  background-color: var(--glass-card-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-2);
  padding: var(--sp-3) var(--sp-3-5);
  color: var(--text);
  font-size: var(--fs-2);
  font-weight: 500;
  font-family: inherit;
  appearance: none;
  -webkit-appearance: none;
  /* R2-01: currentColor collapses the chevron pair — the ink follows --text per
     polarity, so no light-twin background-image is needed. */
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-opacity='0.6'><polyline points='6 9 12 15 18 9'/></svg>");
  background-repeat: no-repeat;
  background-position: left 14px center; /* RTL: chevron sits on the trailing edge (left) */
  padding-inline-end: 38px;
  cursor: pointer;
}
.glass-select:focus{
  outline: none;
  border-color: rgba(var(--accent-rgb), 0.5);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.15);
}
/* Commit 3 (#19): read-only "scope" field — a value the user cannot change but
   must see. Matches .glass-select's height/padding/border so it lines up beside
   real controls, but deliberately has NO chevron and no pointer cursor, because
   it is not a select. Used for sudo's fixed L1 level on representatives.php. */
.glass-scope-field{
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  box-sizing: border-box;
  min-height: 44px;
  padding: var(--sp-3) var(--sp-3-5);
  /* R2-12: token-driven surface/dashed border — no polarity twins. */
  background-color: var(--surface-2);
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-2);
  color: var(--text);
  font-size: var(--fs-2);
  font-weight: 500;
  cursor: default;
}
.glass-scope-field svg{ opacity: 0.65; flex-shrink: 0; }
/* R2-12: light twin deleted — var(--surface-2) / var(--border-strong) carry it. */

/* Commit 3 (#18): pointer-device hover so a select reads as interactive BEFORE
   click. Deliberately subtler than :focus (border tint + faint wash, no ring)
   and suppressed while focused so the two states never compound. */
@media (pointer: fine){
  .glass-select:hover:not(:disabled):not(:focus){
    border-color: var(--border-strong);
    background-color: var(--hover);
  }
}
/* R2-02: the light-twin border/chevron declarations are gone — var(--border)
   and currentColor carry the polarity themselves. */

/* ───────────────────────── 25. Glass method card ─────────────────────── */
.glass-method-card{
  position: relative;
  padding: var(--sp-4) var(--sp-4-5);
  margin-top: 12px;
  background: var(--glass-card-bg);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--r-2);
}
.glass-method-card .method-title{
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: 0.88rem; font-weight: 700;
  margin-bottom: 12px;
  color: rgb(var(--accent-text-rgb));
}
.glass-method-card .method-meta{
  margin-top: 10px;
  font-size: var(--fs-1); opacity: 0.75;
  display: flex; align-items: center; gap: 6px;
  flex-wrap: wrap;
}
/* AUTO-DECISION: card-method and crypto-method use different border
   accents — bank-card (indigo, our brand accent) vs crypto (amber).
   Visual semantic helps users register WHICH method they picked in
   the moments after clicking, before they've read the label. */
.glass-method-card.card-method{ border-color: rgba(var(--accent-rgb), 0.20); }
.glass-method-card.card-method .method-title{ color: rgb(var(--accent-text-rgb)); }
.glass-method-card.crypto-method{ border-color: rgba(var(--warn-rgb), 0.25); }
.glass-method-card.crypto-method .method-title{ color: rgb(var(--warn-text-rgb)); }

.glass-method-card .card-number{
  font-family: ui-monospace, monospace;
  /* User feedback rev 4: needs to fit cleanly inside a 320-px viewport
     even with the .url-copy-glass padding (10/14) and method-card
     inner padding. Smaller font + tighter letter-spacing keep the
     19-char string ("6104 3378 9992 1010") inside ~230 px so it
     never breaks or overflows on the narrowest mobile target. */
  font-size: 0.9rem;
  font-weight: 700;
  letter-spacing: 1.5px;
  /* Safety — if any narrower context still squeezes us, allow shrink
     before overflow. */
  min-width: 0;
  max-width: 100%;
  /* Overflow/wrapping deliberately NOT set here. This rule and
     `.url-copy-glass > .card-number` have equal specificity (0,2,0), so the
     `overflow: hidden` this used to declare won on source order alone and
     silently shadowed the scroll box ~1100 lines earlier — the card number
     was hard-truncated with no scroll and no way to read its tail. Scroll
     behaviour has exactly one owner: the .url-copy-glass rule. Both call
     sites (admin/wallet.php) are inside .url-copy-glass, so it always
     matches; keep it that way rather than re-adding overflow here. */
  direction: ltr;
  unicode-bidi: isolate;
}
.glass-method-card .crypto-address{
  font-family: ui-monospace, monospace;
  font-size: 0.85rem;
  direction: ltr;
  unicode-bidi: isolate;
  /* Overflow/wrapping owned by `.url-copy-glass > .crypto-address` — see the
     note on .card-number above. Mattered most here: a truncated, unscrollable
     crypto deposit address that admin/wallet.php renders with no copy button
     could not be visually verified before sending funds. */
}
html[data-theme="light"] .glass-method-card{
  border-color: rgba(0,0,0,0.06);
}
/* R2-08: light ink override gone — rgb(var(--warn-text-rgb)) carries the
   polarity (122,78,0 in the light base). */

/* ───────────────────────── 26. Glass amount input ────────────────────── */
.glass-amount-input{
  width: 100%;
  background: var(--glass-card-bg);
  /* R1-06: resting border on the token; the focus state keeps its success ring. */
  border: 1px solid var(--border);
  border-radius: var(--r-1);
  padding: var(--sp-4) var(--sp-5);
  font-size: var(--fs-5);
  font-weight: 700;
  text-align: center;
  color: var(--text);
  font-family: inherit;
  direction: ltr;
  unicode-bidi: isolate;
  /* G10-assets: was 0.5px. This field renders «مثال: 500,000» — Persian prefix,
     ASCII digits. DESIGN_RULES is «letter-spacing: 0 on Persian. Tracking
     breaks glyph joining»; the digits do not need the gap (they are already
     tabular) and the Persian word does not want it. */
  letter-spacing: 0;
}
/* `input mask` + `smart prefix suffix`: the deposit field is `type="text"`
   carrying a raw digit run — the placeholder even reads «مثال: 500,000» — while
   every other amount on the panel is rendered through `toman()` with
   separators. The mask itself is JS (it has to preserve the caret and the raw
   value separately, which is the technique's actual requirement), but the
   LAYOUT it needs is here: an affix slot at the inline-end that the value can
   never overlap, plus the toggle class that reserves room for it.
   `.amount-affix` is deliberately pointer-events:none — a currency unit is a
   label, not a control, and a label that swallows a click on the field beside
   it is the classic affix bug. */
.glass-amount-input.has-affix{ padding-inline-end: 64px; }
.amount-affix{
  position: absolute;
  inset-inline-end: 18px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 0.85rem;
  font-weight: 600;
  opacity: 0.55;
  pointer-events: none;
  user-select: none;
  /* Latin/digit affix ("تومان" is Persian but short and standalone) — the
     isolate keeps it from reordering against the digits beside it. */
  unicode-bidi: isolate;
}
.amount-affix-wrap{ position: relative; display: block; }

.glass-amount-input::placeholder{
  /* A-H: match input font-size so the first keystroke doesn't cause a
     visible vertical reflow in the wallet top-up modal. Hint contrast
     is carried by opacity + font-weight instead of size. */
  font-size: var(--fs-5);
  font-weight: 500;
  opacity: 0.40;
  letter-spacing: 0;
}
.glass-amount-input:focus{
  outline: none;
  border-color: rgba(var(--success-rgb), 0.5);
  box-shadow: 0 0 0 3px rgba(var(--success-rgb), 0.12),
              0 0 30px rgba(var(--success-rgb), 0.20);
}
/* R1-06: light twin deleted — var(--border) carries the polarity. */

/* ───────────────────────── 27. Glass input group ─────────────────────── */
.glass-input-group{ margin-bottom: 12px; }
/* `.field-caption`: see the note at §"filter toolbar". Here it covers the
   caption that sits ABOVE a control which is already implicitly labelled by a
   wrapping <label> (the file-upload dropzones). Two labels on one control is
   its own a11y violation, so the caption gives up its <label> tag and keeps
   only the styling. */
.glass-input-group label,
.glass-input-group .field-caption{
  display: block;
  font-size: 0.85rem; font-weight: 600;
  margin-bottom: 6px;
  opacity: 0.85;
}
.glass-input-group label .faint,
.glass-input-group .field-caption .faint{
  font-weight: 400; opacity: 0.6;
  margin-inline-start: 4px;
}
.glass-input-group input,
.glass-input-group textarea{
  width: 100%;
  background: var(--glass-card-bg);
  /* R1-06: resting border on the token; focus states already own the ring. */
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px var(--sp-3);
  color: var(--text);
  font-size: 0.9rem;
  font-family: inherit;
}
.glass-input-group input:focus,
.glass-input-group textarea:focus{
  outline: none;
  border-color: rgba(var(--accent-rgb), 0.45);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.12);
}
/* R1-06: light twin deleted — var(--border) carries the polarity. */

/* Nested form section — visually subordinate to a primary .glass-form-section. */
.glass-form-section.nested{
  background: var(--fill-recessed);
  padding: var(--sp-3-5) var(--sp-4);
  margin-bottom: 10px;
  border-radius: var(--r-2);
  box-shadow: none;
}

/* ───────────────────────── 28. Glass file upload ─────────────────────── */
/* AUTO-DECISION: <label class="glass-file-upload"> wraps a position:absolute
   invisible <input type="file">. We do NOT use <label for=""> because the
   absolute-inside-label approach keeps the entire drop zone clickable on
   older mobile browsers where label-target click can mis-fire on tap. */
.glass-file-upload{
  position: relative;
  display: flex; align-items: center; gap: var(--sp-3-5);
  padding: var(--sp-4) var(--sp-4-5);
  background: var(--glass-card-bg);
  border: 1px dashed rgba(255,255,255,0.18);
  border-radius: var(--r-1);
  cursor: pointer;
  transition: background 0.2s ease, border-color 0.2s ease;
}
.glass-file-upload:hover{
  background: rgba(255,255,255,0.06);
  border-color: rgba(var(--accent-rgb), 0.40);
}
.glass-file-upload input[type="file"]{
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  opacity: 0;
  cursor: pointer;
}
.glass-file-upload .file-icon{ font-size: 1.6rem; opacity: 0.7; }
.glass-file-upload .file-info{ flex: 1; }
.glass-file-upload .file-prompt{ font-size: 0.92rem; font-weight: 600; margin-bottom: 2px; }
.glass-file-upload .file-hint  { font-size: 0.78rem; opacity: 0.6; }
.glass-file-upload .file-name  { font-size: var(--fs-1); opacity: 0.85; word-break: break-all; margin-top: 2px; }
/* UI-0042: visible drag-over affordance — JS toggles .is-dragging in
   pg_bindDropZone (assets/app.js) but the design system lacked a rule. */
.glass-file-upload.is-dragging{
  border-style: solid;
  border-color: rgba(var(--accent-rgb), 0.60);
  background: rgba(var(--accent-rgb), 0.06);
}
.glass-file-upload.has-file{
  border-style: solid;
  border-color: rgba(var(--success-rgb), 0.40);
  background: rgba(var(--success-rgb), 0.08);
}
.glass-file-upload.has-file .file-icon{
  /* R1-12: the bare mint failed 3:1 on white light cards; the system's own
     AA success ink covers both polarities — light twin deleted. */
  color: rgb(var(--success-text-rgb));
}
html[data-theme="light"] .glass-file-upload{
  border-color: rgba(0,0,0,0.18);
}
html[data-theme="light"] .glass-file-upload:hover{ background: rgba(0,0,0,0.04); }
/* The rule above is (0,2,1) and outranks both .is-dragging and .has-file
   (0,2,0), so in light theme the drop zone kept its plain grey edge: the
   drag-over affordance and the "file attached" confirmation were both
   invisible. Restate the state borders under the light scope. The state
   background fills need no restatement — the light base rule doesn't set
   `background`, so they already survive. */
html[data-theme="light"] .glass-file-upload.is-dragging{
  border-color: rgba(var(--accent-rgb), 0.55);
}
html[data-theme="light"] .glass-file-upload.has-file{
  border-color: rgba(var(--success-rgb), 0.45);
}
/* R1-12: light twin deleted — the base rule's --success-text-rgb flips with the system. */
/* `drag & drop upload`, second half. `.is-dragging` was the dragenter half and
   had no counterpart, so a drag that left the zone (or was abandoned, or
   dropped a file the input rejected) left the accent border lit — the zone
   claimed to be receiving a file when nothing was over it.
   `.is-drag-over` is the state with BOTH edges: added on dragenter, removed on
   dragleave AND on drop AND on dragend, so no exit path can strand it. Its
   declarations are deliberately identical to `.is-dragging` rather than a
   second visual — the pair is one state with a reliable off-switch, not two
   states. Scoped after both theme blocks above so it wins on order in either
   theme. */
.glass-file-upload.is-drag-over{
  border-style: solid;
  border-color: rgba(var(--accent-rgb), 0.60);
  background: rgba(var(--accent-rgb), 0.06);
}
html[data-theme="light"] .glass-file-upload.is-drag-over{
  border-color: rgba(var(--accent-rgb), 0.55);
}

/* `upload progress`: a receipt upload on a slow mobile connection can run for
   tens of seconds, and the zone had no progress surface at all — the user saw
   a filename appear and then nothing, with no way to tell "uploading" from
   "stuck" from "silently failed". The technique asks for filename + a stable
   bar + a percentage, updated from real progress events, ending in a
   persistent success state.
   Width comes from `--upload-pct` (the same CSP-safe custom-property pattern
   `.usage-bar` uses), never a computed inline width. */
.upload-progress{
  position: relative;
  height: 6px;
  margin-top: 10px;
  border-radius: var(--r-full);
  background: var(--surface-2);
  overflow: hidden;
}
.upload-progress > span{
  display: block;
  height: 100%;
  width: var(--upload-pct, 0%);
  max-width: 100%;
  border-radius: var(--r-full);
  background: rgb(var(--accent-rgb));
  transition: width 600ms var(--ease-out);
}
.upload-progress-caption{
  display: block;
  margin-top: 6px;
  font-size: 0.78rem;
  opacity: 0.7;
  font-variant-numeric: tabular-nums;
}
.glass-file-upload.has-file .upload-progress > span{ background: rgb(var(--success-rgb)); }

/* ───────────────────────── 29. Glass textarea ────────────────────────── */
.glass-textarea{
  width: 100%;
  background: var(--glass-card-bg);
  /* R1-06: resting border on the token; focus keeps its accent ring. */
  border: 1px solid var(--border);
  border-radius: var(--r-2);
  padding: var(--sp-3) var(--sp-3-5);
  color: var(--text);
  font-size: 0.9rem;
  font-family: inherit;
  resize: vertical;
  min-height: 64px;
}
.glass-textarea:focus{
  outline: none;
  border-color: rgba(var(--accent-rgb), 0.45);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.12);
}
/* R1-06: light twin deleted — var(--border) carries the polarity. */

/* ───────────────────────── 30. Glass deposit CTA ─────────────────────── */
.btn-glass.cta-deposit{
  width: 100%;
  padding: var(--sp-4) var(--sp-6);
  font-size: var(--fs-3);
  font-weight: 800;
  /* Phase 5 stage 2: SOLID primary (was a decorative success gradient) —
     one main action per view, defined once. */
  background: var(--primary);
  color: var(--primary-foreground);
  border-color: transparent;
  /* G10-assets: was 0.3px. The label is «ثبت درخواست» / «پرداخت و ساخت
     اشتراک» — Persian. */
  letter-spacing: 0;
  min-height: 52px;
}
.btn-glass.cta-deposit:hover{ filter: brightness(1.06); }
.btn-glass.cta-deposit:disabled,
.btn-glass.cta-deposit[disabled]{
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}
/* DELETED 2026-09-23: a light-only `color: rgb(20,83,45)` (green ink) sat on
   this CTA. It existed because the light theme's --primary was blue and the
   label had to be forced; --primary-foreground is now chosen per design system
   and AA-checked, so the override turned paper's ink-dark button into dark
   green text on near-black. One token, all six systems. */

/* ── Commit 3 (#43/#55): disabled primary CTAs use a NEUTRAL token ─────────
   Both CTAs kept `background: var(--primary)` when disabled and only dropped
   opacity, so a disabled button still read as the blue primary in dark theme
   (and, with the light-theme green text above, as a *success* state). A
   disabled control must not look like the page's main action. One shared
   neutral surface, readable label, no glow, not-allowed cursor — identical
   semantics in both themes. Placed after both CTA blocks so it wins on order;
   `.btn-glass.cta-*:disabled` also outranks the generic `.btn-glass:disabled`. */
.btn-glass.cta-buy:disabled,      .btn-glass.cta-buy[disabled],
.btn-glass.cta-buy[aria-disabled="true"],
.btn-glass.cta-deposit:disabled,  .btn-glass.cta-deposit[disabled],
.btn-glass.cta-deposit[aria-disabled="true"]{
  /* R1-05: the two polarity-pinned neutral pairs froze 34 systems into
     claude-dark's card. Dark bg rgba(255,255,255,0.07) IS --control-wash;
     the light 0.06 → 0.05 is one alpha step within the family. Ink →
     --text-d, the designated dim-ink token; border → --border. */
  background: var(--control-wash);
  color: var(--text-d);
  -webkit-text-fill-color: var(--text-d);
  border-color: var(--border);
  opacity: 1;              /* the neutral surface carries the state, not a wash */
  cursor: not-allowed;
  box-shadow: none;
  filter: none;
  transform: none;
}
/* R1-05: light-twin block deleted — the §1a tokens carry the polarity. */

/* ───────────────────────── 31. Glass transaction table ───────────────── */
/* AUTO-DECISION: kept as <table> rather than glass-row stack — ledger data
   is genuinely tabular (date, type, signed amount, balance-after, note),
   and aligning numeric columns for at-a-glance scanning matters here. */
.glass-tx-table{
  width: 100%;
  border-collapse: collapse;
  background: transparent;
}
.glass-tx-table th{
  text-align: start;
  padding: 10px var(--sp-3);
  font-size: 0.78rem; font-weight: 600;
  color: var(--muted-foreground); opacity: 1;
  background: var(--muted);                       /* distinct table-header step */
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.glass-tx-table td{
  padding: var(--sp-3);
  /* R3-10: td border steps down to the hairline token. */
  border-bottom: 1px solid var(--hairline);
  font-size: 0.88rem;
  vertical-align: middle;
}
.glass-tx-table tbody tr{ transition: background 0.2s ease; }
.glass-tx-table tbody tr:hover{ background: var(--hover); }
.glass-tx-table tr:last-child td{ border-bottom: none; }
/* Zebra. Fifty-three tables share this rule and the widest of them (audit,
   transactions, subscriptions) are the ones scanned by eye across a row of
   figures, where the 1px border alone is not enough to keep the eye on one line
   — the alternate row is what does it. Deliberately at 2%: the header already
   carries the tone (`--muted`), and a stronger stripe competes with the hover
   state, which is a different signal on the same background.
   It has to be `tbody tr` and not `tr`: a zebra that counts the header row
   stripes the first data row on every page. */
/* R3-10: zebra merges onto the polarity token — same 0.02 values, one owner. */
.glass-tx-table tbody tr:nth-child(even){ background: var(--zebra-row); }
/* Tabular figures on everything `e_num()` produced. Proportional digits have
   different widths per glyph, so a column of money reads as a ragged edge and
   the eye cannot compare two amounts by their digits' position — which is the
   only reason a table of figures beats a list. Keyed off the `<bdi>` `e_num()`
   emits rather than off a per-cell class: the wrapper is already the marker for
   "this is a formatted number" everywhere in this codebase (§5), and a
   hand-placed `.num` would be missing from most of the 53 tables within a week. */
.glass-tx-table td bdi,
.glass-tx-table th bdi{ font-variant-numeric: tabular-nums; }
/* ponytail: no `position: sticky` on the header, and the reason is mechanical
   rather than aesthetic. `.table-wrap` is `overflow-x: auto`, which computes
   `overflow-y` to `auto` as well and therefore makes the wrapper its own scroll
   container — a sticky `th` sticks to THAT box, which has no maximum height and
   so never scrolls vertically. Making the header follow the page would need a
   max-height on every wrap plus a top offset equal to the sticky nav's height,
   and both are page-layout decisions this pass has no evidence for. Add when a
   table is long enough that losing the header mid-scan is the reported problem. */

/* ── §31c Sort indicator (term: `sort indicator`) ───────────────────────
   G10-assets: no column in either shared table was sortable and no arrow was
   defined anywhere. The backend sort is a separate change; what this opens is
   the path — a real <button> inside the <th> carrying aria-sort on the th, so
   the next person does not have to invent the affordance AND the markup AND
   the a11y contract at once.
   The arrow is ONE glyph rotated 180deg, per the technique: "Rotate the header
   arrow 180deg on sort". Rendering two different glyphs is how the two
   directions drift apart. It is a CSS ::after in the border-box sense only —
   no extra element in the markup, and no emoji (HARD RULE). */
.glass-tx-table th,
.glass-card .leaderboard th{ position: relative; }

.glass-tx-table th button,
.glass-card .leaderboard th button{
  /* A <button> in a <th> inherits none of the th's typography — reset it to
     the cell's own so the label does not change weight or size when a column
     becomes sortable. */
  display: inline-flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 0;
  margin: 0;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-align: inherit;
  cursor: pointer;
}
.glass-tx-table th button::after,
.glass-card .leaderboard th button::after{
  /* Neutral rest state: the arrow is present in the layout from the start so
     becoming sortable never reflows the column, but reads as inactive until
     aria-sort names a direction. */
  content: "";
  flex-shrink: 0;
  width: 0; height: 0;
  border-inline-start: 4px solid transparent;
  border-inline-end: 4px solid transparent;
  border-top: 5px solid currentColor;
  opacity: 0.35;
  transition: transform var(--dur-2) var(--ease), opacity var(--dur-1) var(--ease);
}
.glass-tx-table th[aria-sort="ascending"] button::after,
.glass-card .leaderboard th[aria-sort="ascending"] button::after{
  opacity: 1;
}
.glass-tx-table th[aria-sort="descending"] button::after,
.glass-card .leaderboard th[aria-sort="descending"] button::after{
  opacity: 1;
  transform: rotate(180deg);
}
.glass-tx-table th button:hover::after,
.glass-card .leaderboard th button:hover::after{ opacity: 0.7; }
.glass-tx-table th button:focus-visible,
.glass-card .leaderboard th button:focus-visible{
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: var(--r-1);
}

/* ── §31d Density toggle (term: `density toggle`) ───────────────────────
   G10-assets: row padding was a fixed var(--sp-3). The operators who read
   representatives.php scroll several thousand rows, and on a 390px phone the
   visible row count is the whole game. `.density-compact` on a scope that
   contains the table (JS puts it on <body>) halves the vertical rhythm.
   Padding is transitioned, not the row's box — per the technique the point is
   "animating row padding over 280ms", and transitioning height would force
   layout on every frame for a table with no benefit. */
.density-compact .glass-tx-table td,
.density-compact .glass-tx-table th,
.density-compact .leaderboard td,
.density-compact .leaderboard th{
  transition: padding 280ms var(--ease-out);
}
.density-compact .glass-tx-table td,
.density-compact .leaderboard td{ padding: var(--sp-1) var(--sp-3); }
.density-compact .glass-tx-table th,
.density-compact .leaderboard th{ padding: var(--sp-1) var(--sp-3); }
.density-compact .glass-tx-table tbody tr,
.density-compact .leaderboard tbody tr{ line-height: 1.35; }
/* The card-collapse at ≤1023px is a stacked card, not rows — the padding
   there is the card's own and tightening it only makes the card cramped. */
@media (max-width: 1023px){
  .density-compact .glass-tx-table td,
  .density-compact .leaderboard td{ padding: 4px 0; }
}
/* UI-0047: tabular figures so the amount / balance columns align cleanly
   row-to-row (Vazirmatn renders proportional digits by default). */
.glass-tx-table td[data-label="مبلغ"],
.glass-tx-table td[data-label="موجودی پس از"]{ font-variant-numeric: tabular-nums; }

/* ── §4 shared desktop-table states ───────────────────────────────────────
   Every list table (representatives is the reference) shares: a result count
   (in the page head + filter toolbar), footer pagination, row hover (above),
   keyboard focus, and an empty state via .empty-glass. Because the panel is
   server-rendered — a filter submit is a full-page navigation, and query
   errors surface as the page-level .glass-alert — "loading"/"error" are not
   client-side table phases here; .table-state below is the reusable text
   surface (empty today, error-ready) so any future async table stays
   consistent instead of inventing a one-off. */
/* 3b-2c (a11y): a focused child control's own .btn-glass:focus-visible ring
   (glass.css §"btn-glass") is the SINGLE focus indicator. The row must NOT add a
   second full-width outline (which read as a selection state), and it carries no
   background tint here either — :focus-within's tint equalled :hover (rule above)
   and would misread as a hovered/selected row. The <tr> is not itself focusable
   (no tabindex/role), so no tr:focus-visible is needed for direct row focus. */
.glass-tx-table tbody tr:focus-within{ outline: none; }
.table-state{ text-align: center; padding: 40px var(--sp-4); color: var(--muted-foreground); }
.table-state .table-state-icon{ font-size: var(--fs-hero); opacity: 0.5; margin-bottom: 10px; }
.table-state.is-error{ color: rgb(var(--danger-text-rgb)); }

/* NO `direction: ltr` here — same correction as `.balance-value` (§23) and
   `.tx-amount-line` on the wallet page, and the same argument: the figure
   already arrives inside `e_num()`'s `<bdi>`, which IS the isolation, so the
   pin bought nothing and cost the unit's position. `toman()` emits
   `<bdi>N</bdi> <span class="balance-currency">تومان</span>`, and two call
   sites still nest that whole pair in here — `partials/ledger_view.php:107`
   and `search.php:350`. Pinned LTR, the container lays the unit out to the
   RIGHT of its number, so those two tables read «تومان ۱۱۰٬۰۰۰» while every
   other surface in the panel reads «۱۱۰٬۰۰۰ تومان». `bin/test_polish_c14_*`
   asserts the two wallet consumers stop pinning; this is the third. */
.glass-tx-table .tx-amount-positive,
.leaderboard .tx-amount-positive,
.tx-amount-positive{ color: rgb(var(--success-text-rgb)); font-weight: 700; }
.glass-tx-table .tx-amount-negative,
.leaderboard .tx-amount-negative,
.tx-amount-negative{ color: rgb(var(--danger-text-rgb)); font-weight: 700; }
.glass-tx-table .tx-balance-after  { font-weight: 600; opacity: 0.85; direction: ltr; unicode-bidi: isolate; }
.glass-tx-table .tx-date           { font-size: 0.78rem; opacity: 0.65; direction: ltr; unicode-bidi: isolate; white-space: nowrap; }
/* H5.2: cap note width and force long unbroken strings to wrap so a
   single verbose note can't blow out the column width on desktop or
   inflate one mobile card (the table-to-card-collapse at ≤768px
   inherits these constraints — `display:flex` per cell keeps the
   label aligned). max-width is the desktop ceiling; min-width:0
   lets the cell shrink inside the flex card-collapse row. */
.glass-tx-table .tx-note{
  opacity: 0.75;
  font-size: var(--fs-1);
  max-width: 38ch;
  min-width: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
}
@media (max-width: 768px){
  /* Inside the card-collapse the cell is display:flex with the label
     on the leading edge — give the note plenty of room but still cap
     it so a single 400-char note can't render an unbounded card. */
  .glass-tx-table .tx-note{ max-width: 100%; }
}

html[data-theme="light"] .glass-tx-table th{ border-bottom-color: rgba(0,0,0,0.08); }
html[data-theme="light"] .glass-tx-table td{ border-bottom-color: rgba(0,0,0,0.05); }
/* tx-table row hover uses the theme-independent --hover token (above) */
/* R3-11: signed-amount inks follow the system's own AA-asserted text
   primitives — the light-twin literals (pre-palette residue) are gone. */

/* ───────────────────────── 32. Glass tx badge ────────────────────────── */
/* R2-06: container surface now polarity-aware (--hairline twin exists in the
   1a. token block) and the inks bind to the per-system *-text-rgb tokens;
   the 20 light systems get visible chips on white cards. */
.glass-tx-badge{
  display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: 3px 10px;
  border-radius: var(--r-full);
  font-size: 0.75rem; font-weight: 600;
  background: var(--hairline);
  border: 1px solid var(--tx-badge-border, var(--hairline-strong));
  color: var(--tx-badge-text, var(--text));
}
.glass-tx-badge.deposit    { --tx-badge-border: rgba(var(--success-rgb), 0.5); --tx-badge-text: rgb(var(--success-text-rgb)); }
.glass-tx-badge.purchase   { --tx-badge-border: rgba(var(--warn-rgb), 0.5);    --tx-badge-text: rgb(var(--warn-text-rgb)); }
.glass-tx-badge.refund     { --tx-badge-border: rgba(var(--info-rgb), 0.5);    --tx-badge-text: rgb(var(--info-text-rgb)); }
.glass-tx-badge.adjustment { --tx-badge-border: rgba(var(--neutral-rgb), 0.4); --tx-badge-text: var(--text-muted); }
/* W3R3-S02-R1: migration 0036 extended wallet_transactions.type with these
   three, and wallet.php now lets the badge carry them. Every ENUM member needs
   a rule here or the badge silently renders as the untinted base — the exact
   silent-fallback shape this suite's A1 section exists to stop for .btn-glass. */
.glass-tx-badge.referral   { --tx-badge-border: rgba(var(--accent-rgb), 0.5);  --tx-badge-text: rgb(var(--accent-text-rgb)); }
.glass-tx-badge.commission { --tx-badge-border: rgba(var(--success-rgb), 0.45); --tx-badge-text: rgb(var(--success-text-rgb)); }
.glass-tx-badge.lottery    { --tx-badge-border: rgba(var(--danger-rgb), 0.5);  --tx-badge-text: rgb(var(--danger-text-rgb)); }

/* R2-06: the light-twin inks are gone — the *-text-rgb tokens carry polarity
   and are asserted per-system by bin/test_theme_systems.php. */

/* ───────────────────────── 33. Glass deposit row ─────────────────────── */
.glass-deposit-row{
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-3) var(--sp-4);
  background: var(--glass-card-bg);
  /* R3-08: hairline-family border, both polarities. */
  border: 1px solid var(--hairline);
  border-radius: var(--r-2);
  margin-bottom: 8px;
}
/* The `direction: ltr` this carried was dropped 2026-09-23 (design pass). It
   was pinning the WHOLE line — number AND unit — LTR so that «تومان» laid out
   to the right of its figure, which put the wallet page's two money lists two
   conventions apart: this one right of the number, the transaction list (and
   `toman_short()`'s scale words, and the 20 `.glass-tx-table`s) left of it, in
   reading order. The `unicode-bidi: isolate` was doing nothing on its own —
   every figure here already arrives inside `e_num()`'s `<bdi>`, which is the
   isolation the pin was standing in for. Deposit amounts have no sign, so
   nothing needed the LTR container. */
.glass-deposit-row .dep-amount{
  font-size: 1rem;
  font-weight: 700;
}
.glass-deposit-row .dep-meta{
  font-size: 0.78rem;
  opacity: 0.6;
  margin-top: 2px;
}
.glass-deposit-row .dep-actions{
  display: flex; gap: 6px; align-items: center;
}
/* R3-08: light border twin deleted — var(--hairline) carries the polarity. */

/* ─────────────── 33a. Quick charge + the wallet transaction rows ────────
   Ported 2026-09-23 from `vibefarsi.ir/templates/wallet`: the «شارژ سریع»
   preset pills and the transaction list's shape (direction chip · title+date ·
   signed amount and type badge). Both keep this panel's tokens rather than the
   reference's Tailwind names, per §10's no-new-colour rule.

   The amount chips are `<button>` and NOT a form control on purpose: they fill
   the top-up modal's field, they do not submit anything, so §61's iOS zoom
   lift neither applies nor should. What does apply is the 44px floor, below. */
.quick-charge .quick-amounts{
  display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px;
}
.quick-charge .quick-amount{
  /* NOT a flex container, deliberately. The chip's content is
     `<bdi>N</bdi> <span>تومان</span>` and the gap between them is a literal
     space — a margin would be invisible to a screen reader, which then reads
     «۱۰۰٬۰۰۰تومان» as one word. A flex container DISCARDS whitespace-only text
     nodes, so the moment this became `inline-flex` the space vanished and the
     chip rendered «تومان100,000». A <button> centres its own content; it does
     not need to be told to. */
  min-height: 38px; padding: 6px 16px;
  font-size: 0.9rem; font-weight: 600;
  /* The figures are compared column-wise against the field above, so they hold
     their width as the digit count changes. Same reason the tables do. */
  font-variant-numeric: tabular-nums;
  color: var(--text);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  cursor: pointer;
  transition: background-color 150ms ease, border-color 150ms ease;
}
.quick-charge .quick-amount:hover{
  background: var(--hover);
  border-color: rgba(var(--accent-rgb), 0.35);
}
.quick-charge .quick-amount:focus-visible{ outline: 2px solid var(--accent-2); outline-offset: 2px; }
@media (pointer: coarse){ .quick-charge .quick-amount{ min-height: 44px; } }

.glass-tx-list{
  list-style: none; margin: 0; padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-2);
  overflow: hidden;                 /* clip the first/last row to the radius */
}
.glass-tx-row{
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
}
.glass-tx-row + .glass-tx-row{ border-top: 1px solid var(--border); }
.glass-tx-row .tx-chip{
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  border-radius: var(--r-full);
}
/* Colour is the SECOND signal here, never the only one: the glyph points a
   different way in each state and the amount carries its own sign. */
.glass-tx-row.is-in  .tx-chip{ background: rgba(var(--success-rgb), 0.15); color: rgb(var(--success-text-rgb)); }
.glass-tx-row.is-out .tx-chip{ background: var(--control-wash);           color: var(--text-d); }
.glass-tx-row .tx-main{ flex: 1 1 auto; min-width: 0; }
/* Wraps, and it is the only thing in the row that does. A `nowrap` + ellipsis
   was the first attempt and measurement killed it: at 375px the trailing
   amount column leaves the title ~145px, and a real note («شارژ خودکار سوپر
   ادمین #775736») is ~200px — so every row on the phone read as a truncated
   stub, which is strictly worse than the table this replaced. The trailing
   column is the only fixed-width item, so the title is the one that may grow. */
.glass-tx-row .tx-title{
  margin: 0; font-size: 0.92rem; font-weight: 500;
  overflow-wrap: anywhere;
}
.glass-tx-row .tx-when{
  margin: 2px 0 0;
  font-size: 0.78rem; color: var(--text-d);
}
.glass-tx-row .tx-side{
  flex: 0 0 auto;
  display: flex; flex-direction: column; align-items: flex-end; gap: 4px;
  text-align: end;
}
/* The figure and its unit are one line and never split across two: a wrapped
   «تومان» under its number reads as a second figure. */
.glass-tx-row .tx-side .tx-amount-line{ white-space: nowrap; }
.glass-tx-row .tx-side .tx-amount{ font-size: 0.92rem; font-weight: 700; }
html[data-theme="light"] .glass-tx-list{ border-color: rgba(0,0,0,0.08); }
html[data-theme="light"] .glass-tx-row + .glass-tx-row{ border-top-color: rgba(0,0,0,0.06); }
/* R1-12: the two light twins below are gone — .tx-chip now rides --control-wash
   and --success-text-rgb, both of which carry the polarity. */
.glass-tx-row[hidden]{ display: none; }
.tx-filter-empty{
  margin: 0; padding: 22px 16px; text-align: center;
  font-size: 0.85rem; color: var(--text-d);
}

/* ═══════════════════════════════════════════════════════════════════════
   REPRESENTATIVES MANAGEMENT — tier badges, margin cells, actions menu,
   and the form-row helper used by the create-rep + create-child modals.
   ═══════════════════════════════════════════════════════════════════════ */

/* ───────────────────────── 34. Glass tier badge ──────────────────────── */
/* AUTO-DECISION: indigo / blue / purple = depth gradient. L1 sits closest
   to sudo (brand accent indigo), L2 mid (blue), L3 deepest (purple). The
   colors are subtle but scannable: a column of mixed-tier rows reads as
   a visible depth map at a glance. */
.glass-tier-badge{
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 38px;
  padding: 3px 10px;
  border-radius: var(--r-1);
  font-size: 0.78rem; font-weight: 700;
  background: var(--glass-card-bg);
  border: 1px solid var(--tier-border-color, rgba(255,255,255,0.10));
  color: var(--tier-text-color, var(--text));
  direction: ltr;
  unicode-bidi: isolate;
  letter-spacing: 0.5px;
}
.glass-tier-badge.l1{
  --tier-border-color: rgba(var(--accent-rgb), 0.45);
  --tier-text-color: rgb(var(--accent-text-rgb));
  background: rgba(var(--accent-rgb), 0.10);
}
/* R2-07: the l2/l3 inks are the --info-rgb / --purple-rgb tokens (declared
   polarity-wise in §1a), so the light-twin text overrides below are gone —
   the tokens carry the polarity themselves. */
.glass-tier-badge.l2{
  --tier-border-color: rgba(var(--info-rgb), 0.45);
  --tier-text-color: rgb(var(--info-text-rgb));
  background: rgba(var(--info-rgb), 0.10);
}
.glass-tier-badge.l3{
  --tier-border-color: rgba(var(--purple-rgb), 0.45);
  --tier-text-color: rgb(var(--purple-text-rgb));
  background: rgba(var(--purple-rgb), 0.10);
}

/* ───────────────────────── 35. Glass margin cell ─────────────────────── */
/* AUTO-DECISION: negative margins get a stronger glow than positive ones.
   Healthy margins should fade into the data; anomalies (Q9 inversion
   surface) should grab attention. */
.glass-margin-cell{
  display: inline-flex; align-items: center; gap: var(--sp-1);
  font-weight: 700;
  direction: ltr; unicode-bidi: isolate;
  padding: 2px var(--sp-2);
  border-radius: 6px;
  font-size: 0.88rem;
  transition: box-shadow 0.3s ease;
}
/* R1-12 (margin-cell cluster): the four literal inks were claude-dark greens/reds
   pinned across all 34 systems; the -text-rgb tokens are the per-system AA inks.
   Wash alphas unified to the light-polarity value (0.10/0.12) — 0.08 on a white
   card read as a near-invisible smudge. Light twins deleted. */
.glass-margin-cell.positive{
  color: rgb(var(--success-text-rgb));
  background: rgba(var(--success-rgb), 0.10);
  box-shadow: 0 0 12px rgba(var(--success-rgb), 0.12);
}
.glass-margin-cell.negative{
  color: rgb(var(--danger-text-rgb));
  background: rgba(var(--danger-rgb), 0.12);
  box-shadow: 0 0 14px rgba(var(--danger-rgb), 0.20);
}
/* UI-0250: a break-even margin (0) is neither profit nor loss — render it
   muted instead of reusing the red "negative" treatment. */
.glass-margin-cell.neutral{
  color: var(--text-d);
  background: rgba(127,127,127, 0.10);
}

/* ───────────────────────── 36. Glass actions menu — REMOVED ──────────
   RULE-7 (Phase 6): the .glass-actions-* dropdown rules (~95 lines) were
   removed along with the legacy .actions-* rules in app.css. The
   replacement is the .row-actions / .btn-glass.icon-only flexbox added
   at the bottom of this file. No markup references the dropdown classes
   anymore (verified via grep).
   ────────────────────────────────────────────────────────────────── */

/* ───────────────────────── 37. Glass form row ────────────────────────── */
.glass-form-row{
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-3);
}
@media (max-width: 600px) {
  .glass-form-row { grid-template-columns: 1fr; }
}

/* ───────────────────────── 38. Glass input (standalone) ──────────────── */
/* Same input chrome as .glass-input-group input but usable outside a
   .glass-input-group container — handy in the filters bar where the
   search field doesn't need its own label wrapper. */
.glass-input{
  width: 100%;
  background: var(--glass-card-bg);
  /* R1-06: resting border on the token; focus keeps its accent ring. */
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px var(--sp-3);
  color: var(--text);
  font-size: 0.9rem;
  font-family: inherit;
}
.glass-input:focus{
  outline: none;
  border-color: rgba(var(--accent-rgb), 0.45);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.12);
}
html[data-theme="light"] .glass-input{
  border-color: rgba(0,0,0,0.08);
}

/* ═══════════════════════════════════════════════════════════════════════
   AUTH / LOGIN — centered entrance card + page-level atmosphere.

   AUTO-DECISION: cooperation Option B with app.css's --auth-* token block
   (defined at app.css:52-64 dark / :114-126 light). These classes consume
   --auth-bg, --auth-blob1/2/3, --auth-card, --auth-card-border,
   --auth-card-glow rather than hardcoded RGBs — theme switching continues
   to flow through the existing token toggle.
   ═══════════════════════════════════════════════════════════════════════ */

/* ───────────────────────── 41. Glass auth card ───────────────────────── */
/* AUTO-DECISION: co-classed onto .login-card; uses --auth-card / -border /
   -glow tokens so theme switching flows through. The visual upgrade
   layered on top is the outer accent halo (::after radial glow) — the
   detail that gives an auth surface presence without being loud. */
.glass-auth-card{
  position: relative;
  width: 100%;
  max-width: 440px;
  padding: var(--sp-7) 28px;
  /* R1-13/R3-16: the --auth-* trio is defined on every polarity base, so the
     dark-pinned literal fallbacks were dead; the light theme got 20,22,32. */
  background: var(--auth-card);
  border: 1px solid var(--auth-card-border);
  border-radius: 22px;
  box-shadow: var(--auth-card-glow),
              inset 0 1px 0 rgba(255,255,255,0.08);
  z-index: 1;
}
.glass-auth-card .auth-title{
  font-size: 1.4rem;
  font-weight: 800;
  margin-bottom: 6px;
  text-align: center;
}
.glass-auth-card .auth-subtitle{
  font-size: 0.88rem;
  opacity: 0.65;
  text-align: center;
  margin-bottom: 22px;
}
.glass-auth-card .auth-form > * + *{ margin-top: 14px; }
.glass-auth-card .auth-footer{
  margin-top: 22px;
  padding-top: 16px;
  border-top: 1px solid rgba(255,255,255,0.06);
  text-align: center;
  font-size: var(--fs-1);
  opacity: 0.6;
}
html[data-theme="light"] .glass-auth-card .auth-footer{
  border-top-color: rgba(0,0,0,0.06);
}

/* ───────────────────────── 42. Auth submit CTA ───────────────────────── */
/* AUTO-DECISION: .auth-cta is a smaller-padding variant of .cta-buy/.cta-deposit.
   Login isn't a financial CTA — full cta-buy padding (16px 24px, 52px min-height)
   would overweight the auth surface. 14px 20px / 48px min-height matches the
   visual rhythm of the form's inputs. */
.btn-glass.auth-cta{
  width: 100%;
  padding: var(--sp-3-5) var(--sp-5);
  font-size: 1rem;
  font-weight: 700;
  background: linear-gradient(135deg,
    rgba(var(--accent-rgb), 0.18),
    rgba(var(--accent-rgb), 0.08));
  border-color: rgba(var(--accent-rgb), 0.35);
  min-height: 48px;
  /* G10-assets: was 0.3px. «ورود به حساب» / «ایجاد حساب» — Persian. */
  letter-spacing: 0;
}
.btn-glass.auth-cta:hover{ transform: translateY(-1px); }
.btn-glass.auth-cta:disabled,
.btn-glass.auth-cta[disabled]{
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* ═══════════════════════════════════════════════════════════════════════
   MOBILE BOTTOM NAVIGATION — fixed glass bar visible at ≤ 768px.
   Replaces the legacy .mobile-nav / .mbn-* block in app.css:571-603.

   AUTO-DECISION: height uses the existing --mob-nav-h token (68px). The
   universal body padding rule at app.css:146 already accounts for this
   token; matching it keeps the bottom padding correct without touching
   any page-level markup.

   AUTO-DECISION: body padding strategy is opt-out (mirrors existing
   `body.auth-page { padding-bottom: 0 }` at app.css:150) rather than the
   brief's opt-in `body.has-bottom-nav`. Touching every page that
   includes nav.php (8+ pages) to add a class would be churn for zero
   functional gain — the universal rule already exists.
   ═══════════════════════════════════════════════════════════════════════ */

/* ───────────────────────── 43. Bottom-nav surface ────────────────────── */
.glass-bottom-nav{
  position: fixed;
  inset: auto 0 0 0;
  display: none; /* shown on mobile + tablet (≤1023px) via the media query below */
  z-index: 150;  /* matches legacy .mobile-nav z-index */
  background: var(--glass-nav-bg);
  border-top: 1px solid var(--border);
  box-shadow: 0 -6px 24px rgba(0,0,0,0.18);
  padding-bottom: env(safe-area-inset-bottom, 0);
}

/* AUTO-DECISION: `display: none` outside the breakpoint rather than
   `visibility: hidden` — visibility:hidden still claims layout space at
   the bottom of every desktop page; display:none drops it cleanly.
   BREAKPOINT FIX: raised 768→1023 so the bottom nav stays present across the
   whole tablet range, closing the 769–1023px dead zone (no navigation at all
   under the old 768 ceiling). The contract is gapless: bottom nav ≤1023px,
   the rail ≥1024px — exactly one nav at every width.

   "EXACTLY ONE" WAS FALSE WHEN THIS COMMENT WAS WRITTEN, and that is worth
   recording rather than quietly repairing. The sentence above named "the
   desktop top-nav" as the ≥1024px surface, and at the time there were TWO of
   them: the top bar (`#desktop-nav`) AND the M10 rail, painting together at
   1440 / 1280 / 1024. The claim was true of the BREAKPOINTS and false of the
   DOM — this rule retires the bottom bar above 1023px, but nothing retired the
   top bar, so the count at ≥1024px was 2. M10 stage G deleted the top bar; the
   rail is now the only ≥1024px surface and the sentence is true of both. Guard:
   bin/test_nav_single_surface.php. */
@media (max-width: 1023px) {
  .glass-bottom-nav { display: grid; }
}

html[data-theme="light"] .glass-bottom-nav{
  box-shadow: 0 -6px 24px rgba(30,40,70,0.07);
}

/* AUTO-DECISION: extend the existing universal `body { padding-bottom:
   var(--mob-nav-h) }` (app.css:146) with safe-area-inset on mobile so the
   iOS home-indicator area gets glass background and bottom content gets
   breathing room. Auth pages opt out via the existing `body.auth-page`
   rule at app.css:150.
   BREAKPOINT FIX: raised 768→1023 to match the bottom-nav display rule above,
   so tablet-width content clears the now-visible bottom nav. */
@media (max-width: 1023px) {
  body:not(.auth-page) {
    /* Phase 4 §3: +16px (was +8px) so the last card, its buttons, and any
       open menu clear the fixed bottom nav + iOS home indicator at 390px. */
    padding-bottom: calc(var(--mob-nav-h, 68px) + 16px + env(safe-area-inset-bottom, 0));
  }
}

/* ───────────────────────── 44. Nav items layout ──────────────────────── */
/* AUTO-DECISION: grid-auto-flow:column + grid-auto-columns:1fr distributes
   N items equally without needing to know N. Flex space-around with
   `flex: 1` does the same; grid is one declaration shorter and aligns
   with the rest of the design system's grid usage. */
.glass-bottom-nav .nav-items{
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  height: var(--mob-nav-h, 68px);
}

.glass-nav-item{
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  text-decoration: none;
  color: var(--text-soft);
  font-size: 0.7rem;
  font-weight: 600;
  padding: 6px var(--sp-1);
  transition: color 0.2s ease, transform 0.12s ease, opacity 0.12s ease;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.glass-nav-item:active{
  transform: scale(0.96);
  opacity: 0.85;
}
.glass-nav-item .nav-icon{
  font-size: 22px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  z-index: 1;
}
.glass-nav-item .nav-label{
  font-size: 0.7rem;
  font-weight: 600;
  /* G10-assets: was 0.2px. The labels are «داشبورد / نمایندگان» — Persian. */
  letter-spacing: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 64px;
}
/* Phase 1 item 1: icon-only bottom nav. Inactive tabs = icon only; the ACTIVE
   tab reveals its short label. This is what makes 5 slots survive 320px (only
   ever one label on screen) and keeps the bar Blu-calm. aria-label on each tab
   carries the full name, so hiding the visible label costs no a11y.
   Icon-only is scoped to the bottom nav ONLY (A9). */
.glass-bottom-nav .nav-label{ display: none; }
.glass-bottom-nav .glass-nav-item.active .nav-label{ display: block; max-width: 100%; }

/* Phase 5 stage 3a put the desktop theme toggle in the top bar as
   `.nav-theme-toggle` (40px ghost square). M10 stage G deleted that bar, so the
   class has no markup left anywhere — `grep -rn 'nav-theme-toggle'` over
   .php/.js/.css outside docs/ returns only this comment and the one below.
   The desktop control is now `.nav-rail-ctl` in the rail head, sized by the
   nav.php style block (32px, matching the hamburger beside it); the mobile one
   is still `.nav-more-theme` below. Rules deleted rather than left dead: a
   width rule for a class nobody renders is the kind of thing that reads as
   "this element still exists somewhere". */

/* ══ "بیشتر" overflow — desktop popover + mobile bottom sheet (Phase 5 3b) ══
   One markup (#nav-more-menu), two presentations chosen by initNavMore() at
   open time via .is-desktop / .is-mobile:
     • desktop (≥769px): anchored dropdown popover near the trigger, NO overlay,
       flips up when short on room, viewport-clamped.
     • mobile  (≤768px): compact bottom sheet + ~45% backdrop (no heavy blur),
       focus trap + body scroll lock.
   Rows are plain 48px menu items (icon · label · optional trailing badge). The
   shared modal stack is intentionally NOT reused — it stays a centered dialog
   for its other consumers. */
/* The mode classes are added by JS at open time and are declared LATER in this
   file at the SAME specificity (0,2,0) as `.nav-more[hidden]`, so
   `.nav-more.is-mobile{ display: flex }` was beating `[hidden]{ display:none }`
   on source order. Once the sheet had been opened once on a phone, the hidden
   attribute could no longer hide it: close() correctly set `hidden` and removed
   `.open`, but the sheet stayed on screen at full opacity with its backdrop, so
   the menu simply never closed — tapping the X ran close() to no visible effect.
   Desktop hid the same failure because `.is-desktop` collapses the wrapper to
   0x0 with pointer-events:none.
   Listing the mode classes here makes the rule win on specificity (0,3,0)
   rather than on source order, so re-ordering or a new mode can't resurrect it. */
.nav-more[hidden],
.nav-more[hidden].is-mobile,
.nav-more[hidden].is-desktop{ display: none; }
.nav-more{ position: fixed; inset: 0; z-index: 1200; }
/* desktop: the wrapper must not cover the page — the sheet is a floating
   popover positioned (position:fixed) by JS; the wrapper is inert scaffolding. */
.nav-more.is-desktop{ inset: auto; width: 0; height: 0; pointer-events: none; }
.nav-more.is-desktop .nav-more-backdrop,
.nav-more.is-desktop .nav-more-head{ display: none; }

.nav-more-backdrop{
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.45);   /* ~45%, no heavy blur (RULE-9 direction) */
}

.nav-more-sheet{
  background: var(--popover);      /* opaque elevated surface (shadcn) */
  border: 1px solid var(--border);
  color: var(--text);
  display: flex; flex-direction: column;
}

/* ─ desktop popover shape ─ */
.nav-more.is-desktop .nav-more-sheet{
  position: fixed;
  pointer-events: auto;
  width: 264px; max-width: calc(100vw - 24px);
  border-radius: var(--r-2);
  box-shadow: 0 16px 48px rgba(0,0,0,0.45);
  padding: 6px; gap: 2px;
  max-height: calc(100vh - 24px);
  overflow-y: auto; overscroll-behavior: contain;
}
.nav-more.is-desktop.open .nav-more-sheet{ animation: rep-menu-in 160ms ease-out; }
html[data-theme="light"] .nav-more-sheet{ box-shadow: 0 16px 48px rgba(0,0,0,0.14); }

/* ─ mobile bottom-sheet shape ─ */
.nav-more.is-mobile{ display: flex; align-items: flex-end; }
.nav-more.is-mobile .nav-more-sheet{
  position: relative;
  width: 100%; margin-top: auto;
  border-radius: var(--r-3) var(--r-3) 0 0;
  box-shadow: 0 -12px 40px rgba(0,0,0,0.40);
  padding: 8px var(--sp-2) calc(var(--sp-3) + env(safe-area-inset-bottom, 0));
  gap: 2px;
  /* A-M: 82vh precedes 82dvh — see the .filter-fields.open note above. An
     unbounded sheet is the worst of the two failures: it grows past the top
     edge with its first item still below the fold. */
  max-height: 82vh;
  max-height: 82dvh; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.nav-more.is-mobile.open .nav-more-sheet{ animation: nav-more-sheet-in 260ms cubic-bezier(0.32,0.72,0,1); }
@keyframes nav-more-sheet-in { from { transform: translateY(100%); } to { transform: translateY(0); } }

/* mobile header: grabber (::before) + title + close */
.nav-more-head{
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px var(--sp-3) 8px;
}
.nav-more.is-mobile .nav-more-sheet::before{
  content: ""; position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
  width: 36px; height: 4px; border-radius: var(--r-full); background: var(--border);
}
.nav-more-title{ display: inline-flex; align-items: center; font-weight: 700; font-size: var(--fs-3); }
.nav-more-close{
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0; background: transparent; border: none;
  border-radius: var(--r-1); color: var(--text); cursor: pointer;
}
.nav-more-close:hover{ background: var(--hover); }

/* groups + section labels + separators */
.nav-more-group{ display: flex; flex-direction: column; gap: 2px; }
.nav-more-group + .nav-more-group{
  border-top: 1px solid var(--border); margin-top: 6px; padding-top: 6px;
}
.nav-more-group-title{
  /* G10-assets: was 0.3px. The group titles are Persian («فروش و تدارک»،
     «مالی»، «پشتیبانی») — tracking breaks glyph joining. */
  font-size: 0.8rem; font-weight: 700; letter-spacing: 0; /* Commit 1 (#11): 12px floor */
  color: var(--muted-foreground); padding: 4px var(--sp-3) 2px;
}
.nav-more-logout{ margin: 0; }

/* menu rows — icon · label · trailing badge, 48px min tap target */
.nav-more-item{
  display: flex; align-items: center; gap: 10px;
  width: 100%; min-height: 48px;
  padding: 8px var(--sp-3);
  background: transparent; border: none; border-radius: 10px;
  color: var(--text); font-family: inherit; font-size: 0.92rem; font-weight: 500;
  text-align: start; text-decoration: none; cursor: pointer;
}
.nav-more-item > svg,
.nav-more-item .icon-inline{ flex-shrink: 0; opacity: 0.9; margin-inline-end: 0; }
/* Commit 6 (#20): the count badge used to be pushed to the far edge of the row
   (`margin-inline-start:auto` + a growing label), leaving a measured 199px gap
   between "اعلان" and its "1" — the number read as unrelated to the item. The
   label now hugs its text so the badge sits directly beside it, inside the
   row's normal 10px gap. */
.nav-more-item .nav-more-label{ flex: 0 1 auto; min-width: 0; }
.nav-more-item .nb{ margin-inline-start: 0; flex-shrink: 0; }

/* Commit 6 (#22): auto-approve is a SETTING, not a one-shot action — the row
   showed only a verb, so the current state had to be inferred from which verb
   was displayed. Stable name + explicit on/off token; aria-pressed already
   carried the state for AT and the aria-label still names the action. */
.rep-menu-item .rep-menu-state{
  margin-inline-start: auto;
  flex-shrink: 0;
  font-size: 0.8rem;              /* Commit 1 (#11): 12px floor */
  font-weight: 700;
  line-height: 1.4;
  padding: 1px 8px;
  border-radius: var(--r-full);
  border: 1px solid transparent;
}
.rep-menu-item .rep-menu-state.is-on{
  color: rgb(var(--success-text-rgb));
  border-color: rgba(var(--success-rgb), 0.45);
}
.rep-menu-item .rep-menu-state.is-off{
  color: var(--muted-foreground);
  border-color: var(--border, rgba(255,255,255,0.14));
}
.nav-more-item:hover,
.nav-more-item:focus-visible{ background: var(--hover); outline: none; }
/* R1-12 (nav-more cluster): danger ink on the per-system token; light twin deleted. */
.nav-more-item.danger{ color: rgb(var(--danger-text-rgb)); }
.nav-more-item.danger:hover,
.nav-more-item.danger:focus-visible{ background: rgba(var(--danger-rgb), 0.12); }

/* theme-toggle row: label is ::after because app.js overwrites the button
   innerHTML with the sun/moon icon (a .nav-more-label span would be erased). */
.nav-more-theme::after{ content: "تغییر تم"; }

/* body scroll lock — MOBILE sheet only (desktop popover never locks). */
body.nav-more-lock{ overflow: hidden; }

/* ───────────────────────── 45. Active state ──────────────────────────── */
/* PERF/SIMPLIFY: this state used to be "over-determined" with three
   composing signals (color + radial glow + top pill) -- the radial
   backlight glow behind the icon was removed as a pure decorative
   layer; color + the top-edge pill below remain as the active-state
   indicator, which is already sufficient to be glanceable at arm's
   length. */
.glass-nav-item.active{ color: var(--accent); }
.glass-nav-item.active .nav-icon{ color: var(--accent); }

/* AUTO-DECISION: top-edge pill animates IN on every page load (forwards
   fill mode). The user perceives navigation as a discrete jump from page
   to page; an animation each time reinforces that the new page has
   "received" them. CSS-only — no JS sequencing required. */
.glass-nav-item.active::before{
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  /* `indicator slide`: the pill grows in place with scaleX, which settles it
     but does not TRAVEL — and travel is the whole point of the technique
     ("Slide the active tab indicator between tabs with transform ... instead of
     repainting it in place"). It cannot travel on its own because this nav is
     a server render: the pill is re-created at the new tab, so there is no
     previous position in the DOM to slide from.
     `--nav-pill-shift` is the bridge. A small script reads the previous tab's
     index (sessionStorage), and sets this to the distance in nav cells; the
     pill starts offset by it and eases back to 0, so the eye follows the pill
     ACROSS the bar instead of finding it re-drawn under the new tab. Absent
     the variable the offset is 0 and the rule below is exactly the previous
     entrance — the property is purely additive. */
  --nav-pill-shift: 0px;
  transform: translateX(calc(-50% + var(--nav-pill-shift))) scaleX(0);
  width: 32px;
  height: 3px;
  border-radius: 0 0 4px 4px;
  background: rgb(var(--accent-rgb));
  box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.6);
  animation: nav-pill-in 220ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@keyframes nav-pill-in {
  0%   { transform: translateX(calc(-50% + var(--nav-pill-shift))) scaleX(0);    opacity: 0; }
  60%  { transform: translateX(-50%) scaleX(1.08);                               opacity: 1; }
  100% { transform: translateX(-50%) scaleX(1);                                  opacity: 1; }
}


/* ───────────────────────── 46. Notification count badge ──────────────── */
/* Carried over from legacy .mbn-badge — same red dot with count, just
   re-styled to match the glass aesthetic. */
/* H1.14: anchor the badge with logical positioning so the placement
   flips correctly with locale and never overlaps the icon on narrow
   (sub-72px) nav cells. The cell already establishes position:
   relative; inset-inline-end picks the trailing edge in either
   reading direction. The offset is tuned so the badge hugs the
   icon's upper-trailing corner without crossing the cell center —
   width-agnostic, so it stays readable on 50–96 px cells. */
.glass-nav-badge{
  position: absolute;
  top: 2px;
  inset-inline-end: calc(50% - 18px);
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--r-full);
  background: linear-gradient(180deg,
    rgba(var(--danger-rgb), 0.95),
    rgba(var(--danger-rgb), 0.78));
  color: #fff;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.2px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 0 0 2px rgba(20,22,32,0.85),
              0 2px 6px rgba(var(--danger-rgb), 0.35);
  z-index: 2;
}
html[data-theme="light"] .glass-nav-badge{
  box-shadow: 0 0 0 2px rgba(255,255,255,0.92),
              0 2px 6px rgba(var(--danger-rgb), 0.25);
}

/* ══════════════════════════════════════════════════════════════════
   RULE-7 (Phase 6) — Inline row actions (replaces the ⋯ dropdown).
   ══════════════════════════════════════════════════════════════════
   The .row-actions container flows the per-row action buttons
   horizontally in the same <td> as the row content. Each action is
   a .btn-glass.icon-only (emoji-only, with title=/aria-label=).
   .row-actions-cell is applied to the parent <td> so Rule-8 mobile
   CSS can target it for the card-collapsed layout. */
.row-actions{
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
}
.row-action-form{
  /* Each action that needs to POST is wrapped in its own <form>; this
     keeps the form display inline so it doesn't break the flex row. */
  display: inline-flex;
  margin: 0;
}
.btn-glass.icon-only{
  /* AUTO-DECISION (Phase 6 §8 #11): padding + content-box + min-width/height
     trick keeps the visual size small (28×28 desktop, 36×36 mobile) while
     expanding the actual click target to 44×44 — WCAG mobile touch-target. */
  padding: var(--sp-1);
  box-sizing: content-box;
  width: 28px;
  height: 28px;
  min-width: 44px;
  min-height: 44px;
  font-size: var(--fs-2);
  line-height: 1;
  /* Reset rounded-pill from base .btn-glass — square-ish chip looks
     better when multiple icon buttons sit side-by-side. */
  border-radius: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
@media (max-width: 768px){
  .btn-glass.icon-only{
    width: 36px;
    height: 36px;
    font-size: var(--fs-3);
  }
}
/* UI-0017: on a fine pointer (desktop mouse) the 44px touch-target minimum is
   not required (WCAG 2.5.8 allows 24px for pointer input). Relax the icon
   buttons there so a long action row (up to ~9 icons) stops forcing the rep
   table into horizontal scroll on 1366px laptops. Coarse-pointer / mobile
   keeps the 44px target above. The .row-actions container already wraps. */
@media (pointer: fine) and (min-width: 769px){
  .btn-glass.icon-only{
    min-width: 32px;
    min-height: 32px;
  }
}

/* UI-0036: when the buy page can't be afforded, the pay CTA is disabled and
   demoted to a muted treatment so the (actionable) recharge link reads as the
   primary action instead of two competing primaries. */
.btn-glass.cta-buy.cta-muted{
  background: rgba(255,255,255,0.05);
  border-color: rgba(255,255,255,0.12);
  box-shadow: none;
  opacity: 0.55;
}
html[data-theme="light"] .btn-glass.cta-buy.cta-muted{
  background: rgba(0,0,0,0.04);
  border-color: rgba(0,0,0,0.10);
}

/* ══════════════════════════════════════════════════════════════════
   RULE-8 (Phase 6) — Mobile-first responsive overhaul
   ══════════════════════════════════════════════════════════════════
   Breakpoints (consistent across all CSS additions in this rule):
     ≥ 1024px  desktop      tables full-width
     768–1023  tablet       tables scroll inside .table-wrap (existing)
     < 768     phone        tables card-collapse, modals slide from bottom

   Every <td> in body rows must carry data-label="…" matching its column
   header for the card-collapse to render readable rows. Pages touched in
   this rule:
     admin/representatives.php
     admin/dashboard.php  (leaderboard)
     admin/wallet.php     (transactions list)
     admin/deposits.php
     admin/transactions.php
     admin/transfer.php
     admin/queue.php
     admin/subscriptions.php  (already a vertical row-list — verify)
     admin/my_subscriptions.php
*/

/* ─── Table → card collapse on phone ─────────────────────────────── */
/* Selector list covers every table flavor in admin/:
     .glass-tx-table              — representatives, wallet
     .leaderboard                 — dashboard (sudo + rep variants)
     .table-wrap > table          — deposits, transactions, queue, transfer,
                                    my_subscriptions (bare <table>)
   To opt OUT of card-collapse, add class="no-collapse" to the <table>.
   PHASE5-TBL: each `.tbl-wrap > table…` twin was deleted — zero call sites
   (those pages all wrap in `.table-wrap`), and every deleted line had an
   identical `.table-wrap` sibling in the same list. */
@media (max-width: 1023px){
  .glass-tx-table thead,
  .leaderboard thead,
  .responsive-table-cards thead,
  .table-wrap > table:not(.no-collapse) thead{ display: none; }

  .glass-tx-table,
  .glass-tx-table tbody,
  .glass-tx-table tr,
  .glass-tx-table td,
  .leaderboard,
  .leaderboard tbody,
  .leaderboard tr,
  .leaderboard td,
  .responsive-table-cards,
  .responsive-table-cards tbody,
  .responsive-table-cards tr,
  .responsive-table-cards td,
  .table-wrap > table:not(.no-collapse),
  .table-wrap > table:not(.no-collapse) tbody,
  .table-wrap > table:not(.no-collapse) tr,
  .table-wrap > table:not(.no-collapse) td{
    display: block; width: 100%;
  }

  .glass-tx-table tr,
  .leaderboard tr,
  .responsive-table-cards tr,
  .table-wrap > table:not(.no-collapse) tr{
    /* R3-15: the collapsed-card border + surface are the polarity panel tokens. */
    border: 1px solid var(--hairline-strong);
    border-radius: 14px;
    padding: var(--sp-3);
    margin-bottom: 12px;
    background: var(--glass-panel-sm);
  }

  .glass-tx-table td,
  .leaderboard td,
  .responsive-table-cards td,
  .table-wrap > table:not(.no-collapse) td{
    border: none;
    padding: 6px 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--sp-3);
  }
  .glass-tx-table td::before,
  .leaderboard td::before,
  .responsive-table-cards td::before,
  .table-wrap > table:not(.no-collapse) td::before{
    content: attr(data-label);
    font-size: 0.8rem;
    opacity: 0.65;
    flex-shrink: 0;
  }
  /* .row-actions-cell (Rule 7): sit at the bottom of the stacked card,
     separated by a hairline divider. No label prefix; buttons go full
     width on the trailing edge. */
  .glass-tx-table .row-actions-cell,
  .leaderboard .row-actions-cell{
    padding-top: 10px;
    /* R3-15: 0.06 → the hairline family token. */
    border-top: 1px solid var(--hairline);
    margin-top: 8px;
  }
  .glass-tx-table .row-actions-cell::before,
  .leaderboard .row-actions-cell::before{ display: none; }
  .glass-tx-table .row-actions,
  .leaderboard .row-actions{ width: 100%; justify-content: flex-end; }
  /* R3-15: light bg twin deleted — var(--glass-panel-sm)'s light base is the
     same 0.65 white. */
}

/* ── Stage 3c (deposits) ─────────────────────────────────────────────────
   Inline "جزئیات بیشتر" disclosure inside a .glass-alert. Native <details>
   (no JS — same mechanism as queue.php / representatives.php pricing); the
   summary reads as an inline link and the tip flows after it on expand.
   Minimal, scoped to alerts — not a reusable accordion component.

   OPEN STATE: the tip must read as ONE continuous run — «… نمایش داده میشود.
   جزئیات بیشتر برای مشاهده…» — so the summary stays `display: inline` when open
   too. A text node directly inside <details> (the tip is not wrapped in a block)
   flows inline right after the summary once it is inline, which is why the
   earlier `[open] > summary { display: block }` produced exactly the reported
   symptom: «جزئیات بیشتر» stayed put while the sentence it introduces dropped to
   the next line as a separate paragraph instead of continuing the one above. */
.glass-alert .alert-more{ display: inline; }
.glass-alert .alert-more > summary{
  display: inline; cursor: pointer;
  color: rgb(var(--accent-rgb)); font-weight: 600;
  list-style: none;
  /* A wrapped inline link splits mid-phrase and the second half reads as a
     stray line. `white-space: nowrap` keeps the two words together so the
     break happens BEFORE the link, never inside it. */
  white-space: nowrap;
}
.glass-alert .alert-more > summary::-webkit-details-marker{ display: none; }
/* The marker is `display:none`d above, so the ONLY thing that said "this is a
   disclosure" was the two words — and they did not change. Reported from the
   live panel 2026-09-23: after clicking, the control still read «جزئیات بیشتر»,
   which is a label for an action already taken. The affordance is now two
   things, both of which move: the word swaps to «بستن» and a chevron turns over.

   BOTH labels live in the markup and one is `display:none`, rather than one
   label being swapped by a `content:` rule. A CSS-only swap needs the real text
   hidden (font-size:0 or the like), and text hidden that way is still in the
   accessibility tree — a reader would announce «جزئیات بیشتر بستن» on a control
   that says one thing. `display:none` removes it from the tree, so what is
   announced and what is painted are the same string by construction. */
.glass-alert .alert-more .am-less{ display: none; }
.glass-alert .alert-more[open] .am-more{ display: none; }
.glass-alert .alert-more[open] .am-less{ display: inline; }
/* A border-built chevron: no icon font, no extra request, and it inherits
   `currentColor` so it tracks the summary's accent in both themes for free.
   Points down when closed, up when open — the same square rotated 180°, so any
   optical offset is identical in both states instead of being tuned for one. */
.glass-alert .alert-more .am-chev{
  display: inline-block;
  width: 0.42em; height: 0.42em;
  margin-inline-start: 0.4em;
  border-inline-end: 1.5px solid currentColor;
  border-block-end: 1.5px solid currentColor;
  vertical-align: 0.12em;
  transform: rotate(45deg);
  transition: transform var(--dur-2) var(--ease);
}
.glass-alert .alert-more[open] .am-chev{ transform: rotate(-135deg); }
@media (prefers-reduced-motion: reduce){
  .glass-alert .alert-more .am-chev{ transition: none; }
}

/* Mobile deposit-card hierarchy (≤768 card-collapse). Amount + status are what
   a reviewer scans first → primary; user → secondary; date/# → tertiary. Scoped
   to .dep-table (deposits-only — verified) so no other .glass-tx-table consumer
   is affected. Labels keep their fixed 0.8rem ::before, so these only scale the
   VALUE side. Status/type stay as color badges (already prominent). */
@media (max-width: 1023px){
  .dep-table td[data-label="مبلغ"]{ font-size: 1.05rem; font-weight: 700; }
  .dep-table td[data-label="کاربر"]{ font-weight: 500; }
  .dep-table td[data-label="تاریخ"],
  .dep-table td[data-label="#"]{ font-size: 0.78rem; }
}

/* ─── Modal: bottom-sheet on phone ────────────────────────────────── */
/* fixes-merged-02 / Issue 1: bottom-sheet uses 100dvh to honor the mobile
   URL bar, and pins padding-bottom to safe-area-inset so the iOS home
   indicator doesn't cover the CTA. The flex column inherited from §9
   ensures the body scroll happens INSIDE the box. */
@media (max-width: 768px){
  .glass-modal{ align-items: flex-end; }
  .glass-modal .modal-box{
    /* Commit 3 (#47): the sheet was edge-to-edge (width:100%), so at 315px the
       dialog touched both viewport edges with no breathing room. It now floats
       just above the bottom with a 12px gutter on all three free sides — which
       also means all four corners round, not just the top two. */
    border-radius: var(--r-3);
    /* A-M: 92vh fallback before 92dvh for iOS Safari <15.4. */
    max-height: 92vh;
    max-height: 92dvh;
    margin: auto auto 12px;
    width: calc(100% - 24px);
    max-width: 560px;
    /* H4.12: mirror the bottom safe-area padding on the top edge so
       the iOS URL bar / notch can't overlap the modal header. The
       12px baseline matches the existing modal-header offset on
       phone-sized layouts; safe-area-inset-top stays 0 on browsers
       without the notch, so desktop / Android-without-cutout are
       unaffected. */
    padding-top: max(12px, env(safe-area-inset-top, 12px));
    padding-bottom: max(16px, env(safe-area-inset-bottom, 16px));
  }
}

/* ─── Desktop top header: DELETED 2026-09-21 (M10 stage I) ────────────────
   The `@media (max-width:1023px){ .header{display:none} }` rule that stood here
   — and the long CORRECTED-2026-09-21 comment above it — are gone with the
   element they described. `.header` no longer exists: stage G deleted the
   horizontal top bar it contained (`.nav` / `#desktop-nav`) and left the wrapper
   holding one thing, the `.brand` role chip, so at ≥1024px it painted as a
   full-width white strip carrying a single word. Stage I deletes the wrapper and
   the chip together, on the operator's explicit «حذف شود», and moves the role
   into the rail — see the markup comment in `partials/nav.php`.

   WHY NOTHING REPLACES THIS RULE. It existed to retire the header below the
   desktop breakpoint so that ≤1023px painted the bottom bar ALONE. With no
   `.header` in any page, the ≤1023px range has exactly one nav surface by
   construction — the rail hides itself with its own `.nav-rail{display:none}`
   default in the nav.php style block, which is the same boundary (1024/1023) the
   complementarity check in `bin/test_nav_single_surface.php` now pins on the
   RAIL rather than on this retired container.

   The 1023px boundary itself is unchanged and is still asserted: the bottom bar
   turns on at `max-width:1023px` and the rail at `min-width:1024px`. What is no
   longer asserted is a third selector at the same bound, because there is no
   third element.

   NOTHING ELSE WENT WITH IT. The impersonation banner was emitted before the
   header and is a sibling, so it still shows at every width; the theme toggle
   lives in the rail head (≥1024px) and the «بیشتر» sheet (.nav-more-theme, the
   only one reachable ≤1023px); the desktop logout is in the rail head. All three
   are covered by checks in that same suite. */

/* ─── Sticky CTA: buy + wallet topup submit buttons ─────────────────
   H4.1: bump `bottom` so the CTA clears the bottom-nav (var(--mob-nav-h)
   = 68px) on mobile, and raise z-index above the nav (which sits at
   150). Without this the primary buy/deposit button is hidden under
   the nav strip on phones. */
@media (max-width: 768px){
  .btn-glass.cta-buy,
  .btn-glass.cta-deposit{
    position: sticky;
    bottom: calc(var(--mob-nav-h, 68px) + 12px + env(safe-area-inset-bottom, 0px));
    z-index: 160;
  }
}

/* PHASE5-PAYLOAD: the floating theme-toggle FAB (`.theme-fab`, ~10 rules here
   plus one in app.css §responsive and a `body.modal-open` hide) is deleted.
   nav.php stage 3a removed the element — "the floating theme-FAB is removed"
   (nav.php, near the bottom-nav partial) — and the live controls are
   `.nav-rail-ctl` (desktop, rail head, M10 stage G) / `.nav-more-theme`
   (mobile, «بیشتر» sheet). Zero `theme-fab` tokens in any
   .php/.js/.html. The comment that stood here claimed nav.php rendered it on
   dashboard.php, which had not been true since that stage — an ownership
   claim in a stylesheet is a hint to verify, never an answer. */



/* ═══════════════════════════════════════════════════════════════════════
   §47  REPRESENTATIVES MOBILE CARD — fixes-merged-02 / Issue 2 (Stage 1)
   ═══════════════════════════════════════════════════════════════════════
   Card layout that replaces the table at ≤ 768 px. Desktop ≥ 769 px keeps
   the existing .glass-tx-table. Two CSS hooks the patch adds on the PHP
   side:
     - .glass-rep-card-list   wrapper around the foreach
     - .rep-list-table        added to the existing <div class="table-wrap">
   This file owns the visibility swap; the PHP file owns the markup.
   ═══════════════════════════════════════════════════════════════════════ */

.glass-rep-card-list{ display: none; }
@media (max-width: 1023px){
  .glass-rep-card-list{
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--sp-2);
    padding: var(--sp-1) 0;
  }
  /* Hide the desktop table on phone. The .glass-tx-table card-collapse
     rules from RULE-8 above would otherwise render a SECOND copy of the
     listing alongside the new cards. */
  .glass-card > .table-wrap.rep-list-table{ display: none; }
}

/* ═══════════════════════════════════════════════════════════════════════
   §47a REPRESENTATIVES DESKTOP TABLE — Phase 4 (fixed row height + column
   triage, REDESIGN_PROMPT). Everything here is scoped under `.rep-list-
   table` (the wrapper unique to this page's desktop table — AUDIT
   confirmed no other page uses it) so the shared `.glass-tx-table` base
   styling used by wallet/deposits/queue/etc. is untouched.

   Visible columns stay at the max-6 target (نماینده, سطح, وضعیت, موجودی,
   فروش, عملیات); the rest (#, والد, زیرشاخه, عمده, سود, حاشیه, واریز) are
   marked `.rep-col-extra` in the PHP — kept in the DOM (never removed),
   just CSS-hidden here. They surface per row via `.rep-row-detail`, a
   colspan-spanning row toggled by `.rep-row-expand-btn` (plain inline
   onclick in the PHP — no assets/app.js edit for Phase 4). A genuine
   in-place column reveal isn't used for the expander: unhiding one row's
   `.rep-col-extra` cell while every other row keeps that column hidden
   would desync the table's column model across rows (browsers derive
   column count from ALL rows) — the detail row re-presents the same
   values safely instead. */
.rep-list-table .rep-col-extra{ display: none; }

/* §6: intentional column widths via table-layout:fixed (scoped). The hidden
   .rep-col-extra columns are display:none so they claim no track; the six
   visible columns take the target proportions. Non-sudo drops سطح — the
   browser rescales the remaining widths to fill 100%. */
.rep-list-table table{ table-layout: fixed; width: 100%; }
.rep-list-table .rep-w-name  { width: 30%; }
.rep-list-table .rep-w-tier  { width: 8%; }
.rep-list-table .rep-w-status{ width: 10%; }
.rep-list-table .rep-w-bal   { width: 18%; }
.rep-list-table .rep-w-sales { width: 12%; }
.rep-list-table .rep-w-act   { width: 22%; }

/* §6: row height target 56–60px (min — a 2-line name grows the row a little
   but stays well under the ≤72px guard rail). */
.rep-list-table tbody tr:not(.rep-row-detail){ height: 58px; }
.rep-list-table td, .rep-list-table th{ padding-top: 8px; padding-bottom: 8px; }
/* Phase 5 stage 2: table-header as a distinct muted step (ladder layer). */
.rep-list-table thead th{ background: var(--muted); color: var(--muted-foreground); border-bottom: 1px solid var(--border); }
/* Phase 5 stage 2: the desktop table row action (شارژ / هزینه) is OUTLINE, not
   solid blue — repeated row actions must not compete as solid primaries (the
   mobile card action is already the approved tonal/outline; this matches it).
   Overrides .btn-glass.primary here only (equal specificity, later in source). */
.rep-list-table .rep-action-primary{
  background: transparent;
  border-color: rgba(var(--accent-rgb), 0.45);
  color: var(--text);
}
.rep-list-table .rep-action-primary:hover,
.rep-list-table .rep-action-primary:focus-visible{
  background: var(--hover);
  border-color: rgba(var(--accent-rgb), 0.70);
  filter: none;
}
.rep-list-table td{ overflow: hidden; text-overflow: ellipsis; }

/* §6: sticky header — DEFERRED (Phase 5 candidate, see PHASE_LOG). A sticky
   thead here is inert: this page's global `body{overflow:hidden auto}` (in the
   base stylesheet, not glass.css) makes <body> the sticky scroll-container, but
   <html> is what actually scrolls — so position:sticky detaches for the WHOLE
   page (the existing `.header` desktop nav sticky is equally non-functional
   here, verified on the render). Fixing it means changing the global body/html
   overflow model — out of this phase's 2-file scope. Left un-stuck to avoid a
   half-engaged floating-header artifact; the header stays a clean natural row. */

/* §6: financial columns — tabular figures + no-wrap so the number and its
   trailing ت unit stay on one line, aligned row-to-row. */
.rep-list-table td[data-label="موجودی"],
.rep-list-table td[data-label="فروش"],
.rep-list-table td.rep-col-extra{ font-variant-numeric: tabular-nums; }
.rep-list-table td[data-label="موجودی"],
.rep-list-table td[data-label="فروش"]{ white-space: nowrap; }

/* Row-identity cell: expander toggle + name/username. min-width:0 + ellipsis
   let a long name truncate inside the fixed 30% column instead of overflowing. */
.rep-list-table .rep-row-identity{ min-width: 0; overflow: hidden; }
.rep-list-table .rep-row-name,
.rep-list-table .rep-row-username{ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Phase 5 stage 2: bidi-isolate Latin identifiers (usernames / ids) so an
   @handle or latin_username never reorders against the surrounding RTL row. */
.rep-row-username, .rep-card-username, .glass-row .row-id{ unicode-bidi: isolate; }
/* ROW-HEIGHT FIX: this was `.rep-row-name-cell{ display:flex }` — i.e. flex set
   directly on the <td>. A non-table `display` takes the cell OUT of the table's
   cell layout: the browser wraps it in an anonymous table-cell that stretches to
   the row, while the flex box itself stays content-height and top-aligned. The
   نماینده cell therefore measured 45px inside a 62px row, so
   `.rep-row-active` / hover painted its background over only the top 45px and
   left a 392x17 untinted RECTANGLE under that one column (plainly visible the
   moment a row is expanded), and the name rendered ~8px above the badges in its
   own row. A rep with a logo + display_name has a two-line identity that filled
   the cell (68 of 69px), which is exactly why the branded row looked correct and
   every other row did not. The cell stays a real table-cell; the flex context
   moves to an inner wrapper (see admin/representatives.php). */
.rep-row-name-cell{ vertical-align: middle; }
.rep-row-name-inner{ display: flex; align-items: center; gap: 6px; min-width: 0; }
.rep-row-expand-btn{
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px; height: 28px;
  border-radius: var(--r-2, 8px);
  border: 1px solid rgba(255,255,255,0.12);
  background: rgba(255,255,255,0.04);
  color: inherit;
  cursor: pointer;
  padding: 0;
}
.rep-row-expand-btn:hover,
.rep-row-expand-btn:focus-visible{ background: rgba(255,255,255,0.08); }
.rep-row-expand-btn .icon{ width: 16px; height: 16px; transition: transform 0.15s ease; transform: rotate(-90deg); }
.rep-row-expand-btn.is-open .icon{ transform: rotate(90deg); }
html[data-theme="light"] .rep-row-expand-btn{
  border-color: rgba(0,0,0,0.1);
  background: rgba(0,0,0,0.03);
}
html[data-theme="light"] .rep-row-expand-btn:hover,
html[data-theme="light"] .rep-row-expand-btn:focus-visible{ background: rgba(0,0,0,0.06); }

/* §1/§4: FULL-WIDTH, content-height, accordion detail panel. The PHP colspan
   spans every visible column (the old colspan="<bdi>N</bdi>" was invalid and
   collapsed to 1, stranding content in a right-edge strip). One row opens at a
   time (pgRepExpand); the open row gets .rep-row-active and the panel below it
   shares that tint + a clear accent top separator, so the two read as one
   joined block. Responsive key/value grid: 4 cols desktop → 2 cols tablet.
   Phase 5 stage 2: NEUTRAL surfaces (color only for the thin selection bar).
   Active row = neutral hover; expanded panel = the muted surface step, so it
   reads as a distinct layer without a blue wash. The left accent bar (a thin
   selection/primary marker) stays as the only color. */
.rep-list-table{
  --rep-active:      var(--hover);
  --rep-detail-bg:   var(--muted);
  --rep-detail-line: var(--border);
}
/* active parent row — subtle highlight tying it to the panel below */
.rep-list-table tr.rep-row-active > td{ background: var(--rep-active); }
.rep-row-detail > td{
  /* The inset was on the <dl> alone, so the «جزئیات …» heading — a direct child
     of this zero-padded cell — started at the very cell edge and its first glyph
     was clipped by the 3px inset accent bar (and by `.rep-list-table td
     {overflow:hidden}`). Moving the same inset onto the cell puts the heading on
     the grid's alignment edge; total panel height is unchanged because the <dl>
     gives its padding up in exchange. */
  padding: var(--sp-3) var(--sp-5);
  background: var(--rep-detail-bg);
  border-top: 2px solid var(--rep-detail-line);   /* clear top separator */
  box-shadow: inset -3px 0 0 var(--accent); /* leading accent bar — R1-09: --accent is always defined, the #6366f1 fallback was dead */
}
.rep-row-detail-grid{
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-1) var(--sp-6);
  margin: 0;
  padding: 0;             /* inset now lives on .rep-row-detail > td (above) */
  align-content: start;   /* height follows content, no vertical stretch */
}
@media (max-width: 1023.98px){
  .rep-row-detail-grid{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.rep-row-detail-grid > div, .rep-row-detail-tail > div{
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding: 6px 0;
  border-bottom: 1px solid rgba(255,255,255,0.05);  /* subtle dividers */
}
html[data-theme="light"] .rep-row-detail-grid > div, html[data-theme="light"] .rep-row-detail-tail > div{ border-bottom-color: rgba(0,0,0,0.06); }
/* Phase 4 polish: trailing remainder row (see representatives.php) — spans
   the full grid width and distributes its items evenly via flex, instead of
   the ragged short row a fixed 4-col grid leaves when the count isn't a
   multiple of 4 (non-sudo detail panel: 7 fields, not 8). */
.rep-row-detail-tail{
  grid-column: 1 / -1;
  display: flex;
  gap: var(--sp-1) var(--sp-6);
}
.rep-row-detail-tail > div{ flex: 1 1 0; min-width: 0; }
.rep-row-detail-grid dt{ font-size: 0.75rem; opacity: 0.55; margin: 0; flex-shrink: 0; min-width: 62px; }
.rep-row-detail-grid dd{ margin: 0; font-size: 0.9rem; font-weight: 600; font-variant-numeric: tabular-nums; }

.glass-rep-card{
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: var(--sp-2) var(--sp-3);
  /* Dark theme's own surface (unchanged from pre-Phase-3-review) — clearly
     lighter than --bg (#0f1220) so the card keeps a visible edge. Light
     theme gets an independently-tuned override below; the two are NOT
     shared values, since dark and light need different separation math
     against very different page backgrounds. */
  background: rgba(20,22,32,0.55);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--r-2);
  box-shadow: 0 6px 24px rgba(0,0,0,0.30), inset 0 1px 0 rgba(255,255,255,0.05);
  /* Overflow visible so the rep-menu panel can render outside the card
     when the portal append is NOT loaded. */
  overflow: visible;
}
html[data-theme="light"] .glass-rep-card{
  /* Near-white surface against the light page bg (--bg: #f5f7fb) + a clear
     neutral border — no colored/decorative tint (Phase 3 review). Shadow is
     soft and shallow, neutral navy-gray at low opacity (matches how the
     rest of the system tints shadows), not black or accent-colored. */
  background: rgba(255,255,255,0.92);
  border-color: rgba(20,30,50,0.12);
  box-shadow: 0 4px 14px rgba(20,30,50,0.10);
}

/* Row 1 — identity: avatar + name/username + status/tier chip */
.rep-card-head{
  display: grid;
  /* Phase 4 §8b: 4th `auto` track holds the detail affordance (.rep-card-
     detail-btn) inline — without it the extra child wrapped to a second
     implicit grid row and blew the card past the ≤120px budget. On desktop
     the button is display:none so the track collapses to 0. */
  grid-template-columns: 32px minmax(0, 1fr) auto auto;
  gap: 10px;
  align-items: center;
}
.rep-card-avatar{
  width: 32px; height: 32px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-weight: 800;
  font-size: 0.92rem;
  letter-spacing: 0;
  /* Phase 5 stage 3a: no glow/shadow on avatars; calmer (less saturated) fill. */
  box-shadow: none;
  filter: saturate(0.85);
  flex-shrink: 0;
}
.rep-card-identity{
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.rep-card-name{
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-weight: 700;
  font-size: 0.9rem;
  line-height: 1.25;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Commit 4 (#30): the ID moved out of .rep-card-name into .rep-card-submeta,
   so it's no longer descendant-scoped to the name line. */
.rep-card-id{
  font-family: ui-monospace, monospace;
  opacity: 0.55;
  font-weight: 600;
  font-size: 0.8rem;   /* Commit 1 (#11): 12px mobile-metadata floor */
  flex-shrink: 0;      /* the username ellipsizes first, never the ID */
}
/* Secondary identity line: @username (ellipsizes) + #id (fixed). */
.rep-card-submeta{
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}
.rep-card-username{
  font-family: ui-monospace, monospace;
  font-size: 0.8rem;   /* Commit 1 (#11): 12px mobile-metadata floor */
  color: var(--muted-foreground);       /* Phase 5 stage 2: AA-safe muted meta */
  direction: ltr;
  unicode-bidi: isolate;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Commit 4 (#30): status + level are ONE metadata group, so they sit side by
   side. Stacked in a column they formed a tall narrow rail on the card's far
   edge, which is what made the row read as "spread across two sides" — and it
   set the card's height for no informational gain. */
.rep-card-flags{
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: var(--sp-1);
  flex-shrink: 0;
}
/* Safeguard (not a fix for an observed break): laying the flags out in a ROW
   above turned them into flex siblings competing for width, and a flex item's
   default `flex-shrink: 1` would let a pill be squeezed below its content width
   and break its label across two lines. Pills keep their intrinsic width; if the
   row genuinely runs out of space, `flex-wrap` above moves one to the next line,
   which is the correct degradation. Column-stacked flags could not hit this. */
.rep-card-flags > *{
  flex-shrink: 0;
  white-space: nowrap;
}
.rep-card-flags .badge-glass,
.rep-card-flags .glass-tier-badge{ font-size: 0.8rem; padding: 2px var(--sp-2); } /* Commit 1 (#11): 12px floor */

/* Row 2 — primary metric (balance) + actions */
.rep-card-row2{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
}
.rep-card-balance{
  min-width: 0;
  font-weight: 700;
  font-size: 0.92rem;
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Actions */
.rep-card-actions{
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-shrink: 0;
  /* Visible overflow lets the rep-menu panel hang off the card when
     the portal append isn't loaded. */
  position: relative;
}
.rep-card-actions .btn-glass{ min-height: 44px; padding: var(--sp-2) var(--sp-3-5); font-size: 0.84rem; }
/* هزینه/شارژ repeats on every one of ~25 rows — a solid accent fill at that
   repetition rate reads as a wall of blue and drowns out name/balance/status
   (the actually-important data). Tonal/outlined instead: calm at rest, still
   unambiguously clickable (border + icon+word), a real hover/active step so
   it "feels alive", and a visibly flatter :disabled so a dead state never
   gets mistaken for the calm default. Dark and light are independently
   tuned (not shared values) — see the .glass-rep-card comment above for why. */
.rep-card-actions .rep-action-primary{
  background: rgba(70,90,200,0.14);
  border-color: #5267C9;
  color: #E4E9FF;
}
/* This panel is touch-first — hover barely exists on a phone, :active is the
   feedback users actually feel. :active is deliberately the STRONGEST step,
   stronger than :hover (a desktop nicety), while staying tonal/outlined —
   never a solid fill. */
.rep-card-actions .rep-action-primary:hover,
.rep-card-actions .rep-action-primary:focus-visible{
  background: rgba(70,90,200,0.20);
  border-color: #6478D0;
}
.rep-card-actions .rep-action-primary:active{
  background: rgba(70,90,200,0.38);
  border-color: #8296F0;
}
/* Moderated, not erased: label/icon and border stay readable so a disabled
   row still reads as "a real control, currently off" rather than "no button
   here" — alpha-dimmed, not swapped to a near-invisible gray. */
.rep-card-actions .rep-action-primary:disabled{
  background: rgba(255,255,255,0.07);
  border-color: rgba(255,255,255,0.24);
  color: rgba(228,233,255,0.55);
  cursor: not-allowed;
}
html[data-theme="light"] .rep-card-actions .rep-action-primary{
  background: #F4F6FF;
  border-color: #8FA2FF;
  color: #3550C8;
}
html[data-theme="light"] .rep-card-actions .rep-action-primary:hover,
html[data-theme="light"] .rep-card-actions .rep-action-primary:focus-visible{
  background: #ECF0FF;
  border-color: #7C90F5;
}
html[data-theme="light"] .rep-card-actions .rep-action-primary:active{
  background: #D2DBFF;
  border-color: #5267C9;
}
html[data-theme="light"] .rep-card-actions .rep-action-primary:disabled{
  background: rgba(20,30,50,0.06);
  border-color: rgba(20,30,50,0.22);
  color: rgba(53,80,200,0.55);
  cursor: not-allowed;
}
.rep-action-glyph{ font-size: 1rem; line-height: 1; }

/* Overflow trigger sits at the trailing edge of the actions row. */
.rep-card-actions .rep-menu{
  flex-shrink: 0;
}

/* Overflow panel — when the portal append is loaded, it lives in <body>;
   when it's NOT, it lives inside the card. Both code paths share these
   visual rules via .rep-menu-panel. */
.rep-menu-panel{
  position: absolute;
  z-index: 50;
  min-width: 200px;
  margin-top: 8px;
  inset-inline-end: 0;
  background: var(--popover);           /* opaque elevated surface (shadcn) */
  border: 1px solid var(--border);
  border-radius: var(--r-2);            /* 12px — menu radius */
  box-shadow: 0 16px 48px rgba(0,0,0,0.45);  /* shadow OK: this is a dropdown */
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* A CEILING, and it is load-bearing rather than tidy. The panel is 14 rows
     across three groups (`دسترسی سریع` / `تنظیمات` / `مدیریت حساب`) and every
     `.rep-menu-item` is `min-height:44px`, so a sudo who sees all three groups
     gets ~760px of list. Nothing clipped it: the panel simply ran past the
     bottom edge, and because a menu cannot scroll the page behind it, the rows
     below the fold were unreachable — `غیرفعالسازی تأیید خودکار`, `تعلیق`,
     `حذف` among them. `overflow-y:auto` is what makes the ceiling usable; a
     max-height without it clips instead of scrolls, which is the same defect
     with a tidier edge.

     `100vh` precedes `100dvh` in the same rule on purpose (see
     bin/test_phone_hardening.php §3): a browser that does not know `dvh` drops
     the whole declaration, and a dropped one here means no ceiling at all.
     JS narrows this further to the space actually left below the trigger —
     this rule is the floor, not the answer. */
  max-height: calc(100vh - 24px);
  max-height: calc(100dvh - 24px);
  overflow-y: auto;
  /* The wheel over the list must scroll the LIST. Without this it chains to the
     page underneath and the panel slides away from its trigger while the pointer
     is inside it, which reads as the menu breaking. */
  overscroll-behavior: contain;
}
/* The HTML [hidden] attribute is the closed-state contract: the panel
   ships hidden and app.js's RepMenu toggles it (removeAttribute on open,
   setAttribute on close). But [hidden] only hides via the UA rule
   [hidden]{display:none}, which any author `display` declaration beats
   regardless of specificity — so `.rep-menu-panel{display:flex}` above
   was leaving every panel visible on load (piled under the cards) and
   defeating the JS close path. Re-assert the closed state at higher
   specificity (class + attribute) so it wins over the base rule and
   `body > .rep-menu-panel` alike. */
.rep-menu-panel[hidden]{ display: none; }
/* When portaled to <body>, the JS controls top/left in px using
   viewport-relative coords from getBoundingClientRect(). Switch to
   position:fixed so those coords land near the trigger instead of being
   interpreted document-relative (which placed the panel near the top
   of the page once the user scrolled). */
body > .rep-menu-panel{
  position: fixed;
  inset-inline-end: auto;
  margin-top: 0;
}
html[data-theme="light"] .rep-menu-panel{
  /* bg + border now come from --popover / --border; only the shadow is lighter */
  box-shadow: 0 16px 48px rgba(0,0,0,0.14);
}

.rep-menu-form{ margin: 0; }
.rep-menu-item{
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;     /* WCAG 2.5.5: 44 px touch target */
  padding: 10px var(--sp-3);
  background: transparent;
  border: none;
  border-radius: 10px;
  color: var(--text);
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 500;
  text-align: start;
  text-decoration: none;
  cursor: pointer;
}
.rep-menu-item[hidden]{ display: none; }
.rep-menu-group[hidden]{ display: none; }
.rep-menu-item:hover,
.rep-menu-item:focus-visible{
  background: rgba(255,255,255,0.06);
  outline: none;
}
html[data-theme="light"] .rep-menu-item:hover,
html[data-theme="light"] .rep-menu-item:focus-visible{
  background: rgba(0,0,0,0.04);
}
.rep-menu-glyph{ font-size: var(--fs-3); line-height: 1; opacity: 0.95; flex-shrink: 0; }
/* R1-12 (rep-menu cluster): danger ink on the per-system token; light ink twins deleted. */
.rep-menu-item.danger{
  color: rgb(var(--danger-text-rgb));
}
.rep-menu-item.danger:hover,
.rep-menu-item.danger:focus-visible{
  background: rgba(var(--danger-rgb), 0.12);
  color: rgb(var(--danger-text-rgb));
}
html[data-theme="light"] .rep-menu-item.danger:hover,
html[data-theme="light"] .rep-menu-item.danger:focus-visible{
  /* `html[data-theme="light"] .rep-menu-item:hover` is (0,3,1) and beats
     `.rep-menu-item.danger:hover` (0,3,0), so hovering a destructive action
     in light theme gave the same neutral grey wash as a benign one — the
     red warning tint never appeared. The wash alpha stays per-polarity. */
  background: rgba(var(--danger-rgb), 0.10);
}

/* Open-state polish — fade the panel in. The native <details> toggle
   handles the visibility; we just animate the entry. */
details.rep-menu[open] > .rep-menu-panel,
body > .rep-menu-panel{
  animation: rep-menu-in 160ms ease-out;
}
@keyframes rep-menu-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Stacking-context fix — each .glass-rep-card establishes its own
   stacking context (backdrop-filter), so a menu panel inside card N
   would otherwise be painted UNDER card N+1 regardless of the panel's
   own z-index. Lift the whole card that holds the open menu above its
   siblings. Pair :has() with a relative + isolation baseline so we
   never depend on source order. */
.glass-rep-card{ isolation: isolate; }
.glass-rep-card:has(details.rep-menu[open]){ z-index: 60; }
/* Bring the actions row of the open card above the next card too —
   the trigger sits in this row and the panel anchors off it. */
.glass-rep-card:has(details.rep-menu[open]) .rep-card-actions{ z-index: 2; }

/* ═══════════════════════════════════════════════════════════════════════
   §48  5-STEP PROGRESS BAR — fixes-merged-02 / Issue 3b
   ═══════════════════════════════════════════════════════════════════════
   Used by admin/buy.php. Five .step-seg pills inside .glass-step-progress
   (username → volume → duration → share-count → confirm), each transitioning
   between default / .active / .done states. The CSS is segment-count
   agnostic — flex:1 segments split whatever width the bar holds. The bar is
   purely visual — it tracks progress, it doesn't gate submission. */

.glass-step-progress{
  display: flex;
  gap: 6px;
  padding: var(--sp-2) 10px;
  margin-bottom: 14px;
  background: var(--glass-card-bg);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: var(--r-2);
}
html[data-theme="light"] .glass-step-progress{
  border-color: rgba(0,0,0,0.05);
}
/* fixes-merged-02 / Section C: three states must be VISUALLY DISTINCT.
   The previous revision blended active (purple→green gradient) into done
   (green) — they read as the same colour. Fix: pure tokens.
     - default  low-contrast grey, ~30-40% opacity
     - done     saturated --success green, full opacity
     - active   pure --accent purple, full opacity + inner glow
   A side-by-side done+active row must read as two different colours. */
/* Commit 3 (#52): each segment is now a labelled step — the coloured bar moved
   to ::before and the segment carries its number underneath, so the colours are
   decodable against the numbered section headings. The bar's own geometry and
   the done/active colours are unchanged; only the element carrying them moved. */
.glass-step-progress .step-seg{
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  background: none;          /* the bar is ::before now */
}
.glass-step-progress .step-seg::before{
  content: '';
  display: block;
  width: 100%;
  height: 7px;
  border-radius: var(--r-full);
  background: rgba(255,255,255,0.10);
  transition:
    background 320ms cubic-bezier(0.4, 0, 0.2, 1),
    box-shadow 320ms cubic-bezier(0.4, 0, 0.2, 1);
}
html[data-theme="light"] .glass-step-progress .step-seg::before{
  /* Slightly deeper than the dark-theme track: on a white card an 0.10 black
     wash is nearly invisible, so the "not yet reached" steps read as empty. */
  background: rgba(0,0,0,0.13);
}
.glass-step-progress .step-seg.done::before{
  background: rgb(var(--success-rgb));
  box-shadow: 0 0 10px rgba(var(--success-rgb), 0.55);
}
.glass-step-progress .step-seg.active::before{
  /* Pure accent purple (NOT a gradient that bleeds toward green). */
  background: rgb(var(--accent-rgb));
  box-shadow:
    0 0 14px rgba(var(--accent-rgb), 0.70),
    inset 0 0 6px rgba(255,255,255,0.22);
}
/* Light theme — the state fills must be re-declared here, NOT just tuned.
   `html[data-theme="light"] … .step-seg::before` above is (0,3,2) and so
   OUTRANKS `.step-seg.done::before` (0,3,1): without these rules the light
   theme paints every segment with the neutral track and only the blurred
   glow carries colour, which is why done/active read as pale grey smudges.
   Colour choices differ from dark on purpose:
     - fills are the light tokens (5,150,105 / 79,107,255) — both are dark
       enough to hold contrast against the white card, unlike the brighter
       dark-theme tokens;
     - glows are tightened (smaller radius, ~half the alpha). A wide bright
       halo disappears into a light background and just smears the pill edge,
       where on the dark card it reads as a clean emissive glow;
     - the active pill's white inset glow is dropped — on light it bleaches
       the centre of the bar and flattens the accent. */
html[data-theme="light"] .glass-step-progress .step-seg.done::before{
  background: rgb(var(--success-rgb));
  box-shadow: 0 1px 4px rgba(var(--success-rgb), 0.30);
}
html[data-theme="light"] .glass-step-progress .step-seg.active::before{
  background: rgb(var(--accent-rgb));
  box-shadow: 0 1px 6px rgba(var(--accent-rgb), 0.38);
}
.glass-step-progress .step-seg-num{
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1;
  /* 0.40 rendered the not-yet-reached step numbers at 3.61:1 — under AA and
     genuinely hard to read at 12px. 0.55 clears it (5.64:1) while staying
     clearly recessed against the done/active numbers, which sit at 1.0. */
  opacity: 0.55;
  transition: opacity 220ms ease, color 220ms ease;
}
/* Light needs a higher floor than dark for the SAME perceived recession: the
   light ink rgb(29,36,53) on a white card only reaches 3.62:1 at 0.55, where
   the dark ink hits 5.64:1. 0.65 clears AA (4.91:1) and still reads as muted
   next to the done/active numbers at full opacity. */
html[data-theme="light"] .glass-step-progress .step-seg-num{ opacity: 0.65; }
.glass-step-progress .step-seg.done .step-seg-num{ opacity: 1; color: rgb(var(--success-rgb)); }
.glass-step-progress .step-seg.active .step-seg-num{ opacity: 1; color: rgb(var(--accent-rgb)); }
/* Light theme re-inks these from the *-rgb fill tints to the *-text-rgb inks.
   --success-rgb / --accent-rgb are tuned for low-alpha fills and borders; used
   as SOLID TEXT on the white bar they land at 3.77:1 and 4.30:1, both under AA.
   The -text- tokens are the same hues tuned for legible text: 7.13:1 and 7.90:1.
   Dark needs no equivalent — it already measures 7.96:1 and 4.77:1 there, which
   is exactly why this is scoped rather than applied to the base rules.
   Specificity (0,5,1) > the base state rules (0,4,1), and each rule targets its
   OWN state, so neither clobbers the other. */
html[data-theme="light"] .glass-step-progress .step-seg.done .step-seg-num{
  color: rgb(var(--success-text-rgb));
}
html[data-theme="light"] .glass-step-progress .step-seg.active .step-seg-num{
  color: rgb(var(--accent-text-rgb));
}

/* ═══════════════════════════════════════════════════════════════════════
   §49  LEGACY PAGE CLASSES — carried forward from fixes-merged
   ═══════════════════════════════════════════════════════════════════════
   Kept so pages that haven't migrated to the glass system (search,
   payment_methods, settings, plus a handful of helper classes referenced
   by representatives.php / wallet.php / buy.php inline styles) continue
   to render with the new design tokens.
   ═══════════════════════════════════════════════════════════════════════ */

/* search.php's grid container */
.toolbox-grid {
  display: grid;
  gap: var(--sp-3);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 768px)  { .toolbox-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .toolbox-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* Persian faint helpers — used by wallet.php, buy.php, representatives.php */
.faint       { opacity: 0.7; }
.faint-deep  { opacity: 0.55; }

/* A-E: pre-glass .page-head rule removed — bare .page-head is unused
   in admin/, and this rule was fighting the canonical .glass-page-head
   declaration above when the same element carried both classes. */
.page-title h2 { margin-top: 0; }
.page-title p  { opacity: 0.7; font-size: 0.88rem; margin-top: 4px; }

/* A-F: the .mt-N / .mb-N duplicates were removed — app.css declares the same
   utilities via design tokens. The old glass.css copy used `!important` with
   different magnitudes (14 vs 16), forcing every spacing utility to 14px.
   CSS-UTILS: a `.mt-0 { margin-top: 0 !important }` also sat here under a line
   claiming app.css did not define `.mt-0`. It did; both had zero call sites,
   both are gone. `.text-center` stays — unused too, but a generic helper.
   PARSE BUG, fixed here: line 1 used to spell the comment-close sequence
   (star hard against slash, `.mt-*` joined to `.mb-*`). Comments do not nest,
   so ~790 chars of prose became the next rule's PRELUDE and Blink dropped the
   rule. Never put a star directly against a slash. */
.text-center { text-align: center; }

/* representatives.php — .form-control is referenced by the original
   username input and a few inputs in modals. */
.form-control{
  width: 100%;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: 10px;
  padding: 10px var(--sp-3);
  color: var(--text);
  font-size: 0.9rem;
  font-family: inherit;
}
.form-control:focus{
  outline: none;
  border-color: rgba(var(--accent-rgb), 0.45);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.12);
}
html[data-theme="light"] .form-control{
  background: rgba(0,0,0,0.025);
  border-color: rgba(0,0,0,0.08);
}

/* buy.php — .buy-vol-warn is hidden by default, shown via .show.
   The IIFE at the bottom of buy.php toggles the .show class. */
.glass-alert.warn.buy-vol-warn { display: none; }
.glass-alert.warn.buy-vol-warn.show { display: flex; }

/* buy.php §1 — auto-username glass toggle.
   Compact row: a Persian label + a small square "checkbox" pill. The real
   <input type=checkbox> is visually hidden but stays keyboard/SR-accessible.
   Checkbox is OUTSIDE the <label> in source order so we can drive both the
   label row's hover/focus state AND the sibling illustration block from one
   `:checked` selector. */
.auto-username-check{
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}
.auto-username-row{
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 6px 10px 6px 6px;
  border-radius: var(--r-2);
  cursor: pointer;
  user-select: none;
  color: var(--text);
  font-weight: 600;
  font-size: 0.92rem;
  font-family: inherit;
  transition: background 0.18s ease, color 0.18s ease;
}
.auto-username-row:hover{ background: rgba(255,255,255,0.04); }
.auto-username-text{
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.4;
}
/* ── Commit 3 (#50): a real switch, not a glowing check-square ─────────────
   The control was a 26px rounded square that filled green with a tick + glow —
   it read as a decorative checkbox and its on/off state wasn't obvious. Now a
   track + sliding thumb. RTL note: the thumb rests at the inline-start (right)
   and travels LEFT when on; translateX is physical and this UI is RTL-only.
   Travel = 44 track − 18 thumb − 3 − 3 inset = 20px. Glow deliberately dropped. */
.auto-username-mark{
  position: relative;
  display: inline-flex;
  align-items: center;
  width: 44px; height: 26px;
  border-radius: var(--r-full);
  /* R2-11: track surface/border token-driven; the checked state below repaints
     both with --success-rgb, so the dark literals are gone. */
  background: var(--input-bg);
  border: 1px solid var(--border-strong);
  flex-shrink: 0;
  transition: background 0.2s ease, border-color 0.2s ease;
}
.auto-username-mark::after{
  content: '';
  position: absolute;
  inset-inline-start: 3px;
  width: 18px; height: 18px;
  border-radius: 50%;
  /* R2-11: the thumb is the inverted ink per system — a white thumb on a tinted
     track in dark, the dark counterpart in light. */
  background: var(--primary-foreground);
  box-shadow: 0 1px 3px rgba(0,0,0,0.35);
  transition: transform 0.2s ease;
}
.auto-username-check:checked ~ .auto-username-row .auto-username-mark{
  background: rgb(var(--success-rgb));
  border-color: rgb(var(--success-rgb));
}
.auto-username-check:checked ~ .auto-username-row .auto-username-mark::after{
  transform: translateX(-20px);
}
/* Visible on/off token — #50 asked for a clear textual state beside the switch. */
.auto-username-state{
  font-size: 0.8rem;
  font-weight: 600;
  opacity: 0.6;
}
.auto-username-state::after{ content: 'خاموش'; }
.auto-username-check:checked ~ .auto-username-row .auto-username-state::after{ content: 'روشن'; }
/* Commit 3 (#51): in auto mode the manual field is already disabled AND ignored
   server-side ($rawUser = $auto ? null : …), so leaving a full-size input on
   screen advertised an editable control that wasn't one — and it dominated the
   section. Collapse it; unchecking the switch restores it in place. */
.auto-username-check:checked ~ .buy-input-wrap,
.auto-username-check:checked ~ .buy-username-hint{ display: none; }
.auto-username-check:focus-visible ~ .auto-username-row .auto-username-mark{
  outline: 2px solid rgba(255,255,255,0.32);
  outline-offset: 2px;
}
/* A white ring is invisible against the white light-theme card, so keyboard
   focus on the auto-username switch simply vanished. Match the accent ring
   the rest of the system uses. Light-scoped so dark is untouched. */
html[data-theme="light"] .auto-username-check:focus-visible ~ .auto-username-row .auto-username-mark{
  outline-color: rgba(var(--accent-rgb), 0.65);
}
/* Manual username input fades while auto is on. */
.form-control.is-auto-locked,
.form-control:disabled{
  opacity: 0.55;
  cursor: not-allowed;
}
html[data-theme="light"] .auto-username-row:hover{ background: rgba(0,0,0,0.03); }

/* buy.php / wallet.php — form-hint utility */
.form-hint{
  font-size: var(--fs-1);
  opacity: 0.7;
  margin: 4px 0 0;
}

/* End of legacy compat block. */


/* ═══════════════════════════════════════════════════════════════════════
   §50  DEFAULT PAGE BACKGROUND
   ═══════════════════════════════════════════════════════════════════════
   The page background is a single flat solid colour — the fixed design
   system. Two decorative predecessors were removed: a three-layer blurred
   radial-gradient "blob" background, and the sudo-configurable per-theme
   background-image / gradient-preset / overlay feature (body.has-app-bg,
   the admin/settings.php "تنظیمات ظاهری" card). Nothing composites behind
   the page any more. */
body{ background-color: var(--bg, #0f1220); }

/* ═══════════════════════════════════════════════════════════════════════
   §51  STICKY MODAL CTA — fixes-merged-02 / Section B (Issue 1)
   ═══════════════════════════════════════════════════════════════════════
   The three modal-submit CTAs that exist in the unmodified files pin to
   the bottom of the scrolling .modal-box so the user never has to scroll
   to discover the submit. All three selectors below match button classes
   ALREADY present in the unpatched files — no markup change required:

     .cta-deposit             admin/wallet.php  top-up submit
     .wallet-adjust-submit    admin/representatives.php  adjust modal submit
     .cta-buy                 admin/buy.php  (not in a modal — it's a
                              page-level form. Mobile sticky already exists
                              via the RULE-8 sticky-CTA rule at the bottom
                              of the file. Excluded here.)

   Forward-compatible: a future <div class="modal-actions"> wrapper —
   if and when it ships — gets the full glass-strip treatment via the
   trailing rule. Both forms coexist; the wrapper is optional.

   NOTE (rev 4): §53 below narrows the sticky CTA to a centred pill
   (max-width 280/320 px). The sticky position + box-shadow below still
   applies (no backdrop-filter on this rule any more); only the width
   is overridden.
   ═══════════════════════════════════════════════════════════════════════ */

.modal-overlay.glass-modal .modal-box .btn-glass.cta-deposit,
.modal-overlay.glass-modal .modal-box .btn-glass.wallet-adjust-submit{
  position: sticky;
  bottom: 0;
  z-index: 8;
  /* Hairline above + soft drop below + inset highlight along the top
     edge. Together these read as a pinned glass bar that "catches light"
     where it meets the scrolling content above. */
  box-shadow:
    0 -1px 0 0 rgba(255,255,255,0.10),
    0 -14px 28px -10px rgba(0,0,0,0.55);
}
/* PERF: split out of the combined rule above — .modal-actions is a
   repeated generic button-row wrapper (not a single sticky CTA). Neither
   this rule nor the two CTA buttons above carry backdrop-filter any
   more; the box-shadow alone does the "pinned bar" visual work now.
   Same position/box-shadow so that visual is unchanged. */
.modal-overlay.glass-modal .modal-box .modal-actions{
  position: sticky;
  bottom: 0;
  z-index: 8;
  box-shadow:
    0 -1px 0 0 rgba(255,255,255,0.10),
    0 -14px 28px -10px rgba(0,0,0,0.55);
}
html[data-theme="light"] .modal-overlay.glass-modal .modal-box .btn-glass.cta-deposit,
html[data-theme="light"] .modal-overlay.glass-modal .modal-box .btn-glass.wallet-adjust-submit,
html[data-theme="light"] .modal-overlay.glass-modal .modal-box .modal-actions{
  box-shadow:
    0 -1px 0 0 rgba(0,0,0,0.08),
    0 -14px 28px -10px rgba(0,0,0,0.14);
}
/* DELETED 2026-09-23: light-only `background-color` washes on the three modal
   CTAs (`.cta-deposit` on success tint, `.cta-buy` / `.wallet-adjust-submit` on
   accent, all at 0.42). They are the same leftover as the `color: rgb(20,83,45)`
   override deleted from `.cta-deposit` a few hundred lines up: they existed
   because the light theme's `--primary` was blue and the label had to be
   force-inked. `--primary` is now `--p-action` per design system and
   `--primary-foreground` is AA-checked against it, so the wash no longer has a
   job — and it has a cost. It OVERRODE a solid fill with a 42% tint of a
   DIFFERENT hue while the label stayed `var(--primary-foreground)`, which in
   `paper` is `#ffffff`: measured on the assembled values, white on the success
   wash is 2.03:1 and white on the accent wash 1.82:1. Every modal submit button
   on the panel was unreadable in the light theme. Deleting the overrides
   restores each CTA to its own system's fill and ink (16.25:1 in `paper`). */

/* .modal-actions — forward-compatible wrapper. If a future patch wraps
   the modal submit in <div class="modal-actions">, this rule turns it
   into a full-width frosted strip spanning the modal-box edges. Optional
   in this package — see patches/wallet.php.md for the recommended
   minimal markup change. */
.modal-overlay.glass-modal .modal-box .modal-actions{
  display: flex;
  gap: 10px;
  align-items: stretch;
  /* Cancel the modal-box's 24px inner padding so the strip touches the side
     borders. UI-0044: that 24px is now the canonical .modal-box padding
     defined in §9 above (it previously referenced a non-existent app.css
     rule, so this -24px was cancelling padding that did not exist). */
  margin: 0 -24px;
  padding: var(--sp-3-5) var(--sp-6) max(var(--sp-3-5), env(safe-area-inset-bottom, var(--sp-3-5)));
  background: rgba(18,20,30,0.72);
  border-top: 1px solid rgba(255,255,255,0.08);
}
html[data-theme="light"] .modal-overlay.glass-modal .modal-box .modal-actions{
  background: rgba(255,255,255,0.82);
  border-top-color: rgba(0,0,0,0.06);
}
.modal-overlay.glass-modal .modal-box .modal-actions > .btn-glass{ flex: 1; }

/* UI-0158: the deposit-review popup is short and never scrolls, so the sticky
   frosted-strip treatment above reads as a stray bar floating mid-box. Reset
   it to a plain button row. Moved here from an inline <style> in
   admin/deposits.php (the page <style> loaded after glass.css and silently
   overrode the strip); the ID selector keeps the same specificity/behavior. */
#modal-deposit-review .modal-actions{
  position: static;
  display: flex;
  gap: var(--sp-2);
  margin: 0;
  padding: 0;
  background: transparent;
  border-top: 0;
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
#modal-deposit-review .modal-actions > .btn-glass{ flex: 1; }

/* ═══════════════════════════════════════════════════════════════════════
   §52  REDUCED-TRANSPARENCY / NO-BACKDROP-FILTER FALLBACK
   ═══════════════════════════════════════════════════════════════════════
   Disables blur + blobs and collapses surfaces to opaque tinted versions.
   Form factor + dimensions unchanged — only the glass effect is gone.
   ═══════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-transparency: reduce) {
  .glass-card, .glass-stat-card, .glass-page-head, .glass-info-bar,
  .glass-form-section, .glass-balance-card, .glass-method-card,
  .glass-deposit-row, .glass-tx-table, .glass-rep-card, .rep-menu-panel,
  .filters-glass, .btn-glass, .glass-bottom-nav, .glass-amount-input,
  .glass-textarea, .glass-step-progress, .glass-stepper .stepper-input,
  .modal-overlay.glass-modal .modal-box,
  .modal-overlay.glass-modal .modal-box .btn-glass.cta-deposit,
  .modal-overlay.glass-modal .modal-box .btn-glass.wallet-adjust-submit,
  .modal-overlay.glass-modal .modal-box .modal-actions{
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }

  .glass-card, .glass-stat-card, .glass-page-head, .glass-info-bar,
  .glass-form-section, .glass-balance-card, .glass-method-card,
  .glass-deposit-row, .glass-rep-card, .filters-glass, .glass-bottom-nav,
  .modal-overlay.glass-modal .modal-box, .rep-menu-panel{
    /* R1-15: the polarity base owns --elevated; the dark-pinned fallback
       and the light-twin white literal froze the wrong card color onto
       systems of the opposite polarity. Light twin deleted. */
    background: var(--elevated) !important;
  }
}

/* Browsers that don't support backdrop-filter at all (older Edge,
   Firefox < 103 without flag). Same opaque collapse. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass-card, .glass-stat-card, .glass-page-head, .glass-info-bar,
  .glass-form-section, .glass-balance-card, .glass-method-card,
  .glass-deposit-row, .glass-rep-card, .filters-glass, .glass-bottom-nav,
  .modal-overlay.glass-modal .modal-box, .rep-menu-panel{
    background: var(--elevated);
  }
}

/* ═════════════════════════════════════════════════════════════════
   §53  STICKY MODAL CTA — CENTRED PILL  (narrows §51's full-bleed)
   ═════════════════════════════════════════════════════════════════
   The button keeps its §30 / §22 design (green/indigo gradient + glow)
   and just gets a narrower footprint so the sticky CTA reads as a
   centred pill rather than a full-bleed bar. Width caps at 280 px on
   phones and 320 px on tablet+. §51's sticky-position + heavy blur
   scaffolding still applies — the centred pill just sits on top of
   the scrolling content.
   ═════════════════════════════════════════════════════════════════ */
.modal-overlay.glass-modal .modal-box .btn-glass.cta-deposit,
.modal-overlay.glass-modal .modal-box .btn-glass.wallet-adjust-submit,
.modal-overlay.glass-modal .modal-box .btn-glass.cta-buy{
  width: auto;
  max-width: 320px;
  margin-left: auto;
  margin-right: auto;
  display: block;
}
@media (max-width: 480px){
  .modal-overlay.glass-modal .modal-box .btn-glass.cta-deposit,
  .modal-overlay.glass-modal .modal-box .btn-glass.wallet-adjust-submit,
  .modal-overlay.glass-modal .modal-box .btn-glass.cta-buy{
    max-width: 280px;
  }
}

/* ─────────────────────────────────────────────────────────────────
   §53b  TOP-UP CTA — FULL-WIDTH (overrides §53's centred pill)
   ─────────────────────────────────────────────────────────────────
   In admin/wallet.php's top-up modal the submit ("ثبت درخواست") sat
   next to full-width controls (select / amount / receipt / note), so
   the §53 centred pill read as undersized + off-balance against them.
   Restore the §30 full-bleed footprint so the CTA matches the form
   controls above it on phone + desktop alike. Scoped to #modal-topup
   (the only .cta-deposit usage) — the buy/adjust modal pills keep §53.
   The §51 sticky-bottom + frosted-bar scaffolding still applies, so
   this reads as a full-width sticky submit bar. The ID selector
   outranks §53's class chains on every breakpoint — no media query
   needed. */
#modal-topup .btn-glass.cta-deposit{
  display: flex;            /* re-enable .btn-glass's centring of the label */
  width: 100%;
  max-width: none;
  margin-left: 0;
  margin-right: 0;
}

/* ═════════════════════════════════════════════════════════════════
   §54  AMBIENT COLOUR ACCENTS ON GLASS SURFACES
   ═════════════════════════════════════════════════════════════════
   A barely-saturated radial wash on the main glass containers so the
   surfaces read as cooler/warmer rather than flat-white-on-flat-dark.
   The wash is layered as a background-image, leaving the existing
   solid background-color in place — so no surface loses contrast.
   Dark theme leans into the indigo/violet brand colour at low alpha;
   light theme uses indigo + a touch of cyan so the surfaces don't go
   muddy on the cream-coloured app background.
   ═════════════════════════════════════════════════════════════════ */
/* Phase 5 stage 2: REMOVED the decorative purple/indigo/cyan radial
   background-image tints that every card + the modal carried — this was the
   "white cards carry purple/teal tints" washed-out complaint. Cards are now
   flat solid surfaces (--glass-card-bg / --card / --popover); structure comes
   from borders, color only from primary/status/data. */
/* Reduced-transparency override — flatten back to the elevated solid. */
@media (prefers-reduced-transparency: reduce){
  .glass-card, .glass-rep-card, .glass-form-section, .glass-method-card,
  .glass-info-bar, .glass-page-head, .filters-glass, .glass-balance-card,
  .glass-deposit-row, .modal-overlay.glass-modal .modal-box{
    background-image: none !important;
  }
}

/* ═════════════════════════════════════════════════════════════════
   §55  INFO-BANNER NARROW — DELETED 2026-09-23
   ═════════════════════════════════════════════════════════════════
   This rule capped a modal's .glass-alert.info at 360px and centred it
   with `margin-left/right: auto`. It never reliably did either, and the
   way it failed is worth keeping written down.

   Those auto margins are author stylesheet declarations. Every one of
   these banners carries its spacing in an INLINE `style` attribute —
   `.modal-box > .glass-alert.info` at subscription_detail.php:1402 and
   :1511 spell it `margin:0 0 12px`, and the shorthand's horizontal
   halves are `0`. Inline beats a stylesheet rule without `!important`,
   so the centring was defeated and what actually rendered was a 360px
   block pinned to the inline-START edge: flush to the right in RTL,
   empty on the left. Measured on the renew modal, where the summary
   panel and the submit button above and below it are both full-bleed.

   Two writers for one width, one of them silently losing. The base
   .glass-alert rule carries no width and the other 28 banners on the
   panel are full width, so the exception was also a second contract for
   a single component. Deleting it restores the base rule as the only
   writer: the banner now lines up with its siblings inside the modal.

   NOT re-added as a centred variant. If a banner genuinely needs to be
   a contained pill ABOVE a form, it needs its own class and no inline
   margin — not a modal-scoped override that the markup can veto.
   ═════════════════════════════════════════════════════════════════ */

/* End of fixes-merged-02 additions. */

/* ═══════════════════════════════════════════════════════════════════
   §56  AUTH REDESIGN  (login.php — body.auth-redesign)
   ───────────────────────────────────────────────────────────────────
   Scoped under .auth-redesign so it cannot affect any other surface.
   Layout:
     <main.auth-shell>
       <header.auth-hero>  ← sparkle + brand + subtitle (above card)
       <section.glass-auth-card.auth-card>
         <h2.auth-card-title>
         [<glass-alert>?]
         <form.auth-form>
           <.auth-field>
             <label>
             <.auth-field-row>
               <.auth-icon-chip>
               <.auth-input-wrap[.has-eye]>
                 <input.auth-input>
                 [<button.auth-eye-btn>]
         <.auth-cta>
         <.auth-divider>
         <a.auth-secondary-cta>
         <p.auth-hint>
   ═══════════════════════════════════════════════════════════════════ */

body.auth-page.auth-redesign{
  display: block;
  padding: 0;
  min-height: 100vh;
  /* Slightly deeper background so the radial blobs read with more contrast */
  background: var(--auth-bg);
}
body.auth-page.auth-redesign::before{
  /* Replace the default 3-blob field with a calmer two-blob composition.
     The blobs are positioned so the hero gets a faint top-left lift and
     the card sits in front of a soft accent halo. */
  background:
    radial-gradient(46% 38% at 18% 22%,  var(--auth-blob1),  transparent 65%),
    radial-gradient(42% 34% at 82% 78%,  var(--auth-blob2),  transparent 65%),
    radial-gradient(58% 40% at 50% 100%, var(--auth-blob3),  transparent 70%);
  filter: blur(8px);
}

.auth-redesign .auth-shell{
  position: relative;
  z-index: 1;
  min-height: 100vh;
  width: 100%;
  max-width: 480px;
  margin-inline: auto;
  padding: clamp(var(--sp-6), 5vh, 56px) clamp(var(--sp-4), 4vw, 28px);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  gap: clamp(var(--sp-6), 4vh, 40px);
}

/* ─── Floating theme toggle (auth-only positioning) ─────────────── */
.auth-redesign .auth-theme-toggle{
  position: fixed;
  /* Both edges are guarded, because `viewport-fit=cover` is on every head and
     this control is pinned into the two worst corners: the status bar along
     the top and, in landscape, the notch along the leading edge in RTL. It is
     the same `max(16px, env(…))` idiom `#toast-container` uses, so the offset
     stays exactly 16px on every device with no inset. §61 raises it to 44px
     under `pointer: coarse`; this is the position half of the same control. */
  inset-block-start: max(16px, env(safe-area-inset-top, 16px));
  inset-inline-end:  max(16px, env(safe-area-inset-right, 16px));
  width: 38px; height: 38px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-2);
  background: var(--toggle-btn-bg, rgba(255,255,255,0.08));
  /* R1-13/R3-16: --auth-card-border is defined in the polarity base. */
  border: 1px solid var(--auth-card-border);
  cursor: pointer;
  font-size: 16px;
  color: var(--text);
  transition: transform .18s var(--ease), border-color .18s var(--ease);
  z-index: 5;
}
.auth-redesign .auth-theme-toggle:hover{
  transform: translateY(-1px);
  border-color: rgba(255,255,255,0.20);
}
/* UI-0339: on very narrow screens (≤380px) the fixed toggle crowded the
   centered hero. Pull it tighter into the corner and shrink it so it stays
   clear of the hero icon/title. */
@media (max-width: 380px){
  .auth-redesign .auth-theme-toggle{
    /* The base rule's safe-area floor, at this breakpoint's tighter offset.
       This override has the same (0,2,0) specificity and comes later, so it
       REPLACES the guarded position — 8px of chrome is "tighter" only on a
       device with no inset; on a notched phone it is off the top of the
       screen. Both edges keep the `max()`. */
    inset-block-start: max(8px, env(safe-area-inset-top, 8px));
    inset-inline-end:  max(8px, env(safe-area-inset-right, 8px));
    width: 34px; height: 34px;
    font-size: 14px;
  }
}

/* ─── Hero (outside the card) ───────────────────────────────────── */
.auth-redesign .auth-hero{
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding-top: 8px;
}
.auth-redesign .auth-hero-icon{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px; height: 56px;
  color: var(--accent-2);
  filter:
    drop-shadow(0 0 14px rgba(var(--accent-rgb), 0.55))
    drop-shadow(0 0 28px rgba(var(--accent-rgb), 0.25));
  margin-bottom: 4px;
}
.auth-redesign .auth-hero-title{
  font-size: clamp(2rem, 6.5vw, 2.6rem);
  font-weight: 800;
  /* G10-assets: was 0.5px. The hero is the single Persian word «پورتال», and
     it is the largest type on the login page — the one place tracked Persian
     is most visible. */
  letter-spacing: 0;
  line-height: 1.1;
  color: var(--text);
  margin: 0;
}
.auth-redesign .auth-hero-sub{
  font-size: var(--fs-2);
  color: var(--text-m);
  opacity: 0.85;
  margin: 0;
}

/* ─── Card overrides ────────────────────────────────────────────── */
.auth-redesign .auth-card{
  max-width: 100%;
  padding: clamp(22px, 4vw, 30px) clamp(var(--sp-5), 4vw, 28px);
  border-radius: var(--r-5);
  /* R1-13/R3-16: polarity-pinned gradient (20,22,32 + white sheen) was dark
     regardless of theme; the card face now rides the --auth-card token. */
  background:
    linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02)),
    var(--auth-card);
  border: 1px solid rgba(255,255,255,0.10);
  box-shadow:
    0 24px 64px rgba(0,0,0,0.55),
    inset 0 1px 0 rgba(255,255,255,0.08);
}
.auth-redesign .auth-card::after{
  /* Slightly larger, slightly cooler halo than default */
  inset: -56px;
  background:
    radial-gradient(60% 50% at 20% 20%,  rgba(var(--accent-rgb), 0.22), transparent 60%),
    radial-gradient(50% 50% at 85% 85%,  rgba(var(--accent-rgb), 0.12),  transparent 65%);
  filter: blur(50px);
}

.auth-redesign .auth-card-title{
  text-align: center;
  font-size: var(--fs-5);
  font-weight: 800;
  /* G10-assets: was 0.2px. «ورود به حساب» / «ایجاد حساب کاربری» — Persian. */
  letter-spacing: 0;
  color: var(--text);
  margin: 6px 0 20px;
}
/* The line under the title saying what the form is FOR, in the shape
   `signup-card` and `auth-card` both open with. It takes the title's bottom
   margin away so the pair reads as one header, and it is centred with the
   title because the card's own inline axis is centred from the h2 down to the
   CTA.

   ITS BOTTOM MARGIN IS 24px AND THAT NUMBER IS NOT FREE. This paragraph is a
   child of the card, not of the form, so it gets none of
   `.glass-auth-card .auth-form > * + *`'s 14px. 24px is that rule's 14 plus the
   form's own `gap: var(--sp-4)`, i.e. exactly one form row's worth — which is
   what makes the gap above the first field read the same as the gap between
   any two fields. `margin-collapse` is why this is stated here rather than
   left to the two rules to add up. */
.auth-redesign .auth-card-title:has(+ .auth-card-sub){ margin-bottom: 6px; }
.auth-redesign .auth-card-sub{
  text-align: center;
  font-size: 0.85rem;
  line-height: 1.7;
  color: var(--text-d);
  margin: 0 0 24px;
}

/* ─── Form rows ─────────────────────────────────────────────────── */
.auth-redesign .auth-form{ display: flex; flex-direction: column; gap: var(--sp-4); }

.auth-redesign .auth-field{ display: flex; flex-direction: column; gap: var(--sp-2); }
.auth-redesign .auth-field > label{
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--text-m);
  /* Labels right-aligned in RTL — matches target */
  text-align: start;
  padding-inline-start: 4px;
}
.auth-redesign .auth-field > label .faint{ color: var(--text-d); font-weight: 400; }

.auth-redesign .auth-field-row{
  display: flex;
  align-items: stretch;
  gap: 10px;
  min-height: 52px;
}

/* Icon "chip" — small glass square that prefixes each input (visually on
   the right in RTL because it's the first flex child). */
.auth-redesign .auth-icon-chip{
  flex: 0 0 52px;
  width: 52px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: 14px;
  color: var(--text-m);
  transition: color .18s var(--ease), border-color .18s var(--ease), background .18s var(--ease);
}
.auth-redesign .auth-field-row:focus-within .auth-icon-chip{
  color: var(--accent-2);
  border-color: rgba(var(--accent-rgb), 0.45);
  background: rgba(var(--accent-rgb), 0.10);
}

.auth-redesign .auth-input-wrap{
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
}
.auth-redesign .auth-input{
  width: 100%;
  min-height: 52px;
  padding: 0 var(--sp-4);
  border-radius: 14px;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.10);
  color: var(--text);
  font: inherit;
  font-size: 0.98rem;
  outline: none;
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease), background .18s var(--ease);
}
.auth-redesign .auth-input::placeholder{
  color: var(--text-d);
  /* Persian placeholder shouldn't fight the dir="ltr" of the actual value */
  direction: rtl; text-align: start;
  opacity: 0.85;
}
/* H1.15: inside a .has-eye wrap the placeholder must flow LTR so it
   starts at the input's leading edge and never overlaps the eye toggle
   sitting on the right (inline-start in an RTL page). The password
   placeholder is a row of dots, so direction has no aesthetic impact;
   it only changes which edge the row begins from. */
.auth-redesign .has-eye .auth-input::placeholder{
  direction: ltr;
  text-align: start;
}
/* UI-0045: an email placeholder (example@email.com) carries neutral chars
   (@ .) that the RTL default reorders into gibberish; flow it LTR so it
   reads correctly. The field is already dir="ltr". */
.auth-redesign input[type="email"]::placeholder{
  direction: ltr;
  text-align: start;
}
.auth-redesign .auth-input:focus{
  border-color: rgba(var(--accent-rgb), 0.55);
  background: rgba(255,255,255,0.06);
  box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.15);
}
/* Eye-toggle slot: extra padding so the value never collides with the
   absolutely-positioned eye button.
   UI-0054: 44px touch target, so the slot is 52px.

   FIXED: this reserved the padding on the input's LEADING edge. The comment
   above it claimed "in RTL the leading edge is the left" — but the input is
   dir="ltr", so `padding-inline-start` resolved against the INPUT's own LTR
   direction = physical LEFT, while the button resolved `inset-inline-start`
   against the RTL wrap = physical RIGHT. Two consequences, both reported:
     1. the value collided with the eye button on the right, which had no
        padding reserved at all;
     2. the 52px indent sat on the leading edge, so the password caret started
        36px further in than every other field on the form — the "cursor is
        not consistent across fields" complaint.
   Reserving on the TRAILING edge fixes both: the slot is now on the same
   physical side as the button, and the leading padding falls back to the
   shared 16px so all fields start their text at the same x. */
.auth-redesign .has-eye .auth-input{ padding-inline-end: 52px; }

/* Pinned to ltr for the same reason as .pw-wrap: the input inside is dir="ltr",
   so every logical property in this subtree now resolves the same way the
   input's own padding does. Without the pin, `inset-inline-start` here meant
   physical RIGHT (resolved against the RTL page) while
   `padding-inline-start` on the input meant physical LEFT — the opposite side.
   The input carries its own dir="ltr", so this does not change how any
   placeholder or value is aligned; it only fixes where the button lands. */
.auth-redesign .auth-input-wrap.has-eye{ direction: ltr; }

/* The one-time-code field. It is the only auth input that is a CODE rather
   than a name: fixed-width digits, never autocorrected, never autocapitalised.
   Tracking + tabular figures make "123456" read as six positions instead of
   one six-digit quantity, which is the difference between spotting a dropped
   digit and submitting a wrong code. The field also accepts a hyphenated
   20-symbol recovery code, so nothing here assumes digits — only that the
   content is literal, unspaced machine text. */
/* No letter-spacing: `bin/test_polish_c10a_glass.php` §19 bans non-zero
   tracking on any owner not on its verified-non-Persian allowlist, and the
   right answer there was to not need the exemption. A monospace face is the
   whole signal — the field holds 6 digits or a hyphenated recovery code. */
.auth-redesign .auth-input.auth-otp{
  font-family: ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
}

/* ─── The six-box second factor (login / register tab) ──────────────────────
   Six REAL inputs named `totp_digit[]`, not decoration painted over one
   hidden field. That is the whole point: with JavaScript disabled the six
   boxes still post six values and the server rejoins them, so the second
   factor cannot be broken by a script that failed to load. The inline script
   at the foot of login.php adds advance, backspace and paste — ergonomics,
   never the value itself.

   `font-size: 1.05rem` = 16.8px, deliberately ABOVE the iOS floor. Safari
   zooms the page for any text control computing under 16px on focus and does
   not zoom back out, and §61's lift list is re-derived from the rules in THIS
   file — `.otp-cell` would have to be named there if it were small. The
   honest fix is to not need covering. */
/* A grid, not a flex row, and `minmax(0, 1fr)` rather than `1fr`.
   Measured at 375px with a flex row: six 40px cells plus five 8px gaps need
   280px inside a 247px wrap, and the row OVERFLOWED — a nested flex item's
   automatic minimum size is its content's, so the six inputs held the group at
   280px and the shrink never happened. `minmax(0, 1fr)` cannot do that: the
   floor is zero by construction, so the boxes share whatever width exists
   (34.5px each at 375px) and `max-width` still caps them at 40px on a wide
   screen. One rule, no breakpoint, and no viewport it can be wrong at. */
.auth-redesign .otp-field{
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 8px;
  max-width: 280px;   /* six 40px boxes + five 8px gaps */
}
.auth-redesign .otp-cell{
  min-width: 0;
  height: 48px;
  padding: 0;
  text-align: center;
  font-family: ui-monospace, monospace;
  /* `max(16px, …)`, not a bare rem. app.css steps the ROOT down on narrow
     screens, so 1.05rem is 16.8px at a 16px root and **14.7px at the 14px one** —
     measured on this page at 320px, where all six boxes came in under 16 and iOS
     would have zoomed the page for a control that cannot be tapped twice. The
     `max()` is the same shape §61 uses for the controls it lifts; this one is
     stated here because the box size IS the design and §61 would have to be told
     about a control it cannot re-derive (`.otp-cell` names no `input` token). */
  font-size: max(16px, 1.05rem);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--text);
  background: var(--input-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
}
.auth-redesign .otp-cell:focus{
  outline: none;
  border-color: rgba(var(--accent-rgb), 0.7);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.18);
}
.auth-redesign .otp-cell.is-invalid{ border-color: var(--danger); }

/* ─── The recovery-code escape hatch ───────────────────────────────────────
   A native <details>, so it costs no JavaScript and no session state. It is
   what lets six boxes coexist with a hyphenated 20-symbol recovery code,
   which is the reason the field used to be a single input. */
.auth-recovery{ margin-block-start: 8px; }
.auth-recovery > summary{
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  font-size: 0.82rem;
  color: var(--text-m);
  cursor: pointer;
  list-style: none;
  transition: color .15s var(--ease);
}
.auth-recovery > summary::-webkit-details-marker{ display: none; }
.auth-recovery > summary:hover{ color: var(--text); }
/* The disclosure marker is the page's OWN idiom, not a new one: the same
   two-border square rotated 45deg that `.auth-explainer > summary::before`
   already draws in login.php, turned the other way when open. An SVG chevron
   here would have been the second way to draw one marker on one page. */
.auth-recovery > summary::before{
  content: "";
  width: 0.5em; height: 0.5em;
  border-inline-end: 1.5px solid currentColor;
  border-block-end: 1.5px solid currentColor;
  rotate: 45deg;
  translate: 0 -0.15em;
  transition: rotate .22s var(--ease);
  flex-shrink: 0;
}
.auth-recovery[open] > summary::before{ rotate: -135deg; }
.auth-recovery .auth-field-row{ margin-block-start: 6px; }
@media (pointer: coarse){
  .auth-recovery > summary{ min-height: 44px; }
}
/* ─── Password strength meter ──────────────────────────────────────────────
   Four segments plus a level word, driven by `initPasswordMeters()` in app.js.
   The four segments ARE the four clauses of `Security::validatePassword`
   (>= 8 characters, uppercase, lowercase, digit) — a full bar means the server
   accepts the value. That is deliberate and it is the opposite of the usual
   library meter, which scores length and symbol classes on a scale of its own
   and will happily print «قوی» for a password the server then refuses.

   Colour is keyed on the COUNT, not per segment: three-of-four is amber even
   though three segments are filled, because "how much is met" is the reading
   and a per-segment colour would paint a half-satisfied bar green in its
   satisfied half. The word beside it is the same count in prose, so the
   meaning never rests on colour alone. */
.pw-meter-wrap{ margin-block-start: 7px; }
.pw-meter{ display: flex; gap: 5px; }
.pw-seg{
  flex: 1 1 0;
  height: 4px;
  border-radius: 999px;
  background: var(--input-bg);
  border: 1px solid var(--border);
  transition: background .2s var(--ease), border-color .2s var(--ease);
}
.pw-level{
  margin: 5px 0 0;
  min-height: 1.1em;
  font-size: 0.74rem;
  color: var(--text-d);
  transition: color .2s var(--ease);
}
.pw-meter-wrap[data-pw-met="1"] .pw-seg.is-met,
.pw-meter-wrap[data-pw-met="2"] .pw-seg.is-met{
  background: var(--danger);
  border-color: var(--danger);
}
.pw-meter-wrap[data-pw-met="3"] .pw-seg.is-met{
  background: var(--warn);
  border-color: var(--warn);
}
.pw-meter-wrap[data-pw-met="4"] .pw-seg.is-met{
  background: rgb(var(--success-rgb));
  border-color: rgb(var(--success-rgb));
}
.pw-meter-wrap[data-pw-met="3"] .pw-level{ color: var(--warn); }
.pw-meter-wrap[data-pw-met="4"] .pw-level{ color: rgb(var(--success-text-rgb)); }
.auth-redesign .auth-eye-btn{
  position: absolute;
  inset-block: 0;
  inset-inline-end: 6px;
  width: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent;
  border: 0;
  color: var(--text-m);
  cursor: pointer;
  border-radius: 10px;
  transition: color .15s var(--ease), background .15s var(--ease);
}
.auth-redesign .auth-eye-btn:hover{
  color: var(--text);
  background: rgba(255,255,255,0.06);
}
.auth-redesign .auth-eye-btn:focus-visible{
  outline: 2px solid rgba(var(--accent-rgb), 0.45);
  outline-offset: 1px;
}

/* ─── Primary CTA (login / register submit) ─────────────────────────
   Dual-background gradient ring: fill painted to `padding-box`, then a
   second gradient painted to `border-box` shows through a transparent
   border. Result: a clearly visible purple→cyan rim that matches the
   target without relying on a faint 1-px solid. */
.auth-redesign .btn-glass.auth-cta{
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: 54px;
  margin-top: 8px;
  padding: var(--sp-3-5) var(--sp-5);
  border-radius: var(--r-3);
  font-size: 1.02rem;
  font-weight: 700;
  /* G10-assets: was 0.3px. Same Persian auth labels as above. */
  letter-spacing: 0;
  color: #ffffff;
  cursor: pointer;
  border: 1.5px solid transparent;
  background:
    linear-gradient(180deg,
      rgba(var(--accent-rgb),0.30),
      rgba(var(--accent-rgb),0.14)) padding-box,
    linear-gradient(135deg,
      rgba(var(--accent-rgb),0.95) 0%,
      rgba(var(--accent-rgb),0.65) 100%) border-box;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.20),
    0 14px 36px rgba(var(--accent-rgb),0.30);
  transition: transform .18s var(--ease),
              box-shadow .18s var(--ease),
              filter .18s var(--ease);
}
.auth-redesign .btn-glass.auth-cta::before{
  content: "";
  position: absolute;
  inset: -8px;
  border-radius: 22px;
  background: radial-gradient(60% 80% at 50% 50%,
    rgba(var(--accent-rgb),0.55), transparent 70%);
  filter: blur(26px);
  opacity: 0.55;
  z-index: -1;
  pointer-events: none;
  transition: opacity .18s var(--ease);
}
.auth-redesign .btn-glass.auth-cta:hover{
  transform: translateY(-1px);
  filter: brightness(1.06);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.24),
    0 18px 44px rgba(var(--accent-rgb),0.36);
}
.auth-redesign .btn-glass.auth-cta:hover::before{ opacity: 0.9; }
.auth-redesign .btn-glass.auth-cta:active{ transform: translateY(0); }
.auth-redesign .btn-glass.auth-cta:focus-visible{
  outline: 2px solid rgba(var(--accent-rgb),0.55);
  outline-offset: 3px;
}
.auth-redesign .btn-glass.auth-cta svg{
  /* Arrow naturally points right; in RTL we want it to read as "forward"
     toward the leading edge, so flip horizontally. */
  transform: scaleX(-1);
  opacity: 0.95;
}

/* ─── "یا" divider ──────────────────────────────────────────────── */
.auth-redesign .auth-divider{
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: 22px 0 16px;
  color: var(--text-d);
  font-size: 0.85rem;
}
.auth-redesign .auth-divider::before,
.auth-redesign .auth-divider::after{
  content: "";
  flex: 1;
  height: 1px;
  /* A-A: `to inline-start` is invalid CSS — the <side-or-corner> grammar
     accepts only physical keywords. Use `to left` to match the existing
     `.glass-divider` pattern at lines 497/501. */
  background: linear-gradient(to left, transparent, rgba(255,255,255,0.14), transparent);
}
.auth-redesign .auth-divider > span{ padding: 0 var(--sp-1); }

/* ─── Secondary CTA (sign-up link box) ──────────────────────────── */
.auth-redesign .auth-secondary-cta{
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: 52px;
  padding: var(--sp-3) var(--sp-4-5);
  border-radius: 14px;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.10);
  color: var(--text-m);
  text-decoration: none;
  font-size: var(--fs-2);
  font-weight: 500;
  transition: background .18s var(--ease), border-color .18s var(--ease), color .18s var(--ease), transform .18s var(--ease);
}
.auth-redesign .auth-secondary-cta:hover{
  background: rgba(255,255,255,0.06);
  border-color: rgba(var(--accent-rgb), 0.45);
  color: var(--text);
  transform: translateY(-1px);
}
.auth-redesign .auth-secondary-cta strong{
  color: var(--accent-3);
  font-weight: 700;
  margin-inline-start: 4px;
}
.auth-redesign .auth-secondary-cta svg{
  color: var(--text-d);
  flex-shrink: 0;
}

/* ─── Helper hint ───────────────────────────────────────────────── */
.auth-redesign .auth-hint{
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  margin-top: 14px;
  font-size: 0.8rem;
  color: var(--text-d);
  text-align: center;
}
.auth-redesign .auth-hint svg{ flex-shrink: 0; opacity: 0.8; }

/* ─── Light-theme adjustments ───────────────────────────────────── */
html[data-theme="light"] body.auth-page.auth-redesign{
  background: var(--auth-bg);
}
html[data-theme="light"] .auth-redesign .auth-card{
  background:
    linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.78)),
    var(--auth-card);
  border-color: var(--auth-card-border);
  box-shadow:
    0 18px 56px rgba(40,40,60,0.10),
    inset 0 1px 0 rgba(255,255,255,0.40);
}
html[data-theme="light"] .auth-redesign .auth-icon-chip,
html[data-theme="light"] .auth-redesign .auth-input,
html[data-theme="light"] .auth-redesign .auth-secondary-cta{
  background: #ffffff;
  border-color: var(--auth-input-border);
  color: var(--text);
}
html[data-theme="light"] .auth-redesign .auth-icon-chip{ color: var(--text-m); }
html[data-theme="light"] .auth-redesign .auth-input:focus{
  background: #ffffff;
  border-color: rgba(var(--accent-rgb), 0.65);
}
html[data-theme="light"] .auth-redesign .auth-divider::before,
html[data-theme="light"] .auth-redesign .auth-divider::after{
  /* A-A: see note above. */
  background: linear-gradient(to left, transparent, rgba(0,0,0,0.12), transparent);
}
html[data-theme="light"] .auth-redesign .btn-glass.auth-cta{
  color: #ffffff;
  /* Solid coloured fill on light theme — the gradient ring would
     disappear against a white card. The fill itself carries the brand
     gradient instead. */
  border: 1.5px solid transparent;
  background:
    linear-gradient(135deg,
      rgba(var(--accent-rgb), 0.95),
      rgba(var(--accent-rgb), 0.72) 100%) padding-box,
    linear-gradient(135deg,
      rgba(var(--accent-rgb), 1),
      rgba(var(--accent-rgb), 0.7) 100%) border-box;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.28),
    0 10px 28px rgba(var(--accent-rgb), 0.32);
}
html[data-theme="light"] .auth-redesign .btn-glass.auth-cta::before{
  background: radial-gradient(60% 80% at 50% 50%,
    rgba(var(--accent-rgb), 0.32), transparent 70%);
  opacity: 0.5;
}

/* ─── Small screens ─────────────────────────────────────────────── */
@media (max-width: 380px){
  .auth-redesign .auth-icon-chip{ flex-basis: 46px; width: 46px; }
  .auth-redesign .auth-field-row{ gap: var(--sp-2); min-height: 48px; }
  /* The caption's vertical offset is half the ROW's min-height, and the row
     narrows from 52px to 48px here — so the offset must follow it or the caption
     rides 2px low on every phone. Stated as half of the value on the line above
     (48/2), the same way the horizontal offset tracks the chip on the line
     before it. */
  .auth-redesign .auth-field > :is(label):has(+ .auth-field-row){ top: 24px; }
  .auth-redesign .auth-input{ min-height: 48px; padding: 0 var(--sp-3); font-size: 0.94rem; }
  /* `--auth-caption-start` (and its eyed twin, `--auth-caption-clearing-eye`)
     are NOT restated here. They used to be, for the good reason that this block
     is where the chip and the gap they are a sum of are narrowed — but the
     declaration could not win: `.auth-redesign .auth-field` is (0,2,0) in both
     sheets and glass.css loads FIRST, so login.php's inline block always won.
     The caption sat 12px out of alignment at this breakpoint for as long as the
     line existed, and the line read as the value in force the whole time. Both
     tokens are declared by login.php's own `@media (max-width: 380px)`, next to
     the sums. Do not move them back. */

  /* Must clear the eye button, which is NOT narrowed at this breakpoint: it
     stays a 44px touch target at a 6px inset = 50px of occupied space. The old
     40px predates UI-0054 widening the button, so the value overlapped it by
     10px here even after the desktop slot was corrected. This rule only exists
     because the `.auth-input{ padding: 0 var(--sp-3) }` shorthand above resets
     both sides, so the trailing slot has to be re-applied. */
  .auth-redesign .has-eye .auth-input{ padding-inline-end: 52px; }
  .auth-redesign .btn-glass.auth-cta{ min-height: 50px; font-size: 0.98rem; }
  .auth-redesign .auth-hero-title{ font-size: 1.9rem; }
}

/* Respect reduced-motion */
@media (prefers-reduced-motion: reduce){
  .auth-redesign .btn-glass.auth-cta,
  .auth-redesign .auth-secondary-cta,
  .auth-redesign .auth-icon-chip,
  .auth-redesign .auth-input{ transition: none; }
  .auth-redesign .btn-glass.auth-cta:hover,
  .auth-redesign .auth-secondary-cta:hover{ transform: none; }
}
/* End of §56 — Auth redesign. */

/* ───────────────────────── 57. Toast system ──────────────────────────────
   Container is created on first Toast.show() (assets/app.js). Sits above
   modal overlays (modal = 1000, theme-fab = 1100) so notifications stay
   visible while a modal is open. RTL: anchors to inline-end so the toast
   slides in from the same edge in both directions. */
#toast-container{
  position: fixed;
  inset-block-start: max(16px, env(safe-area-inset-top, 16px));
  inset-inline-end:  max(16px, env(safe-area-inset-right, 16px));
  z-index: 1200;
  display: flex;
  flex-direction: column;
  gap: 10px;
  pointer-events: none;
  max-width: min(360px, calc(100vw - 32px));
}
.toast{
  pointer-events: auto;
  /* UI-0287: flex row so the message and the dismiss button sit side by side. */
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-2);
  background: rgba(20,22,32,0.85);
  border: 1px solid rgba(255,255,255,0.10);
  box-shadow: 0 12px 32px rgba(0,0,0,0.45);
  color: var(--text);
  font-size: var(--fs-2);
  line-height: 1.5;
  animation: pg-toast-in 0.22s cubic-bezier(0.2, 0.8, 0.2, 1) both;
  will-change: transform, opacity;
}
/* UI-0287: dismiss button — lets error toasts (which now linger longer) be
   closed immediately instead of waiting out the timer. */
.toast-msg{ flex: 1 1 auto; min-width: 0; }
.toast-close{
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: inherit;
  opacity: 0.6;
  cursor: pointer;
  font-size: var(--fs-3);
  line-height: 1;
  /* `hit area + focus ring` — the second sub-44px control on the same surface
     family as the modal ✕. A toast dismiss button is the one control a user
     reaches for in a hurry (and the error toasts this panel shows linger for
     7s precisely because they matter), so it gets the full target. padding +
     content-box so the glyph stays its size and the negative margin keeps the
     toast's own padding intact instead of growing the toast. */
  box-sizing: content-box;
  min-width: 44px;
  min-height: 44px;
  padding: 2px var(--sp-1);
  margin: -12px -8px -12px 0;
  border-radius: 6px;
}
.toast-close:hover{ opacity: 1; }
.toast-close:focus-visible{ outline: 2px solid rgba(255,255,255,0.4); outline-offset: 1px; opacity: 1; }
/* Same white-on-white focus loss: the light toast is a near-white panel. */
html[data-theme="light"] .toast-close:focus-visible{ outline-color: rgba(var(--accent-rgb), 0.65); }
.toast.info   { border-color: rgba(var(--accent-rgb),  0.45); box-shadow: 0 12px 32px rgba(0,0,0,0.45), 0 0 0 1px rgba(var(--accent-rgb),  0.25); }
.toast.success{ border-color: rgba(var(--success-rgb), 0.45); box-shadow: 0 12px 32px rgba(0,0,0,0.45), 0 0 0 1px rgba(var(--success-rgb), 0.25); }
.toast.warn   { border-color: rgba(var(--warn-rgb),    0.45); box-shadow: 0 12px 32px rgba(0,0,0,0.45), 0 0 0 1px rgba(var(--warn-rgb),    0.25); }
.toast.error  { border-color: rgba(var(--danger-rgb),  0.45); box-shadow: 0 12px 32px rgba(0,0,0,0.45), 0 0 0 1px rgba(var(--danger-rgb),  0.25); }
.toast.hide{
  animation: pg-toast-out 0.28s cubic-bezier(0.4, 0, 0.6, 1) both;
}
@keyframes pg-toast-in{
  from{ opacity: 0; transform: translateX(12px) translateY(-4px); }
  to  { opacity: 1; transform: translateX(0)   translateY(0);    }
}
@keyframes pg-toast-out{
  from{ opacity: 1; transform: translateX(0)   translateY(0);    }
  to  { opacity: 0; transform: translateX(12px) translateY(-4px); }
}
html[data-theme="light"] .toast{
  background: rgba(255,255,255,0.92);
  border-color: rgba(0,0,0,0.08);
  color: var(--text);
  /* 0.45 black at 32px blur is tuned for a dark canvas; on the light page
     it reads as a dirty smudge under the toast. */
  box-shadow: 0 12px 32px rgba(0,0,0,0.16);
}
/* Severity borders MUST be restated: the rule above is (0,2,1) and outranks
   .toast.info/.success/.warn/.error (0,2,0), so in light theme all four
   severities rendered with the same neutral grey edge and the colour coding
   — the toast's only at-a-glance severity signal — was lost. The 1px
   box-shadow ring did survive, but it sits outside a grey border and is far
   too weak to carry the distinction on its own. */
html[data-theme="light"] .toast.info{
  border-color: rgba(var(--accent-rgb), 0.55);
  box-shadow: 0 12px 32px rgba(0,0,0,0.16), 0 0 0 1px rgba(var(--accent-rgb), 0.20);
}
html[data-theme="light"] .toast.success{
  border-color: rgba(var(--success-rgb), 0.55);
  box-shadow: 0 12px 32px rgba(0,0,0,0.16), 0 0 0 1px rgba(var(--success-rgb), 0.20);
}
html[data-theme="light"] .toast.warn{
  border-color: rgba(var(--warn-rgb), 0.55);
  box-shadow: 0 12px 32px rgba(0,0,0,0.16), 0 0 0 1px rgba(var(--warn-rgb), 0.20);
}
html[data-theme="light"] .toast.error{
  border-color: rgba(var(--danger-rgb), 0.55);
  box-shadow: 0 12px 32px rgba(0,0,0,0.16), 0 0 0 1px rgba(var(--danger-rgb), 0.20);
}
@media (prefers-reduced-motion: reduce){
  .toast, .toast.hide{ animation-duration: 0.001s; }
}
/* End of §57 — Toast system. */

/* ═══════════════════════════════════════════════════════════════════════
   §58  BODY SCROLL LOCK + INERT BACKGROUND (H1.4, H1.9)
   ═══════════════════════════════════════════════════════════════════════
   When any glass-modal is open, app.js toggles body.modal-open. We freeze
   page scroll behind the modal on Android Chrome + iOS Safari, and also
   knock the theme-FAB out of the way so it cannot float above the overlay
   (H1.2). The actual focus trap lives in app.js. */
body.modal-open{
  /* UI-0038: keep the scroll-lock (overflow:hidden + overscroll-behavior)
     but DROP `touch-action: none` — that disabled pinch-zoom for the whole
     body, including modal content, so operators couldn't zoom a deposit
     receipt to verify it on iOS Safari. overflow:hidden already prevents the
     page behind from scrolling. */
  overflow: hidden;
  overscroll-behavior: contain;
}
/* Background siblings are flagged with [inert] from app.js. Belt-and-
   suspenders: also hide focus rings from anything inside an inert subtree
   so the visible focus indicator never lives behind the overlay. */
[inert], [inert] *{
  user-select: none;
  -webkit-user-select: none;
}

/* ═══════════════════════════════════════════════════════════════════════
   §59  GLASS CONFIRM MODAL (H1.10, H6.3)
   ═══════════════════════════════════════════════════════════════════════
   A single reusable modal that replaces window.confirm() for every
   data-confirm action. app.js owns the open/close lifecycle and wires
   the destructive variant. Markup is injected once into <body>; the
   selectors below style that template. The host element is a regular
   .modal-overlay.glass-modal so §9 + §58 already cover scroll-lock and
   the bottom-sheet treatment on mobile. */
#glass-confirm-modal .modal-box{
  width: min(440px, calc(100vw - 32px));
  padding: 22px var(--sp-6);
  gap: var(--sp-4);
}
@media (max-width: 768px){
  #glass-confirm-modal .modal-box{
    width: 100%;
    padding: var(--sp-5) var(--sp-5) max(var(--sp-5), env(safe-area-inset-bottom, var(--sp-5)));
  }
}
.glass-confirm-icon{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 14px;
  font-size: var(--fs-5);
  background: rgba(var(--warn-rgb), 0.14);
  border: 1px solid rgba(var(--warn-rgb), 0.35);
  margin-bottom: 4px;
  flex-shrink: 0;
}
#glass-confirm-modal.is-danger .glass-confirm-icon{
  background: rgba(var(--danger-rgb), 0.14);
  border-color: rgba(var(--danger-rgb), 0.40);
}
.glass-confirm-title{
  margin: 0;
  font-size: var(--fs-3);
  font-weight: 700;
  line-height: 1.4;
  color: var(--text);
  /* Persian text often runs longer than English — let it wrap. */
  word-break: break-word;
}
.glass-confirm-message{
  margin: 0;
  font-size: 0.9rem;
  line-height: 1.65;
  color: var(--text-m, var(--text));
  opacity: 0.85;
  word-break: break-word;
}
.glass-confirm-actions{
  display: flex;
  gap: 10px;
  margin-top: 6px;
  flex-wrap: wrap;
}
.glass-confirm-actions > .btn-glass{
  flex: 1 1 0;
  min-width: 0;
  min-height: 46px;
}
@media (max-width: 380px){
  .glass-confirm-actions{ flex-direction: column-reverse; }
  .glass-confirm-actions > .btn-glass{ width: 100%; }
}

/* ═══════════════════════════════════════════════════════════════════════
   §60  REP MENU — BUTTON CONTROLLER (H1.3, H1.19, H6.4)
   ═══════════════════════════════════════════════════════════════════════
   The mobile rep-card overflow trigger is being migrated from
   <details>/<summary> to a <button aria-haspopup="menu" aria-expanded>.
   These selectors mirror the visual rules used for the details version,
   so legacy unopened cards still render correctly while app.js drives
   the new accessible controller. */
.rep-card-actions .rep-menu-button{
  appearance: none;
  -webkit-appearance: none;
  min-height: 44px;
  width: 44px;
  height: 44px;
  padding: 0;
  min-width: 44px;
  font-size: 1.1rem;
  cursor: pointer;
  /* Phase 5 stage 3a: GHOST icon button — no fill, no border (reduce card noise). */
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-2);
  color: var(--muted-foreground);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}
.rep-card-actions .rep-menu-button:hover,
.rep-card-actions .rep-menu-button:focus-visible{
  background: var(--hover);
  outline: none;
  border-color: transparent;
  color: var(--foreground);
}
.rep-card-actions .rep-menu-button:focus-visible{
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
.rep-card-actions .rep-menu-button[aria-expanded="true"]{
  background: rgba(var(--accent-rgb), 0.18);
  border-color: rgba(var(--accent-rgb), 0.45);
}

/* Lift the open card above the bottom-nav (z-index 150). The panel itself
   gets z-index 200 so it floats over every mobile chrome surface. */
.rep-menu-panel{ z-index: 200; }
body > .rep-menu-panel{ z-index: 1050; }
.glass-rep-card:has(.rep-menu-button[aria-expanded="true"]){ z-index: 200; }
.glass-rep-card:has(.rep-menu-button[aria-expanded="true"]) .rep-card-actions{ z-index: 3; }

/* Truncated rep-card usernames need a visible affordance — pair `title=`
   on the markup with a slight underline-on-hover so operators know they
   can hover/tap-long-press to read the full string. */
.rep-card-name,
.rep-card-username{
  /* Keep the existing ellipsis rules from §40 of glass.css but allow the
     hover/tap state to expand on demand on small screens. */
  cursor: help;
}
/* H6.4: tap-to-expand. JS toggles .is-expanded on the same elements; this
   undoes the ellipsis and allows the string to wrap so the operator can
   read the full name/username on mobile without leaving the card. */
.rep-card-name.is-expanded,
.rep-card-username.is-expanded{
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* ═══════════════════════════════════════════════════════════════════════
   §61  RECEIPT PREVIEW — wallet upload + deposit review (H5.1, H5.5)
   ═══════════════════════════════════════════════════════════════════════
   Both surfaces share the same preview chip: a thumbnail (img or a PDF
   placeholder), the filename, and a "remove" link. The wallet upload zone
   shows it inside the drop zone; the deposit-review modal shows it above
   the action buttons so operators verify before approving. */
.receipt-preview{
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 10px var(--sp-3);
  margin-top: 10px;
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: var(--r-2);
  background: rgba(255,255,255,0.03);
}
html[data-theme="light"] .receipt-preview{
  background: rgba(0,0,0,0.025);
  border-color: rgba(0,0,0,0.08);
}
.receipt-preview .receipt-thumb{
  width: 56px;
  height: 56px;
  border-radius: 10px;
  object-fit: cover;
  background: rgba(0,0,0,0.20);
  flex-shrink: 0;
}
.receipt-preview .receipt-thumb.is-pdf{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.4rem;
  color: var(--text-m);
}
.receipt-preview .receipt-meta{
  flex: 1;
  min-width: 0;
  font-size: 0.85rem;
  line-height: 1.5;
}
.receipt-preview .receipt-meta .receipt-name{
  display: block;
  word-break: break-all;
  font-weight: 600;
}
.receipt-preview .receipt-meta .receipt-size{
  display: block;
  opacity: 0.65;
  font-size: 0.78rem;
}
.receipt-preview .receipt-clear{
  flex-shrink: 0;
  background: transparent;
  border: 1px solid rgba(var(--danger-rgb), 0.40);
  color: rgb(var(--danger-rgb));
  border-radius: 10px;
  padding: 6px var(--sp-3);
  font-size: 0.78rem;
  cursor: pointer;
  font-family: inherit;
}
.receipt-preview .receipt-clear:hover,
.receipt-preview .receipt-clear:focus-visible{
  background: rgba(var(--danger-rgb), 0.10);
  outline: none;
}
.glass-file-upload .file-error{
  display: block;
  margin-top: 6px;
  color: rgb(var(--danger-rgb));
  font-size: 0.8rem;
  line-height: 1.5;
}

/* Deposit-review modal: inline receipt block above the notes field. */
.dep-receipt-block{
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}
.dep-receipt-block .dep-receipt-label{
  font-size: 0.78rem;
  opacity: 0.65;
  font-weight: 600;
}
.dep-receipt-block .dep-receipt-img{
  display: block;
  max-width: 100%;
  max-height: 260px;
  border-radius: var(--r-2);
  border: 1px solid rgba(255,255,255,0.10);
  background: rgba(0,0,0,0.20);
  object-fit: contain;
  cursor: zoom-in;
}
.dep-receipt-block .dep-receipt-pdf{
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3-5);
  border: 1px dashed rgba(255,255,255,0.18);
  border-radius: var(--r-2);
  background: rgba(255,255,255,0.03);
  font-size: 0.85rem;
}
.dep-receipt-block .dep-receipt-pdf .dep-receipt-pdf-icon{ font-size: 1.6rem; }
.dep-receipt-block .dep-receipt-pdf a{
  color: rgb(var(--accent-rgb));
  text-decoration: underline;
  margin-inline-start: auto;
}
.dep-receipt-block .dep-receipt-open{
  align-self: flex-start;
  font-size: 0.78rem;
  color: rgb(var(--accent-rgb));
  text-decoration: underline;
}
html[data-theme="light"] .dep-receipt-block .dep-receipt-pdf{
  background: rgba(0,0,0,0.03);
  border-color: rgba(0,0,0,0.18);
}

/* Reject-reason validation — surfaced inline below the textarea. */
.dep-reason-warn{
  display: none;
  margin-top: 6px;
  font-size: 0.78rem;
  color: rgb(var(--danger-rgb));
}
.dep-reason-warn.show{ display: block; }

/* ═══════════════════════════════════════════════════════════════════════
   §62  TABLET BOTTOM SPACING (A-B)
   ═══════════════════════════════════════════════════════════════════════
   Between 768px and 1023px the bottom-nav is hidden (glass.css §41 only
   renders it ≤768px) but app.css §3 still pads the body by --mob-nav-h.
   Collapse the reserved strip to a normal page bottom on tablet. */
@media (min-width: 768px) and (max-width: 1023.98px){
  body{ padding-bottom: var(--sp-7, 32px); }
}

/* ═══════════════════════════════════════════════════════════════════════
   §63  MOBILE/TABLET TABLE COLLAPSE TWEAKS (A-C, H3.3, H3.15)
   ═══════════════════════════════════════════════════════════════════════
   - Defeat the generic .table[data-label] grid in app.css for tables that
     also collapse via the glass selectors here. The class .glass-tx-table
     wins because it carries `display: block` on <td>; app.css's
     `display: contents` on the same <td> would fight that rule. Belt: an
     explicit override below `.glass-tx-table td` ensures grid never wins.
   - Tablet (769–1023px): hide leaderboard's lowest-priority columns so
     the 9-column dashboard table stops overflowing the card.
   - Mobile 360–390: shrink stat-card values that can run to 4 digits + K
     so they don't overflow the card on narrow phones. */

@media (max-width: 767.98px){
  /* If a .glass-tx-table / .leaderboard / .table-wrap table happens to ALSO
     carry the legacy .table class, force the glass rules to win over
     app.css §10's `display: grid` / `display: contents`.
     PHASE5-TBL: `.tbl-wrap > …` twins deleted here too — see §47. */
  .glass-tx-table.table tbody tr,
  .leaderboard.table tbody tr,
  .table-wrap > table.table:not(.no-collapse) tbody tr{
    display: block;
    grid-template-columns: none;
  }
  .glass-tx-table.table tbody td,
  .leaderboard.table tbody td,
  .table-wrap > table.table:not(.no-collapse) tbody td{
    display: flex;
  }
  .glass-tx-table.table tbody td::before,
  .leaderboard.table tbody td::before,
  .table-wrap > table.table:not(.no-collapse) tbody td::before{
    content: attr(data-label);
    font-size: 0.8rem;
    opacity: 0.65;
    font-weight: 600;
  }

  /* Truncate runaway notes so a single card with a 400-char note doesn't
     stretch and starve the rest. Keep break-word so the cell can wrap if
     it must. */
  .glass-tx-table td,
  .leaderboard td,
  .table-wrap > table td{
    overflow-wrap: anywhere;
    word-break: break-word;
    min-width: 0;
  }
  .glass-tx-table td bdi,
  .leaderboard td bdi{ overflow-wrap: anywhere; word-break: break-all; }
}

/* Mobile 360–390: stat-card numbers can overflow if both the K-suffix
   number and label hit the 2-column tile. Lower the font and let long
   words wrap. */
@media (max-width: 390px){
  .dashboard-stats .stat-val{
    font-size: 1.45rem;
    overflow-wrap: anywhere;
    word-break: break-word;
    min-width: 0;
  }
  .dashboard-stats .stat-lbl{
    font-size: 0.74rem;
    overflow-wrap: anywhere;
  }
  .dashboard-stats .glass-stat-card{
    min-width: 0;
    padding: var(--sp-3-5) var(--sp-3);
  }
}

/* Tablet (768–1023px): the leaderboard fits 9 columns ONLY on ≥1024px.
   In between, hide the two lowest-priority columns (cost + wallet) — the
   key info (sales, profit, action) still shows. Sticky thead helps the
   operator scan a long list. */
@media (min-width: 768px) and (max-width: 1023.98px){
  .leaderboard{ font-size: 0.86rem; }
  /* R6-08: the sticky thead was inert — .table-wrap's overflow-x:auto makes
     the wrapper its own scroll box, and with no max-height it never scrolls
     vertically, so `position:sticky; top:0; z-index` pinned nothing. Only
     the tablet-band header washes (dark + the light twin below) survive. */
  .leaderboard thead{ background: rgba(20,22,32,0.85); }
  html[data-theme="light"] .leaderboard thead{ background: rgba(255,255,255,0.92); }
  /* Commit 6 (#8/#28): positional nth-child(6)/(8) hiding removed — superseded
     by the `.lb-extra` class rule (§ tablet column reduction above). The old
     indices were brittle and, worse, hid "موجودی کیف پول" (the wallet balance)
     on tablet while keeping less useful columns. The font-size + sticky thead
     in this block are still the right call and are kept. */
}
/* End of mobile/tablet table tweaks. */

/* ═══════════════════════════════════════════════════════════════════════
   §64  DISABLED FORM-CONTROL FAMILY (H1.5)
   ═══════════════════════════════════════════════════════════════════════
   The legacy .form-control and the cta-* buttons already have :disabled
   rules. Every other glass input/select/textarea/amount/stepper was
   silently identical to its enabled state — operators couldn't tell when
   an input was locked. One shared rule covers them all; component-level
   rules below it stay intact. */
.glass-input:disabled,
.glass-input[aria-disabled="true"],
.glass-select:disabled,
.glass-select[aria-disabled="true"],
.glass-textarea:disabled,
.glass-amount-input:disabled,
.glass-amount-input[readonly],
.glass-stepper .stepper-input:disabled,
.glass-input-group input:disabled,
.glass-input-group textarea:disabled,
.glass-input-group select:disabled,
.btn-glass:disabled,
.btn-glass[aria-disabled="true"]{
  opacity: 0.55;
  cursor: not-allowed;
  /* Soften the focus ring potential. */
  box-shadow: none;
}
/* Keep the inputs visibly "locked" — slightly dimmer background, no
   hover lift, no caret blink. Borders stay visible so the field shape
   reads. */
.glass-input:disabled,
.glass-select:disabled,
.glass-textarea:disabled,
.glass-amount-input:disabled,
.glass-stepper .stepper-input:disabled,
.glass-input-group input:disabled,
.glass-input-group textarea:disabled,
.glass-input-group select:disabled{
  /* R1-08: both literals are the --fill-recessed / --text-d polarity-base
     pair; light twin deleted — no rgba(0,0,0,0.45) ink on light systems. */
  background: var(--fill-recessed);
  color: var(--text-d);
  -webkit-text-fill-color: var(--text-d);
}
.btn-glass:disabled,
.btn-glass[aria-disabled="true"]{
  transform: none;
}
.btn-glass:disabled::before,
.btn-glass[aria-disabled="true"]::before{ opacity: 0; }

/* ═══════════════════════════════════════════════════════════════════════
   §65  KEYBOARD FOCUS-VISIBLE (H1.6)
   ═══════════════════════════════════════════════════════════════════════
   Several interactive surfaces had no :focus-visible style — keyboard
   users got no indication of where they were. One shared rule with the
   accent ring; per-surface rules above (auth, rep-menu, theme-FAB) win
   when more specific. */
.dur-btn:focus-visible,
.stepper-btn:focus-visible,
.glass-nav-item:focus-visible,
.glass-tab:focus-visible,
.queue-err-details summary:focus-visible,
.rep-action-primary:focus-visible,
.rep-action-secondary:focus-visible,
.glass-file-upload:focus-visible,
.glass-file-upload:has(input:focus-visible),
.url-copy-glass .btn-glass:focus-visible,
.modal-close:focus-visible,
[data-modal-close]:focus-visible,
.dep-receipt-block .dep-receipt-img:focus-visible,
.receipt-preview .receipt-clear:focus-visible,
[data-copy]:focus-visible{
  outline: 2px solid rgba(var(--accent-rgb), 0.65);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.18);
}
/* End of §64–§65 — disabled + focus-visible. */

/* ═══════════════════════════════════════════════════════════════════════
   §66  RENEW PREVIEW SUMMARY (H4.8)
   ═══════════════════════════════════════════════════════════════════════
   Shared by my_subscription_detail.php + subscription_detail.php renew
   modals. The .glass-summary-dl host already lays out dt/dd as a grid;
   here we just give the renew variant a soft frame so the live total
   reads as a "receipt block" inside the modal. */
.renew-preview-dl{
  padding: var(--sp-3) var(--sp-3-5);
  margin: 4px 0 12px;
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--r-2);
  background: rgba(255,255,255,0.03);
  display: grid;
  grid-template-columns: minmax(45%, max-content) 1fr;
  gap: 6px var(--sp-3-5);
  font-size: 0.88rem;
}
html[data-theme="light"] .renew-preview-dl{
  background: rgba(0,0,0,0.025);
  border-color: rgba(0,0,0,0.08);
}
.renew-preview-dl dt{
  opacity: 0.7;
  font-size: 0.8rem;
  align-self: center;
}
.renew-preview-dl dd{
  margin: 0;
  font-variant-numeric: tabular-nums;
  align-self: center;
}
.renew-preview-dl dt.renew-total-label,
.renew-preview-dl dd.renew-total-value{
  border-top: 1px dashed rgba(255,255,255,0.08);
  padding-top: 8px;
  margin-top: 4px;
  opacity: 1;
  font-weight: 700;
}
html[data-theme="light"] .renew-preview-dl dt.renew-total-label,
html[data-theme="light"] .renew-preview-dl dd.renew-total-value{
  border-top-color: rgba(0,0,0,0.08);
}
.renew-preview-dl dd.renew-total-value{ color: rgb(var(--accent-rgb)); font-size: 1rem; }

/* ═══════════════════════════════════════════════════════════════════════
   §67  SETTINGS PAGE — JUMP NAV + BULK HINT + PW EYE (H6.6–H6.9)
   ═══════════════════════════════════════════════════════════════════════ */
.settings-jump{
  position: sticky;
  /* `max()` rather than `+`: the safe-area inset is a FLOOR, not an offset to
     add, or the bar sits 16px + 59px down the page on a Dynamic Island phone
     and floats away from the content it belongs to. Sticky, at the top of a
     page — so on a notched device it would otherwise pin itself under the
     status bar and be unreadable for the whole scroll. */
  top: max(16px, env(safe-area-inset-top, 16px));
  z-index: 30;
  display: flex;
  gap: 6px;
  margin-top: 14px;
  padding: 6px;
  /* R6-07: a landscape notch overlaps the bar's inline edges. Logical ↔ physical
     mapping is load-bearing here: in RTL, inline-start IS physical right. */
  padding-inline-start: max(6px, env(safe-area-inset-right, 6px));
  padding-inline-end: max(6px, env(safe-area-inset-left, 6px));
  overflow-x: auto;
  scrollbar-width: thin;
  border-radius: var(--r-2);
  border: 1px solid rgba(255,255,255,0.08);
  background: rgba(20,22,32,0.85);
}
html[data-theme="light"] .settings-jump{
  background: rgba(255,255,255,0.85);
  border-color: rgba(0,0,0,0.08);
}
.settings-jump-link{
  flex: 0 0 auto;
  padding: var(--sp-2) var(--sp-3);
  border-radius: 10px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.06);
  transition: background 0.15s ease, border-color 0.15s ease;
}
.settings-jump-link:hover,
.settings-jump-link:focus-visible{
  background: rgba(var(--accent-rgb), 0.16);
  border-color: rgba(var(--accent-rgb), 0.40);
  outline: none;
}
html[data-theme="light"] .settings-jump-link{
  background: rgba(0,0,0,0.025);
  border-color: rgba(0,0,0,0.06);
}

.settings-bulk-hint{
  display: block;
  margin-top: 8px;
  padding: var(--sp-2) 10px;
  font-size: 0.78rem;
  line-height: 1.65;
  background: rgba(var(--warn-rgb), 0.08);
  border: 1px solid rgba(var(--warn-rgb), 0.30);
  border-radius: 10px;
  color: var(--text);
  opacity: 1;
}
html[data-theme="light"] .settings-bulk-hint{
  background: rgba(var(--warn-rgb), 0.10);
  border-color: rgba(var(--warn-rgb), 0.35);
}

/* H6.7: eye toggle styling — app.js fills the SVG into the button on
   load and on click. Position inside .pw-wrap.

   DIRECTION PINNING (bug fix): the input inside is dir="ltr" (passwords are
   LTR), so its inline `padding-inline-end: 42px` resolved against the INPUT's
   LTR direction = physical RIGHT. The button, however, resolved
   `inset-inline-end` against .pw-wrap, which inherited the page's RTL =
   physical LEFT. Padding was reserved on one side and the button drawn on the
   other, so the typed password ran straight through the eye icon.
   Pinning the wrap to ltr makes both resolve the same way (both = right),
   which is the same fix .buy-input-wrap already documents. */
.pw-wrap{ position: relative; direction: ltr; }
.pw-wrap .pw-eye-btn{
  position: absolute;
  inset-inline-end: 8px;
  inset-block-start: 50%;
  transform: translateY(-50%);
  width: 28px;
  height: 28px;
  padding: 0;
  background: transparent;
  border: none;
  color: var(--text-m, var(--text));
  cursor: pointer;
  border-radius: var(--r-1);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.pw-wrap .pw-eye-btn:hover,
.pw-wrap .pw-eye-btn:focus-visible{
  background: rgba(255,255,255,0.08);
  color: var(--text);
  outline: none;
}
html[data-theme="light"] .pw-wrap .pw-eye-btn:hover,
html[data-theme="light"] .pw-wrap .pw-eye-btn:focus-visible{
  background: rgba(0,0,0,0.05);
}

/* H6.8: tiny unsaved-changes indicator next to each section's save
   button. The settings page is long; without a hint, operators forget
   they edited a field and navigate away. The indicator is rendered by
   the scoped IIFE in settings.php and is only visible while the form
   carries data-dirty="1". The save button gets an extra accent
   highlight in the same state. */
.settings-unsaved-indicator{
  /* UI-0235: keep the pill in the layout (reserved space) and toggle
     visibility/opacity instead of display:none → inline-flex, so the save
     row doesn't jump when the form becomes dirty. */
  display: inline-flex;
  visibility: hidden;
  opacity: 0;
  transition: opacity .15s ease;
  align-items: center;
  gap: 6px;
  margin-inline-start: 10px;
  padding: var(--sp-1) 10px;
  border-radius: var(--r-full);
  font-size: 0.78rem;
  font-weight: 600;
  /* R1-07: warn ink on the per-system token — the previous claude-dark
     amber literal failed contrast on light systems; light twin deleted below. */
  color: rgb(var(--warn-text-rgb));
  background: rgba(var(--warn-rgb), 0.10);
  border: 1px solid rgba(var(--warn-rgb), 0.35);
}
form[data-dirty="1"] .settings-unsaved-indicator{ visibility: visible; opacity: 1; }
form[data-dirty="1"] .btn-glass.primary{
  box-shadow: 0 0 0 2px rgba(var(--warn-rgb), 0.45),
              0 6px 18px rgba(var(--warn-rgb), 0.15);
}
/* R1-07: light twin deleted — the polarity base supplies --warn-text-rgb. */

/* ═══════════════════════════════════════════════════════════════════════
   §68  MOBILE BOTTOM-NAV LOGOUT BUTTON (H2.4)
   ═══════════════════════════════════════════════════════════════════════
   The logout form acts as a 5th grid cell inside .glass-bottom-nav
   .nav-items. The inner button reuses .glass-nav-item visuals but needs
   the button-default reset (background/border/font) and full-cell sizing
   so the icon + label center the same way as the <a> nav items. */
.glass-bottom-nav .glass-nav-form{
  display: flex;
  margin: 0;
  padding: 0;
  height: 100%;
}
.glass-nav-item.glass-nav-item-button{
  width: 100%;
  height: 100%;
  background: transparent;
  border: none;
  font-family: inherit;
}
.glass-nav-item.glass-nav-item-button:focus-visible{
  outline: 2px solid rgba(var(--accent-rgb), 0.55);
  outline-offset: -3px;
}

/* H2.4: align the desktop logout-form so it doesn't break the .nav
   flex/gap rhythm. The form itself sits inline; the button inherits
   its sibling .btn-glass.danger.sm visuals. */
.nav .logout-form{
  display: inline-flex;
  align-items: center;
}

/* ═══════════════════════════════════════════════════════════════════════
   §69  QUEUE — ERROR PREVIEW + EXPANDER (H3.6)
   ═══════════════════════════════════════════════════════════════════════
   Long last_error strings used to render as a wall of micro-text inside
   a 220px column. The PHP now emits an <details> with a short preview;
   expanding shows the full error inside a scrollable <pre>. */
.queue-err-cell{ max-width: 280px; }
.queue-err-cell code{ font-size: 0.78rem; word-break: break-word; }
.queue-err-details summary{
  cursor: pointer;
  list-style: none;
  display: inline-block;
  max-width: 100%;
  overflow-wrap: anywhere;
}
.queue-err-details summary::-webkit-details-marker{ display: none; }
.queue-err-details summary::marker{ content: ""; }
.queue-err-details[open] summary{ opacity: 0.7; }
.queue-err-details .queue-err-full{
  margin: 6px 0 0;
  padding: var(--sp-2) 10px;
  font-size: 0.78rem;
  line-height: 1.55;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--r-1);
  max-height: 240px;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace;
}
html[data-theme="light"] .queue-err-details .queue-err-full{
  background: rgba(0,0,0,0.04);
  border-color: rgba(0,0,0,0.08);
}
@media (max-width: 768px){
  /* On the card-collapsed mobile layout, allow the error cell to take
     full width and the inline-code preview to wrap freely. */
  .queue-err-cell{ max-width: 100%; min-width: 0; }
}

/* ═══════════════════════════════════════════════════════════════════════
   §70  UTILITY: .is-hidden + <mark> highlight (H1.12, H5.10)
   ═══════════════════════════════════════════════════════════════════════
   The tools-search filter in admin/search.php toggles .is-hidden on
   non-matching cards (assets/app.js IIFE). Without this rule the class
   does nothing visually. */
.is-hidden{ display: none !important; }

/* ═══════════════════════════════════════════════════════════════════════
   §71  REPRESENTATIVES — row helpers (H5.12)
   ═══════════════════════════════════════════════════════════════════════
   Promote a handful of repeated inline-style patterns from
   admin/representatives.php into the design system so the page stops
   leaking physical directional properties (margin-left, etc.). Only
   the safe, identifiable patterns are migrated; the rest stay inline
   to keep the diff small and the visual output unchanged. */
.rep-row-logo{
  width: 28px;
  height: 28px;
  border-radius: 6px;
  object-fit: cover;
  vertical-align: middle;
  /* H5.12: physical margin-left → logical margin-inline-end so the
     gap between the logo and the name sits on the trailing inline
     side in either locale. In Persian RTL the logo precedes the
     name; the gap belongs after it (visually to its left). */
  margin-inline-end: 6px;
}
.rep-row-name{ font-weight: 700; }
.rep-row-username{
  /* Phase 5 stage 2: muted-foreground token instead of opacity-dimming, so the
     @handle stays >=4.5:1 (opacity 0.6 dropped effective contrast under AA). */
  color: var(--muted-foreground);
  font-size: 0.78rem;
  font-family: ui-monospace, monospace;
}
/* The pager. Un-scoped from `.rep-row-pagination` when the other six pages
   adopted it: seven pages with one pager means one block, and a class that only
   ever appears under one page's wrapper is how the eighth page gets its own
   copy. `render_pager()` in config.php emits exactly this markup.
   The buttons are `.btn-glass.neutral.sm`, so §61 already raises them to 44px
   on a coarse pointer — the page numbers and the arrows are 37px on a mouse and
   44px under a finger, without a rule here saying so twice. */
.pager-bar{
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 14px;
  padding-inline: 8px;
  flex-wrap: wrap;
  gap: var(--sp-2);
}
.pager-meta{
  opacity: 0.65;
  font-size: 0.85rem;
}
.pager-nav{
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  align-items: center;
}
/* The active page chip — mirrors .btn-glass.neutral.sm visually but sits in a
   non-interactive <span>, because there is no URL for the page you are on. */
.pager-nav .pager-current{
  pointer-events: none;
  background: rgba(var(--accent-rgb), 0.18);
  border-color: rgba(var(--accent-rgb), 0.45);
  color: var(--text);
}
html[data-theme="light"] .pager-nav .pager-current{
  background: rgba(var(--accent-rgb), 0.14);
  border-color: rgba(var(--accent-rgb), 0.40);
}
.pager-nav .pager-ellipsis{
  padding: 0 6px;
  opacity: 0.55;
  align-self: center;
}
/* Icon-only arrows carry no text, so their width would come from the glyph's
   padding and land narrower than a numbered button beside them. Square them to
   the row and hide the icon from the accessibility tree — the <a> already has
   an `aria-label`, and an SVG next to a live label is announced twice. */
/* `align-self: stretch` rather than a height: an icon-only button has no text
   line box, so it came out 36px beside 40px numbered buttons — a row of buttons
   that does not line up reads as a rendering fault. Stretching to the row makes
   both dimensions the row's instead of a number someone has to keep in step, and
   it leaves §61's 44px touch override as the thing that sets the row height
   under a finger. */
.pager-nav .pager-arrow{ padding-inline: 0; min-width: 37px; align-self: stretch; }
.pager-nav .pager-arrow svg{ width: 16px; height: 16px; }

/* ─── Breadcrumb ────────────────────────────────────────────────────────────
   One nav per detail page, emitted by `render_breadcrumb()` in config.php.
   The four rules that used to sit in subscription_detail.php's inline <style>
   are gone with it: at equal specificity the inline block wins, so a copy left
   behind would have shadowed this one and made the shared version look like it
   did nothing. */
.breadcrumb{ margin-block-start: 14px; font-size: 0.86rem; }
.breadcrumb > ol{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.breadcrumb li{ display: flex; align-items: center; gap: 6px; }
.breadcrumb a{ color: var(--text-m); text-decoration: none; transition: color 0.15s var(--ease); }
.breadcrumb a:hover{ color: var(--text); text-decoration: underline; }
.breadcrumb [aria-current="page"]{ color: var(--text); font-weight: 600; }
.breadcrumb .icon{ opacity: 0.6; }

/* ─── Card / SHEBA number field ─────────────────────────────────────────────
   One control for two identifiers, because a payer does the same thing with
   either: copy the digits, check the holder, send from their bank. Laid out
   LTR — the digits are read left to right in either language — inside an RTL
   page, which is what `dir="ltr"` on the wrapper is for.
   Monospace with tabular figures so the four-digit groups line up and a mistyped
   group is visible as a misaligned column rather than as a subtly different
   string. The checksum is NOT checked here: `card_luhn_ok()` and `shaba_valid()`
   are the only implementations and they run server-side, so this control states
   the SHAPE («۱۶ رقم») and the server states the judgement. */
.card-field{
  display: flex;
  align-items: center;
  gap: 8px;
  padding-inline: 12px;
  border-radius: 10px;
  background: var(--input-bg);
  border: 1px solid var(--border);
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
}
.card-field:focus-within{
  border-color: rgba(var(--accent-rgb), .7);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .18);
}
.card-field-icon{ display: inline-flex; flex: 0 0 auto; color: var(--text-m); }
/* No `letter-spacing`, deliberately, though a tracking-open card number is the
   usual garnish. `bin/test_polish_c10a_glass.php` §19 rejects non-zero tracking
   on any owner that is not on its verified-non-Persian allowlist, and the right
   answer there is to not need an exemption rather than to lengthen a list that
   warns about itself — the same call the OTP field already made. The monospace
   face plus the four-digit groups is the whole signal. */
.card-field-prefix{
  flex: 0 0 auto;
  font-family: ui-monospace, monospace;
  font-size: .9rem;
  color: var(--text-m);
}
.card-field .card-field-input{
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: transparent;
  box-shadow: none;
  padding-inline: 0;
  font-family: ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
}
.card-field .card-field-input:focus{ outline: none; box-shadow: none; }
.card-field-hint{ margin: 4px 0 0; font-size: .78rem; min-height: 1.1em; }
/* The two terminal states the client checks can reach (payment_methods.php's
   pgGroupCardDigits). INK tokens, not fills: this is text on the page's own
   surface, so it takes the same `-text-rgb` pair every other semantic label
   uses and flips polarity with the theme by itself — the mistake this file's
   `--accent`/`--accent-text` pairing rule above exists to prevent. `is-invalid`
   is a warning about the number, not a form-level refusal: the server still
   has the last word and still prints its own message. */
.card-field-hint.is-complete{ color: rgb(var(--success-text-rgb)); }
.card-field-hint.is-invalid{ color: rgb(var(--danger-text-rgb)); }

/* ─── عدد به حروف + amount bounds (Step 2.3) ────────────────────────────────
   The reading under an amount field and the bounds hint beside it. Both are
   TEXT on the page surface, so both take ink tokens for the same reason the
   two rules above do. Min-heights reserve the line so a value that appears and
   disappears does not move the submit button under the operator's finger. */
.amount-words{
  margin: 6px 0 0; min-height: 1.2em;
  font-size: .82rem; font-weight: 600;
  color: var(--text-muted);
}
.amount-bounds-hint{
  margin: 4px 0 0; min-height: 1.1em;
  font-size: .78rem;
}
.amount-bounds-hint.is-ok{ color: rgb(var(--success-text-rgb)); }
.amount-bounds-hint.is-bad{ color: rgb(var(--danger-text-rgb)); }
/* Quick-select chips inside the amount field's own group. Same recipe as the
   wallet page's `.quick-amount`, at the smaller scale a dialog row wants —
   a chip that is 44px tall inside a form section pushes the CTA off a phone
   screen, and these are a convenience, not the primary path. */
.amount-chips{
  display: flex; flex-wrap: wrap; gap: 6px;
  margin-top: 8px;
}
.amount-chip{
  flex: 0 0 auto;
  padding: 6px 12px; min-height: 34px;
  border-radius: var(--r-full);
  border: 1px solid var(--border);
  background: var(--surface-2, transparent);
  color: var(--text);
  font-family: inherit; font-size: .8rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.amount-chip:hover, .amount-chip:focus-visible{ background: var(--hover); outline: none; }
.amount-chip:focus-visible{ outline: 2px solid var(--ring); outline-offset: 2px; }
@media (pointer: coarse){ .amount-chip{ min-height: 44px; } }

/* ─── Dashboard KPI tiles: the mini trend, the count-up, and the tilt ───────
   Three touches from the VibeFarsi dashboard-stats / counter / tilt-card set,
   all of them bound to data the page already computes — the 7-day series above
   all. */
/* Seven bars, one per day, sized by the same ratio the chart uses. `--t-h` is
   the only computed value, carried as a custom property so the page stays
   CSP-clean and no style attribute carries a px height. */
.stat-trend{
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 22px;
  margin-block-start: 10px;
}
.stat-trend span{
  flex: 1 1 0;
  min-width: 0;
  /* `max(2px, …)`, not the percentage's own floor. A zero-sale day at 2% of a
     22px box is 0.44px — invisible — so the day reads as a GAP in the week
     rather than as a day with no sales, which are different facts. The floor is
     a length here because the box is a length; the page emits only the ratio. */
  height: max(2px, var(--t-h, 0%));
  border-radius: 2px;
  background: var(--text);
  opacity: 0.15;
}
/* The last bar is today, matching the chart and the emphasised label there. */
.stat-trend span:last-child{ background: rgb(var(--success-rgb)); opacity: 0.85; }

/* THE TILT — one module for every card that takes it. Ported from
   transitions.dev's 3d-tilt (prototype 19: "3D pointer tilt with cursor
   glare"), where the reference subject is a 192×106 payment card rotating up
   to 40deg. Its card is the same object as the top-up method card below, so
   the wallet page follows it closely. The dashboard tiles had a bare transform
   before this and now share the module, gaining the follow/return split — the
   glare stays off them, for the reason at the `::after` rule.

   Pointer-only, by construction rather than by a feature check in script:
   `hover: hover` and `pointer: fine` both have to hold, so a phone never gets
   the perspective AND never gets a `will-change` layer promoted — and the JS
   that sets the transform self-disables under the same two queries, so neither
   layer is load-bearing on its own. 6deg, not the reference's 40: these cards
   carry 1.45rem money figures and a 19-digit card number, and past ~8deg a
   tilted number reads as skew.

   `transform-style: preserve-3d` is deliberately NOT ported — nothing here is
   lifted in Z, and it would turn each card into its own 3D rendering context.
   `isolation: isolate` IS the port's own requirement: without it the glare's
   `mix-blend-mode: screen` blends with whatever the card happens to sit on,
   which for the method card is the modal's own surface rather than the card.

   Also deliberately absent: `overflow: hidden`. A rotation about the centre
   under a PERSPECTIVE projection does project slightly outside the box — the
   near edge of a wide card at 6deg lands ~2-3% beyond it — and clipping that
   would shave the card's own corners while it turns. The overhang is smaller
   than the padding every call site already sits in, so nothing clips it and
   the page never grows a horizontal scrollbar; `--tilt-perspective` is the
   knob if a call site ever does. */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference){
  /* Two ways in on purpose. The dashboard's 19 tiles have matched
     `.dashboard-stats > .glass-stat-card` positionally since they were built,
     and three suites assert those exact open tags; adding an attribute to 19
     lines would say nothing the position does not. New call sites opt in with
     `data-tilt`, which is explicit and survives a re-nest. */
  .dashboard-stats > .glass-stat-card,
  [data-tilt]{
    position: relative;                /* anchor for the glare; all three card
                                          classes already declare it, so this
                                          changes nothing today */
    isolation: isolate;                /* the glare's blend group is the card */
    transform: perspective(var(--tilt-perspective, 800px))
               rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
    /* The RETURN: slow and decelerating. The reference's 1000ms is most of what
       makes a leave settle rather than snap flat. */
    transition: transform 1000ms var(--ease-out);
    will-change: transform;
  }
  /* The FOLLOW: not a second transition, a shorter duration on the same one —
     that is what makes the card track the pointer instead of chasing it, and
     the longhand leaves the whole property list above intact. */
  .dashboard-stats > .glass-stat-card.is-tilting,
  [data-tilt].is-tilting{ transition-duration: 400ms; }

  /* The glare — `data-tilt` only, NOT the dashboard tiles. Their `::after` is
     already suppressed to `display:none` by that page's own block, under a
     design direction that reads "no colored glow on cards": a sheen added on
     top would be this file overruling a page decision it cannot see. The tiles
     get the follow/return split; the wallet's payment card, which is the
     reference's own subject, gets the sheen too.

     Four soft discs of different radii plus an offset glint, layered at the
     pointer: one gradient reads as a spotlight, four read as light spilling
     across a surface. Position updates instantly and is never recentred on
     leave, so the highlight fades out where the pointer left instead of
     sliding back to the middle on its way out. */
  [data-tilt]::after{
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: inherit;
    pointer-events: none;
    opacity: 0;
    mix-blend-mode: screen;
    transition: opacity 300ms var(--ease-out);
    background:
      radial-gradient(circle 95px at var(--tilt-gx, 50%) var(--tilt-gy, 50%),
        rgba(255,255,255,0.48), rgba(255,255,255,0.06) 52%, rgba(255,255,255,0) 84%),
      radial-gradient(circle 200px at var(--tilt-gx, 50%) var(--tilt-gy, 50%),
        rgba(255,255,255,0.22), rgba(255,255,255,0.04) 58%, rgba(255,255,255,0) 78%),
      radial-gradient(circle 360px at var(--tilt-gx, 50%) var(--tilt-gy, 50%),
        rgba(255,255,255,0.10), rgba(255,255,255,0) 88%),
      radial-gradient(circle 55px at calc(var(--tilt-gx, 50%) + 14px) calc(var(--tilt-gy, 50%) - 17px),
        rgba(255,255,255,0.32), rgba(255,255,255,0) 74%);
  }
  [data-tilt].is-tilting::after{ opacity: var(--tilt-glare, 0.32); }
  /* A white sheen screened onto the light theme's #ffffff card is a no-op, so
     the light theme gets the tilt without a glare — rather than a second blend
     mode maintained in parallel for one of two themes. */
  html[data-theme="light"] [data-tilt]::after{ display: none; }
}

/* ─── Confetti ──────────────────────────────────────────────────────────────
   One burst per success, on the two surfaces where the operator has just
   finished something with money in it (a purchase landing, a receipt recorded).
   NOT on every `.glass-alert.success`: that class is on a dozen pages and
   celebrating a saved label is how a celebration becomes wallpaper.
   The container is `position: fixed` at the bottom centre and `pointer-events:
   none`, so it cannot intercept a click on anything, and `aria-hidden`, because
   a decoration that a screen reader announced would be a lie about the page's
   content. Each piece carries its own trajectory as custom properties — set by
   the script from a DETERMINISTIC hash of its index, never `Math.random()`, so
   the same page refresh produces the same pattern and a rerun is reproducible
   in a screenshot. */
.pg-confetti{
  position: fixed;
  /* The burst starts at the bottom edge, so it starts under the home indicator
     on a phone without this — and it is a phone where the swipe-to-confirm sits
     directly above it. Named as an inset rather than moved to a fixed offset,
     because the point is to be AT the edge and the inset is what the edge is. */
  inset-block-end: env(safe-area-inset-bottom, 0px);
  inset-inline-start: 50%;
  width: 0;
  height: 0;
  pointer-events: none;
  z-index: 60;            /* above the card layer, below the toast layer (70) */
}
.pg-confetti span{
  position: absolute;
  display: block;
  inline-size: var(--c-size, 8px);
  block-size: calc(var(--c-size, 8px) * 0.6);
  background: var(--c-color, var(--accent));
  border-radius: 2px;
  opacity: 0;
  animation: pg-confetti var(--c-dur, 1.6s) cubic-bezier(0.2, 0.6, 0.4, 1) var(--c-delay, 0s) both;
}
@keyframes pg-confetti{
  0%   { opacity: 1; transform: translate(0, 0) rotate(0deg); }
  45%  { opacity: 1; transform: translate(var(--c-x, 0px), var(--c-peak, -200px)) rotate(var(--c-rot, 540deg)); }
  100% { opacity: 0; transform: translate(calc(var(--c-x, 0px) * 1.25), var(--c-fall, 80px)) rotate(calc(var(--c-rot, 540deg) * 1.6)); }
}

/* ─── Swipe to confirm ──────────────────────────────────────────────────────
   A mobile-only replacement for the checkout button, and only a replacement:
   the real `<button type="submit">` stays in the DOM and is what actually
   submits, so `data-submit-once`, the RULE-10 form token and every existing
   validation path are untouched. Dragging calls `.click()` on it.
   `dir="rtl"` on the track means the handle starts at the RIGHT and travel is
   measured as a negative dx — the same arithmetic the reference component does,
   read from the computed `direction` rather than assumed. */
.swipe-track{
  position: relative;
  display: none;                    /* the script reveals it under a coarse pointer */
  block-size: 56px;
  border-radius: 999px;
  background: var(--input-bg);
  border: 1px solid var(--border);
  overflow: hidden;
  touch-action: none;
  user-select: none;
}
.swipe-track.is-armed{ display: block; }
.swipe-fill{
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  background: rgba(var(--accent-rgb), 0.22);
  border-radius: 999px;
}
.swipe-label,
.swipe-done{
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text-m);
}
.swipe-done{ color: rgb(var(--success-text-rgb)); opacity: 0; transition: opacity 0.3s var(--ease); }
.swipe-track.is-done .swipe-done{ opacity: 1; }
.swipe-track.is-done .swipe-label{ opacity: 0; }
.swipe-handle{
  position: absolute;
  inset-block: 4px;
  inline-size: 48px;
  border-radius: 999px;
  border: 0;
  background: var(--primary);
  color: var(--primary-foreground);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: grab;
  touch-action: none;
}
.swipe-handle:focus-visible{ outline: 2px solid rgb(var(--accent-rgb)); outline-offset: 2px; }
.swipe-track.is-done .swipe-handle{ background: rgb(var(--success-rgb)); }
/* The desktop button is hidden only on the surface the script armed — the class
   is added by JS, so with scripting off the button is all there is. */
#buy-submit.is-swipe-hidden{ display: none; }

/* ─── Segmented control ─────────────────────────────────────────────────────
   Two or three mutually exclusive views of one list. Both states are visible at
   once, which is the whole point: a single toggle button names only the state it
   would move TO, so a reader has to click it to find out which view they are
   in. The items are LINKS (the notification filter is a query parameter), so
   this works with JavaScript off and each is a real navigation.
   44px under a finger, 30px under a mouse — the same split §61 makes for
   `.btn-glass.sm`, stated here because these are not that button. */
.seg{
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  border-radius: 10px;
  background: var(--input-bg);
  border: 1px solid var(--border);
}
.seg-item{
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 30px;
  padding: 0 12px;
  border-radius: 8px;
  font-size: 0.82rem;
  color: var(--text-m);
  text-decoration: none;
  transition: background 0.15s var(--ease), color 0.15s var(--ease);
}
.seg-item:hover{ color: var(--text); }
.seg-item.is-active{ background: var(--card-soft); color: var(--text); font-weight: 600; }
.seg-count{ font-size: 0.72rem; opacity: 0.75; font-variant-numeric: tabular-nums; }
@media (pointer: coarse){
  .seg-item{ min-height: 44px; }
}

/* ─── Notification inbox ────────────────────────────────────────────────────
   Day headings, a read/unread tone, and an unread dot. The read state used to be
   an inline `style="opacity:.65"` written into the markup of every read row —
   not a rule, so no theme, no media query and no stylesheet could reach it, and
   it is now `.is-read` like everything else. */
.notif-group{
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 18px 0 8px;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-m);
}
.notif-group-n{ font-weight: 400; opacity: 0.7; }
.notif-item.is-read{ opacity: 0.65; }
.notif-dot{
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-inline-start: 8px;
  border-radius: 999px;
  background: var(--accent);
  vertical-align: middle;
}

/* Search-results match highlighting. <mark> default browser styling is
   yellow background + black text — fine on light theme but unreadable
   on dark. Token-aware variant keeps the existing glass identity. */
.search-results mark,
mark.search-hit{
  background: rgba(var(--warn-rgb), 0.30);
  color: inherit;
  padding: 0 2px;
  border-radius: 4px;
  font-weight: 700;
}
html[data-theme="light"] .search-results mark,
html[data-theme="light"] mark.search-hit{
  background: rgba(var(--warn-rgb), 0.45);
}

/* Row-as-link affordance for the search result rows. The PHP wraps a
   data-href onto each <tr>; a tiny JS handler navigates on click. */
.glass-tx-table tr[data-href]{ cursor: pointer; }
.glass-tx-table tr[data-href]:hover td{ background: rgba(var(--accent-rgb), 0.06); }
html[data-theme="light"] .glass-tx-table tr[data-href]:hover td{
  background: rgba(var(--accent-rgb), 0.08);
}

/* ═══════════════════════════════════════════════════════════════════════
   §67  GLASS CHECKBOX / RADIO
   ═══════════════════════════════════════════════════════════════════════
   Drop-in styled replacements for raw browser checkboxes and radios so they
   match the glass input family. OPT-IN via class (.glass-check) — never a
   bare input[type=...] rule — so the pre-existing visually-hidden custom
   toggles (.auto-username-check) keep their own treatment untouched. All colours
   come from the shared tokens (--input-bg / --border / --border-strong /
   --accent / --accent-rgb), so both themes are handled automatically.
   The native control's name/value/checked/JS behaviour is unchanged — this
   only repaints the box. */
/* Selector is `input.glass-check` (not bare `.glass-check`) on purpose: it
   raises specificity to (0,1,1) so the fixed 19×19 box always beats the
   generic `.glass-input-group input { width:100%; padding:10px 12px }` rule
   (also 0,1,1, but earlier in the file). Without this, a .glass-check placed
   inside a .glass-input-group inherits width:100% + padding and — because
   appearance:none lets width apply — stretches to the full container width,
   breaking the row. `padding:0` neutralises the same inherited padding. */
input.glass-check{
  appearance: none; -webkit-appearance: none;
  flex: 0 0 auto;
  box-sizing: border-box;
  width: 19px; height: 19px;
  min-width: 19px;            /* belt-and-braces: never shrink/stretch in a flex row */
  margin: 0;
  padding: 0;                 /* override .glass-input-group input's 10px 12px */
  display: inline-grid;
  place-content: center;
  background: var(--input-bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  vertical-align: middle;
  transition: background var(--dur-1, 120ms) var(--ease, ease),
              border-color var(--dur-1, 120ms) var(--ease, ease),
              box-shadow var(--dur-1, 120ms) var(--ease, ease);
}
.glass-check[type="radio"]{ border-radius: 50%; }

/* The check/dot is drawn in CSS (no SVG asset). Hidden until :checked via a
   scale transform so the toggle animates with the rest of the UI. */
.glass-check::after{
  content: "";
  transform: scale(0);
  transition: transform var(--dur-1, 120ms) var(--ease, ease);
}
.glass-check[type="checkbox"]::after{
  width: 5px; height: 9px;
  border: solid var(--accent-foreground);  /* the system's on-brand ink: white
                                   on indigo, dark on graphite's amber/saffron's gold */
  border-width: 0 2px 2px 0;
  border-radius: 1px;
  margin-block-start: -2px;      /* optical centring after the 45° rotation */
  transform: rotate(45deg) scale(0);
}
.glass-check[type="checkbox"]:checked::after{ transform: rotate(45deg) scale(1); }
.glass-check[type="radio"]::after{
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--accent-foreground);  /* same on-brand ink as the tick */
}
.glass-check[type="radio"]:checked::after{ transform: scale(1); }

.glass-check:checked{
  background: var(--accent);
  border-color: var(--accent);
}
.glass-check:not(:checked):hover{ border-color: var(--border-strong); }
.glass-check:focus-visible{
  outline: none;
  border-color: var(--border-strong);
  box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.18);
}
.glass-check:disabled{ opacity: 0.5; cursor: not-allowed; }


/* ═══════════════════════════════════════════════════════════════════════
   TICKET THREAD — chat-bubble message list for admin/ticket_detail.php
   ═══════════════════════════════════════════════════════════════════════ */
.ticket-thread{
  display: flex;
  flex-direction: column;
  gap: var(--sp-3-5);
}
.ticket-msg{
  max-width: 85%;
  padding: 10px var(--sp-3-5);
  border-radius: 14px;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
}
.ticket-msg.mine{
  align-self: flex-end;
  background: rgba(var(--accent-rgb), 0.14);
  border-color: rgba(var(--accent-rgb), 0.3);
}
.ticket-msg.theirs{ align-self: flex-start; }
.ticket-msg-meta{
  font-size: 0.76rem;
  opacity: 0.6;
  margin-bottom: 6px;
}
.ticket-msg-body{
  font-size: 0.92rem;
  line-height: 1.6;
  word-break: break-word;
  overflow-wrap: anywhere;
}
.ticket-msg-attachments{
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 10px;
}
html[data-theme="light"] .ticket-msg{
  background: rgba(0,0,0,0.03);
  border-color: rgba(0,0,0,0.06);
}
html[data-theme="light"] .ticket-msg.mine{
  background: rgba(var(--accent-rgb), 0.10);
  border-color: rgba(var(--accent-rgb), 0.25);
}
html[data-theme="light"] .ticket-msg-meta{ opacity: 0.78; }
@media (max-width: 600px){
  .ticket-msg{ max-width: 100%; }
}

/* ═══════════════════════════════════════════════════════════════════════
   §47b REPRESENTATIVES — Phase 4 (menu grouping §7, mobile bottom sheets
   §8, consolidated pricing §4/§5, mobile detail affordance). Placed at EOF
   so source order beats the base .rep-menu-panel rules; the mobile
   bottom-sheet override uses !important only where app.js sets inline
   top/left on the portaled panel.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── §7: grouped ⋯ menu (labels + dividers) ──────────────────────────── */
.rep-menu-group{ display: flex; flex-direction: column; gap: 2px; }
.rep-menu-group + .rep-menu-group{
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid rgba(255,255,255,0.09);
}
html[data-theme="light"] .rep-menu-group + .rep-menu-group{ border-top-color: rgba(0,0,0,0.08); }
/* Commit 5 (#84): identity header at the top of the row-actions panel, so a
   charge / impersonate / suspend is always attributable to a named account.
   Sits above the first group and owns the divider, so the group below no
   longer needs its own top border to read as separated. */
.rep-menu-head{
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--border, rgba(255,255,255,0.10));
  margin-bottom: var(--sp-1);
  min-width: 0;
}
.rep-menu-head-name{
  font-weight: 700;
  font-size: 0.86rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rep-menu-head-user{
  font-family: ui-monospace, monospace;
  font-size: 0.75rem;
  opacity: 0.6;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Commit 5 (#86): heading for the expanded row-detail panel. */
.rep-row-detail-title{
  margin: 0 0 var(--sp-2);
  font-size: 0.86rem;
  font-weight: 700;
  opacity: 0.75;
}

.rep-menu-group-label{
  font-size: 0.8rem;   /* Commit 1 (#11): 12px floor */
  font-weight: 700;
  opacity: 0.45;
  padding: var(--sp-1) var(--sp-3) 2px;
}

/* ── §8a: mobile ⋯ becomes a bottom sheet ────────────────────────────── */
@media (max-width: 768px){
  /* app.js portals the panel to <body> and sets inline top/left (no
     !important); these !important rules pin it to the bottom, full-width,
     regardless of where the trigger sits. The big box-shadow spread doubles
     as a full-screen scrim — clicking it lands outside the panel, so app.js's
     existing outside-click handler closes the menu. */
  body > .rep-menu-panel{
    position: fixed !important;
    inset: auto 0 0 0 !important;
    top: auto !important;
    left: 0 !important;
    right: 0 !important;
    width: 100% !important;
    min-width: 0 !important;
    max-width: 100% !important;
    max-height: 72vh;
    max-height: 72dvh;
    overflow-y: auto;
    margin: 0 !important;
    border-radius: var(--r-3) var(--r-3) 0 0;
    /* R6-04: floor clamp on the safe-area inset, matching the .filter-sheet
       above; R6-05: the dvh twin above is load-bearing on pre-15.4 iOS. */
    padding: 10px var(--sp-3) max(var(--sp-3-5), env(safe-area-inset-bottom, var(--sp-3-5)));
    box-shadow: 0 0 0 100vmax rgba(0,0,0,0.5), 0 -12px 40px rgba(0,0,0,0.45);
    animation: rep-bottom-sheet-up 200ms ease-out !important;
  }
  body > .rep-menu-panel .rep-menu-item{ min-height: 48px; }
}
@keyframes rep-bottom-sheet-up{
  from{ transform: translateY(100%); }
  to  { transform: translateY(0); }
}

/* ── §8b: mobile detail affordance on the card + shared detail sheet ──── */
.rep-card-detail-btn{
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px; height: 30px;
  margin-inline-start: 2px;
  border-radius: var(--r-1);
  /* Phase 5 stage 3a: GHOST chevron — no fill/border (reduce card noise). */
  border: 1px solid transparent;
  background: transparent;
  color: var(--muted-foreground);
  cursor: pointer;
  padding: 0;
}
.rep-card-detail-btn .icon{ width: 16px; height: 16px; transform: rotate(-90deg); opacity: 0.8; }
/* Phase 5 stage 3a: soft status/level badges on the mobile rep card — a muted
   fill carries the chip, colored text carries the meaning; no heavy colored
   outline. Scoped to the card so other surfaces' badges are unchanged. */
.glass-rep-card .badge-glass,
.glass-rep-card .glass-tier-badge{
  background: var(--muted);
  border-color: transparent;
}
/* Phase 5 stage 3a: ghost in both themes (was a light-theme box override). */
.rep-card-detail-btn:hover,
.rep-card-detail-btn:focus-visible{ background: var(--hover); outline: none; }
.rep-card-detail-btn:focus-visible{ outline: 2px solid var(--ring); outline-offset: 2px; }
/* Whole mobile card reads as tappable (opens the detail sheet). */
@media (max-width: 1023px){ .glass-rep-card{ cursor: pointer; } }
/* The detail affordance only makes sense on mobile (the desktop table has
   its own inline expander); hide it where the table is shown. */
@media (min-width: 1024px){ .rep-card-detail-btn{ display: none; } }

.rep-sheet-backdrop{
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.5);
  z-index: 1200;
}
.rep-sheet-backdrop[hidden]{ display: none; }
.rep-sheet{
  position: fixed;
  inset: auto 0 0 0;
  z-index: 1201;
  background: var(--surface);
  border-top: 1px solid var(--border);
  border-radius: var(--r-3) var(--r-3) 0 0;
  box-shadow: 0 -12px 40px rgba(0,0,0,0.45);
  padding: var(--sp-2) var(--sp-4-5) max(var(--sp-4-5), env(safe-area-inset-bottom, var(--sp-4-5)));
  max-height: 82vh;
  /* R6-06: dvh twin — a bare 82vh overflows the reduced iOS URL bar. */
  max-height: 82dvh;
  overflow-y: auto;
  animation: rep-bottom-sheet-up 220ms ease-out;
}
.rep-sheet[hidden]{ display: none; }
.rep-sheet-grabber{
  width: 40px; height: 4px; border-radius: var(--r-full);
  background: rgba(255,255,255,0.2);
  margin: 4px auto 12px;
}
html[data-theme="light"] .rep-sheet-grabber{ background: rgba(0,0,0,0.18); }
.rep-sheet-head{ display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); margin-bottom: 14px; }
.rep-sheet-name{ font-size: var(--fs-3); font-weight: 700; }
.rep-sheet-user{ font-size: var(--fs-1); opacity: 0.6; margin-top: 2px; }
.rep-sheet-close{
  flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: transparent; color: inherit; cursor: pointer;
}
.rep-sheet-close .icon{ width: 20px; height: 20px; }
.rep-sheet-grid{
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-3-5) var(--sp-5);
  margin: 0 0 16px;
}
.rep-sheet-grid dt{ font-size: 0.72rem; opacity: 0.55; margin-bottom: 3px; }
.rep-sheet-grid dd{ margin: 0; font-size: 0.92rem; font-variant-numeric: tabular-nums; }
.rep-sheet-foot{ display: flex; }
.rep-sheet-primary{ width: 100%; justify-content: center; }
.rep-sheet-primary[hidden]{ display: none; }

/* ── §4/§5: consolidated, collapsible default-pricing card ───────────── */
details.rep-pricing{ margin-top: 14px; padding: 0; }
.rep-pricing-summary{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-3-5) var(--sp-4-5);
  cursor: pointer;
  list-style: none;
  border-radius: inherit;
}
.rep-pricing-summary::-webkit-details-marker{ display: none; }
.rep-pricing-summary:hover{ background: rgba(255,255,255,0.03); }
html[data-theme="light"] .rep-pricing-summary:hover{ background: rgba(0,0,0,0.02); }
.rep-pricing-summary-main{ display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.rep-pricing-summary-title{ font-weight: 700; font-size: var(--fs-2); }
.rep-pricing-summary-vals{ font-size: 0.8rem; opacity: 0.62; font-variant-numeric: tabular-nums; }
.rep-pricing-edit{
  flex-shrink: 0;
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-1); font-weight: 600;
  color: var(--accent);
  white-space: nowrap;
}
.rep-pricing-edit-icon{ display: inline-flex; }
.rep-pricing-edit-icon .icon{ width: 16px; height: 16px; }
details.rep-pricing[open] .rep-pricing-edit{ opacity: 0.5; }
.rep-pricing-form{ padding: var(--sp-1) var(--sp-4-5) var(--sp-4-5); border-top: 1px solid var(--border); margin-top: 2px; }
.rep-pricing-grid{
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-3-5);
  margin: 14px 0;
}
@media (max-width: 640px){ .rep-pricing-grid{ grid-template-columns: 1fr; } }
.rep-pricing-foot{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
.rep-pricing-foot .faint{ flex: 1; min-width: 200px; font-size: 0.78rem; }
.rep-pricing-save{ flex-shrink: 0; width: 160px; max-width: 100%; justify-content: center; }
@media (max-width: 640px){ .rep-pricing-save{ width: 100%; } }

/* ═══════════════════════════ §52 USAGE METERS ═══════════════════════════
   Volume/time consumption meters. Two forms share one tier colour system:
     • .dot-meter  — minimal 5-dot vector (subscription list rows).
     • .usage-ring — full radial gauge (subscription detail pages).
   Tier colours track % USED (green→amber→orange→red→deep-red), the filled
   dot COUNT tracks % remaining, and both grey out at 100%/expired/deleted.
   State is carried by classes + a --ring-pct custom property (CSP-safe:
   no computed inline widths). See lib/UsageMeter.php + render_dot_meter()/
   render_usage_ring() in config.php. RULE-6 safe — usage reveals no tier. */

:root{
  --dm-green:   var(--success);
  --dm-amber:   var(--warn);
  /* G10-assets: the five-step ramp hardcoded its two middle steps (#ef8a3c /
     #e23b3b) and re-hardcoded both per theme, so the meter's own scale drifted
     from the token scale the rest of the panel uses — and the -rgb triplet
     family (--warn-rgb / --danger-rgb) is already tuned for exactly this job:
     colour used as data, at low alpha, in both themes. The ramp is still five
     visually distinct steps; there is just no longer a second source of truth
     for what "orange" and "deep red" are. */
  --dm-orange:  rgb(var(--warn-rgb));
  --dm-deepred: rgb(var(--danger-rgb));
  --dm-empty:   rgba(255,255,255,0.16);
  --ring-track: rgba(255,255,255,0.10);
  --ring-hole:  var(--card, #14161c);
}
html[data-theme="light"]{
  --dm-empty:   rgba(0,0,0,0.14);
  --ring-track: rgba(0,0,0,0.09);
  --ring-hole:  #ffffff;
}

/* ── Dot meter ─────────────────────────────────────────────────────────── */
.dot-meter{
  display: inline-flex;
  align-items: center;
  gap: 6px;
  vertical-align: middle;
  white-space: nowrap;
}
.dot-meter .dm-icon{
  display: inline-flex;
  align-items: center;
  opacity: 0.75;
}
.dot-meter .dm-icon svg{ width: 14px; height: 14px; }
.dot-meter .dm-dots{ display: inline-flex; gap: 3px; }
.dot-meter .dm-dot{
  width: 7px; height: 7px;
  border-radius: var(--r-full);
  background: var(--dm-empty);
  flex: none;
}
.dot-meter .dm-dot.on{ background: var(--dm-color, var(--dm-green)); }
.dot-meter .dm-label{ font-size: 0.8rem; opacity: 0.85; }

.dot-meter.tier-green  { --dm-color: var(--dm-green);  }
.dot-meter.tier-amber  { --dm-color: var(--dm-amber);  }
.dot-meter.tier-orange { --dm-color: var(--dm-orange); }
.dot-meter.tier-red    { --dm-color: var(--dm-red);    }
.dot-meter.tier-deepred{ --dm-color: var(--dm-deepred);}
.dot-meter.tier-gray, .dot-meter.tier-unknown{ --dm-color: var(--dm-empty); opacity: 0.7; }
.dot-meter.tier-unknown .dm-icon{ opacity: 0.5; }

/* `pulse dot`: this is the "last dot remaining, you are nearly out" signal at
   95–99%, and `steps(2, end)` made it a hard on/off STROBE — a 2-frame flash
   is the shape that reads as a broken element rather than as a warning, and it
   is the shape that matters most to photosensitive users. A halo that scales
   and fades (the same technique `.badge-glass.active` uses above) says the
   same thing without flashing, and shares its 1.8s cycle so the two live
   indicators on a row beat together instead of against each other.
   AUTO-DECISION: opacity on a 7px decorative dot is correct here, and is NOT
   the readability mistake the badge note warns about — the dot carries no
   text, and the tier colour plus the dot COUNT beside it carry the meaning at
   every frame. The rule that note states is about elements whose own text has
   to stay legible. */
.dot-meter .dm-dot.blink{ animation: dm-pulse 1.8s var(--ease-out) infinite; }
@keyframes dm-pulse{
  0%   { opacity: 1;   transform: scale(1); }
  70%  { opacity: 0.35; transform: scale(0.82); }
  100% { opacity: 1;   transform: scale(1); }
}
@media (prefers-reduced-motion: reduce){
  .dot-meter .dm-dot.blink{ animation: none; }
}

/* ── Radial ring (detail pages) ────────────────────────────────────────── */
.usage-rings{
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-5, 20px);
}
.usage-ring-item{
  display: flex;
  align-items: center;
  gap: 14px;
  min-width: 220px;
  flex: 1 1 220px;
}
.usage-ring{
  --ring-color: var(--dm-green);
  position: relative;
  width: 72px; height: 72px;
  flex: none;
  border-radius: var(--r-full);
  display: grid;
  place-items: center;
  background:
    radial-gradient(closest-side, var(--ring-hole) 68%, transparent 69% 100%),
    conic-gradient(var(--ring-color) calc(var(--ring-pct, 0) * 1%), var(--ring-track) 0);
}
.usage-ring .ring-pct{
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text);
}
.usage-ring.tier-green  { --ring-color: var(--dm-green);  }
.usage-ring.tier-amber  { --ring-color: var(--dm-amber);  }
.usage-ring.tier-orange { --ring-color: var(--dm-orange); }
.usage-ring.tier-red    { --ring-color: var(--dm-red);    }
.usage-ring.tier-deepred{ --ring-color: var(--dm-deepred);}
.usage-ring.tier-gray, .usage-ring.tier-unknown{ --ring-color: var(--dm-empty); }
.ring-meta{ min-width: 0; }
.ring-title{ font-weight: 600; font-size: 0.92rem; margin-bottom: 4px; }
.ring-detail{ font-size: 0.82rem; }

/* List-row placement: the two meters sit together, wrapping on narrow rows. */
.row-meters{
  display: inline-flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
@media (max-width: 560px){
  .row-meters{ gap: 8px; }
}

/* ── BRIEF-13: auto-refresh control (tu5pars §4/§10 parity) ────────────────
   .auto-refresh-wrap sits in .head-actions (flex, wraps); the select reuses
   .glass-select sizing, the pill reuses .badge-glass tokens — local rules
   only cover layout + the armed/paused states. Off state reads as a plain
   control; .is-armed tints the pill while counting down. No new colours. */
.auto-refresh-wrap{
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}
.auto-refresh-wrap .glass-select{
  width: auto;
  min-height: 44px;
  height: 44px;
  padding-top: 0;
  padding-bottom: 0;
  font-size: var(--fs-1);
}
.ar-pill{
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-full);
  font-size: 0.8rem;
  font-weight: 600;
  line-height: 1.6;
  background: var(--glass-card-bg);
  border: 1px solid var(--border);
  color: var(--text);
  white-space: nowrap;
}
.auto-refresh-wrap.is-armed .ar-pill{
  border-color: rgba(var(--accent-rgb), 0.5);
}
.auto-refresh-wrap.is-paused .ar-pill{
  opacity: 0.6;
}
.ar-btn{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-1);
  background: var(--glass-btn-bg);
  border: 1px solid var(--border);
  color: var(--text);
  font: inherit;
  font-size: var(--fs-1);
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
}
.ar-btn:hover{
  border-color: rgba(var(--accent-rgb), 0.5);
}
/* .sr-only lives in app.css — not redeclared here (load order: app.css first). */

/* ── BRIEF-15: draggable column layout (tu5pars parity) ───────────────────
   Affordance only; the drag logic lives in initColumnLayout (app.js). */
th[draggable="true"]{ cursor: grab; }
th.is-dragging{ opacity: .5; }
th.is-drop-target{ outline: 1px dashed currentColor; }

/* ═══════════════════════ §60 REDUCED MOTION — the sweep ═════════════════
   G10-assets: there were three `prefers-reduced-motion` blocks in this file
   and none of them was a solution — each named one specific selector
   (`.auth-redesign` transitions, `.toast` duration, `.dm-dot.blink`), so the
   seven @keyframes the file declares were almost all outside any guard:
   `modal-in`, `nav-pill-in`, `nav-more-sheet-in`, `rep-menu-in`,
   `rep-bottom-sheet-up`, `pg-toast-in`/`out` all ran at full motion for a user
   who had asked the OS for less. CRAFT_TIPS_POORIA §3 is blunt about the
   right answer — «همهی انیمیشنها را غیرفعال کن» — and the reason it gives is
   the one that matters here: a cheap phone on a low battery is the device this
   panel is read on, and lag is worse than no animation.
   This block is the CATCH-ALL, deliberately last in the file so it wins on
   order against every animation declared above it. The earlier three blocks
   are left alone: they say something narrower (which transition to drop, how
   long "no animation" should be for a toast) and folding them in here would
   lose that.
   `0.001ms` rather than `animation: none` for the entrance keyframes: several
   of them use `forwards`/`both` fill, and removing the animation outright
   would drop the element back to its 0% frame — an invisible dialog, a
   zero-width nav pill. Ending them immediately keeps the final state, which is
   the entire point of honouring the preference.
   Transforms are reset separately: a transition that is merely shortened still
   moves the element, and `translateX(-2px)` on row hover is motion. */
@media (prefers-reduced-motion: reduce){
  /* One-shot entrances: collapse to their end state. */
  .modal-overlay.glass-modal.open .modal-box,
  .modal-overlay.glass-modal.active .modal-box,
  .modal-overlay.glass-modal[data-open="true"] .modal-box,
  .nav-more.is-desktop.open .nav-more-sheet,
  .nav-more.is-mobile.open .nav-more-sheet,
  .rep-menu-panel,
  .rep-sheet,
  .empty-glass,
  .skeleton,
  .badge-glass.active:not(.glass-tx-table *):not(.leaderboard *):not(.table-wrap *),
  .dot-meter .dm-dot.blink,
  .glass-file-upload,
  /* faq.php's answer reveal. Named here rather than in that page's own block
     for the same reason as everything else in this list: the sweep has to be
     the one place that knows which animations exist, or the next page to add
     one is outside every guard and nobody notices. `!important` is what lets a
     sheet that loads FIRST beat the page's inline declaration — the inline
     block wins on order for everything except this. */
  .faq-item[open] > .faq-answer,
  /* The confetti burst and the swipe handle's release. Both are pure decoration
     over a state the page already shows in words, so the honest answer to "less
     motion" is none at all rather than a shorter version. */
  .pg-confetti span,
  /* The invalid field is the case this block exists for: a 320ms shake is
     EXACTLY the motion a vestibular-sensitive user is asking not to see, and
     it fires at the moment they are already being told something went wrong.
     The red border and the inline message carry the state without it. */
  [aria-invalid="true"],
  .glass-input[aria-invalid="true"],
  .glass-nav-item.active::before,
  .toast,
  .toast.hide,
  /* The wallet card's two hover fades (design pass 2026-09-23). Hover feedback
     survives on background/border, which is the same trade the block above
     makes for `.glass-row:hover`. */
  .glass-balance-card .balance-hide,
  .quick-charge .quick-amount{
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  /* `.glass-row-head` is deliberately NOT in the list above. Its animation is
     scroll-DRIVEN (`animation-timeline: scroll()`), not time-driven, and
     forcing a 0.001ms duration onto a progress-based timeline is
     under-specified — the safe move is to leave the timeline alone and
     override its OUTPUT, which the static hairline below does. */
  /* Positional motion that is not an animation: kill the travel, keep the
     state change. Hover feedback stays legible through background/border. */
  .glass-row:hover,
  .glass-nav-item:active,
  .btn-glass:active,
  .rep-menu-item:active{ transform: none; }
  /* The skeleton and the live dots are LOOPS — they carry meaning by moving.
     With the motion gone they collapse to one frame, so make that frame the
     readable one rather than an arbitrary point of the cycle. */
  .skeleton{ background-image: none; background-color: var(--surface-2); }
  .dot-meter .dm-dot.blink{ opacity: 1; }
  .badge-glass.active:not(.glass-tx-table *):not(.leaderboard *):not(.table-wrap *){ opacity: 1; }
  /* The sticky header keeps its boundary without the scroll-linked shadow —
     a static hairline, since the shadow was the thing conveying "more above". */
  .glass-row-head.is-stuck,
  .glass-row-head{
    animation: none !important;
    box-shadow: var(--row-head-shadow-0, 0 1px 0 rgba(255,255,255,0.06));
  }
  html{ scroll-behavior: auto; }
}
/* End of §60 — reduced motion. */

/* ═══════════════════════════════════════════════════════════════════════
   §61  TOUCH + iOS INPUT ZOOM  (phone / coarse-pointer corrections)

   Two defects measured 2026-09-22 at a 320x568 viewport, both invisible on a
   desktop browser and both on the path of an operator whose device is the
   phone in their pocket (93% of operators are mobile — brief pack).

   1. iOS ZOOM ON FOCUS. The root is 14px on the narrowest phones (app.css
      §responsive), so a control sized from the small tokens computes under
      16px. Measured on the live pages at a 320px viewport: .auth-input 13.16px,
      .glass-input/.glass-textarea/.form-control 12.6px (0.9rem), .glass-select
      13.3px (--fs-2), the audit page's auto-refresh select 11.48px (--fs-1).
      iOS Safari zooms the page for a control under 16px on focus and DOES NOT
      zoom back out, so one tap on the login form leaves the operator magnified
      and panning for the rest of the session.

      `1em` IN A `font-size` RESOLVES AGAINST THE PARENT, not the element's own
      cascaded size — this is the trap in this rule and it is why the list below
      is an enumeration rather than a broad `input, select, textarea` arm. The
      broad arm reaches `.glass-amount-input` (--fs-5 = 1.5rem = 21px) and
      `.glass-stepper .stepper-input` (--fs-4 = 1.2rem = 16.8px), and would
      SHRINK both to 16px, because max(16px, 1em) means "16px, or whatever the
      PARENT is, whichever is larger" — never "or what this control already
      was". Both of those controls are therefore deliberately absent, and the
      first version of this block got it wrong in both directions (it shrank the
      stepper AND named `.glass-amount-inp`, a class that exists nowhere — the
      real one is `.glass-amount-input`).

      GATE CHOICE — `pointer: coarse`, and NOT the `@supports
      (-webkit-touch-callout: none)` that the VibeFarsi rule prescribes, on
      purpose. That gate cannot be exercised anywhere in this repo's toolchain
      (`CSS.supports('-webkit-touch-callout','none')` is false in the only
      browser the suite and the probes can drive), so the fix would ship
      unverifiable and a silent non-match would be indistinguishable from a
      working fix. `(pointer: coarse)` matches on every touch device, was
      confirmed true under the mobile emulation used to measure this, and is
      already this file's idiom for touch corrections (see `filter-clear`).
      Accepted cost: Android also gets 16px controls, which never zoomed —
      that is a readability gain against a 12.6px floor, not a regression.

   2. TARGETS UNDER 44px ON TOUCH. `.filter-clear` was already corrected this
      way (see the `pointer: coarse` block above it) with the note "the 34px box
      is a mouse size" — but the same 34px box on the login page's theme toggle
      and the 37px `.sm` row were never corrected. This is that same correction,
      not a new rule: the idiom is the file's own.

   Appended last in the file so an equal-specificity tie against §13/§20/§38
   resolves here on purpose. `bin/test_touch_and_ios_zoom.php` re-derives the
   list of every control glass.css sizes and fails if one is missing here — it
   caught `.auto-refresh-wrap .glass-select` and the invented class above.
   ═══════════════════════════════════════════════════════════════════════ */

@media (pointer: coarse){
  .glass-input,
  .glass-select,
  .glass-textarea,
  .form-control,
  .auth-input,
  .glass-input-group input,
  .glass-input-group textarea,
  .filters-glass input,
  .filters-glass select,
  /* Two-class selectors that outrank a single class on SPECIFICITY, so file
     order cannot save them. `.auto-refresh-wrap .glass-select` (0,2,0) sizes
     the auto-refresh interval select at var(--fs-1) = 11.48px, and it was the
     one control that survived the first version of this rule — found by
     measuring audit.php, not by reading it. Anything else that sizes a control
     from two classes belongs on this list too. */
  .auto-refresh-wrap .glass-select,
  .auth-redesign .auth-input{
    font-size: max(16px, 1em);
  }
  /* The login page's fixed corner toggle: 38px on desktop, stepped DOWN to
     34px under a mobile media query, i.e. it shrank exactly where the finger
     is. It keeps its 8px inset and its visual weight — only the box grows. */
  .auth-redesign .auth-theme-toggle{ width: 44px; height: 44px; }
  /* `.sm` is the row/pagination/action size and 216 call sites use it. Its
     36px is a mouse height. `.row-actions` already wraps, so the taller box
     costs a wrapped row only where it was 8px away from wrapping anyway. */
  .btn-glass.sm{ min-height: 44px; }
  /* R6-02: `.btn.tiny` is 36px (app.css:1862), a mouse height. Raise the box on
     a coarse pointer so the smallest button still clears the 44px floor. */
  .btn.tiny{ min-height: 44px; }
  /* R6-10: `.actions-toolbar .btn-glass` is 40px for desktop toolbar compactness
     (glass.css:1764). On a coarse pointer, enforce the 44px WCAG 2.5.5 floor. */
  .actions-toolbar .btn-glass{ min-height: 44px; }
  /* The balance card's hide toggle is a bare icon button and was 34px on a
     finger. It grows in place: the head row is `space-between`, so the box
     widens toward the label's side and nothing else on the card moves. */
  .glass-balance-card .balance-hide{ width: 44px; height: 44px; }
}
/* End of §61 — touch. */
