/* =========================================================================
   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: the dark gray the hero's photo dissolves
       into, and the base of the hero's glass panels. A fixed neutral charcoal
       (what --hbl-ink resolves to on a black-text palette), so the hero reads
       as a charcoal stage around the photo rather than a black hole. Not the
       ink itself: the vignette composites this colour over a night photo at
       rising opacity, and it only reads as shadow while it is darker than the
       photo's own darks. A tenant whose text colour is navy or slate gets an
       ink lighter than a night sky, and the same vignette then lifts and fogs
       the photo instead of fading it (Gerten's). A tenant wanting a specific
       tone sets Tenant:Branding:HeroOverlayColor, which _Layout.cshtml emits
       as an override of this token. */
    --hbl-hero-overlay: #232323;

    /* 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 surface: the page background nudged toward the surface
       tone, so it reads as a light branded splash (the treatment the Godot
       app's own loading shell applies once it knows the tenant) rather than a
       bare page. The wordmark, title and progress bar carry the brand. */
    --hbl-loader-bg: color-mix(in srgb, var(--hbl-bg) 60%, var(--hbl-surface));
    --hbl-loader-text: var(--hbl-text);
    --hbl-loader-muted: color-mix(in srgb, var(--hbl-text) 72%, transparent);

    /* The loader's bulb string: warm whites with a tenant-primary bulb every
       third socket, mirroring the Godot shell's branded string. Warm white is
       the catalog's C9 warm-white glass, a product color rather than a theme
       color, which is why it is a literal here. */
    --hbl-loader-bulb: #f9edd7;
    --hbl-loader-bulb-accent: var(--hbl-primary);

    --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; }
}

/* Framed by a host page (class set by _Layout before paint). The host owns the
   outer scroll and may lay a sticky header over the top of the frame, so the
   scroll requests that cross into it (site.js scrollHostToTop and the crop
   step's scroll-margin) leave this much room above the target. */
.hbl-embedded {
    --hbl-host-headroom: 14rem;
}

/* Scroll target for scrollHostToTop: one pixel at the very top of the
   document, out of flow so it adds no scrollable area. It sits inside the
   document on purpose: current Chrome clips a cross-origin frame's
   scroll-into-view rect to the frame's box, so anything parked above the top
   would be clipped away. site.js aligns this pixel's bottom edge with the
   bottom of the host window, which can only scroll the host up, to its top. */
.hbl-embed-top {
    position: absolute;
    top: 0;
    left: 0;
    width: 1px;
    height: 1px;
    pointer-events: none;
}

/* ---------- 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;
}

/* =========================================================================
   "Get Inspired" hero + look carousel
   ========================================================================= */

/* The hero is sized by what it holds — headline, gallery, upload card — and
   never stretched to the viewport: the look's photo is fitted to the hero's
   height (see .hbl-inspire__backdrop), so a taller hero would only mean a
   bigger, more cropped photo, not more of it.

   --hbl-hero-hang: how far the gallery deck and the upload card hang past the
   hero's bottom edge, half on the photo and half on the page. The section pads
   its bottom by the same amount so the next section starts below them. */
.hbl-inspire {
    --hbl-hero-hang: 3.5rem;
    display: flex;
    flex-direction: column;
    padding-bottom: var(--hbl-hero-hang);
    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;

    /* 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 vignette leaves the middle of the photo nearly clear 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);

    /* Where the house is, as a horizontal position in the hero. The photos are
       shot with the house centred, so the photo layers are centred on this
       point and the vignette clears around it. The middle at every width: in
       the two-column layout the house sits between the headline and the two
       panels, framed by them rather than pushed over one column. */
    --hbl-hero-focus-x: 50%;

    /* Vignette, painted on each photo layer over its photo: one circle centred
       on the house, clear at the centre and running linearly to the hero's
       black at a radius of half the photo's width, so the photo dissolves
       into the hero on every side with no edge. Matches the Figma hero mockup;
       there are no other fades.

       The radius is spelled out (the photo is fitted to the layer's height at
       16:9, so half its width is 8/9 of that height, read through the
       backdrop's container units) rather than taken from the gradient box,
       because the gradient box is the whole hero, not the photo: the gradient
       must paint every pixel of the hero, including the plain colour past the
       photo's sides. A solid background and a gradient that ends in the same
       colour do not rasterize to the same 8-bit value (the brand mix lands on
       a half level), so a photo-sized gradient over a solid hero left the
       photo's rectangle a shade darker, with a visible seam down each side. */
    --hbl-hero-vignette: radial-gradient(circle calc(100cqh * 8 / 9) at var(--hbl-hero-focus-x) 46%,
        transparent 0%,
        var(--hbl-hero-overlay) 100%);

    /* Glass for the two panels that sit on the photo: a dark translucent fill
       with the photo blurred through it, edged by a faint light hairline. */
    --hbl-glass-bg: color-mix(in srgb, var(--hbl-hero-overlay) 46%, transparent);
    --hbl-glass-border: color-mix(in srgb, var(--hbl-on-ink) 16%, transparent);
    --hbl-glass-shadow: 0 16px 48px color-mix(in srgb, var(--hbl-hero-overlay) 45%, transparent);

    position: relative;
    isolation: isolate;
    display: grid;
    min-height: clamp(360px, 48vh, 560px);
    /* Kept tight so the headline, the gallery and the upload card all land in a
       laptop's first screen; the svh term gives a little back on short
       viewports. No bottom padding: the panels sit on the hero's bottom edge
       and hang past it (see --hbl-hero-hang). */
    padding: clamp(2.5rem, 7svh, 3.5rem) 0 0;
    /* Shows at the sides where the photo ends; the vignette is nearly opaque
       there, so this is the colour it resolves to. */
    background-color: var(--hbl-hero-overlay);
}

/* The photo's own box, filling the hero and clipping to it. Its ::before and
   ::after are the two photo layers — the current photo and, mid-swap, the
   incoming one fading in over it. Each layer spans the whole hero and paints
   two things: the vignette across all of it, and under that the photo, fitted
   to the hero's full height at the photo's own 16:9 proportion, never scaled
   past it or tiled, and centred on the house. So the hero is only ever as
   tall as the photo, and the photo loses nothing top or bottom; what runs off
   the sides is clipped here, under the vignette. Clipping lives on this box
   rather than the hero so the panels can hang below the hero's edge unclipped.
   The box is a size container so the vignette can take its radius from the
   hero's height (see --hbl-hero-vignette). The image custom properties are set
   on the hero by home-dashboard.js and inherit down. */
.hbl-inspire__backdrop {
    position: absolute;
    inset: 0;
    z-index: -1;
    overflow: hidden;
    pointer-events: none;
    container-type: size;
}

.hbl-inspire__backdrop::before,
.hbl-inspire__backdrop::after {
    content: '';
    position: absolute;
    inset: 0;
    /* vignette, then photo: the gradient fills the box, the photo is fitted
       to its height and placed on the focus point */
    background-size: auto, auto 100%;
    background-position: center, var(--hbl-hero-focus-x) center;
    background-repeat: no-repeat;
}

.hbl-inspire__backdrop::before {
    z-index: 1;
    background-image: var(--hbl-hero-vignette), var(--hbl-hero-image, none);
}

/* Hero grid. Three blocks: the pitch, the upload card, and the inspiration
   gallery. Stacked pitch → upload → gallery while narrow, so on a phone or a
   portrait tablet the upload is the first thing after the headline. From 992px
   the headline runs across the top on two lines, and the gallery (left) and
   upload card (right) sit along the bottom edge, so the band between them is
   open photo — the house. The 1fr first row is what pushes the panels down:
   it absorbs whatever height the hero has to spare, and the hero's min-height
   (see the 992px block) is what guarantees there is some. The two panels share
   the second row's full height: each is a flex column whose card grows to fill
   it, so the deck and the upload card end on the same line whichever side is
   naturally taller. */
.hbl-inspire__hero-inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
        "intro"
        "upload"
        "gallery";
    gap: 2rem;
}

.hbl-inspire__intro { grid-area: intro; }
.hbl-inspire__upload { grid-area: upload; }
.hbl-inspire__gallery { grid-area: gallery; min-width: 0; }

@media (min-width: 992px) {
    .hbl-inspire__hero-inner {
        grid-template-columns: minmax(0, 3fr) minmax(20rem, 2fr);
        grid-template-rows: 1fr auto;
        grid-template-areas:
            "intro intro"
            "gallery upload";
        column-gap: 3rem;
        row-gap: 2rem;
    }

    .hbl-inspire__intro { align-self: start; }
    .hbl-inspire__gallery,
    .hbl-inspire__upload { align-self: stretch; }
}

.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;
    /* No photo to vignette — the circle would only dirty the gradient. */
    --hbl-hero-vignette: none;

    background-color: var(--hbl-bg);
    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. Sits between the current photo
   and the scrim, so both photos composite under the same vignette mid-fade. */
.hbl-inspire__backdrop::after {
    z-index: 2;
    background-image: var(--hbl-hero-vignette), var(--hbl-hero-image-next, none);
    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 .hbl-inspire__backdrop::after {
    transition: none;
}

/* The panels hang past the hero's bottom edge. A negative bottom margin on an
   end-aligned grid item pushes its box down past the row's edge without
   growing the hero; the section's padding (see .hbl-inspire) makes the room.
   Stacked layouts hang the gallery, which is their last block; the two-column
   layout hangs both panels. */
.hbl-inspire__gallery {
    display: flex;
    flex-direction: column;
    margin-bottom: calc(-1 * var(--hbl-hero-hang));
}

/* Two-column layout: taller, so the centred house has a clear band of photo
   between the headline and the panels. */
@media (min-width: 992px) {
    .hbl-inspire__hero {
        padding: clamp(2.5rem, 6svh, 3.5rem) 0 0;
        /* Tall enough that a clear band of photo — the house — opens between
           the headline and the panels; capped so the photo, fitted to this
           height, is never blown up past what a laptop screen can show. */
        min-height: clamp(600px, 84svh, 780px);
    }

    .hbl-inspire__upload {
        margin-bottom: calc(-1 * var(--hbl-hero-hang));
    }
}

/* 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(2rem, 6vw, 3rem);
    font-weight: 600;
    color: var(--hbl-hero-fg);
    max-width: 18ch;
    text-wrap: balance;
    text-shadow: var(--hbl-hero-text-shadow);
    margin-bottom: 2rem;
}

/* Two-column hero: the headline has the full width across the top, so the
   measure opens up enough for a three-clause tenant headline to land on two
   lines, balanced. Must follow the base rule above — same specificity, later
   wins. */
@media (min-width: 992px) {
    .hbl-inspire__heading {
        font-size: clamp(2rem, 4.5vw, 3.25rem);
        max-width: 36ch;
        margin-bottom: 0;
    }
}

/* Words the tenant wrapped in *asterisks*. */
.hbl-inspire__heading-accent {
    color: var(--hbl-primary);
}

/* ---- Panel headings, above each card on the photo's dark lower edge ----
   The gallery and the upload column share these, so the two titles and leads
   line up across the hero. */

.hbl-inspire__gallery-title,
.hbl-inspire__upload-title {
    font-family: var(--hbl-font-serif);
    font-size: clamp(1.25rem, 2vw, 1.5rem);
    font-weight: 700;
    color: var(--hbl-hero-fg);
    text-shadow: var(--hbl-hero-text-shadow);
    margin: 0 0 0.2rem;
}

/* One line under each title: what the panel is for, or, in the gallery's
   empty state, that it's coming. */
.hbl-inspire__gallery-lead,
.hbl-inspire__upload-lead {
    color: var(--hbl-hero-fg-muted);
    font-size: clamp(0.9rem, 1.1vw, 1rem);
    max-width: 40rem;
    text-shadow: var(--hbl-hero-text-shadow);
    margin: 0 0 0.85rem;
}

/* ---- Upload card ---- */

/* The upload column: title and lead on the photo (styled with the gallery's,
   above), then the glass card. A flex column so the card can grow to the
   grid row's height and end level with the deck. */
.hbl-inspire__upload {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* Dark glass on the hero photo, so the card reads as part of the scene rather
   than a white box on it. Everything inside is light-on-dark; the dropzone's
   dashed frame and the coach callout are redrawn below in the same palette.
   The notices (_UploadFormHead) keep their own alert colours. The card is the
   size container for the dropzone's row layout below, keyed to the card's own
   width rather than the viewport's. */
.hbl-inspire__upload-card {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding: 1rem;
    color: var(--hbl-on-ink);
    background: var(--hbl-glass-bg);
    -webkit-backdrop-filter: blur(18px) saturate(1.3);
    backdrop-filter: blur(18px) saturate(1.3);
    border: 1px solid var(--hbl-glass-border);
    border-radius: var(--hbl-radius-lg);
    box-shadow: var(--hbl-glass-shadow);
    container-type: inline-size;
}

.hbl-inspire__upload .hbl-notice { margin: 0; }

/* The dropzone on glass: a faint light fill inside a dashed light frame, with
   its copy flipped to the hero's light-on-dark. Shorter than the Create page's
   dropzone — it shares the hero with the headline — but still an unmistakable
   drop target, and it grows to take up whatever height the card has to spare
   so the card can match the deck beside it. Two-class selectors so these win
   over the dropzone's own hover, drag-over and 768px+ padding rules without an
   ordering dependency. */
.hbl-inspire__upload .hbl-dropzone {
    flex: 1;
    height: auto;
    min-height: 8.5rem;
    padding: 1.25rem;
    /* The tile's radius rather than the dropzone's own larger one, so it sits
       tucked inside the card's corner instead of echoing it. */
    border-radius: var(--hbl-radius-sm);
    display: flex;
    flex-direction: column;
    justify-content: center;
    color: var(--hbl-on-ink);
    background: color-mix(in srgb, var(--hbl-on-ink) 6%, transparent);
    border-color: color-mix(in srgb, var(--hbl-on-ink) 20%, transparent);
}

.hbl-inspire__upload .hbl-dropzone:hover,
.hbl-inspire__upload .hbl-dropzone:focus-within {
    background: color-mix(in srgb, var(--hbl-on-ink) 11%, transparent);
    border-color: color-mix(in srgb, var(--hbl-on-ink) 60%, transparent);
}

.hbl-inspire__upload .hbl-dropzone.is-dragover {
    background: color-mix(in srgb, var(--hbl-primary) 28%, transparent);
    border-color: color-mix(in srgb, var(--hbl-primary) 80%, var(--hbl-on-ink));
}

.hbl-inspire__upload .hbl-dropzone.is-filled {
    min-height: 0;
    padding: 1rem 1.25rem;
}

.hbl-inspire__upload .hbl-dropzone__icon,
.hbl-inspire__upload .hbl-dropzone__filled-count {
    color: var(--hbl-on-ink);
}

.hbl-inspire__upload .hbl-dropzone__sub {
    color: var(--hbl-on-ink-muted);
}

/* The photo limit stays on the Create pages' dropzone; on the hero it is one
   line too many for a card that has to stay level with the deck, and the
   limit is still enforced on submit. */
.hbl-inspire__upload .hbl-dropzone__meta {
    display: none;
}

/* Once the card is wide enough (keyed to the card, not the viewport: at 1024px
   the two-column hero gives it only ~320px, while a full-width card on a 768px
   tablet has plenty of room), the empty dropzone lays out as one row: the icon
   over "drag & drop" on the left, "or" on a hairline in the middle, the upload
   button on the right. Narrower
   than that (the two-column hero below ~1200px, and phones) it compacts the
   other way: the icon beside the label on one centred line, "or" on a
   horizontal hairline, and the button full width under it. Either way the
   card stays close to the deck's height instead of towering over it. */
@container (max-width: 25.99rem) {
    .hbl-inspire__upload .hbl-dropzone__inner {
        display: grid;
        /* The outer 1fr tracks take the slack equally, so the icon and label
           pair sits centred; the divider and the button span the whole width. */
        grid-template-columns: 1fr auto auto 1fr;
        grid-template-areas:
            ". icon title ."
            "or or or or"
            "browse browse browse browse";
        align-items: center;
        column-gap: 0.75rem;
        row-gap: 0.75rem;
    }

    .hbl-inspire__upload .hbl-dropzone__icon {
        grid-area: icon;
        margin: 0;
        font-size: 1.75rem;
    }

    .hbl-inspire__upload .hbl-dropzone__title {
        grid-area: title;
        margin: 0;
        font-size: 1rem;
        text-align: left;
    }

    /* "or" on a horizontal hairline between the prompt and the button. */
    .hbl-inspire__upload .hbl-dropzone__sub {
        grid-area: or;
        display: flex;
        align-items: center;
        gap: 0.6rem;
        margin: 0;
        font-size: 0.85rem;
        font-style: italic;
    }

    .hbl-inspire__upload .hbl-dropzone__sub::before,
    .hbl-inspire__upload .hbl-dropzone__sub::after {
        content: '';
        flex: 1;
        height: 1px;
        background: color-mix(in srgb, var(--hbl-on-ink) 16%, transparent);
    }

    .hbl-inspire__upload .hbl-dropzone__browse {
        grid-area: browse;
        margin: 0;
    }
}

@container (min-width: 26rem) {
    .hbl-inspire__upload .hbl-dropzone__inner {
        /* Fills the dropzone's height (the dropzone grows with the card), with
           the two rows splitting it evenly: the icon sits at the end of the
           top half and the label at the start of the bottom half, so the pair
           stays centred while the "or" divider runs the full height. */
        flex: 1;
        display: grid;
        /* The button keeps its natural width so its label stays on one line;
           the drag-and-drop cell takes whatever is left. */
        grid-template-columns: minmax(0, 1fr) auto auto;
        grid-template-rows: 1fr 1fr;
        grid-template-areas:
            "icon or browse"
            "title or browse";
        align-items: center;
        justify-items: center;
        column-gap: 1rem;
        row-gap: 0.4rem;
    }

    .hbl-inspire__upload .hbl-dropzone__icon {
        grid-area: icon;
        align-self: end;
        margin: 0;
        font-size: 2rem;
    }

    .hbl-inspire__upload .hbl-dropzone__title {
        grid-area: title;
        align-self: start;
        margin: 0;
        font-size: 1rem;
    }

    /* "or" sits in a gap on a vertical hairline that runs the row's full
       height above and below it, small and italic so the line reads as the
       divider and the word as its label. */
    .hbl-inspire__upload .hbl-dropzone__sub {
        grid-area: or;
        align-self: stretch;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 0.4rem;
        margin: 0;
        font-size: 0.85rem;
        font-style: italic;
    }

    .hbl-inspire__upload .hbl-dropzone__sub::before,
    .hbl-inspire__upload .hbl-dropzone__sub::after {
        content: '';
        flex: 1;
        width: 1px;
        background: color-mix(in srgb, var(--hbl-on-ink) 16%, transparent);
    }

    .hbl-inspire__upload .hbl-dropzone__browse {
        grid-area: browse;
        margin: 0;
        white-space: nowrap;
    }
}

/* Upload action tile — a quiet row in the glass palette: the coach's face, a
   label with a line under it, and a chevron. Full width under the dropzone. */
.hbl-upload-action {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    min-width: 0;
    padding: 0.5rem 0.5rem 0.5rem 0.5rem;
    text-align: left;
    font: inherit;
    color: var(--hbl-on-ink);
    text-decoration: none;
    /* A solid dark fill rather than a light wash, so the labels read against
       it instead of against whatever the photo shows through the glass. */
    background: color-mix(in srgb, var(--hbl-hero-overlay) 55%, transparent);
    border: 1px solid color-mix(in srgb, var(--hbl-on-ink) 16%, transparent);
    border-radius: var(--hbl-radius-sm);
    cursor: pointer;
    transition: background-color var(--hbl-transition), border-color var(--hbl-transition);
}

.hbl-upload-action:hover,
.hbl-upload-action:focus-visible {
    color: var(--hbl-on-ink);
    background: color-mix(in srgb, var(--hbl-hero-overlay) 80%, transparent);
    border-color: color-mix(in srgb, var(--hbl-on-ink) 30%, transparent);
}

.hbl-upload-action:focus-visible {
    outline: 2px solid var(--hbl-primary);
    outline-offset: 2px;
}

/* The coach's face, in a small ringed disc. */
.hbl-upload-action__avatar {
    width: 2.25rem;
    height: 2.25rem;
    flex: 0 0 auto;
    border-radius: 50%;
    overflow: hidden;
    border: 2px solid color-mix(in srgb, var(--hbl-on-ink) 45%, transparent);
    box-shadow: 0 1px 3px color-mix(in srgb, var(--hbl-hero-overlay) 40%, transparent);
}

.hbl-upload-action__avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* The label stays on one line and clips with an ellipsis if it can't; the line
   under it carries a tenant string (the coach's name) and may wrap on a narrow
   card rather than lose its end. */
.hbl-upload-action__text {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    flex: 1;
    min-width: 0;
    line-height: 1.25;
}

.hbl-upload-action__text strong {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hbl-upload-action__text strong { font-size: 0.85rem; }
.hbl-upload-action__text span { font-size: 0.78rem; color: var(--hbl-on-ink-muted); }
.hbl-upload-action > i { flex: 0 0 auto; color: var(--hbl-on-ink-muted); font-size: 1rem; }

/* Coach drawer — Bootstrap's offcanvas carrying the same tip card the Create
   and Review pages show inline. */
.hbl-coach-drawer {
    --bs-offcanvas-width: min(26rem, 100vw);
    --bs-offcanvas-bg: var(--hbl-surface);
    --bs-offcanvas-color: var(--hbl-text);
}

.hbl-coach-drawer__header { border-bottom: 1px solid var(--hbl-hairline); }

.hbl-coach-drawer__title {
    font-family: var(--hbl-font-serif);
    font-size: 1.35rem;
    font-weight: 700;
    margin: 0;
}

.hbl-coach-drawer__body .hbl-tip-card { border: 1px solid var(--hbl-hairline-soft); }

/* Framed (see home-dashboard.js initCoachDrawer): a floating card at the top of
   the page instead of a full-height edge drawer. The host clips a scrollbar-
   gutter strip off the frame's right edge, so nothing may sit flush right; and
   only the top of the frame is reliably on the host's screen (the frame is at
   least a viewport tall and the host scrolls it), so the card hangs from the
   top. It is absolute, not fixed, and shows every tip at its natural height
   with no inner scroll area: a fixed box can only be as tall as the frame, and
   a short host viewport cuts its bottom off with no way to reach it. Part of
   the page, the card scrolls with it, so the wheel reaches the end of the
   tips in any viewport. Opening scrolls both page and host to the top first. */
.hbl-embedded .hbl-coach-drawer {
    --bs-offcanvas-width: min(26rem, 100vw - 3.5rem);
    position: absolute;
    top: 1rem;
    right: 1.75rem;
    bottom: auto;
    max-height: none;
    border: 1px solid var(--hbl-hairline);
    border-radius: 1rem;
    box-shadow: var(--hbl-shadow-lg);
    /* Absolute boxes add to the page's scrollable overflow, so the stock
       off-canvas slide (parked a full width past the right edge) would grow a
       horizontal scrollbar. A short slide-and-fade within the inset instead. */
    transform: translateX(1.25rem);
    opacity: 0;
    transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
}

.hbl-embedded .hbl-coach-drawer .offcanvas-body {
    flex-grow: 0;
    overflow: visible;
}

.hbl-embedded .hbl-coach-drawer.show:not(.hiding),
.hbl-embedded .hbl-coach-drawer.showing {
    transform: none;
    opacity: 1;
}

/* ---- Look deck ---- */

/* Carousel card at the bottom of the hero's left column, in the same dark
   glass as the upload card. 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;
    /* Grows to the gallery column's height and keeps the track centred in
       it, so when the upload card beside it is the taller of the two the deck
       stretches to meet it instead of leaving a step between them. */
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
    background: var(--hbl-glass-bg);
    -webkit-backdrop-filter: blur(18px) saturate(1.3);
    backdrop-filter: blur(18px) saturate(1.3);
    border: 1px solid var(--hbl-glass-border);
    border-radius: var(--hbl-radius-lg);
    box-shadow: var(--hbl-glass-shadow);
    padding: 0.25rem 0.5rem;
}

/* On the dark deck the shared card's white edge reads as a frame around each
   photo. Use the deck's own faint hairline instead, and a shadow cast in the
   hero's dark rather than the light-surface one. */
.hbl-inspire__deck .hbl-look-card__link,
.hbl-inspire__deck .hbl-look-card__link:hover,
.hbl-inspire__deck .hbl-look-card__link:focus-visible {
    border-color: var(--hbl-glass-border);
    box-shadow: 0 6px 20px color-mix(in srgb, var(--hbl-hero-overlay) 45%, transparent);
}

/* The arrows in the same dark glass as the deck, not page-white. */
.hbl-inspire__deck .hbl-scroller-btn {
    background: color-mix(in srgb, var(--hbl-hero-overlay) 72%, transparent);
    border-color: var(--hbl-glass-border);
    color: var(--hbl-on-ink);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    box-shadow: var(--hbl-glass-shadow);
}

.hbl-inspire__deck .hbl-scroller-btn:hover,
.hbl-inspire__deck .hbl-scroller-btn:focus {
    background: color-mix(in srgb, var(--hbl-hero-overlay) 90%, transparent);
    color: var(--hbl-on-ink);
}

.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.5rem 1.25rem;
    margin: 0;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.hbl-inspire__track::-webkit-scrollbar { display: none; width: 0; height: 0; }

.hbl-look-card {
    position: relative;
    flex: 0 0 auto;
    width: clamp(180px, 13vw, 200px);
    scroll-snap-align: center;
}

/* "Explore this look" — a link laid over the selected card's photo, so the one
   card that is live carries its own way into the designer. It is a sibling of
   the card button (a button can't hold a link), sized to the 16:9 media, and
   only displayed while that button is pressed — unselected cards keep nothing
   focusable hidden inside them. */
.hbl-look-card__explore {
    display: none;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    aspect-ratio: 16 / 9;
    place-items: center;
    z-index: 1;
    text-decoration: none;
    border-radius: var(--hbl-radius-sm) var(--hbl-radius-sm) 0 0;
    background: color-mix(in srgb, var(--hbl-ink) 15%, transparent);
    transition: background-color var(--hbl-transition);
}

.hbl-look-card__link[aria-pressed="true"] + .hbl-look-card__explore { display: grid; }

.hbl-look-card__explore:hover,
.hbl-look-card__explore:focus-visible {
    background: color-mix(in srgb, var(--hbl-ink) 30%, transparent);
}

.hbl-look-card__explore:focus-visible {
    outline: 2px solid var(--hbl-primary);
    outline-offset: 2px;
}

.hbl-look-card__explore-pill {
    padding: 0.4rem 0.9rem;
    font-size: 0.8rem;
    box-shadow: var(--hbl-shadow);
}

.hbl-look-card__explore-pill .hbl-icon {
    width: 1.1em;
    height: 1.1em;
}

/* 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);
}

/* 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;
}

/* The name is tenant data of unknown length, so it is capped at two lines and
   ellipsised. Without this a wordy name sets the height of every card in the
   row. */
.hbl-look-card__name {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    font-family: var(--hbl-font-serif);
    font-weight: 700;
    font-size: 0.95rem;
    line-height: 1.25;
}

/* 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);
}

/* 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.
   --hbl-scroller-inset is the offset past that edge (negative = outside).

   How far out a button can go is bounded by the page gutter, since both decks
   span 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:
     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+ — recent-designs deck only: the full width plus a gap, clear of
       the deck in the page margin. The inspiration deck sits inside the hero's
       grid with the upload card as its right-hand neighbour, so it has no
       margin to step into and stays centred on its edge. */
.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-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;
}

/* Home's crop step. Hidden until the first photo lands in the hero's dropzone,
   then revealed and scrolled to by design-create.js; the scroll margin keeps
   the sticky header off its stepper. */
.hbl-upload-step {
    padding-top: 0;
    scroll-margin-top: 4.5rem;
}

/* Framed, that scroll also moves the host page, which aligns the step with
   its own viewport top — under its sticky header. The margin rides along with
   the scroll request, so the host headroom keeps the stepper clear. */
.hbl-embedded .hbl-upload-step {
    scroll-margin-top: var(--hbl-host-headroom);
}

.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);
}

/* Framed: the host sizes its iframe by width (a 16:9 shell), so on a wide window
   the frame runs taller than the browser viewport and the bottom of the Godot
   UI lands below the fold with nothing inside the frame able to reach it. The
   app fills whatever box it gets, so the box is capped at the visible band
   instead: designer-embed.js measures how much of this page is on screen with
   the host scrolled to its top and writes it here. The frame, overlay and
   toast all take the shell's height, so one property clamps the lot; whatever
   is left of the page below is plain background. Never taller than the page. */
.hbl-embedded .hbl-designer-shell {
    height: min(100%, var(--hbl-designer-visible-height, 100%));
}

.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. Light and
   page-toned, like the app's own loading shell once it knows the tenant. */
.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);
    /* 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 page-toned 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
   cross-document view transition (see "Page transitions") then crossfades
   this cover into the page 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, warm
   whites with a tenant-colored bulb every third socket; the bulb geometry
   lives in an SVG <symbol> in Views/Designer/Index.cshtml and the bulbs are
   built by designer-embed.js. The shell relays its own progress to us
   (designer.progress), so the string lights up along the download like the
   shell's does, holds all-lit while the app bootstraps, and goes dead on
   failure; until the first report arrives it runs the chase over an estimated
   bar. 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(16px, 3vh, 26px);
}

.hbl-loader__wordmark img {
    display: block;
    width: 100%;
    height: auto;
}

/* The tenant's name for the designer. Sans rather than the site's serif display
   face: this screen mirrors the Godot shell, whose title is a bold sans. */
.hbl-loader__title {
    margin: 0 0 clamp(22px, 4vh, 34px);
    font-family: var(--hbl-font-sans);
    font-size: clamp(1.25rem, 3vw, 1.6rem);
    font-weight: 700;
    letter-spacing: -0.01em;
    line-height: 1.2;
    text-align: center;
    color: var(--hbl-loader-text);
}

.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);
}

/* Nothing reported yet (or an older designer build that never reports): the
   chase runs until the first real signal, or the whole wait without one. */
.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);
}

/* The bulb the download is currently reaching: half lit, a faint glow. */
.hbl-loader__bulb.is-charging {
    --hbl-glass-dim: 0.3;
    --hbl-core-o: 0.35;
    filter: drop-shadow(0 0 3px var(--hbl-bulb-color));
}

/* Dead string behind the failure notice. */
.hbl-loader--failed .hbl-loader__bulb {
    animation: none;
    opacity: 0.35;
    filter: none;
}

/* ---- Progress bar ----
   Fill width is set by designer-embed.js: real values from the shell's
   reports, or the estimate until the first one lands. */
.hbl-loader__track {
    width: 100%;
    height: 8px;
    margin-top: clamp(22px, 4vh, 30px);
    border-radius: 8px;
    overflow: hidden;
    background: var(--hbl-hairline-strong);
}

.hbl-loader__fill {
    height: 100%;
    width: 0%;
    border-radius: inherit;
    background: var(--hbl-primary);
    transition: width 0.4s ease;
}

/* No real number right now (nothing reported yet, or the app is between
   checkpoints): the fill breathes so a stationary bar still reads as working. */
.hbl-loader--estimating .hbl-loader__fill,
.hbl-loader--preparing .hbl-loader__fill {
    animation: hbl-loader-breathe 1.5s ease-in-out infinite;
}

@keyframes hbl-loader-breathe {
    50% { opacity: 0.45; }
}

.hbl-loader__hint {
    margin: 10px 0 0;
    font-size: 0.85rem;
    text-align: center;
    color: var(--hbl-loader-muted);
}

/* A failed load has nothing left to measure. */
.hbl-loader--failed .hbl-loader__track,
.hbl-loader--failed .hbl-loader__hint {
    display: 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 bar holds its value instead of breathing between reports. */
    .hbl-loader--estimating .hbl-loader__fill,
    .hbl-loader--preparing .hbl-loader__fill {
        animation: none;
    }

    /* 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;
    }

    /* 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;
    }
}

/* =========================================================================
   Small-screen notice
   ========================================================================= */

/* A sheet pinned to the bottom of phone-sized viewports, nudging the visitor
   toward a tablet or desktop. site.js unhides it once per session (the
   `hidden` attribute is the switch, so it never flashes before the script
   runs) and hides it again on dismiss. Wider viewports never show it, whatever
   the script does. */
.hbl-small-screen-note {
    position: fixed;
    left: 0.75rem;
    right: 0.75rem;
    bottom: 0.75rem;
    z-index: 1040; /* above page content, under Bootstrap's offcanvas (1045) */
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.85rem 0.9rem 0.85rem 1rem;
    background: var(--hbl-ink);
    color: var(--hbl-on-ink);
    border-radius: var(--hbl-radius);
    box-shadow: var(--hbl-shadow-lg);
    font-size: 0.9rem;
    line-height: 1.45;
    animation: hbl-small-screen-note-in 320ms ease both;
}

.hbl-small-screen-note[hidden] { display: none; }

@media (min-width: 768px) {
    .hbl-small-screen-note { display: none; }
}

/* Framed by a host page, "fixed; bottom" pins to the bottom of the frame, and
   the host's frame shell is at least the host viewport tall and sits under its
   sticky header, so that edge is the host's fold or below it. The markup is the
   first thing in <body>, and every framed page load scrolls the host to its
   top, so the top of our document is the one spot known to be on screen: show
   the note there as a full-width strip that sticks to the frame's top edge
   while our own content scrolls under it, pushing content down instead of
   covering it. */
.hbl-embedded .hbl-small-screen-note {
    position: sticky;
    top: 0;
    left: auto;
    right: auto;
    bottom: auto;
    border-radius: 0;
    box-shadow: var(--hbl-shadow);
    animation-name: hbl-small-screen-note-in-top;
}

@keyframes hbl-small-screen-note-in-top {
    from { opacity: 0; transform: translateY(-0.75rem); }
    to   { opacity: 1; transform: none; }
}

.hbl-small-screen-note > i {
    flex: 0 0 auto;
    font-size: 1.5rem;
    line-height: 1;
    margin-top: 0.1rem;
    color: color-mix(in srgb, var(--hbl-primary) 72%, var(--hbl-on-ink));
}

.hbl-small-screen-note__body {
    flex: 1;
    min-width: 0;
    margin: 0;
}

.hbl-small-screen-note__body strong {
    display: block;
    font-size: 0.95rem;
}

.hbl-small-screen-note__body span {
    color: var(--hbl-on-ink-muted);
}

.hbl-small-screen-note__dismiss {
    flex: 0 0 auto;
    align-self: center;
    padding: 0.4rem 0.85rem;
    font: inherit;
    font-weight: 600;
    font-size: 0.85rem;
    color: var(--hbl-on-ink);
    background: color-mix(in srgb, var(--hbl-on-ink) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--hbl-on-ink) 22%, transparent);
    border-radius: 999px;
    cursor: pointer;
}

.hbl-small-screen-note__dismiss:hover,
.hbl-small-screen-note__dismiss:focus-visible {
    background: color-mix(in srgb, var(--hbl-on-ink) 20%, transparent);
}

.hbl-small-screen-note__dismiss:focus-visible {
    outline: 2px solid var(--hbl-on-ink);
    outline-offset: 2px;
}

@keyframes hbl-small-screen-note-in {
    from { opacity: 0; transform: translateY(0.75rem); }
    to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .hbl-small-screen-note { animation: none; }
}
