/* EZWishlist design system — token set carried over from v1, rebuilt for htmx server-rendered pages */

:root {
    /* WCAG AA: #6366f1 measured 4.47:1 on white and 4.27:1 on --gray-50, just under the
       4.5 needed for body text — and it is both the link colour and the primary button
       background, so links and white-on-button both failed. Darkened one step; hover moved
       down with it. */
    --primary: #5b5bd6;
    --primary-hover: #4f46e5;
    --secondary: #ec4899;
    --success: #10b981;
    --warning: #f59e0b;
    /* #ef4444 gave 3.76:1 both as validation text on white and behind .btn-danger label. */
    --error: #b91c1c;

    --gray-50: #f9fafb;
    --gray-100: #f3f4f6;
    --gray-200: #e5e7eb;
    --gray-300: #d1d5db;
    --gray-400: #9ca3af;
    --gray-500: #6b7280;
    --gray-600: #4b5563;
    --gray-700: #374151;
    --gray-800: #1f2937;
    --gray-900: #111827;

    --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --radius: 0.5rem;
    --radius-lg: 0.75rem;
    --shadow: 0 1px 3px rgba(0,0,0,.1), 0 1px 2px rgba(0,0,0,.06);
    --shadow-md: 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -1px rgba(0,0,0,.06);
    --shadow-lg: 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -2px rgba(0,0,0,.05);
    --container: 1100px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html { font-size: 16px; }
body {
    font-family: var(--font);
    color: var(--gray-800);
    background: var(--gray-50);
    line-height: 1.6;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}
main { flex: 1; }
img { max-width: 100%; }
a { color: var(--primary); }

.container { max-width: var(--container); margin: 0 auto; padding: 0 1rem; }

/* Header */
.site-header {
    background: #fff;
    border-bottom: 1px solid var(--gray-200);
    position: sticky; top: 0; z-index: 50;
}
.site-header .container { display: flex; align-items: center; gap: 1rem; height: 64px; }
/* Wordmark lockup: script "EZ" + typewriter "wishlist". 26px is the wordmark floor —
   below that the clay gift mark stands alone (favicon, app icon). Ratio 0.725 puts
   "wishlist" at two-thirds of the EZ ink height, measured from rendered glyphs. */
.logo { font-weight: 400; font-size: 26px; line-height: 1.1; color: var(--gray-900); text-decoration: none; white-space: nowrap; }
.logo .ez { font-family: 'Caveat Brand', cursive; font-weight: 700; color: var(--primary); }
.logo .rest { font-family: 'Courier Wordmark', 'Courier New', monospace; font-size: .725em; margin-left: .12em; letter-spacing: .035em; }
.site-nav { display: flex; gap: 1rem; align-items: center; margin-left: auto; }
.site-account { flex: none; display: flex; align-items: center; }
.site-account form { margin: 0; }
/* :not(.btn) so the nav's link colour can't outrank button styling — without it
   .site-nav a (0,1,1) beat .btn-primary (0,1,0) and the "Sign in" button rendered
   grey-on-indigo. Buttons keep whatever their .btn-* class sets. */
.site-nav a:not(.btn) { color: var(--gray-600); text-decoration: none; font-weight: 500; font-size: .95rem; }
.site-nav a:not(.btn):hover { color: var(--gray-900); }
/* Phones: the fixed 64px row crams four nav items against the logo and "My wishlists"
   breaks across two lines. Let the header wrap to a second row instead, and keep each
   link on one line. The account button (Sign in / Sign out) rides the logo row and the
   links take the row below, so a signed-in header is two rows, never three. */
@media (max-width: 640px) {
    .site-header .container {
        height: auto; min-height: 56px; flex-wrap: wrap;
        padding-top: .5rem; padding-bottom: .5rem; row-gap: .35rem; column-gap: .75rem;
    }
    /* margin-left:auto + flex-end keep the nav anchored right even when it wraps to its
       own row — matching where it sits when everything fits on one line. */
    .site-account { order: 1; margin-left: auto; }
    .site-nav { order: 2; flex-basis: 100%; flex-wrap: wrap; gap: .4rem .85rem; justify-content: flex-end; }
    .site-nav a:not(.btn) { font-size: .9rem; white-space: nowrap; }
}

/* Footer */
.site-footer {
    background: var(--gray-900); color: var(--gray-400);
    padding: 2rem 0; margin-top: 4rem; font-size: .875rem;
}
.site-footer a { color: var(--gray-300); text-decoration: none; }
.site-footer a:hover { color: #fff; }
/* Flex + gap rather than inline anchors with margin-right. Inline wrapping put no vertical
   space at all between rows once the row stacked on a phone — links a couple of pixels apart,
   which is a misclick waiting to happen — and it let "For shops & bloggers" break across two
   lines mid-phrase. row-gap is what separates the tap targets; the padding is what makes them
   tall enough to hit in the first place (~32px against a 21px line box). */
.footer-links { display: flex; flex-wrap: wrap; gap: .6rem 1.25rem; }
.footer-links a { white-space: nowrap; padding: .35rem 0; }
/* Stacked to two or three rows, so buy a little more separation between them. */
@media (max-width: 640px) { .footer-links { gap: .9rem 1.25rem; } }
/* gray-500 on the gray-900 footer was 3.67:1, and this is the affiliate disclosure — the
   one line with a regulatory reason to be legible. */
.site-footer .disclosure { margin-top: .75rem; font-size: .8rem; color: var(--gray-400); }
/* iOS Safari: overscrolling past the footer reveals the page canvas (near-white), and the
   bottom bar tints itself from that. Fixed elements ride the rubber-band on iOS, so a strip
   hanging one viewport below the layout viewport fills the reveal with the footer's ground.
   On body, not html, so kraft pages' warmed --gray-900 resolves to their espresso footer. */
body::after {
    content: ""; position: fixed; top: 100%; left: 0; right: 0; height: 100vh;
    background: var(--gray-900); z-index: -1; pointer-events: none;
}
body.popup::after { content: none; } /* chrome-less popup: no footer to match */

/* Buttons */
.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
    padding: .625rem 1.25rem; border-radius: var(--radius);
    font: 600 .95rem var(--font); cursor: pointer; text-decoration: none;
    border: 1px solid transparent; transition: all 150ms ease;
}
.btn:focus-visible { outline: 3px solid #c7d2fe; outline-offset: 1px; }
.btn-primary { background: var(--primary); color: #fff; }
.btn-primary:hover { background: var(--primary-hover); }
.btn-secondary { background: #fff; color: var(--gray-700); border-color: var(--gray-300); }
.btn-secondary:hover { background: var(--gray-100); }
.btn-danger { background: var(--error); color: #fff; }
.btn-danger:hover { background: #dc2626; }
.btn-sm { padding: .375rem .75rem; font-size: .85rem; }
.btn-lg { padding: .875rem 1.75rem; font-size: 1.05rem; }
.btn[disabled] { opacity: .55; cursor: not-allowed; }
/* Celebratory animated glow (reserve-success store CTA). The blurred gradient sits
   BEHIND the button (z-index -1) so the solid button face stays readable; the slow
   drift is decorative, so reduced-motion visitors get a static soft glow instead. */
.btn-glow { position: relative; z-index: 0; }
.btn-glow::before {
    content: ''; position: absolute; inset: -3px; z-index: -1;
    border-radius: calc(var(--radius) + 4px);
    background: linear-gradient(120deg, var(--primary), var(--secondary), var(--warning), var(--secondary), var(--primary));
    background-size: 300% 300%;
    filter: blur(7px); opacity: .75;
    animation: ezw-glow 5s ease-in-out infinite;
}
@keyframes ezw-glow {
    0% { background-position: 0% 50%; }
    50% { background-position: 100% 50%; }
    100% { background-position: 0% 50%; }
}
@media (prefers-reduced-motion: reduce) {
    .btn-glow::before { animation: none; }
}

/* Cards */
.card {
    background: #fff; border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg); box-shadow: var(--shadow); padding: 1.5rem;
}
.card + .card { margin-top: 1rem; }

/* Forms */
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; font-weight: 600; font-size: .875rem; margin-bottom: .375rem; color: var(--gray-700); }
.form-control {
    width: 100%; padding: .625rem .75rem; border: 1px solid var(--gray-300);
    border-radius: var(--radius); font: 400 .95rem var(--font); background: #fff;
}
.form-control:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(91,91,214,.15); }
/* iOS Safari zooms the whole page in when it focuses a field whose text is under 16px,
   and it never zooms back out. At .95rem (15.2px) every field here triggered it: tapping
   Title in the add-item dialog scaled the layout a few percent wider than the screen, and
   the modal — sized by position:fixed to the UNZOOMED viewport — ended up hanging off the
   right edge with its left padding intact, which reads as the dialog having slid sideways.
   16px is the only way to opt out that keeps pinch-zoom working (maximum-scale=1 on the
   viewport tag would fix it by banning zoom altogether, and fails WCAG 1.4.4).
   pointer:coarse rather than a width breakpoint — it's the input method that zooms, not
   the screen size, so a narrow desktop window keeps the .95rem the rest of the UI is set
   in. The bare element selectors are a net for any future field that skips .form-control;
   checkboxes, radios, ranges and colour wells never zoom and are sized off other things
   (the item form's "No limit" box off its label's line-height), so they stay out of it. */
@media (pointer: coarse) {
    .form-control,
    input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]),
    select, textarea { font-size: 16px; }
}
.form-hint { font-size: .8rem; color: var(--gray-500); margin-top: .25rem; }
.form-check { display: flex; align-items: flex-start; gap: .5rem; margin-bottom: .75rem; }
.form-check input { margin-top: .3rem; }
/* Hint copy inside a checkbox/radio label gets its own line under the label text —
   inline it ran into the label and read as one long sentence. */
.form-check .form-hint { display: block; font-weight: 400; }

/* Item-priority slider — words, not numbers, do the talking (see Services/Priorities) */
.priority-slider { width: 100%; max-width: 340px; display: block; accent-color: var(--primary); margin: .35rem 0 .1rem; }
.priority-slider:focus-visible { outline: 2px solid var(--primary); outline-offset: 4px; }

/* Inline range input (button builder's corner radius) — the priority slider's
   affordances without its full-width block layout, so it can sit beside a readout. */
.range-input { accent-color: var(--primary); }
.range-input:focus-visible { outline: 2px solid var(--primary); outline-offset: 4px; }

/* Screen-reader-only labels (claim-code input etc.) */
.visually-hidden {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Gifter's "enter claim code" fallback on /w/ — quiet by default */
.claim-code-entry { margin-top: .75rem; font-size: .9rem; }
.claim-code-entry summary { cursor: pointer; color: var(--gray-600); }

/* "Where do I paste this?" disclosure on /button — collapsed by default so the
   builder stays short for people who already know where their theme editor is */
.paste-help { font-size: .9rem; }
.paste-help summary { cursor: pointer; color: var(--gray-600); font-weight: 500; }
.paste-help ul { margin: .65rem 0 .5rem 1.15rem; color: var(--gray-600); }
.paste-help li { margin-bottom: .4rem; }
.paste-help strong { color: var(--gray-700); }
.paste-help .caveat { color: var(--gray-500); font-size: .85rem; margin-top: .6rem; }
/* "Where do I get the link?" (_GetLinkHelp) reuses .paste-help with numbered steps.
   The glyphs sit inline right after the word "Share" they illustrate. */
.paste-help ol { margin: .65rem 0 .1rem 1.15rem; color: var(--gray-600); }
.share-glyph { display: inline-block; vertical-align: -2px; margin-left: .15rem; color: var(--gray-700); }
/* One-tap "Paste" button (_PasteButton) at the head of the paste-a-link rows. Compact
   padding, and the glyph drops at phone widths — the row holds three elements, and every
   pixel the button keeps comes out of the URL box beside it. "Paste" is the meaning;
   the glyph is garnish. */
.paste-btn { gap: .35rem; padding-left: .75rem; padding-right: .75rem; }
.paste-btn svg { flex: none; }
@media (max-width: 430px) { .paste-btn svg { display: none; } }

/* Six-box sign-in code entry on /account/login (JS-revealed; plain input without JS) */
.code-boxes { display: flex; gap: .5rem; }
.code-box {
    width: 3rem; height: 3.4rem; padding: 0; text-align: center;
    font-size: 1.4rem; font-weight: 700; font-variant-numeric: tabular-nums;
}
/* Motion, not just words, while the code is verified: the six-box widget submits itself,
   so a static line can read as part of the page rather than as something happening.
   Reduced-motion visitors get a still ring — the text alone carries the message. */
.spinner {
    display: inline-block; width: .85em; height: .85em; vertical-align: -.1em;
    margin-right: .45em; border: 2px solid var(--gray-300);
    border-top-color: var(--primary); border-radius: 50%;
    animation: ezw-spin .6s linear infinite;
}
@keyframes ezw-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .spinner { animation: none; }
}

/* "Keep track of gifts you've promised?" account offer on /r/ — quiet, not an alert */
.account-offer {
    border: 1px solid var(--gray-200); border-radius: var(--radius);
    padding: .875rem 1rem; font-size: .9rem; color: var(--gray-600);
}
.account-offer h2 { font-size: 1rem; color: var(--gray-700); margin-bottom: .25rem; }

/* Collapsed wrapper for the offer inside the item modal: one enticing line under the
   failed-lookup note, expanding in place — expanded it shoved the form fields down,
   and at the bottom of the modal body it sat under the fold unseen */
.bookmarklet-fold summary {
    cursor: pointer; font-size: .875rem; font-weight: 600; color: var(--primary);
    margin: -.25rem 0 .75rem;
}
.bookmarklet-fold summary::marker { color: var(--primary); }
.bookmarklet-fold[open] summary { margin-bottom: .25rem; }
.bookmarklet-fold .bookmarklet-offer { margin: 0 0 .75rem; }

/* The save button installs by DRAGGING to a bookmarks bar — a fine-pointer gesture.
   Its desktop-only class gates by WIDTH (720px), which lets tablets and landscape
   phones through: both exceed the breakpoint, neither has a usable bar to drag to
   (iPad's is hidden by default and touch-dragging onto it is unreliable; Android has
   none at all). So the pitch also gates on input method, the same signal the iOS
   focus-zoom fix uses. Covers every surface at once: the item modal's fold, /start's
   failed-lookup reveal, and the dashboard card's offer + its "Save in one click"
   bullet (.ways-list). */
@media (pointer: coarse) {
    .bookmarklet-fold, .bookmarklet-offer, .ways-list .desktop-only { display: none !important; }
    /* The "on a desktop there's also…" line stands in for the setup card wherever the
       drag can't happen — which is this media query, not just narrow screens: without
       it a tablet gets neither the pitch nor any mention the feature exists. */
    .bookmarklet-offer + .form-hint.mobile-only { display: block; }
}

/* "Save straight from the store" setup card (_BookmarkletOffer) — the same quiet
   treatment as .account-offer: it's an offer, not an alert, even when it appears
   under a failed-lookup message */
.bookmarklet-offer {
    border: 1px solid var(--gray-200); border-radius: var(--radius);
    padding: .875rem 1rem; font-size: .9rem; color: var(--gray-600);
    margin-top: .75rem;
}
.bookmarklet-offer ol { margin: .6rem 0 .75rem 1.15rem; padding: 0; }
.bookmarklet-offer li { margin-top: .35rem; }
.bookmarklet-offer kbd {
    border: 1px solid var(--gray-300); border-bottom-width: 2px; border-radius: 4px;
    padding: 0 .3rem; font-size: .8rem; background: var(--gray-100); color: var(--gray-700);
}

/* Width-based stand-in for "has a bookmarks bar": the drag-to-bookmarks flow needs a
   desktop browser, phones get the copy-the-link instructions instead */
.mobile-only { display: none; }
@media (max-width: 720px) {
    .desktop-only { display: none; }
    .mobile-only { display: block; }
    /* display:block would break a button's inline-flex label centering */
    .mobile-only.btn { display: inline-flex; }
}
.validation-error, .field-validation-error { color: var(--error); font-size: .85rem; }
.validation-summary-errors ul { list-style: none; color: var(--error); font-size: .9rem; }

/* Alerts */
.alert { padding: .875rem 1rem; border-radius: var(--radius); margin-bottom: 1rem; font-size: .925rem; }
.alert-success { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; }
.alert-error { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
.alert-info { background: #eff6ff; color: #1e40af; border: 1px solid #bfdbfe; }
.alert-warning { background: #fffbeb; color: #92400e; border: 1px solid #fde68a; }

/* Badges */
.badge { display: inline-block; padding: .2rem .6rem; border-radius: 999px; font-size: .75rem; font-weight: 700; }
.badge-available { background: #ecfdf5; color: #065f46; }
.badge-reserved { background: #fffbeb; color: #92400e; }
/* gray-500 on gray-100 was 4.39:1 — the only badge that missed. */
.badge-purchased { background: var(--gray-100); color: var(--gray-600); }

/* Hero */
.hero { background: linear-gradient(135deg, #eef2ff 0%, #fdf2f8 100%); padding: 4.5rem 0; text-align: center; }
.hero h1 { font-size: clamp(2rem, 5vw, 3rem); font-weight: 800; color: var(--gray-900); line-height: 1.15; }
.hero p.lede { font-size: 1.2rem; color: var(--gray-600); max-width: 620px; margin: 1rem auto 2rem; }

/* Item grid (public wishlist) */
.item-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1.25rem; }
/* `.card + .card` adds a top margin every card but the first gets, which in a stretch
   grid leaves the first card exactly 1rem taller than its row. Same fix as .feature-grid. */
.item-grid > .card { margin-top: 0; }
/* List cards on /explore: thumbnails pinned to the bottom so cards of differing title
   length still line their image strips up across a row. */
.list-card { display: flex; flex-direction: column; text-decoration: none; color: inherit; }
.list-thumbs { display: flex; gap: .375rem; margin-top: auto; padding-top: .875rem; }
.list-thumbs img, .list-thumbs-more, .list-thumbs .img-fallback {
    width: 44px; height: 44px; border-radius: var(--radius);
    border: 1px solid var(--gray-200); background: var(--gray-100); flex: none;
}
.list-thumbs img { object-fit: cover; }
.list-thumbs-more {
    display: flex; align-items: center; justify-content: center;
    font-size: .8rem; font-weight: 600; color: var(--gray-500);
}
/* Stands in for a product image, both when an item has none and when the store's CDN
   fails to serve one (see the data-fallback handler in _Layout). One class for both so
   the two cases are indistinguishable — sized per context, since the same glyph fills a
   170px card tile and a 44px thumbnail. */
.img-fallback { display: inline-flex; align-items: center; justify-content: center; line-height: 1; }
.item-img .img-fallback { font-size: 2.5rem; }
.list-thumbs .img-fallback { font-size: 1.35rem; }
.data .img-fallback { font-size: 1.5rem; }
/* /explore trending: compact tiles. minmax(150px) fits two per row on a phone, so the
   trending strip stops shoving the wishlist cards (the section the page is for) below
   the fold. */
.trend-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: .75rem; }
.trend-grid .item-img { height: 110px; }
.trend-grid .item-body { padding: .6rem; gap: .25rem; }
.trend-grid .item-title { font-size: .85rem; font-weight: 600; line-height: 1.3; }
.trend-grid .item-price { font-size: .8rem; }
.trend-grid .item-actions { gap: .35rem; }
.trend-grid .item-actions .btn { padding: .3rem .55rem; font-size: .75rem; }
/* Base action row — also used outside .item-card (QR modal, dashboard cards), where
   wrapped buttons otherwise stack with no gap between rows. */
.item-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
.item-card { background: #fff; border: 1px solid var(--gray-200); border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow); display: flex; flex-direction: column; }
.item-card .item-img { height: 170px; background: var(--gray-100); display: flex; align-items: center; justify-content: center; overflow: hidden; position: relative; }
/* Status badge as a sticker on the photo's corner — see the /w/ card markup. */
.item-card .item-img .badge { position: absolute; top: 10px; right: 10px; }
.item-card .item-img img { width: 100%; height: 100%; object-fit: contain; padding: 10px; }
.item-card .item-body { padding: 1rem; flex: 1; display: flex; flex-direction: column; gap: .5rem; }
/* Same scraped-title problem as the dashboard table, and worse here: cards sit in a grid
   row, so one 200-character Amazon title made every card beside it that tall. Three lines
   (not the table's two) — this is the page a gifter shops from, and the store link is the
   way to the full name. */
.item-card .item-title {
    font-weight: 700; color: var(--gray-900);
    display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 3; line-clamp: 3;
    overflow: hidden; overflow-wrap: anywhere; min-width: 0;
}
/* Explore's tiles are half the size, so they get the tighter clamp. */
.trend-grid .item-card .item-title { -webkit-line-clamp: 2; line-clamp: 2; }
.item-card .item-price { color: var(--gray-600); font-weight: 600; }
.item-card .item-actions { margin-top: auto; display: flex; gap: .5rem; flex-wrap: wrap; }

/* Retailer search links. Deliberately text, not buttons: these are search results we
   haven't verified, so they must not carry the visual weight of "View in store" (a real
   product page) or of "I'll get this" (the actual call to action). Sits under the action
   row, which margin-top:auto has already pushed to the bottom of the card. */
.item-search { font-size: .8rem; margin: .5rem 0 0; display: flex; flex-wrap: wrap; gap: .5rem; align-items: baseline; }
.item-search a { color: var(--gray-700); text-decoration: underline; }
.item-search a:hover, .item-search a:focus-visible { color: var(--primary); }

/* The hidden attribute must beat any display rule below it — .modal-backdrop sets
   display:flex, which would otherwise override the UA stylesheet's [hidden] display:none
   and leave a "hidden" dialog on screen. */
[hidden] { display: none !important; }

/* Modal (htmx-driven) */
.modal-backdrop {
    position: fixed; inset: 0; background: rgba(17,24,39,.55);
    display: flex; align-items: center; justify-content: center; z-index: 100; padding: 1rem;
}
/* Keep touch scrolling INSIDE the dialog — without this, scrolling a modal to its edge
   chains to the page behind it and, on mobile, lets the whole page rubber-band (including
   sideways). */
.modal-backdrop, .modal, .modal-body { overscroll-behavior: contain; }
/* While any modal is open the body is pinned (see the _Layout scroll-lock script that
   toggles .ezw-modal-open): the fixed backdrop covers the screen, but a touch drag would
   otherwise still pan and scroll the page underneath it. position:fixed is the reliable
   cross-browser lock — the script preserves and restores the scroll position. */
html.ezw-modal-open { overflow: hidden; }
html.ezw-modal-open body { position: fixed; left: 0; right: 0; width: 100%; overflow: hidden; }
/* dvh over vh where supported: vh is the viewport WITHOUT the soft keyboard, so a focused
   input inside a 90vh dialog pushed the bottom of it — the save button — under the keyboard
   on iOS. dvh tracks the visible area. The vh line stays as the fallback. */
.modal { background: #fff; border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); width: 100%; max-width: 440px; padding: 1.5rem; max-height: 90vh; max-height: 90dvh; overflow-y: auto; }
.modal h3 { margin-bottom: 1rem; }

/* Long dialogs (the item form): the body scrolls and the header and action row don't, so
   the primary button is reachable at any scroll position. Without it the Add button sat
   below eight fields and a slider — on a phone you had to go looking for it, which is what
   made the whole flow feel like work. */
.modal.modal-scroll { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
/* :not(script) matters — the swapped-in partial carries its own <script>, and a bare "> *"
   gave it display:flex, beating the UA stylesheet's display:none. The source printed itself
   into the dialog as text and ate a third of the flex height. */
.modal.modal-scroll > :not(script) { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.modal-head { padding: 1.25rem 1.5rem .875rem; border-bottom: 1px solid var(--gray-200); }
.modal-head h3 { margin-bottom: 0; }
/* Scroll affordance for the body. The pinned .modal-foot makes a long form look finished
   on a phone — fields, then a Save button — so nothing tells a thumb there is more above
   or below, and mobile scrollbars are overlay-style and fade by design, so they can't be
   relied on to say it.
   Two gradient layers scroll with the content (local) and mask two that stay pinned to the
   container, so each shadow is visible exactly when there IS more content past that edge
   and disappears at the extremes — unlike a static "scroll for more" hint, which starts
   lying the moment you scroll. No JS and no state; unsupported browsers just see no
   shadow. Needs the solid background-color below: the shorthand resets it to transparent,
   and the masking layers only work over an opaque surface. */
.modal-body {
    padding: 1.25rem 1.5rem; overflow-y: auto; flex: 1 1 auto; min-height: 0;
    background:
        linear-gradient(#fff 30%, rgba(255, 255, 255, 0)) top / 100% 28px no-repeat local,
        linear-gradient(rgba(255, 255, 255, 0), #fff 70%) bottom / 100% 28px no-repeat local,
        radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, .13), rgba(0, 0, 0, 0)) top / 100% 11px no-repeat,
        radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, .13), rgba(0, 0, 0, 0)) bottom / 100% 11px no-repeat;
    background-color: #fff;
}
.modal-foot {
    padding: .875rem 1.5rem; border-top: 1px solid var(--gray-200);
    display: flex; flex-direction: row; gap: .5rem; flex: none;
}
.modal-foot .btn { flex: 1; }
/* The ✕ icon button. Styled via .modal-x, NOT .modal-close — modal-close is purely the
   shared close-handler's JS hook, and styling it here clobbered real .btn buttons
   ("Close", "Never mind") that carry the hook class. */
.modal .modal-x {
    float: right; width: 32px; height: 32px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    background: none; border: none; font-size: 1.1rem; line-height: 1;
    cursor: pointer; color: var(--gray-500);
}
.modal .modal-x:hover { background: var(--gray-100); color: var(--gray-800); }
/* Wider than the 440px default: the item form runs two fields to a row on desktop. */
#item-modal-panel { max-width: 560px; }

/* Loading stand-in for the item form, built by List.cshtml's placeholder(): grey blocks
   laid out in the form's own grid, sized to its labels, inputs and buttons. Only the
   spinner line claims activity; the blocks pulse gently, and hold still under
   reduced-motion — the spinner's own rule already covers itself. */
.modal-wait { display: flex; align-items: center; color: var(--gray-600); font-size: .925rem; }
.skel { display: block; border-radius: var(--radius); background: var(--gray-100); animation: ezw-skel 1.3s ease-in-out infinite; }
@keyframes ezw-skel { 50% { background: var(--gray-200); } }
@media (prefers-reduced-motion: reduce) { .skel { animation: none; } }
.skel-field { margin-top: 1rem; }
.skel-label { height: .8rem; width: 38%; margin-bottom: .5rem; }
.skel-input { height: 2.55rem; }
.skel-btn { height: 2.6rem; flex: 1; }
.modal-wait.center { justify-content: center; text-align: center; }

/* "Looking it up" — a magnifier sweeps across a product placeholder while a URL lookup
   runs (the item modal, and the /start paste box). Pure CSS so it drops straight into
   the loading markup. .ezw-scan is the modal size; .ezw-scan-inline the compact /start one. */
.ezw-scan { position: relative; width: 260px; max-width: 82%; height: 72px; margin: 1.6rem auto .9rem; }
.ezw-scan-inline { position: relative; width: 220px; max-width: 100%; height: 56px; margin: .9rem 0 .25rem; }
.ezw-scan-ph { display: flex; align-items: center; gap: 14px; height: 100%; }
.ezw-scan-img { flex: none; width: 54px; height: 54px; border-radius: 10px; background: var(--gray-100); }
.ezw-scan-inline .ezw-scan-img { width: 44px; height: 44px; }
.ezw-scan-lines { flex: 1; display: flex; flex-direction: column; gap: 9px; }
.ezw-scan-l { display: block; height: 10px; border-radius: 999px; background: var(--gray-100); }
.ezw-scan-l.a { width: 86%; }
.ezw-scan-l.b { width: 58%; }
.ezw-scan-l.c { width: 34px; height: 13px; }
.ezw-scan-glass {
    position: absolute; top: 50%; left: 0; width: 40px; height: 40px; margin-top: -24px;
    border: 4px solid var(--primary); border-radius: 50%;
    box-shadow: 0 0 0 3px rgba(91, 91, 214, .10), inset 0 0 10px rgba(91, 91, 214, .12);
    /* translateX, not left, so the sweep stays on the compositor */
    --scan-travel: 190px;
    animation: ezw-scan-sweep 2.2s cubic-bezier(.5, 0, .5, 1) infinite alternate;
}
.ezw-scan-inline .ezw-scan-glass { width: 34px; height: 34px; margin-top: -21px; --scan-travel: 152px; }
/* the handle, angled off the lower-right of the lens */
.ezw-scan-glass::after {
    content: ""; position: absolute; right: -8px; bottom: -6px; width: 14px; height: 5px;
    background: var(--primary); border-radius: 3px; transform: rotate(45deg);
}
@keyframes ezw-scan-sweep { from { transform: translateX(4px); } to { transform: translateX(var(--scan-travel)); } }
@media (prefers-reduced-motion: reduce) {
    .ezw-scan-glass { animation: none; transform: translateX(calc(var(--scan-travel) / 2)); }
}

/* Item photo. With one set, the thumbnail IS the field and the URL box is hidden — but
   hidden, never removed: an absent input posts nothing, and the image would be wiped on
   every save. */
.photo-field .photo-preview { display: none; }
.photo-field.has-photo .photo-preview { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.photo-field.has-photo > .form-control { display: none; }
.photo-field.has-photo.editing > .form-control { display: block; margin-top: .75rem; }
.photo-preview img {
    width: 96px; height: 96px; object-fit: contain;
    background: var(--gray-100); border: 1px solid var(--gray-200); border-radius: var(--radius);
}
/* contain, not the cover used on item cards: this thumbnail answers "is that the right
   product?", so the whole thing has to be visible rather than a centre crop. */
.photo-actions { display: flex; flex-direction: column; gap: .4rem; }

/* Who can find this list, stated on the list page. Quiet by design — it reports a state,
   it isn't a control, and it sits next to a heading. */
/* The visibility chip and the Settings button that changes it, on one line under the
   list name. Pairing them is the point: the chip reports the state and the button is
   how you change it, so they belong together rather than at opposite ends of the
   header. The spacing above the pair lives here, not on the chip. */
.vis-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-top: .35rem; }

.vis-chip {
    display: inline-flex; align-items: center;
    padding: .2rem .55rem; border-radius: 999px;
    font-size: .8rem; font-weight: 600;
    border: 1px solid var(--gray-200); background: var(--gray-50); color: var(--gray-700);
}
.vis-chip.vis-code { border-color: #fde68a; background: #fffbeb; color: #92400e; }
/* The setting's name, quieter than its value so the value stays what the eye lands on. */
.vis-chip .vis-key { font-weight: 500; opacity: .8; }

/* "Use another method" on the passkey-first login screen. Both markers are reset or the
   browser's own triangle shows up beside our chevron. Sized as a real control rather than a
   line of text: it is the recovery route when a passkey has been deleted or the browser is
   shared, so it has to be findable and tappable, not merely present. */
.other-methods > summary {
    display: flex; align-items: center; justify-content: center; gap: .4rem;
    /* -webkit- prefix still required for Safari/iOS, which is most of the phone traffic here */
    min-height: 44px; cursor: pointer; -webkit-user-select: none; user-select: none;
    font-size: .95rem; font-weight: 600; color: var(--gray-700);
    list-style: none;
}
.other-methods > summary::-webkit-details-marker { display: none; }
.other-methods > summary::after {
    content: ''; width: .5rem; height: .5rem; margin-top: -.25rem;
    border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
    transform: rotate(45deg); transition: transform .15s;
}
.other-methods[open] > summary::after { transform: rotate(225deg); margin-top: .15rem; }
.other-methods > summary:hover { color: var(--gray-900); }
.other-methods > summary:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }

/* "Buying doesn't mark it here" line under an unclaimed item's buttons. Quiet — it explains
   the two buttons above it rather than competing with them. */
.item-nudge { font-size: .8rem; color: var(--gray-600); margin-top: .5rem; }
.item-nudge strong { color: var(--gray-800); white-space: nowrap; }

/* Come-back-and-claim prompt (see Wishlist.cshtml). A docked bar, not a modal: they have just
   come back from somewhere else and a dialog demanding dismissal before they can even see the
   list would read as a toll gate. z-index below .modal-backdrop's 100 so a claim modal opened
   from it covers it rather than fighting it. */
#ezw-comeback {
    /* Centred on screen (owner call 2026-08-11: the docked bottom bar was missed), but
       still no backdrop — the page stays clickable, so it never reads as a toll gate. */
    position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
    z-index: 90; width: min(30rem, calc(100vw - 1.5rem));
    background: #fff; border: 1px solid var(--gray-200); border-radius: var(--radius-lg);
    box-shadow: 0 10px 34px rgba(58, 50, 38, .3), 0 2px 8px rgba(58, 50, 38, .16);
    padding: 1rem 1.125rem;
    display: flex; flex-wrap: wrap; align-items: center; gap: .75rem;
}
#ezw-comeback-img {
    width: 56px; height: 56px; object-fit: contain; flex: none;
    background: var(--k-photo-bg); border: 1px solid var(--k-hairline); border-radius: 10px; padding: 4px;
}
#ezw-comeback p { font-size: .9rem; color: var(--gray-800); flex: 1 1 14rem; margin: 0; }
.ezw-comeback-actions { display: flex; gap: .5rem; flex: 0 0 auto; }
/* Phones: buttons go full width on their own row rather than squeezing beside the text —
   "I'll get this" is the whole point of the bar and must be an easy thumb target.
   The flex reset on the paragraph is load-bearing: flex-basis follows the MAIN axis, so the
   14rem that reserves sensible text width in a row becomes a 14rem minimum HEIGHT once the
   container turns into a column, and the bar grew to 224px with a dead gap under the text. */
@media (max-width: 480px) {
    #ezw-comeback { flex-direction: column; align-items: stretch; }
    #ezw-comeback p { flex: 0 0 auto; }
    .ezw-comeback-actions > .btn { flex: 1; }
}

/* Failed-request toast (see _Layout). z-index above .modal-backdrop's 100: the save that
   fails most visibly is the one submitted from inside a dialog. */
#ezw-toast {
    position: fixed; left: 50%; bottom: 1.25rem; transform: translate(-50%, 1rem);
    z-index: 200; max-width: min(30rem, calc(100vw - 2rem));
    padding: .75rem 1.1rem; border-radius: var(--radius);
    background: var(--gray-900); color: #fff; font-size: .9rem; box-shadow: var(--shadow-lg);
    opacity: 0; pointer-events: none; transition: opacity 150ms ease, transform 150ms ease;
}
#ezw-toast.is-shown { opacity: 1; transform: translate(-50%, 0); }

/* Admin stat tiles. /admin has always written .stat-grid / .stat-num / .stat-label, but
   the rules were never here — two inline spans with nothing to separate them, so every
   tile on the page read "3today, of 100" with the number welded to its label. */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: .75rem; }
/* neutralize the global ".card + .card" top margin — in a stretch grid it left the FIRST
   tile of every stat row 1rem taller than its siblings. Same fix as .feature-grid. */
.stat-grid > .card { margin-top: 0; }
.stat-grid .stat { display: flex; flex-direction: column; gap: .15rem; padding: .9rem 1rem; }
.stat-num { font-size: 1.5rem; font-weight: 700; line-height: 1.15; color: var(--gray-900); }
.stat-label { font-size: .8rem; color: var(--gray-600); line-height: 1.3; }

/* Dashboard */
/* Wishlist-card actions: a 2×2 grid so the four buttons come out equal-width and
   centered instead of ragged inline wrapping. */
.list-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: .5rem; }
/* The dashboard's single-column stack. A class rather than a max-width repeated inline on
   each card, so "the same width as the cards below it" is something the markup states once
   instead of three values that have to be kept in step by hand. */
.card-column { max-width: 520px; }
/* One wishlist: contents on the left, actions on the right. The 2×2 action block is already
   the right shape to sit in a column, and keeping it out of the flow leaves the widened card
   room to show the item images without growing a fourth stacked row. */
.list-card-solo { display: flex; align-items: flex-start; gap: 1.5rem; }
.list-card-solo > .list-card-info { flex: 1; min-width: 0; }
.list-card-solo > .list-actions { flex: 0 0 12rem; }
/* Below this the two columns stop fitting: the thumbnail strip needs 194px and the buttons
   ~12rem, which is more than a phone-width card has to give. Actions drop under the contents,
   which is the multi-card arrangement anyway. */
@media (max-width: 560px) {
    .list-card-solo { flex-direction: column; gap: 1rem; }
    .list-card-solo > .list-actions { flex: none; }
}
.dash-layout { display: grid; grid-template-columns: 220px 1fr; gap: 1.5rem; padding: 1.5rem 0; }
.dash-nav { display: flex; flex-direction: column; gap: .25rem; }
.dash-nav a { padding: .5rem .75rem; border-radius: var(--radius); color: var(--gray-600); text-decoration: none; font-weight: 500; }
.dash-nav a.active, .dash-nav a:hover { background: #eef2ff; color: var(--primary); }
.table-wrap { overflow-x: auto; }
table.data { width: 100%; border-collapse: collapse; font-size: .9rem; }
table.data th, table.data td { text-align: left; padding: .6rem .75rem; border-bottom: 1px solid var(--gray-200); }
table.data th { color: var(--gray-500); font-size: .78rem; text-transform: uppercase; letter-spacing: .03em; }
table.data td.row-actions { white-space: nowrap; }
/* Scraped product titles run long — Amazon's routinely pass 200 characters — and one of
   them turned a single row into most of a phone screen. Two lines is enough to tell items
   apart; the full title is in the edit form and in the hover tooltip. Clamped at every
   width so row heights stay even, not just on phones. */
table.data .item-name {
    display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 2; line-clamp: 2;
    overflow: hidden;
    /* A single long word ("THIRDREALITY") sets the column's minimum width, which pushed
       the table 43px wider than the card at 360px — a sideways scroll nobody finds.
       Breaking mid-word costs nothing on a name that's already truncated. */
    overflow-wrap: anywhere;
}
/* Phones: the items table ran off the right edge of the card. Tighter cells, stacked
   action buttons, and no Price column keep every remaining column on screen instead of
   relying on a scroll nobody notices. */
@media (max-width: 640px) {
    table.data { font-size: .85rem; }
    table.data th, table.data td { padding: .5rem .3rem; }
    table.data .col-price { display: none; }
    table.data td.row-actions { white-space: normal; }
    /* width:100% rather than max-content: stacked, each button sized to its own label,
       so "Edit" and "Remove" were visibly different widths in one column. Filling the
       cell makes them match — the cell is still only as wide as the longest label. */
    table.data td.row-actions .btn { display: block; width: 100%; margin: 0 0 .3rem; }
    table.data td img { width: 36px !important; height: 36px !important; }
}

/* "Save from any store" — scannable routes rather than a paragraph */
.ways-list { font-size: .875rem; padding-left: 1.15rem; margin-bottom: .75rem; }
.ways-list li { margin-bottom: .35rem; }
.ways-list strong { color: var(--gray-700); }

.empty-state { text-align: center; padding: 3rem 1rem; color: var(--gray-500); }
.empty-state .big { font-size: 2.5rem; }

/* Utility */
.mt-1 { margin-top: .5rem; } .mt-2 { margin-top: 1rem; } .mt-3 { margin-top: 1.5rem; } .mt-4 { margin-top: 2rem; }
.mb-2 { margin-bottom: 1rem; }
.text-center { text-align: center; }
.text-muted { color: var(--gray-500); }
.flex-between { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
/* same ".card + .card" neutralization for card children (admin Stores panels): the gap
   is the spacing; the margin only knocked later cards' tops out of line. */
.grid-2 > .card { margin-top: 0; }
.honeypot { position: absolute; left: -9999px; opacity: 0; height: 0; overflow: hidden; }

/* Feature cards (landing) — fixed columns + uniform row height so every card matches */
.feature-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.25rem; grid-auto-rows: 1fr; }
/* neutralize the global ".card + .card" top margin, which otherwise shrinks cards 2-4 */
.feature-grid > .card { margin-top: 0; }
@media (max-width: 900px) { .feature-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { .feature-grid { grid-template-columns: 1fr; } }
.feature-icon {
    display: flex; align-items: center; justify-content: center;
    height: 56px; font-size: 1.75rem;
    background: linear-gradient(135deg, #eef2ff 0%, #fdf2f8 100%);
    border-radius: var(--radius); margin-bottom: .75rem;
}
/* Three-column variant (/for-teachers, six cards): 3×2 desktop, 2×3 tablet. Must sit
   after the .feature-grid media queries — same specificity, so source order is what
   lets these override at each breakpoint. */
.feature-grid-3col { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 900px) { .feature-grid-3col { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { .feature-grid-3col { grid-template-columns: 1fr; } }

/* Draft builder (/start) — sticky reminder that the list isn't saved yet */
.draft-banner {
    position: sticky; top: 64px; z-index: 40;
    background: #eef2ff; border-bottom: 1px solid #c7d2fe;
    padding: .625rem 0; font-size: .925rem; color: #3730a3;
}
.draft-banner .container { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
body.popup .draft-banner { top: 0; } /* chrome-less popup mode: no 64px header to sit under */

/* Blog */
.blog-body h2 { font-size: 1.25rem; margin: 1.5rem 0 .5rem; }
.blog-body h3 { font-size: 1.05rem; margin: 1.25rem 0 .4rem; }
.blog-body p { margin-bottom: .875rem; }
.blog-body ul, .blog-body ol { margin: 0 0 .875rem 1.4rem; }
.blog-body li { margin-bottom: .3rem; }
.blog-body blockquote { border-left: 3px solid var(--primary); padding-left: 1rem; color: var(--gray-600); margin-bottom: .875rem; }
/* display:block so a wide comparison table scrolls inside itself on a phone instead of
   forcing the whole page sideways. Markdig emits a bare <table>, so there's no wrapper
   element to hang the overflow on. */
.blog-body table {
    display: block; max-width: 100%; overflow-x: auto;
    border-collapse: collapse; margin-bottom: .875rem; font-size: .9rem;
}
.blog-body th, .blog-body td { border: 1px solid var(--gray-200); padding: .45rem .6rem; text-align: left; vertical-align: top; }
.blog-body th { background: var(--gray-50); font-weight: 600; }

/* Guides index: jump links to the topic clusters. Every post is listed under each tag it
   carries, so the index is already ~21 cards and lands near 36 once the posts scheduled
   through October publish themselves — far enough that the later topics are invisible
   without a nav telling you they exist. */
.topic-nav { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1.25rem; }
.topic-nav a {
    display: inline-block; padding: .35rem .75rem; border-radius: 999px;
    background: var(--gray-100); border: 1px solid var(--gray-200);
    color: var(--gray-700); text-decoration: none; font-size: .85rem; font-weight: 600;
}
.topic-nav a:hover { background: var(--gray-200); color: var(--gray-900); }
.topic-nav a:focus-visible { outline: 3px solid #c7d2fe; outline-offset: 1px; }
/* The 64px header is sticky, so without an offset every jump parks the <h2> underneath it.
   Phones let that header wrap to a second row (see the 640px block up by .site-header), so
   the offset grows to clear the taller case — overshooting only costs whitespace, while
   undershooting hides the heading you just jumped to. */
.blog-section { scroll-margin-top: 80px; }
@media (max-width: 640px) { .blog-section { scroll-margin-top: 116px; } }

.htmx-indicator { display: none; }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { display: inline; }
/* Pairs with .htmx-indicator so a button can swap its label for a progress one
   ("Add item" → "Adding…") rather than showing both at once. */
.htmx-request .htmx-indicator-hide { display: none; }
/* hx-disabled-elt should look disabled, or the only feedback is that nothing happens. */
button[disabled] { opacity: .65; cursor: default; }

@media (max-width: 720px) {
    .dash-layout { grid-template-columns: 1fr; }
    .dash-nav { flex-direction: row; overflow-x: auto; }
    .grid-2 { grid-template-columns: 1fr; }
}

/* ===================================================================================
   Rebrand phase 2 — wordmark fonts, kraft ground (home + /start), occasion tiles,
   gift-tag feature cards, /start occasion backdrop. Decisions and measured AA numbers
   live in the phase-1 artifact; kraft-ground text tokens are overridden per page below.
   =================================================================================== */

/* ---------------------------------------------------------------------------------
   Theme token layer (phase 3 groundwork, docs/design/PHASE3-THEMES.md).

   Every kraft colour used to be typed out by hand, the same card cream repeated across
   seven rules and the same border tan across nine, so a second theme meant duplicating
   forty rules. The rules below now name their colours instead, and a theme becomes a
   short override block on `body.theme-<key>` rather than a copy of the stylesheet.

   Kraft's values ARE the defaults, so this pass changes nothing visually: every token
   resolves to the exact literal it replaced. Defined on :root rather than on
   body.page-kraft so that unscoped brand components (the come-back bar thumbnail, the
   gift tags) resolve them everywhere; a theme class on <body> overrides them for the
   page. Theme blocks must come AFTER `body.page-kraft` in this file: the two selectors
   have identical specificity, so source order decides.

   Measured AA numbers for these values live in docs/design/BRAND.md and in the comments
   below, which deliberately keep their hex literals as the record of what was measured.

   NOT yet tokenised, both structural rather than mechanical:
   - grain opacity, baked into the background SVG data URI as opacity='.055'
   - the step-word squiggle, whose rose is URL-encoded inside its own data URI
   --------------------------------------------------------------------------------- */
:root {
    /* Ground */
    --k-ground-a: #eee2cf;
    --k-ground-b: #e3d3b7;

    /* Surfaces */
    --k-surface: #fdfaf2;      /* card, tag, modal, come-back bar */
    --k-surface-2: #f8f2e5;    /* inset panel, nested form */
    --k-surface-3: #f0e8d5;    /* chip, badge-neutral, kbd, loader block */
    --k-surface-alt: #fffdf8;  /* secondary button */
    --k-img-fill: #f3ecdc;     /* image area, thumb */

    /* Lines */
    --k-hairline: #e5d8bd;
    --k-hairline-2: #e0d3b2;
    --k-input-line: #d9cdb2;
    --k-eyelet: #e6d9ba;

    /* Ink. The --gray-700/800/900 overrides on body.page-kraft draw from these. */
    --k-ink-head: #362e21;     /* h1, h2, h3, item titles */
    --k-ink: #3a3226;
    --k-ink-2: #514734;
    --k-ink-3: #2c2618;        /* footer ground, wordmark rest */
    --k-ink-alt: #4a4232;      /* alert-info text */
    --k-ink-muted: #5e543f;    /* ground-sitting secondary text; feeds --gray-500 */
    --k-ink-muted-2: #55503c;  /* feeds --gray-600 */
    --k-photo-bg: #fff;        /* backdrop behind product photos, inside warm cards */

    /* Accents */
    --k-accent: #d98c94;       /* decorative underline, link underline */
    --k-hand: #a44f5b;         /* the hand writes in rose */
    --k-wordmark-ez: #9c9cf0;  /* wordmark EZ on the dark footer */

    /* Footer text on the warm espresso ground */
    --k-footer-ink: #cfc4ae;
    --k-footer-link: #ded5c0;
    --k-footer-mark: #ece4d2;

    /* Alpha bases. Channel triplets so one shadow colour can retint per theme without
       restating every alpha; a warm-brown shadow under a cool theme reads wrong. */
    --k-shadow-rgb: 120 95 55;
    --k-surface-rgb: 253 250 242;
    --k-tape-bg: rgba(252, 246, 232, .72);
    --k-backdrop: rgba(58, 50, 38, .55);
}

@font-face {
    font-family: 'Caveat Brand';
    font-weight: 700;
    font-style: normal;
    font-display: block;
    src: url('/fonts/caveat-brand.woff2') format('woff2');
}
@font-face {
    font-family: 'Courier Wordmark';
    font-weight: 400;
    font-style: normal;
    font-display: block;
    src: url('/fonts/courier-wordmark.woff2') format('woff2');
}

.footer-wordmark { font-size: 20px; line-height: 1.1; text-decoration: none; white-space: nowrap; display: inline-block; margin-bottom: .75rem; }
.footer-wordmark .ez { font-family: 'Caveat Brand', cursive; font-weight: 700; color: var(--k-wordmark-ez); }
.footer-wordmark .rest { font-family: 'Courier Wordmark', 'Courier New', monospace; font-size: .725em; margin-left: .12em; letter-spacing: .035em; color: var(--gray-300); }

/* Kraft pages (home, /start). Ground-sitting secondary text fails AA on kraft at the
   stock grays (measured 3.3:1), so the muted tokens darken to warm equivalents:
   #5e543f = 5.1:1 on the darkest ground edge, and darker-on-white inside cards. */
body.page-kraft {
    background:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)' opacity='.055'/%3E%3C/svg%3E"),
        radial-gradient(120% 90% at 50% 0%, var(--k-ground-a) 0%, var(--k-ground-b) 100%);
    background-attachment: fixed;
    --gray-500: var(--k-ink-muted);
    --gray-600: var(--k-ink-muted-2);
}
body.page-kraft .hero { background: transparent; }
body.page-kraft .hero h1 { color: var(--k-ink-head); }
body.page-kraft .btn-primary {
    border-radius: 13px;
    box-shadow: 0 6px 14px -6px rgba(91, 91, 214, .55), inset 0 1px 0 rgba(255, 255, 255, .28);
}

/* Hand-drawn underline under the H1's last phrase */
.squig { position: relative; white-space: nowrap; }
.squig svg { position: absolute; left: 2%; bottom: -.12em; width: 96%; height: .14em; }
/* "Zero duplicate gifts." never wraps (the squiggle underlines it as one phrase), and at the
   2rem floor it is 320px wide: on phones under 360px it ran past the gutter and the page
   scrolled sideways. 1.75rem fits it inside a 320px screen's 16px margins. */
@media (max-width: 359px) { .hero h1 { font-size: 1.75rem; } }

/* Handwritten margin note (kraft hero only). Sized into WCAG large-text territory
   (≥24px) on purpose: that's what lets it wear the same rose as the step number words
   (#a44f5b = 3.7:1 on the darkest kraft edge; passes at large text, fails small). One
   rule: the hand writes in rose. */
.hand-note {
    font-family: 'Caveat Brand', cursive; font-weight: 700;
    font-size: clamp(2rem, 3.6vw, 2.6rem); line-height: 1.2;
    color: var(--k-hand); transform: rotate(-1.6deg); margin-top: 1.4rem;
}

/* Occasion tiles under the hero — each links to /start?occasion= */
.occ-label {
    font-size: .75rem; letter-spacing: .14em; text-transform: uppercase;
    color: var(--gray-500); font-weight: 600; margin-top: 2.25rem;
}
.occ-tiles { display: flex; justify-content: center; gap: .875rem; flex-wrap: wrap; margin-top: .875rem; }
.occ-tile {
    width: 118px; box-sizing: border-box;
    background: var(--k-surface); border: 1px solid var(--k-hairline); border-radius: 18px;
    padding: 1rem .625rem .75rem;
    display: flex; flex-direction: column; align-items: center; gap: .5rem;
    text-decoration: none;
    box-shadow: 0 2px 6px rgb(var(--k-shadow-rgb) / .06), 0 16px 30px -20px rgb(var(--k-shadow-rgb) / .5);
}
.occ-tile img { width: 64px; height: 64px; object-fit: contain; }
.occ-tile span { font-size: .78rem; font-weight: 600; color: var(--k-ink); line-height: 1.25; text-align: center; }
.occ-other { font-size: .8rem; color: var(--gray-500); margin-top: .875rem; }
.occ-other a { color: inherit; text-decoration-color: var(--k-accent); text-underline-offset: 3px; }

/* Loop clones (script-made copies of edge tiles) exist only while the row slides; in
   wrap mode they'd add phantom width and force sliding everywhere, so default is gone. */
.occ-tile-clone { display: none; }

/* Click arrows for the sliding row: mouse users get a button where touch users swipe.
   Hidden by default; the media block after the sliding windows shows each side only when
   the page script marks more content there (data-scroll-l/r on the wrapper), and only
   for fine pointers. Siblings of the row, so the edge-fade mask never dims them. */
.occ-row { position: relative; }
.occ-arrow {
    display: none; position: absolute; top: 50%; transform: translateY(-50%);
    z-index: 1; width: 38px; height: 38px; border-radius: 50%; padding: 0;
    background: var(--k-surface); border: 1px solid var(--k-hairline); color: var(--k-ink); cursor: pointer;
    align-items: center; justify-content: center;
    box-shadow: 0 2px 6px rgb(var(--k-shadow-rgb) / .12), 0 10px 22px -12px rgb(var(--k-shadow-rgb) / .5);
}
.occ-arrow:hover { background: #fff; }
.occ-arrow-l { left: -8px; }
.occ-arrow-r { right: -8px; }
@media (prefers-reduced-motion: no-preference) {
    .occ-tile { transition: transform .18s ease; }
    .occ-tile:hover { transform: translateY(-3px); }
}
/* Narrow viewports: the Paperless Post move — one snap-scrolling row. The second window
   is the eight-tile orphan band: when the container fits exactly seven tiles, the eighth
   would strand alone on a second row, so the row slides there instead. A second row of
   two is fine; a row of one reads broken. The math: a row of n tiles needs 132n − 14px
   and the container gives min(1100, layout-width) − 32px, so seven-not-eight happens at
   layout widths 942–1073px — but media queries include the scrollbar and layout doesn't,
   so the top edge carries ~18px of slack. "safe center" keeps the row centered in that
   slack when all eight happen to fit, and start-aligns it when they overflow. Recompute
   if the tile count changes. */
@media (max-width: 640px), (min-width: 942px) and (max-width: 1091.98px) {
    .occ-tiles {
        flex-wrap: nowrap; overflow-x: auto; justify-content: safe center;
        padding: 2px 4px 12px; scroll-snap-type: x mandatory;
    }
    .occ-tile { flex: 0 0 auto; scroll-snap-align: start; }
    .occ-tile-clone { display: flex; }
}
@media (max-width: 640px) {
    .occ-tile { flex: 0 0 96px; width: 96px; }
    .occ-tile img { width: 54px; height: 54px; }
}

/* Gift-tag feature cards: clipped corner + mask-punched eyelet. box-shadow dies inside
   clip-path, so the shadow rides filter:drop-shadow and follows the tag silhouette. */
.tag-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .875rem; }
@media (max-width: 820px) { .tag-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { .tag-grid { grid-template-columns: 1fr; } }
.gift-tag {
    position: relative;
    background: var(--k-surface); border: 1px solid var(--k-hairline); border-radius: 14px;
    padding: .875rem .875rem 1rem;
    clip-path: polygon(24px 0, 100% 0, 100% 100%, 0 100%, 0 24px);
    -webkit-mask: radial-gradient(circle at 22px 22px, transparent 0 4.5px, #000 5px);
    mask: radial-gradient(circle at 22px 22px, transparent 0 4.5px, #000 5px);
    filter: drop-shadow(0 3px 5px rgb(var(--k-shadow-rgb) / .22));
}
.gift-tag::before {
    content: ""; position: absolute; left: 15px; top: 15px; width: 14px; height: 14px;
    box-sizing: border-box; border-radius: 50%; border: 3px solid var(--k-eyelet);
}
.gift-tag .tag-head {
    display: flex; align-items: center; justify-content: space-between; gap: .5rem;
    padding-left: 22px; min-height: 44px; margin-bottom: .375rem;
}
.gift-tag h3 { font-size: .95rem; margin: 0; color: var(--k-ink-head); }
.gift-tag .tag-head img { width: 42px; height: 42px; flex: none; }
.gift-tag p { font-size: .84rem; color: var(--gray-500); line-height: 1.5; margin: 0; }
.tag-grid .gift-tag:nth-child(1) { transform: rotate(-1deg); }
.tag-grid .gift-tag:nth-child(2) { transform: rotate(.8deg); }
.tag-grid .gift-tag:nth-child(3) { transform: rotate(-.6deg); }
.tag-grid .gift-tag:nth-child(4) { transform: rotate(1.1deg); }
.tag-grid .gift-tag:nth-child(5) { transform: rotate(-.8deg); }
.tag-grid .gift-tag:nth-child(6) { transform: rotate(.9deg); }

/* /start: clay backdrop for the chosen occasion. Themes decorate the ground; text always
   sits on a plain card above it — the element hides whenever it could crowd content.
   It rides the end of the content column rather than the viewport: ground decoration has
   to stay behind everything (z-index -1), and the footer is opaque and in flow, so a
   viewport-anchored element lost its bottom 80px to the footer at every scroll position —
   the pencil's collar and tip, a third of the cake. Anchoring to main keeps it clear of
   the footer at any viewport height, and main's 4rem gap above the footer absorbs the
   overhang. */
main:has(> .occ-backdrop) { position: relative; }
.occ-backdrop {
    position: absolute; right: clamp(-40px, 2vw, 48px); bottom: -20px;
    width: min(300px, 24vw); z-index: -1; pointer-events: none;
}
.occ-backdrop img, .occ-backdrop svg { width: 100%; height: auto; display: block; }
@media (max-width: 1200px) { .occ-backdrop { display: none; } }

/* "How it works" — three steps numbered in the brand hand (one/two/three: Caveat's
   subset carries lowercase but no digits, and words read like a note from a person).
   Step rose #a44f5b measures 3.7:1 on the darkest kraft edge (large-text AA is 3:1). */
.steps { list-style: none; display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; max-width: 860px; margin-left: auto; margin-right: auto; padding: 0; }
.step { text-align: center; padding: 0 .75rem; }
.step-word {
    display: block; font-family: 'Caveat Brand', cursive; font-weight: 700;
    font-size: 2.1rem; line-height: 1.1; color: var(--k-hand); transform: rotate(-2deg);
}
/* A hand-drawn stroke under each number word, in the same rose. It lives inside the
   rotated word, so it tilts with it; em units keep it scaled to the word. */
.step-word::after {
    content: ""; display: block; width: 2.6em; height: .3em; margin: .06em auto 0;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 8'%3E%3Cpath d='M2 5 C 14 2.5, 26 6.5, 38 4 S 52 3.5, 58 5' fill='none' stroke='%23a44f5b' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E") center / 100% auto no-repeat;
}
.step strong { display: block; color: var(--gray-800); font-size: 1rem; margin-top: .15rem; }
body.page-kraft .step strong { color: var(--k-ink-head); }
.step p { font-size: .875rem; color: var(--gray-500); margin: .25rem 0 0; }
@media (max-width: 640px) {
    .steps { grid-template-columns: 1fr; gap: 1.5rem; }
}

/* Section heading "on tape": one deliberate piece of washi holding the label to the
   page. This is the sanctioned single-use of tape — scattered tape on tiles was tried
   during the texture round and cut. */
.tape-label {
    position: relative; display: table; margin-left: auto; margin-right: auto;
    padding: .3rem 1.4rem .35rem; transform: rotate(-1.2deg);
    color: var(--k-ink-head); text-align: center;
}
.tape-label::before {
    content: ""; position: absolute; inset: 0; z-index: -1;
    background: var(--k-tape-bg);
    border-left: 2px dashed rgb(var(--k-shadow-rgb) / .28);
    border-right: 2px dashed rgb(var(--k-shadow-rgb) / .28);
    box-shadow: 0 2px 5px rgb(var(--k-shadow-rgb) / .18);
}
body.page-kraft h2 { color: var(--k-ink-head); }

/* Sliding tile row (both windows above): hide the OS scrollbar; the affordance is a
   dynamic edge fade (mask-image, widths set by the page script — fades only where more
   content is) plus the peeking partial tile. */
@media (max-width: 640px), (min-width: 942px) and (max-width: 1091.98px) {
    .occ-tiles {
        scrollbar-width: none;
        -webkit-mask-image: linear-gradient(to right,
            transparent 0, #000 var(--fade-l, 0px),
            #000 calc(100% - var(--fade-r, 0px)), transparent 100%);
        mask-image: linear-gradient(to right,
            transparent 0, #000 var(--fade-l, 0px),
            #000 calc(100% - var(--fade-r, 0px)), transparent 100%);
    }
    .occ-tiles::-webkit-scrollbar { display: none; }
}

/* Arrows appear in the same sliding windows, but only for fine pointers (a phone swipes;
   a mouse clicks) and only on a side that actually has more content. */
@media (pointer: fine) and (max-width: 640px),
       (pointer: fine) and (min-width: 942px) and (max-width: 1091.98px) {
    .occ-row[data-scroll-l] .occ-arrow-l { display: flex; }
    .occ-row[data-scroll-r] .occ-arrow-r { display: flex; }
}

/* "A list that lives all year" — the follow story. One quiet centered moment: the
   sprout, a taped label, one paragraph, and the routes to the audience pages. On-kraft
   links inherit the dark warm text color with the rose underline (the indigo link color
   measures under 4.5:1 on the kraft ground). */
.follow-section { text-align: center; }
.follow-section img { width: 84px; height: 84px; object-fit: contain; margin-bottom: .35rem; }
.follow-blurb { max-width: 56ch; margin: .75rem auto 0; color: var(--gray-500); }
.follow-links { font-size: .875rem; color: var(--gray-500); margin-top: .6rem; }
.follow-links a { color: inherit; text-decoration-color: var(--k-accent); text-underline-offset: 3px; }

/* Kraft pages: plain cards wear the warm surface (the /start form cards were the last
   cool-white surfaces in the flow). */
body.page-kraft .card {
    background: var(--k-surface); border-color: var(--k-hairline); border-radius: 18px;
    box-shadow: 0 2px 6px rgb(var(--k-shadow-rgb) / .06), 0 16px 30px -20px rgb(var(--k-shadow-rgb) / .5);
}
.card-inset { background: var(--gray-50); }
body.page-kraft .card-inset { background: var(--k-surface-2); }

/* Item cards as hang-tags on kraft: eyelet punched straight through (photo included,
   like a real hang tag) plus small alternating tilts. Layout and aspect unchanged.
   Cream ring so the punched hole reads against product photos; drop-shadow instead of
   box-shadow because the mask clips the latter. */
body.page-kraft .item-card {
    position: relative;
    background: var(--k-surface); border-color: var(--k-hairline); border-radius: 16px;
    box-shadow: none;
    filter: drop-shadow(0 3px 5px rgb(var(--k-shadow-rgb) / .2));
    -webkit-mask: radial-gradient(circle at 22px 22px, transparent 0 4.5px, #000 5px);
    mask: radial-gradient(circle at 22px 22px, transparent 0 4.5px, #000 5px);
}
body.page-kraft .item-card::before {
    content: ""; position: absolute; left: 15px; top: 15px; width: 14px; height: 14px;
    box-sizing: border-box; border-radius: 50%; border: 3px solid rgb(var(--k-surface-rgb) / .92);
    z-index: 1; pointer-events: none;
}
body.page-kraft .item-card .item-img { background: var(--k-photo-bg); }
body.page-kraft .item-grid .item-card:nth-child(3n+1) { transform: rotate(-.6deg); }
body.page-kraft .item-grid .item-card:nth-child(3n+2) { transform: rotate(.5deg); }
body.page-kraft .item-grid .item-card:nth-child(3n) { transform: rotate(-.3deg); }

/* Kraft brand-conformance pass for /start (audited against the home standards).
   The full ink ramp warms on kraft pages so headings, body text, labels, item titles,
   the wordmark's "wishlist", and the footer all read in the same warm family the home
   page set. All tones re-measured AA on their grounds (darkest kraft edge #e3d3b7:
   #514734 = 6.2:1, #3a3226 = 8.6:1). */
body.page-kraft {
    --gray-700: var(--k-ink-2);
    --gray-800: var(--k-ink);
    --gray-900: var(--k-ink-3);
}
body.page-kraft h1 { color: var(--k-ink-head); }
body.page-kraft .draft-banner {
    background: rgb(var(--k-surface-rgb) / .95); border-bottom-color: var(--k-hairline); color: var(--k-ink);
}
body.page-kraft .alert-info { background: var(--k-surface-2); border-color: var(--k-hairline-2); color: var(--k-ink-alt); }
/* alert-warning stays amber on purpose: semantic warning color, not a surface. */
body.page-kraft .btn-secondary { background: var(--k-surface-alt); border-color: var(--k-input-line); color: var(--k-ink-2); }
body.page-kraft .btn-secondary:hover { background: var(--k-surface-2); }
body.page-kraft .form-control { border-color: var(--k-input-line); }
body.page-kraft .ezw-scan-img, body.page-kraft .ezw-scan-l { background: var(--k-surface-3); }
body.page-kraft .bookmarklet-offer kbd { background: var(--k-surface-2); border-color: var(--k-input-line); color: var(--k-ink-2); }
/* Footer on kraft pages goes warm espresso, matching the decided mock (gray-900 is
   overridden above, which turns the footer ground warm; the text follows). */
body.page-kraft .site-footer { color: var(--k-footer-ink); }
body.page-kraft .site-footer a { color: var(--k-footer-link); }
body.page-kraft .site-footer .disclosure { color: var(--k-footer-ink); }
body.page-kraft .footer-wordmark .rest { color: var(--k-footer-mark); }

/* Kraft reaches /explore and /w/: modals, badges, list thumbnails, and the come-back
   bar wear the warm surface. Semantic colors stay untouched on purpose: available
   (green), taken (amber), error (red), warning (amber) are states, not surfaces. */
body.page-kraft .modal { background: var(--k-surface); }
body.page-kraft .modal-backdrop { background: var(--k-backdrop); }
body.page-kraft .badge-purchased { background: var(--k-surface-3); }
body.page-kraft .list-thumbs img,
body.page-kraft .list-thumbs-more,
body.page-kraft .list-thumbs .img-fallback { border-color: var(--k-hairline-2); background: var(--k-img-fill); }
body.page-kraft #ezw-comeback { background: var(--k-surface); border-color: var(--k-hairline); }

/* Kraft pages: horizontal rules and blog hairlines warm with the page. */
body.page-kraft hr { border-top-color: var(--k-hairline); }

/* About page: the motto as quoted content, and small element shelves between sections. */
.about-motto { font-size: clamp(1.8rem, 4vw, 2.4rem); margin: .25rem 0 0; }
.about-shelf { display: flex; gap: 1.25rem; align-items: flex-end; justify-content: center; margin: .75rem 0 1rem; }
.about-shelf img { display: block; }
.about-contact { display: flex; align-items: center; gap: .875rem; }

/* No-photo / broken-image fallback is the clay gift mark (an <img> inside the
   .img-fallback span; the old emoji sizing rules above are inert once no text renders). */
.img-fallback img { width: 44px; height: 44px; object-fit: contain; }
.item-img .img-fallback img { width: 84px; height: 84px; }
.list-thumbs .img-fallback img { width: 26px; height: 26px; }

/* ===================================================================================
   Phase 3 — list themes (docs/design/PHASE3-THEMES.md).

   The masthead is the page's one big expressive surface; item cards, the claim button
   and the chrome stay frame. Theme blocks are token overrides on body.theme-<key> and
   MUST stay below every body.page-kraft rule: the selectors tie on specificity, so
   source order is what lets a theme win. Palettes mirror Services/ListThemes.cs — the
   CSS-parity test asserts every catalog key has a block here, and the AA numbers are
   enforced by ListThemeTests, not by comments.
   =================================================================================== */

/* --- Display faces, one per authored theme (docs/design/tools/build_display_faces.py
   builds these and the matching Assets/Fonts TTFs the share card draws with). Declared
   globally and fetched lazily: a browser downloads a face only when a rendered element
   asks for it, so a Bloom page never pays for Fredoka. Swap, not block — the title is
   real content and must never sit invisible behind a font request. --- */
@font-face { font-family: 'Cormorant EZ'; font-weight: 600; font-display: swap; src: url('/fonts/cormorant-600.woff2') format('woff2'); }
@font-face { font-family: 'Fraunces EZ'; font-weight: 600; font-display: swap; src: url('/fonts/fraunces-600.woff2') format('woff2'); }
@font-face { font-family: 'Fredoka EZ'; font-weight: 600; font-display: swap; src: url('/fonts/fredoka-600.woff2') format('woff2'); }
@font-face { font-family: 'Patrick Hand EZ'; font-weight: 400; font-display: swap; src: url('/fonts/patrickhand-400.woff2') format('woff2'); }
@font-face { font-family: 'Zilla Slab EZ'; font-weight: 600; font-display: swap; src: url('/fonts/zillaslab-600.woff2') format('woff2'); }
@font-face { font-family: 'Bodoni Moda EZ'; font-weight: 600; font-display: swap; src: url('/fonts/bodonimoda-600.woff2') format('woff2'); }
@font-face { font-family: 'Josefin Sans EZ'; font-weight: 600; font-display: swap; src: url('/fonts/josefinsans-600.woff2') format('woff2'); }
@font-face { font-family: 'Marcellus EZ'; font-weight: 400; font-display: swap; src: url('/fonts/marcellus-400.woff2') format('woff2'); }
@font-face { font-family: 'Quicksand EZ'; font-weight: 600; font-display: swap; src: url('/fonts/quicksand-600.woff2') format('woff2'); }
@font-face { font-family: 'Righteous EZ'; font-weight: 400; font-display: swap; src: url('/fonts/righteous-400.woff2') format('woff2'); }
@font-face { font-family: 'Lilita One EZ'; font-weight: 400; font-display: swap; src: url('/fonts/lilitaone-400.woff2') format('woff2'); }

/* --- The public-list masthead: the card face. Art, title, flourish and date are one
   centred composition, set like the top of an invitation. Themes speak through tokens:
     --k-display / --k-display-weight   the title face (Kraft and Plain keep the house sans)
     --k-display-scale                  optical size correction: Cormorant and Patrick Hand
                                        set small on the body, so they scale up to match
     --k-display-track                  letter-spacing for the face
   The flourish is a rule-mark-rule ornament in the theme accent; themes restyle the mark.
   Text never sits on the art (zones rule): the art is a block above the title. --- */
.list-mast {
    text-align: center; max-width: 46rem; margin: 0 auto 1.25rem; padding-top: .25rem;
}
.list-mast .mast-art { margin: 0 auto .35rem; pointer-events: none; }
.list-mast .mast-art svg, .list-mast .mast-art img {
    display: block; margin: 0 auto; max-width: 100%;
    height: clamp(92px, 20vw, 136px); width: auto;
}
.list-mast .mast-title {
    font-family: var(--k-display, var(--font));
    font-weight: var(--k-display-weight, 700);
    font-size: calc(clamp(1.7rem, 1.25rem + 1.9vw, 2.5rem) * var(--k-display-scale, 1));
    letter-spacing: var(--k-display-track, -.01em);
    line-height: 1.12; text-wrap: balance; overflow-wrap: anywhere;
    margin: 0 auto; max-width: 30ch;
}
.mast-flourish {
    display: flex; align-items: center; justify-content: center; gap: .55rem;
    margin: .75rem auto .55rem; color: var(--k-accent);
}
.mast-flourish::before, .mast-flourish::after {
    content: ""; width: clamp(28px, 8vw, 52px); height: 1px; background: currentColor; opacity: .75;
}
.mast-flourish > span { width: 6px; height: 6px; background: currentColor; transform: rotate(45deg); }
.mast-date {
    margin: 0; font-size: .78rem; font-weight: 600; letter-spacing: .14em; text-transform: uppercase;
    color: var(--k-ink-muted);
}
.mast-desc { margin: .65rem auto 0; max-width: 54ch; color: var(--k-ink-muted); }
.mast-desc.is-long { text-align: left; }
.mast-actions { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; margin-top: .9rem; }
.list-mast.no-art { padding-top: .75rem; }

/* Frame, not theme: the one acquisition line on a gifter-facing page. Identical and
   brand-indigo in every theme — the link that turns a gifter into an owner must not get
   quieter on some looks than others. */
/* Scoped to .site-nav so it ties `.site-nav a:not(.btn)` on specificity and wins on order;
   a bare .nav-cta lost to it and the link shipped in nav gray. */
.site-nav a.nav-cta { color: var(--primary); font-weight: 600; }
.site-nav a.nav-cta:hover { color: var(--primary-hover); }

/* --- Theme picker (Settings): choose, don't compose. Swatches carry their palette as
   inline styles rendered from the catalog, so they cannot drift from the real thing. --- */
.theme-picker { display: flex; flex-wrap: wrap; gap: .5rem; }
.theme-chip { position: relative; }
.theme-chip > input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.theme-chip .chip-body {
    display: flex; flex-direction: column; align-items: center; gap: .3rem;
    min-width: 76px; padding: .5rem .6rem;
    background: var(--k-surface); border: 2px solid var(--k-input-line); border-radius: 12px;
}
.theme-chip > input:checked + .chip-body { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(91, 91, 214, .15); }
.theme-chip > input:focus-visible + .chip-body { outline: 2px solid var(--primary); outline-offset: 2px; }
.chip-sw { position: relative; width: 46px; height: 30px; border-radius: 6px; border: 1px solid rgba(0, 0, 0, .12); overflow: hidden; }
.chip-sw .chip-card { position: absolute; left: 7px; right: 7px; top: 9px; bottom: -4px; border-radius: 4px 4px 0 0; border: 1px solid; }
.chip-sw .chip-dot { position: absolute; right: 4px; top: 4px; width: 8px; height: 8px; border-radius: 50%; }
.theme-chip .chip-name { font-size: .72rem; font-weight: 600; color: var(--gray-700); }
/* The occasion's own look. Muted ink, not the brand indigo: the page wears the picked
   theme, and indigo measures 2.4:1 on Dusk and Rocket. */
.theme-chip .chip-tag {
    margin-top: -.2rem; font-size: .62rem; font-weight: 600; letter-spacing: .02em;
    color: var(--k-ink-muted);
}
.theme-chip .chip-tag[hidden] { display: none; }
/* /dashboard rows: each list's look as a swatch beside its title — the same chip at a
   smaller size, linking to the setting. Un-themed rows show their occasion default,
   which is what advertises the feature (PHASE3-THEMES.md, surfaces table). */
.list-title-row { display: flex; align-items: center; gap: .55rem; min-width: 0; }
.list-title-row h3 { min-width: 0; }
.list-look { flex: none; display: block; border-radius: 7px; }
.list-look .chip-sw { width: 36px; height: 24px; }
.list-look .chip-sw .chip-card { left: 6px; right: 6px; top: 7px; }
.list-look .chip-sw .chip-dot { width: 6px; height: 6px; right: 3px; top: 3px; }
.list-look:hover .chip-sw { border-color: var(--primary); }
.list-look:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.more-looks summary { font-size: .85rem; color: var(--gray-500); cursor: pointer; margin: .5rem 0; }
.cw-row { display: flex; align-items: center; gap: .5rem; margin-top: .5rem; }
.cw-row .cw-label { font-size: .78rem; color: var(--gray-500); }
.cw-dot { position: relative; }
.cw-dot > input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.cw-dot .dot { display: block; width: 24px; height: 24px; border-radius: 50%; border: 2px solid rgba(0, 0, 0, .15); box-sizing: border-box; }
.cw-dot > input:checked + .dot { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(91, 91, 214, .25); }
.cw-dot > input:focus-visible + .dot { outline: 2px solid var(--primary); outline-offset: 2px; }
/* Look preview on Settings: the real masthead, smaller, on the list's own ground. A fixed
   height, so trying looks does not shove the picker up and down under the owner's finger:
   faces set at different sizes, Plain has no art, and a long title would otherwise grow
   the box. The title clamps to two lines and the block centres in the space. */
.look-preview {
    display: flex; flex-direction: column; height: 15rem; overflow: hidden;
    margin: .4rem 0 .9rem; padding: .6rem 1rem .75rem; border-radius: 14px;
    border: 1px solid var(--k-hairline);
    background: linear-gradient(160deg, var(--k-ground-a), var(--k-ground-b));
}
#look-preview-body { flex: 1; min-height: 0; display: flex; align-items: center; }
.look-preview .list-mast, .look-preview .list-mast.no-art { width: 100%; padding-top: 0; }
.look-preview .mast-title {
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.look-preview-label {
    font-size: .7rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--k-ink-muted);
}
.look-preview .list-mast { margin-bottom: 0; }
.look-preview .mast-art svg, .look-preview .mast-art img { height: 84px; }
.look-preview .mast-title { font-size: calc(1.45rem * var(--k-display-scale, 1)); }
.look-preview .mast-flourish { margin: .5rem auto .35rem; }
/* Unsaved-changes bar: frame, so the same white bar on every look. */
.unsaved-bar {
    position: fixed; left: 50%; transform: translateX(-50%); z-index: 60;
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .5rem .75rem;
    width: max-content; max-width: calc(100% - 32px); padding: .6rem .75rem .6rem 1rem;
    background: #fff; color: #2c2618; border: 1px solid #e5d8bd; border-radius: 14px;
    box-shadow: 0 12px 30px -12px rgba(0, 0, 0, .35); font-size: .9rem; font-weight: 600;
}
/* The bar is white on every look, so its secondary button resets to the kraft frame values
   instead of a theme's surface (on Dusk or Rocket that would be a dark button on white). */
.unsaved-bar .btn-secondary { background: #fffdf8; border-color: #d9cdb2; color: #514734; }
.unsaved-bar .btn-secondary:hover { background: #f8f2e5; }
/* Art-set row: the same choose-don't-compose chip, holding a thumbnail of the drawing. */
.art-row { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; margin-top: .5rem; }
.art-row .cw-label { font-size: .78rem; color: var(--gray-500); }
.art-chip { position: relative; display: flex; flex-direction: column; align-items: center; gap: .2rem; }
.art-chip > input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.art-chip .art-thumb {
    display: flex; align-items: center; justify-content: center; width: 72px; height: 48px; padding: 4px;
    background: var(--k-surface); border: 2px solid var(--k-input-line); border-radius: 10px;
}
.art-chip .art-thumb svg { max-width: 100%; max-height: 100%; width: auto; height: auto; }
.art-chip .chip-name { font-size: .72rem; font-weight: 600; color: var(--gray-700); }
.art-chip > input:checked + .art-thumb { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(91, 91, 214, .15); }
.art-chip > input:focus-visible + .art-thumb { outline: 2px solid var(--primary); outline-offset: 2px; }

/* --- Themes. Ground rides the tokenised gradient already on body.page-kraft; themes
   that need a different ground STRUCTURE (flat, ruled lines, weave) restate background.
   Footer and hand tokens are deliberately absent everywhere: frame. --- */

body.theme-plain {
    --k-ground-a: #faf8f4; --k-ground-b: #faf8f4;
    --k-surface: #ffffff; --k-surface-2: #f6f3ee; --k-surface-3: #f0ece4; --k-surface-alt: #fffefb;
    --k-img-fill: #f4f1ea;
    --k-hairline: #e6e0d6; --k-hairline-2: #ddd6c9; --k-input-line: #ddd6c9; --k-eyelet: #e9e3d9;
    --k-ink-head: #2b2823; --k-ink: #33302a; --k-ink-2: #4d493f; --k-ink-alt: #45413a;
    --k-ink-muted: #625c52; --k-ink-muted-2: #57534a;
    --k-accent: #7a7468;
    --k-shadow-rgb: 110 105 95;
    background: var(--k-ground-a); /* flat, no grain, no gradient — deliberately nothing */
}

body.theme-bloom {
    --k-ground-a: #fbf6f2; --k-ground-b: #f1e5df;
    --k-surface: #fffcfa; --k-surface-2: #f8efe9; --k-surface-3: #f3e6de; --k-surface-alt: #fffdfb;
    --k-img-fill: #f7efe9;
    --k-hairline: #ecdfd7; --k-hairline-2: #e3d2c9; --k-input-line: #e3d2c9; --k-eyelet: #eaddd4;
    --k-ink-head: #322926; --k-ink: #3b322e; --k-ink-2: #55463f; --k-ink-alt: #4b3e38;
    --k-ink-muted: #6a5a53; --k-ink-muted-2: #5e504a;
    --k-accent: #c2808d;
    --k-art-fill: #f2d8dc; --k-art-fill-2: #dcaab4;
    --k-shadow-rgb: 121 90 77;
}
body.theme-bloom.cw-mist {
    --k-ground-a: #f4f6fa; --k-ground-b: #e8edf5;
    --k-surface: #fdfeff; --k-surface-2: #f3f6fb; --k-surface-3: #eaf0f8; --k-surface-alt: #feffff;
    --k-img-fill: #f1f5fa;
    --k-hairline: #dfe6f0; --k-hairline-2: #d3dded; --k-input-line: #d3dded; --k-eyelet: #dee6f1;
    --k-ink-head: #2a3040; --k-ink: #333a4b; --k-ink-2: #485063; --k-ink-alt: #414a5d;
    --k-ink-muted: #565f77; --k-ink-muted-2: #4c556c;
    --k-accent: #7f95c2;
    --k-art-fill: #dfe8f6; --k-art-fill-2: #b9c8e2;
    --k-shadow-rgb: 76 90 112;
}

body.theme-evergreen {
    --k-ground-a: #f3f6ef; --k-ground-b: #e4ebdd;
    --k-surface: #fcfdfa; --k-surface-2: #f4f7f0; --k-surface-3: #ebf1e6; --k-surface-alt: #fdfefb;
    --k-img-fill: #eff3ea;
    --k-hairline: #dbe4d3; --k-hairline-2: #cdd9c4; --k-input-line: #cdd9c4; --k-eyelet: #dde5d5;
    --k-ink-head: #28302a; --k-ink: #2f3630; --k-ink-2: #444f46; --k-ink-alt: #3c463e;
    --k-ink-muted: #586255; --k-ink-muted-2: #4e574c;
    --k-accent: #5f7a58;
    --k-shadow-rgb: 85 105 80;
}

/* Evergreen colorways (catalog: evergreen.frost / evergreen.candy). Only the colour story
   moves; the pine recolours itself through --k-art-* because its strokes read those
   tokens with the Classic greens as literal fallbacks. */
body.theme-evergreen.cw-frost {
    --k-ground-a: #f3f7fa; --k-ground-b: #e3ebf1;
    --k-surface: #fcfdfe; --k-surface-2: #f2f6f9; --k-surface-3: #e9f0f5; --k-surface-alt: #feffff;
    --k-img-fill: #eff4f8;
    --k-hairline: #d5dfe7; --k-hairline-2: #c7d4de; --k-input-line: #c7d4de; --k-eyelet: #dbe4eb;
    --k-ink-head: #24303a; --k-ink: #2c3842; --k-ink-2: #435160; --k-ink-alt: #3a4856;
    --k-ink-muted: #55636f; --k-ink-muted-2: #4b5964;
    --k-accent: #6f93ad;
    --k-shadow-rgb: 70 90 110;
    /* Frosted boughs: four depths of silver-blue, berries gone to ice and pale gold. */
    --k-art-1: #dbe5ec; --k-art-2: #b9cbd8; --k-art-3: #8fa9bb; --k-art-4: #5f7d93;
    --k-art-berry: #a9bccb; --k-art-berry-2: #d9cfb0;
}
body.theme-evergreen.cw-candy {
    --k-ground-a: #fff8f6; --k-ground-b: #fbe9e6;
    --k-surface: #ffffff; --k-surface-2: #fbf1ef; --k-surface-3: #f7e7e4; --k-surface-alt: #fffbfa;
    --k-img-fill: #fbf2f0;
    --k-hairline: #f0dcd8; --k-hairline-2: #e6cbc6; --k-input-line: #e6cbc6; --k-eyelet: #f1dfdb;
    --k-ink-head: #33272a; --k-ink: #3a2e31; --k-ink-2: #55464a; --k-ink-alt: #4a3c40;
    --k-ink-muted: #6c5a5e; --k-ink-muted-2: #604f53;
    --k-accent: #c8353f;
    --k-shadow-rgb: 130 70 75;
    /* Peppermint: red-foil boughs in four depths, berries inverted to holly green + gold. */
    --k-art-1: #f3cfd2; --k-art-2: #e8a3a9; --k-art-3: #d5717a; --k-art-4: #b6444e;
    --k-art-berry: #4f7049; --k-art-berry-2: #e2b03a;
}

body.theme-confetti {
    --k-ground-a: #fffbf4; --k-ground-b: #fdf0e0;
    --k-surface: #ffffff; --k-surface-2: #fbf4e9; --k-surface-3: #f8eedd; --k-surface-alt: #fffef9;
    --k-img-fill: #fcf5eb;
    --k-hairline: #f1e4d3; --k-hairline-2: #e8d7c0; --k-input-line: #e8d7c0; --k-eyelet: #f3e7d7;
    --k-ink-head: #2e2823; --k-ink: #36302a; --k-ink-2: #514840; --k-ink-alt: #473f37;
    --k-ink-muted: #695d51; --k-ink-muted-2: #5d5248;
    --k-accent: #d98c94;
}

body.theme-ruled {
    --k-ground-a: #f8fafc; --k-ground-b: #eef3f7;
    --k-surface: #ffffff; --k-surface-2: #f4f8fb; --k-surface-3: #ecf2f7; --k-surface-alt: #fdfeff;
    --k-img-fill: #f2f6f9;
    --k-hairline: #dce4ea; --k-hairline-2: #cfdae3; --k-input-line: #cfdae3; --k-eyelet: #e3eaf0;
    --k-ink-head: #262e38; --k-ink: #2e3742; --k-ink-2: #47525f; --k-ink-alt: #3e4954;
    --k-ink-muted: #57636f; --k-ink-muted-2: #4d5864;
    --k-accent: #e2a84b;
    --k-shadow-rgb: 71 86 100;
    /* The ruled page IS this theme's motif: margin line + rules over the wash. */
    background:
        linear-gradient(90deg, transparent 0 46px, #e8a3ab 46px 47px, transparent 47px),
        repeating-linear-gradient(180deg, transparent 0 27px, #d6e2ec 27px 28px),
        radial-gradient(120% 90% at 50% 0%, var(--k-ground-a) 0%, var(--k-ground-b) 100%);
    background-attachment: fixed;
}

body.theme-canvas {
    --k-ground-a: #f5f3e9; --k-ground-b: #e6e4d3;
    --k-surface: #fdfcf6; --k-surface-2: #f6f4ea; --k-surface-3: #efecdd; --k-surface-alt: #fefdf8;
    --k-img-fill: #f1efe2;
    --k-hairline: #ddd9c4; --k-hairline-2: #d2cdb5; --k-input-line: #d2cdb5; --k-eyelet: #e0dcc8;
    --k-ink-head: #2b2e26; --k-ink: #33362c; --k-ink-2: #4b4f41; --k-ink-alt: #424637;
    --k-ink-muted: #5c6152; --k-ink-muted-2: #525748;
    --k-accent: #6f7a52;
    --k-shadow-rgb: 100 102 74;
    /* Cotton-canvas weave: a 4px olive crosshatch over the wash. */
    background:
        repeating-linear-gradient(90deg, rgba(111, 122, 82, .055) 0 1px, transparent 1px 4px),
        repeating-linear-gradient(0deg, rgba(111, 122, 82, .055) 0 1px, transparent 1px 4px),
        radial-gradient(120% 90% at 50% 0%, var(--k-ground-a) 0%, var(--k-ground-b) 100%);
    background-attachment: fixed;
}

/* Ink: engraving on laid paper. Near-black ink, one brick-red seal. The ground restates
   background to add laid lines (fine horizontal rules, the texture of mould-made paper)
   over the wash; the grain SVG is dropped because laid lines are the grain here. */
body.theme-ink {
    --k-ground-a: #f7f3ea; --k-ground-b: #ebe4d6;
    --k-surface: #fdfbf6; --k-surface-2: #f4efe4; --k-surface-3: #ece5d6; --k-surface-alt: #fffdf9;
    --k-img-fill: #f2ede2;
    --k-hairline: #ddd5c4; --k-hairline-2: #cfc6b2; --k-input-line: #cfc6b2; --k-eyelet: #e0d8c6;
    --k-ink-head: #1d1a16; --k-ink: #26221d; --k-ink-2: #3d3830; --k-ink-alt: #35302a;
    --k-ink-muted: #5a544b; --k-ink-muted-2: #4f4a41;
    --k-accent: #8a3a34;
    --k-shadow-rgb: 60 52 40;
    background:
        repeating-linear-gradient(180deg, transparent 0 3px, rgba(29, 26, 22, .035) 3px 4px),
        radial-gradient(120% 90% at 50% 0%, var(--k-ground-a) 0%, var(--k-ground-b) 100%);
    background-attachment: fixed;
}

/* Dusk: the first dark ground. The ink ramp inverts to light, which the page-kraft gray
   remaps pick up from the tokens — with three exceptions pinned below as frame, because
   --gray-900 also paints the footer ground and the iOS overscroll strip, and the header
   stays white with dark ink in every theme. */
body.theme-dusk {
    color-scheme: dark;
    --k-ground-a: #34302c; --k-ground-b: #23201d;
    --k-surface: #3b3632; --k-surface-2: #332f2b; --k-surface-3: #2e2a27; --k-surface-alt: #443f3a;
    --k-img-fill: #2e2a27;
    --k-hairline: #4d4640; --k-hairline-2: #574f48; --k-input-line: #5c544c; --k-eyelet: #4d4640;
    --k-ink-head: #f4ede3; --k-ink: #e9e1d6; --k-ink-2: #d9cfc2; --k-ink-alt: #ddd3c6;
    --k-ink-muted: #b8ad9f; --k-ink-muted-2: #c2b7a9;
    --k-accent: #e0b46a;
    --k-shadow-rgb: 0 0 0;
    --k-surface-rgb: 59 54 50;
    --k-backdrop: rgba(0, 0, 0, .65);
    --gray-900: var(--k-ink-head); /* item titles, stat numbers: light, like the rest of the ramp */
}
/* Frame pins. Scoped token resets, so nothing inside the header has to know it sits on a
   dark page: it resolves kraft's own inks and surfaces. Footer and overscroll keep the
   espresso ground the footer text tokens were measured against. */
body.theme-dusk .site-header, body.theme-rocket .site-header {
    --gray-500: #5e543f; --gray-600: #55503c; --gray-700: #514734; --gray-800: #3a3226; --gray-900: #2c2618;
    --k-surface-alt: #fffdf8; --k-surface-2: #f8f2e5; --k-input-line: #d9cdb2; --k-ink-2: #514734;
}
body.theme-dusk .site-footer, body.theme-dusk::after,
body.theme-rocket .site-footer, body.theme-rocket::after { background: #2c2618; }
/* Espresso on charcoal is a near match, so the footer takes a hairline to read as a base. */
body.theme-dusk .site-footer, body.theme-rocket .site-footer { border-top: 1px solid var(--k-hairline); }
/* Links sit on the dark ground, where the brand indigo measures 2.4:1. Inside the page they
   take the footer's lightened indigo (5.5:1 on the card surface, 6.0:1 on the ground);
   buttons keep their own colours and the header's links are on white and untouched. */
body.theme-dusk main a:not(.btn), body.theme-rocket main a:not(.btn) { color: #a9a9f4; }
/* Inputs are dark fields, not white wells — white boxes on a charcoal card read as holes. */
body.theme-dusk .form-control, body.theme-rocket .form-control { background: var(--k-surface-2); color: var(--k-ink); }
body.theme-dusk .form-control::placeholder, body.theme-rocket .form-control::placeholder { color: var(--k-ink-muted); }

/* Colorways for existing themes (catalog: confetti.bright / confetti.pastel / dusk.midnight).
   Confetti's art and its three-dot flourish read --k-art-*, with the Classic party colours
   as literal fallbacks, so a colorway repaints the balloons, the cap and the throw. Dusk's
   moon reads --k-accent: Midnight is navy with a silver moon. */
body.theme-confetti.cw-bright {
    --k-ground-a: #fffdf8; --k-ground-b: #fff3e6; --k-surface: #ffffff; --k-surface-2: #fffbf6;
    --k-surface-3: #fff8f0; --k-surface-alt: #ffffff; --k-img-fill: #fffaf4; --k-hairline: #f3e5d4;
    --k-hairline-2: #e2d4c4; --k-input-line: #e2d4c4; --k-eyelet: #f8efe5; --k-ink-head: #2e2823;
    --k-ink: #36302a; --k-ink-2: #4c433a; --k-ink-alt: #443d35; --k-ink-muted: #665a4e;
    --k-ink-muted-2: #5a5045; --k-accent: #e8547a; --k-shadow-rgb: 150 100 70; --k-art-1: #ec5a82;
    --k-art-1d: #d23f68; --k-art-2: #f6b52d; --k-art-2d: #e09a10; --k-art-3: #4f86e8;
    --k-art-3d: #356ad0; --k-art-4: #3fb27f;
}
body.theme-confetti.cw-pastel {
    --k-ground-a: #fdf9fb; --k-ground-b: #f6edf2; --k-surface: #ffffff; --k-surface-2: #fcf9fa;
    --k-surface-3: #faf4f7; --k-surface-alt: #ffffff; --k-img-fill: #fbf7f9; --k-hairline: #efe2ea;
    --k-hairline-2: #dfd2da; --k-input-line: #dfd2da; --k-eyelet: #f5eef2; --k-ink-head: #2f2a2e;
    --k-ink: #373135; --k-ink-2: #4d444a; --k-ink-alt: #453e43; --k-ink-muted: #675b63;
    --k-ink-muted-2: #5b5058; --k-accent: #e6aebd; --k-shadow-rgb: 130 100 115; --k-art-1: #f2b8c8;
    --k-art-1d: #e59db1; --k-art-2: #f5d98f; --k-art-2d: #ebc76a; --k-art-3: #b8c9f0;
    --k-art-3d: #9cb3e6; --k-art-4: #b9dcbb; --k-art-cap: #7d8bb5; --k-art-cap-d: #66739c;
}
body.theme-dusk.cw-midnight {
    --k-ground-a: #272c3d; --k-ground-b: #181c29; --k-surface: #2e3447; --k-surface-2: #24293a;
    --k-surface-3: #1f2332; --k-surface-alt: #3c4154; --k-img-fill: #1f2332; --k-hairline: #3f4659;
    --k-hairline-2: #4d5466; --k-input-line: #565d6f; --k-eyelet: #3f4659; --k-ink-head: #f1f3fa;
    --k-ink: #e3e7f2; --k-ink-2: #d6dbe8; --k-ink-alt: #dbe0ec; --k-ink-muted: #b0b7c9;
    --k-ink-muted-2: #bdc3d3; --k-accent: #c9d4ea; --k-shadow-rgb: 0 0 0; --k-surface-rgb: 46 52 71;
    --k-backdrop: rgba(0, 0, 0, .65);
}

/* Snowfall: winter that is not Christmas. Navy ink and snowflakes on a cold white wash,
   gold for the sparkles and the menorah's flames. No tree and no ornament anywhere, so it
   serves any "holidays" list; the Menorah art set is the Hanukkah option. */
body.theme-snowfall {
    --k-ground-a: #f6f8fb; --k-ground-b: #e7edf5; --k-surface: #fdfeff; --k-surface-2: #f5f8fc;
    --k-surface-3: #f0f4f9; --k-surface-alt: #fefeff; --k-img-fill: #f3f6fa; --k-hairline: #d7e0eb;
    --k-hairline-2: #c7d0dd; --k-input-line: #c7d0dd; --k-eyelet: #e6ecf3; --k-ink-head: #1c2840;
    --k-ink: #24304a; --k-ink-2: #37435c; --k-ink-alt: #313d56; --k-ink-muted: #4f5b73;
    --k-ink-muted-2: #445069; --k-accent: #3b5b8f; --k-shadow-rgb: 60 80 115; --k-art-1: #c7d6e8;
    --k-art-2: #8ea7c8; --k-art-3: #4a6796; --k-art-gold: #c9a24a;
}

/* Lullaby: the nursery. A crib mobile on a soft wash; Sage is the default because it is
   neither pink nor blue, and Butter, Blush and Sky are the picks either way. */
body.theme-lullaby {
    --k-ground-a: #f7f8f2; --k-ground-b: #eaeee1; --k-surface: #fdfefb; --k-surface-2: #f6f8f2;
    --k-surface-3: #f2f4eb; --k-surface-alt: #fefefd; --k-img-fill: #f4f7ef; --k-hairline: #dde4d5;
    --k-hairline-2: #cdd4c6; --k-input-line: #cdd4c6; --k-eyelet: #eaeee4; --k-ink-head: #29302a;
    --k-ink: #313831; --k-ink-2: #424a42; --k-ink-alt: #3c443c; --k-ink-muted: #576056;
    --k-ink-muted-2: #4e564d; --k-accent: #8fae8b; --k-shadow-rgb: 100 110 90; --k-art-1: #f0d488;
    --k-art-2: #a9c3a5; --k-art-3: #b8c7b3; --k-art-wood: #d6c19f; --k-art-string: #b5aa98;
}
body.theme-lullaby.cw-butter {
    --k-ground-a: #fdf9ec; --k-ground-b: #f5ecd2; --k-surface: #fffdf6; --k-surface-2: #fcf7e9;
    --k-surface-3: #f9f3e0; --k-surface-alt: #fffefa; --k-img-fill: #faf5e6; --k-hairline: #efe3c4;
    --k-hairline-2: #ded3b5; --k-input-line: #ded3b5; --k-eyelet: #f5edd8; --k-ink-head: #33302a;
    --k-ink: #3a3630; --k-ink-2: #4c463c; --k-ink-alt: #464138; --k-ink-muted: #615a4b;
    --k-ink-muted-2: #575144; --k-accent: #d9b54a; --k-shadow-rgb: 140 120 70; --k-art-1: #f3cf63;
    --k-art-2: #f2bf8f; --k-art-3: #e3d3a8;
}
body.theme-lullaby.cw-blush {
    --k-ground-a: #fdf6f5; --k-ground-b: #f5e4e2; --k-surface: #fffcfb; --k-surface-2: #fcf4f2;
    --k-surface-3: #f9eeec; --k-surface-alt: #fffefd; --k-img-fill: #faf1f0; --k-hairline: #efdcd9;
    --k-hairline-2: #dfccca; --k-input-line: #dfccca; --k-eyelet: #f5e9e7; --k-ink-head: #352a2b;
    --k-ink: #3d3133; --k-ink-2: #514345; --k-ink-alt: #4a3d3f; --k-ink-muted: #69585a;
    --k-ink-muted-2: #5e4e50; --k-accent: #d79aa3; --k-shadow-rgb: 140 95 95; --k-art-1: #f3d58f;
    --k-art-2: #e6a9b3; --k-art-3: #e8c9cc;
}
body.theme-lullaby.cw-sky {
    --k-ground-a: #f4f8fc; --k-ground-b: #e3ecf6; --k-surface: #fcfeff; --k-surface-2: #f3f8fc;
    --k-surface-3: #edf3fa; --k-surface-alt: #fefeff; --k-img-fill: #f1f6fb; --k-hairline: #d9e4f0;
    --k-hairline-2: #c9d4e1; --k-input-line: #c9d4e1; --k-eyelet: #e7eef6; --k-ink-head: #26303d;
    --k-ink: #2e3846; --k-ink-2: #3f4a5c; --k-ink-alt: #394455; --k-ink-muted: #536077;
    --k-ink-muted-2: #4a566b; --k-accent: #8fb0d6; --k-shadow-rgb: 70 95 125; --k-art-1: #f3d98f;
    --k-art-2: #9dbbe0; --k-art-3: #bccfe6;
}

/* Rocket: the kids' look. The second dark ground, deep navy with a bright flat rocket; it
   takes Dusk's frame pins above (white header, espresso footer, light links, dark inputs)
   through the shared selector lists, and IsDark gives its share card the light indigo. */
body.theme-rocket {
    color-scheme: dark;
    --k-ground-a: #1f2a4d; --k-ground-b: #141b36; --k-surface: #263258; --k-surface-2: #1e2849;
    --k-surface-3: #192240; --k-surface-alt: #344064; --k-img-fill: #192240; --k-hairline: #35416a;
    --k-hairline-2: #454f77; --k-input-line: #4f5980; --k-eyelet: #35416a; --k-ink-head: #f5f3ff;
    --k-ink: #e8e6fa; --k-ink-2: #dcdaf1; --k-ink-alt: #e1dff5; --k-ink-muted: #b9b8d6;
    --k-ink-muted-2: #c5c4df; --k-accent: #ffb04a; --k-shadow-rgb: 0 0 0; --k-surface-rgb: 38 50 88;
    --k-backdrop: rgba(0, 0, 0, .65); --k-art-body: #f4f1ea; --k-art-1: #ef6b6b; --k-art-2: #b48cf2;
    --k-art-3: #f5d36e; --k-art-4: #6fd6c4; --k-art-glass: #7fc8f8; --k-art-flame: #ffe08a;
    --k-art-star: #fff3c4; --k-art-metal: #9aa3c0;
    --gray-900: var(--k-ink-head); /* item titles, stat numbers: light, like the rest of the ramp */
}

/* Sticker: kids' birthdays beside Confetti. Die-cut sticker drawings that recolour from
   --k-art-*: Bubblegum is the default, Lagoon, Lime, Sunset, Grape and Rainbow the picks.
   The capybara (brown), Santa's hat (red) and the white border are fixed in every palette.
   Generated with docs/design/explorations/sticker-drafts/tokens.py, which checks AA. */
body.theme-sticker {
    --k-ground-a: #fff0f5; --k-ground-b: #fde1ec; --k-surface: #fffafc; --k-surface-2: #fff4f8;
    --k-surface-3: #feeff5; --k-surface-alt: #fffcfe; --k-img-fill: #feebf2;
    --k-hairline: #f3cfdd; --k-hairline-2: #dbb6c8; --k-input-line: #dbb6c8;
    --k-eyelet: #f8e2eb; --k-ink-head: #3a1b36; --k-ink: #42213e; --k-ink-2: #512e4c;
    --k-ink-alt: #4b2846; --k-ink-muted: #6e4566; --k-ink-muted-2: #633c5c; --k-accent: #e0679a;
    --k-shadow-rgb: 153 116 142; --k-art-1: #f59bbf; --k-art-2: #ffd36e; --k-art-3: #79cfc4;
    --k-art-4: #b69cf0; --k-art-line: #4a2346; --k-art-blush: #ff7f9e; --k-art-cut: #ffffff;
    --k-art-santa: #d9413a; --k-art-capy: #b4835d; --k-art-capy-d: #7e5539;
}
body.theme-sticker.cw-lagoon {
    --k-ground-a: #eaf7f8; --k-ground-b: #d3eef1; --k-surface: #f9feff; --k-surface-2: #f1fafb;
    --k-surface-3: #e8f7f9; --k-surface-alt: #fcfeff; --k-img-fill: #e2f4f6;
    --k-hairline: #c2e3e9; --k-hairline-2: #aacbd3; --k-input-line: #aacbd3;
    --k-eyelet: #dbeff3; --k-ink-head: #0f2a3d; --k-ink: #163447; --k-ink-2: #244255;
    --k-ink-alt: #1e3c4f; --k-ink-muted: #3d5c6e; --k-ink-muted-2: #335264; --k-accent: #2f86c8;
    --k-shadow-rgb: 106 136 149; --k-art-1: #56c1d4; --k-art-2: #ffcd5a; --k-art-3: #2f86c8;
    --k-art-4: #ff8b78; --k-art-line: #163a52; --k-art-blush: #ff8b8b;
}
body.theme-sticker.cw-lime {
    --k-ground-a: #f4fae6; --k-ground-b: #e5f2cc; --k-surface: #fdfff7; --k-surface-2: #f8fcee;
    --k-surface-3: #f2f9e4; --k-surface-alt: #fefffb; --k-img-fill: #eff7dd;
    --k-hairline: #d6e8b8; --k-hairline-2: #bdcfa2; --k-input-line: #bdcfa2;
    --k-eyelet: #e8f2d4; --k-ink-head: #1d3116; --k-ink: #25391d; --k-ink-2: #324629;
    --k-ink-alt: #2d4124; --k-ink-muted: #4b5f3f; --k-ink-muted-2: #425636; --k-accent: #3aa76d;
    --k-shadow-rgb: 121 139 105; --k-art-1: #93d35a; --k-art-2: #ffd24a; --k-art-3: #3aa76d;
    --k-art-4: #ff8452; --k-art-line: #24401d; --k-art-blush: #ff8d7a;
}
body.theme-sticker.cw-sunset {
    --k-ground-a: #fff3e7; --k-ground-b: #ffe3cb; --k-surface: #fffbf6; --k-surface-2: #fff7ee;
    --k-surface-3: #fff0e3; --k-surface-alt: #fffdfa; --k-img-fill: #ffeddd;
    --k-hairline: #f6d4b8; --k-hairline-2: #ddbaa5; --k-input-line: #ddbaa5;
    --k-eyelet: #fae6d4; --k-ink-head: #381823; --k-ink: #40202b; --k-ink-2: #502d38;
    --k-ink-alt: #492732; --k-ink-muted: #6d4550; --k-ink-muted-2: #623c47; --k-accent: #e8587c;
    --k-shadow-rgb: 153 116 117; --k-art-1: #ff8f5c; --k-art-2: #ffc94a; --k-art-3: #e8587c;
    --k-art-4: #7b6cf0; --k-art-line: #43202f; --k-art-blush: #ff7a8a;
}
body.theme-sticker.cw-grape {
    --k-ground-a: #f3effd; --k-ground-b: #e4dcfb; --k-surface: #fcfaff; --k-surface-2: #f7f4fe;
    --k-surface-3: #f1ecfd; --k-surface-alt: #fefcff; --k-img-fill: #eee8fc;
    --k-hairline: #d8cdf5; --k-hairline-2: #c0b5de; --k-input-line: #c0b5de;
    --k-eyelet: #e8e1fa; --k-ink-head: #231841; --k-ink: #2b1f4b; --k-ink-2: #392e59;
    --k-ink-alt: #332853; --k-ink-muted: #544a74; --k-ink-muted-2: #4a3f6a; --k-accent: #9b7cea;
    --k-shadow-rgb: 127 118 156; --k-art-1: #9b7cea; --k-art-2: #ffd166; --k-art-3: #f27db1;
    --k-art-4: #5fcfdc; --k-art-line: #2b1f52; --k-art-blush: #f78fb6;
}
body.theme-sticker.cw-rainbow {
    --k-ground-a: #fffaf0; --k-ground-b: #faeed8; --k-surface: #fffdf8; --k-surface-2: #fffbf4;
    --k-surface-3: #fdf6ea; --k-surface-alt: #fffefc; --k-img-fill: #fdf6e8;
    --k-hairline: #efe0c4; --k-hairline-2: #d4c6b3; --k-input-line: #d4c6b3;
    --k-eyelet: #f6eddb; --k-ink-head: #28213a; --k-ink: #302842; --k-ink-2: #3f364f;
    --k-ink-alt: #38304a; --k-ink-muted: #5a5068; --k-ink-muted-2: #50465e; --k-accent: #ff6b6b;
    --k-shadow-rgb: 138 127 138; --k-art-1: #ff6b6b; --k-art-2: #ffd93d; --k-art-3: #3cc49e;
    --k-art-4: #4d8df5; --k-art-line: #2c2440; --k-art-blush: #ff8fa0;
}
/* Skin tones for drawings of people (ListThemes.SkinTones; Sticker's mermaid and Santa). The
   theme block above sets the middle tone; a picked tone overrides it from later in the file. */
body.theme-sticker { --k-art-skin: #d29b70; }
body.skin-t1 { --k-art-skin: #f9dcc8; }
body.skin-t2 { --k-art-skin: #efc19f; }
body.skin-t3 { --k-art-skin: #d29b70; }
body.skin-t4 { --k-art-skin: #a56c47; }
body.skin-t5 { --k-art-skin: #6f4631; }

/* --- Theme type and flourish: the display face (one of the six authored decisions, and
   the one the spec calls the biggest quality lever) and the mark between the title and the
   date. Faces are mirrored in ListThemes.cs (DisplayFace) so the share card sets the same
   type; a test holds the two in step. Kraft and Plain keep the house sans on purpose:
   Kraft is the brand's own voice, Plain is the deliberate absence of a look. --- */
body.theme-bloom { --k-display: 'Cormorant EZ', Georgia, serif; --k-display-weight: 600; --k-display-scale: 1.2; --k-display-track: 0; }
body.theme-evergreen { --k-display: 'Fraunces EZ', Georgia, serif; --k-display-weight: 600; --k-display-scale: 1.02; --k-display-track: -.01em; }
body.theme-confetti { --k-display: 'Fredoka EZ', var(--font); --k-display-weight: 600; --k-display-scale: 1.04; --k-display-track: 0; }
body.theme-ruled { --k-display: 'Patrick Hand EZ', var(--font); --k-display-weight: 400; --k-display-scale: 1.16; --k-display-track: .01em; }
body.theme-canvas { --k-display: 'Zilla Slab EZ', Georgia, serif; --k-display-weight: 600; --k-display-scale: 1.04; --k-display-track: 0; }
body.theme-ink { --k-display: 'Bodoni Moda EZ', Georgia, serif; --k-display-weight: 600; --k-display-scale: 1.04; --k-display-track: -.005em; }
body.theme-dusk { --k-display: 'Josefin Sans EZ', var(--font); --k-display-weight: 600; --k-display-scale: 1; --k-display-track: .015em; }
body.theme-snowfall { --k-display: 'Marcellus EZ', Georgia, serif; --k-display-weight: 400; --k-display-scale: 1.04; --k-display-track: .01em; }
body.theme-lullaby { --k-display: 'Quicksand EZ', var(--font); --k-display-weight: 600; --k-display-scale: 1.02; --k-display-track: 0; }
body.theme-rocket { --k-display: 'Righteous EZ', var(--font); --k-display-weight: 400; --k-display-scale: 1.04; --k-display-track: .01em; }
body.theme-sticker { --k-display: 'Lilita One EZ', var(--font); --k-display-weight: 400; --k-display-scale: 1.06; --k-display-track: .01em; }

/* Plain: one quiet rule, no mark. */
body.theme-plain .mast-flourish > span, body.theme-plain .mast-flourish::after { display: none; }
body.theme-plain .mast-flourish::before { width: 56px; }
/* Bloom: a petal. */
body.theme-bloom .mast-flourish > span { width: 9px; height: 9px; border-radius: 9px 0; transform: rotate(45deg); }
/* Evergreen: a berry between needle-green rules. */
body.theme-evergreen .mast-flourish > span {
    width: 7px; height: 7px; border-radius: 50%; transform: none; background: var(--k-art-berry, #a8474f);
}
/* Confetti: three dots in the theme's party colours, no rules. */
body.theme-confetti .mast-flourish::before, body.theme-confetti .mast-flourish::after { display: none; }
body.theme-confetti .mast-flourish > span {
    width: 7px; height: 7px; border-radius: 50%; transform: none; margin: 0 14px;
    background: var(--k-art-1, #d98c94);
    box-shadow: -14px 0 0 var(--k-art-2, #e2a84b), 14px 0 0 var(--k-art-3, #94a7cc);
}
/* Ruled: a pencil squiggle, the stroke the pencil in the art is drawing. */
body.theme-ruled .mast-flourish > span, body.theme-ruled .mast-flourish::after { display: none; }
body.theme-ruled .mast-flourish::before {
    width: 96px; height: 10px; opacity: 1;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 10'%3E%3Cpath d='M2 6c8-6 12 4 20-1s12 4 20-1 12 4 20-1 12 4 20-1 9 3 12 1' fill='none' stroke='%23e2a84b' stroke-width='2.2' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
/* Canvas: stitched rules and a grommet. */
body.theme-canvas .mast-flourish::before, body.theme-canvas .mast-flourish::after {
    height: 2px; background: repeating-linear-gradient(90deg, currentColor 0 5px, transparent 5px 9px);
}
body.theme-canvas .mast-flourish > span {
    width: 9px; height: 9px; border-radius: 50%; transform: none; background: none; border: 2px solid currentColor;
}
/* Ink: engraved double rules in the ink, the one brick-red seal between them. */
body.theme-ink .mast-flourish { color: var(--k-ink-head); }
body.theme-ink .mast-flourish::before, body.theme-ink .mast-flourish::after {
    height: 4px; background: none; border-top: 1px solid; border-bottom: 1px solid; opacity: .5;
}
body.theme-ink .mast-flourish > span { width: 8px; height: 8px; border-radius: 50%; transform: none; background: var(--k-accent); }
/* Dusk: a four-point star in the moon's amber. */
body.theme-dusk .mast-flourish > span {
    width: 12px; height: 12px; transform: none;
    clip-path: polygon(50% 0, 61% 39%, 100% 50%, 61% 61%, 50% 100%, 39% 61%, 0 50%, 39% 39%);
}

/* Snowfall: a snowflake, drawn as a mask so it takes the theme's navy from currentColor. */
body.theme-snowfall .mast-flourish > span {
    width: 17px; height: 17px; transform: none;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 1.5v21M2.9 6.75l18.2 10.5M2.9 17.25l18.2-10.5M12 5.5l-2.5-2.5M12 5.5l2.5-2.5M12 18.5l-2.5 2.5M12 18.5l2.5 2.5' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 1.5v21M2.9 6.75l18.2 10.5M2.9 17.25l18.2-10.5M12 5.5l-2.5-2.5M12 5.5l2.5-2.5M12 18.5l-2.5 2.5M12 18.5l2.5 2.5' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
/* Lullaby: a soft five-point star between rounded rules. */
body.theme-lullaby .mast-flourish::before, body.theme-lullaby .mast-flourish::after { height: 2px; border-radius: 2px; }
body.theme-lullaby .mast-flourish > span {
    width: 13px; height: 13px; transform: none;
    clip-path: polygon(50% 0, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}
/* Rocket: a dotted flight path and a star in the flame's orange. */
body.theme-rocket .mast-flourish::before, body.theme-rocket .mast-flourish::after {
    height: 2px; background: repeating-linear-gradient(90deg, currentColor 0 3px, transparent 3px 7px);
}
body.theme-rocket .mast-flourish > span {
    width: 12px; height: 12px; transform: none;
    clip-path: polygon(50% 0, 61% 39%, 100% 50%, 61% 61%, 50% 100%, 39% 61%, 0 50%, 39% 39%);
}

/* Sticker: a sticker dot, the palette's main colour inside a white die-cut ring, between
   thick rounded rules. */
body.theme-sticker .mast-flourish::before, body.theme-sticker .mast-flourish::after { height: 3px; border-radius: 3px; }
body.theme-sticker .mast-flourish > span {
    width: 12px; height: 12px; border-radius: 50%; transform: none;
    background: var(--k-art-1, #f59bbf); box-shadow: 0 0 0 3px #fff, 0 0 0 4.5px var(--k-art-line, #4a2346);
}

/* --- Accessibility: prefers-contrast: more collapses every theme to Plain (PHASE3-THEMES.md
   hard rule). Plain's tokens are restated on the body so nothing needs to know which theme
   it was; the attribute selector outranks every theme and colorway block, and Dusk's frame
   pins (light links, dark inputs, the gray-900 remap) are undone explicitly. Kraft carries
   no theme class and is untouched: it already meets AA on its own ground. --- */
@media (prefers-contrast: more) {
    body[class*="theme-"].page-kraft {
        color-scheme: light;
        --k-ground-a: #faf8f4; --k-ground-b: #faf8f4;
        --k-surface: #ffffff; --k-surface-2: #f6f3ee; --k-surface-3: #f0ece4; --k-surface-alt: #fffefb;
        --k-img-fill: #f4f1ea; --k-photo-bg: #fff;
        --k-hairline: #e6e0d6; --k-hairline-2: #ddd6c9; --k-input-line: #ddd6c9; --k-eyelet: #e9e3d9;
        --k-ink-head: #2b2823; --k-ink: #33302a; --k-ink-2: #4d493f; --k-ink-alt: #45413a;
        --k-ink-muted: #625c52; --k-ink-muted-2: #57534a;
        --k-accent: #7a7468;
        --k-shadow-rgb: 110 105 95; --k-surface-rgb: 255 255 255; --k-backdrop: rgba(0, 0, 0, .55);
        --gray-900: var(--k-ink-3);
        background: var(--k-ground-a);
    }
    body[class*="theme-"].page-kraft main a:not(.btn) { color: var(--primary); }
    body[class*="theme-"].page-kraft .form-control { background: #fff; color: var(--gray-800); }
    body[class*="theme-"].page-kraft .form-control::placeholder { color: var(--gray-500); }
    body[class*="theme-"].page-kraft .site-footer { border-top: 0; }
    /* Plain has no art and no display face; under increased contrast the masthead drops
       both, and the flourish falls back to Plain's single rule. */
    body[class*="theme-"].page-kraft .list-mast .mast-art { display: none; }
    body[class*="theme-"].page-kraft {
        --k-display: var(--font); --k-display-weight: 700; --k-display-scale: 1; --k-display-track: -.01em;
    }
    body[class*="theme-"].page-kraft .mast-flourish { color: var(--k-accent); }
    body[class*="theme-"].page-kraft .mast-flourish > span,
    body[class*="theme-"].page-kraft .mast-flourish::after { display: none; }
    body[class*="theme-"].page-kraft .mast-flourish::before {
        display: block; width: 56px; height: 1px; border: 0; background: currentColor; opacity: .75;
    }
}
