/* ============================================================
   Brother QL — Console design system
   Light = default (clean console), Dark = body.dark-mode
   ============================================================ */

:root {
    /* Light Console palette */
    --bg: #F4F6F9;
    --bg-glow: rgba(34, 211, 238, 0.06);
    --panel: #FFFFFF;
    --panel-2: #F1F4F8;
    --rail-bg: #FFFFFF;
    --ink: #0E141B;
    --muted: #56616F;
    --muted-dim: #8A93A3;
    --border: #DDE3EA;
    --border-soft: #E8ECF1;
    --accent: #0891B2;
    --accent-strong: #0E7490;
    --accent-dim: #67E8F0;
    --accent-glow: rgba(8, 145, 178, 0.16);
    --accent-soft: rgba(8, 145, 178, 0.10);
    --green: #059669;
    --red: #DC2626;
    /* Caution. Sits between "fine" and "wrong": something worked but not the way
       it was asked for, or something disagrees without blocking anything. */
    --warn: #D97706;
    --label-bg: #FFFFFF;
    --label-ink: #0a0a0a;
    /* Die-cut overlay. It always sits on the (white) label surface, so the veil
       and hatch are identical in both themes; only the cut edge follows the
       accent that reads best on white. The veil is paper-coloured on purpose:
       it fades ink that falls outside the cut towards the paper it will never
       reach, while the hatch marks the discarded area itself. */
    --diecut-veil: rgba(255, 255, 255, 0.55);
    --diecut-hatch: rgba(14, 20, 27, 0.28);
    --diecut-edge: var(--accent);
    --radius: 12px;
    --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
    --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    --shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 8px 24px rgba(16, 24, 40, 0.04);
}

body.dark-mode {
    --bg: #0B0E14;
    --bg-glow: rgba(34, 211, 238, 0.05);
    --panel: #11151D;
    --panel-2: #0E121A;
    --rail-bg: #0E121A;
    --ink: #E6EAF0;
    --muted: #8A93A3;
    --muted-dim: #5C6473;
    --border: #1E2530;
    --border-soft: #171C26;
    --accent: #22D3EE;
    --accent-strong: #67E8F0;
    --accent-dim: #0E7490;
    --accent-glow: rgba(34, 211, 238, 0.18);
    --accent-soft: rgba(34, 211, 238, 0.10);
    --green: #34D399;
    --red: #F87171;
    /* Brightened for dark panels, the same way --green and --red are: the light
       theme's #D97706 is a brown smudge against #11151D. */
    --warn: #FBBF24;
    --label-bg: #FFFFFF;
    --label-ink: #0a0a0a;
    /* Dark theme brightens --accent for dark panels; on the white label the
       dimmed accent keeps the cut edge readable. */
    --diecut-edge: var(--accent-dim);
    --shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
}

* { box-sizing: border-box; }

html, body {
    margin: 0;
    padding: 0;
    min-height: 100%;
    background: var(--bg);
    color: var(--ink);
    font-family: var(--sans);
    font-size: 14px;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    /* Native-app feel on iOS: no text auto-resizing, no tap flash, no rubber-band scroll. */
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
    -webkit-tap-highlight-color: transparent;
    overscroll-behavior: none;
}

/* Respect the notch / home indicator when running full-screen ("Add to Home Screen"). */
@supports (padding: env(safe-area-inset-top)) {
    .topbar { padding-left: max(20px, env(safe-area-inset-left)); padding-right: max(20px, env(safe-area-inset-right)); }
    .shell { padding-top: env(safe-area-inset-top); }
    .main { padding-bottom: max(20px, calc(20px + env(safe-area-inset-bottom))); }
    .rail { padding-bottom: env(safe-area-inset-bottom); padding-left: env(safe-area-inset-left); }
}

body {
    background:
        radial-gradient(900px 600px at 82% -10%, var(--bg-glow), transparent 60%),
        var(--bg);
}

/* ====== Shell layout ====== */
.shell {
    display: grid;
    grid-template-columns: 240px 1fr;
    grid-template-rows: 60px 1fr;
    min-height: 100vh;
}

/* ====== Nav rail / sidebar ====== */
.rail {
    grid-row: 1 / 3;
    grid-column: 1;
    background: var(--rail-bg);
    border-right: 1px solid var(--border);
}
.rail-inner {
    position: sticky;
    top: 0;
    height: 100vh;
    display: flex;
    flex-direction: column;
    padding: 16px 14px;
}

.brand {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 6px 8px 18px;
}
.brand .glyph {
    width: 32px; height: 32px;
    border-radius: 9px;
    display: grid; place-items: center;
    color: var(--accent);
    font-size: 16px;
    background: linear-gradient(145deg, var(--panel-2), var(--panel));
    border: 1px solid var(--border);
    box-shadow: 0 0 0 1px var(--accent-soft), 0 0 18px var(--accent-glow);
}
.brand .brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.brand .name { font-weight: 600; font-size: 14px; letter-spacing: -0.01em; }
.brand .sub { font-family: var(--mono); font-size: 10px; color: var(--muted-dim); }

.nav-label {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    color: var(--muted-dim);
    padding: 16px 10px 8px;
    font-weight: 600;
}

/* Pushes the System nav group to the bottom of the rail (flush above the foot) */
.nav-label--bottom { margin-top: auto; }

.nav-list { display: flex; flex-direction: column; gap: 2px; }
.nav-item {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 9px 11px;
    border-radius: 9px;
    color: var(--muted);
    cursor: pointer;
    border: 1px solid transparent;
    background: transparent;
    position: relative;
    text-align: left;
    width: 100%;
    font-family: var(--sans);
    transition: background .14s, color .14s, border-color .14s;
}
.nav-item i { font-size: 16px; width: 18px; text-align: center; flex: none; }
.nav-item span { font-size: 13px; font-weight: 500; }
.nav-item:hover { background: var(--panel-2); color: var(--ink); }
.nav-item:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.nav-item.active {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--ink);
}
.nav-item.active::before {
    content: "";
    position: absolute;
    left: -14px; top: 50%; transform: translateY(-50%);
    width: 3px; height: 20px;
    background: var(--accent);
    border-radius: 0 3px 3px 0;
}
.nav-item.active i { color: var(--accent); }

.rail-foot {
    margin-top: 16px;
    padding-top: 14px;
    border-top: 1px solid var(--border-soft);
    display: flex;
    gap: 8px;
}
.rail-action {
    flex: 1;
    height: 38px;
    display: grid;
    place-items: center;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--panel);
    color: var(--muted);
    font-size: 15px;
    cursor: pointer;
    text-decoration: none;
    transition: border-color .14s, color .14s, background .14s;
}
.rail-action:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.rail-action:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }

.rail-scrim {
    position: fixed;
    inset: 0;
    background: rgba(8, 11, 16, 0.5);
    backdrop-filter: blur(2px);
    z-index: 90;
    border: none;
}
.rail-scrim[hidden] { display: none; }

/* ====== Top bar ====== */
.topbar {
    grid-column: 2;
    grid-row: 1;
    position: sticky;
    top: 0;
    z-index: 50;
    background: color-mix(in srgb, var(--panel) 78%, transparent);
    border-bottom: 1px solid var(--border);
    backdrop-filter: blur(10px);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 22px;
}
.topbar-left { display: flex; align-items: center; gap: 12px; }
.hamburger {
    display: none;
    width: 38px; height: 38px;
    border-radius: 9px;
    border: 1px solid var(--border);
    background: var(--panel);
    color: var(--ink);
    font-size: 18px;
    cursor: pointer;
}
.title h1 { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; margin: 0; }
.title p { font-size: 11px; color: var(--muted); margin: 0; }

/* The cluster is the popover's anchor: the medium switcher's list is measured
   from the cluster's right edge, so it can never be pushed off screen by the
   pill it belongs to sitting far from the edge. */
.status-cluster { position: relative; display: flex; align-items: center; gap: 14px; }
.medium-switcher { display: inline-flex; }

/* status pill (navbar-check-status / status-indicator) */
.pill {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 6px 13px 6px 10px;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--panel);
    font-family: var(--mono);
    font-size: 11px;
    color: var(--ink);
    cursor: pointer;
    transition: border-color .14s, background .14s;
}
.pill:hover { border-color: var(--accent); }
.pill:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.pill .dot {
    width: 8px; height: 8px; border-radius: 50%;
    background: var(--muted-dim);
    flex: none;
    transition: background .2s, box-shadow .2s;
}
/* Compact icon shown instead of the dot+label on phones (see mobile media query). */
.pill .pill-icon { display: none; }
.pill.online { border-color: color-mix(in srgb, var(--green) 45%, var(--border)); }
.pill.online .dot { background: var(--green); box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 18%, transparent); }
.pill.offline { border-color: color-mix(in srgb, var(--red) 45%, var(--border)); }
.pill.offline .dot { background: var(--red); box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 18%, transparent); }
/* keep-alive pill: active (thread running) vs idle */
.pill.ka-active { border-color: color-mix(in srgb, var(--green) 45%, var(--border)); }
.pill.ka-active .dot { background: var(--green); box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 18%, transparent); }
#navbar-keepalive.busy { opacity: .6; pointer-events: none; }

/* What the queue is doing, in the header, beside the printer it is doing it to.
   Present only while a job is actually being held or printed, so its arrival is
   itself the signal: the row is otherwise the same four pills it always is.
   Accented rather than red or green -- a job waiting out a boot is the app
   working, not a fault. */
.pill-activity {
    border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
    background: color-mix(in srgb, var(--accent) 7%, var(--panel));
    color: var(--accent-strong);
    gap: 6px;
    padding-left: 11px;
}
.pill-activity[hidden] { display: none; }
.pill-activity .pill-icon { display: inline-flex; font-size: 12px; }
.pill-activity .pill-activity-elapsed {
    font-variant-numeric: tabular-nums;
    color: var(--muted);
}
.pill-activity .pill-activity-elapsed:empty { display: none; }
/* A retry is the one phase here that is not simply progress. */
.pill-activity.is-retrying {
    border-color: color-mix(in srgb, var(--warn) 55%, var(--border));
    background: color-mix(in srgb, var(--warn) 10%, var(--panel));
    color: var(--warn);
}
/* On a phone the cluster has no room for prose: the icon and the clock say
   which phase and how long, and the queue panel says the rest. */
@media (max-width: 560px) {
    .pill-activity #navbar-activity-label { display: none; }
}

/* Manual "ask the printer now" button, sitting with the pills it refreshes.
   Round and icon-only at every width: it has no state of its own to spell out,
   and the cluster has no room to spare on a phone. */
.pill-refresh {
    width: 30px; height: 30px;
    padding: 0;
    justify-content: center;
    border-radius: 50%;
    flex: none;
}
.pill-refresh .pill-icon { display: inline-flex; font-size: 14px; color: var(--muted); }
.pill-refresh:hover .pill-icon { color: var(--accent-strong); }
/* In flight: the printer may take seconds to answer, or never, and a button
   that looks idle while it waits is a button people press again. */
.pill-refresh[aria-disabled="true"] { pointer-events: none; }
.pill-refresh.is-busy .pill-icon { color: var(--accent); animation: pill-spin .9s linear infinite; }
@keyframes pill-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .pill-refresh.is-busy .pill-icon { animation: none; opacity: .55; }
}

/* Loaded-medium pill. Three states, and only the disagreement is loud:
   media-ok   - the printer's roll is what the app prints for
   media-warn - the printer has one roll in it and the settings say another
   media-idle - nothing is known, or a roll is being changed right now. */
.pill-medium { max-width: 190px; }
.pill-medium #medium-indicator {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.pill.media-ok { border-color: color-mix(in srgb, var(--green) 45%, var(--border)); }
.pill.media-ok .dot { background: var(--green); box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 18%, transparent); }
.pill.media-warn { border-color: color-mix(in srgb, var(--red) 45%, var(--border)); }
.pill.media-warn .dot { background: var(--red); box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 18%, transparent); }
.pill.media-idle .dot { background: var(--muted-dim); box-shadow: none; }

/* "auto": automatic switching is on. It is the only pill marker that reports a
   setting rather than a reading, so it is a chip and not another dot - and it
   is deliberately quiet, because it is on screen for as long as the mode is. */
.pill-medium { position: relative; }
.pill-auto {
    flex: 0 0 auto;
    padding: 1px 5px;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
    background: var(--accent-soft);
    color: var(--accent-strong);
    font-family: var(--mono);
    font-size: 8.5px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    line-height: 1.5;
}
.pill-auto[hidden] { display: none; }
/* The medium name has to give the chip its room without the pill growing into
   the buttons beside it. */
.pill-medium.is-auto { max-width: 224px; }
/* Open state matches the picker's own trigger, so the pill reads as the
   control the list belongs to. */
.medium-switcher.is-open .pill { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }

/* ====== Main ====== */
.main {
    grid-column: 2;
    grid-row: 2;
    padding: 22px;
    overflow-x: hidden;
}

/* work grid: compose form + preview, with preview mounted into active pane */
.work-grid {
    display: grid;
    grid-template-columns: minmax(380px, 1fr) 1.05fr;
    gap: 22px;
    align-items: start;
}
.settings-wrap { max-width: 820px; }

/* Tab-pane visibility: only the active compose/settings pane is shown.
   Restores Bootstrap's hiding in case a layout/display override leaks. */
.tab-content > .tab-pane { display: none; }
.tab-content > .tab-pane.active { display: block; }

/* ====== Panel ====== */
.panel {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}
.panel-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 14px 18px;
    border-bottom: 1px solid var(--border-soft);
}
.panel-head h2 { font-size: 13px; font-weight: 600; letter-spacing: 0.01em; margin: 0; }
.tag {
    font-family: var(--mono); font-size: 10px; color: var(--muted);
    border: 1px solid var(--border); padding: 2px 7px; border-radius: 5px;
}
.panel-body { padding: 18px; }

/* ====== Form bits ====== */
.field { margin-bottom: 16px; }
.field:last-child { margin-bottom: 0; }
.lbl {
    display: block;
    font-size: 11px; font-weight: 500;
    color: var(--muted);
    margin-bottom: 7px;
    letter-spacing: 0.01em;
}
/* Terse annotations next to a field: a range ("1-100"), a unit, a list of
   accepted file types. Monospaced because they are values, not sentences. */
.hint { font-family: var(--mono); font-size: 10px; color: var(--muted-dim); margin-top: 6px; }
/* The same slot, but carrying a sentence. Prose in the mono face reads as code
   and is hard work at 10px, so it keeps the placement and drops the typeface. */
.hint-prose {
    font-family: var(--sans);
    font-size: 11px;
    line-height: 1.6;
}

textarea,
input[type=text],
input[type=number],
input[type=file],
select {
    width: 100%;
    background: var(--panel-2);
    border: 1px solid var(--border);
    color: var(--ink);
    border-radius: 9px;
    padding: 10px 12px;
    font-size: 13px;
    font-family: var(--sans);
    outline: none;
    transition: border-color .14s, box-shadow .14s;
}
textarea:focus,
input[type=text]:focus,
input[type=number]:focus,
input[type=file]:focus,
select:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-glow);
}
textarea:disabled,
input:disabled,
select:disabled { opacity: .55; cursor: not-allowed; }
textarea { resize: vertical; min-height: 88px; line-height: 1.55; }
input.mono { font-family: var(--mono); letter-spacing: 0.01em; }
input[type=file] { padding: 8px 10px; cursor: pointer; }
input[type=file]::file-selector-button {
    margin-right: 10px;
    border: 1px solid var(--border);
    background: var(--panel);
    color: var(--ink);
    padding: 6px 12px;
    border-radius: 7px;
    font-family: var(--sans);
    font-size: 12px;
    cursor: pointer;
}
select {
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238A93A3' stroke-width='2.4'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 12px center;
    padding-right: 34px;
    cursor: pointer;
}

/* minmax(0, 1fr) rather than 1fr: an "auto" minimum lets one cell's widest
   unbreakable content push the track past its share and squeeze its neighbour
   to nothing, which is exactly what a long line inside a field would do. */
.grid-2 { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; margin-bottom: 16px; }
.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin-bottom: 16px; }
.grid-2 .field, .grid-3 .field { margin-bottom: 0; min-width: 0; }

.section-sep { height: 1px; background: var(--border-soft); margin: 18px 0; }
.section-cap { font-size: 10px; text-transform: uppercase; letter-spacing: 0.09em; color: var(--muted-dim); font-weight: 600; margin-bottom: 14px; }

/* print options strip */
.print-options {
    border: 1px solid var(--border-soft);
    background: var(--panel-2);
    border-radius: 10px;
    padding: 14px;
    margin-bottom: 18px;
}
.print-options .grid-2 { margin-bottom: 0; }

/* toggle row (qr-show-text) */
.toggle-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 14px;
}
.toggle-row .meta .t { font-size: 13px; font-weight: 500; color: var(--ink); }
.toggle-row .meta .d { font-family: var(--mono); font-size: 10px; color: var(--muted-dim); margin-top: 2px; }
.switch { position: relative; width: 40px; height: 23px; flex: none; cursor: pointer; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; }
.switch .track {
    position: absolute; inset: 0; border-radius: 999px;
    background: var(--panel-2); border: 1px solid var(--border); transition: .18s;
}
.switch .knob {
    position: absolute; top: 4px; left: 4px; width: 13px; height: 13px;
    border-radius: 50%; background: var(--muted); transition: .18s;
}
.switch input:checked + .track { background: var(--accent-soft); border-color: var(--accent); }
.switch input:checked ~ .knob { left: 22px; background: var(--accent); }
.switch input:focus-visible + .track { box-shadow: 0 0 0 2px var(--accent-soft); }

#qr-text-options { margin-top: 16px; }

/* primary print button */
.btn-print {
    display: inline-flex; align-items: center; justify-content: center; gap: 9px;
    width: 100%;
    padding: 12px 22px;
    border-radius: 10px;
    border: 1px solid var(--accent-strong);
    background: var(--accent);
    color: #FFFFFF;
    font-size: 14px; font-weight: 600;
    font-family: var(--sans);
    cursor: pointer;
    box-shadow: 0 1px 1px rgba(16, 24, 40, 0.06);
    transition: background .14s, border-color .14s, transform .06s;
}
body.dark-mode .btn-print { color: #06222B; }
.btn-print i { font-size: 15px; }
.btn-print:hover { background: var(--accent-strong); border-color: var(--accent-strong); }
.btn-print:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--accent-soft); }
.btn-print:active { transform: translateY(1px); }
.btn-print:disabled { opacity: .55; cursor: not-allowed; }

.btn-ghost {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    padding: 11px 18px;
    border-radius: 10px;
    border: 1px solid var(--border);
    background: var(--panel);
    color: var(--ink);
    font-size: 13px; font-weight: 500;
    font-family: var(--sans);
    cursor: pointer;
    transition: border-color .14s, background .14s;
}
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
.btn-ghost:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }

.settings-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 4px; }
.settings-actions .btn-print { width: auto; }

/* keep-alive status injected by api.js */
#keep-alive-status { font-family: var(--mono); font-size: 11px; margin-top: 8px; }
#keep-alive-status.text-success { color: var(--green); }
#keep-alive-status.text-muted { color: var(--muted); }

/* ====== Preview panel ====== */
.preview-panel { position: sticky; top: 82px; }
.preview-container {
    position: relative;
    min-height: 340px;
    border-radius: 10px;
    background:
        radial-gradient(600px 320px at 50% 30%, var(--accent-soft), transparent 70%),
        var(--panel-2);
    border: 1px dashed var(--border);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 28px;
    overflow: auto;
}

.preview-text,
.preview-image,
.preview-qrcode,
.preview-label {
    background: var(--label-bg);
    color: var(--label-ink);
    border-radius: 6px;
    box-shadow: 0 18px 50px rgba(0, 0, 0, 0.28), 0 0 0 1px rgba(255, 255, 255, 0.06);
    max-width: 100%;
}
.preview-text {
    padding: 24px 28px;
    font-weight: 600;
    line-height: 1.25;
    word-break: break-word;
    /* Fill the label width so the chosen alignment (left/center/right) is
       actually visible — a shrink-to-fit box would always look centered. */
    width: 100%;
    box-sizing: border-box;
    text-align: left;
}
.preview-image { display: block; max-height: 60vh; object-fit: contain; padding: 6px; }
/* Server-rendered, true-to-print label preview. The stage carries the "label"
   look (white card, shadow) so the die-cut overlay can be laid out exactly on
   top of the image area; the image itself stays untouched and renders the exact
   1-bit print output crisply. When visible it sits on top of the client
   previews. */
.preview-stage {
    --preview-pad: 6px;
    position: relative;
    display: block;
    background: var(--label-bg);
    border-radius: 6px;
    box-shadow: 0 18px 50px rgba(0, 0, 0, 0.28), 0 0 0 1px rgba(255, 255, 255, 0.06);
    max-width: 100%;
    padding: var(--preview-pad);
}
.preview-server {
    display: block;
    max-width: 100%;
    max-height: 60vh;
    object-fit: contain;
    image-rendering: crisp-edges;
    image-rendering: pixelated;
}

/* ---- Round die-cut media (d12 / d24 / d58) ----
   The printer receives a square raster, but the label itself is punched out as
   a circle: only the inscribed circle survives the die-cut. The overlay marks
   the cut edge and veils + hatches everything outside it, so content escaping
   the circle stays visible while clearly reading as "gets trimmed off".
   The image below is never touched — this is presentation only. */
.preview-die-cut { display: none; }
.preview-stage.is-round {
    /* Square media: capping the width caps the height too, which keeps the
       overlay locked to the image box at any viewport size. */
    max-width: min(100%, 60vh);
}
.preview-stage.is-round .preview-server { max-height: none; }
.preview-stage.is-round .preview-die-cut {
    display: block;
    position: absolute;
    inset: var(--preview-pad);
    pointer-events: none;
}
.preview-stage.is-round .preview-die-cut::before,
.preview-stage.is-round .preview-die-cut::after {
    content: '';
    position: absolute;
    inset: 0;
}
/* Discarded area: hatch + veil everywhere, masked away inside the circle. */
.preview-stage.is-round .preview-die-cut::before {
    background:
        repeating-linear-gradient(45deg,
            transparent 0 5px,
            var(--diecut-hatch) 5px 6px),
        var(--diecut-veil);
    -webkit-mask-image: radial-gradient(circle closest-side at 50% 50%, transparent 99.5%, #000 100%);
    mask-image: radial-gradient(circle closest-side at 50% 50%, transparent 99.5%, #000 100%);
}
/* The die-cut edge itself (unmasked, so the full ring stays visible). */
.preview-stage.is-round .preview-die-cut::after {
    border: 1px dashed var(--diecut-edge);
    border-radius: 50%;
}

.preview-medium-hint {
    margin-top: 14px;
    max-width: 320px;
    text-align: center;
    line-height: 1.6;
}
.preview-qrcode { padding: 20px; display: flex; flex-direction: column; align-items: center; gap: 10px; }
.preview-qrcode img { display: block; max-width: 100%; height: auto; image-rendering: pixelated; }
.preview-qrcode .qr-text { font-weight: 600; line-height: 1.2; word-break: break-word; }
.preview-label {
    padding: 22px 26px;
    display: flex;
    align-items: center;
    gap: 20px;
}
.preview-label .label-text { flex: 1; font-weight: 600; line-height: 1.25; word-break: break-word; }
.preview-label .label-qr img { display: block; max-width: 200px; height: auto; image-rendering: pixelated; }
.preview-label.qr-left { flex-direction: row-reverse; }

.pdf-preview { width: 100%; max-height: 70vh; overflow-y: auto; }
.pdf-preview img,
.pdf-preview canvas {
    max-width: 100%;
    border-radius: 6px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.22), 0 0 0 1px rgba(255, 255, 255, 0.06);
    background: #fff;
}

.preview-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    color: var(--muted-dim);
    text-align: center;
}
.preview-placeholder i { font-size: 40px; opacity: .7; }
.preview-placeholder p { margin: 0; font-size: 13px; max-width: 240px; }

/* ====== Bootstrap overrides: modal ====== */
.modal-content {
    background: var(--panel);
    color: var(--ink);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}
.modal-header, .modal-footer { border-color: var(--border-soft); }
.modal-title { font-size: 15px; font-weight: 600; }
body.dark-mode .modal-header .btn-close { filter: invert(1) grayscale(1) brightness(1.6); }

.btn.btn-secondary {
    background: var(--panel-2);
    border: 1px solid var(--border);
    color: var(--ink);
}
.btn.btn-secondary:hover { border-color: var(--accent); color: var(--accent); }

/* ---- The destructive confirmation ---- */
/* Severity is carried by the icon and by the button that does the damage, not
   by a coloured bar or a shouting panel — the same division the notifications
   use. --red rather than --warn: --warn means "this worked, but not the way you
   asked", while this is a question about destroying something. */
.confirm-body { display: flex; align-items: flex-start; gap: 12px; }
.confirm-body p { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--ink); }
.confirm-icon { flex: 0 0 auto; margin-top: 1px; font-size: 19px; color: var(--red); }
.confirm-icon[hidden] { display: none; }

/* Tinted rather than filled, so the label keeps its contrast in both themes —
   --red is a bright #F87171 on dark panels, which white text does not survive. */
.btn.btn-danger {
    background: color-mix(in srgb, var(--red) 12%, var(--panel));
    border: 1px solid color-mix(in srgb, var(--red) 55%, var(--border));
    color: var(--red);
    font-weight: 600;
}
.btn.btn-danger:hover {
    background: color-mix(in srgb, var(--red) 20%, var(--panel));
    border-color: var(--red);
    color: var(--red);
}
.btn.btn-danger:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 22%, transparent);
}
/* The way out must not itself look like a hazard: it stays the calm one, and it
   is the button that holds the focus when the question opens. */
#confirmModal.is-danger .btn-secondary:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--accent-soft);
    border-color: var(--accent);
}

/* alerts inside status modal */
.alert { border-radius: 9px; border: 1px solid var(--border); }
.alert-success { background: color-mix(in srgb, var(--green) 12%, var(--panel)); border-color: color-mix(in srgb, var(--green) 35%, var(--border)); color: var(--ink); }
.alert-warning { background: color-mix(in srgb, var(--warn) 12%, var(--panel)); border-color: color-mix(in srgb, var(--warn) 35%, var(--border)); color: var(--ink); }
.alert-danger  { background: color-mix(in srgb, var(--red) 12%, var(--panel)); border-color: color-mix(in srgb, var(--red) 35%, var(--border)); color: var(--ink); }
.spinner-border.text-primary { color: var(--accent) !important; }

/* ====== Notifications ====== */
.notifications {
    position: fixed;
    bottom: 20px;
    right: 20px;
    z-index: 1100;
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-width: 360px;
}
/* One card, at one weight, whatever it is saying. Severity used to be a coloured
   bar down the left edge; it is now carried by the icon that was already there
   and by the wording, so a notification never shouts with its frame. */
.notification {
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 12px 14px;
    box-shadow: var(--shadow);
    color: var(--ink);
    font-size: 13px;
    animation: notif-in .2s ease;
}
@keyframes notif-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
/* The one coloured thing in the card. Kept small and never a background, so a
   stack of notifications still reads as a stack of the same object. */
.notification .notification-icon { flex: 0 0 auto; color: var(--accent); font-size: 14px; }
.notification.success .notification-icon { color: var(--green); }
.notification.error .notification-icon { color: var(--red); }
.notification.warning .notification-icon { color: var(--warn); }
.notification .notification-close {
    margin-left: auto;
    background: none;
    border: none;
    color: var(--muted);
    cursor: pointer;
    font-size: 16px;
    line-height: 1;
}
.notification .notification-close:hover { color: var(--ink); }

/* ====== Sidebar queue badge ====== */
.queue-badge {
    margin-left: auto;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--accent);
    color: #FFFFFF;
    font-family: var(--mono);
    font-size: 10px;
    font-weight: 600;
    line-height: 18px;
    text-align: center;
    flex: none;
}
body.dark-mode .queue-badge { color: #06222B; }
.queue-badge[hidden] { display: none; }

/* ====== Print queue panel ====== */
.queue-head-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.btn-ghost.btn-sm {
    padding: 6px 12px;
    font-size: 12px;
    border-radius: 8px;
}

/* A ghost button the interface offers but cannot carry out for this row --
   today only "Open" on a job with no compose form behind it. It keeps its place
   in the row so the action bar does not change shape from job to job, and it
   stays clickable (not `disabled`) so the tooltip appears and a press can say
   why; the pointer is the one that means "this will not go anywhere". */
.btn-ghost[aria-disabled="true"] {
    opacity: .45;
    cursor: not-allowed;
}
.btn-ghost[aria-disabled="true"]:hover {
    border-color: var(--border);
    color: var(--ink);
}

/* Danger-accented ghost buttons (Stop / Clear all) */
.btn-ghost.btn-danger-ghost:hover,
.btn-ghost.btn-danger-ghost:focus-visible {
    border-color: var(--red);
    color: var(--red);
    box-shadow: none;
}
.btn-ghost.btn-danger-ghost:focus-visible {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 18%, transparent);
}

/* "Queue paused" badge shown in the panel header when the queue is halted */
.queue-paused-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 11px;
    font-weight: 600;
    color: var(--red);
    border: 1px solid color-mix(in srgb, var(--red) 40%, var(--border));
    background: color-mix(in srgb, var(--red) 12%, var(--panel));
    border-radius: 999px;
    padding: 3px 10px;
    white-space: nowrap;
}
.queue-paused-badge i { font-size: 12px; }

/* ====== Queue: what the job in hand is doing ======
   A job can sit in the queue for minutes with nothing wrong: relay power
   control switches the printer's mains on and then waits for it to boot and
   join the network. The status stays "queued" throughout -- which is what it
   is -- so this strip carries the reason, attached to the control bar above it
   and present only while there is something to say. */
.queue-activity {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 14px 18px 16px;
    border-bottom: 1px solid var(--border-soft);
    background: var(--accent-soft);
    overflow: hidden;
}
.queue-activity[hidden] { display: none; }

/* The rail. Four phases, laid out edge to edge, with the connector between two
   marks carrying the progress: a wait that names its place in a sequence reads
   as a process rather than as a stall. */
.queue-steps {
    display: flex;
    align-items: center;
    gap: 0;
    margin: 0;
    padding: 0;
    list-style: none;
}
.queue-step {
    display: flex;
    align-items: center;
    gap: 7px;
    min-width: 0;
    color: var(--muted);
    font-size: 11.5px;
    font-weight: 600;
    letter-spacing: 0.01em;
    white-space: nowrap;
}
/* The connector, drawn before every step but the first, so the rail is one line
   through all four marks and its filled part is the distance already covered.
   Every step after the first takes a share of the leftover width, which is what
   lets the connectors stretch the rail across the strip instead of leaving it
   huddled at the left. */
.queue-step + .queue-step { flex: 1 1 auto; }
.queue-step + .queue-step::before {
    content: "";
    flex: 1 1 auto;
    min-width: 14px;
    height: 2px;
    margin: 0 9px;
    border-radius: 2px;
    background: color-mix(in srgb, var(--accent) 18%, transparent);
}
.queue-step + .queue-step.is-done::before,
.queue-step + .queue-step.is-current::before {
    background: color-mix(in srgb, var(--accent) 55%, transparent);
}
/* A retry has still got this far, so the rail up to it stays filled; only the
   mark itself changes colour. */
.queue-step + .queue-step.is-retrying::before {
    background: color-mix(in srgb, var(--warn) 45%, transparent);
}
.queue-step-mark {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    border: 1.5px solid color-mix(in srgb, var(--accent) 22%, transparent);
    background: var(--panel);
    color: var(--muted);
    font-size: 11px;
}
.queue-step.is-done { color: var(--muted); }
.queue-step.is-done .queue-step-mark {
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
    color: var(--accent-strong);
}
.queue-step.is-current { color: var(--accent-strong); }
.queue-step.is-current .queue-step-mark {
    border-color: var(--accent);
    background: var(--accent);
    color: var(--panel);
    animation: queue-pulse 1.9s ease-out infinite;
}
/* A retry is the printing step going wrong rather than a fifth phase: same
   place on the rail, different colour, so "it is trying again" does not read as
   progress. */
.queue-step.is-retrying { color: var(--warn); }
.queue-step.is-retrying .queue-step-mark {
    border-color: var(--warn);
    background: var(--warn);
    color: var(--panel);
    animation: none;
}
/* Names collapse before the rail does: the marks and the current phase's own
   name are what carry it on a narrow screen. */
@media (max-width: 720px) {
    .queue-step .queue-step-name { display: none; }
    .queue-step.is-current .queue-step-name { display: inline; }
}

.queue-activity-line {
    display: flex;
    align-items: flex-start;
    gap: 10px;
}
/* The server's own sentence, shown as written. It runs to a line and a half, so
   it is prose in the sans face rather than a mono one-liner, and the measure is
   capped so it stays readable on a wide screen. */
.queue-activity-message {
    flex: 1 1 auto;
    min-width: 0;
    max-width: 78ch;
    margin: 0;
    font-size: 13px;
    line-height: 1.45;
    color: var(--ink);
}
.queue-activity-elapsed {
    flex: none;
    padding-top: 1px;
    font-family: var(--mono);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    color: var(--muted);
}
.queue-activity-elapsed[hidden] { display: none; }
/* Indeterminate on purpose: nothing here knows how long the wait will be, and a
   bar that filled at a guessed rate would be inventing a promise. It only says
   that something is still running -- which over four minutes is the thing a
   static line fails to say. */
.queue-activity-track {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: 2px;
    background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.queue-activity-sweep {
    position: absolute;
    top: 0; bottom: 0; left: 0;
    width: 32%;
    background: linear-gradient(90deg, transparent, var(--accent), transparent);
    animation: queue-sweep 2.2s ease-in-out infinite;
}
@keyframes queue-sweep {
    from { transform: translateX(-120%); }
    to   { transform: translateX(420%); }
}

/* The row the strip is talking about. Marked so the sentence above has an
   owner; the rows behind it are ordinary waiting jobs and keep looking like it. */
.queue-item.is-active {
    border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
    background: color-mix(in srgb, var(--accent) 5%, var(--panel-2));
}
.queue-phase {
    display: flex;
    align-items: center;
    gap: 7px;
    margin-top: 8px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--accent-strong);
}
.queue-phase-dot {
    flex: none;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--accent);
    animation: queue-pulse 1.9s ease-out infinite;
}
@keyframes queue-pulse {
    0%   { box-shadow: 0 0 0 0 var(--accent-glow); }
    70%  { box-shadow: 0 0 0 6px transparent; }
    100% { box-shadow: 0 0 0 0 transparent; }
}

@media (prefers-reduced-motion: reduce) {
    .queue-activity-sweep { animation: none; width: 100%; opacity: .45; }
    .queue-phase-dot { animation: none; }
    .queue-step.is-current .queue-step-mark { animation: none; }
}

.queue-list { display: flex; flex-direction: column; gap: 8px; }

.queue-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 40px 20px;
    color: var(--muted-dim);
    text-align: center;
}
.queue-empty i { font-size: 34px; opacity: .7; }
.queue-empty p { margin: 0; font-size: 13px; }

.queue-item {
    border: 1px solid var(--border-soft);
    background: var(--panel-2);
    border-radius: 10px;
    padding: 12px 14px;
}
.queue-item-main {
    display: grid;
    grid-template-columns: auto auto 1fr auto auto;
    align-items: center;
    gap: 12px;
}

.queue-status {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 9px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 600;
    white-space: nowrap;
    border: 1px solid var(--border);
    color: var(--muted);
    background: var(--panel);
}
.queue-status.queued { color: var(--muted); border-color: var(--border); }
.queue-status.printing {
    color: var(--accent-strong);
    border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
    background: var(--accent-soft);
}
.queue-status.done {
    color: var(--green);
    border-color: color-mix(in srgb, var(--green) 40%, var(--border));
    background: color-mix(in srgb, var(--green) 12%, var(--panel));
}
.queue-status.failed {
    color: var(--red);
    border-color: color-mix(in srgb, var(--red) 40%, var(--border));
    background: color-mix(in srgb, var(--red) 12%, var(--panel));
}
.queue-status.cancelled { color: var(--muted-dim); border-color: var(--border-soft); opacity: .85; }
.queue-status .spinner-border-sm { width: 0.7rem; height: 0.7rem; border-width: 2px; }

.queue-type {
    font-family: var(--mono);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    border: 1px solid var(--border);
    border-radius: 5px;
    padding: 2px 7px;
    white-space: nowrap;
}
.queue-label {
    font-size: 13px;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.queue-time {
    font-family: var(--mono);
    font-size: 11px;
    color: var(--muted-dim);
    white-space: nowrap;
}
.queue-actions { display: flex; justify-content: flex-end; gap: 6px; flex-wrap: wrap; }
.queue-cancel,
.queue-reprint,
.queue-open,
.queue-delete { padding: 5px 11px; font-size: 12px; border-radius: 8px; white-space: nowrap; }
.queue-reprint:hover,
.queue-reprint:focus-visible {
    border-color: var(--accent);
    color: var(--accent);
}
.queue-open i,
.queue-reprint i { font-size: 12px; }
.queue-delete { padding: 5px 9px; }
.queue-delete i { font-size: 12px; }
.queue-delete:hover,
.queue-delete:focus-visible {
    border-color: var(--red);
    color: var(--red);
    box-shadow: none;
}

/* A failed job's reason. The gate's failure is a paragraph -- it names how long
   it waited, how many times it checked and over what period, and what the three
   possible causes are -- so it is set as prose and shown whole: 11px mono red
   turned that into a wall, and clamping it would throw away the half that says
   what to do about it. */
.queue-error {
    display: flex;
    gap: 8px;
    margin-top: 10px;
    padding: 9px 11px;
    border: 1px solid color-mix(in srgb, var(--red) 30%, var(--border));
    border-radius: 8px;
    background: color-mix(in srgb, var(--red) 8%, var(--panel));
    font-size: 12px;
    line-height: 1.5;
    color: var(--ink);
}
.queue-error i {
    flex: none;
    font-size: 13px;
    line-height: 1.5;
    color: var(--red);
}
/* Capped measure so a long reason does not run the full width of a desktop
   panel, and break-word so the webhook URL inside it wraps instead of pushing
   the row sideways. */
.queue-error span {
    min-width: 0;
    max-width: 78ch;
    overflow-wrap: break-word;
}

/* ====== Scrollbar ====== */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 6px; border: 2px solid var(--bg); }
::-webkit-scrollbar-thumb:hover { background: var(--muted-dim); }

/* ====== Responsive ====== */
@media (max-width: 1080px) {
    .work-grid { grid-template-columns: 1fr; }
    .preview-panel { position: static; }
}

@media (max-width: 768px) {
    .shell { grid-template-columns: 1fr; grid-template-rows: 60px 1fr; }
    .topbar { grid-column: 1; }
    .main { grid-column: 1; padding: 16px; }

    /* iOS auto-zooms a focused field whose font-size is < 16px. Bumping the
       form controls to 16px on phones prevents the zoom (the reliable fix,
       since iOS ignores maximum-scale). */
    textarea,
    input[type=text],
    input[type=number],
    input[type=file],
    select {
        font-size: 16px;
    }

    .hamburger { display: grid; place-items: center; }

    /* sidebar becomes off-canvas drawer */
    .rail {
        position: fixed;
        top: 0; left: 0; bottom: 0;
        width: 264px;
        z-index: 100;
        transform: translateX(-100%);
        transition: transform .22s ease;
        box-shadow: 0 0 40px rgba(0, 0, 0, 0.25);
    }
    .rail.open { transform: translateX(0); }
    .rail-inner { height: 100%; }

    .grid-2, .grid-3 { grid-template-columns: 1fr; }
    .work-grid { gap: 16px; }
    .settings-actions { flex-direction: column; }
    .settings-actions .btn-print, .settings-actions .btn-ghost { width: 100%; }
    .notifications { left: 16px; right: 16px; max-width: none; bottom: 16px; }

    /* Queue: stack each job's fields so long labels wrap instead of overflow,
       and give the Cancel button a finger-friendly hit area. */
    .queue-item-main {
        grid-template-columns: auto 1fr;
        grid-template-areas:
            "status type"
            "label  label"
            "time   actions";
        gap: 8px 10px;
    }
    .queue-status { grid-area: status; justify-self: start; }
    .queue-type   { grid-area: type; justify-self: start; }
    .queue-label  { grid-area: label; white-space: normal; }
    .queue-time   { grid-area: time; align-self: center; }
    .queue-actions { grid-area: actions; }
    .queue-cancel,
    .queue-reprint,
    .queue-open { padding: 9px 16px; font-size: 13px; }

    /* Tighter gutters on a phone. The icon and the elapsed time stay pinned to
       the ends and the message wraps between them, which is the only part long
       enough to need the room. */
    .queue-activity { padding: 10px 14px 12px; gap: 8px; }
    .queue-activity-message { font-size: 12.5px; }
}

/* Phones: the top bar is too narrow for the title subtitle + full-text status
   pills (they wrap awkwardly). Drop the subtitle, keep the title on one line,
   and collapse both pills to round, icon-only buttons (power = keep-alive,
   printer = status) coloured by state. */
@media (max-width: 600px) {
    .topbar { padding: 0 14px; }
    .title h1 { white-space: nowrap; }
    .title p { display: none; }
    .status-cluster { gap: 8px; }
    .pill {
        width: 36px; height: 36px;
        padding: 0;
        justify-content: center;
        border-radius: 50%;
        flex: none;
    }
    .pill .dot,
    .pill span:not(.dot) { display: none; }
    .pill .pill-icon {
        display: inline-flex;
        font-size: 15px;
        color: var(--muted);
    }
    .pill.online .pill-icon,
    .pill.ka-active .pill-icon,
    .pill.media-ok .pill-icon { color: var(--green); }
    .pill.offline .pill-icon,
    .pill.media-warn .pill-icon { color: var(--red); }
    /* Three round pills fit; the medium's text is carried by its tooltip and
       by the list it opens, so nothing is lost by collapsing it too. */
    .pill-medium,
    .pill-medium.is-auto { max-width: none; }
    /* The word does not fit in a 36 px circle, but the fact that the mode is on
       still has to be on screen, so the same chip becomes a dot on the rim.
       font-size:0 drops the text without hiding the element from the tooltip
       and without a second element to keep in step. The selector carries an
       element name because the rule above that hides every span in a pill
       carries one too, and it would otherwise win. */
    .pill span.pill-auto {
        display: block;
        position: absolute;
        top: 2px; right: 2px;
        width: 9px; height: 9px;
        padding: 0;
        font-size: 0;
        letter-spacing: 0;
        background: var(--accent);
        border-color: var(--panel);
    }
    .pill span.pill-auto[hidden] { display: none; }
}

/* ===================== Demo mode banner ===================== */
/* Thin, dismissible notice shown only when the static demo build is active. */
.demo-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin: 0 0 18px;
    padding: 9px 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--accent-soft);
    color: var(--ink);
    font-size: 13px;
    font-weight: 500;
}

.demo-banner-text {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

.demo-banner-text i {
    color: var(--accent);
}

.demo-banner-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border: none;
    border-radius: 8px;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}

.demo-banner-close:hover {
    background: var(--panel-2);
    color: var(--ink);
}

/* Inline notice shown inside a demo-limited panel (e.g. PDF needs the backend). */
.demo-notice {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 16px;
    padding: 10px 14px;
    border: 1px dashed var(--border);
    border-radius: var(--radius);
    background: var(--panel-2);
    color: var(--muted);
    font-size: 13px;
}
.demo-notice i { color: var(--accent); }

/* ===================== Label type picker ===================== */
/* A search-first replacement for the raw label dropdown. The native
   <select id="label-size"> stays in the DOM as the source of truth and is only
   taken out of sight (and out of the tab order) once the picker is live. */

.lp-native {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.label-picker { position: relative; }

/* ---- Closed state ---- */
/* One line, and the same box as the selects around it (padding and radius
   match), so the closed control reads as a form field rather than as a card.
   min-width:0 lets it be narrower than its text, which is what makes the
   ellipsis below able to bite. */
.lp-trigger {
    width: 100%;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 8px;
    text-align: left;
    background: var(--panel-2);
    border: 1px solid var(--border);
    color: var(--ink);
    border-radius: 9px;
    padding: 10px 12px;
    font-family: var(--sans);
    cursor: pointer;
    transition: border-color .14s, box-shadow .14s;
}
.lp-trigger:hover { border-color: var(--muted-dim); }
.lp-trigger:focus-visible,
.label-picker.is-open .lp-trigger {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-glow);
}

/* The name is what is scanned for, so it takes the room and is the only part
   allowed to shrink; the full name stays in the trigger's tooltip. */
.lp-trigger-name {
    flex: 0 1 auto;
    min-width: 0;
    font-size: 13px;
    font-weight: 600;
    color: var(--ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* "DK-22205 +11": the code to reorder by, and how many others share this
   identifier. Never shrinks - a half-printed product code is worse than none. */
.lp-trigger-meta {
    flex: 0 0 auto;
    margin-left: auto;
    font-family: var(--mono);
    font-size: 10.5px;
    color: var(--muted-dim);
    white-space: nowrap;
}
.lp-trigger-meta[hidden] { display: none; }
.lp-chevron { flex: 0 0 auto; color: var(--muted-dim); font-size: 13px; }

/* Copy-the-product-code button under the picker. The code itself is monospaced
   inside an otherwise normal button label, so it reads as the literal string
   that lands on the clipboard. */
.lp-copy { margin-top: 8px; font-family: var(--mono); font-size: 11px; }
.lp-copy.is-copied { border-color: var(--green); color: var(--green); }

/* ---- Popover ---- */
.lp-pop {
    position: absolute;
    z-index: 60;
    top: calc(100% + 6px);
    left: 0; right: 0;
    display: flex;
    flex-direction: column;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    overflow: hidden;
}
.lp-pop[hidden] { display: none; }

/* The Settings field is one half of a two-column row, which is narrower than
   the entries want. Wherever that row is actually two columns, the list is let
   out into the other half - it is a popover, it sits over the page for as long
   as it is open, and the panel has the room. Below the one-column breakpoint it
   stays exactly as wide as the field. */
@media (min-width: 769px) {
    .label-picker .lp-pop { right: auto; width: max(100%, 420px); }
    /* The owned-media field is the right-hand half of its row, so its list
       spills the other way to stay inside the panel. */
    .owned-media .lp-pop { left: auto; right: 0; width: max(100%, 420px); }
}

/* The same popover, borrowed by the top bar's medium switcher. It hangs from
   the status cluster's right edge so it stays on screen at every width, and
   above the sticky top bar's own stacking context. */
.lp-pop--header {
    top: calc(100% + 10px);
    left: auto;
    right: 0;
    width: min(360px, calc(100vw - 28px));
}

.lp-search {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 9px 12px;
    background: var(--panel-2);
    border-bottom: 1px solid var(--border-soft);
}
.lp-search i { flex: 0 0 auto; color: var(--muted-dim); font-size: 13px; }
.lp-search input[type=text] {
    flex: 1 1 auto;
    width: 100%;
    background: transparent;
    border: 0;
    border-radius: 0;
    padding: 2px 0;
    font-size: 13px;
    color: var(--ink);
}
.lp-search input[type=text]:focus { border: 0; box-shadow: none; }

.lp-list {
    list-style: none;
    margin: 0;
    padding: 6px;
    max-height: min(52vh, 360px);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
}

.lp-grouphead {
    padding: 9px 10px 5px;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    font-weight: 600;
    color: var(--muted-dim);
}
.lp-list .lp-grouphead:first-child { padding-top: 4px; }

/* What the printer says is loaded, offered first. Marked with the accent so it
   reads as an answer rather than as just another heading - but it is only ever
   put at the top, never chosen. */
.lp-grouphead--loaded { color: var(--accent-strong); }
/* The user's own rolls, directly under the loaded one. A step brighter than an
   ordinary heading because it is a shortcut worth finding, but deliberately not
   the accent: the accent means "the printer said so", and this group is a
   statement by the user rather than a report from the device. Both themes carry
   --muted, so this follows the theme without a second rule. */
.lp-grouphead--mine { color: var(--muted); }
.lp-groupnote {
    padding: 0 10px 7px;
    font-size: 11px;
    line-height: 1.55;
    color: var(--muted);
}
/* Sits above the first heading rather than under one, so it needs the top
   padding the heading would otherwise have carried. */
.lp-groupnote--lead { padding: 6px 10px 9px; border-bottom: 1px solid var(--border-soft); margin-bottom: 4px; }
.lp-list .lp-groupnote--lead + .lp-grouphead { padding-top: 4px; }

/* ---- Ticking mode: the list as the inventory of what the user owns ---- */
/* Same rows, same search, same grouping. Only the meaning of a row changes, so
   the only visual difference is the tick box at its head. */
.lp-pop--own .lp-opt-head { justify-content: flex-start; gap: 8px; }
.lp-pop--own .lp-opt-codes { margin-left: auto; }
.lp-tick { flex: 0 0 auto; font-size: 13px; color: var(--muted-dim); }
.lp-opt--owned .lp-tick { color: var(--accent); }
/* A ticked row is a member of a list, not the app's current label type, so it
   must not borrow the ring that means "this is what gets printed". */
.lp-pop--own .lp-opt[aria-selected="true"] { box-shadow: none; background: var(--accent-soft); }
.lp-pop--own .lp-opt[aria-selected="true"] .lp-opt-codes { color: var(--muted-dim); }
.lp-pop--own .lp-opt.is-active { box-shadow: inset 0 0 0 1px var(--accent-dim); }
.lp-opt--loaded .lp-opt-head { justify-content: flex-start; gap: 8px; }
.lp-opt--loaded .lp-opt-codes { margin-left: auto; }
.lp-loaded-mark {
    flex: 0 0 auto;
    padding: 1px 7px;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
    background: var(--accent-soft);
    color: var(--accent-strong);
    font-family: var(--mono);
    font-size: 9.5px;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    white-space: nowrap;
}

.lp-opt {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px 10px;
    border-radius: 8px;
    cursor: pointer;
}
.lp-opt.is-active { background: var(--accent-soft); }
.lp-opt[aria-selected="true"] { box-shadow: inset 0 0 0 1px var(--accent-dim); }

.lp-opt-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
}
.lp-opt-name {
    font-size: 13px;
    font-weight: 600;
    color: var(--ink);
}
.lp-opt-codes {
    flex: 0 0 auto;
    font-family: var(--mono);
    font-size: 10.5px;
    color: var(--muted-dim);
    white-space: nowrap;
}
.lp-opt[aria-selected="true"] .lp-opt-codes { color: var(--accent-strong); }
/* Identifiers shared by several products list every code, wrapped, on a line
   of its own rather than losing the tail to an ellipsis. */
.lp-opt-codes--block {
    white-space: normal;
    margin-top: 3px;
    line-height: 1.6;
}
.lp-opt-desc {
    font-size: 11.5px;
    color: var(--muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.lp-flag {
    align-self: flex-start;
    margin-top: 3px;
    padding: 2px 7px;
    border-radius: 999px;
    font-size: 10px;
    letter-spacing: 0.01em;
    border: 1px solid var(--border);
    background: var(--panel-2);
    color: var(--muted);
}
.lp-empty {
    padding: 14px;
    font-size: 12px;
    line-height: 1.6;
    color: var(--muted);
    border-top: 1px solid var(--border-soft);
}
.lp-empty[hidden] { display: none; }

/* ---- The configured type versus the loaded one ---- */

/* Whether the label type is stored on the server. Quiet by default; the failed
   state is the only one that needs to be noticed. */
.media-save-state {
    margin-top: 8px;
    font-size: 11.5px;
    line-height: 1.55;
    color: var(--muted);
}
.media-save-state[hidden] { display: none; }
/* Under the two-column row rather than inside one half of it: that row already
   carries the gap, so do not stack a second one on top of it. */
.grid-2 + .media-save-state { margin-top: 0; }
.media-save-state.is-saved { color: var(--green); }
.media-save-state.is-failed { color: var(--red); }

/* The disagreement itself: the printer has one roll in it and the settings say
   another. Loud enough to catch the eye, but it never blocks a print. */
.media-mismatch {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin-top: 10px;
    padding: 11px 13px;
    border: 1px solid color-mix(in srgb, var(--red) 40%, var(--border));
    border-radius: var(--radius);
    background: color-mix(in srgb, var(--red) 7%, var(--panel-2));
}
.media-mismatch-icon { flex: 0 0 auto; margin-top: 1px; color: var(--red); font-size: 14px; }
.media-mismatch-body { flex: 1 1 auto; min-width: 0; }
.media-mismatch-text {
    margin: 0;
    font-size: 12.5px;
    line-height: 1.55;
    color: var(--ink);
}
.media-mismatch-reason {
    margin: 5px 0 0;
    font-size: 11.5px;
    line-height: 1.55;
    color: var(--muted);
}
.media-mismatch-reason[hidden] { display: none; }
.media-mismatch-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 9px; }
.media-mismatch-actions:empty { display: none; }
.media-mismatch-action { font-size: 11.5px; }
.media-mismatch-action:hover { border-color: var(--accent); color: var(--accent-strong); }

/* The same fact, one line, inside the printer status dialog. */
.media-status-line { font-size: 12.5px; }
.media-status-line.is-warn { color: var(--red); }

/* ---- The media the user owns ---- */
/* The control is the label picker again, opened in its ticking mode; what has
   been ticked is read off the chips below rather than by scrolling the list, so
   the list itself needs no separate summary. */
.owned-media { position: relative; }
/* The plus leads rather than trails: this button adds to a list, where the
   picker's chevron opens a choice. Same box either way, so the two fields in
   the row still line up. */
.owned-media-add { justify-content: flex-start; }
.owned-media-add .lp-trigger-name { font-weight: 500; color: var(--muted); }
.owned-media-add .lp-chevron { color: var(--muted-dim); font-size: 12px; }
.owned-media.is-open .lp-trigger {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-glow);
}
.lbl-count {
    display: inline-block;
    margin-left: 6px;
    padding: 0 6px;
    border-radius: 999px;
    background: var(--accent-soft);
    color: var(--accent-strong);
    font-family: var(--mono);
    font-size: 10px;
}
.lbl-count[hidden] { display: none; }

.owned-chips {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 8px 0 0;
    padding: 0;
}
.owned-chips:empty { display: none; }
.owned-chip {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 3px 4px 3px 9px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--panel-2);
    font-size: 11.5px;
    color: var(--ink);
}
.owned-chip-name { min-width: 0; }
.owned-chip-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px; height: 18px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--muted-dim);
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
}
.owned-chip-remove:hover { color: var(--red); background: color-mix(in srgb, var(--red) 12%, transparent); }
.owned-chip-remove:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--accent-soft); color: var(--ink); }
/* The explanation is only worth the room while there is nothing to explain. */
#owned-media-empty[hidden] { display: none; }

@media (max-width: 768px) {
    /* Match the 16px rule that stops iOS from zooming into a focused field. */
    .lp-search input[type=text] { font-size: 16px; }
    .lp-opt-head { flex-wrap: wrap; gap: 2px 10px; }
    .lp-list { max-height: 60vh; }
    /* Finger-sized targets, and one full-width button per candidate so a long
       medium name is never clipped. */
    .media-mismatch-actions { flex-direction: column; align-items: stretch; }
    .media-mismatch-action { padding: 9px 14px; font-size: 13px; }
}

@media (max-width: 600px) {
    /* The header list is measured from the cluster, which sits inside the top
       bar's 14px padding, so it spans the viewport without touching either
       edge. Keep it short enough to leave the page visible behind it. */
    .lp-pop--header { width: calc(100vw - 28px); }
    .lp-pop--header .lp-list { max-height: 55vh; }

    /* On a phone the field is the full width of the panel and the code chip
       ends up floating far from the name it belongs to. Drop it and give the
       whole line to the name: the copy button directly underneath spells out
       the same leading code ("Copy DK-22205"), so nothing is lost. */
    .lp-trigger-meta { display: none; }
}

/* ===================== Print alignment calibration ===================== */
/* Two surfaces: a compact summary inside Settings (which media are calibrated,
   and the way in), and the dialog that drives the print/measure/nudge loop.
   Both are built from the shared tokens, so they follow the theme. */

.cal-intro {
    margin: 0 0 12px;
    font-size: 12px;
    line-height: 1.6;
    color: var(--muted);
}

.cal-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 6px; }
.cal-list-note { margin-bottom: 12px; }

.cal-empty {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 14px;
    border: 1px dashed var(--border);
    border-radius: var(--radius);
    background: var(--panel-2);
    color: var(--muted);
    font-size: 12px;
}
.cal-empty i { font-size: 15px; color: var(--muted-dim); }

/* One calibrated medium: the row itself opens the dialog, the trailing button
   resets that medium in a single click. */
.cal-item {
    display: flex;
    align-items: stretch;
    gap: 6px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel-2);
    overflow: hidden;
}
.cal-item.is-current { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent-soft); }

.cal-item-main {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
    padding: 10px 12px;
    border: 0;
    background: transparent;
    color: var(--ink);
    text-align: left;
    cursor: pointer;
}
.cal-item-main:hover { background: var(--accent-soft); }
.cal-item-main:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent-soft); }

.cal-item-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.cal-item-name { font-size: 13px; font-weight: 500; }
.cal-item-id { font-size: 11px; color: var(--muted-dim); }
.cal-item-current {
    font-family: var(--mono);
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    padding: 2px 6px;
    border-radius: 999px;
    background: var(--accent-soft);
    color: var(--accent-strong);
}
/* A calibration can name a shift and a size correction at once, so the value
   wraps rather than pushing the row out on a phone. */
.cal-item-value { font-size: 11px; color: var(--accent-strong); text-align: right; }

.cal-item-clear {
    flex: 0 0 auto;
    width: 38px;
    display: grid;
    place-items: center;
    border: 0;
    border-left: 1px solid var(--border);
    background: transparent;
    color: var(--muted-dim);
    cursor: pointer;
}
.cal-item-clear i { font-size: 11px; }
.cal-item-clear:hover { color: var(--red); background: color-mix(in srgb, var(--red) 10%, transparent); }
.cal-item-clear:focus-visible { outline: none; color: var(--red); box-shadow: inset 0 0 0 2px var(--accent-soft); }

.cal-open { width: auto; }

/* ---- Dialog ---- */

.cal-medium {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel-2);
    margin-bottom: 14px;
}
.cal-medium-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.cal-medium-name { font-size: 14px; font-weight: 600; }
.cal-medium-codes { font-size: 10px; color: var(--muted-dim); }
.cal-medium-state {
    font-family: var(--mono);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 4px 9px;
    border-radius: 999px;
    border: 1px solid var(--border);
    color: var(--muted);
    white-space: nowrap;
}
.cal-medium-state.is-set {
    border-color: var(--accent);
    background: var(--accent-soft);
    color: var(--accent-strong);
    text-transform: none;
    letter-spacing: 0;
    /* "calibrated: 0.5 mm left, 1 mm down, 2 % smaller" is too long to hold on
       one line on a phone. */
    white-space: normal;
    text-align: right;
}

.cal-medium-elsewhere { margin-top: -6px; margin-bottom: 12px; }
.cal-medium-elsewhere[hidden] { display: none; }

.cal-copy {
    margin: 0 0 16px;
    font-size: 12px;
    line-height: 1.6;
    color: var(--muted);
}

.cal-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 18px;
    align-items: start;
}

.cal-target-stage {
    display: grid;
    place-items: center;
    min-height: 190px;
    padding: 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel-2);
}
/* The stage reuses .preview-stage (and .is-round) from the live preview, so a
   round die-cut target is shown with the same cut treatment. */
.cal-target-stage .preview-stage { max-width: 100%; }
/* The dialog is short, so the target is capped tighter than the 60vh the main
   preview allows. Round media is square, so capping the width caps both. */
.cal-target-stage .preview-server { max-height: 240px; }
.cal-target-stage .preview-stage.is-round { max-width: min(100%, 240px); }

.cal-preview-empty {
    max-width: 260px;
    font-size: 11px;
    line-height: 1.6;
    text-align: center;
    color: var(--muted-dim);
}
.cal-preview-empty.d-none { display: none; }

.cal-controls { display: flex; flex-direction: column; gap: 14px; }

.cal-step-row { display: flex; align-items: center; justify-content: space-between; gap: 8px 10px; flex-wrap: wrap; }
.cal-step-row .lbl { margin-bottom: 0; }

.cal-steps { display: inline-flex; border: 1px solid var(--border); border-radius: 9px; overflow: hidden; }
.cal-step {
    padding: 7px 12px;
    border: 0;
    border-left: 1px solid var(--border);
    background: var(--panel);
    color: var(--muted);
    font-family: var(--mono);
    font-size: 11px;
    cursor: pointer;
}
.cal-step:first-child { border-left: 0; }
.cal-step:hover { color: var(--ink); }
.cal-step.is-active { background: var(--accent-soft); color: var(--accent-strong); font-weight: 600; }
.cal-step:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent-soft); }

/* Direction pad: the primary control. The user picks a direction, never a sign. */
.cal-pad {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-areas:
        ".    up    ."
        "left reset right"
        ".    down  .";
    gap: 6px;
    max-width: 260px;
    margin: 0 auto;
}
.cal-pad-up    { grid-area: up; }
.cal-pad-left  { grid-area: left; }
.cal-pad-reset { grid-area: reset; }
.cal-pad-right { grid-area: right; }
.cal-pad-down  { grid-area: down; }

.cal-pad-btn {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    /* Comfortable touch target on phones. */
    min-height: 52px;
    padding: 6px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--panel-2);
    color: var(--ink);
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}
.cal-pad-btn i { font-size: 16px; }
.cal-pad-btn span {
    font-family: var(--mono);
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--muted-dim);
}
.cal-pad-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.cal-pad-btn:hover span { color: var(--accent); }
.cal-pad-btn:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.cal-pad-btn:active { transform: translateY(1px); }
.cal-pad-reset { color: var(--muted); }
.cal-pad-reset i { font-size: 14px; }

/* Size correction: the same "pick a direction, never a sign" treatment as the
   pad, one axis instead of two. The percentage is what the user works in; the
   multiplier that is actually stored sits next to it, read-only. */
.cal-size {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 12px;
    border: 1px solid var(--border-soft);
    border-radius: var(--radius);
    background: var(--panel-2);
}

.cal-size-row { display: flex; align-items: stretch; gap: 8px; }

.cal-size-btn {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    /* Comfortable touch target, matching the direction pad. */
    min-width: 62px;
    min-height: 52px;
    padding: 6px 8px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--panel);
    color: var(--ink);
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}
.cal-size-btn i { font-size: 15px; }
.cal-size-btn span {
    font-family: var(--mono);
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--muted-dim);
}
.cal-size-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.cal-size-btn:hover span { color: var(--accent); }
.cal-size-btn:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.cal-size-btn:active { transform: translateY(1px); }

.cal-size-value {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    min-width: 0;
}
.cal-size-input { position: relative; width: 100%; }
.cal-size-input input[type=number] { padding-right: 26px; text-align: center; }
.cal-size-unit {
    position: absolute;
    top: 50%;
    right: 10px;
    transform: translateY(-50%);
    font-family: var(--mono);
    font-size: 11px;
    color: var(--muted-dim);
    pointer-events: none;
}
/* The raw factor, so the percentage can always be checked against what is
   stored. Muted until it actually corrects something. */
.cal-size-factor { font-size: 10px; color: var(--muted-dim); }
.cal-size-factor.is-set { color: var(--accent-strong); }

.cal-size-clear {
    flex: 0 0 auto;
    width: 38px;
    display: grid;
    place-items: center;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: transparent;
    color: var(--muted-dim);
    cursor: pointer;
}
.cal-size-clear i { font-size: 12px; }
.cal-size-clear:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.cal-size-clear:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.cal-size-clear:disabled { opacity: .4; cursor: default; }

.cal-size-hint { margin-top: 0; }

.cal-readout {
    margin: 0;
    text-align: center;
    font-size: 13px;
    color: var(--ink);
}
.cal-readout strong { color: var(--accent-strong); }
.cal-readout-zero { color: var(--muted); }

.cal-numbers { margin-bottom: 0; }

.cal-history {
    margin: 0;
    padding: 10px 12px;
    border: 1px solid var(--border-soft);
    border-radius: var(--radius);
    background: var(--panel-2);
}
.cal-history-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.cal-history-row + .cal-history-row { margin-top: 6px; }
.cal-history-row.d-none { display: none; }
.cal-history dt {
    font-family: var(--mono);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--muted-dim);
    font-weight: 500;
}
.cal-history dd { margin: 0; font-size: 12px; color: var(--muted); text-align: right; }

.cal-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.cal-actions .btn-print { width: auto; }

/* Same rule as the notifications it echoes: one card, and the icon says how bad
   it is. This line used to have no icon at all and leaned entirely on a coloured
   left edge, so it gains one rather than losing its severity with the bar. */
.cal-status {
    display: flex;
    align-items: flex-start;
    gap: 9px;
    margin-top: 14px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: 9px;
    background: var(--panel-2);
    font-size: 12px;
    line-height: 1.6;
    color: var(--muted);
}
.cal-status.d-none { display: none; }
.cal-status .cal-status-icon { flex: 0 0 auto; margin-top: 2px; color: var(--accent); font-size: 13px; }
.cal-status--success { color: var(--ink); }
.cal-status--success .cal-status-icon { color: var(--green); }
.cal-status--warning { color: var(--ink); }
.cal-status--warning .cal-status-icon { color: var(--warn); }
.cal-status--error { color: var(--ink); }
.cal-status--error .cal-status-icon { color: var(--red); }

.cal-footer { justify-content: space-between; gap: 8px; }
.cal-footer-remove { margin-right: auto; }
.cal-footer-remove:disabled { opacity: .5; }

@media (max-width: 768px) {
    .cal-grid { grid-template-columns: 1fr; }
    .cal-target-stage { min-height: 150px; }
    /* Match the 16px rule that stops iOS from zooming into a focused field. */
    .cal-numbers input[type=number],
    .cal-size-input input[type=number] { font-size: 16px; }
    .cal-pad-btn { min-height: 58px; }
    .cal-size-btn { min-width: 56px; min-height: 58px; }
    .cal-size-btn span { font-size: 8px; }
    .cal-footer { flex-direction: column-reverse; align-items: stretch; }
    .cal-footer .btn { width: 100%; }
    .cal-footer-remove { margin-right: 0; }
    .cal-actions { flex-direction: column; }
    .cal-actions .btn-print, .cal-actions .btn-ghost { width: 100%; }
}

/* ===================== Relay power control ===================== */
/* All of it lives inline in Settings, under Keep Alive, and unfolds from the
   master switch: with the feature off there is an introduction and that switch
   and nothing else, so a reader who does not use this pays almost nothing for
   it. Switched on, the configuration appears in place. */

.relay-intro {
    font-size: 12.5px;
    color: var(--muted);
    line-height: 1.55;
    margin: 0 0 12px;
}
.relay-intro strong { color: var(--ink); font-weight: 600; }

.relay-summary {
    display: flex;
    flex-direction: column;
    gap: 7px;
    padding: 11px 13px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--panel-2);
    margin-bottom: 10px;
}
.relay-summary-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.relay-summary-detail { font-size: 12.5px; color: var(--muted); min-width: 0; }
.relay-summary-row {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    font-size: 12.5px;
    color: var(--muted);
    line-height: 1.5;
}
.relay-summary-row i { flex: 0 0 auto; margin-top: 2px; font-size: 12px; color: var(--muted-dim); }
.relay-summary-row--error { color: var(--ink); }

/* State chip. Three states, three readings: not switching anything, switching
   the printer on, and armed to cut mains power — which is the one worth a
   colour of its own. */
.relay-summary-state {
    font-family: var(--mono);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    padding: 3px 8px;
    border-radius: 999px;
    border: 1px solid var(--border);
    color: var(--muted-dim);
    white-space: nowrap;
    flex: 0 0 auto;
}
.relay-state--on {
    color: var(--green);
    border-color: color-mix(in srgb, var(--green) 45%, var(--border));
    background: color-mix(in srgb, var(--green) 10%, transparent);
}
.relay-state--armed {
    color: var(--warn);
    border-color: color-mix(in srgb, var(--warn) 45%, var(--border));
    background: color-mix(in srgb, var(--warn) 10%, transparent);
}

.relay-icon-ok { color: var(--green) !important; }
.relay-icon-error { color: var(--red) !important; }
.relay-countdown { font-variant-numeric: tabular-nums; color: var(--accent-strong); font-weight: 600; }

.relay-summary[hidden] { display: none; }

/* ---- The configuration, once it is switched on ---- */
/* Its own captions, spaced as a block inside a form rather than as the top of a
   panel, so the three parts read apart without a rule between each of them. */
.relay-cap { margin-top: 22px; }

/* The turn-off fields say nothing while turn_off is never sent, so they are
   dimmed rather than removed: the margin is still worth reading as the value
   that would apply. */
.relay-inert { opacity: .5; }

/* ---- The warning ---- */
/* Full width, at the setting it belongs to, in the server's own words. It is
   the only element in the app that keeps a tinted panel: a line of prose in the
   body colour is exactly what gets skipped, and this one must not be. */
.relay-warning {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin-top: 10px;
    padding: 12px 14px;
    border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--border));
    border-radius: 10px;
    background: color-mix(in srgb, var(--warn) 10%, var(--panel));
}
.relay-warning[hidden] { display: none; }
.relay-warning-icon { flex: 0 0 auto; margin-top: 2px; color: var(--warn); font-size: 15px; }
.relay-warning-text {
    margin: 0;
    font-size: 12.5px;
    line-height: 1.6;
    color: var(--ink);
}
/* Nothing has been read from the server yet: same box, no alarm. */
.relay-warning.is-pending {
    border-color: var(--border);
    background: var(--panel-2);
}
.relay-warning.is-pending .relay-warning-icon { color: var(--muted-dim); }
.relay-warning.is-pending .relay-warning-text { color: var(--muted); }

/* ---- The timing chain ---- */
.relay-chain {
    list-style: none;
    margin: 0 0 8px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
}
.relay-chain-row {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 10px 13px;
    background: var(--panel);
}
.relay-chain-row + .relay-chain-row { border-top: 1px solid var(--border-soft); }
.relay-chain-time {
    flex: 0 0 auto;
    min-width: 46px;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    color: var(--accent-strong);
    font-weight: 600;
    padding-top: 1px;
}
.relay-chain-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.relay-chain-what { font-size: 12.5px; color: var(--ink); }
.relay-chain-why { font-size: 11px; color: var(--muted-dim); line-height: 1.5; }
.relay-chain-row.is-quiet .relay-chain-time { color: var(--muted-dim); }
.relay-chain-row.is-cut .relay-chain-time { color: var(--warn); }
.relay-chain-empty {
    padding: 12px 13px;
    font-size: 12.5px;
    color: var(--muted);
    background: var(--panel);
}

/* ---- The two webhooks, side by side ---- */
/* One column per action: what it is, where it posts, and the button that fires
   it. The columns are symmetric on purpose, but they are not equals: turn-on is
   what the feature needs to run at all, turn-off is a choice made on top of it,
   which is what the tags and the turn-off column's own switch carry.

   That difference is in the content, and it is the only difference the two are
   allowed to show. Each column is the same three parts in the same order —
   body, button, result — and the columns are stretched to a common height so
   the two buttons sit on one line however much configuration is above them. */
.relay-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 14px;
}
.relay-action {
    display: flex;
    flex-direction: column;
    padding: 14px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--panel);
    min-width: 0;
}
/* Takes the slack, so the foot below it is at the bottom of the card. */
.relay-action-body { flex: 1 1 auto; min-width: 0; }
.relay-action-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 6px;
}
.relay-action-name { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.relay-action-tag {
    font-family: var(--mono);
    font-size: 9px;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    padding: 2px 7px;
    border-radius: 999px;
    border: 1px solid var(--border);
    color: var(--muted-dim);
    white-space: nowrap;
}
.relay-action-tag--required {
    color: var(--accent);
    border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
.relay-action-copy {
    margin: 0 0 12px;
    font-size: 11.5px;
    line-height: 1.6;
    color: var(--muted);
}
/* The button, held off whatever the body above it ends with. */
.relay-action-foot { padding-top: 12px; }

/* Where a browser has subgrid, the alignment stops depending on the two cards
   being the same height: the three parts sit in three shared rows, so the
   buttons are on one line and so are the two result lines, even once one of
   them has been fired and the other has not. The flex rules above are the
   fallback for a browser without it, and they get the resting state right on
   their own. Row gaps come from the parts themselves here, so the tracks are
   set flush. */
@supports (grid-template-rows: subgrid) {
    .relay-actions {
        grid-template-rows: auto auto auto;
        row-gap: 0;
    }
    .relay-action {
        display: grid;
        grid-row: span 3;
        grid-template-rows: subgrid;
    }
}

/* ---- Where turn_off posts ---- */
/* A stated choice rather than an empty box to interpret: reusing the turn-on
   URL is the default and reads as one of two options, not as something left
   undone. */
.relay-choice { display: flex; flex-direction: column; gap: 6px; }
.relay-choice-option {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    font-size: 12px;
    line-height: 1.5;
    color: var(--ink);
    cursor: pointer;
}
.relay-choice-option input { margin-top: 2px; accent-color: var(--accent); cursor: pointer; }
.relay-choice-field { margin-top: 8px; }
.relay-choice-field[hidden] { display: none; }

/* ---- Firing a webhook by hand ---- */
.relay-fire-btn { width: 100%; }
.relay-fire-btn[disabled] { opacity: .5; cursor: not-allowed; }
.relay-fire-btn[disabled]:hover { border-color: var(--border); color: var(--ink); }
/* Cutting mains power is the one destructive thing this app can do, so the
   button that does it reads as such before it is pressed. */
.relay-fire-btn--cut:hover:not([disabled]),
.relay-fire-btn--cut:focus-visible:not([disabled]) {
    border-color: var(--warn);
    color: var(--warn);
    box-shadow: none;
}
.relay-fire-btn--cut:focus-visible:not([disabled]) {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--warn) 18%, transparent);
}
.relay-action > .relay-check {
    margin-top: 10px;
    padding: 8px 10px;
    font-size: 11.5px;
    line-height: 1.5;
}
.relay-url-note { margin-top: 12px; }
.relay-fire-note { margin-top: 10px; }

/* ---- The line that says what would be refused ---- */
/* Same shape as the calibration status line: no colour of its own, the icon
   carries the severity, exactly like the notifications. */
.relay-check {
    display: flex;
    align-items: flex-start;
    gap: 9px;
    margin-top: 14px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: 9px;
    background: var(--panel-2);
    font-size: 12.5px;
    line-height: 1.55;
    color: var(--ink);
}
.relay-check.d-none { display: none; }
.relay-check-icon { flex: 0 0 auto; margin-top: 2px; color: var(--accent); font-size: 13px; }
.relay-check--success .relay-check-icon { color: var(--green); }
.relay-check--warning .relay-check-icon { color: var(--warn); }
.relay-check--error .relay-check-icon { color: var(--red); }

/* ---- The outcome of a webhook fired by hand ---- */
/* One line of facts, and the server's own longer account of the same request
   folded under it: that text names the URL the field above already shows, but
   it also carries the caveat that the printer itself was not probed, which is
   worth keeping within reach rather than on screen. */
.relay-check-body { flex: 1 1 auto; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.relay-check-more {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 0;
    border: 0;
    background: none;
    font-family: var(--sans);
    font-size: 11px;
    color: var(--muted);
    cursor: pointer;
    transition: color .14s;
}
.relay-check-more:hover,
.relay-check-more:focus-visible { color: var(--accent); }
.relay-check-chevron { font-size: 9px; }
.relay-check-detail {
    margin: 0;
    font-size: 11px;
    line-height: 1.6;
    color: var(--muted);
    /* It quotes a URL, in a column half the width of the panel. */
    overflow-wrap: anywhere;
}
.relay-check-detail[hidden] { display: none; }

/* The constraint line sits between the master switch and the configuration it
   is about, so it needs the spacing of a block in a form. */
#relay-constraint { margin-top: 0; margin-bottom: 12px; }

@media (max-width: 768px) {
    .relay-chain-row { gap: 10px; }
    .relay-chain-time { min-width: 40px; }
    /* Stacked, there is no pair to line up, and the shared rows would only take
       the gap out from between the two cards. Back to the flow layout.
       Deliberately after the @supports block above, which it overrides. */
    .relay-actions {
        grid-template-columns: 1fr;
        grid-template-rows: none;
        row-gap: 14px;
    }
    .relay-action {
        display: flex;
        grid-row: auto;
    }
}
