/* ==========================================================================
   Design layer — M6.10
   ==========================================================================

   Supersedes glass.css. That sheet restyled Bootstrap's classes and edited no
   templates, which got the *surface* right — radii, frost, bevel — and left the
   product looking like a Bootstrap form in a rounded box. Surface treatment is
   not design. What was missing is the part that actually carries quality:

     * a component vocabulary (stat, panel, dtable, pill, empty, toolbar) instead
       of `<div class="card p-3 text-center">` written out thirty times,
     * a real application shell — sidebar + topbar — instead of seventeen emoji
       links wrapped in a navbar,
     * typography: Tajawal for Arabic. An Arabic UI set in Segoe UI reads as
       unconsidered no matter what the boxes look like,
     * icons from one set, not emoji,
     * empty states, loading skeletons, and data visualisation.

   Ported from the owner's Dorak frontend — soul, not copy. Its own recipe is in
   DESIGN-SYSTEM.md; this sheet adds the structural half that document did not
   cover because M6.7 only extracted tokens.

   Per-tenant colour still arrives as `--dk-*` custom properties inlined by
   base.html, so everything below tints itself from the shop's own brand.
   ========================================================================== */

/* --------------------------------------------------------------- typography */
/* Three faces, matching the owner's Dorak frontend (M6.19): Tajawal is the Arabic
   identity, Inter is the Latin body and the numerals, and Cause is the display face
   headings are set in — the missing third is most of why Dorak's headings read as
   designed and these read as bold body text.

   Self-hosted, because these shops are on Syrian mobile connections and a blocked or
   slow font CDN means a page rendered in a fallback serif. Split by `unicode-range`
   the way @fontsource ships them, so an Arabic page never downloads Tajawal's Latin
   half to set `MTN` and an English one never downloads the Arabic half at all: 19 files
   on disk, a fraction of that per page. Ranges are @fontsource's verbatim.

   Weights are the ones Dorak loads (src/fonts.ts): Tajawal 300/400/500/700,
   Inter 300/400/500/600/700, Cause 400/600/700. Anything else is faux-bolded by the
   browser — which is what was happening to the 800s Dorak references and never loads. */

@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('../fonts/tajawal-arabic-300.woff2') format('woff2');
  unicode-range: U+0600-06FF,U+0750-077F,U+0870-088E,U+0890-0891,U+0897-08E1,U+08E3-08FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1;
}
@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('../fonts/tajawal-latin-300.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/tajawal-arabic-400.woff2') format('woff2');
  unicode-range: U+0600-06FF,U+0750-077F,U+0870-088E,U+0890-0891,U+0897-08E1,U+08E3-08FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1;
}
@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/tajawal-latin-400.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/tajawal-arabic-500.woff2') format('woff2');
  unicode-range: U+0600-06FF,U+0750-077F,U+0870-088E,U+0890-0891,U+0897-08E1,U+08E3-08FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1;
}
@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/tajawal-latin-500.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/tajawal-arabic-700.woff2') format('woff2');
  unicode-range: U+0600-06FF,U+0750-077F,U+0870-088E,U+0890-0891,U+0897-08E1,U+08E3-08FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1;
}
@font-face {
  font-family: 'Tajawal';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/tajawal-latin-700.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('../fonts/inter-latin-300.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/inter-latin-400.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/inter-latin-500.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/inter-latin-600.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/inter-latin-700.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Cause';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/cause-latin-400.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Cause';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/cause-latin-600.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Cause';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/cause-latin-700.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

/* --------------------------------------------------------------- tokens */

:root {
  /* Brand. base.html overrides every one of these per tenant; the values here
     are the platform default (presentation.DEFAULT_PRIMARY) so the sheet stands
     alone. The *-rgb triplets are what let the tints below follow the tenant's
     hue — CSS cannot decompose a hex. See presentation.palette(). */
  --dk-primary: #9B2C5E;
  --dk-primary-dark: #691E40;
  --dk-primary-darker: #46142A;
  --dk-primary-rgb: 155, 44, 94;
  --dk-grad-rgb: 105, 30, 64;
  --dk-accent-rgb: 105, 30, 64;
  --dk-wash-1: #f9f0f4;
  --dk-wash-2: #f2eaee;
  --dk-on-primary: #ffffff;

  /* Glass alpha ladder: the more "on top" a surface is, the more opaque. */
  --dk-glass: rgba(255, 255, 255, .58);
  --dk-glass-hover: rgba(255, 255, 255, .78);
  --dk-glass-inner: rgba(255, 255, 255, .50);
  --dk-glass-strong: rgba(255, 255, 255, .90);
  --dk-rim: rgba(255, 255, 255, .85);
  --dk-rim-top: rgba(255, 255, 255, .95);
  /* A selected segment, as three values rather than as a rule (M14.17). Every switch in
     the product is the same component -- the `.seg` toolbars and the CSS-only `.swap`
     strips alike: a raised light card in a recessed track. It used to be written out
     four times, twice per theme, and that is how it broke. See `.seg > .is-active`. */
  --dk-seg-on-bg: var(--dk-surface);
  --dk-seg-on-fg: var(--dk-primary-darker);
  --dk-seg-on-shadow: 0 2px 8px rgba(15, 23, 42, .08), inset 0 1px 0 var(--dk-rim-top);

  --dk-blur: blur(24px) saturate(140%);
  --dk-blur-bar: blur(16px) saturate(1.6);

  /* ~x1.45 progression. 14px as the default medium is the single biggest
     contributor to the perceived softness — Bootstrap's default is 6px. */
  --dk-r-xs: 7px;
  --dk-r-sm: 10px;
  --dk-r-md: 14px;
  --dk-r-lg: 22px;
  --dk-r-xl: 32px;
  --dk-r-pill: 999px;

  /* Shadows are cool-tinted slate, never neutral black. */
  --dk-sh-glass: 0 8px 32px rgba(15, 23, 42, .09);
  --dk-sh-lg: 0 20px 60px rgba(15, 23, 42, .14);
  --dk-bevel: inset 0 1px 0 rgba(255, 255, 255, .90);   /* THE signature */
  --dk-bevel-dim: inset 0 1px 0 rgba(255, 255, 255, .22);

  /* The rest of the reference's shadow ladder (M12). Three of these were being
     *used* by this sheet and had never been declared — `--dk-sh-sm` on the palette
     swatch, `--dk-t-pop` and `--dk-ease-spring` on its transition. An undefined
     custom property makes the whole declaration invalid at computed-value time, so
     the swatch had no shadow and no animation at all, and nothing said so. */
  --dk-sh-sm: 0 1px 3px rgba(15, 23, 42, .06), 0 1px 2px rgba(15, 23, 42, .04);
  --dk-sh-md: 0 4px 6px -1px rgba(15, 23, 42, .07), 0 2px 4px -2px rgba(15, 23, 42, .05);
  --dk-sh-subtle: 0 2px 8px rgba(15, 23, 42, .04);
  /* The app-bar islands. Softer and shorter than a panel's: a pill floating 12px
     from the top of the page reads as lifted, not as a card. */
  --dk-sh-bubble: 0 4px 16px rgba(15, 23, 42, .06);
  /* Brand-tinted glows. The reference's `--shadow-primary-*`; a filled button's
     shadow is its own hue at ~30% rather than a neutral drop, which is most of why
     a primary button there reads as lit instead of stuck on. */
  --dk-sh-brand-sm: 0 6px 16px rgba(var(--dk-primary-rgb), .30);
  --dk-sh-brand-md: 0 8px 24px rgba(var(--dk-primary-rgb), .28);
  --dk-sh-brand-lg: 0 14px 32px rgba(var(--dk-primary-rgb), .35);
  --dk-glow: 0 0 0 4px rgba(var(--dk-primary-rgb), .12);
  --dk-glow-ring: 0 0 0 3px rgba(var(--dk-primary-rgb), .26);
  /* One focus ring for every button, in the shop's own hue (M14.31). It sits on
     the page ground rather than on the control, which is what lets a single
     value serve a pale ghost button and a filled brand one. */
  --dk-focus-ring: 0 0 0 3px rgba(var(--dk-primary-rgb), .45);
  --dk-glow-bad: 0 0 0 4px rgba(239, 68, 68, .12);

  /* The hover sweep on a button, and the skeleton's travelling highlight. Light
     variants get the dark sweep, or a white streak over a white fill is invisible. */
  --dk-shimmer: linear-gradient(90deg, transparent, rgba(255, 255, 255, .30), transparent);
  --dk-shimmer-dark: linear-gradient(90deg, transparent, rgba(15, 23, 42, .07), transparent);
  --dk-skel-base: rgba(15, 23, 42, .06);
  --dk-skel-hi: rgba(255, 255, 255, .70);

  /* Text ramp. Inverts as a ramp in dark mode, not value by value. */
  --dk-text-1: #0F172A;
  --dk-text-2: #64748B;
  --dk-text-3: #6B7280;   /* darkened from the reference's #94A3B8, which failed AA */
  /* Field labels, and the same argument one step further (M14.16, owner direction).
     4.76:1 is what `--dk-text-2` gets against pure white, and that was the number that
     made this look like a pass. The card is not white: sampled off a render, the glass
     under a label runs #FDF8FA at the top of the card to #FBF6F9 at the bottom, where
     #64748B measures 4.53:1 and 4.45:1 - straddling AA rather than clearing it, at 12px,
     on the shortest and most-read text in any form. #475569 is 7.21:1 / 7.09:1 on the
     same two pixels.

     Both figures come from sampling the login PNG, not from reading the token: a glass
     card's colour is whatever the ladder above it composited to, and the first version of
     this comment quoted 4.44 / 7.07 off the tokens instead. Measure the picture. */
  --dk-label: #475569;

  /* Semantic triad: saturated text + ~12% tint + ~20% border, same hue each.
     That consistency is what makes pills and alerts feel like one family. */
  --tone-ok: #15803D;
  --tone-ok-bg: rgba(34, 197, 94, .12);
  --tone-ok-bd: rgba(34, 197, 94, .22);
  --tone-warn: #92400E;
  --tone-warn-bg: rgba(245, 158, 11, .12);
  --tone-warn-bd: rgba(245, 158, 11, .22);
  --tone-bad: #991B1B;
  --tone-bad-bg: rgba(239, 68, 68, .10);
  --tone-bad-bd: rgba(239, 68, 68, .20);
  --tone-info: #1E40AF;
  --tone-info-bg: rgba(59, 130, 246, .10);
  --tone-info-bd: rgba(59, 130, 246, .18);
  --tone-mute: #6B7280;
  --tone-mute-bg: rgba(100, 116, 139, .10);
  --tone-mute-bd: rgba(100, 116, 139, .15);
  --tone-brand: var(--dk-primary-darker);
  --tone-brand-bg: rgba(var(--dk-primary-rgb), .12);
  --tone-brand-bd: rgba(var(--dk-primary-rgb), .20);
  /* The brand hue as a *secondary* foreground: the icon, or the language code, that sits
     beside a `--tone-brand` label one step below it. It needs a token because
     `--dk-primary` is the one brand value base.html does not vary by theme -- the shop's
     mulberry is #9B2C5E in both -- and on dark it is darker than the ground it is being
     painted on. Every site that spent it as a colour rather than as a tint measured, with
     `dev.py`, somewhere nobody could read it (M14.22). */
  --tone-brand-soft: var(--dk-primary);

  /* The carriers' own colours (M14.32, owner: "syriatel in red and mtn yellow their
     traditional color but change the color bit to fit dark mode").

     They were `brand` and `info` — the shop's pink and a generic blue — which made
     Syriatel indistinguishable from every other branded thing on the page and gave MTN
     a colour it has never used. An operator scanning a queue of two hundred rows reads
     the carrier by colour long before the word, so these are the two colours the SIM
     cards themselves are printed in.

     The pill keeps the wash-and-ink shape of every other tone rather than going solid.
     `--net-*-solid` is the real brand hex, and it is used where a shape is large enough
     to carry it — the stock bars. In the ink slot it could not be: MTN's #FFCB05 on a
     pale wash is about 1.6:1, which is not a colour, it is a rumour. So the ink darkens
     to the same hue's shadow in light mode and becomes the true yellow in dark, which is
     exactly what `--tone-warn` already does with amber and why the pattern is reused. */
  --net-syr: #B3000C;
  --net-syr-bg: rgba(227, 6, 19, .10);
  --net-syr-bd: rgba(227, 6, 19, .28);
  --net-syr-solid: #E30613;
  --net-mtn: #6B5200;
  --net-mtn-bg: rgba(255, 203, 5, .20);
  --net-mtn-bd: rgba(255, 203, 5, .48);
  --net-mtn-solid: #FFCB05;
  /* The ink a figure is written in *inside* a segment of that colour: the one
     each carrier uses itself. White on Syriatel's red measures 5.35:1; MTN
     prints black on its yellow and that is about 13:1. */
  --net-syr-on: #ffffff;
  --net-mtn-on: #14181F;

  --dk-line: rgba(15, 23, 42, .08);
  --dk-line-glass: rgba(255, 255, 255, .40);
  /* Controls, as distinct from panels.
     `--dk-rim` is the near-white edge that makes a glass panel read as raised against the
     brand-tinted page mesh, and it was being used for inputs and secondary buttons too.
     Those sit INSIDE panels, which are near-white — so the edge vanished and the fill with
     it: the treasury page's "0" button rendered as bare text next to three real buttons,
     and every amount field was an invisible white gap. A control needs a hairline and a
     fill that is darker than its container, not lighter. */
  --dk-ctl-bd: rgba(15, 23, 42, .14);
  --dk-ctl-bg: rgba(15, 23, 42, .035);
  --dk-ctl-bg-hover: rgba(15, 23, 42, .07);
  /* The zebra stripe (M14.34). Deliberately weak: it is a reading aid for tracking one
     row across seven columns, not a second kind of row. Strong enough to survive the
     de-nested fill inside a panel, which is where these tables mostly live. */
  --dk-zebra: rgba(15, 23, 42, .032);
  /* The rows card's fill *inside a panel* (M14.34). De-nesting gave it
     `--dk-glass-inner`, a white at 50% — which over a panel that is already near-white
     composited to (254,253,254) against a (253,251,252) ground. A difference of 5 across
     three channels: the card's 22px corners were being drawn correctly and could not be
     seen, which is exactly what "the left body top border radius is not there" was
     describing. There is no headroom to make it brighter in the light theme, so it reads
     by being a shade darker instead; the dark theme still lifts. */
  --dk-row-inner: rgba(15, 23, 42, .022);
  --dk-surface: rgba(255, 255, 255, .88);
  --dk-surface-2: rgba(255, 255, 255, .50);
  --dk-surface-hover: rgba(255, 255, 255, .82);

  /* One easing for nearly everything. Enter is longer than exit. */
  --dk-ease: cubic-bezier(.22, 1, .36, 1);
  /* The overshoot, for the two controls whose whole job is to *travel*: the switch
     thumb and the checkbox tick. Everything else uses `--dk-ease`. */
  --dk-ease-spring: cubic-bezier(.34, 1.56, .64, 1);
  --dk-t-exit: .18s;
  --dk-t-pop: .22s;
  --dk-t-base: .25s;
  --dk-t-enter: .28s;
  /* The theme sweep's origin and reach. `viewer.js` overwrites all three on the root
     for the length of one theme change -- the centre of the switch that was pressed,
     and the distance from it to the furthest corner of the viewport. Declared here, and
     not left to a `var()` fallback in the keyframes, so the sheet says out loud what
     those keyframes are reading; the values below are what a sweep would look like if
     the script never set them (down the middle from the top edge). */
  --sweep-x: 50%;
  --sweep-y: 0%;
  --sweep-r: 150vmax;

  /* The modal's own measurements (M14.46, owner: "why i do not see white spaces between
     element like we compress the modal to preserve sapce wahil we only have few element").
     Three numbers, because the spacing inside a dialog was nobody's: `.dlg-body` set an
     inset, `.field-row` set a gap, and everything between two blocks was a hand-written
     `mt-3` at four call sites and nothing at the fifth. */
  --dk-dlg-pad: 1.5rem;    /* the modal's inset */
  --dk-dlg-gap: 1.25rem;   /* between the blocks inside one */
  --dk-dlg-gap-x: .75rem;  /* between two fields on the same line */

  --dk-field-h: 46px;   /* the reference's control height; Bootstrap's is 38px */
  --dk-ctl-h: 38px;     /* a toolbar / table-row control, one step down */
  /* The small step, and a token rather than three 30px literals (M14.36): `.btn-sm`,
     the square `.btn-sm.btn-icon` a row of actions is built from, and the carrier-reply
     icon in the status cell of the same row are one unit, and the reply icon was 22px
     until somebody looked at it. */
  --dk-ctl-sm-h: 30px;
  --dk-appbar-h: 48px;  /* every island in the app bar is this tall */
  --dk-orb-1: rgba(var(--dk-primary-rgb), .22);
  /* The accent, not the gradient end-stop: the orbs are the one place a shop's
     second brand colour can be itself, because nothing is ever read on top of it. */
  --dk-orb-2: rgba(var(--dk-accent-rgb), .20);

  --shell-sidebar: 252px;
  --shell-topbar: 64px;

  /* Dorak's three, name for name (M6.19), so the two codebases read side by side.
     `--dk-` stays because base.html inlines the tenant's palette into the same
     namespace. Arabic first: Tajawal is the identity, and it now carries its own
     Latin subset, so `MTN` inside an Arabic line is Tajawal's Latin rather than a
     jump into Inter. Inter stays the face of the numerals — `.money` asks for it
     explicitly, with tabular figures — and of the whole page in LTR. */
  --dk-font-arabic: 'Tajawal', 'Segoe UI', Tahoma, sans-serif;
  /* Tajawal is in both Latin stacks, one word more than Dorak, for a case Dorak does
     not have: an English page here still shows Arabic *data* — a customer's name in a
     table cell, a shop's name in a page title — and neither Inter nor Cause has an
     Arabic glyph. Without this the names fall through to Segoe UI, which is the exact
     "an Arabic UI set in Segoe UI reads as unconsidered" problem this design layer
     exists to fix, surviving in the one place nobody looks: the English rendering.
     A fallback stack, not a `:lang(ar)` rule — see the note by the heading rule. */
  --dk-font-body-en: 'Inter', 'Tajawal', system-ui, sans-serif;
  --dk-font-heading-en: 'Cause', 'Tajawal', system-ui, sans-serif;
  /* The fourth face is the system's. Two rules want it — an audit log's field key and
     the USSD dial templates — and it was being asked for by a name nothing declared,
     each with its own inline fallback list. One token, one stack. */
  --dk-font-mono: ui-monospace, "Cascadia Mono", "Cascadia Code", Consolas,
                  "Courier New", monospace;

  /* Bootstrap bridge — the radius alone carries much of the aesthetic. */
  --bs-border-radius: var(--dk-r-md);
  --bs-border-radius-sm: var(--dk-r-sm);
  --bs-border-radius-lg: var(--dk-r-lg);
  --bs-focus-ring-width: 3px;
  --bs-focus-ring-color: rgba(var(--dk-primary-rgb), .40);
  --bs-body-font-family: var(--dk-font-arabic);
  --bs-body-color: var(--dk-text-1);
}

html[dir="ltr"] { --bs-body-font-family: var(--dk-font-body-en); }

/* Dark: derived, not inverted. Naive inversion gives milky panels with glaring
   white rims. Only the ~20 values that genuinely differ. */
:root[data-bs-theme="dark"] {
  --dk-glass: rgba(255, 255, 255, .06);
  --dk-glass-hover: rgba(255, 255, 255, .10);
  --dk-glass-inner: rgba(255, 255, 255, .04);
  --dk-glass-strong: rgba(255, 255, 255, .12);
  --dk-rim: rgba(255, 255, 255, .10);          /* .85 here would be a glare */
  --dk-rim-top: rgba(255, 255, 255, .16);
  /* The raised segment, on dark. `--dk-surface` is a 5% overlay here, and the segment
     has to be roughly double that to read as raised at all; the bevel that carries it in
     light is invisible, so the shadow does the work instead. */
  --dk-seg-on-bg: rgba(255, 255, 255, .13);
  --dk-seg-on-fg: var(--dk-text-1);
  --dk-seg-on-shadow: 0 2px 8px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255, 255, 255, .12);
  --dk-blur: blur(24px) saturate(120%);        /* 140% oversaturates on dark */
  --dk-sh-glass: 0 8px 32px rgba(0, 0, 0, .45);
  --dk-sh-lg: 0 20px 60px rgba(0, 0, 0, .55);
  --dk-bevel: inset 0 1px 0 rgba(255, 255, 255, .10);
  /* A slate-tinted drop is invisible on a dark ground; the whole ladder has to get
     both darker and stronger, which is the same correction the panel shadow needed. */
  --dk-sh-sm: 0 1px 3px rgba(0, 0, 0, .40), 0 1px 2px rgba(0, 0, 0, .30);
  --dk-sh-md: 0 4px 8px -1px rgba(0, 0, 0, .45), 0 2px 4px -2px rgba(0, 0, 0, .35);
  --dk-sh-subtle: 0 2px 8px rgba(0, 0, 0, .30);
  --dk-sh-bubble: 0 4px 16px rgba(0, 0, 0, .38);
  /* A white streak over a 6%-white fill does nothing; on dark the sweep is the
     light one, and the "dark sweep" for light-variant controls inverts too. */
  --dk-shimmer: linear-gradient(90deg, transparent, rgba(255, 255, 255, .14), transparent);
  --dk-shimmer-dark: linear-gradient(90deg, transparent, rgba(255, 255, 255, .09), transparent);
  --dk-skel-base: rgba(255, 255, 255, .06);
  --dk-skel-hi: rgba(255, 255, 255, .16);

  --dk-text-1: #F1F5F9;
  --dk-text-2: #94A3B8;
  --dk-text-3: #7C8A9C;
  /* Lighter, not darker: on the dark card's own #202228-#1F2026, `--dk-text-2` already
     measures 6.20:1 / 6.33:1, so the move is the other way. #A3B0C2 is 7.23:1 / 7.38:1
     there - past AAA without a label shouting on a dark ground. Sampled, as above. */
  --dk-label: #A3B0C2;

  /* Tints must roughly double — 10% is invisible on dark; text moves to the
     400 shade of the same hue rather than the 800. */
  --tone-ok: #4ADE80;
  --tone-ok-bg: rgba(34, 197, 94, .18);
  --tone-ok-bd: rgba(34, 197, 94, .28);
  --tone-warn: #FBBF24;
  --tone-warn-bg: rgba(245, 158, 11, .18);
  --tone-warn-bd: rgba(245, 158, 11, .28);
  --tone-bad: #FCA5A5;
  --tone-bad-bg: rgba(239, 68, 68, .18);
  --tone-bad-bd: rgba(239, 68, 68, .28);
  --tone-info: #93C5FD;
  --tone-info-bg: rgba(59, 130, 246, .18);
  --tone-info-bd: rgba(59, 130, 246, .28);
  --tone-mute: #94A3B8;
  --tone-mute-bg: rgba(148, 163, 184, .16);
  --tone-mute-bd: rgba(148, 163, 184, .22);
  --tone-brand-bg: rgba(var(--dk-primary-rgb), .20);
  --tone-brand-bd: rgba(var(--dk-primary-rgb), .32);

  /* The carriers, lifted off the dark ground (M14.32). Syriatel's #E30613 is a dark red
     — on a near-black panel it reads as a stain rather than a colour, so the ink and the
     bar both move up to a lighter tint of the same hue. MTN's yellow needs no help in
     the dark; it is the light theme that could not use it. */
  --net-syr: #FF8A80;
  --net-syr-bg: rgba(255, 65, 54, .20);
  --net-syr-bd: rgba(255, 65, 54, .38);
  --net-syr-solid: #FF4136;
  --net-mtn: #FFCB05;
  --net-mtn-bg: rgba(255, 203, 5, .16);
  --net-mtn-bd: rgba(255, 203, 5, .36);
  --net-mtn-solid: #FFCB05;
  --net-syr-on: #2B0A0C;
  --net-mtn-on: #14181F;
  /* `--dk-primary-darker` is the *light* brand shade here -- presentation.palette() pushes
     the readable shade the other way per theme -- so on dark the secondary foreground and
     the label's own colour meet at one value. That compression is the same one the tints
     above take; what carries the step between them instead is the label lifting to
     `--dk-text-1`, which is what every brand-tinted face in this sheet already does. */
  --tone-brand-soft: var(--dk-primary-darker);

  --dk-line: rgba(255, 255, 255, .08);
  --dk-line-glass: rgba(255, 255, 255, .10);
  --dk-ctl-bd: rgba(255, 255, 255, .16);
  --dk-ctl-bg: rgba(255, 255, 255, .06);
  --dk-ctl-bg-hover: rgba(255, 255, 255, .11);
  --dk-zebra: rgba(255, 255, 255, .035);
  --dk-row-inner: rgba(255, 255, 255, .055);
  --dk-surface: rgba(255, 255, 255, .05);
  --dk-focus-ring: 0 0 0 3px rgba(var(--dk-primary-rgb), .60);
  --dk-surface-2: rgba(255, 255, 255, .03);
  --dk-surface-hover: rgba(255, 255, 255, .09);
}

/* --------------------------------------------------------------- base */

/* ------------------------------------------ the scrollbar belongs on the leading edge */
/* An RTL page's scrollbar goes on the **left**, and Chromium will not put the *viewport's*
   there no matter what `direction` says (M14.27). Measured rather than assumed, by
   painting the bar a colour nothing else uses and reading the pixels back:

     plain rtl, viewport scroller     left=0     right=1120     <- the bug
     plain ltr, viewport scroller     left=0     right=1120
     rtl, body as scroller            left=624   right=0        <- honours direction
     rtl, div as scroller             left=480   right=0

   So Blink honours `direction` for an *element* scroller and pins the viewport's to the
   right in both directions. The only fix is to stop scrolling the viewport, which is
   what these two rules do.

   **Gated on `pointer: fine`, deliberately.** Making body the scroll container costs
   things that only matter on a phone: the URL bar stops collapsing on scroll, and
   momentum scrolling goes through a different path on cheap Android WebViews — which is
   the hardware these shops run. A touch device has no visible scrollbar to put on either
   side, so it has nothing to gain here and keeps the native viewport scroll.

   Two dependents, both updated with this: `ui.js` reads the scroll offset from whichever
   element is actually scrolling (an element scroller's `scroll` event does not reach
   `window`), and `tools/dev.py` measures page height off the same, or every full-page
   screenshot crops to one viewport. */
@media (pointer: fine) {
  html { height: 100%; overflow: hidden; }
  body { height: 100%; overflow-y: auto; overflow-x: hidden; }
}

body {
  font-family: var(--bs-body-font-family);
  color: var(--dk-text-1);
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  /* Mesh on body itself, with --bs-body-bg unset, or Bootstrap paints over the orbs. */
  background:
    radial-gradient(circle at 15% 10%, rgba(var(--dk-primary-rgb), .18), transparent 30%),
    radial-gradient(circle at 85% 15%, rgba(var(--dk-accent-rgb), .16), transparent 28%),
    linear-gradient(135deg, var(--dk-wash-1), var(--dk-wash-2));
  background-attachment: fixed;
}

/* Arabic needs a touch more line height than Latin at the same size. */
:lang(ar), [dir="rtl"] { line-height: 1.7; }

h1, h2, h3, h4, h5, h6 { font-weight: 700; color: var(--dk-text-1); }

/* The display face, in LTR only and on h1-h3 only — where Dorak puts it. Arabic keeps
   Tajawal throughout: Cause has no Arabic glyphs at all.

   Dorak also carries a `:lang(ar) { font-family: var(--font-arabic) }` catch-all, and
   porting it cost the entire icon set for one render. Font Awesome sets its family on
   `.fa-solid` — the same specificity as `:lang(ar)` — and ui.css loads second, so on an
   Arabic page every `<i>` inside Arabic content was handed Tajawal and drew tofu. On the
   English page the same markup was perfect, which is why a sweep of one language would
   have shipped it. Arabic-inside-English is handled by the fallback stacks in the tokens
   above instead: fallback resolves per glyph, applies to unmarked text, and cannot lose a
   specificity fight with a component's own font rule. */
html[dir="ltr"] :is(h1, h2, h3) {
  font-family: var(--dk-font-heading-en);
  letter-spacing: -.02em;
}

/* Controls inherit rather than reverting to the UA's own stack — the one line that
   stops a `<select>` and a `<button>` being the two things on the page in Arial. */
input, textarea, select, button { font-family: inherit; }

a { color: var(--dk-primary-darker); text-decoration: none; }
a:hover { color: var(--tone-brand-soft); }
/* Hovering moves a link toward *more* light, and on light that is what the mid brand is:
   #9B2C5E over #46142A. On dark the palette has already given the resting link the light
   shade, so the same rule was making a hovered link recede into the page -- brightening
   is `--dk-text-1` there, the direction the rest of the sheet's dark hovers take. */
:root[data-bs-theme="dark"] a:hover { color: var(--dk-text-1); }

::selection { background: rgba(var(--dk-primary-rgb), .22); }

::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: rgba(var(--dk-primary-rgb), .28);
  border-radius: 4px;
}

/* Decorative orbs. Logical insets, so they mirror in RTL. */
/* The clipping frame — see the comment in base.html for why it exists. */
.orbs {
  position: fixed;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: -1;
}
/* They do not move, and that is the fix for the one performance defect this design had
   (M14.27). They used to drift on `orb-float 14s infinite alternate`, and on a page where
   nothing at all was happening that cost **47% of a core, forever**.

   Measured, because the obvious culprit is the wrong one. The orbs are blurred layers, so
   the guess is that re-blurring three of them 60 times a second is the cost. It is not:
   with `filter: blur()` removed and the animation left running the page still burned
   44.7%, and painting them as pre-softened gradients on a `will-change: transform` layer
   still burned 43.5%. What actually costs is the **nineteen `backdrop-filter` surfaces
   above them**. A frosted surface has to re-blur whatever is behind it whenever that
   changes, so anything moving anywhere under the glass re-blurs most of the page every
   frame — the orbs were merely what was moving. Turning the frost off instead (keeping
   the drift) took it to 12.4%; stopping the drift took it to 4.8%.

   So the choice was not "cheap motion or expensive motion", it was motion or frost, and
   the frost is the design. A 22px drift over 14 seconds is not something anyone sees; the
   fan coming on is. The keyframes are gone rather than commented out, and this note is
   the reason they are not coming back.

   Everything else that animates here is *transient* — an entrance, a toast, a caret — and
   pays this cost for its own duration, which is what an animation is for. `infinite` is
   the word to look for: `skel` and `spin` are the only two left, and both mean "waiting". */
.orb {
  position: absolute;
  border-radius: 999px;
  opacity: .55;
  filter: blur(24px);
}
.orb-1 { width: 340px; height: 340px; background: var(--dk-orb-1); top: -90px; inset-inline-end: -80px; }
.orb-2 { width: 280px; height: 280px; background: var(--dk-orb-2); bottom: 40px; inset-inline-start: -70px; }
.orb-3 { width: 220px; height: 220px; background: var(--dk-orb-1); top: 42%; inset-inline-start: 22%; opacity: .35; }

/* --------------------------------------------------------------- glass */

.glass {
  position: relative;
  background: var(--dk-surface);
  border: 1px solid var(--dk-rim);
  border-top-color: var(--dk-rim-top);
  box-shadow: var(--dk-sh-glass), var(--dk-bevel);
  border-radius: var(--dk-r-md);
}
/* Sheen on ::before — Bootstrap owns ::after for carets and chevrons, and
   putting it there deletes the affordance. */
.glass::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  background: linear-gradient(130deg, rgba(255, 255, 255, .45), transparent 26%,
                              rgba(255, 255, 255, .12) 55%, transparent);
}
:root[data-bs-theme="dark"] .glass::before {
  background: linear-gradient(130deg, rgba(255, 255, 255, .06), transparent 26%,
                              rgba(255, 255, 255, .03) 55%, transparent);
}
@supports (backdrop-filter: blur(1px)) {
  .glass {
    background: linear-gradient(145deg, var(--dk-glass), rgba(255, 255, 255, .38));
    backdrop-filter: var(--dk-blur);
    -webkit-backdrop-filter: var(--dk-blur);
  }
  :root[data-bs-theme="dark"] .glass {
    background: linear-gradient(145deg, var(--dk-glass), rgba(255, 255, 255, .03));
  }
}

/* De-nesting (M12). The reference has an explicit discipline: whenever glass lands
   inside glass it strips the inner one to a flat translucent fill, kills its border,
   its shadow and its sheen, and drops the blur — a second frost over an already
   frosted ground is what turns the effect to mush. Written as a rule rather than as
   a class each nested panel has to remember, so a stat tile inside a panel and a
   table's row card inside a panel are both handled by being nested at all.

   `:where()` keeps it at zero specificity so a component that *wants* to be a real
   raised card inside a panel (`.pop-body`) still wins by naming itself, without an
   `!important` anywhere. */
:where(.glass .glass) {
  background: var(--dk-glass-inner);
  border-color: transparent;
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
:where(.glass .glass)::before { content: none; }

/* The inner surface, named. A panel's sub-panel: a fill and a hairline, no frost. */
.glass-inner {
  background: var(--dk-glass-inner);
  border: 1px solid var(--dk-line-glass);
  border-radius: var(--dk-r-md);
}

/* ==========================================================================
   APP SHELL — sidebar + topbar
   The old navbar was seventeen emoji links on one line, which is a menu, not
   navigation: no grouping, no current-section legibility, unusable at 360px.
   ========================================================================== */

.shell {
  display: grid;
  grid-template-columns: var(--shell-sidebar) minmax(0, 1fr);
  min-height: 100vh;
  transition: grid-template-columns .35s var(--dk-ease);
}
/* Grid tracks follow the writing direction, so track 1 lands on the right in
   RTL with no separate rule. */

/* Collapsed by the burger (ui.js), restored from localStorage before first paint so
   the sidebar does not swing shut a frame after the page appears. The track goes to
   zero and the panel shrinks inside it — animating `max-inline-size` rather than a
   transform, so the main column widens with it instead of being overlapped. */
:root.nav-collapsed .shell { grid-template-columns: 0 minmax(0, 1fr); }
@media (min-width: 901px) {
  .shell-side {
    max-inline-size: var(--shell-sidebar);
    transition: max-inline-size .35s var(--dk-ease), opacity var(--dk-t-base) var(--dk-ease);
  }
  :root.nav-collapsed .shell-side {
    max-inline-size: 0;
    opacity: 0;
    pointer-events: none;
  }
}

.shell-side {
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  display: flex;
  flex-direction: column;
  gap: .25rem;
  padding: 1.125rem .75rem;
  overflow-y: auto;
  overflow-x: hidden;
  /* Glass, like the reference's, rather than a brand-tinted column: the sidebar is a
     surface the page shows through, and the tint was making it the second most
     saturated thing on screen after the primary button. Squared off and hairlined on
     the inner edge, so it reads as a wall rather than as a floating card. */
  border-radius: 0;
  border: 0;
  border-inline-end: 1px solid var(--dk-rim);
  box-shadow: none;
}
.shell-side::before { content: none; }   /* no sheen on a full-height wall */

/* The brand block: mark, name, and the shop's own name as a tinted chip under it,
   divided from the nav by a hairline — the reference's `.sb-logo`. */
.shell-brand {
  display: flex;
  align-items: center;
  gap: .625rem;
  padding: 0 .375rem 1rem;
  margin-bottom: .375rem;
  border-bottom: 1px solid var(--dk-line-glass);
  min-width: 0;
  flex-shrink: 0;
}
/* One number, and both boxes are built from it (M14.81, owner, on a render of a shop
   whose logo is a tall portrait: "see image how it overlap the continer"). The mark was
   `34px` square and the image inside it `width: 100%; height: 100%` — which reads as the
   same size and measured 34x54: twenty pixels of cartoon hanging out of a box with no
   `overflow` to catch it, over the sidebar's hairline and out of the app bar's island.

   **A percentage on a grid item does not resolve against a definite container**, which is
   the whole of it. `place-items: center` leaves the implicit row `auto`-sized, so
   `height: 100%` on the item is a cycle — the track would size the item that sizes the
   track — and Chrome treats it as `auto`. `width: 100%` has a definite inline axis and
   worked, so the image kept its 300x478 aspect and grew downward from a width that was
   right. The two rules beside this one, `.auth-mark img` and `.appbar-mark img`, are
   written in pixels and were never wrong.

   So the size is a variable rather than a percentage: it cannot cycle, the two call sites
   that resize the mark set one value instead of four, and `object-fit: contain` letterboxes
   a portrait logo against the brand fill rather than cropping it. */
.shell-brand-mark {
  --mark: 34px;
  flex: 0 0 auto;
  inline-size: var(--mark); block-size: var(--mark);
  display: grid; place-items: center;
  border-radius: var(--dk-r-sm);
  background: linear-gradient(135deg, var(--dk-primary), var(--dk-primary-dark));
  color: var(--dk-on-primary);
  font-weight: 700;
  font-size: .9rem;
  box-shadow: 0 4px 12px rgba(var(--dk-primary-rgb), .30), var(--dk-bevel-dim);
}
.shell-brand-mark img {
  inline-size: var(--mark); block-size: var(--mark);
  object-fit: contain;
  border-radius: inherit;
}
.shell-brand-text { min-width: 0; }
.shell-brand-name {
  display: block;
  font-weight: 700;
  font-size: .9375rem;
  color: var(--dk-text-1);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.shell-brand-role {
  display: block;
  font-size: .6875rem;
  color: var(--dk-text-2);
}

.shell-group {
  padding: .875rem .625rem .375rem;
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--dk-text-3);
}
[dir="rtl"] .shell-group { text-transform: none; letter-spacing: 0; }

/* A nav item is 44px tall, 14px-rounded and set at 700 — the reference's `.sb-item`.
   It nudges toward the content on hover, which is a directional gesture and therefore
   one of the four things DESIGN-SYSTEM.md says cannot be written logically: the sign
   is flipped by hand per direction. */
.shell-link {
  display: flex;
  align-items: center;
  gap: .625rem;
  min-block-size: 44px;
  padding: .625rem .75rem;
  border-radius: var(--dk-r-md);
  color: var(--dk-text-2);
  font-size: .875rem;
  font-weight: 700;
  transition: background var(--dk-t-pop) var(--dk-ease), color var(--dk-t-pop) var(--dk-ease),
              transform var(--dk-t-pop) var(--dk-ease), box-shadow var(--dk-t-pop) var(--dk-ease);
  position: relative;
}
.shell-link i, .shell-link .shell-ico {
  flex: 0 0 1.125rem;
  width: 1.125rem;
  text-align: center;
  font-size: 1.0625rem;
  color: var(--dk-text-3);
  transition: color var(--dk-t-base);
}
.shell-link:hover { background: var(--dk-glass-inner); color: var(--dk-primary-darker); }
.shell-link:hover i { color: var(--dk-primary-darker); }
[dir="rtl"] .shell-link:hover { transform: translateX(-2px); }
[dir="ltr"] .shell-link:hover { transform: translateX(2px); }
.shell-link.is-active {
  background: var(--dk-surface-hover);
  color: var(--dk-primary-darker);
  box-shadow: 0 4px 12px rgba(15, 23, 42, .06), inset 0 1px 0 var(--dk-rim-top);
}
.shell-link.is-active i { color: var(--tone-brand-soft); }
/* The 3px edge marker that used to sit here is gone (M12). It was carrying "which
   section am I in" back when the active state was a faint tint; the active item is now
   a raised light card with its own shadow, which says it louder — and the marker sat at
   `inset-inline-start: -.75rem`, exactly on the sidebar's own edge, so `overflow-x:
   hidden` was cutting it in half. The reference marks the active item the same way and
   has no marker either. */
:root[data-bs-theme="dark"] .shell-link.is-active { color: var(--dk-text-1); }

.shell-link-badge {
  margin-inline-start: auto;
  min-width: 1.25rem;
  padding: 0 .375rem;
  border-radius: var(--dk-r-pill);
  background: var(--tone-warn-bg);
  color: var(--tone-warn);
  font-size: .6875rem;
  font-weight: 700;
  text-align: center;
}
.shell-spacer { flex: 1 1 auto; }
/* The trailing nav block, divided from the rest — the reference's `.sb-bottom`.
   Logout is not a destination, and putting it in the same undifferentiated run as
   the eighteen that are is how a person clicks it by accident. */
.shell-bottom {
  margin-top: auto;
  padding-top: .5rem;
  border-top: 1px solid var(--dk-line-glass);
  flex-shrink: 0;
}

.shell-main { display: flex; flex-direction: column; min-width: 0; }

/* The mobile drawer's backdrop. `display: none` HAS to be the default, not just a
   mobile rule: the scrim is a child of .shell, so while it is displayed it is a
   grid ITEM — it claimed track 2 and pushed .shell-main onto a second row, 252px
   wide and below a 100vh sidebar. The whole page rendered blank on desktop. */
.shell-scrim { display: none; }

/* ------------------------------------------------------------------ app bar */
/* Islands, not a bar (M12). The reference's app bar is the single most recognisable
   thing about it: the bar itself is *transparent* and holds three floating glass
   pills — controls at the leading edge, the page's name centred, viewer controls at
   the trailing edge — and frosts into a solid bar only once the page has scrolled
   under it. What was here instead was a conventional 64px solid header, which is
   why the two apps read as different products despite sharing every token.

   Three columns of `1fr auto 1fr` and not flexbox, because the centre island has to
   be centred on the *page*, not on what is left after the start zone. */
.shell-top {
  position: sticky;
  top: 0;
  z-index: 1020;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: .625rem;
  padding: .75rem 1rem;
  background: transparent;
  transition: background var(--dk-t-base) ease, box-shadow var(--dk-t-base) ease;
}
/* Stamped by ui.js past 8px of scroll. Without JS the bar simply stays transparent,
   which is the correct fallback: the islands carry their own ground. */
.shell-top.is-scrolled {
  background: var(--dk-glass);
  box-shadow: 0 1px 0 var(--dk-rim), var(--dk-sh-sm);
  -webkit-backdrop-filter: var(--dk-blur-bar);
          backdrop-filter: var(--dk-blur-bar);
}

.shell-zone { display: flex; align-items: center; gap: .5rem; min-width: 0; }
.shell-zone--start { justify-content: flex-start; }
.shell-zone--center { justify-content: center; min-width: 0; }
.shell-zone--end { justify-content: flex-end; }

/* The island itself. Every element in the bar is one of these, so they share a
   height, a rim and a shadow and read as one row of objects. */
.island {
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
  block-size: var(--dk-appbar-h);
  padding-inline: .875rem;
  gap: .5rem;
  border-radius: var(--dk-r-pill);
  border: 1px solid var(--dk-rim);
  border-top-color: var(--dk-rim-top);
  background: var(--dk-glass);
  box-shadow: var(--dk-sh-bubble), var(--dk-bevel);
  color: var(--dk-text-1);
  transition: background var(--dk-t-exit) var(--dk-ease),
              box-shadow var(--dk-t-exit) var(--dk-ease);
}
@supports (backdrop-filter: blur(1px)) {
  .island { -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px); }
}
.island:where(a, button, [role="button"]) { cursor: pointer; }
.island:where(a, button, [role="button"]):hover { background: var(--dk-glass-hover); color: var(--dk-text-1); }
.island:where(a, button, [role="button"]):active { transform: scale(.97); }

/* Square variant — one icon, no label. */
.island--icon {
  inline-size: var(--dk-appbar-h);
  padding-inline: 0;
  justify-content: center;
  color: var(--dk-primary-darker);
  font-size: 1.0625rem;
}
:root[data-bs-theme="dark"] .island--icon { color: var(--dk-text-1); }

/* The way back, beside the burger (M14.44), where the reference puts it. An island like
   everything else in the bar; the arrow points the way the language reads. */
.island--back { font-size: .8125rem; font-weight: 700; color: var(--dk-primary-darker); }
:root[data-bs-theme="dark"] .island--back { color: var(--dk-text-1); }
[dir="ltr"] .island--back .fa-arrow-right { transform: rotate(180deg); }
/* At phone widths it is a square icon: the bar already carries a burger, a title and the
   account menu, and the label is what would push the title island down to nothing. */
@media (max-width: 640px) {
  .island--back {
    inline-size: var(--dk-appbar-h);
    padding-inline: 0;
    justify-content: center;
  }
  .island--back .back-label { display: none; }
}

/* Centre island: the page's name. Capped and ellipsised, because an Arabic page
   title is a phrase and the bar must not grow a second line to hold one. */
.island--title {
  padding-inline: 1.25rem;
  justify-content: center;
  max-inline-size: min(26rem, 46vw);
  animation: island-in var(--dk-t-enter) var(--dk-ease);
}
@keyframes island-in { from { opacity: 0; transform: scale(.88) translateY(-6px); } }

/* Brand island. Present only while the sidebar is collapsed — with the sidebar
   open the shop's name is already on screen 200px away, and two of it is noise.
   Below 900px the sidebar is a drawer, so it is never on screen and the bar carries
   the identity instead; below 640px it goes again, because at 430px the bar is
   already holding a burger, a title and two toggles and the brand is what makes the
   title island 90px wide. The reference draws the same line at the same width. */
.island--brand { display: none; padding-inline: 1rem; }
:root.nav-collapsed .island--brand { display: inline-flex; }
@media (max-width: 900px) { .island--brand { display: inline-flex; } }
@media (max-width: 640px) {
  .island--brand, :root.nav-collapsed .island--brand { display: none; }
  .shell-top, .appbar { padding: .625rem .75rem; gap: .375rem; }
  .shell-top { grid-template-columns: auto 1fr auto; }
  .island--title { max-inline-size: none; padding-inline: .875rem; }
  .shell-zone--start .pill, .shell-zone--end .pill { padding-inline: .75rem; }
}

/* The page's heading, and the only one (M10.1). `min-width: 0` plus the ellipsis
   below is what lets a long title share a 430px bar with a burger and two toggles
   without pushing them off — the failure this replaced was a second `h1` in the
   content, so the bar never had to hold anything difficult. */
.shell-head {
  min-width: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  line-height: 1.15;
}
.shell-title {
  font-size: .9375rem; font-weight: 800; margin: 0;
  line-height: 1.2;
  color: var(--dk-primary-darker);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-inline-size: 100%;
}
:root[data-bs-theme="dark"] .shell-title { color: var(--dk-text-1); }
/* `:empty`, not a conditional in the template: the subtitle block is optional on
   every page and an `if` around a wrapper is one more thing each page can get wrong. */
.shell-sub {
  font-size: .6875rem; font-weight: 600; color: var(--dk-text-3); margin: 0;
  line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-inline-size: 100%;
}
.shell-sub:empty { display: none; }
/* On a phone the bar holds a burger, the title and two toggles, and the subtitle is
   explanatory rather than load-bearing — truncated to "Every order not yet dialed,…"
   it reads as a defect. It goes, rather than growing a sticky bar to two lines. */
@media (max-width: 34rem) { .shell-sub { display: none; } }

.shell-brand-inline { display: inline-flex; align-items: center; gap: .5rem; min-width: 0; }
.shell-brand-inline .shell-brand-mark { --mark: 28px; font-size: .8125rem; }
.shell-brand-inline b {
  font-size: 1rem; font-weight: 800; color: var(--dk-primary-darker);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
:root[data-bs-theme="dark"] .shell-brand-inline b { color: var(--dk-text-1); }

/* A status pill handed to the bar by a page (`top_actions`) rides in the start zone,
   the way the reference's `startSlot` does — and it becomes an island while it is
   there, or it reads as a sticker on the bar rather than as one of its objects.
   The tone keeps its own fill; what it borrows is the height, the rim and the lift. */
.shell-zone--start .pill,
.shell-zone--end .pill {
  block-size: var(--dk-appbar-h);
  padding-inline: 1rem;
  font-size: .75rem;
  border-top-color: var(--dk-rim-top);
  box-shadow: var(--dk-sh-bubble), var(--dk-bevel-dim);
}
/* A page may also hand the bar a real control. Same island height, so the row of
   objects stays one row. */
.shell-zone .btn { min-block-size: var(--dk-appbar-h); border-radius: var(--dk-r-pill); padding-inline: 1.125rem; }

.shell-content { padding: .5rem 1.75rem 2rem; }
@media (max-width: 900px) { .shell-content { padding: .5rem 1rem 1.5rem; } }

/* Page entrance, replayed on every navigation. Server-rendered, so "on route change"
   and "on load" are the same moment — which makes this cheaper here than it is in the
   reference, where it needs a key to retrigger. */
@keyframes page-enter { from { opacity: 0; transform: translateY(8px); } }
.shell-content > *, .app-main > * { animation: page-enter var(--dk-t-enter) var(--dk-ease); }

/* Mobile: the sidebar becomes an off-canvas drawer, opened by :target so it
   needs no JavaScript — this app must work on a cheap Android browser. */
@media (max-width: 900px) {
  .shell, :root.nav-collapsed .shell { grid-template-columns: minmax(0, 1fr); }
  .shell-side {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    width: min(84vw, var(--shell-sidebar));
    z-index: 1080;
    transform: translateX(-102%);
    transition: transform .3s var(--dk-ease);
    box-shadow: var(--dk-sh-lg);
  }
  [dir="rtl"] .shell-side { transform: translateX(102%); }
  .shell-side:target,
  .shell-side.is-open { transform: translateX(0); }
  .shell-side:target + .shell-scrim {
    display: block;
    position: fixed; inset: 0; z-index: 1070;
    background: rgba(15, 23, 42, .35);
  }
}

/* ==========================================================================
   COMPONENTS
   ========================================================================== */

/* `.page-head*` was here and is gone with the macro that drew it (M14.39). Its last
   three callers were the control-plane pages, and they are inside the shell now — the
   topbar holds the name on every surface that has one, which since M14.39 is all of
   them. The comment here used to say "only two surfaces still call `page_head`"; that
   count had been zero for a while and nothing said so, which is the argument for
   deleting a rule rather than leaving it to be rediscovered. */

/* --------------------------------------------------------------- card grid */
/* auto-fit + minmax, not Bootstrap's 12 columns: tiles reflow to the content
   instead of snapping to twelfths, and one rule covers every breakpoint. */
.card-grid {
  display: grid;
  gap: .75rem;
  grid-template-columns: repeat(auto-fit, minmax(var(--min-col, 11rem), 1fr));
  /* `min(..., 100%)` because a minmax() floor larger than the container does not
     shrink — it overflows it. `--min-col: 26rem` is 416px, so on a 430px phone
     every half-width grid pushed its panels 18px past a 398px container and gave
     the whole page a 2px horizontal scroll. Seven pages had it and none of them
     looked wrong; it took measuring the document's scrollWidth to see. The plain
     declaration above stays as the fallback for a browser without min(). */
  grid-template-columns: repeat(auto-fit, minmax(min(var(--min-col, 11rem), 100%), 1fr));
  margin-bottom: 1.25rem;
}
.card-grid--wide { --min-col: 20rem; }
.card-grid--half { --min-col: 26rem; }

/* Staggered entrance, split into two animations on purpose.
   The obvious way — one keyframe fading opacity 0->1, `backwards` fill, a delay
   per child — hides content whenever the animation does not actually run: the
   fill holds the from-state (opacity 0) through the delay, so a print, a
   screenshot, or a WebView that suppresses animations renders blank tiles. It
   did exactly that the first time this page was photographed.
   So: opacity fades once on the CONTAINER with no delay, and the per-child
   stagger moves transform only. If either never runs, the content is visible. */
/* The reference's `card-group-in`, at its duration and its stagger: fade + rise, 0.4s,
   70ms apart. `backwards` hides each tile through its own delay and leaves no transform
   behind once it lands. */
@keyframes tile-rise { from { opacity: 0; transform: translateY(12px); } }
.card-grid > * { animation: tile-rise .4s var(--dk-ease) backwards; }
.card-grid > :nth-child(2) { animation-delay: .07s; }
.card-grid > :nth-child(3) { animation-delay: .14s; }
.card-grid > :nth-child(4) { animation-delay: .21s; }
.card-grid > :nth-child(5) { animation-delay: .28s; }
.card-grid > :nth-child(6) { animation-delay: .35s; }
.card-grid > :nth-child(7) { animation-delay: .42s; }
.card-grid > :nth-child(8) { animation-delay: .49s; }

/* --------------------------------------------------------------- stat tile */
.stat {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .75rem 1rem;
  border-radius: var(--dk-r-md);
  --c: var(--tone-brand-soft);
  --c-bg: rgba(var(--dk-primary-rgb), .12);
}
.stat--ok { --c: var(--tone-ok); --c-bg: var(--tone-ok-bg); }
.stat--warn { --c: var(--tone-warn); --c-bg: var(--tone-warn-bg); }
.stat--bad { --c: var(--tone-bad); --c-bg: var(--tone-bad-bg); }
.stat--info { --c: var(--tone-info); --c-bg: var(--tone-info-bg); }
.stat--mute, .stat--zero { --c: var(--tone-mute); --c-bg: var(--tone-mute-bg); }
/* Quiet, still legible: a zero is information, and it was disappearing into the
   background wash at the muted tint. */
.stat--zero .stat-value { color: var(--dk-text-2); }

.stat-ico {
  flex: 0 0 auto;
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border-radius: var(--dk-r-md);
  background: var(--c-bg);
  color: var(--c);
  font-size: 1rem;
}
.stat-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
/* The label wraps (M10.2). `white-space: nowrap` kept tiles tidy right up until a label
   was longer than its tile, and then it clipped: the control plane's first tile read
   "Today's net profit (all" in English at 430px, with the currency colliding with the
   edge. A grid row stretches to its tallest tile, so a two-line label costs one row a
   few pixels and never costs a word. */
.stat-label { font-size: .75rem; font-weight: 600; color: var(--dk-text-2); line-height: 1.3; }
.stat-value {
  font-size: 1.5rem;
  /* 800, the reference's weight for a headline figure. It only reads as a jump from
     700 at this size — which is exactly where it is used. */
  font-weight: 800;
  line-height: 1;
  color: var(--c);
  font-variant-numeric: tabular-nums;
  /* Latin digits read better in Inter even inside Arabic copy. */
  font-family: var(--dk-font-body-en);
}
.stat-hint { font-size: .6875rem; color: var(--dk-text-3); }
/* The reference's three sizes, name for name. */
.stat--sm { padding: .625rem .75rem; gap: .625rem; }
.stat--sm .stat-ico { width: 32px; height: 32px; font-size: .875rem; }
.stat--sm .stat-label { font-size: .6875rem; }
.stat--sm .stat-value { font-size: 1.25rem; }
.stat--lg { padding: 1rem 1.25rem; gap: .875rem; }
.stat--lg .stat-label { font-size: .8125rem; }
.stat--lg .stat-value { font-size: 2rem; }
.stat--lg .stat-ico { width: 46px; height: 46px; font-size: 1.125rem; }
/* A headline figure takes the row on a phone (M14.39).

   `.stat` is a flex row, and a flex item does not shrink below its content -- so a tile
   holding `37,675 SYP` at 2rem has a min-content width of about 230px and simply
   overflows anything narrower. Measured on the control-plane dashboard at 430px: the
   tile ran x=221..414 and the money inside it x=186..333, with the currency unit sitting
   entirely outside the card.

   Three of the four grids that hold an `lg` tile are at 14-15rem and are already one per
   row here, so this changes only the one that is not -- and it is the right rule for all
   of them: the tile that carries the page's headline number is the tile that should not
   be sharing a row on a 430px screen. Fixing it here rather than by widening that grid's
   `min_col` keeps the three two-digit counts beside it two-up, which is what its own
   comment asks for. */
@media (max-width: 34rem) {
  .card-grid > .stat--lg { grid-column: 1 / -1; }
}

/* --------------------------------------------------------------- panel */
.panel {
  border-radius: var(--dk-r-lg);
  margin-bottom: 1.25rem;
  overflow: hidden;
}
/* Except where something inside has to escape it (M14.58, owner: "it is capped"). The clip
   is what holds the radius over a flush table's cells and `.panel--alert`'s tinted head —
   both of which paint to the edge — but a `<details>` popover is positioned out of flow and
   was being cut off at the panel's bottom edge, so a select opened into a sliver. A panel
   whose body is a form paints nothing to its own edge and loses nothing by not clipping. */
.panel--open { overflow: visible; position: relative; }
/* And raised while one is actually open. Not clipping is only half of escaping: `.glass`
   carries a `backdrop-filter`, which makes **every panel its own stacking context**, so a
   popover that hangs below its panel is still painted inside it and the *next* sibling
   panel covers it — whatever `z-index` the popover asks for, because that index is scoped
   to a context the sibling is not in. Measured: the cash drawer's adjust popover came out
   underneath the MTN stock card, with a carrier pill sitting on top of a button label.
   The panel is the only element where the two can be compared. `:has` is the condition
   because the raise has to be per-open rather than per-panel — two raised panels are the
   same tie, decided by source order again — and where it is missing this rule simply does
   not match, which is the behaviour of the day before. */
.panel--open:has(.pop[open]) { z-index: 5; }
.panel-head {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .875rem 1.25rem;
  border-bottom: 1px solid var(--dk-line-glass);
  /* Above the body, not level with it (M14.71, owner: "see how the table row is
     overlaying the popover"). Both were `position: relative; z-index: 1`, which makes two
     stacking contexts of equal rank — so source order decided it, `.panel-body` comes
     second, and the table painted over anything the head floated. The popover's own
     `z-index: 20` cannot help: it resolves *inside* the head's context, never against the
     body's. Unconditional rather than `:has(.pop[open])`, because a head belongs above
     its body at all times and this way it is also true on the phones `:has()` misses. */
  position: relative; z-index: 2;
}
.panel-title {
  font-size: 1.0625rem;
  font-weight: 800;
  margin: 0;
  color: var(--dk-primary-darker);
  display: flex; align-items: center; gap: .625rem;
}
:root[data-bs-theme="dark"] .panel-title { color: var(--dk-text-1); }
/* The section icon as a tinted **chip**, not a bare glyph beside the words — the
   reference's `.settings-card-icon`. A 14px-rounded 36px square in the brand tint is
   what makes a panel head read as a heading rather than as a line of text with a
   decoration in front of it. */
.panel-title i {
  flex: 0 0 auto;
  display: grid; place-items: center;
  inline-size: 2.25rem; block-size: 2.25rem;
  border-radius: var(--dk-r-md);
  background: rgba(var(--dk-primary-rgb), .10);
  color: var(--tone-brand-soft);
  font-size: .9375rem;
}
.panel--alert .panel-title i { background: var(--tone-bad-bg); }
.panel-head-actions { margin-inline-start: auto; display: flex; gap: .5rem; align-items: center; }
.panel-body { padding: 1.25rem; position: relative; z-index: 1; }
/* Not zero any more (M12): the table inside a flush body is now a floating header bar
   over a rounded card of rows, and both of those need the panel's edge to be somewhere
   other than exactly on them. Small — this is an inset, not padding. */
.panel-body--flush { padding: .625rem; }
/* A panel whose contents are the problem (M6.11): the queue's intervention
   section. The tint is on the head only — tinting the whole panel would put a
   red wash behind every status pill in the table and flatten them. */
.panel--alert .panel-head {
  background: var(--tone-bad-bg);
  border-bottom-color: var(--tone-bad-bd);
  /* Its own corners: the panel's clip used to round these, and a panel holding a
     popover no longer clips (M14.64). */
  border-start-start-radius: var(--dk-r-lg);
  border-start-end-radius: var(--dk-r-lg);
}
.panel--alert .panel-title,
.panel--alert .panel-title i { color: var(--tone-bad); }

/* One line of guidance under a panel head, inside a flush (table) body. */
.panel-note {
  margin: 0;
  padding: .625rem 1.125rem;
  font-size: .8125rem;
  color: var(--dk-text-2);
  border-bottom: 1px solid var(--dk-line);
  position: relative; z-index: 1;
}
/* Inside a flush body the note sits above the header bar, so it takes the body's own
   inset rather than adding a second one on top of it. */
.panel-body--flush > .panel-note { padding: .375rem .625rem .75rem; border-bottom: 0; }

.panel-foot {
  padding: .75rem 1.125rem;
  border-top: 1px solid var(--dk-line-glass);
  display: flex; align-items: center; gap: .5rem;
  position: relative; z-index: 1;
}

/* --------------------------------------------------------------- data table */
/* A real <table> (screen readers and copy-paste both want one) but styled as
   rows: 3.25rem tall, brand-tinted sticky head, hairline separators. */
.dtable-wrap { overflow-x: auto; position: relative; z-index: 1; }
/* `overflow-x: auto` computes `auto` on both axes, so the wrapper cut popovers off — 4
   of 15 rows on /admin/users, by up to 182px. Only while one is open, so a phone keeps its
   sideways scroll the rest of the time (M14.64). */
.dtable-wrap:has(.pop[open]) { overflow: visible; }
.dtable {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: .8125rem;
  /* The gap between the header bar and the card of rows. */
  --dtable-gap: .5rem;
  /* One radius for the bar and the card under it (M14.34, owner: "the header border
     radius bottom is not symmetric with the top one"). They were `--dk-r-md` and
     `--dk-r-lg`: measured off the render, the bar's corner curved over 9px and the card's
     over 18px, so a pair meant to read as one object read as two that happened to be
     stacked. The bar is the smaller box and it is the one that moves. */
  /* **Ceiling: `2 x --dtable-r + --dtable-gap` must not exceed the bar's border-box
     height** (44px). Over that the two vertical radii overlap, CSS scales every corner
     down proportionally to fit — and the scaling is what breaks the `+ gap` compensation
     below, because it shrinks the compensated bottom pair and the plain top pair by the
     same factor rather than by the same amount. M14.34 set this to `--dk-r-lg` (22px),
     which needs 52px of a 44px box: the ends came out visibly flattened, which is the
     "capped" the owner reported. 18px is the largest value that fits, and it happens to
     be exactly half the painted height, so the ends are true semicircles. */
  --dtable-r: 1.125rem;
  --dtable-rule: rgba(var(--dk-primary-rgb), .24);
  /* Physical x offset of the header bar's *leading* hairline — see the two corner rules
     below. Positive is leftward-inset, which is the start edge in LTR. */
  --dtable-side: 1px;
}
html[dir="rtl"] .dtable { --dtable-side: -1px; }

/* The header bar (M12). The reference does not put its column headings inside the
   row card: they are a **separate floating pill** above it — brand-tinted, 14px
   rounded, hairlined all round — and the rows are a card of their own below. That
   detachment is the strongest single signal in its data-heavy pages, and this app
   had the tint but not the shape: the heading strip was welded to the top of the
   panel like any other table.

   Done without leaving `<table>`, which stays for the reasons it always did (a screen
   reader and a copy-paste both want one, and these tables' columns are sized from
   their content — the reference can only use a grid because every column there is
   given an explicit width). The trick is a **transparent bottom border** the width of
   the gap plus `background-clip: padding-box`: the tint stops at the padding edge, so
   the bar ends `--dtable-gap` above the first row while the cell still occupies the
   space. The bottom hairline is an inset shadow, which is clipped to the padding box
   and therefore lands on the bar's own edge rather than the cell's. */
.dtable thead th {
  padding: .5rem 1.25rem;
  text-align: start;
  font-size: .75rem;
  font-weight: 800;
  color: var(--dk-primary-darker);
  white-space: nowrap;
  vertical-align: middle;
  height: 2.375rem;
  /* Vertical, not 135deg: the bar is made of one `th` per column, and each cell paints
     its own copy of the gradient. A diagonal one therefore restarts at full strength at
     every column boundary — measured at 12-14/255 on the portal's history table, which
     is a visible seam in a bar the rule right above calls "one bar". A `to bottom` ramp
     is identical in every cell, so there is nothing to line up. */
  background: linear-gradient(to bottom, rgba(var(--dk-primary-rgb), .20),
                              rgba(var(--dk-primary-rgb), .12));
  background-clip: padding-box;
  border: 0;
  border-bottom: var(--dtable-gap) solid transparent;
  /* All four hairlines are inset shadows, and that is a correction (M14.27). The top and
     the sides used to be real borders while the bottom was this shadow, and the two live
     in different boxes: a border is painted on the *border* box, which extends
     `--dtable-gap` below the visible bar, while an inset shadow is clipped to the
     *padding* box, which is the bar itself. So each end of the bar had a 1px vertical
     tail hanging into the gap under it — the leftover of a side border mitring into a
     transparent bottom one — and the bottom hairline curved at a different radius from
     the tail it was supposed to meet. It read as a cropped shadow under the pill, which
     is exactly what it was. On one box the four join. */
  box-shadow: inset 0 1px 0 var(--dtable-rule), inset 0 -1px 0 var(--dtable-rule);
}
/* Logical corner radii, so the leading end of the bar is the right one in both
   directions without a `[dir]` rule.

   The *bottom* pair is `+ --dtable-gap` in its vertical axis and that is not a fudge: a
   padding-box corner radius is the border-box radius minus that side's border width, so
   an 8px transparent bottom border would otherwise flatten the bar's bottom corners by
   exactly that while its top stayed put. Adding the gap back cancels the subtraction
   and the painted bar is a pill again. Two values per corner — horizontal, then
   vertical. Verified by mirroring one corner onto the other off a render, because a
   computed `border-radius` reports what was asked for, not what the padding box
   painted. */
/* `box-shadow` has no logical form, so the start edge's offset is a variable and the
   direction flips its sign. One declaration per end either way, rather than a second
   copy of both under an `[dir="rtl"]` selector. */
.dtable thead th:first-child {
  box-shadow: inset 0 1px 0 var(--dtable-rule), inset 0 -1px 0 var(--dtable-rule),
              inset var(--dtable-side) 0 0 var(--dtable-rule);
  border-start-start-radius: var(--dtable-r);
  border-end-start-radius: var(--dtable-r) calc(var(--dtable-r) + var(--dtable-gap));
}
.dtable thead th:last-child {
  box-shadow: inset 0 1px 0 var(--dtable-rule), inset 0 -1px 0 var(--dtable-rule),
              inset calc(-1 * var(--dtable-side)) 0 0 var(--dtable-rule);
  border-start-end-radius: var(--dtable-r);
  border-end-end-radius: var(--dtable-r) calc(var(--dtable-r) + var(--dtable-gap));
}
:root[data-bs-theme="dark"] .dtable thead th { color: var(--dk-text-1); }

/* The rows, as one card: a brighter fill than the panel it sits in, hairline
   separators, and the four outer corners rounded at the panel radius. `tbody` cannot
   take a border-radius in any shipping browser, so the corners belong to the four
   corner cells — which is also why the row card survives `overflow-x: auto`. */
.dtable tbody td {
  padding: .75rem 1.25rem;
  background: var(--dk-surface);
  border: 0;
  border-bottom: 1px solid var(--dk-line-glass);
  vertical-align: middle;
  height: 3.5rem;
  color: var(--dk-text-1);
  transition: background var(--dk-t-exit) var(--dk-ease);
}
/* The card's own outer rim: a hairline on the four edges, drawn by the cells that
   are on them. */
.dtable tbody tr:first-child > td { border-top: 1px solid var(--dk-rim); }
.dtable tbody tr:last-child > td { border-bottom: 1px solid var(--dk-rim); }
.dtable tbody td:first-child { border-inline-start: 1px solid var(--dk-rim); }
.dtable tbody td:last-child { border-inline-end: 1px solid var(--dk-rim); }
/* Physical corners, switched on the *page's* direction — and this is the one place in
   this file that may not use logical properties (M14.34, owner: "the left body top
   border radius is not there").

   A logical corner resolves against the element's **own** direction, and ~20 of these
   cells carry `dir="ltr"` so a timestamp or a phone number is not mangled by the RTL
   run around it. On those cells `border-start-end-radius` stops meaning top-left and
   starts meaning top-right: the browser computed `se=22px` into `border-top-right-radius`
   and left `border-top-left-radius: 0`. So the card's left corner was square while its
   right one curved, and a 22px notch was being carved into the middle of the first row
   where that cell meets its neighbour. The radius was never missing — it was on the
   wrong corner, put there by an attribute added for the text inside the cell.

   Same root cause as the reply popover's `dir="auto"` in M14.33, and the general rule
   both of them teach: a logical property describes the box it is written on, so a box
   carrying a text direction of its own cannot be positioned with one. The corner belongs
   to the table's geometry, not to the cell's sentence. */
html[dir="rtl"] .dtable tbody tr:first-child td:first-child { border-top-right-radius: var(--dk-r-lg); }
html[dir="rtl"] .dtable tbody tr:first-child td:last-child { border-top-left-radius: var(--dk-r-lg); }
html[dir="rtl"] .dtable tbody tr:last-child td:first-child { border-bottom-right-radius: var(--dk-r-lg); }
html[dir="rtl"] .dtable tbody tr:last-child td:last-child { border-bottom-left-radius: var(--dk-r-lg); }
html[dir="ltr"] .dtable tbody tr:first-child td:first-child { border-top-left-radius: var(--dk-r-lg); }
html[dir="ltr"] .dtable tbody tr:first-child td:last-child { border-top-right-radius: var(--dk-r-lg); }
html[dir="ltr"] .dtable tbody tr:last-child td:first-child { border-bottom-left-radius: var(--dk-r-lg); }
html[dir="ltr"] .dtable tbody tr:last-child td:last-child { border-bottom-right-radius: var(--dk-r-lg); }
.dtable tbody tr:hover > td { background: var(--dk-surface-hover); }

/* Inside a panel the panel *is* the card, so the row block flattens: a quieter fill
   and no rim of its own. This is the reference's de-nesting discipline applied to the
   one place it matters most — its own dashboard does exactly this to the table inside
   a titled card (`.dash-recent .gtbl-section`), for the same reason. */
.panel .dtable tbody td { background: var(--dk-row-inner); }
.panel .dtable tbody tr:first-child > td { border-top: 0; }
.panel .dtable tbody tr:last-child > td { border-bottom: 0; }
.panel .dtable tbody td:first-child { border-inline-start: 0; }
.panel .dtable tbody td:last-child { border-inline-end: 0; }

/* Zebra (M14.34, owner: "the table should be zebra"). Written as a `background-image`
   overlay rather than a `background-color`, and that is what makes it one rule instead of
   two: the cell's own colour is set by a shorthand in two places — `--dk-surface` for a
   bare table and the quieter `--dk-glass-inner` for one inside a panel — and an overlay
   tints whichever of them is underneath without either needing to know about striping.

   It also gives the row block a fill it can be *seen* to have. Measured on the dashboard
   before this: every pixel of the rows card's corners was within tolerance of the panel
   behind it, so the card's 22px corners were correct and invisible, which is the whole of
   the owner's second complaint — the radius was there, the card was not. */
.dtable tbody tr:nth-child(even) > td {
  background-image: linear-gradient(var(--dk-zebra), var(--dk-zebra));
}
/* Hover is a state and outranks the stripe, or an even row under the pointer reads as
   both at once and lands somewhere between the two. */
.dtable tbody tr:hover > td { background-image: none; }
.dtable .num {
  font-family: var(--dk-font-body-en);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
}
/* An empty table has no columns to head (M10.2). `table_empty` renders a real empty
   state — an icon, a title, a hint — and it was rendering it *under a full row of
   column headings*, which on the SMS inbox was the entire page: a header strip, a
   shrug, and a pager. The `<thead>` stays in the DOM, so a screen reader still gets
   the structure; only the strip goes.

   `:has()` earns its exception here. The alternative is a flag on `table_open` at
   twenty-three call sites, which is twenty-three chances to forget — and on a browser
   too old for `:has()` the fallback is exactly today's rendering, not a broken one. */
.dtable:has(.empty) thead { display: none; }
/* The figures column (M14.35, owner: "الكمية الوقت has wrong text alignment in header",
   "الكمية number should be right align but in a way to easy read the number").

   `end` was two bugs at once. In RTL it resolves to the left, so a column of amounts
   aligned on its *most* significant digit — measured, the ink's left edges all landed on
   619 while the right edges came in at 664/636/644, so the units digits sat in a
   different place on every row and no two figures could be compared at a glance.
   `tabular-nums` was already on and could not help: equal-width digits still need a
   common edge to line up against.

   And `end` resolves against each element's own direction, so the timestamp cells —
   which carry `dir="ltr"` so the clock is not mangled by the RTL run — took `end` to
   mean *right* while their header, in the page's RTL, took it to mean left. The column's
   heading and its contents pointed at opposite edges. Third instance of that trap in as
   many slices, after M14.33's popover and M14.34's corners.

   Physical `right`, therefore, in both directions: a number is LTR even inside an RTL
   table, and every column wearing this class is a figure, a timestamp or an age. In LTR
   this is exactly what `end` already meant, so nothing there moves. */
.dtable .t-end { text-align: right; font-variant-numeric: tabular-nums; }
.dtable .t-center { text-align: center; }
.dtable .id { color: var(--dk-text-3); font-family: var(--dk-font-body-en); font-size: .75rem; }
.dtable .muted { color: var(--dk-text-2); }
/* A timestamp is one token. Without this, `2026-08-23 14:01` wrapped onto three
   lines in a phone-width column and made every row twice as tall as its content. */
/* Super-admin impersonation banner (M8.7). Deliberately the loudest thing on the page
   and deliberately not dismissible: it reports a state, not an event. Sticky, because the
   risk is forgetting halfway down a long queue that these actions are not your own. */
.impersonation {
  position: sticky; top: 0; z-index: 1080;
  display: flex; align-items: center; gap: .5rem;
  padding: .45rem 1rem; font-size: .8125rem; font-weight: 600;
  color: #3b1d00; background: #ffc65c;
  border-bottom: 1px solid rgba(0, 0, 0, .18);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .18);
}
.impersonation a { margin-inline-start: auto; color: inherit; font-weight: 700; }

.dtable .when { white-space: nowrap; color: var(--dk-text-2); font-size: .6875rem; }

/* Audit detail: the changed field's name, so an operator can match a log line against
   the form control it refers to. Identifiers stay LTR inside an RTL table cell. */
.audit-k { font-family: var(--dk-font-mono); font-size: .6875rem;
  color: var(--dk-text-2); background: var(--dk-surface-2); border-radius: .25rem;
  padding: .05em .3em; }
.when-part + .when-part { margin-inline-start: .3em; }
.dtable .acts { display: flex; gap: .25rem; align-items: center; }

/* Column priority (M6.17). The CRM tables run to seven and eight columns and a
   shop owner reads them on a phone. Measured at 430px: the queue table was 909px
   wide and the customers table 926px, so on both of them the action controls —
   the entire point of those pages — sat ~490px off the screen, inside a
   `overflow-x: auto` that gives no sign it has anything more to show.

   Below the breakpoint a `lo` column is dropped and its value reappears in the
   row's primary cell as `.tmeta`. The cell padding tightens at the same time,
   which is worth ~0.75rem per cell per side and is on its own enough to bring
   the two tables that only just overflowed back inside the screen. */
.tmeta { display: none; }
/* A control sized to hold a short amount or a two-option select. Five of these in
   one table cell is the customers page's inline top-up form. */
.ctl-narrow { inline-size: 6rem; }
.ctl-wide { inline-size: 7rem; }
@media (max-width: 34rem) {
  .dtable .lo { display: none; }
  .dtable thead th,
  .dtable tbody td { padding-inline: .625rem; }
  /* An action group that carries a text field stacks instead of running off the
     edge. The rescue row was the other caller until M14.57 put its reason field in a
     popover; the customers page's row menu is what is left, and it still needs this. */
  /* Only the stacking variant wraps. `flex-wrap: wrap` on every action group made
     the column's minimum width the widest single button instead of the row of
     them, so three ghost icons broke onto two lines in a column wide enough for
     all three. */
  .dtable .acts--stack,
  .dtable .acts--wrap { flex-wrap: wrap; }
  .dtable .acts--stack > form.acts { flex: 1 0 100%; }
  /* The top-up form's five controls become two rows rather than one long one. */
  .ctl-narrow,
  .ctl-wide { inline-size: auto; flex: 1 1 4rem; min-inline-size: 0; }
  .dtable .acts--wrap > .btn { flex: 1 1 auto; }
  /* Except a square one (M14.36). `.btn-icon` is a fixed inline size so that a row
     of icons is a row of matched squares; `flex: 1 1 auto` reads that size as a
     basis and then grows past it, which is the one place the phone layout would
     have undone the thing the class exists for. The labelled buttons beside it
     still share the leftover width. */
  .dtable .acts--wrap > .btn-icon { flex: none; }
  /* The column the row is *about* takes the room the folded ones gave back;
     without this the table splits it evenly and the meta line wraps three deep. */
  .dtable .t-main { width: 45%; }
  .tmeta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .1875rem .5rem;
    margin-top: .3125rem;
    font-size: .6875rem;
    color: var(--dk-text-2);
  }
  /* A pill in the meta line is a footnote, not the row's headline. */
  .tmeta .pill { padding: .0625rem .375rem; font-size: .625rem; }
  .tmeta .id { font-size: .625rem; }
  /* The date stacks over the time rather than the year being dropped: inside a shop's
     own log you do page back far enough for the year to matter (M6.17).

     M10.3 moved this up from 26rem. `2026-08-23 14:00` is nowrap, so on one line it is
     a ~100px column, and at 430px — above the old breakpoint — that was the last four
     pixels of two tables: the customer portal's history clipped its time to
     "2026-08-23 14:0", and the operations log clipped the third of its three rescue
     buttons. It belongs with the rest of the folding, which happens here: a row that
     has already grown a meta line has room for a two-line timestamp. The `.acts`
     wrapping below stays at 26rem, where the comment explains why. */
  .when-part { display: block; }
  .when-part + .when-part { margin-inline-start: 0; }
}
/* Narrower still — 360px is a very common cheap Android. */
@media (max-width: 26rem) {
  /* Down here every action group may wrap. At 430px this was wrong — it let the
     table squeeze a column that had room for all three buttons, because a wrapping
     flex container's minimum is its widest single item. At 360px it is the only
     way the log's rescue buttons fit at all. */
  .dtable .acts { flex-wrap: wrap; }
}
/* The carrier's raw reply, under the target it belongs to (M6.11). It used to
   have a column of its own, truncated to 60 characters — which is exactly long
   enough to cut a failure message in half. Full text, wrapped, quiet, and
   capped in width so one verbose carrier cannot stretch the table. */
/* Queue position, as a leading badge on the target cell rather than a column of
   its own — it is one or two digits and did not earn 4ch of a phone's width. */
.qpos {
  display: inline-grid; place-items: center;
  min-inline-size: 1.375rem;
  padding: 0 .25rem;
  margin-inline-end: .375rem;
  border-radius: var(--dk-r-xs);
  background: var(--tone-mute-bg);
  color: var(--dk-text-2);
  font-family: var(--dk-font-body-en);
  font-size: .6875rem; font-weight: 700;
}
/* A cell holding a short Arabic phrase that must not stretch the table. */
.dtable .tight { font-size: .75rem; line-height: 1.35; }

/* The status cell (M14.33, owner: "for statuse make pollit same width").

   The pills were as wide as their own words, so a status column was a ragged edge —
   measured across the ten states, 70-87px in Arabic and 77-120px in English. Rather than
   a magic minimum wide enough for "Under review" — which would have padded every Arabic
   pill by a third of its width for a label it never shows — the pill simply fills its
   cell. The column still sizes itself to its widest pill, and every pill in it is then
   exactly that wide, in whichever language, with no number written down anywhere.

   `.st-slot` is the icon's place, reserved for the whole table by `tx_status(reserve=)`
   rather than per row: a slot that appears only on rows that have a reply would make
   those rows' pills one icon narrower than their neighbours, which is the ragged edge
   again in a smaller size. */
.st-cell {
  display: flex;
  align-items: center;
  gap: .375rem;
}
/* Only in a column. Inside `.tmeta` — the line a phone folds a row's dropped columns
   onto — a chip at a fixed 9.25rem is a chip pretending to be a banner. */
td > .st-cell { inline-size: 100%; }
.st-cell > .pill {
  /* **A width, not a floor** (M14.36, owner: "make them equal width but not full width").
     M14.35 gave the two chip columns a shared `min-inline-size` and left `flex: 1 1 auto`
     to fill the cell, which made them agree with each other and made both of them as wide
     as whatever the column happened to be — so the status ran the full width of a cell
     sized by the word "الحالة" plus its padding, and read as a banner across the row
     rather than as a chip in it.

     Fixed and `flex: none` is the shape that satisfies both halves at once: every chip in
     both columns is exactly this wide, and the cell keeps the slack around it. The column
     is still sized by the table, so what changes is that the chip stops growing into it.

     Sized to Arabic, which is the language these shops run in: 9.25rem clears the widest
     Arabic status. English's "Under review" is wider than any Arabic status and
     ellipsises here rather than widening the column — the alternative is pinning both at
     11.25rem, which is a lot of width to spend on the word "MTN". */
  flex: none;
  inline-size: 9.25rem;
  justify-content: center;
}
/* Below the phone breakpoint the chip columns give the width back: seven columns are
   already folding onto two lines there, and 148px of chip is width the row cannot
   spare. */
@media (max-width: 30rem) {
  /* `auto` and a zero floor together: a flex item's own floor is its content, so without
     the second half a long label refuses to shrink and pushes the column past the edge of
     the phone. `.pill` already ellipsises. */
  .st-cell > .pill { inline-size: auto; min-inline-size: 0; flex: 0 1 auto; }
}
.st-slot {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Sized from the icon, so the two move together. */
  inline-size: var(--dk-ctl-sm-h);
}

/* The icon itself: quiet until it is wanted, and a real target on a phone. Not a `.btn`
   — it opens a note, it does not do anything — so it takes the bare treatment and the
   colour of the text around it.

   **22px was too small to hit and too small to see** (M14.36, owner: "network responce
   button should be larger"). It is `btn-sm`'s 30px now, which is the height of every
   other control in a row of this table, so the status cell and the actions cell are
   built out of the same unit — and it carries a resting rim rather than appearing only
   on hover, because a control that is invisible until the pointer finds it is a control
   nobody knows is there. Still under the 44px touch target, and deliberately: this sits
   inside a table row on a page that folds to two columns on a phone, where the note
   moves into the meta line and 44px of chrome would push the row's own figures out. */
.reply-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--dk-ctl-sm-h);
  block-size: var(--dk-ctl-sm-h);
  border-radius: 999px;
  border: 1px solid var(--dk-ctl-bd);
  background: var(--dk-ctl-bg);
  color: var(--dk-text-2);
  font-size: .875rem;
  cursor: pointer;
  transition: background var(--dk-t-exit) var(--dk-ease),
              border-color var(--dk-t-exit) var(--dk-ease),
              color var(--dk-t-exit) var(--dk-ease);
}
.reply-dot:hover { background: var(--dk-ctl-bg-hover); color: var(--dk-text-1); }

/* The sentence, and the third appearance of one rule (M14.36, owner: "the content should
   be aligned based on message langauge english left to right arabic right to left").

   `dir="auto"` was already on the span and was already doing its half of the job — the
   *ordering* of an English reply was correct. What it could not do is align it: an inline
   element is not a block container, so `text-align` never reached it and the alignment
   came from the panel around it, which is in the page's direction. An English reply
   set ragged-left against the RTL edge, with its trailing phone number alone on the right.

   So the direction has to sit on a box that lays out its own lines. `display: block`
   makes this that box; `text-align: start` then resolves against *this* element's
   resolved direction rather than the panel's, which is the whole fix.

   The panel above still must not carry `dir` — M14.33 measured an English reply pinning
   `inset-inline-end` to the wrong edge and opening at x=-6. Direction on the sentence,
   never on the box that is positioned: the same rule M14.34 wrote for the table's
   corners and M14.35 for its columns. */
/* `.reply-line` is the same thing in a table cell (M14.81): the USSD page's reply column.
   A block, for the reason above — `dir="auto"` on an inline span orders the run and
   aligns nothing, because `text-align` needs a block container, and the nearest one is
   the cell, in the page's direction. So an English carrier reply read left-to-right and
   sat against the RTL cell's right edge with its last line stranded. */
.reply-text,
.reply-line {
  display: block;
  text-align: start;
}


/* Wait age (M6.11). Not a pill: one per row would double the pill count and
   compete with the status it sits next to. Tabular figures so the column scans
   as a column, and colour only once the number means something. */
.age {
  font-family: var(--dk-font-body-en);
  font-variant-numeric: tabular-nums;
  font-size: .75rem;
  font-weight: 600;
  white-space: nowrap;
  color: var(--dk-text-3);
}
.age--warn { color: var(--tone-warn); }
.age--bad  { color: var(--tone-bad); font-weight: 700; }
/* An age can also *be* a headline figure (the queue's "longest wait" tile), where
   the table-cell size would read as a caption. Inherit the tile's scale and let
   the tile's tone own the colour. */
.stat-value .age { font-size: inherit; font-weight: inherit; color: inherit; }

/* `.row--flag` was here and is gone (M14.36, owner: "left border should not be there").
   It marked a row needing a human with a 3px inset shadow on `td:first-child`, and two
   things were wrong with it. It never landed where the comment claimed: since M12 the
   rows are a rounded card inside a panel, `td:first-child` drops its inline border and
   carries a 22px corner, so the stripe followed that radius and floated beside the card
   as a detached red bracket rather than sitting against the row.

   And it was saying nothing the row had not already said. All three call sites had a
   louder marker of their own in the same row: the queue's intervention panel is built
   entirely from MANUAL_CHECK under a head already tinted `--tone-bad`; the operations log
   carries the amber `مراجعة` pill with its warning icon; the platform table prints the
   stuck count itself in `.stuck`, red and bold. A second marker for a fact already on
   screen is decoration, and this one was decoration drawn in the wrong place. */

/* Pagination (M6.11). `is-off` instead of removing the link: the control keeps
   its width, so the page does not reflow between page 1 and page 2. */
.pager {
  display: flex;
  align-items: center;
  gap: .3125rem;
  flex-wrap: wrap;
  /* The controls at one end, the count at the other, placed by the container. See
     `.pager-at` for why this is not an auto margin. */
  justify-content: space-between;
  /* No border-top now that the rows above are a rounded card: a rule drawn across
     the panel under a card with rounded corners cuts the corners off. */
  padding: .75rem .625rem .25rem;
}
.pager .is-off { opacity: .45; pointer-events: none; }
/* The reference's `.pgn-btn`: a 2rem square, quiet until hovered, and the current
   position between the two as a figure rather than as a third button.

   Descendant, not `>`: the controls sit inside `.pager-ctl` since M14.36 and a child
   combinator stopped matching them, which turned every one of these back into an
   ordinary `.btn` — including the current page, which lost its fill and left a pager
   with no indication of where you were. */
.pager .btn {
  min-block-size: 2rem;
  min-inline-size: 2rem;
  padding: 0 .625rem;
  font-size: .8125rem;
  font-weight: 600;
  border-radius: var(--dk-r-md);
  background: var(--dk-surface);
  border: 1px solid var(--dk-line);
  box-shadow: none;
  color: var(--dk-text-2);
}
.pager .btn:hover:not(.is-off) {
  background: var(--dk-surface);
  border-color: var(--tone-brand-soft);
  color: var(--dk-primary-darker);
}
/* The numbered pages (M14.36). Same `.pgn-btn` treatment as the ends, and square: a
   page number is one or two digits and a `1` should not be narrower than a `12`. */
.pager-n { padding: 0; min-inline-size: 2rem; }
/* Where you are. The reference marks it with the brand fill rather than a heavier
   border, because a border on one of nine equal boxes is a box you have to look for. */
.pager .btn.is-at,
.pager .btn.is-at:hover {
  background: linear-gradient(135deg, var(--dk-primary), var(--dk-primary-dark));
  border-color: transparent;
  color: var(--dk-on-primary);
  cursor: default;
}
/* An elided run. Not a button, so it does not invite the click it cannot take. */
.pager-gap {
  min-inline-size: 1.25rem;
  text-align: center;
  color: var(--dk-text-3);
  font-size: .8125rem;
  user-select: none;
}
.pager-ctl { display: flex; align-items: center; gap: .3125rem; flex-wrap: wrap; }
.pager-at {
  font-size: .8125rem; font-weight: 600;
  color: var(--dk-text-3);
  padding-inline: .375rem;
  white-space: nowrap;
}
/* **Why this is not `margin-inline-start: auto` on `.pager-at`.** That is what it was,
   and it did nothing — measured, the count sat against the controls with 845px of free
   space beyond it. `.pager-at` also carries `.num`, which sets `direction: ltr` so a
   range like `1-20` is not reordered by the Arabic around it; a logical margin then
   resolves against *that* direction and lands on the row's main-END, where an auto
   margin has no free space to absorb and pushes nothing.

   Fourth time in four slices, and the same rule every time: **a box carrying a text
   direction of its own cannot be laid out with logical properties** (M14.33's popover,
   M14.34's corners, M14.35's columns, and now this). `justify-content` is the container's
   own property and asks nothing of the item's direction, which is why the fix is a group
   wrapper and a `space-between` rather than a cleverer margin. */
/* On a phone the numbers go and the two ends stay. Nine 32px boxes plus two words plus
   the count is ~360px of control under a table that is already folding two columns into
   its rows at that width — and the count still says how far there is to go. The current
   page survives so the control is not two arrows around nothing. */
@media (max-width: 30rem) {
  .pager-n:not(.is-at), .pager-gap { display: none; }
}

/* Rows per page, for the tables that live in a card (M14.68).

   `.pager` now has three children where it had two, and `space-between` on three strands
   the count in the middle of the row — away from the control that qualifies it. The count
   and the size belong at the same end, so the controls take the free space instead.

   `margin-inline-end` on `.pager-ctl` and not on either of the other two, which is the
   rule the comment above spells out: `.pager-ctl` carries no text direction of its own,
   so a logical margin on it resolves against the page's. `.pager-at` is `.num`, and this
   is the fifth slice to be caught by that. */
.pager:has(.pager-size) { justify-content: flex-start; }
.pager:has(.pager-size) .pager-ctl { margin-inline-end: auto; }
.pager-size { display: flex; align-items: center; }

/* **Bootstrap's utilities are live in this product** (M14.73), and a rule was once
   written here on the belief that they are not. `base.html` links Bootstrap 5.3.3's own
   CSS ahead of this file, in an RTL build and an LTR one, and some fifty template sites
   lean on it -- so a class that is absent from *this* stylesheet has not been shown to do
   nothing. `test_m1473_vendor_utilities.py` is the way to settle it. */
/* A column, and narrow: three numbers, so the menu should be the width of a number and
   not of the widest thing `.pop-body` would otherwise stretch to. */
.pop-body--sizes { flex-direction: column; align-items: stretch; min-inline-size: 5.5rem; }
.pop-opt {
  padding: .375rem .625rem;
  border-radius: var(--dk-r-sm);
  font-size: .8125rem;
  font-weight: 700;
  color: var(--dk-text-1);
  text-align: center;
  transition: background var(--dk-t-pop) var(--dk-ease);
}
.pop-opt:hover, .pop-opt:focus-visible {
  background: var(--dk-ctl-bg-hover);
  color: var(--dk-text-1);
}
/* `.is-at`, the same name the current page uses in this control, and filled for the same
   reason: the reader needs to know which of the three they are already on. */
.pop-opt.is-at { background: var(--dk-primary); color: var(--dk-on-primary); }

/* Reveal-on-demand (M6.11). A <details> whose summary is a button and whose body
   floats over the row. Used for a secondary action that needs one input — the
   bill-order reject reason — so the field is not open on every row of the table.
   No JavaScript, which matters: these shops run cheap Android browsers and a nav
   or an action must never depend on a bundle arriving.

   `list-style: none` plus the ::-webkit-details-marker reset removes the disclosure
   triangle; without both, WebKit keeps drawing it inside the button. */
.pop { position: relative; display: inline-block; }
.pop > summary { list-style: none; cursor: pointer; }
.pop > summary::-webkit-details-marker { display: none; }
.pop > summary::marker { content: ""; }
/* Open, not wrong (M14.58, owner: "the bg color is not correct when active or foucs /
   opened"). This was `--tone-bad-bg` for *every* open popover — a red wash, because the
   first one written was the reject-reason form. The reply note had already noticed and
   patched itself out of it; the select kept it, so opening a dropdown turned
   its face pink. The tone follows the control's own face now instead of the disclosure's
   openness, which is the version that cannot be wrong for the next caller. */
.pop[open] > summary { background: var(--dk-ctl-bg-hover); }
.pop[open] > summary.btn-outline-danger { background: var(--tone-bad-bg); }
/* A field that is open reads as a field that is focused, not as a pressed button. */
.selpop[open] > summary.selpop-face { background: var(--dk-ctl-bg); }
/* The floating surface, shared with `.dropdown-menu`: the reference's popover recipe —
   a 145° near-opaque glass gradient, a near-white top edge, the large cool shadow, and
   14px corners. */
.pop-body {
  position: absolute;
  z-index: 20;
  inset-inline-end: 0;
  /* Below the summary, said out loud (M14.13). With no block start this rode its static
     position, and that is not one position: `.pop` is `inline-block`, where it lands
     under the summary as intended, but `.lang-pick` is an `.island` — a flex container —
     which resolves an out-of-flow child through `align-items: center` instead, half way
     up the pill. So the language menu opened across the bottom third of its own handler:
     `top` computed to 23px on a 48px island, 16px of overlap, measured with
     `dev.py probe login --details`. `100%` is the positioned ancestor's padding box,
     which is the one place both parents agree on. */
  inset-block-start: 100%;
  margin-top: .375rem;
  display: flex;
  /* Roomier since M14.46, with the dialogs: this is the other floating surface a person
     types into, and 8px of inset around a field and a button read as the same compression
     the modals did. Still tighter than a dialog — it hangs off a table row. */
  gap: .5rem;
  padding: .75rem;
  min-inline-size: 17rem;
  border-radius: var(--dk-r-md);
  border: 1px solid var(--dk-rim);
  border-top-color: var(--dk-rim-top);
  /* **Opaque, over the glass** (M14.58) — the fix the reply note already carried, promoted
     to the surface every popover uses. `backdrop-filter` does not nest: an element with one
     filters its own backdrop root, and every one of these floats inside a `.glass` panel,
     which is one. So the blur was never happening and `--dk-glass-strong` at 90% was
     letting 10% of the *sharp* row through — which reads as noise under a sentence and as a
     defect under a control. It cost a render to see: a yellow carrier pill showing through
     the treasury popover, directly behind a red button label. The filter stays for the one
     case that is not nested; the wash underneath is what makes it not matter. */
  background: linear-gradient(145deg, var(--dk-glass-strong), var(--dk-glass-hover)),
              var(--dk-wash-1);
  box-shadow: var(--dk-sh-lg);
  -webkit-backdrop-filter: var(--dk-blur);
          backdrop-filter: var(--dk-blur);
}
/* The field gives up the room; the commit keeps its label (M14.57). A flex item is
   `flex: 0 1 auto` by default, so the refund popover's استرداد المبلغ was shrunk below its
   own text and clipped — and the panel cannot grow to absorb it: `.pop` is an
   inline-block wrapped around a 32px summary, so an absolutely positioned child's
   shrink-to-fit width resolves to `min-inline-size` and stops there. `min-inline-size: 0`
   is the half that lets the input actually go below its intrinsic width, which `auto`
   otherwise floors it at. Rendered, at 1750px and at 430px. */
.pop-body > .form-control { flex: 1 1 auto; min-inline-size: 0; }
.pop-body > .btn { flex: none; }

/* Opening upward, for the rows that have no room below them (M14.33).

   `.dtable-wrap` is `overflow-x: auto` so that a wide table scrolls sideways on a phone
   — and a box that is `auto` on one axis computes to `auto` on the other, so it clips
   vertically too. Measured: the last row's note ran 16px past the bottom and the wrapper
   answered by growing its own scrollbar, which is a panel that scrolls inside a page that
   also scrolls, to show one sentence.

   `:nth-last-child` rather than a script: the row's position in the table is a fact the
   stylesheet already has. Two rows rather than one, so a three-line reply on the
   second-to-last row is covered as well. The origin flips with the direction or the
   unfurl would grow from the edge it is no longer attached to.

   `:not(:nth-child(-n+3))` because a short table has no room above it either: on the
   two-row agents table both rows are "the last two", and flipping put the popover under
   the sticky app bar — measured, three of five hit tests landed on `.shell-top` and its
   heading (M14.65). */
/* `:not(.selpop-menu)` since M14.81: a select menu picks its own direction by measuring
   (see `placeSelpop`), and the two decisions disagreed. A dialog is *written* inside the
   `<td>` it belongs to and rendered in the top layer, so a row's `nth-last-child` says
   nothing about where the menu inside it actually is — measured, the adjust dialogs on
   the customers page's last two rows were flipping upward because of their row. */
.dtable tbody tr:nth-last-child(-n+2):not(:nth-child(-n+3)) .pop-body:not(.selpop-menu) {
  inset-block-start: auto;
  inset-block-end: 100%;
  margin-top: 0;
  margin-bottom: .375rem;
  transform-origin: bottom center;
}

/* The same flip, asked for by name (M14.66). The rule above is scoped to `.dtable`, and
   the one control outside a table that needs it is the APK delete in the platform
   dialog's `.dlg-foot`: `.dlg-body` is `overflow-y: auto`, so a popover unfurling
   downward from the dialog's own last row is clipped by the scroll box it sits at the
   bottom of. Upward it opens over the dialog's content, which is inside that box.

   A modifier rather than a `:has()` on the foot, because the foot is not the condition —
   being at the bottom of a scroll container is, and only the call site knows that. */
.pop--up > .pop-body {
  inset-block-start: auto;
  inset-block-end: 100%;
  margin-top: 0;
  margin-bottom: .375rem;
  transform-origin: bottom center;
}

/* The unfurl (M12). Everything that floats in the reference opens by growing down
   from its own top edge — `scaleY(.88)` at `transform-origin: top center` — rather
   than appearing. `<details>` cannot transition across a display change, so it is an
   animation on the open state, which is the same motion for a tenth of the code. */
@keyframes unfurl { from { opacity: 0; transform: scaleY(.88) translateY(-8px); } }
.pop-body, .dropdown-menu.show { transform-origin: top center; }
.pop[open] > .pop-body,
.lang-pick[open] > .pop-body,
.user-menu[open] > .pop-body,
.dropdown-menu.show { animation: unfurl var(--dk-t-pop) var(--dk-ease); }

/* And the furl (M14.13). The comment above is right that a `<details>` cannot transition
   across a display change — but that argument only ever covered the opening. On the way
   out the body is gone on the same frame `open` is, so an exit animation on `:not([open])`
   never gets a chance to run and the menu did not close so much as vanish.

   The reference animates both halves and keeps the panel mounted for the second one
   (`um-open .22s` / `um-close .18s` in Dorak's UserMenu, which is where `--dk-t-pop` and
   `--dk-t-exit` came from). Radix hands it a `data-state="closed"` to render during the
   exit; `viewer.js` does the same job with a class, and drops `open` when the animation
   ends. `forwards` matters: without it the panel snaps back to full opacity for the frame
   between the last keyframe and the attribute going.

   Nothing here is load-bearing. Unbound — no script, an old browser, a failed request —
   the class is never added, this rule never matches, and the menu closes the instant it
   always did. */
@keyframes furl { to { opacity: 0; transform: scaleY(.88) translateY(-8px); } }
/* The account menu joined this in M14.29 (owner: "no animation when open and close
   it"). Half of that was true and it was the half with no rule: `unfurl` did run on the
   way in — seeked by hand in headless Chrome, opacity 0→1 and scaleY .88→1 across the
   220ms — but the menu had no exit at all, so it did not close so much as disappear,
   and an entrance nobody sees the counterpart of does not register as motion. */
.lang-pick.is-closing > .pop-body,
.user-menu.is-closing > .pop-body,
.pop.is-closing > .pop-body {
  animation: furl var(--dk-t-exit) var(--dk-ease) forwards;
}

/* The reveal panel was here (M6.14–M14.81). A `<details>` whose summary was a button and
   whose body was a full glass panel dropped out of the toolbar's flex row on
   `position: absolute` — the create-account form, and the last floating surface in the
   product still built out of a disclosure three milestones after M14.75–M14.79 moved
   every other one into the top layer. It is `admin/_account_wizard.html` in a `ui.dialog`
   now, so all nine rules are gone with it. */

/* The named-palette picker and the logo preview (M6.20).

   A colour control that does not show the colour is not a colour control, so the eight
   presets are drawn rather than listed. The chip carries both halves of the preset — the
   primary and the decorative accent — because that pairing is what a shop is choosing;
   the accent reaches the page mesh and the orbs and never sits under a label, which is
   why it can contrast this hard with the primary.

   `:checked +` sibling styling: no JS, no `:has()`, and it works in the older WebViews
   the rest of this sheet is careful about. */
.swatches { display: flex; flex-wrap: wrap; gap: .75rem; }
.swatch {
  display: flex; flex-direction: column; align-items: center; gap: .375rem;
  cursor: pointer; position: relative;
}
/* Off-screen rather than `display: none`: the radio has to stay focusable, or the
   keyboard cannot reach the control at all. */
.swatch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.swatch-chip {
  width: 3.25rem; height: 2.25rem;
  border-radius: var(--dk-r-sm);
  background: linear-gradient(135deg, var(--sw-a), var(--sw-b));
  border: 1px solid var(--dk-rim);
  box-shadow: var(--dk-sh-sm), var(--dk-bevel);
  transition: transform var(--dk-t-pop) var(--dk-ease-spring);
}
.swatch:hover .swatch-chip { transform: translateY(-2px); }
/* The pick, felt (M14.44, owner: "change the selected item should have animation,
   creative one"). It lands with a spring, keeps a little of the scale, and carries a
   shadow in its own colour so the chosen chip reads as lifted off the row. */
@keyframes swatch-pick {
  0%   { transform: scale(1) rotate(0); }
  35%  { transform: scale(1.18) rotate(-4deg); }
  70%  { transform: scale(.99) rotate(1.5deg); }
  100% { transform: scale(1.06) rotate(0); }
}
.swatch input:checked ~ .swatch-chip {
  outline: 2px solid var(--tone-brand-soft);
  outline-offset: 2px;
  transform: scale(1.06);
  box-shadow: var(--dk-sh-sm), var(--dk-bevel), 0 8px 18px -8px var(--sw-a);
  animation: swatch-pick .45s var(--dk-ease-spring);
}
.swatch input:checked ~ .swatch-name { animation: swatch-name .45s var(--dk-ease); }
@keyframes swatch-name { from { opacity: .4; transform: translateY(3px); } }
.swatch input:focus-visible ~ .swatch-chip {
  box-shadow: 0 0 0 3px rgba(var(--dk-primary-rgb), .40);
}
.swatch-name { font-size: .75rem; color: var(--dk-text-2); }
.swatch input:checked ~ .swatch-name { color: var(--dk-primary-darker); font-weight: 600; }

/* The advanced hex fields: present, deliberately not prominent. */
.advanced > summary {
  cursor: pointer;
  font-size: .8125rem;
  color: var(--dk-text-2);
  padding: .25rem 0;
}
.advanced > summary:hover { color: var(--dk-primary-darker); }
/* Expand and collapse, seen (M14.44). `::details-content` with `interpolate-size` is the
   platform handing over an auto-height transition; without it the disclosure toggles the
   way it always did. Scoped to `.advanced`, not `:root` — the keyword changes how every
   `auto` in its subtree animates. */
.advanced { interpolate-size: allow-keywords; }
.advanced::details-content {
  block-size: 0;
  overflow: hidden;
  transition: block-size var(--dk-t-base) var(--dk-ease),
              content-visibility var(--dk-t-base) var(--dk-ease) allow-discrete;
}
/* The clip above is what makes the reveal a reveal, and it also cut the open-bill select
   off 207px short. A discrete `overflow` with no duration flips at the end of its delay,
   so it lifts once the height has settled and clips again on close (M14.64). */
.advanced[open]::details-content {
  block-size: auto;
  overflow: visible;
  transition: block-size var(--dk-t-base) var(--dk-ease),
              content-visibility var(--dk-t-base) var(--dk-ease) allow-discrete,
              overflow 0s var(--dk-t-base) allow-discrete;
}
.advanced > summary { transition: color var(--dk-t-exit) var(--dk-ease); }

/* The select, wearing the product's popover instead of the browser's list (M14.44, owner:
   "these are not using the correct dropdown component we created"). `ui.js` builds this
   around the real `<select>`, which stays in the form and stays what submits — so a
   browser that never runs the bundle gets the native control, which is what it had.

   `.pop` and `data-pop`, so the unfurl, the furl, the outside click and Escape are the
   ones M14.43 unified rather than a fourth set. */
.selpop { display: block; }
.selpop > summary.selpop-face { display: flex; align-items: center; }
.selpop-face::-webkit-details-marker { display: none; }
.selpop-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.selpop[open] > summary.selpop-face { border-color: var(--tone-brand); }
/* The real control: reachable by the form, never by the eye. */
.selpop-native { position: absolute; opacity: 0; pointer-events: none; inline-size: 1px; block-size: 1px; }
.selpop-menu {
  flex-direction: column;
  align-items: stretch;
  inline-size: 100%;
  min-inline-size: 0;
  max-block-size: 15rem;
  overflow-y: auto;
}
.selpop-opt { border: 0; background: none; inline-size: 100%; text-align: start; font: inherit; }

.advanced[open] > summary { color: var(--dk-primary-darker); font-weight: 600; }

/* A file input, brought into the design system. `::file-selector-button` is the only
   part of this control a page may style; its *label* is the browser's and localised by
   the browser, which is right — the file picker it opens is the browser's too. */
/* Flush and full-height (M14.44, owner: "choose file still not fully aligned with box").
   It was a bordered pill floating inside the field's own padding, so it met none of the
   field's edges. `line-height` centres the filename now that the padding is gone. */
.form-control[type="file"] { padding: 0; line-height: calc(var(--dk-field-h) - 2px); }
/* And the label inside the button, centred on the same line (M14.48, owner: "choose file
   is not horizen centerd"). Measured against the field's own "No file chosen", the button's
   word sat several pixels lower.

   M14.44 centred it by *inheritance* — the button takes the field's `line-height` through
   `font: inherit`, and a line box as tall as the box centres its text. That works only if
   the button's box is the one this rule thinks it is, and two things it never set decided
   that: `box-sizing`, because a `*, *::before, *::after` reset does not match
   `::file-selector-button` and it is therefore `content-box`, so the declared height is the
   *content* and anything else adds to it; and `padding-block`, which Chrome's own button
   styling supplies and which nothing here was overriding. Both are stated now, and the
   line-height is written rather than inherited, so the centring does not depend on a
   shorthand's ordering in this rule. */
.form-control[type="file"]::file-selector-button {
  /* A definite height, and off the baseline. `100%` had no definite box to resolve
     against and the button then hung below the line: measured 58px in a 46px row. */
  box-sizing: border-box;
  block-size: calc(var(--dk-field-h) - 2px);
  line-height: calc(var(--dk-field-h) - 2px);
  vertical-align: top;
  margin: 0;
  margin-inline: 0 .75rem;
  padding-block: 0;
  padding-inline: 1rem;
  border: 0;
  border-inline-end: 1px solid var(--dk-ctl-bd);
  background: var(--dk-ctl-bg-hover);
  color: var(--dk-text-1);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--dk-t-base);
}
.form-control[type="file"]::file-selector-button:hover { background: var(--dk-surface-hover); }
/* Physical corners, per direction. A logical radius resolves against the *button's* own
   direction and this one is a shadow child with English in it, so `border-start-start`
   rounded the far corner and left the near one square inside the field's curve. */
[dir="rtl"] .form-control[type="file"]::file-selector-button {
  border-radius: 0 calc(var(--dk-r-md) - 1px) calc(var(--dk-r-md) - 1px) 0;
}
[dir="ltr"] .form-control[type="file"]::file-selector-button {
  border-radius: calc(var(--dk-r-md) - 1px) 0 0 calc(var(--dk-r-md) - 1px);
}

/* The uploaded mark, at the size it is actually used at in the nav. */
.logo-preview {
  display: grid; place-items: center;
  width: 3.5rem; height: 3.5rem;
  border-radius: var(--dk-r-md);
  background: var(--dk-glass-inner);
  border: 1px solid var(--dk-rim);
  overflow: hidden; flex: 0 0 auto;
}
.logo-preview img { max-width: 100%; max-height: 100%; object-fit: contain; }
/* The mark, then everything else about the look, side by side (M14.44). */
.brand-row { display: flex; gap: 1rem; align-items: flex-start; }
.brand-facts { flex: 1 1 auto; min-inline-size: 0; }
.logo-preview--empty { color: var(--dk-text-3); }

/* A pop body that stacks its rows instead of laying them out inline. */
.pop-body--stack { flex-direction: column; align-items: stretch; min-inline-size: 21rem; }

/* One wallet: what it is, what is in it, and the two ways to move it (M14.58). A grid and
   not a flex row so the figures line up down the card whatever their label's length — the
   whole reason this reads better than the six-row table it replaces is that a column of
   numbers can be *compared*. */
.stock { display: flex; flex-direction: column; gap: .25rem; }
.stock-row {
  display: grid;
  grid-template-columns: minmax(4.5rem, auto) 1fr auto;
  align-items: center;
  gap: .75rem;
  padding: .625rem .75rem;
  border-radius: var(--dk-r-md);
  background: var(--dk-row-inner);
}
.stock-label { font-size: .8125rem; font-weight: 700; color: var(--dk-text-2); }
/* The figure is what the card is for, so it gets the size and the tabular digits that let
   one line up against the next. */
.stock-val { font-size: 1.0625rem; font-weight: 800; color: var(--dk-text-1); text-align: end; }
.stock-acts { display: flex; gap: .375rem; }
/* A card whose title already names the wallet (M14.59): the label column would repeat the
   heading two lines below it, so the figure takes the room and the size that go with being
   the only thing on the row. */
.stock-row--lead { grid-template-columns: 1fr auto; padding: .25rem .25rem .25rem .5rem;
                   background: none; }
.stock-row--lead .stock-val { font-size: 1.375rem; text-align: start; }
@media (max-width: 34rem) {
  /* The label keeps its line and the figure keeps the next one, rather than three columns
     fighting over 360px. */
  .stock-row { grid-template-columns: 1fr auto; }
  .stock-val { grid-column: 1; text-align: start; }
  .stock-acts { grid-row: 1 / span 2; grid-column: 2; align-self: center; }
}

/* A rule inside a panel: two related forms in one panel, separated but not boxed. */
.panel-rule {
  border: 0;
  border-top: 1px solid var(--dk-line);
  margin: 1rem 0;
  opacity: 1;
}

/* --------------------------------------------------------------- pill */
/* The reference's `.badge`: 3px/12px, a 5px gap, .75rem at 700. Wider and a step up
   in size from what was here — a status is a label, and at .6875rem in a 12px-padded
   pill it was reading as a footnote next to the row it describes. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: .3125rem;
  padding: .1875rem .75rem;
  border-radius: var(--dk-r-pill);
  border: 1px solid var(--tone-mute-bd);
  background: var(--tone-mute-bg);
  color: var(--tone-mute);
  font-size: .75rem;
  font-weight: 700;
  white-space: nowrap;
  line-height: 1.5;
}
.pill > i { font-size: .6875rem; }
.pill--ok { background: var(--tone-ok-bg); color: var(--tone-ok); border-color: var(--tone-ok-bd); }
.pill--warn { background: var(--tone-warn-bg); color: var(--tone-warn); border-color: var(--tone-warn-bd); }
.pill--bad { background: var(--tone-bad-bg); color: var(--tone-bad); border-color: var(--tone-bad-bd); }
.pill--info { background: var(--tone-info-bg); color: var(--tone-info); border-color: var(--tone-info-bd); }
.pill--brand { background: var(--tone-brand-bg); color: var(--tone-brand); border-color: var(--tone-brand-bd); }
.pill--syriatel { background: var(--net-syr-bg); color: var(--net-syr); border-color: var(--net-syr-bd); }
.pill--mtn { background: var(--net-mtn-bg); color: var(--net-mtn); border-color: var(--net-mtn-bd); }

/* A card that belongs to one carrier wears its border (M14.71, owner: "color the border
   whith carier color"). The rim only — the fill stays the shared glass, because two
   tinted grounds side by side read as two *states* of one thing rather than as two
   carriers, and the pill in the heading already says which is which in solid colour. */
.panel--syriatel { border-color: var(--net-syr-bd); }
.panel--mtn { border-color: var(--net-mtn-bd); }
:root[data-bs-theme="dark"] .pill--brand { color: var(--dk-text-1); }
.pill-dot { width: .4375rem; height: .4375rem; border-radius: 50%; background: currentColor; }
.pill .money-val { margin-inline-start: .25rem; font-weight: 700; }

/* --------------------------------------------------------------- empty state */
/* The old pages rendered a bare sentence in a table cell. An empty state is a
   real state and deserves composing. */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .625rem;
  padding: 2.75rem 1.5rem;
  text-align: center;
}
/* A faded glyph, not a tinted chip (M12). The reference's empty states are a large
   icon at 35% and a sentence — nothing that could be mistaken for a control. The
   chip that was here read as a button with its label missing, which on the SMS
   inbox was the entire page. */
.empty-ico {
  font-size: 2rem;
  line-height: 1;
  color: var(--dk-text-3);
  opacity: .35;
}
.empty-title { font-weight: 700; font-size: .9375rem; color: var(--dk-text-1); }
.empty-hint { font-size: .8125rem; color: var(--dk-text-2); max-width: 32ch; }

/* --------------------------------------------------------------- toolbar */
.toolbar {
  display: flex;
  align-items: center;
  gap: .75rem;
  flex-wrap: wrap;
  margin-bottom: .875rem;
  /* Was the positioning context for `.reveal-body`, which dropped out of the flex row.
     That is a dialog since M14.81 and needs nothing; kept because `.searchbar`'s auto
     margin and any absolutely-positioned control a toolbar grows next would want it, and
     a bare `position: relative` on a flex row costs nothing. */
  position: relative;
}
/* How many rows the table below holds (M14.81) — the figure that used to sit in the app
   bar. A wrapper rather than a class on the pill itself, because what this names is a
   *place* in the toolbar and not a new kind of chip: `ui.pill` already draws it, and the
   one thing a bare `<span>` in a flex row would get wrong is stretching to the row's
   height, which would make the chip taller than the buttons beside it. */
.toolbar-count { display: inline-flex; align-items: center; }
/* A toolbar is a row of controls above a table, and its controls are one step down
   from a form's — the reference's `.tbar` pins everything in it to 38px so the row
   reads as a strip rather than as a form that happens to be horizontal. */
.toolbar .form-control, .toolbar .form-select, .searchbar .form-control {
  min-height: var(--dk-ctl-h);
  padding-block: .25rem;
  font-size: .8125rem;
}
.toolbar-grow { flex: 1 1 12rem; min-width: 0; }
.toolbar-end { margin-inline-start: auto; display: flex; gap: .5rem; align-items: center; }

.search {
  position: relative;
  display: flex; align-items: center;
}
.search > i {
  position: absolute;
  inset-inline-start: .875rem;
  color: var(--dk-text-3);
  font-size: .8125rem;
  pointer-events: none;
  transition: color var(--dk-t-base);
  z-index: 1;
}
/* The magnifier takes the brand colour while the field is focused — the reference's
   `.inp-wrap:focus-within .inp-icon`, and the one detail that makes an icon inside a
   field read as part of the control rather than as decoration painted on top of it. */
.search:focus-within > i { color: var(--tone-brand-soft); }
.search > .form-control { padding-inline-start: 2.375rem; }

/* Field + submit as one group, pushed to the far end of a toolbar and capped in
   width. Without the cap the field stretched to fill the row and the button
   ended up the width of the page from it. */
.searchbar {
  display: flex;
  gap: .5rem;
  align-items: center;
  flex: 0 1 32rem;
  min-width: 0;
  margin-inline-start: auto;
}
.searchbar .search { flex: 1 1 auto; }

/* --------------------------------------------------------------- segmented */
/* Replaces .nav-tabs for period/network switches: one control, not a row of browser
   tabs. Retuned to the reference's `.seg` (M12) — and the change is not cosmetic. It
   was a pill track with a **brand-filled** thumb, which made every tab strip on the
   page compete with the page's actual primary button for the same "this is the thing
   to press" reading. The reference inverts it: the track is a recessed dark tint and
   the selected segment is a *raised light card* — 14px track, 10px segments, a lifted
   white face with a bevel. Selection reads as position rather than as emphasis. */
.seg {
  display: inline-flex;
  gap: .1875rem;
  padding: .25rem;
  border-radius: var(--dk-r-md);
  /* The reference's track is a 6% dark tint — a groove the selected segment sits
     raised inside. At 3.5% the track disappeared and the control read as one floating
     white pill with three words next to it. */
  background: var(--dk-ctl-bg-hover);
  border: 1px solid var(--dk-line-glass);
}
.seg > a, .seg > button {
  display: inline-flex; align-items: center; gap: .375rem;
  min-block-size: var(--dk-ctl-h);
  padding: .5rem .875rem;
  border: 0;
  background: transparent;
  border-radius: var(--dk-r-sm);
  font-size: .8125rem;
  font-weight: 700;
  color: var(--dk-text-2);
  transition: background var(--dk-t-pop) var(--dk-ease), color var(--dk-t-pop) var(--dk-ease),
              box-shadow var(--dk-t-pop) var(--dk-ease);
  white-space: nowrap;
}
.seg > a:hover, .seg > button:hover { color: var(--dk-primary-darker); background: var(--dk-surface-hover); }
/* The selected segment, once -- both components, both themes (M14.17).

   Six positions written out by hand, because no selector can say "the label at the same
   index as the checked radio": `:has()` could come close but it is Chrome 105+, and this
   page is an installed PWA on phones older than that. What is *not* written out by hand
   any more is a second copy of the six per theme, which is where this went wrong. The
   light copy stopped at position 4, and positions 5 and 6 had been appended to the dark
   rule's selector list without its `:root[data-bs-theme="dark"]` prefix. Measured on the
   portal's two five-tab strips with `dev.py`:

     - light, tab 5 selected: `rgba(0, 0, 0, 0)` / `rgb(100, 116, 139)` / no shadow --
       byte for byte an *unselected* tab. Nothing on screen said which pane was open, in
       the default theme, on the surface a shop's own customers use.
     - light, tab 6: the dark treatment, a 13% white wash under a 35%-black shadow, on a
       light ground.
     - dark, tab 4: the light treatment, so 5% instead of 13% -- precisely the failure
       the dark token's own comment describes.

   The tokens are the fix for the class rather than for the instance: a theme cannot own
   a partial copy of the position list when it does not own a copy at all. */
.seg > .is-active,
.seg > .is-active:hover,
.swap > .swap-radio:nth-of-type(1):checked ~ .swap-tabs > label:nth-child(1),
.swap > .swap-radio:nth-of-type(2):checked ~ .swap-tabs > label:nth-child(2),
.swap > .swap-radio:nth-of-type(3):checked ~ .swap-tabs > label:nth-child(3),
.swap > .swap-radio:nth-of-type(4):checked ~ .swap-tabs > label:nth-child(4),
.swap > .swap-radio:nth-of-type(5):checked ~ .swap-tabs > label:nth-child(5),
.swap > .swap-radio:nth-of-type(6):checked ~ .swap-tabs > label:nth-child(6) {
  background: var(--dk-seg-on-bg);
  color: var(--dk-seg-on-fg);
  box-shadow: var(--dk-seg-on-shadow);
}

/* Pane switch, no JavaScript (M6.12). Hidden radios + `:checked ~`.

   The position rules are written out once per position rather than derived, because
   no CSS selector can say "the pane at the same index as the checked radio" - `:has()`
   could come close but it is Chrome 105+, and this page is an installed PWA on phones
   older than that. **Six**, since M14.2: the portal's records switch grew an account
   pane and its send switch split bills from games.

   A seventh pane needs a seventh entry in each of the **three** lists - panes, selected
   tabs, focused tabs - and will silently render as a dead tab without one. It was four
   until M14.17, where the fourth turned out to be a per-theme copy of the selected list
   that had drifted two positions short; the selected look is three tokens now, so the
   only list that can go short is one a viewer's theme has nothing to do with.
   `test_m14_portal.py` holds each list against the templates separately, which is the
   part it was not doing: it took the highest position mentioned anywhere in the sheet,
   so one complete list covered for two incomplete ones.

   `clip` rather than `display:none` on the radio: a hidden-by-display input is
   removed from the tab order, which would take the arrow-key navigation the whole
   pattern is chosen for. */
.swap-radio {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.swap-tabs { max-width: 100%; overflow-x: auto; flex-wrap: nowrap; }
/* Breathing room between the switch and whatever it revealed. */
.swap-body { margin-top: 1rem; }
/* A pane holding a table gives most of the body padding back, keeping the same
   .625rem inset a flush body leaves — the table is a floating bar over a rounded
   card now, and taking it all the way to the panel's edge crops both. */
.swap-pane > .dtable-wrap { margin: 0 -.625rem -.625rem; }
.swap-tabs > label {
  min-block-size: var(--dk-ctl-h);
  padding: .5rem .875rem;
  border-radius: var(--dk-r-sm);
  font-size: .8125rem;
  font-weight: 700;
  color: var(--dk-text-2);
  cursor: pointer;
  white-space: nowrap;
  display: inline-flex; align-items: center; gap: .375rem;
  transition: background var(--dk-t-pop) var(--dk-ease), color var(--dk-t-pop) var(--dk-ease),
              box-shadow var(--dk-t-pop) var(--dk-ease);
}
.swap-tabs > label:hover { color: var(--dk-primary-darker); background: var(--dk-surface-hover); }
/* On a phone the strip is three or four Arabic phrases in one control, and it scrolls
   sideways with nothing to say so — the same unhinted overflow M6.17 measured on the
   tables. Tightening the segments is what makes it fit instead. */
@media (max-width: 34rem) {
  .seg { padding: .1875rem; }
  .seg > a, .seg > button, .swap-tabs > label {
    padding-inline: .5rem;
    font-size: .75rem;
    gap: .25rem;
  }
}
.swap-body > .swap-pane { display: none; }
.swap-body > .swap-pane { animation: pane-in var(--dk-t-pop) var(--dk-ease); }
@keyframes pane-in { from { opacity: 0; transform: translateY(4px); } }

.swap > .swap-radio:nth-of-type(1):checked ~ .swap-body > .swap-pane:nth-child(1),
.swap > .swap-radio:nth-of-type(2):checked ~ .swap-body > .swap-pane:nth-child(2),
.swap > .swap-radio:nth-of-type(3):checked ~ .swap-body > .swap-pane:nth-child(3),
.swap > .swap-radio:nth-of-type(4):checked ~ .swap-body > .swap-pane:nth-child(4),
.swap > .swap-radio:nth-of-type(5):checked ~ .swap-body > .swap-pane:nth-child(5),
.swap > .swap-radio:nth-of-type(6):checked ~ .swap-body > .swap-pane:nth-child(6) {
  display: block;
}
/* The focus ring has to be drawn on the label, since the input it belongs to is
   clipped out of sight. Without these four rules the pattern is unusable by
   keyboard — which is the defect DESIGN-SYSTEM.md flags in the reference. */
.swap > .swap-radio:nth-of-type(1):focus-visible ~ .swap-tabs > label:nth-child(1),
.swap > .swap-radio:nth-of-type(2):focus-visible ~ .swap-tabs > label:nth-child(2),
.swap > .swap-radio:nth-of-type(3):focus-visible ~ .swap-tabs > label:nth-child(3),
.swap > .swap-radio:nth-of-type(4):focus-visible ~ .swap-tabs > label:nth-child(4),
.swap > .swap-radio:nth-of-type(5):focus-visible ~ .swap-tabs > label:nth-child(5),
.swap > .swap-radio:nth-of-type(6):focus-visible ~ .swap-tabs > label:nth-child(6) {
  outline: 3px solid rgba(var(--dk-primary-rgb), .45);
  outline-offset: 2px;
}
.no-fx .swap-body > .swap-pane { animation: none; }

/* --------------------------------------------------------------- form field */
.field { display: flex; flex-direction: column; gap: .375rem; }
.field-label { font-size: .8125rem; font-weight: 700; letter-spacing: .01em; color: var(--dk-label); }
/* Arabic joins, and tracking of any size works against that. The same reason
   `.shell-group` zeroes it in RTL 1200 lines up; kept for Latin, where it is what the
   reference does. Both label classes, because they are the same component twice. */
[dir="rtl"] .field-label,
[dir="rtl"] .form-label { letter-spacing: 0; }
.field-hint { font-size: .6875rem; color: var(--dk-text-3); }
/* **Tops, not bottoms** (M14.42, owner: "not aligned verticaly, look like the page has
   issue"). `flex-end` aligned the row on its *bottom* edge, so a field carrying a
   `.field-hint` sat its label and its input a hint's height above the field beside it —
   two controls on one line at two different heights, on every form with a hint in a row.
   The shop-creation wizard is where it finally showed, because its first row pairs a
   hinted field with an unhinted one.

   Tops line up instead: labels are one line, so the controls under them land level and a
   hint hangs below where it belongs. `flex-start` is also the safe direction — what it
   can misalign is a label wrapping to two lines, and what `flex-end` misaligned was a
   hint, which is far commoner. */
.field-row { display: flex; gap: .5rem; align-items: flex-start; flex-wrap: wrap; }
/* A button standing in a row of fields is one of them, so it takes the field height
   rather than the toolbar height — otherwise a form's submit sits 8px short of the
   input beside it and the row reads as two rows that happen to overlap.

   And it keeps the bottom edge it always had: a button has no label above it, so aligning
   its *top* with a labelled field's top would float it a label's height above the input it
   stands beside. Fourteen of the thirty-nine field rows in the product carry one, which is
   why the flip above had to arrive with this.

   **The bottom edge is the control's bottom only while nothing hangs below the control**
   (M14.81, owner on the password panel: "this need a few alignment for button"). A
   `.field-hint` does: `flex-end` pins the button to the *tallest item's* bottom, so a row
   holding one hinted field dropped its submit by the hint's height — measured on that
   panel, inputs 1326→1364, hint 1370→1388, button 25px low, and the same 25px on the
   employee's own password form.

   Offsetting the button was tried and reverted: it is right on a shared line and wrong the
   moment the row wraps, where the button is alone on its line and a phantom label's worth
   of margin is just a gap. The cause is that a submit was standing in a row of fields at
   all — every other commit in the product sits on a line of its own, which is what
   `.dlg-foot`, `.wiz-nav` and now `.form-foot` are. Three password forms did it, all three
   moved, and `test_m1481` holds the invariant: no `.field-row` carries both a hint and a
   button. */
.field-row > .btn, .field-row > form > .btn {
  min-block-size: var(--dk-field-h);
  align-self: flex-end;
}
/* A page form's commit line — `.dlg-foot` for a form that is not in a dialog. The button
   is at the start, where a panel's own controls are, rather than pushed to the end: a
   panel is not a modal and has no cancel to be kept away from. */
.form-commit { display: flex; align-items: center; gap: .5rem; margin-block-start: 1rem; }
/* A colour input is 3rem of swatch under a label that is a phrase — five of them in a row
   let the labels collide into one line of run-on text and squeezed the last swatch to a
   sliver. The field is sized by its label, not by its control. */
/* A run of short fields that will not fit one line. A grid, not a wrapping flex row, so
   the inputs of a row sit level whatever their labels above them did (M14.44). */
.field-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
  gap: .75rem .5rem;
}
.field-grid > .field { justify-content: flex-end; }
/* The mark stands beside the control that replaces it, on the same bottom edge. */
.field-row > .logo-preview { align-self: flex-end; }

.field--color { min-inline-size: 8.5rem; }
.field--color .field-label { white-space: normal; line-height: 1.35; }
.field--color .form-control { padding: .25rem; block-size: 2.75rem; cursor: pointer; }

/* --------------------------------------------------------------- money */
/* `direction: ltr` + `unicode-bidi: isolate` is not cosmetic. A negative figure is
   `-2,375,000`, and inside RTL text the bidi algorithm moves the leading sign to the other
   end: the reconciliation panel was reporting a shortfall as `2,375,000-`. Isolating the
   figure keeps the sign where it belongs and stops a number adjacent to Arabic from being
   reordered against it.

   It goes on the FIGURE, not on `.money` as a whole: isolating the wrapper would take the
   currency suffix with it and put `ل.س` on the right of the digits, which is the wrong side
   in Arabic. `.num` (phone numbers, ids, usernames) has no trailing unit and is isolated
   outright. */
.money-val, .num {
  direction: ltr;
  unicode-bidi: isolate;
}
.money { font-family: var(--dk-font-body-en); font-variant-numeric: tabular-nums; font-weight: 600; }
/* The unit must never compete with the figure. At .75em of a 2rem stat value the
   currency rendered at 1.5rem and read as part of the number — "0ل.س". */
.money-cur {
  font-size: .68em;
  font-weight: 500;
  color: var(--dk-text-2);
  margin-inline-start: .3em;
}
.stat-value .money-cur { font-size: .42em; }
.money--pos { color: var(--tone-ok); }
.money--neg { color: var(--tone-bad); }

/* --------------------------------------------------------------- chart */
.chart { width: 100%; display: block; overflow: visible; }
/* A chart has a measure, the way text does (M10.4). These are server-rendered SVGs with
   a fixed viewBox and `preserveAspectRatio: meet`, so `width: 100%` does not stretch
   them — it leaves them at their drawn size, anchored to one edge, with whatever the
   container has left over sitting empty beside them. That was invisible while every
   chart lived in a half-width card and became obvious the moment one did not. The cap
   is a little over the 520-unit viewBox `hbars` draws in; the block then sits at the
   panel's start edge, under its title, in either direction. */
.chart-wrap { max-inline-size: 36rem; }
.chart-legend { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: .75rem; }
.chart-legend-item { display: flex; align-items: center; gap: .375rem; font-size: .75rem; color: var(--dk-text-2); }
.chart-legend-swatch { width: .625rem; height: .625rem; border-radius: 3px; }

/* --------------------------------------------- stock, as small multiples (M14.27) */
/* `chart.stock_split` (M14.33). CSS and not SVG, for the reason the rule it replaced
   gave: a fixed `viewBox` in a fluid panel draws in the leading half and leaves the rest
   empty. What changed is the shape. Six bars became three, each split between the two
   carriers who hold that stock, because they are parts of one total and two separate
   lengths asked the eye to subtract. The carrier names came out of the rows and went
   into one legend — printed twice now instead of twelve times, which is most of what
   made the panel look busy without saying anything. */
.stockbars {
  display: flex;
  flex-direction: column;
  gap: .875rem;
}

/* The legend, once, at the top. Right-aligned in both directions: it is a key to the
   bars below it, not a heading of its own, so it sits at the quiet end of the line. */
.stocklegend {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: .875rem;
  margin-block-end: .125rem;
}
.stocklegend-item {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  font-size: .75rem;
  font-weight: 700;
  color: var(--dk-text-2);
}
.stocklegend-dot {
  inline-size: .625rem;
  block-size: .625rem;
  border-radius: 999px;
  flex: none;
  /* The same inner bevel the segments carry, so the key and the thing it keys look like
     one material rather than a swatch beside a chart. */
  box-shadow: var(--dk-bevel-dim);
}

.stockbar { display: flex; flex-direction: column; gap: .375rem; }
.stockbar-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .5rem;
}
.stockbar-name { font-size: .8125rem; font-weight: 800; color: var(--dk-text-1); }
/* The kind's total. Tabular, because three of these sit under each other and a
   proportional comma makes a column of figures wobble. */
.stockbar-total {
  font-size: .8125rem;
  font-weight: 700;
  color: var(--dk-text-2);
  font-variant-numeric: tabular-nums;
}

/* The bar. Wider and taller than the pair it replaces — it has to hold type now. */
.stockbar-track {
  display: flex;
  gap: 2px;
  inline-size: 100%;
  block-size: 1.75rem;
  border-radius: var(--dk-r-sm);
  background: var(--dk-ctl-bg);
  overflow: hidden;
}
.stockbar-seg {
  display: flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 0;
  padding-inline: .5rem;
  font-size: .75rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  /* Every segment rounds every corner. With a 2px gap between them the bar reads as two
     objects sharing a track rather than one object with a seam, which is what it is:
     these are two carriers' wallets, not one quantity cut in half. */
  border-radius: var(--dk-r-sm);
  box-shadow: var(--dk-bevel-dim);
  white-space: nowrap;
  overflow: hidden;
}
/* Too narrow to hold its own figure. The segment stays — it is the share, and the share
   is the point — but the number moves to `.stockbar-aside`, because a stock level is
   something an operator acts on and must not be reachable only by hovering. */
.stockbar-seg.is-tight { padding-inline: 0; }
.stockbar-seg.is-tight > .stockbar-fig { display: none; }

.stockbar-aside {
  display: flex;
  justify-content: flex-end;
  gap: .75rem;
  font-size: .75rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* A phone. The head's two halves stay on one line — they are short — but the bar loses
   the type inside it before it loses the bar, so the figures fall to the aside line. */
@media (max-width: 30rem) {
  .stockbar-track { block-size: 1.375rem; }
  .stockbar-seg { font-size: .6875rem; padding-inline: .3125rem; }
}

/* No motion, no fill animation: the panel is read, not watched. `.no-fx` has nothing to
   turn off here, which is the correct amount. */

/* ------------------------------------------------- is the dialer alive (M14.27) */
/* `ui.device_bar`, the dashboard's first block. Asymmetric by design: alive is a
   hairline strip, stopped is a tinted block with the consequence spelled out. A
   dashboard that shouts good news teaches people to ignore it. */
.devbar {
  display: flex;
  align-items: center;
  gap: .625rem;
  margin-block-end: 1rem;
  padding: .5rem .875rem;
  border-radius: var(--dk-r-md);
  border: 1px solid var(--tone-ok-bd);
  background: var(--tone-ok-bg);
  color: var(--dk-text-2);
  font-size: .8125rem;
}
.devbar-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 1.5rem;
  block-size: 1.5rem;
  border-radius: 999px;
  background: var(--tone-ok);
  color: #fff;
  font-size: .75rem;
  flex: none;
}
.devbar-say { display: flex; flex-wrap: wrap; align-items: baseline; gap: .25rem .625rem; }
.devbar-say b { color: var(--tone-ok); font-weight: 800; }
.devbar-why { color: var(--dk-text-2); }
/* Pushed to the trailing end — `margin-inline-start: auto` on the flex child, so it
   mirrors with the page instead of needing a `[dir]` rule. Without it the block was a
   1,100px band with 300px of words at one end of it. */
.devbar-seen {
  margin-inline-start: auto;
  flex: none;
  font-size: .75rem;
  color: var(--dk-text-3);
}
.devbar-seen .num { font-family: var(--dk-font-body-en); font-weight: 700; }

/* Stopped. Bigger mark, bigger type, and the sentence that says what it costs. */
.devbar--down {
  gap: .875rem;
  padding: .875rem 1rem;
  border-color: var(--tone-bad-bd);
  background: var(--tone-bad-bg);
}
.devbar--down .devbar-mark {
  inline-size: 2.25rem;
  block-size: 2.25rem;
  background: var(--tone-bad);
  font-size: 1.0625rem;
}
.devbar--down .devbar-say { gap: .125rem 0; flex-direction: column; align-items: flex-start; }
.devbar--down .devbar-say b { color: var(--tone-bad); font-size: .9375rem; }
/* Both secondary runs go to the darkest ink on the stopped band, and that is a measured
   correction rather than a preference. The greys are calibrated against a near-white
   panel; this band is `--tone-bad-bg` over the page wash, which sampled at (242,214,219)
   — dark enough that `--dk-text-2` came out at **3.49:1** and `--dk-text-3` at 3.55:1,
   both under AA's 4.5 for 13px and 12px text. `--dk-text-3`'s own token comment records
   the same class of defect being fixed once already. On this ground `--dk-text-1` is
   ~15:1, and the red bold title still leads the block. */
.devbar--down .devbar-why,
.devbar--down .devbar-seen { color: var(--dk-text-1); }
/* On a phone the trailing figure has nowhere to be: pushed to the end by `margin-inline-
   start: auto` it squeezed the sentence beside it, which then wrapped *around* it in two
   ragged lines. Below 700px it takes its own row under the message, indented to the copy
   — which is the reading order anyway. */
@media (max-width: 700px) {
  .devbar { flex-wrap: wrap; }
  .devbar-seen {
    margin-inline-start: 0;
    inline-size: 100%;
    padding-inline-start: calc(2.25rem + .875rem);
  }
}

/* --------------------------------------------------------------- skeleton */
.skel {
  border-radius: var(--dk-r-sm);
  background: linear-gradient(90deg, var(--dk-skel-base), var(--dk-skel-hi), var(--dk-skel-base));
  background-size: 240% 100%;
  animation: skel 1.8s infinite;
}
@keyframes skel { to { background-position: -240% 0; } }
.no-fx .skel { animation: none; background: var(--dk-skel-base); }

/* --------------------------------------------------------------- spinner */
/* The reference's, at its size and its speed. A ring of the brand tint with one
   quadrant in the brand colour; nothing here spins as a page state, but a form that
   posts and a chart that is still drawing both want one. */
.spinner {
  inline-size: 32px; block-size: 32px;
  border: 3px solid rgba(var(--dk-primary-rgb), .18);
  border-top-color: var(--tone-brand-soft);
  border-radius: 50%;
  animation: spin .7s linear infinite;
}
.spinner--sm { inline-size: 15px; block-size: 15px; border-width: 2px; }
@keyframes spin { to { transform: rotate(360deg); } }
.loading-center { display: grid; place-items: center; min-block-size: 200px; }

/* --------------------------------------------------------------- avatar */
/* A brand-gradient disc with an initial in it — the reference's `.avatar`. Used by
   the app bar's identity island and by a customer row that has no logo. */
.avatar {
  display: grid; place-items: center;
  flex: 0 0 auto;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--dk-primary), var(--dk-primary-dark));
  color: var(--dk-on-primary);
  font-weight: 800;
  user-select: none;
}
.avatar--sm { inline-size: 30px; block-size: 30px; font-size: .75rem; }
.avatar--md { inline-size: 38px; block-size: 38px; font-size: .9375rem; }
.avatar--lg { inline-size: 56px; block-size: 56px; font-size: 1.5rem; }

/* ==========================================================================
   BOOTSTRAP BRIDGE
   Thirty templates already use these classes; they get the design language so
   nothing has to be rewritten just to look right.
   ========================================================================== */

.card {
  border-radius: var(--dk-r-md);
  border: 1px solid var(--dk-rim);
  border-top-color: var(--dk-rim-top);
  background: var(--dk-surface);
  box-shadow: var(--dk-sh-glass), var(--dk-bevel);
}
@supports (backdrop-filter: blur(1px)) {
  .card {
    background: linear-gradient(145deg, var(--dk-glass), rgba(255, 255, 255, .38));
    backdrop-filter: var(--dk-blur);
    -webkit-backdrop-filter: var(--dk-blur);
  }
  :root[data-bs-theme="dark"] .card {
    background: linear-gradient(145deg, var(--dk-glass), rgba(255, 255, 255, .03));
  }
}

/* A button (M12). Four things the reference's has and this one did not, and together
   they are most of why its controls feel physical:

     * `border-radius: 14px` and weight 700, not 10px and 600 — the same one-step-up
       in softness that makes its cards read the way they do;
     * a **shimmer sweep**: a diagonal highlight parked off the trailing edge that
       crosses the face on hover over .6s. GPU-composited (`transform` only), gated
       by `no-fx`, and on `::before` rather than the reference's `::after` because
       Bootstrap owns `::after` for carets and chevrons (DESIGN-SYSTEM.md trap 1);
     * a press that **scales** rather than nudging down — `scale(.97)` reads as the
       control taking the touch, which a 1px translate does not;
     * a fixed control height, so a row of buttons is a row and not a set of boxes
       each sized by its own label.

   `min-block-size` and not `height`: a label that wraps has to grow the button, and
   these are Arabic phrases whose translations are not all short. */
.btn {
  position: relative;
  z-index: 1;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  min-block-size: var(--dk-ctl-h);
  padding: .375rem 1rem;
  border-radius: var(--dk-r-md);
  font-weight: 700;
  font-size: .875rem;
  line-height: 1.35;
  white-space: nowrap;
  transition: transform var(--dk-t-exit) var(--dk-ease),
              box-shadow var(--dk-t-base) var(--dk-ease),
              background var(--dk-t-exit) var(--dk-ease),
              border-color var(--dk-t-exit) var(--dk-ease);
}
.btn::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: var(--dk-shimmer);
  transform: translateX(110%);
  transition: transform .6s var(--dk-ease);
}
[dir="rtl"] .btn::before { transform: translateX(-110%); }
.btn:hover:not(:disabled)::before { transform: translateX(-110%); }
[dir="rtl"] .btn:hover:not(:disabled)::before { transform: translateX(110%); }
.btn:active:not(:disabled) { transform: scale(.97); }
.btn:disabled, .btn.disabled { opacity: .5; }
/* The lift, for a button that is the page's one action rather than one of five. */
.btn--lift:hover:not(:disabled) { transform: translateY(-2px); }

.btn-sm {
  min-block-size: var(--dk-ctl-sm-h);
  padding: .25rem .625rem;
  font-size: .8125rem;
  gap: .375rem;
  border-radius: var(--dk-r-sm);
}
.btn-lg {
  min-block-size: 48px;
  padding: .625rem 1.5rem;
  font-size: 1rem;
  border-radius: var(--dk-r-md);
}
/* Square, for an icon with no label: a table row's rescue actions. */
.btn-icon { inline-size: var(--dk-ctl-h); padding-inline: 0; }
.btn-sm.btn-icon { inline-size: var(--dk-ctl-sm-h); }
.btn-full { inline-size: 100%; }

.btn-primary {
  background: linear-gradient(135deg, var(--dk-primary), var(--dk-primary-dark));
  border: none;
  color: var(--dk-on-primary);
  box-shadow: var(--dk-sh-brand-md), var(--dk-bevel-dim);
}
.btn-primary:hover {
  background: linear-gradient(135deg, var(--dk-primary), var(--dk-primary-dark));
  color: var(--dk-on-primary);
  box-shadow: var(--dk-sh-brand-lg), var(--dk-bevel-dim);
}

/* The glass variant. It keeps the *control* fill rather than the reference's white
   glass, and that difference is deliberate: a near-white button inside a near-white
   panel is the invisible-control defect M10 fixed — measured, on the treasury page,
   where the "0" button rendered as bare text beside three real buttons. What it
   takes from the reference is everything else: the bevel, the soft outer shadow, the
   sweep and the press. */
.btn-secondary, .btn-glass, .btn-light,
.btn-outline-secondary, .btn-outline-dark, .btn-outline-light {
  background: var(--dk-ctl-bg);
  border: 1px solid var(--dk-ctl-bd);
  color: var(--dk-text-1);
  box-shadow: var(--dk-sh-subtle), var(--dk-bevel-dim);
}
.btn-secondary:hover, .btn-glass:hover, .btn-light:hover, .btn-outline-secondary:hover,
.btn-outline-dark:hover, .btn-outline-light:hover {
  background: var(--dk-ctl-bg-hover);
  border-color: var(--dk-ctl-bd);
  color: var(--dk-text-1);
}
/* Light fills get the dark sweep — a white streak over a white face is nothing. */
.btn-secondary::before, .btn-glass::before, .btn-light::before,
.btn-outline-primary::before, .btn-outline-secondary::before, .btn-outline-danger::before,
.btn-outline-warning::before, .btn-outline-success::before, .btn-outline-info::before,
.btn-outline-dark::before, .btn-outline-light::before { background: var(--dk-shimmer-dark); }

/* Bare: an action that is a word, not an object. The reference's `.btn-bare`. */
.btn-bare {
  background: transparent;
  border: 0;
  box-shadow: none;
  color: var(--dk-text-2);
  padding: 0;
  min-block-size: 0;
}
.btn-bare:hover { background: transparent; color: var(--dk-text-1); }
.btn-bare::before { content: none; }

.btn-success { background: linear-gradient(135deg, #22C55E, #15803D); border: none; box-shadow: 0 8px 24px rgba(34, 197, 94, .28), var(--dk-bevel-dim); }
.btn-danger { background: linear-gradient(135deg, #EF4444, #B91C1C); border: none; box-shadow: 0 8px 24px rgba(239, 68, 68, .26), var(--dk-bevel-dim); }
.btn-warning { background: linear-gradient(135deg, #F59E0B, #B45309); border: none; color: #fff; box-shadow: 0 8px 24px rgba(245, 158, 11, .26), var(--dk-bevel-dim); }
.btn-info { background: linear-gradient(135deg, #3B82F6, #1D4ED8); border: none; color: #fff; box-shadow: 0 8px 24px rgba(59, 130, 246, .26), var(--dk-bevel-dim); }
.btn-outline-primary { border-color: rgba(var(--dk-primary-rgb), .45); color: var(--dk-primary-darker); background: var(--dk-ctl-bg); }
.btn-outline-primary:hover { background: rgba(var(--dk-primary-rgb), .12); border-color: var(--tone-brand-soft); color: var(--dk-primary-darker); }
.btn-outline-danger { border-color: var(--tone-bad-bd); color: var(--tone-bad); background: var(--dk-ctl-bg); }
.btn-outline-danger:hover { background: var(--tone-bad-bg); border-color: var(--tone-bad); color: var(--tone-bad); }
.btn-outline-warning { border-color: var(--tone-warn-bd); color: var(--tone-warn); background: var(--dk-ctl-bg); }
.btn-outline-warning:hover { background: var(--tone-warn-bg); color: var(--tone-warn); }
.btn-outline-success { border-color: var(--tone-ok-bd); color: var(--tone-ok); background: var(--dk-ctl-bg); }
.btn-outline-success:hover { background: var(--tone-ok-bg); color: var(--tone-ok); }
/* Completing the set: the queue's watch-only rows use the ghost variant of all
   three rescue actions, and `info` was the one Bootstrap default still showing
   through. */
.btn-outline-info { border-color: var(--tone-info-bd); color: var(--tone-info); background: var(--dk-ctl-bg); }
.btn-outline-info:hover { background: var(--tone-info-bg); color: var(--tone-info); }
/* One bevel and one soft shadow across the whole ghost set, so a row of three of them
   sits on the same plane as the filled button beside it. */
.btn-outline-primary, .btn-outline-danger, .btn-outline-warning,
.btn-outline-success, .btn-outline-info {
  box-shadow: var(--dk-sh-subtle), var(--dk-bevel-dim);
}

/* ------------------------------------------------- one state system per button (M14.31) */
/* Owner: "unify all button of same type to have same hover and active/focus effect",
   pointing at two `btn-sm btn-outline-primary` buttons rendering as a pale outline and a
   solid Bootstrap blue.

   They were the same button in two states. This file styled `:hover` for every variant
   and then stopped: `:focus-visible` existed only for `.btn-primary`, `:active` only as
   the shared `transform: scale(.97)`, and `:disabled` only as an opacity. Every state it
   did not claim fell through to Bootstrap, which is still loaded from the CDN and has
   opinions in its own palette. Measured on the page, not read off a changelog:

     .btn-outline-primary  press -> #0d6efd   focus -> white text on the resting near-white fill
     .btn-outline-danger   press -> #dc3545   focus -> white text on the resting near-white fill
     .btn-secondary        press -> #565e64   focus -> white text on the resting near-white fill
     .btn-primary          press -> #0a58ca   focus -> no ring at all
     .btn-warning/.btn-info  focus -> black text where the rest state is white

   Two separate defects in that. The shop's palette disappears the moment anybody presses
   anything — a Syrian telecom brand turning Bootstrap blue under the finger. And four
   variants put white text on their own near-white fill while focused, which is not a
   colour clash, it is a button with no legible label for as long as it holds focus.

   **Written as `--bs-btn-*` rather than as more rules.** Bootstrap's press selectors are
   `.btn:first-child:active` and `:not(.btn-check) + .btn:active` — 0,3,0 — which no
   single-class rule here can outrank; the file would have to grow a matching pile of
   `.btn-outline-primary:first-child:active` selectors and keep them in step for ever.
   Its state rules read these variables, so setting them turns Bootstrap's own machinery
   into a renderer for these tokens, and `:active`, `:focus-visible`, `.active`, `.show`
   and `:disabled` all follow from one declaration each.

   The `:hover` rules above are left exactly as they were and still win the tie (this
   sheet loads after Bootstrap's). The hover variables here are set to the same values
   they declare, so **focus and hover are the same appearance** — deliberately: a button
   under the pointer and a button reached by keyboard are in the same state as far as the
   viewer is concerned, and the ring is what distinguishes them. Press is one step deeper
   than hover, and nothing else moves, because `scale(.97)` is already the press. */
.btn {
  --bs-btn-focus-box-shadow: var(--dk-focus-ring);
  /* Bootstrap's is `inset 0 3px 5px rgba(0,0,0,.125)`: a 2012 inset press under a design
     whose press is a scale. */
  --bs-btn-active-shadow: none;
}
/* The ring is declared here as well as handed to Bootstrap through the variable above,
   and that is not belt-and-braces: `--bs-btn-focus-box-shadow` reaches only the paths
   Bootstrap's own selectors cover, while a variant's *resting* `box-shadow` in this file
   (`.btn-outline-*`, `.btn-primary`) is a plain 0,1,0 rule that a 0,2,0 focus rule has to
   outrank to be seen at all. Stated once, at the specificity that wins.

   `:focus-visible`, not `:focus`: a ring that survives a mouse click is the thing the
   owner photographed — a button left looking pressed after it had been used. Keyboard
   focus gets the ring; a click gets the button back. */
.btn:focus-visible { outline: 0; box-shadow: var(--dk-focus-ring); }

/* The filled set. Their fill is a gradient — a `background-image` — so Bootstrap's
   state `background-color` was always painting behind an opaque layer and was never the
   visible half of the bug. The text colour was: `.btn-warning` and `.btn-info` went from
   white to black on focus, because Bootstrap picks its own contrast colour for those
   two. Pinned to what the rest state already says. */
.btn-primary, .btn-success, .btn-danger, .btn-warning, .btn-info {
  --bs-btn-hover-color: var(--dk-on-primary);
  --bs-btn-active-color: var(--dk-on-primary);
  --bs-btn-disabled-color: var(--dk-on-primary);
  --bs-btn-hover-bg: transparent;
  --bs-btn-active-bg: transparent;
  --bs-btn-disabled-bg: transparent;
  --bs-btn-hover-border-color: transparent;
  --bs-btn-active-border-color: transparent;
  --bs-btn-disabled-border-color: transparent;
}

/* The neutral set, which is one control fill wearing six class names. */
.btn-secondary, .btn-glass, .btn-light,
.btn-outline-secondary, .btn-outline-dark, .btn-outline-light {
  --bs-btn-hover-color: var(--dk-text-1);
  --bs-btn-hover-bg: var(--dk-ctl-bg-hover);
  --bs-btn-hover-border-color: var(--dk-ctl-bd);
  --bs-btn-active-color: var(--dk-text-1);
  --bs-btn-active-bg: var(--dk-ctl-bd);
  --bs-btn-active-border-color: var(--dk-ctl-bd);
  --bs-btn-disabled-color: var(--dk-text-1);
  --bs-btn-disabled-bg: var(--dk-ctl-bg);
  --bs-btn-disabled-border-color: var(--dk-ctl-bd);
}

/* The ghost set. Each keeps its own ink through every state — that is the whole point of
   a ghost button — and deepens its wash by one step on press. The `-bg` and `-bd` tokens
   are already a pair at two strengths, and both are redefined for the dark theme, so
   this needs no second set of values for it. */
.btn-outline-primary {
  --bs-btn-hover-color: var(--dk-primary-darker);
  --bs-btn-hover-bg: rgba(var(--dk-primary-rgb), .12);
  --bs-btn-hover-border-color: var(--tone-brand-soft);
  --bs-btn-active-color: var(--dk-primary-darker);
  --bs-btn-active-bg: rgba(var(--dk-primary-rgb), .22);
  --bs-btn-active-border-color: var(--tone-brand-soft);
  --bs-btn-disabled-color: var(--dk-primary-darker);
  --bs-btn-disabled-bg: var(--dk-ctl-bg);
  --bs-btn-disabled-border-color: rgba(var(--dk-primary-rgb), .45);
}
.btn-outline-danger {
  --bs-btn-hover-color: var(--tone-bad);
  --bs-btn-hover-bg: var(--tone-bad-bg);
  --bs-btn-hover-border-color: var(--tone-bad);
  --bs-btn-active-color: var(--tone-bad);
  --bs-btn-active-bg: var(--tone-bad-bd);
  --bs-btn-active-border-color: var(--tone-bad);
  --bs-btn-disabled-color: var(--tone-bad);
  --bs-btn-disabled-bg: var(--dk-ctl-bg);
  --bs-btn-disabled-border-color: var(--tone-bad-bd);
}
.btn-outline-success {
  --bs-btn-hover-color: var(--tone-ok);
  --bs-btn-hover-bg: var(--tone-ok-bg);
  --bs-btn-hover-border-color: var(--tone-ok-bd);
  --bs-btn-active-color: var(--tone-ok);
  --bs-btn-active-bg: var(--tone-ok-bd);
  --bs-btn-active-border-color: var(--tone-ok);
  --bs-btn-disabled-color: var(--tone-ok);
  --bs-btn-disabled-bg: var(--dk-ctl-bg);
  --bs-btn-disabled-border-color: var(--tone-ok-bd);
}
.btn-outline-warning {
  --bs-btn-hover-color: var(--tone-warn);
  --bs-btn-hover-bg: var(--tone-warn-bg);
  --bs-btn-hover-border-color: var(--tone-warn-bd);
  --bs-btn-active-color: var(--tone-warn);
  --bs-btn-active-bg: var(--tone-warn-bd);
  --bs-btn-active-border-color: var(--tone-warn);
  --bs-btn-disabled-color: var(--tone-warn);
  --bs-btn-disabled-bg: var(--dk-ctl-bg);
  --bs-btn-disabled-border-color: var(--tone-warn-bd);
}
.btn-outline-info {
  --bs-btn-hover-color: var(--tone-info);
  --bs-btn-hover-bg: var(--tone-info-bg);
  --bs-btn-hover-border-color: var(--tone-info-bd);
  --bs-btn-active-color: var(--tone-info);
  --bs-btn-active-bg: var(--tone-info-bd);
  --bs-btn-active-border-color: var(--tone-info);
  --bs-btn-disabled-color: var(--tone-info);
  --bs-btn-disabled-bg: var(--dk-ctl-bg);
  --bs-btn-disabled-border-color: var(--tone-info-bd);
}
/* Bare is a word, not an object: no fill in any state, and no ring box to draw around
   nothing — it gets the ring on its text box, which is what it is. */
.btn-bare {
  --bs-btn-hover-color: var(--dk-text-1);
  --bs-btn-active-color: var(--dk-text-1);
  --bs-btn-disabled-color: var(--dk-text-2);
  --bs-btn-hover-bg: transparent;
  --bs-btn-active-bg: transparent;
  --bs-btn-disabled-bg: transparent;
  --bs-btn-hover-border-color: transparent;
  --bs-btn-active-border-color: transparent;
  --bs-btn-disabled-border-color: transparent;
}

/* A field (M12). The reference's `.inp-wrap` anatomy, minus the one thing that would
   break here: it fills with white glass, and inside these near-white panels a white
   field is the invisible-control defect M10 measured. So the fill stays the control
   fill, and everything that gives the reference's fields their character comes over —
   the 14px radius, the bright hairline just inside the top edge, the soft outer
   shadow, and a focus that **lifts the field a pixel** while a 4px brand halo blooms
   around it. */
.form-control, .form-select {
  min-height: var(--dk-field-h);
  padding: .5rem .875rem;
  border-radius: var(--dk-r-md);
  border: 1px solid var(--dk-ctl-bd);
  background: var(--dk-ctl-bg);
  color: var(--dk-text-1);
  font-size: .875rem;
  box-shadow: var(--dk-bevel-dim), var(--dk-sh-subtle);
  transition: border-color var(--dk-t-base) var(--dk-ease),
              box-shadow var(--dk-t-base) var(--dk-ease),
              background var(--dk-t-base) var(--dk-ease),
              transform var(--dk-t-base) var(--dk-ease);
}
.form-control-sm, .form-select-sm {
  min-height: var(--dk-ctl-h);
  padding: .25rem .625rem;
  font-size: .8125rem;
  border-radius: var(--dk-r-sm);
}
.form-control::placeholder { color: var(--dk-text-3); }
.form-control:focus, .form-select:focus {
  background: var(--dk-glass-strong);
  border-color: rgba(var(--dk-primary-rgb), .55);
  box-shadow: var(--dk-glow), var(--dk-bevel-dim), 0 4px 16px rgba(var(--dk-primary-rgb), .10);
  transform: translateY(-1px);
}
:root[data-bs-theme="dark"] .form-control:focus,
:root[data-bs-theme="dark"] .form-select:focus { background: rgba(255, 255, 255, .09); }
textarea.form-control { min-height: 5.5rem; }

/* The chevron, in the design system's own grey rather than Bootstrap's slate, and
   pointing at the trailing edge in both directions. `background-position` cannot be
   logical, so it is written out per direction — the RTL build flips its own value and
   would otherwise fight this one. */
.form-select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-size: 16px 16px;
  padding-inline-end: 2.375rem;
}
[dir="ltr"] .form-select { background-position: right .875rem center; }
[dir="rtl"] .form-select { background-position: left .875rem center; }
:root[data-bs-theme="dark"] .form-select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394A3B8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

.form-label { font-size: .8125rem; font-weight: 700; letter-spacing: .01em; color: var(--dk-label); margin-bottom: .375rem; }

/* The notched alert (M12) — the reference's single most distinctive small component,
   and the one this app had none of. Two pieces that read as one shape: a **solid**
   filled circle carrying the icon, and a tinted message block with a **concave bite**
   cut out of the edge facing it. The circle nests into the bite with a hairline gap,
   so the two share one continuous border line and the icon looks extracted from the
   block rather than dropped on top of it.

   The bite is a radial-gradient mask. Its centre is a physical edge — `at 100%` is the
   right edge whatever `dir` says — so the two directions are written out rather than
   left to a logical property that does not exist for gradient positions.

   Where the reference uses `color-mix()` for the border, this uses a plain rgba: the
   Android WebViews these shops run are older than `color-mix`, and a border that
   computes to nothing would take the shared line with it.

   Degrades honestly: a browser with no mask support draws a straight-edged block with
   a circle beside it, which is a normal alert. */
.alert {
  display: flex;
  align-items: center;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  font-size: .875rem;
  font-weight: 600;
  --alert-circle: 2.75rem;
  --alert-bite: 1.65rem;   /* slightly more than half the circle — that is the gap */
}
.alert-icon {
  flex: 0 0 auto;
  inline-size: var(--alert-circle);
  block-size: var(--alert-circle);
  border-radius: 50%;
  display: grid;
  place-items: center;
  position: relative;
  z-index: 2;
  margin-inline-end: calc(var(--alert-circle) / -2);   /* nest into the bite */
  background: var(--alert-c);
  color: var(--alert-on);
  border: 1px solid var(--alert-bd);
  font-size: 1rem;
}
.alert-msg {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: .5rem;
  padding-block: .625rem;
  padding-inline-start: calc(var(--alert-circle) / 2 + .9rem);   /* clear the bite */
  padding-inline-end: 1rem;
  line-height: 1.5;
  background: var(--alert-bg);
  color: var(--alert-text);
  border: 1px solid var(--alert-bd);
  border-radius: var(--dk-r-md);
  -webkit-mask: var(--alert-mask);
          mask: var(--alert-mask);
}
[dir="rtl"] .alert-msg {
  --alert-mask: radial-gradient(circle var(--alert-bite) at 100% center,
                transparent calc(var(--alert-bite) - .5px), #000 var(--alert-bite));
}
[dir="ltr"] .alert-msg {
  --alert-mask: radial-gradient(circle var(--alert-bite) at 0% center,
                transparent calc(var(--alert-bite) - .5px), #000 var(--alert-bite));
}
.alert-text { flex: 1 1 auto; min-width: 0; }
.alert-x {
  flex: 0 0 auto;
  margin-inline-start: auto;
  padding: .25rem;
  border: 0;
  background: none;
  color: inherit;
  opacity: .55;
  cursor: pointer;
  border-radius: var(--dk-r-xs);
  transition: opacity var(--dk-t-exit);
}
.alert-x:hover { opacity: 1; }

/* Per-tone: a solid circle in the saturated hue, a ~12% tinted block, and a border
   at ~45% shared by both. Bootstrap's own category names, so the templates keep
   theirs and only the markup inside changed. */
.alert-success { --alert-c: #16A34A; --alert-bg: var(--tone-ok-bg);   --alert-text: var(--tone-ok);   --alert-on: #fff; --alert-bd: rgba(34, 197, 94, .45); }
.alert-danger  { --alert-c: #DC2626; --alert-bg: var(--tone-bad-bg);  --alert-text: var(--tone-bad);  --alert-on: #fff; --alert-bd: rgba(239, 68, 68, .45); }
.alert-warning { --alert-c: #D97706; --alert-bg: var(--tone-warn-bg); --alert-text: var(--tone-warn); --alert-on: #fff; --alert-bd: rgba(245, 158, 11, .45); }
.alert-info    { --alert-c: var(--dk-primary); --alert-bg: var(--tone-brand-bg); --alert-text: var(--dk-primary-darker); --alert-on: var(--dk-on-primary); --alert-bd: rgba(var(--dk-primary-rgb), .45); }
:root[data-bs-theme="dark"] .alert-info { --alert-text: var(--dk-text-1); }
/* The message text is the 800 shade in light and the 400 in dark, but the circle is
   a fill with a label on it and has to stay saturated in both. */
:root[data-bs-theme="dark"] .alert { --alert-on: #0B1220; }
:root[data-bs-theme="dark"] .alert-info { --alert-on: var(--dk-on-primary); }

/* The `.flashes` stack that used to live here is gone with the include that produced it
   (M14.13): nothing renders that class any more, so the rules and the `flash-in`
   keyframes were dead. `ui.alert` itself is untouched -- a page that wants a standing
   explanation in its own flow still renders one. */

/* ---------------------------------------------------------------- toasts */
/* Flashes, as a toast over the page rather than a block inside it (M14.13, owner
   direction).

   In the card layouts a flash was laid out *between* the step dots and the form, which
   put a refused login code in the one place that pushed everything below it down — the
   card grew, the field moved out from under the cursor, and the message competed with
   the brand for the top of the card. A message about what just happened is not part of
   the form's structure, and the reference does not treat it as one: its `Toaster` is
   fixed, narrow, and outside the layout entirely (`organisms/Toaster/Toaster.css`).

   Ported from that, with two departures. It is centred on the inline axis instead of
   pinned left, because this product has no fixed topbar on the auth pages to sit under
   and a centred strip reads the same in both directions. And there is no icon circle and
   no dismiss button: the reference's toast nests the same 44px filled circle the inline
   alert does, which on a 26rem strip is most of the height, and between that circle, the
   emoji the message text used to carry, and a close button, one sentence arrived wearing
   three marks. One small glyph in the tone colour, and the message. */
.toasts {
  position: fixed;
  inset-block-start: 1rem;
  inset-inline: 0;
  /* Above Bootstrap's modal (1055) and its backdrop: a flash that fires while a dialog
     is open is usually about the dialog. */
  z-index: 1090;
  margin-inline: auto;
  inline-size: min(92vw, 26rem);
  display: flex;
  flex-direction: column;
  /* Each toast is as wide as its sentence and no wider, centred in the strip. `الكود
     خاطئ` is three words; in a fixed 26rem bar it is three words and a lot of tinted
     nothing. The strip keeps the 26rem as the point at which a long message wraps. */
  align-items: center;
  gap: .5rem;
  /* The strip spans the viewport to centre itself, so it must not eat clicks across
     the whole width — only the toasts themselves are targets. */
  pointer-events: none;
}
/* The tone tokens live on `.alert-success` / `.alert-danger` / … , which are pure
   custom-property carriers with no appearance of their own. Reused here rather than
   redeclared, so a tone stays one definition. */
.toast-item {
  pointer-events: auto;
  cursor: pointer;
  display: flex;
  align-items: center;
  max-inline-size: 100%;
  gap: .625rem;
  padding: .8125rem 1rem;
  border-radius: var(--dk-r-md);
  border: 1px solid var(--alert-bd);
  background: var(--alert-bg);
  color: var(--alert-text);
  font-size: .875rem;
  font-weight: 600;
  line-height: 1.5;
  box-shadow: var(--dk-sh-lg);
  /* In, hold, out — and the out is a *delayed* second animation rather than late
     keyframes in one. Two reasons, both about not needing a script. A fixed toast that
     nobody dismisses is not a block that scrolls away, it is an overlay that stays for
     the rest of the page view, so the hide cannot be JavaScript's job. And the a11y
     block near the end of this sheet clamps `animation-duration` to .001ms for a
     reduced-motion viewer but leaves `animation-delay` alone: a hold encoded as
     duration would collapse to nothing and the message would be gone before it was
     read, while a hold encoded as delay survives exactly as written. `ui.js` only adds
     click-to-dismiss on top. */
  animation: toast-in var(--dk-t-pop) var(--dk-ease),
             toast-out var(--dk-t-exit) var(--dk-ease) 5s forwards;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .toast-item { -webkit-backdrop-filter: var(--dk-blur); backdrop-filter: var(--dk-blur); }
}
.toast-ico { flex: 0 0 auto; font-size: 1rem; color: var(--alert-c); }
.toast-text { flex: 1 1 auto; min-width: 0; }
/* `forwards`, and the element is removed by `ui.js` on animationend — the same shape as
   the language menu's exit, and for the same reason: there is no state between "here"
   and "gone" that CSS can hold on its own. */
/* Dismissed by hand: the same exit, now rather than at 5s. */
.toast-item.is-going {
  animation: toast-out var(--dk-t-exit) var(--dk-ease) forwards;
}
@keyframes toast-in  { from { opacity: 0; transform: translateY(-12px) scale(.97); } }
/* `visibility` is discrete but animatable, and with `forwards` it is what the ended
   state holds: without it the toast is a transparent 26rem strip still answering
   clicks across the top of the page. */
@keyframes toast-out { to   { opacity: 0; transform: translateY(-12px) scale(.97); visibility: hidden; } }

.badge { border-radius: var(--dk-r-pill); font-weight: 700; padding: .1875rem .75rem; font-size: .75rem; }

.nav-tabs { border-bottom: 1px solid var(--dk-line-glass); gap: .25rem; }
.nav-tabs .nav-link {
  border: 0;
  border-radius: var(--dk-r-sm) var(--dk-r-sm) 0 0;
  color: var(--dk-text-2);
  font-size: .875rem;
  font-weight: 600;
  padding: .5rem .875rem;
}
.nav-tabs .nav-link:hover { background: var(--dk-surface-hover); color: var(--dk-text-1); }
.nav-tabs .nav-link.active {
  background: var(--dk-glass-strong);
  color: var(--dk-primary-darker);
  box-shadow: inset 0 -2px 0 var(--tone-brand-soft);
}
:root[data-bs-theme="dark"] .nav-tabs .nav-link.active { color: var(--dk-text-1); background: rgba(255,255,255,.07); }

.table { --bs-table-bg: transparent; color: var(--dk-text-1); font-size: .8125rem; }
.table > :not(caption) > * > * { background: transparent; border-bottom-color: var(--dk-line); }
.table thead th {
  font-size: .6875rem;
  font-weight: 700;
  color: var(--dk-primary-darker);
  /* `to bottom` for the same reason as `.dtable thead th` above: a per-cell diagonal
     seams at every column boundary. */
  background: linear-gradient(to bottom, rgba(var(--dk-primary-rgb), .20), rgba(var(--dk-primary-rgb), .12));
  border-bottom: 1px solid rgba(var(--dk-primary-rgb), .24);
  white-space: nowrap;
}
:root[data-bs-theme="dark"] .table thead th { color: var(--dk-text-1); }
.table tbody tr:hover > * { background: var(--dk-surface-hover); }

.modal-content { border-radius: var(--dk-r-lg); border: 1px solid var(--dk-rim); background: var(--dk-glass-strong); }
.dropdown-menu {
  border-radius: var(--dk-r-md);
  border: 1px solid var(--dk-rim);
  background: var(--dk-glass-strong);
  box-shadow: var(--dk-sh-lg);
  padding: .375rem;
}
.dropdown-item { border-radius: var(--dk-r-xs); font-size: .875rem; padding: .5rem .75rem; }
.dropdown-item:hover { background: rgba(var(--dk-primary-rgb), .10); color: var(--dk-primary-darker); }

/* --------------------------------------------------------- viewer controls */
/* Language and theme (M14.10). Both change the page **in place** — `js/viewer.js` flips
   the theme with no network at all, and fetches the same URL in the other language and
   adopts the document rather than navigating to it. Everything below is what they look
   like while they do it.

   They were two segment strips (M14.9), and before that two icon links that each showed
   the state you would *get*: a moon meant "you are in light". Segments fixed the reading
   and kept the page reload; this fixes the reload and keeps the reading.

   One track holds both, with a hairline between them, because two adjacent bordered
   pills read as loose buttons and one track reads as one utility control — which is what
   it is. */
/* **Each control is its own `.island`** — the app bar's component, carried on the element
   rather than restated here (owner direction 2026-08-30: "use the same island styles used
   in the title bar", then "separated island for each").

   It went one step at a time and the last step is the one that matters. It began as a
   recessed `--dk-ctl-bg-hover` groove holding both, which is the right ground for a
   segment strip and the wrong one here: in the CRM this cluster stands in a row with the
   burger, the page title and the dialer pill, and it was the only object in that row
   pressed *into* the bar while everything else floated above it. Making the groove an
   island fixed the ground. Splitting it fixed the grouping — the bar's other islands are
   one object each, and language and theme are two unrelated settings that were sharing a
   pill only because they had shared a strip before it.

   So there is nothing left here but the row, at the bar's own `.5rem`. Height, radius,
   rim, glass, bubble shadow and blur all come from `.island`; the hairline that used to
   separate the two is gone with the thing it was separating. */
.viewer-controls {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
}

/* With no shell to hold them, they pin where a topbar would be — not floating over a
   bottom corner, which is the furthest point from anything they affect. */
.viewer-controls--float {
  position: fixed;
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
  z-index: 1030;
}
@media print { .viewer-controls--float { display: none; } }

/* ------------------------------------------------------------ language menu */
/* A dropdown, on owner direction, and explicitly so a third language is a row in
   `presentation.LANGUAGES` and nothing else. A `<details>` and not a `<select>`: it
   opens with no script, and the native select on the Android browsers these shops run
   is a full-screen list of bare strings — no room for a language named in its own
   script beside its code, which is the whole reason the menu has two columns. */
/* The island is the `<details>`, so the whole pill lifts when the menu opens and the
   popover positions against it. */
/* `gap: 0` because the island has exactly one child in flow. `.island`'s own `.5rem` is
   for a bar full of icon-plus-label pills; here it was measurably widening the pill by
   8px past its summary, leaving a dead strip at the trailing end that looked like
   padding and did not answer a click. Measured with `dev.py probe`, not spotted. */
.lang-pick { position: relative; padding-inline: 0; gap: 0; }
.lang-pick:hover { background: var(--dk-glass-hover); }
.lang-pick[open] { background: var(--dk-glass-strong); box-shadow: var(--dk-sh-md), var(--dk-bevel); }
/* Both marker resets are needed or WebKit keeps drawing its triangle inside the face. */
.lang-pick > summary { list-style: none; cursor: pointer; }
.lang-pick > summary::-webkit-details-marker { display: none; }
.lang-pick > summary::marker { content: ""; }

.lang-pick-face {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  /* Carries the island's inline padding rather than the island doing it, so the hit area
     is the whole pill: a summary that stopped short of its own padding would be a control
     with a dead 14px margin, which is exactly the sort of thing nobody reports and
     everybody feels. `block-size: 100%` for the same reason on the other axis. */
  block-size: 100%;
  padding-inline: .75rem;
  border-radius: var(--dk-r-pill);
  font-size: .8125rem;
  font-weight: 700;
  color: var(--dk-text-2);
  transition: color var(--dk-t-pop) var(--dk-ease);
}
.lang-pick:hover > .lang-pick-face,
.lang-pick[open] > .lang-pick-face { color: var(--dk-primary-darker); }
:root[data-bs-theme="dark"] .lang-pick[open] > .lang-pick-face { color: var(--dk-text-1); }
.lang-pick-face .fa-globe { font-size: .9375rem; opacity: .8; }
/* `ع` and `EN` are nowhere near the same width; a floor on the label keeps the whole
   cluster from resizing under the pointer when the language changes. */
.lang-pick-now { min-inline-size: 1.75rem; text-align: center; }
.lang-pick-caret {
  font-size: .625rem;
  opacity: .55;
  transition: transform var(--dk-t-pop) var(--dk-ease);
}
.lang-pick[open] .lang-pick-caret { transform: rotate(180deg); }
/* Turns back with the panel rather than after it (M14.13). Through the exit the element
   is still `[open]` — that is the whole mechanism — so the rule above still matches and
   the caret would hold at 180° until the attribute went, then unwind into a panel that
   had already gone. Same weight as that rule, so it has to sit after it: written up
   beside the `furl` keyframes it lost the tie and did nothing, which `dev.py` reported
   as a caret transform of `matrix(-1, 0, 0, -1, 0, 0)` mid-close. */
.lang-pick.is-closing .lang-pick-caret { transform: none; }
/* Waiting on the page in the other language. The caret is already the thing that
   turns, so it is the thing that spins — no second indicator appears. */
.lang-pick-face.is-busy { pointer-events: none; }
.lang-pick-face.is-busy .lang-pick-caret { animation: spin .7s linear infinite; opacity: .8; }

/* `.pop-body` is the design system's floating surface — the same glass, rim and shadow
   the reject-reason popover and the row menus use, so this is not a fourth kind of
   panel. Only the things a menu needs differently are restated. */
.lang-menu {
  flex-direction: column;
  gap: .125rem;
  min-inline-size: 11.5rem;
  padding: .4375rem;
  margin-block-start: .5rem;
}
.lang-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  padding: .5rem .75rem;
  border-radius: var(--dk-r-sm);
  font-size: .875rem;
  font-weight: 700;
  color: var(--dk-text-1);
  transition: background var(--dk-t-pop) var(--dk-ease);
}
.lang-opt:hover, .lang-opt:focus-visible { background: var(--dk-ctl-bg-hover); color: var(--dk-text-1); }
/* The row keeps the page's direction so the codes stay in one column; only the name
   itself is isolated, which is all the bidi algorithm needs to set it correctly. */
.lang-opt-name { unicode-bidi: isolate; }
.lang-opt-short { font-size: .75rem; color: var(--dk-text-3); }
/* The live one is marked the way the segments marked it — a tinted face, not a tick. */
.lang-opt.is-active { background: var(--tone-brand-bg); color: var(--tone-brand); }
.lang-opt.is-active .lang-opt-short { color: var(--tone-brand-soft); }
/* On dark that tinted face is not a face (M14.22). A mulberry tint has no luminance to
   spend on a near-black ground: the selected row measured 1.07:1 against its unselected
   neighbour -- a 7% step, where a boundary wants 3:1 -- so nothing on screen marked the
   live language, while its label sat at 4.91:1 against the unselected label's 10.89:1
   and the row read as the *disabled* one of the two. Both halves are the standing
   dark-mode answer for a brand-tinted face and `.nav-tabs .nav-link.active` already
   gives it: a white wash for the ground, `--dk-text-1` for the label. Through
   `--dk-seg-on-bg` rather than a fresh 13%, since a selected row raised out of a track
   is the thing that token is for. The hue stays on the row in its code, which
   `--tone-brand-soft` has just made readable. */
:root[data-bs-theme="dark"] .lang-opt.is-active {
  background: var(--dk-seg-on-bg);
  color: var(--dk-text-1);
}

/* ------------------------------------------------------------- account menu */
/* `ui.user_menu` (M14.27). The CRM's end zone was a theme island and a language island
   and nothing that said whose session this was; the identity is the island now and both
   knobs moved inside it.

   Built on `.island` and `.pop-body` rather than on its own ground, for the reason
   `.viewer-controls` is not allowed any: there is one definition of what a floating bar
   object is and one of what a menu surface is. What is here is only the parts a menu
   with a face has — the avatar, the two-line name, and the rows. */
/* `gap: 0` is not tidiness (M14.29, owner: "the bg white color is shorter than it
   should be"). `.island` sets `gap: .5rem` for the icon-and-label islands, and a
   `<details>` has *two* flex children — the summary and the slot its content sits in.
   `.user-pop` is `position: absolute` and so out of flow, but the slot still counts,
   and the island reserved 8px of gap for a box with nothing in it. Measured: the island
   was 212px wide holding a 202px face, and the 8px landed entirely on one side, at the
   inline end. What showed there was `.island`'s own weaker `--dk-glass` next to the
   face's `--dk-glass-strong`, and at pill radius that reads as a crescent of the wrong
   colour outside the white — an outline with too much room in it. `padding-inline: 0`
   on the line below is the same override for the same reason: the summary does the
   spacing, so the island must claim none. */
.user-menu { position: relative; padding-inline: 0; gap: 0; }
.user-face {
  display: flex;
  align-items: center;
  gap: .5rem;
  block-size: 100%;
  padding-inline: .5rem .875rem;
  border-radius: var(--dk-r-pill);
  cursor: pointer;
  list-style: none;
}
/* Safari still paints a disclosure triangle without this, and the caret below is the
   affordance. `::marker` for everyone else. */
.user-face::-webkit-details-marker { display: none; }
.user-face::marker { content: ''; }
.user-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 1.75rem;
  block-size: 1.75rem;
  border-radius: 999px;
  background: linear-gradient(135deg, var(--dk-primary), var(--dk-primary-dark));
  color: var(--dk-on-primary);
  font-size: .8125rem;
  font-weight: 800;
  flex: none;
}
.user-who { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.15; }
/* Capped like the title island: a real name is a phrase and the bar must not grow a
   second line to hold one. 150px is the reference's own cap, on both the trigger's copy
   and the card head's. */
.user-name,
.user-pop-head b {
  max-inline-size: 9.375rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: .875rem;
  font-weight: 800;
  line-height: 1.1;
  color: var(--dk-primary-darker);
}
.user-role,
.user-pop-head small {
  max-inline-size: 9.375rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: .75rem;
  line-height: 1.1;
  color: var(--dk-text-2);
}
:root[data-bs-theme="dark"] .user-name,
:root[data-bs-theme="dark"] .user-pop-head b { color: var(--dk-text-1); }
.user-caret {
  font-size: .625rem;
  flex: none;
  color: var(--dk-text-3);
  transition: transform var(--dk-t-pop) var(--dk-ease), color var(--dk-t-pop) var(--dk-ease);
}
/* Open reads as open on the island itself, not only on the caret — the reference lifts
   the trigger's ground and rings it while its panel is out. Without that the bar has an
   object in it that looks unpressed with a menu hanging off it. */
.user-menu[open] > .user-face { background: var(--dk-glass-strong); border-radius: var(--dk-r-pill); }
.user-menu[open] { border-color: var(--tone-brand-soft); box-shadow: var(--dk-glow), var(--dk-sh-bubble); }
/* `--tone-brand-soft` and not `--dk-primary`, which is what the reference uses here: the
   raw brand value is the one colour base.html does *not* vary by theme, so as ink on a
   near-black ground it is darker than what it sits on (M14.22). The token is that value
   made readable, and a caret is an icon beside a label — which is the case it is for. */
.user-menu[open] .user-caret { transform: rotate(180deg); color: var(--tone-brand-soft); }
/* The name and role are the whole face at phone widths; below 640px — the reference's
   own breakpoint for this — only the avatar survives, which is what the burger and the
   page title leave room for. */
@media (max-width: 640px) {
  .user-who, .user-caret { display: none; }
  .user-face { padding-inline: .5rem; }
  .user-pop { inline-size: min(16.25rem, calc(100vw - 2rem)); }
}

.user-pop {
  position: absolute;
  inset-inline-end: 0;
  inset-block-start: calc(100% + .5rem);
  z-index: 40;
  flex-direction: column;
  gap: .125rem;
  /* The reference's 260px. Fixed rather than a minimum: a menu whose width depends on
     the longest row in it is a menu that changes width when the language does. */
  inline-size: 16.25rem;
  min-inline-size: 0;
  padding: .5rem;
  /* The same glass, over an opaque base, and the base is the correction. `.pop-body`'s
     ground is 78-90% white and leans on `backdrop-filter` for the rest — but a
     backdrop-filter element inside another one (this menu inside its `.island`) filters
     only its parent's backdrop root, not the page. So nothing behind it was ever
     blurred: over the login card's empty wash that is invisible, and over the CRM's
     dashboard the customer count read straight through the menu. Layering the same
     gradient onto `--dk-wash-1` keeps the surface identical and makes it opaque, in
     both themes, in the shop's own hue — the wash is a hex from the tenant palette. */
  background: linear-gradient(145deg, var(--dk-glass-strong), var(--dk-glass-hover)),
              var(--dk-wash-1);
}
/* The card head, and the divider under it. This is the anatomy that makes the panel an
   account card rather than a list of links, and it is the one part of the reference's
   `UserMenu` this did not have on the first pass: the avatar and the name appear again,
   larger, inside the panel. It reads as confirmation of *whose* menu is open — which
   matters most in the case the header exists for, a super-admin four shops deep. */
.user-pop-head {
  display: flex;
  align-items: center;
  gap: .625rem;
  padding: .5rem .5rem .625rem;
}
.user-pop-head .user-mark { inline-size: 2.25rem; block-size: 2.25rem; font-size: 1rem; }
.user-pop-who { display: flex; flex-direction: column; gap: .125rem; min-inline-size: 0; }
.user-pop-sep {
  block-size: 1px;
  margin: .125rem .25rem .375rem;
  background: var(--dk-line);
}
.user-item {
  display: flex;
  align-items: center;
  gap: .625rem;
  padding: .5rem .75rem;
  border-radius: var(--dk-r-sm);
  font-size: .875rem;
  font-weight: 700;
  color: var(--dk-text-1);
  transition: background var(--dk-t-pop) var(--dk-ease);
}
.user-item:hover, .user-item:focus-visible { background: var(--dk-ctl-bg-hover); color: var(--dk-text-1); }
.user-item .fa-solid { inline-size: 1rem; text-align: center; color: var(--tone-brand-soft); }
/* The way out is the way out, and it is the one row here that ends a session. */
.user-item--exit { color: var(--tone-bad); }
.user-item--exit:hover { background: var(--tone-bad-bg); color: var(--tone-bad); }
.user-item--exit .fa-solid { color: var(--tone-bad); }

/* The appearance block: a label, then the two controls the bar used to carry. */
.user-pop-block {
  display: flex;
  flex-direction: column;
  gap: .375rem;
  margin-block: .25rem;
  padding: .5rem .75rem;
  border-radius: var(--dk-r-sm);
  border: 1px solid var(--dk-line);
  background: var(--dk-ctl-bg);
}
.user-pop-label {
  font-size: .625rem;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--dk-text-3);
}
/* In here the cluster stacks: the switch is a wide object and the language rows are
   full-width, so side by side they would each get half of a 15rem menu. */
.viewer-controls--menu { flex-direction: column; align-items: stretch; gap: .5rem; }
.viewer-controls--menu .daynight { align-self: flex-start; }
/* The flat language list — `.lang-opt` unchanged, without the surface `.lang-menu`
   would have put under it. A menu inside a menu is the thing this layout avoids. */
.lang-flat { display: flex; flex-direction: column; gap: .125rem; }

/* -------------------------------------------------------- day / night switch */
/* The theme control, and the one place in this sheet that draws a picture.

   M14.9's sun|moon strip reported the state correctly and the owner's objection to it
   was that it had no imagination, which is fair on this control specifically: a strip
   can say which of two states you are in, and this is the only control in the product
   whose entire subject is the *change* between them. So it depicts rather than labels —
   a sky that travels from day to night, the sun setting into a crescent as it goes.

   Its colours are literal and not the shop's. That is the same licence the semantic
   triad already takes (`--tone-ok` is green in a mulberry shop and in a gold one): a
   control that draws a thing is coloured like the thing. Everything around it — the
   track it sits in, the focus ring, the hairline — stays on the brand.

   Anatomy. All of it decorative and `aria-hidden`; the state itself lives in
   `aria-checked`, which is also the only selector below that reads it:

     .dn-sky        the track — day gradient to night gradient, and the clip for
                    everything inside it
     .dn-sky::after the sun's eight rays, one 4px dot wearing a ring of box-shadows,
                    so they spin and shrink away as one object
     .dn-star       three, fading and settling in on night, staggered
     .dn-cloud      one, with a second puff on ::before, dropping out of frame on night
     .dn-orb        the traveller: sun, then moon once ::before — a disc of night sky,
                    clipped to the orb — scales in over its trailing edge

   Geometry is a 72x36 track with a 24px orb inset 6px, so the travel is 36px and the
   rays at a 15px radius clear the disc by 1.5px and the track's edge by 1.5px. It was
   56x28 until the owner asked for the controls to be bigger; every number below is that
   one scaled, which is why they are written as pixels and not as a scale factor —
   the rays have to miss two edges at once and only arithmetic says whether they do. */
/* `.island` supplies the pill, the glass, the shadow and the 48px; this is only the
   inset that seats the 36px sky inside it. `.island`'s own `.875rem` is sized for a word
   and would leave the switch swimming. */
.daynight {
  padding-inline: .375rem;
  -webkit-tap-highlight-color: transparent;
}
/* A softer, wider ring than the global one: these two sit on the page background rather
   than inside a card, where 2px of solid brand at 2px offset reads as a box drawn round
   them. (This used to restate `border-radius: var(--dk-r-pill)` too, to undo the radius
   the a11y rule forced on anything focused. That line is gone from the a11y rule, so the
   pill comes from `.island` and `.lang-pick-face` as it should.) */
.daynight:focus-visible,
.lang-pick-face:focus-visible {
  outline: 3px solid rgba(var(--dk-primary-rgb), .45);
  outline-offset: 3px;
}
.dn-sky {
  /* One number for the travel, used by the orb and by the rays that follow it. */
  --dn-travel: 2.25rem;
  /* The crescent's bite: the night sky as it is *at the orb's night seat*, which is
     part way down a gradient. It is only ever seen clipped inside the moon, where a
     percent or two out reads as the dark side rather than as a wrong colour. */
  --dn-night: #1b2547;
  position: relative;
  display: block;
  inline-size: 4.5rem;
  block-size: 2.25rem;
  border-radius: var(--dk-r-pill);
  /* Clipped: the rays and the cloud are drawn to the sky's edges, and a sun spilling
     amber dots onto the grey cluster around it is not a lit control, it is a leak. */
  overflow: hidden;
  background: linear-gradient(165deg, #8ecdf3 0%, #4f9fe0 100%);
  box-shadow: inset 0 1px 3px rgba(15, 23, 42, .3), inset 0 -1px 0 rgba(255, 255, 255, .22);
  transition: background var(--dk-t-base) var(--dk-ease),
              box-shadow var(--dk-t-base) var(--dk-ease);
}
.daynight[aria-checked="true"] .dn-sky {
  background: linear-gradient(165deg, #26325c 0%, #0f1730 100%);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, .5), inset 0 -1px 0 rgba(255, 255, 255, .07);
}

.dn-star {
  position: absolute;
  inline-size: 3px; block-size: 3px;
  border-radius: 50%;
  background: #fff;
  opacity: 0;
  transform: scale(.2);
  transition: opacity var(--dk-t-base) var(--dk-ease),
              transform .4s var(--dk-ease-spring);
}
.dn-star--1 { inset-block-start: 8px;  inset-inline-start: 12px; }
.dn-star--2 { inset-block-start: 17px; inset-inline-start: 21px; inline-size: 4px; block-size: 4px; }
.dn-star--3 { inset-block-start: 26px; inset-inline-start: 10px; }
.daynight[aria-checked="true"] .dn-star { opacity: .92; transform: scale(1); }
.daynight[aria-checked="true"] .dn-star--2 { transition-delay: .07s; }
.daynight[aria-checked="true"] .dn-star--3 { transition-delay: .13s; }

.dn-cloud {
  position: absolute;
  inset-block-end: 6px;
  inset-inline-end: 9px;
  inline-size: 18px; block-size: 8px;
  border-radius: var(--dk-r-pill);
  background: rgba(255, 255, 255, .92);
  transition: opacity var(--dk-t-pop) var(--dk-ease),
              transform .38s var(--dk-ease);
}
/* The second puff, so it is a cloud and not a lozenge. */
.dn-cloud::before {
  content: "";
  position: absolute;
  inset-block-end: 4px;
  inset-inline-start: 4px;
  inline-size: 9px; block-size: 9px;
  border-radius: 50%;
  background: inherit;
}
.daynight[aria-checked="true"] .dn-cloud { opacity: 0; transform: translateY(15px); }

.dn-orb {
  position: absolute;
  inset-block-start: 6px;
  inset-inline-start: 6px;
  inline-size: 24px; block-size: 24px;
  border-radius: 50%;
  /* Clips its own ::before, which is what turns the disc into a crescent instead of
     laying a dark blob across the sky beside it. An outer box-shadow is not clipped
     by overflow, so the glow below survives. */
  overflow: hidden;
  background: radial-gradient(circle at 34% 30%, #fff3c4 0%, #ffc94f 55%, #f5a623 100%);
  box-shadow: 0 0 12px rgba(255, 178, 62, .8), 0 1px 4px rgba(15, 23, 42, .3);
  transition: transform .42s var(--dk-ease-spring),
              background var(--dk-t-base) var(--dk-ease),
              box-shadow var(--dk-t-base) var(--dk-ease);
}
.daynight[aria-checked="true"] .dn-orb {
  background: radial-gradient(circle at 34% 30%, #ffffff 0%, #dde5f4 70%, #c3cee3 100%);
  box-shadow: 0 0 12px rgba(198, 214, 255, .55), 0 1px 4px rgba(0, 0, 0, .45);
}
.dn-orb::before {
  content: "";
  position: absolute;
  inset-block-start: -4px;
  inset-inline-end: -4px;
  inline-size: 20px; block-size: 20px;
  border-radius: 50%;
  background: var(--dn-night);
  transform: scale(0);
  transition: transform .38s var(--dk-ease-spring);
}
.daynight[aria-checked="true"] .dn-orb::before { transform: scale(1); }

/* The rays. On the sky rather than on the orb, because the orb has to clip its crescent
   and would clip these with it — so they carry the same travel, written out twice. */
.dn-sky::after {
  content: "";
  position: absolute;
  inset-block-start: 16px; inset-inline-start: 16px;
  inline-size: 4px; block-size: 4px;
  border-radius: 50%;
  box-shadow:
    15px 0 0 -.5px #ffd97a, 11px 11px 0 -.5px #ffd97a, 0 15px 0 -.5px #ffd97a,
    -11px 11px 0 -.5px #ffd97a, -15px 0 0 -.5px #ffd97a, -11px -11px 0 -.5px #ffd97a,
    0 -15px 0 -.5px #ffd97a, 11px -11px 0 -.5px #ffd97a;
  transition: opacity var(--dk-t-pop) var(--dk-ease),
              transform .42s var(--dk-ease);
}
.daynight[aria-checked="true"] .dn-sky::after { opacity: 0; }

/* A transform has no logical form, so the travel is mirrored by hand — the same one of
   DESIGN-SYSTEM.md's four exceptions that the `.switch` thumb and the login card's
   track are written out for. */
[dir="ltr"] .daynight[aria-checked="true"] .dn-orb { transform: translateX(var(--dn-travel)); }
[dir="rtl"] .daynight[aria-checked="true"] .dn-orb { transform: translateX(calc(var(--dn-travel) * -1)); }
[dir="ltr"] .daynight[aria-checked="true"] .dn-sky::after {
  transform: translateX(var(--dn-travel)) rotate(75deg) scale(.35);
}
[dir="rtl"] .daynight[aria-checked="true"] .dn-sky::after {
  transform: translateX(calc(var(--dn-travel) * -1)) rotate(-75deg) scale(.35);
}

/* ------------------------------------------------- the theme, as a whole page */
/* Light to dark across the entire page, not just on the control (M14.13, owner
   direction).

   The switch already flips `data-bs-theme` with no network — every colour in the product
   is a custom property and base.html emits both palettes, so the repaint is one
   attribute. What it could not do is *show* the repaint: an attribute change is a single
   frame, and there is nothing in CSS to transition between two whole palettes. A blanket
   `transition: background-color, color` on everything is the usual answer and it is the
   wrong one here — it puts a transition on every element in a table of 200 rows for the
   one moment in a session that anybody changes theme, and it still cannot animate a
   gradient, a shadow or a mask.

   So the page is animated instead of its properties: `viewer.js` wraps the attribute
   flip in a View Transition, which snapshots the page before and after, and the new
   palette is revealed by a circle growing out of the switch that was just pressed. The
   dark page is literally drawn over the light one from the point of contact. Nothing is
   transitioned, so the cost is two snapshots and one clip-path — no matter how much is
   on screen.

   `--sweep-x/y` are the switch's centre and `--sweep-r` the distance to the furthest
   corner, both set by `viewer.js` on the root before the transition starts. Scoped to a
   class so this is the only view transition it can ever describe, and to
   `no-preference` so a reduced-motion viewer gets the plain flip; `viewer.js` reads the
   same query and does not open a transition at all in that case. A browser without
   `::view-transition-*` drops these rules and flips instantly, which is exactly what it
   did before. */
@media (prefers-reduced-motion: no-preference) {
  /* The default is a cross-fade of the two snapshots. Both are wanted fully opaque —
     the reveal is the clip, and a fade underneath it turns the leading edge to mush. */
  :root.theme-sweep::view-transition-old(root),
  :root.theme-sweep::view-transition-new(root) {
    animation: none;
    mix-blend-mode: normal;
  }
  :root.theme-sweep::view-transition-old(root) { z-index: 1; }
  :root.theme-sweep::view-transition-new(root) {
    z-index: 2;
    /* Slower than anything else in the sheet, deliberately: this one is meant to be
       watched, and it crosses the whole viewport rather than a control. */
    animation: theme-sweep .6s var(--dk-ease) forwards;
  }
  :root.no-fx.theme-sweep::view-transition-new(root) { animation: none; }
}
@keyframes theme-sweep {
  from { clip-path: circle(0 at var(--sweep-x) var(--sweep-y)); }
  to   { clip-path: circle(var(--sweep-r) at var(--sweep-x) var(--sweep-y)); }
}

/* A viewer who asked for no motion gets the two states and nothing between them. The
   control still says which one it is in, which is the part that carries the meaning. */
@media (prefers-reduced-motion: reduce) {
  .dn-sky, .dn-sky::after, .dn-orb, .dn-orb::before, .dn-star, .dn-cloud,
  .lang-pick-caret { transition: none; }
  .lang-pick-face.is-busy .lang-pick-caret { animation: none; }
  /* The menu's two halves go together or not at all — an exit that is suppressed while
     the entrance still plays is not less motion, it is lopsided motion. `viewer.js`
     reads the same query and drops `open` without waiting, so the panel does not sit
     there for 180ms with nothing happening to it. */
  .lang-pick[open] > .pop-body,
  .lang-pick.is-closing > .pop-body,
  .user-menu[open] > .pop-body,
  .user-menu.is-closing > .pop-body,
  .pop.is-closing > .pop-body { animation: none; }
  /* NOT `animation: none` — that would take the delayed hide with it and leave the
     toast on screen for good. The a11y block's duration clamp already removes the
     travel; nothing more is wanted here. */
  /* `auth.js` already swaps the step in place when this query matches (`still`), so the
     fade has nothing to run on; declared anyway, because the classes are cheap to set
     and a future caller should not be able to reintroduce motion here by accident. */
  .auth-slide { transition: none; }
}
.no-fx .dn-sky, .no-fx .dn-sky::after, .no-fx .dn-orb, .no-fx .dn-orb::before,
.no-fx .dn-star, .no-fx .dn-cloud, .no-fx .lang-pick-caret { transition: none; }
.no-fx .lang-pick-face.is-busy .lang-pick-caret { animation: none; }

/* --------------------------------------------------------------- auth pages */
/* The login screen (M6.12). It was a `col-md-4` card on a `mt-5` — a desktop
   measure on the page most likely to be opened on a phone, and the only screen a
   customer sees before deciding whether to trust the shop with money. Centred on
   both axes, brand mark at a size that reads, and full-height on a phone so the
   keyboard opening does not leave the card stranded mid-scroll. */

.auth {
  min-height: 100vh;
  min-height: 100dvh;   /* dvh so a phone's collapsing URL bar cannot clip it */
  display: grid;
  place-items: center;
  padding: 1.25rem;
}
/* The refusal page (M14.28) borrows the login card, and only needs the two blocks the
   login form does not have. Centred like the brand block above it: the card's own
   alignment is centre, and a left-aligned paragraph under a centred title reads as two
   cards stacked. */
.error-body {
  margin: .25rem 0 1.5rem;
  text-align: center;
  color: var(--dk-text-2);
  line-height: 1.7;
}
.error-act { display: flex; justify-content: center; }

.auth-card {
  inline-size: 100%;
  max-inline-size: 28rem;      /* the reference's `.login-outer` measure */
  padding: 2rem 2.5rem 2.25rem;
  border-radius: var(--dk-r-lg);
  box-shadow: var(--dk-sh-lg), var(--dk-bevel);
}
@media (max-width: 30rem) { .auth-card { padding: 1.75rem 1.375rem 2rem; } }
.auth-brand { text-align: center; margin-bottom: 1.5rem; }
/* The brand mark at the reference's large size and its radius — `.app-brand--lg` is a
   52px 10px-rounded gradient tile, not a 68px 14px one. */
.auth-mark {
  inline-size: 60px; block-size: 60px;
  margin: 0 auto .875rem;
  display: grid; place-items: center;
  border-radius: var(--dk-r-sm);
  background: linear-gradient(135deg, var(--dk-primary), var(--dk-primary-dark));
  color: var(--dk-on-primary);
  font-size: 1.625rem;
  font-weight: 800;
  line-height: 1;
  box-shadow: var(--dk-sh-brand-lg), var(--dk-bevel-dim);
}
.auth-mark img { max-block-size: 42px; max-inline-size: 46px; object-fit: contain; }
.auth-name {
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: -.02em;
  margin: 0;
  color: var(--dk-primary-darker);
}
:root[data-bs-theme="dark"] .auth-name { color: var(--dk-text-1); }
.auth-kind { font-size: .9375rem; color: var(--dk-text-2); margin: .25rem 0 0; }
.auth-form { display: flex; flex-direction: column; gap: 1.25rem; }
.auth-form .btn { margin-top: .25rem; }

/* --------------------------------------------------------------- customer app */
/* The portal is an installed PWA, so it is styled as an app screen rather than a
   web page: a sticky bar that survives scrolling, and a content column that is
   one column on a phone regardless of what Bootstrap's grid would have done. */
/* Islands here too (M12). The portal, the counter POS and the control plane all had a
   solid frosted strip; the CRM's bar is now the reference's floating pills, and three
   surfaces of one product cannot have two app bars. Same transparent-until-scrolled
   treatment, two zones instead of three — the identity island *is* the title here, so
   there is nothing to centre. */
.appbar {
  position: sticky;
  top: 0; z-index: 1020;
  display: flex; align-items: center; gap: .5rem;
  padding: .75rem 1rem;
  background: transparent;
  transition: background var(--dk-t-base) ease, box-shadow var(--dk-t-base) ease;
}
.appbar.is-scrolled {
  background: var(--dk-glass);
  box-shadow: 0 1px 0 var(--dk-rim), var(--dk-sh-sm);
  -webkit-backdrop-filter: var(--dk-blur-bar);
          backdrop-filter: var(--dk-blur-bar);
}
/* The identity island: mark + greeting + name, in one pill. */
.appbar-id { padding-inline: .375rem .875rem; gap: .5rem; min-width: 0; }
.appbar-mark {
  inline-size: 36px; block-size: 36px;
  flex: 0 0 auto;
  display: grid; place-items: center;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--dk-primary), var(--dk-primary-dark));
  color: var(--dk-on-primary);
  font-weight: 800; font-size: 1rem;
  box-shadow: var(--dk-bevel-dim);
}
.appbar-mark img { max-block-size: 26px; max-inline-size: 30px; object-fit: contain; }
.appbar-who { min-width: 0; line-height: 1.15; display: flex; flex-direction: column; gap: 1px; }
.appbar-hi { display: block; font-size: .6875rem; font-weight: 600; color: var(--dk-text-3); }
.appbar-name {
  display: block; font-size: .875rem; font-weight: 800;
  color: var(--dk-primary-darker);
  max-inline-size: 12rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
:root[data-bs-theme="dark"] .appbar-name { color: var(--dk-text-1); }
.appbar-end { margin-inline-start: auto; display: flex; gap: .5rem; align-items: center; }

.app-main {
  max-inline-size: 46rem;
  margin: 0 auto;
  padding: 1.25rem 1rem 3rem;
}
/* Balance strip. Three cells to a row at every width — a balance is a short
   string, and stacking them cost most of a phone's first screen. Divided by
   hairlines rather than gaps so it reads as one figure group, not three cards. */
.bal {
  /* Flex, not `repeat(3, 1fr)`: the customer portal has three cells and the POS and a
     customer's profile have four, and a hardcoded three dropped the fourth onto its own
     line with a gap where it had been. A strip has as many cells as it is given. */
  display: flex;
  border-radius: var(--dk-r-md);
  overflow: hidden;
  margin-bottom: 1.25rem;
}
.bal-cell {
  flex: 1 1 0;
  padding: .75rem .625rem;
  text-align: center;
  border-inline-start: 1px solid var(--dk-line);
  position: relative; z-index: 1;
  min-width: 0;
}
.bal-cell:first-child { border-inline-start: 0; }
.bal-label {
  display: block;
  font-size: .6875rem;
  font-weight: 600;
  color: var(--dk-text-2);
  margin-bottom: .1875rem;
}
.bal-value {
  display: block;
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1.15;
  color: var(--dk-text-1);
  overflow-wrap: anywhere;
}
.bal-value .money-cur { font-size: .58em; }
.bal-cell--bad .bal-value { color: var(--tone-bad); }
.bal-cell--ok .bal-value { color: var(--tone-ok); }
.bal-cell--brand .bal-value { color: var(--tone-brand); }
.bal-cell--info .bal-value { color: var(--tone-info); }

/* A form on a phone is one control per line. `field-row` wraps by default, which
   left an amount field and its submit sharing a line in whatever order the wrap
   happened to produce. */
@media (max-width: 34rem) {
  .field-row { flex-direction: column; align-items: stretch; }
  .field-row > .btn { width: 100%; }
}

/* Chips (M6.12). One-tap shortcuts — the customer portal's favourite numbers,
   sitting directly above the field they fill. A chip is a button, not a badge: it
   is pressed, so it gets a press target and a hover state. */
.chips { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: .875rem; }
.chips--list { margin: .875rem 0 0; }
/* The reference's `.radio-pill`: 36px tall, 700, a bevelled hairline pill that lifts a
   pixel on hover. A favourite number is a *choice*, and it should read as one of a row
   of selectable pills rather than as a tag. */
.chip {
  display: inline-flex; align-items: center; gap: .375rem;
  min-block-size: var(--dk-ctl-h);
  padding: .5rem 1rem;
  border-radius: var(--dk-r-pill);
  border: 1px solid var(--tone-brand-bd);
  background: var(--tone-brand-bg);
  color: var(--tone-brand);
  font-size: .8125rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: var(--dk-bevel-dim);
  transition: background var(--dk-t-pop) var(--dk-ease), transform var(--dk-t-pop) var(--dk-ease);
}
.chip:hover { background: rgba(var(--dk-primary-rgb), .20); transform: translateY(-1px); }
.chip:active { transform: scale(.97); }
.chip i { font-size: .6875rem; opacity: .8; }
/* The quieter chip (M14.2): a number you happen to have sent to, next to numbers you
   took the trouble to name. Same shape, no brand tint — the favourites have to stay the
   ones that read as chosen, or the row becomes ten equal buttons. */
.chip--quiet {
  border-color: var(--dk-line-glass);
  background: var(--dk-ctl-bg-hover);
  color: var(--dk-text-2);
  font-weight: 600;
}
.chip--quiet:hover { background: var(--dk-surface-hover); color: var(--dk-text-1); }

/* A chip that is a form rather than a button — it holds its own delete submit. */
.chip--static { cursor: default; }
.chip--static:hover { background: var(--tone-brand-bg); transform: none; }
.chip--static:active { transform: none; }
.chip-x {
  border: 0; background: none; padding: 0 0 0 .125rem;
  color: var(--tone-bad); font-size: .6875rem; cursor: pointer;
  line-height: 1;
}
.chip-x:hover { color: var(--tone-bad); opacity: .7; }

/* A block that exists only on a development deployment — today, the bot's polling switch,
   which BASE_DOMAIN gates in the service. It is deliberately not styled like the controls
   around it: a control that is absent from production must not look like one an operator
   will find there, or the panel teaches a habit that does not transfer. Hatched rim, muted
   ground, and a top rule that separates it from the facts above it. */
.dev-only {
  margin-block-start: .875rem;
  padding: .75rem .875rem .5rem;
  /* The warn tone, not a rim token: `--dk-rim` is the white *highlight* (rgba(255,255,255,.85)),
     which drew this border invisibly against a near-white panel — the same mistake M14.46
     found on every divider in a dialog. Amber is also the honest colour: this block is not a
     fault, it is a control that does not exist in production. Both themes define it. */
  border: 1px dashed var(--tone-warn-bd);
  border-radius: 12px;
  background: var(--tone-warn-bg);
}
.dev-only .pane-hint { margin-block: .5rem; }

/* Two one-line notes that appear inside a pane, above its form or table. */
.pane-hint { font-size: .8125rem; color: var(--dk-text-2); margin: 0 0 .75rem; }
.pane-total {
  font-size: .875rem; font-weight: 600;
  margin: 0 0 .875rem;
  display: flex; align-items: baseline; gap: .5rem;
}

/* The input lines a bulk transfer refused, under the alert that says some were. The
   class was on the markup from the start and defined nowhere in this file, so these
   rendered with the browser's disc bullets and 40px of indent -- the only bulleted list
   in the product (M14.73). A reset list, which is what `.replies` and `.tg-list` are.

   Physical margin and padding, not logical: the `<ul>` carries `dir="ltr"` so the phone
   numbers inside it are not mangled by the RTL run around them, and a logical property
   on a box with its own direction resolves against *that box* -- M14.34's trap. The
   lines keep the body colour rather than taking `--dk-text-2` like `.replies`, because
   this is the customer's own text to find a typo in and not reference material. */
.bulk-rejects {
  margin: .5rem 0 .875rem;
  padding: 0;
  list-style: none;
  display: grid;
  gap: .25rem;
  font-size: .75rem;
}

/* ------------------------------------------------------- the bulk row composer */
/* M14.85. The textarea this replaced needed no CSS, which was the tell: it was a chat
   message in a form's clothing. A row is a grid rather than a flex line so the phone
   column, the amount column and the delete button line up down the list -- the whole
   point of rows is that the eye can run the column, which is exactly what the paragraph
   of `<phone> <amount>` could not offer. */
.bulkc-rows { display: grid; gap: .5rem; margin-block: .75rem; }

/* Same three tracks as a row, so the two headings sit over the columns they name. The
   third is the delete button's, which needs no heading and gets an empty track. */
.bulkc-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, .62fr) auto;
  gap: .5rem;
  margin-block-start: .75rem;
  font-size: .75rem;
  color: var(--dk-label);
}
.bulkc-head::after { content: ''; inline-size: 2rem; }

/* The send control is an `<a>` (it opens the confirm dialog), and an anchor has no
   `disabled`. Offering it with nothing to send would flash "no lines" back at somebody
   who can see there are none. */
.btn.is-off { opacity: .45; pointer-events: none; }

.bulkc-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, .62fr) auto;
  gap: .5rem;
  align-items: start;
}

/* The network, under the number it was derived from: the server decides it and says so
   as the row is typed, so the badge is an answer rather than a label. */
.bulkc-net {
  display: block;
  margin-block-start: .25rem;
  font-size: .6875rem;
  color: var(--dk-text-2);
  min-block-size: 1rem;
}
.bulkc-row--bad .bulkc-net { color: var(--tone-bad); }
.bulkc-row--bad .form-control { border-color: rgba(239, 68, 68, .55); }

.bulkc-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: .75rem;
  margin-block-start: .25rem;
}
/* The mode sits above the list it governs, at the start edge, clear of the column
   headings below it. */
.bulkc-mode { margin-block-start: .25rem; }

/* A row echoing the first one's amount. Recessed rather than greyed out: it is not
   disabled — it is showing what the first row says, and a field that looks broken invites
   a click to find out why. */
.form-control.is-mirrored {
  background: var(--dk-line);
  color: var(--dk-text-2);
  cursor: default;
}

.bulkc-paste { margin-block-start: .875rem; }
.bulkc-paste > summary {
  cursor: pointer;
  font-size: .8125rem;
  color: var(--dk-text-2);
  inline-size: fit-content;
}

/* The running answer. `space-between` with the count at the start edge and the figure at
   the end, so the total sits under the amount column it is the sum of. */
.bulkc-sum {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: .5rem;
  margin-block-start: .875rem;
  padding-block-start: .75rem;
  border-block-start: 1px solid var(--dk-line);
  font-size: .875rem;
}
.bulkc-sum b { font-size: 1.0625rem; }
.bulkc-warn { flex: 1 0 100%; color: var(--tone-warn); font-size: .8125rem; }

.bulkc-send { margin-block-start: .875rem; }

/* One column on a phone: 320px cannot hold two fields and a button, and the amount box
   shrinking to four characters is how a 25,000 gets typed as 2,500. */
@media (max-width: 30rem) {
  .bulkc-head { display: none; }
  .bulkc-row { grid-template-columns: minmax(0, 1fr) auto; }
  .bulkc-row .bulkc-amount { grid-column: 1; }
}

/* --------------------------------------------------------------- equation */
/* Liquidity and reconciliation are arithmetic, and were written out as prose:
   "drawer 2,380,000 + debts 512,000 − suppliers 90,000 = 2,802,000". Rendering the
   operators as operators lets the eye do what the sentence was asking it to do, and
   the total takes the brand colour so the answer is findable. Wraps on a phone, where
   each term becomes its own line rather than the row scrolling. */
.equation {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: .625rem 1rem;
}
.eq-term {
  display: flex;
  flex-direction: column;
  gap: .125rem;
  font-family: var(--dk-font-body-en);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: 1rem;
}
.eq-label {
  font-family: var(--bs-body-font-family);
  font-size: .6875rem;
  font-weight: 600;
  color: var(--dk-text-2);
}
.eq-op {
  font-family: var(--dk-font-body-en);
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--dk-text-3);
  padding-bottom: .0625rem;
}
.eq-term--total { font-size: 1.375rem; color: var(--dk-primary-darker); }
:root[data-bs-theme="dark"] .eq-term--total { color: var(--dk-text-1); }

/* A note that sits inside a padded panel body rather than above a flush table. */
.panel-note--inline { padding: 0 0 .75rem; border-bottom: 0; }

/* --------------------------------------------------------------- switch */
/* A two-state setting is a switch (M6.15). The settings page had fifteen `<select>`
   dropdowns of on/off, which made "is anything switched off right now" unanswerable without
   opening each one — the whole point of a kill switch is that its state is visible. The
   track is the control; the label is part of the hit area, so the whole row is clickable. */
.switch {
  display: flex;
  align-items: center;
  gap: .625rem;
  cursor: pointer;
  padding: .375rem 0;
  /* Hugs the control and its label. As a block it spanned the panel, so the empty half of
     the row was a live target and a click far from the switch flipped it (M14.102). */
  inline-size: max-content;
  max-inline-size: 100%;
}
.switch > input {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}
/* Retuned to the reference's Toggle (M12): a 48x26 track with a 20px thumb, a recessed
   inset on the off state so the track reads as a groove rather than as a flat pill, and
   a thumb that travels on the **overshoot** easing. That last one is the whole
   character of the control — a switch is the one thing on the page whose entire job is
   to move, and a linear glide reads as a checkbox that happens to be pill-shaped. */
.switch-track {
  flex: 0 0 auto;
  position: relative;
  inline-size: 3rem;
  block-size: 1.625rem;
  border-radius: var(--dk-r-pill);
  background: var(--dk-ctl-bg-hover);
  border: 1px solid var(--dk-ctl-bd);
  box-shadow: inset 0 2px 4px rgba(15, 23, 42, .08);
  transition: background var(--dk-t-base) var(--dk-ease),
              border-color var(--dk-t-base),
              box-shadow var(--dk-t-base);
}
.switch-track::after {
  content: "";
  position: absolute;
  inset-block-start: 2px;
  inset-inline-start: 2px;
  inline-size: 1.25rem;
  block-size: 1.25rem;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 6px rgba(15, 23, 42, .28);
  transition: transform .32s var(--dk-ease-spring);
}
.switch > input:checked + .switch-track {
  background: linear-gradient(135deg, var(--dk-primary), var(--dk-primary-dark));
  border-color: transparent;
  box-shadow: var(--dk-glow-ring), inset 0 1px 0 rgba(255, 255, 255, .20);
}
/* `translateX` and not a logical property, because a transform has no logical form — so it
   is mirrored by hand for RTL, which is one of the four things DESIGN-SYSTEM.md warns
   cannot be done logically. (The reference animates `inset-inline-end` instead, which
   needs no flip but also cannot be composited; on the phones these shops run, a
   transform is the right trade.) */
.switch > input:checked + .switch-track::after { transform: translateX(1.375rem); }
[dir="rtl"] .switch > input:checked + .switch-track::after { transform: translateX(-1.375rem); }
.switch > input:focus-visible + .switch-track {
  outline: 3px solid rgba(var(--dk-primary-rgb), .45);
  outline-offset: 2px;
}
.switch-label { font-size: .875rem; font-weight: 700; }
.switch-desc { font-size: .75rem; font-weight: 400; color: var(--dk-text-2); }
.switch-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.switch--inline { padding: 0; }
/* The card variant — the reference's default. A setting that is its own row in a
   bordered, tinted block, rather than a line of text with a control at the end. */
.switch--card {
  padding: .75rem .875rem;
  background: var(--dk-ctl-bg);
  border: 1px solid var(--dk-line);
  border-radius: var(--dk-r-md);
  transition: background var(--dk-t-base) var(--dk-ease);
}
.switch--card:hover { background: var(--dk-surface-hover); }
/* A grid of settings, each one its own card — the reference's Toggle is a bordered,
   tinted row and not a bare label with a control at the end of it. Fifteen kill
   switches in a grid is exactly the case that needs the row: the card is what tells
   you where one setting stops and the next begins. */
.switch-grid {
  display: grid;
  gap: .5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
}
/* The card. Three things were wrong with it at once and the owner's report — *"good
   stares syriatel ont top and mtn in down :)"*, on the denomination composer — is what
   the sum of them looks like. All three measured before the fix:

     both cards   y=330 h=88     a grid item stretches to its row
     Syriatel     track y=343    two lines: the switch, then its note
     MTN          track y=361    one line, centred in the same 88px
     card fill    1.01:1         against the dialog's own ground
     card border  1.02:1
     settings     267px card, 149px used, 118px of dead space after the label

   **The stagger is `align-content`, and `flex-wrap` is what makes it apply.** It is
   ignored on a `nowrap` container, so the card without a note had no way to say "top" and
   sat its single line in the middle of a height the card beside it had set. Wrapping costs
   the fifteen on the settings page nothing — measured, a 267px card holding an 82px label
   and a 48px track has no line to wrap onto — and where a label ever did overflow,
   wrapping it beats the overflow it has now.

   **The ground is ink, not a white overlay.** `--dk-glass-inner` is
   `rgba(255,255,255,.50)` and `--dk-line-glass` is white at .40: they *lighten*, which
   reads on the shell's tinted mesh and does nothing at all on a surface that is already
   near-white. The card was not faint inside a dialog, it was absent, and two toggles with
   no visible container read as two controls at arbitrary distances. This is M14.46's
   finding one token along ("`--dk-rim` is the *highlight* … drawn across a near-white
   dialog it is invisible"). `--dk-ctl-bg` and `--dk-line` are the ink pair the buttons and
   the panel hairlines already use, and both are redefined for the dark theme — where a
   white overlay *is* the right direction — so this needs no second set of values.
   Re-sampled after, and the point is that the two surfaces now agree: fill **1.07:1** and
   border **1.26:1** against the dialog's ground *and* against the settings panel's, where
   the overlay had measured 1.09 and 1.00. In dark, 1.19 and 1.50. A card boundary is a
   divider, not a surface — it does not want AA contrast, it wants to be findable, and
   1.02:1 is not. */
.switch-grid > .switch {
  padding: .75rem .875rem;
  flex-wrap: wrap;
  align-content: flex-start;
  background: var(--dk-ctl-bg);
  border: 1px solid var(--dk-line);
  border-radius: var(--dk-r-md);
  transition: background var(--dk-t-base) var(--dk-ease);
}
.switch-grid > .switch:hover { background: var(--dk-surface-hover); }
/* Name at the start of the card, control at its end — which is what the auto margin was
   written for and what it never did. The DOM order is track *then* label, so the free
   space landed after the label and the pair huddled against the start edge.

   `order` and not a template change, because the DOM order is the one that is right: the
   input has to precede the track for `+ .switch-track` and the note has to follow it for
   `~ .switch-note`, and those two selectors are the whole of what makes this control work
   with no script. Visual order is a presentation question and belongs here.

   Scoped to a card, like the padding above it. A bare `.switch` — the maintenance kill
   switch, the USSD form's `--inline` — is a checkbox in a sentence, and its label belongs
   next to the thing it labels. */
.switch-grid > .switch .switch-label { order: -1; margin-inline-end: auto; }

/* A note belonging to one switch (M14.72). Syriatel's USSD value is a hundred times its
   denomination and MTN's is not, so the divisor is a property of *that* toggle rather than
   of the form around it — it wraps to its own line inside the row and is shown only while
   the switch is on.

   `~` from the input, which is its sibling inside the label, and not `:has()` on an
   ancestor: `.seg`'s comment records that this is an installed PWA on phones older than
   `:has()`, and a rule that silently stops applying there is worse than one that never
   applied. `--noted` rather than putting `flex-wrap` on every `.switch`, because the
   fifteen on the settings page have nothing to wrap. */
.switch--noted { flex-wrap: wrap; }
.switch-note {
  flex-basis: 100%;
  display: none;
  margin-block-start: .375rem;
  font-size: .75rem;
  font-weight: 600;
  color: var(--dk-text-3);
}
.switch > input:checked ~ .switch-note { display: block; }

/* --------------------------------------------------------------- act menu */
/* A menu of acts is a list of named rows (M14.103), not a strip of bare icon circles: the
   name says what it is, the state line says where the account stands, and the button
   carries the verb. Built on `.switch--card`'s box, because both are one setting in a row. */
.act-menu { display: flex; flex-direction: column; gap: .5rem; }
.act-item {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .75rem;
  padding: .625rem .75rem;
  background: var(--dk-ctl-bg);
  border: 1px solid var(--dk-line);
  border-radius: var(--dk-r-md);
}
.act-item--danger { background: var(--tone-bad-bg); border-color: var(--tone-bad-bd); }
/* The state at a glance: toned while the flag is set, grey while it is not. */
.act-ico {
  flex: none;
  display: grid;
  place-items: center;
  inline-size: 2.125rem;
  block-size: 2.125rem;
  border-radius: var(--dk-r-sm);
  background: var(--dk-ctl-bg-hover);
  color: var(--dk-text-2);
}
.act-ico--brand { background: var(--tone-brand-bg); color: var(--tone-brand); }
.act-ico--warn { background: var(--tone-warn-bg); color: var(--tone-warn); }
.act-ico--bad { background: var(--tone-bad-bg); color: var(--tone-bad); }
.act-ico--info { background: var(--tone-info-bg); color: var(--tone-info); }
.act-body {
  display: flex;
  flex-direction: column;
  gap: 1px;
  flex: 1 1 9rem;
  min-inline-size: 0;
}
.act-name { font-size: .875rem; font-weight: 700; }
.act-state { font-size: .75rem; color: var(--dk-text-2); }
/* The control closes the row whatever the name beside it ran to. */
.act-item > .btn { flex: none; margin-inline-start: auto; }

/* A pill that is also a submit: reading the state and changing it are one object, which
   is what a toggle is. Replaces a button whose whole content was a coloured circle. */
.pill--button {
  cursor: pointer;
  font-family: inherit;
  transition: filter var(--dk-t-base);
}
.pill--button:hover { filter: brightness(.96) saturate(1.15); }

/* A monospace field, for the dial templates. The one place where every character matters
   and an `l` must not be readable as a `1`. */
.form-control.code {
  font-family: var(--dk-font-mono);
  font-size: .875rem;
  letter-spacing: .01em;
}

/* Numbered step marker, replacing 1️⃣/2️⃣ — an emoji digit renders at whatever size the
   system font decides and does not inherit the button's colour.

   The disc and the digit are a declared pair, and neither is `currentColor` or a surface
   token. Both of those painted the marker out, in different ways and in different
   themes, and neither shows up anywhere but on screen:

     * `background: currentColor` beside `color: …` — currentColor on an element resolves
       to *that element's own* computed colour, which the next line was setting. Both
       came out identical: a white digit on a white disc, in light mode.
     * `color: var(--dk-surface)` — right-sounding name, wrong ladder. In dark that token
       is `rgba(255,255,255,.05)`, a translucent overlay meant to sit *on* a ground, so
       the digit rendered at 5% alpha on a near-white disc.

   A numbered step that shows no number, on the control that changes the shop admin's
   password. The emoji digits it replaced at least had a glyph. */
.step {
  display: inline-grid; place-items: center;
  inline-size: 1.125rem; block-size: 1.125rem;
  margin-inline-end: .25rem;
  border-radius: 50%;
  background: var(--step-disc, var(--dk-text-1));
  color: var(--step-ink, #FFFFFF);
  font-family: var(--dk-font-body-en);
  font-size: .625rem; font-weight: 700;
  line-height: 1;
}
/* The default ink is a literal in each theme, and has to be: it is knocked out of a disc
   painted in `--dk-text-1`, so it must be that token's opposite, and the ladder has no
   "inverse of the text colour". `:where()` keeps the theme override at zero specificity,
   so it cannot out-specify the `.btn-primary` pair below and silently discard it. */
:where(:root[data-bs-theme="dark"]) .step { --step-ink: #0B1220; }
/* On a filled button the pair is the button's own: the label colour for the disc and the
   fill for the digit. `presentation._label_for` already proves those two contrast — it
   picks the label by measuring it — so this holds for a pale gold shop as well as a dark
   mulberry one, which a fixed light-on-dark pair would not. */
.btn-primary .step { --step-disc: var(--dk-on-primary); --step-ink: var(--dk-primary); }

/* The save bar for a long form: the button that commits fifteen settings should not be
   the fifteenth item in a grid. At the end, like a dialog's foot, because it closes a page.

   `.form-commit` is its sibling and deliberately not the same rule (M14.81): a commit
   inside a *panel* sits at the start, where that panel's own controls are, and takes its
   space above rather than below. The password panel has both — a numbered step 1 that is a
   bare button and a step 2 that is this — and a sequence whose two buttons sit at opposite
   ends of the card does not read as a sequence. See `.field-row > .btn` for why the button
   left the field row in the first place. */
.form-foot {
  display: flex;
  justify-content: flex-end;
  margin: 0 0 1.25rem;
}

/* A single-column page (the broadcast composer): a measure, not the full width. */
/* Centred (M14.69). It was `col-md-8` and centred before M6.15 and has not been since:
   a plain block with a max width sits at the inline-start, so measured at 1440 the
   broadcast card ran 456→1160 in a 28→1160 content region — 428px of dead space at one
   end and none at the other. Two pages use this, and a lone form card is centred on both. */
.narrow { max-inline-size: 44rem; margin-inline: auto; }

/* A table cell holding prose rather than a value — an SMS body, a captured reply. */
.dtable .wrap {
  white-space: normal;
  line-height: 1.5;
  font-size: .75rem;
  max-width: 52ch;
}

/* The carrier's recent replies, as a reference list beside the keyword fields. */
/* The carrier-reply list was here (M6.15–M14.81): a `<ul>` of bare sentences on the
   USSD page. It is a table now — status, the reply, the target, the time — because a
   keyword list is written by reading a reply against the verdict it produced, and a
   sentence on its own carries no verdict. Both rules went with it. */

/* Three across on a wide screen. */
/* Through the custom property, not a second grid-template-columns: overriding the
   template here would have dropped the min() guard above with it. */
.card-grid--third { --min-col: 17rem; }

/* --------------------------------------------------------------- modal + wizard */
/* A task that is one step of a page's work, kept on the page (M14.40, owner: "new store
   should be in this page not in side bar and it should open popup not new page ... see
   wizard is easy to manage and better UX").

   Native `<dialog>`, not a div with a z-index. Three things come free and all three are
   things a hand-rolled overlay in this codebase would have had to add: Escape closes it,
   focus is trapped inside it, and the top layer puts it above every stacking context on
   the page without a single z-index. The last one matters here more than usual --
   `.shell-top` and the cards are their own stacking contexts, and the reply popover
   already taught this file what nesting inside one costs.

   And because the top layer is outside every ancestor's filter, a `backdrop-filter` on
   `::backdrop` filters the *page*, which is the one place in this stylesheet where that
   actually works as written. The panel itself is still painted opaque over `--dk-wash-1`
   rather than trusting the blur: the same correction M14.27 made for the account menu and
   M14.33 for the note, and a modal wants the ground anyway. */
/* **`.dlg`, not `.modal`** -- and this cost a render to find. `base.html` loads Bootstrap
   5.3.3 from a CDN, and Bootstrap owns `.modal`, `.modal-title`, `.modal-body` and
   `.modal-content`. Its `.modal { display: none }` is a class selector, so it outranks the
   UA stylesheet's `dialog[open] { display: block }` -- `showModal()` opened the dialog, the
   top layer had it, and it painted nothing. Measured: `display=none`, `z-index=1055`, a
   value this file never wrote. The one Bootstrap class this stylesheet still keeps on
   purpose is `.modal-content`, further down, and that is a leftover with its own note. */
.dlg[open] { display: flex; flex-direction: column; }
.dlg {
  inline-size: min(38rem, calc(100vw - 2rem));
  max-block-size: min(44rem, calc(100vh - 3rem));
  padding: 0;
  border: 1px solid var(--dk-rim);
  border-top-color: var(--dk-rim-top);
  border-radius: var(--dk-r-xl);
  background: linear-gradient(145deg, var(--dk-glass-strong), var(--dk-glass-hover)),
              var(--dk-wash-1);
  box-shadow: var(--dk-sh-lg);
  color: var(--dk-text-1);
  overflow: visible;
}
/* The entrance and the exit (M14.43, owner on a render of the wizard: "THERE IS NOT
   ANIMATION when open and close this diablog").

   The same report the account menu got in M14.29, and the same shape: not a motion that
   looked wrong, a motion that had no rule at all in either direction. A modal that is
   simply *there* on one frame and gone on the next reads as a page that jumped rather
   than as a surface that opened, and the dialog was the last floating thing in the
   product still doing it -- the popovers, the account menu, the language menu and the
   toasts all animate both ways.

   A `<details>` needed a class and a timer for its half (see `furl`), because a
   disclosure cannot transition across a display change. A `<dialog>` can, and this is the
   one place in this file where the platform hands it over: `display` and `overlay`
   listed with `allow-discrete` keep the element painted and in the top layer for the
   length of the exit, and `@starting-style` supplies the frame before the open. That
   covers *every* way this closes -- the X, a backdrop click, Escape, a
   `method="dialog"` submit -- which an `is-closing` class in JavaScript would have had to
   intercept one at a time, and would have missed the one it did not know about.

   Nothing here is load-bearing. A browser without `@starting-style` skips the entrance,
   one without `allow-discrete` closes on the frame it always did, and both get exactly
   the modal that shipped before this. */
.dlg {
  opacity: 0;
  transform: translateY(10px) scale(.98);
  transition: opacity var(--dk-t-exit) var(--dk-ease),
              transform var(--dk-t-exit) var(--dk-ease),
              overlay var(--dk-t-exit) var(--dk-ease) allow-discrete,
              display var(--dk-t-exit) var(--dk-ease) allow-discrete;
}
/* Longer in than out, the ratio every other surface in this file already uses: an
   entrance is worth watching and a dismissal is worth getting out of the way. The
   duration is read from the state being transitioned *to*, so dropping `open` picks
   `--dk-t-exit` back up without a second rule saying so. */
.dlg[open] {
  opacity: 1;
  transform: none;
  transition-duration: var(--dk-t-enter);
}
@starting-style {
  .dlg[open] { opacity: 0; transform: translateY(10px) scale(.98); }
}
/* The backdrop fades with it, through `opacity` rather than `background` -- that is one
   property doing both halves, the tint and the blur, since a filtered backdrop is painted
   as part of the pseudo-element and takes its opacity with everything else. Fading the
   `background` alone would have snapped the page behind out of focus on the first frame
   while the darkening took the whole 280ms, which is the glitch it is meant to avoid. */
.dlg::backdrop {
  background: rgba(15, 23, 42, .38);
  -webkit-backdrop-filter: blur(3px);
          backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity var(--dk-t-exit) var(--dk-ease),
              overlay var(--dk-t-exit) var(--dk-ease) allow-discrete,
              display var(--dk-t-exit) var(--dk-ease) allow-discrete;
}
.dlg[open]::backdrop { opacity: 1; transition-duration: var(--dk-t-enter); }
@starting-style {
  .dlg[open]::backdrop { opacity: 0; }
}
/* The panel scrolls, not the page behind it. `overscroll-behavior` so that reaching the
   end of a long step does not start scrolling the dashboard underneath. */
.dlg-body {
  padding: var(--dk-dlg-pad);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Measured against the head rather than guessed at (M14.95): this was
     `calc(min(44rem, 100vh - 3rem) - 4rem)`, and a two-line head is taller than 4rem, so a
     long step overflowed the dialog and its own footer buttons were cut in half by the
     bottom edge. The column below makes the head's real height the thing that is
     subtracted. */
  flex: 1 1 auto;
  min-block-size: 0;
}
/* One open select menu, out of a dialog that is not scrolling anyway (M14.102). */
.dlg-body--spill {
  overflow: visible;
}
/* `--dk-line` and not `--dk-rim` (M14.46). `--dk-rim` is the *highlight* — white at .85 —
   which is a top edge catching the light, not a divider; drawn across a near-white dialog
   it is invisible, and the head, the separator between the mark form and the colour form,
   and the rule between two handover rows were all drawn in it. Three lines nobody could
   see, on the surface the owner was reading as one undifferentiated block. `--dk-line` is
   the hairline the panels already use, and it is defined for both themes. */
.dlg-head {
  display: flex;
  align-items: center;
  gap: .625rem;
  padding: 1.125rem var(--dk-dlg-pad);
  border-bottom: 1px solid var(--dk-line);
  flex: none;
}

/* **The rhythm between blocks, owned in one place** (M14.46). Every dialog in the product
   spaced its own blocks: four rows carried a hand-written `mt-3`, the wizard's colour block
   carried one and its `.field-row`s carried none, and `.dlg-sep` brought a third number of
   its own. So two modals showing the same kind of content did not agree, and the owner read
   the result as compression — correctly, because 8px between a hint and the field it
   introduces is not spacing, it is a line break.

   The adjacent-sibling form rather than a class, so a call site cannot forget it. Hidden
   inputs are skipped on both sides: a `<form>`'s first three children are the CSRF token
   and its action fields, and `display: none` generates no box but is still a sibling — the
   first *visible* row would otherwise open with a 20px margin under a border that already
   separates it from the head. */
.dlg-body > :not([type="hidden"]) + :not([type="hidden"]),
.dlg-body form > :not([type="hidden"]) + :not([type="hidden"]),
.wiz-pane > :not([type="hidden"]) + :not([type="hidden"]) {
  margin-block-start: var(--dk-dlg-gap);
}
/* Except after a hint, which belongs to what follows it rather than to what came before.
   A full block gap there reads as two blocks, which is the opposite of what a hint is. */
.dlg .pane-hint + *,
.wiz .pane-hint + * { margin-block-start: .625rem; }
.dlg .pane-hint,
.wiz .pane-hint { margin-block-end: 0; }
/* Both grids carry the same two numbers as a field row, for the same reason: a switch and
   a field are both one control with a label, and a modal holding one of each should not
   space them differently. */
.dlg .switch-grid,
.dlg .field-grid { gap: var(--dk-dlg-gap) var(--dk-dlg-gap-x); }
.dlg-title { margin: 0; font-size: 1rem; font-weight: 800; color: var(--dk-primary-darker); }
:root[data-bs-theme="dark"] .dlg-title { color: var(--dk-text-1); }
.dlg-sub { margin: 0; font-size: .75rem; color: var(--dk-text-2); }
/* Pushed to the far end, and it is a `.btn-icon` so it is the same square as every other
   icon control in the product (M14.36). */
.dlg-x { margin-inline-start: auto; }

/* The dialog's own footer (M14.80, owner: "button should be next to each other and one
   primary and the other secondary").

   **The two answers to a question belong together.** They were at opposite ends of the
   panel, a whole dialog's width apart, which is not a choice being offered -- it reads as
   two unrelated controls that happen to share a row, and the eye has to travel the width
   of the modal to compare them. Grouped at the end, in reading order, with the commit
   last: cancel is the thing you pass on the way to it.

   This was never anyone's decision. `.dlg-foot > .btn:first-child` pushed the *first*
   child away, which was written for the one foot that has three (M14.66, below) -- and
   in a two-button foot the first child is cancel, so the rule fired on every dialog in
   the product and split the pair. */
.dlg-foot {
  display: flex; align-items: center; justify-content: flex-end; gap: .5rem;
  margin-block-start: var(--dk-dlg-gap);
}
/* The exception, now asked for by name rather than inferred from position (M14.66): a
   destructive control sharing a foot with cancel and a commit -- the platform APK's
   delete, beside its own upload form -- stands apart from both, because a mis-aim there
   deletes the build every shop links to. Opt-in, so the two-button feet keep their pair
   together. */
.dlg-foot > .dlg-foot-apart { margin-inline-end: auto; }
/* Between two forms that share one dialog. No margin of its own since M14.46 — it is a
   block like any other and the rule above spaces it, on both sides. */
.dlg-sep { margin: 0; border: 0; border-top: 1px solid var(--dk-line); }

/* The confirm (M14.76). One sentence and two buttons, so it takes the width of the
   question rather than the 38rem a form needs — a modal wider than its content reads as
   a dialog that failed to load the rest of itself. */
.dlg--ask { inline-size: min(26rem, calc(100vw - 2rem)); }
.dlg-ask { margin: 0; font-size: .875rem; line-height: 1.55; color: var(--dk-text-1); }
/* The adjust (M14.77). An amount and at most two pickers stacked under it, so the 38rem a
   record form needs leaves it mostly empty — the same reasoning as `.dlg--ask` one line
   up, one size less severe. */
.dlg--adjust { inline-size: min(30rem, calc(100vw - 2rem)); }
/* The carrier's reply (M14.78): one SMS, so narrower again than the adjust. */
.dlg--reply { inline-size: min(28rem, calc(100vw - 2rem)); }

/* No bundle, no top layer: a closed `<dialog>` is `display: none`, so every dialog would
   be unreachable and the page read-only. Flattened into an ordinary panel instead, which
   is the shape it had before M14.44.

   `.no-dlg` is the same condition arrived at from the other side (M14.45): scripting is
   on, so `.no-js` came off in the head, but the browser has no `showModal` to open any of
   this with. It was the narrower thing these rules always meant. It also matters more
   than the section editors now — the handover carries a password, and `display: none`
   would lose it silently.

   **Opened by `:target`, not flattened open** (M14.75). Flattening every dialog was
   affordable while there were twelve of them and each was a section editor on one record
   page. It stops being affordable the moment a confirm is a dialog: those are drawn one
   per table row, so a users page with twenty rows would have rendered twenty expanded
   "are you sure" panels stacked down the page, one under every row.

   `:target` is the whole disclosure and it needs no script: the trigger is an `<a>`
   whose href is the dialog's own id, so pressing it names that dialog in the URL and this
   rule shows it. One at a time — a document has one `:target` — which is exactly what a
   modal is. `.dlg-x` and the foot's cancel are `<a href="#">` for the same reason: they
   clear the fragment, which closes it. Chrome 1 and IE 9 have `:target`; nothing this
   runs on is without it.

   The handover keeps the old behaviour: it is the answer to a redirect and has no trigger
   to press, so `[data-modal-open]` shows it the moment the page renders. */
.no-js .dlg, .no-dlg .dlg { display: none; }
.no-js .dlg:target, .no-dlg .dlg:target,
.no-js .dlg[data-modal-open], .no-dlg .dlg[data-modal-open] {
  display: block;
  position: static;
  inline-size: auto;
  max-block-size: none;
  opacity: 1;
  transform: none;
  /* And not animated into place. `.dlg` transitions `opacity` from 0, which needs a
     frame to land -- fine for a modal that a script opened, wrong for the one surface
     whose whole premise is that no script ran. A browser this old gets the panel, not
     the entrance. */
  transition: none;
  margin-block-start: .75rem;
}
/* Nothing to close, so nothing to close it with. */
.no-js .hand-foot, .no-dlg .hand-foot { display: none; }

/* The handover (M14.45): one row per value — the value across the full width, its copy
   and Open buttons underneath. Beside it was the first shape and it was wrong: a one-time
   login URL is sixty characters, and two buttons on the same line cut it in half. */
.dlg--hand { max-inline-size: 32rem; }
/* The margin comes from the block rule; this is the half below the rule. */
.hand-row + .hand-row {
  padding-block-start: var(--dk-dlg-gap);
  border-top: 1px solid var(--dk-line);
}
.hand-row .field { inline-size: 100%; }
.hand-row .act-row { margin-block-start: .625rem; }
.hand-foot { display: flex; justify-content: flex-end; margin-block-start: var(--dk-dlg-gap); }

/* A row of buttons inside a panel or a dialog, where `.field-row` is the wrong tool: that
   one pins its buttons to the *field* height so a submit lines up with the input beside
   it, and a row with no input in it just gets two 46px buttons where the rest of the page
   uses 34px ones. */
.act-row { display: flex; flex-wrap: wrap; gap: .5rem; }

/* The copy button's two faces. Both are in the markup and one is hidden, rather than the
   text being rewritten from script: a swap that goes through `textContent` loses the icon
   and cannot be styled back out again. */
.copy-btn .copy-done { display: none; }
.copy-btn.is-copied .copy-idle { display: none; }
.copy-btn.is-copied .copy-done { display: inline-block; }
.copy-btn.is-copied {
  color: var(--tone-ok);
  border-color: var(--tone-ok-bd);
  background: var(--tone-ok-bg);
}


/* A read-only fact inside a section, which is what a panel shows until Edit is pressed. */
.kv { display: flex; align-items: baseline; gap: .625rem; font-size: .875rem; }
.kv + .kv { margin-block-start: .625rem; }
.kv-k { flex: none; min-inline-size: 8.5rem; color: var(--dk-text-2); }
/* The value hugs its text and sits beside the key, whatever direction it reads in
   (M14.92 — an `@handle` alone at the far end of a 1100px row is not a pair). */
.kv-v {
  color: var(--dk-text-1);
  font-weight: 600;
  overflow-wrap: anywhere;
  flex: 0 1 auto;
  min-inline-size: 0;
}

/* Grown instead, so several Latin values down a column begin level rather than sharing
   their ragged end edge — what M14.81 measured on the eight USSD cards. */
.kv-v--level { flex: 1 1 auto; }

/* A fact that carries the control which changes it — the USSD card's PIN pill and the
   button that erases it (M14.82). `.kv` aligns its two spans on their baselines, which is
   right for text and wrong for a pill beside a button: the two boxes have different
   heights and sat a couple of pixels apart. Centred inside the value, so the row's
   baseline still belongs to the key. */
.kv-acts { display: inline-flex; align-items: center; gap: .5rem; }

/* The wizard (M14.40). Ported from the reference app's `WizardShell`, which is where the
   owner pointed: a strip of numbered dots, one pane at a time, and a footer whose cancel
   sits apart from the two navigation buttons.

   **Everything here is opt-in from JavaScript, and the no-JS shape is the whole form.**
   `.wiz` on its own renders every pane stacked, with the plain submit at the bottom --
   which is exactly the page this replaces, and is what `/tenants/new` still serves when
   the bundle has not arrived. `ui.js` adds `.wiz--on`, and only then do the panes start
   hiding and the step strip and the Back/Next pair appear. A wizard that showed step one
   and no way forward would be a worse page than the flat form, not a better one. */
.wiz { display: flex; flex-direction: column; gap: var(--dk-dlg-gap); }
.wiz-steps,
.wiz-nav { display: none; }
.wiz--on .wiz-steps { display: flex; }
.wiz--on .wiz-nav { display: flex; }
.wiz--on .wiz-solo { display: none; }

.wiz-steps {
  align-items: center;
  justify-content: center;
  gap: .5rem;
  flex-wrap: wrap;
  list-style: none;
  margin: 0;
  padding: 0;
}
.wiz-step { display: flex; align-items: center; gap: .45rem; color: var(--dk-text-2); }
/* The connector. A plain `width` with a logical margin, so it flips with the page and
   needs no direction of its own -- the rule M14.33 through M14.39 kept re-learning. */
.wiz-step:not(:last-child)::after {
  content: "";
  inline-size: 2.5rem;
  block-size: 2px;
  border-radius: var(--dk-r-pill);
  background: var(--dk-ctl-bd);
  margin-inline-start: .5rem;
}
.wiz-step-dot {
  flex: none;
  display: grid;
  place-items: center;
  inline-size: 1.625rem;
  block-size: 1.625rem;
  border-radius: 50%;
  border: 2px solid var(--dk-ctl-bd);
  background: transparent;
  color: var(--dk-text-2);
  font-family: var(--dk-font-body-en);
  font-size: .75rem;
  font-weight: 700;
  transition: background var(--dk-t-exit) var(--dk-ease),
              border-color var(--dk-t-exit) var(--dk-ease),
              color var(--dk-t-exit) var(--dk-ease);
}
.wiz-step-label {
  font-size: .8125rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* `--tone-brand`, not `--dk-primary`: the raw brand value is the one base.html does not
   vary by theme, so spending it as ink is readable on light and not on dark (M14.22).
   The dot's *fill* may be the raw value -- a fill is not ink -- and its border becomes
   transparent rather than matching, which paints the same disc without naming the
   forbidden value twice. */
.wiz-step.is-on { color: var(--tone-brand); }
.wiz-step.is-on .wiz-step-dot,
.wiz-step.is-done .wiz-step-dot {
  background: var(--dk-primary);
  border-color: transparent;
  color: var(--dk-on-primary);
}
.wiz-step.is-done { color: var(--dk-primary-darker); }
:root[data-bs-theme="dark"] .wiz-step.is-done { color: var(--dk-text-1); }
.wiz-step.is-done:not(:last-child)::after { background: var(--dk-primary); }
/* Below the phone breakpoint the labels go and the dots stay: three words plus three
   connectors do not fit 430px, and a numbered dot still says where you are. */
@media (max-width: 30rem) {
  .wiz-step-label { display: none; }
  .wiz-step:not(:last-child)::after { inline-size: 1.5rem; }
}

/* A completed step shows a tick instead of its number. Both glyphs are in the markup and
   this picks one, so advancing a step is a class change and never a DOM edit. */
.wiz-tick { display: none; font-size: .6875rem; }
.wiz-step.is-done .wiz-tick { display: inline; }
.wiz-step.is-done .wiz-num { display: none; }

/* The pane, and the height it animates to. `.wiz-clip` is measured and set by `ui.js`;
   with the bundle absent it has no height at all and simply wraps its content.

   **The inline padding is a gutter for things that draw outside their box** (M14.42,
   owner: "the لون العلامة is caped when first one is selected"). The selected swatch's
   marker is an `outline` with a 2px offset, and an outline takes no part in layout — so
   on the chip sitting against the row's start edge it drew 4px past the pane and
   `overflow: hidden` cut it in half. Every `:focus-visible` ring in here has the same
   problem. The clip cannot simply stop hiding: `overflow-y: hidden` with a visible x
   computes x to `auto`, which clips anyway and grows a scrollbar doing it — the trap
   M14.33 hit with the popover. The negative margin gives the width back, so nothing
   moves. */
.wiz-clip {
  overflow: hidden;
  padding-inline: .375rem;
  margin-inline: -.375rem;
  transition: block-size var(--dk-t-base) var(--dk-ease);
}
/* Except while something inside it is open (M14.81, owner: "dropdown is caped in new
   account in users page"). The third box in this stylesheet to cut a `<select>` menu off,
   after `.panel` (M14.58) and `.dtable-wrap` (M14.64), and it is the same one line for the
   same reason: the clip's height is pinned to the pane, and a `.pop-body` hangs below it.
   Measured on the account wizard's role field — menu 557→671, clip bottom 578, so 21px of
   114 was on screen.

   **The second clip is `.dlg-body` and it is deliberately left alone.** It is
   `overflow-y: auto` because a tall dialog has to scroll, and lifting it would spill that
   content outside the panel — a worse bug than this one. It has room: the same
   measurement puts the menu's foot at 671 against a body ending at 680. A *long* menu at
   the bottom of a dialog is what `.pop--up` exists for (M14.66), asked for by the call
   site, because being at the bottom of a scroll box is not something a rule can see.

   Not `:has(.pop[open]:not(.is-closing))`, which would re-clip the moment a menu starts
   leaving: the exit is 180ms of `furl` and cropping it is a worse artifact than the one it
   would prevent. What it would prevent is a step change made while a menu is open — the
   pane slides in from 16px out for 250ms with nothing clipping it, which lands inside the
   dialog's own padding and is invisible. */
.wiz-clip:has(.pop[open]) { overflow: visible; }
/* **Two columns, and never three** (M14.46, owner, on step two of the wizard: "do a full
   system review for dialog only allow 2 column in the row 3 not allows / see it look
   redculasly small").

   M14.42 made these equal columns with `flex: 1 1 0` — a zero basis, so the share is the
   whole width rather than the leftover, which is what "they are not 50% 50% width" needed.
   The cost of a zero basis is that N fields become N columns: the wizard's admin step put
   a username, a phone and a password on one 608px line and each got 190px.

   A half-width basis is the same idea with a floor under it. Two fields still fill the
   line; a third wraps and, with `flex-grow`, takes the whole of the next one — so a row
   of three reads as 2 + 1 rather than as thirds. No row class, and no way for a template
   to ask for three across. `.field-row` in a *page* is untouched: the POS and the customer
   portal put five and six controls on a filter line on purpose. */
.wiz .field-row > .field,
.dlg .field-row > .field {
  flex: 1 1 calc(50% - (var(--dk-dlg-gap-x) / 2));
  min-inline-size: 0;
}
/* The gap between two fields on a line, and between the lines they wrap onto — which is a
   block boundary and gets the block spacing, not the column spacing. */
.wiz .field-row,
.dlg .field-row { gap: var(--dk-dlg-gap) var(--dk-dlg-gap-x); }
/* Label to control. A sixth of the space between two blocks was not enough to say the
   label belongs to the input under it rather than to the row above. */
.wiz .field,
.dlg .field { gap: .5rem; }
.wiz-pane { min-block-size: 3rem; }
/* Only ever seen with the bundle absent, where every pane renders at once and they would
   otherwise run together into one undifferentiated column of fields.

   **And it has to be taken back off once the wizard is on** (M14.43, owner, on a render of
   step two: "see now secound page is capped"). `+` is a DOM relationship and not a
   rendering one, so this kept matching while the pane before it was `display: none` --
   every step but the first carried a 20px top margin nobody had asked for. The clip is
   `overflow: hidden` and therefore its own formatting context, so the margin could not
   collapse out of it either: measured, clip 428-536 and the pane 448-556, its last 20px
   of inputs cut off. One pane is on screen at a time here; there is nothing for it to be
   separated from. */
.wiz-pane + .wiz-pane { margin-block-start: 1.25rem; }
.wiz--on .wiz-pane + .wiz-pane { margin-block-start: 0; }
.wiz-pane > .field-row:first-child,
.wiz-pane > .pane-hint:first-child { margin-block-start: 0; }
/* The unfurl, in the direction you are travelling. `translateX` and not a logical
   equivalent because there is none -- and the sign is flipped under RTL by the `[dir]`
   rule below rather than by asking the box for its direction. */
.wiz-anim-fwd { animation: wiz-fwd var(--dk-t-base) var(--dk-ease); }
.wiz-anim-back { animation: wiz-back var(--dk-t-base) var(--dk-ease); }
@keyframes wiz-fwd { from { opacity: 0; transform: translateX(-16px); } to { opacity: 1; transform: none; } }
@keyframes wiz-back { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: none; } }
[dir="rtl"] .wiz-anim-fwd { animation-name: wiz-fwd-rtl; }
[dir="rtl"] .wiz-anim-back { animation-name: wiz-back-rtl; }
@keyframes wiz-fwd-rtl { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: none; } }
@keyframes wiz-back-rtl { from { opacity: 0; transform: translateX(-16px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .wiz-anim-fwd, .wiz-anim-back { animation: none; }
  .wiz-clip { transition: none; }
}

/* The footer. Cancel is pushed to the opposite end from the pair that moves you through
   the wizard, so "leave" and "continue" are never adjacent targets. */
/* The subdomain's note, in three states (M14.47): the hint until something legal is
   typed, then the address it becomes, or the refusal it would get. All three are in the
   markup and a class picks one — `ui.js` writes the host and nothing else, so the note is
   still the plain hint with the bundle absent. */
.slug-note > .slug-addr,
.slug-note > .slug-bad { display: none; }
.slug-note.is-free > .slug-hint,
.slug-note.is-taken > .slug-hint,
.slug-note.is-taken > .slug-addr { display: none; }
.slug-note.is-free > .slug-addr { display: inline; color: var(--tone-ok); }
.slug-note.is-taken > .slug-bad { display: inline; color: var(--tone-bad); }
.slug-note .num { color: var(--dk-text-1); font-weight: 700; }

/* The wizard's nav lives inside the scrolling dialog body, so a tall step pushed it past
   the dialog's edge and the buttons were cut in half (M14.95). Pinned to the bottom of the
   scroller, on the dialog's own opaque surface so the panes pass behind it rather than
   through it -- the same `glass over wash` idiom the account menu uses for the same reason. */
.dlg .wiz-nav {
  position: sticky;
  /* Above the panes, not merely after them: `.btn` is `position: relative`, so a row of
     buttons scrolling under this bar painted straight through it. */
  z-index: 2;
  inset-block-end: calc(var(--dk-dlg-pad) * -1);
  margin-block-start: .75rem;
  padding-block: .75rem;
  border-block-start: 1px solid var(--dk-line);
  background: linear-gradient(145deg, var(--dk-glass-strong), var(--dk-glass-hover)),
              var(--dk-wash-1);
}

.wiz-nav { align-items: center; gap: .5rem; }
.wiz-cancel { margin-inline-end: auto; }
.wiz-solo { display: flex; justify-content: flex-end; }

/* --------------------------------------------------------------- control plane */
/* The super-admin surface (M6.16). It keeps the design language and drops the tenant's
   colour: this is where someone creates and suspends whole businesses, and a shop's colour
   here would be a lie about which shop you are looking at. The chrome is the platform's own
   mulberry, pinned rather than inherited. */
.is-super {
  --dk-primary: #9B2C5E;
  --dk-primary-dark: #691E40;
  --dk-primary-darker: #46142A;
  --dk-primary-rgb: 155, 44, 94;
  --dk-grad-rgb: 105, 30, 64;
  --dk-accent-rgb: 105, 30, 64;
  --dk-on-primary: #ffffff;
}
:root[data-bs-theme="dark"] .is-super { --dk-primary-darker: #E9A9C4; }
/* `.appbar--super` went with the strip it modified (M14.39): the control plane is
   built from `.shell` now, like the CRM, and its mark is a `.shell-brand-mark`. What
   stays is `.is-super` above — the platform's own mulberry, which is the part that
   must never look like a tenant's. */

/* A wider content column than the customer PWA: these two are desktop surfaces. */
.app-main--wide { max-inline-size: 76rem; }

/* A suspended shop's row recedes without disappearing — it still has to be findable, and
   `opacity: .5` on a whole card took its link's contrast down with it. */
.dtable tbody tr.row--off td { color: var(--dk-text-3); }
.dtable tbody tr.row--off a { color: var(--dk-text-2); }

/* The one number on the control plane worth crossing the room for. */
.dtable .stuck { color: var(--tone-bad); font-weight: 700; }

/* --------------------------------------------------------------- a11y */
/* The reference has essentially no keyboard focus indicator — :focus-visible
   twice against eleven `outline: none`. Its biggest defect; not copied. */
/* `summary` is in the list because three controls in this product are one — the
   reject-reason popover, the create-account panel and the language menu — and a
   `<summary>` is focusable without carrying a `tabindex` to be caught by. */
/* The ring only. It used to set `border-radius: var(--dk-r-xs)` as well, which is the
   one thing a focus rule must not do: it reshapes the control (M14.13). An `outline`
   already follows whatever radius the element has, in every browser this ships to, so
   the declaration bought nothing and cost the shape of every field in the product — a
   14px `--dk-r-md` box snapping to 7px for exactly as long as somebody was typing in it,
   with the 4px focus glow still drawn at the old corner. On the login screen, which is
   the first thing a customer sees and where the field is focused on arrival, that read
   as a dent in the bottom edge.

   Written after every component and with a `:where()` that contributes nothing, this
   rule sits at one pseudo-class — the same weight as the `.form-control` it was
   overriding, winning only on source order. Which is also why the fix is to delete the
   line rather than to out-specify it in eleven places: the pill controls below already
   had to restate their own radius to get it back, and that is the shape of a workaround
   spreading. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  /* Through `--tone-brand-soft` since M14.22, and this is the one site in that round
     whose old value broke a rule rather than a preference: a focus indicator wants 3:1
     against what it is drawn against, and the shop's own mulberry measured 2.48:1 on the
     dark page ground and 2.09:1 on a dark control's -- the ring was hardest to see
     exactly where a keyboard is the only way in. The pale shade puts it at 6.78:1 there
     and leaves every light-mode ring at the pixel it was. */
  outline: 2px solid var(--tone-brand-soft);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  /* `*::backdrop` is listed because `*` does not reach it: a backdrop is not a child of
     anything, and the modal's fade (M14.43) is the first thing in this file to animate
     one. Without it the blanket rule below covered the dialog and left the page behind
     it darkening over 280ms. */
  *, *::before, *::after, *::backdrop {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  /* `.orb { animation: none }` used to be here, because the blanket rule above only
     shortens an animation and an `infinite alternate` one still ran. The orbs no longer
     animate at all (M14.27), so the exemption has nothing left to exempt. */
}
/* Per-tenant "reduce effects", independent of the OS setting. The reference's `no-fx`
   architecture, kept because it is genuinely good and doubles as a shop-level setting —
   but wired to the media query as well, which its own comment admits was intended and
   never done. Everything M12 added is listed here: an effect that is not gated is an
   effect a shop cannot turn off. */
.no-fx .orb { display: none; }
.no-fx .glass, .no-fx .card, .no-fx .island, .no-fx .shell-top.is-scrolled,
.no-fx .appbar.is-scrolled, .no-fx .pop-body, .no-fx .dlg::backdrop {
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
/* The modal is the one surface here that moves on a `transition` rather than an
   `animation` (M14.43 -- a `<dialog>` can transition across a display change and a
   `<details>` cannot), so the `animation: none` list below would never have reached it.
   A shop that turned effects off still had a panel sliding up at it. */
.no-fx .dlg, .no-fx .dlg::backdrop { transition: none; }
.no-fx .advanced::details-content { transition: none; }
.no-fx .card-grid > *,
.no-fx .shell-content > *, .no-fx .app-main > *,
.no-fx .island--title,
.no-fx .pop[open] > .pop-body, .no-fx .lang-pick[open] > .pop-body,
.no-fx .lang-pick.is-closing > .pop-body,
.no-fx .user-menu[open] > .pop-body, .no-fx .user-menu.is-closing > .pop-body,
.no-fx .pop.is-closing > .pop-body,
.no-fx .swatch input:checked ~ .swatch-chip,
.no-fx .swatch input:checked ~ .swatch-name,
.no-fx .dropdown-menu.show { animation: none; }
/* The toast keeps its two animations — they are what hides it — and loses only the
   distance they cover. `animation: none` here would strand it on screen. */
.no-fx .toast-item { animation-duration: .001s, .001s; }
.no-fx .btn::before { content: none; }
.no-fx .btn:active:not(:disabled),
.no-fx .shell-link:hover,
.no-fx .chip:hover,
.no-fx .form-control:focus, .no-fx .form-select:focus,
.no-fx .island:where(a, button, [role="button"]):active { transform: none; }
.no-fx .shell { transition: none; }

/* ---------------------------------------------------------------- login steps
   M13.5. Login is three submissions now, and a form that replaces itself with a
   different form is disorienting without something saying that progress was made.
   Three dots rather than "step 2 of 3": the count is the reassurance, and it needs no
   translation. `aria-hidden` because the heading and the field label already say where
   the person is — a screen reader announcing "list, three items, item two" adds nothing. */
/* The login card's sliding track (M14.9).

   Inert until `auth.js` binds it: one slide, no transform, height:auto. Everything below
   only does something once a second slide is appended, which is why a browser with no JS
   sees exactly the card it saw before. */
.auth-clip {
  /* `inline-size` container so a slide can be exactly one card wide without JS having to
     measure it — the same trick the reference uses. */
  container-type: inline-size;
  overflow: hidden;
  transition: height .32s cubic-bezier(.4, 0, .2, 1);

  /* M14.10 — room for the things that are drawn OUTSIDE their own box.

     The clip has to hide the slide that is off to the side, and `overflow-x: hidden`
     with `overflow-y: visible` is not a thing CSS can express: one axis hidden forces
     the other to `auto`. So it hid everything, and the two most expensive details on
     the screen were being cut in half by it — the primary button sits flush at the
     clip's bottom edge, so its whole `0 14px 32px` brand glow was gone, and a focused
     field's ring and glow were clipped at both sides.

     Padding buys the room and an equal negative margin gives it back, so the *content*
     box is untouched: `100cqi` is still exactly one card wide and the slide still
     travels exactly `100cqi + gap`. The inline figure has to stay under that 1.5rem gap
     or the next slide would show in the gutter before it is asked to; 20px does, and
     covers the 16px a hovered button's glow reaches sideways. The block figure matches
     the card's own bottom padding, so the glow lands exactly on the card's edge.

     `content-box` because `auth.js` animates `height` with a slide's `offsetHeight`.
     Under the inherited `border-box` that measurement would now include the padding and
     the card would close by 36px on every step. */
  box-sizing: content-box;
  padding: .5rem 1.25rem 2.25rem;
  margin: -.5rem -1.25rem -2.25rem;
}
.auth-track {
  display: flex;
  gap: 1.5rem;
  /* Each step is its own height, not the tallest one's (M14.13).
     `stretch` is the flex default and it made a slide report its *sibling's* height
     while both were in the track — which is exactly when both measurements are taken.
     Going forward that was invisible, because the step being measured happened to be
     the taller one; going back it meant the card was told to shrink to the height it
     already had, and the second phase did nothing at all. Top-aligned is also what the
     card wants anyway: a step belongs at the top of it, not stretched down it. */
  align-items: flex-start;
  /* How many slides the track has moved. Declared here rather than left to a `var()`
     fallback so the sheet says the resting value out loud; `auth.js` sets it to 1 for the
     length of one transition and removes it again. */
  --slide: 0;
}
.auth-track.is-sliding {
  transition: transform .32s cubic-bezier(.4, 0, .2, 1);
}
/* The two directions are separate rules because a translate is signed and RTL runs the
   other way; `100cqi` is the slide's own width, so the gap is the only thing to add.

   `.is-shifted` is the same transform without the transition, which is what going back
   needs (M14.13): the previous step is inserted *before* the one on screen, so the track
   has to be moved one slide along before anything animates or the live step would jump
   sideways. Placed with `.is-shifted`, then animated by adding `.is-sliding` and setting
   `--slide` back to 0. Still two classes rather than a transform on the bare track,
   because a resting `translateX(0)` is a containing block and a compositing layer that
   the card does not otherwise pay for. */
[dir="ltr"] .auth-track.is-shifted,
[dir="ltr"] .auth-track.is-sliding { transform: translateX(calc(var(--slide) * -1 * (100cqi + 1.5rem))); }
[dir="rtl"] .auth-track.is-shifted,
[dir="rtl"] .auth-track.is-sliding { transform: translateX(calc(var(--slide) * (100cqi + 1.5rem))); }
.auth-slide { flex: 0 0 100cqi; inline-size: 100cqi; }

/* The fade, which rides with the slide — the second of the two phases, never with the
   resize (M14.13, owner direction). The reference moves the track and eases the
   clip's height and does not touch opacity; the fade was asked for on top, and it earns
   its place: mid-slide the two steps are side by side and both fully opaque, so for a
   tenth of a second the card reads as two cards. Fading them past each other leaves one
   legible thing on screen at a time.

   Shorter than the .32s slide on purpose (`--dk-t-pop`): run over the full distance, the
   two steps cross at half opacity in the middle, which looks like a wash rather than a
   change. `auth.js` sets the two classes; unbound, neither ever appears. */
.auth-slide { transition: opacity var(--dk-t-pop) var(--dk-ease); }
.auth-slide.is-entering { opacity: 0; }
.auth-track.is-sliding .auth-slide.is-entering { opacity: 1; }
.auth-track.is-sliding .auth-slide.is-leaving { opacity: 0; }

/* Follows the card's own bottom padding down at the phone breakpoint, so the button's
   glow keeps landing exactly on the card's edge rather than 4px past it.

   It has to live *here*, after the rule it overrides, and not beside `.auth-card`'s own
   breakpoint 600 lines up: a media query adds no specificity, so from up there the
   `padding` shorthand above simply won it back and the override did nothing at all. */
@media (max-width: 30rem) {
  .auth-clip { padding-block-end: 2rem; margin-block-end: -2rem; }
}

@media (prefers-reduced-motion: reduce) {
  .auth-clip, .auth-track.is-sliding { transition: none; }
}

/* A password box with an eye in its trailing end. */
.pw-wrap { position: relative; display: block; }
.pw-wrap .form-control { padding-inline-end: 2.75rem; }
.pw-eye {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-end: .5rem;
  transform: translateY(-50%);
  inline-size: 2rem; block-size: 2rem;
  display: grid; place-items: center;
  padding: 0; border: 0;
  border-radius: var(--dk-r-sm);
  background: transparent;
  color: var(--dk-text-3);
  cursor: pointer;
  transition: color var(--dk-t-pop) var(--dk-ease), background var(--dk-t-pop) var(--dk-ease);
}
.pw-eye:hover { color: var(--tone-brand-soft); background: var(--dk-surface-hover); }
/* Both icons ship in the button and CSS picks one, so the swap needs no icon name in
   the script — `_ui.html`'s map stays the only place they are spelled. */
.pw-eye .pw-ico-hide, .pw-eye.is-on .pw-ico-show { display: none; }
.pw-eye.is-on .pw-ico-hide { display: inline-block; }
.pw-eye.is-on { color: var(--tone-brand-soft); }

/* A submit that is waiting on the network. Not a spinner — the card is about to move,
   and two things moving at once is noise. */
.btn.is-busy { opacity: .65; pointer-events: none; }

/* Ghost: the reference's third button weight, for an action that is real but secondary.
   The way out of a half-finished login is one — at 13px underlined it read as fine print
   on the one screen where somebody is already unsure of themselves. */
.btn-ghost {
  background: transparent;
  color: var(--dk-text-2);
  border: 1px solid var(--dk-ctl-bd);
  box-shadow: none;
}
.btn-ghost:hover, .btn-ghost:focus-visible {
  background: var(--dk-surface-hover);
  color: var(--dk-text-1);
  border-color: var(--dk-line);
}

.auth-steps {
  display: flex; justify-content: center; gap: .4rem;
  margin: 0 0 1.1rem; padding: 0; list-style: none;
}
.auth-step {
  inline-size: .5rem; block-size: .5rem; border-radius: 50%;
  background: var(--dk-ctl-bd);
  transition: background var(--dk-t-pop) var(--dk-ease),
              transform var(--dk-t-pop) var(--dk-ease);
}
.auth-step.is-done { background: var(--dk-primary); }
.auth-step.is-now { background: var(--dk-primary); transform: scale(1.5); }

/* The number typed at step 1, shown back at step 2 — so somebody who mistyped can see it
   before they wonder why their password is being refused. */
.auth-echo {
  text-align: center; margin: 0 0 .9rem;
  font-weight: 600; color: var(--dk-text-2);
}
.auth-hint { text-align: center; margin: 0 0 .9rem; color: var(--dk-text-2); }

/* Six digits, spaced and large: this is read off one screen and typed into another. */
.auth-code {
  text-align: center; font-size: 1.5rem; letter-spacing: .45em;
  text-indent: .45em;  /* the tracking after the last glyph, else it looks off-centre */
  font-variant-numeric: tabular-nums;
}

/* The way out of a half-finished login. A ghost button since M14.9 — it used to be a
   13px underlined link, which read as fine print on the one screen where somebody is
   already unsure. `1.25rem` is the form's own `gap`, so it keeps the card's rhythm even
   though it sits outside the form. */
/* The way back out (M14.13, owner direction). It was a full-width ghost button, which
   is what the reference does (`Login.tsx` renders back as `variant="ghost" full`) — but
   two stacked full-width controls make the card look like it is asking two questions of
   equal weight, and only one of them is the thing to do next. Demoted to a quiet inline
   link with a chevron: it keeps a real tap target on a phone through the block padding,
   and it stops competing with the primary. A deliberate departure from Dorak, not drift. */
.auth-restart {
  /* `fit-content` + auto inline margins, not `align-self`: the slide is a plain block,
     so there is no flex context here to align against. */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .375rem;
  inline-size: fit-content;
  margin-inline: auto;
  margin-block-start: 1rem;
  padding: .5rem .75rem;
  border: 0;
  background: none;
  border-radius: var(--dk-r-sm);
  font-size: .8125rem;
  font-weight: 700;
  color: var(--dk-text-2);
  text-decoration: none;
  transition: color var(--dk-t-pop) var(--dk-ease),
              background var(--dk-t-pop) var(--dk-ease);
}
.auth-restart:hover, .auth-restart:focus-visible {
  color: var(--tone-brand-soft);
  background: var(--dk-ctl-bg-hover);
}
/* Waiting on step one to come back. `auth.js` sets this the way `busy()` sets it on a
   submit button, and the button's own rule is scoped to `.btn` — which this is not any
   more, so without this the link had no feedback and, worse, no `pointer-events: none`
   to stop a second click starting a second fetch. */
.auth-restart.is-busy { opacity: .55; pointer-events: none; }
/* Points the way it goes: back is the leading edge, whichever way the page runs. */
.auth-restart .fa-chevron-down { transform: rotate(90deg); }
[dir="ltr"] .auth-restart .fa-chevron-down { transform: rotate(-90deg); }
.auth-restart-ico { font-size: .625rem; opacity: .7; }

/* ---------------------------------------------------------------- Telegram linking
   M13.4. The page an admin turns towards a customer standing at the counter, so the QR
   is the page's subject rather than an illustration beside one. */
.link-page { max-width: 44rem; }

/* The plate stays white in both themes and the QR stays black on it. Everything else in
   this UI follows the viewer's palette; a QR is a machine target, and scanners are
   calibrated for dark modules on a light ground. The padding is the second half of the
   quiet zone -- `qr.svg` draws four modules of its own, and a code flush against a
   coloured panel edge is the commonest reason one that looks fine will not scan. */
.qr-plate {
  display: grid; place-items: center; margin-inline: auto;
  padding: .75rem; background: #fff;
  border-radius: var(--dk-r-md); box-shadow: var(--dk-sh-sm);
  width: max-content;
}
.qr-plate svg { display: block; }

/* The creation wizard's first step: the code on one side, who answered it on the other.
   The surface is the panel's, not a bare row — this is the step's subject, and it sat on
   the dialog's own ground looking like two loose things beside each other. */
.scan-plate {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: .75rem;
  margin-block: .875rem;
  padding: 1.25rem 1rem;
  border: 1px solid var(--dk-rim);
  border-top-color: var(--dk-rim-top);
  border-radius: var(--dk-r-lg);
  background: linear-gradient(145deg, var(--dk-glass-strong), var(--dk-glass-hover)),
              var(--dk-wash-1);
  box-shadow: var(--dk-sh-sm);
}
.scan-bot { margin: 0; font-weight: 700; color: var(--dk-text-1); }
.scan-state { margin: 0; display: flex; align-items: center; justify-content: center;
              gap: .5rem; font-size: .875rem; color: var(--dk-text-2); }
.scan-state--ok { color: var(--tone-ok); font-weight: 600; }
.scan-acts { display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem; }
/* The code being replaced, rather than the previous one lingering under a new fetch. */
.qr-skel { inline-size: 190px; block-size: 190px; border-radius: var(--dk-r-sm); }
/* The one in a dialog, beside a hint rather than under a counter (M14.49). */
.qr-plate--sm { padding: .5rem; }

/* The shop's Telegram admins (M14.49): a name, its number, and the way to remove it.
   A list and not a table — three shops have one of these each and the widest has three,
   and a `<table>` at that size is a header row explaining two columns. */
.tg-list { list-style: none; margin: 0; padding: 0; }
.tg-row {
  display: flex; align-items: center; gap: .625rem;
  padding-block: .5rem;
}
.tg-row + .tg-row { border-top: 1px solid var(--dk-line); }
.tg-name { font-weight: 600; font-size: .875rem; color: var(--dk-text-1); }
.tg-id { font-size: .8125rem; color: var(--dk-text-2); }
/* The controls at the far end, not the phone: a name and its number read as one fact and
   should not be pushed apart by the space. On the *group* since M14.53, because the row
   carries two of them now — correct the name, disable the person — and an auto margin on
   a lone `form` left them stacking one above the other. */
.tg-acts { display: flex; align-items: center; gap: .375rem; margin-inline-start: auto; }
/* A disabled admin is still listed, so that bringing them back is a click and not a
   database edit — but it should not read as the same row as a working one. */
.tg-row.is-off .tg-name, .tg-row.is-off .tg-id { color: var(--dk-text-3); }

.link-steps { margin: 0 0 1rem; padding-inline-start: 1.25rem; line-height: 1.9; }
.link-steps li { color: var(--dk-text-2); }
/* The same list inside the account wizard's second pane (M14.81), where the modal's own
   block rhythm spaces the blocks — its 1rem tail lands on top of that and separates the
   list from the note under it by half again as much as anything else in the dialog. */
.wiz .link-steps { margin-block-end: 0; }

@media print {
  .shell { grid-template-columns: minmax(0, 1fr); }
  .shell-side, .shell-top, .appbar, .viewer-controls, .orb { display: none !important; }
  .panel, .card { box-shadow: none; border-color: #ddd; }
  .card-grid, .card-grid > *, .shell-content > *, .app-main > * { animation: none !important; }
  .dtable tbody td { background: transparent !important; }
}
