/* inter-cyrillic-ext-wght-normal */
@font-face {
  font-family: 'Inter Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2) format('woff2-variations');
  unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* inter-cyrillic-wght-normal */
@font-face {
  font-family: 'Inter Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2) format('woff2-variations');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* inter-greek-ext-wght-normal */
@font-face {
  font-family: 'Inter Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2) format('woff2-variations');
  unicode-range: U+1F00-1FFF;
}

/* inter-greek-wght-normal */
@font-face {
  font-family: 'Inter Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(/assets/inter-greek-wght-normal-CkhJZR-_.woff2) format('woff2-variations');
  unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}

/* inter-vietnamese-wght-normal */
@font-face {
  font-family: 'Inter Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2) format('woff2-variations');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* inter-latin-ext-wght-normal */
@font-face {
  font-family: 'Inter Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2) format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* inter-latin-wght-normal */
@font-face {
  font-family: 'Inter Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(/assets/inter-latin-wght-normal-Dx4kXJAl.woff2) format('woff2-variations');
  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;
}/* dancing-script-vietnamese-wght-normal */
@font-face {
  font-family: 'Dancing Script Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 400 700;
  src: url(/assets/dancing-script-vietnamese-wght-normal-BsUJvdxu.woff2) format('woff2-variations');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* dancing-script-latin-ext-wght-normal */
@font-face {
  font-family: 'Dancing Script Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 400 700;
  src: url(/assets/dancing-script-latin-ext-wght-normal-DJGK6qJa.woff2) format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* dancing-script-latin-wght-normal */
@font-face {
  font-family: 'Dancing Script Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 400 700;
  src: url(/assets/dancing-script-latin-wght-normal-CyISZbeS.woff2) format('woff2-variations');
  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;
}/* playfair-display-cyrillic-wght-normal */
@font-face {
  font-family: 'Playfair Display Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 400 900;
  src: url(/assets/playfair-display-cyrillic-wght-normal-5WvUvBgz.woff2) format('woff2-variations');
  unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}

/* playfair-display-vietnamese-wght-normal */
@font-face {
  font-family: 'Playfair Display Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 400 900;
  src: url(/assets/playfair-display-vietnamese-wght-normal-Cabi7G8-.woff2) format('woff2-variations');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* playfair-display-latin-ext-wght-normal */
@font-face {
  font-family: 'Playfair Display Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 400 900;
  src: url(/assets/playfair-display-latin-ext-wght-normal-CT1r92Rl.woff2) format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* playfair-display-latin-wght-normal */
@font-face {
  font-family: 'Playfair Display Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 400 900;
  src: url(/assets/playfair-display-latin-wght-normal-BOwq7MWX.woff2) format('woff2-variations');
  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;
}/*
  LDV Invoicing design tokens — "rose" theme.
  Deep rose as the primary, warm blush neutrals, antique gold as the secondary accent.
  Every colour is a variable — nothing is hardcoded per mode, so light/dark both work.
*/

:root {
  --bg: #fdf6f7;
  --bg-elevated: #ffffff;
  --surface: #ffffff;
  --surface2: #f9eff1;
  --surface3: #f2e2e6;
  --border: #ecdde1;
  --border-strong: #dcc4cb;

  --txt1: #2c1c22;
  --txt2: #6a5158;
  --txt3: #93777f;

  --brand: #ac3b5e;
  --brand-ink: #86294a;
  --brand-soft: #fbe7ec;
  --brand-contrast: #ffffff;

  --accent: #b0793f;
  --accent-soft: #f9ecdc;
  --gold: #a9803d;
  --gold-soft: #f8eeda;

  /* Money semantics: emerald = settled, red = still owed. Deliberately distinct from the
     rose brand colour so an amount is never mistaken for a heading. */
  --ok: #0f8a63;
  --ok-soft: #ddf1e8;
  --warn: #a3702a;
  --warn-soft: #f9edd8;
  --danger: #b3241f;
  --danger-soft: #f9dedd;

  --shadow-sm: 0 1px 2px rgba(44, 28, 34, 0.06), 0 1px 3px rgba(44, 28, 34, 0.04);
  --shadow-md: 0 4px 16px rgba(44, 28, 34, 0.1);
  --shadow-lg: 0 12px 32px rgba(44, 28, 34, 0.16);

  /* Frosted panels. The page paints two soft colour washes behind everything, and panels
     blur what is under them — that is what makes the glass read as glass. */
  --glass-bg: rgba(255, 255, 255, 0.74);
  --glass-border: rgba(255, 255, 255, 0.9);
  --glass-sheen: linear-gradient(180deg, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0) 46%);
  --glass-ring: inset 0 1px 0 rgba(255, 255, 255, 0.85);
  --glass-blur: saturate(150%) blur(16px);
  /* Shared by the drawer scrim and the modal scrim so the no-blur fallback can darken both. */
  --scrim-bg: rgba(32, 18, 24, 0.42);
  --wash-1: rgba(172, 59, 94, 0.13);
  --wash-2: rgba(169, 128, 61, 0.12);

  /* The sign-in screen gets a warmer, brighter treatment than the app itself. */
  --login-base: #fbeff1;
  --login-glow-1: rgba(172, 59, 94, 0.2);
  --login-glow-2: rgba(201, 143, 60, 0.22);
  --login-glow-3: rgba(232, 139, 162, 0.24);

  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-xl: 22px;

  --header-h: 56px;
  /* iPhone notch / Dynamic Island and the home-bar area. The left/right insets are non-zero
     in landscape and around curved edges, so every edge-anchored element uses them. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  --safe-right: env(safe-area-inset-right, 0px);

  /* Inter everywhere — self-hosted via @fontsource, so it works offline and renders the
     same on every phone. No serif: elegance comes from weight, spacing and the palette. */
  --font: 'Inter Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-display: var(--font);

  /* Script face for the brand wordmark only — never for UI text or numbers. */
  --font-script: 'Dancing Script Variable', 'Segoe Script', cursive;

  /* Serif, scoped to the printed Suki Rewards card only — the rest of the app stays serif-free
     per --font-display above; a physical card worn in a wallet earns the one exception. */
  --font-card: 'Playfair Display Variable', Georgia, serif;
}

/* Dark mode is a deep plum rather than neutral black — keeps the rose feeling warm. */
:root[data-theme='dark'] {
  --bg: #160f13;
  --bg-elevated: #1f1519;
  --surface: #1f1519;
  --surface2: #281a20;
  --surface3: #33222a;
  --border: #3a2731;
  --border-strong: #513743;

  --txt1: #f5eaee;
  --txt2: #c3a8b1;
  --txt3: #9d818a;

  --brand: #e88ba2;
  --brand-ink: #f3b4c3;
  --brand-soft: #351d27;
  --brand-contrast: #2b0d17;

  --accent: #dda96a;
  --accent-soft: #33251a;
  --gold: #d8ab6b;
  --gold-soft: #322818;

  --ok: #3ddba6;
  --ok-soft: #10322a;
  --warn: #d9ab6a;
  --warn-soft: #322818;
  /* A true red, not salmon — dark enough to read as "owed" against the plum surface,
     light enough to stay legible on it. */
  --danger: #e0322b;
  --danger-soft: #3a1917;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.45);
  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.5);
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.6);

  --glass-bg: rgba(38, 25, 31, 0.62);
  --glass-border: rgba(255, 255, 255, 0.09);
  --glass-sheen: linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0) 46%);
  --glass-ring: inset 0 1px 0 rgba(255, 255, 255, 0.08);
  --glass-blur: saturate(140%) blur(18px);
  --scrim-bg: rgba(10, 5, 8, 0.52);
  --wash-1: rgba(232, 139, 162, 0.16);
  --wash-2: rgba(216, 171, 107, 0.12);

  /* Lifted well above the app background so signing in never feels like a black screen. */
  --login-base: #2a1c24;
  --login-glow-1: rgba(232, 139, 162, 0.34);
  --login-glow-2: rgba(216, 171, 107, 0.22);
  --login-glow-3: rgba(172, 59, 94, 0.38);
}

/*
  Appearance presets — "Rose" above is the default and stays hardcoded exactly as it always was.
  Everything below only ever redefines the same brand-tinted tokens Rose itself varies between
  light and dark: backgrounds, borders, ink, --brand/--accent/--gold and their soft tints, the
  decorative washes and the login glow. Money semantics (--ok/--warn/--danger), shadows, and every
  glass token (--glass-bg/--glass-border/--glass-sheen/--glass-ring/--glass-blur) are deliberately
  left untouched by every preset below — paid/overdue stays unambiguous, and the glass/shadow feel
  that makes this app's panels read as glass is the same no matter which preset is active.
*/

:root[data-palette='jade'] {
  --bg: hsl(162, 40%, 97%);
  --bg-elevated: #ffffff;
  --surface: #ffffff;
  --surface2: hsl(162, 28%, 94%);
  --surface3: hsl(162, 28%, 89%);
  --border: hsl(162, 25%, 85%);
  --border-strong: hsl(162, 25%, 75%);
  --txt1: hsl(162, 22%, 14%);
  --txt2: hsl(162, 12%, 35%);
  --txt3: hsl(162, 12%, 52%);
  --brand: hsl(162, 45%, 32%);
  --brand-ink: hsl(162, 50%, 24%);
  --brand-soft: hsl(162, 55%, 93%);
  --brand-contrast: #ffffff;
  --accent: hsl(38, 47%, 47%);
  --accent-soft: hsl(38, 55%, 91%);
  --gold: hsl(38, 46%, 44%);
  --gold-soft: hsl(38, 55%, 91%);
  --wash-1: hsla(162, 45%, 32%, 0.13);
  --wash-2: hsla(38, 47%, 47%, 0.12);
  --login-base: hsl(162, 45%, 94%);
  --login-glow-1: hsla(162, 45%, 32%, 0.2);
  --login-glow-2: hsla(38, 50%, 51%, 0.22);
  --login-glow-3: hsla(162, 55%, 60%, 0.24);
}

:root[data-theme='dark'][data-palette='jade'] {
  --bg: hsl(162, 22%, 7%);
  --bg-elevated: hsl(162, 20%, 10%);
  --surface: hsl(162, 20%, 10%);
  --surface2: hsl(162, 20%, 13%);
  --surface3: hsl(162, 20%, 17%);
  --border: hsl(162, 22%, 21%);
  --border-strong: hsl(162, 22%, 28%);
  --txt1: hsl(162, 28%, 93%);
  --txt2: hsl(162, 16%, 72%);
  --txt3: hsl(162, 10%, 57%);
  --brand: hsl(162, 55%, 62%);
  --brand-ink: hsl(162, 60%, 74%);
  --brand-soft: hsl(162, 35%, 16%);
  --brand-contrast: hsl(162, 45%, 9%);
  --accent: hsl(38, 50%, 64%);
  --accent-soft: hsl(38, 35%, 15%);
  --gold: hsl(38, 48%, 63%);
  --gold-soft: hsl(38, 35%, 14%);
  --wash-1: hsla(162, 55%, 62%, 0.16);
  --wash-2: hsla(38, 50%, 64%, 0.12);
  --login-base: hsl(162, 25%, 12%);
  --login-glow-1: hsla(162, 55%, 62%, 0.34);
  --login-glow-2: hsla(38, 50%, 64%, 0.22);
  --login-glow-3: hsla(162, 45%, 40%, 0.38);
  /* Rose's own dark --glass-bg is a rose-tinted neutral, not a true gray — it has to be redone
     per palette or every sheet/drawer reads as rose no matter what's selected. */
  --glass-bg: hsla(162, 20%, 11%, 0.62);
  --scrim-bg: hsla(162, 25%, 4%, 0.52);
}

:root[data-palette='marigold'] {
  --bg: hsl(32, 48%, 97%);
  --bg-elevated: #ffffff;
  --surface: #ffffff;
  --surface2: hsl(32, 32%, 94%);
  --surface3: hsl(32, 32%, 89%);
  --border: hsl(32, 28%, 85%);
  --border-strong: hsl(32, 28%, 75%);
  --txt1: hsl(26, 30%, 15%);
  --txt2: hsl(26, 16%, 36%);
  --txt3: hsl(26, 16%, 53%);
  --brand: hsl(32, 62%, 40%);
  --brand-ink: hsl(28, 65%, 31%);
  --brand-soft: hsl(34, 65%, 93%);
  --brand-contrast: #ffffff;
  --accent: hsl(190, 45%, 36%);
  --accent-soft: hsl(190, 40%, 91%);
  --gold: hsl(190, 42%, 34%);
  --gold-soft: hsl(190, 40%, 91%);
  --wash-1: hsla(32, 62%, 40%, 0.13);
  --wash-2: hsla(190, 45%, 36%, 0.12);
  --login-base: hsl(32, 55%, 94%);
  --login-glow-1: hsla(32, 62%, 40%, 0.2);
  --login-glow-2: hsla(190, 45%, 42%, 0.22);
  --login-glow-3: hsla(32, 70%, 65%, 0.24);
}

:root[data-theme='dark'][data-palette='marigold'] {
  --bg: hsl(28, 24%, 7%);
  --bg-elevated: hsl(28, 20%, 10%);
  --surface: hsl(28, 20%, 10%);
  --surface2: hsl(28, 20%, 13%);
  --surface3: hsl(28, 20%, 17%);
  --border: hsl(28, 22%, 21%);
  --border-strong: hsl(28, 22%, 28%);
  --txt1: hsl(30, 32%, 93%);
  --txt2: hsl(30, 18%, 72%);
  --txt3: hsl(30, 11%, 57%);
  --brand: hsl(32, 75%, 66%);
  --brand-ink: hsl(34, 80%, 77%);
  --brand-soft: hsl(30, 40%, 16%);
  --brand-contrast: hsl(26, 55%, 9%);
  --accent: hsl(190, 48%, 60%);
  --accent-soft: hsl(190, 32%, 15%);
  --gold: hsl(190, 45%, 58%);
  --gold-soft: hsl(190, 32%, 14%);
  --wash-1: hsla(32, 75%, 66%, 0.16);
  --wash-2: hsla(190, 48%, 60%, 0.12);
  --login-base: hsl(28, 28%, 12%);
  --login-glow-1: hsla(32, 75%, 66%, 0.34);
  --login-glow-2: hsla(190, 48%, 60%, 0.22);
  --login-glow-3: hsla(32, 62%, 40%, 0.38);
  --glass-bg: hsla(28, 20%, 11%, 0.62);
  --scrim-bg: hsla(28, 25%, 4%, 0.52);
}

:root[data-palette='azure'] {
  --bg: hsl(212, 48%, 97%);
  --bg-elevated: #ffffff;
  --surface: #ffffff;
  --surface2: hsl(212, 32%, 94%);
  --surface3: hsl(212, 32%, 89%);
  --border: hsl(212, 28%, 86%);
  --border-strong: hsl(212, 28%, 76%);
  --txt1: hsl(214, 30%, 15%);
  --txt2: hsl(214, 16%, 37%);
  --txt3: hsl(214, 16%, 53%);
  --brand: hsl(212, 55%, 42%);
  --brand-ink: hsl(214, 60%, 32%);
  --brand-soft: hsl(212, 65%, 94%);
  --brand-contrast: #ffffff;
  --accent: hsl(38, 47%, 47%);
  --accent-soft: hsl(38, 55%, 91%);
  --gold: hsl(38, 46%, 44%);
  --gold-soft: hsl(38, 55%, 91%);
  --wash-1: hsla(212, 55%, 42%, 0.13);
  --wash-2: hsla(38, 47%, 47%, 0.12);
  --login-base: hsl(212, 55%, 95%);
  --login-glow-1: hsla(212, 55%, 42%, 0.2);
  --login-glow-2: hsla(38, 50%, 51%, 0.22);
  --login-glow-3: hsla(212, 65%, 68%, 0.24);
}

:root[data-theme='dark'][data-palette='azure'] {
  --bg: hsl(214, 26%, 7%);
  --bg-elevated: hsl(214, 22%, 10%);
  --surface: hsl(214, 22%, 10%);
  --surface2: hsl(214, 22%, 13%);
  --surface3: hsl(214, 22%, 17%);
  --border: hsl(214, 22%, 21%);
  --border-strong: hsl(214, 22%, 28%);
  --txt1: hsl(214, 30%, 94%);
  --txt2: hsl(214, 18%, 73%);
  --txt3: hsl(214, 11%, 58%);
  --brand: hsl(212, 72%, 68%);
  --brand-ink: hsl(214, 78%, 79%);
  --brand-soft: hsl(214, 40%, 17%);
  --brand-contrast: hsl(216, 55%, 10%);
  --accent: hsl(38, 50%, 64%);
  --accent-soft: hsl(38, 35%, 15%);
  --gold: hsl(38, 48%, 63%);
  --gold-soft: hsl(38, 35%, 14%);
  --wash-1: hsla(212, 72%, 68%, 0.16);
  --wash-2: hsla(38, 50%, 64%, 0.12);
  --login-base: hsl(214, 28%, 12%);
  --login-glow-1: hsla(212, 72%, 68%, 0.34);
  --login-glow-2: hsla(38, 50%, 64%, 0.22);
  --login-glow-3: hsla(212, 55%, 42%, 0.38);
  --glass-bg: hsla(214, 20%, 11%, 0.62);
  --scrim-bg: hsla(214, 25%, 4%, 0.52);
}

:root[data-palette='orchid'] {
  --bg: hsl(288, 38%, 97%);
  --bg-elevated: #ffffff;
  --surface: #ffffff;
  --surface2: hsl(288, 26%, 95%);
  --surface3: hsl(288, 26%, 90%);
  --border: hsl(288, 22%, 86%);
  --border-strong: hsl(288, 22%, 77%);
  --txt1: hsl(288, 24%, 15%);
  --txt2: hsl(288, 13%, 37%);
  --txt3: hsl(288, 13%, 53%);
  --brand: hsl(288, 40%, 42%);
  --brand-ink: hsl(288, 45%, 32%);
  --brand-soft: hsl(288, 55%, 95%);
  --brand-contrast: #ffffff;
  --accent: hsl(40, 50%, 47%);
  --accent-soft: hsl(40, 58%, 91%);
  --gold: hsl(40, 48%, 44%);
  --gold-soft: hsl(40, 58%, 91%);
  --wash-1: hsla(288, 40%, 42%, 0.13);
  --wash-2: hsla(40, 50%, 47%, 0.12);
  --login-base: hsl(288, 48%, 95%);
  --login-glow-1: hsla(288, 40%, 42%, 0.2);
  --login-glow-2: hsla(40, 52%, 52%, 0.22);
  --login-glow-3: hsla(288, 55%, 70%, 0.24);
}

:root[data-theme='dark'][data-palette='orchid'] {
  --bg: hsl(288, 22%, 8%);
  --bg-elevated: hsl(288, 20%, 11%);
  --surface: hsl(288, 20%, 11%);
  --surface2: hsl(288, 20%, 14%);
  --surface3: hsl(288, 20%, 18%);
  --border: hsl(288, 20%, 22%);
  --border-strong: hsl(288, 20%, 29%);
  --txt1: hsl(288, 26%, 94%);
  --txt2: hsl(288, 15%, 73%);
  --txt3: hsl(288, 10%, 58%);
  --brand: hsl(288, 58%, 74%);
  --brand-ink: hsl(288, 63%, 83%);
  --brand-soft: hsl(288, 35%, 17%);
  --brand-contrast: hsl(288, 45%, 10%);
  --accent: hsl(40, 52%, 65%);
  --accent-soft: hsl(40, 35%, 15%);
  --gold: hsl(40, 50%, 64%);
  --gold-soft: hsl(40, 35%, 14%);
  --wash-1: hsla(288, 58%, 74%, 0.16);
  --wash-2: hsla(40, 52%, 65%, 0.12);
  --login-base: hsl(288, 26%, 13%);
  --login-glow-1: hsla(288, 58%, 74%, 0.34);
  --login-glow-2: hsla(40, 52%, 65%, 0.22);
  --login-glow-3: hsla(288, 40%, 42%, 0.38);
  --glass-bg: hsla(288, 20%, 12%, 0.62);
  --scrim-bg: hsla(288, 25%, 5%, 0.52);
}

:root[data-palette='terracotta'] {
  --bg: hsl(14, 42%, 97%);
  --bg-elevated: #ffffff;
  --surface: #ffffff;
  --surface2: hsl(14, 28%, 94%);
  --surface3: hsl(14, 28%, 89%);
  --border: hsl(14, 24%, 85%);
  --border-strong: hsl(14, 24%, 75%);
  --txt1: hsl(14, 26%, 15%);
  --txt2: hsl(14, 14%, 37%);
  --txt3: hsl(14, 14%, 53%);
  --brand: hsl(14, 52%, 42%);
  --brand-ink: hsl(12, 56%, 32%);
  --brand-soft: hsl(16, 60%, 94%);
  --brand-contrast: #ffffff;
  --accent: hsl(175, 35%, 33%);
  --accent-soft: hsl(175, 32%, 90%);
  --gold: hsl(175, 32%, 31%);
  --gold-soft: hsl(175, 32%, 90%);
  --wash-1: hsla(14, 52%, 42%, 0.13);
  --wash-2: hsla(175, 35%, 33%, 0.12);
  --login-base: hsl(14, 48%, 94%);
  --login-glow-1: hsla(14, 52%, 42%, 0.2);
  --login-glow-2: hsla(175, 38%, 38%, 0.22);
  --login-glow-3: hsla(14, 62%, 67%, 0.24);
}

:root[data-theme='dark'][data-palette='terracotta'] {
  --bg: hsl(14, 22%, 7%);
  --bg-elevated: hsl(14, 19%, 10%);
  --surface: hsl(14, 19%, 10%);
  --surface2: hsl(14, 19%, 13%);
  --surface3: hsl(14, 19%, 17%);
  --border: hsl(14, 20%, 21%);
  --border-strong: hsl(14, 20%, 28%);
  --txt1: hsl(14, 26%, 93%);
  --txt2: hsl(14, 15%, 72%);
  --txt3: hsl(14, 10%, 57%);
  --brand: hsl(14, 68%, 66%);
  --brand-ink: hsl(14, 72%, 77%);
  --brand-soft: hsl(14, 35%, 16%);
  --brand-contrast: hsl(12, 50%, 9%);
  --accent: hsl(175, 38%, 55%);
  --accent-soft: hsl(175, 28%, 14%);
  --gold: hsl(175, 36%, 53%);
  --gold-soft: hsl(175, 28%, 13%);
  --wash-1: hsla(14, 68%, 66%, 0.16);
  --wash-2: hsla(175, 38%, 55%, 0.12);
  --login-base: hsl(14, 26%, 12%);
  --login-glow-1: hsla(14, 68%, 66%, 0.34);
  --login-glow-2: hsla(175, 38%, 55%, 0.22);
  --login-glow-3: hsla(14, 52%, 42%, 0.38);
  --glass-bg: hsla(14, 20%, 11%, 0.62);
  --scrim-bg: hsla(14, 25%, 4%, 0.52);
}

/*
  The frosted panels are only readable because the blur destroys the detail behind them. Where the
  engine can't blur — old browsers, GPU compositing disabled, a build that strips the property —
  translucency turns into three layers of overlapping text, so go nearly opaque instead.
*/
@supports not ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  :root {
    --glass-bg: rgba(255, 252, 253, 0.975);
    --scrim-bg: rgba(32, 18, 24, 0.62);
  }

  :root[data-theme='dark'] {
    --glass-bg: rgba(30, 20, 25, 0.975);
    --scrim-bg: rgba(10, 5, 8, 0.72);
  }
}

* {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

/* LastPass (and some other password managers) inject their own icon-root element straight into
   the page next to a field, in the light DOM — `data-lpignore`/`autocomplete="off"` on the input
   itself are routinely ignored by the extension in practice. It also sets its own inline
   `display: initial !important`, which an external `display: none !important` can never beat (an
   inline !important always wins over a stylesheet !important on the same property) — so this
   kills it via properties LastPass's own inline style never touches instead. */
[data-lastpass-icon-root] {
  opacity: 0 !important;
  pointer-events: none !important;
  clip-path: inset(100%) !important;
}

html,
body {
  margin: 0;
  padding: 0;
  min-height: 100%;
}

body {
  background: var(--bg);
  color: var(--txt1);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}

/* Two fixed colour washes give the frosted panels something to blur. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(60% 45% at 12% 0%, var(--wash-1), transparent 70%),
    radial-gradient(55% 40% at 92% 100%, var(--wash-2), transparent 70%);
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

/* No native up/down caret anywhere — every numeric field here already sits next to its own +/-
   stepper buttons where one exists, and the browser's spinner only ever duplicated or crowded it. */
input[type='number'] {
  appearance: textfield;
  -moz-appearance: textfield;
}

input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
}

button {
  cursor: pointer;
  border: none;
  background: none;
}

a {
  color: var(--brand);
  text-decoration: none;
}

/* ---------- App shell ---------- */

.app {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.appbar {
  position: sticky;
  top: 0;
  z-index: 30;
  padding-top: var(--safe-top);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--glass-border);
  /* Same treatment as the landing bar: a soft drop below the hairline so the header reads as
     floating over the page it scrolls under. Tinted with the brand rather than plain black, which
     greys the blush. Tracks --brand so every palette gets its own tint, not just Rose. */
  box-shadow: 0 6px 18px -8px color-mix(in srgb, var(--brand) 35%, transparent);
}

:root[data-theme='dark'] .appbar {
  /* No brand tint registers on a dark background; depth has to come from shadow instead. */
  box-shadow: 0 8px 20px -10px rgba(0, 0, 0, 0.65);
}

.appbar-row {
  height: var(--header-h);
  display: flex;
  align-items: center;
  gap: 4px;
  padding-left: calc(6px + var(--safe-left));
  padding-right: calc(6px + var(--safe-right));
}

/* Every screen title is set in the script face; subtitles stay in Inter for legibility. */
.appbar-title {
  flex: 1;
  text-align: center;
  font-family: var(--font-script);
  font-size: 25px;
  font-weight: 700;
  letter-spacing: 0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The brand name, wherever it appears. Script needs more size and weight than Inter to
   hold its own at these sizes. */
.wordmark {
  font-family: var(--font-script);
  font-weight: 700;
  letter-spacing: 0.01em;
  line-height: 1.1;
}

/* Lockup: solid initials, script word. Inter's cap height is shorter than the script's
   ascender, so "LDV" is sized up to sit level with "Invoicing". */
.wordmark-solid {
  font-family: var(--font);
  font-weight: 800;
  font-size: 0.94em;
  letter-spacing: 0.01em;
  margin-right: 0.3em;
  vertical-align: 0.02em;
  color: var(--brand);
}

.appbar-title .wordmark {
  font-size: 25px;
  display: inline-block;
  vertical-align: -2px;
}

/* Opt-out for titles that are codes or numbers — a script face makes INV-000002 hard to read. */
.appbar-title .title-plain {
  font-family: var(--font);
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.appbar-title small {
  display: block;
  font-family: var(--font);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0;
  margin-top: -3px;
  color: var(--txt2);
}

/*
  Every icon-only button gets a visible circle rather than sitting bare with no boundary — that
  read as a plain glyph rather than a button (first noticed on the header's "⋮" trigger, then on
  the invoice line stepper/delete icons; applied once here instead of re-adding it per screen).
  Scoped to an actual <button> so the two hidden spacer spans (AppShell's empty right slot, the
  loading-state placeholder in DocumentEditorPage) don't pick up a phantom circle.
*/
.iconbtn {
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: var(--txt1);
  flex: 0 0 auto;
}

button.iconbtn {
  background: var(--surface2);
  border: 1px solid var(--border);
}

button.iconbtn:active {
  background: var(--surface3);
}

.iconbtn[disabled] {
  color: var(--txt3);
  opacity: 0.55;
}

/*
  Icon-button tooltip bubble. Rendered through a portal into document.body (see
  components/IconTooltip.tsx) rather than as a plain ::after on the button itself — a ::after
  clips against the first ancestor with overflow:hidden, and every list row's glass card uses
  exactly that to round its own corners, so the tooltip was getting cut off mid-word.
*/
.icon-tooltip {
  position: fixed;
  z-index: 70;
  background: #1c1014;
  color: #fdf6f7;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.3;
  white-space: nowrap;
  padding: 6px 10px;
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  pointer-events: none;
  animation: icon-tooltip-in 0.12s ease;
}

@keyframes icon-tooltip-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

.page {
  flex: 1;
  padding-top: 12px;
  padding-bottom: calc(96px + var(--safe-bottom));
  padding-left: calc(14px + var(--safe-left));
  padding-right: calc(14px + var(--safe-right));
  max-width: 720px;
  width: 100%;
  margin: 0 auto;
}

/* ---------- Drawer ---------- */

.scrim {
  /* Only shown while the mobile drawer is open; never on the desktop pinned sidebar. */
  display: none;
  position: fixed;
  inset: 0;
  z-index: 40;
  background: var(--scrim-bg);
  /* Frosted, not just dimmed — the content behind stays present but clearly out of focus. */
  backdrop-filter: blur(10px) saturate(115%);
  -webkit-backdrop-filter: blur(10px) saturate(115%);
  animation: fadeIn 0.2s ease;
}

.drawer {
  position: fixed;
  z-index: 41;
  top: 0;
  bottom: 0;
  left: 0;
  width: min(84vw, 320px);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-right: 1px solid var(--glass-border);
  box-shadow: var(--shadow-lg);
  padding: calc(18px + var(--safe-top)) 0 calc(18px + var(--safe-bottom)) var(--safe-left);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  /* Always mounted now — hidden off-canvas until .app.drawer-open slides it in (mobile). */
  transform: translateX(-100%);
  transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}

.app.drawer-open .scrim {
  display: block;
}

.app.drawer-open .drawer {
  transform: translateX(0);
}

.drawer-head {
  padding: 0 18px 16px;
  border-bottom: 1px solid var(--border-strong);
}

.drawer-brand {
  display: flex;
  align-items: center;
  gap: 10px;
}

.drawer-logo {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 auto;
  box-shadow: var(--shadow-sm);
}

.drawer-name {
  font-size: 15.5px;
  font-weight: 650;
  line-height: 1.25;
}

.drawer-name.wordmark {
  font-size: 20px;
  color: var(--brand-ink);
}

.drawer-sub {
  font-size: 12px;
  color: var(--txt2);
}

.drawer-nav {
  padding: 10px 8px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.drawer-item {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 14px;
  border-radius: var(--r-md);
  color: var(--txt1);
  font-weight: 550;
  text-align: left;
  width: 100%;
}

/* A badge sits with its label, not at the far edge: pushed right it read as a separate column and
   lost its connection to the word it qualifies. The negative margin trims the row's 14px gap to
   something that reads as attached. */
.drawer-item .badge {
  margin-left: -7px;
}

.drawer-item .ico {
  width: 22px;
  display: grid;
  place-items: center;
  color: var(--txt2);
}

.drawer-group-chevron {
  margin-left: auto;
  flex: 0 0 auto;
  color: var(--txt3);
  transition: transform 0.18s ease;
}

.drawer-group-chevron.is-expanded {
  transform: rotate(90deg);
}

/* Indented under the group head, with a thin rail connecting the sub-items back to it — the usual
   "this belongs to that" cue for a nested menu, instead of just floating at the same level. */
.drawer-subnav {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 2px 0 4px 29px;
  padding-left: 12px;
  border-left: 1.5px solid var(--border);
}

.drawer-subitem {
  padding: 8px 10px;
  font-size: 13px;
  font-weight: 500;
}

.drawer-subitem .ico {
  width: 18px;
}

/* A small in/out tag at the far end of the row: money coming in (Invoices) vs going out
   (Expenses). A direction, not a +/- value — an unpaid invoice is still the money-in side.
   Right-aligned so the two adjacent items line their tags up. */
.drawer-item .nav-flag {
  margin-left: auto;
  flex: 0 0 auto;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  line-height: 1.6;
}

.nav-flag.is-in {
  color: var(--ok);
  background: var(--ok-soft);
}

.nav-flag.is-out {
  color: var(--danger);
  background: var(--danger-soft);
}

.drawer-item[aria-current='true'] {
  background: var(--brand-soft);
  color: var(--brand-ink);
}

.drawer-item[aria-current='true'] .ico {
  color: var(--brand);
}

.drawer-foot {
  margin-top: auto;
  padding: 12px 18px 0;
  border-top: 1px solid var(--border-strong);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: 12px;
  color: var(--txt2);
}

.drawer-foot span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.drawer-version {
  flex: 0 0 auto;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--txt3);
  font-size: 10.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.drawer-version:hover {
  color: var(--brand);
  border-color: var(--brand);
}

/* ---------- Cards & lists ---------- */

.card {
  position: relative;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm), var(--glass-ring);
  overflow: hidden;
}

/* Light catch along the top edge — the detail that sells the glass. */
.card::before,
.list::before,
.stat::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: var(--glass-sheen);
  opacity: 0.55;
}

.card + .card {
  margin-top: 12px;
}

.card-pad {
  position: relative;
  z-index: 1;
  padding: 14px;
}

.section-title {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--txt2);
  margin: 18px 2px 8px;
}

.list {
  position: relative;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm), var(--glass-ring);
  overflow: hidden;
}

/* Rows sit above the sheen layer. */
.list > * {
  position: relative;
  z-index: 1;
}

.row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 14px;
  width: 100%;
  text-align: left;
  background: none;
  /* A repeated hairline down a long list reads very differently from a one-off section divider —
     stacked dozens of times, var(--border-strong) looked like a grid of boxes, not a plain list. */
  border-bottom: 1px solid var(--border);
}

.row:last-child {
  border-bottom: none;
}

/* The POS cart row the keyboard shortcuts (+/-/Delete/Enter) currently act on — a left accent
   bar rather than a full background tint, so it still reads clearly against the badge/stock
   colours already competing for attention in a cart row. */
.row.is-selected {
  background: var(--brand-soft);
  box-shadow: inset 3px 0 0 var(--brand);
}

/* ---------- POS sheet callout — a brand-tinted "how this works" note, reused above the Held
   sales list and the Daily Sales line list ---------- */

.pos-hint-callout {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 14px;
  padding: 10px 13px;
  border-radius: 12px;
  background: var(--brand-soft);
  color: var(--brand);
  font-size: 12.5px;
  font-weight: 600;
}

.pos-hint-callout svg {
  flex: 0 0 auto;
}

.pos-hint-callout kbd {
  font-family: monospace;
  font-weight: 800;
}

/* ---------- Cart keyboard-shortcut remark ---------- */

/* A quiet caption, not a control — no box, no bordered pills, nothing that reads as clickable.
   Each key → action pair is still its own non-breaking unit so a narrow cart wraps between pairs
   (at the · separators) rather than splitting a label across two lines. */
.cart-shortcuts-hint {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  row-gap: 4px;
  margin: 2px 2px 10px;
  color: var(--txt3);
  font-size: 11.5px;
}

.cart-shortcuts-hint > svg {
  flex: 0 0 auto;
  margin-right: 8px;
  opacity: 0.5;
}

.cart-shortcuts-hint-item {
  white-space: nowrap;
}

.cart-shortcuts-hint-item:not(:last-child)::after {
  content: '·';
  margin: 0 8px;
  opacity: 0.5;
}

.cart-shortcuts-hint kbd {
  color: var(--txt2);
  font-family: monospace;
  font-weight: 700;
}

.row:active {
  background: var(--surface2);
}

.row-main {
  flex: 1;
  min-width: 0;
}

/* A small product-photo thumbnail in front of a list row — same soft brand-tinted placeholder
   treatment as the POS grid tile's, so an item with no photo yet still reads as intentional. */
.item-thumb {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: var(--r-sm);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--brand) 12%, var(--surface2));
  color: color-mix(in srgb, var(--brand) 45%, var(--txt3));
}

.item-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The step chips hold their place while the counted list scrolls under them. With twenty items
   picked, the chip you want otherwise sits several screens above the row you are keying, and
   changing step means scrolling up and back down for every change of pack size.

   Same glass treatment as .actionbar, the sheet's other sticky strip, and pulled out to the
   sheet body's own edges so no row slides past it at the sides. */
.stock-step-chips {
  position: sticky;
  top: 0;
  z-index: 3;
  margin: 0 -16px 10px;
  padding: 8px 16px;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--glass-border);
}

/* The pick marker on a stock-take row. Always drawn, so the row never changes height as it is
   picked and unpicked — an empty ring reads as "you can choose this" where an icon appearing
   from nowhere just shifts everything under it. */
.stock-take-tick {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 1.5px solid var(--border);
  color: transparent;
  transition:
    background 0.15s ease,
    border-color 0.15s ease,
    color 0.15s ease;
}

.stock-take-tick.is-on {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-contrast);
}

.stock-take-tick svg {
  display: block;
}

/* The mini barcode glyph next to an item's name in the list row — on a narrow phone it was taking
   enough width to crush the name itself down to a sliver ("10ft…", "Air Fr…"). Shrinking it there
   instead of hiding it keeps "this item has a barcode" visible at a glance, just smaller than the
   name actually matters more than it does. BarcodeGlyph gives its svg a matching viewBox so this
   scales the whole glyph proportionally rather than clipping it. */
@media (max-width: 480px) {
  .row-barcode-glyph svg {
    width: 34px;
    height: auto;
  }
}

/* Tappable part of a row that also carries a trailing action button. */
.row-tap {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0;
  background: none;
  text-align: left;
}

.row-title {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row-sub {
  font-size: 12.5px;
  color: var(--txt2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row-end {
  text-align: right;
  flex: 0 0 auto;
}

/* Date range for the list: field first, then from/to. Wraps rather than shrinking the inputs —
   a squeezed native date input becomes unreadable long before it becomes unusable. */
.date-filter {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 10px;
}

.date-filter-field {
  flex: 0 0 auto;
  width: auto;
  padding: 8px 10px;
  font-size: 12.5px;
  font-weight: 600;
}

.date-filter-input {
  flex: 1 1 128px;
  min-width: 128px;
  padding: 8px 10px;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
}

.date-filter-sep {
  flex: 0 0 auto;
  color: var(--txt3);
  font-size: 12px;
}

.date-filter-clear {
  flex: 0 0 auto;
}

/* The pair of figures at the top of the invoice list. One card, split — they are two halves of the
   same money, and two separate cards made them read as unrelated. */
.kpi-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  margin-bottom: 10px;
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.kpi {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 14px;
  min-width: 0;
}

/* A hairline between the halves rather than a gap: the divider says "same card, two sides". */
.kpi + .kpi {
  border-left: 1px solid var(--border);
}

.kpi-label {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--txt3);
}

.kpi-value {
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
}

.kpi-value.is-due {
  color: var(--danger);
}

.kpi-value.is-in {
  color: var(--ok);
}

.kpi-sub {
  font-size: 11.5px;
  color: var(--txt3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Spans both halves, flush to the bottom edge of the card. */
.kpi-meter {
  grid-column: 1 / -1;
  height: 4px;
  background: var(--danger-soft);
}

.kpi-meter-fill {
  display: block;
  height: 100%;
  background: var(--ok);
  transition: width 0.35s ease;
}

@media (prefers-reduced-motion: reduce) {
  .kpi-meter-fill {
    transition: none;
  }
}

/* One-line total for a filtered list: a quiet strip, not a card — the cards belong to All, where
   there are two numbers worth comparing. */
.list-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
  padding: 8px 14px;
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface2);
  font-size: 12.5px;
  color: var(--txt2);
}

.list-total strong {
  font-size: 15px;
  font-weight: 800;
  color: var(--txt1);
  font-variant-numeric: tabular-nums;
}

.list-total strong.is-paid {
  color: var(--ok);
}

/* Small caps for the due label, so "DUE" reads as a tag rather than a shout. */
.row-due {
  letter-spacing: 0.02em;
}

/* Context for a part-paid invoice: the balance leads, the original total follows quietly. */
.row-of {
  font-size: 11px;
  color: var(--txt3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Deliberately not .row-sub: that truncates with an ellipsis, which is right for a reference
   number and wrong for a date the seller is chasing. */
.row-due {
  margin-top: 1px;
  font-size: 12px;
  font-weight: 600;
  color: var(--txt2);
  white-space: nowrap;
}

.row-due.is-late {
  color: var(--danger);
}

.row-amount {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.avatar {
  width: 40px;
  height: 40px;
  /* A squircle, not a full circle — matches the logo image branch below it, so swapping between
     "has a logo" and "initials fallback" never flips the bubble's shape. */
  border-radius: 22%;
  display: grid;
  place-items: center;
  font-weight: 700;
  color: var(--brand-contrast);
  background: var(--brand);
  flex: 0 0 auto;
}

.avatar.rose {
  background: var(--brand-soft);
  color: var(--brand-ink);
  border: 1px solid var(--border-strong);
}

.avatar.gold {
  background: var(--gold-soft);
  color: var(--gold);
  border: 1px solid var(--border-strong);
}

.deleted .row-title {
  text-decoration: line-through;
  color: var(--txt2);
}

/* ---------- Badges & chips ---------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 3px 8px;
  border-radius: 999px;
  white-space: nowrap;
}

.badge-paid {
  background: var(--ok-soft);
  color: var(--ok);
}

/* Money is owed, but the due date has not passed — red, held quietly. */
.badge-unpaid {
  background: var(--danger-soft);
  color: var(--danger);
}

/* Something has been paid: amber says "in progress", not "in trouble". Distinguishing it from
   unpaid at a glance is the whole point of the colour. */
.badge-partial {
  background: var(--warn-soft);
  color: var(--warn);
}

/* The one status that needs chasing today, so it is the only solid badge in the list: filled red
   with a soft red glow, which carries across a screen of tinted pills where another pale badge
   would not. */
.badge-overdue {
  background: var(--danger);
  color: #fff;
  box-shadow: 0 2px 8px color-mix(in srgb, var(--danger) 45%, transparent);
}

.badge-draft {
  background: var(--surface3);
  color: var(--txt2);
}

.badge-brand {
  background: var(--brand-soft);
  color: var(--brand-ink);
}

/* An elegant, higher-contrast pill for an active promo — gold-to-rose, deliberately warmer than
 * the flat status badges so it reads as "special" rather than just another status label. */
.badge-promo {
  background: linear-gradient(135deg, var(--gold), var(--brand));
  color: var(--brand-contrast);
}

/* "Active" (no end date set, so there's no countdown to show) gets its own success-toned
 * gradient — distinct from the gold-to-rose "X days left" pill above, so the two read as
 * different kinds of information at a glance rather than the same badge with different text. */
.badge-active {
  background: linear-gradient(135deg, var(--ok), color-mix(in srgb, var(--ok), black 25%));
  color: #ffffff;
}

/* ---------- Dashboard "Active promos" carousel ---------- */

.promo-strip {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  padding: 2px 2px 10px;
  margin-bottom: 14px;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
}

.promo-strip-card {
  flex: 0 0 auto;
  width: 220px;
  /* A <button>'s native padding is never reset elsewhere in this file — without zeroing it here,
     the browser default pushed .promo-strip-art in from every edge, exposing the card's own
     background as a visible margin around the banner image. */
  padding: 0;
  display: block;
  scroll-snap-align: start;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  box-shadow: var(--shadow-sm), var(--glass-ring);
  text-align: left;
  cursor: pointer;
  transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}
@media (hover: hover) {
  .promo-strip-card:hover {
    transform: translateY(-2px);
    border-color: color-mix(in srgb, var(--brand) 30%, var(--glass-border));
    box-shadow: var(--shadow-md), var(--glass-ring);
  }
}

/* Rounds its own top corners to nest inside .promo-strip-card's 1px border. The card uses
   box-sizing: border-box, so this element's content box sits 1px inside the card's border —
   using the SAME radius as the card leaves this corner's curve drawn tighter than the border's,
   exposing a sliver of the blurred page background right at the seam. Subtracting the border
   width from the radius makes the two curves nest exactly, with no gap. */
.promo-strip-art {
  height: 96px;
  border-radius: calc(var(--r-lg) - 1px) calc(var(--r-lg) - 1px) 0 0;
  overflow: hidden;
  position: relative;
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--brand-contrast);
}

/* No banner uploaded yet — a flat two-colour gradient with one small icon floating in the middle
   read as a missing/broken image rather than finished brand art. Layers in a diagonal hairline
   texture, a soft highlight for depth, and sits the icon inside its own medallion badge instead
   of floating alone, so a bannerless promo still looks deliberately designed. */
.promo-strip-art-fallback {
  background-image:
    repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.16) 0 2px, transparent 2px 22px),
    linear-gradient(135deg, var(--gold), var(--brand));
}

.promo-strip-art-fallback::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 28% 22%, rgba(255, 255, 255, 0.4), transparent 55%);
}

/* A big, cropped-off-the-edge icon as background art, rendered behind the badge — a small centred
   icon alone still read as "an icon with nothing around it" rather than designed artwork. Tucked
   into the corner and oversized so only a slice of it shows past the card's own clipped edge. */
.promo-strip-art-deco {
  position: absolute;
  right: -30px;
  bottom: -38px;
  color: rgba(255, 255, 255, 0.18);
  transform: rotate(-18deg);
}

.promo-strip-art-badge {
  position: relative;
  width: 72px;
  height: 72px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.22);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35);
}

.promo-strip-body {
  padding: 12px 14px 14px;
}

.promo-strip-name {
  font-weight: 750;
  font-size: 14px;
  color: var(--txt1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.promo-strip-mechanics {
  margin-top: 4px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--txt2);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.badge-deleted {
  background: var(--danger-soft);
  color: var(--danger);
}

/* Arrows sit beside the row, not over it. An overlay needed a gradient to fade the chips out
   underneath, and that gradient read as a dirty smudge across whichever chip it covered. */
/* ---------- Landing page ---------- */

/* Sits on the warmer sign-in background rather than the app's, so a stranger's first screen
   feels like a product page instead of a tool that is already open. */
.landing {
  min-height: 100dvh;
  background:
    radial-gradient(90% 55% at 50% -10%, var(--login-glow-1), transparent 70%),
    radial-gradient(70% 45% at 10% 105%, var(--login-glow-3), transparent 70%),
    radial-gradient(60% 40% at 95% 40%, var(--login-glow-2), transparent 70%),
    var(--login-base);
  padding: calc(28px + var(--safe-top)) calc(16px + var(--safe-right)) calc(28px + var(--safe-bottom))
    calc(16px + var(--safe-left));
}

.landing-hero,
.landing-section,
.landing-footer {
  max-width: 760px;
  margin: 0 auto;
}

.landing-footer {
  margin-top: 48px;
  padding: 32px 18px 28px;
  /* A soft fade-in rather than a hard rule — the page ends, it doesn't just get cut off. */
  border-top: 1px solid transparent;
  background: linear-gradient(var(--border), var(--border)) top / 100% 1px no-repeat;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  text-align: center;
}

.landing-footer-brand {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 22px;
  font-weight: 700;
  color: var(--txt1);
}

.landing-footer-brand img {
  width: 40px;
  height: 40px;
  border-radius: 11px;
  box-shadow: var(--shadow-sm);
}

/* A short brand-colour accent under the lockup — the one deliberately "designed" touch in an
   otherwise plain, text-heavy footer. */
.landing-footer-rule {
  width: 36px;
  height: 3px;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--brand), var(--gold));
  opacity: 0.75;
}

.landing-footer-links {
  display: flex;
  align-items: center;
  gap: 24px;
}

.landing-footer-link {
  padding: 0;
  border: none;
  background: none;
  color: var(--txt2);
  font-size: 13px;
  font-weight: 600;
  border-bottom: 1px solid transparent;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.landing-footer-link:hover {
  color: var(--brand);
  border-bottom-color: var(--brand);
}

.landing-footer-meta {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 9px;
  color: var(--txt3);
  font-size: 12px;
}

.landing-footer-dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--txt3);
  flex: 0 0 auto;
}

.landing-footer-version {
  padding: 2px 9px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--txt3);
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.landing-footer-version:hover {
  color: var(--brand);
  border-color: var(--brand);
}

/* At this width the 3-up link row no longer fits one line, so each button's own text wraps
   instead — a ragged, uneven look. A bordered list card, one link per row, reads as deliberate
   rather than cramped, and gives each link a full-width tap target. */
@media (max-width: 480px) {
  .landing-footer {
    padding: 36px 20px 30px;
    gap: 22px;
  }

  .landing-footer-brand {
    font-size: 24px;
    gap: 14px;
  }

  .landing-footer-brand img {
    width: 48px;
    height: 48px;
    border-radius: 13px;
  }

  .landing-footer-links {
    flex-direction: column;
    gap: 0;
    width: 100%;
    max-width: 300px;
    border-radius: var(--r-lg);
    overflow: hidden;
    border: 1px solid var(--border);
    /* --surface reads darker than this page's own gradient background in the dark palettes — a
       "raised" card needs to be lighter than the page behind it, not darker. */
    background: var(--surface3);
    box-shadow: var(--shadow-sm);
  }

  .landing-footer-link {
    width: 100%;
    padding: 15px 18px;
    font-size: 14.5px;
    border-bottom: 1px solid var(--border);
  }
  .landing-footer-link:last-child {
    border-bottom: none;
  }
  .landing-footer-link:hover,
  .landing-footer-link:active {
    color: var(--brand);
    background: var(--surface2);
  }

  .landing-footer-meta {
    flex-wrap: wrap;
    justify-content: center;
    row-gap: 6px;
  }
}

/* Sticky glass bar. Negative margins pull it out of the page padding so the blur spans the full
   width, while the inner max-width keeps it aligned with the content. */
.landing-nav {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 10px;
  margin: calc(-28px - var(--safe-top)) calc(-16px - var(--safe-right)) 18px calc(-16px - var(--safe-left));
  padding: calc(8px + var(--safe-top)) calc(14px + var(--safe-right)) 8px calc(14px + var(--safe-left));
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--glass-border);
  /* A soft drop below the hairline so the bar reads as floating over the page rather than
     painted onto it. Tracks --brand so every palette gets its own tint, not just Rose. */
  box-shadow: 0 6px 18px -8px color-mix(in srgb, var(--brand) 35%, transparent);
}

:root[data-theme='dark'] .landing-nav {
  /* No brand tint registers on a dark background; depth has to come from shadow instead. */
  box-shadow: 0 8px 20px -10px rgba(0, 0, 0, 0.65);
}

.landing-nav-brand {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  padding: 0;
  border: none;
  background: none;
  font-size: 17px;
}

.landing-nav-brand img {
  /* Matches .landing-logo: a squircle, not a full circle — this icon's checkmark badge is
     anchored to its corner and looks off-balance once cropped into a perfect circle. */
  border-radius: 7px;
}

/* The script tagline next to the brand name has no room in the mobile bar — it was crowding the
   theme toggle and burger button. The hero below still shows it in full. */
@media (max-width: 640px) {
  .landing-nav-tagline {
    display: none;
  }
}

/* On mobile the five links don't fit, so they move into a burger dropdown (.landing-nav-menu)
   and this inline strip is hidden. It reappears as a single centred row from 640px up. */
.landing-nav-links {
  display: none;
  gap: 2px;
  flex: 1 1 auto;
  min-width: 0;
  justify-content: flex-end;
  margin-left: auto;
}

.landing-nav-brand {
  margin-right: auto;
}

/* Palette toggle, theme toggle and burger are the round icon-buttons on the right of the bar —
   same bordered-circle look as .iconbtn in the signed-in app, so the landing nav reads like the
   same product rather than a bare, unstyled icon bolted on. The burger is hidden once the inline
   links fit (640px). */
.landing-nav-theme,
.landing-nav-burger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  flex: 0 0 auto;
  border: 1px solid var(--border);
  background: var(--surface2);
  border-radius: 50%;
  color: var(--txt1);
  touch-action: manipulation;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.landing-nav-theme:hover,
.landing-nav-burger:hover {
  border-color: var(--brand);
}

.landing-nav-theme:active,
.landing-nav-burger:active {
  background: var(--surface3);
}

/* Dropdown of the section links, anchored under the bar on the right. */
.landing-nav-menu {
  position: absolute;
  top: 100%;
  right: calc(14px + var(--safe-right));
  margin-top: 6px;
  min-width: 210px;
  display: flex;
  flex-direction: column;
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  z-index: 31;
  animation: fabPop 0.18s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.landing-nav-menu .landing-nav-link {
  width: 100%;
  justify-content: flex-start;
  text-align: left;
  padding: 11px 12px;
  font-size: 14px;
}

.landing-nav-palette-link {
  display: flex;
  align-items: center;
  gap: 8px;
}

.landing-nav-menu .btn {
  width: 100%;
}

.landing-nav-menu-divider {
  height: 1px;
  background: var(--border);
  margin: 6px 4px 8px;
}

/* Catches a tap outside the open dropdown to close it. Sits under the sticky bar and its menu. */
.landing-nav-scrim {
  position: fixed;
  inset: 0;
  z-index: 29;
  border: none;
  background: transparent;
}

@media (min-width: 640px) {
  .landing-nav-links {
    display: flex;
    flex-wrap: wrap;
  }
  .landing-nav-burger {
    display: none;
  }
}

.landing-nav-link {
  flex: 0 0 auto;
  padding: 7px 10px;
  border: none;
  border-radius: 999px;
  background: none;
  color: var(--txt2);
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  /* No double-tap zoom delay, and the tap is never interpreted as the start of a pan — which is
     what let a press slide onto the neighbouring link. */
  touch-action: manipulation;
  min-height: 34px;
}

.landing-nav-link.active {
  background: var(--brand-soft);
  color: var(--brand);
}

/* The inline CTA rides with the inline links: hidden on mobile (it lives in the burger menu
   instead), shown from 640px up. Scoped to .landing-nav so it beats .btn's display:inline-flex. */
.landing-nav .landing-nav-cta {
  display: none;
  flex: 0 0 auto;
}

@media (min-width: 640px) {
  .landing-nav .landing-nav-cta {
    display: inline-flex;
  }
}

/* Same reasoning as the CTA above: hidden until the inline links (and the room they imply) show
   up, with the "Colour palette" row in the burger menu covering it below that. */
.landing-nav-palette {
  display: none;
}

@media (min-width: 640px) {
  .landing-nav-palette {
    display: inline-flex;
  }
}

/* A pill with its own gradient and glow instead of the app's flat, square-ish .btn-primary — this
   is the one button on the whole marketing page actually asking for a click, so it earns a touch
   more polish than the rest of .btn-sm. */
.landing-nav-cta {
  border-radius: 999px;
  padding: 9px 20px;
  background: linear-gradient(135deg, var(--brand), var(--brand-ink));
  box-shadow: 0 6px 16px -4px color-mix(in srgb, var(--brand) 55%, transparent);
  letter-spacing: 0.01em;
  transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.18s ease;
}

.landing-nav-cta:hover {
  transform: translateY(-1px);
  box-shadow: 0 10px 24px -4px color-mix(in srgb, var(--brand) 60%, transparent);
}

.landing-nav-cta:active {
  transform: translateY(0) scale(0.97);
  box-shadow: 0 4px 12px -4px color-mix(in srgb, var(--brand) 55%, transparent);
}

/* Desktop: brand hugs the left, theme + CTA hug the right, and the links sit dead-centre in the
   bar (absolute so their position ignores the differing widths of brand vs actions). */
@media (min-width: 900px) {
  .landing-nav-links {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    justify-content: center;
    flex: 0 0 auto;
    flex-wrap: nowrap;
    margin: 0;
  }
}

/* Clears the sticky bar when a link jumps to a section. */
.landing-section,
.landing-hero {
  scroll-margin-top: 64px;
}

.landing-hero {
  text-align: center;
  padding: 8px 0 30px;
}

/* A soft brand glow breathing behind the logo — the one continuous motion on the page, so the
   very first thing a visitor sees feels alive rather than a static screenshot. */
.landing-logo-wrap {
  position: relative;
  display: inline-flex;
  width: 92px;
  height: 92px;
}

.landing-logo-wrap::before {
  content: '';
  position: absolute;
  inset: -24px;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--brand) 38%, transparent), transparent 72%);
  animation: heroGlowPulse 3.4s ease-in-out infinite;
}

.landing-logo {
  position: relative;
  width: 92px;
  height: 92px;
  /* A squircle, not a full circle — the icon's checkmark badge is anchored to its corner, and a
     hard circular crop throws it off-balance against the glow behind it. */
  border-radius: 22px;
  object-fit: cover;
  box-shadow: var(--shadow-md);
}

/* The hero's own content cascades in once, on load — not scroll-triggered, since it is the first
   thing on the page and already in view. */
.hero-in {
  opacity: 0;
  animation: heroRise 0.85s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.hero-delay-0 { animation-delay: 0.05s; }
.hero-delay-1 { animation-delay: 0.2s; }
.hero-delay-2 { animation-delay: 0.34s; }
.hero-delay-3 { animation-delay: 0.48s; }
.hero-delay-4 { animation-delay: 0.6s; }

@keyframes heroRise {
  from {
    opacity: 0;
    transform: translateY(18px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes heroGlowPulse {
  0%,
  100% {
    transform: scale(0.92);
    opacity: 0.55;
  }
  50% {
    transform: scale(1.08);
    opacity: 0.95;
  }
}

/* Cards and the phone mockups fade up into place as they scroll into view, a little later per
   item in the same row so the group arrives as a gentle cascade rather than all at once. */
.reveal {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .hero-in,
  .reveal {
    animation: none !important;
    transition: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .landing-logo-wrap::before {
    animation: none;
  }
}

.landing-wordmark {
  font-size: 34px;
  margin: 14px 0 0;
  line-height: 1.1;
}

/* On a narrow phone, "QuickResibo Sell," was wrapping mid-tagline — "Sell," stranded next to the
   brand name and "Track, Grow" alone on the next line. Forcing the tagline onto its own line keeps
   it together as one phrase instead of splitting wherever the width happens to run out. */
@media (max-width: 480px) {
  .landing-wordmark-tagline {
    display: block;
  }
}

.landing-lead {
  margin: 12px auto 0;
  max-width: 30ch;
  font-size: 15.5px;
  line-height: 1.55;
  color: var(--txt2);
}

.landing-cta {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 22px auto 0;
  max-width: 340px;
}

.btn-lg {
  padding: 14px 18px;
  font-size: 15px;
  border-radius: var(--r-lg);
}

.landing-note {
  margin: 14px 0 0;
  font-size: 12px;
  color: var(--txt3);
}

/* A slim trust strip in place of the customer logos/quotes we don't have yet — a badge plus one
   honest line, not a heavy section. */
.landing-earlyaccess {
  max-width: 560px;
  margin: 4px auto 30px;
  padding: 14px 18px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  text-align: center;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm), var(--glass-ring);
}

.landing-earlyaccess-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--brand-soft);
  color: var(--brand-ink);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.landing-earlyaccess p {
  margin: 0;
  max-width: 44ch;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--txt2);
}

@media (min-width: 640px) {
  .landing-earlyaccess {
    flex-direction: row;
    text-align: left;
    gap: 14px;
  }
}

/* The final section must be tall enough to scroll its own top under the nav, otherwise the page
   runs out of scroll first and the section before it can never become current. */
.landing-section:last-of-type {
  min-height: calc(100vh - 180px);
}

.landing-section {
  padding: 26px 0 6px;
}

.landing-h2 {
  margin: 0 0 6px;
  font-size: 20px;
  line-height: 1.25;
  text-align: center;
}

.landing-sub {
  margin: 0 auto 18px;
  max-width: 40ch;
  text-align: center;
  font-size: 13.5px;
  color: var(--txt2);
}

.landing-grid {
  display: grid;
  gap: 12px;
  margin-top: 16px;
}

@media (min-width: 620px) {
  .landing-grid {
    grid-template-columns: 1fr 1fr;
  }
}

.landing-card {
  padding: 16px;
  border-radius: var(--r-xl);
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--shadow-sm);
}

.landing-icon {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 12px;
  background: var(--brand-soft);
  color: var(--brand);
  margin-bottom: 10px;
}

.landing-card-title {
  margin: 0 0 4px;
  font-size: 15px;
  font-weight: 700;
}

.landing-card-body {
  margin: 0;
  font-size: 13px;
  line-height: 1.55;
  color: var(--txt2);
}

/* What is on the invoice is the point of this screen, so the list owns a rose edge while the two
   add buttons below it stay quiet. They were reading as three cards of equal weight, which made
   the actual contents hard to pick out. */
.line-list {
  border-color: color-mix(in srgb, var(--brand) 55%, var(--border));
  box-shadow: var(--shadow-sm), 0 0 0 3px color-mix(in srgb, var(--brand) 12%, transparent);
}

/*
  An invoice line reads as a thing, not as a table row: a quantity chip leads it, the name carries
  the weight, and the amount closes it. Restrained on purpose — this list is read constantly.

  Laid out as flat flex children reordered by CSS alone, not as two different JSX structures: one
  line on desktop, where there's room for the name, the stepper and the total side by side, two
  lines on a phone, where all of that together squeezed the name down to a single letter. The
  .line-break span is the hinge — a forced wrap point that only exists below 620px.
*/
.line-row {
  flex-wrap: wrap;
  row-gap: 8px;
  column-gap: 10px;
}

.line-row .row-main {
  order: 1;
  flex: 1 1 auto;
  min-width: 0;
}

.line-row .line-delete {
  order: 2;
  flex: 0 0 auto;
}

.line-row .line-break {
  order: 3;
  flex-basis: 100%;
  width: 0;
  height: 0;
}

.line-row .line-stepper {
  order: 4;
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
}

.line-row .row-amount {
  order: 5;
  flex: 0 0 auto;
  margin-left: auto;
}

@media (min-width: 620px) {
  .line-row .line-break {
    display: none;
  }

  .line-row .line-stepper {
    order: 1;
  }

  .line-row .row-main {
    order: 2;
  }

  .line-row .row-amount {
    order: 3;
    margin-left: 0;
  }

  .line-row .line-delete {
    order: 4;
  }
}

/* Smaller than the default 40px circle — this row already carries a lot (name, stepper, price,
   delete), and the base .iconbtn styling (background, border, active/disabled state) applies here
   same as everywhere else. */
.line-row .iconbtn {
  width: 32px;
  height: 32px;
}

.line-row .line-delete {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 30%, var(--border));
}

.line-qty {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: baseline;
  gap: 1px;
  min-width: 34px;
  justify-content: center;
  padding: 5px 7px;
  border-radius: var(--r-sm);
  background: var(--brand-soft);
  color: var(--brand-ink);
  font-size: 13px;
  font-weight: 750;
  font-variant-numeric: tabular-nums;
}

.line-qty-x {
  font-size: 10.5px;
  font-weight: 600;
  opacity: 0.7;
}

.line-row .row-title {
  font-weight: 650;
}

.line-row .row-amount {
  font-size: 15px;
}

/* A hairline rule between lines, not a boxed row each: the list stays one block of information. */
.line-row + .line-row {
  border-top: 1px solid var(--border);
}

/* ---------- Adding an invoice line ---------- */

.add-line {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 10px;
}

.add-line-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 12px;
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  /* Transparent, not a surface: these are controls under the list, not more content in it. */
  background: transparent;
  text-align: left;
  min-width: 0;
}

/* Dashed for the one that invents something new — the same language a blank slot uses elsewhere.
   A stronger line than the solid card next to it: at the same weight the dashes read as a faded
   edge rather than a deliberate one. */
.add-line-btn.is-custom {
  border-style: dashed;
  border-color: var(--border-strong);
  background: transparent;
}

.add-line-btn:active {
  border-color: var(--brand);
  background: var(--brand-soft);
}

.add-line-mark {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--brand-soft);
  color: var(--brand-ink);
}

.add-line-btn.is-custom .add-line-mark {
  background: var(--surface3);
  color: var(--txt2);
}

.add-line-text {
  display: grid;
  min-width: 0;
}

.add-line-title {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--txt1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.add-line-sub {
  font-size: 11.5px;
  color: var(--txt3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Below this the two labels start truncating, so they stack instead. */
@media (max-width: 359px) {
  .add-line {
    grid-template-columns: 1fr;
  }
}

/* Shown when a name is already in the catalogue: an offer, not a rejection. */
.clash {
  margin-bottom: 12px;
  padding: 12px 14px;
  border-radius: var(--r-md);
  border: 1px solid var(--brand);
  background: var(--brand-soft);
}

.clash-title {
  font-weight: 700;
  color: var(--brand-ink);
}

.clash-body {
  margin: 4px 0 10px;
  font-size: 12.5px;
  color: var(--txt2);
}

.clash-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

/* Shortcut row above the cost box — small enough to read as helpers, not as the main control. */
.cost-quick {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-bottom: 6px;
}

.cost-quick-btn {
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--txt2);
  font-size: 11.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The selected state comes from the value, not from the tap: an :active style lingers after a
   touch on mobile, so whichever button was pressed last looked permanently chosen. */
.cost-quick-btn[aria-pressed='true'] {
  background: var(--brand-soft);
  border-color: var(--brand);
  color: var(--brand-ink);
}

.cost-quick-btn:disabled {
  opacity: 0.45;
}

/* ---------- Guided tour ---------- */

.tour {
  position: fixed;
  inset: 0;
  z-index: 90;
}

/* Four panels around the lit element rather than one overlay with a punched hole: box-shadow
   spills are unreliable across browsers, and this leaves the highlighted control genuinely
   untouched underneath. */
.tour-shade {
  position: fixed;
  background: rgba(18, 10, 14, 0.62);
  animation: fadeIn 0.18s ease;
}

.tour-ring {
  position: fixed;
  border-radius: var(--r-md);
  border: 2px solid var(--brand);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--brand) 28%, transparent);
  pointer-events: none;
  animation: fadeIn 0.18s ease;
}

.tour-card {
  position: fixed;
  max-width: 460px;
  margin: 0 auto;
  padding: 16px;
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
  animation: slideUp 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}

.tour-step-count {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--brand);
}

.tour-title {
  margin-top: 4px;
  font-size: 16px;
  font-weight: 750;
  text-wrap: balance;
}

.tour-body {
  margin: 6px 0 14px;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--txt2);
}

.tour-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.tour-actions-end {
  display: flex;
  gap: 8px;
}

@media (prefers-reduced-motion: reduce) {
  .tour-shade,
  .tour-ring,
  .tour-card {
    animation: none;
  }
}

/* ---------- Slide-to-fit captcha ---------- */

.puzzle {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface3);
  /* The image is 640x360, and the box keeps that ratio at any width so the piece lands true. */
  aspect-ratio: 16 / 9;
  touch-action: pan-y;
}

.puzzle-bg,
.puzzle-piece {
  display: block;
  user-select: none;
  -webkit-user-drag: none;
}

.puzzle-bg {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Positioned from the left and moved by the slider alone: the answer is a distance, never a
   coordinate the page knows. */
.puzzle-piece {
  position: absolute;
  left: 0;
  filter: drop-shadow(0 2px 6px rgba(30, 16, 22, 0.45));
  will-change: transform;
}

.puzzle-piece.is-dragging {
  filter: drop-shadow(0 4px 10px rgba(30, 16, 22, 0.55));
}

.puzzle-loading {
  width: 100%;
  height: 100%;
  background: linear-gradient(100deg, var(--surface3) 30%, var(--surface2) 50%, var(--surface3) 70%);
  background-size: 200% 100%;
  animation: shimmer 1.2s linear infinite;
}

.puzzle-track {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 46px;
  margin-top: 8px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface2);
  overflow: hidden;
  touch-action: pan-y;
}

.puzzle-track:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.puzzle-track.is-done {
  border-color: var(--ok);
  background: var(--ok-soft);
}

.puzzle-track.is-wrong {
  border-color: var(--danger);
  background: var(--danger-soft);
}

.puzzle-track-hint {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  color: var(--txt3);
  pointer-events: none;
}

.puzzle-track.is-done .puzzle-track-hint {
  color: var(--ok);
  font-weight: 600;
}

.puzzle-track.is-wrong .puzzle-track-hint {
  color: var(--danger);
  font-weight: 600;
}

/* A short shake on a miss: the piece is where you left it, so movement is what says "not there". */
.puzzle-piece.is-wrong {
  animation: puzzle-miss 0.35s ease;
}

@keyframes puzzle-miss {
  0%, 100% { margin-left: 0; }
  25% { margin-left: -5px; }
  75% { margin-left: 5px; }
}

@media (prefers-reduced-motion: reduce) {
  .puzzle-piece.is-wrong {
    animation: none;
  }
}

/* The handle is the only draggable thing; the piece above follows it. */
.puzzle-handle {
  position: absolute;
  left: 0;
  top: 0;
  display: grid;
  place-items: center;
  width: 52px;
  height: 100%;
  border: none;
  border-radius: 999px;
  background: var(--brand);
  color: var(--brand-contrast);
  cursor: grab;
  will-change: transform;
}

.puzzle-handle:active {
  cursor: grabbing;
}

.puzzle-handle:disabled {
  background: var(--ok);
  cursor: default;
}

@media (prefers-reduced-motion: reduce) {
  .puzzle-loading {
    animation: none;
  }
}

/* ---------- Paying for a plan ---------- */

/* The figure to send, stated once and unmissably: getting this wrong wastes a transfer. */
.pay-amount {
  display: grid;
  gap: 2px;
  justify-items: center;
  text-align: center;
  padding: 14px 12px;
  border-radius: var(--r-lg);
  background: var(--brand-soft);
  border: 1px solid var(--border);
}

.pay-amount strong {
  font-size: 28px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--brand-ink);
}

/* Separates the two ways to pay. A plain heading read as a section; a ruled "or" reads as a
   choice between equals, which is what it is. */
/* The second way to pay, when a gateway is also on offer. An outline button read as disabled
   next to the filled one; this is tinted and bordered in the brand so it is unmistakably a
   real action, while still yielding to the primary. */
.btn-soft {
  background: var(--brand-soft);
  border: 1px solid color-mix(in srgb, var(--brand) 32%, transparent);
  color: var(--brand-ink);
  font-weight: 700;
}

.btn-soft:active {
  background: color-mix(in srgb, var(--brand) 18%, var(--brand-soft));
}

/* The tick sits in its own disc so the button reads as "confirm what you did", not "submit". */
.btn-soft .btn-mark {
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  margin-right: 2px;
  border-radius: 999px;
  background: var(--brand);
  color: var(--brand-contrast);
}

.pay-or {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 14px 0 2px;
  color: var(--txt3);
  font-size: 12px;
}

.pay-or::before,
.pay-or::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}

.pay-card {
  margin-top: 12px;
}

.pay-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}

.pay-qr {
  display: grid;
  justify-items: center;
  gap: 6px;
  margin-bottom: 12px;
}

/* White ground: a QR with a transparent background cannot be scanned on a dark theme.
   Capped by height as well as width — owners upload a full phone screenshot as often as a cropped
   code, and an uncapped tall image pushes the account number and the button off the screen. */
.pay-qr img {
  width: auto;
  max-width: 100%;
  max-height: 230px;
  object-fit: contain;
  border-radius: var(--r-md);
  background: #fff;
  padding: 8px;
  border: 1px solid var(--border);
}

/* An inline action inside running text — "copy" beside a number. */
.linkbtn {
  border: none;
  background: none;
  padding: 0 0 0 6px;
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  color: var(--brand);
  text-decoration: underline;
}

/* Tap target for the receipt, showing the chosen image in place rather than a file name. */
.upload-drop {
  display: grid;
  place-items: center;
  min-height: 120px;
  padding: 10px;
  border-radius: var(--r-md);
  border: 1px dashed var(--border);
  background: var(--surface3);
  cursor: pointer;
}

.upload-drop img {
  max-height: 220px;
  max-width: 100%;
  border-radius: var(--r-sm);
}

/* ---------- Subscription payments (admin) ---------- */

/* Two buttons of equal weight, side by side — approve/reject, back/confirm. */
.two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.channel-actions {
  display: flex;
  align-items: center;
  gap: 4px;
}

/* The QR thumbnail stands in for an avatar on a channel row. White ground because a QR with a
   transparent background is unreadable against the dark theme. */
.channel-qr {
  flex: 0 0 auto;
  width: 42px;
  height: 42px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: #fff;
  object-fit: contain;
  padding: 2px;
}

.channel-qr-empty {
  display: grid;
  place-items: center;
  background: var(--surface3);
  color: var(--txt3);
}

/* The receipt as sent, capped so a tall phone screenshot cannot push the buttons out of reach —
   it opens full size in a new tab. */
.receipt-frame {
  display: block;
  max-height: 320px;
  overflow: hidden;
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface3);
}

.receipt-frame img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: contain;
}

/* ---------- Landing device preview ---------- */

/* Built from markup rather than screenshots: it follows the live theme and branding, and it can
   never drift out of date the way a captured PNG does. */
.devices {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  justify-content: center;
  margin-top: 18px;
}

/* Above mobile width, an odd device count otherwise wraps a lone phone onto its own row (three
   across, then one) — a fixed 2-column grid keeps every row balanced instead. */
@media (min-width: 620px) {
  .devices {
    display: grid;
    grid-template-columns: repeat(2, auto);
    justify-content: center;
  }
}

.device {
  position: relative;
  width: 216px;
  margin: 0;
  padding: 9px;
  background: linear-gradient(160deg, #3a2b32, #241a1f);
  box-shadow: 0 18px 40px rgba(32, 18, 24, 0.28);
}

.device.android {
  border-radius: 26px;
}

.device.iphone {
  border-radius: 34px;
  padding: 10px;
}

/* Camera cut-outs — the quickest way to say "Android" and "iPhone" without naming them. */
.device-punch {
  position: absolute;
  top: 16px;
  left: 50%;
  width: 6px;
  height: 6px;
  margin-left: -3px;
  border-radius: 50%;
  background: #0d0a0b;
  z-index: 2;
}

.device-notch {
  position: absolute;
  top: 15px;
  left: 50%;
  width: 62px;
  height: 15px;
  margin-left: -31px;
  border-radius: 999px;
  background: #0d0a0b;
  z-index: 2;
}

.device-screen {
  position: relative;
  height: 380px;
  overflow: hidden;
  padding: 24px 10px 10px;
  border-radius: 20px;
  background: var(--bg);
  color: var(--txt1);
}

.device.iphone .device-screen {
  border-radius: 26px;
}

.device-caption {
  margin-top: 10px;
  text-align: center;
  font-size: 11.5px;
  color: var(--txt3);
}

/* On a phone the mockups sit one per row with width going spare, and at 216px the figures inside
   were smaller than in the real app they are meant to be showing.
   `zoom` rather than a bigger width: it scales the whole frame — screen, rows, type, caption — in
   proportion, where widening alone would only stretch the frame and leave the contents the same
   size against a taller empty screen. Browsers without zoom simply keep the current size. */
@media (max-width: 620px) {
  .devices {
    gap: 22px;
  }

  .device {
    zoom: 1.4;
  }
}

/* The narrowest phones cannot take the full step without the frame touching the page edges. */
@media (max-width: 359px) {
  .device {
    zoom: 1.2;
  }
}

.mini-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding-bottom: 8px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--border-strong);
}

.mini-burger {
  width: 12px;
  height: 8px;
  border-top: 1.5px solid var(--txt2);
  border-bottom: 1.5px solid var(--txt2);
}

.mini-title {
  font-size: 13px;
  font-weight: 700;
  margin: 0 auto;
}

.mini-title.script {
  font-family: var(--font-script), cursive;
  font-size: 16px;
  color: var(--txt1);
}

.mini-stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  margin-bottom: 8px;
}

.mini-stat,
.mini-card {
  padding: 7px 8px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--surface);
}

/* The grid already makes both tiles the same height; this pushes the amount to the bottom of
   each so a label that wraps onto two lines cannot leave the figures at different heights. */
.mini-stat {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-height: 46px;
}

.mini-card {
  margin-bottom: 8px;
}

.mini-label {
  display: block;
  font-size: 7.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--txt3);
}

.mini-value {
  display: block;
  margin-top: auto;
  padding-top: 2px;
  font-size: 13px;
  font-weight: 800;
}

.mini-value.brand {
  color: var(--brand);
}

.mini-value.accent {
  color: var(--accent);
}

.mini-value.ok {
  color: var(--ok);
}

.mini-chips {
  display: flex;
  gap: 4px;
  margin-bottom: 8px;
}

.mini-chip {
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid var(--border);
  font-size: 8px;
  color: var(--txt2);
}

.mini-chip.on {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-contrast);
}

.mini-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 8px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--surface);
  margin-bottom: 6px;
}

.mini-row-title {
  font-size: 9.5px;
  font-weight: 700;
}

.mini-row-sub {
  font-size: 8px;
  color: var(--txt3);
}

.mini-row-end {
  text-align: right;
}

.mini-amount {
  font-size: 10px;
  font-weight: 800;
}

.mini-badge {
  display: inline-block;
  margin-top: 2px;
  padding: 1px 5px;
  border-radius: 999px;
  font-size: 7px;
  font-weight: 700;
}

.mini-badge.paid {
  background: var(--ok-soft);
  color: var(--ok);
}

.mini-badge.partial {
  background: var(--warn-soft);
  color: var(--warn);
}

.mini-badge.unpaid {
  background: var(--danger-soft);
  color: var(--danger);
}

.mini-fab {
  position: absolute;
  right: 14px;
  bottom: 14px;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--brand);
  color: var(--brand-contrast);
  font-size: 15px;
  font-weight: 700;
  box-shadow: var(--shadow-md);
}

.mini-amount.brand {
  color: var(--brand);
}

/* The POS mockup is the one device shown in landscape, so it gets its own wider frame rather
   than the portrait-phone .device base — it spans both grid columns so it doesn't force the
   phone mockups' column width up to match it. */
.device.tablet {
  grid-column: 1 / -1;
  width: min(460px, 100%);
  padding: 12px;
  border-radius: 22px;
}

/* Flex column so .mini-pos-split can claim exactly "whatever is left under the bar" via flex:1
   rather than height:100% of the screen — the latter stacked on top of the bar's own height
   and pushed the cart panel's Charge button past the bottom edge. */
.device.tablet .device-screen {
  height: 250px;
  padding: 14px;
  border-radius: 16px;
  display: flex;
  flex-direction: column;
}

.device.tablet .mini-bar {
  flex: 0 0 auto;
}

/* The phone frames scale up via zoom at mobile widths because their fixed 216px has room to
   spare; the tablet frame is already sized to fill the row, so the same zoom would push it
   past the viewport edge. */
@media (max-width: 620px) {
  .device.tablet {
    zoom: 1;
  }
}

.mini-pos-split {
  display: flex;
  gap: 10px;
  flex: 1;
  min-height: 0;
}

.mini-pos-products {
  flex: 1.4;
  min-width: 0;
}

.mini-pos-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
}

.mini-pos-tile {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 7px 8px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--surface);
}

.mini-pos-cart-panel {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  padding: 8px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--surface);
}

.mini-pos-cart-line {
  display: flex;
  justify-content: space-between;
  gap: 6px;
  margin-bottom: 5px;
}

.mini-pos-cart-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-top: auto;
  padding-top: 6px;
  border-top: 1px solid var(--border-strong);
  margin-bottom: 6px;
}

.mini-pos-charge {
  display: block;
  text-align: center;
  padding: 6px 0;
  border-radius: 8px;
  background: var(--brand);
  color: var(--brand-contrast);
  font-size: 9.5px;
  font-weight: 700;
}

.mini-pos-cart-fab {
  position: absolute;
  right: 14px;
  bottom: 14px;
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 6px 11px;
  border-radius: 999px;
  background: var(--brand);
  color: var(--brand-contrast);
  font-size: 9.5px;
  font-weight: 700;
  box-shadow: var(--shadow-md);
}

.mini-chart {
  width: 100%;
  height: 40px;
  margin-top: 4px;
}

.mini-chart path {
  stroke: var(--brand);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.mini-meter-row {
  margin-top: 6px;
}

.mini-meter {
  display: block;
  height: 4px;
  margin-top: 3px;
  border-radius: 999px;
  background: var(--surface3);
  overflow: hidden;
}

.mini-meter-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--brand);
}

.landing-steps {
  list-style: none;
  margin: 16px 0 0;
  padding: 0;
  display: grid;
  gap: 12px;
}

.landing-step {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 14px 16px;
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  background: var(--surface);
}

.landing-step-num {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--brand);
  color: var(--brand-contrast);
  font-size: 13px;
  font-weight: 800;
}

.landing-step-text {
  min-width: 0;
}

/* Title and estimate share a line, and the estimate drops below it when the title is long. */
.landing-step-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 2px;
}

.landing-step-title {
  font-weight: 700;
}

.landing-step-time {
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
}

/* Detail under the step: quieter and smaller than the step's own line, so scanning the numbered
   titles still works and the extra reading is optional. */
.landing-step-points {
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 5px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--txt3);
}

.landing-step-points li {
  position: relative;
  padding-left: 14px;
}

.landing-step-points li::before {
  content: '';
  position: absolute;
  left: 3px;
  top: 8px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0.55;
}

.landing-plans {
  display: grid;
  gap: 12px;
}

@media (min-width: 620px) {
  .landing-plans {
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  }
}

.landing-plan {
  position: relative;
  padding: 18px;
  border-radius: var(--r-xl);
  border: 1px solid var(--border);
  background: var(--surface);
}

.landing-plan.featured {
  border-color: var(--brand);
  box-shadow: var(--shadow-md);
}

.landing-tag {
  position: absolute;
  top: -10px;
  left: 18px;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--brand);
  color: var(--brand-contrast);
  font-size: 11px;
  font-weight: 700;
}

.landing-plan-name {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--txt2);
}

.landing-plan-price {
  margin: 4px 0 8px;
  font-size: 26px;
  font-weight: 800;
  color: var(--brand);
}

.landing-plan-per {
  font-size: 13px;
  font-weight: 600;
  color: var(--txt3);
}

/* Hairline-separated rows with a ticked disc, instead of disc bullets with a hanging indent. The
   dividers do the aligning, so a wrapped perk stays flush with the one above it. */
.landing-list {
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
  font-size: 13px;
  color: var(--txt2);
}

.landing-list li {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 8px 0;
  line-height: 1.45;
  border-top: 1px solid var(--glass-border);
}

.landing-list li:first-child {
  border-top: none;
  padding-top: 0;
}

.landing-list li:last-child {
  padding-bottom: 0;
}

.landing-check {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 17px;
  height: 17px;
  /* Nudged down so the tick centres on the first line of text, not on the whole wrapped block. */
  margin-top: 1px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
}

/* The featured card carries the accent already; a filled tick there keeps it from going busy. */
.landing-plan.featured .landing-check {
  background: var(--accent);
  color: #fff;
}

.landing-final {
  max-width: 760px;
  margin: 26px auto 0;
  padding: 26px 18px;
  border-radius: var(--r-xl);
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  text-align: center;
}

/* Sort pill: states the current order and opens the chooser. Reads as one control, so the
   field name is emphasised and the direction sits quietly beside it. */
.sortbar {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  margin-bottom: 10px;
  padding: 9px 12px;
  border-radius: var(--r-lg);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--txt2);
  font-size: 13px;
}

.sortbar-label {
  font-weight: 650;
  color: var(--txt1);
}

.sortbar-dir {
  margin-left: auto;
  color: var(--txt2);
}

.loadmore {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  margin-top: 14px;
}

.loadmore .tiny {
  margin: 0;
}

/* Input with a button beside it, e.g. the document number and its Generate action. */
.input-with-action {
  display: flex;
  /* stretch, not center: the button then takes the input's height instead of its own padding. */
  align-items: stretch;
  gap: 8px;
}

.input-with-action .input {
  flex: 1 1 auto;
  min-width: 0;
}

.input-with-action .btn {
  flex: 0 0 auto;
  gap: 6px;
  /* Let the flex line set the height; the button's own vertical padding would fight it. */
  height: auto;
  padding-top: 0;
  padding-bottom: 0;
  white-space: nowrap;
}

/* A dropdown trigger + searchable panel — for picking one of a closed set of options (a category
   filter), where a growing row of chips stopped scaling once a shop had more than a couple. */
.category-filter {
  position: relative;
  flex: 0 0 auto;
  /* height:100% doesn't reliably work here — .input-with-action itself has no explicit height (it
     sizes to its tallest child), so a percentage height has nothing definite to resolve against.
     display:flex + stretch sidesteps that entirely: the outer flex row stretches this div directly
     (no percentage math involved), and this div's own flex context then stretches the button
     inside it the same way, one flex stretch after another instead of a height percentage chain. */
  display: flex;
  align-items: stretch;
}

/* .btn-outline.category-filter-trigger (two classes), not just .category-filter-trigger — needs
   to outrank .btn-outline's own background/color by specificity, not just by being declared later,
   since a bold, hollow button next to the search box read as a second, louder call-to-action
   rather than the quiet filter control it's supposed to match. */
.btn-outline.category-filter-trigger {
  max-width: 180px;
  /* No explicit height here — .input-with-action .btn already sets height:auto, which is exactly
     what lets .category-filter's align-items:stretch actually stretch it. A percentage height
     would just reopen the same resolution problem one level down. */
  background: var(--surface2);
  font-weight: 500;
  color: var(--txt2);
}

.btn-outline.category-filter-trigger:hover {
  background: var(--surface2);
}

.category-filter-trigger span:first-child {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.category-filter-trigger span:last-child {
  flex: 0 0 auto;
  color: var(--txt3);
  transition: transform 0.15s ease;
}

.category-filter-panel {
  position: absolute;
  /* On the POS Terminal, every product tile below is its own position:relative box too — a
     low z-index here was losing the stacking fight against them and rendering underneath the
     grid instead of over it. High enough to clear any in-page content; toasts/sheets still win. */
  z-index: 45;
  top: calc(100% + 6px);
  right: 0;
  width: 240px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  padding: 8px;
}

.category-filter-list {
  max-height: 260px;
  overflow-y: auto;
  margin-top: 6px;
  display: flex;
  flex-direction: column;
}

.category-filter-option {
  display: block;
  width: 100%;
  text-align: left;
  padding: 9px 10px;
  border-radius: var(--r-sm);
  font-size: 14px;
  font-weight: 600;
  color: var(--txt1);
}

.category-filter-option:hover {
  background: var(--surface2);
}

.category-filter-option.active {
  background: var(--brand-soft);
  color: var(--brand);
}

/* Bulk actions above the bin list. Sits outside .list so it does not read as another row. */
.bulkbar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
  padding: 0 2px;
}

.bulkbar .tiny {
  flex: 1 1 auto;
  text-align: center;
}

/* Per-row tick. Reuses .check-box so it matches every other boolean in the app. */
.row-select {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 40px;
  align-self: stretch;
  background: none;
  border: none;
  padding: 0;
}

.chips-wrap {
  display: flex;
  align-items: center;
  gap: 6px;
}

.chips {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding: 2px 2px 6px;
  scrollbar-width: none;
  /* min-width lets the flex child actually shrink instead of pushing the arrows off-screen. */
  flex: 1 1 auto;
  min-width: 0;
}

/* Styled like a chip so the row still reads as one control, only round. Rendered solely on a
   side that has more content, so its presence is the hint. */
.chips-arrow {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  margin-bottom: 4px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--txt2);
}

.chips-arrow:active {
  background: var(--surface2);
  color: var(--txt1);
}

/* One icon, mirrored — no second glyph to keep in step with the first. */
.chips-arrow.left span {
  display: grid;
  transform: scaleX(-1);
}

.chips::-webkit-scrollbar {
  display: none;
}

.chip {
  padding: 7px 13px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--txt2);
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  flex: 0 0 auto;
}

.chip[aria-pressed='true'] {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-contrast);
}

/* ---------- Segmented control ---------- */

/* Tab bar: a recessed track with a raised active tab, so the selected one is unmistakable. */
.segmented {
  display: flex;
  gap: 4px;
  padding: 4px;
  margin-bottom: 14px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 14px;
}

.segmented button {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 9px 8px;
  border-radius: 10px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--txt2);
  font-weight: 650;
  font-size: 13.5px;
  transition: background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease;
}

/* Underline tabs for four or more: no track, no pills, just labels with a rose rule under the
   active one. Fits a single row on a phone and reads as quieter chrome than the pill control. */
.segmented.tabs {
  display: flex;
  gap: 0;
  padding: 0 2px;
  background: none;
  border: none;
  border-bottom: 1px solid var(--border-strong);
  border-radius: 0;
  margin-bottom: 16px;
  /* Safety valve: if a future tab set really is too wide, it scrolls rather than clipping. */
  overflow-x: auto;
  scrollbar-width: none;
}

.segmented.tabs::-webkit-scrollbar {
  display: none;
}

.segmented.tabs button {
  position: relative;
  /* Size to the label, then share the leftover space — equal thirds would starve the longest
     word while "App" sat in whitespace. */
  flex: 1 1 auto;
  min-width: max-content;
  gap: 5px;
  padding: 9px 6px 11px;
  border: none;
  border-radius: 0;
  background: none;
  font-size: 13px;
  letter-spacing: 0.01em;
}

/* Never abbreviate a tab label. */
.segmented.tabs button span {
  overflow: visible;
  text-overflow: clip;
}

.segmented.tabs button svg {
  opacity: 0.55;
}

.segmented.tabs button[aria-pressed='true'] {
  background: none;
  border: none;
  box-shadow: none;
  color: var(--brand-ink);
  font-weight: 750;
}

/* The active marker sits on the container's rule, so tabs never shift as you switch. */
.segmented.tabs button[aria-pressed='true']::after {
  content: '';
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: -1px;
  height: 2px;
  border-radius: 2px;
  background: var(--brand);
}

.segmented.tabs button:active {
  transform: none;
  opacity: 0.75;
}

/* Very narrow phones: drop the icons rather than the words. */
@media (max-width: 359px) {
  .segmented.tabs button svg {
    display: none;
  }
}

.segmented button span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.segmented button svg {
  flex: 0 0 auto;
  opacity: 0.7;
}

.segmented button[aria-pressed='true'] {
  background: var(--surface);
  border-color: var(--brand);
  color: var(--brand-ink);
  box-shadow: var(--shadow-sm);
}

.segmented button[aria-pressed='true'] svg {
  opacity: 1;
  color: var(--brand);
}

.segmented button:active {
  transform: scale(0.99);
}

/* ---------- Forms ---------- */

.field {
  display: block;
  margin-bottom: 12px;
}

.field-label {
  display: block;
  font-size: 12.5px;
  font-weight: 650;
  color: var(--txt2);
  margin-bottom: 5px;
}

.field-hint {
  display: block;
  font-size: 11.5px;
  color: var(--txt2);
  margin-top: 4px;
}

/* Fields carry a visible edge — on a frosted card an unbordered fill reads as a hole. */
.input,
.select,
.textarea {
  width: 100%;
  background: var(--surface2);
  border: 1.5px solid var(--border-strong);
  border-radius: var(--r-md);
  padding: 12px 13px;
  color: var(--txt1);
  font-size: 15px;
  outline: none;
  transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
  appearance: none;
}

.input::placeholder,
.textarea::placeholder {
  color: var(--txt3);
}

.input:focus,
.select:focus,
.textarea:focus {
  border-color: var(--brand);
  background: var(--surface);
  box-shadow: 0 0 0 3px var(--brand-soft);
}

.textarea {
  min-height: 84px;
  resize: vertical;
}

.select {
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 19px) calc(50% + 2px), calc(100% - 13px) calc(50% + 2px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  padding-right: 34px;
}

.password-field {
  position: relative;
}

.password-field .input {
  padding-right: 44px;
}

.password-toggle {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 8px;
  border: none;
  background: none;
  color: var(--txt3);
  cursor: pointer;
}

.password-toggle:hover {
  color: var(--txt2);
}

.input.money {
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* ---------- Inline form error ---------- */

.form-error {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0 0 14px;
  padding: 10px 12px;
  border-radius: var(--r-md);
  border: 1px solid var(--danger);
  background: var(--danger-soft);
  color: var(--danger);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.45;
  animation: slideUpSm 0.18s cubic-bezier(0.16, 1, 0.3, 1);
}

.form-error svg {
  flex: 0 0 auto;
  margin-top: 1px;
}

/* ---------- Captcha ---------- */

/* Label row carries the refresh control on the right. The label keeps its own class so there
   is no display:block / display:flex fight on the same element. */
.captcha-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
}

.captcha-head .field-label {
  margin-bottom: 0;
}

.captcha-refresh {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 9px;
  border-radius: 999px;
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--brand);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.captcha-refresh:active {
  background: var(--brand-soft);
}

.captcha-image {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: 14px;
  border-radius: 18px;
  box-shadow: var(--shadow-sm);
}

.captcha-loading {
  aspect-ratio: 420 / 150;
  background: linear-gradient(90deg, var(--surface2), var(--surface3), var(--surface2));
  background-size: 200% 100%;
  animation: shimmer 1.2s ease-in-out infinite;
}

.captcha-input {
  text-align: center;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: 0.36em;
  text-transform: uppercase;
}

/* One-time code: wide tracking so six digits are easy to check against the email. */
.input.code-input {
  text-align: center;
  font-size: 26px;
  font-weight: 750;
  letter-spacing: 0.32em;
  font-variant-numeric: tabular-nums;
  padding: 14px 13px;
}

.grid2 {
  display: grid;
  /* minmax(0, 1fr), not plain 1fr: a grid track's implicit minimum is its content's own
     min-content size, not 0 — a native date input refuses to shrink below roughly its own
     dd/mm/yyyy-plus-calendar-icon width, which was exactly wide enough to push the Due date
     column past the card's edge on a phone screen. This lets the track actually shrink to fit. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 10px;
}

/* An uneven split for a pair where the second field needs more room than the first — e.g. SKU
   next to Barcode, which carries two action buttons alongside its input. Barcode's placeholder and
   buttons still don't fit next to anything on a phone-width screen, so both fields stack full width
   there instead, and only split 5/7 once there's room at 620px and up. */
.grid2.cols-5-7 {
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 620px) {
  .grid2.cols-5-7 {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  }
}

.picker {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px;
  border-radius: var(--r-md);
  background: var(--surface2);
  border: 1.5px dashed var(--border-strong);
  color: var(--txt2);
  font-weight: 600;
}

.picker.filled {
  border-style: solid;
  border-color: var(--border);
  color: var(--txt1);
}

/* ---------- Buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 13px 18px;
  border-radius: var(--r-md);
  font-weight: 700;
  font-size: 15px;
  transition: transform 0.08s ease, opacity 0.15s ease;
}

.btn:active {
  transform: scale(0.985);
}

.btn[disabled] {
  opacity: 0.55;
  pointer-events: none;
}

/* ---------- Keyboard hints, on a device with no keyboard ---------- */

/* A phone has nothing to press these with, so the chip is pure noise — and on a narrow button
   it steals the room the label needs, wrapping a two-word action onto two lines. Whatever hosts
   one also reserves its space with an inline padding, so that has to come back too; :has lets
   both happen here instead of threading a prop through every call site. */
@media (max-width: 620px) {
  .keyhint {
    display: none;
  }

  /* !important only because the padding being undone is an inline style. */
  .btn:has(.keyhint) {
    padding-left: 18px !important;
    justify-content: center !important;
  }

  .input:has(~ .keyhint) {
    padding-right: 14px !important;
  }
}

.btn-primary {
  background: var(--brand);
  color: var(--brand-contrast);
}

.btn-accent {
  background: var(--accent);
  color: #3d1a16;
}

.btn-ghost {
  background: var(--surface2);
  color: var(--txt1);
}

.btn-outline {
  background: transparent;
  border: 1.5px solid var(--border-strong);
  color: var(--txt1);
}

.btn-danger {
  background: var(--danger-soft);
  color: var(--danger);
}

.btn-block {
  width: 100%;
}

.btn-sm {
  padding: 8px 12px;
  font-size: 13.5px;
  border-radius: var(--r-sm);
}

.fab {
  position: fixed;
  right: 16px;
  right: calc(16px + var(--safe-right));
  bottom: calc(20px + var(--safe-bottom));
  z-index: 25;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 15px 20px;
  border-radius: 999px;
  background: var(--brand);
  color: var(--brand-contrast);
  font-weight: 700;
  box-shadow: var(--shadow-lg);
}

/* ---------- Speed-dial FAB: one button that reveals labelled actions ---------- */
.fab-dial {
  position: fixed;
  right: calc(16px + var(--safe-right));
  bottom: calc(20px + var(--safe-bottom));
  z-index: 25;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 12px;
}

/* The trigger reuses .fab's look but drops its own fixed positioning inside the dial. */
.fab-dial .fab {
  position: static;
  right: auto;
  bottom: auto;
}

/* ---------- Icon-only wrappers ---------- */

/* An inline SVG sits on the text baseline, so an element holding nothing but an icon comes out
   taller than the glyph — the line box reserves descender space the icon never uses — and the
   icon then reads a couple of pixels high inside it. Centring the wrapper as a box removes the
   line box entirely, which is the actual fix; .fab-icon is what showed it on Product Items,
   Expenses and Inventory Items, and the rest had it latent for the same reason.

   These live together rather than each beside its own component because the cause is one thing,
   not four. An element that wraps an icon AND text is deliberately not here: it needs its line
   box, so a warning line or hint still flows with the words beside it.

   The +-to-× rotation on .fab-icon keeps working — that is a transform on this same element,
   which grid leaves alone. */
.fab-icon,
.row-icon,
.qr-placeholder,
.promo-strip-art-deco {
  display: grid;
  place-items: center;
}

.fab-icon svg,
.row-icon svg,
.qr-placeholder svg,
.promo-strip-art-deco svg {
  display: block;
}

/* The trigger's icon morphs from + to × as the menu opens. */
.fab-dial .fab .fab-icon {
  transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}
.fab-dial.is-open .fab .fab-icon {
  transform: rotate(135deg);
}

.fab-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
}

.fab-action {
  display: inline-flex;
  align-items: center;
  gap: 11px;
  padding: 11px 16px 11px 14px;
  border-radius: 999px;
  background: var(--surface);
  color: var(--txt1);
  font-weight: 650;
  border: 1px solid var(--border);
  box-shadow: var(--shadow-md);
  white-space: nowrap;
  animation: fabPop 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.fab-action:disabled {
  opacity: 0.6;
}

.fab-action .fab-action-ico {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--brand-soft);
  color: var(--brand);
  flex: 0 0 auto;
}

@keyframes fabPop {
  from {
    opacity: 0;
    transform: translateY(10px) scale(0.94);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Invisible catch-layer: a tap anywhere else closes the open dial. */
.fab-scrim {
  position: fixed;
  inset: 0;
  z-index: 24;
  background: transparent;
  border: none;
}

/* The shortcut chip on a dial action, matching KeyHint's look but laid out inline — KeyHint
   itself is absolutely positioned, which a pill that sizes to its own content can't host. */
.fab-action-key {
  margin-left: 2px;
  padding: 3px 7px;
  border-radius: 999px;
  background: var(--surface3);
  border: 1px solid var(--border);
  color: var(--txt3);
  font-family: monospace;
  font-size: 11px;
  font-weight: 800;
}

/* The cart's own dial. Unlike the page-level .fab-dial this one belongs to the cart panel, so it
   sticks to the bottom of the cart's scroll area rather than the viewport — on a wide screen the
   cart is a sidebar, and a viewport-anchored FAB would float over the product grid beside it,
   nowhere near the sale it acts on. Sticky also means it keeps its place in the flow, so it can
   never cover the last row of a long cart the way an absolutely positioned one would. */
.pos-cart-dial {
  position: sticky;
  right: auto;
  bottom: 0;
  margin-top: 12px;
  /* So the cart list stays reachable through the gap the dial leaves around its buttons. */
  pointer-events: none;
}

.pos-cart-dial > * {
  pointer-events: auto;
}

/* Icon-only here, so a circle rather than .fab's label-shaped pill. The gradient is the brand
   mixed a little toward its own contrast colour, which lifts the top edge in either theme
   without naming a literal white. */
.pos-cart-dial .fab {
  position: relative;
  width: 56px;
  height: 56px;
  padding: 0;
  gap: 0;
  justify-content: center;
  background: linear-gradient(
    152deg,
    color-mix(in srgb, var(--brand) 88%, var(--brand-contrast) 12%),
    var(--brand) 62%
  );
  /* Brand-tinted rather than the generic drop shadow: this floats over a tinted cart panel,
     where a neutral black shadow reads as grime instead of lift. */
  box-shadow:
    0 2px 5px color-mix(in srgb, var(--brand) 28%, transparent),
    0 12px 26px -8px color-mix(in srgb, var(--brand) 55%, transparent);
  transition:
    transform 0.2s cubic-bezier(0.16, 1, 0.3, 1),
    box-shadow 0.2s ease;
}

/* A hairline of the contrast colour along the rim, so the circle keeps a defined edge against
   a surface close to the brand in tone. */
.pos-cart-dial .fab::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid color-mix(in srgb, var(--brand-contrast) 22%, transparent);
  pointer-events: none;
}

/* Transform stays on the button and rotation on the icon, so the two never fight. */
@media (hover: hover) {
  .pos-cart-dial .fab:hover {
    transform: translateY(-2px);
    box-shadow:
      0 3px 7px color-mix(in srgb, var(--brand) 32%, transparent),
      0 18px 34px -8px color-mix(in srgb, var(--brand) 62%, transparent);
  }
}

.pos-cart-dial .fab:active {
  transform: scale(0.94);
  box-shadow:
    0 1px 3px color-mix(in srgb, var(--brand) 26%, transparent),
    0 6px 14px -6px color-mix(in srgb, var(--brand) 45%, transparent);
}

/* The cart is a narrow column — never let an action grow wider than it. */
.pos-cart-dial .fab-action {
  max-width: 100%;
}

/* Sticky save/action bar. Needs its own stacking level: the cards above it are positioned
   (for the glass sheen), so without a z-index they paint over it. */
.actionbar {
  position: sticky;
  bottom: 0;
  z-index: 20;
  margin-top: 18px;
  margin-left: calc(-14px - var(--safe-left));
  margin-right: calc(-14px - var(--safe-right));
  padding: 10px calc(14px + var(--safe-right)) calc(12px + var(--safe-bottom)) calc(14px + var(--safe-left));
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-top: 1px solid var(--glass-border);
  box-shadow: 0 -6px 20px rgba(32, 18, 24, 0.18);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* The one figure that matters while scrolling through notes and terms a screen below it — kept
   in the same sticky bar as Save rather than buried at the bottom of the page. */
.actionbar-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 0 2px;
  font-size: 13px;
}

.actionbar-total .val {
  font-size: 19px;
  font-weight: 800;
  color: var(--brand);
}

.actionbar-buttons {
  display: flex;
  gap: 10px;
}

/* Some pages want an even split rather than .actionbar-buttons' usual "small Cancel, big primary
   action" shape — flex:1 on both overrides .btn-block's width:100% (flex-basis wins once set),
   so either button's own text length can't throw the row off balance. */
.actionbar-buttons-equal > .btn {
  flex: 1;
}

/* ---------- Custom toggles (never native checkboxes) ---------- */

.check {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 14px;
  width: 100%;
  text-align: left;
}

.check-box {
  width: 22px;
  height: 22px;
  border-radius: 6px;
  border: 2px solid var(--border-strong);
  display: grid;
  place-items: center;
  color: transparent;
  font-size: 13px;
  font-weight: 900;
  flex: 0 0 auto;
  transition: background 0.15s ease, border-color 0.15s ease;
}

/* Any element carrying aria-checked, not just .check — the bin's per-row tick is its own
   control and was silently missing the checked styling. */
.check[aria-checked='true'] .check-box,
[aria-checked='true'] > .check-box {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-contrast);
}

.check-label {
  flex: 1;
  font-weight: 550;
}

.check-sub {
  display: block;
  font-size: 12px;
  color: var(--txt2);
  font-weight: 450;
}

.switch {
  width: 46px;
  height: 28px;
  border-radius: 999px;
  background: var(--surface3);
  border: 1px solid var(--border);
  padding: 2px;
  display: flex;
  flex: 0 0 auto;
  transition: background 0.18s ease, border-color 0.18s ease;
}

.switch-knob {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Standalone switch: the button itself carries the state. */
.switch[aria-checked='true'] {
  background: var(--brand);
  border-color: var(--brand);
}

.switch[aria-checked='true'] .switch-knob {
  transform: translateX(18px);
}

/* Labelled switch: the state lives on the wrapping .check button, and the track inside is
   aria-hidden — so it has to be styled from the parent or the knob never moves. */
.check[aria-checked='true'] .switch {
  background: var(--brand);
  border-color: var(--brand);
}

.check[aria-checked='true'] .switch .switch-knob {
  transform: translateX(18px);
}

/* ---------- Modals & sheets ---------- */

.modal-scrim {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: var(--scrim-bg);
  backdrop-filter: blur(12px) saturate(115%);
  -webkit-backdrop-filter: blur(12px) saturate(115%);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  animation: fadeIn 0.2s ease;
}

.modal-scrim.center {
  align-items: center;
  padding: 18px;
}

.sheet {
  width: 100%;
  max-width: 560px;
  max-height: 88dvh;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  border-bottom: none;
  box-shadow: var(--shadow-lg), var(--glass-ring);
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  padding-bottom: var(--safe-bottom);
  padding-left: var(--safe-left);
  padding-right: var(--safe-right);
  display: flex;
  flex-direction: column;
  animation: slideUp 0.22s cubic-bezier(0.16, 1, 0.3, 1);
  overflow: hidden;
}

.sheet.center {
  border-radius: var(--r-lg);
  animation: slideUpSm 0.2s cubic-bezier(0.16, 1, 0.3, 1);
  max-width: 420px;
}

.sheet.tall {
  height: 94dvh;
  max-height: 94dvh;
}

/* ---------- "Who it's for" story modal ---------- */

/* Wide enough for the two-column layout below; still narrows to the ordinary centred-sheet width
   on a phone, where the columns stack instead (see .story-body). */
.sheet.story-sheet {
  max-width: 720px;
}

/* A concrete max-height rather than height:100% off a flex:1 parent — .sheet itself only ever
   has max-height:88dvh, not an explicit height, so a percentage height here had nothing definite
   to resolve against and the body ended up sized to only its first bit of content, silently
   cutting off the description and burying it behind flushBody's own overflow:hidden (which is
   the right call for other sheets, e.g. a PDF preview that scrolls itself — just not this one).
   128px is roughly the sheet's own header + footer chrome, so this always leaves it genuinely
   scrollable inside the sheet's existing 88dvh cap, on any screen, without guessing at flex math. */
.story-scroll {
  max-height: calc(88dvh - 128px);
  overflow-y: auto;
}

.story-body {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 28px;
  align-items: center;
  padding: 26px 28px;
}

@media (max-width: 620px) {
  .story-body {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
    padding: 20px;
    gap: 10px;
  }

  /* The full-size ~400px-tall device frame is the right call side-by-side with text on a wide
     screen, but stacked above it on a phone it buried the actual description below a scroll —
     exactly what looked broken. zoom (not transform:scale, which repaints smaller but keeps the
     original layout box, so it would not have freed up any of that height) shrinks it to a quick
     glanceable thumbnail instead of the hero of the slide. */
  .story-preview .device {
    zoom: 0.5;
  }
}

/* Re-keyed per slide so the animation replays on every next/prev — a quiet cue that something
   actually changed, not just new text popping in place. */
@keyframes storySlide {
  from {
    opacity: 0;
    transform: translateX(14px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

.story-preview {
  flex: 0 0 auto;
  animation: storySlide 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

.story-text {
  min-width: 0;
  animation: storySlide 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

.story-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 12px;
  background: var(--brand-soft);
  color: var(--brand);
  margin-bottom: 12px;
}

.story-title {
  margin: 0 0 10px;
  font-size: 20px;
  font-weight: 800;
  line-height: 1.25;
}

.story-pain {
  margin: 0 0 14px;
  font-size: 14px;
  color: var(--txt2);
  line-height: 1.5;
}

.story-detail {
  margin: 0;
  font-size: 14.5px;
  color: var(--txt1);
  line-height: 1.6;
}

.story-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
}

.story-dots {
  display: flex;
  align-items: center;
  gap: 7px;
}

.story-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: none;
  padding: 0;
  background: var(--border-strong);
  transition: background-color 0.15s ease, transform 0.15s ease;
}

.story-dot.is-active {
  background: var(--brand);
  transform: scale(1.3);
}

.story-arrow {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--txt1);
  flex: 0 0 auto;
}
.story-arrow:hover {
  border-color: var(--brand);
  color: var(--brand);
}

/* Clicking a bucket card is a real affordance, not a static tile — a real <button>, not a div with
   an onClick, so it's reachable and announced properly without extra aria plumbing. */
.landing-card.is-clickable {
  display: block;
  width: 100%;
  border: 1px solid var(--glass-border);
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
  transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.18s ease;
}
.landing-card.is-clickable:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-lg);
}

.sheet.tall .sheet-body {
  flex: 1;
  min-height: 0;
}

/* Generous hit area around a small visual bar — it is a real close button, and a drag handle. */
.sheet-grip {
  display: block;
  width: 100%;
  padding: 11px 0 7px;
  background: none;
  border: none;
  touch-action: none;
}

.sheet-grip-bar {
  display: block;
  width: 42px;
  height: 4px;
  border-radius: 999px;
  margin: 0 auto;
  background: var(--border-strong);
  transition: background 0.15s ease, width 0.15s ease;
}

.sheet-grip:active .sheet-grip-bar {
  background: var(--brand);
  width: 54px;
}

.sheet-head {
  padding: 10px 16px 12px;
  border-bottom: 1px solid var(--border-strong);
  display: flex;
  align-items: center;
  gap: 10px;
}

.sheet-title {
  flex: 1;
  font-size: 16px;
  font-weight: 700;
}

.sheet-body {
  padding: 14px 16px;
  overflow-y: auto;
}

.sheet-body.flush {
  padding: 0;
  overflow: hidden;
}

/* ---------- PDF preview ---------- */

.pdf-wrap {
  height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--surface2);
}

.pdf-frame {
  flex: 1;
  width: 100%;
  border: 0;
  background: var(--surface2);
}

.pdf-formats {
  padding: 12px 16px 10px;
  background: var(--surface);
  border-bottom: 1px solid var(--border-strong);
}

.pdf-formats .segmented {
  margin-bottom: 8px;
}

.pdf-image-scroll {
  flex: 1;
  overflow: auto;
  padding: 14px;
  display: flex;
  justify-content: center;
  align-items: flex-start;
}

.pdf-image {
  width: 100%;
  max-width: 460px;
  height: auto;
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-md);
  background: #ffffff;
}

.pdf-fallback {
  padding: 10px 14px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--brand);
  background: var(--surface);
  border-top: 1px solid var(--border-strong);
}

/* Prev/next for a multi-page image export — one PNG per PDF page, same as the real document. */
.pdf-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 8px 14px 12px;
  background: var(--surface2);
}

/* ---------- "or" divider ---------- */

.or-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 14px 0 12px;
  color: var(--txt2);
  font-size: 12px;
}

.or-divider::before,
.or-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* ---------- Numbered instructions ---------- */

.steps {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
}

.steps li {
  counter-increment: step;
  position: relative;
  padding: 0 0 12px 32px;
  font-size: 13.5px;
  color: var(--txt1);
}

.steps li:last-child {
  padding-bottom: 0;
}

.steps li::before {
  content: counter(step);
  position: absolute;
  left: 0;
  top: 0;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 11.5px;
  font-weight: 750;
  color: var(--brand-ink);
  background: var(--brand-soft);
  border: 1px solid var(--border-strong);
}

/* ---------- Subscription banner ---------- */

.sub-banner {
  /* The banner's tone as a variable rather than via currentColor: the pill below fills itself from
     this, and deriving a fill from the element's own colour meant the label and the fill could
     never differ. */
  --sub-tone: var(--warn);
  width: 100%;
  display: flex;
  align-items: center;
  gap: 11px;
  text-align: left;
  padding: 11px 13px;
  margin-bottom: 12px;
  border-radius: var(--r-md);
  border: 1px solid var(--warn);
  background: var(--warn-soft);
  color: var(--warn);
}

.sub-banner.urgent,
.sub-banner.expired {
  --sub-tone: var(--danger);
  border-color: var(--danger);
  background: var(--danger-soft);
  color: var(--danger);
}

.sub-banner-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.sub-banner-text strong {
  font-size: 13.5px;
  font-weight: 750;
}

.sub-banner-text span {
  font-size: 11.5px;
  color: var(--txt2);
}

.sub-banner-cta {
  flex: 0 0 auto;
  font-size: 12.5px;
  font-weight: 750;
  padding: 6px 11px;
  border-radius: 999px;
  background: var(--sub-tone);
  /* Always light-on-tone. Both tones are saturated enough in either theme to carry white text, and
     white cannot collide with the fill the way a theme surface colour did. */
  color: #fff;
}

/* ---------- Payment QR ---------- */

.qr-block {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 12px;
  margin-bottom: 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface2);
}

.qr-preview {
  width: 84px;
  height: 84px;
  flex: 0 0 auto;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: #ffffff;
  border: 1px solid var(--border);
  display: grid;
  place-items: center;
}

.qr-preview img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Same box as .qr-preview, minus the white backing — a payment QR needs white behind it to stay
   scannable, but a brand logo (usually already opaque, sometimes transparent-cornered) just
   picks up an unwanted white halo from that. Sits on the card's own dark surface instead. */
.brand-logo-preview {
  width: 84px;
  height: 84px;
  flex: 0 0 auto;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--surface3);
  border: 1px solid var(--border);
  display: grid;
  place-items: center;
}

.brand-logo-preview img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.qr-placeholder {
  color: var(--txt3);
}

.pdf-loading {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
}

.sheet-foot {
  padding: 12px 16px;
  border-top: 1px solid var(--border-strong);
  display: flex;
  gap: 10px;
}

/* ---------- Toasts ---------- */

.toasts {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(84px + var(--safe-bottom));
  z-index: 70;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(92vw, 460px);
}

.toast {
  padding: 12px 14px;
  border-radius: var(--r-md);
  background: var(--txt1);
  color: var(--bg);
  font-weight: 600;
  font-size: 14px;
  box-shadow: var(--shadow-lg);
  animation: slideUpSm 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}

.toast.ok {
  background: var(--ok);
  color: #ffffff;
}

.toast.err {
  background: var(--danger);
  color: #ffffff;
}

/* ---------- Admin: impersonation banner ---------- */

.impersonation-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 90;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 9px calc(14px + var(--safe-right)) 9px calc(14px + var(--safe-left));
  padding-top: calc(9px + var(--safe-top));
  background: var(--warn);
  color: #1a1206;
  font-size: 13px;
  font-weight: 700;
  text-align: center;
  box-shadow: var(--shadow-md);
}

.impersonation-banner .btn {
  background: rgba(0, 0, 0, 0.12);
  color: inherit;
  border: none;
  padding: 5px 14px;
}

/* ---------- Admin: permanent-delete stepper ---------- */

.purge-stepper {
  margin-bottom: 16px;
}

.purge-stepper-dots {
  display: flex;
  align-items: center;
}

.purge-stepper-line {
  flex: 1 1 auto;
  height: 2px;
  margin: 0 4px;
  background: var(--border);
  transition: background 0.2s ease;
}

.purge-stepper-line.done {
  background: var(--brand);
}

.purge-stepper-dot {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2px solid var(--border);
  background: var(--surface);
  color: var(--txt3);
  font-size: 12px;
  font-weight: 800;
  transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}

.purge-stepper-dot.active {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-contrast);
  box-shadow: 0 0 0 4px var(--brand-soft);
}

.purge-stepper-dot.done {
  background: var(--brand-soft);
  border-color: var(--brand);
  color: var(--brand);
}

.purge-stepper-labels {
  display: flex;
  margin-top: 6px;
}

.purge-stepper-labels span {
  flex: 1 1 0;
  font-size: 11px;
  font-weight: 700;
  color: var(--txt3);
}

.purge-stepper-labels span:first-child {
  text-align: left;
}

.purge-stepper-labels span:last-child {
  text-align: right;
}

.purge-stepper-labels span:not(:first-child):not(:last-child) {
  text-align: center;
}

.purge-stepper-labels span.active {
  color: var(--danger);
}

/* A background action running out of sight (currently just the admin "permanently delete this
   subscriber" purge) — deliberately not .toasts' own spot (bottom-center everywhere) so the two
   can never collide: this sits bottom-right on anything wider than a phone, bottom-center on one,
   same corner .toasts already owns there but a run like this is rare enough not to worry about a
   second one appearing at the same moment. */
.background-progress {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(20px + var(--safe-bottom));
  z-index: 75;
  width: min(92vw, 320px);
  padding: 12px 14px;
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px solid var(--glass-border);
  box-shadow: var(--shadow-lg);
  animation: slideUpSm 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}

@media (min-width: 640px) {
  .background-progress {
    left: auto;
    right: calc(20px + var(--safe-right));
    transform: none;
  }
}

.background-progress-bar {
  height: 6px;
  border-radius: 999px;
  background: var(--surface3);
  overflow: hidden;
}

.background-progress-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--brand);
  transition: width 0.3s ease;
}

.background-progress.ok .background-progress-fill {
  background: var(--ok);
}

.background-progress.err .background-progress-fill {
  background: var(--danger);
}

.background-progress-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--txt1);
}

.background-progress-percent {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  color: var(--txt2);
}

/* A new build is waiting — the one deliberately visible sign of a deploy (see UpdatePrompt.tsx).
   Top, not bottom: the bottom is already owned by .toasts and every page's sticky .actionbar. */
.update-prompt {
  position: fixed;
  z-index: 80;
  top: calc(14px + var(--safe-top));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 10px 10px 16px;
  background: var(--surface);
  color: var(--txt1);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  box-shadow: var(--shadow-lg);
  font-size: 13.5px;
  font-weight: 600;
  animation: slideUpSm 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}

.update-prompt > svg {
  color: var(--brand);
  flex: 0 0 auto;
}

.update-prompt-btn {
  flex: 0 0 auto;
  padding: 7px 14px;
  border-radius: 999px;
  background: var(--brand);
  color: var(--brand-contrast);
  font-size: 13px;
  font-weight: 700;
}

.update-prompt-close {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  color: var(--txt3);
}

.update-prompt-close:hover {
  background: var(--surface2);
  color: var(--txt1);
}

/* ---------- Stats & report ---------- */

.stats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
}

.stat {
  position: relative;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm), var(--glass-ring);
  padding: 13px 14px;
  overflow: hidden;
}

.stat > * {
  position: relative;
  z-index: 1;
}

.stat-label {
  font-size: 11.5px;
  font-weight: 650;
  color: var(--txt2);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.stat-value {
  font-size: 21px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  margin-top: 3px;
}

.stat.brand .stat-value {
  color: var(--brand);
}

.stat.accent .stat-value {
  color: var(--accent);
}

.stat.danger .stat-value {
  color: var(--danger);
}

/* Same green as a paid invoice: profit and money received should read alike. */
.stat.ok .stat-value {
  color: var(--ok);
}

/* Same amber as a low-stock badge — a tier of its own, distinct from "danger" (out of stock). */
.stat.warn .stat-value {
  color: var(--warn);
}

/* A stat card doubling as a filter: cursor + motion say "tap me" on their own, so this never
   reads as just another readout. */
.stat-clickable {
  display: block;
  width: 100%;
  text-align: left;
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}

.stat-clickable:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md), var(--glass-ring);
}

.stat-clickable:active {
  transform: translateY(0) scale(0.98);
}

.stat-clickable.is-selected {
  border-color: var(--brand);
  box-shadow: var(--shadow-sm), 0 0 0 2px var(--brand-soft);
}

.stat-check {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--brand);
  color: var(--brand-contrast);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-sm);
}

/* ---------- Trend chart ---------- */

.chart-caption {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 10px;
}

.chart-caption-value {
  font-size: 17px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  display: block;
  line-height: 1.15;
}

.chart-caption-label {
  font-size: 11px;
  color: var(--txt2);
  letter-spacing: 0.02em;
}

.chart-plot {
  position: relative;
  height: 116px;
}

.chart-svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.chart-dot {
  position: absolute;
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-radius: 50%;
  background: var(--brand);
  border: 2px solid var(--surface);
  box-shadow: var(--shadow-sm);
}

.chart-axis {
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
  font-size: 11px;
  color: var(--txt2);
}

.chart-bars {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  height: 116px;
}

.chart-bar-slot {
  flex: 1;
  min-width: 0;
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.chart-bar-track {
  flex: 1;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  border-bottom: 1px solid var(--border-strong);
}

.chart-bar {
  width: 100%;
  max-width: 30px;
  min-height: 2px;
  border-radius: 6px 6px 0 0;
  background: linear-gradient(180deg, var(--brand) 0%, var(--brand-soft) 190%);
  opacity: 0.55;
  transition: height 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease;
}

.chart-bar.peak {
  opacity: 1;
}

.chart-bar-label {
  font-size: 10.5px;
  color: var(--txt2);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chart-empty {
  height: 116px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  font-size: 13px;
  color: var(--txt2);
}

.chart-empty-line {
  width: 100%;
  height: 1px;
  background: repeating-linear-gradient(
    90deg,
    var(--border-strong) 0 6px,
    transparent 6px 12px
  );
}

.meter {
  height: 7px;
  border-radius: 999px;
  background: var(--surface3);
  overflow: hidden;
}

.meter-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--brand);
}

/* ---------- Enquiry thread ---------- */

.mail {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.mail-subject {
  font-size: 19px;
  font-weight: 750;
  line-height: 1.3;
  color: var(--txt1);
}

.mail-msg {
  padding: 14px 16px 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
}

/* Your own reply is tinted and indented, the way a mail client sets a sent message apart from a
   received one without making it a different kind of card. */
.mail-msg.is-mine {
  margin-left: 18px;
  border-color: color-mix(in srgb, var(--brand) 32%, var(--border));
  background: color-mix(in srgb, var(--brand) 7%, var(--surface));
}

.mail-from {
  display: flex;
  align-items: center;
  gap: 11px;
  padding-bottom: 11px;
  margin-bottom: 11px;
  border-bottom: 1px solid var(--border);
}

.mail-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: 999px;
  background: var(--surface3);
  color: var(--txt2);
  font-size: 15px;
  font-weight: 800;
}

.mail-avatar.is-mine {
  background: var(--brand);
  color: #fff;
}

.mail-from-main {
  flex: 1;
  min-width: 0;
}

.mail-name {
  font-size: 14px;
  font-weight: 700;
  color: var(--txt1);
}

.mail-addr {
  font-size: 12px;
  color: var(--txt3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mail-when {
  flex: 0 0 auto;
  font-size: 11.5px;
  color: var(--txt3);
}

.mail-body {
  margin: 0;
  font-size: 14.5px;
  line-height: 1.65;
  color: var(--txt1);
  white-space: pre-wrap;
  word-break: break-word;
}

.mail-compose {
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--surface);
  overflow: hidden;
  transition: border-color 0.14s ease, box-shadow 0.14s ease;
}

.mail-compose:focus-within {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 14%, transparent);
}

/* The recipient line a mail client always shows, so it is never a guess who this is going to. */
.mail-compose-to {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 11px 15px;
  border-bottom: 1px solid var(--border);
  background: var(--surface2);
}

.mail-compose-label {
  font-size: 11px;
  font-weight: 750;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--txt3);
}

.mail-compose-addr {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--txt1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mail-input {
  display: block;
  width: 100%;
  padding: 14px 15px;
  border: 0;
  background: transparent;
  color: var(--txt1);
  font: inherit;
  font-size: 14.5px;
  line-height: 1.6;
  resize: vertical;
  min-height: 120px;
}

.mail-input:focus {
  outline: none;
}

.mail-compose-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 12px 12px 15px;
  border-top: 1px solid var(--border);
  background: var(--surface2);
}

.mail-note {
  margin: 0;
  padding: 14px 16px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-lg);
  font-size: 13.5px;
  color: var(--txt2);
}

@media (prefers-reduced-motion: reduce) {
  .mail-compose {
    transition: none;
  }
}

/* ---------- Landing assistant ---------- */

.assist-fab {
  position: fixed;
  right: calc(20px + var(--safe-right));
  bottom: calc(20px + var(--safe-bottom));
  z-index: 48;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 58px;
  height: 58px;
  border: 0;
  border-radius: 999px;
  background: linear-gradient(145deg, var(--brand), color-mix(in srgb, var(--brand) 72%, #000));
  color: #fff;
  cursor: pointer;
  box-shadow: 0 10px 30px color-mix(in srgb, var(--brand) 42%, transparent), var(--shadow-lg);
  transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
}

.assist-fab:hover {
  transform: scale(1.07);
}

.assist-fab-ico {
  display: flex;
  transition: transform 0.42s cubic-bezier(0.34, 1.3, 0.64, 1);
}

/* Half a turn each way, so opening and closing are the same motion reversed rather than the icon
   snapping from one glyph to the other. */
.assist-fab.is-open .assist-fab-ico {
  transform: rotate(180deg);
}

.assist-fab:active .assist-fab-ico {
  transform: rotate(180deg) scale(0.9);
}

.assist-fab.is-open {
  background: var(--surface);
  color: var(--txt1);
  box-shadow: var(--shadow-lg);
}

/* One slow ring, not a loop of them — enough to be noticed on arrival without nagging. */
.assist-fab-ping {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  border: 2px solid var(--brand);
  animation: assistPing 2.8s ease-out infinite;
  pointer-events: none;
}

@keyframes assistPing {
  0% {
    opacity: 0.55;
    transform: scale(1);
  }
  60%,
  100% {
    opacity: 0;
    transform: scale(1.5);
  }
}

.assist-panel {
  position: fixed;
  right: calc(20px + var(--safe-right));
  bottom: calc(88px + var(--safe-bottom));
  z-index: 48;
  display: flex;
  flex-direction: column;
  width: min(380px, calc(100vw - 32px));
  max-height: min(620px, calc(100dvh - 140px));
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  animation: assistOpen 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Grows from the corner the button sits in, so the panel reads as having come out of it. */
@keyframes assistOpen {
  from {
    opacity: 0;
    transform: translateY(12px) scale(0.96);
  }
}

.assist-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 15px 18px 14px;
  background: linear-gradient(140deg, color-mix(in srgb, var(--brand) 16%, var(--surface)), var(--surface));
  border-bottom: 1px solid var(--border);
}

.assist-logo {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: 11px;
  object-fit: cover;
  box-shadow: var(--shadow-sm);
}

.assist-head-main {
  min-width: 0;
}

.assist-title {
  font-size: 19px;
  color: var(--txt1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.assist-sub {
  margin-top: 2px;
  font-size: 12px;
  color: var(--txt3);
}

.assist-tabs {
  display: flex;
  gap: 6px;
  padding: 10px 14px 0;
}

.assist-tab {
  flex: 1 1 0;
  padding: 8px 10px;
  border: 0;
  border-radius: 10px 10px 0 0;
  background: transparent;
  color: var(--txt3);
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transition: color 0.14s ease, border-color 0.14s ease;
}

.assist-tab.is-on {
  color: var(--brand-ink);
  border-bottom-color: var(--brand);
}

.assist-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 14px;
}

.assist-search {
  margin-bottom: 10px;
}

.assist-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.assist-item {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface2);
  overflow: hidden;
  transition: border-color 0.14s ease;
}

.assist-item.is-open {
  border-color: var(--brand);
}

.assist-q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 11px 13px;
  border: 0;
  background: transparent;
  color: var(--txt1);
  font-size: 13.5px;
  font-weight: 650;
  text-align: left;
  cursor: pointer;
}

.assist-item.is-open .assist-q svg {
  transform: rotate(180deg);
}

.assist-a {
  margin: 0;
  padding: 0 13px 13px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--txt2);
}

.assist-empty {
  padding: 20px 4px;
  text-align: center;
}

.assist-sent {
  padding: 26px 10px;
  text-align: center;
}

.assist-tick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  margin-bottom: 12px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ok) 18%, transparent);
  color: var(--ok);
}

.assist-sent-title {
  margin-bottom: 6px;
  font-size: 16px;
  font-weight: 750;
  color: var(--txt1);
}

@media (prefers-reduced-motion: reduce) {
  .assist-panel,
  .assist-fab-ping {
    animation: none;
  }

  .assist-fab,
  .assist-fab-ico,
  .assist-item,
  .assist-tab {
    transition: none;
  }
}

/* ---------- Quick nav pad ---------- */

/* Below the drawer (60) and the lightbox (90) on purpose: this follows you everywhere, so it
   must never end up on top of something you deliberately opened. */
.quicknav {
  position: fixed;
  z-index: 45;
  width: 56px;
  height: 56px;
  padding: 4px;
  border: 0;
  border-radius: 18px;
  background: var(--surface);
  box-shadow: var(--shadow-lg);
  cursor: pointer;
  touch-action: none;
  user-select: none;
  opacity: 0.72;

  /* One transform for every state: --grab is the press, --pull the drag. Separate rules each
     setting their own transform would mean the later one silently cancelling the other. */
  transform: translateX(calc(var(--pull, 0) * -30px)) scale(var(--grab, 1));
  transition:
    transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
    box-shadow 0.3s ease,
    opacity 0.2s ease;
}

.quicknav:hover,
.quicknav.is-pulling {
  opacity: 1;
}

/* Pressed: lifts, brightens and takes a soft brand halo, so the pad answers the finger before it
   has moved anywhere. */
.quicknav.is-held {
  --grab: 1.34;
  cursor: grabbing;
  opacity: 1;
  box-shadow:
    var(--shadow-lg),
    0 0 0 7px color-mix(in srgb, var(--brand) 16%, transparent),
    0 0 34px color-mix(in srgb, var(--brand) 48%, transparent);
}

/* --pull is 0 → 1 across the gesture. Tracking the finger has to be instant, so the easing that
   makes the press and the release feel soft is dropped for the duration of the drag. */
.quicknav.is-pulling {
  --grab: 1.42;
  transition: none;
  box-shadow:
    var(--shadow-lg),
    0 0 0 8px color-mix(in srgb, var(--brand) 22%, transparent),
    0 0 calc(18px + var(--pull, 0) * 30px) color-mix(in srgb, var(--brand) 70%, transparent);
}

/* Chevrons trailing the pad, fading in with the pull. */
.quicknav-wake {
  position: absolute;
  right: calc(100% + 6px);
  top: 50%;
  display: flex;
  align-items: center;
  gap: 1px;
  transform: translateY(-50%);
  color: var(--brand);
  opacity: var(--pull, 0);
  pointer-events: none;
}

.quicknav-wake svg {
  transform: rotate(180deg);
}

.quicknav-wake svg:first-child {
  opacity: 0.45;
}

.quicknav-hint {
  position: fixed;
  right: 82px;
  z-index: 46;
  padding: 9px 14px;
  border-radius: 999px;
  background: var(--txt1);
  color: var(--bg);
  font-size: 12.5px;
  font-weight: 650;
  white-space: nowrap;
  box-shadow: var(--shadow-lg);
  pointer-events: none;
  animation: hintIn 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes hintIn {
  from {
    opacity: 0;
    transform: translateX(10px);
  }
}

/* The avatar inside is an <img>, and on a desktop browser pressing and moving one starts a native
   drag-and-drop. That swallows the pointermove stream, which is why dragging worked on a phone
   and did nothing at all with a mouse. */
.quicknav img,
.quicknav .avatar {
  -webkit-user-drag: none;
  user-select: none;
  pointer-events: none;
}

.quicknav-badge {
  position: absolute;
  top: -5px;
  left: -5px;
  min-width: 20px;
  height: 20px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--brand);
  color: #fff;
  font-size: 11px;
  font-weight: 800;
  line-height: 20px;
}

.ring-scrim {
  position: fixed;
  inset: 0;
  z-index: 85;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: var(--scrim-bg);
  backdrop-filter: blur(10px) saturate(115%);
  -webkit-backdrop-filter: blur(10px) saturate(115%);
  animation: fadeIn 0.16s ease;
}

.ring-title {
  margin-bottom: 14px;
  color: var(--txt1);
  font-size: 15px;
  font-weight: 750;
  letter-spacing: 0.02em;
  text-align: center;
}

/* The spokes sit on the ring line, so they stand half their width — 26px — proud of the circle on
   every side. Without that much room the outer ones are clipped by the scrim and the top one
   climbs over the title. */
.ring {
  /* The whole ring is drawn from these: circle size, spoke size, and the radius the spokes sit
     at. A phone overrides the three and everything else follows. */
  --ring-size: 320px;
  --spoke: 52px;
  --ring-r: 160px;

  /* Placed from the pad, not laid out by the scrim: top comes in as the pad's own centre and the
     translate puts the circle level with it, so the ring opens where your finger already is. */
  position: absolute;
  right: 26px;
  transform: translateY(-50%);
  padding: calc(var(--spoke) / 2 + 4px);
  animation: ringOpen 0.24s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Grows out of the pad rather than rising from below, which is what a menu opening from a point
   should look like. */
@keyframes ringOpen {
  from {
    opacity: 0;
    transform: translateY(-50%) scale(0.86);
  }
}

/* Taken out of the flow so .ring is exactly the circle plus its overhang — otherwise the title's
   height would push the circle off the pad it is meant to line up with. */
/* The app names its screens in the script face — this is a screen of sorts, so it wears the same
   wordmark rather than a shouty sans caption. The brand hairline under it ties the pill to the
   ring without colouring the whole thing. */
.ring .ring-title {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 4px);
  transform: translateX(-50%);
  margin: 0;
  padding: 7px 22px 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 1px solid var(--border-strong);
  color: var(--txt1);
  font-family: var(--font-script);
  font-size: 20px;
  font-weight: 700;
  letter-spacing: 0.015em;
  box-shadow: var(--shadow-lg);
  white-space: nowrap;
}

.ring .ring-title::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 5px;
  width: 34px;
  height: 2px;
  border-radius: 999px;
  transform: translateX(-50%);
  background: linear-gradient(90deg, transparent, var(--brand), transparent);
}

/* The ring carries its own dark surface rather than borrowing the page behind it. In the preview
   there is no backdrop at all, so on a light page the white rule, arrows and labels had nothing to
   sit against and simply disappeared. */
.ring-circle {
  position: relative;
  width: var(--ring-size);
  height: var(--ring-size);
  margin: 0 auto;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  box-shadow: var(--shadow-lg);
}

/* The diagonals, drawn once as one element rather than four rotated rules. */
.ring-cross {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  overflow: hidden;
  background:
    linear-gradient(45deg, transparent calc(50% - 0.5px), var(--border-strong) 50%, transparent calc(50% + 0.5px)),
    linear-gradient(-45deg, transparent calc(50% - 0.5px), var(--border-strong) 50%, transparent calc(50% + 0.5px));
}

.ring-hub {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  /* Flex-centred as well as absolutely placed: the squircle avatar used to leave uneven gaps
     inside the round rim, which read as the logo sitting off to one side. */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 5px;
  border-radius: 999px;
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--border-strong), 0 0 30px color-mix(in srgb, var(--brand) 32%, transparent);
}

/* Dark discs with a light rim, sitting on the ring line. */
.ring-spoke {
  position: absolute;
  left: 50%;
  top: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--spoke);
  height: var(--spoke);
  margin: calc(var(--spoke) / -2) 0 0 calc(var(--spoke) / -2);
  transform:
    rotate(var(--angle, 0deg))
    translate(var(--ring-r))
    rotate(calc(-1 * var(--angle, 0deg)))
    scale(var(--spoke-scale, 1));
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface2);
  box-shadow: var(--shadow-sm);
  color: var(--txt1);
  cursor: pointer;
  transition: background 0.14s ease, transform 0.14s ease, border-color 0.14s ease;
}

/* Scaled through a variable, because the transform itself is what places the spoke on the ring —
   overwriting it with a plain scale() would fling every spoke back to the middle. */
.ring-spoke:hover {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
  --spoke-scale: 1.1;
}

/* Counter-rotated with its spoke, so it stays upright wherever on the ring that spoke sits. */
.ring-spoke-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 19px;
  height: 19px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--danger);
  color: #fff;
  font-size: 10px;
  font-weight: 800;
  line-height: 19px;
  text-align: center;
  box-shadow: 0 0 0 2px var(--surface);
}

/* Named only on hover: eight labels at once would be unreadable at this size. */
.ring-spoke-label {
  position: absolute;
  top: calc(100% + 7px);
  left: 50%;
  transform: translateX(-50%);
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--txt1);
  color: var(--bg);
  font-size: 10.5px;
  font-weight: 700;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.14s ease;
}

.ring-spoke:hover .ring-spoke-label {
  opacity: 1;
}

.ring-dir {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--txt1);
  cursor: pointer;
}

.ring-dir-label {
  font-size: 9.5px;
  font-weight: 800;
  letter-spacing: 0.14em;
  color: var(--txt2);
}

/* Each arrow drifts the way it points and settles back, rather than all four pulsing in place.
   On the arrow and not the button, because the button is already using transform to position
   itself around the circle and the two would overwrite each other. */
.ring-arrow {
  display: block;
  line-height: 0;
  animation: 2.6s ease-in-out infinite;
}

@keyframes driftUp {
  0%, 100% { transform: translateY(0); opacity: 0.72; }
  50% { transform: translateY(-9px); opacity: 1; }
}

@keyframes driftDown {
  0%, 100% { transform: translateY(0); opacity: 0.72; }
  50% { transform: translateY(9px); opacity: 1; }
}

@keyframes driftLeft {
  0%, 100% { transform: translateX(0); opacity: 0.72; }
  50% { transform: translateX(-9px); opacity: 1; }
}

@keyframes driftRight {
  0%, 100% { transform: translateX(0); opacity: 0.72; }
  50% { transform: translateX(9px); opacity: 1; }
}

/* Staggered, so the four read as one slow sweep around the ring instead of four things
   twitching in lockstep. */
.ring-dir.is-up .ring-arrow { animation-name: driftUp; }
.ring-dir.is-right .ring-arrow { animation-name: driftRight; animation-delay: 0.35s; }
.ring-dir.is-down .ring-arrow { animation-name: driftDown; animation-delay: 0.7s; }
.ring-dir.is-left .ring-arrow { animation-name: driftLeft; animation-delay: 1.05s; }

.ring-dir:hover .ring-arrow {
  animation-play-state: paused;
  opacity: 1;
}

/* Parked at a radius that clears the spokes on the ring line and the hub in the middle, with
   each label on the outward side of its arrow. */
.ring-dir.is-up {
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) translateY(calc(var(--ring-r) * -0.53));
}

.ring-dir.is-up .ring-arrow svg {
  transform: rotate(-90deg);
}

.ring-dir.is-down {
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) translateY(calc(var(--ring-r) * 0.53));
}

.ring-dir.is-down .ring-arrow svg {
  transform: rotate(90deg);
}

.ring-dir.is-left {
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) translateX(calc(var(--ring-r) * -0.58));
}

.ring-dir.is-left .ring-arrow svg {
  transform: rotate(180deg);
}

.ring-dir.is-right {
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) translateX(calc(var(--ring-r) * 0.58));
}
/* The Navigator hangs off the right edge rather than sitting in the middle — it is a trail you
   pull out from where the pad lives, not a dialog about the page. */
.ring-scrim.is-right {
  justify-content: flex-end;
  align-items: center;
  padding: 24px 26px 20px;
}

/* The Navigator still lays out normally; only the ring places itself. */
.ring-scrim.is-right .ring {
  margin: 0;
}

.ring-scrim.is-right .navigator {
  align-self: stretch;
}

/* Mid-drag: the circle and nothing else. No backdrop to tap through and nothing yet to press,
   because the finger is still on the pad. */
.ring-scrim.is-preview {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  pointer-events: none;
  animation: none;
}

.ring-scrim.is-preview .ring-spoke,
.ring-scrim.is-preview .ring-dir {
  pointer-events: none;
}

.navigator {
  width: min(470px, 100%);
  display: flex;
  flex-direction: column;
  animation: slideUp 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}

.navigator .ring-title {
  text-align: center;
  font-size: 19px;
  margin-bottom: 18px;
}

.nav-trail {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Headroom so the first row has somewhere to fold into instead of starting against the edge. */
  padding: 40px 2px 10px;
  perspective: 900px;

  /* A scroll container clips at its padding box, so a folding row was being cut in half by a hard
     line. The mask turns that edge into a fade, which is what "tucked out of sight" needs — the
     row dissolves as it leaves rather than meeting a guillotine. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 46px);
  mask-image: linear-gradient(to bottom, transparent 0, #000 46px);
  scroll-behavior: smooth;
  /* The trail is the only thing on a near-black sheet; a scrollbar track down its edge would be
     the brightest element on screen. */
  scrollbar-width: none;
}

.nav-trail::-webkit-scrollbar {
  display: none;
}

/* --fold is written by QuickNav on scroll: 0 while the row sits in the body of the list, 1 once
   it has reached the top. Everything below is driven off that one number.

   Transform rather than margin, deliberately: changing layout here would move the very rows the
   scroll handler is measuring, and the two would chase each other. */
.nav-step {
  display: flex;
  align-items: center;
  gap: 0;
  margin-bottom: 9px;
  transform-origin: left center;
  transform:
    translateY(calc(var(--fold, 0) * 20px))
    rotate(calc(var(--fold, 0) * -10deg))
    scale(calc(1 - var(--fold, 0) * 0.14));
  opacity: calc(1 - var(--fold, 0) * 0.8);
  /* The lag is the point: the transition keeps chasing a target the scroll keeps moving, and the
     per-row delay turns that into a cascade down the trail. */
  transition:
    transform 0.42s cubic-bezier(0.16, 1, 0.3, 1) var(--lag, 0ms),
    opacity 0.42s ease var(--lag, 0ms),
    margin-left 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Later rows sit over earlier ones, so a folding row tucks underneath the one following it. */
.nav-step:hover {
  z-index: 2;
}

.nav-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 54px;
  height: 54px;
  border: 1px solid var(--border);
  border-radius: 15px;
  background: var(--surface2);
  color: var(--txt1);
  cursor: pointer;
  transition: background 0.14s ease, transform 0.14s ease;
}

.nav-tile:hover {
  background: var(--surface3);
  transform: scale(1.05);
}

/* Where you are now leads the trail, so it is the one solid tile. */
.nav-tile.is-here {
  border-color: var(--brand);
  background: var(--brand);
  color: #fff;
  box-shadow: 0 0 22px color-mix(in srgb, var(--brand) 45%, transparent);
}

/* Tilted and overlapping its tile, the way a label pinned to a map pin sits. The negative
   margin is what makes it overlap rather than sit in a neat column beside it. */
.nav-chip {
  display: flex;
  align-items: baseline;
  gap: 9px;
  margin-left: -10px;
  padding: 9px 16px;
  border: 0;
  border-radius: 999px;
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--txt1);
  cursor: pointer;
  transform: rotate(var(--tilt, -4.5deg));
  transform-origin: left center;
  box-shadow: var(--shadow-lg);
  transition: background 0.14s ease, transform 0.14s ease;
}

.nav-chip:hover {
  background: var(--surface3);
  transform: rotate(var(--tilt, -4.5deg)) translateX(4px);
}

.nav-chip.is-here {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}

.nav-chip.is-here:hover {
  background: var(--brand);
}

.nav-chip-label {
  font-size: 13.5px;
  font-weight: 750;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 190px;
}

.nav-chip-age {
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 650;
  opacity: 0.6;
  font-variant-numeric: tabular-nums;
}

.nav-star {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  margin-left: 6px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--txt3);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.14s ease, color 0.14s ease;
}

.nav-step:hover .nav-star,
.nav-star.is-on {
  opacity: 1;
}

/* Nothing hovers on a touch screen, so a star that only appears on hover never appears at all.
   Shown faintly instead — present enough to find, quiet enough not to compete with the label. */
@media (hover: none) {
  .nav-star {
    opacity: 0.4;
  }
}

.nav-star.is-on {
  color: var(--warn);
}

/* One row, always. Wrapping put Close on a line of its own and left the group looking broken; if
   four will not fit the row scrolls sideways instead.

   "safe center" is what keeps that honest: plain centring on an overflowing flex row pushes the
   first item off the left edge where it can never be scrolled back to. */
.nav-tabs {
  display: flex;
  gap: 8px;
  flex-wrap: nowrap;
  justify-content: safe center;
  margin-top: 14px;
  padding-bottom: 2px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}

.nav-tabs::-webkit-scrollbar {
  display: none;
}

.nav-tab {
  flex: 0 0 auto;
  padding: 9px 22px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--txt1);
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.14s ease;
}

.nav-tab:hover {
  background: var(--surface2);
}

.nav-tab.is-on {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}
@media (prefers-reduced-motion: reduce) {
  .ring,
  .navigator,
  .ring-scrim {
    animation: none;
  }

  .ring {
    transform: translateY(-50%);
  }

  .ring-arrow {
    animation: none;
    opacity: 1;
  }

  .quicknav-hint {
    animation: none;
  }

  .quicknav,
  .quicknav.is-held,
  .ring-spoke,
  .ring-dir,
  .nav-step,
  .nav-tile,
  .nav-chip {
    transition: none;
  }

  /* No fold either — it is motion tied to scrolling, which is exactly what this setting is for. */
  .nav-step {
    transform: none;
    opacity: 1;
  }

  .nav-trail {
    scroll-behavior: auto;
  }
}

/* A 360px phone cannot take a 320px ring plus 26px of spoke on each side. */
@media (max-width: 420px) {
  .ring {
    --ring-size: 232px;
    --spoke: 44px;
    --ring-r: 116px;

    right: 50%;
    transform: translate(50%, -50%);
  }

  @keyframes ringOpen {
    from {
      opacity: 0;
      transform: translate(50%, -50%) scale(0.86);
    }
  }

  .ring .ring-title {
    font-size: 17px;
    padding: 5px 18px 6px;
  }

  .ring-dir-label {
    font-size: 8.5px;
    letter-spacing: 0.1em;
  }

  .nav-tab {
    padding: 9px 16px;
    font-size: 12.5px;
  }
}

/* ---------- Message Us ---------- */

/* Sits at the end of the nav row, after the label has taken the space it needs. */
.nav-badge {
  margin-left: auto;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--danger);
  color: #fff;
  font-size: 11px;
  font-weight: 800;
  line-height: 20px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 18%, transparent);
}

/* One surface, head to compose bar. Sized to the viewport so the conversation fills the screen
   and the compose bar sits on its bottom edge — the first version let the two drift apart with a
   lake of page background between them on anything wider than a phone. */
.chat-shell {
  display: flex;
  flex-direction: column;
  /* Replaced at runtime by ChatSurface, which measures where the card actually starts. This is
     only what shows before that first measurement. */
  height: calc(100dvh - 190px);
  min-height: 320px;

  /* .page reserves 96px at the bottom for a floating action button. This screen has none, and
     without cancelling it the card is followed by a band of empty scrollable page. */
  margin-bottom: calc(14px - 96px - var(--safe-bottom));
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.chat-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--border);
  background: linear-gradient(180deg, var(--surface2), var(--surface));
}

.chat-back {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  margin-left: -4px;
  border: 0;
  border-radius: 9px;
  background: transparent;
  color: var(--txt2);
  cursor: pointer;
}

.chat-back:hover {
  background: var(--surface3);
}

.chat-head-main {
  min-width: 0;
}

.chat-head-title {
  font-size: 14px;
  font-weight: 700;
  color: var(--txt1);
}

.chat-head-sub {
  font-size: 11.5px;
  color: var(--txt3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chat-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 999px;
  background: var(--brand-soft);
  color: var(--brand-ink);
  font-weight: 800;
  font-size: 14px;
}

/* The conversation list puts the avatar before .row-main, which has no gap of its own. */
.chat-row-avatar {
  margin-right: 11px;
}

.chat-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;

  /* A scroll container clips at its padding box, and two things deliberately sit outside their
     bubble: the reaction chip hangs 11px below it, and the picker opens ~50px above it. Without
     room reserved, the last message loses its reaction and the first loses its picker. */
  padding: 54px 14px 26px;
  display: flex;
  flex-direction: column;
}

/* A hairline rule that fades out towards the edges, with the day sitting in the gap — the same
   gradient divider the POS cart uses. A filled pill read as a control you could press. */
.chat-day {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 16px 2px 8px;
  color: var(--txt3);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
}

.chat-day::before,
.chat-day::after {
  content: "";
  flex: 1;
  height: 1px;
}

.chat-day::before {
  background: linear-gradient(90deg, transparent, var(--border-strong));
}

.chat-day::after {
  background: linear-gradient(90deg, var(--border-strong), transparent);
}

.chat-bubble {
  position: relative;
  max-width: min(78%, 520px);
  width: fit-content;
  margin: 7px 0 0;
  padding: 9px 13px;
  border-radius: 17px;
  font-size: 14px;
  line-height: 1.45;
  animation: chatIn 0.18s ease;
}

/* A run from the same side closes up and drops its tail, so three in a row read as one turn. */
.chat-bubble.is-grouped {
  margin-top: 2px;
}

/* A reaction hangs below its bubble, and bubbles are siblings — so without this the next message
   simply paints over the lower half of it. The extra margin is what stops them overlapping at
   all; the z-index covers the case where one still does. */
.chat-bubble.is-reacted {
  z-index: 2;
  margin-bottom: 12px;
}

/* The open picker has to clear every bubble below it, not just the next one. */
.chat-bubble.is-picking {
  z-index: 6;
}

/* The tail corner is what separates the two sides before colour registers — which is what
   anyone who cannot rely on the colour difference is reading. */
.chat-bubble.is-me {
  margin-left: auto;
  background: var(--brand);
  color: #fff;
  border-bottom-right-radius: 6px;
}

.chat-bubble.is-me.is-grouped {
  border-top-right-radius: 6px;
}

.chat-bubble.is-them {
  margin-right: auto;
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--txt1);
  border-bottom-left-radius: 6px;
}

.chat-bubble.is-them.is-grouped {
  border-top-left-radius: 6px;
}

.chat-body {
  white-space: pre-wrap;
  word-break: break-word;
}

/* The quoted line, inside the bubble above its own text — the shape every messenger uses, so
   it needs no label to be understood. */
.chat-quote {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-bottom: 6px;
  padding: 6px 10px;
  border-left: 3px solid currentColor;
  border-radius: 6px;
  background: rgb(0 0 0 / 0.14);
  opacity: 0.82;
}

.chat-bubble.is-them .chat-quote {
  background: color-mix(in srgb, var(--txt1) 7%, transparent);
}

.chat-quote-who {
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.chat-quote-text {
  font-size: 12.5px;
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Overlapping the bubble edge rather than inside it: in the flow it would shove the timestamp
   around every time one appeared or went. */
.chat-reaction {
  position: absolute;
  bottom: -11px;
  padding: 1px 6px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  font-size: 12px;
  line-height: 1.5;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
}

.chat-bubble.is-me .chat-reaction {
  right: 10px;
}

.chat-bubble.is-them .chat-reaction {
  left: 10px;
}

/* Beside the bubble, on the side the conversation is not using, so they never cover the text.

   The spacing is padding on this element, not an offset away from the bubble. Offset, there was a
   6px dead gap: the pointer left the bubble crossing it, :hover ended, and the buttons vanished
   before you could reach them. As a padded child the hover area is continuous. */
.chat-acts {
  position: absolute;
  top: 50%;
  display: flex;
  align-items: center;
  gap: 3px;
  transform: translateY(-50%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.14s ease;
}

.chat-bubble.is-me .chat-acts {
  right: 100%;
  padding-right: 7px;
}

.chat-bubble.is-them .chat-acts {
  left: 100%;
  padding-left: 7px;
}

/* Hovering the buttons keeps the bubble hovered — they are its children — so the pair stays up
   for as long as the pointer is anywhere over either. */
.chat-acts:hover {
  opacity: 1;
  pointer-events: auto;
}

.chat-bubble:hover .chat-acts,
.chat-acts:focus-within {
  opacity: 1;
  pointer-events: auto;
}

/* On a touch screen the actions are revealed by tapping the bubble. Shown on all of them at
   once — the only other option without hover — the thread became a wall of buttons with the
   messages squeezed between them. */
@media (hover: none) {
  .chat-bubble.is-tapped .chat-acts {
    opacity: 1;
    pointer-events: auto;
  }

  .chat-act {
    width: 30px;
    height: 30px;
  }
}

.chat-act {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 27px;
  height: 27px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--txt2);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
}

.chat-act:hover {
  background: var(--surface3);
}

/* Floats above the bubble it belongs to, so the six sit over the conversation rather than pushing
   it apart. z-index because a later bubble would otherwise paint over it. */
.chat-react-bar {
  position: absolute;
  bottom: calc(100% + 6px);
  z-index: 5;
  display: flex;
  gap: 2px;
  padding: 5px 7px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface);
  box-shadow: var(--shadow-lg);
  animation: slideUpSm 0.16s cubic-bezier(0.16, 1, 0.3, 1);
}

.chat-bubble.is-me .chat-react-bar {
  right: 0;
}

.chat-bubble.is-them .chat-react-bar {
  left: 0;
}

.chat-react-pick {
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  font-size: 21px;
  line-height: 1;
  cursor: pointer;
  transition: transform 0.12s ease, background 0.12s ease;
}

.chat-react-pick:hover {
  background: var(--surface3);
  transform: scale(1.22);
}

/* The one already set stays marked, so the same row shows the current reaction and changes it. */
.chat-react-pick.is-on {
  background: var(--brand-soft);
}

.chat-replying {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px 9px 14px;
  border-top: 1px solid var(--border);
  background: var(--surface2);
}

.chat-replying-main {
  flex: 1;
  min-width: 0;
  border-left: 3px solid var(--brand);
  padding-left: 10px;
}

.chat-replying-label {
  display: block;
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--brand-ink);
}

.chat-replying-text {
  display: block;
  font-size: 12.5px;
  color: var(--txt2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chat-time {
  margin-top: 2px;
  font-size: 10px;
  opacity: 0.68;
  text-align: right;
}

@keyframes chatIn {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
}

/* Centred in the space rather than pinned to the top — an empty conversation with its prompt
   stranded above a tall gap was the whole problem with the first attempt. */
.chat-empty {
  margin: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 20px;
  text-align: center;
}

.chat-empty-title {
  font-size: 16px;
  font-weight: 750;
  color: var(--txt1);
}

.chat-empty-hint {
  max-width: 330px;
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--txt3);
}

.chat-loading {
  margin: auto;
  display: flex;
  gap: 6px;
}

.chat-dot {
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--border-strong);
  animation: chatPulse 1.1s ease-in-out infinite;
}

.chat-dot:nth-child(2) {
  animation-delay: 0.15s;
}

.chat-dot:nth-child(3) {
  animation-delay: 0.3s;
}

@keyframes chatPulse {
  0%, 100% {
    opacity: 0.35;
    transform: translateY(0);
  }
  50% {
    opacity: 1;
    transform: translateY(-3px);
  }
}

.chat-compose {
  /* The input box is sized from these, so both sides of the bar end up exactly the same height
     by construction rather than by a number someone has to keep in step by hand. */
  --chat-action-size: 38px;
  --chat-action-gap: 6px;

  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 10px;
  border-top: 1px solid var(--border);
  background: var(--surface2);
}

/* The field and its tools share one rounded box, so the tools read as belonging to what is
   being written rather than to the bar around it. */
.chat-box {
  flex: 1 1 0;
  min-width: 0;
  min-height: calc(var(--chat-action-size) * 2 + var(--chat-action-gap));
  display: flex;
  flex-direction: column;
  /* Pins the tool row to the bottom edge once the box is taller than its content. */
  justify-content: space-between;
  border: 1px solid var(--border-strong);
  border-radius: 18px;
  background: var(--surface);
  transition: border-color 0.14s ease, box-shadow 0.14s ease;
}

.chat-box:focus-within {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 16%, transparent);
}

.chat-box-tools {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 0 6px 5px;
}

/* Stacked beside the box: the quick reaction above, send below. */
.chat-actions {
  display: flex;
  flex-direction: column;
  gap: var(--chat-action-gap);
  flex: 0 0 auto;
}

/* Each one its own bordered circle — a single capsule around the pair read as one control split
   in half, not as two buttons. */
.chat-actions .chat-tool {
  width: var(--chat-action-size);
  height: var(--chat-action-size);
  margin-bottom: 0;
  border: 1px solid var(--border-strong);
  background: var(--surface);
}

.chat-actions .chat-tool:hover:not(:disabled) {
  border-color: var(--brand);
}

.chat-input {
  width: 100%;
  max-height: 140px;
  padding: 11px 14px 4px;
  border: 0;
  background: transparent;
  color: var(--txt1);
  font: inherit;
  font-size: 14px;
  line-height: 1.45;
  resize: none;
  /* Flipped to auto by ChatSurface's resize() only once the text outgrows max-height. */
  overflow-y: hidden;
}

/* The box carries the focus ring for the whole group, so the field itself must not draw one. */
.chat-input:focus {
  outline: none;
}

/* Same chrome as the attach and emoji buttons beside it — the arrow and its colour carry the
   emphasis, so a filled pill next to two ghost circles was the odd one out. */
.chat-send {
  color: var(--brand);
}

.chat-send:hover:not(:disabled) {
  background: var(--brand-soft);
  color: var(--brand-ink);
}

@media (prefers-reduced-motion: reduce) {
  .chat-bubble,
  .chat-dot {
    animation: none;
  }

  .chat-acts {
    transition: none;
  }

  .chat-react-bar {
    animation: none;
  }

  .chat-react-pick {
    transition: none;
  }

  .chat-send {
    transition: none;
  }
}

/* ---------- Chat: typing, screenshots, emoji, viewer ---------- */

/* Reuses the bubble so the indicator appears exactly where the message it precedes will. */
.chat-bubble.is-typing {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 13px 15px;
}

.chat-bubble.is-typing .chat-dot {
  background: var(--txt3);
}

/* An image with no caption needs no padding around it — the picture is the message. */
.chat-bubble.is-media {
  padding: 4px;
}

.chat-shots {
  display: grid;
  gap: 3px;
  border-radius: 13px;
  overflow: hidden;
}

.chat-shots.count-1 {
  grid-template-columns: 1fr;
}

.chat-shots.count-2,
.chat-shots.count-3 {
  grid-template-columns: 1fr 1fr;
}

/* With three, the first one leads at full width and the other two sit under it — three equal
   slivers would be too small to tell apart. */
.chat-shots.count-3 .chat-shot:first-child {
  grid-column: 1 / -1;
}

.chat-shot {
  display: block;
  padding: 0;
  border: 0;
  background: var(--surface3);
  cursor: zoom-in;
  line-height: 0;
}

.chat-shot img {
  display: block;
  width: 100%;
  max-height: 260px;
  object-fit: cover;
}

.chat-shots.count-1 .chat-shot img {
  max-height: 320px;
  object-fit: contain;
  background: var(--surface3);
}

.chat-bubble.is-media .chat-time {
  padding: 3px 7px 1px;
}

/* Staging area between the conversation and the compose bar, so what is about to be sent is
   visible without being mistaken for something already sent. */
.chat-tray {
  display: flex;
  gap: 8px;
  padding: 10px 10px 0;
  border-top: 1px solid var(--border);
  background: var(--surface2);
}

.chat-tray-item {
  position: relative;
  width: 62px;
  height: 62px;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface3);
}

.chat-tray-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.chat-tray-item.is-failed img {
  opacity: 0.4;
}

.chat-tray-remove {
  position: absolute;
  top: 3px;
  right: 3px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 19px;
  height: 19px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.62);
  color: #fff;
  cursor: pointer;
}

/* Sits over the thumbnail while the upload runs, so a slow phone photo still looks like it is
   doing something. */
.chat-tray-spinner {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 20px;
  height: 20px;
  border-radius: 999px;
  border: 2px solid rgba(255, 255, 255, 0.35);
  border-top-color: #fff;
  animation: copy-step-spin 0.7s linear infinite;
}

.chat-tray-failed {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--danger);
  font-size: 22px;
  font-weight: 800;
}

.chat-tool {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  margin-bottom: 2px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--txt2);
  cursor: pointer;
  transition: background 0.14s ease, color 0.14s ease;
}

.chat-tool:hover:not(:disabled),
.chat-tool.is-on {
  background: var(--surface3);
  color: var(--brand-ink);
}

.chat-tool:disabled {
  opacity: 0.35;
  cursor: default;
}

.chat-tool.is-sm {
  width: 30px;
  height: 30px;
  margin-bottom: 0;
}

.chat-pop-wrap {
  position: relative;
  flex: 0 0 auto;
}

/* Two rows of five, each one a whole reply. */
.quick-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 2px;
}

.quick-cell {
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: transparent;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  transition: transform 0.12s ease, background 0.12s ease;
}

.quick-cell:hover {
  background: var(--surface3);
  transform: scale(1.18);
}

/* Replaces the whole bar while recording — there is nothing else to do until it stops. Carries
   the same rounded border as the input box it stands in for, tinted so the state is obvious
   without reading anything. */
.chat-rec {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: calc(var(--chat-action-size) * 2 + var(--chat-action-gap));
  padding: 8px 8px 8px 16px;
  border: 1px solid color-mix(in srgb, var(--danger) 42%, var(--border-strong));
  border-radius: 22px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--danger) 9%, var(--surface)), var(--surface) 65%);
}

/* Bars driven by the real microphone level — see RecordingWave. */
.rec-wave {
  display: flex;
  align-items: center;
  gap: 3px;
  flex: 1;
  min-width: 0;
  height: 28px;
}

.rec-wave-bar {
  flex: 1;
  min-width: 2px;
  height: 12%;
  border-radius: 999px;
  background: var(--danger);
  opacity: 0.75;
  /* Just enough to smooth the jitter between frames without making it feel laggy. */
  transition: height 0.08s linear;
}

/* Discard and stop sit in their own bordered circles, matching the send column opposite. */
.chat-rec .chat-tool {
  width: var(--chat-action-size);
  height: var(--chat-action-size);
  margin-bottom: 0;
  border: 1px solid var(--border-strong);
  background: var(--surface);
}

.chat-rec .chat-tool:hover:not(:disabled) {
  border-color: var(--danger);
}

/* The one filled control on the bar: stopping is the action everything else here leads to. */
.chat-rec .chat-stop {
  border-color: var(--danger);
  background: var(--danger);
  color: #fff;
}

.chat-rec .chat-stop:hover:not(:disabled) {
  background: var(--danger);
  color: #fff;
  filter: brightness(1.08);
}

.chat-rec-dot {
  width: 10px;
  height: 10px;
  border-radius: 999px;
  background: var(--danger);
  animation: recPulse 1.2s ease-in-out infinite;
}

@keyframes recPulse {
  50% {
    opacity: 0.25;
  }
}

.chat-rec-time {
  font-size: 14px;
  font-weight: 750;
  color: var(--txt1);
  font-variant-numeric: tabular-nums;
}


/* Red, because it ends something. The send arrow beside it in the review step stays brand, so
   the two steps never look like the same button in two colours. */
.chat-stop {
  color: var(--danger);
}

.chat-stop:hover:not(:disabled) {
  background: var(--danger-soft);
  color: var(--danger);
}

/* Nothing is being captured any more, so the review step drops the red tint and goes back to
   the ordinary box chrome. */
.chat-rec.is-review {
  padding: 8px;
  border-color: var(--border-strong);
  background: var(--surface);
}

.chat-rec.is-review .chat-tool:hover:not(:disabled) {
  border-color: var(--brand);
}

/* The player fills the bar in review, so the clip is the thing being decided about. */
.chat-rec-player {
  flex: 1;
  min-width: 0;
  padding: 6px 14px;
  border-radius: 18px;
  background: var(--surface2);
  color: var(--txt1);
}

.chat-rec-player .voice-play {
  background: var(--brand-soft);
  color: var(--brand-ink);
}

.voice {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 190px;
  padding: 2px 0;
}

.voice-play {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.22);
  color: inherit;
  cursor: pointer;
}

.chat-bubble.is-them .voice-play {
  background: var(--brand-soft);
  color: var(--brand-ink);
}

.voice-bars {
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 1;
  height: 22px;
}

.voice-bar {
  flex: 1;
  min-width: 2px;
  border-radius: 999px;
  background: currentColor;
  opacity: 0.32;
  transition: opacity 0.1s linear;
}

.voice-bar.is-played {
  opacity: 1;
}

.voice-time {
  flex: 0 0 auto;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  opacity: 0.8;
}

.chat-tool-emoji {
  font-size: 19px;
  line-height: 1;
}

.chat-emoji-wrap {
  position: relative;
  flex: 0 0 auto;
}

/* The chrome for every chat popover. Positioned in viewport coordinates because it is portalled
   to <body> — see Popover, which sets left/bottom/width inline from the trigger's rect. */
.popover {
  position: fixed;
  z-index: 60;
  padding: 8px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--shadow-lg);
  animation: slideUpSm 0.16s cubic-bezier(0.16, 1, 0.3, 1);
}

.emoji-tabs {
  display: flex;
  gap: 2px;
  margin-bottom: 6px;
}

.emoji-tab {
  flex: 1 1 0;
  min-width: 0;
  padding: 5px 2px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--txt3);
  font-size: 11px;
  font-weight: 650;
  cursor: pointer;
}

.emoji-tab.is-on {
  background: var(--brand-soft);
  color: var(--brand-ink);
}

.emoji-grid {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 2px;
}

.emoji-cell {
  padding: 4px 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  font-size: 20px;
  line-height: 1.35;
  cursor: pointer;
}

.emoji-cell:hover {
  background: var(--surface3);
}

/* Portalled to <body>, so it covers the screen instead of being clipped by the chat card. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px 16px;
  background: rgba(0, 0, 0, 0.92);
  animation: fadeIn 0.16s ease;
}

.lightbox-img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: 6px;
  cursor: default;
}

.lightbox-close,
.lightbox-nav {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  cursor: pointer;
  transition: background 0.14s ease;
}

.lightbox-close:hover,
.lightbox-nav:hover {
  background: rgba(255, 255, 255, 0.24);
}

.lightbox-close {
  top: 14px;
  right: 14px;
  width: 38px;
  height: 38px;
}

.lightbox-nav {
  top: 50%;
  width: 42px;
  height: 42px;
  margin-top: -21px;
}

.lightbox-nav.is-prev {
  left: 12px;
  transform: rotate(90deg);
}

.lightbox-nav.is-next {
  right: 12px;
  transform: rotate(-90deg);
}

.lightbox-count {
  position: absolute;
  top: 20px;
  left: 50%;
  transform: translateX(-50%);
  color: rgba(255, 255, 255, 0.78);
  font-size: 12.5px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
  .popover,
  .chat-rec-dot,
  .lightbox {
    animation: none;
  }

  .quick-cell,
  .chat-box,
  .rec-wave-bar {
    transition: none;
  }

  .chat-tray-spinner {
    animation: none;
  }

  .chat-tool {
    transition: none;
  }
}

/* ---------- New-subscriber wizard ---------- */

/* Two steps only, so a bar of labels says where you are without the ceremony of a full
   stepper component. */
.wizard-steps {
  display: flex;
  gap: 6px;
  margin-bottom: 16px;
}

.wizard-step {
  flex: 1;
  padding: 7px 10px;
  border-radius: 10px;
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--txt3);
  font-size: 12px;
  font-weight: 650;
  text-align: center;
  transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}

.wizard-step.is-done {
  color: var(--txt2);
  border-color: var(--border-strong);
}

.wizard-step.is-on {
  background: var(--brand-soft);
  border-color: var(--brand);
  color: var(--brand-ink);
}

@media (prefers-reduced-motion: reduce) {
  .wizard-step {
    transition: none;
  }
}

/* Buttons side by side, each taking exactly half however long its label is.

   `flex: 1` on its own is not enough and `width: 100%` (what .btn-block gives) is not either:
   a flex item is laid out from its content first, so "Create subscriber" out-measures "Back"
   and the pair ends up lopsided. The `0` flex-basis is what discards the content width and
   makes the split even. */
.btn-row {
  display: flex;
  gap: 10px;

  /* Grows when it is itself a flex item — .sheet-foot is a flex row, so without this the row
     sizes to its buttons and never reaches the full width there is to split. */
  flex: 1 1 auto;
  min-width: 0;
}

.btn-row .btn {
  flex: 1 1 0;
  min-width: 0;
}

/* Generate / Copy / Show under the password field — the same split, tighter. */
.pw-actions {
  display: flex;
  gap: 8px;
  margin-top: -2px;
}

.pw-actions .btn {
  flex: 1 1 0;
  min-width: 0;
}

/* ---------- Copying data between stores ---------- */

.copy-progress-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 7px;
  font-size: 14px;
  font-weight: 650;
  color: var(--txt1);
}

.copy-progress-count {
  font-size: 12px;
  font-weight: 600;
  color: var(--txt3);
  font-variant-numeric: tabular-nums;
}

.copy-step {
  gap: 10px;
  transition: opacity 0.2s ease;
}

/* Steps not reached yet recede rather than disappear, so the whole plan stays readable and the
   list does not reflow as each one lands. */
.copy-step.is-waiting {
  opacity: 0.45;
}

.copy-step-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  border-radius: 999px;
  border: 1.5px solid var(--border-strong);
  color: var(--surface);
}

.copy-step.is-done .copy-step-mark {
  background: var(--ok);
  border-color: var(--ok);
}

/* The one in flight gets the only motion on screen — a ring being drawn, not a spinner bolted
   on beside the label. */
.copy-step.is-running .copy-step-mark {
  border-color: var(--brand);
  border-right-color: transparent;
  animation: copy-step-spin 0.7s linear infinite;
}

@keyframes copy-step-spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .copy-step.is-running .copy-step-mark {
    animation: none;
    border-color: var(--brand);
  }
}

.copy-step.is-running .row-sub,
.copy-step.is-done .row-sub {
  color: var(--txt1);
}

/* ---------- Subscription page ---------- */

.plan-now-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* The figure someone opens this page to check — it was set in the same 11px muted type as
   every incidental caption, so it read as fine print rather than as the answer. */
.plan-now-price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 7px;
  margin-top: 12px;
  font-size: 30px;
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--txt1);
}

.plan-now-per {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--txt2);
}

.plan-now-note {
  margin-top: 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--txt2);
}

.plan-now-note.is-ending {
  color: var(--danger);
}

.usage-row {
  margin-bottom: 12px;
}

.usage-row:last-child {
  margin-bottom: 0;
}

/* Sits after the count, dimmer than it — the ratio is the fact, the percentage is the gloss. */
.usage-pct {
  margin-left: 7px;
  font-weight: 600;
  color: var(--txt3);
  font-variant-numeric: tabular-nums;
}

.usage-pct.is-near {
  color: var(--warn);
}

.usage-pct.is-full {
  color: var(--danger);
}

/* ---------- Misc ---------- */

.empty {
  text-align: center;
  padding: 52px 20px;
  color: var(--txt2);
}

/* A soft brand-tinted disc gives the empty state a focal point without a cartoon emoji. */
.empty-icon {
  width: 64px;
  height: 64px;
  margin: 0 auto 14px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--brand);
  background: var(--brand-soft);
  border: 1px solid var(--glass-border);
  box-shadow: var(--shadow-sm), var(--glass-ring);
}

/* Trailing icons in rows: present, but quieter than the label. */
.row-icon {
  color: var(--txt3);
  flex: 0 0 auto;
}

.empty-title {
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 600;
  color: var(--txt1);
  margin-bottom: 4px;
}

.muted {
  color: var(--txt2);
}

.tiny {
  font-size: 12px;
}

.right {
  text-align: right;
}

.rowline {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 5px 0;
}

.rowline.total {
  border-top: 1px solid var(--border-strong);
  margin-top: 6px;
  padding-top: 10px;
  font-weight: 750;
  font-size: 18px;
}

/* What the customer still owes is the number the seller looks for first. */
.rowline.balance {
  align-items: center;
  margin-top: 2px;
  font-weight: 750;
  font-size: 15.5px;
}

.rowline.balance .val {
  font-size: 24px;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.2;
}

.rowline .val {
  font-variant-numeric: tabular-nums;
}

.spinner {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2.5px solid var(--border);
  border-top-color: var(--brand);
  animation: spin 0.7s linear infinite;
  margin: 32px auto;
}

.skeleton {
  height: 62px;
  border-radius: var(--r-md);
  background: linear-gradient(90deg, var(--surface2), var(--surface3), var(--surface2));
  background-size: 200% 100%;
  animation: shimmer 1.2s ease-in-out infinite;
  margin-bottom: 8px;
}

.upgrade {
  background: linear-gradient(135deg, var(--brand-soft), var(--gold-soft));
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 16px;
  text-align: center;
}

.divider {
  height: 1px;
  /* --border reads fine in the dark palettes but is nearly indistinguishable from a light-mode
     card's own background — this needs real contrast to actually read as a divider. */
  background: var(--border-strong);
  margin: 14px 0;
}

.login-wrap {
  position: relative;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: calc(24px + var(--safe-top)) calc(22px + var(--safe-right)) calc(28px + var(--safe-bottom))
    calc(22px + var(--safe-left));
  max-width: 460px;
  margin: 0 auto;
}

/* A flex item's default min-width is its content's own size, not 0 (the same gotcha fixed
   elsewhere in this file, e.g. .pos-products) — the plan carousel's scrollable min-content width
   was bubbling up through this card and forcing it wider than .login-wrap's own column, wide
   enough that a sliver of the next plan card poked out past its edge. */
.login-wrap > .card {
  min-width: 0;
}

/* ---------- Auth: split layout ----------

   On a phone this is a single column and .login-wrap behaves exactly as it always did — the
   brand panel below simply isn't rendered at that width. From 1024px up it becomes the usual
   two-pane sign-in: brand and what the product is on the left, the form alone on the right,
   instead of one narrow column stranded in the middle of an empty desktop window. */

.auth-split {
  min-height: 100dvh;
}

.auth-aside {
  display: none;
}

@media (min-width: 1024px) {
  .auth-split {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-items: stretch;
  }

  .auth-aside {
    position: relative;
    display: flex;
    flex-direction: column;
    padding: 56px clamp(40px, 5vw, 80px);
    overflow: hidden;
    /* Its own wash, a touch deeper than the page behind it, so the two panes read as a designed
       split rather than the form just drifting off-centre. */
    background:
      radial-gradient(80% 60% at 20% 10%, color-mix(in srgb, var(--brand) 20%, transparent), transparent 70%),
      radial-gradient(70% 55% at 90% 95%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 72%),
      color-mix(in srgb, var(--brand) 7%, var(--bg));
    border-right: 1px solid var(--glass-border);
  }

  /* auto top and bottom: takes the free space on both sides of itself, which centres this group
     in the panel and leaves the appearance bar resting on the bottom edge. */
  .auth-aside-main {
    display: flex;
    flex-direction: column;
    gap: 34px;
    margin: auto 0;
  }

  /* A horizontal lockup: the logo beside the wordmark, sized to stand as tall as both of its
     lines together, rather than stacked above it. */
  .auth-aside-brand {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 20px;
    padding: 0;
    border: none;
    background: none;
    cursor: pointer;
    text-align: left;
    transition: opacity 0.15s ease;
  }

  .auth-aside-brand:hover {
    opacity: 0.85;
  }

  /* The shared logo carries margin:0 auto for the centred phone header; here it sits inline with
     the text instead, and grows to roughly the height of the two wordmark lines next to it. */
  .auth-aside-brand .login-logo-wrap {
    margin: 0;
    width: clamp(78px, 7vw, 104px);
    height: clamp(78px, 7vw, 104px);
  }

  .auth-aside-brand .login-logo {
    width: 100%;
    height: 100%;
    border-radius: 24px;
  }

  .auth-aside-wordmark {
    margin: 0;
    font-size: clamp(38px, 4vw, 52px);
    line-height: 1.04;
    color: var(--txt1);
  }

  /* Mark above, script word below — the same stacked lockup the compact header uses, just bigger. */
  .auth-aside-wordmark .wordmark-solid {
    display: block;
    margin-right: 0;
  }

  .auth-aside-word {
    display: block;
  }

  .auth-aside-list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 16px;
    max-width: 30ch;
    font-size: 14.5px;
    line-height: 1.5;
    color: var(--txt2);
  }

  .auth-aside-list li {
    display: flex;
    align-items: flex-start;
    gap: 11px;
  }

  /* The column no longer owns the whole viewport width, so it centres within its own pane. */
  .auth-split .login-wrap {
    min-height: 100%;
    max-width: 480px;
  }

  /* The brand panel is already saying all of this, far larger, right beside it. */
  .auth-split .login-brand,
  .auth-split .auth-subtitle {
    display: none;
  }
}

/* The invoice email rendered as it will actually arrive. White background regardless of theme —
   it is an email, and email clients render it on white; previewing it on a dark card would be
   showing something the recipient will never see. */
.email-preview-frame {
  width: 100%;
  height: 460px;
  border: 1px solid var(--glass-border);
  border-radius: var(--r-md);
  background: #ffffff;
}

/* ---------- Auth: appearance bar (brand panel foot) ---------- */

.auth-appearance {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-top: 22px;
}

/* The same soft fading accent line .pos-cart::before and .sale-detail-total use, rather than a
   flat ruled border running wall to wall. */
.auth-appearance::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 1px;
  background: linear-gradient(
    to right,
    transparent,
    color-mix(in srgb, var(--brand) 32%, var(--glass-border)) 15%,
    color-mix(in srgb, var(--brand) 32%, var(--glass-border)) 85%,
    transparent
  );
}

/* Swatches and the light/dark pill share one line, under the label that heads them both —
   the pill sitting up on the label's own line left it floating with nothing beside it. */
.auth-appearance-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px 16px;
}

.auth-appearance-label {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--txt3);
}

.auth-theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  color: var(--txt2);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.auth-theme-toggle:hover {
  border-color: color-mix(in srgb, var(--brand) 40%, var(--glass-border));
  color: var(--txt1);
}

.auth-palette-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

/* The ring is drawn with box-shadow rather than a border so selecting one never nudges the row's
   layout by the border's own width. */
.auth-palette-dot {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: none;
  padding: 0;
  cursor: pointer;
  box-shadow: 0 0 0 1px var(--glass-border);
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}

.auth-palette-dot:hover {
  transform: translateY(-1px);
}

.auth-palette-dot.active {
  box-shadow:
    0 0 0 2px var(--bg),
    0 0 0 4px var(--brand);
}

/* ---------- Auth: footer links ---------- */

.auth-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  margin-top: 18px;
  text-align: center;
}

.auth-footer-line {
  margin: 0;
  font-size: 13.5px;
  color: var(--txt2);
}

.auth-footer-link {
  padding: 0;
  border: none;
  background: none;
  color: var(--brand);
  font-size: inherit;
  font-weight: 700;
  cursor: pointer;
}

.auth-footer-link:hover {
  text-decoration: underline;
}

.auth-footer-pos {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 16px;
  border-radius: 999px;
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  color: var(--txt2);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.auth-footer-pos:hover {
  border-color: color-mix(in srgb, var(--brand) 40%, var(--glass-border));
  color: var(--txt1);
}

/* ---------- POS Terminal ---------- */

/* A fixed-height app shell, not a page that scrolls — .pos-products is the one region that
   scrolls internally (its own overflow-y: auto below), so the cart sidebar next to it never
   scrolls out of view on desktop/tablet. Without this, nothing capped the shell's height, so a
   long product grid just grew the whole page and scrolled past the cart with it. */
.pos-shell {
  background: var(--bg);
  color: var(--txt1);
  height: 100dvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.pos-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  padding: calc(14px + var(--safe-top)) calc(16px + var(--safe-right)) 14px calc(16px + var(--safe-left));
  border-bottom: 1px solid var(--glass-border);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
}

/* The business name was getting clipped to "Lov…" fighting five icon buttons for the same row —
   the actions drop to their own row underneath instead of squeezing the name down further. */
@media (max-width: 620px) {
  .pos-header-identity {
    flex: 1 1 100%;
  }

  .pos-header-actions {
    flex: 1 1 100%;
    justify-content: flex-end;
  }
}

.pos-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

@media (min-width: 820px) {
  .pos-body {
    flex-direction: row;
  }
}

/* Same fixed-header-plus-scrolling-body split as .pos-header/.pos-body one level up: the scan
   tip and search box sit in a plain, never-scrolling strip, and only .pos-products-scroll below
   it has its own overflow. No position: sticky involved, so there's no stuck-offset edge case
   where a sliver of the grid shows above it mid-scroll. */
.pos-products {
  flex: 1 1 60%;
  min-width: 0;
  min-height: 240px;
  display: flex;
  flex-direction: column;
}

.pos-products-topbar {
  flex: 0 0 auto;
  position: relative;
  /* Every .pos-tile below is also position:relative — a bare z-index:1 here wasn't a safe enough
     margin over however those resolve, which let the category dropdown's panel render under the
     grid instead of over it. */
  z-index: 10;
  padding: 14px calc(16px + var(--safe-right)) 10px calc(16px + var(--safe-left));
  background: var(--bg);
  /* A translucent black shadow alone reads fine in light mode but all but disappears against a
     dark background — the border is the part that's actually guaranteed visible either way,
     since --border-strong is an opaque, theme-tuned color rather than a black with alpha. */
  border-bottom: 1px solid var(--border-strong);
  box-shadow: 0 10px 14px -10px rgba(0, 0, 0, 0.3);
}

.pos-products-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 0 calc(16px + var(--safe-right)) 14px calc(16px + var(--safe-left));
}

.pos-topbar-actions {
  display: flex;
  gap: 8px;
}

/* The inline category dropdown next to search — same elegant filterable select Inventory Report
   uses. Shares .pos-topbar-actions' own breakpoint below: a phone-width screen has no room for it
   either, so narrow screens fall back to the Filters sheet's category chips instead. */
.pos-category-inline {
  flex: 0 0 auto;
  display: flex;
}

/* Three separate icon buttons on a phone-width screen squeeze the search box down to almost
   nothing — below 620px they collapse into one "Tools" button (.pos-tools-btn) opening a sheet
   with the same three actions instead. */
.pos-tools-btn {
  display: none;
  position: relative;
}

@media (max-width: 619px) {
  .pos-topbar-actions {
    display: none;
  }

  .pos-category-inline {
    display: none;
  }

  .pos-tools-btn {
    display: inline-flex;
  }
}

/* The search row's Filters button gets a small dot when a non-default category or pricing mode
   is active, so that's visible without opening the sheet. */
.pos-filters-btn {
  position: relative;
}

.pos-filters-dot {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--brand);
  border: 2px solid var(--surface);
}

/* Below 820px the floating cart button sits on top of this scroll area — without extra bottom
   room, its last row (the "Showing X of Y" / Load more) ends up underneath it. On .pos-products-
   scroll (the part that actually scrolls), not .pos-products itself — that's just a non-scrolling
   flex wrapper now, so padding there was only ever adding dead space at the very bottom. */
@media (max-width: 819px) {
  .pos-products-scroll {
    padding-bottom: calc(84px + var(--safe-bottom));
  }
}

.pos-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 10px;
  margin-top: 12px;
}

/* The actual root cause of "the first tile looks different": .card + .card (below) adds
   margin-top to any .card preceded by another .card — meant for cards stacked in a single column,
   but every grid tile here is a DOM sibling of the one before it, so it silently fired on every
   tile except the very first one. Grid spacing already comes from .pos-grid's own `gap`. */
.pos-tile + .pos-tile {
  margin-top: 0;
}

/* Layered onto .card — only the grid-item-specific bits live here. A fixed height (not min-height)
   so every tile matches regardless of name length — grid rows otherwise stretch to their tallest
   item, which is exactly what made some rows visibly taller than others. */
.pos-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* Top-aligned, not centered — centering floated a short one-line name like "10ft" lower than a
     longer, two-line name that fills more of the fixed height, which read as misaligned. */
  justify-content: flex-start;
  gap: 4px;
  text-align: left;
  /* Tall enough for the photo band on top (now 112px plus 6px clearance into the text below)
     plus the same text stack every tile already had — every tile reserves that band, photo or
     not, so a grid mixing both stays even. */
  height: 234px;
  /* A flex item's default min-width is its content's own size, not 0 — without this the item
     name refused to wrap and ran past the tile's edge instead, exactly what was clipping off. */
  min-width: 0;
  width: 100%;
  cursor: pointer;
  /* A touch warmer/brighter than a plain .card so a whole grid of these reads as glass, not just
     a flat panel — the base glass tokens are tuned for big single panels, too subtle at this
     tile size to notice. */
  background: linear-gradient(165deg, color-mix(in srgb, var(--brand) 9%, var(--glass-bg)), var(--glass-bg) 65%);
  border-color: color-mix(in srgb, var(--brand) 14%, var(--glass-border));
  box-shadow: var(--shadow-sm), var(--glass-ring);
  transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease, background 0.16s ease;
}

/* A brighter, brand-tinted sheen than the plain .card::before gets — same technique (a soft
   highlight band along the top), just turned up so it actually reads at this tile size. */
.pos-tile::before {
  background: linear-gradient(165deg, color-mix(in srgb, var(--brand) 20%, transparent), rgba(255, 255, 255, 0.06) 42%, transparent 75%);
  opacity: 1;
}

@media (hover: hover) {
  .pos-tile:hover {
    transform: translateY(-2px);
    border-color: color-mix(in srgb, var(--brand) 30%, var(--glass-border));
    box-shadow: var(--shadow-md), var(--glass-ring);
  }
}

.pos-tile-name {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  word-break: break-word;
}

/* The photo band on top — bled to the card's own edges rather than inset by .card-pad's 14px
   padding like the text below it, so the image reads as full-bleed instead of floating with a
   visible margin. The negative margin cancels exactly that padding (14px is .card-pad's own
   value, not a variable, so it's hardcoded here too); .pos-tile's own overflow:hidden + rounded
   corners clip the image's top corners to match, so no radius is needed here. */
.pos-tile-media {
  width: calc(100% + 28px);
  height: 112px;
  flex: 0 0 auto;
  margin: -14px -14px 6px;
  overflow: hidden;
  /* Mostly clipped on the left/right/top by .pos-tile's own overflow:hidden now that the image is
     flush with those edges — what's left is a soft separator shadow under the photo, into the
     text below. */
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.22);
}

.pos-tile-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pos-tile-img-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--brand) 12%, var(--surface2));
  color: color-mix(in srgb, var(--brand) 45%, var(--txt3));
}

.pos-tile:active {
  border-color: var(--brand);
  background: var(--brand-soft);
}

/* Already in the cart — a brand-tinted tile plus a corner badge carrying the quantity, the same
   "added" language a shopping-cart badge uses elsewhere, so a glance at the grid alone says what's
   already been rung up without checking the cart column. */
.pos-tile.is-in-cart {
  border-color: var(--brand);
  background: var(--brand-soft);
}

.pos-tile-cart-badge {
  /* Inset, not overlapping the edge — .card clips anything outside its own box (so its rounded
     corners stay clean), which cut this down to a sliver when it sat half outside the border. */
  position: absolute;
  top: 7px;
  right: 7px;
  min-width: 20px;
  height: 20px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--brand);
  color: #fff;
  font-size: 11px;
  font-weight: 800;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-sm);
  line-height: 1;
}

/* Opposite corner from the cart-quantity badge — an item on an active promo, visible on the tile
   without opening it. */
.pos-tile-promo-badge {
  position: absolute;
  top: 7px;
  left: 7px;
  padding: 2px 8px;
  border-radius: 999px;
  background: linear-gradient(135deg, var(--gold), var(--brand));
  color: var(--brand-contrast);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.02em;
  box-shadow: var(--shadow-sm);
  line-height: 1.4;
  max-width: calc(100% - 14px);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Out of stock — still visible (so the cashier can see it exists and why it's unavailable) but
   visibly inert, same muting convention as a disabled button elsewhere in the app. */
.pos-tile.is-out-of-stock {
  opacity: 0.55;
  cursor: not-allowed;
}

.pos-tile-stock-badge {
  font-size: 10px;
  padding: 1px 6px;
  line-height: 1.5;
  /* Pinned to the bottom of the (fixed-height) tile regardless of how many lines the name above
     it takes — without this, a 1-line name left the badge sitting higher than a 2-line name's,
     so tiles never lined up with each other despite being the same height. */
  margin-top: auto;
  align-self: flex-start;
}

.pos-cart {
  /* Below 820px this moves into a drawer (the floating .pos-cart-fab opens it as a Sheet)
     instead of sitting in the document below a product grid that can run to hundreds of rows. */
  display: none;
  position: relative;
  flex: 1 1 40%;
  min-width: 0;
  flex-direction: column;
}

@media (min-width: 820px) {
  .pos-cart {
    display: flex;
  }
}

@media (min-width: 820px) {
  .pos-cart-fab {
    display: none;
  }
}

/* A soft, fading accent line rather than a flat ruled border — brand-tinted so it reads as part
   of the app's own glass styling instead of a plain CSS divider. */
.pos-cart::before {
  content: '';
  position: absolute;
  background: linear-gradient(
    to right,
    transparent,
    color-mix(in srgb, var(--brand) 32%, var(--glass-border)) 15%,
    color-mix(in srgb, var(--brand) 32%, var(--glass-border)) 85%,
    transparent
  );
  left: 0;
  right: 0;
  top: 0;
  height: 1px;
}

@media (min-width: 820px) {
  .pos-cart {
    max-width: 400px;
  }

  .pos-cart::before {
    top: 0;
    bottom: 0;
    left: 0;
    right: auto;
    width: 1px;
    height: auto;
    background: linear-gradient(
      to bottom,
      transparent,
      color-mix(in srgb, var(--brand) 32%, var(--glass-border)) 12%,
      color-mix(in srgb, var(--brand) 32%, var(--glass-border)) 88%,
      transparent
    );
  }
}

.pos-cart-body {
  flex: 1;
  min-height: 160px;
  overflow-y: auto;
  padding: 10px calc(14px + var(--safe-right)) 10px calc(14px + var(--safe-left));
}

/* Same soft fading accent line as .pos-cart::before, used above a sheet's closing total row
   instead of a flat ruled border. */
.sale-detail-total {
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-top: 16px;
  padding-top: 18px;
}

.sale-detail-total::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 1px;
  background: linear-gradient(
    to right,
    transparent,
    color-mix(in srgb, var(--brand) 32%, var(--glass-border)) 15%,
    color-mix(in srgb, var(--brand) 32%, var(--glass-border)) 85%,
    transparent
  );
}

.pos-cart-foot {
  padding: 14px calc(16px + var(--safe-right)) calc(16px + var(--safe-bottom)) calc(16px + var(--safe-left));
  border-top: 1px solid var(--glass-border);
}

/* ---------- Legal pages (privacy) ---------- */
.legal-wrap {
  max-width: 680px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  padding: calc(20px + var(--safe-top)) calc(22px + var(--safe-right)) calc(44px + var(--safe-bottom))
    calc(22px + var(--safe-left));
  color: var(--txt1);
}

.legal-back {
  align-self: flex-start;
  padding: 6px 2px;
  border: none;
  background: none;
  color: var(--txt2);
  font-size: 13px;
  font-weight: 600;
  transition: color 0.15s ease;
}
.legal-back:hover {
  color: var(--brand);
}

.legal-title {
  font-family: var(--font-display);
  font-size: 30px;
  font-weight: 600;
  margin: 14px 0 2px;
  color: var(--txt1);
}
.legal-updated {
  margin: 0 0 26px;
  font-size: 12.5px;
  color: var(--txt3);
}

.legal-body {
  color: var(--txt2);
  line-height: 1.7;
  font-size: 14.5px;
}
.legal-body h2 {
  color: var(--txt1);
  font-size: 15px;
  font-weight: 700;
  margin: 26px 0 8px;
}
.legal-body p {
  margin: 0 0 12px;
}
.legal-body ul {
  margin: 0 0 12px;
  padding-left: 20px;
}
.legal-body li {
  margin: 0 0 6px;
}
.legal-body strong {
  color: var(--txt1);
  font-weight: 650;
}
.legal-mail {
  color: var(--brand);
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.legal-mail:hover {
  border-bottom-color: var(--brand);
}

/* ---------- Changelog ---------- */

/* A real timeline — an icon "node" per release threaded on one continuous rail, rather than a
   stack of boxed cards. Every colour here is a token, so it repaints correctly across all 6
   palettes and both light/dark, the same as the rest of the app. */
.changelog-timeline {
  display: flex;
  flex-direction: column;
  gap: 28px;
}

.changelog-entry {
  position: relative;
  display: flex;
  gap: 16px;
}

.changelog-entry-rail {
  position: relative;
  flex: 0 0 auto;
  width: 34px;
}

/* The rail itself: one line per entry, stretched past this entry's own height into the 28px gap
   below so it meets the next node's top exactly — reads as one unbroken line down the page. The
   node's own opaque background then simply paints over it where they cross. */
.changelog-entry-rail::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: -28px;
  left: 50%;
  width: 2px;
  margin-left: -1px;
  background: var(--border);
}

.changelog-entry:last-child .changelog-entry-rail::before {
  display: none;
}

.changelog-entry-node {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--surface2);
  color: var(--txt2);
  border: 1px solid var(--border);
}

/* The newest release — a filled, brand-coloured node (echoing the reference's checkmarked
   "New Version" row) instead of the same quiet grey circle as the rest of the history. */
.changelog-entry.is-latest .changelog-entry-node {
  background: var(--brand);
  color: var(--brand-contrast);
  border-color: var(--brand);
}

.changelog-entry-card {
  flex: 1;
  min-width: 0;
  padding-bottom: 2px;
}

.changelog-entry-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.changelog-date-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 4px 0 14px;
}

.changelog-date {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--brand);
}

.changelog-release-title {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 650;
  color: var(--txt1);
  margin: 0;
}

.changelog-items {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 11px;
}

.changelog-item {
  display: flex;
  align-items: baseline;
  gap: 10px;
  color: var(--txt2);
  font-size: 14px;
  line-height: 1.55;
}

.changelog-tag {
  flex: 0 0 auto;
  transform: translateY(1px);
}

.changelog-show-more {
  display: block;
  margin: 24px auto 0;
  padding: 10px 20px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--txt1);
  font-size: 13.5px;
  font-weight: 600;
}
.changelog-show-more:hover {
  border-color: var(--brand);
  color: var(--brand);
}

/* ---------- Sign-up stepper + plan picker ---------- */
.signup-steps {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin: 0 auto 18px;
}
.signup-step {
  display: flex;
  align-items: center;
  gap: 7px;
  color: var(--txt3);
  font-size: 12px;
  font-weight: 600;
}
.signup-step-dot {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--txt2);
  font-size: 12px;
  flex: 0 0 auto;
}
.signup-step.current {
  color: var(--txt1);
}
.signup-step.current .signup-step-dot {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-contrast);
}
.signup-step.done .signup-step-dot {
  background: color-mix(in srgb, var(--brand) 18%, transparent);
  border-color: transparent;
  color: var(--brand);
}
.signup-step:not(:last-child)::after {
  content: '';
  width: 18px;
  height: 1px;
  background: var(--border);
  margin-left: 6px;
}

/* The outer white card around the plan carousel — narrowed to hug one plan card's own width
   instead of sitting at the full ~460px login column width that every other step of this form
   uses. That extra width was exactly the room a sliver of the next card kept finding to show in. */
.plan-step-card {
  max-width: 340px;
  width: 100%;
  margin: 0 auto;
}

/* Stays fully inside the parent .card-pad's own padding — an earlier version bled out to that
   card's outer edge for a peek of the next plan, which butted the carousel's square corners
   straight into the parent's rounded ones. One full card per view instead (the established
   pattern for this: see shadcnblocks' pricing carousels), no peek to clash with anything. */
.plan-picker {
  display: flex;
  /* Not stretch — a short plan (Free) stretched to match a longer sibling's height left a wall of
     empty space at the bottom of its card. Each one is only as tall as its own content now. */
  align-items: flex-start;
  overflow-x: auto;
  /* Hidden, paired with the explicit height set inline from JS. Every card in this row — the
     off-screen ones included — contributes to the row's own height, so the box is naturally as
     tall as the TALLEST plan no matter which one is in view, and a min-height smaller than that
     does nothing at all. Forcing the height down to the active card's own and clipping the rest
     is the only thing that actually collapses that dead space. */
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  /* Narrows the card itself (not a bleed-and-cancel trick, which is what caused the sub-pixel
     sliver bug last time) — real padding, so the card sits inset with visible room on both sides
     instead of running edge to edge. */
  padding: 0 16px;
  /* Animates to match whichever card is actually in view (set inline, from JS) instead of jumping. */
  transition: height 0.2s ease;
  /* The dots below are the only scroll indicator this needs. */
  scrollbar-width: none;
}

.plan-picker::-webkit-scrollbar {
  display: none;
}

.plan-carousel-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin-top: 8px;
}

.plan-carousel-arrow {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--txt2);
  flex: 0 0 auto;
}

.plan-carousel-arrow[disabled] {
  opacity: 0.35;
}

.plan-carousel-dots {
  display: flex;
  align-items: center;
  gap: 6px;
}

.plan-carousel-dot {
  width: 7px;
  height: 7px;
  border-radius: 999px;
  border: none;
  padding: 0;
  background: var(--border-strong);
  transition: background 0.2s ease, width 0.2s ease;
}

.plan-carousel-dot.active {
  width: 18px;
  background: var(--brand);
}

.plan-card {
  position: relative;
  display: block;
  /* flex-grow:0 — growing was pointless in an already-overflowing row, and left enough sub-pixel
     slack in the width math for the next card's edge to show through on the side. */
  flex: 0 0 100%;
  width: 100%;
  box-sizing: border-box;
  scroll-snap-align: start;
  text-align: left;
  padding: 16px 18px;
  border-radius: var(--r-lg);
  border: 1.5px solid var(--border);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.plan-card.selected {
  border-color: var(--brand);
  background: var(--brand-soft);
  box-shadow: var(--shadow-md);
}
.plan-card-check {
  position: absolute;
  top: 12px;
  right: 12px;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--brand);
  color: var(--brand-contrast);
  opacity: 0;
  transform: scale(0.8);
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.plan-card.selected .plan-card-check {
  opacity: 1;
  transform: scale(1);
}
.plan-card-name {
  font-weight: 700;
  font-size: 15px;
  color: var(--txt1);
}
.plan-card-price {
  font-size: 22px;
  font-weight: 800;
  color: var(--brand);
  margin-top: 2px;
}
.plan-card-per {
  font-size: 13px;
  font-weight: 600;
  color: var(--txt2);
}
.plan-card-trial {
  font-size: 12px;
  font-weight: 600;
  color: var(--ok);
  margin-top: 3px;
}
.plan-card-trial.muted {
  color: var(--txt3);
}
.plan-card-desc {
  font-size: 12.5px;
  color: var(--txt2);
  margin-top: 6px;
  line-height: 1.45;
}

/* Three overlapping washes on a lifted base — warm and lit rather than flat dark. */
.login-wrap::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(95% 55% at 50% -8%, var(--login-glow-1), transparent 68%),
    radial-gradient(75% 45% at 6% 104%, var(--login-glow-2), transparent 70%),
    radial-gradient(70% 45% at 104% 88%, var(--login-glow-3), transparent 70%),
    var(--login-base);
}

/* Wraps the logo + wordmark so tapping either one heads back to the landing page — a plain reset,
   same centred column the two children already laid out in before this became a button. */
.login-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  transition: opacity 0.15s ease;
}
.login-brand:hover {
  opacity: 0.85;
}

.link-reset {
  display: block;
  margin: 8px 0 2px;
  padding: 0;
  border: none;
  background: none;
  color: var(--brand);
  font-size: 13px;
  font-weight: 600;
  text-align: right;
  width: 100%;
}
.link-reset:hover {
  text-decoration: underline;
}

/* Same lockup as the landing page's hero logo: a soft brand glow breathing behind a squircle (not
   a full circle — the icon's checkmark badge is anchored to its corner and looks off-balance once
   cropped into a perfect circle), instead of the flat glass-ring box-shadow this used to carry. */
.login-logo-wrap {
  position: relative;
  display: inline-flex;
  width: 88px;
  height: 88px;
  margin: 0 auto 14px;
}

.login-logo-wrap::before {
  content: '';
  position: absolute;
  inset: -22px;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--brand) 38%, transparent), transparent 72%);
  animation: heroGlowPulse 3.4s ease-in-out infinite;
}

.login-logo {
  position: relative;
  display: block;
  width: 88px;
  height: 88px;
  border-radius: 20px;
  object-fit: cover;
  box-shadow: var(--shadow-md);
}

/* ---------- Animations ---------- */

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(24px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slideUpSm {
  from {
    opacity: 0;
    transform: translateY(10px) scale(0.99);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes slideIn {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

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

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* Content column wrapper — sits beside the sidebar on desktop, full width on mobile. */
.app-main {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
}

/* Desktop: the menu becomes a pinned, in-flow sidebar (no scrim, no overlay). The appbar burger
   collapses/expands it rather than opening a modal. Mobile is untouched — it keeps the drawer. */
@media (min-width: 1024px) {
  .app {
    flex-direction: row;
    align-items: stretch;
  }

  .app .scrim {
    display: none !important;
  }

  .app .drawer {
    position: sticky;
    top: 0;
    height: 100dvh;
    flex: 0 0 272px;
    width: 272px;
    transform: none !important;
    box-shadow: none;
    transition: width 0.2s ease, flex-basis 0.2s ease, padding 0.2s ease;
  }

  /* Collapsed = an icon-only rail, not fully tucked away — the appbar burger still brings back
     full labels, but a narrow rail stays useful (and visibly present) in between. */
  .app.nav-collapsed .drawer {
    flex-basis: 76px;
    width: 76px;
    padding-left: 0;
    padding-right: 0;
  }

  .app.nav-collapsed .drawer-head {
    padding: 0 10px 16px;
  }

  .app.nav-collapsed .drawer-brand {
    justify-content: center;
  }

  /* Only the avatar stays — name, email and the plan badge have nowhere to go in 76px. */
  .app.nav-collapsed .drawer-brand > :not(.drawer-logo),
  .app.nav-collapsed .drawer-plan,
  .app.nav-collapsed .drawer-foot {
    display: none;
  }

  .app.nav-collapsed .drawer-item {
    position: relative;
    justify-content: center;
    gap: 0;
    padding: 12px 0;
  }

  /* The unread count is the one thing here worth keeping in a bare icon column — but inline it
     took the remaining width and pushed its icon out of line with every other one. Pinned to the
     icon corner instead, so the column stays a straight run of icons. */
  .app.nav-collapsed .nav-badge {
    position: absolute;
    top: 8px;
    left: calc(50% + 2px);
    margin-left: 0;
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    font-size: 9.5px;
    line-height: 17px;
    box-shadow: 0 0 0 2px var(--surface);
  }

  /* The label, flags and badges all read as text next to an icon — none of it fits a bare icon
     column, and the custom rail-flyout tooltip below is what replaces it on hover instead. */
  .app.nav-collapsed .drawer-item-label,
  .app.nav-collapsed .drawer-item .badge,
  .app.nav-collapsed .drawer-item .nav-flag,
  .app.nav-collapsed .drawer-group-chevron {
    display: none;
  }

  /* Owner Access can't expand inline with no room for the indented list below it — its rail-flyout
     popover (rendered via portal, see AppShell.tsx) replaces this entirely while collapsed. */
  .app.nav-collapsed .drawer-subnav {
    display: none;
  }
}

/* The collapsed rail's own tooltip/popover. Portaled to <body> (not a child of .drawer) so the
   rail's vertical scrollbar can never clip it sideways, and styled like the app's own floating
   panels — a surface card with a border and shadow — rather than a plain native title tooltip or
   a small dark Bootstrap-style box. */
.rail-flyout {
  position: fixed;
  z-index: 70;
  transform: translateY(-50%);
  background: var(--surface);
  color: var(--txt1);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  padding: 9px 14px;
  font-size: 13.5px;
  font-weight: 600;
  white-space: nowrap;
  pointer-events: none;
  animation: railFlyoutIn 0.14s ease both;
}

.rail-flyout::before {
  content: '';
  position: absolute;
  left: -4px;
  top: 50%;
  width: 9px;
  height: 9px;
  transform: translateY(-50%) rotate(45deg);
  background: var(--surface);
  border-left: 1px solid var(--border-strong);
  border-bottom: 1px solid var(--border-strong);
}

@keyframes railFlyoutIn {
  from {
    opacity: 0;
    transform: translateY(-50%) translateX(-6px);
  }
  to {
    opacity: 1;
    transform: translateY(-50%) translateX(0);
  }
}

/* The Owner Access popover needs real interaction (clickable sub-pages), unlike a plain tooltip —
   it stays open while the cursor travels onto it instead of disappearing the instant it leaves
   the trigger icon (AppShell.tsx's scheduleHide grace period handles the gap in between). */
.rail-flyout-group {
  padding: 8px;
  pointer-events: auto;
  min-width: 220px;
}

.rail-flyout-title {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--txt3);
  padding: 4px 10px 8px;
}

.rail-flyout-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 9px 10px;
  border-radius: var(--r-sm);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--txt1);
}

.rail-flyout-item:hover {
  background: var(--surface2);
  color: var(--brand);
}

/* Thin, elegant scrollbar tinted with the brand accent (translucent so it reads the same in light
   and dark). Applied to the sidebar and the main document scroller so both match. */
.drawer,
html {
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--accent) 42%, transparent) transparent;
}
.drawer::-webkit-scrollbar,
html::-webkit-scrollbar {
  width: 6px;
}
.drawer::-webkit-scrollbar-track,
html::-webkit-scrollbar-track {
  background: transparent;
}
.drawer::-webkit-scrollbar-thumb,
html::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--accent) 42%, transparent);
  border-radius: 999px;
}
.drawer::-webkit-scrollbar-thumb:hover,
html::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--accent) 68%, transparent);
}

/* ---------- Appearance picker ---------- */

.appearance-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}

/* Same fix as .pos-tile above: .card + .card's margin-top fires on every card here except the
   first (each is a DOM sibling of the one before it), which is exactly what made Rose — the first
   palette — stretch taller than Jade next to it. Spacing already comes from the grid's own gap. */
.appearance-card + .appearance-card {
  margin-top: 0;
}

@media (max-width: 420px) {
  .appearance-grid {
    grid-template-columns: 1fr;
  }
}

/* Same .card glass/shadow treatment as every other panel — only the content differs. */
.appearance-card {
  position: relative;
  display: block;
  width: 100%;
  text-align: left;
  padding: 0;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.appearance-card:active {
  transform: scale(0.98);
}

.appearance-card.is-selected {
  border-color: var(--brand);
  box-shadow: var(--shadow-md), 0 0 0 2px var(--brand-soft);
}

.appearance-card:disabled {
  cursor: default;
  opacity: 0.7;
}

.appearance-card-pad {
  padding: 12px;
}

.appearance-swatch {
  position: relative;
  height: 44px;
  border-radius: var(--r-md);
  border: 1px solid var(--glass-border);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 12px;
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}

.appearance-dot {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
  flex: 0 0 auto;
}

.appearance-name {
  font-weight: 700;
  font-size: 14px;
  color: var(--txt1);
  margin-top: 10px;
}

.appearance-blurb {
  font-size: 11.5px;
  color: var(--txt2);
  margin-top: 2px;
  line-height: 1.4;
}

.appearance-check {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--brand);
  color: var(--brand-contrast);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-sm);
}

/* ---------- Loyalty card ---------- */

/* On screen, a comfortable 2x of the real card so the design can actually be read; for print,
   .loyalty-card-print below locks it to the genuine ISO/IEC 7810 ID-1 size (85.6 x 54mm) so it
   trims to an actual wallet card instead of printing at some arbitrary browser-chosen scale. */
.loyalty-card-face {
  /* Fixed "Midnight Foil" look, deliberately NOT the shop's own --brand/--accent theme — a
     physical rewards card reads as one consistent, deliberately designed object, not something
     that happens to look different depending on which of the 6 in-app palettes is picked. This
     --accent override only reaches this card's own descendants (.loyalty-card-label etc. below
     already read var(--accent)), everything else in the app keeps its real theme accent. */
  --accent: #d4af6a;
  position: relative;
  width: 336px;
  aspect-ratio: 85.6 / 54;
  border-radius: 16px;
  overflow: hidden;
  background: linear-gradient(135deg, #0b1220, #1c2b4a 55%, #2b4a80);
  color: #f5e9c9;
  box-shadow: var(--shadow-md);
  flex: 0 0 auto;
}

.loyalty-card-watermark {
  position: absolute;
  top: -0.15em;
  right: 0.1em;
  font-family: var(--font-card);
  font-style: italic;
  font-size: 150px;
  line-height: 1;
  color: rgba(255, 255, 255, 0.08);
  pointer-events: none;
  user-select: none;
}

.loyalty-card-content {
  position: relative;
  height: 100%;
  padding: 18px 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.loyalty-card-logo {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  object-fit: cover;
  background: #fff;
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.6);
  margin-bottom: 8px;
}

.loyalty-card-business {
  font-family: var(--font-card);
  font-weight: 700;
  font-size: 18px;
}

.loyalty-card-label {
  margin-top: 2px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.2em;
  color: var(--accent);
}

.loyalty-card-rule {
  margin-top: 8px;
  width: 36px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
}

.loyalty-card-spacer {
  flex: 1;
  min-height: 10px;
}

.loyalty-card-client {
  font-family: var(--font-card);
  font-size: 15px;
  font-weight: 600;
}

.loyalty-card-number {
  margin-top: 6px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.1em;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

.loyalty-card-contact {
  font-size: 9.5px;
  color: rgba(255, 255, 255, 0.85);
}

.loyalty-card-qr-wrap {
  margin-top: auto;
  padding: 8px;
  border-radius: 10px;
  background: #fff;
  box-shadow: var(--shadow-sm);
}

.loyalty-card-qr {
  display: block;
  width: 72px;
  height: 72px;
}

.loyalty-card-rates {
  margin-top: auto;
  padding-top: 10px;
  font-size: 9.5px;
  font-weight: 600;
  color: var(--accent);
}

.loyalty-card-terms {
  margin-top: 6px;
  font-size: 8px;
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.65);
}

.loyalty-card-preview-row {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  justify-content: center;
  padding: 6px 0;
}

.loyalty-card-preview-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

/* No @media print rules here on purpose — printing the card now goes through a real,
   client-built PDF (see LoyaltyCardsPage's buildPdf) instead of window.print() on this page.
   The custom @page size that needed turned out not to be honoured consistently (confirmed: the
   print driver this was tested against ignored it and fell back to a full-sized default page
   instead), where a PDF's own embedded page size is something every viewer gets right regardless
   of browser or driver. .loyalty-card-print/.loyalty-card-caption below are now just plain
   on-screen layout classes. */

/* ---------- Scanner reticle ---------- */

/* box-shadow rather than a separate overlay element: a shadow spread of 9999px, clipped by the
   parent's own overflow:hidden, dims everything outside this box while leaving the inside
   completely untouched — a cheap, robust way to spotlight the frame regardless of the actual
   video aspect ratio, where a plain border just floated over an otherwise unchanged feed. */
.scanner-frame {
  position: absolute;
  inset: 18% 12%;
  border-radius: 14px;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
}

.scanner-corner {
  position: absolute;
  width: 26px;
  height: 26px;
  border: 3px solid #fff;
  opacity: 0.9;
}

.scanner-corner.tl {
  top: -3px;
  left: -3px;
  border-right: none;
  border-bottom: none;
  border-top-left-radius: 10px;
}

.scanner-corner.tr {
  top: -3px;
  right: -3px;
  border-left: none;
  border-bottom: none;
  border-top-right-radius: 10px;
}

.scanner-corner.bl {
  bottom: -3px;
  left: -3px;
  border-right: none;
  border-top: none;
  border-bottom-left-radius: 10px;
}

.scanner-corner.br {
  bottom: -3px;
  right: -3px;
  border-left: none;
  border-top: none;
  border-bottom-right-radius: 10px;
}
