/* ==========================================================================
   QRMask Panel — components and layout
   --------------------------------------------------------------------------
   Consumes tokens.css exclusively. Mobile-first: base styles target small
   screens and media queries enhance upward.

   THREE RULES THIS FILE ENFORCES

   1. State layers, not per-variant hover colours. Every interactive element has a
      ::before overlay tinted with currentColor at --state-hover / --state-press.
      A variant therefore cannot get its hover "wrong" - there is nothing to set.

   2. A strict 4px spacing grid. Only --s-1..--s-8 may be used for padding, gap
      and margin, so alignment is consistent by construction.

   3. Underlines belong to links, not to controls. `a:hover` underlines prose
      links only; every button-like anchor opts out via .btn / .nav-link, which
      carry text-decoration: none at a specificity that actually wins.
   ========================================================================== */

/* --- Reset --------------------------------------------------------------- */
*,
*::before,
*::after {
    box-sizing: border-box;
}

* {
    margin: 0;
}

html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
}

body {
    min-height: 100dvh;
    font-family: var(--font-sans);
    font-size: var(--fs-body);
    line-height: var(--lh-normal);
    color: var(--on-surface);
    background-color: var(--bg);
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    /* No ambient gradients: a neutral surface is the point of this theme. */
}

img,
svg {
    display: block;
    max-width: 100%;
}

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

kbd {
    font-family: var(--font-mono);
    font-size: 0.85em;
    padding: 0 var(--s-1);
    border: 1px solid var(--outline);
    border-radius: var(--r-xs);
    background: var(--surface-container);
}

/* --- Typography and links ------------------------------------------------ */
h1, h2, h3, h4 {
    line-height: var(--lh-tight);
    letter-spacing: var(--tracking-tight);
    font-weight: var(--fw-semibold);
    text-wrap: balance;
}

h1 { font-size: var(--fs-headline); }
h2 { font-size: var(--fs-title); }
h3 { font-size: var(--fs-body-lg); }

/* Prose links underline; controls do not.
   The opt-out below MUST keep the `a` in front of :where(). :where() contributes ZERO
   specificity, so a bare :where(.nav-link) rule loses to the `a` rule above it and the
   nav links and brand wordmark render underlined at rest - with the underline vanishing
   on hover, because :where(...):hover does outrank a. That read as a broken nav, which is
   what it was. Qualified with `a` the two rules tie at (0,0,1) and (0,1,1) and win on
   source order, which is the only reason this line looks redundant. */
a {
    color: var(--primary);
    text-decoration: underline;
    text-underline-offset: 2px;
    text-decoration-thickness: 1px;
    border-radius: var(--r-xs);
    transition: color var(--t-fast) var(--ease-standard);
}

a:hover {
    color: var(--on-primary-container);
}

a:where(.btn, .nav-link, .brand, .chip),
a:where(.btn, .nav-link, .brand, .chip):hover,
a:where(.btn, .nav-link, .brand, .chip):focus-visible {
    text-decoration: none;
}

/* Visible focus everywhere. Never removed without a replacement. */
:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* --- App shell ----------------------------------------------------------- */
.shell {
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
}

.topbar {
    position: sticky;
    top: 0;
    z-index: 40;
    display: flex;
    align-items: center;
    gap: var(--s-3);
    min-height: var(--header-h);
    padding-inline: var(--s-4);
    background: var(--surface);
    border-bottom: 1px solid var(--outline-variant);
}

.brand {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    min-inline-size: 0;
    color: var(--on-surface);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-tight);
    white-space: nowrap;
}

.brand:hover {
    color: var(--on-surface);
}

.brand__mark {
    display: grid;
    place-items: center;
    inline-size: 32px;
    block-size: 32px;
    flex: none;
    border-radius: var(--r-sm);
    background: var(--primary);
    color: var(--on-primary);
}

.brand__mark svg {
    /* 20 rather than 18: the mark is stroke-drawn, and at 18px a 2-unit stroke in a 24
       viewBox lands under 1.5px and the finder squares blur into blobs. */
    inline-size: 20px;
    block-size: 20px;
}

.brand__text {
    display: flex;
    flex-direction: column;
    line-height: 1.2;
    min-inline-size: 0;
}

.brand__sub {
    font-size: var(--fs-label);
    font-weight: var(--fw-normal);
    color: var(--on-surface-dim);
    letter-spacing: var(--tracking-normal);
    overflow: hidden;
    text-overflow: ellipsis;
}

.topbar__spacer {
    display: none;
}

.topbar__actions {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    margin-inline-start: auto;
    flex: none;
}

/* The signed-in chip and Log out live in the drawer below 900px, so the topbar
   still fits a 360px viewport alongside the burger and the brand. */
.topbar__user {
    display: none;
    align-items: center;
    gap: var(--s-2);
}

.topbar__user form {
    display: flex;
}

/* A long username must truncate rather than push the buttons off-screen. */
.topbar__user .chip {
    max-inline-size: 12rem;
    overflow: hidden;
    text-overflow: ellipsis;
}

.brand__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* An anonymous visitor can only ever reach /login or /register - every other page
   redirects. On a narrow viewport, hide the button for the page they are already
   on so the topbar cannot overflow; both come back from 600px up. */
@media (max-width: 599px) {
    body[data-page="login"] .auth-login,
    body[data-page="register"] .auth-register {
        display: none;
    }
}

/* --- Navigation ---------------------------------------------------------- */
/*
   Two containers, ONE set of links (the layout renders both from the same array).

   .nav--bar    the inline topbar nav. Desktop only. This was the mobile collision:
                it used to stay display:flex at every width, so a phone showed the
                burger AND all the links fighting for the same row.
   .nav--drawer the vertical list inside the slide-in drawer. Mobile only in effect,
                because the whole drawer is display:none from 900px up.
*/
.nav {
    display: flex;
    align-items: center;
    gap: var(--s-1);
}

.nav--bar {
    display: none;
}

.nav--drawer {
    flex-direction: column;
    align-items: stretch;
    gap: var(--s-1);
}

.nav-link {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    padding: var(--s-2) var(--s-4);
    min-block-size: 40px;
    border-radius: var(--r-full);
    color: var(--on-surface-variant);
    font-size: var(--fs-body);
    font-weight: var(--fw-medium);
    white-space: nowrap;
    overflow: hidden;
    isolation: isolate;
    transition: color var(--t-fast) var(--ease-standard);
}

.nav-link::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background: currentColor;
    opacity: 0;
    transition: opacity var(--t-fast) var(--ease-standard);
}

.nav-link:hover {
    color: var(--on-surface);
}

.nav-link:hover::before {
    opacity: var(--state-hover);
}

.nav-link[aria-current="page"] {
    background: var(--primary-container);
    color: var(--on-primary-container);
    font-weight: var(--fw-semibold);
}

/* Burger: hidden on desktop, the only nav affordance on mobile. */
.nav-toggle {
    display: inline-flex;
}

.drawer {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    z-index: 60;
    inline-size: min(var(--drawer-w), 85vw);
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    padding: var(--s-4);
    background: var(--surface-container);
    border-inline-end: 1px solid var(--outline-variant);
    box-shadow: var(--e-4);
    overflow-y: auto;
    transform: translateX(-100%);
    visibility: hidden;
    transition:
        transform var(--t-slow) var(--ease-emphasized),
        visibility 0s linear var(--t-slow);
}

.drawer.is-open {
    transform: translateX(0);
    visibility: visible;
    transition:
        transform var(--t-slow) var(--ease-emphasized),
        visibility 0s;
}

.drawer .nav-link {
    inline-size: 100%;
    justify-content: flex-start;
    min-block-size: var(--touch-min);
}

.drawer__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-2);
    padding-block-end: var(--s-3);
    margin-block-end: var(--s-2);
    border-block-end: 1px solid var(--outline-variant);
}

.drawer__user {
    display: flex;
    flex-direction: column;
    gap: var(--s-hair);
    min-inline-size: 0;
    padding-block: var(--s-3);
}

.drawer__name {
    font-weight: var(--fw-semibold);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.drawer__role {
    font-size: var(--fs-label);
    color: var(--on-surface-dim);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
}

.drawer__spacer {
    flex: 1;
}

.drawer__actions {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    padding-block-start: var(--s-3);
    border-block-start: 1px solid var(--outline-variant);
}

/* Backdrop: clicking it closes the drawer. */
.scrim {
    position: fixed;
    inset: 0;
    z-index: 50;
    background: rgb(0 0 0 / 0.5);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--t-slow) var(--ease-standard), visibility 0s linear var(--t-slow);
}

.scrim.is-open {
    opacity: 1;
    visibility: visible;
    transition: opacity var(--t-slow) var(--ease-standard), visibility 0s;
}

/* Lock scroll while the drawer is open, without layout shift. */
body.is-drawer-open {
    overflow: hidden;
}

/* --- Confirmation dialog ------------------------------------------------- */
/*
   z-index sits above the drawer (60), because the drawer's own logout button opens
   this dialog, and below the toasts (100), so a confirmation never covers the message
   about the action that preceded it.

   The closed state is the [hidden] attribute, which the global rule turns into
   display:none !important. That wins over display:grid here, and it is what removes the
   dialog from the accessibility tree rather than only from view.
*/
.modal {
    position: fixed;
    inset: 0;
    z-index: 80;
    display: grid;
    place-items: center;
    padding: var(--s-4);
    opacity: 0;
    transition: opacity var(--t-base) var(--ease-standard);
}

.modal.is-open {
    opacity: 1;
}

.modal__scrim {
    position: absolute;
    inset: 0;
    z-index: -1;
    background: rgb(0 0 0 / 0.55);
}

.modal__panel {
    inline-size: 100%;
    max-inline-size: 26rem;
    max-block-size: calc(100dvh - var(--s-8));
    overflow-y: auto;
    padding: var(--s-6);
    background: var(--surface-container-high);
    border: 1px solid var(--outline-variant);
    border-radius: var(--r-lg);
    box-shadow: var(--e-4);
    transform: translateY(var(--s-2)) scale(0.98);
    transition: transform var(--t-base) var(--ease-emphasized);
}

.modal.is-open .modal__panel {
    transform: none;
}

.modal__title {
    font-size: var(--fs-title);
    font-weight: var(--fw-semibold);
    color: var(--on-surface);
}

.modal__body {
    margin-block-start: var(--s-2);
    font-size: var(--fs-body);
    color: var(--on-surface-variant);
}

.modal__actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--s-3);
    margin-block-start: var(--s-6);
}

/* P9-17 status modal, generalised in P9-19: the tone picks the icon and its colour on
   ANY house modal - confirm included - so the two dialogs share one visual grammar;
   no tone hides the icon entirely and the dialog is title and text only. Colours are
   tokens, so both themes carry the pair without a second rule. */
.modal__icon {
    display: none;
    margin-block-end: var(--s-3);
}

.modal__icon svg {
    inline-size: 2rem;
    block-size: 2rem;
}

.modal.tone-danger .modal__icon {
    display: grid;
    place-items: center;
    color: var(--danger);
}

.modal.tone-success .modal__icon {
    display: grid;
    place-items: center;
    color: var(--success);
}

.modal.tone-danger .i-ok,
.modal.tone-success .i-alert {
    display: none;
}

/* Lock scroll behind the dialog, the same mechanism the drawer uses. */
body.is-modal-open {
    overflow: hidden;
}

.main {
    flex: 1;
    inline-size: 100%;
    max-inline-size: var(--shell-max);
    margin-inline: auto;
    padding: var(--s-5) var(--s-4) var(--s-8);
}

.page-head {
    margin-block-end: var(--s-5);
}

.page-head p {
    margin-block-start: var(--s-2);
    color: var(--on-surface-variant);
    font-size: var(--fs-body);
    max-inline-size: 64ch;
}

/* P9-24 global rhythm: helper text, empty-state hints and list notes share the
   page-head lede's reading measure, so no hint runs the width of a desktop viewport
   while the lede beside it wraps at 64ch. */
.field__hint,
.empty__hint,
.datalist__note {
    max-inline-size: 64ch;
}

/* --- Cards --------------------------------------------------------------- */
.card {
    background: var(--surface);
    border: 1px solid var(--outline-variant);
    border-radius: var(--r-lg);
    box-shadow: var(--e-1);
    overflow: hidden;
    /* Same trap as .tabpanel, one level up: as a grid item a card defaults to
       min-inline-size:auto and would size itself to a wide table inside it, stretching the
       page instead of letting the table's wrapper scroll. Text wraps and scroll containers
       scroll, so nothing in a card actually needs the card to grow. */
    min-inline-size: 0;
}

.card__head {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    border-bottom: 1px solid var(--outline-variant);
    background: var(--surface-container);
    min-block-size: 52px;
}

.card__title {
    font-size: var(--fs-body);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-normal);
}

.card__hint {
    margin-inline-start: auto;
    font-size: var(--fs-label);
    color: var(--on-surface-dim);
}

.card__body {
    padding: var(--s-4);
}

.workspace {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-4);
    align-items: start;
}

.workspace__source,
.workspace__result {
    display: grid;
    gap: var(--s-4);
    align-content: start;
    min-inline-size: 0; /* lets long payloads shrink instead of overflowing */
}

/* --- Buttons ------------------------------------------------------------- */
/*
   The state layer is the ::before overlay, tinted with currentColor. Because it
   uses currentColor it works for every variant automatically, including ones
   whose background is a colour this file never anticipated.
*/
.btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    padding: var(--s-2) var(--s-4);
    min-block-size: var(--touch-min);
    border: 1px solid transparent;
    border-radius: var(--r-full);
    background: var(--surface-container-high);
    color: var(--on-surface);
    font-size: var(--fs-body);
    font-weight: var(--fw-medium);
    letter-spacing: var(--tracking-normal);
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    overflow: hidden;
    isolation: isolate;
    transition:
        background-color var(--t-fast) var(--ease-standard),
        border-color var(--t-fast) var(--ease-standard),
        color var(--t-fast) var(--ease-standard),
        box-shadow var(--t-base) var(--ease-standard),
        opacity var(--t-fast) var(--ease-standard);
}

.btn::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background: currentColor;
    opacity: 0;
    transition: opacity var(--t-fast) var(--ease-standard);
    pointer-events: none;
}

.btn:hover:not(:disabled) {
    text-decoration: none;
}

.btn:hover:not(:disabled)::before {
    opacity: var(--state-hover);
}

.btn:active:not(:disabled)::before {
    opacity: var(--state-press);
}

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

.btn:disabled {
    opacity: 0.38; /* Material disabled opacity */
    cursor: not-allowed;
}

.btn svg {
    inline-size: 18px;
    block-size: 18px;
    flex: none;
}

/* Filled: the one high-emphasis action per view. */
.btn--primary {
    background: var(--primary);
    color: var(--on-primary);
    box-shadow: var(--e-1);
    font-weight: var(--fw-semibold);
}

.btn--primary:hover:not(:disabled) {
    box-shadow: var(--e-2);
}

/* Outlined: medium emphasis. */
.btn--ghost {
    background: transparent;
    border-color: var(--outline);
    color: var(--primary);
}

/* Text: lowest emphasis, used in the topbar and toolbars. */
.btn--text {
    background: transparent;
    color: var(--on-surface-variant);
    padding-inline: var(--s-3);
}

.btn--text:hover:not(:disabled) {
    color: var(--on-surface);
}

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

.btn--icon {
    padding: 0;
    inline-size: var(--touch-min);
    min-block-size: var(--touch-min);
    border-radius: var(--r-full);
    background: transparent;
    color: var(--on-surface-variant);
}

.btn--icon:hover:not(:disabled) {
    color: var(--on-surface);
}

.btn--block {
    inline-size: 100%;
}

.btn--sm {
    min-block-size: 36px;
    padding: var(--s-1) var(--s-3);
    font-size: var(--fs-body);
}

.btn__spin {
    inline-size: 16px;
    block-size: 16px;
    flex: none;
    border: 2px solid currentColor;
    border-block-start-color: transparent;
    border-radius: var(--r-full);
    animation: spin 700ms linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* --- Dropzone ------------------------------------------------------------ */
.dropzone {
    position: relative;
    display: grid;
    place-items: center;
    gap: var(--s-3);
    padding: var(--s-6) var(--s-4);
    border: 1px dashed var(--outline);
    border-radius: var(--r-lg);
    background: var(--surface-container);
    text-align: center;
    cursor: pointer;
    isolation: isolate;
    transition:
        border-color var(--t-base) var(--ease-standard),
        background-color var(--t-base) var(--ease-standard);
}

.dropzone::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background: var(--primary);
    opacity: 0;
    transition: opacity var(--t-base) var(--ease-standard);
    pointer-events: none;
}

.dropzone:hover::before {
    opacity: var(--state-hover);
}

.dropzone:hover {
    border-color: var(--primary);
}

.dropzone.is-over {
    border-color: var(--primary);
    border-style: solid;
}

.dropzone.is-over::before {
    opacity: var(--state-drag);
}

.dropzone.is-busy {
    cursor: progress;
}

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

.dropzone__icon {
    display: grid;
    place-items: center;
    inline-size: 56px;
    block-size: 56px;
    border-radius: var(--r-md);
    background: var(--primary-container);
    color: var(--on-primary-container);
}

.dropzone__icon svg {
    inline-size: 26px;
    block-size: 26px;
}

.dropzone__title {
    font-size: var(--fs-body-lg);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-tight);
}

.dropzone__hint {
    font-size: var(--fs-body);
    color: var(--on-surface-variant);
    max-inline-size: 42ch;
}

.dropzone__formats {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-2);
    justify-content: center;
}

.dropzone__file {
    position: absolute;
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    opacity: 0;
    cursor: pointer;
}

/* --- Chips --------------------------------------------------------------- */
.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--s-1);
    padding: var(--s-hair) var(--s-3);
    min-block-size: 24px;
    border-radius: var(--r-sm);
    border: 1px solid var(--outline-variant);
    background: var(--surface-container);
    color: var(--on-surface-variant);
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    font-weight: var(--fw-medium);
    letter-spacing: var(--tracking-normal);
    white-space: nowrap;
}

.chip--accent  { background: var(--primary-container);  color: var(--on-primary-container);  border-color: transparent; }
.chip--success { background: var(--success-container);  color: var(--on-success-container);  border-color: transparent; }
.chip--warning { background: var(--warning-container);  color: var(--on-warning-container);  border-color: transparent; }
.chip--danger  { background: var(--danger-container);   color: var(--on-danger-container);   border-color: transparent; }

/* --- Forms --------------------------------------------------------------- */
.field {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    min-inline-size: 0;
}

.field__label {
    display: flex;
    align-items: baseline;
    gap: var(--s-2);
    font-size: var(--fs-body);
    font-weight: var(--fw-medium);
    color: var(--on-surface-variant);
}

.field__label .req {
    color: var(--danger);
}

.field__hint {
    font-size: var(--fs-label);
    color: var(--on-surface-dim);
    font-weight: var(--fw-normal);
    line-height: var(--lh-normal);
}

.field__error {
    display: none;
    font-size: var(--fs-label);
    color: var(--danger);
}

.field.has-error .field__error {
    display: block;
}

.input,
.textarea,
.select {
    inline-size: 100%;
    min-block-size: var(--touch-min);
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--outline);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--on-surface);
    font-size: var(--fs-body-lg); /* 16px: stops iOS zooming on focus */
    transition:
        border-color var(--t-fast) var(--ease-standard),
        box-shadow var(--t-fast) var(--ease-standard);
}

.input::placeholder,
.textarea::placeholder {
    color: var(--on-surface-dim);
}

.input:hover:not(:disabled):not([readonly]),
.textarea:hover:not(:disabled):not([readonly]) {
    border-color: var(--on-surface-variant);
}

.input:focus,
.textarea:focus,
.select:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 1px var(--primary);
}

.input:disabled,
.textarea:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    background: var(--surface-container);
}

.input.is-invalid,
.textarea.is-invalid {
    border-color: var(--danger);
}

.input.is-invalid:focus,
.textarea.is-invalid:focus {
    box-shadow: 0 0 0 1px var(--danger);
}

/* Read-only parsed values are visually distinct from editable ones, so it is
   obvious at a glance what the operator may change. */
.input[readonly] {
    background: var(--surface-container);
    color: var(--on-surface-variant);
    border-color: var(--outline-variant);
    cursor: default;
}

.textarea {
    min-block-size: 128px;
    padding: var(--s-3);
    resize: vertical;
    font-family: var(--font-mono);
    font-size: var(--fs-body);
    line-height: 1.6;
    word-break: break-all;
}

.select {
    appearance: none;
    padding-inline-end: var(--s-6);
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
}

.counter {
    margin-inline-start: auto;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    color: var(--on-surface-dim);
    font-variant-numeric: tabular-nums;
}

.counter.is-near { color: var(--warning); }
.counter.is-over { color: var(--danger); }

.form-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-4);
}

.section-label {
    display: flex;
    align-items: baseline;
    gap: var(--s-2);
    margin-block: var(--s-5) var(--s-3);
    font-size: var(--fs-label);
    font-weight: var(--fw-semibold);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--on-surface-dim);
}

/* --- Data list ----------------------------------------------------------- */
.datalist {
    display: grid;
    border: 1px solid var(--outline-variant);
    border-radius: var(--r-md);
    overflow: hidden;
    background: var(--outline-variant); /* shows through the hairline row gaps */
    gap: var(--hairline);
}

.datalist__row {
    display: grid;
    grid-template-columns: minmax(6.5rem, 0.85fr) minmax(0, 2fr);
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    background: var(--surface);
    font-size: var(--fs-body);
}

.datalist__key {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    min-inline-size: 0;
    color: var(--on-surface-variant);
    font-weight: var(--fw-medium);
}

.datalist__val {
    font-family: var(--font-mono);
    font-size: var(--fs-body);
    color: var(--on-surface);
    overflow-wrap: anywhere;
    word-break: break-word;
}

.datalist__tag {
    flex: none;
    padding: var(--hairline) var(--s-1);
    border-radius: var(--r-xs);
    background: var(--surface-container-high);
    color: var(--on-surface-dim);
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    line-height: 1.4;
}

.datalist__sub {
    grid-column: 1 / -1;
    display: grid;
    gap: var(--hairline);
    margin-block-start: var(--s-3);
    padding-inline-start: var(--s-3);
    border-inline-start: 2px solid var(--outline);
}

/* --- Result panel -------------------------------------------------------- */
.result {
    display: grid;
    gap: var(--s-4);
}

.qr-preview {
    display: grid;
    place-items: center;
    gap: var(--s-3);
    padding: var(--s-5);
    border: 1px solid var(--outline-variant);
    border-radius: var(--r-md);
    background: var(--surface-container);
}

.qr-preview__frame {
    padding: var(--s-3);
    background: #fff; /* a QR needs a white quiet zone regardless of theme */
    border-radius: var(--r-sm);
    box-shadow: var(--e-1);
    line-height: 0;
    /* Clicking opens the global viewer (P8-05); the cursor says so before the click. */
    cursor: zoom-in;
}

.qr-preview__frame img {
    inline-size: 200px;
    block-size: 200px;
    image-rendering: pixelated;
}

.qr-preview__caption {
    font-size: var(--fs-body);
    color: var(--on-surface-variant);
    text-align: center;
    overflow-wrap: anywhere;
}

.copywrap {
    position: relative;
}

.copywrap .textarea {
    padding-inline-end: var(--s-8);
}

.copybtn {
    position: absolute;
    inset-block-start: var(--s-2);
    inset-inline-end: var(--s-2);
    inline-size: 36px;
    block-size: 36px;
    display: grid;
    place-items: center;
    border: 1px solid var(--outline);
    border-radius: var(--r-sm);
    background: var(--surface-container-high);
    color: var(--on-surface-variant);
    cursor: pointer;
    isolation: isolate;
    overflow: hidden;
    transition: color var(--t-fast) var(--ease-standard), border-color var(--t-fast) var(--ease-standard);
}

.copybtn::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: currentColor;
    opacity: 0;
    transition: opacity var(--t-fast) var(--ease-standard);
}

.copybtn:hover {
    color: var(--on-surface);
}

.copybtn:hover::before {
    opacity: var(--state-hover);
}

.copybtn.is-copied {
    color: var(--success);
    border-color: var(--success);
}

.copybtn svg {
    inline-size: 18px;
    block-size: 18px;
}

/* Icon swap is driven by the state class, not by inline display manipulation, so the
   template carries no style attribute for it. */
.copybtn .i-check        { display: none; }
.copybtn.is-copied .i-copy  { display: none; }
.copybtn.is-copied .i-check { display: block; }

/* --- Toolbar ------------------------------------------------------------- */
/* Filter and action bar above a list. Wraps rather than scrolls, because its contents
   are controls a user has to be able to reach. */
.toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-3);
}

/* Pushes whatever follows it to the trailing edge, without the last-child guessing that
   .row--between does. */
.toolbar__spacer {
    flex: 1;
}

/*
   Controls inside a toolbar are sized to their content, not to the row.

   .input and .select both set inline-size:100%, which is right for a form field and wrong
   here: in a flex row, 100% resolves against the toolbar, so the control takes the whole
   width and flex-wrap pushes the trailing button onto a second line. Reported by the user
   on the audit page - a dropdown showing only "10 / 25 / 50 / 100" was spanning a 1200px
   card, with Refresh stranded underneath it.

   A literal 50% would not fix that: 50% of a wide card is still far wider than the content
   needs, and 50% of a phone is too narrow to hit. Sizing to content with a floor for the
   touch target and a ceiling so a search field cannot sprawl is what actually reads as
   intentional at every width.
*/
.toolbar .select {
    inline-size: auto;
    min-inline-size: 8rem;
    flex: 0 0 auto;
}

.toolbar .input {
    inline-size: auto;
    min-inline-size: 12rem;
    max-inline-size: 24rem;
    flex: 1 1 12rem;
}

/* --- Tabs ---------------------------------------------------------------- */
/*
   Tabs scroll horizontally instead of wrapping. A wrapped second row of tabs reads as a
   second control rather than a continuation of the first, and the members page has four
   of them, which is exactly the count that wraps on a phone.

   Selection state lives on aria-selected rather than a class, so the ARIA contract and
   the styling cannot disagree - which is the failure mode a class-based tab list has.
*/
.tabs {
    display: flex;
    gap: var(--s-1);
    border-block-end: 1px solid var(--outline-variant);
    overflow-x: auto;
    scrollbar-width: thin;
}

.tab {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    padding: var(--s-3) var(--s-4);
    min-block-size: var(--touch-min);
    border: 0;
    background: none;
    color: var(--on-surface-variant);
    font: inherit;
    font-size: var(--fs-body);
    font-weight: var(--fw-medium);
    white-space: nowrap;
    cursor: pointer;
    overflow: hidden;
    isolation: isolate;
    transition: color var(--t-fast) var(--ease-standard);
}

/* State layer, same mechanism as .btn: a currentColor wash rather than a second colour
   to keep in sync with the theme. */
.tab::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: currentColor;
    opacity: 0;
    transition: opacity var(--t-fast) var(--ease-standard);
    pointer-events: none;
}

.tab:hover:not(:disabled)::before  { opacity: var(--state-hover); }
.tab:active:not(:disabled)::before { opacity: var(--state-press); }

.tab:hover:not(:disabled) {
    color: var(--on-surface);
}

/* The selected indicator is a bar on the tab's own bottom edge, so it lines up with the
   container border instead of floating over it. */
.tab::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    inset-block-end: 0;
    block-size: var(--s-hair);
    background: currentColor;
    opacity: 0;
    transition: opacity var(--t-fast) var(--ease-standard);
    pointer-events: none;
}

.tab[aria-selected="true"] {
    color: var(--primary);
    font-weight: var(--fw-semibold);
}

.tab[aria-selected="true"]::after {
    opacity: 1;
}

.tab:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.tab__count {
    padding-inline: var(--s-2);
    border-radius: var(--r-full);
    background: var(--surface-container-high);
    color: var(--on-surface-variant);
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    font-variant-numeric: tabular-nums;
}

.tab[aria-selected="true"] .tab__count {
    background: var(--primary-container);
    color: var(--on-primary-container);
}

.tabpanel {
    padding-block-start: var(--s-4);
    /*
       A tabpanel usually sits between a grid (.stack) and a scroll container
       (.tablewrap), and that middle position is a trap: grid items default to
       min-inline-size:auto, which sizes the panel to its content's min-content width.
       A wide table then stretches the panel, the card and finally the page past the
       viewport - the page pans, the wrapper is exactly as wide as its table, and there is
       nothing to scroll, which a user experiences as "the list will not scroll". This is
       what happened on the members page at phone width while the audit page scrolled fine:
       there the tablewrap IS the grid item, and a scroll container as a grid item gets an
       automatic minimum of zero.
    */
    min-inline-size: 0;
}

/* --- Table --------------------------------------------------------------- */
/*
   The wrapper owns the horizontal scroll, not the table. That is what stops a wide table
   from stretching the page and pushing the topbar sideways on a phone - the layout stays
   put and the table slides inside its own box.
*/
.tablewrap {
    overflow-x: auto;
    border: 1px solid var(--outline-variant);
    border-radius: var(--r-md);
    background: var(--surface);
    -webkit-overflow-scrolling: touch;
}

.table {
    inline-size: 100%;
    border-collapse: collapse;
    font-size: var(--fs-body);
}

.table caption {
    padding: var(--s-3) var(--s-4);
    color: var(--on-surface-variant);
    font-size: var(--fs-label);
    text-align: start;
    /* P11-04 (q2, announced): a caption explains the TABLE, not a form field, so the
       P9-24 hint measure above must not apply to it - captions span their table. */
    max-inline-size: none;
}

.table th,
.table td {
    padding: var(--s-3) var(--s-4);
    text-align: start;
    vertical-align: middle;
    border-block-end: 1px solid var(--outline-variant);
}

.table thead th {
    background: var(--surface-container);
    color: var(--on-surface-variant);
    font-size: var(--fs-label);
    font-weight: var(--fw-semibold);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    white-space: nowrap;
}

/* The last row's border would double up with the wrapper's own. */
.table tbody tr:last-child th,
.table tbody tr:last-child td {
    border-block-end: 0;
}

.table tbody tr {
    transition: background-color var(--t-fast) var(--ease-standard);
}

.table tbody tr:hover {
    background: var(--surface-container);
}

/* The signed-in user's own row, in a list of everyone. */
.table tbody tr.is-current {
    background: var(--primary-container);
    color: var(--on-primary-container);
}

.table tbody tr.is-current:hover {
    background: var(--primary-container);
}

/*
   A failed sign-in attempt (P5-03). Tinted with the danger surface rather than merely
   badged, because in a long list the eye scans colour first, and a burst of failures is
   exactly what an auditor must find without reading every row. The hover rule keeps the
   tint from being replaced by the generic row hover, which would make a failure look
   ordinary the moment the pointer crosses it.
*/
.table tbody tr.is-failed {
    background: var(--danger-container);
    color: var(--on-danger-container);
}

.table tbody tr.is-failed:hover {
    background: var(--danger-container);
}

.table--compact th,
.table--compact td {
    padding-block: var(--s-2);
}

/* A user looking at their own history: "generated by" would repeat their name on every
   row, so the column is dropped rather than left to say the same thing 25 times. Set by
   audit.js from the scope the server reports - it is a readability choice, not a
   permission, and the server applies the real one. */
.table--self .col-user {
    display: none;
}

/* Expand/collapse control in a table row - the by-user audit view (P5-05). The rotation is
   driven by aria-expanded rather than a class, so the visual state cannot disagree with the
   state a screen reader is told about. */
.row-toggle svg {
    inline-size: 18px;
    block-size: 18px;
    transition: transform var(--t-fast) var(--ease-standard);
}

.row-toggle[aria-expanded="true"] svg {
    transform: rotate(90deg);
}

/* The expanded detail is a full-width row beneath the one that opened it, so it reads as
   belonging to that user rather than as an unrelated table. */
.detail-row > td {
    padding: var(--s-4);
    background: var(--surface-container);
}

.detail-row .tablewrap {
    border-color: var(--outline);
}

/* A two-line identity cell: username (plus a "you" chip) on the first line, display name
   on the second. The flex column is a div inside the td rather than the td itself, because
   display:flex on a table cell takes it out of the table layout and the column stops
   sizing against its neighbours. */
.user-cell {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-hair);
}

/* The "before" half of a customization (D20): the source's own merchant name, under the
   name it became. Dim and one step smaller so the after-name stays the row's answer, and
   overflow-wrap because a raw tag-59 value has no spaces guaranteed. */
.was-name {
    color: var(--on-surface-dim);
    font-size: var(--fs-label);
    overflow-wrap: anywhere;
}

/* --- Panel history (P8-04) ----------------------------------------------- */
/*
   The user's own generations on the QR panel: one row per code, grouped under a day
   heading, expanding in place. The row is a real button with aria-expanded, so the
   list is keyboard-operable and a screen reader hears the state change - a clickable
   div would be neither.
*/
.history-item + .history-item {
    margin-block-start: var(--s-2);
}

.history-row {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--s-3);
    inline-size: 100%;
    padding: var(--s-2) var(--s-3);
    border: 1px solid var(--outline-variant);
    border-radius: var(--r-sm);
    background: var(--surface-container);
    color: var(--on-surface);
    font: inherit;
    text-align: start;
    cursor: pointer;
    overflow: hidden;
    isolation: isolate;
}

.history-row::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: currentColor;
    opacity: 0;
    transition: opacity var(--t-fast) var(--ease-standard);
    pointer-events: none;
}

.history-row:hover::before  { opacity: var(--state-hover); }
.history-row:active::before { opacity: var(--state-press); }

.history-row[aria-expanded="true"] {
    border-color: var(--outline);
}

.history-row__main {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-hair);
    flex: 1;
    min-inline-size: 0;
}

.history-row__time {
    color: var(--on-surface-dim);
    font-size: var(--fs-label);
    font-variant-numeric: tabular-nums;
}

.history-detail {
    display: grid;
    gap: var(--s-3);
    margin-block-start: var(--s-2);
    padding: var(--s-3);
    border: 1px solid var(--outline-variant);
    border-radius: var(--r-sm);
    background: var(--surface-container-high);
}

/* Right-align a column of numbers, and keep the digits from jittering between rows. */
.table .num {
    text-align: end;
    font-variant-numeric: tabular-nums;
}

/*
   Scroll, don't squeeze - but only where squeezing is the alternative.

   Without a width floor, inline-size:100% lets the browser COMPRESS the columns to fit a
   narrow screen, wrapping text into unreadable slivers, and .tablewrap's overflow-x never
   engages because nothing overflows. The floor fixes that on a phone.

   Applied unconditionally it broke the desktop: a table wider than its card overflowed
   behind a scrollbar nobody expected and pushed the row action buttons out of view
   (reported by the user on the members and audit lists). So the floor lives below the
   drawer breakpoint, where the container is narrower than any data table in the product
   and scrolling is the only honest option; at 900px and up the table fits its container
   and columns may wrap, exactly as they did before either change.
*/
@media (max-width: 899px) {
    .table {
        min-inline-size: max-content;
    }
}

/* Audit list thumbnail. Sets no background: the PNG carries its own light one, because a
   QR symbol needs it to scan, and a colour here would be a hardcoded value outside the
   token layer that the dark theme could invert into an unreadable symbol. */
.qr-thumb {
    display: block;
    inline-size: var(--s-7);
    block-size: var(--s-7);
    border-radius: var(--r-xs);
}

/* D11: a stored payload that no longer renders answers non-200, and an <img> would show a
   broken-image icon that says nothing about why. This replaces it with the reason. */
.qr-thumb-failed {
    display: flex;
    align-items: center;
    inline-size: var(--s-7);
    block-size: var(--s-7);
    padding: var(--s-1);
    border: 1px dashed var(--outline);
    border-radius: var(--r-xs);
    color: var(--on-surface-dim);
    font-size: var(--fs-label);
    line-height: 1.2;
    text-align: center;
}

/* --- Pager --------------------------------------------------------------- */
.pager {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    margin-block-start: var(--s-4);
}

.pager__info {
    color: var(--on-surface-variant);
    font-size: var(--fs-label);
    font-variant-numeric: tabular-nums;
}

.pager__btns {
    display: flex;
    gap: var(--s-2);
}

/* --- Password reveal ----------------------------------------------------- */
/*
   Assembled by app.js around every password input, so a new password field gets the
   toggle without anyone remembering to add markup. The button sits inside the input's
   right edge rather than beside it: the input keeps its full width, and the control is
   under the same thumb that types.

   The icon swap keys off aria-pressed, the same house pattern as the tabs and the copy
   button - the accessible state and the visual state are one value, so they cannot
   disagree.
*/
.reveal {
    position: relative;
}

.reveal .input {
    padding-inline-end: var(--s-8);
}

.reveal__btn {
    position: absolute;
    inset-block: 0;
    inset-inline-end: 0;
    inline-size: var(--s-8);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    background: none;
    color: var(--on-surface-variant);
    cursor: pointer;
    border-radius: 0 var(--r-sm) var(--r-sm) 0;
    overflow: hidden;
    isolation: isolate;
}

.reveal__btn::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: currentColor;
    opacity: 0;
    transition: opacity var(--t-fast) var(--ease-standard);
    pointer-events: none;
}

.reveal__btn:hover::before  { opacity: var(--state-hover); }
.reveal__btn:active::before { opacity: var(--state-press); }

.reveal__btn svg {
    inline-size: 18px;
    block-size: 18px;
}

.reveal__btn .i-eye-off { display: none; }

.reveal__btn[aria-pressed="true"] .i-eye     { display: none; }
.reveal__btn[aria-pressed="true"] .i-eye-off { display: block; }

/* --- Alerts and empty states --------------------------------------------- */
.alert {
    display: flex;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--outline-variant);
    border-radius: var(--r-md);
    background: var(--surface-container);
    font-size: var(--fs-body);
}

.alert svg {
    inline-size: 20px;
    block-size: 20px;
    flex: none;
    /* Optical alignment against the first text line, not a spacing value. */
    margin-block-start: var(--hairline);
}

.alert--error   { background: var(--danger-container);  color: var(--on-danger-container);  border-color: transparent; }
.alert--success { background: var(--success-container); color: var(--on-success-container); border-color: transparent; }
.alert--info    { background: var(--primary-container); color: var(--on-primary-container); border-color: transparent; }
.alert--warning { background: var(--warning-container); color: var(--on-warning-container); border-color: transparent; }

.alert__title {
    display: block;
    font-weight: var(--fw-semibold);
    margin-block-end: var(--s-hair);
}

.empty {
    display: grid;
    place-items: center;
    gap: var(--s-3);
    padding: var(--s-7) var(--s-5);
    text-align: center;
    border: 1px dashed var(--outline);
    border-radius: var(--r-lg);
    background: var(--surface-container);
    color: var(--on-surface-dim);
}

.empty svg {
    inline-size: 32px;
    block-size: 32px;
    opacity: 0.6;
}

.empty__title {
    font-size: var(--fs-body-lg);
    font-weight: var(--fw-semibold);
    color: var(--on-surface-variant);
}

.empty__hint {
    font-size: var(--fs-body);
    max-inline-size: 46ch;
}

/* The brief requires the data panel to appear only after a successful parse.
   [hidden] is used rather than a display class so assistive tech agrees. */
[hidden] {
    display: none !important;
}

/* --- Skeleton ------------------------------------------------------------ */
.skeleton {
    position: relative;
    overflow: hidden;
    background: var(--surface-container-high);
    border-radius: var(--r-sm);
    /* A default height: an empty element with no size renders nothing at all, so a
       loading state that forgot to set one would be invisible rather than obviously
       unfinished. */
    block-size: var(--s-4);
}

.skeleton + .skeleton {
    margin-block-start: var(--s-2);
}

.skeleton::after {
    content: "";
    position: absolute;
    inset: 0;
    transform: translateX(-100%);
    background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.06), transparent);
    animation: shimmer 1.4s infinite;
}

@keyframes shimmer {
    100% { transform: translateX(100%); }
}

/* --- Toasts -------------------------------------------------------------- */
.toasts {
    position: fixed;
    inset-block-end: var(--s-4);
    inset-inline: var(--s-4);
    z-index: 100;
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    align-items: center;
    pointer-events: none;
}

.toast {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    max-inline-size: min(440px, 100%);
    padding: var(--s-3) var(--s-4);
    border-radius: var(--r-sm);
    background: var(--surface-container-highest);
    color: var(--on-surface);
    box-shadow: var(--e-3);
    font-size: var(--fs-body);
    pointer-events: auto;
    animation: toast-in var(--t-base) var(--ease-standard);
}

.toast.is-leaving {
    animation: toast-out var(--t-base) var(--ease-standard) forwards;
}

.toast__dot {
    inline-size: 8px;
    block-size: 8px;
    flex: none;
    border-radius: var(--r-full);
    background: var(--primary);
}

.toast--success .toast__dot { background: var(--success); }
.toast--error .toast__dot   { background: var(--danger); }

@keyframes toast-in {
    from { opacity: 0; transform: translateY(8px); }
}

@keyframes toast-out {
    to { opacity: 0; transform: translateY(4px); }
}

/* --- Switch -------------------------------------------------------------- */
.switch {
    display: flex;
    align-items: flex-start;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--outline-variant);
    border-radius: var(--r-md);
    background: var(--surface-container);
    cursor: pointer;
    font-size: var(--fs-body);
    font-weight: var(--fw-medium);
    transition: border-color var(--t-fast) var(--ease-standard);
}

.switch:hover {
    border-color: var(--outline);
}

.switch input {
    position: absolute;
    opacity: 0;
    inline-size: 1px;
    block-size: 1px;
}

.switch__track {
    position: relative;
    flex: none;
    inline-size: 44px;
    block-size: 24px;
    /* Optical alignment with the label's first line, not a spacing value. */
    margin-block-start: var(--hairline);
    border: 2px solid var(--outline);
    border-radius: var(--r-full);
    background: var(--surface-container-highest);
    transition: background-color var(--t-base) var(--ease-standard), border-color var(--t-base) var(--ease-standard);
}

.switch__thumb {
    position: absolute;
    inset-block-start: 3px;
    inset-inline-start: 3px;
    inline-size: 14px;
    block-size: 14px;
    border-radius: var(--r-full);
    background: var(--on-surface-dim);
    transition: transform var(--t-base) var(--ease-standard),
                background-color var(--t-base) var(--ease-standard),
                inline-size var(--t-base) var(--ease-standard),
                block-size var(--t-base) var(--ease-standard);
}

.switch input:checked + .switch__track {
    background: var(--primary);
    border-color: var(--primary);
}

.switch input:checked + .switch__track .switch__thumb {
    /* Material grows the thumb when on: 14px -> 20px, offset 3px -> 21px. */
    inline-size: 20px;
    block-size: 20px;
    inset-block-start: 0;
    transform: translateX(20px);
    background: var(--on-primary);
}

.switch input:focus-visible + .switch__track {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* --- Auth pages ---------------------------------------------------------- */
.auth {
    display: grid;
    place-items: center;
    padding-block: var(--s-6) var(--s-8);
    /* Centre the card in the viewport rather than pinning it under the topbar. */
    min-height: calc(100dvh - var(--header-h) - var(--s-8));
}

.auth__card {
    inline-size: 100%;
    max-inline-size: 26rem;
    box-shadow: var(--e-2);
}

.auth__card:has(.form-grid--2) {
    max-inline-size: 36rem;
}

.auth__head {
    margin-block-end: var(--s-5);
}

.auth__head p {
    margin-block-start: var(--s-2);
    font-size: var(--fs-body);
    color: var(--on-surface-variant);
}

.auth__foot {
    margin-block-start: var(--s-5);
    padding-block-start: var(--s-4);
    border-block-start: 1px solid var(--outline-variant);
    font-size: var(--fs-body);
    text-align: center;
    color: var(--on-surface-variant);
}

/* --- Settings ------------------------------------------------------------ */
/* A form page, not a dashboard: the column is capped so input lines stay short
   enough to read on a wide monitor instead of stretching the full shell width. */
.settings {
    display: grid;
    gap: var(--s-5);
    align-content: start;
    max-inline-size: 46rem;
}

/* --- Utilities ----------------------------------------------------------- */
.stack {
    display: grid;
    gap: var(--s-4);
}

.row {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    flex-wrap: wrap;
}

.row--between { justify-content: space-between; }

/* Actions row at the foot of a form. A named class rather than an inline margin so
   every form in the product spaces its buttons identically. */
.form-actions {
    margin-block-start: var(--s-5);
}

/* Note under a data list, e.g. the provenance caveat on decoded sub-tags. */
.datalist__note {
    margin-block-start: var(--s-2);
}

.muted { color: var(--on-surface-variant); }
.dim   { color: var(--on-surface-dim); }

.mono {
    font-family: var(--font-mono);
    font-size: var(--fs-body);
}

.sr-only {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* --- Responsive ---------------------------------------------------------- */
@media (min-width: 600px) {
    .form-grid--2 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .qr-preview__frame img {
        inline-size: 240px;
        block-size: 240px;
    }
}

@media (min-width: 900px) {
    .main {
        padding: var(--s-6) var(--s-6) var(--s-8);
    }

    .page-head h1 {
        font-size: var(--fs-display);
    }

    .card__body {
        padding: var(--s-5);
    }

    .workspace {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        gap: var(--s-5);
    }

    .toasts {
        inset-inline: auto;
        inset-inline-end: var(--s-5);
        align-items: flex-end;
    }
}

/* Desktop navigation: inline links, no burger, no drawer. */
@media (min-width: 900px) {
    .nav-toggle {
        display: none;
    }

    .nav--bar {
        display: flex;
    }

    .topbar__user {
        display: flex;
    }

    .drawer,
    .scrim {
        display: none;
    }

    .topbar__spacer {
        display: block;
        flex: 1;
    }
}

/* Narrow phones: drop the brand tagline and tighten the bar so the burger, brand
   and actions fit a 320px viewport without wrapping. */
@media (max-width: 599px) {
    .topbar {
        gap: var(--s-2);
        padding-inline: var(--s-3);
    }

    .brand__sub {
        display: none;
    }

    .btn {
        padding-inline: var(--s-3);
    }

    .page-head h1 {
        font-size: var(--fs-headline);
    }
}

/* Very narrow: stack the data rows instead of squeezing two columns. */
@media (max-width: 479px) {
    .datalist__row {
        grid-template-columns: minmax(0, 1fr);
        gap: var(--s-1);
    }

    .datalist__key {
        font-size: var(--fs-label);
    }

    .qr-preview {
        padding: var(--s-4);
    }

    /* Stack the dialog actions so the primary one sits at the bottom, within reach of
       a thumb, instead of two narrow buttons side by side. */
    .modal__actions {
        flex-direction: column;
    }

    .modal__actions .btn {
        inline-size: 100%;
        justify-content: center;
    }
}

@media (min-width: 1200px) {
    /* Keep the result in view while editing on wide screens. */
    .workspace__result {
        position: sticky;
        inset-block-start: calc(var(--header-h) + var(--s-5));
    }
}

/* Short viewports: trim the dropzone so the form stays reachable. */
@media (max-height: 640px) {
    .dropzone {
        padding: var(--s-4);
    }

    .dropzone__icon {
        inline-size: 44px;
        block-size: 44px;
    }
}
