/* ============================================================
   MEMO – Warm Premium Dark Theme
   Boutique photo-memories aesthetic · mobile-first · no Bootstrap
   ============================================================ */

/* ---- Design tokens ---- */
:root {
    /* Warm graphite surfaces */
    --bg-primary:      #141210;
    --bg-secondary:    #1a1714;
    --bg-card:         #1f1b17;
    --bg-card-hover:   #26221c;
    --bg-card-active:  #2f2a22;

    /* Soft gold accent */
    --accent:          #c9a96a;
    --accent-strong:   #b08d4f;
    --accent-soft:     #e4cf9f;
    --accent-glow:     rgba(201, 169, 106, 0.22);

    /* Warm ink / cream text */
    --text-primary:    #f5f1ea;
    --text-secondary:  #b8ae9e;
    --text-muted:      #8a8175;

    --border:          rgba(245, 241, 234, 0.08);
    --border-strong:   rgba(245, 241, 234, 0.14);
    --border-focus:    rgba(201, 169, 106, 0.55);

    --danger:          #e28a7a;

    --radius-sm:       10px;
    --radius-md:       16px;
    --radius-lg:       22px;
    --radius-xl:       30px;

    --shadow-card:     0 10px 34px rgba(0,0,0,0.42);
    --shadow-soft:     0 4px 18px rgba(0,0,0,0.30);
    --shadow-glow:     0 0 24px rgba(201,169,106,0.14);

    --font-body:       'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --font-display:    'Fraunces', Georgia, 'Times New Roman', serif;

    --transition:      0.22s cubic-bezier(0.4, 0, 0.2, 1);
}

/* ---- Reset / Base ---- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
    font-size: 16px;
    -webkit-text-size-adjust: 100%;
}

body {
    font-family: var(--font-body);
    background: var(--bg-primary);
    color: var(--text-primary);
    line-height: 1.6;
    min-height: 100vh;
    overflow-x: hidden;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

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

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

button { cursor: pointer; border: none; background: none; font-family: inherit; font-size: inherit; color: inherit; }

/* The controls the browser draws itself. Since the page declares itself dark (App.razor,
   2026-10-02) they come in their dark form, and the tick would otherwise be the system's own pale
   blue — the one blue on the site. Gold, as the delivery options and the photo sliders already were. */
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }

/* ---- Typography ---- */
h1 { font-family: var(--font-display); font-size: clamp(2.5rem, 10vw, 5rem); font-weight: 500; letter-spacing: -0.02em; line-height: 1.05; }
h2 { font-family: var(--font-display); font-size: clamp(1.35rem, 4vw, 1.9rem); font-weight: 500; letter-spacing: -0.01em; }
h3 { font-size: 1.05rem; font-weight: 600; letter-spacing: -0.01em; }

/* ================================================================
   LANDING PAGE
   ================================================================ */
.landing {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.landing-hero {
    width: 100%;
    min-height: 100svh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 2rem 1.5rem;
    background:
        radial-gradient(ellipse 90% 55% at 50% -5%, rgba(201,169,106,0.12) 0%, transparent 65%),
        radial-gradient(ellipse 70% 45% at 85% 90%, rgba(176,141,79,0.10) 0%, transparent 60%),
        var(--bg-primary);
    gap: 1.5rem;
}

.landing-logo {
    margin-bottom: 0.25rem;
}

.logo-text {
    display: block;
    font-family: var(--font-display);
    font-size: clamp(2.4rem, 13vw, 7rem);
    font-weight: 500;
    letter-spacing: 0.04em;
    background: linear-gradient(180deg, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-strong) 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    text-shadow: 0 2px 40px rgba(201,169,106,0.18);
}

/* Samsung Internet only, and only the gold (2026-10-02). With the phone in dark mode it recolours
   every page itself, and it darkens a background where it keeps a text colour light — so the
   gradient above, being a background, came out as a brown barely off the black behind it, on the
   owner's own phone. A plain text colour is what survives it: the navbar's name, the same gold
   written as `color`, stayed gold in the same screenshot. The brand gold rather than one of the
   paler ones beside it: measured on that screenshot, the browser keeps a colour's hue but takes
   about half its strength, and a paler gold would arrive as beige. Why the class is there, and
   what it is keyed on: App.razor. */
.samsung-internet .logo-text {
    background: none;
    color: var(--accent);
}

.btn-start {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 1.05rem 3.5rem;
    background: linear-gradient(180deg, var(--accent-soft), var(--accent) 60%, var(--accent-strong));
    color: #241d12;
    font-size: 1rem;
    font-weight: 700;
    letter-spacing: 0.22em;
    border-radius: 100px;
    transition: var(--transition);
    box-shadow: 0 8px 22px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.25);
    text-decoration: none;
    margin: 0.5rem 0;
}

.btn-start:hover {
    transform: translateY(-2px);
    box-shadow: 0 12px 30px rgba(0,0,0,0.5), 0 0 26px var(--accent-glow), inset 0 1px 0 rgba(255,255,255,0.3);
    text-decoration: none;
}

/* A tagline between the logo and the button since 2026-09-11, not a footnote under it — and no
   longer a pill. A bordered capsule sitting directly on top of the gold capsule read as two
   buttons competing for the same press. Bare letterspaced type is a line of text; the button
   stays the only thing on the screen shaped like something you click. */
.landing-badge {
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.28em;
    color: var(--text-muted);
    text-transform: uppercase;
}

/* ---- Landing: story + how-to ---- */
.landing-story {
    width: 100%;
    max-width: 720px;
    padding: 1rem 1.5rem 2rem;
    text-align: center;
    display: flex;
    flex-direction: column;
    gap: 1.15rem;
}

.story-subtitle {
    font-size: clamp(0.7rem, 2.5vw, 0.85rem);
    font-weight: 600;
    letter-spacing: 0.28em;
    color: var(--accent);
    text-transform: uppercase;
}

.story-lead {
    font-family: var(--font-display);
    font-size: clamp(1.25rem, 4vw, 1.75rem);
    font-weight: 500;
    line-height: 1.35;
    color: var(--text-primary);
}

.story-lead em {
    font-style: italic;
    color: var(--accent);
}

.story-text {
    font-size: 1rem;
    line-height: 1.7;
    color: var(--text-secondary);
}

.landing-how {
    width: 100%;
    max-width: 720px;
    padding: 1rem 1.5rem 4rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1.5rem;
}

.landing-how h2 {
    text-align: center;
    color: var(--text-primary);
}

.how-steps {
    list-style: none;
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 0.85rem;
}

.how-steps li {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: 1.1rem 1.3rem;
    transition: var(--transition);
}

.how-steps li:hover {
    background: var(--bg-card-hover);
    border-color: var(--border-focus);
}

.how-num {
    flex-shrink: 0;
    width: 2rem;
    height: 2rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: linear-gradient(180deg, var(--accent-soft), var(--accent) 60%, var(--accent-strong));
    color: #241d12;
    font-weight: 700;
    font-size: 0.95rem;
    box-shadow: inset 0 1px 0 rgba(255,255,255,0.25);
}

.how-steps li p {
    color: var(--text-secondary);
    line-height: 1.55;
    padding-top: 0.15rem;
}

.how-note {
    max-width: 620px;
    text-align: center;
    font-size: 0.95rem;
    line-height: 1.7;
    color: var(--text-muted);
}

/* The delivery time and the line about why the wait is worth it are one thought in two
   paragraphs. Left to the section's own 1.5rem gap they would sit as far from each other as from
   the link to the magazine, which is a different subject entirely. */
.how-outro {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.7rem;
}

/* The film under "Sådan gør du" (2026-10-08). Closed, it is one muted line and two small outlined
   tabs, so that on a phone the section still opens on its five steps; the player only takes room
   once a tab is pressed. */
.how-video {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.6rem;
}

.how-video-label {
    font-size: 0.85rem;
    color: var(--text-muted);
    text-align: center;
}

.how-video-tabs {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.5rem;
}

.how-video-tab {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    min-height: 2.5rem;
    padding: 0.4rem 1.05rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--accent);
    font-size: 0.9rem;
    font-weight: 600;
    transition: var(--transition);
}

/* U+FE0E asks for the text form of the triangle: without it iOS draws a coloured emoji button. */
.how-video-tab::before { content: "\25B6\FE0E"; font-size: 0.7em; }
/* Pointer devices only: a phone keeps :hover on whatever was last tapped, so a tab just closed
   would go on looking half-pressed. */
@media (hover: hover) {
    .how-video-tab:hover { border-color: var(--border-focus); background: var(--bg-card-hover); }
}
.how-video-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The open film's tab is filled and shows the cross, since pressing it again is how it closes. */
.how-video-tab[aria-pressed="true"] {
    background: var(--accent);
    border-color: var(--accent);
    color: #241d12;
}
.how-video-tab[aria-pressed="true"]::before { content: "\2715"; font-size: 0.8em; }

/* Sized by its height, so the 9:16 frame keeps its shape: 568 px tall (320 wide) on a phone held
   upright, and less when the window is shorter than that — a phone on its side still sees the
   whole film without scrolling. Opening a film scrolls it to the top of the window (see the script
   in App.razor); the margin is what keeps the fixed navbar off it there. */
.how-video-player {
    display: block;
    height: min(568px, calc(100svh - 7.5rem));
    width: auto;
    max-width: 100%;
    aspect-ratio: 9 / 16;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: #000;
    scroll-margin-top: calc(54px + env(safe-area-inset-top) + 0.75rem);
}

/* `display: block` above outranks the browser's own rule for the hidden attribute, which is how
   a "hidden" element stayed on screen in the PR #131 sweep. */
.how-video-player[hidden] { display: none; }

/* ================================================================
   BUILDER
   ================================================================ */
.builder {
    min-height: 100svh;
    display: flex;
    flex-direction: column;
    background: var(--bg-primary);
}

/* Mobile-first: back arrow + five step dots + "Nyt magasin" have to share a
   375px phone, so the tighter padding and gap are the default and the roomier
   ones are restored at min-width: 480px below. */
.builder-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* Five step dots plus the back arrow and "Nyt magasin" stop fitting on one line somewhere
       under 320px. body carries overflow-x:hidden, so the row did not scroll — it was simply
       cut off, and "Nyt magasin" lost a third of itself past the right edge with no way to
       reach it. Wrapping costs nothing at ordinary phone widths, where it never triggers. */
    flex-wrap: wrap;
    gap: 0.4rem;
    padding: 0.75rem 0.7rem;
    background: rgba(26, 23, 20, 0.85);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: calc(54px + env(safe-area-inset-top)); /* below fixed navbar */
    z-index: 100;
}

/* Just the arrow: the wordmark already sits in the navbar above this header. */
.builder-back {
    font-size: 1.25rem;
    line-height: 1;
    font-weight: 700;
    color: var(--text-secondary);
    text-decoration: none;
    padding: 0.25rem 0.5rem 0.25rem 0;
    flex-shrink: 0;
}

.builder-back:hover { color: var(--accent); text-decoration: none; }

.builder-newalbum {
    background: none;
    border: none;
    cursor: pointer;
    font-family: inherit;
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--text-secondary);
    white-space: nowrap;
    flex-shrink: 0;
}

.builder-newalbum:hover { color: var(--accent); }

/* "Gemt automatisk" and "Nyt magasin" share the header's third slot. They wrap against each
   other before the header itself wraps, which is what keeps the step dots on one line at 320px —
   see the note on .builder-header. */
.builder-header-end {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.3rem 0.7rem;
    min-width: 0;
}

/* Deliberately quiet: it is a fact to be found when looked for, not an announcement. A saved
   indicator that draws the eye every time something is written would be its own kind of noise. */
.builder-saved {
    font-size: 0.7rem;
    letter-spacing: 0.02em;
    color: var(--text-muted);
    white-space: nowrap;
}

.builder-saved::before {
    content: "✓";
    margin-right: 0.25rem;
    color: var(--accent);
}

.step-indicators {
    display: flex;
    gap: 0.5rem;
    /* The dots read as one control, so they stay on a single line and keep their size: when
       the header runs out of room it is "Nyt magasin" that drops to the next line. */
    flex-shrink: 0;
}

.step-dot {
    width: 1.9rem;
    height: 1.9rem;
    border-radius: 50%;
    background: var(--bg-card);
    border: 1px solid var(--border-strong);
    color: var(--text-muted);
    font-size: 0.75rem;
    font-weight: 600;
    transition: var(--transition);
    display: flex;
    align-items: center;
    justify-content: center;
}

.step-dot.active {
    background: linear-gradient(180deg, var(--accent-soft), var(--accent) 65%, var(--accent-strong));
    border-color: var(--accent);
    color: #241d12;
    box-shadow: 0 2px 10px rgba(0,0,0,0.35), 0 0 14px var(--accent-glow);
    transform: scale(1.12);
}

.step-dot.done {
    background: rgba(201,169,106,0.12);
    border-color: rgba(201,169,106,0.45);
    color: var(--accent);
}

/* Above phone width the builder header can breathe again. The dots keep their
   full size at every width now that the back link is a single arrow. */
@media (min-width: 480px) {
    .builder-header { gap: 1rem; padding: 1rem 1.25rem; }
    .builder-newalbum { font-size: 0.8125rem; letter-spacing: 0.04em; }
}

.builder-content {
    flex: 1;
    padding: 1.25rem;
    max-width: 900px;
    width: 100%;
    margin: 0 auto;
}

.builder-nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1rem;
    padding: 0.9rem 1.25rem calc(0.9rem + env(safe-area-inset-bottom));
    background: rgba(26, 23, 20, 0.9);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-top: 1px solid var(--border);
    position: sticky;
    bottom: 0;
    z-index: 100;
}

.builder-nav .app-btn { flex: 0 1 auto; min-width: 7rem; justify-content: center; }

.builder-loading {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    color: var(--text-secondary);
}

.spinner {
    width: 40px;
    height: 40px;
    border: 3px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

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

/* ---- StepCard ---- */
.step-card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 1.5rem 1.35rem;
    margin-bottom: 1rem;
    box-shadow: var(--shadow-soft);
}

.step-card__title {
    font-family: var(--font-display);
    font-size: 1.5rem;
    font-weight: 500;
    letter-spacing: -0.01em;
    margin-bottom: 0.25rem;
}

.step-card__subtitle {
    color: var(--text-secondary);
    font-size: 0.85rem;
    margin-bottom: 1rem;
}

.step-card__content { margin-top: 1rem; }

/* ---- AppButton ---- */
.app-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.75rem 1.75rem;
    border-radius: 100px;
    font-weight: 700;
    font-size: 0.9rem;
    letter-spacing: 0.05em;
    transition: var(--transition);
    white-space: nowrap;
}

.app-btn--primary {
    background: linear-gradient(180deg, var(--accent-soft), var(--accent) 60%, var(--accent-strong));
    color: #241d12;
    box-shadow: 0 4px 14px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.25);
}

.app-btn--primary:hover:not(:disabled) {
    transform: translateY(-1px);
    box-shadow: 0 6px 18px rgba(0,0,0,0.4), 0 0 18px var(--accent-glow), inset 0 1px 0 rgba(255,255,255,0.3);
}

.app-btn--secondary {
    background: var(--bg-card-hover);
    color: var(--text-secondary);
    border: 1px solid var(--border);
}

.app-btn--secondary:hover:not(:disabled) {
    background: var(--bg-card-active);
    color: var(--text-primary);
}

.app-btn--disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

.app-btn--danger {
    background: var(--danger);
    color: #241d12;
    border: 1px solid transparent;
}

.app-btn--danger:hover:not(:disabled) { filter: brightness(1.08); transform: translateY(-1px); }

.app-btn--danger-ghost {
    background: transparent;
    color: var(--danger);
    border: 1px solid var(--border-strong);
}

.app-btn--danger-ghost:hover:not(:disabled) { background: rgba(226,138,122,0.12); border-color: var(--danger); }

/* ---- Upload Zone ---- */
.upload-zone {
    border: 2px dashed var(--border);
    border-radius: var(--radius-md);
    padding: 2.5rem 1rem;
    text-align: center;
    transition: var(--transition);
    cursor: pointer;
    position: relative;
}

.upload-zone:hover, .upload-zone.drag-over {
    border-color: var(--accent);
    background: rgba(201,169,106,0.04);
}

.upload-input {
    position: absolute;
    inset: 0;
    opacity: 0;
    width: 100%;
    height: 100%;
    cursor: pointer;
}

.upload-label {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    align-items: center;
    color: var(--text-secondary);
    font-size: 0.9rem;
    pointer-events: none;
}

.upload-icon { font-size: 2rem; }

.upload-hint {
    font-size: 0.75rem;
    color: var(--text-muted);
}

/* The batching note under the drop zone. The bare .upload-hint is a span inside the zone's own
   label, which is centred and pointer-events: none; this one is a paragraph in the flow of the
   step, so it needs its own measure and room to breathe. */
.upload-hint--batches {
    margin: 0.75rem 0 0;
    line-height: 1.5;
    max-width: 46ch;
}

/* The zone while a selection goes up: nothing to press, and the same dashed box as before, drawn in
   the gold it takes under a hovering file so it reads as the thing at work. */
.upload-zone.is-busy {
    cursor: default;
    border-color: var(--accent);
    background: rgba(201,169,106,0.04);
}

.upload-busy {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.6rem;
}

.upload-busy-title {
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--text-primary);
}

.upload-busy .upload-progress {
    align-self: stretch;
    max-width: 320px;
    width: 100%;
    margin: 0.2rem auto 0;
    height: 6px;
    border-radius: 3px;
}

.upload-busy .upload-status { margin-top: 0; }

.upload-busy-note {
    font-size: 0.75rem;
    line-height: 1.5;
    color: var(--text-muted);
    max-width: 34ch;
}

/* After a selection has gone up: the word that it worked, in the green the preview's "Klar til
   tryk" uses. */
.upload-done {
    margin-top: 0.9rem;
    padding: 0.55rem 0.8rem;
    border-radius: var(--radius-sm);
    background: rgba(143, 214, 164, 0.10);
    color: #8fd6a4;
    font-size: 0.85rem;
    text-align: center;
}

.upload-progress {
    height: 4px;
    background: var(--border);
    border-radius: 2px;
    margin-top: 1rem;
    overflow: hidden;
}

.upload-progress-bar {
    height: 100%;
    background: var(--accent);
    border-radius: 2px;
    transition: width 0.3s ease;
}

.upload-status {
    font-size: 0.8rem;
    color: var(--text-secondary);
    margin-top: 0.5rem;
    text-align: center;
}

.upload-errors {
    margin-top: 0.75rem;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.upload-error-item {
    font-size: 0.8rem;
    color: #f87171;
    background: rgba(248,113,113,0.1);
    border-radius: var(--radius-sm);
    padding: 0.4rem 0.75rem;
}

.upload-empty {
    text-align: center;
    color: var(--text-muted);
    font-size: 0.9rem;
    padding: 1.5rem;
}

/* ---- Photo Grid ---- */
.photo-grid-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin: 1.25rem 0 0.75rem;
}

.photo-count {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-secondary);
    letter-spacing: 0.05em;
    text-transform: uppercase;
}

.photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
    gap: 0.5rem;
}

/* Non-clipped wrapper so the quality-warning popup can escape the card's overflow:hidden. */
.photo-card-wrap {
    position: relative;
}

.photo-card {
    position: relative;
    border-radius: var(--radius-sm);
    overflow: hidden;
    aspect-ratio: 1;
    background: var(--bg-card-hover);
    border: 2px solid transparent;
    transition: var(--transition);
}

.photo-card.is-cover  { border-color: var(--accent); }
.photo-card.is-back-cover { border-color: #818cf8; }

.photo-thumb {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Shown in a photo slot while its thumbnail is still being generated in the background. */
.photo-thumb-pending {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-card);
}

.spinner-sm {
    width: 20px;
    height: 20px;
    border: 2px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

.photo-badge {
    position: absolute;
    bottom: 2px;
    left: 2px;
    font-size: 0.6rem;
    font-weight: 700;
    padding: 1px 5px;
    border-radius: 4px;
    letter-spacing: 0.05em;
}

.cover-badge  { background: var(--accent); color: #241d12; }
.back-badge   { background: #818cf8; color: #fff; }

.photo-delete-btn {
    position: absolute;
    top: 3px;
    right: 3px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: rgba(0,0,0,0.7);
    color: #fff;
    font-size: 0.65rem;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Visible without hovering: the app is mobile-first, and a touch screen never produces the
       hover that used to be the only way to reveal this button. */
    opacity: 0.6;
    transition: var(--transition);
    border: none;
}

.photo-card:hover .photo-delete-btn,
.photo-delete-btn:focus-visible { opacity: 1; }
.photo-delete-btn:hover { background: #ef4444; opacity: 1; }

/* --- Reordering ------------------------------------------------------------------------- */

/* The one place on the card where a finger means "move this photo" instead of "scroll the
   page" — which is what touch-action says, and saying it here rather than on the card is the
   whole reason the drag has a handle at all. It sits opposite the delete button so a thumb
   reaching for one is not over the other. */
.photo-drag-handle {
    position: absolute;
    top: 3px;
    left: 3px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    border: none;
    background: rgba(0,0,0,0.7);
    color: #fff;
    font-size: 0.7rem;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0.6;
    transition: var(--transition);
    cursor: grab;
    touch-action: none;
}

.photo-card:hover .photo-drag-handle,
.photo-drag-handle:focus-visible { opacity: 1; }
.photo-drag-handle:active { cursor: grabbing; }

/* Lifted, and out from under the pointer's own hit-testing so the cards underneath can still
   report where the drop would land. */
.photo-card-wrap.is-dragging {
    z-index: 10;
    pointer-events: none;
}

.photo-card-wrap.is-dragging .photo-card {
    box-shadow: 0 8px 24px rgba(0,0,0,0.35);
    opacity: 0.9;
    transform: scale(1.04);
}

/* Where it would land. A line down the edge of a card rather than a gap opening up: opening a
   gap means moving every card after it, and the drag deliberately moves nothing — see
   js/photo-reorder.js. */
.photo-grid.is-reordering .photo-card-wrap::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    width: 3px;
    border-radius: 2px;
    background: var(--accent, #2563eb);
    opacity: 0;
    transition: opacity 0.1s ease;
}

.photo-grid.is-reordering .photo-card-wrap.is-drop-before::after { left: -0.3rem; opacity: 1; }
.photo-grid.is-reordering .photo-card-wrap.is-drop-after::after { right: -0.3rem; opacity: 1; }

/* Nothing is grabbable while the server is renumbering — a second drag would be measured
   against a list the first one has already replaced. */
.photo-grid.is-busy .photo-drag-handle {
    opacity: 0.3;
    pointer-events: none;
}

.photo-grid-tools {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
    justify-content: flex-end;
}

.photo-grid-hint {
    font-size: 0.75rem;
    color: var(--text-secondary);
}

.photo-sort-btn {
    padding: 0.3rem 0.7rem;
    font-size: 0.75rem;
}

/* The two orders sit together as one control, so it reads as a choice between them rather than
   as two unrelated buttons. */
.photo-sort-switch {
    display: flex;
    gap: 0.25rem;
}

/* The order the album is already in. It is disabled as well as marked — pressing it would do
   nothing — so it has to stay legible rather than fade out like an unavailable button. */
.photo-sort-btn.is-active,
.photo-sort-btn.is-active:disabled {
    background: rgba(201, 169, 106, 0.16);
    border-color: var(--border-focus);
    color: var(--accent);
    opacity: 1;
    cursor: default;
}

.photo-clear-btn:hover:not(:disabled) {
    border-color: var(--danger);
    color: var(--danger);
}

/* Asks before the whole album goes. Not on a card, because it is not about one photo. */
.photo-clear-confirm {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin: 0 0 0.75rem;
    padding: 0.75rem 1rem;
    border: 1px solid var(--danger);
    border-radius: 8px;
    background: rgba(226, 138, 122, 0.1);
}

.photo-clear-confirm-text {
    margin: 0;
    font-size: 0.85rem;
    line-height: 1.4;
}

.photo-clear-confirm-text small { color: var(--text-secondary); }

/* The same box once the answer has been given, so the panel does not jump: the question and the
   work that follows it sit in one place. It drops the danger red — nothing is being asked any
   more, and red on a progress line reads as a failure. */
.photo-clear-confirm--working {
    border-color: var(--border);
    background: var(--bg-card-hover);
}

/* The spinner is a span inside a paragraph here, so it has to be told to take a box. */
.photo-clear-confirm--working .spinner-sm {
    display: inline-block;
    vertical-align: -0.3rem;
    margin-right: 0.5rem;
}

/* Asks before the photo goes. It covers the card it belongs to, so there is never a question
   about which photo is being deleted. */
.photo-delete-confirm {
    position: absolute;
    inset: 0;
    z-index: 4;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.4rem;
    text-align: center;
    background: rgba(12, 10, 8, 0.92);
    backdrop-filter: blur(2px);
}

.photo-delete-confirm-text {
    font-size: 0.68rem;
    line-height: 1.35;
    color: var(--text-primary);
}

.photo-delete-confirm-text small { color: var(--text-muted); }

.photo-delete-confirm-actions {
    display: flex;
    gap: 0.35rem;
    flex-wrap: wrap;
    justify-content: center;
}

.photo-delete-confirm-actions .app-btn {
    padding: 0.25rem 0.5rem;
    font-size: 0.65rem;
    min-height: 0;
}

/* Low print-quality warning: small round red badge with "!" over the thumbnail. */
.photo-quality-warning {
    position: absolute;
    bottom: 4px;
    right: 4px;
    z-index: 3;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: #ef4444;
    color: #fff;
    font-size: 0.8rem;
    font-weight: 800;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px solid #fff;
    box-shadow: 0 1px 4px rgba(0,0,0,0.5);
    cursor: pointer;
}

.photo-quality-warning:hover,
.photo-quality-warning.is-open {
    background: #dc2626;
    transform: scale(1.08);
}

.photo-quality-info {
    position: absolute;
    bottom: calc(100% - 2px);
    right: 0;
    z-index: 10;
    width: max(200px, 100%);
    max-width: 260px;
    background: var(--bg-card-active);
    color: var(--text-primary);
    border: 1px solid var(--danger);
    border-radius: var(--radius-sm);
    padding: 0.7rem 0.8rem;
    font-size: 0.75rem;
    line-height: 1.45;
    box-shadow: var(--shadow-card);
}

.photo-quality-info strong { color: var(--danger); }

/* Cover selector selectable */
.cover-selectable {
    padding: 0;
    text-align: left;
    width: 100%;
    cursor: pointer;
}

/* ---- Layout Selector ---- */
.layout-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 0.75rem;
}

.layout-option {
    background: var(--bg-card-hover);
    border: 2px solid var(--border);
    border-radius: var(--radius-md);
    padding: 1rem 0.75rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    transition: var(--transition);
    cursor: pointer;
}

.layout-option:hover {
    border-color: rgba(201,169,106,0.4);
    background: var(--bg-card-active);
}

.layout-option.active {
    border-color: var(--accent);
    background: rgba(201,169,106,0.08);
    box-shadow: 0 0 16px var(--accent-glow);
}

.layout-preview {
    display: grid;
    gap: 3px;
    width: 72px;
    height: 72px;
}

.layout-preview--1  { grid-template-columns: 1fr; }
.layout-preview--2  { grid-template-columns: 1fr; grid-template-rows: repeat(2, 1fr); }
.layout-preview--4  { grid-template-columns: 1fr 1fr; }
.layout-preview--8  { grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(4, 1fr); }
.layout-preview--12 { grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(4, 1fr); }

.layout-cell {
    background: var(--text-muted);
    border-radius: 2px;
}

.layout-option.active .layout-cell { background: var(--accent); }

.layout-label {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-secondary);
}

.layout-option.active .layout-label { color: var(--accent); }

/* The capacity sits under the name, a step quieter: it is what the choice buys, not what the
   choice is called. Tabular figures so 40 / 80 / 160 / 320 / 480 line up down the row. */
.layout-capacity {
    display: block;
    margin-top: 0.15rem;
    font-size: 0.7rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.layout-option.active .layout-capacity { color: var(--accent-soft); }

/* ---- Options (size/orientation) ---- */
.option-group { margin-bottom: 1.25rem; }
.option-group:last-child { margin-bottom: 0; }

.option-label {
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 0.5rem;
}

.option-pills {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.pill {
    padding: 0.5rem 1.25rem;
    border: 1px solid var(--border);
    border-radius: 100px;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text-secondary);
    background: var(--bg-card-hover);
    transition: var(--transition);
    cursor: pointer;
}

.pill:hover { border-color: rgba(201,169,106,0.4); color: var(--text-primary); }

.pill.active {
    background: var(--accent);
    border-color: var(--accent);
    color: #241d12;
}

/* ---- Cover Sections ---- */
.cover-section { margin-bottom: 2rem; }
.cover-section:last-child { margin-bottom: 0; }

.cover-section-title {
    font-family: var(--font-display);
    font-size: 1.2rem;
    font-weight: 500;
    margin-bottom: 0.25rem;
}

.cover-hint {
    font-size: 0.8rem;
    color: var(--text-secondary);
    margin-bottom: 0.75rem;
}

.cover-text-label {
    display: block;
    font-size: 0.8rem;
    color: var(--text-secondary);
    margin: 0.25rem 0 0.35rem;
}

.cover-text-input {
    width: 100%;
    padding: 0.7rem 0.9rem;
    margin-bottom: 0.75rem;
    background: var(--bg-card-hover);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    font-family: inherit;
    font-size: 0.95rem;
    transition: var(--transition);
}

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

.cover-text-input:focus {
    outline: none;
    border-color: var(--border-focus);
    box-shadow: 0 0 0 3px rgba(201,169,106,0.12);
}

/* ---- The inside greeting ----
   Not a third cover control but a note written into the book, so it gets a panel of its own,
   set back from the two cover sides above it rather than starting where they end. */
.cover-greeting {
    margin-top: 2rem;
    padding: 1.35rem 1.15rem 1.25rem;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.cover-greeting > :last-child { margin-bottom: 0; }

/* Prose wants a measure. Run to the full width of the step the hint made a line too long to
   track back from, and left the field looking like it began mid-sentence. */
.cover-greeting-hint {
    max-width: 62ch;
    margin-bottom: 1.1rem;
}

/* Field and counter share one frame: the count belongs to the box it counts, and standing on
   its own under it, it read as a stray line of the page's own text. */
.greeting-field {
    background: var(--bg-card-hover);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    overflow: hidden;
    transition: var(--transition);
}

.greeting-field:focus-within {
    border-color: var(--border-focus);
    box-shadow: 0 0 0 3px rgba(201,169,106,0.12);
}

/* The greeting is prose, not a title: it gets several lines and grows downwards only, so
   dragging it wider cannot break the column the rest of the step is laid out in. It is centred
   and set in the cover's face because that is how it prints — the family comes from the
   component, which knows which face the cover was given. */
.greeting-input {
    display: block;
    width: 100%;

    /* Grows with the greeting where the browser can do it on its own, so a dedication of a few
       lines is read whole instead of scrolling inside a four-line box with its last line cut
       across the middle. Where it cannot, the field keeps the height below and the customer can
       still drag it taller — the same field, one convenience less. */
    field-sizing: content;
    min-height: 7rem;
    max-height: 24rem;
    padding: 1.2rem 1.2rem 0.85rem;
    background: none;
    border: none;
    color: var(--text-primary);
    font-size: 1.05rem;
    line-height: 1.75;
    text-align: center;
    resize: vertical;
}

.greeting-input:focus { outline: none; }
.greeting-input::placeholder { color: var(--text-muted); }

/* Quiet until the room is nearly used up: a reassurance while there is space and a warning when
   there is not, and it should only raise its voice for the second one. */
.greeting-counter {
    font-size: 0.72rem;
    color: var(--text-muted);
    text-align: right;
    padding: 0 1.2rem 0.75rem;
}

.greeting-counter--low { color: var(--accent); }

/* ---- Front/back cover side by side ----
   They print on the same sheet, so showing both at once keeps the customer from
   filling in the front and never noticing the back. Stacks on narrow screens. */
.cover-sides {
    display: grid;
    /* min() rather than a bare 240px: on a screen narrower than the track minimum the column
       refuses to shrink, and the card's right edge disappears under the viewport. */
    grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
    gap: 1.5rem;
}

.cover-side {
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: 1rem;
}

.cover-side-drop { padding: 1.75rem 0.75rem; }

.cover-side-photo {
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--bg-card-hover);
    aspect-ratio: 3 / 4;
}

.cover-side-thumb {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.cover-side-actions {
    display: flex;
    gap: 0.5rem;
    margin-top: 0.6rem;
}

.cover-side-btn {
    position: relative;
    overflow: hidden;
    flex: 1;
    text-align: center;
    padding: 0.5rem 0.6rem;
    background: var(--bg-card-hover);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 0.8rem;
    cursor: pointer;
    transition: var(--transition);
}

.cover-side-btn:hover { border-color: var(--accent); color: var(--text-primary); }
.cover-side-btn-remove:hover { border-color: #ef4444; color: #f87171; }

/* ---- Cover design controls (photo placement + free text position) ---- */
.cover-control-heading {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-secondary);
    margin: 1rem 0 0.55rem;
}

/* Photo placement: three clean framed thumbnails */
.photo-placement-picker {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.6rem;
    margin-bottom: 0.5rem;
}

.placement-opt {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    padding: 0.7rem 0.4rem;
    background: var(--bg-card-hover);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    color: var(--text-secondary);
    font-family: inherit;
    cursor: pointer;
    transition: var(--transition);
}

.placement-opt:hover { border-color: var(--border-strong); }

.placement-opt.is-active {
    border-color: var(--accent);
    background: rgba(201,169,106,0.12);
    color: var(--text-primary);
}

.placement-name { font-size: 0.72rem; font-weight: 600; }

.placement-mini {
    --mf: 4px;
    position: relative;
    display: block;
    width: 46px;
    height: 60px;
    border-radius: 5px;
    overflow: hidden;
    background: var(--bg-primary);
    border: 1px solid var(--border-strong);
}

.placement-mini .pm-photo {
    position: absolute;
    left: var(--mf);
    right: var(--mf);
    border-radius: 2px;
    background: linear-gradient(135deg, var(--accent-soft), var(--accent-strong));
}

.pm-fill   .pm-photo { top: var(--mf); bottom: var(--mf); }
.pm-top    .pm-photo { top: var(--mf); height: 58%; }
.pm-bottom .pm-photo { bottom: var(--mf); height: 58%; }

/* ---- Page-limit banner ---- */
.page-limit-banner {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    padding: 0.85rem 1rem;
    border-radius: var(--radius-md);
    margin-bottom: 1.25rem;
    border: 1px solid transparent;
}

.page-limit-banner--ok {
    background: rgba(201,169,106,0.10);
    border-color: rgba(201,169,106,0.35);
}

.page-limit-banner--warn {
    background: rgba(226,138,122,0.10);
    border-color: rgba(226,138,122,0.4);
}

.page-limit-icon { font-size: 1.1rem; line-height: 1.3; flex-shrink: 0; }
.page-limit-banner--ok .page-limit-icon { color: var(--accent); }
.page-limit-banner--warn .page-limit-icon { color: var(--danger); }

.page-limit-title { font-weight: 700; font-size: 0.9rem; margin-bottom: 0.15rem; }
.page-limit-hint { font-size: 0.8rem; color: var(--text-secondary); }
.page-limit-note { font-size: 0.72rem; color: var(--text-muted); margin-top: 0.3rem; }

/* ---- PDF Preview ---- */
.preview-info {
    display: flex;
    gap: 1rem;
    margin-bottom: 1.25rem;
    flex-wrap: wrap;
}

.preview-stat {
    flex: 1;
    min-width: 80px;
    background: var(--bg-card-hover);
    border-radius: var(--radius-md);
    padding: 0.75rem;
    text-align: center;
}

.preview-stat-value {
    display: block;
    font-size: 1.5rem;
    font-weight: 800;
    color: var(--accent);
}

.preview-stat-label {
    font-size: 0.7rem;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

/* ---- Album preview: readiness, view modes, spreads (.prv) ---- */
.prv-status {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 1rem;
    margin-bottom: 1rem;
    background: var(--bg-card-hover);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-md);
}

.prv-status--ok { border-left-color: #8fd6a4; }
.prv-status--warn { border-left-color: var(--danger); }

/* The ring reads "how far along is the album" before any number is parsed. */
.prv-ring {
    --pct: 0%;
    width: 76px;
    height: 76px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: conic-gradient(var(--ring-colour, var(--accent)) var(--pct), var(--bg-card-active) 0);
}

.prv-status--ok .prv-ring { --ring-colour: #8fd6a4; }

.prv-ring-face {
    width: 62px;
    height: 62px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--bg-card-hover);
    line-height: 1.1;
}

.prv-ring-value { font-size: 1.35rem; font-weight: 800; color: var(--text-primary); }
.prv-ring-unit  { font-size: 0.62rem; color: var(--text-muted); }

.prv-status-text { flex: 1; min-width: 190px; }
.prv-status-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.15rem; }
.prv-status--ok .prv-status-title { color: #8fd6a4; }
.prv-status-hint { font-size: 0.8rem; color: var(--text-secondary); }
.prv-status-note { font-size: 0.72rem; color: var(--text-muted); margin-top: 0.35rem; }

.prv-status-figures {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    font-size: 0.75rem;
    color: var(--text-secondary);
    white-space: nowrap;
}

.prv-status-figures b { color: var(--text-primary); font-size: 0.95rem; }

/* A resolution problem is only fixable if you can find which photo has it. */
.prv-alert {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    width: 100%;
    text-align: left;
    padding: 0.7rem 0.85rem;
    margin-bottom: 1rem;
    background: rgba(226,138,122,0.1);
    border: 1px solid rgba(226,138,122,0.35);
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    font-size: 0.82rem;
    transition: var(--transition);
}

.prv-alert:hover { background: rgba(226,138,122,0.18); }
.prv-alert-icon { color: var(--danger); font-size: 1rem; line-height: 1.3; }
.prv-alert-hint { display: block; color: var(--text-secondary); font-size: 0.75rem; font-weight: 400; }

/* The photos that will print soft, listed in reading order. The list is the fix-it queue: the
   customer works down it and a row leaves as soon as its crop is good enough, so what is on screen
   is always what is left. */
.prv-soft { margin-bottom: 1rem; }
.prv-soft .prv-alert { margin-bottom: 0; }
.prv-soft-chevron { margin-left: auto; color: var(--text-secondary); font-size: 0.7rem; }

.prv-soft-list {
    list-style: none;
    margin: 0.4rem 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.prv-soft-item {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    padding: 0.4rem 0.6rem;
    text-align: left;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    font-size: 0.8rem;
    cursor: pointer;
    transition: var(--transition);
}

.prv-soft-item:hover { background: var(--bg-card-hover); border-color: var(--border-strong); }

.prv-soft-thumb {
    width: 34px;
    height: 34px;
    object-fit: cover;
    border-radius: 3px;
    flex: none;
}

.prv-soft-where { display: flex; flex-direction: column; min-width: 0; }
.prv-soft-why { color: var(--text-secondary); font-size: 0.72rem; }
.prv-soft-go { margin-left: auto; color: var(--accent); font-size: 0.75rem; white-space: nowrap; }

.prv-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-bottom: 0.85rem;
}

.prv-modes {
    display: flex;
    gap: 2px;
    padding: 2px;
    background: var(--bg-card-hover);
    border: 1px solid var(--border);
    border-radius: 100px;
}

.prv-mode {
    padding: 0.35rem 0.9rem;
    border-radius: 100px;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-secondary);
    white-space: nowrap;
    transition: var(--transition);
}

.prv-mode:hover { color: var(--text-primary); }
.prv-mode.is-active { background: var(--accent); color: #241d12; }

.prv-hint {
    font-size: 0.8rem;
    color: var(--text-secondary);
    line-height: 1.5;
    margin-bottom: 0.85rem;
}

/* Centred, not packed to the left: the pages are a fixed width, so whatever the row cannot use
   is left over — and on a phone, where two of them fit and the third does not, all of that
   leftover collected against the right edge and stood the whole album off-centre. */
.preview-pages {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.75rem;
    padding-bottom: 0.5rem;
}

.preview-page {
    flex-shrink: 0;
    /* The page stands in for a real sheet, whose width in mm and aspect ratio come from the
       component (they differ between magazine/A4 and portrait/landscape). Print measurements
       below are scaled through --mm, so they hold at any preview size. */
    --page-w: 120px;
    --page-mm-w: 210;
    --page-ar: 0.75;
    /* Paper. The photo pages print white, and the printer's 1 cm border is white paper — drawn in
       the app's card colour it read as part of the surrounding UI rather than as the margin the
       customer is being asked to judge. */
    --paper: #f4f2ee;
    --mm: calc(var(--page-w) / var(--page-mm-w));
    width: var(--page-w);
    height: calc(var(--page-w) / var(--page-ar));
    border-radius: var(--radius-sm);
    overflow: hidden;
    position: relative;
    background: var(--bg-card-hover);
    /* An outline, not a border: with box-sizing:border-box a border eats into --page-w, and
       the millimetre maths above would then lay out against a page 2 px smaller than it
       thinks — shaving the 1 cm print border. An outline draws without taking layout space. */
    outline: 1px solid var(--border);
    outline-offset: -1px;
}

/* The large view is the same page at a bigger --page-w: as wide as the viewport allows,
   but never so tall that it stops fitting on screen — with the viewer's title bar and pager
   pinned above and below it, which is why it is 64 and not the whole height. svh rather than vh
   where the browser has it: on a phone vh is measured with the toolbars hidden, so a page sized
   by it is taller than the screen the customer actually has. */
.preview-page--lg {
    --page-w: min(84vw, calc(64vh * var(--page-ar)), 560px);
    --page-w: min(84vw, calc(64svh * var(--page-ar)), 560px);
}

/* The slot is the button that opens the page — resets first, then the affordance that says so:
   the page lifts under the cursor and the caption warms with it, so a thumbnail reads as
   something to press rather than something to look at. */
.preview-page-slot {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.2rem;
    padding: 0;
    border: none;
    background: none;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

.preview-page-slot .preview-page {
    transition: transform 130ms ease, box-shadow 130ms ease, outline-color 130ms ease;
}

.preview-page-slot:hover .preview-page {
    outline-color: var(--accent);
    transform: translateY(-2px);
    box-shadow: var(--shadow-card), var(--shadow-glow);
}

.preview-page-slot:hover .prv-page-caption { color: var(--accent-soft); }

/* Focus lands on the slot, but the page is what the customer is looking at, so that is where
   the ring goes. An outline again, not a border: the page's millimetre maths depend on it. */
.preview-page-slot:focus-visible { outline: none; }

.preview-page-slot:focus-visible .preview-page {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.prv-page-caption {
    font-size: 0.62rem;
    color: var(--text-muted);
    letter-spacing: 0.04em;
}

/* ---- Spreads: the magazine as it opens in the hand ---- */
.prv-spreads {
    display: flex;
    flex-direction: column;
    gap: 1.1rem;
    align-items: center;
}

/* Two pages meeting at the fold. The gradient down the join is the shadow a bound
   magazine casts into its own gutter — it is what makes a pair read as one opening. */
.prv-spread {
    align-items: stretch;
    position: relative;
    display: flex;
    background: var(--bg-card-hover);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-soft);
    padding: 0;
    border: none;
    font: inherit;
    color: inherit;
    cursor: pointer;
    transition: transform 130ms ease, box-shadow 130ms ease;
}

.prv-spread:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-card), var(--shadow-glow);
}

.prv-spread:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
}

.prv-spread::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 22px;
    transform: translateX(-50%);
    pointer-events: none;
    background: linear-gradient(to right,
        rgba(0,0,0,0) 0%, rgba(0,0,0,0.34) 45%, rgba(0,0,0,0.34) 55%, rgba(0,0,0,0) 100%);
    z-index: 5;
}

/* A cover has no facing page, so it has no fold to shade. */
.prv-spread--single::after { content: none; }

.prv-spread .preview-page { --page-w: min(38vw, 168px); }

.prv-spread-caption {
    position: absolute;
    bottom: -1.05rem;
    left: 50%;
    transform: translateX(-50%);
    font-size: 0.62rem;
    color: var(--text-muted);
    white-space: nowrap;
}

/* The unprinted faces of the cover sheet: real pages in the file, blank on paper. */
.preview-page--inside {
    background: var(--paper);
    display: flex;
    align-items: center;
    justify-content: center;
}

.prv-inside-hint {
    font-size: 0.55rem;
    line-height: 1.4;
    text-align: center;
    color: #9a938a;
    letter-spacing: 0.03em;
}

.prv-inside-hint small { font-size: 0.85em; opacity: 0.75; }

/* The greeting on page 2 or 43. Black on white, centred, and the line breaks the customer typed
   are the line breaks that print — so pre-wrap, not the collapsing whitespace of ordinary markup. */
.prv-inside-text {
    color: #000;
    text-align: center;
    line-height: 1.5;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.page-viewer-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(10,9,8,0.82);
    backdrop-filter: blur(2px);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    z-index: 520;
}

/* The viewer scrolls within itself. In Sider the page is drawn nearly as tall as the screen, and
   with the text fields under it the whole column is taller still — without a scroll of its own
   the pager below the fold could not be reached at all, and the swipe that tried went through to
   the album behind (reported 2026-09-24). overscroll-behavior keeps a scroll that hits the end
   from carrying on into that album; the rule on body below keeps it still in the first place. */
.page-viewer {
    /* Solid: at 0.94 the hint scrolling under the pager still showed through its buttons. */
    --viewer-bar: #0c0b0a;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    max-width: 100%;
    max-height: 100%;
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* The column scrolls when it runs out of room; nothing inside it gets squashed to make room. */
.page-viewer > * { flex-shrink: 0; }

body:has(.page-viewer-backdrop) { overflow: hidden; }

/* Pinned to the top of the scrolling column, so the ✕ is on screen wherever the customer has
   scrolled to. The bar takes the backdrop's own colour, near enough, and none of its
   transparency, so what scrolls under it disappears rather than showing through. */
.page-viewer-head {
    position: sticky;
    top: 0;
    z-index: 10;
    align-self: stretch;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 2.25rem;
    /* Room for the ✕ on both sides, so the title stays centred over the page. */
    padding: 0 2.5rem;
    background: var(--viewer-bar);
}

.page-viewer-close {
    position: absolute;
    right: 0.25rem;
    top: 50%;
    transform: translateY(-50%);
}

.page-viewer-title {
    font-family: var(--font-display);
    font-size: 1.05rem;
    text-align: center;
}

.page-viewer-of { color: var(--text-secondary); font-size: 0.8rem; margin-left: 0.4rem; }

.page-viewer-note {
    font-size: 0.75rem;
    color: var(--text-secondary);
    text-align: center;
}

/* Pinned to the bottom for the same reason the title bar is pinned to the top: Luk and the pager
   stay in reach however far down the text fields the customer has gone. */
.page-viewer-actions {
    position: sticky;
    bottom: 0;
    z-index: 10;
    align-self: stretch;
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    justify-content: center;
    padding: 0.5rem 0 0.25rem;
    background: var(--viewer-bar);
}

/* Turning a whole page at once. One button per content page on show — a spread gets two, each
   naming its own page — sitting under the sheet they act on and above the pager. */
.prv-turn-row { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; }

.prv-turn {
    padding: 0.4rem 0.85rem;
    background: transparent;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-size: 0.8rem;
    font-weight: 600;
    cursor: pointer;
    transition: var(--transition);
}

.prv-turn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent-soft); }
.prv-turn:disabled { opacity: 0.5; cursor: default; }

/* ---- The words on a page ------------------------------------------------------------------
   The fields where they are typed live under the opened page; the boxes themselves are placed on
   the page and styled with .page-text further down. One group per page on show — a spread has two
   — and inside a group one row per box, in the order they were added. */
.prv-pagetext {
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
    width: 100%;
    max-width: 30rem;
    margin: 0 auto;
}

.prv-pagetext-group {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

/* The label names the page the group writes on, so over a spread it is what tells the two groups
   apart. Its own line rather than beside the field: a group can hold several rows, and a label
   attached to the first of them would read as belonging to that one alone. */
.prv-pagetext-label {
    margin: 0;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.prv-pagetext-row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

.prv-pagetext-input {
    flex: 1 1 auto;
    min-width: 0;
    background: var(--bg-card-hover);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.45rem 0.7rem;
    font-size: 0.85rem;
    font-family: inherit;
    color: var(--text-primary);
}

.prv-pagetext-input:focus {
    border-color: var(--border-focus);
    box-shadow: 0 0 0 3px rgba(201,169,106,0.22);
    outline: none;
}

/* Square, so it cannot be mistaken for part of the field, and wide enough to be a real target on
   a phone — 2.25rem is 36 px, which is the smallest thing a finger hits reliably. The tilt
   buttons are the same square, so the row reads as a field and three controls. */
.prv-pagetext-remove,
.prv-pagetext-turn {
    flex: 0 0 auto;
    width: 2.25rem;
    height: 2.25rem;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: 0.85rem;
    line-height: 1;
    cursor: pointer;
    transition: var(--transition);
}

.prv-pagetext-remove:hover { border-color: var(--danger); color: var(--danger); }

.prv-pagetext-turn { font-size: 1rem; }
.prv-pagetext-turn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent-soft); }
.prv-pagetext-turn:disabled { opacity: 0.35; cursor: default; }

.prv-pagetext-add {
    align-self: flex-start;
    padding: 0.3rem 0.7rem;
    background: transparent;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-size: 0.78rem;
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
    transition: var(--transition);
}

.prv-pagetext-add:hover { border-color: var(--accent); color: var(--accent-soft); }

.prv-pagetext-hint {
    margin: 0;
    font-size: 0.75rem;
    line-height: 1.4;
    color: var(--text-secondary);
    text-align: center;
}

/* The backdrop takes focus so ← and → page through the album; the ring would only
   distract from the page it is wrapped around. */
.page-viewer-backdrop:focus { outline: none; }

/* A spread is shown as the two facing pages it really is, so each page is given half
   the room a single page gets. */
.page-viewer-sheet { display: flex; position: relative; }

.page-viewer-sheet--spread .preview-page--lg {
    --page-w: min(42vw, calc(64vh * var(--page-ar)), 320px);
    --page-w: min(42vw, calc(64svh * var(--page-ar)), 320px);
}

.page-viewer-sheet--spread::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 30px;
    transform: translateX(-50%);
    pointer-events: none;
    background: linear-gradient(to right,
        rgba(0,0,0,0) 0%, rgba(0,0,0,0.38) 45%, rgba(0,0,0,0.38) 55%, rgba(0,0,0,0) 100%);
    z-index: 5;
}

/* The strip is for looking at, so its photos carry no editor affordance. The compound
   selector is deliberate: the pencil is declared further down the file at the same
   specificity, so a lone .preview-photo-btn--static::after loses on source order. */
.preview-photo-btn--static { cursor: default; }
.preview-photo-btn.preview-photo-btn--static::after { content: none; }

/* ---- Ordering without an account ---- */
.guest-order {
    margin: 0.75rem 0 1rem;
    padding: 0.9rem 1rem;
    background: var(--bg-card-hover);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

/* The link is the customer's only way back to the order, so it is shown as something to keep
   rather than as an afterthought — and it wraps, because it is long and gets copied. */
.order-track-box {
    margin: 1rem auto 0;
    max-width: 34rem;
    padding: 0.9rem 1rem;
    border: 1px dashed var(--accent);
    border-radius: var(--radius-sm);
    background: rgba(176, 141, 79, 0.08);
    text-align: center;
}

.order-track-label {
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 0.35rem;
}

.order-track-link {
    display: block;
    word-break: break-all;
    font-size: 0.8rem;
    color: var(--accent-soft);
    margin-bottom: 0.4rem;
}

/* ---- Back-cover imprint ---- */
/* Shown, not offered: the customer cannot change these words, but they should know they are
   coming. Set in the gold they print in, on both the panel and the previewed page. */
.cover-imprint-note {
    margin-top: 0.5rem;
    padding: 0.75rem 0.9rem;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-sm);
}

.cover-imprint-preview {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    margin-bottom: 0.5rem;
    text-align: center;
}

/* The name leads and the promise follows, at the sizes BackCoverImprint prints them — the two
   swapped on 2026-08-28 and these have to swap with them, or the panel advertises a back cover
   the press does not produce. */
.cover-imprint-site {
    font-family: 'Playfair Display', Georgia, serif;
    font-size: 0.95rem;
    color: var(--accent);
}

.cover-imprint-tagline {
    font-family: 'Playfair Display', Georgia, serif;
    font-size: 0.72rem;
    color: var(--accent);
    opacity: 0.85;
}

/* On the previewed page the imprint sits at the foot of the lower half — the same place and the
   same proportions PdfGenerationService gives it, scaled through --mm. justify-content is the
   AlignBottom of the PDF: both moved off centre on 2026-08-28 on the client's instruction, and
   they have to move together or the preview stops being a preview. */
.preview-page--back .prv-imprint {
    position: absolute;
    left: var(--cf);
    right: var(--cf);
    bottom: var(--cf);
    height: calc(var(--back-panel-mm, 0) * var(--mm));
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    gap: calc(2 * var(--mm));
    text-align: center;
    pointer-events: none;
}

/* The gold changes with the cover colour — deeper on white, where the brand value is too pale
   to hold. The component sets --imprint-ink from BackCoverImprint, so both renderers pick the
   same one. */
.prv-imprint-tagline,
.prv-imprint-site {
    font-family: 'Playfair Display', Georgia, serif;
    color: var(--imprint-ink, #B08D4F);
    line-height: 1;
}

/* 4.6 mm and 3.4 mm are the 13 pt and 9.5 pt of BackCoverImprint. They belong to the sizes, not
   to the lines, so they swapped when the two lines did. */
.prv-imprint-site { font-size: max(4px, calc(4.6 * var(--mm))); }
.prv-imprint-tagline { font-size: max(3px, calc(3.4 * var(--mm))); }

/* The photo takes the top half, so the slot stops where the imprint begins. Both class names
   are on the selector deliberately: the generic .preview-page--cover rule that sets inset to the
   safe margin appears further down the file, and would otherwise win on source order. */
.preview-page--cover.preview-page--back .preview-photo-btn-wrap {
    inset: var(--cf) var(--cf) calc(var(--cf) + var(--back-panel-mm, 0) * var(--mm)) var(--cf);
}

/* ---- Cover typography ---- */
/* Each option is set in the face it offers: the label says "Klassisk", the sample says what that
   actually looks like, which is the only part of the choice that matters. */
.cover-font-picker {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 0.75rem;
}

.cover-font-opt {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    padding: 0.5rem 0.35rem;
    background: var(--bg-card-hover);
    border: 2px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    transition: var(--transition);
}

.cover-font-opt:hover { border-color: var(--border-strong); color: var(--text-primary); }

.cover-font-opt.is-active {
    border-color: var(--accent);
    color: var(--text-primary);
    background: var(--bg-card-active);
}

.cover-font-sample { font-size: 1.35rem; line-height: 1.1; }

/* The name is UI, not a sample, so it steps out of the face being offered. */
.cover-font-name {
    font-family: var(--font-body);
    font-size: 0.62rem;
    letter-spacing: 0.03em;
    color: var(--text-muted);
}

.cover-font-opt.is-active .cover-font-name { color: var(--accent-soft); }

.cover-bg-picker { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; }

.cover-bg-opt {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    background: var(--bg-card-hover);
    border: 2px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    cursor: pointer;
    transition: var(--transition);
}

.cover-bg-opt:hover { border-color: var(--border-strong); }
.cover-bg-opt.is-active { border-color: var(--accent); }

.cover-bg-swatch {
    width: 20px;
    height: 26px;
    border-radius: 2px;
    /* An outline, so a white swatch still reads as a page on the dark card. */
    outline: 1px solid var(--border-strong);
    outline-offset: -1px;
}

.cover-bg-sort .cover-bg-swatch { background: #000; }
.cover-bg-hvid .cover-bg-swatch { background: #fff; }

/* The text colour's swatch is a letter in that ink on the cover's own background, in the cover's
   own face — what the choice looks like on the magazine, not a chip of colour on the dark UI. */
.cover-ink-swatch {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1rem;
    font-weight: 700;
    line-height: 1;
}

/* The cover prints on white or black stock — never the app's card colour — so the preview
   shows the chosen background rather than the surrounding UI. */
.preview-page--cover {
    outline-color: var(--accent);
    background: var(--cover-bg, #000);
}

.preview-page--full { outline-color: var(--accent); }

.preview-page-star {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--accent);
    color: #241d12;
    font-size: 0.7rem;
    line-height: 18px;
    text-align: center;
    pointer-events: none;
    z-index: 2;
}

/* Geometry here mirrors PdfGenerationService, so the preview shows what gets printed:
   the printer's 1 cm border, and a 4 mm gutter between photos (2 mm of cell padding on
   each of two neighbouring cells). Percentages are no use for this — a percentage padding
   resolves against the containing block's width, not the page's own. */
/* The grid divides the whole page inside the border — equal fractions on both axes, so the
   cells carry the sheet's proportions and nothing is left over. It used to size square cells
   against both axes and centre the block, which meant an 8-up page spent two thirds of its
   height on white. --cols/--rows come from the component. */
.preview-page--content {
    --gap: calc(4 * var(--mm));

    display: grid;
    grid-template-columns: repeat(var(--cols), 1fr);
    grid-template-rows: repeat(var(--rows), 1fr);
    gap: var(--gap);
    padding: calc(10 * var(--mm));
    background: var(--paper);
}

/* Fallback counts; the component overrides them inline per page. */
.preview-page--content { --cols: 2; --rows: 2; }

/* A promoted favourite is cropped to fill everything inside the border, so it is the one
   page that ignores the grid and takes the whole area. */
.preview-page--content.preview-page--full {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
}

/* The page number, placed through --mm at the millimetres PageGeometry gives the PDF: 4 mm up
   from the trimmed edge, 10 mm in from the side. Odd pages are right-hand pages once the
   magazine is stitched, so the folio always lands on the outer edge. It is absolutely
   positioned, which also keeps it out of the grid flow above. The floor on the font size is
   for the thumbnails, where a true-to-scale 9 pt would be under two pixels tall. */
.preview-page-folio {
    position: absolute;
    bottom: calc(4 * var(--mm));
    font-size: max(4px, calc(3.2 * var(--mm)));
    line-height: 1;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    pointer-events: none;
    z-index: 2;
}

.preview-page-folio.is-left { left: calc(10 * var(--mm)); }
.preview-page-folio.is-right { right: calc(10 * var(--mm)); }

.preview-page-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* ---- Tap-to-edit photo (zoom/position within its slot) ---- */
/* The wrapper is the slot; the button is the photo inside it, and the badge sits on top
   without being part of the button (a warning you can click is a warning you can misfire). */
.preview-photo-btn-wrap {
    position: relative;
    display: block;
    width: 100%;
    height: 100%;
}

.preview-photo-btn {
    position: absolute;
    inset: 0;
    display: block;
    padding: 0;
    border: none;
    background: none;
    overflow: hidden;
    cursor: pointer;
}

/* ---- Quarter turns ---- */
/* The turn happens on this wrapper rather than on the photo, which already spends its own
   transform on the zoom about the focal point. Used by both the preview and the crop editor, so
   a turned photo looks the same in the strip, on the opened page and in the dialog. */
.photo-turn {
    position: absolute;
    inset: 0;
    display: block;
}

.photo-turn--turned {
    inset: auto;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(var(--turn, 0deg));
}

/* A quarter turn also has to swap the box it turns inside, or the result stops covering the
   frame. --slot-ar is the frame's width ÷ height, so the frame's height expressed against its
   own width is 100%/ar, and its width against its own height is 100%·ar — which is exactly the
   swap, without either measurement having to reach JavaScript. */
.photo-turn--quarter {
    width: calc(100% / var(--slot-ar));
    height: calc(100% * var(--slot-ar));
}

/* Covers keep the photo a safe distance (--cf) from the page edge, framed by the cover colour —
   the print safe-margin. It is measured through --mm from the millimetres PageGeometry gives the
   PDF, not set in pixels: a fixed value is only ever right at one preview size, and it was tuned
   for the thumbnail, so on the opened page — the one place the customer inspects the margin — the
   frame was drawn several times too narrow. */
/* Sets --cf only. It used to repaint the page in --bg-primary as well, which — same selector,
   further down the file — quietly beat the --cover-bg rule above and painted every cover in the
   app's near-black: a white cover looked black in the preview and white in the PDF. */
.preview-page--cover { --cf: calc(var(--cover-margin-mm, 12) * var(--mm)); }
/* The slot is placed by its insets alone. The base rule above sizes it 100% x 100% — right for
   the grid cell it usually sits in, wrong here: an absolutely positioned box that is given left,
   right and a width is over-constrained, so CSS drops the right offset and keeps the width (and
   the same for bottom against the height). The cover photo was therefore laid out a full margin
   wide and tall, shifted down and to the right, hanging off the page instead of stopping at the
   safe frame — and on a top/bottom cover it ran straight over the panel the band leaves for the
   colour. Resetting both to auto hands the box back to the insets. */
.preview-page--cover .preview-photo-btn-wrap {
    position: absolute;
    inset: var(--cf);
    width: auto;
    height: auto;
    border-radius: 2px;
}

/* Flags a photo whose current crop falls below what prints acceptably at this size. It rides the
   thumbnails in Sider and Opslag as well as the opened page: the banner says how many and the list
   says where, but neither answers "is this one of them" while you are looking at a page. Sized off
   the page rather than in fixed pixels for that reason — on a strip thumbnail a 15 px disc is most
   of the photo, and a mark that swamps what it marks cannot be read against its neighbours. */
.prv-soft-badge {
    position: absolute;
    top: 3px;
    left: 3px;
    width: clamp(10px, calc(9 * var(--mm)), 15px);
    height: clamp(10px, calc(9 * var(--mm)), 15px);
    font-size: clamp(0.4rem, calc(5 * var(--mm)), 0.55rem);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--danger);
    color: #2a120e;
    font-weight: 700;
    pointer-events: none;
    z-index: 3;
}

/* ---- Moving a photo along the printed album ---- */
/* Sits on the photo, at the foot of its slot, and only ever on an opened page — the strip
   renders its slots non-interactive. Sized off the page like the marks above it: a 12-up grid
   gives each photo a third of the sheet, and a fixed-pixel control that is comfortable there is
   most of the picture on a spread, where two pages share the width. */
.prv-move {
    position: absolute;
    left: 50%;
    bottom: 4px;
    transform: translateX(-50%);
    display: flex;
    gap: 2px;
    z-index: 3;
}

.prv-move-btn {
    width: clamp(18px, calc(11 * var(--mm)), 26px);
    height: clamp(18px, calc(11 * var(--mm)), 26px);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: rgba(20, 18, 16, 0.72);
    color: var(--text-primary);
    font-size: clamp(0.6rem, calc(7 * var(--mm)), 0.85rem);
    line-height: 1;
    cursor: pointer;
}

.prv-move-btn:hover:not(:disabled) {
    background: var(--accent);
    color: #1a1712;
}

/* Kept in place rather than hidden: the row would otherwise shift under the finger every time a
   photo reached the end of the album, and the arrow that is left would move to where the other
   one just was. */
.prv-move-btn:disabled {
    opacity: 0.3;
    cursor: not-allowed;
}

.preview-photo-btn::after {
    content: "✎";
    position: absolute;
    bottom: 3px;
    right: 3px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: rgba(20,18,16,0.65);
    color: var(--text-primary);
    font-size: 0.55rem;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}

/* ---- Cover photo placement in the album preview (framed by --cf) ---- */
/* The panel beside the photo is what is left of the framed area once the photo has taken its
   band — PageGeometry.CoverPhotoBandFractionFor(placement) of it — so the component hands the leftover down in
   millimetres. It used to be 40% of the whole page, which is a different measurement entirely and
   only looked close by coincidence. */
.preview-page--cover.cph-fill   .preview-photo-btn-wrap { inset: var(--cf); }

.preview-page--cover.cph-top .preview-photo-btn-wrap {
    inset: var(--cf) var(--cf) calc(var(--cf) + var(--cover-panel-mm, 0) * var(--mm)) var(--cf);
}

.preview-page--cover.cph-bottom .preview-photo-btn-wrap {
    inset: calc(var(--cf) + var(--cover-panel-mm, 0) * var(--mm)) var(--cf) var(--cf) var(--cf);
}

/* ---- Draggable cover text ---- */
/* The layer spans the safe framed area; only the text chip captures pointers,
   so tapping elsewhere still opens the photo editor underneath. */
.cover-text-layer {
    position: absolute;
    inset: var(--cf);
    pointer-events: none;
    z-index: 3;
}

.preview-page-text {
    position: absolute;
    transform: translate(-50%, -50%);
    /* As wide as its words, and wrapped only at the max-width the inline style sets — the photo's
       width plus CoverTypography.LineOverhangMm, which is where the PDF wraps them. Without
       max-content the browser wraps an absolutely placed box at the room between its left edge and
       the layer's right one: half the photo for a title at 50 %, so a two-word title broke onto two
       lines on screen and printed on one. Content-box so that width is the words' alone, with the
       padding running on past it as the chip's padding does on paper (2026-09-29). */
    box-sizing: content-box;
    width: max-content;
    max-width: 100%;
    /* Padding comes from CoverTypography through the inline style, in millimetres scaled by --mm,
       so the chip is the size it prints at on the thumbnail and on the opened page alike. It used
       to be a fixed 3px 6px here, which is a different chip at every preview size and a fourth one
       on paper. */
    background: var(--cover-chip, rgba(0,0,0,0.6));
    /* The words' own ink — the standard one, or the brand gold since 2026-09-24. */
    color: var(--cover-text-ink, var(--cover-ink, #fff));
    font-size: 0.58rem;
    font-weight: 700;
    text-align: center;
    line-height: 1.2;
    border-radius: 3px;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
}

/* The chip is a control, so it is not text to select: a drag that highlights the words instead of
   moving them reads as a broken gesture rather than the wrong one. */
.cover-text-drag {
    pointer-events: auto;
    cursor: grab;
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
    border: 1px dashed transparent;
}

.cover-text-drag:hover { border-color: rgba(228,207,159,0.6); }
.cover-text-drag.dragging { cursor: grabbing; border-color: var(--accent); background: var(--cover-chip-active, rgba(20,18,16,0.8)); }

/* Sizing looks the same as moving on purpose — one handle, two gestures — so the chip says
   "I am the thing you are changing" either way. */
.cover-text-drag.is-resizing { border-color: var(--accent); background: var(--cover-chip-active, rgba(20,18,16,0.8)); }

/* It takes focus for the +/- keys, so it has to show where the focus went. */
.cover-text-drag:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-color: var(--accent);
}


/* ---- The words on a page ------------------------------------------------------------------
   The cover's chip, laid over the whole trimmed page — which is what lets a box cross the gutter
   between two pictures or sit out in the white margin, and what the owner asked for on
   2026-09-22. The layer is the page element, so the percentages the box is placed at mean the
   same thing here as they do in PdfGenerationService. A page may carry several.

   z-index above the folio for a reason the customer would notice: the page number sits in the
   bottom margin, and a box dragged down there would otherwise slide behind it and be impossible
   to pick back up. */
.page-text-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 5;
}

/* Centred on its X/Y and turned about its own centre — the same point QuestPDF turns it about, and
   clockwise-positive in both, so --rot is the stored number as it is.

   width:max-content is what makes the box the same width wherever it stands. Without it an
   absolutely placed box is squeezed into whatever room is left between its left edge and the right
   of the page, so it wrapped narrower the further right it went — which looked like the margin
   holding on the right and not on the left, and was never how the PDF set it (2026-09-24). The
   max-width from PageTextStyle is the one QuestPDF wraps at. */
.page-text {
    position: absolute;
    width: max-content;
    transform: translate(-50%, -50%) rotate(var(--rot, 0deg));
    /* Opaque white and black type, as the client asked. On an interior page it also means a box
       dragged into the margin disappears into the paper and leaves only the words. */
    background: #fff;
    color: #000;
    font-weight: 700;
    text-align: center;
    line-height: 1.2;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* No line-clamp, no overflow:hidden, no percentage max-width and no pixel floor on the size, for
   the reason spelled out above .photo-caption-drag: QuestPDF clamps none of those, so a preview
   that hid the fourth line would be hiding a line the press will print. PageText is what keeps
   the words on the sheet — a length, a size ceiling, and a wrap width in millimetres measured
   inside the print margin. */

.page-text-drag {
    pointer-events: auto;
    cursor: grab;
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
    outline: 1px dashed transparent;
    outline-offset: 1px;
}

.page-text-drag:hover { outline-color: rgba(228,207,159,0.8); }

.page-text-drag.dragging,
.page-text-drag.is-resizing { cursor: grabbing; outline: 2px solid var(--accent); }

.page-text-drag:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* A box the customer has added and not yet written in. It says "Skriv her" in grey so there is
   something to take hold of and drag, and it is drawn only on the opened page — never on the
   strip and never on paper, where an empty box prints nothing at all. */
.page-text--empty {
    color: var(--text-muted);
    font-style: italic;
    font-weight: 400;
    outline: 1px dashed rgba(228,207,159,0.5);
}

/* Blank cover (no photo/text): a subtle centered hint inside the frame. */
/* An untouched cover is no longer empty — it carries the name — so this is the name as the press
   will set it: gold, near the top, at DefaultCover's 34 pt scaled through --mm (34 pt is 12 mm).
   The dashed placeholder box that used to say "Tom forside" is gone with the blankness. */
.prv-default-cover {
    position: absolute;
    left: var(--cf);
    right: var(--cf);
    top: calc(var(--cf) + 18 * var(--mm));
    text-align: center;
    font-family: 'Playfair Display', Georgia, serif;
    font-size: max(7px, calc(12 * var(--mm)));
    line-height: 1;
    color: var(--imprint-ink, #B08D4F);
    pointer-events: none;
}

/* ---- Download ---- */
.download-section {
    text-align: center;
    padding: 1.5rem 0.5rem;
}

.download-icon { font-size: 3rem; margin-bottom: 0.75rem; }

.download-title {
    margin-bottom: 0.5rem;
}

.download-subtitle {
    color: var(--text-secondary);
    font-size: 0.9rem;
    margin-bottom: 1.5rem;
}

.btn-download {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.9rem 2.5rem;
    background: var(--accent);
    color: #241d12;
    font-size: 1rem;
    font-weight: 800;
    border-radius: 100px;
    text-decoration: none;
    transition: var(--transition);
    box-shadow: 0 0 24px var(--accent-glow);
}

.btn-download:hover {
    background: var(--accent-strong);
    transform: translateY(-2px);
    text-decoration: none;
}

.download-note {
    margin-top: 1.25rem;
    color: var(--text-muted);
    font-size: 0.8rem;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

/* The review invitation at the foot of the confirmation. Set apart by a rule rather than by a box
   of its own: this is a postscript, and a panel here would carry the same weight as the tracking
   link above it — the one thing on that card a guest cannot afford to skim past. */
.order-review {
    margin-top: 1.75rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--border);
}

.order-review-text {
    color: var(--text-secondary);
    font-size: 0.9rem;
    margin-bottom: 0.75rem;
}

/* Outlined where .btn-download is filled. The gold pill on this page belongs to the customer's own
   order, and a second one would make the link that leaves the site the louder of the two. */
.order-review-link {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.6rem 1.4rem;
    border: 1px solid var(--border-strong);
    border-radius: 100px;
    color: var(--text-primary);
    font-size: 0.9rem;
    font-weight: 700;
    text-decoration: none;
    transition: var(--transition);
}

.order-review-link:hover,
.order-review-link:focus-visible {
    border-color: var(--accent);
    color: var(--accent);
    background: var(--bg-card-hover);
    text-decoration: none;
}

.order-review-hint {
    margin-top: 0.7rem;
    color: var(--text-muted);
    font-size: 0.8rem;
}

.download-made { font-weight: 700; }

.download-error { color: #f87171; font-size: 0.9rem; }

.order-teaser {
    margin-top: 1.5rem;
    padding: 1.25rem;
    background: var(--bg-card-hover);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    text-align: center;
}

.order-teaser h3 { margin-bottom: 0.35rem; }
.order-teaser p  { color: var(--text-secondary); font-size: 0.875rem; margin-bottom: 1rem; }

/* ---- Order history (Min konto) ---- */
.order-history {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.order-history-item {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    padding: 0.75rem;
    background: var(--bg-card-hover);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.order-history-thumb {
    width: 52px;
    height: 52px;
    border-radius: var(--radius-sm);
    object-fit: cover;
    flex-shrink: 0;
}

.order-history-thumb--empty {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-card-active);
    font-size: 1.4rem;
}

.order-history-info { flex: 1; min-width: 0; }

.order-history-date { font-weight: 700; font-size: 0.9rem; }
.order-history-meta { color: var(--text-secondary); font-size: 0.8rem; margin-bottom: 0.3rem; }

.order-history-status {
    display: inline-block;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    padding: 0.15rem 0.55rem;
    border-radius: 100px;
    background: var(--bg-card-active);
    color: var(--text-secondary);
}

.order-history-status--pending   { background: rgba(201,169,106,0.18); color: var(--accent); }
.order-history-status--confirmed,
.order-history-status--completed { background: rgba(122,190,140,0.18); color: #8fd6a4; }
.order-history-status--cancelled { background: rgba(226,138,122,0.18); color: var(--danger); }

.btn-order {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.65rem 1.75rem;
    border: 2px solid var(--accent);
    color: var(--accent);
    font-weight: 700;
    border-radius: 100px;
    font-size: 0.9rem;
    text-decoration: none;
    transition: var(--transition);
}

.btn-order:hover {
    background: var(--accent);
    color: #241d12;
    text-decoration: none;
}

/* ---- Error banner ---- */
.error-banner {
    background: rgba(248,113,113,0.1);
    border: 1px solid #f87171;
    color: #f87171;
    border-radius: var(--radius-sm);
    padding: 0.75rem 1rem;
    font-size: 0.875rem;
    margin-top: 0.75rem;
}

/* Same shape as the error banner, said in the house gold rather than in red: it reports
   something that went right — the last magazine was ordered, so this is a fresh one. */
.notice-banner {
    background: var(--accent-glow);
    border: 1px solid var(--border-focus);
    color: var(--accent-soft);
    border-radius: var(--radius-sm);
    padding: 0.75rem 1rem;
    font-size: 0.875rem;
    margin-top: 0.75rem;
}

/* ---- Auth pages ---- */
.auth-page {
    min-height: 100svh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
    background:
        radial-gradient(ellipse 60% 40% at 50% 0%, rgba(201,169,106,0.06) 0%, transparent 70%),
        var(--bg-primary);
}

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

.auth-logo {
    text-align: center;
    font-family: var(--font-display);
    font-size: 2.4rem;
    font-weight: 500;
    letter-spacing: 0.12em;
    color: var(--accent);
    display: block;
    margin-bottom: 0.25rem;
}

.auth-title {
    text-align: center;
    font-size: 1.15rem;
    font-weight: 700;
    margin-bottom: 0.25rem;
}

.auth-subtitle {
    text-align: center;
    font-size: 0.85rem;
    color: var(--text-secondary);
    margin-bottom: 1.75rem;
}

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

/* Postnr. and by belong on one line — they are one thought, and stacking them makes a short
   Danish address look like a long form. The postcode is four digits, so it gets just enough
   room for them; the city takes the rest. Collapses to stacked on a narrow phone.

   A grid rather than a flex row, and the reason is the one thing flex cannot do: put the boxes
   of two fields at the same height. Side by side, each field was its own column of label →
   box → hint, so a label that took two lines pushed its box down and the pair no longer lined
   up — 21 px apart on /admin/rabatkoder at 430 px, where "Kampagnepris pr. stk." wraps and
   "Kampagnens navn" does not, and again under it where "Fragt, hjemmelevering" wraps and
   "Fragt, pakkeshop" does not. Nothing about a label's length should move a box, and no
   min-height guessed per label survives the next label.

   So the row owns three bands — label, box, hint — and each field is a subgrid spanning them.
   The label band is as tall as the tallest label in the row, which is what makes every box
   in the row start at the same y whatever its label did. Three is the shape every field in
   the app has (a `<label>`, one control, an optional `<small class="cover-hint">`); a fourth
   child would land in an implicit row outside the span and would need a fourth band here.

   `auto-fit` + a 10 rem minimum reproduces exactly when the old `flex: 1 1 10rem` wrapped —
   two fields share a line from 332 px of row and stack below it, which is a small phone —
   and an only child still takes the full width, because auto-fit collapses the empty track.
   `column-gap` alone: a `gap` would also space the three bands apart, and the label's own
   0.4rem margin already does that. */
.form-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
    grid-template-rows: auto auto auto;
    column-gap: 0.75rem;
}

/* `min-width: 0` is what lets a field shrink below what is inside it — without it a date
   input, whose own minimum is wide, pushed the page 21 px off the side of a 390 px phone.
   `align-items: start` keeps a control its natural height inside a band that a taller
   neighbour has stretched; the label is the exception and hangs from the bottom of its band,
   so a one-line label sits against its own box instead of floating a line above it. */
.form-row .form-field {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 3;
    align-items: start;
    min-width: 0;
}

.form-row .form-field > label { align-self: end; }

/* In a grid a width belongs to the track, not to the item in it, so the postcode's 7.5 rem is
   set here rather than on `.form-field--postal` — which is also what stops it losing to
   `.form-row .form-field` on specificity, the way it silently had been. The city takes the
   rest, with a `minmax(0, …)` so a long by-name cannot push the row wider than the card. */
.form-row:has(.form-field--postal) {
    grid-template-columns: 7.5rem minmax(0, 1fr);
}

@media (max-width: 380px) {
    .form-row:has(.form-field--postal) { grid-template-columns: minmax(0, 1fr); }
}

/* "(valgfrit)" sits inside the label, so it must not inherit the label's uppercase shouting. */
.cover-hint-inline {
    text-transform: none;
    letter-spacing: normal;
    font-weight: 400;
    opacity: 0.75;
}

.form-field label {
    display: block;
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 0.4rem;
}

.form-field input {
    width: 100%;
    background: var(--bg-card-hover);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.75rem 1rem;
    font-size: 0.95rem;
    color: var(--text-primary);
    font-family: inherit;
    transition: var(--transition);
    outline: none;
}

/* The halo used to be rgba(201,169,106,0.1) — a tenth of an alpha of gold on a dark card, which
   is to say invisible. Tabbing worked and looked like nothing had happened, which is one of the
   two halves of "Tab virker ikke" from 2026-09-22; the other was the missing form. Keyboard focus
   gets a real outline, and a click keeps the quiet border it always had. */
.form-field input:focus {
    border-color: var(--border-focus);
    box-shadow: 0 0 0 3px rgba(201,169,106,0.22);
}

.form-field input:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.form-field input::placeholder { color: var(--text-muted); }

.auth-submit {
    width: 100%;
    padding: 0.85rem;
    background: var(--accent);
    color: #241d12;
    font-size: 0.95rem;
    font-weight: 800;
    border-radius: 100px;
    border: none;
    cursor: pointer;
    transition: var(--transition);
    box-shadow: 0 0 20px var(--accent-glow);
    margin-top: 0.5rem;
}

.auth-submit:hover { background: var(--accent-strong); transform: translateY(-1px); }
.auth-submit:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }

.auth-divider {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin: 1.1rem 0;
    color: var(--text-muted);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
}

.auth-divider::before,
.auth-divider::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--border-strong);
}

.auth-google-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.65rem;
    width: 100%;
    padding: 0.8rem;
    background: #ffffff;
    color: #1f1f1f;
    font-family: inherit;
    font-size: 0.95rem;
    font-weight: 600;
    border-radius: 100px;
    text-decoration: none;
    transition: var(--transition);
}

.auth-google-btn:hover {
    text-decoration: none;
    transform: translateY(-1px);
    box-shadow: 0 6px 18px rgba(0,0,0,0.35);
}

.auth-google-icon { width: 18px; height: 18px; }

.auth-error {
    background: rgba(248,113,113,0.1);
    border: 1px solid rgba(248,113,113,0.4);
    color: #fca5a5;
    border-radius: var(--radius-sm);
    padding: 0.65rem 1rem;
    font-size: 0.85rem;
    margin-bottom: 1rem;
}

.auth-link-row {
    text-align: center;
    margin-top: 1.25rem;
    font-size: 0.85rem;
    color: var(--text-secondary);
}

.auth-link-row a { color: var(--accent); font-weight: 600; }

/* ---- Bestil (Order) page ---- */
.order-page {
    min-height: 100svh;
    padding: 1.5rem;
    max-width: 600px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

/* Since 14-09-2026 this page is step 5 and sits inside the flow's frame, under the same sticky
   header /byg has. The frame is what fills the viewport now, so the column inside it must not
   also claim a screenful — two of them stacked put a screen of empty board under the order. */
.builder > .order-page {
    min-height: 0;
    flex: 1;
    width: 100%;
}

/* Nine other pages use this — /konto, /mine-bestillinger, /ordre, /tryk and the five admin
   screens — even though /bestil, which named it, no longer does: since 14-09-2026 that page wears
   the flow's own header instead. Do not tidy it away with the markup that left. */
.order-header {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-bottom: 0.5rem;
}

/* The 🎉 and "Dit magasin er nu klar til print!", carried over from the step this page absorbed.
   Tighter than the standalone .download-section it borrows from: there it was the whole screen,
   here it introduces a form and the form is the point. */
.download-section--order {
    padding: 0.25rem 0.5rem 0.75rem;
}

.download-section--order .download-icon {
    font-size: 2.25rem;
    margin-bottom: 0.35rem;
}

/* ---- Validation ---- */
.validation-message { color: #f87171; font-size: 0.8rem; margin-top: 0.25rem; display: block; }
.valid.modified:not([type=checkbox]) { border-color: var(--accent) !important; }
.invalid { border-color: #f87171 !important; }

/* ================================================================
   RESPONSIVE: Tablet and up
   ================================================================ */
@media (min-width: 640px) {
    .photo-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); }
    .layout-grid { grid-template-columns: repeat(4, 1fr); }
    .landing-hero { padding: 3rem 2rem; }
}

@media (min-width: 900px) {
    .builder-content { padding: 1.75rem 2rem; }
}

/* ================================================================
   Blazor error UI
   Lives in the GLOBAL stylesheet (not component-scoped) so it stays
   applied after enhanced navigation, which can drop the scoped CSS
   bundle from <head>. #blazor-error-ui is a framework-level element,
   so it belongs here — matching the default Blazor template.
   ================================================================ */
#blazor-error-ui {
    background: #1a0a0a;
    color: #ff6b6b;
    bottom: 0;
    box-shadow: 0 -2px 8px rgba(0,0,0,0.5);
    display: none;
    left: 0;
    padding: 0.75rem 1.5rem;
    position: fixed;
    width: 100%;
    z-index: 9999;
    font-size: 0.9rem;
    border-top: 1px solid #ff6b6b44;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 1rem;
    top: 50%;
    transform: translateY(-50%);
    opacity: 0.7;
}

#blazor-error-ui .reload {
    color: #c9a96a;
    margin-left: 0.5rem;
}

/* ================================================================
   Photo crop editor (.pfx) — pan, zoom, and what it costs in print
   ================================================================ */
/* Above the page viewer (520): the editor is opened from inside it. */
.pfx-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(10,9,8,0.86);
    backdrop-filter: blur(4px);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    z-index: 560;
}

/* Three rows: the head and the footer keep their height, and the body between them takes what is
   left and scrolls it. The dialog as a whole never scrolls, so ✕ and Gem cannot be scrolled away
   — on a phone they used to start out below the fold (reported 2026-09-25). */
.pfx {
    width: 100%;
    max-width: 420px;
    max-height: 100%;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: var(--bg-card);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-card);
}

.pfx-head {
    flex-shrink: 0;
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    padding: 1.1rem 1.1rem 0.75rem;
}

/* overscroll-behavior keeps a scroll that reaches the end from carrying on into the page viewer
   the editor was opened from, which scrolls too. */
.pfx-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding: 0.25rem 1.1rem 0.9rem;
}

/* The body scrolls when it runs out of room; nothing inside it gets squashed to make room. */
.pfx-body > * { flex-shrink: 0; }

.pfx-head-text { flex: 1; min-width: 0; }

.pfx-title {
    font-family: var(--font-display);
    font-size: 1.05rem;
    font-weight: 500;
    line-height: 1.2;
}

.pfx-sub {
    font-size: 0.75rem;
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.pfx-step { display: flex; align-items: center; gap: 0.2rem; }

.pfx-step-count {
    font-size: 0.72rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.pfx-icon {
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--bg-card-hover);
    color: var(--text-secondary);
    font-size: 0.95rem;
    line-height: 1;
    transition: var(--transition);
}

.pfx-icon:hover:not(:disabled) { background: var(--bg-card-active); color: var(--text-primary); }
.pfx-icon:disabled { opacity: 0.3; cursor: default; }

/* ---- The frame is the printed slot ---- */
/* Sized from the slot's own aspect (--stage-ar, set by the component from the print geometry)
   rather than from the space the column has left over. flex:none is load-bearing: as a flex item
   it would otherwise be shrunk to fit the dialog, and a squashed frame is a frame showing a crop
   that is not the one being printed — which is the whole thing this editor promises. Height is
   bounded by capping the width, so the shape survives on short screens too.

   The height it is capped at leaves the screen room for the head, the footer and the zoom row
   below the photo: a finger on the photo moves the crop, so the rows under it are where the body
   is scrolled from, and they have to be on screen for that. svh rather than vh where the browser
   has it — on a phone vh is measured with the toolbars hidden, and is taller than the screen.
   The fallback is written out on width itself: a custom property accepts any value, so a second
   declaration of one would win even where svh does not parse, and take the width down with it. */
.pfx-stage {
    position: relative;
    flex: none;
    --stage-ar: 1;
    width: min(100%, calc(min(52vh, 100vh - 20rem) * var(--stage-ar)));
    width: min(100%, calc(min(52svh, 100svh - 20rem) * var(--stage-ar)));
    aspect-ratio: var(--stage-ar);
    margin: 0 auto;
    overflow: hidden;
    border-radius: var(--radius-md);
    background: var(--bg-primary);
    outline: 1px solid var(--border-strong);
    outline-offset: -1px;
    touch-action: none;
    cursor: grab;
}

.pfx-stage:focus-visible { outline: 2px solid var(--accent); }
.pfx-stage.is-adjusting { cursor: grabbing; }

.pfx-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    user-select: none;
    -webkit-user-drag: none;
    pointer-events: none;
}

/* Rule-of-thirds guides, shown only while the crop is being moved: they are a tool for
   the gesture, not decoration to look at for the rest of the time. */
.pfx-guides {
    position: absolute;
    inset: 0;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.18s ease;
    background:
        linear-gradient(to right, transparent calc(33.333% - 0.5px), rgba(255,255,255,0.45) calc(33.333% - 0.5px),
            rgba(255,255,255,0.45) calc(33.333% + 0.5px), transparent calc(33.333% + 0.5px),
            transparent calc(66.666% - 0.5px), rgba(255,255,255,0.45) calc(66.666% - 0.5px),
            rgba(255,255,255,0.45) calc(66.666% + 0.5px), transparent calc(66.666% + 0.5px)),
        linear-gradient(to bottom, transparent calc(33.333% - 0.5px), rgba(255,255,255,0.45) calc(33.333% - 0.5px),
            rgba(255,255,255,0.45) calc(33.333% + 0.5px), transparent calc(33.333% + 0.5px),
            transparent calc(66.666% - 0.5px), rgba(255,255,255,0.45) calc(66.666% - 0.5px),
            rgba(255,255,255,0.45) calc(66.666% + 0.5px), transparent calc(66.666% + 0.5px));
}

.pfx-stage.is-adjusting .pfx-guides { opacity: 1; }

.pfx-hint {
    font-size: 0.72rem;
    color: var(--text-secondary);
    text-align: center;
    line-height: 1.45;
}

.pfx-hint-keys { display: block; color: var(--text-muted); font-size: 0.68rem; }

/* ---- Zoom ---- */
.pfx-zoom { display: flex; align-items: center; gap: 0.6rem; }

.pfx-zoom-label {
    font-size: 0.72rem;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

/* The mark on the track is where this photo stops printing at 300 dpi. Everything to the
   left of it is press-sharp; the tinted stretch to the right is the part you pay for. */
.pfx-track {
    position: relative;
    flex: 1;
    display: flex;
    align-items: center;
}

.pfx-track::before {
    content: "";
    position: absolute;
    left: var(--quality-stop, 100%);
    right: 0;
    top: 50%;
    height: 4px;
    transform: translateY(-50%);
    border-radius: 2px;
    background: repeating-linear-gradient(45deg,
        rgba(226,138,122,0.5) 0 3px, rgba(226,138,122,0.15) 3px 6px);
    pointer-events: none;
}

.pfx-range { width: 100%; accent-color: var(--accent); position: relative; }

.pfx-mini {
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--text-secondary);
    padding: 0.3rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: 100px;
    white-space: nowrap;
    transition: var(--transition);
}

.pfx-mini:hover { border-color: var(--border-strong); color: var(--text-primary); }

/* ---- Print resolution readout ---- */
.pfx-quality {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.5rem 0.7rem;
    background: var(--bg-card-hover);
    border-radius: var(--radius-sm);
}

.pfx-meter {
    flex: 1;
    height: 5px;
    border-radius: 3px;
    background: var(--bg-card-active);
    overflow: hidden;
    position: relative;
}

.pfx-meter::after {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: var(--fill, 0%);
    border-radius: 3px;
    background: var(--accent);
    transition: width 0.12s linear, background 0.2s ease;
}

.pfx-meter[data-level="sharp"]::after { background: #8fd6a4; }
.pfx-meter[data-level="ok"]::after    { background: var(--accent); }
.pfx-meter[data-level="soft"]::after  { background: var(--danger); }

.pfx-dpi {
    font-size: 0.75rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    color: var(--text-secondary);
}

.pfx-dpi[data-level="sharp"] { color: #8fd6a4; }
.pfx-dpi[data-level="ok"]    { color: var(--accent-soft); }
.pfx-dpi[data-level="soft"]  { color: var(--danger); }

.pfx-quality-help {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    border: 1px solid var(--border-strong);
    color: var(--text-muted);
    font-size: 0.6rem;
    font-weight: 700;
    cursor: help;
}

/* ---- Toggles + actions ---- */
.pfx-tools { display: flex; flex-direction: column; gap: 0.4rem; }

.pfx-toggle {
    width: 100%;
    padding: 0.6rem 0.9rem;
    background: transparent;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-size: 0.85rem;
    font-weight: 600;
    transition: var(--transition);
}

.pfx-toggle:hover:not(:disabled) { border-color: var(--accent); color: var(--accent-soft); }
.pfx-toggle:disabled { opacity: 0.4; cursor: default; }

.pfx-toggle--danger { color: var(--danger); }
.pfx-toggle--danger:hover:not(:disabled) { border-color: var(--danger); color: var(--danger); }

/* What the button above it will cost the page count. In the album's own gold rather than the
   delete red: pressing it is allowed and undoable — this is a price, not a refusal. */

.pfx-note {
    /* Closer to the button it belongs to than to the one below it — inside an evenly spaced
       stack, a note the same distance from both reads as a note about the wrong button. */
    margin-top: -0.15rem;
    padding: 0.55rem 0.75rem;
    background: rgba(201,169,106,0.10);
    border-radius: var(--radius-sm);
    color: var(--accent-soft);
    font-size: 0.72rem;
    line-height: 1.45;
    text-align: center;
}

/* The delete confirmation takes the footer's place, just under the button that opened it and on
   screen however the body is scrolled — rather than as a second dialog on top of the one the
   customer is already in. */
.pfx-confirm {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    padding: 0.75rem 0.9rem;
    border: 1px solid var(--danger);
    border-radius: var(--radius-sm);
    background: rgba(226, 138, 122, 0.08);
}

.pfx-confirm-text {
    font-size: 0.8rem;
    line-height: 1.45;
    color: var(--text-secondary);
}

.pfx-confirm-actions { display: flex; gap: 0.5rem; justify-content: flex-end; }

.pfx-toggle.is-active {
    background: var(--accent);
    border-color: var(--accent);
    color: #241d12;
}

/* Outside the scrolling body, so it is always on screen; the rule above it says where the scroll
   ends. It also holds the delete question while that is being asked. */
.pfx-actions {
    flex-shrink: 0;
    display: flex;
    gap: 0.65rem;
    padding: 0.75rem 1.1rem 1.1rem;
    border-top: 1px solid var(--border);
}
.pfx-actions .app-btn { flex: 1; justify-content: center; }
.pfx-actions .pfx-confirm { flex: 1; }
.pfx-actions .pfx-confirm .app-btn { flex: none; }

/* ---- Admin orders panel ---- */
.admin-orders { display: flex; flex-direction: column; gap: 0.6rem; }

.admin-order-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.75rem;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    flex-wrap: wrap;
}

.admin-order-main { flex: 1; min-width: 12rem; }
.admin-order-customer { margin: 0; display: flex; flex-direction: column; }
.admin-order-email { color: var(--text-secondary); font-size: 0.8rem; }
.admin-order-meta { margin: 0.25rem 0 0; color: var(--text-muted); font-size: 0.8rem; }

/* Was the status <select>. Nothing in the panel sets a status any more — the payment does — so
   this is a badge that reports one. */
.admin-order-status {
    font-size: 0.75rem;
    font-weight: 600;
    padding: 0.3rem 0.65rem;
    border: 1px solid var(--border-strong);
    border-radius: 100px;
    color: var(--text-secondary);
    white-space: nowrap;
}

.admin-order-status--paid { color: var(--accent-soft); border-color: rgba(228,207,159,0.35); }

.admin-order-nopdf { color: var(--text-muted); font-size: 0.8rem; }

/* A print job is two downloads — the cover and the interior — so they stack as a small block
   rather than sitting loose in the row next to the order's own details. */
.print-job-files { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.print-job-hint {
    flex-basis: 100%;
    color: var(--text-muted);
    font-size: 0.75rem;
}
/* Said to the owner on /tryk: opening the files there is not the shop collecting them. */
.print-owner-note {
    margin: 0 0 1rem;
    padding: 0.75rem 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    color: var(--text-secondary);
    font-size: 0.85rem;
}

/* "Afhentet til og med nr." — the discount box's shape, because it is the same gesture: type a
   short value, press once to see what it means, press again to apply it. */
.pickup-box {
    margin: 0 0 1rem;
    padding: 1rem;
    background: var(--bg-card);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-md);
}
.pickup-box-label {
    display: block;
    margin-bottom: 0.5rem;
    color: var(--text-secondary);
    font-size: 0.9rem;
}
.pickup-box-row { display: flex; gap: 0.5rem; align-items: stretch; flex-wrap: wrap; }
.pickup-box-input {
    flex: 1 1 8rem;
    min-width: 0;
    max-width: 12rem;
    padding: 0.7rem 0.9rem;
    background: var(--bg-primary);
    color: var(--text-primary);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: 1rem;
}
.pickup-box-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pickup-box-input::placeholder { color: var(--text-muted); }
.pickup-box .cover-hint { margin: 0.6rem 0 0; }

/* Afsluttet is history, folded away until somebody goes looking. */
.print-finished > summary {
    cursor: pointer;
    color: var(--text-secondary);
    font-size: 0.9rem;
    padding: 0.25rem 0;
}
.print-finished[open] > summary { margin-bottom: 0.75rem; }

/* A job coming back through the press. It has to read as a badge rather than as part of the
   reference, because the reference is what the shop matches against work they have already done.

   It sits in .admin-order-customer, which is a column flexbox, and a flex item in a column is
   stretched to the full width unless told otherwise — so the "badge" was an orange bar across
   the whole row (noticed on /tryk on 2026-10-04). align-self keeps it its own size, under the
   reference it belongs to. */
.print-job-reprint {
    align-self: flex-start;
    margin-top: 0.3rem;
    padding: 0.1rem 0.4rem;
    border-radius: 999px;
    background: #b45309;
    color: #fff;
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.08em;
}

/* An order with nowhere to ship it needs to stand out — it can't be fulfilled. */
.admin-order-noaddress { color: #b45309; }

/* Who the album is for, on /tryk: set as an address block, one element per line, in the app's
   reading colour rather than the muted one the surrounding job details use. It was a label to copy
   while the print shop posted the parcels; since 2026-09-23 it is what they sort the copies by. */
.print-job-address {
    display: flex;
    flex-direction: column;
    margin-top: 0.5rem;
    padding: 0.5rem 0.65rem;
    background: var(--bg-card-hover);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-style: normal;
    font-size: 0.85rem;
    line-height: 1.35;
    color: var(--text-primary);
    width: fit-content;
    min-width: 12rem;
}

.print-job-address-label {
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 0.2rem;
}

.print-job-address-phone { color: var(--text-secondary); margin-top: 0.2rem; }

/* The chosen pakkeshop, above the receiver, in the accent so it is not read as the receiver's
   own address. */
.print-job-parcelshop {
    color: var(--accent);
    padding-bottom: 0.3rem;
    margin-bottom: 0.3rem;
    border-bottom: 1px dashed var(--border);
}

/* "Modtager:", separating the counter above from the person below. */
.print-job-address-sublabel {
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.order-price-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin: 0.75rem 0;
    padding-top: 0.75rem;
    border-top: 1px solid var(--border);
    font-size: 1.05rem;
}
.order-price-row strong { color: var(--accent); }
/* Sits under the total: the moms is part of that figure, not another charge. */
.order-price-vat { color: var(--text-muted); font-size: 0.85rem; margin: 0.25rem 0 0; text-align: right; }

/* ---- Checkout: the terms consent and the card marks ---- */

/* The tick box between the customer and the order button. Given a height floor the same way the
   delivery options were: one line of text is 29 px, under the ~44 px a fingertip actually covers,
   and a consent that is awkward to hit on a phone is a consent that gets pressed twice. */
.terms-consent {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    box-sizing: border-box;
    min-height: 2.75rem;
    margin-top: 1.25rem;
    padding: 0.5rem 0;
    font-size: 0.9rem;
    line-height: 1.5;
    color: var(--text-primary);
    cursor: pointer;
}
.terms-consent input {
    width: auto;
    margin-top: 0.25rem;
    flex-shrink: 0;
    accent-color: var(--accent);
    cursor: pointer;
}
.terms-consent a { color: var(--accent); }

/* The order button spends the first part of every visit disabled — the terms are not ticked yet —
   so it has to look it. An enabled-looking button that does nothing reads as the page being
   broken, which is the same reason the soft-photo dialog's button carries this. */
.order-actions .app-btn[disabled] {
    opacity: 0.45;
    cursor: not-allowed;
}

.order-payment-marks { margin-top: 1.25rem; text-align: center; }

.payment-marks {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.5rem;
}

/* The tiles are white — the marks are drawn for light backgrounds — so they need an edge of their
   own, or they read as holes cut in the page rather than as cards. */
.payment-mark {
    display: inline-flex;
    line-height: 0;
    border-radius: 4px;
    box-shadow: 0 0 0 1px var(--border-strong);
}

.order-payment-note {
    margin-top: 0.5rem;
    color: var(--text-muted);
    font-size: 0.8rem;
}

/* ---- Admin shell (dashboard / orders / messages) ---- */
.admin-page {
    min-height: 100svh;
    padding: 1.5rem;
    max-width: 880px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.admin-nav {
    display: flex;
    gap: 0.4rem;
    flex-wrap: wrap;
    margin-bottom: 0.25rem;
}

.admin-nav-link {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.5rem 1rem;
    border-radius: 100px;
    font-size: 0.85rem;
    font-weight: 700;
    letter-spacing: 0.03em;
    color: var(--text-secondary);
    background: var(--bg-card);
    border: 1px solid var(--border);
    text-decoration: none;
}

.admin-nav-link:hover { color: var(--text-primary); background: var(--bg-card-hover); text-decoration: none; }

.admin-nav-link--active {
    color: #241d12;
    background: linear-gradient(180deg, var(--accent-soft), var(--accent) 60%, var(--accent-strong));
    border-color: transparent;
}

.admin-nav-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.25rem;
    height: 1.25rem;
    padding: 0 0.35rem;
    border-radius: 100px;
    background: var(--danger);
    color: #241d12;
    font-size: 0.7rem;
    font-weight: 800;
}

.admin-nav-link--active .admin-nav-badge { background: #241d12; color: var(--accent-soft); }

/* Dashboard KPI grid */
.admin-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 0.75rem;
}

.admin-stat {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: 1.1rem 1.2rem;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.admin-stat__value {
    font-family: var(--font-display);
    font-size: 1.9rem;
    font-weight: 600;
    color: var(--text-primary);
    line-height: 1;
}

.admin-stat__label {
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.admin-stat--accent .admin-stat__value { color: var(--accent); }

.admin-shortcuts { display: flex; gap: 0.6rem; flex-wrap: wrap; }

/* Statistik: the period switch reads as one choice, like the photo sort switch in the builder. */
.admin-period {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin-bottom: 1.1rem;
}

.admin-period-btn {
    padding: 0.35rem 0.8rem;
    font-size: 0.8rem;
}

/* The period on screen is disabled as well as marked, so it must stay legible. */
.admin-period-btn.is-active,
.admin-period-btn.is-active:disabled {
    background: rgba(201, 169, 106, 0.16);
    border-color: var(--border-focus);
    color: var(--accent);
    opacity: 1;
    cursor: default;
}

/* Statistik: the funnel, one row per step, each bar measured against the albums started. */
.admin-funnel {
    list-style: none;
    margin: 0 0 1rem;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.admin-funnel-step {
    padding: 0.9rem 1rem;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.admin-funnel-top {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.75rem;
}

.admin-funnel-label {
    font-weight: 600;
    color: var(--text-primary);
}

.admin-funnel-count {
    font-family: var(--font-display);
    font-size: 1.6rem;
    font-weight: 600;
    line-height: 1;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
}

.admin-funnel-bar {
    height: 8px;
    margin: 0.6rem 0 0.45rem;
    border-radius: 100px;
    background: var(--border);
    overflow: hidden;
}

.admin-funnel-bar > span {
    display: block;
    height: 100%;
    border-radius: inherit;
    background: var(--accent);
}

.admin-funnel-meta {
    margin: 0;
    font-size: 0.8rem;
    color: var(--text-muted);
}

/* Messages inbox */
.admin-messages { display: flex; flex-direction: column; gap: 0.6rem; }

.admin-msg {
    padding: 0.9rem 1rem;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.admin-msg--unread { border-color: var(--border-focus); background: var(--bg-card-hover); }

.admin-msg-top {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
}

.admin-msg-from { font-weight: 700; color: var(--text-primary); font-size: 0.95rem; }
.admin-msg-from a { color: var(--text-secondary); font-weight: 500; font-size: 0.85rem; margin-left: 0.4rem; }
.admin-msg-date { font-size: 0.75rem; color: var(--text-muted); white-space: nowrap; }
.admin-msg-subject { font-weight: 600; color: var(--accent); font-size: 0.9rem; }
.admin-msg-body { color: var(--text-secondary); font-size: 0.9rem; line-height: 1.55; white-space: pre-wrap; }
.admin-msg-actions { display: flex; gap: 0.5rem; margin-top: 0.2rem; flex-wrap: wrap; align-items: center; }

/* Only when something follows it. On /admin/ordrer this row is the PostNord download and the
   order list comes straight after — with nothing between them the button's edge touched the
   first order's card, measured at 0 px, and controls do not sit flush against what they act on.
   In a message card the same row is the last thing in the card, and a margin there would only
   pad the bottom of every message. */
.admin-msg-actions:not(:last-child) { margin-bottom: 1rem; }

/* The row that holds the long ones. "Hent 4 forsendelser som CSV til PostNord Send" is a
   sentence, not a label, and .app-btn's nowrap — the rule that keeps "Næste →" on one line — ran
   it off the side of the card on a phone: the row wraps, but a pill whose text cannot break has
   nothing to wrap. Here a button may break its lines, centre them, and never grow past the row.
   The short buttons in this row are unaffected: text that fits does not wrap. */
.admin-msg-actions .app-btn {
    white-space: normal;
    text-align: center;
    max-width: 100%;
    /* A pill's 12 px of vertical padding is judged against one line of text. Two lines fill it to
       the edges, so the label reads as crammed into the shape rather than set inside it. */
    padding-top: 0.85rem;
    padding-bottom: 0.85rem;
}
.admin-msg-confirm { color: var(--danger); font-size: 0.85rem; font-weight: 600; margin-left: auto; }

.admin-unread-dot {
    display: inline-block;
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    background: var(--accent);
    margin-right: 0.4rem;
}

/* ---- Contact form extras ---- */
.auth-card--wide { max-width: 520px; }

.form-field textarea {
    width: 100%;
    background: var(--bg-card-hover);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.75rem 1rem;
    font-size: 0.95rem;
    color: var(--text-primary);
    font-family: inherit;
    line-height: 1.5;
    resize: vertical;
    transition: var(--transition);
}

.form-field textarea:focus {
    border-color: var(--border-focus);
    box-shadow: 0 0 0 3px rgba(201,169,106,0.1);
}

.form-field textarea::placeholder { color: var(--text-muted); }

.auth-success {
    background: rgba(201,169,106,0.1);
    border: 1px solid var(--border-focus);
    color: var(--text-primary);
    border-radius: var(--radius-sm);
    padding: 0.85rem 1rem;
    font-size: 0.9rem;
    line-height: 1.5;
    margin-bottom: 1rem;
}

.auth-submit--link { display: block; text-align: center; text-decoration: none; }
.auth-submit--link:hover { text-decoration: none; }

/* ---- Sticky footer layout ---- */
.memo-app { display: flex; flex-direction: column; min-height: 100vh; }
.memo-main { flex: 1 0 auto; }

/* ---- Site footer ---- */
.site-footer {
    flex-shrink: 0;
    margin-top: 3rem;
    padding: 2.5rem 1.25rem calc(2.5rem + env(safe-area-inset-bottom));
    background: rgba(26, 23, 20, 0.6);
    border-top: 1px solid var(--border);
}

/* Three bands — the pages, the marks, the company — and the gap between them is deliberately
   larger than any gap inside one. That is the whole of the grouping: every row sat 1rem from the
   next before, so six links, two glyphs, two cards and five scraps of company detail read as one
   undifferentiated heap. */
.site-footer-inner {
    max-width: 900px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 2rem;
}

/* Wide apart across, close together down: on a phone these wrap to two lines, and the wrap has to
   look like one list continuing rather than two rows of something. */
.site-footer-links { display: flex; flex-wrap: wrap; gap: 0.6rem 1.75rem; }
.site-footer-links a { color: var(--text-secondary); text-decoration: none; font-size: 0.875rem; }
.site-footer-links a:hover { color: var(--accent); }

/* The glyphs and the cards on one line, with a gap wide enough that they read as two groups and
   not as four icons in a row. */
.site-footer-marks {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 1rem 2rem;
}

/* The social marks. Sized as touch targets rather than as glyphs — the icon is 20 px and the
   thing you press is 44, which is the difference between a link and a link a thumb can hit. */
.site-footer-social { display: flex; gap: 0.5rem; }

.site-social {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    margin: -0.6rem 0;
    border-radius: 50%;
    color: var(--text-secondary);
    transition: var(--transition);
}

.site-social:hover,
.site-social:focus-visible {
    color: var(--accent);
    background: var(--bg-card-hover);
}

/* The marks are centred where they sit at the end of the checkout, under the button they belong
   to. Down here they are one item in a column of left-aligned rows, and centring them would be
   the only thing in the footer that does not start at the same edge. */
.site-footer .payment-marks { justify-content: flex-start; }

/* One detail per line on a phone. Wrapping them was what made this corner look like a heap: five
   scraps of company laid out by whatever happened to fit turned into a two-column table by
   accident — "MemoMinder ApS" over there, "CVR: …" over here — with no rule behind either column
   and a different shape at every width. */
.site-footer-legal {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    color: var(--text-muted);
    font-size: 0.8rem;
    line-height: 1.5;
    border-top: 1px solid var(--border);
    padding-top: 1.25rem;
}
.site-footer-legal a { color: var(--text-muted); }

/* Where the whole run fits on one line it can have it, and then the details need a separator of
   their own: a dot rather than a wider gap, because at that point they are one sentence's worth
   of company rather than a list. Kept to the width where they genuinely fit — a dot left hanging
   at the end of a wrapped line is exactly the sloppiness this is here to avoid, and measuring is
   the only way to know: the run wants ~720 px, so it wraps at 720 and clears at 900 with room to
   spare for a machine that has to fall back from Inter to something wider. */
@media (min-width: 900px) {
    .site-footer-legal { flex-direction: row; flex-wrap: wrap; gap: 0.3rem 0.85rem; }

    .site-footer-legal span:not(:last-child)::after {
        content: "·";
        margin-left: 0.85rem;
        color: var(--border-strong);
    }
}

/* ---- Cookie notice ----
   A card that floats above the bottom of the page, not a bar welded across it: the full-width
   strip is the shape of a consent wall, and this is a sentence. It is inset on every side so the
   page it sits over is still visibly the page, and it is as quiet as anything in the footer —
   0.8rem, secondary text, no accent except the two things you can press.

   Below the album viewer and the dialogs (520/560) on purpose: a modal is a question being asked,
   and it should cover this. Above the navbar (100), because a notice that a sticky header can
   slide over is a notice nobody reads. */
.cookie-notice {
    position: fixed;
    /* Clear of the screen on every side, at every width — a card glued to the bottom edge shows
       its own rounded corners as two notches cut out of it, and that is the whole difference
       between something floating over the page and something bolted to the window.

       The insets carry the safe area rather than the padding doing it: on an iPhone the last
       34 px of the viewport are under the bar the system draws, and the sides go under the notch
       in landscape. Put in the padding, that space is inside the card and looks like a mistake;
       put here, the card simply starts above it. */
    left: max(0.75rem, env(safe-area-inset-left));
    right: max(0.75rem, env(safe-area-inset-right));
    bottom: max(0.75rem, env(safe-area-inset-bottom));
    z-index: 400;

    max-width: 900px;
    margin: 0 auto;
    padding: 0.85rem 1rem;

    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.6rem 1rem;

    background: var(--bg-card);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-card);
}

/* Wide enough to be worth setting as prose, and allowed to take the whole line under ~320 px,
   where the button then drops below it. `flex: 1 1 16rem` is what keeps that from happening at
   430 px, the width where a phone in a big font still has room for both on one line. */
.cookie-notice-text {
    flex: 1 1 16rem;
    margin: 0;
    color: var(--text-secondary);
    font-size: 0.8rem;
    line-height: 1.5;
}

.cookie-notice-text a { color: var(--accent); white-space: nowrap; }

/* Pushed to the far end, and on its own line when the text has taken the first one. Sized as a
   touch target rather than as a word: the label is two characters and the thing a thumb has to
   hit is 44 px tall. */
.cookie-notice-ok {
    margin-left: auto;
    flex: 0 0 auto;
    min-height: 2.75rem;
    padding: 0 1.4rem;
    border-radius: var(--radius-sm);
    background: var(--accent);
    color: var(--bg-primary);
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    transition: var(--transition);
}

.cookie-notice-ok:hover { background: var(--accent-soft); }
.cookie-notice-ok:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; }

/* Without this line the notice never goes away, and that is not a bug in the script.

   `hidden` hides an element through the *browser's* stylesheet, which says `[hidden] { display:
   none }` — and a declaration in an author stylesheet beats the browser's whatever its
   specificity, so the `display: flex` above simply outranked it. The attribute went on, the
   localStorage key was written, and the card stayed exactly where it was. Steven reported it the
   day after it shipped.

   Any rule that gives an element a `display` takes on the job of honouring `hidden` itself. This
   is that rule, and it is an author rule matching class-plus-attribute, so it outranks the
   `display: flex` it is correcting. CookieNoticeHidesTests fails if it goes missing again. */
.cookie-notice[hidden] { display: none; }

/* ---- Legal / info pages ---- */
.legal-page {
    max-width: 760px;
    margin: 0 auto;
    padding: 1.5rem 1.25rem 2rem;
    line-height: 1.7;
}
.legal-page h1 { font-family: 'Fraunces', serif; margin-bottom: 0.5rem; }

/* Prose spacing. The reset zeroes every margin, which on a page that is nothing but prose meant
   two paragraphs ran together as one block — "…bæredygtighed for øje." and "Vi holder til på Als"
   read as a single thought on /om-os, and the terms and the privacy policy are pages of that.
   Only unclassed paragraphs: the ones that carry a class (.legal-lead, .how-note,
   .product-price-offer) are laid out by their own rule, and this selector would outrank it. */
.legal-page p:not([class]) { margin-bottom: 0.9rem; }

/* The same reset takes the indent off lists, and a marker with nothing to sit in is drawn
   *outside* the text column — into the page's own 1.25rem of padding, which on a phone left the
   bullets a few pixels from the edge of the screen. The indent puts them back inside the measure
   they belong to. Bare lists only; .how-steps and the rest bring their own layout. */
.legal-page ul,
.legal-page ol {
    margin: 0 0 1rem;
    padding-left: 1.35rem;
}

.legal-page li { margin-bottom: 0.35rem; }
.legal-page li:last-child { margin-bottom: 0; }

/* The cookie names in clause 7. Monospace so a visitor can match one against what their browser
   shows them, and `anywhere` because ".AspNetCore.Identity.Application" is 31 unbroken characters
   — a browser will not break on the dots by itself, and on a 320 px screen it ran out past the
   edge of the page. Sized at 0.9em rather than a fixed size: monospace reads larger than Inter at
   the same point size, and next to it at 1em these looked shouted. */
.legal-page code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.9em;
    color: var(--text-secondary);
    overflow-wrap: anywhere;
}
.legal-page h2 { margin: 1.75rem 0 0.5rem; font-size: 1.15rem; color: var(--accent); }
/* Anything a link can land on (/om-os#teamet from the front page, 2026-10-08). The navbar is fixed
   and 54px tall, so a heading scrolled to the very top of the window ends up underneath it — the
   visitor would arrive at the team with the team's own title hidden. */
.legal-page [id] { scroll-margin-top: calc(54px + env(safe-area-inset-top) + 1rem); }
/* Subheadings inside a numbered clause. The reset zeroes every margin, so without this they sit
   flush against the paragraph above and read as part of it. Not accent-coloured: the colour is
   what tells a clause heading apart from a heading inside one. */
.legal-page h3 { margin: 1.25rem 0 0.4rem; font-size: 1rem; }
/* Prose links only. A link that is dressed as a button brings its own colours, and this rule
   outranks them — `.legal-page a` is a class plus an element, `.btn-start` is one class — so
   without the exclusion the gold here lands on top of the gold button and the label disappears.
   That is what happened to "START DIT MAGASIN" on /magasinet. */
.legal-page a:not(.btn-start):not(.app-btn) { color: var(--accent); }
.legal-lead { font-size: 1.05rem; color: var(--text-secondary); }
.legal-updated { margin-top: 2rem; color: var(--text-muted); font-size: 0.8rem; }

/* ---- Price breakdown, discount code and mængderabat ---------------------------------
   The lines above the total. They are deliberately quieter than .order-price-row: that row
   is the number the customer is agreeing to, and everything here is the arithmetic behind it. */
.order-price-lines {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    margin-top: 1rem;
}
.order-price-line {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    color: var(--text-secondary);
    font-size: 0.92rem;
}
.order-price-line--save { color: var(--accent-soft); }

/* ---- Delivery method, on the order form -------------------------------------------
   Two radios under the address. The whole row is the label, so the price is as clickable
   as the dot is — on a phone the dot alone is a target worth missing.

   Qualified as label.delivery-option, and the four reverts below are not decoration. These
   radios sit inside a .form-field, whose rules are written for a captioned text box: its
   `label` is a block of uppercase muted grey with a caption's margin under it, and a lone
   class does not outrank `.form-field label`. Left to it, the row stopped being a row — the
   text read as a field caption and the dot, stretched to `width: 100%` by `.form-field
   input`, drew itself centred on a line of its own above it. */
label.delivery-option {
    display: flex;
    align-items: baseline;
    gap: 0.55rem;
    cursor: pointer;
    color: var(--text-primary);
    font-size: 0.95rem;
    font-weight: 400;
    letter-spacing: normal;
    text-transform: none;
    margin-bottom: 0;

    /* A finger, not a cursor. The row is the target, and one line of text is 29 px of it —
       under the ~44 px a fingertip actually covers, which is how a tap lands between the two
       options and does nothing. min-height sets the floor rather than padding alone doing it,
       so the row that wraps to two lines on a narrow phone grows from its own height instead
       of being padded twice. */
    box-sizing: border-box;
    min-height: 2.75rem;
    padding: 0.5rem 0;
}
.delivery-option input { width: auto; accent-color: var(--accent); cursor: pointer; }

/* No top margin of its own: the row above it now ends in half a rem of padding, and the two
   together read as a gap between the option and its own explanation. */
.delivery-option + .cover-hint { margin: 0 0 0.7rem 1.55rem; }

/* The pakkeshop picker, under the option it belongs to. Indented to the same 1.55rem the hint
   above uses, so the shops read as part of that choice rather than as a third one. */
.parcelshop-picker {
    margin: 0 0 0.9rem 1.55rem;
}

.parcelshop-picker .cover-hint { margin: 0 0 0.5rem; }

/* Tighter than the two delivery options above it: this is a list of five, and five rows at the
   full 2.75rem tap target would push the order button off a phone screen on their own. Still
   well over a fingertip once the two lines of a shop are counted. */
.parcelshop-picker label.delivery-option {
    min-height: 2.25rem;
    padding: 0.35rem 0;
    font-size: 0.9rem;
}

.parcelshop-distance { color: var(--text-muted); }
.order-price-was {
    color: var(--text-muted);
    margin-right: 0.4rem;
}
.order-price-saved {
    margin: 0.25rem 0 0;
    text-align: right;
    color: var(--accent-soft);
    font-size: 0.9rem;
}

/* The box the client asked for: somewhere to type a code at payment. */
.discount-box {
    margin-top: 1.25rem;
    padding: 1rem;
    background: var(--bg-card);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-md);
}
.discount-box-label {
    display: block;
    margin-bottom: 0.5rem;
    color: var(--text-secondary);
    font-size: 0.9rem;
}
.discount-box-row {
    display: flex;
    gap: 0.5rem;
    align-items: stretch;
    flex-wrap: wrap;
}
.discount-box-input {
    flex: 1 1 10rem;
    min-width: 0;
    padding: 0.7rem 0.9rem;
    background: var(--bg-primary);
    color: var(--text-primary);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: 1rem;
    /* Codes are stored upper case, so they are shown that way while being typed — it saves
       explaining that "sommer2026" and "SOMMER2026" are the same code. */
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.discount-box-input:focus {
    outline: none;
    border-color: var(--border-focus);
}
/* See .form-field input:focus-visible — a keyboard needs to be able to see where it is. */
.discount-box-input:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.discount-box-input::placeholder { color: var(--text-muted); letter-spacing: normal; }
.discount-box-btn { flex: 0 0 auto; }
.discount-box-ok {
    margin: 0.6rem 0 0;
    color: var(--accent-soft);
    font-size: 0.9rem;
}
.discount-box-error {
    margin: 0.6rem 0 0;
    color: var(--danger);
    font-size: 0.9rem;
}

/* ---- Newsletter sign-up ---------------------------------------------------------- */
.newsletter-box {
    margin: 2rem 0 0;
    padding: 1.25rem;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}
.newsletter-title {
    margin: 0 0 0.35rem;
    font-family: 'Fraunces', serif;
    font-size: 1.1rem;
    color: var(--accent);
}
.newsletter-text {
    margin: 0 0 0.9rem;
    color: var(--text-secondary);
    font-size: 0.95rem;
}
.newsletter-row {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
}
.newsletter-input {
    flex: 1 1 12rem;
    min-width: 0;
    padding: 0.7rem 0.9rem;
    background: var(--bg-primary);
    color: var(--text-primary);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: 1rem;
}
.newsletter-input:focus { outline: none; border-color: var(--border-focus); }
.newsletter-input::placeholder { color: var(--text-muted); }
.newsletter-btn { flex: 0 0 auto; }
.newsletter-consent {
    margin: 0.75rem 0 0;
    color: var(--text-muted);
    font-size: 0.8rem;
    line-height: 1.5;
}

/* A consent tick-box: the label is the hit area, so the whole line is clickable on a phone. */
.auth-check {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    margin: 1rem 0;
    color: var(--text-secondary);
    font-size: 0.9rem;
    line-height: 1.5;
    cursor: pointer;
}
.auth-check input { margin-top: 0.2rem; flex: 0 0 auto; }

/* ---- Admin: discount codes and the newsletter list -------------------------------- */
.admin-price-summary {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    margin: 0.75rem 0;
}

/* The save button and the "last changed" line beside it. They wrap on a narrow screen rather
   than pushing the stamp off the edge — the owner does this from a phone as often as not. */
.admin-price-actions {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-top: 0.5rem;
}
.admin-code-list {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin-top: 1rem;
}
.admin-code {
    padding: 0.9rem 1rem;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}
/* A retired code is kept and dimmed rather than removed — orders still name it. */
.admin-code--off { opacity: 0.55; }
/* Wraps, so that a name too long to share the line takes the line and sends the badge to the
   next one — rather than the badge being broken, squeezed, or pushed out of the card. */
.admin-code-top {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.25rem 1rem;
}

/* The name is a discount code on /admin/rabatkoder and an e-mail address on /admin/nyhedsbrev,
   and an address is both longer and — being one unbroken token — unbreakable. On a 390 px phone
   `cabuxito1995@gmail.com` measured 225 px against 263 px of row, which left too little for the
   badge beside it: neither could shrink, so the badge hung 25 px off the right side of its own
   card. `min-width: 0` lets the name give way and `overflow-wrap` lets it break where there is
   no space to break at, which is what keeps the pair inside the card. */
.admin-code-name {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 1.05rem;
    letter-spacing: 0.06em;
    color: var(--text-primary);
    min-width: 0;
    overflow-wrap: anywhere;
}

/* The badge keeps its whole width — it is one word, and a "Tilmeldt" broken in half to save a
   character of the address beside it is the wrong trade. `margin-left: auto` is what keeps it on
   the right of its line once it has been sent to a line of its own, where space-between has
   nothing left to space it against. */
.admin-code-value {
    color: var(--accent);
    font-weight: 600;
    flex-shrink: 0;
    margin-left: auto;
}
.admin-code-desc { margin: 0.35rem 0 0; color: var(--text-secondary); font-size: 0.9rem; }
.admin-code-meta { margin: 0.35rem 0 0; color: var(--text-muted); font-size: 0.82rem; }
/* The edit form opens inside the row it belongs to, ruled off from the code's own lines so it
   reads as part of that code and not as a second form on the page. */
.admin-code-edit {
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--border);
}

/* ---- The product shot, on the front page and /magasinet ----------------------------
   ProductShot checks for the file before rendering the figure at all, so nothing here has to
   cope with it being absent. */
.product-shot {
    margin: 1.5rem 0;
    text-align: center;
}
/* On the front page it sits inside the story, between the lead and the paragraphs (since
   2026-10-08), whose own gap already spaces it; this adds the same little on both sides. */
.product-shot--landing {
    margin: 0.5rem 0;
}
.product-shot img {
    max-width: 100%;
    height: auto;
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-card);
}
.product-shot figcaption {
    margin-top: 0.6rem;
    color: var(--text-muted);
    font-size: 0.85rem;
}

/* ---- The team on /om-os -------------------------------------------------------------
   Photo above the text on a phone, beside it from 640 px. Beside it on a phone would leave the
   words a column of three or four to a line next to the photo. Both photographs are 4:5, so the
   cards line up without the browser cropping either of them. The photo is dressed like the
   product shot above, so the two read as the same site. */
.team-member {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0.9rem;
    margin: 1rem 0 1.75rem;
}
.team-member-photo { max-width: 200px; }
.team-member-photo img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-card);
}
/* `.legal-page h3` gives a heading room above it to part it from the clause before; at the top of
   a card the photo is what it sits beside, and that room would push the name below its top edge. */
.team-member h3 { margin: 0; font-size: 1.1rem; }
.team-member-role {
    margin: 0.15rem 0 0.75rem;
    color: var(--text-muted);
    font-size: 0.8rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}
.team-member-lead {
    margin-bottom: 0.9rem;
    color: var(--text-secondary);
    font-style: italic;
}
/* The card's own bottom margin parts it from the next one; the prose rule's would add to it. */
.team-member-text p:last-child { margin-bottom: 0; }
@media (min-width: 640px) {
    .team-member {
        grid-template-columns: 170px minmax(0, 1fr);
        gap: 1.5rem;
        align-items: start;
    }
}
.product-price-offer {
    padding: 0.9rem 1rem;
    background: var(--bg-card);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm);
    color: var(--accent-soft);
}
.product-price-offer s { color: var(--text-muted); margin: 0 0.15rem; }

/* The admin discount form has the only <select> on the site, so it gets the same treatment the
   inputs beside it already have — otherwise it renders as the browser's own grey control in the
   middle of a dark card. */
.form-field select {
    width: 100%;
    background: var(--bg-card-hover);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.75rem 1rem;
    font-size: 0.95rem;
    color: var(--text-primary);
    font-family: inherit;
    outline: none;
}
.form-field select:focus { border-color: var(--border-focus); }

/* ---- Admin: the mængderabat tiers ---------------------------------------------------
   One row per tier: the quantity it starts at, the percentage as an editable field, and a way
   to remove it. The quantity is text and not an input on purpose — moving a tier from 3 to 5
   is "remove this one and add another", so editing it in place is not offered. */
.admin-tier-list {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin: 1rem 0;
}
.admin-tier {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
    padding: 0.7rem 0.9rem;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}
.admin-tier-qty {
    flex: 1 1 8rem;
    color: var(--text-primary);
}
.admin-tier-edit {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    flex: 0 0 auto;
}
.admin-tier-edit input {
    width: 5rem;
    padding: 0.5rem 0.6rem;
    background: var(--bg-card-hover);
    color: var(--text-primary);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-family: inherit;
    font-size: 0.95rem;
    text-align: right;
    outline: none;
}
.admin-tier-edit input:focus { border-color: var(--border-focus); }
.admin-tier-unit { color: var(--text-muted); }
.admin-tier-new { margin-top: 1.25rem; }

/* ---- Soft-photo consent dialog ---------------------------------------------------------- */
/* The last thing between the customer and paying, so it is a real modal rather than a banner
   they can scroll past: the client asked for it precisely so a complaint about a blurred print
   can be answered, and that only holds if it was unmissable. */

/* The uploader's duplicate question shares the shape of the soft-photo dialog below rather than
   inventing a second one: they are the same kind of interruption, and two dialogs that look
   almost alike read as a bug. Only what differs — a list of file names instead of thumbnails —
   has rules of its own, further down. */
.soft-photo-backdrop,
.dup-photo-backdrop {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    background: rgba(10, 8, 6, 0.78);
    backdrop-filter: blur(3px);
}

.soft-photo-dialog,
.dup-photo-dialog {
    width: min(34rem, 100%);
    max-height: min(90vh, 44rem);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 1.5rem;
    background: var(--bg-card);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-card);
}

.soft-photo-title,
.dup-photo-title {
    margin: 0;
    font-size: 1.15rem;
    color: var(--text-primary);
}

.soft-photo-text,
.dup-photo-text {
    margin: 0;
    font-size: 0.9rem;
    line-height: 1.55;
    color: var(--text-secondary);
}

/* Sized so a handful of photos are all visible at once and forty do not push the consent and
   its button off the bottom of the dialog. */
.soft-photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));
    gap: 0.5rem;
    max-height: 14rem;
    overflow-y: auto;
    padding: 0.25rem;
}

.soft-photo-item { margin: 0; }

.soft-photo-item img,
.soft-photo-item-blank {
    display: block;
    width: 100%;
    aspect-ratio: 1;
    object-fit: cover;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border-strong);
    background: var(--bg-secondary);
}

.soft-photo-item figcaption {
    margin-top: 0.2rem;
    font-size: 0.62rem;
    text-align: center;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.soft-photo-consent {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--text-primary);
    cursor: pointer;
}

.soft-photo-consent input { margin-top: 0.2rem; flex-shrink: 0; }

.soft-photo-actions,
.dup-photo-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    justify-content: flex-end;
}

/* Disabled until the box is ticked, and it has to look it — an enabled-looking button that does
   nothing reads as the page being broken. */
.soft-photo-actions .app-btn[disabled] {
    opacity: 0.45;
    cursor: not-allowed;
}

/* The duplicates are named rather than shown: the files have not been uploaded, so there is no
   thumbnail to show, and a name is what the customer recognises them by anyway. Capped at a few
   rows tall so a selection where everything is a duplicate does not push the buttons off screen. */
.dup-photo-list {
    margin: 0;
    padding: 0.5rem 0.75rem;
    max-height: 11rem;
    overflow-y: auto;
    list-style: none;
    background: var(--bg-secondary);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    font-size: 0.85rem;
    line-height: 1.7;
    color: var(--text-primary);
}

/* A long file name wraps rather than widening the dialog or hiding its own extension. */
.dup-photo-list li {
    overflow-wrap: anywhere;
}

.dup-photo-more {
    color: var(--text-muted);
    font-style: italic;
}
