/* ============================================================
   QR CODE GENERATOR — the student-facing page

   Now speaks the same language as the admin pages (see
   students-page.css): sky/cyan accent (#0ea5e9 → #06b6d4), dark
   surfaces (var(--surface)), a hero with a rule on top, and round chips.
   Students and staff now see one product.

   The old look had its own vocabulary, separate from the whole
   app: three different typefaces in one view (Times New Roman for
   the body, Poppins on the buttons, Fira Code in the inputs), and
   two competing color families (sky #38bdf8 against the neon cyan
   #00f2ea + violet #a855f7 of the "holo" input).

   THE RESERVED COLORS — never use as decoration:
     green  #10b981 → verified / success
     amber  #f59e0b → warning, connection trouble
     red    #ef4444 → bad format, no record found
   Sky is only the page's identity; it carries no status meaning,
   so it does not compete with the three above.
   ============================================================ */

.qr-page {
    --qr: #0ea5e9;
    --qr-deep: #06b6d4;
    --qr-soft: rgba(14, 165, 233, .14);
    --qr-line: rgba(14, 165, 233, .3);
    --qr-ink: #7dd3fc;

    --surface-2: var(--bg);
    --border: rgba(var(--tint), .07);

    --text: var(--ink-2);
    --text-dim: rgba(var(--tint), .55);
    --text-faint: rgba(var(--tint), .38);

    --ok: #10b981;
    --ok-ink: #34d399;
    --warn: #f59e0b;
    --warn-ink: #fcd34d;
    --bad: #ef4444;
    --bad-ink: #f87171;

    --radius: 16px;
    --radius-sm: 10px;
}

/* One typeface for the whole page. Native stack — no webfont to
   wait on from a CDN, so no flash of unstyled text on a slow
   connection. */
.qr-page,
.qr-page input,
.qr-page button,
.qr-page textarea,
.qr-page select {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        "Helvetica Neue", Arial, sans-serif;
}

html {
    height: -webkit-fill-available;
    /* iOS */
    box-sizing: border-box;
}

.qr-page *,
.qr-page *::before,
.qr-page *::after {
    box-sizing: border-box;
}

body.qr-page {
    margin: 0;
    min-height: 100vh;
    min-height: -webkit-fill-available;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(12px, 3vw, 32px);
    background: var(--bg);
    color: var(--text);
    font-size: 16px;
    line-height: 1.5;
    overflow-x: hidden;
    overflow-y: auto;
}

/* The particles are decoration only — behind everything, matching
   the body color so there is no seam at the edge. */
#particles-js {
    position: fixed;
    inset: 0;
    width: 100vw;
    height: 100vh;
    height: 100dvh;
    background-color: var(--bg);
    z-index: 0;
    pointer-events: none;
}

/* ─── Card ────────────────────────────────────────────────── */

.qr-shell {
    position: relative;
    z-index: 10;
    width: 100%;
    max-width: 980px;
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
    gap: 1.25rem;
    padding: clamp(1rem, 2.5vw, 1.6rem);
    border-radius: 20px;
    border: 1px solid var(--border);
    background: var(--surface);
    box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
    opacity: 0;
    transform: translateY(24px);
    animation: qr-rise .6s cubic-bezier(.2, .7, .3, 1) forwards;
}

@keyframes qr-rise {
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* ─── Hero ────────────────────────────────────────────────── */

.qr-hero {
    grid-column: 1 / -1;
    position: relative;
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 1.15rem 1.4rem;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    background:
        radial-gradient(circle at 0% 0%, rgba(14, 165, 233, .16), transparent 55%),
        linear-gradient(135deg, rgba(var(--tint), .04), rgba(var(--tint), .015));
    overflow: hidden;
}

.qr-hero::before {
    content: '';
    position: absolute;
    inset: 0 0 auto 0;
    height: 3px;
    background: linear-gradient(90deg, var(--qr), var(--qr-deep));
    box-shadow: 0 0 12px rgba(14, 165, 233, .6);
}

.qr-hero-icon {
    flex-shrink: 0;
    width: 52px;
    height: 52px;
    border-radius: 14px;
    display: grid;
    place-items: center;
    background: linear-gradient(135deg, var(--qr), var(--qr-deep));
    box-shadow: 0 8px 20px rgba(14, 165, 233, .35);
    overflow: hidden;
}

.qr-hero-icon img {
    width: 34px;
    height: 34px;
    object-fit: contain;
    border-radius: 8px;
}

.qr-hero-text {
    flex: 1 1 240px;
    min-width: 0;
}

.qr-hero-text h1 {
    margin: 0 0 3px;
    font-size: 1.35rem;
    font-weight: 700;
    letter-spacing: -.02em;
    color: var(--ink);
}

.qr-hero-text p {
    margin: 0;
    font-size: .85rem;
    color: var(--text-faint);
}

.qr-chips {
    display: flex;
    gap: .5rem;
    flex-wrap: wrap;
    margin-left: auto;
}

.qr-chip {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .42rem .85rem;
    border-radius: 999px;
    font-size: .78rem;
    font-weight: 600;
    color: var(--qr-ink);
    background: var(--qr-soft);
    border: 1px solid var(--qr-line);
    white-space: nowrap;
}

/* ─── Panels ──────────────────────────────────────────────── */

.left-panel,
.right-panel {
    min-width: 0;
    padding: 1.15rem;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    background: var(--surface);
}

.right-panel {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
    background:
        radial-gradient(circle at 50% 0%, rgba(14, 165, 233, .07), transparent 60%),
        var(--surface);
}

.panel-title {
    display: flex;
    align-items: center;
    gap: .55rem;
    margin: 0 0 1rem;
    font-size: .72rem;
    font-weight: 700;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    color: var(--qr-ink);
}

.panel-title i {
    font-size: .95rem;
}

/* ─── Help / instructions ─────────────────────────────────── */

.instructions-container {
    margin-bottom: 1.1rem;
}

.toggle-btn {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .5rem;
    min-height: 42px;
    padding: .55rem 1rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: rgba(var(--tint), .04);
    color: var(--ink-3);
    font-size: .84rem;
    font-weight: 600;
    cursor: pointer;
    transition: background .18s ease, color .18s ease, border-color .18s ease;
}

.toggle-btn:hover {
    background: rgba(var(--tint), .08);
    border-color: var(--qr-line);
    color: var(--ink);
}

.toggle-btn i {
    color: var(--qr-ink);
}

.instructions-content {
    max-height: 0;
    overflow: hidden;
    padding: 0 1rem;
    margin-top: .55rem;
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    border: 1px solid transparent;
    color: var(--ink-3);
    font-size: .84rem;
    transition: max-height .35s ease, padding .35s ease, border-color .35s ease;
}

.instructions-content.show {
    max-height: 420px;
    padding: .9rem 1rem;
    border-color: var(--border);
}

.instructions-content ol {
    margin: 0;
    padding-left: 1.15rem;
    line-height: 1.7;
}

.instructions-content li::marker {
    color: var(--qr-ink);
    font-weight: 700;
}

.instructions-content strong {
    color: var(--ink);
    font-weight: 600;
}

.instructions-content i {
    color: var(--qr-ink);
    margin-right: .35rem;
}

/* ─── Form ────────────────────────────────────────────────── */

.qr-field {
    margin-bottom: 1rem;
}

.qr-field label,
.field-label {
    display: block;
    margin-bottom: .4rem;
    font-size: .78rem;
    font-weight: 600;
    letter-spacing: .3px;
    color: var(--ink-3);
}

.field-hint {
    display: block;
    margin-top: .35rem;
    font-size: .74rem;
    font-weight: 400;
    color: var(--text-faint);
}

/* Scoped to .qr-shell rather than .qr-page — the chat widget
   (assistant.php + widget.css) is a white surface that lives
   outside the card. A `.qr-page input[type=text]` rule would beat
   `.chat-input` on specificity and turn the typing box inside the
   white widget dark. */
.qr-shell input[type="text"] {
    display: block;
    width: 100%;
    min-height: 46px;
    padding: .6rem .85rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--surface-3);
    background: var(--surface-2);
    color: var(--ink-2);
    font-size: .95rem;
    outline: none;
    transition: border-color .18s ease, box-shadow .18s ease, background .18s ease;
}

.qr-shell input[type="text"]::placeholder {
    color: rgba(var(--tint), .25);
}

.qr-shell input[type="text"]:focus {
    border-color: var(--qr);
    box-shadow: 0 0 0 3px rgba(14, 165, 233, .18);
}

/* The only field you type into. Slightly larger than the locked
   ones below — this page asks exactly one question. */
.qr-field-primary input[type="text"] {
    min-height: 54px;
    font-size: 1.1rem;
    font-weight: 600;
    letter-spacing: .06em;
}

/* Field status. Swap the class, never the inline style — the JS
   used to set `style.border` directly, which CSS can no longer
   win back. */
.qr-field.is-ok input[type="text"] {
    border-color: rgba(16, 185, 129, .55);
}

.qr-field.is-bad input[type="text"] {
    border-color: rgba(239, 68, 68, .55);
}

.qr-field.is-warn input[type="text"] {
    border-color: rgba(245, 158, 11, .55);
}

.qr-field.is-checking input[type="text"] {
    border-color: var(--qr-line);
}

/* ── Locked fields ──
   Filled in automatically from the record. They used to look
   exactly like the field you type into — you could not tell they
   were read-only until you tried. */
.locked-fields {
    display: grid;
    gap: .6rem;
    padding: .9rem;
    border-radius: var(--radius-sm);
    border: 1px dashed rgba(var(--tint), .12);
    background: rgba(var(--tint), .02);
    margin-bottom: 1rem;
    transition: border-color .25s ease, background .25s ease;
}

/* Once verified the box firms up and the edge turns green — you
   can see at a glance that the contents are real. */
.locked-fields.is-filled {
    border-style: solid;
    border-color: rgba(16, 185, 129, .28);
    background: rgba(16, 185, 129, .04);
}

.locked-row {
    display: grid;
    grid-template-columns: 5.5rem minmax(0, 1fr);
    align-items: center;
    gap: .75rem;
}

.locked-row .field-label {
    margin: 0;
    font-size: .74rem;
    color: var(--text-faint);
    text-transform: uppercase;
    letter-spacing: .5px;
}

.locked-row input[type="text"] {
    min-height: 38px;
    padding: .35rem .6rem .35rem 2rem;
    background: transparent;
    border-color: transparent;
    font-size: .9rem;
    font-weight: 600;
    color: var(--ink);
    cursor: default;
    /* An <input> cannot wrap, and it cannot become a <div> — this is
       where fetch_students.js writes `.value`. When a long name is
       cut off on a narrow phone, show the ellipsis rather than
       clipping silently. The JS also sets `title` so the full text
       is still reachable. */
    text-overflow: ellipsis;
}

.locked-row input[type="text"]:focus {
    border-color: var(--border);
    box-shadow: none;
}

.locked-input {
    position: relative;
    min-width: 0;
}

/* The padlock says "this one is not yours to edit" without you
   having to try. It is a real element rather than
   `content: "\f47b"` — no relying on a font codepoint that could
   move in the next version of Bootstrap Icons. */
.locked-icon {
    position: absolute;
    left: .55rem;
    top: 50%;
    transform: translateY(-50%);
    font-size: .8rem;
    line-height: 1;
    color: var(--text-faint);
    pointer-events: none;
    transition: color .25s ease;
}

.locked-fields.is-filled .locked-icon {
    color: var(--ok-ink);
}

/* ─── Status message ──────────────────────────────────────── */
/* The JS used to paint these bright pastels (#fee2e2, #dcfce7) —
   colors meant for a white page, glaring against a dark form.
   Tinted boxes now, matching the admin badges. */

.qr-status {
    display: none;
    align-items: center;
    gap: .5rem;
    margin-top: .55rem;
    padding: .6rem .8rem;
    border-radius: var(--radius-sm);
    border: 1px solid transparent;
    font-size: .82rem;
    font-weight: 500;
    line-height: 1.45;
}

.qr-status.is-visible {
    display: flex;
    animation: qr-fade .25s ease;
}

@keyframes qr-fade {
    from {
        opacity: 0;
        transform: translateY(-3px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.qr-status.is-ok {
    background: rgba(16, 185, 129, .12);
    border-color: rgba(16, 185, 129, .25);
    color: var(--ok-ink);
}

.qr-status.is-bad {
    background: rgba(239, 68, 68, .12);
    border-color: rgba(239, 68, 68, .25);
    color: var(--bad-ink);
}

.qr-status.is-warn {
    background: rgba(245, 158, 11, .12);
    border-color: rgba(245, 158, 11, .28);
    color: var(--warn-ink);
}

.qr-status.is-checking {
    background: var(--qr-soft);
    border-color: var(--qr-line);
    color: var(--qr-ink);
}

.spinner {
    display: inline-block;
    flex-shrink: 0;
    width: 14px;
    height: 14px;
    border: 2px solid rgba(125, 211, 252, .25);
    border-top-color: var(--qr-ink);
    border-radius: 50%;
    animation: spin .8s linear infinite;
}

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

/* ─── Terms ───────────────────────────────────────────────── */

.terms-agree {
    padding: .8rem;
    margin-bottom: 1rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: rgba(var(--tint), .02);
}

.terms-check {
    display: flex;
    align-items: flex-start;
    gap: .65rem;
    cursor: pointer;
    font-size: .84rem;
    font-weight: 400;
    line-height: 1.5;
    color: var(--ink-3);
    margin: 0;
}

/* 20px with padding on the label itself — easy to tap on a phone,
   not just the small box. */
.terms-check input[type="checkbox"] {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    margin-top: 1px;
    cursor: pointer;
    accent-color: var(--qr);
}

/* This is a link, not a button — so it has to undo the mobile rule
   `button { width: 100% }`, which would push it onto its own line
   and center the text on a phone. */
.terms-link {
    background: none;
    border: none;
    padding: 0;
    color: var(--qr-ink);
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
    font-size: inherit;
    font-family: inherit;
    font-weight: 600;
    display: inline;
    width: auto;
    max-width: none;
    margin: 0;
    text-align: left;
}

.terms-link:hover {
    color: var(--ink);
}

/* ── Modal ── */
.terms-modal {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 10000;
    background: rgba(0, 0, 0, .75);
    backdrop-filter: blur(4px);
    padding: 16px;
    padding-top: max(16px, env(safe-area-inset-top));
    padding-bottom: max(16px, env(safe-area-inset-bottom));
    overscroll-behavior: contain;
}

.terms-modal.show {
    display: flex;
    align-items: center;
    justify-content: center;
}

.terms-modal-content {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    width: 100%;
    max-width: 620px;
    max-height: 100%;
    display: flex;
    flex-direction: column;
    box-shadow: 0 20px 60px rgba(0, 0, 0, .6);
    overflow: hidden;
}

.terms-modal-header {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 16px 18px;
    border-bottom: 1px solid var(--border);
}

.terms-modal-header::before {
    content: '';
    position: absolute;
    inset: 0 0 auto 0;
    height: 3px;
    background: linear-gradient(90deg, var(--qr), var(--qr-deep));
}

.terms-modal-header h3 {
    margin: 0;
    font-size: 1rem;
    font-weight: 700;
    color: var(--ink);
    display: flex;
    align-items: center;
    gap: .5rem;
}

.terms-modal-header h3 i {
    color: var(--qr-ink);
}

.terms-close {
    width: 44px;
    height: 44px;
    flex-shrink: 0;
    border: none;
    border-radius: 50%;
    background: rgba(var(--tint), .06);
    color: var(--text);
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    transition: background .18s ease;
}

.terms-close:hover {
    background: rgba(var(--tint), .12);
}

/* Only the text scrolls — the header and the buttons stay in
   view. */
.terms-modal-body {
    padding: 16px 18px;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    color: var(--ink-3);
    font-size: .88rem;
    line-height: 1.65;
}

.terms-modal-body h4 {
    color: var(--ink);
    font-size: .9rem;
    font-weight: 700;
    margin: 18px 0 6px;
}

.terms-modal-body h4:first-child {
    margin-top: 0;
}

.terms-modal-body p {
    margin: 0 0 4px;
}

.terms-modal-footer {
    display: flex;
    gap: 10px;
    justify-content: flex-end;
    padding: 14px 18px;
    border-top: 1px solid var(--border);
}

.terms-btn {
    min-height: 44px;
    padding: 0 18px;
    border-radius: var(--radius-sm);
    font-size: .88rem;
    font-weight: 600;
    cursor: pointer;
    border: 1px solid transparent;
    transition: background .18s ease, color .18s ease, transform .18s ease;
}

.terms-btn-ghost {
    background: rgba(var(--tint), .05);
    border-color: rgba(var(--tint), .12);
    color: var(--ink-3);
}

.terms-btn-ghost:hover {
    background: rgba(var(--tint), .1);
    color: var(--ink);
}

.terms-btn-primary {
    background: linear-gradient(135deg, var(--qr), var(--qr-deep));
    color: var(--on-accent);
    box-shadow: 0 6px 16px rgba(14, 165, 233, .3);
}

.terms-btn-primary:hover {
    transform: translateY(-2px);
    filter: brightness(1.08);
}

/* ─── Primary button: Generate ────────────────────────────── */
/* The page has exactly one primary action. Like .stud-btn.primary
   on the admin side: gradient, soft shadow, no theatrics.

   The .btn-letter spans stay because that is where
   fetch_students.js and terms.js write when they swap the label
   ("Verify First" → "Agree First" → "Generate"). Accessibility:
   the letters are aria-hidden and the button as a whole carries
   the aria-label, otherwise a screen reader spells it out one
   letter at a time. */

.btn-wrapper {
    position: relative;
    display: block;
    width: 100%;
}

.qr-shell .btn {
    width: 100%;
    min-height: 50px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .55rem;
    padding: .7rem 1.2rem;
    border: none;
    border-radius: 12px;
    background: linear-gradient(135deg, var(--qr), var(--qr-deep));
    color: var(--on-accent);
    font-size: .95rem;
    font-weight: 700;
    letter-spacing: .01em;
    cursor: pointer;
    user-select: none;
    box-shadow: 0 6px 16px rgba(14, 165, 233, .3);
    transition: transform .18s ease, box-shadow .18s ease, filter .18s ease;
}

.qr-shell .btn:hover:not(:disabled) {
    transform: translateY(-2px);
    filter: brightness(1.08);
    box-shadow: 0 10px 22px rgba(14, 165, 233, .4);
}

.qr-shell .btn:active:not(:disabled) {
    transform: translateY(0);
}

/* Not ready yet — flat, grey, and clearly not tappable. The label
   says what is missing. */
.qr-shell .btn:disabled {
    background: rgba(var(--tint), .05);
    border: 1px solid var(--border);
    color: var(--text-faint);
    box-shadow: none;
    cursor: not-allowed;
    transform: none;
    filter: none;
}

.btn-svg {
    flex-shrink: 0;
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
}

.qr-shell .btn:disabled .btn-svg {
    opacity: .5;
}

.txt-wrapper {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 1.2em;
}

/* The two are stacked; .txt-2 is the state shown while working. */
.txt-1,
.txt-2 {
    display: flex;
    justify-content: center;
    white-space: nowrap;
}

.txt-2 {
    position: absolute;
    inset: 0;
    opacity: 0;
    pointer-events: none;
}

.btn-letter {
    display: inline-block;
    white-space: pre;
}

/* ─── Right panel: QR ─────────────────────────────────────── */

/* The right half used to be empty until you generated something —
   the page looked half-loaded when you arrived. This is what waits
   there instead. */
.qr-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .6rem;
    width: 100%;
    /* Fills the remaining height of the column. With only
       `min-height` there is a large gap beneath it whenever the form
       on the left is taller than the waiting state. */
    flex: 1 1 auto;
    min-height: 300px;
    padding: 1.5rem;
    border-radius: var(--radius);
    border: 1px dashed rgba(var(--tint), .13);
    background: rgba(var(--tint), .015);
    text-align: center;
}

.qr-placeholder i {
    font-size: 2.6rem;
    color: rgba(var(--tint), .16);
}

.qr-placeholder p {
    margin: 0;
    max-width: 22ch;
    font-size: .84rem;
    color: var(--text-faint);
    line-height: 1.55;
}

.qr-placeholder .qr-placeholder-title {
    font-size: .9rem;
    font-weight: 600;
    color: var(--text-dim);
}

#qrWrapper {
    display: none;
    width: 100%;
    padding: 1.25rem;
    border-radius: var(--radius);
    border: 1px solid var(--qr-line);
    background: var(--surface-2);
    text-align: center;
    box-shadow: 0 0 0 1px rgba(14, 165, 233, .06), 0 12px 30px rgba(0, 0, 0, .35);
    opacity: 0;
    transform: scale(.96);
    transition: opacity .35s ease, transform .35s ease;
}

#qrLogo {
    width: 56px;
    display: block;
    margin: 0 auto .5rem;
}

.qr-card-title {
    margin: 0 0 .9rem;
    font-size: .72rem;
    font-weight: 700;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    color: var(--qr-ink);
}

/* This plate is the QR's quiet zone, and it must NOT follow the
   theme. The canvas inside it is drawn light-on-dark with fixed
   colors (colorDark #38bdf8 on colorLight #0f172a in
   js/scriptv2.js), so in light mode `var(--bg)` wrapped a dark code
   in a near-white ring. A decoder has to invert an inverted code to
   read it — which turned that ring dark and left the finder patterns
   with no quiet zone at all, so the card read as one dark square and
   nothing decoded. Dark mode happened to work only because --bg was
   already near #0f172a.

   The background is the same #0f172a as the downloaded card's QR_BG,
   and the padding matches that card's 28px margin — the old 14px was
   barely one module wide. The hairline is tinted instead of
   var(--border) so light mode cannot put a bright ring back around
   the code. */
#qrcode {
    display: inline-block;
    padding: 28px;
    border-radius: 12px;
    background: #0f172a;
    border: 1px solid rgba(56, 189, 248, .18);
    line-height: 0;
}

/* Do not set `display` here. qrcodejs builds BOTH a canvas and an
   img: after makeImage() it hides the canvas and shows the img.
   Forcing `display: block` on both renders two overlapping QRs. */
#qrcode img,
#qrcode canvas {
    max-width: 100%;
    height: auto;
}

/* scriptv2.js sets the details as one continuous string with `\n`
   in it, which is why this stays `pre-line`. */
#qrText {
    margin: .9rem 0 0;
    padding: .75rem .85rem;
    border-radius: var(--radius-sm);
    background: rgba(var(--tint), .03);
    border: 1px solid var(--border);
    color: var(--ink-3);
    font-family: inherit;
    font-size: .8rem;
    line-height: 1.7;
    white-space: pre-line;
    word-wrap: break-word;
    text-align: left;
}

.download-btn {
    display: none;
    align-items: center;
    justify-content: center;
    gap: .5rem;
    width: 100%;
    min-height: 46px;
    padding: .6rem 1.2rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: rgba(var(--tint), .05);
    color: var(--ink-3);
    font-size: .88rem;
    font-weight: 600;
    cursor: pointer;
    transition: background .18s ease, color .18s ease, transform .18s ease;
}

/* This is a secondary action, not the primary one — it only picks
   up a tint on hover, never the full gradient. Generate is the
   only thing entitled to the gradient. */
.download-btn:hover:not(:disabled) {
    background: var(--qr-soft);
    border-color: var(--qr-line);
    color: var(--on-accent);
    transform: translateY(-2px);
}

.download-btn:disabled {
    opacity: .6;
    cursor: not-allowed;
    transform: none;
}

.btn-icon {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
}

.loading-icon {
    animation: spin 1s linear infinite;
}

.success-icon {
    color: var(--ok-ink);
}

/* ─── Footer ──────────────────────────────────────────────── */

.qr-shell footer {
    grid-column: 1 / -1;
    width: 100%;
    margin: 0;
    padding: 1rem 0 .25rem;
    text-align: center;
    background: none;
    border-top: 1px solid var(--border);
    color: var(--text-faint);
    font-size: .8rem;
}

.qr-shell footer a {
    color: var(--qr-ink);
    text-decoration: none;
}

.qr-shell footer a:hover {
    text-decoration: underline;
}

/* ─── Accessibility ───────────────────────────────────────── */

/* A visible ring for keyboard users. There was none before — the
   input's glow was the only cue, and the buttons had nothing at
   all. */
.qr-page :focus-visible {
    outline: 2px solid var(--qr-ink);
    outline-offset: 2px;
    border-radius: 6px;
}

.qr-page :focus:not(:focus-visible) {
    outline: none;
}

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

@media (prefers-reduced-motion: reduce) {

    .qr-page *,
    .qr-page *::before,
    .qr-page *::after {
        animation-duration: .001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .001ms !important;
    }

    .qr-shell {
        opacity: 1;
        transform: none;
    }
}

/* ─── Responsive ──────────────────────────────────────────── */

@media (max-width: 860px) {
    .qr-shell {
        grid-template-columns: minmax(0, 1fr);
        max-width: 560px;
    }

    /* The hero becomes a single column when narrow. Made a real
       column rather than relying on flex wrapping — that way the
       logo is guaranteed its own row instead of breaking wherever
       the title length happens to put it. */
    .qr-hero {
        flex-direction: column;
        align-items: stretch;
        gap: .85rem;
    }

    /* The whole hero centers when narrow: logo, title, subtitle and
       chips. With only the logo centered and everything else left
       aligned it looks like an accident — they should all follow one
       axis. */
    .qr-hero-icon {
        align-self: center;
    }

    .qr-hero-text {
        flex-basis: auto;
        text-align: center;
    }

    .qr-chips {
        margin-left: 0;
        width: 100%;
        justify-content: center;
    }
}

@media (max-width: 480px) {
    body.qr-page {
        align-items: flex-start;
        padding: 10px;
    }

    .qr-shell {
        padding: .85rem;
        gap: .85rem;
        border-radius: var(--radius);
    }

    .qr-hero {
        padding: 1rem;
        gap: .75rem;
    }

    .qr-hero-text h1 {
        font-size: 1.15rem;
    }

    .left-panel,
    .right-panel {
        padding: .9rem;
    }

    /* One column on a phone — a 5.5rem label next to a long name is
       too tight. */
    .locked-row {
        grid-template-columns: minmax(0, 1fr);
        gap: .2rem;
    }

    .qr-placeholder {
        min-height: 220px;
    }

    .terms-modal-footer {
        flex-direction: column-reverse;
    }

    .terms-btn {
        width: 100%;
    }
}
