/* =========================================================================
   Holiday Bright Lights Designer — global theme
   -------------------------------------------------------------------------
   Tenant brand colors are injected by _Layout.cshtml as CSS custom
   properties on :root (var(--hbl-*)). Everything in this file consumes
   those tokens so every deployment can be re-themed without rebuilding
   styles. Colors the Branding section doesn't define — hairlines, shadows,
   scrims, inverted surfaces — are color-mix()'d from the ones it does, in
   the derived block below; no rule outside :root may use a literal color.
   Typography: Besley (serif display) + Barlow (sans UI). Icons:
   Lucide (icon font, .icon-* classes) + the custom string-lights inline
   SVG (.hbl-icon).
   Layout is mobile-first; desktop tweaks live in @media queries.
   ========================================================================= */

:root {
    /* Fallbacks — overridden per-tenant inline in _Layout.cshtml */
    --hbl-primary: #D40029;
    --hbl-primary-contrast: #ffffff;
    --hbl-secondary: #6D6E71;
    --hbl-secondary-contrast: #ffffff;
    --hbl-tertiary: #9E0620;
    --hbl-tertiary-contrast: #ffffff;
    --hbl-accent: #5B0014;
    --hbl-accent-contrast: #5B0014;
    --hbl-surface: #E6E7E8;
    --hbl-bg: #ffffff;
    --hbl-text: #000000;
    --hbl-muted: #6D6E71;

    /* ---- Derived neutrals -------------------------------------------------
       Nothing below this line is configurable. Each value is mixed from one of
       the tenant tokens above, so hairlines, shadows, scrims and inverted
       surfaces follow a re-brand instead of staying a fixed slate gray. The
       per-tenant :root block in _Layout.cshtml only overrides the tokens above;
       custom properties resolve lazily at use, so these pick up the tenant's
       values automatically without being restated there. */

    /* Hairlines: card borders, dividers, rules. Three weights, all ink-tinted. */
    --hbl-hairline-soft: color-mix(in srgb, var(--hbl-text) 5%, transparent);
    --hbl-hairline: color-mix(in srgb, var(--hbl-text) 9%, transparent);
    --hbl-hairline-strong: color-mix(in srgb, var(--hbl-text) 15%, transparent);

    /* Card outline: the surface tone stepped down toward the ink until it reads
       as a solid mid-grey (#D4D5D5 against the stock #E6E7E8 surface). Opaque
       rather than an alpha hairline so it holds up over tinted backdrops. */
    --hbl-card-border: color-mix(in srgb, var(--hbl-surface) 92%, var(--hbl-text));

    /* Deep surface for inverted panels (card captions, the crop stage) and the
       two foreground weights that sit on it. */
    --hbl-ink: color-mix(in srgb, var(--hbl-text) 85%, var(--hbl-surface));
    --hbl-on-ink: var(--hbl-bg);
    --hbl-on-ink-muted: color-mix(in srgb, var(--hbl-bg) 75%, transparent);

    /* Inspiration hero photo overlay. Ink-derived so the hero copy sits
       light-on-dark over photography; a tenant wanting a specific tone sets
       Tenant:Branding:HeroOverlayColor, which _Layout.cshtml emits as an
       override of this token. */
    --hbl-hero-overlay: var(--hbl-ink);

    /* Keyboard focus ring: a halo of page background to lift it off whatever is
       behind, then the brand ring itself. Shared by the focus rule and by
       Bootstrap's own --bs-btn-focus-box-shadow so both agree. */
    --hbl-focus-ring: 0 0 0 0.1rem var(--hbl-bg), 0 0 0 0.25rem var(--hbl-primary);

    /* Translucent overlays laid over photography. */
    --hbl-scrim: color-mix(in srgb, var(--hbl-text) 55%, transparent);
    --hbl-scrim-strong: color-mix(in srgb, var(--hbl-text) 65%, transparent);
    --hbl-scrim-light: color-mix(in srgb, var(--hbl-bg) 35%, transparent);

    /* Status colors. "Done" has to read as green, "this failed" as red and
       "careful" as amber whatever the brand palette is, so these three hues
       can't be brand-derived — but every tint of them below is mixed against a
       tenant token. (Red especially: on a red-branded tenant an accent-derived
       error state would be invisible as an error.) */
    --hbl-success: #2f9e44;
    --hbl-danger: #c92a2a;
    --hbl-warning: #e8590c;

    /* Designer-loading backdrop. Deliberately NOT tenant-derived: the loader is
       a night scene for every tenant (the product is lighting houses after
       dark), matching the Godot app's own loading shell so the overlay-to-app
       hand-off reads as one screen. Branding enters via the tenant wordmark on
       top of it, not the backdrop. */
    --hbl-loader-bg: #101418;
    --hbl-loader-text: #f4f4f2;
    --hbl-loader-muted: color-mix(in srgb, var(--hbl-loader-text) 78%, transparent);

    --hbl-radius-sm: 0.5rem;
    --hbl-radius: 0.875rem;
    --hbl-radius-lg: 1.25rem;
    --hbl-shadow-sm: 0 1px 3px color-mix(in srgb, var(--hbl-text) 7%, transparent), 0 1px 2px color-mix(in srgb, var(--hbl-text) 6%, transparent);
    --hbl-shadow: 0 6px 24px color-mix(in srgb, var(--hbl-text) 9%, transparent), 0 2px 8px color-mix(in srgb, var(--hbl-text) 6%, transparent);
    --hbl-shadow-lg: 0 20px 48px color-mix(in srgb, var(--hbl-text) 16%, transparent);
    /* Ambient, directionless glow shared by every card surface (.hbl-card). */
    --hbl-shadow-card: 0 0 21px color-mix(in srgb, var(--hbl-text) 10%, transparent);
    --hbl-transition: 180ms cubic-bezier(.2,.7,.3,1);

    --hbl-font-sans: 'Barlow', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
    --hbl-font-serif: 'Besley', 'Georgia', serif;
}

html { font-size: 15px; }

@media (min-width: 768px) {
    html { font-size: 16px; }
}

/* ---------- Page transitions ----------
   Same-origin navigations crossfade instead of cutting (cross-document view
   transitions). The case that earns this is leaving /Designer: the app is a
   night scene, so its dark exit cover fades into the lit landing page here,
   across the navigation itself, giving eyes a beat to adjust — and every
   other page pair gets the same gentle handoff. Browsers without support
   (Firefox) just cut, which is what the site always did. Both pages of a
   navigation see this rule since site.css is on every page. */
@view-transition {
    navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
    animation-duration: 400ms;
}

/* A navigation is the one motion that can't be kept while zeroed elsewhere —
   without the crossfade it is simply the cut it always was. */
@media (prefers-reduced-motion: reduce) {
    ::view-transition-group(*),
    ::view-transition-image-pair(*),
    ::view-transition-old(*),
    ::view-transition-new(*) {
        animation: none !important;
    }
}

.hbl-body {
    background-color: var(--hbl-bg);
    color: var(--hbl-text);
    font-family: var(--hbl-font-sans);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    font-size: 1rem;
    line-height: 1.6;
}

.hbl-main {
    flex: 1 0 auto;
    padding-bottom: 3rem;
}

/* Page gutters. Every section of the site lays out inside a .container-xxl, so
   the breathing room between content and the window edge is set once here.
   Bootstrap's own 0.75rem is right for a phone, where width is scarce, but it
   leaves content pinned to the glass on tablets and small laptops — the sizes
   between "no room to spare" and "wide enough for the 1320px max-width to
   supply its own margin". So the gutter grows through that middle band and
   eases back off once the container stops filling the window.
   padding-inline rather than --bs-gutter-x: the gutter variable is inherited by
   .row descendants as their column gap, which is a separate decision. */
.container-xxl {
    padding-inline: 1rem;
}

@media (min-width: 576px) {
    .container-xxl { padding-inline: 1.5rem; }
}

@media (min-width: 768px) {
    .container-xxl { padding-inline: 2.5rem; }
}

@media (min-width: 992px) {
    .container-xxl { padding-inline: 3.5rem; }
}

@media (min-width: 1400px) {
    .container-xxl { padding-inline: 2rem; }
}

h1, h2, h3, h4 {
    font-family: var(--hbl-font-serif);
    font-weight: 700;
    letter-spacing: -0.015em;
    line-height: 1.2;
}

p {
    line-height: 1.65;
}

/* Custom inline SVG icons (string lights) — size like a Lucide glyph. */
.hbl-icon {
    width: 1.25em;
    height: 1.25em;
    display: inline-block;
    vertical-align: -0.25em;
    flex: 0 0 auto;
}

/* Shared card surface. Every card-like block (look, design, tip, review item)
   carries this alongside its own class so they all sit on the same shadow and
   outline. Component rules must not redeclare box-shadow or border at this
   specificity or they win on source order — override only on :hover/:focus-visible. */
.hbl-card {
    box-shadow: var(--hbl-shadow-card);
    border: 1px solid var(--hbl-card-border);
}

/* Focus styles: keep accessible, override Bootstrap blue with brand color. */
.btn:focus, .btn:active:focus,
.form-control:focus, .form-check-input:focus,
a:focus-visible {
    box-shadow: var(--hbl-focus-ring);
    outline: none;
}

/* Bootstrap paints the ring itself while a button is both held and
   keyboard-focused (`.btn:first-child:active:focus-visible`, four selectors
   deep), from a variable its base never sets — so the ring would vanish at
   exactly the moment a keyboard user activates the control. Hand it the same
   ring the rule above uses. */
.btn {
    --bs-btn-focus-box-shadow: var(--hbl-focus-ring);
}

.hbl-skip-link {
    position: absolute;
    left: 1rem;
    top: 1rem;
    background: var(--hbl-primary);
    color: var(--hbl-primary-contrast);
    padding: 0.5rem 0.875rem;
    border-radius: var(--hbl-radius-sm);
    z-index: 1080;
}

/* ============================ Header / Nav ============================ */

.hbl-header {
    position: sticky;
    top: 0;
    z-index: 1030;
    background: var(--hbl-bg);
    border-bottom: 1px solid var(--hbl-hairline);
    box-shadow: 0 1px 0 color-mix(in srgb, var(--hbl-text) 6%, transparent), 0 2px 8px color-mix(in srgb, var(--hbl-text) 4%, transparent);
}

.hbl-navbar { padding-block: 0.75rem; }

.hbl-brand {
    display: inline-flex;
    align-items: center;
    gap: 0.625rem;
    font-weight: 700;
    color: var(--hbl-text) !important;
}

.hbl-brand__logo {
    height: 1.75rem;
    width: auto;
    max-width: 8rem;
    object-fit: contain;
}

.hbl-brand__name {
    font-family: var(--hbl-font-sans);
    font-size: 1rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    line-height: 1;
    color: var(--hbl-text);
}

.hbl-navbar .nav-link {
    color: var(--hbl-text);
    font-weight: 500;
    border-radius: var(--hbl-radius-sm);
    transition: background-color var(--hbl-transition), color var(--hbl-transition);
}

.hbl-navbar .nav-link:hover,
.hbl-navbar .nav-link:focus {
    color: var(--hbl-primary);
}

/* ============================== Footer ==============================
   .hbl-body is a column flex box and .hbl-main takes the free space, so this
   sits against the bottom of the viewport on short pages without needing to be
   pinned. */

.hbl-footer {
    flex: 0 0 auto;
    background: color-mix(in srgb, var(--hbl-surface) 70%, var(--hbl-bg));
    border-top: 1px solid var(--hbl-hairline);
    padding-block: 1.5rem;
    font-size: 0.85rem;
    color: var(--hbl-muted);
}

.hbl-footer__inner {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.25rem 1.5rem;
}

.hbl-footer__copy,
.hbl-footer__note {
    margin: 0;
    line-height: 1.5;
}

.hbl-footer__copy {
    font-weight: 600;
    color: var(--hbl-text);
}

/* ============================ Buttons ============================
   Every button here is a Bootstrap .btn wearing a brand class, and Bootstrap
   owns the pressed state outright: `.btn:first-child:active` (and the sibling
   form for a second button in a row) is a three-part selector, so it beats any
   .btn-hbl-*:hover/:focus rule below no matter the source order. Those rules
   paint from --bs-btn-active-*, which base .btn never defines — an undefined
   var() is invalid at computed-value time, so the background falls back to
   transparent and the border to currentColor, and the button disappears for as
   long as it is held down.
   Escalating specificity to out-shout it would mean maintaining a parallel set
   of :active rules, so each class instead publishes the variables Bootstrap is
   already reaching for, and the two state systems agree. */

.btn-hbl-primary {
    /* Pressed = the hover treatment; Bootstrap paints it from these. */
    --bs-btn-active-color: var(--hbl-tertiary-contrast);
    --bs-btn-active-bg: var(--hbl-tertiary);
    --bs-btn-active-border-color: var(--hbl-tertiary);

    background-color: var(--hbl-primary);
    color: var(--hbl-primary-contrast);
    border: 1px solid var(--hbl-primary);
    border-radius: 999px;
    font-weight: 600;
    font-size: 0.925rem;
    padding: 0.625rem 1.375rem;
    letter-spacing: 0.01em;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    transition: transform var(--hbl-transition), box-shadow var(--hbl-transition), background-color var(--hbl-transition);
}

.btn-hbl-primary:hover,
.btn-hbl-primary:focus {
    background-color: var(--hbl-tertiary);
    border-color: var(--hbl-tertiary);
    color: var(--hbl-tertiary-contrast);
    transform: translateY(-1px);
    box-shadow: 0 6px 20px color-mix(in srgb, var(--hbl-muted) 40%, transparent);
}

.btn-hbl-primary:active {
    transform: translateY(0);
}

.btn-hbl-primary:disabled,
.btn-hbl-primary.disabled {
    background-color: color-mix(in srgb, var(--hbl-primary) 35%, var(--hbl-bg));
    border-color: transparent;
    color: var(--hbl-primary-contrast);
    transform: none;
    box-shadow: none;
    opacity: 1;
}

.btn-hbl-primary__label-group {
    display: flex;
    gap: 0.5rem;
    align-items: center;
}

.btn-hbl-ghost {
    --bs-btn-active-color: var(--hbl-muted);
    --bs-btn-active-bg: color-mix(in srgb, var(--hbl-muted) 8%, transparent);
    --bs-btn-active-border-color: color-mix(in srgb, var(--hbl-muted) 65%, transparent);
    background-color: transparent;
    color: var(--hbl-muted);
    border: 2px solid color-mix(in srgb, var(--hbl-muted) 40%, transparent);
    border-radius: 999px;
    font-weight: 600;
    font-size: 0.925rem;
    padding: 0.625rem 1.375rem;
    letter-spacing: 0.01em;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    transition: background-color var(--hbl-transition), color var(--hbl-transition), border-color var(--hbl-transition);
}

.btn-hbl-ghost:hover,
.btn-hbl-ghost:focus {
    background-color: color-mix(in srgb, var(--hbl-muted) 8%, transparent);
    border-color: color-mix(in srgb, var(--hbl-muted) 65%, transparent);
    color: var(--hbl-muted);
}

.btn-hbl-dark {
    --bs-btn-active-color: var(--hbl-secondary-contrast);
    --bs-btn-active-bg: color-mix(in srgb, var(--hbl-secondary) 80%, var(--hbl-text));
    --bs-btn-active-border-color: color-mix(in srgb, var(--hbl-secondary) 80%, var(--hbl-text));

    background-color: var(--hbl-secondary);
    color: var(--hbl-secondary-contrast);
    border: 1px solid var(--hbl-secondary);
    border-radius: 999px;
    font-weight: 600;
    font-size: 0.925rem;
    padding: 0.625rem 1.375rem;
    letter-spacing: 0.01em;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    transition: transform var(--hbl-transition), box-shadow var(--hbl-transition), background-color var(--hbl-transition);
}

.btn-hbl-dark:hover,
.btn-hbl-dark:focus {
    background-color: color-mix(in srgb, var(--hbl-secondary) 80%, var(--hbl-text));
    border-color: color-mix(in srgb, var(--hbl-secondary) 80%, var(--hbl-text));
    color: var(--hbl-secondary-contrast);
    transform: translateY(-1px);
    box-shadow: 0 6px 20px color-mix(in srgb, var(--hbl-secondary) 40%, transparent);
}

/* ============================ Section headings ============================ */

.hbl-section-title {
    font-family: var(--hbl-font-serif);
    font-size: clamp(1.75rem, 3vw, 2.5rem);
    font-weight: 700;
    text-align: center;
    color: var(--hbl-text);
    margin-bottom: 0.5rem;
}

.hbl-section-sub {
    text-align: center;
    color: var(--hbl-muted);
    max-width: 34rem;
    margin: 0 auto 1.5rem;
    font-size: 0.95rem;
}

/* "or" divider between the hero and the upload section */
.hbl-or-divider {
    display: flex;
    align-items: center;
    gap: 1rem;
    max-width: 34rem;
    margin: 2rem auto 1.5rem;
    color: var(--hbl-muted);
}

.hbl-or-divider__rule {
    flex: 1;
    height: 1px;
    background: var(--hbl-hairline-strong);
}

.hbl-or-divider__text {
    font-family: var(--hbl-font-serif);
    font-style: italic;
    font-size: 1rem;
}

/* =========================================================================
   "Get Inspired" hero + look carousel
   ========================================================================= */

/* Hero + deck own most of the first screenful: the section stretches toward
   the viewport height and the hero soaks up the free space, but it stops short
   by the sticky header (~3.25rem) plus the "or" divider that follows (~3.5rem
   with its collapsed margin) and a little slack, so the divider peeks above
   the fold and hints at the upload path below. */
.hbl-inspire {
    display: flex;
    flex-direction: column;
    min-height: calc(100svh - 6rem);
    margin-bottom: 1rem;
}

.hbl-inspire__hero {
    /* Swiping the hero photo steps the look gallery below it (home-dashboard.js);
       vertical drags still scroll the page. */
    touch-action: pan-y;

    /* Swap timings. home-dashboard.js reads these back, so the reduced-motion
       override below zeroes the script's waits too — there is no second switch
       to keep in sync. */
    --hbl-hero-fade: 420ms;
    --hbl-content-fade: 200ms;

    /* Hero foreground. The photo sits under a dark overlay-derived scrim, so
       copy is light-on-dark; the --plain variant below has no photo and stays
       a light gradient, so it flips these back to dark-on-light. */
    --hbl-hero-fg: var(--hbl-on-ink);
    --hbl-hero-fg-muted: var(--hbl-on-ink-muted);

    /* Accent for the words a tenant marks up in its headline. The brand primary
       at full strength is a dark ink on most palettes and loses the fight with
       the scrim, so it is lifted toward the hero foreground until it reads as
       brand-colored light rather than a dark red hole in the photo. */
    --hbl-hero-accent: color-mix(in srgb, var(--hbl-primary) 72%, var(--hbl-hero-fg));

    /* The scrim thins toward the right and the photo underneath is uncontrolled,
       so a bright patch can leave the accent word short of the 3:1 large-text
       floor on its own. This halo re-darkens just the pixels behind the glyphs,
       which holds the contrast wherever the headline lands. */
    --hbl-hero-text-shadow: 0 2px 18px color-mix(in srgb, var(--hbl-hero-overlay) 70%, transparent);

    /* Named so the incoming crossfade layer can reuse the exact same scrim. */
    --hbl-hero-scrim:
        linear-gradient(90deg,
            color-mix(in srgb, var(--hbl-hero-overlay) 82%, transparent) 0%,
            color-mix(in srgb, var(--hbl-hero-overlay) 62%, transparent) 35%,
            color-mix(in srgb, var(--hbl-hero-overlay) 30%, transparent) 60%,
            color-mix(in srgb, var(--hbl-hero-overlay) 10%, transparent) 100%);

    position: relative;
    isolation: isolate;
    flex: 1 0 auto;
    display: grid;
    min-height: clamp(360px, 48vh, 560px);
    /* Top padding gives way on short viewports (svh term) so the deck — and the
       "or" divider after it — stay in reach of the fold; roomy screens keep the
       full airy spacing via the cap. */
    padding: clamp(5rem, 12svh, 7rem) 0 10rem;
    background-color: color-mix(in srgb, var(--hbl-primary) 8%, var(--hbl-surface));
    background-image: var(--hbl-hero-scrim), var(--hbl-hero-image, none);
    background-size: cover;
    background-position: center;
}

/* The hero's two content groups sit at opposite ends of its height: the
   heading and subtitle pitch the page from the top, the featured look and its
   CTA sit at the bottom, directly above the gallery that changes them. The
   hero's own bottom padding is the deck's overlap zone, so "bottom" lands just
   clear of the cards. The gap is the floor on short viewports, where the two
   groups meet in the middle instead of spreading. */
.hbl-inspire__hero-inner {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 2rem;
}

.hbl-inspire__hero--plain {
    --hbl-hero-fg: var(--hbl-text);
    --hbl-hero-fg-muted: var(--hbl-muted);
    /* Dark copy on a light gradient — the brand primary needs no lifting, and
       there is no photo to defend the contrast against. */
    --hbl-hero-accent: var(--hbl-primary);
    --hbl-hero-text-shadow: none;

    background-image:
        radial-gradient(1000px 500px at 90% -10%, color-mix(in srgb, var(--hbl-primary) 22%, transparent), transparent 60%),
        linear-gradient(180deg, color-mix(in srgb, var(--hbl-primary) 8%, var(--hbl-bg)) 0%, var(--hbl-bg) 100%);
}

/* Incoming photo for a look swap, faded in over the current one and then
   committed to the base layer by the script. It carries the same scrim as the
   base so both layers composite identically mid-fade. Negative z-index keeps it
   above the hero's own background but behind the hero content. */
.hbl-inspire__hero::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background-image: var(--hbl-hero-scrim), var(--hbl-hero-image-next, none);
    background-size: cover;
    background-position: center;
    opacity: var(--hbl-hero-next-opacity, 0);
    transition: opacity var(--hbl-hero-fade) ease;
}

/* Set for one frame while the faded-in photo is promoted to the base layer, so
   the incoming layer resets to transparent without animating back out. */
.hbl-inspire__hero.is-committing::after {
    transition: none;
}

/* Look name, tags, and CTA fade down and back as their contents are replaced. */
.hbl-inspire__featured {
    transition: opacity var(--hbl-content-fade) ease,
                transform var(--hbl-content-fade) ease;
}

.hbl-inspire__featured.is-swapping {
    opacity: 0;
    transform: translateY(0.4rem);
}

@media (min-width: 992px) {
    .hbl-inspire__hero { padding: clamp(5rem, 11svh, 5rem) 0 10rem; }
}

/* Capped in ch so the measure holds as the clamp resizes the type: the headline
   wraps to a couple of lines against the photo instead of running the full width
   of the hero, which reads as a banner rather than a headline. Balanced wrapping
   keeps a longer tenant headline from ending on one orphaned word — headings are
   short enough for the browser's line-balancing to apply. */
.hbl-inspire__heading {
    font-family: var(--hbl-font-serif);
    font-size: clamp(3rem, 6vw, 4.5rem);
    font-weight: 600;
    color: var(--hbl-hero-fg);
    max-width: 18ch;
    text-wrap: balance;
    text-shadow: var(--hbl-hero-text-shadow);
    margin-bottom: 2rem;
}

/* Words the tenant wrapped in *asterisks*. */
.hbl-inspire__heading-accent {
    color: var(--hbl-primary);
}

/* The two groups are spaced by the inner's gap now, so the trailing margin the
   subtitle used to carry would only push the intro block taller. */
.hbl-inspire__subtitle {
    color: var(--hbl-hero-fg-muted);
    font-size: clamp(1.1rem, 1.4vw, 1.5rem);
    max-width: 36rem;
    margin-bottom: 0;
}

.hbl-inspire__look-name {
    font-family: var(--hbl-font-serif);
    font-size: clamp(1.35rem, 2.4vw, 1.75rem);
    font-weight: 700;
    color: var(--hbl-hero-fg);
    margin-bottom: 0.25rem;
}

/* The featured design's note. Free text from the design record, so it wraps and
   is width-capped rather than assumed short. */
.hbl-inspire__look-note {
    color: var(--hbl-hero-fg-muted);
    font-size: clamp(0.9rem, 1.2vw, 1rem);
    max-width: 34rem;
    margin: 0 0 0.85rem;
}

/* Collapse for looks with no note, so the name sits straight above the tags. */
.hbl-inspire__look-note:empty {
    display: none;
}

.hbl-inspire__tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin: 0 0 1.1rem;
    padding: 0;
}

/* The list is always rendered so the gallery can fill it — collapse it when the
   selected look has no tags, otherwise its margin leaves a gap under the name. */
.hbl-inspire__tags:empty {
    display: none;
}

.hbl-inspire__tag {
    display: inline-flex;
    align-items: center;
    background: color-mix(in srgb, var(--hbl-hero-overlay) 55%, transparent);
    border: 1px solid color-mix(in srgb, var(--hbl-hero-fg) 30%, transparent);
    border-radius: 4px;
    color: var(--hbl-hero-fg);
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    gap: 0.35rem;
    padding: 0.2rem 0.5rem;
}

/* On the --plain (no-photo) hero the fg is dark, so the chip backdrop has to
   lighten with it or the label would sit dark-on-dark. */
.hbl-inspire__hero--plain .hbl-inspire__tag {
    background: color-mix(in srgb, var(--hbl-bg) 85%, transparent);
}

/* Color swatches shown ahead of the tag label when the look defines them */
.hbl-inspire__tag-dots {
    display: inline-flex;
    align-items: center;
    gap: 0.15rem;
}

.hbl-inspire__tag-dot {
    display: block;
    width: 0.55rem;
    height: 0.55rem;
    border-radius: 50%;
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--hbl-hero-fg) 30%, transparent);
}

.hbl-inspire__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
}

.hbl-inspire__cta {
    font-size: 1rem;
    padding: 0.7rem 1.5rem;
}

/* The stock ghost button is muted-on-light and would vanish over the dark
   scrim, so the hero's secondary CTA runs on the hero foreground instead. */
.hbl-inspire__cta--secondary {
    /* Restate the pressed colors too: .btn-hbl-ghost's are muted-on-light and
       would drop out against the photo the moment the button is held down. */
    --bs-btn-active-color: var(--hbl-hero-fg);
    --bs-btn-active-bg: color-mix(in srgb, var(--hbl-hero-fg) 12%, transparent);
    --bs-btn-active-border-color: color-mix(in srgb, var(--hbl-hero-fg) 75%, transparent);

    color: var(--hbl-hero-fg);
    border-color: color-mix(in srgb, var(--hbl-hero-fg) 45%, transparent);
}

.hbl-inspire__cta--secondary:hover,
.hbl-inspire__cta--secondary:focus {
    color: var(--hbl-hero-fg);
    border-color: color-mix(in srgb, var(--hbl-hero-fg) 75%, transparent);
    background-color: color-mix(in srgb, var(--hbl-hero-fg) 12%, transparent);
}

/* Carousel card overlapping the hero bottom.
   Most of the inner spacing lives on the track rather than here — see its
   padding note. The deck keeps just enough to clear its own rounded border. */
.hbl-inspire__deck {
    position: relative;
    margin-top: -6rem;
    background: color-mix(in srgb, var(--hbl-surface) 70%, var(--hbl-bg));
    border: 1px solid var(--hbl-hairline-soft);
    border-radius: var(--hbl-radius-lg);
    box-shadow: var(--hbl-shadow);
    padding: 0.25rem 0.5rem;
}

.hbl-inspire__track {
    display: flex;
    gap: 1rem;
    overflow-x: auto;
    /* Touch drags pan the track natively — the cards move under the finger and
       the snap below settles one on centre. Reaching either end must not hand
       the drag on to the page's own horizontal gestures (iOS back/forward). */
    overscroll-behavior-x: contain;
    /* proximity + centre, matching .hbl-recent__track: the arrows scroll this track
       programmatically to centre the selected look, and mandatory/start snapping
       would pull it back to the leading edge. */
    scroll-snap-type: x proximity;
    scroll-behavior: smooth;
    /* overflow-x: auto makes overflow-y auto too, so this padding is the only
       room the cards' ambient glow and hover lift have before the scrollport
       clips them — it has to clear the largest shadow (0 6px 24px on hover)
       plus the 1.03 scale, not just the 3px lift. */
    padding: 1.75rem 1.5rem;
    margin: 0;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.hbl-inspire__track::-webkit-scrollbar { display: none; width: 0; height: 0; }

.hbl-look-card {
    flex: 0 0 auto;
    width: clamp(230px, 16vw, 250px);
    scroll-snap-align: center;
}

/* A <button>, not a link — selecting a look repaints the hero instead of
   navigating. Reset the UA button styling; .hbl-card supplies border + shadow. */
.hbl-look-card__link {
    display: flex;
    flex-direction: column;
    width: 100%;
    /* The cards are stretch-aligned flex items, so the li's already share the
       tallest card's height — this passes that height down to the card itself
       so a one-line note doesn't leave a shorter card next to a two-line one. */
    height: 100%;
    padding: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
    border-radius: var(--hbl-radius-sm);
    overflow: hidden;
    text-decoration: none;
    transition: transform var(--hbl-transition), box-shadow var(--hbl-transition);
}

.hbl-look-card__link:hover,
.hbl-look-card__link:focus-visible {
    transform: translateY(-3px) scale(1.03);
    box-shadow: var(--hbl-shadow);
}

.hbl-look-card__link:focus-visible {
    outline: 2px solid var(--hbl-primary);
    outline-offset: 2px;
}

/* The look currently shown in the hero */
.hbl-look-card__link[aria-pressed="true"] {
    outline: 2px solid var(--hbl-primary);
    outline-offset: -2px;
}

.hbl-look-card__link[aria-pressed="true"] .hbl-look-card__caption {
    background: var(--hbl-primary);
    color: var(--hbl-primary-contrast);
}

.hbl-look-card__link[aria-pressed="true"] .hbl-look-card__subtitle {
    color: color-mix(in srgb, var(--hbl-primary-contrast) 85%, transparent);
}

/* Neutral, deliberately untinted: these are sky-removed PNGs, so whatever sits
   here shows through the transparent sky as if it were the weather. A brand
   tint reads as a colour cast over the house rather than a card background. */
.hbl-look-card__media {
    display: block;
    aspect-ratio: 16 / 9;
    background-color: var(--hbl-surface);
    background-size: cover;
    background-position: center;
}

.hbl-look-card__media--placeholder {
    display: grid;
    place-items: center;
    color: color-mix(in srgb, var(--hbl-primary) 45%, transparent);
}

.hbl-look-card__media--placeholder .hbl-icon {
    width: 2.5rem;
    height: 2.5rem;
}

.hbl-look-card__caption {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    padding: 0.6rem 0.75rem 0.7rem;
    background: var(--hbl-ink);
    color: var(--hbl-on-ink);
    /* Absorbs the slack on a card stretched to match a taller neighbour, so the
       caption fill reaches the bottom edge instead of stopping mid-card. */
    flex: 1;
}

/* Both caption lines are tenant data of unknown length, so each is capped at two
   lines and ellipsised. Without this a wordy name or note sets the height of
   every card in the row. */
.hbl-look-card__name,
.hbl-look-card__subtitle {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
}

.hbl-look-card__name {
    font-family: var(--hbl-font-serif);
    font-weight: 700;
    font-size: 0.95rem;
    line-height: 1.25;
    margin-bottom: 0.25rem;
}

.hbl-look-card__subtitle {
    font-size: 0.78rem;
    color: var(--hbl-on-ink-muted);
}

/* Empty-state gallery: ghost cards hold the deck's shape until the tenant has
   demo designs to show. Same anatomy as a real card, drained of affordance —
   no hover, no press state, everything washed toward the surface. */
.hbl-look-card--ghost {
    display: flex;
    flex-direction: column;
    border-radius: var(--hbl-radius-sm);
    overflow: hidden;
    border: 1px dashed var(--hbl-hairline-strong);
    opacity: 0.75;
}

.hbl-look-card--ghost .hbl-look-card__media {
    background-color: color-mix(in srgb, var(--hbl-primary) 6%, var(--hbl-surface));
}

.hbl-look-card--ghost .hbl-look-card__media--placeholder {
    color: color-mix(in srgb, var(--hbl-primary) 25%, transparent);
}

.hbl-look-card--ghost .hbl-look-card__caption {
    background: color-mix(in srgb, var(--hbl-surface) 80%, var(--hbl-bg));
    color: var(--hbl-muted);
}

.hbl-look-card--ghost .hbl-look-card__subtitle {
    color: color-mix(in srgb, var(--hbl-muted) 75%, transparent);
}

/* The one visible (non-aria-hidden) line of the empty state. */
.hbl-inspire__ghost-note {
    margin: 0.25rem 0 0;
    text-align: center;
    font-family: var(--hbl-font-serif);
    font-style: italic;
    color: var(--hbl-muted);
}

/* Edge fade shared by both carousels — cards dissolve as they run off either end
   of the track instead of being cut off by a hard overflow edge.
   home-dashboard.js writes --hbl-scrolled-start/-end as the pixel distance still
   scrollable in each direction; min() caps each at --hbl-track-fade. So the fade
   grows in as you scroll away from an edge and is fully absent when there is
   nothing off-screen on that side — no pop at the extremes, and no fade at all on
   a track short enough not to scroll. The prev/next buttons sit outside the track,
   so they stay fully opaque over the faded area. */
.hbl-inspire__track,
.hbl-recent__track,
.hbl-recent__ghosts {
    --hbl-track-fade: 5rem;
    --hbl-scrolled-start: 0px;
    --hbl-scrolled-end: 0px;

    -webkit-mask-image: linear-gradient(to right,
        transparent 0,
        #000 min(var(--hbl-scrolled-start), var(--hbl-track-fade)),
        #000 calc(100% - min(var(--hbl-scrolled-end), var(--hbl-track-fade))),
        transparent 100%);
    mask-image: linear-gradient(to right,
        transparent 0,
        #000 min(var(--hbl-scrolled-start), var(--hbl-track-fade)),
        #000 calc(100% - min(var(--hbl-scrolled-end), var(--hbl-track-fade))),
        transparent 100%);
}

/* Circular prev/next buttons shared by both carousels */
.hbl-scroller-btn {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 50%;
    border: 1px solid var(--hbl-hairline);
    background: var(--hbl-bg);
    color: var(--hbl-text);
    display: grid;
    place-items: center;
    font-size: 1.1rem;
    box-shadow: var(--hbl-shadow-sm);
    cursor: pointer;
    z-index: 2;
    transition: background-color var(--hbl-transition), color var(--hbl-transition), opacity var(--hbl-transition);
}

    .hbl-scroller-btn:hover,
    .hbl-scroller-btn:focus {
        background: var(--hbl-bg);
        color: var(--hbl-text);
    }

/* At an end there is nothing to go back to, so the button goes rather than
   dimming — a ghosted arrow still reads as a control worth pressing. */
.hbl-scroller-btn:disabled {
    opacity: 0;
    pointer-events: none;
    cursor: default;
}

/* The buttons never sit over the cards: they ride the deck's own edge, and step
   further out as the page gutter can afford it. --hbl-scroller-inset is the
   offset past that edge (negative = outside), shared by both decks.

   How far out we can go is bounded by the page gutter, since the deck spans the
   container and anything past the gutter would overflow the viewport. The
   gutters (see .container-xxl) are 1rem / 1.5rem / 2.5rem / 3.5rem / 2rem, so:
     below 768px — the gutter is smaller than a half-button, so the overlap stays
       shallow and the button leans into the deck rather than off the page;
     768px+ — half the 2.5rem button, centring it on the deck edge;
     1450px+ — the full width plus a gap, clear of the deck in the page margin. */
.hbl-inspire__deck,
.hbl-recent__deck {
    --hbl-scroller-inset: -0.5rem;
}

@media (min-width: 768px) {
    .hbl-inspire__deck,
    .hbl-recent__deck { --hbl-scroller-inset: -1.25rem; }
}

@media (min-width: 1450px) {
    .hbl-inspire__deck,
    .hbl-recent__deck { --hbl-scroller-inset: -3.25rem; }
}

.hbl-scroller-btn--prev { left: var(--hbl-scroller-inset, -1.25rem); }
.hbl-scroller-btn--next { right: var(--hbl-scroller-inset, -1.25rem); }

/* =========================================================================
   Get started (upload section shell) + stepper band
   ========================================================================= */

.hbl-getstarted {
    padding-top: 1rem;
}

.hbl-getstarted--page {
    padding-top: 2rem;
}

.hbl-getstarted__band {
    background: color-mix(in srgb, var(--hbl-surface) 70%, var(--hbl-bg));
    padding-bottom: 2.5rem;
}

.hbl-getstarted__back {
    margin-top: 1.5rem;
    text-align: left;
}

.hbl-stepband {
    padding: 1.5rem 0 1.75rem;
}

.hbl-stepband__list {
    list-style: none;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: clamp(0.75rem, 3vw, 2rem);
    margin: 0;
    padding: 0;
}

.hbl-stepband__step {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}

.hbl-stepband__circle {
    position: relative;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: color-mix(in srgb, var(--hbl-muted) 22%, var(--hbl-bg));
    color: var(--hbl-muted);
    font-size: 1.3rem;
    flex: 0 0 auto;
}

.hbl-stepband__circle .hbl-icon {
    width: 1.4rem;
    height: 1.4rem;
    vertical-align: middle;
}

.hbl-stepband__label {
    font-family: var(--hbl-font-serif);
    font-weight: 700;
    font-size: 1.05rem;
    color: var(--hbl-muted);
}

.hbl-stepband__step.is-current .hbl-stepband__circle {
    background: var(--hbl-primary);
    color: var(--hbl-primary-contrast);
}

.hbl-stepband__step.is-current .hbl-stepband__label {
    color: var(--hbl-text);
}

.hbl-stepband__step.is-done .hbl-stepband__circle {
    background: var(--hbl-secondary);
    color: var(--hbl-secondary-contrast);
}

.hbl-stepband__step.is-done .hbl-stepband__label {
    color: var(--hbl-text);
}

/* Green check badge — only visible on completed steps */
.hbl-stepband__badge {
    display: none;
    position: absolute;
    top: -0.3rem;
    right: -0.3rem;
    font-size: 1.05rem;
    line-height: 1;
    color: var(--hbl-success);
    background: var(--hbl-bg);
    border-radius: 50%;
    border: 1px solid var(--hbl-bg);
}

.hbl-stepband__badge i {
    vertical-align: middle;
}


.hbl-stepband__step.is-done .hbl-stepband__badge { display: block; }


.hbl-stepband__arrow {
    align-self: center;
    color: var(--hbl-muted);
    font-size: 1.1rem;
    display: grid;
    place-items: center;
}

@media (max-width: 575.98px) {
    .hbl-stepband__step { flex-direction: column; gap: 0.35rem; text-align: center; }
    .hbl-stepband__circle { width: 2.25rem; height: 2.25rem; font-size: 1.05rem; }
    .hbl-stepband__label { font-size: 0.85rem; }
    .hbl-stepband__arrow { align-self: flex-start; margin-top: 0.6rem; }
}

/* =========================================================================
   Upload panel (shared by Home + Design/Create)
   ========================================================================= */

.hbl-upload-panel__columns {
    align-items: stretch;
}

/* Left column is a flex stack: the dropzone absorbs whatever height is left
   beside the coach card and the action bar sits directly under it, matching
   the layout on Design/Review. */
.hbl-upload-panel__main {
    display: flex;
    flex-direction: column;
}

.hbl-upload-panel__main .hbl-dropzone {
    height: auto;
    flex: 1 1 auto;
}

/* Tips hug their own content. The dropzone still stretches down to meet them
   when they're the taller side, but once the left column outgrows the card
   (photos added), the card stays tucked up top instead of running down. */
.hbl-upload-panel__aside {
    align-self: flex-start;
}

.hbl-upload-panel__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem 1rem;
    margin-top: 1.75rem;
}

.hbl-upload-panel__actions .btn-hbl-primary { margin-left: auto; }

@media (max-width: 575.98px) {
    .hbl-upload-panel__actions { justify-content: center; }
    .hbl-upload-panel__actions .btn-hbl-primary { margin-left: 0; }
}

.hbl-upload-panel__or {
    color: var(--hbl-muted);
    font-size: 0.9rem;
}

.hbl-upload-panel__status {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--hbl-muted);
    font-size: 0.9rem;
}

.hbl-upload-panel__status i { font-size: 1rem; }
.hbl-upload-panel__status i.text-success { color: var(--hbl-success) !important; }

/* ============================ Dropzone ============================ */

.hbl-dropzone {
    display: block;
    position: relative;
    width: 100%;
    height: 100%;
    background: var(--hbl-bg);
    border: 2px dashed color-mix(in srgb, var(--hbl-muted) 45%, transparent);
    border-radius: var(--hbl-radius);
    padding: 2rem 1.25rem;
    text-align: center;
    cursor: pointer;
    transition: border-color var(--hbl-transition), background-color var(--hbl-transition), transform var(--hbl-transition), padding var(--hbl-transition);
}

.hbl-dropzone.is-filled { padding: 1.25rem; }

@media (min-width: 768px) {
    .hbl-dropzone {
        padding: 3rem 2rem;
        display: flex;
        flex-direction: column;
        justify-content: center;
    }

    .hbl-dropzone.is-filled { padding: 1.25rem 1.5rem; }
}

/* Hover stays neutral — the dashed edge darkens and the fill warms to a grey
   wash. Brand color is saved for .is-dragover, so "you can drop here" and
   "you are dropping now" don't look like the same state. */
.hbl-dropzone:hover,
.hbl-dropzone:focus-within {
    border-color: color-mix(in srgb, var(--hbl-muted) 85%, transparent);
    background: color-mix(in srgb, var(--hbl-surface) 20%, var(--hbl-bg));
}

.hbl-dropzone.is-dragover {
    border-color: var(--hbl-primary);
    background: color-mix(in srgb, var(--hbl-primary) 7%, var(--hbl-bg));
    transform: scale(1.005);
}

.hbl-dropzone__input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
    z-index: 1;
}

.hbl-dropzone__inner,
.hbl-dropzone__filled { position: relative; z-index: 2; }

.hbl-dropzone__icon {
    margin: 0 auto 0.75rem;
    display: grid;
    place-items: center;
    color: var(--hbl-text);
    font-size: 2.5rem;
    line-height: 1;
}

.hbl-dropzone__title {
    font-family: var(--hbl-font-sans);
    font-size: 1.15rem;
    font-weight: 700;
    margin-bottom: 0.25rem;
}

.hbl-dropzone__sub {
    color: var(--hbl-muted);
    margin-bottom: 0.75rem;
}

.hbl-dropzone__browse {
    pointer-events: none; /* the label/input handles clicks */
    margin: 0 auto 0.75rem;
}

.hbl-dropzone__meta {
    color: var(--hbl-muted);
    font-size: 0.85rem;
    margin-bottom: 0;
}

/* Compact state: a single row that echoes the empty prompt's visual language —
   same icon treatment, same bold title type, same primary pill button. */
.hbl-dropzone__filled-header {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}

.hbl-dropzone__filled-count {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    font-family: var(--hbl-font-sans);
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--hbl-text);
}

.hbl-dropzone__filled-count i { font-size: 1.5rem; line-height: 1; }

.hbl-dropzone__add-more {
    padding: 0.4rem 1rem;
    font-size: 0.875rem;
    position: relative;
    z-index: 3; /* above the full-bleed invisible file input */
}

/* Cross-fade the empty/filled swap: the entering state animates in each time
   d-none is removed (display change restarts the animation — no JS needed). */
@keyframes hbl-dropzone-state-in {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: none; }
}

.hbl-dropzone__inner,
.hbl-dropzone__filled { animation: hbl-dropzone-state-in var(--hbl-transition) both; }

@media (prefers-reduced-motion: reduce) {
    .hbl-dropzone__inner,
    .hbl-dropzone__filled { animation: none; }
}

/* =========================================================================
   Lighting coach tip card
   ========================================================================= */

.hbl-tip-stack { display: grid; gap: 1rem; }

.hbl-tip-card {
    background: var(--hbl-bg);
    border-radius: var(--hbl-radius);
    overflow: hidden;
}

/* Tinted band across the top of the card, holding just the coach's face and
   the card title — everything else lives on the white body below. */
.hbl-tip-card__coach {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    padding: 0.9rem 1.1rem;
    background: color-mix(in srgb, var(--hbl-muted) 3%, var(--hbl-bg));
    border-bottom: 1px solid var(--hbl-hairline);
}

.hbl-tip-card__avatar {
    width: 3rem;
    height: 3rem;
    border-radius: 50%;
    background: var(--hbl-bg);
    color: var(--hbl-primary);
    display: grid;
    place-items: center;
    font-size: 1.75rem;
    flex: 0 0 auto;
    overflow: hidden;
    border: 2px solid var(--hbl-bg);
    box-shadow: 0 1px 3px color-mix(in srgb, var(--hbl-text) 18%, transparent);
}

.hbl-tip-card__avatar-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.hbl-tip-card__title {
    font-family: var(--hbl-font-sans);
    font-size: 1.05rem;
    font-weight: 700;
    margin-bottom: 0;
    min-width: 0;
}

.hbl-tip-card__coach .hbl-tip-card__title { font-size: 1.3rem; }

.hbl-tip-card__bubble {
    background: color-mix(in srgb, var(--hbl-primary) 5%, var(--hbl-surface));
    border-radius: var(--hbl-radius-sm);
    padding: 0.6rem 0.8rem;
    color: var(--hbl-text);
    font-size: 0.9rem;
    margin: 0.9rem 1.1rem 0;
}

.hbl-tip-card__figure {
    position: relative;
    margin: 0;
    padding: 0.9rem 1.1rem 0;
}

.hbl-tip-card__figure-heading {
    font-family: var(--hbl-font-sans);
    font-weight: 700;
    color: var(--hbl-text);
    font-size: 0.95rem;
    margin-bottom: 0.6rem;
}

.hbl-tip-card__image {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    background: var(--hbl-surface);
    border-radius: var(--hbl-radius-sm);
}

.hbl-tip-card__list {
    list-style: none;
    padding: 0.9rem 1.1rem;
    margin: 0;
    display: grid;
    gap: 0.4rem;
}

.hbl-tip-card__list + .hbl-tip-card__list { border-top: 1px solid var(--hbl-hairline-soft); }

.hbl-tip-card__list li {
    display: grid;
    grid-template-columns: 1.15rem 1fr;
    gap: 0.55rem;
    align-items: start;
    color: var(--hbl-muted);
    font-size: 0.92rem;
}

/* Do/avoid markers are solid discs with a white glyph knocked out of them,
   so the tint carries the meaning rather than the glyph's stroke color. */
.hbl-tip-card__marker {
    width: 1.15rem;
    height: 1.15rem;
    border-radius: 50%;
    display: grid;
    place-items: center;
    font-size: 0.7rem;
    line-height: 1;
    color: var(--hbl-bg);
    margin-top: 0.15rem;
}

.hbl-tip-card__list--dos .hbl-tip-card__marker   { background: var(--hbl-success); }
.hbl-tip-card__list--donts .hbl-tip-card__marker { background: color-mix(in srgb, var(--hbl-muted) 45%, var(--hbl-bg)); }

.hbl-tip-card__list .hbl-tip-card__list-heading {
    display: block;
    grid-template-columns: none;
    font-family: var(--hbl-font-sans);
    font-weight: 700;
    color: var(--hbl-text);
    font-size: 0.95rem;
    margin-bottom: 0.15rem;
}

/* =========================================================================
   "Your Recent Designs" carousel
   ========================================================================= */

.hbl-recent {
    padding-block: 2.5rem 1.5rem;
}

.hbl-recent .hbl-section-title { margin-bottom: 1.5rem; }

.hbl-recent__deck {
    position: relative;
    padding: 0.5rem 0;

    /* On the deck rather than the track: the ghost row that stands in for an empty
       deck is a sibling of the track and has to size its cards identically. */
    --hbl-design-card-w: clamp(180px, 20vw, 240px);
    --hbl-design-card-featured-w: clamp(200px, 22vw, 270px);
    /* Caption box: name line + hint line + the caption's own padding. Not derivable
       in CSS, so it is stated here and deliberately rounded up — slack shows as a
       little breathing room around a vertically centred row, whereas a value that
       is too small lets the row move again. */
    --hbl-design-card-caption-h: 5rem;

    /* The tallest state the row can reach, including a track's own 1rem vertical
       padding. Both the track and the ghost row claim it, so neither the featured
       card's growth animation nor the swap from ghosts to real cards moves the
       page. */
    --hbl-recent-row-h: calc(var(--hbl-design-card-featured-w) * 3 / 4
                             + var(--hbl-design-card-caption-h)
                             + 2rem);
}

.hbl-recent__track {
    min-height: var(--hbl-recent-row-h);

    display: flex;
    align-items: center;
    gap: 1.25rem;
    overflow-x: auto;
    /* Touch drags pan the track natively; once a hand scroll settles,
       home-dashboard.js features whichever card landed nearest the centre.
       Reaching either end must not hand the drag on to the page's own
       horizontal gestures (iOS back/forward). */
    overscroll-behavior-x: contain;
    /* proximity, not mandatory: the arrows scroll this track programmatically, and
       when it is only slightly wider than its viewport the later cards' snap points
       sit past max scroll. Mandatory snapping has to resolve to a reachable snap
       point, which can pull the track back off the card just featured. proximity
       leaves the clamped position alone while still snapping hand scrolls. */
    scroll-snap-type: x proximity;
    scroll-behavior: smooth;
    padding: 1rem 0.25rem;
    margin: 0;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.hbl-recent__track::-webkit-scrollbar { display: none; width: 0; height: 0; }

.hbl-design-card {
    flex: 0 0 auto;
    width: var(--hbl-design-card-w);
    /* centre, to agree with where the arrows scroll the featured card — a `start`
       snap point would pull it back to the track's leading edge. */
    scroll-snap-align: center;
    /* Grows/shrinks as the arrows move which card is featured. The script scrolls
       from cached card metrics rather than live measurements, so this animating
       does not throw off where it lands. */
    transition: width var(--hbl-transition);
}

.hbl-design-card__link {
    display: flex;
    flex-direction: column;
    border-radius: var(--hbl-radius);
    overflow: hidden;
    text-decoration: none;
    background: var(--hbl-bg);
    transition: transform var(--hbl-transition), box-shadow var(--hbl-transition);
}

.hbl-design-card__link:hover,
.hbl-design-card__link:focus-visible {
    transform: translateY(-3px) scale(1.03);
    box-shadow: var(--hbl-shadow);
}

.hbl-design-card__media {
    display: block;
    /* The featured card scales in both directions. The row is kept from moving by
       the min-height reserved on .hbl-recent__track, not by pinning this. */
    aspect-ratio: 4 / 3;
    background-color: color-mix(in srgb, var(--hbl-muted) 12%, var(--hbl-bg));
    background-size: cover;
    background-position: center;
}

.hbl-design-card__media--placeholder {
    display: grid;
    place-items: center;
    font-size: 2.25rem;
    color: color-mix(in srgb, var(--hbl-muted) 45%, transparent);
}

.hbl-design-card__caption {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding: 0.6rem 0.8rem 0.7rem;
    background: color-mix(in srgb, var(--hbl-primary) 82%, var(--hbl-bg));
    color: var(--hbl-primary-contrast);
    /* Deepens to full primary when the card becomes the featured one. */
    transition: background-color var(--hbl-transition);
}

.hbl-design-card__name {
    font-family: var(--hbl-font-serif);
    font-weight: 700;
    font-size: 1rem;
    line-height: 1.25;
}

/* Rendered on every card but revealed only on the featured one, so the arrows can
   move which card is featured without the server re-rendering the deck.
   Hidden with opacity + visibility rather than display:none for two reasons: it
   can be animated, and it keeps occupying its line so every caption is the same
   height whichever card is featured. visibility is what keeps the text out of the
   accessibility tree on the cards that aren't featured — it transitions
   stepwise, so it flips to visible immediately on the way in and holds until the
   fade finishes on the way out. */
.hbl-design-card__hint {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    font-size: 0.8rem;
    color: color-mix(in srgb, var(--hbl-primary-contrast) 90%, transparent);
    opacity: 0;
    visibility: hidden;
    transform: translateY(0.2rem);
    transition: opacity var(--hbl-transition),
                transform var(--hbl-transition),
                visibility var(--hbl-transition);
}

/* Revealed on the featured card, and on any card being hovered or keyboard
   focused. The space is always reserved, so this never reflows the caption. */
.hbl-design-card.is-featured .hbl-design-card__hint,
.hbl-design-card__link:hover .hbl-design-card__hint,
.hbl-design-card__link:focus-visible .hbl-design-card__hint {
    opacity: 1;
    visibility: visible;
    transform: none;
}

/* The selected card gets the spotlight — scales in both directions, with the
   track's min-height absorbing the change so the page doesn't move. */
.hbl-design-card.is-featured {
    width: var(--hbl-design-card-featured-w);
}

.hbl-design-card.is-featured .hbl-design-card__link {
    box-shadow: var(--hbl-shadow);
}

.hbl-design-card.is-featured .hbl-design-card__caption {
    background: var(--hbl-primary);
}

/* ---------- Empty deck ----------
   Before a visitor has any designs the deck shows a row of drained cards instead
   of the track, with the message in the middle one. Same geometry, no content —
   so the section reads as "your designs will live here" rather than as a carousel
   that failed to load. The row is decoration apart from that message. */
.hbl-recent.is-empty .hbl-recent__track,
.hbl-recent.is-empty .hbl-scroller-btn {
    display: none;
}

.hbl-recent:not(.is-empty) .hbl-recent__ghosts {
    display: none;
}

.hbl-recent__ghosts {
    display: flex;
    align-items: center;
    /* Centres the message card and lets the plain ghosts run off both edges
       symmetrically — the row is meant to read as wider than the page. */
    justify-content: center;
    gap: 1.25rem;
    min-height: var(--hbl-recent-row-h);
    padding: 1rem 0.25rem;
    margin: 0;
    /* Decorative overflow: nothing out there is reachable or worth reaching. */
    overflow: hidden;

    /* Reuse the carousels' edge fade at a fixed width. min() caps both sides at
       --hbl-track-fade, so anything past it holds the fade steady. */
    --hbl-scrolled-start: 999px;
    --hbl-scrolled-end: 999px;
}

.hbl-recent__ghost {
    flex: 0 0 auto;
    width: var(--hbl-design-card-w);
    aspect-ratio: 4 / 3;
    border-radius: var(--hbl-radius);
    background: color-mix(in srgb, var(--hbl-muted) 12%, var(--hbl-bg));
}

/* The one ghost with something to say takes the featured card's width and a
   little of its prominence. Height comes from its content rather than the 4:3
   ratio, so the copy can breathe without being clipped. */
.hbl-recent__ghost--message {
    width: var(--hbl-design-card-featured-w);
    aspect-ratio: auto;
    min-height: calc(var(--hbl-design-card-featured-w) * 3 / 4 + 2rem);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    padding: 1.25rem 1.1rem;
    text-align: center;
    background: color-mix(in srgb, var(--hbl-muted) 18%, var(--hbl-bg));
    box-shadow: var(--hbl-shadow-sm);
}

.hbl-recent__ghost--message .hbl-icon {
    width: 2rem;
    height: 2rem;
    color: color-mix(in srgb, var(--hbl-primary) 55%, var(--hbl-muted));
    margin-bottom: 0.1rem;
}

.hbl-recent__empty-title {
    font-family: var(--hbl-font-serif);
    font-weight: 700;
    font-size: 1.05rem;
    line-height: 1.2;
    color: var(--hbl-text);
}

.hbl-recent__empty-sub {
    font-size: 0.82rem;
    line-height: 1.4;
    color: var(--hbl-muted);
}

.hbl-recent__cta {
    text-align: center;
    margin-top: 1.5rem;
}

/* One call to action at a time. Only a togglable row has both to choose between —
   a configuration-driven deck keeps its create link in every state. */
.hbl-recent:not(.is-empty) .hbl-recent__cta--togglable .hbl-recent__cta-create,
.hbl-recent.is-empty .hbl-recent__cta-clear {
    display: none;
}

/* Asking before clearing takes the button's place rather than sitting beside it:
   the thing being confirmed can't be pressed again while the question is up, and
   the row keeps its height instead of pushing the page down. */
.hbl-recent__confirm {
    display: none;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0.6rem;
}

.hbl-recent__cta.is-confirming .hbl-recent__cta-clear { display: none; }
.hbl-recent__cta.is-confirming .hbl-recent__confirm { display: flex; }

.hbl-recent__confirm-text {
    font-family: var(--hbl-font-sans);
    font-weight: 500;
    font-size: 1rem;
    color: var(--hbl-text);
}

/* Animated rather than transitioned: the row goes from display:none to display:flex,
   which a transition can't start from. An animation plays as soon as the element is
   displayed, so no double-frame dance is needed. Show only — cancelling snaps back,
   because an exit animation on a question that was just dismissed reads as lag. */
.hbl-recent__cta.is-confirming .hbl-recent__confirm > * {
    animation: hbl-confirm-in var(--hbl-confirm-in, 220ms) cubic-bezier(.2, .7, .3, 1);
}

/* The stagger is longer durations rather than animation-delay: holding the opening
   frames inside the keyframes gets the same cascade without needing
   fill-mode: backwards to keep a delayed item hidden until its turn. One less
   moving part, and every item is governed by the same timeline. */
.hbl-recent__cta.is-confirming .hbl-recent__confirm > :nth-child(2) { animation-duration: calc(var(--hbl-confirm-in, 220ms) * 1.3); }
.hbl-recent__cta.is-confirming .hbl-recent__confirm > :nth-child(3) { animation-duration: calc(var(--hbl-confirm-in, 220ms) * 1.6); }

@keyframes hbl-confirm-in {
    /* The held opening is what staggers the row: same start, later arrivals. */
    0%, 35% {
        opacity: 0;
        transform: translateY(0.4rem);
    }

    100% {
        opacity: 1;
        transform: none;
    }
}

/* =========================================================================
   Design / Review — crop step
   ========================================================================= */

.hbl-review {
    background: color-mix(in srgb, var(--hbl-surface) 70%, var(--hbl-bg));
    padding: 1.5rem 0 3rem;
    margin-top: -0.5rem;
}

.hbl-review__title {
    font-family: var(--hbl-font-serif);
    font-size: clamp(1.6rem, 2.8vw, 2.1rem);
    margin-bottom: 0.6rem;
    color: var(--hbl-text);
}

.hbl-review__sub {
    color: var(--hbl-muted);
    max-width: 42rem;
    margin-bottom: 1.5rem;
}

.hbl-review__design-name {
    max-width: 34rem;
    margin-bottom: 1.5rem;
}

.hbl-field-label {
    display: block;
    font-family: var(--hbl-font-sans);
    font-weight: 700;
    font-size: 0.95rem;
    color: var(--hbl-text);
    margin-bottom: 0.35rem;
}

/* Tips hug their own content — a long photo list on the left must never
   stretch the coach card down the page. */
.hbl-review__aside {
    align-self: flex-start;
}

/* Back on the left, forward on the right, pinned to the panel's edges. */
.hbl-review__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem 1rem;
    margin-top: 1.75rem;
}

/* Too narrow to sit at opposite edges — center whatever wraps. */
@media (max-width: 575.98px) {
    .hbl-review__actions { justify-content: center; }
}

.hbl-review-list {
    display: grid;
    gap: 1.25rem;
    margin: 0;
}

.hbl-review-item {
    background: var(--hbl-bg);
    border-radius: var(--hbl-radius);
    padding: 1rem;
}

@media (min-width: 768px) {
    .hbl-review-item { padding: 1.25rem 1.5rem 1.5rem; }
}

.hbl-review-item__header {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 1rem;
}

.hbl-review-item__num {
    width: 1.9rem;
    height: 1.9rem;
    border-radius: 999px;
    background: var(--hbl-primary);
    color: var(--hbl-primary-contrast);
    display: grid;
    place-items: center;
    font-weight: 700;
    font-size: 0.9rem;
    flex: 0 0 auto;
}

.hbl-review-item__heading { min-width: 0; }

.hbl-review-item__title {
    font-family: var(--hbl-font-sans);
    font-size: 1.05rem;
    font-weight: 700;
    margin: 0;
    color: var(--hbl-text);
}

.hbl-review-item__filename {
    margin: 0;
    color: var(--hbl-muted);
    font-size: 0.82rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hbl-review-item__fields {
    margin-bottom: 1rem;
}

.hbl-review-item__body {
    margin-bottom: 0.75rem;
}

.hbl-review-item__hint {
    margin: 0;
    color: var(--hbl-muted);
    font-size: 0.82rem;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

.hbl-review-item__hint i { color: inherit; line-height: 1; margin-top: -2px; }

/* ---------- Crop overlay ---------- */

.hbl-crop {
    position: relative;
    background: var(--hbl-ink);
    border-radius: var(--hbl-radius-sm);
    overflow: hidden;
    box-shadow: var(--hbl-shadow-sm);
}

.hbl-crop__stage {
    position: relative;
    width: 100%;
    /* Limit the stage so very tall photos don't blow out the panel. */
    max-height: 70vh;
    display: flex;
    justify-content: center;
    align-items: center;
    overflow: hidden;
}

/* Fill the panel width — a photo you're framing shouldn't be shrunk by the
   height cap. Very tall photos letterbox inside the box instead (the crop
   script measures the rendered picture, not the element box). */
.hbl-crop__image {
    display: block;
    width: 100%;
    max-width: 100%;
    height: auto;
    max-height: 70vh;
    object-fit: contain;
    user-select: none;
    -webkit-user-drag: none;
    pointer-events: none;
}

.hbl-crop__mask {
    position: absolute;
    background: var(--hbl-scrim);
    pointer-events: none;
}

.hbl-crop__frame {
    position: absolute;
    border: 2px solid var(--hbl-primary-contrast);
    box-shadow: 0 0 0 1px var(--hbl-primary), 0 6px 18px color-mix(in srgb, var(--hbl-text) 25%, transparent);
    cursor: move;
    touch-action: none;
    background:
        linear-gradient(transparent calc(33.333% - 0.5px), var(--hbl-scrim-light) calc(33.333% - 0.5px) calc(33.333% + 0.5px), transparent calc(33.333% + 0.5px)) no-repeat,
        linear-gradient(transparent calc(66.666% - 0.5px), var(--hbl-scrim-light) calc(66.666% - 0.5px) calc(66.666% + 0.5px), transparent calc(66.666% + 0.5px)) no-repeat,
        linear-gradient(90deg, transparent calc(33.333% - 0.5px), var(--hbl-scrim-light) calc(33.333% - 0.5px) calc(33.333% + 0.5px), transparent calc(33.333% + 0.5px)) no-repeat,
        linear-gradient(90deg, transparent calc(66.666% - 0.5px), var(--hbl-scrim-light) calc(66.666% - 0.5px) calc(66.666% + 0.5px), transparent calc(66.666% + 0.5px)) no-repeat;
}

.hbl-crop__frame:focus-visible {
    outline: 2px solid var(--hbl-primary-contrast);
    outline-offset: 2px;
}

.hbl-crop__handle {
    position: absolute;
    width: 14px;
    height: 14px;
    background: var(--hbl-primary-contrast);
    border: 2px solid var(--hbl-primary);
    border-radius: 50%;
    touch-action: none;
}

.hbl-crop__handle--nw { top: -8px;    left: -8px;    cursor: nwse-resize; }
.hbl-crop__handle--ne { top: -8px;    right: -8px;   cursor: nesw-resize; }
.hbl-crop__handle--sw { bottom: -8px; left: -8px;    cursor: nesw-resize; }
.hbl-crop__handle--se { bottom: -8px; right: -8px;   cursor: nwse-resize; }

.hbl-crop__ratio {
    position: absolute;
    top: 0.4rem;
    left: 0.5rem;
    background: var(--hbl-scrim-strong);
    color: var(--hbl-on-ink);
    font-size: 0.72rem;
    font-weight: 600;
    padding: 0.1rem 0.4rem;
    border-radius: 999px;
    letter-spacing: 0.04em;
    pointer-events: none;
}

/* ---------- Background photo processing: spinner / error overlays ---------- */

/* The app's one loading indicator: the inline Lucide spinner SVG
   (Icons/_IconSpinner). Sized in em so it tracks whatever text it sits with,
   and square so the rotation origin is the artwork's own center. */
.hbl-spin {
    display: inline-block;
    width: 1em;
    height: 1em;
    flex: 0 0 auto;
    animation: hbl-spin 0.9s linear infinite;
    transform-origin: 50% 50%;
}

@keyframes hbl-spin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .hbl-spin { animation-duration: 2.4s; }
}

/* ---------- Notices ----------
   One banner component for everything that speaks to the user in-page:
   validation summaries, empty states, and the live photo-processing status.
   Tone modifier carries the color; the icon inherits it. The processing
   banner's tone is set by design-review.js and by the first server render. */
.hbl-notice {
    display: flex;
    align-items: flex-start;
    gap: 0.65rem;
    padding: 0.75rem 1rem;
    margin-bottom: 1.1rem;
    border: 1px solid var(--hbl-hairline);
    border-left-width: 4px;
    border-radius: var(--hbl-radius-sm);
    background: color-mix(in srgb, var(--hbl-surface) 55%, var(--hbl-bg));
    color: var(--hbl-text);
    font-size: 0.92rem;
    font-weight: 500;
    line-height: 1.5;
}

/* Icon glyph or inline SVG, nudged down so it centers on the first text line
   rather than on the whole (possibly wrapped) message. */
.hbl-notice > i,
.hbl-notice > svg {
    font-size: 1.15rem;
    line-height: 1;
    flex: 0 0 auto;
    margin-top: 0.11rem;
}

.hbl-notice__body { min-width: 0; }

/* Validation summaries arrive as a <ul> of messages. */
.hbl-notice ul {
    list-style: none;
    margin: 0;
    padding: 0;
}

.hbl-notice li + li { margin-top: 0.2rem; }

.hbl-notice a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* Working: neutral fill with a brand edge and spinner. Deliberately NOT
   brand-tinted — on a red-branded tenant a tinted "busy" is indistinguishable
   from the red "failed" banner. */
.hbl-notice--busy {
    border-left-color: var(--hbl-primary);
}

.hbl-notice--busy > i { color: var(--hbl-primary); }

.hbl-notice--ready {
    background: color-mix(in srgb, var(--hbl-success) 9%, var(--hbl-bg));
    border-color: color-mix(in srgb, var(--hbl-success) 25%, transparent);
    border-left-color: var(--hbl-success);
}

.hbl-notice--ready > i { color: var(--hbl-success); }

.hbl-notice--error {
    background: color-mix(in srgb, var(--hbl-danger) 9%, var(--hbl-bg));
    border-color: color-mix(in srgb, var(--hbl-danger) 25%, transparent);
    border-left-color: var(--hbl-danger);
}

.hbl-notice--error > i { color: var(--hbl-danger); }

.hbl-notice--warning {
    background: color-mix(in srgb, var(--hbl-warning) 9%, var(--hbl-bg));
    border-color: color-mix(in srgb, var(--hbl-warning) 25%, transparent);
    border-left-color: var(--hbl-warning);
}

.hbl-notice--warning > i { color: var(--hbl-warning); }

.hbl-submit-busy {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

.hbl-photo-overlay {
    position: absolute;
    inset: 0;
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    text-align: center;
    padding: 1.5rem;
    background: var(--hbl-scrim-strong);
    color: var(--hbl-on-ink);
    z-index: 2;
}

.hbl-photo-overlay__text {
    margin: 0;
    font-size: 0.9rem;
    max-width: 26rem;
}

/* Sizes both the error glyph and the inline spinner SVG (which is 1em square). */
.hbl-photo-overlay__icon {
    display: inline-flex;
    font-size: 1.75rem;
    line-height: 1;
}

/* Lifted toward the on-ink foreground: a saturated red alone would sink into
   the scrim, and on a red-branded tenant it would just read as the brand. */
.hbl-photo-overlay--error .hbl-photo-overlay__icon {
    color: color-mix(in srgb, var(--hbl-danger) 55%, var(--hbl-on-ink));
}

/* Retry sits on the scrim, so it inverts: the on-ink tone as its surface. */
.hbl-photo-overlay .btn {
    background: var(--hbl-on-ink);
    border: 1px solid transparent;
    color: var(--hbl-ink);
    font-weight: 600;
}

.hbl-photo-overlay .btn:hover,
.hbl-photo-overlay .btn:focus-visible {
    background: color-mix(in srgb, var(--hbl-on-ink) 88%, var(--hbl-ink));
    color: var(--hbl-ink);
}

/* Only one overlay (or none, once Ready) is shown at a time, driven by the
   review item's data-hbl-photo-status attribute. */
[data-hbl-photo-status="Pending"] [data-hbl-photo-overlay="loading"],
[data-hbl-photo-status="Processing"] [data-hbl-photo-overlay="loading"] {
    display: flex;
}

[data-hbl-photo-status="Failed"] [data-hbl-photo-overlay="error"] {
    display: flex;
}

/* Nothing has been painted into the stage yet while a photo is processing, so
   hold open a 16:9 box rather than letting it collapse to a sliver — and keep
   the empty <img> (no src, hence a broken-image glyph) out of the way. */
[data-hbl-photo-status="Pending"] .hbl-crop__stage,
[data-hbl-photo-status="Processing"] .hbl-crop__stage,
[data-hbl-photo-status="Failed"] .hbl-crop__stage {
    aspect-ratio: 16 / 9;
    max-height: 22rem;
}

[data-hbl-photo-status="Pending"] .hbl-crop__image,
[data-hbl-photo-status="Processing"] .hbl-crop__image,
[data-hbl-photo-status="Failed"] .hbl-crop__image {
    display: none;
}

/* There is nothing to drag yet. */
[data-hbl-photo-status="Pending"] .hbl-review-item__hint,
[data-hbl-photo-status="Processing"] .hbl-review-item__hint,
[data-hbl-photo-status="Failed"] .hbl-review-item__hint {
    display: none;
}

[data-hbl-photo-status="Failed"] .hbl-crop__frame,
[data-hbl-photo-status="Pending"] .hbl-crop__frame,
[data-hbl-photo-status="Processing"] .hbl-crop__frame,
[data-hbl-photo-status="Failed"] .hbl-crop__mask,
[data-hbl-photo-status="Pending"] .hbl-crop__mask,
[data-hbl-photo-status="Processing"] .hbl-crop__mask {
    display: none;
}

[data-hbl-photo-status="Failed"] .hbl-review-item__body,
[data-hbl-photo-status="Pending"] .hbl-review-item__body,
[data-hbl-photo-status="Processing"] .hbl-review-item__body {
    opacity: 0.85;
}

[data-hbl-photo-status="Failed"] {
    border-color: color-mix(in srgb, var(--hbl-danger) 35%, transparent);
}

.hbl-photo-status-badge {
    display: none;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.78rem;
    font-weight: 600;
    padding: 0.2rem 0.6rem;
    border-radius: 999px;
    margin-left: auto;
    flex: 0 0 auto;
}

[data-hbl-photo-status="Pending"] .hbl-photo-status-badge,
[data-hbl-photo-status="Processing"] .hbl-photo-status-badge {
    display: inline-flex;
    background: color-mix(in srgb, var(--hbl-primary) 12%, transparent);
    color: var(--hbl-primary);
}

[data-hbl-photo-status="Pending"] .hbl-photo-status-badge::before,
[data-hbl-photo-status="Processing"] .hbl-photo-status-badge::before {
    content: "Processing\2026";
}

[data-hbl-photo-status="Failed"] .hbl-photo-status-badge {
    display: inline-flex;
    background: color-mix(in srgb, var(--hbl-danger) 12%, transparent);
    color: var(--hbl-danger);
}

[data-hbl-photo-status="Failed"] .hbl-photo-status-badge::before {
    content: "Couldn't process";
}

/* ---------- Create step: crop section & cards ---------- */

.hbl-crop-section {
    margin-top: 1.5rem;
}

.hbl-crop-section__title {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 1.15rem;
    font-weight: 700;
    margin-bottom: 0.25rem;
}

.hbl-crop-section__title i {
    color: var(--hbl-primary);
}

.hbl-crop-section__sub {
    color: var(--hbl-muted);
    font-size: 0.92rem;
    margin-bottom: 1rem;
}

.hbl-crop-list {
    display: grid;
    gap: 1.25rem;
    margin: 0;
}

.hbl-crop-card {
    background: var(--hbl-surface);
    border: 1px solid rgba(15, 23, 42, 0.08);
    border-radius: var(--hbl-radius-sm);
    padding: 0.9rem;
    box-shadow: var(--hbl-shadow-sm);
}

.hbl-crop-card__header {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-bottom: 0.75rem;
}

.hbl-crop-card__num {
    display: grid;
    place-items: center;
    width: 1.75rem;
    height: 1.75rem;
    flex: 0 0 auto;
    border-radius: 50%;
    background: color-mix(in srgb, var(--hbl-primary) 12%, transparent);
    color: var(--hbl-primary);
    font-weight: 700;
    font-size: 0.85rem;
}

.hbl-crop-card__meta {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.25;
}

.hbl-crop-card__name {
    font-weight: 600;
    font-size: 0.88rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hbl-crop-card__size {
    color: var(--hbl-muted);
    font-size: 0.78rem;
}

.hbl-crop-card__remove {
    margin-left: auto;
    flex: 0 0 auto;
    font-size: 0.85rem;
    text-decoration: none;
    color: #dc3545;
}

.hbl-crop-card__remove:hover,
.hbl-crop-card__remove:focus {
    color: #b02a37;
    text-decoration: underline;
}

.hbl-crop-card__hint {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0.6rem 0 0;
    color: var(--hbl-muted);
    font-size: 0.82rem;
}

.hbl-crop-card__fallback {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 1rem;
    border: 1px dashed rgba(15, 23, 42, 0.2);
    border-radius: var(--hbl-radius-sm);
    background: var(--hbl-bg);
    color: var(--hbl-muted);
    font-size: 0.88rem;
}

.hbl-crop-card__fallback i {
    font-size: 1.5rem;
    color: var(--hbl-primary);
}

/* ---------- Processing indicators (uploading state + per-photo overlay) ---------- */

/* Continue was clicked and the browser is POSTing the photos. Dim and lock
   everything except the submit button (which carries its own spinner) so
   nothing invites a click that would abandon the upload. */
.hbl-upload-panel__form.is-uploading .hbl-dropzone,
.hbl-upload-panel__form.is-uploading .hbl-crop-section,
.hbl-upload-panel__form.is-uploading .hbl-upload-panel__actions .btn-hbl-ghost {
    opacity: 0.45;
    pointer-events: none;
    user-select: none;
    transition: opacity 0.25s ease;
}

.hbl-photo-overlay__bar {
    display: block;
    width: 100%;
    max-width: 16rem;
    height: 6px;
    margin: 0 auto;
    border-radius: 999px;
    background: color-mix(in srgb, var(--hbl-primary) 15%, transparent);
    overflow: hidden;
}

.hbl-photo-overlay__bar span {
    display: block;
    width: 40%;
    height: 100%;
    border-radius: 999px;
    background: var(--hbl-primary);
    animation: hbl-indeterminate 1.4s ease-in-out infinite;
}

.hbl-photo-overlay__ring {
    position: relative;
    display: grid;
    place-items: center;
    width: 3.25rem;
    height: 3.25rem;
    border-radius: 50%;
    color: #fff;
    font-size: 1.2rem;
}

.hbl-photo-overlay__ring::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 3px solid rgba(255, 255, 255, 0.25);
    border-top-color: #fff;
    animation: hbl-spin 1s linear infinite;
}

.hbl-photo-overlay__ring i {
    animation: hbl-twinkle 1.6s ease-in-out infinite;
}

.hbl-photo-overlay__title {
    font-weight: 700;
    font-size: 1rem;
}

.hbl-photo-overlay__bar {
    max-width: 12rem;
    background: rgba(255, 255, 255, 0.2);
}

.hbl-photo-overlay__bar span {
    background: #fff;
}

@keyframes hbl-indeterminate {
    0% { transform: translateX(-110%); }
    100% { transform: translateX(280%); }
}

@keyframes hbl-twinkle {
    0%, 100% { transform: scale(1); opacity: 1; }
    50% { transform: scale(1.15); opacity: 0.75; }
}

/* ============================ Misc form styles preserved ============================ */

.form-floating > .form-control-plaintext::placeholder,
.form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder,
.form-floating > .form-control:focus::placeholder {
    text-align: start;
}

/* ---------- Embedded designer ----------
   On /Designer the Godot app owns the whole viewport, so the page opts out of the
   site's normal scrolling column via ViewData["BodyClass"]. Pinning the body to the
   viewport is what lets the shell take a percentage height: .hbl-main then has a
   definite size to hand down. */
.hbl-body--embed {
    height: 100dvh;
    overflow: hidden;
}

.hbl-body--embed .hbl-main {
    padding-bottom: 0;
    /* Without this the flex child is floored at its content height and can push
       past the viewport instead of fitting inside it. */
    min-height: 0;
}

.hbl-designer-shell {
    position: relative;
    height: 100%;
    background: var(--hbl-bg);
}

.hbl-designer-shell__frame {
    display: block;
    width: 100%;
    height: 100%;
    border: 0;
}

/* Covers the frame until the designer announces itself. Also the surface for the
   "it never loaded" message, which is why it can hold buttons. Dark, like the
   app's own loading shell behind it. color-scheme keeps browser forced-dark
   modes from inverting the lit/unlit bulb contrast on this one element. */
.hbl-designer-shell__overlay {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    padding: 1.5rem;
    background: var(--hbl-loader-bg);
    color: var(--hbl-loader-text);
    color-scheme: dark;
    /* Declared here rather than only as a var() fallback so designer-embed.js
       can read it to time the exit cover, and so the prefers-reduced-motion
       block can zero it in one place. */
    --hbl-embed-fade: 240ms;
    transition: opacity var(--hbl-embed-fade, 240ms) ease;
}

/* Faded rather than removed, so a half-painted first frame is never revealed.
   pointer-events lifts with the opacity so clicks reach the designer at once. */
.hbl-designer-shell__overlay.is-dismissed {
    opacity: 0;
    pointer-events: none;
}

/* Leaving the designer: the overlay is a plain dark cover with nothing on it.
   Set before the cover fades back in — while it is still fully transparent —
   so the loader's furniture never flashes through it. The dark-to-light
   adjustment then happens across the navigation itself: the cross-document
   view transition (see "Page transitions") crossfades this dark page into the
   lit one we land on. */
.hbl-designer-shell__overlay.is-exiting .hbl-loader {
    display: none;
}

.hbl-designer-shell__panel {
    width: min(32rem, 100%);
}

.hbl-designer-shell__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

/* Storage warnings are recoverable, so they sit over the designer instead of
   blocking it the way the overlay does. */
.hbl-designer-shell__toast {
    position: absolute;
    inset: auto 1rem 1rem;
    margin-inline: auto;
    width: min(32rem, calc(100% - 2rem));
}

.hbl-designer-shell__toast .hbl-notice { margin-bottom: 0; }

/* ---------- Designer loading: faceted C9 bulb stringer ----------
   Port of the Godot designer's own loading shell (loading_shell.html, owned by
   the Godot team) so our covering overlay and the app's loader underneath look
   like the same screen. A run of faceted C9 bulbs hung in a sagging row, one
   per canonical HBL C9 color; the bulb geometry lives in an SVG <symbol> in
   Views/Designer/Index.cshtml and the bulbs are built by designer-embed.js.
   Unlike the shell we get no download progress from the iframe — the string
   runs the indeterminate chase until designer.ready, flips all-lit for the
   dismissal fade, and goes dead on failure. Re-diff against the Godot team's
   shell whenever they revise theirs. */

/* Registered so lit class flips cross-fade instead of snapping. Browsers
   without @property still render every state, just without the fade. */
@property --hbl-glass-dim {
    syntax: "<number>";
    inherits: true;
    initial-value: 0.62;
}

@property --hbl-core-o {
    syntax: "<number>";
    inherits: true;
    initial-value: 0;
}

.hbl-loader {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.hbl-loader__wordmark {
    width: min(220px, 52vw);
    margin-bottom: clamp(28px, 6vh, 48px);
}

.hbl-loader__wordmark img {
    display: block;
    width: 100%;
    height: auto;
}

.hbl-loader__bulbs {
    display: flex;
    flex-direction: row;
    align-items: flex-start;
    justify-content: center;
    gap: clamp(6px, 1.6vw, 11px);
    width: 100%;
    padding-top: 6px;
}

.hbl-loader__bulb {
    --hbl-glass-dim: 0.62; /* black overlay opacity on the glass (unlit = dark) */
    --hbl-core-o: 0;       /* white-hot core opacity (lit = glowing filament) */
    position: relative;
    width: clamp(12px, 2.8vw, 18px);
    aspect-ratio: 24 / 44; /* matches the hbl-c9-bulb symbol viewBox */
    flex: 0 0 auto;
    transform-origin: 50% 4px; /* hang tilt pivots at the socket, on the cord */
    transition: filter 0.3s ease, opacity 0.3s ease, --hbl-glass-dim 0.3s ease, --hbl-core-o 0.3s ease;
    /* Mobile WebKit doesn't expand the compositing layer to fit an animated
       filter's blur extent, so the drop-shadow glow gets clipped to the
       unblurred box mid-animation. will-change pre-allocates render bounds
       that include the glow, avoiding the clip. */
    will-change: filter;
}

.hbl-loader__bulb svg {
    display: block;
    width: 100%;
    height: 100%;
    overflow: visible;
}

.hbl-loader__bulb.is-lit {
    --hbl-glass-dim: 0;
    --hbl-core-o: 0.85;
    filter: drop-shadow(0 0 3px var(--hbl-bulb-color)) drop-shadow(0 0 10px var(--hbl-bulb-color));
}

/* All-lit twinkle held while the dismissal fade runs. */
.hbl-loader--starting .hbl-loader__bulb.is-lit {
    animation: hbl-bulb-twinkle 2.6s ease-in-out infinite;
    animation-delay: calc(var(--hbl-i, 0) * 0.18s);
}

/* No progress signal from the iframe, so this chase runs the whole wait. */
.hbl-loader--indeterminate .hbl-loader__bulb {
    --hbl-glass-dim: 0.3;
    animation: hbl-bulb-chase 1.6s ease-in-out infinite;
    animation-delay: calc(var(--hbl-i, 0) * 0.09s);
}

/* Dead string behind the failure notice. */
.hbl-loader--failed .hbl-loader__bulb {
    animation: none;
    opacity: 0.35;
    filter: none;
}

@keyframes hbl-bulb-twinkle {
    0%, 100% { filter: drop-shadow(0 0 3px var(--hbl-bulb-color)) drop-shadow(0 0 10px var(--hbl-bulb-color)) brightness(1); }
    50%      { filter: drop-shadow(0 0 4px var(--hbl-bulb-color)) drop-shadow(0 0 13px var(--hbl-bulb-color)) brightness(1.25); }
}

@keyframes hbl-bulb-chase {
    0%, 100% { opacity: 0.3; filter: none; }
    50%      { opacity: 1; filter: drop-shadow(0 0 4px var(--hbl-bulb-color)) drop-shadow(0 0 11px var(--hbl-bulb-color)); }
}

/* On the dark overlay the status notice becomes the shell's plain centered
   label: no box, no border, muted light text under the bulb string. */
.hbl-designer-shell__overlay .hbl-notice {
    background: transparent;
    border: 0;
    padding: 0;
    margin: clamp(18px, 3.2vh, 26px) 0 0;
    justify-content: center;
    text-align: center;
    color: var(--hbl-loader-muted);
    font-size: 0.92rem;
    font-weight: 500;
    letter-spacing: 0.02em;
}

/* ...except the failure state, which needs to read as a panel again: the
   shell's dark red-bordered notice rather than the light in-page banner. */
.hbl-designer-shell__overlay .hbl-notice--error {
    background: color-mix(in srgb, var(--hbl-loader-bg) 88%, var(--hbl-danger));
    border: 1px solid color-mix(in srgb, var(--hbl-danger) 55%, transparent);
    border-radius: var(--hbl-radius-sm);
    padding: 1.1rem 1.4rem;
    color: var(--hbl-loader-text);
    text-align: left;
}

/* The escape-hatch link sits directly on the dark backdrop. */
.hbl-designer-shell__overlay .btn-link {
    color: var(--hbl-loader-text);
}

@media (prefers-reduced-motion: reduce) {
    .hbl-inspire__track,
    .hbl-recent__track { scroll-behavior: auto; }

    /* Cut straight to the designer rather than fading the cover off it.
       designer-embed.js reads this value, so zeroing it here also collapses
       the exit wait it schedules. */
    .hbl-designer-shell__overlay { --hbl-embed-fade: 0ms; }

    /* The bulb string holds still: no chase, no twinkle. Indeterminate bulbs
       rest at a visible mid-opacity so the string still reads as "working". */
    .hbl-loader--starting .hbl-loader__bulb.is-lit,
    .hbl-loader--indeterminate .hbl-loader__bulb {
        animation: none !important;
    }

    .hbl-loader--indeterminate .hbl-loader__bulb {
        opacity: 0.75;
    }

    /* The confirmation still appears, it just stops sliding and staggering in —
       a question needs to be there, not to arrive. */
    .hbl-recent__cta.is-confirming .hbl-recent__confirm > * {
        animation: none;
    }

    /* Zeroing these also zeroes the waits home-dashboard.js reads from them, so
       a look swap becomes an instant cut rather than a fade with the visual part
       switched off. The slide is dropped outright — motion is the thing being
       opted out of. */
    .hbl-inspire__hero {
        --hbl-hero-fade: 0ms;
        --hbl-content-fade: 0ms;
    }

    .hbl-inspire__featured.is-swapping {
        transform: none;
    }

    /* Keep the hover shadow as the affordance; drop the lift and scale. */
    .hbl-look-card__link:hover,
    .hbl-look-card__link:focus-visible,
    .hbl-design-card__link:hover,
    .hbl-design-card__link:focus-visible {
        transform: none;
    }

    /* The featured card still changes size and its hint still appears — they just
       don't animate getting there. */
    .hbl-design-card,
    .hbl-design-card__caption,
    .hbl-design-card__hint {
        transition: none;
    }

    .hbl-design-card__hint {
        transform: none;
    }
}
