/* Farmacias Suiza design system.

   GENERATED FILE -- do not edit by hand.
   Source: design-system.html (the reference page; open it in a browser).
   Regenerate: venv/Scripts/python scripts/extract_design_css.py

   Only the reference page's own chrome (.container, .header, .section,
   .color-*, .type-sample, .component-grid) is dropped; every product component
   is carried over verbatim.
*/

/* Self-hosted, like Leaflet: this console loads nothing from
   third-party hosts. Pulling these from Google cost 345ms of
   render-blocking latency on every page -- roughly ten times the
   server's own response time -- plus nine requests to fonts.gstatic.com.

   Both families are VARIABLE fonts, so one file covers the whole
   weight range rather than one file per weight: four files, 126KB.

   The URL is root-absolute because this block is extracted into two
   stylesheets at different depths (static/css/ and extension/vendor/)
   and a relative path cannot be right for both. The consequence:
   opening design-system.html straight off disk falls back to the
   system stack -- serve the repo root (`python -m http.server`) to
   see the real faces. */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 200 300;
    font-display: swap;
    src: url("/static/vendor/fonts/JetBrainsMono-latin-ext.edfd46a1e798.woff2") format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 200 300;
    font-display: swap;
    src: url("/static/vendor/fonts/JetBrainsMono-latin.cca21204605b.woff2") format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'Work Sans';
    font-style: normal;
    font-weight: 200 500;
    font-display: swap;
    src: url("/static/vendor/fonts/WorkSans-latin-ext.1f8c41030121.woff2") format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: 'Work Sans';
    font-style: normal;
    font-weight: 200 500;
    font-display: swap;
    src: url("/static/vendor/fonts/WorkSans-latin.4890688e6436.woff2") format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Asap -- the corporate identity manual's PRIMARY face: "body copy,
   headings, navigation, labels, forms, and the default web interface".
   It is loaded here for the one screen that is the default web
   interface, `/chat/`; the console keeps Work Sans, because the console
   is not the brand talking to a customer.

   Variable, latin + latin-ext, self-hosted like the other two -- 76KB
   for the pair. The manual names Roboto as the fallback when Asap is
   unavailable, so that is what the stack below says rather than a
   generic sans. */
@font-face {
    font-family: 'Asap';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url("/static/vendor/fonts/Asap-latin-ext.eab066369b9b.woff2") format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: 'Asap';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url("/static/vendor/fonts/Asap-latin.b27b9055a984.woff2") format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
    /* Dark palette */
    --bg-primary: #0a0a0f;
    --bg-secondary: #13131a;
    --bg-tertiary: #1a1a24;
    --bg-elevated: #202030;

    /* Light accents */
    --text-primary: #e8e8f0;
    --text-secondary: #a8a8b8;
    --text-tertiary: #70707e;
    --border-color: #2a2a38;
    --border-light: #3a3a48;

    /* Primary colors */
    --red-primary: #ef4444;
    --red-bright: #f87171;
    --red-dark: #dc2626;
    --red-muted: #991b1b;

    --blue-primary: #3b82f6;
    --blue-bright: #60a5fa;
    --blue-dark: #2563eb;
    --blue-muted: #1e3a8a;

    /* Functional colors */
    --success: #10b981;
    --warning: #f59e0b;
    --error: var(--red-primary);

    /* Scrim behind a modal. Its own token because it is the one colour
       a dialog needs and no existing one is transparent. */
    --overlay: rgba(0, 0, 0, 0.66);

    /* Thermal label ink and paper. Deliberately pure black on pure
       white, and deliberately NOT --text-primary: even the light
       palette's is #0f0f14, a near-black *grey*, and a thermal head
       renders grey by dithering it into dots. On a 30mm QR that is the
       difference between a code that scans and one that does not.
       Named tokens rather than hex in a rule, so the "no hard-coded
       colours" rule still holds and the exception explains itself. */
    --label-ink: #000000;
    --label-paper: #ffffff;

    /* Typography */
    --font-display: 'Work Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
    --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

    /* Spacing scale */
    --space-xs: 0.25rem;
    --space-sm: 0.5rem;
    --space-md: 1rem;
    --space-lg: 1.5rem;
    --space-xl: 2rem;
    --space-2xl: 3rem;
    --space-3xl: 4rem;

    /* Border radius */
    --radius-sm: 0.25rem;
    --radius-md: 0.5rem;
    --radius-lg: 0.75rem;
    --radius-xl: 1rem;

    /* Shadows */
    --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.5);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.6);
    --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.7);
    --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.8);

    /* Transitions */
    --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
    --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
    --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/* The `hidden` attribute has to actually hide.

   It works through the UA stylesheet's `[hidden] { display: none }`,
   and an AUTHOR rule beats the UA stylesheet whatever the specificity
   -- so the moment a component sets `display` on its own class, every
   `hidden` on it silently stops working. `.webchat-locate` is
   `display: flex`, so the customer's map panel sat open on page load
   showing an empty grey box, and only filled in when the button was
   pressed. Nothing errors; it just renders.

   `!important` is the right tool exactly once, here: `hidden` is the
   platform's own "this is not here", and no component rule should be
   able to argue with it. Every reset in circulation carries this line
   for the same reason. */
[hidden] {
    display: none !important;
}

/* A touch screen flashes a grey box on whatever is tapped and zooms in
   on a fast second tap. A control has its own pressed state, and nobody
   double-taps a button to read it closer; pinch-zoom is left alone, so
   this costs accessibility nothing. Global because it is true of every
   screen. `touch-action` on a touch is the intersection up the tree, so
   `.webchat-map { touch-action: none }` still wins inside the map. */
a, button, .btn, input, textarea, select {
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
}

body {
    font-family: var(--font-display);
    background: var(--bg-primary);
    color: var(--text-primary);
    line-height: 1.6;
    font-weight: 300;
    padding: var(--space-2xl);
    min-height: 100vh;
}

/* Header */

/* Section */

/* Color swatches */

/* Typography showcase */

.type-display-1 { font-size: 4rem; font-weight: 200; line-height: 1.1; }
.type-display-2 { font-size: 3rem; font-weight: 200; line-height: 1.2; }
.type-heading-1 { font-size: 2.5rem; font-weight: 300; }
.type-heading-2 { font-size: 2rem; font-weight: 300; }
.type-heading-3 { font-size: 1.5rem; font-weight: 300; }
.type-body-1 { font-size: 1.125rem; font-weight: 300; }
.type-body-2 { font-size: 1rem; font-weight: 300; }
.type-small { font-size: 0.875rem; font-weight: 300; }
.type-mono { font-family: var(--font-mono); font-size: 0.875rem; font-weight: 200; }

/* Components */

/* Buttons */
.btn {
    font-family: var(--font-display);
    font-weight: 300;
    padding: var(--space-sm) var(--space-lg);
    border: none;
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: all var(--transition-base);
    font-size: 0.875rem;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    /* A button is a button whether it is a <button> or an <a>. Without
       this every link styled as a button rendered underlined, because
       .nav-item was the only rule in the whole system that turned the
       UA default off -- so "Configurar etiqueta", "Abrir", "Ver pedido"
       and every pagination control read as underlined text inside a
       box. Nothing errors and nothing logs; it only shows in a browser. */
    text-decoration: none;
    /* An <a> is inline, so btn-block and the flex centring below did
       nothing for it and the label sat left-aligned against the padding. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-sm);
}

.btn-primary {
    background: var(--red-primary);
    color: white;
}

.btn-primary:hover {
    background: var(--red-bright);
    transform: translateY(-1px);
    box-shadow: var(--shadow-md);
}

.btn-secondary {
    background: white;
    color: var(--bg-primary);
}

.btn-secondary:hover {
    background: var(--text-primary);
    transform: translateY(-1px);
    box-shadow: var(--shadow-md);
}

.btn-primary-blue {
    background: var(--blue-primary);
    color: white;
}

.btn-primary-blue:hover {
    background: var(--blue-bright);
    transform: translateY(-1px);
    box-shadow: var(--shadow-md);
}

.btn-outline-red {
    background: transparent;
    border: 1px solid var(--red-primary);
    color: var(--red-primary);
}

.btn-outline-red:hover {
    background: var(--red-primary);
    color: white;
}

.btn-outline-blue {
    background: transparent;
    border: 1px solid var(--blue-primary);
    color: var(--blue-primary);
}

.btn-outline-blue:hover {
    background: var(--blue-primary);
    color: white;
}

.btn-ghost {
    background: transparent;
    color: var(--text-primary);
    border: 1px solid var(--border-light);
}

.btn-ghost:hover {
    background: var(--bg-elevated);
    border-color: var(--text-tertiary);
}

/* Cards */
.card {
    background: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    padding: var(--space-xl);
    transition: all var(--transition-base);
}

.card:hover {
    border-color: var(--border-light);
    box-shadow: var(--shadow-md);
}

.card h3 {
    font-weight: 300;
    margin-bottom: var(--space-md);
    font-size: 1.25rem;
}

.card p {
    color: var(--text-secondary);
    font-weight: 300;
    line-height: 1.7;
}

/* Inputs */
.input {
    width: 100%;
    font-family: var(--font-display);
    font-weight: 300;
    padding: var(--space-sm) var(--space-md);
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    color: var(--text-primary);
    font-size: 0.875rem;
    transition: all var(--transition-base);
}

/* A file picker is the one control the browser draws itself, and left
   alone it draws an OS button: white on a dark console, in the system
   font, ignoring every token on this page. `::file-selector-button` is
   the hook for the button half; the field keeps `.input`'s frame. */
.input[type="file"] {
    padding: var(--space-xs) var(--space-sm);
    line-height: 1.9;
    cursor: pointer;
}

.input[type="file"]::file-selector-button {
    margin-right: var(--space-md);
    padding: var(--space-xs) var(--space-md);
    background: var(--bg-elevated);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    font-family: var(--font-display);
    font-size: 0.8125rem;
    cursor: pointer;
    transition: all var(--transition-base);
}

.input[type="file"]::file-selector-button:hover {
    background: var(--bg-tertiary);
    border-color: var(--text-tertiary);
}

/* A number that is a number, not a field: quantities inside a table
   row, where .input's width:100% would eat the column. */
.input-qty {
    width: 4.5rem;
    text-align: center;
}

.input:focus {
    outline: none;
    border-color: var(--blue-primary);
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.input::placeholder {
    color: var(--text-tertiary);
}

/* Select/Dropdown */
.select {
    width: 100%;
    font-family: var(--font-display);
    font-weight: 300;
    padding: var(--space-sm) var(--space-md);
    background: var(--bg-primary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    color: var(--text-primary);
    font-size: 0.875rem;
    transition: all var(--transition-base);
    cursor: pointer;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%23a8a8b8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right var(--space-md) center;
    padding-right: var(--space-xl);
}

.select:focus {
    outline: none;
    border-color: var(--blue-primary);
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.select option {
    background: var(--bg-secondary);
    color: var(--text-primary);
}

/* Input with icon */
.input-group {
    position: relative;
    width: 100%;
}

.input-group .input {
    padding-left: var(--space-xl);
}

.input-icon {
    position: absolute;
    left: var(--space-md);
    top: 50%;
    transform: translateY(-50%);
    color: var(--text-tertiary);
    pointer-events: none;
}

/* Label */
.label {
    display: block;
    font-size: 0.75rem;
    font-weight: 400;
    color: var(--text-secondary);
    margin-bottom: var(--space-xs);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

/* Badges */
.badge {
    display: inline-block;
    padding: var(--space-xs) var(--space-md);
    border-radius: var(--radius-sm);
    font-size: 0.75rem;
    font-weight: 400;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.badge-red {
    background: rgba(239, 68, 68, 0.1);
    color: var(--red-bright);
    border: 1px solid rgba(239, 68, 68, 0.2);
}

.badge-blue {
    background: rgba(59, 130, 246, 0.1);
    color: var(--blue-bright);
    border: 1px solid rgba(59, 130, 246, 0.2);
}

.badge-success {
    background: rgba(16, 185, 129, 0.1);
    color: var(--success);
    border: 1px solid rgba(16, 185, 129, 0.2);
}

/* Tiles */
.tile {
    background: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    padding: var(--space-xl);
    transition: all var(--transition-base);
    position: relative;
    overflow: hidden;
}

.tile:hover {
    border-color: var(--border-light);
    transform: translateY(-4px);
    box-shadow: var(--shadow-xl);
}

.tile::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    background: var(--red-primary);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--transition-slow);
}

.tile:hover::before {
    transform: scaleX(1);
}

.tile-icon {
    width: 48px;
    height: 48px;
    border-radius: var(--radius-md);
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: var(--space-md);
    background: rgba(239, 68, 68, 0.1);
    border: 1px solid rgba(239, 68, 68, 0.2);
}

.tile-icon svg {
    width: 24px;
    height: 24px;
    color: var(--red-primary);
}

.tile-icon.blue {
    background: rgba(59, 130, 246, 0.1);
    border: 1px solid rgba(59, 130, 246, 0.2);
}

.tile-icon.blue svg {
    color: var(--blue-primary);
}

.tile h4 {
    font-size: 1.25rem;
    font-weight: 300;
    margin-bottom: var(--space-sm);
}

.tile p {
    color: var(--text-secondary);
    font-weight: 300;
    line-height: 1.6;
    margin-bottom: var(--space-md);
}

.tile-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: var(--space-lg);
    padding-top: var(--space-md);
    border-top: 1px solid var(--border-color);
}

.tile-meta {
    font-size: 0.75rem;
    color: var(--text-tertiary);
    font-family: var(--font-mono);
    font-weight: 200;
}

/* Table */
.table-container {
    background: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    /* Scrolls, rather than clips. `overflow: hidden` was here to keep
       the table's corners inside the radius, and it did -- but a table
       wider than its box then had its last column cut off with no way
       to reach it. Found on /staff/pedidos/ in a 889px viewport: the
       «Abrir» button was 14px past the edge and unreachable, which is
       the primary action on that screen. overflow-y stays hidden so the
       corners are still clipped. */
    overflow-x: auto;
    overflow-y: hidden;
}

.table-header {
    padding: var(--space-xl);
    border-bottom: 1px solid var(--border-color);
}

.table-header h3 {
    font-size: 1.5rem;
    font-weight: 300;
    margin-bottom: var(--space-xs);
}

.table-header p {
    color: var(--text-secondary);
    font-size: 0.875rem;
    font-weight: 300;
}

.table {
    width: 100%;
    border-collapse: collapse;
}

.table thead {
    background: var(--bg-tertiary);
}

.table th {
    padding: var(--space-md) var(--space-xl);
    text-align: left;
    font-size: 0.75rem;
    font-weight: 400;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-secondary);
    border-bottom: 1px solid var(--border-color);
}

.table td {
    padding: var(--space-md) var(--space-xl);
    font-size: 0.875rem;
    font-weight: 300;
    border-bottom: 1px solid var(--border-color);
}

.table tbody tr {
    transition: background var(--transition-fast);
}

.table tbody tr:hover {
    background: var(--bg-tertiary);
}

.table tbody tr:last-child td {
    border-bottom: none;
}

.table-actions {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}

/* ...but not when it *is* the cell. A `<td>` set to display:flex stops
   being a table cell, so it leaves the row's layout: it no longer
   stretches to the row height and the row background shows through
   beside it as a mismatched band. Found by looking at Despacho, where
   the buttons are big enough to make the gap obvious; the cart panel
   had been doing it quietly for as long as it existed. Markup does not
   change -- the cell stays a cell and the buttons lay out inside it. */
td.table-actions,
th.table-actions {
    display: table-cell;
    white-space: nowrap;
}

td.table-actions > *,
th.table-actions > * {
    display: inline-flex;
    vertical-align: middle;
}

td.table-actions > * + *,
th.table-actions > * + * {
    margin-left: var(--space-sm);
}

.table-action-btn {
    padding: var(--space-xs) var(--space-sm);
    font-size: 0.75rem;
    background: transparent;
    border: 1px solid var(--border-light);
    color: var(--text-secondary);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: all var(--transition-fast);
    font-family: var(--font-display);
    font-weight: 300;
    /* Same reason as .btn: this is nearly always an <a>. */
    text-decoration: none;
    display: inline-block;
}

.table-action-btn:hover {
    background: var(--bg-elevated);
    color: var(--text-primary);
    border-color: var(--text-tertiary);
}

/* Alerts */
.alert {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    padding: var(--space-md);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    background: var(--bg-tertiary);
    color: var(--text-secondary);
    font-size: 0.875rem;
    font-weight: 300;
    line-height: 1.5;
}

.alert-error {
    border-color: var(--red-muted);
    background: rgba(239, 68, 68, 0.1);
    color: var(--red-bright);
}

.alert-success {
    border-color: var(--success);
    background: rgba(16, 185, 129, 0.1);
    color: var(--success);
}

.alert-icon {
    flex-shrink: 0;
    width: 18px;
    height: 18px;
    margin-top: 2px;
}

.field-error {
    display: block;
    margin-top: var(--space-xs);
    font-size: 0.75rem;
    font-weight: 300;
    color: var(--red-bright);
}

/* Auth shell — one centred panel on an otherwise empty screen
   (sign in, PIN entry, password reset). */
.auth-shell {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-xl);
}

.auth-card {
    width: 100%;
    max-width: 420px;
    padding: var(--space-2xl);
    background: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-xl);
}

.auth-brand {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    margin-bottom: var(--space-xl);
}

.auth-brand-mark {
    flex-shrink: 0;
    width: 42px;
    height: 42px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-md);
    background: linear-gradient(135deg, var(--red-primary), var(--red-dark));
}

.auth-brand-mark svg {
    width: 22px;
    height: 22px;
    stroke: #ffffff;
}

.auth-brand-name {
    font-size: 1.125rem;
    font-weight: 400;
    color: var(--text-primary);
}

.auth-brand-sub {
    font-family: var(--font-mono);
    font-size: 0.75rem;
    font-weight: 200;
    color: var(--text-tertiary);
}

.auth-footer {
    margin-top: var(--space-xl);
    padding-top: var(--space-lg);
    border-top: 1px solid var(--border-color);
    text-align: center;
    font-size: 0.75rem;
    color: var(--text-tertiary);
}

/* App shell — sidebar + content, the frame every internal screen sits
   in. Pair it with .page-flush on <body>. */
.app-shell {
    display: grid;
    grid-template-columns: 260px 1fr;
    min-height: 100vh;
}

.app-sidebar {
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
    padding: var(--space-lg);
    background: var(--bg-secondary);
    border-right: 1px solid var(--border-color);
}

.app-sidebar-nav {
    flex: 1;
}

.app-sidebar-footer {
    padding-top: var(--space-lg);
    border-top: 1px solid var(--border-color);
}

.app-main {
    display: flex;
    flex-direction: column;
    /* Without this a wide table stretches the grid column instead of
       scrolling inside .table-container. */
    min-width: 0;
}

.app-topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-lg) var(--space-xl);
    background: var(--bg-secondary);
    border-bottom: 1px solid var(--border-color);
}

.app-topbar-title {
    font-size: 1.125rem;
    font-weight: 300;
    color: var(--text-primary);
}

.app-content {
    flex: 1;
    padding: var(--space-xl);
}

/* A menu entry whose screen does not exist yet. */
.nav-item.is-pending {
    color: var(--text-tertiary);
    cursor: default;
    opacity: 0.6;
}

.nav-item.is-pending:hover {
    background: transparent;
    color: var(--text-tertiary);
}

.nav-item.is-pending:hover .nav-item-icon {
    color: var(--text-tertiary);
}

.nav-item-tag {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    font-weight: 200;
    color: var(--text-tertiary);
}

/* Unread counter on a menu row (notifications waiting, orders pending).
   Sits where .nav-item-tag sits, but reads as a signal rather than a
   note, so it takes the brand red. Render it only when the count is
   non-zero -- a permanent "0" is noise the eye learns to skip. */
.nav-item-count {
    margin-left: auto;
    min-width: 1.25rem;
    padding: 0 var(--space-xs);
    border-radius: var(--radius-sm);
    background: var(--red-primary);
    color: #fff;
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    font-weight: 400;
    line-height: 1.25rem;
    text-align: center;
}

/* Narrow: the sidebar becomes a strip, not a stack.
   It used to keep its column layout and simply go full width, which on a
   phone meant the brand, every menu row, the signed-in name and the
   sign-out button filled the entire first screen -- the courier scrolled
   past the whole menu before seeing a single address. A menu is how you
   leave the screen you are on; it does not get the first viewport. */
@media (max-width: 900px) {
    .app-shell {
        grid-template-columns: 1fr;
        /* Explicit rows, or the grid stretches both to fill its
           `min-height: 100vh` and the strip ends up floating in the
           middle of a third of the screen. */
        grid-template-rows: auto 1fr;
    }

    .app-sidebar {
        flex-direction: row;
        align-items: center;
        gap: var(--space-md);
        padding: var(--space-sm) var(--space-md);
        border-right: none;
        border-bottom: 1px solid var(--border-color);
        /* More rows than fit slide sideways rather than wrapping into a
           second and third line, which would be the stack again. */
        overflow-x: auto;
    }

    /* The lockup keeps its mark and drops the words: at this width the
       person holding the phone knows whose console it is. */
    .app-sidebar .auth-brand {
        margin-bottom: 0;
        flex-shrink: 0;
    }

    .app-sidebar .auth-brand > span:last-child {
        display: none;
    }

    .app-sidebar-nav {
        display: flex;
        border: none;
        background: transparent;
        border-radius: 0;
        overflow: visible;
    }

    .app-sidebar .nav-item {
        border-bottom: none;
        white-space: nowrap;
        border-radius: var(--radius-md);
    }

    /* The active row is marked on its left edge in the column layout,
       which reads as an underline once the rows sit side by side. */
    .app-sidebar .nav-item.active {
        border-left: none;
        background: var(--bg-tertiary);
    }

    .app-sidebar-footer {
        margin-left: auto;
        padding-top: 0;
        border-top: none;
        flex-shrink: 0;
    }

    /* Whose session it is already shows in the topbar's branch badge;
       what has to survive here is the way out. */
    .app-sidebar-footer p {
        display: none;
    }

    .app-topbar {
        padding: var(--space-md) var(--space-lg);
    }

    .app-content {
        padding: var(--space-lg);
    }
}

/* Field — a label, its control and its error, stacked. This is the
   wrapper for an ordinary form field. Use .input-group instead only
   when the control carries a leading .input-icon: that one indents the
   input to clear the icon, which looks like a typo without one. */
.field {
    display: block;
    width: 100%;
    margin-bottom: var(--space-lg);
}

.field:last-child {
    margin-bottom: 0;
}

/* Filter bar — the row of controls above a table. */
.filter-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-md);
    padding: var(--space-lg);
    border-bottom: 1px solid var(--border-color);
}

.filter-bar .field {
    width: auto;
    min-width: 180px;
    margin-bottom: 0;
}

.filter-bar-grow {
    flex: 1;
}

/* Pagination */
.pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-md);
    padding: var(--space-lg);
    border-top: 1px solid var(--border-color);
}

.pagination-status {
    font-family: var(--font-mono);
    font-size: 0.75rem;
    font-weight: 200;
    color: var(--text-tertiary);
}

/* Detail layout — the record on the left, its side panels on the right. */
.detail-grid {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: var(--space-lg);
    align-items: start;
}

@media (max-width: 1100px) {
    .detail-grid {
        grid-template-columns: 1fr;
    }
}

/* Modal — a native <dialog>, opened with showModal().

   The browser gives us the scrim (::backdrop), Escape to close, focus
   trapping and the top layer for free. An earlier version did this with
   :target to avoid JavaScript, which cost a full page reload per search
   and put #buscador in the URL; none of that was worth avoiding a
   six-line event handler. */
.modal {
    width: min(44rem, 92vw);
    /* The UA centres a <dialog> with margin:auto; resetting padding and
       border loses it, and the dialog pins itself to the top-left. */
    margin: auto;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--text-primary);
    overflow: visible;
}

/* The element's own scrim: no extra node, and it fades with the dialog. */
.modal::backdrop {
    background: var(--overlay);
}

.modal-card {
    padding: var(--space-xl);
    background: var(--bg-secondary);
    border: 1px solid var(--border-light);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-xl);
}

.modal-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    margin-bottom: var(--space-lg);
}

/* − n + : a quantity a person adjusts by pressing, not by typing.
   Each button is its own submit, so it works before the JavaScript
   loads and the server sees an ordinary form post. */
.qty-group {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
}

.qty-step {
    min-width: 1.75rem;
    text-align: center;
}

.qty-value {
    min-width: 1.5rem;
    text-align: center;
    font-family: var(--font-mono);
}

/* Density.

   `.card` is 2rem of padding with a 1.25rem heading, which is right for
   a page you read and wrong for a panel you work in: an operational
   screen nests cards two deep and the padding compounds until the work
   is 1600px tall with a third of it empty. `.is-compact` is that same
   card, tightened, and `.card-section-title` is a heading inside one
   that must not compete with the card's own. Measured, not guessed. */
.card.is-compact {
    padding: var(--space-md) var(--space-lg);
}

.card.is-compact h3 {
    font-size: 1rem;
    margin-bottom: var(--space-sm);
}

.card-section {
    margin-top: var(--space-lg);
}

.card-section-title {
    display: block;
    margin-bottom: var(--space-sm);
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-tertiary);
}

/* The rest of the density story: a 3rem empty state and a 2rem header
   bar are fine on a page, and absurd inside a working panel where the
   empty state was 200px of nothing. */
.card.is-compact .empty-state {
    padding: var(--space-lg);
}

.card.is-compact .table-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    padding: 0 0 var(--space-sm);
    border-bottom: none;
}

/* Money, in a column too narrow for a row of figures.

   A label-and-amount list: amounts right-aligned in the mono face so
   the decimal points line up, and the last row carries the weight. The
   ERP bridge popup grew its own version of this (.bridge-figures)
   before the system had one. */
.money-list {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--space-xs) var(--space-md);
    align-items: baseline;
}

.money-list-label {
    color: var(--text-tertiary);
    font-size: 0.8125rem;
}

.money-list-value {
    font-family: var(--font-mono);
    text-align: right;
}

.money-list-total .money-list-label,
.money-list-total .money-list-value {
    color: var(--text-primary);
    font-size: 1rem;
    font-weight: 500;
}

/* 2rem of horizontal cell padding wraps a column header in half a
   screen's width. This is the same table for a narrow panel. */
.table.is-compact th,
.table.is-compact td {
    padding: var(--space-sm) var(--space-md);
    white-space: nowrap;
}

/* The first column takes the slack and the rest shrink to their
   content, so a product name is not squeezed into three lines while an
   empty flags column holds a third of the width. */
.table.is-compact td:first-child,
.table.is-compact th:first-child {
    width: 100%;
    white-space: normal;
}

/* Summary bar — a screen's facts in one strip instead of a card each.
   A detail screen that opens with two padded cards of labelled values
   pushes the actual work below the fold; this puts the same values on
   one line, wraps them when it must, and keeps the badges on the right. */
.summary-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-md) var(--space-xl);
    padding: var(--space-md) var(--space-lg);
    background: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
}

.summary-item {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    min-width: 0;
}

.summary-label {
    font-size: 0.6875rem;
    color: var(--text-tertiary);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.summary-value {
    color: var(--text-primary);
}

/* Pushed to its own line: the caveats, not the facts. */
.summary-note {
    flex-basis: 100%;
    color: var(--text-secondary);
}

.summary-badges {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs);
    margin-left: auto;
}

@media (max-width: 700px) {
    .summary-badges {
        margin-left: 0;
    }
}

/* Two peers side by side, where .detail-grid is a record plus its
   side panel. Used for a screen where the pharmacist works in both
   halves at once -- reading the chat while building the order. */
.split-view {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-lg);
    align-items: start;
}

@media (max-width: 1200px) {
    .split-view {
        grid-template-columns: 1fr;
    }
}

/* Step tracker — a sequence that has to happen in order, and where it
   stands right now.

   Not a progress bar: the question is not "how far along" but "what is
   next, and what would be refused if I tried it". A blocked step is
   drawn as blocked because the service behind it *will* refuse, so the
   screen must not offer it as a choice — the tracker and the gate read
   the same booleans. */
.step-list {
    display: flex;
    flex-direction: column;
}

.step {
    position: relative;
    display: flex;
    gap: var(--space-md);
    padding-bottom: var(--space-lg);
}

.step:last-child {
    padding-bottom: 0;
}

/* The line joining one marker to the next. Drawn from the marker's
   centre, so it stays put when a step grows a note or a form. */
.step:not(:last-child)::before {
    content: "";
    position: absolute;
    left: calc(0.875rem - 1px);
    top: 1.75rem;
    bottom: 0;
    width: 2px;
    background: var(--border-color);
}

.step.is-done:not(:last-child)::before {
    background: var(--success);
}

.step-marker {
    flex-shrink: 0;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    background: var(--bg-tertiary);
    border: 2px solid var(--border-color);
    color: var(--text-tertiary);
}

.step.is-done .step-marker {
    background: var(--success);
    border-color: var(--success);
    color: var(--bg-primary);
}

/* The one the person is meant to act on, in the brand accent. */
.step.is-current .step-marker {
    border-color: var(--red-primary);
    color: var(--red-primary);
}

.step-body {
    flex: 1;
    min-width: 0;
    padding-top: 0.25rem;
}

.step-title {
    color: var(--text-primary);
    font-size: 0.875rem;
}

.step.is-blocked .step-title {
    color: var(--text-tertiary);
}

.step-note {
    display: block;
    margin-top: var(--space-xs);
    font-size: 0.75rem;
    color: var(--text-tertiary);
}

/* Whatever the current step lets you do: a form, a button, a reply to
   read. Nothing renders here for a step that is done or blocked.

   Capped, because the tracker is a narrow column of titles inside a
   full-width card: a text input stretched to the card's edge is a
   40-character answer in a 900px box. */
.step-action {
    margin-top: var(--space-md);
    max-width: 30rem;
}

/* Checkbox — a physical step somebody confirms, not a preference.

   The native box cannot be styled consistently, so `appearance: none`
   strips it and the tick is drawn on ::after. The input itself stays
   in the DOM rather than being replaced by a span: it is what submits,
   what a screen reader announces, and what a keyboard reaches — a
   packer works down this form with a scanner in one hand. */
.checkbox {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    padding: var(--space-sm) 0;
    cursor: pointer;
    color: var(--text-primary);
    font-size: 0.875rem;
}

.checkbox input {
    appearance: none;
    -webkit-appearance: none;
    position: relative;
    flex-shrink: 0;
    width: 1.15rem;
    height: 1.15rem;
    margin: 0;
    border: 2px solid var(--border-light);
    border-radius: var(--radius-sm);
    background: var(--bg-tertiary);
    cursor: pointer;
    transition: background var(--transition-fast),
                border-color var(--transition-fast);
}

.checkbox input:hover {
    border-color: var(--red-primary);
}

.checkbox input:focus-visible {
    outline: 2px solid var(--blue-primary);
    outline-offset: 2px;
}

.checkbox input:checked {
    background: var(--red-primary);
    border-color: var(--red-primary);
}

.checkbox input:checked::after {
    content: "";
    position: absolute;
    left: 0.3rem;
    top: 0.05rem;
    width: 0.25rem;
    height: 0.6rem;
    border: solid var(--bg-primary);
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

/* Why this box exists, under the thing it is confirming. */
.checkbox-note {
    display: block;
    margin-top: var(--space-xs);
    font-size: 0.75rem;
    color: var(--text-tertiary);
}

/* Despacho label — 80x40 mm of thermal paper.

   Sized in millimetres on screen as well as on paper, so the preview on
   the configuration screen IS the artifact rather than an impression of
   it. A label you can only judge by printing it is a label nobody
   checks — and the first draft was 42.1 mm tall in a 40 mm space, which
   only measuring it showed.

   **Flex, not grid**, and the second thing measuring showed. With the QR
   spanning three grid rows against a fixed 40 mm height, the `1fr` row
   claimed the QR's whole 26 mm and the two auto rows above it were
   squeezed to 11 px each — so the street, at 16.6 px, was rendered with
   its top and bottom sliced off. A spanning item plus a flexible track
   inside a fixed height is a negotiation with three parties; two nested
   flex boxes are just a column beside a square. */
.label-sheet {
    width: 80mm;
    height: 40mm;
    box-sizing: border-box;
    padding: 2mm 2.5mm;
    display: flex;
    flex-direction: column;
    background: var(--label-paper);
    color: var(--label-ink);
    font-family: var(--font-display);
    overflow: hidden;
}

.label-body {
    flex: 1;
    min-height: 0;
    display: flex;
    align-items: center;
    gap: 2mm;
}

.label-text {
    flex: 1;
    min-width: 0;
    align-self: stretch;
    display: flex;
    flex-direction: column;
}

/* The biggest thing on the label: a stack of bags sorts into route
   order by eye before the driver leaves.

   Two lines, then an ellipsis. One line looked tidier and truncated
   "Av. 24 de Mayo y Bolívar" to "AV. 24 DE MAYO Y B…" — losing the
   cross street, which is exactly what puts somebody at the wrong door.
   The column has the room, so it takes it; the clamp is what keeps a
   four-line address from pushing the footer off the paper. */
.label-sector {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    font-size: 4.4mm;
    line-height: 1.1;
    font-weight: 500;
    text-transform: uppercase;
    overflow: hidden;
}

/* What finds the door once they are on the right street. Two lines at
   most: past that it is stealing room from the footer. */
.label-reference {
    font-size: 2.9mm;
    line-height: 1.15;
    max-height: 6.7mm;
    overflow: hidden;
}

/* Pushed to the bottom of its column, so the name sits against the rule
   above the footer however short the address is. */
.label-who {
    margin-top: auto;
    font-size: 3.2mm;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* 26 mm across 33 modules is ~0.8 mm each, comfortably above what a
   phone camera needs, and it leaves the footer its room. */
.label-qr {
    flex: none;
    width: 26mm;
    height: 26mm;
}

.label-qr svg {
    width: 100%;
    height: 100%;
    display: block;
}

.label-foot {
    flex: none;
    display: flex;
    align-items: center;
    gap: 1.5mm;
    font-size: 2.9mm;
    border-top: 0.3mm solid var(--label-ink);
    padding-top: 0.8mm;
    white-space: nowrap;
    overflow: hidden;
}

/* Reversed out, because these are the two things that change what the
   driver does at the door and they have to survive a glance. */
.label-flag {
    flex: none;
    padding: 0.3mm 1mm;
    border: 0.3mm solid var(--label-ink);
    border-radius: 0.8mm;
    font-size: 2.5mm;
    font-weight: 500;
    text-transform: uppercase;
}

.label-flag.is-strong {
    background: var(--label-ink);
    color: var(--label-paper);
}

.label-cash {
    margin-left: auto;
    padding-left: 1.5mm;
    font-family: var(--font-mono);
    font-size: 3.4mm;
    font-weight: 500;
}

.label-note {
    flex: none;
    font-size: 2.5mm;
    line-height: 1.2;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* One label, one page, no margins. Without the @page rule the browser
   adds its own and the 80x40 spills onto a second sheet. */
@media print {
    @page {
        size: 80mm 40mm;
        margin: 0;
    }

    html, body {
        width: 80mm;
        height: 40mm;
        margin: 0;
        padding: 0;
        background: var(--label-paper);
    }

    .label-noprint {
        display: none !important;
    }
}

/* Map canvas — where a route gets drawn.

   Only the box is ours. The map inside it is Leaflet, whose stylesheet
   is vendored separately at static/vendor/leaflet/ and is a third-party
   library like the fonts, not part of this system. What belongs here is
   the frame: the size, the radius and the border, so a map sits in a
   card like every other panel does.

   Fixed height rather than an aspect ratio: a dispatcher is comparing
   it against the stop list beside it, and a box that changes height
   with the viewport makes the two stop lining up. */
.map-canvas {
    height: 22rem;
    width: 100%;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    background: var(--bg-tertiary);
    overflow: hidden;
}

/* Leaflet paints its own white controls and attribution; on the dark
   console they need the surrounding box to stay dark rather than the
   map to go light, so nothing here recolours the tiles themselves. */
.map-canvas .leaflet-container {
    height: 100%;
    width: 100%;
    background: var(--bg-tertiary);
    font-family: var(--font-display);
}

/* The numbered stop markers, drawn as divIcons so they carry the stop
   order rather than a generic pin. */
.map-pin {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.6rem;
    height: 1.6rem;
    border-radius: 50%;
    background: var(--red-primary);
    color: #fff;
    border: 2px solid #fff;
    box-shadow: var(--shadow-md);
    font-family: var(--font-mono);
    font-size: 0.75rem;
    font-weight: 500;
}

/* The branch the van leaves from and returns to. */
.map-pin.is-origin {
    background: var(--text-primary);
    color: var(--bg-primary);
    border-radius: var(--radius-sm);
}

/* Conversation thread */
.chat-thread {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

/* A long conversation must not push the rest of the screen below the
   fold. The modifier, not the base rule: a thread on a screen of its
   own should still grow. */
.chat-thread.is-scrollable {
    max-height: 28rem;
    overflow-y: auto;
    padding-right: var(--space-sm);
}

.chat-message {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    max-width: 68ch;
}

.chat-message.is-customer {
    align-self: flex-start;
}

/* Anything the pharmacy said sits on the right. */
.chat-message.is-pharmacist {
    align-self: flex-end;
    align-items: flex-end;
}

.chat-bubble {
    padding: var(--space-md);
    background: var(--bg-tertiary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    font-size: 0.9375rem;
    font-weight: 300;
    /* Customers send line breaks; keep them. */
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.chat-message.is-pharmacist .chat-bubble {
    background: var(--blue-muted);
    border-color: var(--blue-dark);
    color: var(--text-primary);
}

/* Dashed = written by the agent, not by a person. */
.chat-message.is-ai .chat-bubble {
    border-style: dashed;
}

.chat-message.is-system .chat-bubble {
    background: transparent;
    border-style: dotted;
    color: var(--text-tertiary);
    font-size: 0.875rem;
}

.chat-meta {
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    font-weight: 200;
    color: var(--text-tertiary);
}

/* The customer's own view of a thread.

   The console renders a conversation from the pharmacy's side, so
   `.is-pharmacist` sits on the right. On the customer's screen the
   perspective is the other way round and THEY are the right-hand
   side. The `is-*` modifier still means the message's `sender` value,
   which is the whole point of not redefining it: the flip is a
   property of who is looking, so it lives on the thread. */
.chat-thread.is-mine-right .chat-message.is-customer {
    align-self: flex-end;
    align-items: flex-end;
}

.chat-thread.is-mine-right .chat-message.is-customer .chat-bubble {
    background: var(--blue-muted);
    border-color: var(--blue-dark);
}

.chat-thread.is-mine-right .chat-message.is-pharmacist,
.chat-thread.is-mine-right .chat-message.is-ai {
    align-self: flex-start;
    align-items: flex-start;
}

.chat-thread.is-mine-right .chat-message.is-pharmacist .chat-bubble {
    background: var(--bg-tertiary);
    border-color: var(--border-color);
}

/* A file the customer attached, inside their own bubble. */
.chat-attachment {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    margin-top: var(--space-sm);
    padding: var(--space-xs) var(--space-sm);
    background: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-sm);
    font-size: 0.8125rem;
    text-decoration: none;
    color: var(--text-secondary);
}

/* A picture the PHARMACY sent into the thread -- today a payment QR.

   Shown, not linked: a code the customer has to tap through to see is
   a code nobody scans. Capped rather than full-width because a QR is
   square and a full-width one on a phone would push the message that
   explains it off the screen. White ground regardless of theme: a
   scanner reads dark-on-light, and a QR inverted by a dark palette is
   a QR that does not scan. */
.chat-image {
    display: block;
    width: 100%;
    max-width: 15rem;
    aspect-ratio: 1 / 1;
    object-fit: contain;
    margin-top: var(--space-sm);
    padding: var(--space-sm);
    background: #ffffff;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
}

/* "Escribiendo…" — three dots, because a chat that goes silent for the
   three to six seconds a model turn takes reads as broken. */
.chat-typing {
    display: inline-flex;
    gap: 0.25rem;
    align-items: center;
    padding: var(--space-md);
}

.chat-typing span {
    width: 0.4rem;
    height: 0.4rem;
    border-radius: 50%;
    background: var(--text-tertiary);
    animation: chat-typing-dot 1.2s infinite ease-in-out;
}

.chat-typing span:nth-child(2) { animation-delay: 0.15s; }
.chat-typing span:nth-child(3) { animation-delay: 0.3s; }

@keyframes chat-typing-dot {
    0%, 60%, 100% { opacity: 0.25; }
    30% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .chat-typing span { animation: none; opacity: 0.6; }
}

/* --- Reading a code with the camera ------------------------------

   The courier scanning bags into the van. A fixed aspect ratio rather
   than a height, because the preview has to stay square-ish while the
   card around it is whatever width a phone is; `object-fit: cover`
   means a portrait camera fills it instead of letterboxing.

   `.scanner-frame` is the sight. It is the whole affordance -- there is
   no shutter button and nothing to press, so without a mark in the
   middle nobody knows where to hold the label. */
.scanner {
    position: relative;
    width: 100%;
    max-width: 22rem;
    aspect-ratio: 4 / 3;
    margin-bottom: var(--space-md);
    background: var(--bg-tertiary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    overflow: hidden;
}

.scanner-video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.scanner-frame {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 58%;
    aspect-ratio: 1 / 1;
    transform: translate(-50%, -50%);
    border: 2px solid var(--red-primary);
    border-radius: var(--radius-sm);
    box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.35);
    pointer-events: none;
}

/* --- The wordmark ---------------------------------------------------

   Set, not drawn. The identity manual locks the name up as two
   lowercase lines with the tagline beneath, and there is no logo file
   in this repository to serve instead -- so it is type, which stays
   sharp at every pixel density and costs no request. The day somebody
   supplies the real artwork this becomes an <img> and this rule goes.

   `line-height: 0.78` is the whole trick: it is what makes two words
   read as one mark rather than as two lines of a paragraph. */
.brand-lockup {
    display: inline-flex;
    flex: 0 0 auto;
    flex-direction: column;
    gap: 0.35em;
    text-decoration: none;
}

.brand-wordmark {
    display: flex;
    flex-direction: column;
    color: var(--red-primary);
    font-size: 1.2rem;
    font-weight: 800;
    line-height: 0.78;
    /* The manual sets the name lowercase; forcing it here means a
       template may write it however it reads best in a title. */
    text-transform: lowercase;
}

.brand-tagline {
    color: var(--red-primary);
    font-size: 0.5rem;
    font-weight: 800;
    line-height: 1;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}

/* --- The customer's chat window -------------------------------------

   A full-height column: a bar saying who they reached, the thread, and
   the composer pinned to the bottom. Not `.app-shell`, which is the
   console's frame and carries a sidebar a customer has no use for. */
.webchat-shell {
    display: grid;
    /* header · thread · map · notice · composer. Every row after the
       thread is `auto` and its content is `hidden` until it is wanted,
       so a dismissed notice and a closed map collapse their tracks to
       nothing rather than leaving gaps. */
    grid-template-rows: auto 1fr auto auto auto;
    /* Not 100vh: mobile browsers put the URL bar inside that, so the
       composer ends up under it. `dvh` is the one that shrinks -- and
       `100%` is the fallback for a browser without it, NOT `100vh`:
       with the document locked (`html.page-app`) 100% of body is
       exactly the visible height everywhere, while 100vh is the
       bar-collapsed height and would put the composer under the bar
       on precisely the browsers that need the fallback. webchat.js
       overrides this in px while the keyboard is up on iOS, where
       neither unit moves. */
    height: 100%;
    height: 100dvh;
    /* Double-tap on the thread zooms a page; an app does not. Pinch
       still works. */
    touch-action: manipulation;
    max-width: 46rem;
    margin: 0 auto;
    background: var(--bg-primary);
    border-left: 1px solid var(--border-color);
    border-right: 1px solid var(--border-color);
}

.webchat-header {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    /* The vertical padding is a token so the phone breakpoint can
       shrink it without restating the safe-area sum below. env() is
       0 unless the viewport meta says `viewport-fit=cover`, and
       non-zero only where the page really reaches the notch or the
       home indicator (landscape, or a Home Screen install). */
    --bar-pad: var(--space-md);
    padding: calc(var(--bar-pad) + env(safe-area-inset-top, 0px))
             var(--space-lg)
             var(--bar-pad);
    border-bottom: 1px solid var(--border-color);
    background: var(--bg-secondary);
    /* A long press on the brand must not select it. */
    -webkit-user-select: none;
    user-select: none;
}

.webchat-header-title {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    min-width: 0;
    /* Pushed to the far end, because the other thing in this bar is
       the brand lockup: whose pharmacy on the left, which branch and
       whether anyone is listening on the right. */
    margin-left: auto;
    text-align: right;
}

/* One line each, whatever the branch happens to be called. «Farmacias
   Suiza Centro» beside the lockup wrapped to three lines at 360px and
   took the header from 78px to 101px -- an eighth of a phone screen
   spent on a bar. Ellipsised, the header is the same height at every
   width, which is also what stops the thread jumping when the status
   line changes. */
.webchat-header-title > * {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A phone needs the 24px of side padding for the conversation, not
   for the bars above and below it.

   The composer is the tighter of the two: three 44px buttons and
   their gaps leave the text box 156px at 360px wide, and «Escriba su
   mensaje…» wrapped inside it and was clipped mid-word -- a message
   box that looks broken before anything has been typed in it. 40px
   buttons and 16px of padding give the box 184px, which the
   placeholder fits on one line. */
@media (max-width: 600px) {
    /* Longhands, never the shorthand: `padding:` here would silently
       throw away the env() safe-area sum in the base rules. */
    .webchat-header,
    .webchat-composer {
        --bar-pad: var(--space-sm);
        padding-left: var(--space-md);
        padding-right: var(--space-md);
    }

    .icon-btn {
        width: 2.5rem;
        height: 2.5rem;
    }
}

.webchat-body {
    overflow-y: auto;
    /* A grid item's minimum is its content, so a 1fr track cannot
       shrink below the thread and the composer is pushed off the
       shell. A scroll container is exempt (its auto minimum is 0),
       which is the only reason this ever worked; said out loud so
       changing `overflow` cannot silently bring the trap back. */
    min-height: 0;
    /* Reaching the end of the thread must not hand the drag to the
       page: no rubber band, no pull-to-refresh from inside a chat. */
    overscroll-behavior: contain;
    padding: var(--space-lg);
}

/* On a first visit the thread is empty and this is the whole window:
   two lines of text pinned to the top of 700px of white read as a page
   that half-loaded. Centred, they read as an invitation.

   Scoped with `:has()` so it lasts exactly as long as the empty state
   does -- webchat.js removes that on the first message and the body
   goes back to ordinary flow, which is what a thread needs. A browser
   without `:has()` simply keeps the old top-aligned version; nothing
   else here depends on it. Centring cannot clip, because the only
   thing this rule can ever be centring is the empty state and (on a
   maintenance window) one alert. */
.webchat-body:has(> .empty-state) {
    display: grid;
    align-content: center;
}

/* The privacy notice, between the conversation and the composer.

   Not over the thread and not a modal: everything this page stores is
   strictly necessary for the chat to work, so this is a NOTICE and not
   a consent gate -- putting a wall in front of somebody trying to ask a
   pharmacist about their medication would buy nothing. */
.webchat-notice {
    padding: var(--space-sm) var(--space-lg) 0;
}

.webchat-notice .alert {
    align-items: center;
    gap: var(--space-md);
}

/* The text takes the slack; the button keeps its size. Same reasoning
   as `.input-row`. */
.webchat-notice .alert > span {
    flex: 1;
}

.webchat-notice .alert > .btn {
    flex: 0 0 auto;
}

/* On a phone the two-column arrangement is the wrong one: the button
   keeps its ~130px and the text is left with about the same, so four
   sentences become a dozen short lines and the notice takes a THIRD of
   the screen (measured: 335px of a 780px viewport at 360px wide).
   Stacked, the text gets the full width and the notice is a fifth of
   that. Same lesson as `.input-row` -- a flex item that keeps its size
   is taking it from the one beside it. */
@media (max-width: 600px) {
    .webchat-notice .alert {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-sm);
    }

    .webchat-notice .alert > .btn {
        align-self: flex-end;
    }
}

/* --- pointing at a map ----------------------------------------------

   The other way to give a location, and the one that cannot be taken
   away: `navigator.geolocation` can be refused once and is then
   refused for ever on that origin, but a map needs no permission, no
   GPS and no radio. It sits between the thread and the composer, in
   the notice's row, and it opens to about a third of the window --
   enough to recognise a street, small enough that the conversation is
   still on screen behind it. */
.webchat-locate {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-lg) 0;
}

.webchat-locate-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
}

/* A height in `dvh`, like the shell: a fixed pixel map is a postage
   stamp on a phone and half the screen on a desktop. */
.webchat-map {
    /* px first: `dvh` is unsupported before iOS 15.4, and an unsupported
       unit drops the whole declaration -- leaving the box on
       `min-height` alone, which is not what the layout was drawn for. */
    height: 20rem;
    height: 34dvh;
    min-height: 11rem;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    overflow: hidden;
    /* Leaflet drags the map; without this the browser claims the
       vertical drag for the page and the pin will not move on a
       phone. */
    touch-action: none;
}

.webchat-locate-actions {
    display: flex;
    gap: var(--space-sm);
}

/* The confirm is the one that commits, so it takes the room. */
.webchat-locate-actions .btn-primary {
    flex: 1;
}

.webchat-composer {
    display: flex;
    gap: var(--space-sm);
    align-items: flex-end;
    --bar-pad: var(--space-md);
    /* The send button must clear the home-indicator swipe zone, or a
       tap on ➤ becomes "go home". */
    padding: var(--bar-pad)
             var(--space-lg)
             calc(var(--bar-pad) + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--border-color);
    background: var(--bg-secondary);
}

/* The composer's own text box: one line that grows, not the 96px
   `textarea.input` a pharmacist writes a paragraph in.

   Selected via the composer on purpose. `textarea.input` is (0,1,1) and
   is declared further down this stylesheet, so a plain `.webchat-input`
   loses to it twice over -- and the box rendered 96px tall with the
   camera and location buttons stranded at the bottom of it. Nothing in
   a template or a test can show that; it took opening the page. */
.webchat-composer textarea.webchat-input {
    flex: 1;
    min-height: 2.75rem;
    max-height: 8rem;
    resize: none;
    overflow-y: auto;
}

/* 16px, on everything a customer types into. iOS Safari zooms the
   page on focusing any text control under 16px and never zooms back,
   so every message left the layout at 114%. `.input` is 14px for the
   console's dense forms and stays so -- desktops do not zoom on
   focus. Scoped to the shell so it also catches the phone box in the
   identity card, not only the composer. (0,2,0) outranks `.input`.
   Fixing the cause is the rule: `user-scalable=no` would take
   pinch-zoom away from the people who need it. */
.webchat-shell .input {
    font-size: 1rem;
}

/* A square button that carries an icon rather than a word — the camera
   and the location pin. Sized to the composer's own height so the row
   lines up. */
.icon-btn {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    font-size: 1.1rem;
    line-height: 1;
    /* A long press on 📷 or 📍 otherwise selects the emoji and opens
       the copy callout. */
    -webkit-user-select: none;
    user-select: none;
}

textarea.input {
    min-height: 96px;
    resize: vertical;
    font-family: var(--font-display);
    line-height: 1.5;
}

.empty-state {
    padding: var(--space-2xl);
    text-align: center;
    color: var(--text-tertiary);
    font-weight: 300;
}

/* White background theme */
.theme-light {
    --bg-primary: #ffffff;
    --bg-secondary: #f8f9fa;
    --bg-tertiary: #f1f3f5;
    --bg-elevated: #e9ecef;

    --text-primary: #0f0f14;
    --text-secondary: #3a3a48;
    --text-tertiary: #5a5a68;
    --border-color: #d8d8e0;
    --border-light: #c0c0cc;

    /* Adjust primary colors for better readability on white */
    --red-primary: #dc2626;
    --red-bright: #ef4444;
    --blue-primary: #2563eb;
    --blue-bright: #3b82f6;
    /* Lighter scrim: on a white page a heavy one reads as a page load. */
    --overlay: rgba(15, 15, 20, 0.45);
}

.theme-light .tile-icon {
    background: rgba(220, 38, 38, 0.1);
    border-color: rgba(220, 38, 38, 0.25);
}

.theme-light .tile-icon.blue {
    background: rgba(37, 99, 235, 0.1);
    border-color: rgba(37, 99, 235, 0.25);
}

.theme-light .badge-red {
    background: rgba(220, 38, 38, 0.1);
    color: #991b1b;
    border-color: rgba(220, 38, 38, 0.3);
}

.theme-light .badge-blue {
    background: rgba(37, 99, 235, 0.1);
    color: #1e40af;
    border-color: rgba(37, 99, 235, 0.3);
}

.theme-light .badge-success {
    background: rgba(5, 150, 105, 0.1);
    color: #065f46;
    border-color: rgba(5, 150, 105, 0.3);
}

.theme-light .btn-ghost {
    color: var(--text-primary);
    border-color: #b8b8c4;
}

.theme-light .btn-ghost:hover {
    background: var(--bg-tertiary);
    border-color: #9898a8;
}

.theme-light .table tbody tr:hover {
    background: var(--bg-tertiary);
}

.theme-light .input {
    border-color: #c8c8d4;
}

.theme-light .input:focus {
    border-color: var(--blue-primary);
    box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}

.theme-light .select {
    border-color: #c8c8d4;
    background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%235a5a68' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

.theme-light .select:focus {
    border-color: var(--blue-primary);
    box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}

.theme-light .card {
    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}

.theme-light .card:hover {
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}

.theme-light .tile {
    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}

.theme-light .tile:hover {
    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}

.theme-light .table-container {
    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}

.theme-light .table-action-btn {
    border-color: #b8b8c4;
}

.theme-light .table-action-btn:hover {
    background: var(--bg-tertiary);
    border-color: #9898a8;
}

.theme-light .tile-footer {
    border-top-color: #d8d8e0;
}

.theme-light .label {
    color: var(--text-secondary);
}

.theme-light .alert {
    background: var(--bg-tertiary);
    border-color: #e0e0e8;
}

.theme-light .alert-error {
    background: #fef2f2;
    border-color: #fecaca;
    color: var(--red-primary);
}

.theme-light .alert-success {
    background: #ecfdf5;
    border-color: #a7f3d0;
    color: #047857;
}

.theme-light .auth-card {
    background: #ffffff;
    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.04);
}

.theme-light .chat-message.is-pharmacist .chat-bubble {
    background: #dbeafe;
    border-color: #bfdbfe;
    color: var(--text-primary);
}

/* --- The customer's brand skin --------------------------------------

   The console is dark because it is a tool somebody stares into for
   eight hours. `/chat/` is the one screen a CUSTOMER sees, and a
   customer has already met this pharmacy on a shelf, a bag and a
   poster -- so it wears the corporate identity manual instead: Asap,
   midnight blue on white over a platinum canvas, and red kept for the
   things you press.

   Self-contained rather than layered on `.theme-light`. A skin that
   only works while another class happens to be on the same element is
   a skin that breaks the first time somebody uses it on its own, and
   the breakage is silent.

   Two of the manual's own rules are easy to get wrong here and
   expensive to discover afterwards:

   * **White on red is «large only»** in the manual's contrast table --
     it does not pass for body text. Message bodies are 15px. So the
     customer's own bubble is MIDNIGHT BLUE, which is AAA against
     white, and red stays where the manual puts it: primary actions and
     brand emphasis. A red chat bubble would have been the obvious
     choice and the wrong one.
   * **The manual's body weight is 400/500; this system's is 300.** A
     300 on near-black reads elegant and on white reads anaemic, so
     every weight this screen uses is restated below. */
.theme-suiza {
    /* Manual, Color: platinum is the canvas, white the working
       surface, midnight blue the ink. */
    --bg-primary: #ffffff;
    --bg-secondary: #f4f3f3;
    --bg-tertiary: #e5e4e4;   /* Platinum */
    --bg-elevated: #d7d6d6;

    --text-primary: #1c2241;  /* Midnight Blue: body, icons, titles */
    --text-secondary: #474c69;
    --text-tertiary: #6e7288;
    --border-color: #d5d4d4;
    --border-light: #b7b7b7;  /* the manual's own gray */

    /* Default / hover / pressed, exactly the sequence the manual's
       component states table gives. `--red-bright` keeps meaning "the
       lighter one" -- the places that need the DARKER red on a white
       background ask for it by name further down. */
    --red-primary: #e52e30;   /* Bright Red */
    --red-bright: #f0595b;
    --red-dark: #ad191c;      /* Dark Red */
    --red-muted: #931916;     /* Dark Rosewood */

    --blue-primary: #1c2241;  /* Midnight Blue */
    --blue-bright: #2c3462;
    --blue-dark: #0c2e47;     /* Deep Blue, from the secondary scale */
    --blue-muted: #1c2241;    /* the customer's own bubble */

    --warning: #dec508;       /* Mustard Yellow */
    --success: #0f7a5a;
    --error: var(--red-dark);

    --font-display: 'Asap', 'Roboto', system-ui, -apple-system, 'Segoe UI', sans-serif;

    /* Navy-tinted, like the manual's own two shadow values -- a grey
       shadow under a blue-black palette reads as dirt. */
    --shadow-sm: 0 2px 6px rgba(28, 34, 65, 0.08);
    --shadow-md: 0 8px 20px rgba(28, 34, 65, 0.10);
    --shadow-lg: 0 14px 32px rgba(28, 34, 65, 0.12);
    --shadow-xl: 0 28px 70px rgba(28, 34, 65, 0.16);

    --overlay: rgba(28, 34, 65, 0.45);

    font-weight: 400;
    /* Stated, not inherited. Most components take their colour from a
       token, but a chat bubble and a `.type-*` span take it from
       whatever they are inside -- which is `body`, which is dark. Put
       the skin on anything other than <body> without this and the AI's
       messages render pale grey on platinum: legible in a screenshot,
       invisible on a screen. */
    color: var(--text-primary);
}

/* The window is white; the canvas behind it is platinum. On a phone
   the shell fills the width and this is never seen -- it is what makes
   the chat read as a card on a desktop, where the 46rem shell leaves a
   margin on both sides. */
body.theme-suiza {
    background: var(--bg-tertiary);
}

/* Everything that hard-codes the console's 300. */
.theme-suiza .type-body-1,
.theme-suiza .type-body-2,
.theme-suiza .type-small,
.theme-suiza .chat-bubble,
.theme-suiza .alert,
.theme-suiza .input,
.theme-suiza .card p,
.theme-suiza .empty-state,
.theme-suiza .field-error {
    font-weight: 400;
}

/* The manual's buttons are heavy and uppercase. `.btn` is already
   uppercase; only the weight and the tracking differ. */
.theme-suiza .btn {
    font-weight: 800;
    letter-spacing: 0.04em;
}

.theme-suiza .label,
.theme-suiza .badge,
.theme-suiza .card-section-title {
    font-weight: 700;
}

/* A timestamp is the one thing on this screen the console sets in
   JetBrains Mono, and the manual has no monospaced face at all. On the
   customer's side it is ordinary small type. */
.theme-suiza .chat-meta {
    font-family: var(--font-display);
    font-weight: 500;
    font-size: 0.75rem;
}

/* Default, hover, pressed. */
.theme-suiza .btn-primary:hover {
    background: var(--red-dark);
}

.theme-suiza .btn-primary:active {
    background: var(--red-muted);
    transform: none;
}

.theme-suiza .btn-ghost {
    border-color: var(--border-light);
}

.theme-suiza .btn-ghost:hover {
    background: var(--bg-tertiary);
    border-color: var(--text-tertiary);
}

/* Mustard, at 4px, offset from the control -- the manual's focus
   treatment, and the one place its accent colour earns its keep. It is
   the only token in the palette that is neither red nor blue, so a
   focus ring can never be mistaken for a state. */
.theme-suiza .btn:focus-visible {
    outline: 4px solid rgba(222, 197, 8, 0.78);
    outline-offset: 3px;
}

.theme-suiza .input {
    border-color: var(--border-light);
}

.theme-suiza .input:focus {
    border-color: var(--blue-primary);
    box-shadow: 0 0 0 3px rgba(222, 197, 8, 0.55);
}

.theme-suiza .badge-red {
    background: rgba(229, 46, 48, 0.10);
    border-color: rgba(229, 46, 48, 0.30);
    color: var(--red-muted);
}

.theme-suiza .badge-blue {
    background: rgba(28, 34, 65, 0.08);
    border-color: rgba(28, 34, 65, 0.25);
    color: var(--blue-primary);
}

.theme-suiza .alert {
    background: var(--bg-secondary);
    border-color: var(--border-light);
    color: var(--text-secondary);
}

.theme-suiza .alert-error {
    background: rgba(229, 46, 48, 0.08);
    border-color: rgba(229, 46, 48, 0.35);
    color: var(--red-dark);
}

.theme-suiza .field-error {
    color: var(--red-dark);
}

.theme-suiza .card {
    box-shadow: var(--shadow-sm);
}

/* A card on the canvas, not a panel ruled off from it. Only visible
   where the canvas is: on a phone the shell fills the width and the
   shadow falls off both edges. */
.theme-suiza .webchat-shell {
    border: none;
    box-shadow: var(--shadow-lg);
}

/* The header carries the brand and nothing else has to. */
.theme-suiza .webchat-header {
    position: relative;
    background: var(--bg-primary);
    border-bottom: none;
}

/* The manual's stripe module -- a long red bar, a shorter navy one, a
   short red one, in its own 360/126/30 proportions -- reduced to the
   4px a chat header can spare. It is the identity's one repeating
   graphic device, so four pixels of it say whose window this is
   faster than any amount of copy. */
.theme-suiza .webchat-header::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 4px;
    background: linear-gradient(
        to right,
        var(--red-primary) 0 66%,
        transparent 66% 69%,
        var(--blue-primary) 69% 92%,
        transparent 92% 95%,
        var(--red-primary) 95% 100%
    );
}

/* Who is talking, said with the palette rather than with a legend.

   `.is-ai` loses the console's dashed edge on purpose: dashed means
   "provisional" to a customer and "written by the agent" only to
   somebody who has been told. The pharmacist's bubble takes a red left
   edge instead -- the moment a person arrives is the one thing on this
   screen worth marking, and it is brand emphasis, which is what the
   manual keeps red for. */
.theme-suiza .chat-thread.is-mine-right .chat-message.is-customer .chat-bubble {
    color: white;
}

.theme-suiza .chat-thread.is-mine-right .chat-message.is-ai .chat-bubble {
    background: var(--bg-tertiary);
    border-style: solid;
    border-color: var(--border-color);
}

.theme-suiza .chat-thread.is-mine-right .chat-message.is-pharmacist .chat-bubble {
    background: var(--bg-primary);
    border-color: var(--border-color);
    border-left: 4px solid var(--red-primary);
}

.theme-suiza .chat-message.is-system .chat-bubble {
    color: var(--text-tertiary);
}

/* Navigation Menu */
.nav-menu {
    background: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    overflow: hidden;
}

.nav-item {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-lg);
    color: var(--text-secondary);
    text-decoration: none;
    font-weight: 300;
    font-size: 0.875rem;
    transition: all var(--transition-fast);
    border-bottom: 1px solid var(--border-color);
    cursor: pointer;
}

.nav-item:last-child {
    border-bottom: none;
}

.nav-item:hover {
    background: var(--bg-tertiary);
    color: var(--text-primary);
}

.nav-item.active {
    background: var(--bg-tertiary);
    color: var(--text-primary);
    border-left: 3px solid var(--red-primary);
    padding-left: calc(var(--space-lg) - 3px);
}

.nav-item-icon {
    width: 20px;
    height: 20px;
    color: var(--text-tertiary);
}

.nav-item.active .nav-item-icon {
    color: var(--red-primary);
}

.nav-item:hover .nav-item-icon {
    color: var(--red-primary);
}

/* Dropdown Menu */
.dropdown {
    position: relative;
    display: inline-block;
}

.dropdown-menu {
    background: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    min-width: 200px;
    overflow: hidden;
}

.dropdown-item {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    color: var(--text-primary);
    font-weight: 300;
    font-size: 0.875rem;
    transition: all var(--transition-fast);
    cursor: pointer;
    border-bottom: 1px solid var(--border-color);
}

.dropdown-item:last-child {
    border-bottom: none;
}

.dropdown-item:hover {
    background: var(--bg-tertiary);
    color: var(--red-primary);
}

.dropdown-divider {
    height: 1px;
    background: var(--border-color);
    margin: var(--space-xs) 0;
}

/* Dashboard Components */
.dashboard-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--space-lg);
}

.stat-card {
    background: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    padding: var(--space-xl);
    transition: all var(--transition-base);
}

.stat-card:hover {
    border-color: var(--border-light);
    transform: translateY(-2px);
}

.stat-label {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-tertiary);
    font-weight: 400;
    margin-bottom: var(--space-sm);
}

.stat-value {
    font-size: 2.5rem;
    font-weight: 200;
    color: var(--text-primary);
    line-height: 1;
    margin-bottom: var(--space-sm);
}

.stat-change {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    font-size: 0.75rem;
    font-weight: 300;
}

.stat-change.positive {
    color: var(--success);
}

.stat-change.negative {
    color: var(--red-primary);
}

.stat-icon {
    width: 40px;
    height: 40px;
    border-radius: var(--radius-md);
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: var(--space-md);
}

.stat-icon.red {
    background: rgba(239, 68, 68, 0.1);
    border: 1px solid rgba(239, 68, 68, 0.2);
}

.stat-icon.red svg {
    color: var(--red-primary);
}

.stat-icon.blue {
    background: rgba(59, 130, 246, 0.1);
    border: 1px solid rgba(59, 130, 246, 0.2);
}

.stat-icon.blue svg {
    color: var(--blue-primary);
}

.stat-icon.green {
    background: rgba(16, 185, 129, 0.1);
    border: 1px solid rgba(16, 185, 129, 0.2);
}

.stat-icon.green svg {
    color: var(--success);
}

/* Activity Feed */
.activity-feed {
    background: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    overflow: hidden;
}

.activity-item {
    display: flex;
    gap: var(--space-md);
    padding: var(--space-lg);
    border-bottom: 1px solid var(--border-color);
    transition: background var(--transition-fast);
}

.activity-item:last-child {
    border-bottom: none;
}

.activity-item:hover {
    background: var(--bg-tertiary);
}

.activity-icon {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    background: var(--bg-tertiary);
    border: 1px solid var(--border-color);
}

.activity-icon svg {
    width: 18px;
    height: 18px;
    color: var(--text-secondary);
}

.activity-content {
    flex: 1;
}

.activity-title {
    font-weight: 400;
    font-size: 0.875rem;
    color: var(--text-primary);
    margin-bottom: var(--space-xs);
}

.activity-description {
    font-size: 0.75rem;
    color: var(--text-tertiary);
    font-weight: 300;
}

.activity-time {
    font-size: 0.75rem;
    color: var(--text-tertiary);
    font-family: var(--font-mono);
    font-weight: 200;
}

/* Progress Bar */
.progress-container {
    margin-bottom: var(--space-md);
}

.progress-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: var(--space-xs);
}

.progress-label {
    font-size: 0.75rem;
    color: var(--text-secondary);
    font-weight: 300;
}

.progress-value {
    font-size: 0.75rem;
    color: var(--text-primary);
    font-weight: 400;
    font-family: var(--font-mono);
}

.progress-bar {
    height: 8px;
    background: var(--bg-tertiary);
    border-radius: var(--radius-sm);
    overflow: hidden;
}

.progress-fill {
    height: 100%;
    background: var(--red-primary);
    border-radius: var(--radius-sm);
    transition: width var(--transition-slow);
}

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

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

/* Utility classes */
.mt-md { margin-top: var(--space-md); }
.mb-xs { margin-bottom: var(--space-xs); }
.mb-md { margin-bottom: var(--space-md); }
.mb-lg { margin-bottom: var(--space-lg); }
.mb-xl { margin-bottom: var(--space-xl); }

/* A full-bleed page (.auth-shell centres itself against the viewport,
   so the body's own padding would fight it). */
.page-flush { padding: 0; }

/* A page that IS the app: one screen, and nothing scrolls except the
   component that says it does. On <html>, because `height: 100%` on
   body needs a parent with a height and only the root's 100% is the
   viewport.

   Three things it undoes. `body { min-height: 100vh }` is the
   URL-bar-collapsed height, so on a phone the document was taller
   than the screen by the bar and could be dragged -- which is what
   makes a page feel like a page. `overflow: hidden` on both stops the
   drag; `overscroll-behavior: none` stops the browser's own
   pull-to-refresh and rubber band, which no app has. It does NOT stop
   iOS Safari scrolling a locked document to reveal a focused box
   under the keyboard -- webchat.js puts it back at 0. Pair it with
   .page-flush on <body>, as the chat does. */
html.page-app {
    height: 100%;
    overflow: hidden;
    overscroll-behavior: none;
}

html.page-app body {
    height: 100%;
    min-height: 0;
    overflow: hidden;
    overscroll-behavior: none;
}

/* A button that fills its container -- forms, cards, mobile rows. */
.btn-block { width: 100%; justify-content: center; }

.text-red { color: var(--red-primary); }
.text-blue { color: var(--blue-primary); }
.text-secondary { color: var(--text-secondary); }
.text-tertiary { color: var(--text-tertiary); }

.divider {
    height: 1px;
    background: var(--border-color);
    margin: var(--space-xl) 0;
}

.button-group {
    display: flex;
    gap: var(--space-md);
    flex-wrap: wrap;
}

/* A control and the button that acts on it, on one line -- a value
   somebody edits in place, inside a table row or a narrow panel.
   `.button-group` cannot do this: `.input` is `width: 100%`, and a flex
   item asking for the whole line pushes the button onto the next one,
   so the pair reads as two unrelated controls stacked up. Here the
   input flexes and the button keeps its size. */
.input-row {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}

.input-row .input,
.input-row .select {
    width: auto;
    /* `min-width: 0` alone lets a flex item collapse to nothing in a
       shrink-to-fit table cell. `ch` because what goes in one of these
       is a short value -- 150.00, 18:00, 6 -- and the field should be
       about as wide as the thing it holds. */
    flex: 1 1 8ch;
    min-width: 8ch;
}

.input-row .btn {
    flex-shrink: 0;
}

/* A QR meant to be read off a screen, not off a thermal label.

   `--label-paper`/`--label-ink` again, and for exactly the reason those
   tokens exist: a scanner needs real contrast. segno draws the modules
   and leaves the background transparent, which on the dark palette's
   card is a black QR on a near-black ground -- invisible, and
   unscannable. The white block and its quiet zone are what make the
   code work, so they belong to the component and not to one screen. */
.qr-block {
    flex: none;
    width: 10rem;
    height: 10rem;
    padding: var(--space-sm);
    background: var(--label-paper);
    border-radius: var(--radius-sm);
}

.qr-block svg {
    width: 100%;
    height: 100%;
    display: block;
}

/* A URL that is checked character by character against artwork before
   anything goes to print, so it wraps anywhere rather than ellipsising:
   the tail of a link is where the campaign slug lives, and that is the
   half somebody is actually proofreading. */
.qr-url {
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--text-secondary);
    word-break: break-all;
}
