/* ==========================================================================
   Design tokens (adapted from the "modern POS" reference Richard supplied)
   ========================================================================== */
:root {
    --bg-main: #F8FAFC;
    --bg-card: #FFFFFF;
    --text-main: #1E293B;
    --text-muted: #64748B;
    --accent: #2563EB;
    /* Text/icon colour that sits ON an --accent fill. White for every dark
       accent; gold overrides it, because a gold light enough to read as gold
       cannot carry white text at an accessible contrast ratio. */
    --accent-fg: #fff;
    --accent-hover: #1D4ED8;
    --success: #10B981;
    --success-bg: #D1FAE5;
    --warning: #F59E0B;
    --warning-bg: #FEF3C7;
    --danger: #EF4444;
    --danger-bg: #FEE2E2;
    /* The Today's Sales donut. Fixed rather than themed: --accent follows the
       shop's chosen colour, so a shop on the green or gold accent got a "card"
       slice that was green or gold - the same colour as cash, or as credit.
       A chart legend has to mean the same thing in every shop. */
    --chart-cash: #10B981;      /* green  */
    --chart-card: #2563EB;      /* blue   */
    --chart-credit: #F59E0B;    /* yellow */
    --chart-payphone: #8B5CF6;  /* violet - must not read as the blue card slice */
    --chart-refund: #EF4444;    /* red    */
    --card-accent-bg: #DBEAFE;
    --border-color: #E2E8F0;

    --radius-lg: 16px;
    --radius-md: 12px;
    --radius-sm: 8px;
    --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
    --shadow-premium: 0 10px 15px -3px rgba(0, 0, 0, 0.03), 0 4px 6px -4px rgba(0, 0, 0, 0.03);

    --font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg-main: #0F172A;
        --bg-card: #1E293B;
        --text-main: #F8FAFC;
        --text-muted: #94A3B8;
        --border-color: #334155;
        --card-accent-bg: #1E3A5F;
        --success-bg: rgba(16, 185, 129, 0.18);
        --warning-bg: rgba(245, 158, 11, 0.18);
        --danger-bg: rgba(239, 68, 68, 0.18);
        --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
        --shadow-premium: 0 10px 15px -3px rgba(0, 0, 0, 0.25);
    }
}

/* Explicit Settings override (data-theme on <html>, see base.html) - wins
   over the OS preference above regardless of which way the OS is set,
   since an attribute selector outranks a bare :root in specificity. Only
   present when the shop picked Light or Dark instead of "Auto (System)". */
:root[data-theme="dark"] {
    --bg-main: #0F172A;
    --bg-card: #1E293B;
    --text-main: #F8FAFC;
    --text-muted: #94A3B8;
    --border-color: #334155;
    --card-accent-bg: #1E3A5F;
    --success-bg: rgba(16, 185, 129, 0.18);
    --warning-bg: rgba(245, 158, 11, 0.18);
    --danger-bg: rgba(239, 68, 68, 0.18);
    --chart-cash: #34D399;
    --chart-card: #60A5FA;
    --chart-credit: #FBBF24;
    --chart-payphone: #A78BFA;
    --chart-refund: #F87171;
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
    --shadow-premium: 0 10px 15px -3px rgba(0, 0, 0, 0.25);
}
:root[data-theme="light"] {
    --bg-main: #F8FAFC;
    --bg-card: #FFFFFF;
    --text-main: #1E293B;
    --text-muted: #64748B;
    --border-color: #E2E8F0;
    --card-accent-bg: #DBEAFE;
    --success-bg: #D1FAE5;
    --warning-bg: #FEF3C7;
    --danger-bg: #FEE2E2;
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
    --shadow-premium: 0 10px 15px -3px rgba(0, 0, 0, 0.03), 0 4px 6px -4px rgba(0, 0, 0, 0.03);
}

/* SnapPOS Glass - frosted cards over a moving background: the shop's own
   video if it uploaded one, otherwise the stock video that ships with the app
   (static/img/snappos-bg.mp4, rendered by base.html). There is no still
   fallback any more - the background is that video, or a picture the shop
   chose, and nothing else. Before the first frame decodes, the opaque base
   colour below is what shows.
   Card surfaces are a dark, blurred glass
   (not white glass) since dense UI text needs strong contrast against a
   busy photographic background - a bright/airy glass card reads fine for
   a single hero moment but not for tables full of 13px text.

   Shop-uploaded custom backgrounds can be ANY brightness (a bright,
   sunlit storefront photo is a completely normal upload) - a fixed
   0.46-opacity tint over a bright photo settles to a muddy gray-brown
   rather than deep navy, which still holds up for large bold text but
   erodes contrast for --text-muted (nav links, timestamps, secondary
   labels). Rather than sampling the uploaded image's brightness and
   flipping text color per-photo (fragile - a single photo can have both
   bright and dark regions, and it'd fight the app's one white-on-dark-
   glass design language everywhere else), the fix is a stronger,
   CONSTANT dark treatment that reliably dominates any photo:
   (1) a fixed dark scrim gradient layered on top of the photo itself at
   the body level, so even the raw image visible in card gaps/margins is
   never too bright, and (2) higher-opacity card/bar surfaces so they
   read as navy, not gray, regardless of what's under them. */
:root[data-theme="glass"] {
    /* Glass is a dark theme made of translucent surfaces, and both of these
       say so before the photo behind them exists.

       Every surface in this theme is see-through by design, so on the first
       load - before the background image has arrived - they were sitting on
       the browser's default white canvas and the whole page rendered washed
       out, then snapped dark when the photo landed. An opaque base underneath
       means the pre-image state is simply the theme, slightly plainer.

       color-scheme matters for the same reason one step earlier: between
       navigations, before a single rule here has been parsed, the browser
       paints a canvas from what it knows about the page. Told nothing, it
       assumes light. */
    color-scheme: dark;
    background-color: #040812;
    --bg-main: rgba(6, 12, 26, 0.55);
    --bg-card: rgba(6, 11, 24, 0.7);
    --text-main: #F1F5FB;
    --text-muted: #B7C3DA;
    --accent: #4F8BFF;
    --accent-hover: #6FA1FF;
    --glass-btn-base: #1D4ED8;   /* solid button fill in glass mode */
    --glass-btn-glow: #22D3EE;   /* electric-cyan expanding-circle glow (Btn-47) */
    --border-color: rgba(255, 255, 255, 0.08);
    --card-accent-bg: rgba(79, 139, 255, 0.28);
    --success-bg: rgba(16, 185, 129, 0.24);
    --warning-bg: rgba(245, 158, 11, 0.24);
    --danger-bg: rgba(239, 68, 68, 0.24);
    --chart-cash: #34D399;
    --chart-card: #60A5FA;
    --chart-credit: #FBBF24;
    --chart-payphone: #A78BFA;
    --chart-refund: #F87171;
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.35);
    --shadow-premium: 0 16px 36px -4px rgba(0, 0, 0, 0.5), 0 4px 12px -2px rgba(0, 0, 0, 0.3);
}
:root[data-theme="glass"] body {
    /* --custom-bg-image is set inline on <html> (base.html) only when the
       shop has uploaded a still IMAGE. A video background - theirs or the
       stock one - is a real <video> element behind everything instead.

       No fallback in the var(): with the property unset this whole
       declaration is invalid and drops, which is what we want - the video is
       already covering it, and the opaque base colour handles the moment
       before its first frame.

       The linear-gradient darkens the picture itself (not just the cards on
       top of it) so it can never wash out muted text sitting directly over a
       bright region between cards. */
    background-image: linear-gradient(rgba(4, 8, 18, 0.4), rgba(4, 8, 18, 0.4)), var(--custom-bg-image);
    background-size: cover;
    background-position: center;
    background-attachment: fixed;
}
/* A video background is a real element pinned behind the page at z-index:-2
   (see .bg-video). The opaque background-color on :root above means body's
   background no longer propagates to the canvas, so body paints as an
   ordinary in-flow block - and an in-flow block's background paints AFTER
   negative-z-index descendants, which puts it ON TOP of the video. Left
   alone, two layers cover the video: the still-image layer, which falls back
   to the stock photo whenever --custom-bg-image is unset (and it is always
   unset for a video, since a video cannot be a CSS background-image), and
   body's own translucent tint. Both stay partly see-through, so the shop
   sees the video and the stock photo at the same time. The darkening they
   contributed is what .bg-video-scrim is for. */
:root[data-theme="glass"][data-bg="video"] body {
    background-image: none;
    background-color: transparent;
}
/* No brightness(0) invert(1) on either logo in glass: the sidebar icon
   (.brand-logo--paired) is a colored mark that inverts to a featureless
   white blob (looked like a blank white circle), and the login lockup
   (.login-logo) needs its "Snap"/"POS" colors. Both read fine on the glass
   dark backdrop in their natural colors, exactly like every other theme. */
:root[data-theme="glass"] .sidebar,
:root[data-theme="glass"] .topbar,
:root[data-theme="glass"] .chart-card,
:root[data-theme="glass"] .sidebar-panel,
:root[data-theme="glass"] .centered-card,
:root[data-theme="glass"] .modal,
:root[data-theme="glass"] .alert-widget,
:root[data-theme="glass"] .sales-table,
:root[data-theme="glass"] .summary-table,
:root[data-theme="glass"] .reports-picker,
:root[data-theme="glass"] .toast,
:root[data-theme="glass"] .scanner-card {
    backdrop-filter: blur(18px) saturate(150%);
    -webkit-backdrop-filter: blur(18px) saturate(150%);
}

* { box-sizing: border-box; }

body {
    font-family: var(--font-stack);
    background-color: var(--bg-main);
    color: var(--text-main);
    margin: 0;
    min-height: 100vh;
    -webkit-font-smoothing: antialiased;
}

h1 { font-size: 22px; margin: 0 0 16px; letter-spacing: -0.3px; }
h2 { font-size: 16px; margin: 24px 0 10px; color: var(--text-main); }
a { color: var(--accent); }

/* ==========================================================================
   App shell: left sidebar nav + top utility bar
   ========================================================================== */
.app-shell { display: flex; min-height: 100vh; align-items: stretch; }

.sidebar {
    width: 220px;
    flex: none;
    background: var(--bg-card);
    border-right: 1px solid var(--border-color);
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 0;
    align-self: flex-start;
    height: 100vh;
    overflow-y: auto;
    z-index: 100;
}

.sidebar-brand { padding: 8px 12px; border-bottom: 1px solid var(--border-color); display: flex; align-items: center; gap: 10px; min-height: 0; }
.brand { font-weight: 800; font-size: 18px; letter-spacing: -0.5px; color: var(--accent); min-width: 0; overflow-wrap: break-word; line-height: 1.15; }
.brand-logo { max-height: 64px; max-width: 100%; width: auto; object-fit: contain; }
/* Sidebar/header default logo is the icon-only mark - a self-contained
   opaque circle, legible on any background as-is, so no backdrop needed. */
/* A mark shown BESIDE the business name is not doubled: the sidebar only has
   ~196px for the pair, and at 64px the two would fight over it. This covers
   both the default icon and a square uploaded logo; a wide logo stands alone
   and keeps the full 64px. */
.brand-logo--paired { max-height: 44px; flex: none; }
/* Login page keeps the full icon+wordmark+tagline lockup, sized up from the
   old cramped 52px so the wordmark/tagline are actually readable. Its
   wordmark is white-on-transparent (designed for a dark surface) - the
   light theme's login card is white, so without a dark backdrop the "Snap"
   half disappears. */
.login-logo { display: block; width: 200px; height: auto; margin: 0 auto 16px; background: #0F172A; border-radius: 14px; padding: 14px; box-sizing: border-box; }
.login-reset { margin-top: 14px; font-size: 14px; }

.sidebar-nav { flex: 1; padding: 12px 10px; display: flex; flex-direction: column; gap: 2px; }

.sidebar-link {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 10px 12px;
    border-radius: var(--radius-sm);
    text-decoration: none;
    color: var(--text-muted);
    font-weight: 500;
    font-size: 14px;
    transition: all 0.15s ease;
}
.sidebar-link svg { width: 18px; height: 18px; flex: none; }
.sidebar-link:hover { color: var(--text-main); background-color: var(--bg-main); }
.sidebar-link.active { color: var(--accent); font-weight: 600; background-color: var(--card-accent-bg); }

.main-area { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* Same fix, one level down. A flex item defaults to min-width:auto, meaning it
   refuses to shrink below its content's minimum - so a wide table made <main>
   734px inside a 390px column and pushed the whole page sideways on a phone.
   At widths where the content already fits this is a no-op. */
/* Both halves of the same flexbox trap. <main> is a flex item, so it defaults
   to min-width:auto (refuses to shrink below its content) and, with only a
   px max-width, sizes to fit-content rather than to its parent. A wide table
   therefore made it 734px inside a 390px column and dragged the whole page
   sideways. Neither declaration changes anything at widths where content
   already fits. See main.page's max-width: min(900px, 100%) below. */
.page { min-width: 0; }

.topbar {
    background-color: var(--bg-card);
    border-bottom: 1px solid var(--border-color);
    padding: 10px 24px;
    min-height: 56px;
    display: flex;
    justify-content: flex-end;
    align-items: center;
    position: sticky;
    top: 0;
    z-index: 90;
    gap: 10px;
    flex-wrap: wrap;
}

.nav-link {
    padding: 8px 14px;
    text-decoration: none;
    color: var(--text-muted);
    font-weight: 500;
    font-size: 14px;
    border-radius: var(--radius-sm);
    transition: all 0.15s ease;
    display: inline-flex;
    align-items: center;
    gap: 7px;
}
.nav-link:hover { color: var(--text-main); background-color: var(--bg-main); }
.nav-link svg { width: 16px; height: 16px; }

.status-widget {
    background: var(--bg-main);
    padding: 6px 12px;
    border-radius: var(--radius-sm);
    font-size: 13px;
    font-weight: 500;
    color: var(--text-muted);
    white-space: nowrap;
}

@media (max-width: 900px) {
    .app-shell { flex-direction: column; }
    .sidebar {
        width: 100%;
        height: auto;
        position: static;
        border-right: none;
        border-bottom: 1px solid var(--border-color);
    }
    .sidebar-nav { flex-direction: row; flex-wrap: wrap; }
}

/* ==========================================================================
   Language toggle (EN/ES pill switch)
   ========================================================================== */
.lang-toggle-form { display: inline-flex; margin: 0; }
.lang-toggle {
    display: inline-flex;
    padding: 3px;
    gap: 2px;
    background: var(--bg-main);
    border: 1.5px solid var(--border-color);
    border-radius: 999px;
    font-weight: 700;
    font-size: 11px;
}
.lang-toggle span {
    padding: 4px 10px;
    border-radius: 999px;
    color: var(--text-muted);
    transition: all 0.15s ease;
}
.lang-toggle span.active { background: var(--accent); color: var(--accent-fg); }
.lang-toggle:hover { border-color: var(--accent); }
.lang-toggle-standalone { position: fixed; top: 16px; right: 16px; z-index: 100; }

/* ==========================================================================
   Layout / generic page shell
   ========================================================================== */
/* width:100% matters as much as the max-width. <main> is a flex item, so
   without an explicit width it takes its fit-content size - and once the chart
   grid was allowed to shrink (min-width:0 on its children), fit-content
   collapsed the whole page to ~450px and squashed every chart. The pair means:
   fill the column, but never exceed 900px or the parent. */
main.page { width: 100%; max-width: min(900px, 100%); margin: 24px auto; padding: 0 16px; }
main.page.page-wide { max-width: 1400px; }

.flash { background: var(--warning-bg); border: 1px solid var(--warning); padding: 10px 14px; border-radius: var(--radius-sm); margin-bottom: 16px; color: var(--text-main); }

.centered-card {
    max-width: 380px;
    margin: 72px auto;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    padding: 28px;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-premium);
}

/* ==========================================================================
   Forms & buttons (shared across every page)
   ========================================================================== */
label { display: block; font-size: 13px; color: var(--text-muted); margin-bottom: 10px; }

input, select, textarea {
    font-size: 15px;
    padding: 10px 12px;
    margin: 4px 4px 4px 0;
    border: 1.5px solid var(--border-color);
    border-radius: var(--radius-sm);
    background: var(--bg-main);
    color: var(--text-main);
    outline: none;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
input:focus, select:focus, textarea:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
    background: var(--bg-card);
}
/* Full-width only for labeled fields (Settings/Open Shift/Close Day) and the
   login card - bare inline inputs (Items "add an item" row, Export dates)
   keep their natural compact width. */
label input, label select, label textarea, .centered-card input { width: 100%; margin-left: 0; }

button {
    cursor: pointer;
    /* inline-flex + center on both axes so the label sits dead-center in the
       button regardless of line-height or icon content - the alternative
       (relying on default text-align + padding) drifts off-center on
       buttons whose glyphs have uneven vertical metrics. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    background: var(--accent);
    color: var(--accent-fg);
    border: none;
    border-radius: var(--radius-sm);
    padding: 11px 20px;
    font-size: 14px;
    font-family: var(--font-stack);
    font-weight: 600;
    line-height: 1.2;
    letter-spacing: -0.1px;
    transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
button:hover { background: var(--accent-hover); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25); }
button:active { transform: translateY(0px); }

/* ==========================================================================
   Ridged bevel on primary action buttons
   --------------------------------------------------------------------------
   A 1px light line inside the top edge, a dark one inside the bottom, a
   hairline outer ring and a second inner step. That combination is what makes
   a flat fill read as a domed, pressable key instead of a coloured label -
   which matters more on a till than in a web app, because a cashier is
   aiming a finger at it, not a cursor.

   Scoped with the same exclusion list as the glass glow below: a machined
   edge on a 26px +/- stepper or the plain-text Remove link reads as broken,
   not fancy. Gradients and box-shadows only - no backdrop-filter and no new
   composite layers, so this costs nothing on the refurbished boxes this runs
   on.
   ========================================================================== */
button:not(.btn-secondary):not(.stepper-btn):not(.remove-link):not(.modal-cancel):not(.modal-tab) {
    background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.26), rgba(255, 255, 255, 0.02) 48%, rgba(0, 0, 0, 0.18));
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55),
                inset 0 2px 0 rgba(255, 255, 255, 0.14),
                inset 0 -1px 0 rgba(0, 0, 0, 0.38),
                inset 0 -2px 3px rgba(0, 0, 0, 0.20),
                0 0 0 1px rgba(0, 0, 0, 0.30),
                0 4px 9px -2px rgba(0, 0, 0, 0.45);
}
/* Hover deepens the drop shadow to match the -1px lift, rather than replacing
   the bevel with the plain glow the base button:hover would otherwise apply. */
button:not(.btn-secondary):not(.stepper-btn):not(.remove-link):not(.modal-cancel):not(.modal-tab):hover {
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55),
                inset 0 -1px 0 rgba(0, 0, 0, 0.34),
                0 0 0 1px rgba(0, 0, 0, 0.28),
                0 7px 16px -3px rgba(0, 0, 0, 0.45);
}
/* Pressed: the bevel inverts and the lift is cancelled, so the key physically
   sinks. Must come after :hover - equal specificity, so source order decides
   which one wins while a pressed button is also hovered. */
button:not(.btn-secondary):not(.stepper-btn):not(.remove-link):not(.modal-cancel):not(.modal-tab):active {
    transform: translateY(1px);
    box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.38),
                inset 0 -1px 0 rgba(255, 255, 255, 0.18),
                0 0 0 1px rgba(0, 0, 0, 0.30),
                0 1px 2px -1px rgba(0, 0, 0, 0.35);
}

/* ==========================================================================
   Glass theme: Btn-47 expanding-circle cyan glow (primary buttons only)
   --------------------------------------------------------------------------
   Scoped away from the small/link-style buttons - a full-button cyan flood on
   a 26px +/- stepper or the plain-text Remove link reads as broken, not
   fancy - so the effect lands only on real primary action buttons (Add,
   Save, Import, modal Save...). .btn-secondary and the micro-buttons keep
   their own subdued treatments below.
   ========================================================================== */
:root[data-theme="glass"] button:not(.btn-secondary):not(.stepper-btn):not(.remove-link):not(.modal-cancel):not(.modal-tab) {
    background-color: var(--glass-btn-base);
    color: var(--glass-btn-fg, #fff);
    position: relative;
    overflow: hidden;
    transition-duration: .5s;
    z-index: 1;
    /* Repeated rather than inherited: this selector is more specific than the
       all-themes bevel above, so it would otherwise win with the old flat
       shadow and the glass theme alone would stay flat. */
    background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.26), rgba(255, 255, 255, 0.02) 48%, rgba(0, 0, 0, 0.18));
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55),
                inset 0 2px 0 rgba(255, 255, 255, 0.14),
                inset 0 -1px 0 rgba(0, 0, 0, 0.38),
                inset 0 -2px 3px rgba(0, 0, 0, 0.20),
                0 0 0 1px rgba(0, 0, 0, 0.30),
                0 4px 9px -2px rgba(0, 0, 0, 0.45);
}
/* The circle blooms FROM THE POINTER.
   It had no top/left at all, so an absolutely positioned box fell back to its
   static position - which inside an inline-flex button is the top centre. The
   light therefore always started from the same spot on the button no matter
   where the hand was, which reads as a canned animation rather than a
   response to being touched.
   --glow-x / --glow-y are written by the pointer handler in base.html. They
   DEFAULT TO THE CENTRE, so a touchscreen tap, a keyboard focus, or the
   script not running at all still gives a bloom from the middle - which is
   the right answer when there is no pointer to bloom from. */
:root[data-theme="glass"] button:not(.btn-secondary):not(.stepper-btn):not(.remove-link):not(.modal-cancel):not(.modal-tab):after {
    content: "";
    position: absolute;
    left: var(--glow-x, 50%);
    top: var(--glow-y, 50%);
    /* SIZED TO REACH. It was a 12px dot scaled 25x - 300px across - which
       covered the button while the bloom always started from the middle of
       its top edge. Starting from wherever the pointer is puts the far
       corner further away than that, so on a wide button (the two in the
       invoice payment modal stretch to fill the row) the light stopped
       short of the edges.
       --glow-d is twice the distance from the pointer to the furthest
       corner, measured in the handler, so the circle covers the button
       exactly from any point on it and no more. The fallback is generous
       enough for a centred bloom on any button in the app. */
    width: var(--glow-d, 620px);
    height: var(--glow-d, 620px);
    margin: 0;
    background-color: var(--glass-btn-glow);
    border-radius: 50%;
    z-index: -1;
    opacity: 0;
    /* Its own centre sits on the pointer, so it grows outwards in every
       direction from there rather than down and to the right of it. */
    transform: translate(-50%, -50%) scale(0);
    transition: transform .5s ease-out, opacity .5s ease-out;
}
:root[data-theme="glass"] button:not(.btn-secondary):not(.stepper-btn):not(.remove-link):not(.modal-cancel):not(.modal-tab):hover:after {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
}
/* Keyboard focus has no pointer, so it blooms from the middle by way of the
   defaults above - and it must bloom at all, or a keyboard user gets no
   feedback that the button is the one that will fire. */
:root[data-theme="glass"] button:not(.btn-secondary):not(.stepper-btn):not(.remove-link):not(.modal-cancel):not(.modal-tab):focus-visible:after {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
    :root[data-theme="glass"] button:after { transition-duration: 0.01ms; }
}
/* Text flips to dark for contrast against the cyan flood, with an electric
   glow ring. transform:none cancels the base -1px lift so the effect reads
   as a flood, not a bounce. */
:root[data-theme="glass"] button:not(.btn-secondary):not(.stepper-btn):not(.remove-link):not(.modal-cancel):not(.modal-tab):hover {
    color: #0f172a;
    background-color: var(--glass-btn-base);
    transform: none;
    box-shadow: 0 0 8px var(--glass-btn-glow),
                0 0 15px var(--glass-btn-glow),
                0 0 35px var(--glass-btn-glow);
    transition-duration: .6s;
}
/* Press feedback survives the hover flood: keep the two inner glow rings but
   sink the key. Without this the glass :hover rule above outranks the generic
   :active and a tap produces no visible change at all. */
:root[data-theme="glass"] button:not(.btn-secondary):not(.stepper-btn):not(.remove-link):not(.modal-cancel):not(.modal-tab):active {
    transform: translateY(1px);
    box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.45),
                0 0 8px var(--glass-btn-glow),
                0 0 15px var(--glass-btn-glow);
    transition-duration: .1s;
}
:root[data-theme="glass"] button:disabled {
    pointer-events: none;
    opacity: .45;
    color: var(--text-muted);
    background: rgba(255, 255, 255, 0.05);
    box-shadow: none;
}
/* Secondary buttons: a readable outline that brightens to cyan on hover,
   rather than the full flood (which would need a background the transparent
   outline doesn't have, leaving dark text on a dark glass card). */
:root[data-theme="glass"] .btn-secondary {
    background-color: rgba(255, 255, 255, 0.04);
    color: var(--text-main);
    border: 1.5px solid var(--border-color);
}
:root[data-theme="glass"] .btn-secondary:hover {
    background-color: rgba(255, 255, 255, 0.10);
    border-color: var(--glass-btn-glow);
    color: #fff;
    box-shadow: 0 0 8px var(--glass-btn-glow);
    transform: none;
}

/* ==========================================================================
   Register: scan box
   ========================================================================== */
/* Named grid areas (not just column count) so visual order can differ from
   DOM order per breakpoint: wide screens put Current Sale beside everything
   as a tall sidebar; narrow screens put it directly under the scanner
   instead of at the very bottom below the stat cards/chart/alerts - a
   cashier on a narrow till screen needs the basket visible right after
   scanning, not buried under the daily-totals cards. .main-workspace stays
   in the HTML purely for organization (display: contents removes it from
   the box model), so scanner-card/chart-card/alerts-section
   become direct grid items of .pos-container and can be placed
   independently of their shared wrapper. */
.pos-container {
    display: grid;
    grid-template-columns: 1fr 380px;
    grid-template-areas:
        "scanner sidebar"
        "chart   sidebar"
        "alerts  sidebar";
    align-items: start;
    gap: 24px;
    padding: 24px;
    max-width: 1400px;
    margin: 0 auto;
}
@media (max-width: 900px) {
    .pos-container {
        grid-template-columns: 1fr;
        grid-template-areas:
            "scanner"
            "sidebar"
            "chart"
            "alerts";
    }
}

.main-workspace { display: contents; }
.pos-container .scanner-card { grid-area: scanner; }
.pos-container .chart-card { grid-area: chart; }
.pos-container .alerts-section { grid-area: alerts; }
.pos-container .sidebar-panel { grid-area: sidebar; }

.scanner-card {
    background: var(--bg-card);
    padding: 22px;
    border-radius: var(--radius-lg);
    border: 1px solid var(--border-color);
    box-shadow: var(--shadow-premium);
    /* This card hosts an overlay - the customer suggestions - so it has to sit
       above the cards below it. The glass theme gives every card a
       backdrop-filter, and that creates a stacking context, so Today's Sales
       painted OVER the suggestions whatever z-index the dropdown carried.
       Raising the dropdown alone cannot escape a sibling's stacking context;
       the card that owns the overlay has to be promoted. */
    position: relative;
    z-index: 2;
}

#scan-input {
    width: 100%;
    padding: 16px 20px;
    font-size: 24px;
    font-weight: 400;
    letter-spacing: -0.5px;
    border-radius: var(--radius-md);
    border: 1px solid var(--border-color);
    background: rgba(0, 0, 0, 0.2) !important;
}
#scan-input::placeholder {
    color: var(--text-muted);
    opacity: 0.6;
}
#scan-input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(79, 139, 255, 0.25);
}

.scanner-hint { margin-top: 8px; font-size: 12px; color: var(--text-muted); display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.btn-small { padding: 5px 10px; font-size: 12px; flex: none; }

/* ==========================================================================
   Register: dashboard stat cards (no trend stats - just the numbers)
   ========================================================================== */
.chart-card {
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-premium);
    padding: 18px 20px;
}
.chart-card .panel-header { padding: 0 0 14px; border-bottom: none; }
.chart-row { display: grid; grid-template-columns: 1fr 260px; gap: 24px; }
@media (max-width: 900px) { .chart-row { grid-template-columns: 1fr; } }
.chart-row-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
.chart-row-2col > *, .chart-row > * { min-width: 0; }
@media (max-width: 900px) { .chart-row-2col { grid-template-columns: 1fr; } }
.date-range-form { display: flex; gap: 16px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 20px; }
.date-range-form label { margin: 0; }
/* Date fields only ever hold one date, but the generic `label input`
   width:100% rule above stretched them across the whole page. No min-width:
   the browser already sizes a date input to fit its own picker, and Firefox
   needs less room than Chromium - forcing a floor just padded it again. */
.date-range-form input[type="date"] { width: auto; }
.date-range-form label { flex: 0 0 auto; }
.chart-canvas-wrap { position: relative; height: 220px; }
/* The canvas is taken OUT OF FLOW deliberately. Chart.js writes a pixel width
   onto the canvas, and while the canvas is in flow that width sizes its own
   container - so after a card was expanded and collapsed, a 1210px canvas kept
   its grid column 1210px wide, Chart.js read the container as still 1210px, and
   resize() became a no-op. The canvas defined its own container: a feedback
   loop no amount of resize() could break. Absolute positioning is Chart.js's
   documented pattern for maintainAspectRatio:false for exactly this reason. */
.chart-canvas-wrap canvas { position: absolute; inset: 0; }
.chart-canvas-wrap canvas.hidden { display: none; }
.chart-legend { display: flex; flex-direction: column; justify-content: center; gap: 14px; }
.chart-legend-row { display: flex; align-items: center; gap: 10px; }
.chart-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }
.chart-legend-label { font-size: 13px; color: var(--text-muted); }
.chart-legend-value { font-size: 15px; font-weight: 700; margin-left: auto; }
/* The day's total, promoted out of its own tile into the panel that already
   breaks that same total down by tender. */
.summary-total { padding-bottom: 10px; border-bottom: 1px solid var(--border-color); }
.summary-total-value { font-size: 30px; font-weight: 700; letter-spacing: -0.6px; line-height: 1.1; }
/* A count, not money, so it sits apart from the two tender rows. */
.summary-units { padding-top: 10px; border-top: 1px solid var(--border-color); }
.chart-empty { font-size: 13px; color: var(--text-muted); padding: 40px 0; text-align: center; }
.chart-empty.hidden { display: none; }
.chart-caveat { font-size: 12px; color: var(--warning); margin-top: 10px; }
.chart-caveat.hidden { display: none; }

/* Inventory reports (age/turnover) render a scrollable table instead of a
   canvas - same card, same 220px viewport as the chart archetypes so the
   grid stays visually even regardless of which report a shop picked. */
.table-report-wrap { max-height: 220px; overflow-y: auto; }
.table-report-wrap.hidden { display: none; }
.inventory-report-table { width: 100%; border-collapse: collapse; }
.inventory-report-table th {
    position: sticky; top: 0; text-align: left; font-size: 12px; text-transform: uppercase;
    color: var(--text-muted); padding: 6px 10px; background: var(--bg-card);
    border-bottom: 1px solid var(--border-color);
}
.inventory-report-table td { padding: 7px 10px; border-bottom: 1px solid var(--border-color); font-size: 14px; }
.inventory-report-table tr:last-child td { border-bottom: none; }

/* ==========================================================================
   Analytics: "Customize reports" picker
   ========================================================================== */
.reports-picker {
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    padding: 4px 16px;
    margin-bottom: 20px;
}
.reports-picker summary { cursor: pointer; padding: 12px 0; font-weight: 600; font-size: 14px; }
.reports-picker-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px 16px; margin: 10px 0 16px; }
.reports-picker-item { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--text-main); margin: 0; width: auto; }
.reports-picker-item input { width: auto; margin: 0; }
.reports-picker form { padding-bottom: 16px; }

/* ==========================================================================
   Register: alerts (low stock + best seller)
   ========================================================================== */
.alerts-section { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }

.alert-widget {
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-left: 4px solid var(--accent);
    border-radius: var(--radius-md);
    padding: 14px 16px;
    display: flex;
    align-items: flex-start;
    gap: 12px;
}
.alert-widget.warning { border-left-color: var(--warning); }
.alert-icon { font-size: 20px; line-height: 1; }
.alert-title { font-weight: 600; font-size: 14px; }
.alert-sub { font-size: 13px; color: var(--text-muted); margin-top: 2px; }

/* ==========================================================================
   Register: sidebar (recent sales as "receipt" rows)
   ========================================================================== */
.sidebar-panel {
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-premium);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    height: fit-content;
    position: sticky;
    top: 88px;
    max-height: calc(100vh - 110px);
}
.panel-header { padding: 16px 20px; border-bottom: 1px solid var(--border-color); font-weight: 600; font-size: 15px; }
.transaction-list { overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 8px; }

/* Unified card treatment (applies in every theme, not just Glass): same
   corner radius across every panel, plus a faint diagonal sheen to simulate
   light hitting the glass from above. Overrides the per-class border-radius
   values above (the panels set --radius-lg) in favor of one consistent one. */
.scanner-card, .sidebar-panel, .chart-card {
    border: 1px solid var(--border-color);
    box-shadow: var(--shadow-premium);
    border-radius: var(--radius-md) !important;
    background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0) 100%), var(--bg-card) !important;
}

.receipt-row { background: var(--bg-main); border-radius: var(--radius-sm); padding: 12px 14px; border: 1px solid transparent; }
.receipt-row.voided { opacity: 0.45; text-decoration: line-through; }
.receipt-top { display: flex; justify-content: space-between; gap: 8px; }
.item-name { font-weight: 600; font-size: 14px; }
/* An invoice payment line is the one basket row that points at a document.
   Underlined on hover rather than always, so a basket of ordinary items does
   not suddenly look like a page of links. */
a.invoice-line-link { color: var(--accent); text-decoration: none; }
a.invoice-line-link:hover, a.invoice-line-link:focus-visible { text-decoration: underline; }
.item-price { font-weight: 700; font-size: 14px; }
/* The three buttons are ONE group, sat together at the right.
   It was `justify-content: space-between` with `margin-left: auto` on Void
   only, which did two unwanted things: it opened a wide gap between Qty and
   Void while leaving Disc and Qty 6px apart, so the three read as two things
   rather than one; and with no `gap`, a long line of meta text ran straight
   into the Discount button with nothing between them. The auto margin moves
   to the FIRST button, so the whole group is pushed right as a unit, and the
   gap does the spacing everywhere instead of per-button margins. */
.receipt-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    font-size: 12px;
    color: var(--text-muted);
    margin-top: 6px;
}
/* 12px text and 4px of padding made these about 22px tall and 6px apart, on
   a touchscreen, where the 44px minimum applied only under the phone
   breakpoint - a WIDTH test the till never matches. Qty is the most-used
   line action and Void is destructive.
   
   Colour alone did not tell them apart either, and could not: a shop on the
   rose accent gets #E11D48 for Qty next to #EF4444 for Void. Void now
   carries an outline as well, so it is distinguishable in sunlight, in a
   photocopy, and to a red-green colour-blind cashier. */
.void-btn, .qty-btn, .disc-btn {
    background: transparent;
    border: 1px solid transparent;
    min-height: 40px;
    padding: 8px 12px;
    font-size: 13px;
    font-weight: 500;
    border-radius: var(--radius-sm);
    transition: background 0.15s ease, color 0.15s ease;
}
/* Set apart from the pair before it with a real gap, so the destructive one
   is not the neighbour of the one pressed forty times a day. */
.void-btn { color: var(--danger); border-color: var(--danger); margin-left: 28px; }
.void-btn:hover { background: var(--danger-bg); }
.qty-btn, .disc-btn { color: var(--accent); }
/* Disc and Qty are ONE pair - two harmless edits to a line - and the auto
   margin on the first of them carries both to the right-hand end. Void keeps
   a real gap after them, per the note above: it is the destructive one and it
   must not become the neighbour of the button pressed forty times a day. What
   changed is that the gap is now a stated 28px instead of "whatever space is
   left", which on a wide till was most of the row. */
.disc-btn { margin-left: auto; }
.qty-btn:hover, .disc-btn:hover { background: var(--card-accent-bg); }
/* Wide tables scroll INSIDE their box. This class is used on six screens and
   was never defined outside @media print, so the only thing it did was undo
   an overflow that had never been set - and Inventory (11 columns) and the
   shift history (11 columns) pushed the whole document sideways instead,
   taking the nav and topbar with them. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* The reversal controls on an invoice payment row.
   These used to be an amount box, a checkbox carrying a full sentence, and a
   button, all laid out inline in one cell. The sentence set the cell's width,
   the table then ran past the edge of the page, and the right-hand side of
   every column was cut off. The checkbox is gone - it is two buttons now - and
   what remains is stacked so the cell is as narrow as its widest control
   rather than as wide as a sentence. */
.reversal-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}
.reversal-actions .refund-amount { display: inline-flex; align-items: center; gap: 6px; }
.reversal-actions .refund-amount input { width: 6.5em; }
.reversal-buttons { display: flex; gap: 6px; flex-wrap: wrap; }

.discount-badge { background: var(--danger-bg); color: var(--danger); padding: 2px 7px; border-radius: 4px; font-size: 10px; font-weight: 700; }

.current-sale-footer { border-top: 1px solid var(--border-color); padding: 12px; }
.sale-summary { font-size: 13px; color: var(--text-muted); margin-bottom: 10px; line-height: 1.6; }
.sale-summary.hidden { display: none; }
.sale-summary .total-line { font-size: 16px; font-weight: 700; color: var(--text-main); }
.current-sale-buttons { display: flex; gap: 8px; }
.current-sale-buttons button { flex: 1; }
.btn-secondary { background: rgba(255, 255, 255, 0.03); color: var(--text-main); border: 1px solid var(--border-color); }
.btn-secondary:hover { background: rgba(255, 255, 255, 0.08); }

/* Printed receipt: hidden on screen, the only thing visible when printing.
   Themed pages (dark/glass) set a body background-image/color and light
   --text-main - both must be forcibly overridden here with !important,
   since a plain `body { background: #fff }` in a media query has lower
   specificity than the `:root[data-theme="glass"] body {...}` rule and
   would otherwise lose, letting the theme's background photo and
   near-white text print instead of black-on-white. */
#receipt-print-area { display: none; }
@media print {
    body { background: #fff !important; background-image: none !important; }
    body * { visibility: hidden; }
    #receipt-print-area, #receipt-print-area * { visibility: visible; color: #000 !important; }
    #receipt-print-area {
        display: block; position: absolute; top: 0; left: 0; width: 100%;
        color: #000; background: #fff; padding: 20px; font-family: monospace;
    }
    #receipt-print-area h2 { margin-bottom: 6px; text-align: center; }
    #receipt-print-area img { display: block; margin: 0 auto 8px; }
    #receipt-print-area .receipt-line { display: flex; justify-content: space-between; padding: 2px 0; }
    #receipt-print-area .receipt-no { text-align: center; font-weight: 700; margin-top: 2px; }
    #receipt-print-area hr { border: none; border-top: 1px dashed #000; margin: 8px 0; }
    /* A delivery prints as a table rather than the receipt's flex lines. */
    #receipt-print-area table { width: 100%; border-collapse: collapse; margin: 8px 0; }
    #receipt-print-area th, #receipt-print-area td {
        border-bottom: 1px solid #999; padding: 4px 6px; text-align: left; font-size: 12px;
    }
    #receipt-print-area tfoot th { border-top: 2px solid #000; font-weight: 700; }
    /* Controls are for the screen; they must not appear on the sheet. */
    #receipt-print-area button, #receipt-print-area .remove-link { display: none !important; }
}

/* ==========================================================================
   Sales history (transaction register)
   ========================================================================== */
.history-filter { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; margin: 12px 0 18px; }

/* The printed document is on the app's scale too now. It had run its own -
   11.5px labels and a 12.5px body - which was internally consistent and still
   meant the sheet carried two scales, so a size read off one told you nothing
   about the other. 11 is the uppercase micro-label size used elsewhere; the
   body went DOWN to 12 rather than up to 13 because a printed invoice can only
   get shorter that way, and a one-page job must not become two. */

/* A bare button in a filter row has no caption above it, so `align-items:
   flex-end` lines its bottom up with the LABEL's bottom - and a label's bottom
   sits 4px below its input's, because inputs carry `margin: 4px 4px 4px 0`.
   The button therefore sat low against the fields it belongs with, and two
   pixels short of them. The same bottom margin and the same height as an input
   puts it back on the line.
   An input is 15px text (~18px line) + 10px padding twice + 1.5px border
   twice = 41px. Stated here because it is arithmetic, not a magic number. */
.history-filter > button { margin-bottom: 4px; min-height: 41px; }

/* The card tokenizer ends in an action button and the next form starts with
   one. What separated them was the tokenizer's help paragraph - which a shop
   can switch off, and the demo has - so on those installs "Charge this card"
   and "Email to customer" sat flush against each other and read as one
   control. Spacing that a setting can remove is not spacing. */
.card-tokenizer { margin-bottom: 18px; }
.history-filter label { font-size: 13px; color: var(--text-muted); }
.history-filter input { display: block; margin-top: 4px; }
.history-filter button { align-self: flex-end; }
.history-table td.num, .history-table th.num { text-align: right; }
.history-table tr.is-return td { color: var(--text-muted); }
.history-table .muted { color: var(--text-muted); }
.tag-return {
    display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 600;
    background: var(--bg-main); color: var(--text-muted); border: 1px solid var(--border-color);
}
.reprint-link { font-size: 13px; font-weight: 600; color: var(--accent, #2563eb); text-decoration: none; }
.reprint-link:hover { text-decoration: underline; }
.history-empty { margin-top: 20px; }

/* ==========================================================================
   Multi-register (topbar widget, management list, picker)
   ========================================================================== */
.register-widget { text-decoration: none; cursor: pointer; }
.registers-table { width: 100%; border-collapse: collapse; margin: 8px 0 4px; }
.registers-table td { padding: 4px 6px 4px 0; vertical-align: middle; }
.register-inactive { opacity: 0.5; }
.register-row-form { display: inline-flex; gap: 6px; align-items: center; }
.register-row-form input { max-width: 200px; }
.register-toggle-btn { background: none; border: none; color: var(--text-muted); font-size: 13px; cursor: pointer; text-decoration: underline; padding: 2px 4px; }
.register-add-form { display: flex; gap: 8px; margin-top: 6px; }
.register-picker { max-width: 560px; margin: 48px auto; text-align: center; }
.register-choices { display: flex; flex-wrap: wrap; gap: 14px; justify-content: center; margin-top: 22px; }
.register-choice { padding: 18px 30px; font-size: 18px; font-weight: 600; border-radius: 12px; cursor: pointer;
    border: 1px solid var(--border-color); background: var(--bg-card); color: inherit; }
.register-choice:hover { border-color: var(--accent, #2563eb); }
.register-choice.is-current { border-color: var(--accent, #2563eb); box-shadow: 0 0 0 2px rgba(37,99,235,0.25); }
.register-choice-new { border-style: dashed; color: var(--accent, #2563eb); font-weight: 700; }
.register-picker-hint { margin-top: 18px; color: var(--text-muted); font-size: 13px; }

/* ==========================================================================
   Toast notifications
   ========================================================================== */
/* Above .modal-overlay (2000), not below it. The change-due figure is shown
   as a toast while the card-waiting modal is still open, so at z-index 1000
   it was painted underneath that modal's 50%-black scrim. */
#toast-container { position: fixed; bottom: 24px; left: 24px; display: flex; flex-direction: column; gap: 10px; z-index: 2100; }

.toast {
    background: var(--bg-card);
    border-left: 4px solid var(--success);
    box-shadow: 0 20px 25px -5px rgba(0,0,0,0.12), 0 10px 10px -5px rgba(0,0,0,0.05);
    border-radius: var(--radius-sm);
    padding: 14px 18px;
    display: flex;
    align-items: center;
    gap: 12px;
    animation: slideUp 0.25s cubic-bezier(0.16, 1, 0.3, 1);
    min-width: 240px;
}
.toast.err { border-left-color: var(--danger); }
/* Server-rendered confirmation toasts follow the highlight colour rather than
   the fixed green, so they match whichever accent preset is active. */
.toast--accent { border-left-color: var(--accent); }
.toast--accent .toast-icon { background: var(--card-accent-bg); color: var(--accent); }
.toast-icon { width: 22px; height: 22px; border-radius: 50%; background: var(--success-bg); color: var(--success); display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 13px; flex-shrink: 0; }
.toast.err .toast-icon { background: var(--danger-bg); color: var(--danger); }
.toast-title { font-weight: 600; font-size: 14px; }
.toast-sub { font-size: 12px; color: var(--text-muted); }

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

/* ==========================================================================
   Data tables (Items / History / Export) - shared look
   ========================================================================== */
.sales-table, .summary-table {
    width: 100%;
    border-collapse: collapse;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    overflow: hidden;
    box-shadow: var(--shadow-sm);
}
.sales-table th {
    text-align: left; font-size: 12px; text-transform: uppercase; color: var(--text-muted);
    padding: 12px 14px; background: var(--bg-main); border-bottom: 1px solid var(--border-color);
    vertical-align: middle;
}
.sales-table td, .summary-table td {
    padding: 12px 14px; border-bottom: 1px solid var(--border-color); font-size: 14px;
    vertical-align: middle;
}
/* Plain text cells (SKU, prices, qty) baseline-align to the top by default,
   which reads as jagged next to the taller stepper/action controls. Force
   nowrap + middle-align so every column shares one centered baseline, and
   let only the product-name column wrap so long names don't stretch rows. */
.sales-table td { white-space: nowrap; }
.sales-table td.col-name { white-space: normal; min-width: 160px; max-width: 245px; }
.sales-table td input,
.sales-table td select,
.sales-table td button { margin: 0; vertical-align: middle; }
.row-actions { display: flex; gap: 8px; align-items: center; }
.category-dropdown-list {
    position: fixed;
    z-index: 2500;   /* above .modal-overlay (2000) so it shows inside the Edit modal */
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
    max-height: 220px;
    overflow-y: auto;
    padding: 4px;
    margin: 0;
    list-style: none;
}
.category-dropdown-list li {
    padding: 6px 10px;
    font-size: 13px;
    border-radius: 4px;
    cursor: pointer;
    color: var(--text-main);
}
.category-dropdown-list li:hover { background: var(--accent); color: var(--accent-fg); }
.category-dropdown-list li.no-match {
    color: var(--text-muted);
    cursor: default;
    font-size: 12px;
}
.category-dropdown-list li.no-match:hover { background: none; color: var(--text-muted); }
.sales-table tr:last-child td { border-bottom: none; }
.sales-table tr.voided { opacity: 0.45; text-decoration: line-through; }
.sales-table tr.low-stock { background: var(--danger-bg); }
.summary-table tr.highlight td { font-weight: 700; background: var(--card-accent-bg); }
.variance-ok { color: var(--success); font-weight: 600; }

/* Stock stepper (Items table Adjust column): +/- apply immediately using
   whatever count is in the middle field (default 1, capped 1-100 - see
   items.html's inline script), replacing the old fixed -1/+1/+10 buttons
   with one control that covers any of those cases and more. */
.stock-stepper { display: inline-flex; align-items: center; gap: 4px; }
.stepper-btn {
    width: 26px;
    height: 26px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 15px;
    font-weight: 700;
    line-height: 1;
    border-radius: var(--radius-sm);
    background: var(--card-accent-bg);
    color: var(--accent);
}
.stepper-btn:hover { background: var(--accent); color: var(--accent-fg); transform: none; box-shadow: none; }
.stepper-value {
    width: 46px;
    text-align: center;
    padding: 4px 2px;
    margin: 0;
    font-size: 13px;
}
/* Remove/Eliminar as a plain text link, not a boxed button - matches the
   Void/Qty/Disc treatment on the Register page's current-sale rows. */
.remove-link {
    background: transparent;
    border: none;
    padding: 4px 6px;
    color: var(--danger);
    font-size: 13px;
    font-weight: 500;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.remove-link:hover { background: var(--danger-bg); text-decoration: none; transform: none; box-shadow: none; }

/* ==========================================================================
   Unknown-barcode modal
   ========================================================================== */
/* QUICK PICKS. Lite's tile grid, borrowed rather than reinvented - see
   .kt-tile in kitchen.css and docs/kitchen-register-design.md for why there
   are no photos on it. */
/* .modal fixes every modal at 380px, which is right for a numeric prompt and
   far too narrow for a grid of tiles. Overridden here rather than by adding a
   width to the shared class, which every other modal is sized around. */
/* THE PANEL IS SIZED BY WHAT IS IN IT.
   It was a fixed 720px with the tiles wrapping inside, so four tiles sat in a
   wide half-empty box and twenty scrolled in the same box. --qp-cols is set
   by the register from the tile count and steps 2, 3, 4, 5, 6 - a square-ish
   grid that grows with the shop rather than a fixed frame the shop grows
   inside.
   Six is the ceiling. A seventh column puts the far tiles beyond a comfortable
   reach on a counter-top screen, and past about 36 tiles the answer is not a
   bigger grid, it is a scanner.
   The arithmetic is stated rather than magic: columns x tile + gaps + the
   panel's own 24px padding on each side. */
/* .modal.quickpick-panel, not .quickpick-panel. Both are single-class
   selectors, so they weigh the same and SOURCE ORDER decides - and `.modal {
   width: 380px }` is defined 170 lines further down, which means this override
   never once took effect. The panel has been 380px since it was written, which
   is why the tiles cramped instead of the box growing. Two classes beats one,
   wherever either sits in the file. */
.modal.quickpick-panel {
    width: calc(var(--qp-cols, 3) * 150px
                + (var(--qp-cols, 3) - 1) * 10px
                + 48px);
    max-width: calc(100vw - 32px);
}

.qp-grid {
    display: grid;
    grid-template-columns: repeat(var(--qp-cols, 3), minmax(0, 1fr));
    grid-auto-rows: 88px;
    gap: 10px;
    margin: 12px 0;
    /* Still capped. A 6x6 grid is 578px of tiles before the header and the
       controls, and a 1366x768 till has less room than that - the same reason
       .modal carries a max-height. It grows to the screen, then scrolls. */
    max-height: 62vh;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.qp-tile {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 10px;
    text-align: left;
    font: inherit;
    color: var(--text-main);
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-top: 4px solid var(--accent);
    border-radius: var(--radius-md);
    cursor: pointer;
    /* THE one that would be diagnosed at 6am: without it, four fast taps on
       the same tile trigger double-tap-to-zoom instead of adding four. */
    touch-action: manipulation;
    overflow: hidden;
}
.qp-tile:active { transform: scale(0.97); }
.qp-name { font-weight: 600; font-size: 14px; line-height: 1.2; }
.qp-price { font-size: 13px; color: var(--text-muted); }

/* REMOVAL IS A MODE, so it has to look like one. A cashier who taps a tile
   expecting to sell a coffee must be able to see, without reading anything,
   that this tap will unpin it instead. */
.qp-grid.qp-deleting .qp-tile {
    border-top-color: var(--danger, #dc2626);
    border-style: dashed;
    opacity: 0.75;
}
.qp-grid.qp-deleting .qp-tile.qp-chosen {
    opacity: 1;
    outline: 2px solid var(--danger, #dc2626);
    background: rgba(220, 38, 38, 0.10);
}
.qp-grid.qp-deleting .qp-name { text-decoration: line-through; }

.qp-add { display: flex; gap: 8px; margin-bottom: 8px; }
.qp-add input { flex: 1 1 auto; }
.qp-controls { display: flex; gap: 8px; justify-content: flex-end; }
/* .btn-secondary is a 3%-white wash, which is legible as a word on a card and
   almost invisible as a single glyph. These two ARE single glyphs, and they
   are the only way to change what is on this screen. */
#qp-plus, #qp-minus {
    min-width: 3rem;
    font-size: 20px;
    font-weight: 700;
    line-height: 1;
    background: var(--bg-main);
    color: var(--text-main);
    border: 1px solid var(--text-muted);
}
#qp-plus:hover, #qp-minus:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
/* Armed for removal: it stops being a symbol and starts being a verb. */
.qp-grid.qp-deleting ~ .qp-controls #qp-minus,
#qp-minus.qp-armed {
    background: var(--danger, #dc2626);
    border-color: var(--danger, #dc2626);
    color: #fff;
}

/* The open-price prompt has to paint ABOVE the quick-pick popup: an item
   pinned for its open price is exactly the sort a shop pins, and the prompt
   arriving underneath would look like the tile had done nothing. Same
   reasoning as the toast container above .modal-overlay. */
.modal-overlay.modal-stacked { z-index: 2200; }

.modal-overlay {
    position: fixed; top: 0; left: 0; right: 0;
    /* Height tracks the visual viewport (set by base.html while the
       on-screen keyboard is open) so the dimmed backdrop stops at the top
       of the keyboard instead of being drawn over it. Unset on desktop
       -> 100% == the full viewport, unchanged. */
    height: var(--vvh, 100%); background: rgba(0, 0, 0, 0.5);
    display: flex; align-items: center; justify-content: center; z-index: 2000;
    /* Belt to the modal's braces. A centred flex item taller than its fixed
       container overflows BOTH ends and cannot be scrolled to, so if anything
       ever escapes the max-height below, this is what stops it being lost. */
    overflow-y: auto;
    padding: 16px 0;
}
.modal-overlay.hidden { display: none; }


/* ==========================================================================
   .btn-lit - a lit outline
   --------------------------------------------------------------------------
   For an action that is neither the primary one nor a secondary one: it does
   something DIFFERENT in kind. Smart Order is the case it was made for - Add
   Item and Receive Stock act on one item, Smart Order reads the whole shelf.
   Filled buttons compete with each other for the eye; an outline that glows
   sits apart from both without shouting over them.
   The glow is a box-shadow rather than a border, so it does not move anything
   when it brightens on hover - a border that thickens shifts every button
   beside it by a pixel.
   ========================================================================== */
.btn-lit {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 41px;
    padding: 10px 18px;
    border: 1.5px solid var(--accent);
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--accent);
    font-size: 14px;
    font-weight: 600;
    font-family: var(--font-stack);
    letter-spacing: -0.1px;
    text-decoration: none;
    cursor: pointer;
    box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.10),
                0 0 10px -2px var(--accent);
    transition: box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease;
}
.btn-lit:hover,
.btn-lit:focus-visible {
    background: var(--card-accent-bg);
    box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.18),
                0 0 16px -1px var(--accent),
                0 0 30px -6px var(--accent);
}
.btn-lit:active { transform: translateY(1px); }

/* ==========================================================================
   Screen animation
   --------------------------------------------------------------------------
   Zoom and fade: opacity and scale together, from 0.88 to 1. Chosen at the
   counter out of eight candidates that were briefly selectable in Settings -
   fade, slide, zoom, cross-fade, lateral, mask, grow and none - and the
   picker came out again once this one won. A setting nobody needs to change
   is a setting to remove.

   320ms for a panel, 300ms for a page. Slower than the 120-260ms the first
   attempt used, because that set was tuned for the fiftieth sale of the day
   and turned out to be too quick to see at all.

   ONLY transform and opacity. Both run on the compositor; width, height, top
   and left force layout on every frame, which is where a refurbished till
   drops them. Nothing animates on the way OUT - the decision to dismiss is
   already made, and watching it leave reads as lag.
   ========================================================================== */
@keyframes motionFade     { from { opacity: 0; } to { opacity: 1; } }
@keyframes motionZoomFade { from { opacity: 0; transform: scale(0.88); }
                            to   { opacity: 1; transform: scale(1); } }

.modal-overlay:not(.hidden) { animation: motionFade 220ms ease-out both; }
.modal-overlay:not(.hidden) > .modal {
    animation: motionZoomFade 320ms cubic-bezier(0.16, 1, 0.3, 1) both; }
main.page { animation: motionZoomFade 300ms cubic-bezier(0.16, 1, 0.3, 1) both; }

/* A till is a workplace and somebody may have asked for less movement. It
   still ARRIVES - it just stops travelling. */
@media (prefers-reduced-motion: reduce) {
    .modal-overlay:not(.hidden),
    .modal-overlay:not(.hidden) > .modal,
    main.page { animation-duration: 0.01ms !important; }
}

.modal {
    /* Centre when it fits; when the modal is taller than the (possibly
       keyboard-shrunk) overlay, auto margins collapse to 0 so it anchors to the
       TOP and stays fully scrollable, instead of centring off both ends where
       its top is unreachable without scrolling up to find it. */
    margin: auto;
    background: var(--bg-card);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-premium);
    padding: 24px;
    width: 380px;
    max-width: calc(100vw - 32px);
    /* The return modal grows 60px per line of the receipt being refunded, so
       a three-line receipt passed the bottom of a 1366x768 till and a six-line
       one put Confirm at y=794 and the top of the modal at y=-90: both buttons
       off screen, no scrollbar, and nothing a touch-only cashier could do
       about it. Measured, not guessed.

       These two lines existed already, but only inside the phone breakpoint -
       a WIDTH test the shop's touchscreen never matches. They belong here,
       where every screen gets them. */
    max-height: calc(100vh - 32px);
    overflow-y: auto;
    /* Anchor for the close button below. */
    position: relative;
}

/* Every modal gets one, added by base.html rather than by thirteen templates
   that would each have to remember - and a fourteenth would forget. A visible
   way out matters more than it sounds: Escape already worked, but a till is
   touched with a finger, and on the change calculator the ONLY visible
   control completed the sale as cash.

   The POSITIONING lives on a wrapper span, not on the button. Buttons in this
   stylesheet are claimed by a chain of five :not() selectors per theme, which
   sets position: relative for the glass glow and outranks anything sensible
   this rule could say. Wrapping sidesteps the whole argument and leaves the
   button looking like every other button in the shop's chosen theme. */
.modal-close-wrap {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 2;
    line-height: 0;
}
/* Square, and the icon centres by geometry rather than by font metrics. */
.modal-close {
    width: 40px;
    height: 40px;
    min-height: 40px;
    padding: 0;
}
.modal-close svg { display: block; }

/* Trial countdown and the lapsed notice. Full width at the top of the page
   because it must survive being ignored for a fortnight - a shop that never
   reads it is exactly the shop that gets caught out. */
.licence-banner {
  padding: 10px 14px;
  border-radius: 8px;
  margin-bottom: 14px;
  font-weight: 600;
  border: 1px solid transparent;
}
.licence-banner-trial {
  background: color-mix(in srgb, var(--warning) 14%, transparent);
  border-color: color-mix(in srgb, var(--warning) 45%, transparent);
  color: var(--text-main);
}
/* Licence, shown with the shop name it is bound to. The status is a pill so
   "is this till activated" is answerable at a glance, without reading. */
.licence-box {
  border: 1px solid var(--border-color);
  border-radius: 8px;
  padding: 12px 14px;
  margin: 6px 0 16px;
  background: var(--bg-main);
}
.licence-status {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.licence-pill {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  border: 1px solid var(--border-color);
}
.licence-pill-ok {
  background: color-mix(in srgb, var(--success) 18%, transparent);
  border-color: color-mix(in srgb, var(--success) 55%, transparent);
}
.licence-pill-trial {
  background: color-mix(in srgb, var(--warning) 18%, transparent);
  border-color: color-mix(in srgb, var(--warning) 55%, transparent);
}
.licence-pill-bad {
  background: color-mix(in srgb, var(--danger) 18%, transparent);
  border-color: color-mix(in srgb, var(--danger) 55%, transparent);
}
.licence-form { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* Pasting is the fallback for a mail client that ate the attachment, so it is
   folded away rather than competing with the file picker. */
.licence-paste { flex-basis: 100%; margin-top: 4px; }
.licence-paste summary { cursor: pointer; font-size: 13px; color: var(--text-muted); }
.licence-paste textarea { width: 100%; margin-top: 6px; font-family: monospace; }

.licence-banner-expired {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  border-color: color-mix(in srgb, var(--danger) 50%, transparent);
  color: var(--text-main);
}

/* An add-on's settings panel. Wider than a till modal because it holds a
   form, and scrolling inside itself like every other modal. */
.modal.addon-modal { width: 520px; }

/* Pay by phone: a waiting state, not a form. The figure is the biggest thing
   on it because the cashier is reading it out to the customer, and the two
   actions sit together at the bottom so neither is the obvious default -
   cancelling and checking again are equally reasonable at any moment. */
.modal.payphone-modal { width: 380px; text-align: center; }
.payphone-amount { font-size: 34px; font-weight: 800; margin: 6px 0 2px; letter-spacing: -.02em; }
.payphone-status { color: var(--ink-3, #667); margin: 0 0 18px; min-height: 2.4em; }
.payphone-actions { display: grid; gap: 8px; }
/* Left-aligned inside a centred dialog: a checkbox reads as a checkbox
   next to its label, not as a centred sentence with a box in front. */
/* The text is a real element, not a bare node: an anonymous flex item wraps
   where a span does not.
   The width is the important part. A global rule - `label input { width: 100% }`
   at the top of this file - stretches any input inside a label to fill it,
   which is right for a text field and disastrous for a checkbox: it ate the
   whole row and pushed the text out of the dialog. Overridden here rather than
   loosened there, because every other form on the till depends on that rule. */

/* Multi-register carries a table of tills and a second form of equipment
   settings, which is far more than the numeric fields the other add-on modals
   hold. 520px crops the table; this is still narrow enough to read on a
   tablet, which is where a second till gets set up in the first place. */
.modal.addon-modal-wide { width: min(760px, 92vw); }

/* The sections inside a modal that holds more than one thing. Without this the
   register table and the equipment form run together with no gap between. */
.modal-block + .modal-block { margin-top: 26px; padding-top: 22px; border-top: 1px solid var(--line, #DFE5EE); }
.modal-block h4 { margin: 0 0 10px; }

/* An error toast waits to be read and dismissed, so it says it is clickable. */
.toast-sticky { cursor: pointer; }

/* A disabled button looked exactly like a live one in the light and dark
   themes - the only :disabled rule in this file was scoped to glass. Charge
   sits disabled on an empty basket, and Take payment ships disabled, so the
   cashier's first read of both was a button that simply ignored them. */
button:disabled, button[disabled] {
    opacity: 0.45;
    cursor: not-allowed;
    transform: none;
}
button:disabled:hover { transform: none; box-shadow: none; }
/* Offered but not usable right now - the Points tender with nobody attached.
   Dimmed like a disabled control, but still pressable, because pressing it is
   how the cashier finds out why. */
.pay-tender-btn.tender-unavailable { opacity: 0.55; }

/* padding-right so the heading does not run under the close button. */
.modal h3 { margin: 0 0 16px; font-size: 16px; padding-right: 56px; }
.modal-tabs { display: flex; gap: 6px; margin-bottom: 16px; }
.modal-tab { flex: 1; background: var(--bg-main); color: var(--text-muted); padding: 8px; font-size: 13px; }
.modal-tab.active { background: var(--accent); color: var(--accent-fg); }
.modal-panel.hidden { display: none; }
.modal-cancel { background: transparent; color: var(--text-muted); border: 1px solid var(--border-color); margin-top: 10px; width: 100%; }
.modal-cancel:hover { background: var(--bg-main); }
/* Inline checkbox+label row inside a modal (e.g. Taxable on the Edit-item
   form) - the global `label { display:block }` would otherwise stack the box
   above its text. */
.modal-check { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.modal-check input { width: auto; margin: 0; }

/* ==========================================================================
   Settings: collapsible <details> sections
   ========================================================================== */
.settings-section {
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    background: var(--bg-card);
    margin-bottom: 12px;
    overflow: hidden;
}
.settings-section > summary {
    padding: 14px 16px;
    font-size: 15px;
    font-weight: 600;
    color: var(--text-main);
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    user-select: none;
}
.settings-section > summary::-webkit-details-marker { display: none; }
.settings-section > summary::after {
    content: "\203A";  /* single right-angle quote as a chevron */
    color: var(--text-muted);
    font-size: 18px;
    transition: transform 0.15s ease;
}
.settings-section[open] > summary::after { transform: rotate(90deg); }
.settings-section > summary:hover { background: var(--bg-main); }
.settings-section-body { padding: 4px 16px 18px; }
.settings-section-body h3 { font-size: 14px; margin: 18px 0 8px; color: var(--text-main); }
.settings-hint { font-size: 13px; color: var(--text-muted); margin: 4px 0 12px; }

/* A settings value that is displayed and not edited - the shop name, which
   comes from the licence. Spaced like the labels around it so it reads as part
   of the same list rather than as a stray paragraph. */
.settings-readonly-field { display: block; margin: 0 0 4px; }
.settings-readonly-field strong { font-size: 15px; }

/* Notification opt-in rows + the sub-settings each one reveals when enabled. */
.notif-toggles {
    margin-top: 8px;
    border-top: 1px solid var(--border-color);
    padding-top: 12px;
    /* Two alert switches to a row. auto-fit means a narrow shop monitor drops
       back to one column instead of clipping. */
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    gap: 2px 24px;
    align-items: start;
}
.notif-toggle-row { display: flex; align-items: center; gap: 8px; margin: 12px 0; font-size: 14px; color: var(--text-main); }
.notif-toggle-row input { width: auto; margin: 0; }
.notif-opts { margin: 0 0 14px 26px; padding-left: 14px; border-left: 2px solid var(--border-color); }
.notif-opts[hidden] { display: none; }

/* ==========================================================================
   Misc page-specific tweaks
   ========================================================================== */
#scan-feedback, #add-item-feedback, #import-feedback, #logo-feedback { margin-top: 10px; font-size: 14px; min-height: 20px; }
#import-feedback { white-space: pre-wrap; }
#scan-feedback.ok, .ok { color: var(--success); }
#scan-feedback.err, .err { color: var(--danger); }
#scan-feedback.warn, .warn { color: var(--warning); }

/* Delivery-status panel (Settings > Email notifications). Alert failures used
   to reach only the log file, which nobody on a shop floor reads. */
.delivery-bad { color: var(--danger, #b42318); }
.delivery-table { width: 100%; border-collapse: collapse; font-size: 14px; margin: 6px 0 10px; }
.delivery-table th, .delivery-table td { text-align: left; padding: 4px 6px; border-bottom: 1px solid rgba(128,128,128,0.25); vertical-align: top; }
.delivery-table td:last-child { word-break: break-word; }

/* Video background. The still-image background is a CSS background-image on
   body (see --custom-bg-image above); a video cannot be, so it is a real
   element pinned behind everything. Same darkening layer as the photo version
   gets via its linear-gradient, applied here as a ::after overlay so muted
   text over a bright frame stays readable. */
.bg-video {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: -2;
    pointer-events: none;
}
/* Rendered only alongside .bg-video, so the still-image background - which
   bakes its own linear-gradient into background-image - is not darkened twice. */
.bg-video-scrim {
    position: fixed;
    inset: 0;
    background: rgba(4, 8, 18, 0.4);
    z-index: -1;
    pointer-events: none;
}
/* A reduced-motion preference is honoured by PAUSING playback in JS (see
   base.html) - CSS cannot stop a <video>; animation-play-state applies to CSS
   animations only. The frame stays on screen so the glass theme keeps a
   backdrop behind its cards rather than falling back to nothing. */

/* Named tax rates (Settings > tax). Inline add-row under the rates table. */
.tax-rate-add { display: flex; gap: 6px; align-items: center; margin: 6px 0 4px; flex-wrap: wrap; }
.tax-rate-add input[type="text"] { flex: 1 1 10rem; min-width: 0; }
.tax-rate-add input[type="number"] { width: 6rem; }

/* CSV import review panel. Scrolls rather than pushing the page around when a
   large file has many rejected rows. */
.import-plan { max-height: 22rem; overflow-y: auto; margin: 8px 0; padding: 8px 10px;
               border: 1px solid rgba(128,128,128,0.3); border-radius: 6px; }
.import-plan p { margin: 4px 0; }

/* Toggle switches
   ----------------------------------------------------------------------
   Used for every on/off in Settings. A switch reads as a deliberate act in a
   way a checkbox does not, which matters when flipping one mutes a shop's
   alerts or turns off card payments.

   The look borrows from the "liquid glass" style - spring overshoot, a knob
   with real highlights, a track that lights up - but is deliberately plain CSS:
   no JavaScript, no SVG goo/displacement filters, no container queries, and no
   round()/linear()/light-dark(). Two reasons. These run on refurbished PCs
   whose browser version is not something we control, and the glass theme
   already spends a backdrop-filter on every card, sometimes over a playing
   video - stacking an SVG filter on top risks stutter on the till itself.

   The input stays a real checkbox and keeps its name/form attributes, so the
   settings form still posts without JavaScript. The span is decoration only. */
.switch {
    flex: 0 0 auto;
    width: 50px;
    height: 23px;
    border-radius: 999px;
    position: relative;
    background: rgba(128, 128, 128, 0.35);
    box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.28),
                inset 0 0 0 1px rgba(255, 255, 255, 0.06);
    transition: background 0.22s ease;
}
.switch::after {
    content: "";
    position: absolute;
    top: 2.5px;
    left: 2.5px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: linear-gradient(180deg, #fff, #e7e9ee);
    /* The knob's own highlight and drop shadow - this is what reads as glass
       rather than as a flat dot. */
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35),
                0 0 0 0.5px rgba(0, 0, 0, 0.06),
                inset 0 1px 0 rgba(255, 255, 255, 0.9),
                inset 0 -1px 1px rgba(0, 0, 0, 0.08);
    /* Slight overshoot on the way across, the one thing that makes a switch
       feel physical. cubic-bezier, not linear(), for browser reach. */
    transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* The real control: visually hidden, still focusable, still posts, and still
   a real target for assistive tech and automation.

   Clipped rather than sized to zero. A 0x0/opacity:0 input is treated as not
   visible - screen readers can cope, but anything driving the page by the
   accessibility tree cannot click it, which is how the first version of this
   silently refused to toggle under test. Clipping keeps the box a real size. */
.notif-toggle-row input[type="checkbox"],
.channel-row input[type="checkbox"] {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}
/* The input itself is off-screen, so the ring has to be drawn on the track or
   keyboard users get no focus indication at all. */
/* Honour a reduced-motion preference: the switch still moves, it just stops
   springing. */
@media (prefers-reduced-motion: reduce) {
    .switch, .switch::after { transition-duration: 0.01ms; }
}
/* Rows that carry a switch line up on one baseline. */

:is(.notif-toggle-row, .channel-row) input:checked + .switch {
    background: var(--accent, #2563eb);
    box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2),
                inset 0 0 0 1px rgba(255, 255, 255, 0.12),
                0 0 0 1px rgba(37, 99, 235, 0.25);
}
:is(.notif-toggle-row, .channel-row) input:checked + .switch::after {
    transform: translateX(27px);
}
/* The state in words as well as colour: colour alone is hard to read in
   sunlight and for colour-blind staff. The words come from the page (base.html
   sets --switch-on / --switch-off), so Spanish reads SI / NO. */
.switch::before {
    content: var(--switch-off, "OFF");
    position: absolute;
    top: 0; bottom: 0; right: 6px;
    display: flex; align-items: center;
    font-size: 10px; font-weight: 800; letter-spacing: 0.3px;
    color: var(--text-muted, #888);
}
:is(.notif-toggle-row, .channel-row) input:checked + .switch::before {
    content: var(--switch-on, "ON");
    right: auto; left: 7px;
    color: #fff;
}
/* The input itself is off-screen, so the ring has to be drawn on the track or
   keyboard users get no focus indication at all. */
:is(.notif-toggle-row, .channel-row) input:focus-visible + .switch {
    outline: 2px solid var(--accent, #2563eb);
    outline-offset: 3px;
}
:is(.notif-toggle-row, .channel-row) input:disabled + .switch {
    opacity: 0.5;
}
/* Honour a reduced-motion preference: the switch still moves, it just stops
   springing. */
@media (prefers-reduced-motion: reduce) {
    .switch, .switch::after { transition-duration: 0.01ms; }
}
/* Rows that carry a switch line up on one baseline. */
.notif-toggle-row, .channel-row {
    display: flex;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    margin: 6px 0;
}

/* Notification channels - one row per channel, the switch doubling as the
   channel's on/off and as the control that reveals its fields. */
.channel { border-top: 1px solid rgba(128,128,128,0.22); padding: 10px 0 2px; }
.channel:first-of-type { border-top: 0; }
.channel-row { font-weight: 600; }
.channel-name { font-size: 14px; }
.channel-tag { font-size: 11px; font-weight: 600; padding: 2px 7px; border-radius: 999px;
               background: var(--success-bg, rgba(16,185,129,0.15));
               color: var(--success, #10b981); }
.channel-status { font-size: 12px; font-weight: 400; color: var(--text-muted); }
.channel-opts { padding: 8px 0 6px 50px; }
@media (max-width: 900px) { .channel-opts { padding-left: 12px; } }

/* Segmented EN/ES control (Settings > App & appearance).
   Looks like the toggle that used to sit in the header, but is built from
   radios so it posts with the rest of the settings form. The header version is
   an instant-apply form that reloads the page - fine in a topbar, destructive
   here, where it would discard anything typed and not yet saved. */
.field-label { display: block; margin-bottom: 4px; }
.recovery-code-out { margin-top: 10px; padding: 12px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-card); }
.recovery-code-value { font-size: 20px; letter-spacing: 2px; }
/* A settings label with its action on the same line: the label to the left,
   the button to the right, and the hint on its own line beneath. */
.setting-inline-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.setting-inline-row .field-label { margin-bottom: 0; }
/* The display links are a full-width row under the Hardware toggles, not a
   grid cell squeezed into the second column beside the scale. */
.settings-groups > .display-cell { grid-column: 1 / -1; }
.segmented { display: inline-flex; padding: 3px; gap: 2px; border-radius: 999px;
             background: rgba(128,128,128,0.18);
             box-shadow: inset 0 1px 2px rgba(0,0,0,0.2); margin-bottom: 10px; }
.segmented-option { position: relative; margin: 0; cursor: pointer; }
.segmented-option input { position: absolute; width: 1px; height: 1px; margin: -1px;
                          padding: 0; border: 0; overflow: hidden;
                          clip: rect(0 0 0 0); clip-path: inset(50%); }
.segmented-option span { display: block; padding: 5px 18px; border-radius: 999px;
                         font-size: 13px; font-weight: 600; line-height: 1;
                         color: var(--text-muted);
                         transition: background 0.18s ease, color 0.18s ease; }
.segmented-option input:checked + span { background: var(--accent, #2563eb); color: var(--accent-fg);
                                         box-shadow: 0 1px 2px rgba(0,0,0,0.25); }
.segmented-option input:focus-visible + span { outline: 2px solid var(--accent, #2563eb);
                                               outline-offset: 2px; }

/* ==========================================================================
   Accent colours
   --------------------------------------------------------------------------
   Six presets a shop can pick to suit the background they uploaded, set as
   data-accent on <html> exactly the way data-theme already is.

   Every accented thing in this stylesheet already reads var(--accent), so a
   preset only has to redefine the tokens - nothing else changes. Each is
   declared twice because glass needs a brighter, more saturated accent to stay
   legible over a photo or video, while the flat themes need a darker one to
   hold contrast against white. Blue is the default and matches the original
   values exactly, so an install that never touches this looks identical.
   ========================================================================== */
:root[data-accent="neutral"] { --accent: #334155; --accent-hover: #1E293B; --card-accent-bg: #E2E8F0; }
:root[data-accent="violet"] { --accent: #7C3AED; --accent-hover: #6D28D9; --card-accent-bg: #EDE9FE; }
:root[data-accent="orange"] { --accent: #EA580C; --accent-hover: #C2410C; --card-accent-bg: #FFEDD5; }
:root[data-accent="rose"]   { --accent: #E11D48; --accent-hover: #BE123C; --card-accent-bg: #FFE4E6; }
:root[data-accent="green"]  { --accent: #059669; --accent-hover: #047857; --card-accent-bg: #D1FAE5; }
/* Gold is the one preset that flips the foreground: #EAB308 carries white text
   at only 2.9:1, but dark warm brown at 7.6:1. Darkening the gold until white
   worked would have produced bronze, not gold. */
:root[data-accent="gold"]   { --accent: #EAB308; --accent-hover: #FACC15; --accent-fg: #422006; --card-accent-bg: #FEF9C3; }

:root[data-theme="glass"][data-accent="neutral"] {
    --accent: #E2E8F0; --accent-hover: #F1F5F9;
    --glass-btn-base: #475569; --glass-btn-glow: #F1F5F9;
    --card-accent-bg: rgba(226, 232, 240, 0.24);
}
:root[data-theme="glass"][data-accent="violet"] {
    --accent: #A78BFA; --accent-hover: #C4B5FD;
    --glass-btn-base: #6D28D9; --glass-btn-glow: #C4B5FD;
    --card-accent-bg: rgba(167, 139, 250, 0.28);
}
:root[data-theme="glass"][data-accent="orange"] {
    --accent: #FB923C; --accent-hover: #FDBA74;
    --glass-btn-base: #C2410C; --glass-btn-glow: #FDBA74;
    --card-accent-bg: rgba(251, 146, 60, 0.28);
}
:root[data-theme="glass"][data-accent="rose"] {
    --accent: #FB7185; --accent-hover: #FDA4AF;
    --glass-btn-base: #BE123C; --glass-btn-glow: #FDA4AF;
    --card-accent-bg: rgba(251, 113, 133, 0.28);
}
:root[data-theme="glass"][data-accent="gold"] {
    --accent: #FCD34D; --accent-hover: #FDE68A; --accent-fg: #422006;
    /* The only preset that flips the button label dark. A gold dark enough to
       carry white text is bronze, and bronze was indistinguishable from the
       orange preset sitting next to it. #422006 on #EAB308 is 7.6:1. */
    --glass-btn-base: #EAB308; --glass-btn-fg: #422006; --glass-btn-glow: #FDE68A;
    --card-accent-bg: rgba(252, 211, 77, 0.28);
}
:root[data-theme="glass"][data-accent="green"] {
    --accent: #34D399; --accent-hover: #6EE7B7;
    --glass-btn-base: #047857; --glass-btn-glow: #6EE7B7;
    --card-accent-bg: rgba(52, 211, 153, 0.28);
}

/* ==========================================================================
   A shop's own accent colour.

   The seven presets above hand-write their values. This one cannot: the shop
   picked a single colour and the three themes each need a different shade of
   it, so base.html ships the derived palette as --accent-custom-* properties
   and these rules only decide WHICH of them each theme reads. The maths that
   produced them, and the contrast guarantees, live in accent.py.

   Order is load-bearing. The prefers-color-scheme block below also matches
   glass (glass is not light), so the glass rule has to come after it at equal
   specificity to win - moving it above would give a glass till the dark-theme
   accent, which is tuned for a solid card rather than a video.
   ========================================================================== */
/* Defaults for the twelve properties base.html injects for a custom accent.
   They are never the values actually used - the page overrides all twelve in a
   <style> block that loads after this file - but they have to exist here for
   two reasons.

   The first is that a var() naming a token nothing defines fails silently: the
   browser drops the declaration and the element renders unstyled, with no
   error anywhere. StylesheetIntegrityTests exists to catch exactly that, and
   it should not have to special-case these.

   The second is the failure they cover. If a page ever carries
   data-accent="custom" without its style block - a half-finished deploy, a
   stale cached document - the till falls back to blue rather than losing every
   accented control on screen. Blue because it is the default accent, so the
   fallback is a shop that looks untouched rather than a shop that looks
   broken. Values match the blue preset exactly. */
:root {
    --accent-custom-light: #2563EB;
    --accent-custom-light-hover: #1D4ED8;
    --accent-custom-light-fg: #FFFFFF;
    --accent-custom-light-card: #DBEAFE;
    --accent-custom-dark: #4F8BFF;
    --accent-custom-dark-hover: #6FA1FF;
    --accent-custom-dark-fg: #FFFFFF;
    --accent-custom-dark-card: #1E3A5F;
    --accent-custom-glass: #4F8BFF;
    --accent-custom-glass-hover: #6FA1FF;
    --accent-custom-glass-fg: #FFFFFF;
    --accent-custom-glass-card: rgba(79, 139, 255, 0.28);
}

:root[data-accent="custom"] {
    --accent: var(--accent-custom-light);
    --accent-hover: var(--accent-custom-light-hover);
    --accent-fg: var(--accent-custom-light-fg);
    --card-accent-bg: var(--accent-custom-light-card);
}

@media (prefers-color-scheme: dark) {
    /* Follows the system, unless the shop pinned the light theme. */
    :root[data-accent="custom"]:not([data-theme="light"]) {
        --accent: var(--accent-custom-dark);
        --accent-hover: var(--accent-custom-dark-hover);
        --accent-fg: var(--accent-custom-dark-fg);
        --card-accent-bg: var(--accent-custom-dark-card);
    }
}

:root[data-theme="dark"][data-accent="custom"] {
    --accent: var(--accent-custom-dark);
    --accent-hover: var(--accent-custom-dark-hover);
    --accent-fg: var(--accent-custom-dark-fg);
    --card-accent-bg: var(--accent-custom-dark-card);
}

/* Restated rather than left to the base rule: a shop that pinned light on a
   machine set to dark needs something at this specificity to beat the media
   block above, which the (0,2,0) base rule cannot do. */
:root[data-theme="light"][data-accent="custom"] {
    --accent: var(--accent-custom-light);
    --accent-hover: var(--accent-custom-light-hover);
    --accent-fg: var(--accent-custom-light-fg);
    --card-accent-bg: var(--accent-custom-light-card);
}

:root[data-theme="glass"][data-accent="custom"] {
    --accent: var(--accent-custom-glass);
    --accent-hover: var(--accent-custom-glass-hover);
    --accent-fg: var(--accent-custom-glass-fg);
    /* The solid button fill follows the accent the way every glass preset's
       does, and takes the same label colour the accent was given - which is
       what keeps a pale custom accent from printing white-on-white on a
       button, the failure the gold preset exists to avoid. */
    --glass-btn-base: var(--accent-custom-glass);
    --glass-btn-fg: var(--accent-custom-glass-fg);
    --glass-btn-glow: var(--accent-custom-glass-hover);
    --card-accent-bg: var(--accent-custom-glass-card);
}

/* Swatch picker (Settings > App & appearance). Radios drawn as colour discs,
   so it posts with the settings form like every other field. */
/* 12px above, not 2. The label and the circles are siblings, so their margins
   COLLAPSE to the larger of the two - 2px here against the label's 4px meant a
   4px gap, and with help text switched off there is no hint paragraph between
   them to make up the difference. The heading read as touching the swatches. */
.swatches { display: flex; gap: 10px; flex-wrap: wrap; margin: 12px 0 14px; }
.swatch { position: relative; margin: 0; cursor: pointer; line-height: 0; }
.swatch input { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
                border: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); }
.swatch span { display: block; width: 30px; height: 30px; border-radius: 50%;
               box-shadow: inset 0 1px 2px rgba(255,255,255,0.4),
                           inset 0 -1px 2px rgba(0,0,0,0.25),
                           0 1px 3px rgba(0,0,0,0.3);
               transition: transform 0.16s cubic-bezier(0.34, 1.56, 0.64, 1); }
.swatch input:checked + span { transform: scale(1.15);
                               box-shadow: inset 0 1px 2px rgba(255,255,255,0.4),
                                           0 0 0 2px var(--bg-card, #fff),
                                           0 0 0 4px currentColor; }
.swatch input:focus-visible + span { outline: 2px solid var(--text-main); outline-offset: 3px; }
.swatch--blue   span { background: #2563EB; } .swatch--blue   { color: #2563EB; }
.swatch--neutral span { background: #334155; } .swatch--neutral { color: #334155; }
.swatch--violet span { background: #7C3AED; } .swatch--violet { color: #7C3AED; }
.swatch--orange span { background: #EA580C; } .swatch--orange { color: #EA580C; }
.swatch--rose   span { background: #E11D48; } .swatch--rose   { color: #E11D48; }
.swatch--green  span { background: #059669; } .swatch--green  { color: #059669; }
/* Swatch ring uses a darker gold than the dot: #EAB308 as a text/ring colour
   is illegible on a white card. */
.swatch--gold   span { background: #EAB308; } .swatch--gold   { color: #A16207; }

/* The eighth well, for a shop's own colour. Unset, it shows the spectrum
   rather than a grey dot - grey among seven colours reads as a disabled
   preset, not as an invitation. Once picked, an inline background overrides
   this. The picker itself sits beside the well as its own tab stop, so the
   colour is reachable by keyboard and not only by clicking a swatch. */
.swatch--custom span[aria-hidden] {
    background: conic-gradient(#E11D48, #EAB308, #059669, #2563EB, #7C3AED, #E11D48);
}
.swatch-picker {
    width: 30px; height: 30px; padding: 0; border: none;
    background: none; cursor: pointer; align-self: center;
}
.swatch-picker::-webkit-color-swatch-wrapper { padding: 0; }
.swatch-picker::-webkit-color-swatch {
    border: 2px solid var(--border-color); border-radius: 50%;
}
.swatch-picker::-moz-color-swatch {
    border: 2px solid var(--border-color); border-radius: 50%;
}
.swatch-picker:focus-visible { outline: 2px solid var(--text-main); outline-offset: 3px; }

/* Visually hidden but still announced. Used for the accent swatches, whose
   only visible content is a coloured disc - without this a screen reader would
   read six identical unlabelled radios. */
.sr-only {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    border: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
    white-space: nowrap;
}

/* Swatches preview the accent as it will actually appear. Each preset has a
   brighter glass variant, so on the glass theme the discs must show that
   variant - otherwise orange looks muddy in the picker and bright on the page. */
:root[data-theme="glass"] .swatch--blue   span { background: #4F8BFF; }
:root[data-theme="glass"] .swatch--blue         { color: #4F8BFF; }
:root[data-theme="glass"] .swatch--neutral span { background: #E2E8F0; }
:root[data-theme="glass"] .swatch--neutral       { color: #E2E8F0; }
:root[data-theme="glass"] .swatch--violet span { background: #A78BFA; }
:root[data-theme="glass"] .swatch--violet       { color: #A78BFA; }
:root[data-theme="glass"] .swatch--orange span { background: #FB923C; }
:root[data-theme="glass"] .swatch--orange        { color: #FB923C; }
:root[data-theme="glass"] .swatch--rose   span { background: #FB7185; }
:root[data-theme="glass"] .swatch--rose         { color: #FB7185; }
:root[data-theme="glass"] .swatch--green  span { background: #34D399; }
:root[data-theme="glass"] .swatch--green        { color: #34D399; }
:root[data-theme="glass"] .swatch--gold   span { background: #FCD34D; }
:root[data-theme="glass"] .swatch--gold         { color: #FCD34D; }

/* Logo cropper. The canvas is the output, not a preview of one - what is drawn
   is exactly what gets uploaded, so there is no second rendering path that
   could disagree with it. */
#logo-crop { margin: 10px 0; }
#logo-canvas { display: block; margin: 8px 0; border-radius: 10px; cursor: grab;
               background: repeating-conic-gradient(rgba(128,128,128,.18) 0% 25%, transparent 0% 50%) 50% / 16px 16px;
               box-shadow: 0 0 0 1px var(--border-color), var(--shadow-sm); touch-action: none; }
#logo-canvas:active { cursor: grabbing; }
.crop-shapes { display: inline-flex; padding: 3px; gap: 2px; border-radius: 999px;
               background: rgba(128,128,128,0.18); margin-bottom: 4px; }
.crop-zoom { display: flex; align-items: center; gap: 10px; font-size: 13px;
             color: var(--text-muted); max-width: 360px; }
.crop-zoom input[type="range"] { flex: 1; accent-color: var(--accent); }


/* ==========================================================================
   Phone layout (<= 600px)
   --------------------------------------------------------------------------
   Width-triggered, deliberately NOT `pointer: coarse` and not user-agent
   sniffing. Coarse-pointer would also catch the shop's touchscreen till and
   resize controls there, which is not wanted; UA strings lie and break with
   every new device. Width is the thing that actually constrains the layout.

   The 900px breakpoint above already stacks the shell. This narrower one is
   about a hand holding a phone: targets big enough for a thumb, no sideways
   scrolling, and tables cut down to what identifies a row.
   ========================================================================== */
.modal-narrow-only { display: none; }

@media (max-width: 600px) {
    /* --- touch targets ---------------------------------------------------
       44px is the long-standing minimum for a fingertip. The steppers and the
       Remove link were 24-26px, which is a mis-tap waiting to happen when the
       thing next to them deletes an item. */
    button:not(.stepper-btn), .btn-secondary, .btn-small {
        min-height: 44px;
    }
    .stepper-btn { width: 44px; height: 44px; font-size: 18px; }
    .remove-link { min-height: 44px; padding: 10px 12px; }
    .sidebar-link { min-height: 44px; }
    .swatch { width: 44px; height: 44px; }
    .switch { transform: scale(1.25); transform-origin: left center; }

    /* --- iOS zoom trap ---------------------------------------------------
       Safari zooms the whole page when a focused input's font is under 16px,
       and does not zoom back out. Every form field here was 15px. */
    input:not([type="checkbox"]):not([type="radio"]), select, textarea {
        font-size: 16px;
    }

    /* --- keyboard-only affordances ---------------------------------------
       Hide ONLY the keyboard tip. .scanner-hint is the shared class for modal
       help text - the change due, the card-waiting message, "refund this on
       the terminal" - so blanking the class took all of those with it. */
    .kbd-hint { display: none; }       /* "Press F2 to jump back" - no F2 key */
    /* Buttons keep their row and wrap onto a second line if they must. */
    .scanner-card .scanner-hint { flex-wrap: wrap; justify-content: flex-start; }

    /* --- Inventory: identify the row, tap it for the rest ----------------
       Nine columns cannot fit 390px. Name, price and qty identify a row; the
       rest (and every action) live in the popup the row opens. */
    .items-table .col-sku,
    .items-table .col-category,
    .items-table .col-cost,
    .items-table .col-taxable,
    .items-table .col-threshold,
    .items-table .col-adjust,
    .items-table .row-actions { display: none; }
    .items-table tbody tr { cursor: pointer; }
    .items-table td.col-name { min-width: 0; max-width: none; white-space: normal; font-weight: 600; }

    /* --- History: five columns nearly fit; drop the two least useful -----
       Item count and tender are both on the receipt itself, which the Reprint
       link still opens. */
    .history-table .col-count,
    .history-table .col-payment { display: none; }

    /* --- Analytics: the date range was one unbreakable row --------------- */
    .date-range-form { flex-wrap: wrap; }
    .date-range-form label { flex: 1 1 45%; }
    .date-range-form button { flex: 1 1 100%; }

    /* --- the popup is the phone UI now, so give it the screen ------------ */
    .modal { width: 100%; max-width: none; }
    .modal-narrow-only { display: block; }
    .modal-narrow-only .stock-stepper { display: flex; gap: 10px; align-items: center; margin-bottom: 14px; }
    .modal-narrow-only .stock-stepper input { width: 5rem; text-align: center; }
    #edit-remove-btn { width: 100%; margin-top: 4px; }

    /* Any table that still exceeds the screen scrolls inside its own box
       rather than dragging the whole page sideways with it. */
    .sales-table, .delivery-table { display: block; overflow-x: auto; }

    /* The Add-an-item form is eight fields deep and sat between the top of the
       page and the item list, which is what people actually come here for. */
    .collapse-on-phone > summary { cursor: pointer; list-style: revert; }
    .collapse-on-phone > summary h2 { display: inline; }

    /* The CSV header example is one long comma-separated token with no spaces,
       so normal word wrapping can never break it and it pushed the page out on
       its own. */
    code { overflow-wrap: anywhere; }
}

/* The collapsible section is a phone affordance only: on a wide screen JS
   leaves it open and the marker is hidden, so it reads exactly as it did. */
@media (min-width: 601px) {
    .collapse-on-phone > summary { list-style: none; }
    .collapse-on-phone > summary::-webkit-details-marker { display: none; }
}

/* ==========================================================================
   Vendor attribution (foot of Settings)
   --------------------------------------------------------------------------
   Quiet by design: this identifies the vendor to whoever is configuring the
   till, it is not advertising aimed at the shop's customers, so it sits below
   Save at reduced weight rather than in the chrome.
   ========================================================================== */
.about-block {
    margin: 32px 0 8px;
    padding-top: 18px;
    border-top: 1px solid var(--border-color);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
}
.about-product { font-size: 13px; font-weight: 600; color: var(--text-muted); }
.about-version { font-weight: 400; opacity: .75; }
.about-vendor { display: flex; align-items: center; gap: 9px; font-size: 12px; color: var(--text-muted); }
.about-vendor-logo { height: 26px; width: auto; display: block; opacity: .9; }

/* The supplied logo is a white wordmark with a cyan mark - drawn for dark
   backgrounds, and invisible on the Light theme. There is no dark variant of
   the asset, so invert it and rotate the hue back: invert() flips white to
   black but also turns the cyan red, and hue-rotate(180deg) returns it to
   cyan. Cheaper and more maintainable than shipping a second image. */
:root[data-theme="light"] .about-vendor-logo { filter: invert(1) hue-rotate(180deg); }
@media (prefers-color-scheme: light) {
    :root[data-theme="auto"] .about-vendor-logo { filter: invert(1) hue-rotate(180deg); }
}

/* History tabs. Sales, Close Day, and whatever an add-on contributes. */
.history-tabs { display: flex; gap: 6px; margin-bottom: 14px; flex-wrap: wrap; }
.history-tab {
    background: var(--bg-main);
    color: var(--text-muted);
    border: 1px solid var(--border-color);
    padding: 8px 16px;
    font-size: 13px;
}
.history-tab.active { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
.history-panel.hidden { display: none; }
/* Only an out-of-balance drawer is coloured; a correct one stays neutral. */
.variance-bad { color: var(--danger); font-weight: 700; }
.variance-over { color: var(--warning); font-weight: 700; }
@media (max-width: 600px) {
    .closeday-table .col-register,
    .closeday-table .col-closedby,
    .closeday-table .col-cash,
    .closeday-table .col-card,
    .closeday-table .col-moves { display: none; }
}

/* Day x hour heatmap. A plain table of tinted cells - no chart library needed
   for a 7x24 grid, and the accent colour carries the branding while opacity
   carries the value. */
.heatmap-table { width: 100%; border-collapse: collapse; font-size: 10px; }
.heatmap-table th { color: var(--text-muted); font-weight: 500; padding: 1px 2px; text-align: center; }
.heatmap-table tbody th { text-align: right; padding-right: 6px; white-space: nowrap; }
.heat-cell { background: var(--accent); height: 14px; border-radius: 2px; }
.heatmap-table td { padding: 1px; }

/* ==========================================================================
   Expand one analytics chart to fill the screen
   --------------------------------------------------------------------------
   A fixed overlay rather than the Fullscreen API: on a till the browser is
   often already fullscreen or kiosked, where requestFullscreen() either does
   nothing visible or throws up a "now fullscreen" notification. This behaves
   identically in Firefox and Chromium and needs no permission.
   ========================================================================== */
/* The inline height moved here so the expanded rule can override it. */
.chart-card .chart-canvas-wrap { height: 240px; }

.chart-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    cursor: pointer;
    min-height: 34px;
}
.chart-expand-icon {
    width: 13px; height: 13px; flex: none; opacity: .45;
    border: 1.5px solid currentColor;
    border-radius: 2px;
}
.chart-header:hover .chart-expand-icon,
.chart-header:focus-visible .chart-expand-icon { opacity: .9; }

.chart-backdrop {
    position: fixed; inset: 0; z-index: 2400;
    background: rgba(0, 0, 0, 0.55);
}
.chart-backdrop.hidden { display: none; }

.chart-card.expanded {
    position: fixed;
    inset: 14px;
    z-index: 2410;
    margin: 0;
    overflow: auto;
    display: flex;
    flex-direction: column;
}
/* Use the screen instead of staying 240px tall in a large empty box - but only
   up to a point. A chart stretched to the full height of a 1440p monitor puts
   enormous vertical gaps between consecutive points, which reads as dramatic
   swings in takings that are not there: the same week's sales look calm in the
   small card and alarming expanded. Capping the shape at 16:9 keeps the
   distance between points proportional to the distance along the axis, and
   margin:auto centres what is left rather than leaving it stuck to the header.

   max-height is what stops aspect-ratio winning on a short or wide window;
   without it a wide monitor would demand a box taller than the card. */
.chart-card.expanded .chart-canvas-wrap {
    height: auto;
    flex: 0 1 auto;
    min-height: 0;
    width: 100%;
    aspect-ratio: 16 / 9;
    max-height: 100%;
    margin: auto;
}
/* max-height is the one that matters here, and it was the one missing. The
   collapsed card caps a table at 220px and scrolls it; flex:1 alone does not
   lift that cap, so an expanded report kept scrolling a 528px table through a
   220px slot with six hundred pixels of empty card underneath it. The whole
   point of expanding is to see more rows at once, so the cap goes and the
   table takes the height the card actually has - still scrolling, but only
   once it has run out of screen rather than straight away. */
.chart-card.expanded .table-report-wrap {
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
    overflow: auto;
}
/* A long report is easier to read when the headings stay put while the rows
   move under them, which only becomes true once the table is tall enough to
   scroll meaningfully. */
.chart-card.expanded .table-report-wrap thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    background: var(--bg-card);
}
.chart-card.expanded .heat-cell { height: 26px; }

@media print {
    /* Expanding a chart and hitting print is the obvious way to get one report
       onto paper, so that is what it does: the chart becomes the page. Without
       this the fixed overlay printed at its on-screen position, clipped to the
       first sheet, with the rest of the report grid following it on sheet two.

       No fixed positioning on paper - there is no viewport to be fixed to, and
       the value is whatever the last screen layout happened to be. */
    .chart-card.expanded {
        position: static !important;
        inset: auto !important;
        margin: 0 !important;
        width: 100% !important;
        height: auto !important;
        overflow: visible !important;
        box-shadow: none !important;
        border: none !important;
    }
    /* Sized against the sheet rather than the screen. 60% leaves room for the
       heading and the caveat line under it, so a chart and its own explanation
       stay on one page instead of the note orphaning onto a second. */
    .chart-card.expanded .chart-canvas-wrap {
        aspect-ratio: 16 / 9;
        max-height: 60vh !important;
        margin: 0 auto !important;
        page-break-inside: avoid;
        break-inside: avoid;
    }
    /* A canvas is painted, not laid out, so it needs telling too. */
    .chart-card.expanded .chart-canvas-wrap canvas {
        max-width: 100% !important;
        max-height: 100% !important;
    }
    /* On paper there is no scrolling, so a capped, scrollable table would
       print as a cropped window with the rest of the rows simply absent. */
    .chart-card.expanded .table-report-wrap {
        max-height: none !important;
        height: auto !important;
        overflow: visible !important;
    }
    .chart-card.expanded .table-report-wrap thead { display: table-header-group; }
    .chart-card.expanded .table-report-wrap tr {
        page-break-inside: avoid;
        break-inside: avoid;
    }
    .chart-backdrop { display: none !important; }
    /* Everything else is screen furniture round the one chart being printed. */
    body:has(.chart-card.expanded) .chart-card:not(.expanded) { display: none !important; }
    body:has(.chart-card.expanded) .analytics-controls,
    body:has(.chart-card.expanded) .chart-expand-icon { display: none !important; }
}

/* A frozen settings page. Muted rather than hidden: a shop needs to see what
   it is about to get back, and the licence box inside this same section stays
   at full strength because it is the way out. */
.settings-frozen label,
.settings-frozen .settings-hint,
.settings-frozen h3 { opacity: .55; }
.settings-frozen .licence-box,
.settings-frozen .licence-box * { opacity: 1; }

/* Return flags on a History invoice row. A full return is the louder of the
   two because the money is entirely reversed; a partial keeps the warning
   colour so a glance can tell them apart without reading. */
/* Receipt-driven return: one row per sale line, ticked and quantified. */
.refund-lookup-row { display: flex; gap: 8px; align-items: stretch; }
.refund-lookup-row input { flex: 1 1 auto; min-width: 0; }
.refund-lookup-row button { flex: 0 0 auto; width: auto; margin: 0; padding: 0 14px; }
.refund-line {
  display: grid;
  grid-template-columns: auto 1fr 5.5em auto;
  gap: 8px;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid var(--border-color);
}
.refund-line-name { min-width: 0; overflow-wrap: anywhere; }
.refund-line-qty { width: 100%; margin: 0; }
.refund-line-price { text-align: right; white-space: nowrap; }
/* Spans the full row under the controls rather than squeezing a fifth column. */
.refund-line-note { grid-column: 1 / -1; font-size: 12px; opacity: 0.7; }
#refund-lines-total { font-weight: 700; margin: 10px 0; }

.tag-returned-full { background: var(--danger-bg); color: var(--danger); }
.tag-returned-part { background: var(--warning-bg); color: var(--warning); }

/* Every hide rule here was scoped to one component (.modal-overlay.hidden,
   .sale-summary.hidden, ...), so a plain class="hidden" on anything else did
   nothing at all - the card-waiting Done button showed while the terminal was
   still thinking, and the return modal displayed its receipt lines and its
   no-receipt fields at the same time. Last in the file so it wins ties. */
.hidden { display: none; }

/* The two things you can do from Inventory, side by side and equally weighted.
   Both are <button>: one opens a popup, one navigates, but to the shop they are
   the same kind of choice and should not look like different kinds of control. */
.items-actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 18px; }
.items-actions button { width: auto; margin: 0; }

/* Receiving: header fields sit two-up on a desktop, stacked on a phone. */
.receive-header-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0 14px; }
.receive-header-grid > * { min-width: 0; }
#rc-totals { font-weight: 700; margin: 10px 0; }
.rc-edit { width: auto; padding: 4px 12px; margin: 0; }

/* The sale just finished, kept on screen so the change can be checked after
   the basket clears. Distinguished from the in-progress summary so nobody
   mistakes a completed sale for one still being rung up. */
.sale-summary.last-sale { border-left: 3px solid var(--success); opacity: 0.9; }

/* --- Payment screen (Charge button -> tender types) --- */
.pay-due { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
.pay-remaining { font-size: 18px; font-weight: 600; color: #b35c00; margin-bottom: 4px; }
.pay-taken { font-size: 13px; opacity: 0.8; margin-bottom: 10px; }
.pay-tenders { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
/* Tenders share the row evenly, so two read as halves and three as thirds
   without the template knowing how many the shop has enabled. */
.pay-tender-btn { flex: 1 1 120px; min-height: 52px; font-size: 18px; }

/* --- What the card terminal answered (Datacap cert 3.2/3.3) --- */
/* --- Card outcome ---
   One panel for every card answer, whether it paid the whole basket or one leg
   of a split. A cashier with a customer waiting reads the word and the amount
   from arm's length; the table under it is the processor's own answer, which
   certification 3.2/3.3 wants visible and which support reads down the phone.
   The colour is a second signal, never the only one - the word says it too,
   for the till that has been left on a washed-out screen in a sunny window. */
.terminal-outcome { margin: 12px 0 4px; text-align: center; }
.terminal-outcome .outcome-word {
    font-size: 26px;
    font-weight: 800;
    letter-spacing: 1px;
    line-height: 1.1;
}
.terminal-outcome .outcome-amount { font-size: 30px; font-weight: 700; margin-top: 2px; }
.terminal-outcome .outcome-amount:empty { display: none; }
.terminal-outcome .outcome-note { font-size: 13px; opacity: 0.85; margin-top: 6px; }
.terminal-outcome.approved .outcome-word, .terminal-outcome.approved .outcome-amount { color: #16a34a; }
/* A partial approval is not a success. It is money taken and a sale unfinished,
   so it gets its own colour rather than borrowing the green that tells a
   cashier they are done. */
.terminal-outcome.partial .outcome-word, .terminal-outcome.partial .outcome-amount { color: #d97706; }
.terminal-outcome.declined .outcome-word, .terminal-outcome.error .outcome-word { color: #dc2626; }

.terminal-detail { width: 100%; border-collapse: collapse; margin: 10px 0;
                   font-size: 13px; text-align: left; }
.terminal-detail th, .terminal-detail td { padding: 3px 6px; border-bottom: 1px solid rgba(128,128,128,0.25); }
.terminal-detail th { font-weight: 600; white-space: nowrap; opacity: 0.75; }
.terminal-detail td { font-family: ui-monospace, Consolas, monospace; word-break: break-word; }

/* The vendor logo is a support link; make it feel like one without turning it
   into a blue underlined image. */
.about-vendor-link { display: inline-block; border-radius: 6px; transition: opacity .15s ease; }
.about-vendor-link:hover, .about-vendor-link:focus-visible { opacity: 0.75; }
.about-vendor-link:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }

/* --- Shift sales report (readable on screen, sane on paper) --- */
.shift-report-actions { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; }
.shift-report-meta { max-width: 420px; margin-bottom: 16px; }
.shift-report-totals { max-width: 420px; margin-top: 18px; }
.shift-report-expected td { border-top: 2px solid currentColor; font-size: 16px; }
.shift-report-items { font-size: 12px; opacity: 0.85; }
/* The browser prints its own header and footer -- the page URL and the date --
   in the page margin, and a customer receipt with the till's LAN address
   across the top looks like a printout, not a receipt. The margin is the only
   thing the page controls, and at zero the header has nowhere to render.
   Every print area below sets its own padding, so this takes away the
   browser's whitespace and nothing else. .invoice-doc is the exception and
   gets its margin back explicitly. */
@page { margin: 0; }

/* PRINTING: take the chrome out, leave the layout alone.
   Every print area hides the rest of the screen with
   `body * { visibility: hidden }` and lays itself over the top, absolutely
   positioned. visibility:hidden HIDES but still OCCUPIES, so the app shell
   kept its `min-height: 100vh` and the sidebar its `height: 100vh` and a
   short receipt printed a second, blank page.

   An earlier attempt at this collapsed the shell to `height: 0` with
   `overflow: hidden`. That stopped the blank page and stopped the printing:
   a zero-height html/body leaves pagination nothing to lay out, and receipts
   came out blank. Twice, because the first repair only addressed a transform
   that was compounding it. The lesson is in the shape of the fix below -
   remove what is genuinely not wanted (the chrome, the 100vh floors, the
   entrance animation) and do not try to collapse the page the print area
   still lives inside.

   The trailing blank page is the lesser fault and is left for now: a receipt
   that prints with a spare sheet is a nuisance, a receipt that prints blank
   is a shop that cannot trade. */
@media print {
    /* The 100vh floors, which are what made a short receipt a viewport tall.
       Heights go to auto, NOT to zero. */
    html, body { height: auto !important; min-height: 0 !important; }
    .app-shell, .main-area, main.page {
        height: auto !important;
        min-height: 0 !important;
    }
    /* Neither ever contains a print area, so both can go outright rather than
       merely turn invisible. */
    .sidebar, .topbar { display: none !important; }
    /* No transform on main.page. The zoom-and-fade entrance leaves
       transform: scale(1) behind it (animation-fill-mode: both), and ANY
       transform other than none makes an element the containing block for its
       absolutely positioned descendants - which is every print area in the
       app. Measured: the print area is painted with transform:none and is not
       painted with transform:scale(1). */
    *, *::before, *::after {
        animation: none !important;
        transition: none !important;
        transform: none !important;
    }
}


@media print {
    /* Same trick the receipt uses: keep this block, hide the shell round it. */
    body * { visibility: hidden; }
    #shift-report, #shift-report * { visibility: visible; color: #000 !important; }
    #shift-report { position: absolute; top: 0; left: 0; width: 100%;
                    background: #fff; padding: 12px; }
    #shift-report table { width: 100%; border-collapse: collapse; }
    #shift-report th, #shift-report td { padding: 3px 6px; border-bottom: 1px solid #ccc; }
    .no-print { display: none !important; }
}

/* --- Card receipt copies ---
   A card sale prints the same receipt twice, one copy for the customer to sign
   and one for the shop. The second starts on its own page so the cashier can
   tear them apart. The processor's portion is monospaced because the pad
   aligns its amount column with spaces - a proportional font turns that into
   a mess. */
#receipt-print-area .receipt-doc + .receipt-doc { page-break-before: always; }
#receipt-print-area .card-receipt-body {
    font-family: "Courier New", monospace;
    white-space: pre;
    text-align: left;
    margin: 6px 0;
    font-size: 10px;
    line-height: 1.25;
}
#receipt-print-area .card-receipt-copy { font-weight: bold; text-align: center; margin-top: 6px; }

/* --- Receipt barcode + paper width --- */
#receipt-print-area .receipt-barcode { text-align: center; margin: 4px 0 8px; }
#receipt-print-area .receipt-barcode svg { max-width: 100%; height: auto; }
/* Thermal rolls are 58mm or 80mm wide. Printing a full-page layout onto one
   crops the money column off the right-hand side, which is the kind of fault
   a shop discovers with a customer waiting. */
@media print {
    body.receipt-58mm #receipt-print-area,
    body.receipt-58mm #shift-report { width: 54mm; padding: 2mm; font-size: 10px; }
    body.receipt-80mm #receipt-print-area,
    body.receipt-80mm #shift-report { width: 76mm; padding: 3mm; font-size: 11px; }
    body.receipt-58mm #receipt-print-area table,
    body.receipt-80mm #receipt-print-area table { font-size: inherit; }
}

/* --- Delivery record ---------------------------------------------------
   Visible on screen, unlike #receipt-print-area which is an off-screen print
   buffer. Wrapping this page in that id is what made it render blank. */
#delivery-print-area { max-width: 720px; }
@media print {
    body * { visibility: hidden; }
    #delivery-print-area, #delivery-print-area * { visibility: visible; color: #000 !important; }
    #delivery-print-area { position: absolute; top: 0; left: 0; width: 100%;
                           background: #fff; padding: 12px; }
    #delivery-print-area table { width: 100%; border-collapse: collapse; }
    #delivery-print-area th, #delivery-print-area td { padding: 3px 6px; border-bottom: 1px solid #ccc; }
}

/* --- Where this till is on the network --- */
.lan-address { margin: 10px 0 16px; }
.lan-address-label { font-weight: 600; margin-bottom: 6px; }
.lan-address-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 6px; }
.lan-address-url { font-family: ui-monospace, Consolas, monospace; font-size: 16px;
                   padding: 6px 10px; border-radius: 6px;
                   background: rgba(128,128,128,0.14); user-select: all; }
.lan-copy-btn { padding: 6px 12px; }

/* ==========================================================================
   Customer database & invoicing (customer_db_enabled add-on)

   Every colour here comes from the theme variables defined at the top of this
   file, so these screens follow the light/dark/glass themes and the accent
   picker without knowing which one is active.
   ========================================================================== */

/* Headline figures: the A/R buckets, and a customer's spend/visits/balance.
   Wraps rather than scrolls, because six buckets on a 1024px shop monitor
   would otherwise push the page sideways. */
.stat-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 16px 0 8px;
}

.stat {
  flex: 1 1 140px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 16px;
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}

.stat-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.stat-value {
  font-size: 22px;
  font-weight: 600;
  color: var(--text-main);
  font-variant-numeric: tabular-nums;
}

/* A deactivated customer stays visible but reads as archived. */
.is-inactive td, .is-inactive td a { color: var(--text-muted); opacity: 0.7; }

/* Stacked label/field form used by the customer screens. */
.cust-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 520px;
  margin-bottom: 24px;
}

.cust-form label { display: flex; flex-direction: column; gap: 4px; }
.cust-form input[type="text"], .cust-form input[type="tel"],
.cust-form input[type="email"], .cust-form input[type="number"],
.cust-form textarea, .cust-form select { width: 100%; }
.cust-form .notif-toggle-row { flex-direction: row; align-items: center; gap: 8px; }
.cust-form button { align-self: flex-start; }

/* The Details panel's body: the edit form, and the card controls in the space
   beside it. cust-form is capped at 520px, so on anything wider than a phone
   there is a column of nothing down its right - which is where a shop looks
   for the things it does to a customer rather than the fields it types into
   them. Wraps under the form when the screen is too narrow to hold both. */
.details-body { display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap; }
.details-aside { display: flex; flex-direction: column; gap: 8px; min-width: 12rem; }

/* The invoice/quote totals block, on both the working screen and the printed
   copy. Right-aligned and tabular so the figures line up under each other. */
.invoice-doc-totals {
  margin: 16px 0 24px auto;
  border-collapse: collapse;
  min-width: 260px;
}

.invoice-doc-totals td { padding: 6px 12px; }
.invoice-doc-totals td.num { text-align: right; font-variant-numeric: tabular-nums; }
.invoice-doc-totals tr:last-child td { border-top: 2px solid var(--border-color); }

/* ---- the customer's copy: invoice, quote, credit note, statement ----

   Modelled on the layout Richard supplied. Every accent comes from --accent,
   so a shop that picked violet or green gets a coherent document without a
   second template - and the same colour is what drives the payment page's
   header, so the invoice, the email and the page a customer pays on all
   match. */

.invoice-doc {
  background: #fff;
  color: #1f2933;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  padding: 48px 52px;
  max-width: 860px;
  margin: 0 auto 24px;
  font-size: 14px;
}

/* Header: meta box left, the shop's own logo right. */
.invoice-doc-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 32px;
  margin-bottom: 44px;
}

.invoice-doc-meta {
  border: 2px solid var(--accent);
  min-width: 300px;
}

.invoice-doc-meta-row {
  display: flex;
  justify-content: space-between;
  gap: 24px;
  padding: 9px 14px;
}

.invoice-doc-meta-row + .invoice-doc-meta-row { border-top: 2px solid var(--accent); }
.invoice-doc-meta-label {
  color: var(--accent);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.invoice-doc-meta-value { color: #1f2933; font-variant-numeric: tabular-nums; }

/* The shop's uploaded logo. Contained rather than stretched, because a shop
   uploads whatever aspect ratio it has and a squashed logo looks worse than
   a small one. */
.invoice-doc-logo {
  max-height: 96px;
  max-width: 240px;
  object-fit: contain;
}

/* Bill-to left, shop details right. */
.invoice-doc-parties {
  display: flex;
  justify-content: space-between;
  gap: 40px;
  margin-bottom: 36px;
}

.invoice-doc-party-label {
  color: var(--accent);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin-bottom: 10px;
}

.invoice-doc-party p { margin: 0; line-height: 1.55; }
.invoice-doc-party--shop { text-align: right; }
.invoice-doc-party--shop .invoice-doc-shop-name { font-weight: 700; }

/* Line items. */
.invoice-doc-items { width: 100%; border-collapse: collapse; margin-bottom: 28px; }

.invoice-doc-items thead th {
  background: var(--accent);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: left;
  padding: 11px 14px;
}

.invoice-doc-items thead th.num, .invoice-doc-items tbody td.num { text-align: right; }
.invoice-doc-items tbody td {
  padding: 12px 14px;
  border-bottom: 1px solid #e3e6ea;
  vertical-align: top;
}
.invoice-doc-items tbody td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Subtotal / tax / paid, right-aligned above the headline figure. */
.invoice-doc-subtotals { margin-left: auto; border-collapse: collapse; min-width: 300px; }
.invoice-doc-subtotals td { padding: 5px 14px; }
.invoice-doc-subtotals td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* The headline. Deliberately the largest thing on the page - it is the one
   number the customer is looking for. */
.invoice-doc-total {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 24px;
  margin: 26px 0 8px;
  color: var(--accent);
  font-size: 34px;
  font-weight: 800;
  letter-spacing: -0.01em;
  /* Uppercased in CSS rather than in the string, so "Invoice"/"Quote"/"Credit
     note" and their Spanish equivalents all read as one headline without a
     translated variant of each per document type. */
  text-transform: uppercase;
}
.invoice-doc-total-amount { font-variant-numeric: tabular-nums; }

/* Shown only when something has been paid, so the customer is never asked
   for a figure they have already partly settled. */
.invoice-doc-balance-note { text-align: right; color: #52606d; margin: 0 0 8px; }

.invoice-doc-terms { margin-top: 56px; }
.invoice-doc-terms-label {
  color: var(--accent);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin-bottom: 6px;
}
.invoice-doc-terms p { margin: 0 0 4px; }

/* Pay-by-link block: the QR is for the paper copy, the URL for anyone
   reading it on screen. */
.invoice-doc-pay {
  display: flex;
  align-items: center;
  gap: 18px;
  margin-top: 32px;
  padding: 16px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
}
.invoice-doc-pay img { width: 104px; height: 104px; }
.invoice-doc-pay-text { font-size: 13px; line-height: 1.5; }

/* ---- printing ------------------------------------------------------------

   Three earlier @media print blocks in this file each do
   `body * { visibility: hidden }` and then re-show one specific print area.
   Those rules apply to EVERY page, so a document without one of those ids
   printed as a blank sheet. This block gives the customer-facing documents
   their own print area and re-shows it, the same way the receipt does. */
@media print {
  body * { visibility: hidden; }
  #document-print-area, #document-print-area * {
    visibility: visible;
    color: #1f2933;
  }

  #document-print-area {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    background: #fff;
  }

  /* A shop on the dark or glass theme still hands the customer a white sheet. */
  .invoice-doc {
    border: none;
    box-shadow: none;
    border-radius: 0;
    /* Was 0, which leaned on the browser's page margin for all its whitespace.
       @page above takes that away, so the document carries its own now. */
    padding: 12mm 14mm;
    max-width: none;
    background: #fff;
    color: #1f2933;
    font-size: 12px;
  }

  /* Filled header bars and coloured headings are the design, not decoration,
     so they have to survive the browser's default of dropping backgrounds. */
  #document-print-area, #document-print-area * {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  /* Paper is white whatever the shop runs on screen, so the document uses the
     LIGHT accent even on a dark or glass install. The dark themes lighten
     --accent for contrast against a dark background (#2563EB -> #4F8BFF), and
     that lighter tone prints washed out on white. Re-asserted per accent so a
     shop on violet still prints violet. */
  #document-print-area { --accent: #2563EB; }
  :root[data-accent="neutral"] #document-print-area { --accent: #334155; }
  :root[data-accent="violet"]  #document-print-area { --accent: #7C3AED; }
  :root[data-accent="orange"]  #document-print-area { --accent: #EA580C; }
  :root[data-accent="rose"]    #document-print-area { --accent: #E11D48; }
  :root[data-accent="green"]   #document-print-area { --accent: #059669; }
  :root[data-accent="gold"]    #document-print-area { --accent: #A16207; }
  /* Paper has no dark mode, so a custom accent prints its light-theme shade -
     the one already proven to clear 4.5:1 on white. */
  :root[data-accent="custom"]  #document-print-area { --accent: var(--accent-custom-light); }
  .invoice-doc-items thead th { color: #fff !important; }
  .invoice-doc-meta-label,
  .invoice-doc-party-label,
  .invoice-doc-terms-label,
  .invoice-doc-total { color: var(--accent) !important; }

  /* A long itemised job should not split a row across two sheets. */
  .invoice-doc-items tr { break-inside: avoid; }
  .invoice-doc-items thead { display: table-header-group; }

  .no-print { display: none !important; }
  .table-scroll { overflow-x: visible; }
}

/* Customer picker on the register (customer_db add-on).

   Sits under the scan box rather than beside it: the scanner input must keep
   full width, because a cashier looks at the item and not at the screen. */
.customer-picker { margin-top: 10px; position: relative; }

.customer-picker-search { display: flex; gap: 8px; align-items: center; }
.customer-picker-search input { flex: 1 1 auto; }

/* Results float over the page. Anchored to the picker so a long list cannot
   push the totals panel down and move the buttons under the cashier's hand. */
.customer-results {
  position: absolute;
  z-index: 20;
  left: 0;
  right: 0;
  margin-top: 4px;
  max-height: 240px;
  overflow-y: auto;
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  /* --bg-card is translucent on the glass theme, so a menu floating over a
     chart was legible only where the chart happened to be empty. The blur
     keeps it in the same visual language as the cards while making the names
     readable over whatever is behind them. */
  backdrop-filter: blur(18px) saturate(1.5);
  -webkit-backdrop-filter: blur(18px) saturate(1.5);
}

.customer-result {
  display: block;
  width: 100%;
  text-align: left;
  padding: 10px 14px;
  background: none;
  border: none;
  border-bottom: 1px solid var(--border-color);
  color: var(--text-main);
  cursor: pointer;
  font-size: 15px;
}

.customer-result:last-child { border-bottom: none; }
.customer-result:hover, .customer-result:focus { background: var(--card-accent-bg); }
.customer-result-sub { color: var(--text-muted); font-size: 13px; margin-left: 8px; }
.customer-result-empty { padding: 10px 14px; color: var(--text-muted); }

.customer-attached {
  margin-top: 8px;
  padding: 6px 12px;
  border-radius: var(--radius-sm);
  background: var(--card-accent-bg);
  color: var(--text-main);
  font-size: 14px;
}

/* Scan-to-pay URL: long, and it must not push the block wider than the page. */
.invoice-doc-pay-url { word-break: break-all; color: var(--text-muted); font-size: 12px; }

/* PAID, on the document itself. Ruled and letter-spaced rather than a rotated
   rubber-stamp graphic: this prints on a customer's invoice, and a tilted
   stencil across the figures makes the figures harder to read, which is the
   one thing the page exists to do. */
.invoice-doc-paid {
    display: flex;
    align-items: baseline;
    gap: 12px;
    margin-top: 14px;
}
.invoice-doc-paid-stamp {
    border: 2px solid var(--accent);
    color: var(--accent);
    border-radius: var(--radius-sm);
    padding: 6px 14px;
    font-size: 14px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.invoice-doc-paid-on { font-size: 12px; color: var(--text-muted); }

/* An outstanding balance should read as outstanding at a glance. Colour alone
   is not the signal - the figure is also bold - because red on its own fails
   for the ~8% of men with a colour vision deficiency, and a till is not a
   place to make someone squint at a number they are chasing. */
.balance-owing, .balance-owing strong {
  color: var(--danger);
  font-weight: 700;
}

.stat--owing {
  border-color: var(--danger);
  background: var(--danger-bg);
}

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

/* A nested options panel (the backup-email schedule) belongs under the toggle
   that reveals it, full width - not squeezed into one column. */
.notif-toggles > .notif-opts { grid-column: 1 / -1; }

/* Modal used by "Add customer". <dialog> supplies the backdrop and focus
   trapping; this is only how it looks. */
.app-modal {
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  background: var(--bg-card);
  color: var(--text-main);
  padding: 24px 28px;
  max-width: 520px;
  width: calc(100% - 32px);
  box-shadow: var(--shadow-premium);
}

.app-modal::backdrop { background: rgba(15, 23, 42, 0.55); }
.app-modal h2 { margin-top: 0; }
.app-modal .cust-form { margin-bottom: 0; max-width: none; }

/* The Cancel buttons are gone - every modal has a close button in the corner
   that does the same job without spending a row on it. What is left is the
   action the cashier actually came for, so it gets the whole width and a
   fingertip's height rather than sharing a row with its own escape hatch. */
.modal-actions {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-top: 14px;
}
.modal-actions > button { flex: 1; min-height: 46px; font-size: 14px; }
/* Most modals never used .modal-actions - their one button is a direct child
   of .modal. Checked against every modal in the app: each such button IS the
   primary action (Save, Add note, Refund, Split payment, Done), so there is
   nothing here to catch by accident. The close button sits inside its own
   wrapper and is not a direct child. */
.modal > button {
  width: 100%;
  min-height: 46px;
  font-size: 14px;
  margin-top: 14px;
}
/* A second real action alongside the primary one - "Return without a
   receipt", "Discard" - stays secondary in weight, not in size. */
.modal-actions > button.modal-cancel { flex: 0 1 auto; }

/* A toggle that cannot apply right now, with the reason still readable. */
.notif-toggle-row.is-disabled { opacity: 0.6; }

/* Textareas inherit the input styling above; these are the only differences a
   multi-line field needs. */
textarea {
  font-family: inherit;
  line-height: 1.5;
  resize: vertical;
}

/* Invoice payment modal: the list of what a customer still owes. */
.invoice-pay-option {
  display: flex;
  align-items: baseline;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 10px 12px;
  margin: 4px 0;
  background: var(--bg-main);
  border: 1.5px solid var(--border-color);
  border-radius: var(--radius-sm);
  color: var(--text-main);
  cursor: pointer;
}

.invoice-pay-option:hover { border-color: var(--accent); }
.invoice-pay-option.is-selected {
  border-color: var(--accent);
  background: var(--card-accent-bg);
}

/* The balance is what the cashier is aiming at, so it sits hard right and
   reads as the figure rather than as part of the description. */
.invoice-pay-balance {
  margin-left: auto;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Payment receipt: a till slip that is also a real page.

   NOT #receipt-print-area - that is the register's off-screen buffer and
   anything wrapped in it renders blank on screen. This one is visible, and
   reveals itself for printing the same way the documents do. */
#payment-print-area {
  max-width: 380px;
  margin: 0 auto 20px;
  padding: 20px 24px;
  background: #fff;
  color: #1f2933;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
}

#payment-print-area h2 { margin: 0 0 6px; text-align: center; font-size: 16px; }
#payment-print-area .receipt-no { text-align: center; font-weight: 700; margin: 6px 0; }
#payment-print-area .receipt-line {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 2px 0;
}
#payment-print-area hr { border: none; border-top: 1px dashed #999; margin: 8px 0; }

@media print {
  body * { visibility: hidden; }
  #payment-print-area, #payment-print-area * {
    visibility: visible;
    color: #000 !important;
  }
  #payment-print-area {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    max-width: none;
    margin: 0;
    border: none;
    border-radius: 0;
    padding: 20px;
    background: #fff;
  }
  /* Same slip widths the sale receipt honours, so one printer setting covers
     both. */
  body.receipt-58mm #payment-print-area { width: 54mm; padding: 2mm; font-size: 10px; }
  body.receipt-80mm #payment-print-area { width: 76mm; padding: 3mm; font-size: 11px; }
}

/* ==========================================================================
   Chart series colours

   A validated categorical set, not a hand-picked one. The palette that was
   here before failed colourblind separation outright: teal (#14b8a6) against
   pink (#ec4899) came out at deltaE 3.7 under deuteranopia, which is close to
   identical - two donut slices a red-green colourblind reader could not tell
   apart. Roughly one man in twelve.

   These steps clear the lightness band, the chroma floor, adjacent-pair CVD
   separation and the normal-vision floor in BOTH modes. Dark is its own set of
   steps rather than an automatic lightening, because contrast against the dark
   surface has to be earned separately.

   Slots are assigned in fixed order and never cycled or generated. If a chart
   ever needs a ninth, the answer is to fold the tail into "Other", not to
   invent a hue.
   ========================================================================== */
:root {
    --series-1: #2a78d6;   /* blue    */
    --series-2: #eb6834;   /* orange  */
    --series-3: #1baf7a;   /* aqua    */
    --series-4: #eda100;   /* yellow  */
    --series-5: #e87ba4;   /* magenta */
    --series-6: #008300;   /* green   */
    --series-7: #4a3aa7;   /* violet  */
    --series-8: #e34948;   /* red     */
}

:root[data-theme="dark"],
:root[data-theme="glass"] {
    --series-1: #3987e5;
    --series-2: #d95926;
    --series-3: #199e70;
    --series-4: #c98500;
    --series-5: #d55181;
    --series-6: #008300;
    --series-7: #9085e9;
    --series-8: #e66767;
}

/* The system honours a shop that has asked for less colour. */
@media (prefers-contrast: more) {
    .chart-legend-dot { outline: 1px solid var(--text-main); }
}

/* Work waiting on a person. Its own panel rather than .flash - that is the
   error style, and a to-do list is not a problem. */
.ready-panel {
  border: 1px solid var(--accent);
  border-radius: var(--radius-md);
  background: var(--card-accent-bg);
  padding: 16px 20px;
  /* Space either side. It only had margin BELOW, so a panel following a table
     - Customer details under Customer history - sat flush against it and the
       two read as one block. */
  margin: 24px 0;
}

.ready-panel h2 { margin-top: 0; }

/* Both panels collapse. The h2 sits inside the summary so the disclosure
   triangle lines up with the heading rather than above it, and inline-block
   keeps the marker on the same line without losing the heading's weight. */
.ready-panel summary { cursor: pointer; }
.ready-panel summary h2 { display: inline-block; margin: 0; vertical-align: middle; }
.ready-panel summary { margin-bottom: 8px; }

/* An acknowledged failure keeps its row but stops shouting: somebody has
   looked at it, and the red is for the ones nobody has. */
.tag-muted { background: var(--border-color); color: var(--muted, inherit); }
.tag-paid { background: var(--ok, #2e7d32); color: #fff; }
.pay-links { margin: 16px 0; }
.pay-links h3 { margin-bottom: 4px; }
.inline-form { display: inline; }

.ready-actions {
  display: flex;
  gap: 12px;
  align-items: center;
  white-space: nowrap;
}

/* Screen guidance - intros and settings hints - which a shop can switch off
   once it knows the app (Settings > help text).

   Marked with a class of its own rather than reusing .muted: muted is how
   something looks, help-text is what it IS, and conflating them would sweep up
   any grey text that happens to report a state.

   Only text that EXPLAINS carries this. Anything announcing a state or
   blocking an action - "open the register first", "no email on file" - stays
   visible whatever the setting, because a reason that arrives after the dead
   end is worse than no reason. */
:root[data-help="off"] .help-text { display: none; }

/* Never on a customer's paperwork, whatever the setting says. */
@media print {
  .help-text { display: none !important; }
}

/* The advanced block is a grid of SETTINGS, not of elements.

   .notif-toggles alone was built for the notifications page, where every child
   is one switch of the same shape. Reused here it dealt each <h3>, <label> and
   <p> into the next free cell, so on a wide screen the "Receipt printing"
   heading landed in column three level with a card-payments switch, and hints
   sat beside controls they did not belong to.

   A heading now breaks the row, and each switch travels with its hint and its
   dependent fields as one unit. */
.settings-groups > h3 {
  grid-column: 1 / -1;
  margin: 22px 0 0;
}
.settings-groups > h3:first-of-type { margin-top: 6px; }
.settings-groups > .lan-address,
.settings-groups > .settings-subsection,
.settings-groups > .notif-opts { grid-column: 1 / -1; }

.setting-cell { display: flex; flex-direction: column; min-width: 0; }
.setting-cell > .settings-hint,
.setting-cell > .muted { margin: 0 0 4px; }
.setting-cell > .notif-opts { margin-left: 12px; }

/* An add-on this shop has not bought. The switch stays on the page - hidden,
   a feature can never be sold - but it cannot be operated, and it reads as
   unavailable rather than as broken. */
.addon-locked > .notif-toggle-row { opacity: 0.55; cursor: not-allowed; }
.addon-locked > .notif-toggle-row input { cursor: not-allowed; }
.addon-locked-note { font-style: italic; }

/* Points beside the attached customer on the register. Sits inside the
   scanner card, so it inherits the stacking context fix that keeps the
   customer suggestions above the day's totals. */
.loyalty-line {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
  font-size: 14px;
  color: var(--text-main);
}
.loyalty-line.hidden { display: none; }
.loyalty-points { font-weight: 700; }

/* "Sent" on the receivables list. Quiet on purpose: the column exists so an
   UNSENT invoice stands out by being blank, not so a sent one shouts. */
.tag-sent {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  background: var(--bg-main);
  color: var(--text-muted);
  border: 1px solid var(--border-color);
}

/* Clock in/out, to the left of the topbar clock. Styled as a status widget
   rather than a button so it sits with the clock rather than competing with
   the register's own controls. */
.timecard-btn {
  cursor: pointer;
  border: 1px solid var(--border-color);
  background: var(--bg-card);
  color: var(--text-main);
  font: inherit;
  font-size: 13px;
}
.timecard-btn:hover { border-color: var(--accent, #2563eb); }

/* --- Invoices list: the title row, the filter bar and the new-document
   dialog. A document is raised a few times a day; the list is read every
   time the page opens, so the form went behind a button. --- */
.page-title-row {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
}
.page-title-row h1 { margin: 0; }
.page-title-row button { margin-left: auto; }

.invoice-filter {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.invoice-filter input[type="search"] {
    min-width: 200px;
    flex: 1 1 200px;
    max-width: 340px;
}

/* The stat tiles filter the list, so they have to look pressable. */
a.stat { text-decoration: none; color: inherit; }
a.stat:hover { background: var(--card-accent-bg); }

dialog.modal-panel {
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    background: var(--bg-card);
    color: var(--text-main);
    padding: 22px 24px;
    max-width: min(460px, 92vw);
    box-shadow: var(--shadow-md);
}
dialog.modal-panel::backdrop { background: rgba(15, 23, 42, .55); }
dialog.modal-panel h3 { margin-top: 0; }

/* A heading, its search box and its filter chips are one thing. Without
   this the search sat wherever the flow put it, which on a wide screen was
   alongside the figures above rather than under its own heading. */
.section-head {
    display: flex;
    align-items: baseline;
    gap: 16px;
    flex-wrap: wrap;
    margin-top: 32px;
    /* The chips sit directly beneath this row on both screens that use it,
       and the h2 inside has its margin zeroed - so without this the filters
       are crammed against the heading and its button, and read as part of
       the same control rather than as the row that filters what is below. */
    margin-bottom: 14px;
}
.section-head h2 { margin: 0; }
.section-head .invoice-filter { margin: 0 0 0 auto; }

/* A plan whose charge was refused looks healthy otherwise, so this is the
   one place on the list that says the money is not arriving. */

/* The failure flag. Short line on the row, the processor's own wording and
   the two things you can do about it a click away - a support call needs
   the real string, not a summary of it. */
details.failure { margin-top: 4px; }
details.failure > summary {
    cursor: pointer;
    color: var(--danger);
    font-size: 12px;
    font-weight: 600;
    list-style: none;
}
details.failure > summary::-webkit-details-marker { display: none; }
details.failure > summary::before { content: "\26A0  "; }
details.failure[open] > summary { margin-bottom: 6px; }
.failure-body {
    background: var(--danger-bg);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
    max-width: min(420px, 60vw);
}
.failure-detail {
    margin: 0 0 8px;
    font-size: 12px;
    word-break: break-word;
    /* .sales-table td is nowrap so money columns share one baseline, and this
       inherited it - which sent a processor's error string off the side of the
       screen. anywhere rather than break-word because the string that matters
       most, Recurring|00|461343602583926KVPF, has nothing in it to break on. */
    white-space: normal;
    overflow-wrap: anywhere;
}
.failure-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.failure-actions .inline-form { display: inline; }

/* Same reason, for the panel itself inside a numeric table. */
.sales-table td details.failure,
.sales-table td .failure-body {
    white-space: normal;
    overflow-wrap: anywhere;
}

/* The instruction, and the processor's own words folded away beneath it.
   A cashier needs the first; a support call needs the second verbatim. */
.failure-advice {
    margin: 0 0 8px;
    font-size: 13px;
    font-weight: 600;
}
details.failure-raw > summary {
    cursor: pointer;
    font-size: 12px;
    opacity: 0.8;
    margin-bottom: 4px;
}
details.failure-raw .failure-detail {
    font-family: ui-monospace, Menlo, Consolas, monospace;
    font-size: 11px;
    opacity: 0.85;
}

/* Who is at the till - the cashier badge in the current-sale header, a quick
   tap to switch. Only rendered when timecards is on. */
.cashier-badge {
    float: right;
    font-size: 13px;
    font-weight: 600;
    color: var(--accent);
    text-decoration: none;
    padding: 2px 8px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-sm);
}
.cashier-badge:hover { background: var(--card-accent-bg); }

/* "Email me offers and reminders" beside the attached customer
   (Customer Marketing add-on, marketing-consent.js). */
.consent-line {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  font-size: 14px;
  color: var(--text-main);
  cursor: pointer;
}
.consent-line.hidden { display: none; }
.consent-note { color: var(--text-muted); font-size: 13px; }

/* Offers beside the attached customer (marketing-offers.js). */
.offer-line { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; }
.offer-line.hidden { display: none; }
.offer-applied { font-size: 14px; color: var(--text-muted); }
.offer-error { font-size: 13px; color: var(--danger, #b02a37); }

/* An email address that bounced (Customer Marketing). */
.email-bounce-banner { margin: 8px 0 16px; }
.email-bounced-tag { color: var(--danger, #b02a37); font-size: 13px; white-space: nowrap; }
.consent-note.consent-warning { color: var(--danger, #b02a37); font-weight: 600; }

/* Email wording: one collapsed block per automatic email. */
.marketing-wording { margin: 6px 0; padding: 6px 10px; border: 1px solid var(--border, #ddd); border-radius: 6px; }
.marketing-wording summary { cursor: pointer; font-weight: 600; }
.marketing-wording textarea, .marketing-wording input[type="text"] { width: 100%; box-sizing: border-box; }

/* Settings > Stations: the jobs a screen can be paired to. */
.station-jobs { margin: 8px 0 12px; padding-left: 18px; }
.station-jobs li { margin: 6px 0; }
.station-jobs .nav-link { margin-left: 6px; }

/* Settings: the unsaved-changes bar. position: sticky, never fixed - see the
   note in settings.html (main.page keeps a transform). */
.settings-savebar {
    position: sticky;
    bottom: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 16px -16px 0;
    padding: 12px 16px;
    background: var(--bg-card);
    border-top: 1px solid var(--border-color);
    box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.25);
}
.settings-savebar span { margin-right: auto; color: var(--text-muted); font-weight: 600; }
.settings-savebar[hidden] { display: none; }
/* Glass cards are see-through by design; this bar sits over moving page
   content, so it gets a near-solid fill instead (one element, no blur). */
:root[data-theme="glass"] .settings-savebar { background: rgba(15, 23, 42, 0.94); }


/* My hours, inside the Clock in/out box. */
.timecard-mine h4 { margin: 12px 0 4px; font-size: 14px; }
.timecard-mine p { margin: 2px 0; }
.timecard-mine-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.timecard-mine-table td { padding: 4px 6px; border-bottom: 1px solid var(--border-color); }
#timecard-modal .hidden { display: none; }
/* Payroll tables on Edit employees: wide, so they scroll rather than squeeze. */
.payroll-table td, .payroll-table th, .payroll-pay-table td, .payroll-pay-table th { white-space: nowrap; }
.payroll-table small { white-space: nowrap; }
.flash-inline { color: var(--danger, #b91c1c); font-weight: 600; }
.payroll-pay-table th { white-space: normal; vertical-align: bottom; }

/* Payroll, round 2: three carded blocks, fields on one row and the buttons
   that act on them on the next, and the inventory screen's ruled tables. */
.payroll-block { border: 1px solid var(--border-color); border-radius: var(--radius-md);
  background: var(--bg-card); padding: 14px 16px 16px; margin: 14px 0; }
.payroll-block > h3:first-child { margin-top: 0; }
.payroll-block .settings-hint { margin: 4px 0 10px; }
.payroll-toolbar { display: flex; flex-wrap: wrap; gap: 12px 16px; align-items: flex-end; margin: 4px 0 14px; }
.payroll-toolbar label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--text-muted); }
.payroll-toolbar label input, .payroll-toolbar label select { margin: 0; min-height: 41px; }
.payroll-toolbar > button { margin: 0; min-height: 41px; }
.payroll-or { padding-bottom: 11px; font-size: 13px; color: var(--text-muted); }
.payroll-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 12px; }
.payroll-actions button { margin: 0; min-height: 41px; }
.payroll-table th.num, .payroll-table td.num { text-align: right; }
.payroll-table .tip-split { display: block; font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.payroll-table .ytd { border-left: 1px solid var(--border-color); }
.payroll-table tfoot th { padding: 12px 14px; background: var(--bg-main); border-top: 2px solid var(--border-color);
  font-size: 14px; text-transform: none; color: var(--text-main); }
.payroll-badge { display: inline-block; margin-left: 8px; padding: 1px 8px; border-radius: 999px;
  font-size: 11px; font-weight: 600; vertical-align: middle; color: var(--text-muted);
  border: 1px solid var(--border-color); }
.payroll-warnings { list-style: none; margin: 12px 0 0; padding: 10px 14px; border-radius: var(--radius-md);
  background: var(--danger-bg); }
.payroll-warnings li { padding: 3px 0; font-size: 14px; font-weight: 600; color: var(--danger, #dc2626); }
.payroll-warnings li.payroll-warning-mild { font-weight: 500; color: var(--text-main); }
.payroll-pay-table input[type="number"] { width: 8em; }
.payroll-pay-table input[name^="occupation_"] { width: 9.5em; }
.payroll-pay-table td { vertical-align: middle; }
.payroll-pay-table .notif-toggle-row { margin: 0; }
.payroll-table th, .payroll-table td { padding: 10px 10px; }
.payroll-table th { white-space: normal; vertical-align: bottom; line-height: 1.25; }
.payroll-table td.col-name { min-width: 0; }
.payroll-table tfoot th { white-space: nowrap; }

/* Staff list, in the same ruled table as Pay details and Payroll. */
.staff-table input[type="text"] { width: 100%; min-width: 7em; max-width: 220px; margin: 0; }
.staff-table input[name="pin"] { width: 6em; min-width: 0; }
.staff-table .notif-toggle-row { margin: 0; }
.staff-table .row-actions form { margin: 0; }
.staff-table .row-actions button { margin: 0; }

/* Analytics headline numbers. */
.kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
.kpi-tile { background: var(--surface, rgba(255,255,255,.04)); border: 1px solid var(--border, rgba(255,255,255,.08));
  border-radius: 10px; padding: 12px 14px; }
.kpi-label { font-size: 13px; opacity: .75; }
.kpi-value { font-size: 24px; font-weight: 700; margin: 4px 0 2px; font-variant-numeric: tabular-nums; }
.kpi-share { font-size: 12px; opacity: .75; }
.kpi-change { font-size: 12px; opacity: .8; margin-top: 2px; }

/* The public demo's banner. */
.demo-banner { background: #f5c542; color: #1d1d1d; }
