/* ==========================================================================
   Design tokens
   Dark, high-contrast theme with a single warm accent. Built on top of
   Bootstrap 5.3's native dark mode (`data-bs-theme="dark"` on <html>) by
   overriding its CSS custom properties, rather than fighting the light
   theme with overrides everywhere - this keeps free-riding on Bootstrap's
   form/validation styling (bootstrap_5_layout.html.twig) intact.
   ========================================================================== */

:root,
[data-bs-theme='dark'] {
    /* Accent - warm amber, used for primary actions, links, focus rings */
    --accent-400: #f7b171;
    --accent-500: #f2994a;
    --accent-600: #d97f2e;
    --accent-rgb: 242, 153, 74;

    /* Surfaces */
    --surface-0: #0a0a0c;
    /* page background */
    --surface-1: #141417;
    /* card background */
    --surface-2: #1c1c20;
    /* raised / hovered surface */
    --border-subtle: rgba(255, 255, 255, 0.08);
    --border-strong: rgba(255, 255, 255, 0.16);

    --text-primary: #f2f2f0;
    --text-muted: #9a9aa2;

    color-scheme: dark;

    --bs-body-font-family: 'Inter Variable', system-ui, -apple-system, 'Segoe UI', sans-serif;
    --bs-font-sans-serif: 'Inter Variable', system-ui, -apple-system, 'Segoe UI', sans-serif;
    --bs-body-bg: var(--surface-0);
    --bs-body-color: var(--text-primary);
    --bs-secondary-color: var(--text-muted);
    --bs-tertiary-bg: var(--surface-1);
    --bs-secondary-bg: var(--surface-2);
    --bs-border-color: var(--border-subtle);
    --bs-border-color-translucent: var(--border-subtle);

    --bs-border-radius: 0.6rem;
    --bs-border-radius-lg: 1rem;
    --bs-border-radius-sm: 0.4rem;
    --bs-border-radius-xl: 1.25rem;

    --bs-primary: var(--accent-500);
    --bs-primary-rgb: var(--accent-rgb);
    --bs-primary-text-emphasis: var(--accent-400);
    --bs-primary-bg-subtle: #2a1f14;
    --bs-primary-border-subtle: #4a3520;
    --bs-link-color: var(--accent-400);
    --bs-link-color-rgb: var(--accent-rgb);
    --bs-link-hover-color: var(--accent-400);

    --bs-emphasis-color: #fff;
}

body {
    background: radial-gradient(ellipse 80% 50% at 50% -10%, #201a12 0%, transparent 60%), var(--surface-0);
    min-height: 100vh;
}

::selection {
    background: var(--accent-500);
    color: #14100a;
}

/* Off-screen until keyboard-focused, so sighted mouse users never see it but
   keyboard/screen-reader users can jump past the nav (which got a fair bit
   bigger once the tool switcher was added) straight to page content. */
.skip-link {
    position: absolute;
    top: -3rem;
    left: 1rem;
    z-index: 1090;
    padding: 0.6rem 1.1rem;
    border-radius: var(--bs-border-radius);
    background: var(--accent-500);
    color: #14100a;
    font-weight: 600;
    text-decoration: none;
    transition: top 0.15s ease;
}

.skip-link:focus {
    top: 1rem;
}

/* ==========================================================================
   Navbar
   ========================================================================== */

.navbar-app {
    background: rgba(10, 10, 12, 0.85);
    backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--border-subtle);
}

.brand-mark {
    display: inline-flex;
    align-items: center;
    font-weight: 700;
    font-size: 1.05rem;
    letter-spacing: -0.01em;
    color: var(--text-primary) !important;
}

.brand-mark-cos {
    color: var(--text-primary);
}

.brand-mark-faro {
    color: var(--accent-400);
}

.brand-mark-icon {
    width: 2.15rem;
    height: 2.15rem;
    margin-right: 0.55rem;
}

.navbar-app .nav-link {
    color: var(--text-muted);
    font-weight: 500;
}

.navbar-app .nav-link:hover {
    color: var(--text-primary);
}

.nav-tool-switcher-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: var(--bs-border-radius);
    color: var(--text-muted);
    background: transparent;
    border: 1px solid transparent;
    font-size: 1.1rem;
    line-height: 1;
    transition: color .15s ease, background-color .15s ease, border-color .15s ease;
}

.nav-tool-switcher-toggle:hover,
.nav-tool-switcher-toggle:focus-visible,
.nav-tool-switcher-toggle[aria-expanded="true"] {
    color: var(--text-primary);
    background: var(--surface-1);
    border-color: var(--border-subtle);
}

/* Bootstrap's .dropdown-toggle caret isn't used here - the apps-grid icon
   already reads as "switch/open", and a trailing caret would unbalance a
   button this small - so the caret markup is simply never added, rather
   than added and hidden. */

.nav-tool-switcher-menu-icon {
    display: block;
    flex-shrink: 0;
}

.pref-toggle {
    display: inline-flex;
    border: 1px solid var(--border-strong);
    border-radius: var(--bs-border-radius-pill);
    overflow: hidden;
}

.pref-toggle-option {
    /* Reset so this class renders identically whether it's still an <a>
       somewhere or, since the 2026-08 code review moved these to
       CSRF-protected POST forms, a <button> - neither browser's default
       button chrome (border/background) nor its font should show through. */
    appearance: none;
    background: none;
    border: none;
    font: inherit;
    cursor: pointer;
    padding: 0.2rem 0.6rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--text-muted);
    text-decoration: none;
}

.pref-toggle-option:hover {
    color: var(--text-primary);
}

.pref-toggle-option.active {
    background: var(--accent-500);
    color: #14100a;
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
    font-weight: 600;
    letter-spacing: 0.01em;
    padding-inline: 1.15rem;
}

/*
 * Bootstrap's precompiled CSS bakes theme colors into each button variant
 * as literal hex values on these --bs-btn-* custom properties (NOT as
 * var(--bs-primary) references) - overriding the root --bs-primary token
 * alone has no effect on .btn-primary. Every color has to be restated here
 * using our accent palette instead.
 */
.btn-primary {
    --bs-btn-color: #14100a;
    --bs-btn-bg: var(--accent-500);
    --bs-btn-border-color: var(--accent-500);
    --bs-btn-hover-color: #14100a;
    --bs-btn-hover-bg: var(--accent-400);
    --bs-btn-hover-border-color: var(--accent-400);
    --bs-btn-active-color: #14100a;
    --bs-btn-active-bg: var(--accent-600);
    --bs-btn-active-border-color: var(--accent-600);
    --bs-btn-focus-shadow-rgb: var(--accent-rgb);
    --bs-btn-disabled-bg: var(--accent-500);
    --bs-btn-disabled-border-color: var(--accent-500);
}

.btn-outline-primary {
    --bs-btn-color: var(--accent-400);
    --bs-btn-border-color: var(--accent-600);
    --bs-btn-hover-color: #14100a;
    --bs-btn-hover-bg: var(--accent-500);
    --bs-btn-hover-border-color: var(--accent-500);
    --bs-btn-active-color: #14100a;
    --bs-btn-active-bg: var(--accent-500);
    --bs-btn-active-border-color: var(--accent-500);
    --bs-btn-focus-shadow-rgb: var(--accent-rgb);
    --bs-btn-disabled-color: var(--accent-600);
}

.btn-lg {
    padding: 0.75rem 1.75rem;
    border-radius: var(--bs-border-radius-lg);
}

/* ==========================================================================
   Cards / surfaces
   ========================================================================== */

.card,
.panel {
    background: var(--surface-1);
    border: 1px solid var(--border-subtle);
}

.panel {
    border-radius: var(--bs-border-radius-lg);
    padding: 1.75rem;
}

.feature-card {
    background: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--bs-border-radius-lg);
    padding: 1.75rem;
    height: 100%;
    transition: border-color 0.15s ease, transform 0.15s ease;
}

.feature-card:hover {
    border-color: var(--border-strong);
    transform: translateY(-2px);
}

.feature-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: 0.75rem;
    background: var(--bs-primary-bg-subtle);
    color: var(--accent-400);
    font-size: 1.3rem;
    margin-bottom: 1rem;
}

/* The 4 tools' own icon images (as opposed to the generic Bootstrap-icon
   glyphs .feature-icon otherwise holds for unrelated per-feature bullets
   on landing pages) - sized to fill most of the badge rather than sitting
   at glyph scale, since the artwork reads better bigger. */
.feature-icon img {
    width: 2.35rem;
    height: 2.35rem;
}

/* ==========================================================================
   Hero (home page)
   ========================================================================== */

.hero {
    padding: 6rem 0 4rem;
    text-align: center;
}

/* Homepage and all four tool landing pages: hero title sits directly
   below a right-aligned action toolbar and/or directly above a banner
   image, both of which bring their own margin - the plain .hero padding
   above was tuned for standalone use (no toolbar/image immediately
   adjacent) and doubles up with the surrounding <main class="py-5">
   padding here. */
.hero-top {
    padding: 1.5rem 0 2rem;
}

.hero-title {
    font-size: clamp(2.25rem, 5vw, 3.75rem);
    font-weight: 700;
    letter-spacing: -0.03em;
    line-height: 1.08;
    margin-bottom: 1.25rem;
}

.gradient-text {
    background: linear-gradient(90deg, var(--accent-400), #f2c078 60%, #cfe0f5);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.hero-image-wrap {
    max-width: 64rem;
    margin: 3rem auto 0;
}

.hero-image {
    display: block;
    width: 100%;
    height: auto;
    border-radius: var(--bs-border-radius-lg);
    border: 1px solid var(--border-subtle);
    /* The source render's corners are already near-black, close to
       --surface-0 - fading the edges to transparent lets it melt into the
       page background instead of showing a hard rectangular boundary. */
    mask-image: linear-gradient(
        to bottom,
        transparent 0,
        black 2rem,
        black calc(100% - 3rem),
        transparent 100%
    );
}

/* All four tool landing pages (Poster Maker, Prop Scaler, Unfolder,
   Pattern Editor): same visual language as the homepage hero-banner below
   (full-width, rounded, a bottom scrim with a feature list overlaid on
   the image). Each page has its own *_hero_bg.webp, a render
   pre-composited on a dark background so it sits flush in the box with
   no mask needed. Reuses the homepage's .hero-banner-features list
   styling (and its sub-576px hidden state) directly, just in a
   tool-specific wrapper. */
.tool-hero-banner {
    position: relative;
    margin: 3rem auto 0;
    border-radius: var(--bs-border-radius-lg);
    overflow: hidden;
    border: 1px solid var(--border-subtle);
    aspect-ratio: 1915 / 821;
}

.tool-hero-banner-image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.tool-hero-banner-scrim {
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, rgba(10, 10, 12, 0) 35%, rgba(10, 10, 12, .85) 100%);
}

/* Element-qualified (ul.___) so this outranks the later .hero-banner-features
   { padding: 0; } rule below - equal-specificity classes on the same element
   would otherwise let source order silently zero out this padding. */
ul.tool-hero-banner-features {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 0 clamp(1rem, 4vw, 3rem) clamp(0.6rem, 1.5vw, 1.1rem);
}

/* The homepage's six items wrap onto 2-3 lines, which fills each equal
   .hero-banner-features li column edge to edge - the horizontal spacing
   there comes for free from justify-content: space-between. These labels
   are short one-liners (three or four per tool page vs. six on the
   homepage), so the same equal-width columns would leave each one
   left-aligned with a lot of dead space after it. Size each item to its
   own content instead, so space-between distributes gaps between the
   actual icon+text groups the same way it visually reads on the homepage. */
ul.tool-hero-banner-features li {
    flex: 0 0 auto;
}

/* ==========================================================================
   Hero banner (home page, full-width image with text overlay)
   ========================================================================== */

.hero-banner {
    position: relative;
    margin: 2rem auto 0;
    border-radius: var(--bs-border-radius-lg);
    overflow: hidden;
    border: 1px solid var(--border-subtle);
    /* Matches the source render's own proportions (1916x821) so the whole
       scene shows uncropped, exactly as composed - the render already has
       a near-black band above the mannequin's head and a darker workbench
       band at the bottom, which is where the text below is placed to sit
       on, rather than over the busy middle of the image. */
    aspect-ratio: 1916 / 821;
}

.hero-banner-image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
}

.hero-banner-scrim {
    position: absolute;
    inset: 0;
    /* Light touch only - the render's own top/bottom bands already do
       most of the contrast work, this just reinforces them a bit and
       stays fully clear over the middle so the illustration reads at
       full brightness there, same as the reference layout. */
    background: linear-gradient(
        180deg,
        rgba(10, 10, 12, .55) 0%,
        rgba(10, 10, 12, .18) 22%,
        rgba(10, 10, 12, 0) 34%,
        rgba(10, 10, 12, 0) 66%,
        rgba(10, 10, 12, .2) 80%,
        rgba(10, 10, 12, .62) 100%
    );
}

.hero-banner-content {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: center;
    padding: clamp(0.5rem, 1.5vw, 1.25rem) clamp(1rem, 4vw, 3rem) clamp(0.25rem, 0.75vw, 0.5rem);
}

.hero-banner-text {
    text-align: center;
}

.hero-banner-title {
    font-size: clamp(1.15rem, 3.6vw, 2.75rem);
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--text-primary);
    margin-bottom: 0.2em;
    text-shadow: 0 2px 16px rgba(0, 0, 0, .7);
}

.hero-banner-dot {
    color: var(--accent-400);
    margin: 0 0.2em;
}

.hero-banner-subtitle {
    max-width: 46rem;
    margin: 0 auto;
    color: rgba(242, 242, 240, .9);
    font-size: clamp(0.6rem, 1.35vw, 1.05rem);
    line-height: 1.3;
    text-shadow: 0 1px 10px rgba(0, 0, 0, .7);
}

.hero-banner-features {
    list-style: none;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    width: 100%;
    margin: 0;
    padding: 0;
    gap: clamp(0.25rem, 1.5vw, 1rem);
}

.hero-banner-features li {
    display: flex;
    align-items: center;
    gap: 0.5em;
    flex: 1 1 0;
    min-width: 0;
    text-align: left;
}

.hero-banner-features span {
    display: block;
    font-size: clamp(0.5rem, 1vw, 0.8rem);
    font-weight: 600;
    line-height: 1.25;
    color: rgba(242, 242, 240, .92);
    text-shadow: 0 1px 8px rgba(0, 0, 0, .7);
}

.hero-banner-features i {
    flex-shrink: 0;
    color: var(--accent-400);
    font-size: clamp(0.85rem, 1.8vw, 1.4rem);
    text-shadow: 0 1px 8px rgba(0, 0, 0, .7);
}

@media (max-width: 575.98px) {
    /* The locked aspect-ratio leaves ~170px of height at phone width, not
       enough room for the subtitle and six badges to stay legible - drop
       both and keep just the headline as the image's caption. The
       full-text hero right below repeats this same copy accessibly. */
    .hero-banner-subtitle,
    .hero-banner-features {
        display: none;
    }
}

/* ==========================================================================
   Step indicator (wizard pages)
   ========================================================================== */

.step-indicator {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin-bottom: 2.5rem;
    padding: 0;
    list-style: none;
}

.step-indicator li {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

.step-indicator .step-dot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    border: 1px solid var(--border-strong);
    color: var(--text-muted);
    font-size: 0.8rem;
    font-weight: 600;
    background: var(--surface-1);
}

.step-indicator .step-label {
    font-size: 0.85rem;
    color: var(--text-muted);
    margin-right: 0.5rem;
}

.step-indicator .step-connector {
    width: 1.5rem;
    height: 1px;
    background: var(--border-subtle);
    margin-inline: 0.25rem;
}

.step-indicator li.is-complete .step-dot {
    background: var(--accent-500);
    border-color: var(--accent-500);
    color: #14100a;
}

.step-indicator li.is-current .step-dot {
    border-color: var(--accent-500);
    color: var(--accent-400);
}

.step-indicator li.is-current .step-label {
    color: var(--text-primary);
    font-weight: 600;
}

/* ==========================================================================
   Forms
   ========================================================================== */

.form-control,
.form-select {
    background-color: var(--surface-1);
    border-color: var(--border-strong);
    color: var(--text-primary);
    padding: 0.55rem 0.85rem;
}

.form-control:focus,
.form-select:focus {
    background-color: var(--surface-1);
    color: var(--text-primary);
    border-color: var(--accent-500);
    box-shadow: 0 0 0 0.2rem rgba(var(--accent-rgb), 0.25);
}

.form-check-input {
    background-color: var(--surface-1);
    border-color: var(--border-strong);
}

.form-check-input:checked {
    background-color: var(--accent-500);
    border-color: var(--accent-500);
}

.form-label {
    font-weight: 500;
    color: var(--text-muted);
    font-size: 0.9rem;
}

/* ==========================================================================
   Image / canvas preview containers (calibrate + configure)
   ========================================================================== */

.preview-frame {
    display: inline-block;
    position: relative;
    border-radius: var(--bs-border-radius-lg);
    overflow: hidden;
    border: 1px solid var(--border-subtle);
    background: var(--surface-1);
    line-height: 0;
}

.preview-frame img {
    display: block;
    max-width: 100%;
    height: auto;
}

/* Shown while the source image Blob is still being read out of IndexedDB
   and decoded - without a forced min-height here the frame has zero
   content box (the <img> has no src yet) and collapses to nothing, so the
   spinner would have no visible frame to sit inside. */
.preview-frame.is-loading {
    min-width: 260px;
    min-height: 260px;
}

.preview-frame-loading {
    position: absolute;
    inset: 0;
}

input[type="color"].form-control {
    max-width: 4.5rem;
    padding: 0.25rem;
}

.crop-mark-preview-canvas {
    display: block;
    width: 100%;
    max-width: 260px;
    height: auto;
    aspect-ratio: 260 / 180;
    border-radius: var(--bs-border-radius);
    border: 1px solid var(--border-subtle);
}

.grid-drag-surface {
    cursor: grab;
    touch-action: none;
}

.grid-drag-surface.is-grabbing {
    cursor: grabbing;
}

/* Prop Scaler's fit screen: the overlay image is deliberately allowed to
   render larger than its frame (max-width: none) since scaling it past the
   background's own size is a normal part of fitting a prop - .preview-frame's
   overflow: hidden clips it visually rather than the image being capped.
   draggable="false"/-webkit-user-drag are defensive leftovers from an
   earlier attempt at listening for drag directly on this <img> - dragging
   is now handled entirely by .prop-scaler-drag-surface (see below), which
   sidesteps browsers' native image-drag-out gesture entirely rather than
   fighting it, but there's no harm in also telling the browser this image
   itself isn't meant to be dragged. */
.prop-scaler-overlay-img {
    max-width: none;
    -webkit-user-drag: none;
    user-select: none;
    pointer-events: none;
}

/* An otherwise-invisible layer over the whole frame that owns all pointer
   interaction for the fit screen - both dragging (pointerdown) and
   measuring (click) - so neither has to fight the overlay <img>'s native
   drag-out behavior, mirroring tiling_preview_controller.js's own
   canvas-based (not img-based) drag surface. Cursor alone communicates
   which of the two modes is currently active. */
.prop-scaler-drag-surface {
    cursor: grab;
    touch-action: none;
}

.prop-scaler-drag-surface.is-grabbing {
    cursor: grabbing;
}

.prop-scaler-drag-surface.is-measuring {
    cursor: crosshair;
}

/* Sits directly on top of .prop-scaler-drag-surface (later in DOM order,
   so it wins hit-testing) with the exact same left/top/width/height/rotate
   transform as .prop-scaler-overlay-img, kept in sync imperatively in
   prop_scaler_fit_controller.js's render() - a plain child-element
   position/rotate follows its parent's CSS transform for free, so each
   handle only needs a fixed local position within this box, not its own
   rotation math. pointer-events: none here (re-enabled per-handle below)
   lets clicks in the empty parts of the box fall through to the drag
   surface underneath for ordinary move/measure interaction. */
.prop-scaler-handle-layer {
    pointer-events: none;
}

.prop-scaler-handle-layer.is-measuring {
    display: none;
}

.prop-scaler-handle {
    position: absolute;
    width: 12px;
    height: 12px;
    margin: -6px 0 0 -6px;
    border-radius: 2px;
    background: #fff;
    border: 2px solid #0d6efd;
    pointer-events: auto;
    touch-action: none;
}

.prop-scaler-handle-nw { top: 0; left: 0; cursor: nwse-resize; }
.prop-scaler-handle-ne { top: 0; left: 100%; cursor: nesw-resize; }
.prop-scaler-handle-se { top: 100%; left: 100%; cursor: nwse-resize; }
.prop-scaler-handle-sw { top: 100%; left: 0; cursor: nesw-resize; }
.prop-scaler-handle-n { top: 0; left: 50%; cursor: ns-resize; }
.prop-scaler-handle-s { top: 100%; left: 50%; cursor: ns-resize; }
.prop-scaler-handle-e { top: 50%; left: 100%; cursor: ew-resize; }
.prop-scaler-handle-w { top: 50%; left: 0; cursor: ew-resize; }

/* Floats above the box's top-center edge, connected by a thin stem - the
   whole thing rotates for free along with .prop-scaler-handle-layer. */
.prop-scaler-handle-rotate {
    top: -28px;
    left: 50%;
    border-radius: 50%;
    cursor: grab;
}

.prop-scaler-handle-rotate.is-rotating {
    cursor: grabbing;
}

.prop-scaler-handle-rotate::before {
    content: '';
    position: absolute;
    left: 50%;
    top: 100%;
    width: 1px;
    height: 16px;
    background: #0d6efd;
    transform: translateX(-50%);
}

/* ==========================================================================
   Nudge pad (configure page - manual grid offset)
   ========================================================================== */

.nudge-pad {
    display: grid;
    grid-template-columns: repeat(3, 2.25rem);
    grid-template-rows: repeat(3, 2.25rem);
    gap: 0.25rem;
    flex-shrink: 0;
}

.nudge-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--surface-1);
    border: 1px solid var(--border-strong);
    border-radius: var(--bs-border-radius-sm);
    color: var(--text-primary);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}

.nudge-btn:hover {
    border-color: var(--accent-500);
    color: var(--accent-400);
}

.nudge-btn:active {
    background: var(--bs-primary-bg-subtle);
}

.nudge-up {
    grid-column: 2;
    grid-row: 1;
}

.nudge-left {
    grid-column: 1;
    grid-row: 2;
}

.nudge-reset {
    grid-column: 2;
    grid-row: 2;
    font-size: 0.9rem;
}

.nudge-right {
    grid-column: 3;
    grid-row: 2;
}

.nudge-down {
    grid-column: 2;
    grid-row: 3;
}

/* ==========================================================================
   Option groups + tooltips (configure/calibrate forms)
   ========================================================================== */

.option-group + .option-group {
    margin-top: 1.75rem;
    padding-top: 1.75rem;
    border-top: 1px solid var(--border-subtle);
}

.option-group-title {
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 1rem;
}

/* Compact variant for settings panels denser than a typical form (e.g. the
   Unfolder's pattern-layout options and its Blender-style toolbars/
   sidebars) - a toolbar reads as a toolbar through small, tight controls
   with minimal always-on prose, not through a smaller copy of a regular
   form. Overrides Bootstrap's own --bs-*-font-size/padding custom
   properties rather than fighting specificity, since .btn-sm/.form-
   select-sm/.input-group-sm set their sizing that way - EXCEPT for
   padding, which two site-wide rules (.btn's own padding-inline, and
   .form-control/.form-select's own padding) set as literal physical
   properties rather than through Bootstrap's --bs-btn-padding-*-sm
   custom-property mechanism. Those two rules load after Bootstrap's own
   -sm padding at equal-or-lower specificity, so they win regardless of
   which --bs-* custom property a button/control carries - overriding
   just the custom properties here has no visible effect on padding.
   Fixed below by setting padding directly at higher specificity
   (.options-compact <selector>), the only thing that reliably wins. */
.options-compact {
    font-size: 0.6875rem;
}

.options-compact.panel {
    padding: 1.25rem 1.75rem;
}

.options-compact .option-group + .option-group {
    margin-top: 0.65rem;
    padding-top: 0.65rem;
}

.options-compact .option-group-title {
    margin-bottom: 0.3rem;
    font-size: 0.62rem;
}

.options-compact .btn-sm,
.options-compact .btn-group-sm > .btn {
    --bs-btn-font-size: 0.6875rem;
    --bs-btn-padding-y: 0.15rem;
    --bs-btn-padding-x: 0.4rem;
    --bs-btn-border-radius: 0.25rem;
    padding-block: 0.15rem;
    padding-inline: 0.4rem;
    font-size: 0.6875rem;
}

/* Every button site-wide is bold with generous letter-spacing (see .btn's
   own rule above) - reads as a prominent CTA, which is right for a
   marketing page but wrong for a dense tool surface where every row is
   a button; Blender's own toolbar/panel buttons use plain body-weight
   text throughout, nothing shouting for attention. */
.options-compact .btn {
    font-weight: 500;
    letter-spacing: normal;
}

/* Blender's toolbar/panel buttons are flat - a faint fill (barely
   distinct from the panel itself) with NO outline in their resting
   state, only gaining a visible border/highlight on hover or when
   toggled active. Bootstrap's stock .btn-outline-secondary is the
   opposite (a full 1px border always visible, transparent fill) - the
   single biggest reason this still reads as "a web form" rather than
   "a tool panel." Restated via the same --bs-btn-* custom-property
   mechanism .btn-primary/.btn-outline-primary already use above
   (Bootstrap bakes literal colors onto these per variant, so overriding
   the root --bs-secondary token alone has no effect). */
.options-compact .btn-outline-secondary {
    --bs-btn-color: var(--text-primary);
    --bs-btn-bg: var(--surface-2);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-color: var(--text-primary);
    --bs-btn-hover-bg: var(--surface-2);
    --bs-btn-hover-border-color: var(--border-strong);
    --bs-btn-active-color: var(--accent-400);
    --bs-btn-active-bg: rgba(var(--accent-rgb), 0.18);
    --bs-btn-active-border-color: var(--accent-500);
    --bs-btn-focus-shadow-rgb: var(--accent-rgb);
    --bs-btn-disabled-color: var(--text-muted);
    --bs-btn-disabled-bg: transparent;
    --bs-btn-disabled-border-color: transparent;
}

/* Same "flat, not bordered" idea as the buttons above, applied to fields -
   Bootstrap's default border-color (--border-strong) is the same weight
   whether the field sits in a roomy form or a dense toolbar; against
   this sidebar's identical-color panel background it's the field's ONLY
   visual definition, so it has to stay fairly visible - Blender solves
   this differently, giving fields a darker, recessed fill (looks "sunken"
   into the panel) so the boundary reads from the fill contrast instead of
   a bright outline, and the border itself can drop to barely-there. */
.options-compact .form-select,
.options-compact .form-control {
    background-color: var(--surface-0);
    border-color: var(--border-subtle);
}

.options-compact .form-select-sm {
    font-size: 0.6875rem;
    border-radius: 0.25rem;
    /* Right side stays roomy - the base .form-select's own dropdown-arrow
       icon (a background-image, not a real child element) is positioned
       at a fixed offset from the right edge regardless of padding, so a
       uniformly tight padding here would visually collide with it. Fixed
       in rem (root-relative), not em, so it stays anchored to that same
       pixel offset regardless of how small this rule's own font-size
       gets - only the other 3 sides track the tighter scale. */
    padding: 0.15rem 1.6rem 0.15rem 0.35rem;
}

.options-compact .input-group-sm > .form-control,
.options-compact .input-group-sm > .input-group-text,
.options-compact .form-control-sm {
    font-size: 0.6875rem;
    border-radius: 0.25rem;
    padding: 0.15rem 0.35rem;
}

.options-compact .input-group-sm > .input-group-text {
    background-color: var(--surface-2);
    border-color: var(--border-subtle);
    color: var(--text-muted);
}

/* Toolbar rows (measure/scale/reset-view above the 3D view; select/pan/
   rotate + zoom above the pattern layout) reuse gap-2 for spacing between
   buttons, same as the sidebar's own control rows - tightened here so the
   whole tool surface (toolbar + sidebar) reads as one consistently dense
   unit rather than two different densities. */
.options-compact .gap-2 {
    gap: 0.3rem;
}

/* .tooltip-icon is a shared site-wide class (login, calibrate, upload
   pages) - scoped down here rather than shrinking its own base rule,
   which those other, roomier forms should keep as-is. */
.options-compact .tooltip-icon {
    width: 0.9rem;
    height: 0.9rem;
    font-size: 0.58rem;
}

/* Pattern Editor's menu bar (File/Edit/Object/View) - a flat, borderless
   look (no visible button chrome until hover/open), the same "reads as a
   tool surface, not a web form" idea .options-compact .btn-outline-
   secondary already applies elsewhere, just for a plain .btn with no
   color variant (Bootstrap's bare .btn has no background/border of its
   own to override). */
.editor-menu-bar .btn {
    background: transparent;
    border-color: transparent;
    color: var(--text-primary);
}

.editor-menu-bar .btn:hover,
.editor-menu-bar .btn:focus,
.editor-menu-bar .btn.show {
    background: var(--surface-2);
    border-color: var(--border-subtle);
}

/* Bootstrap's own .dropdown-menu-dark ships a hardcoded generic dark
   palette (its own $dark variable), not this site's theme tokens -
   remapped here so the menu bar's dropdowns match the rest of the dark
   theme exactly rather than a slightly-different generic gray. */
.editor-menu-bar .dropdown-menu-dark {
    --bs-dropdown-bg: var(--surface-1);
    --bs-dropdown-border-color: var(--border-subtle);
    --bs-dropdown-link-color: var(--text-primary);
    --bs-dropdown-link-hover-color: var(--text-primary);
    --bs-dropdown-link-hover-bg: var(--surface-2);
    --bs-dropdown-link-disabled-color: var(--text-muted);
    --bs-dropdown-divider-bg: var(--border-subtle);
    font-size: 0.75rem;
}

/* Hover flyout submenu for menu-bar dropdowns (e.g. Object > Alignment) -
   a plain nested <ul class="dropdown-menu"> inside the trigger <li>,
   revealed by CSS :hover alone rather than Bootstrap's own JS (which has
   no concept of nested dropdowns) - the outer dropdown stays open the
   whole time since it's still the one Bootstrap is managing. */
.editor-menu-bar .dropdown-submenu {
    position: relative;
}

.editor-menu-bar .dropdown-submenu > .dropdown-menu {
    top: -0.3rem;
    left: 100%;
    margin-left: 0.1rem;
}

.editor-menu-bar .dropdown-submenu:hover > .dropdown-menu {
    display: block;
}

.editor-options-bar {
    padding-bottom: 0.4rem;
    border-bottom: 1px solid var(--border-subtle);
}

/* Shared slot for Pattern Editor's mutually-exclusive options-bar rows
   (polygon sides / freehand smoothness / stroke style / text style - see
   the Twig template's own doc on this class) - min-height matches one
   row's own rendered height (.editor-options-bar's compact controls +
   its own padding-bottom/border above), so switching which row is
   showing, or going from none to one, no longer changes this slot's
   total height and shunts the tool-rail/canvas/side-panel row below it
   up or down (a real, reported bug, 2026-09) - each row still collapses
   to nothing via its own d-none when hidden, this floor just keeps the
   SLOT from collapsing along with it. */
.pattern-editor-options-bar-slot {
    /* Measured at ~33.9px for both the stroke and text-align rows (2026-09)
       - a couple px of headroom above that so sub-pixel font-rendering
       differences across browsers/zoom never make the slot come up just
       short and reintroduce the jump this exists to prevent. */
    min-height: 2.15rem;
}

/* The tool rail: a narrow icon column on large screens (flex-lg-column,
   set in the Twig), collapsing to a horizontal row on narrower ones via
   the same flex-row/flex-lg-column breakpoint pair the canvas/sidebar
   split already uses elsewhere in this template - no separate JS/
   Stimulus logic needed for the responsive collapse, purely the existing
   Bootstrap breakpoint utility. */
.editor-tool-rail {
    background: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--bs-border-radius);
    padding: 0.4rem;
    flex-wrap: wrap;
}

.editor-tool-rail .btn {
    width: 2rem;
    height: 2rem;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* Rectangle/Ellipse/Triangle/Polygon share one flyout slot in the rail
   (see the Twig template's own pattern-editor-shape-tool-group) - the
   group itself is the position:relative anchor the flyout (absolute)
   pops out from, sized to exactly its main button so it takes no extra
   space in the rail's own flex layout. */
.pattern-editor-shape-tool-group {
    position: relative;
    display: inline-flex;
}

.pattern-editor-shape-tool-main {
    position: relative;
}

/* Purely a passive "there's more here" affordance - the interaction
   itself is the main button's own press-and-hold (see
   shapeToolPressStart()'s own doc), not a click target of its own, so
   this stays out of the hit-testing/pointer-event path entirely. */
.pattern-editor-shape-tool-caret {
    position: absolute;
    right: 1px;
    bottom: 1px;
    font-size: 0.4rem;
    opacity: 0.75;
    pointer-events: none;
}

/* Pops open BELOW the main button (not to the side) so it reads
   correctly whether the rail is a vertical column (large screens) or a
   horizontal row (narrow ones, see .editor-tool-rail's own
   responsive doc) - a sideways flyout would run off-canvas in the
   narrow/horizontal layout. */
.pattern-editor-shape-tool-flyout {
    position: absolute;
    top: 100%;
    left: 0;
    margin-top: 0.3rem;
    display: flex;
    flex-direction: row;
    gap: 0.2rem;
    background: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--bs-border-radius);
    padding: 0.3rem;
    z-index: 20;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}

.editor-tool-rail-divider {
    width: 100%;
    margin: 0.3rem 0;
    border-color: var(--border-subtle);
    opacity: 1;
}

/* On the horizontal (narrow-viewport) layout the divider should run
   vertically between the tools and the swatches instead of splitting the
   row in two - flex-lg-column above only changes the RAIL's own
   direction, this keeps the divider visually correct in both. */
@media (max-width: 991.98px) {
    .editor-tool-rail-divider {
        width: 1px;
        height: 2rem;
        margin: 0 0.3rem;
    }
}

.pattern-editor-current-color-swatches {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.pattern-editor-swatch-label {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    font-size: 0.58rem;
    color: var(--text-muted);
    margin: 0;
}

.pattern-editor-swatch-label input[type='color'] {
    width: 1.6rem;
    height: 1.4rem;
    padding: 0;
    border: 1px solid var(--border-subtle);
    border-radius: 0.2rem;
    background: transparent;
    cursor: pointer;
}

/* Right-panel collapsible sections (Transform/Path Tools/Appearance/Tile
   Printing) - the fix for the panel "getting crowded fast" (a real user
   report): Illustrator/Photoshop both organize their own side panels as
   independently-titled, collapsible sections rather than one long
   scrolling list under a single "Properties" heading, so this mirrors
   that same structural convention (not their specific icon artwork/
   colors - same "adopt the structure, not the skin" approach the menu
   bar redesign already established). Uses Bootstrap's own real .collapse
   JS component (data-bs-toggle="collapse") for the expand/collapse
   mechanics - same deliberate exception to this codebase's own "hand-
   roll everything in Stimulus" convention the menu bar's dropdowns
   already made, for the same reason: already loaded, zero bundle cost,
   well-tested open/close + ARIA state management, and this is genuinely
   new interaction surface no existing Stimulus code already handled. */
.editor-panel-section + .editor-panel-section {
    margin-top: 0.7rem;
    padding-top: 0.6rem;
    border-top: 1px solid var(--border-subtle);
}

/* Pattern Editor's own sidebar (pattern_editor_local/edit.html.twig)
   follows its .editor-panel-section accordion sections with plain
   .option-group elements - same divider treatment as between two panel
   sections, so the visual rhythm stays consistent across the handoff. */
.editor-panel-section + .option-group {
    margin-top: 0.7rem;
    padding-top: 0.6rem;
    border-top: 1px solid var(--border-subtle);
}

.editor-section-header {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    width: 100%;
    background: transparent;
    border: none;
    padding: 0.15rem 0;
    color: var(--text-primary);
    font-size: 0.65rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    cursor: pointer;
}

.editor-section-header:hover {
    color: var(--accent-400);
}

.editor-section-header .editor-section-icon {
    font-size: 0.8rem;
    color: var(--text-muted);
}

.editor-section-chevron {
    margin-left: auto;
    font-size: 0.7rem;
    color: var(--text-muted);
    transition: transform 0.15s ease;
}

/* Bootstrap's own collapse JS keeps aria-expanded in sync on the trigger
   button automatically - no Stimulus code needed to drive this rotation. */
.editor-section-header[aria-expanded='false'] .editor-section-chevron {
    transform: rotate(-90deg);
}

.editor-panel-section .collapse.show,
.editor-panel-section .collapsing {
    margin-top: 0.5rem;
}

/* Path Tools sub-groups (Seams & Notches / Corners & Offset / Symmetry) -
   the first collapsible-sections pass above still left "Path Tools"
   itself a flat list of 7 unrelated features (fold lines, bevel, seam
   allowance, notches, corner rounding, offset path, symmetry), each with
   its own always-visible instructional paragraph - a follow-up user
   report ("packed underneath each other with no layout"). These give
   each cluster its own togglable header WITHIN Path Tools, deliberately
   lighter-weight than .editor-section-header above (muted by default
   rather than text-primary, smaller icon, no letter-spacing bump) so the
   hierarchy reads as section > subsection rather than two rows of
   equal-weight headers. */
.editor-panel-subsection + .editor-panel-subsection {
    margin-top: 0.55rem;
    padding-top: 0.5rem;
    border-top: 1px dashed var(--border-subtle);
}

.editor-panel-subsection-header {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    width: 100%;
    background: transparent;
    border: none;
    padding: 0.1rem 0;
    color: var(--text-muted);
    font-size: 0.62rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    cursor: pointer;
}

.editor-panel-subsection-header:hover {
    color: var(--accent-400);
}

.editor-panel-subsection-icon {
    font-size: 0.72rem;
}

.editor-panel-subsection-chevron {
    margin-left: auto;
    font-size: 0.65rem;
    transition: transform 0.15s ease;
}

.editor-panel-subsection-header[aria-expanded='false'] .editor-panel-subsection-chevron {
    transform: rotate(-90deg);
}

.editor-panel-subsection .collapse.show,
.editor-panel-subsection .collapsing {
    margin-top: 0.4rem;
}

/* Photoshop-style rulers: a CSS grid inset into canvasHolder (20px top row
   for the horizontal ruler, 20px left column for the vertical one, a
   corner square where they meet) with the real drawing canvas occupying
   the remaining cell - the rulers sit at the canvas's own inner edge,
   not as separate outer chrome, per the user's own explicit request.
   Hiding rulers collapses both to 0 via .editor-ruler-grid.
   rulers-hidden rather than removing the elements, so toggling doesn't
   need to touch canvas sizing logic beyond the one handleResize() call
   already triggered on toggle. */
.editor-ruler-grid {
    display: grid;
    grid-template-columns: 20px 1fr;
    grid-template-rows: 20px 1fr;
    width: 100%;
    height: 100%;
}

.editor-ruler-grid.editor-rulers-hidden {
    grid-template-columns: 0 1fr;
    grid-template-rows: 0 1fr;
}

.editor-ruler-corner,
.editor-ruler-h,
.editor-ruler-v {
    background: var(--surface-1);
    overflow: hidden;
}

.editor-ruler-corner {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.55rem;
    color: var(--text-muted);
    border-right: 1px solid var(--border-subtle);
    border-bottom: 1px solid var(--border-subtle);
}

.editor-ruler-h {
    width: 100%;
    height: 100%;
    display: block;
    cursor: ns-resize;
    border-bottom: 1px solid var(--border-subtle);
}

.editor-ruler-v {
    width: 100%;
    height: 100%;
    display: block;
    cursor: ew-resize;
    border-right: 1px solid var(--border-subtle);
}

.editor-canvas-viewport {
    position: relative;
    min-width: 0;
    min-height: 0;
}

/* In-canvas inline text/label editor overlay - see
   pattern_editor_canvas_controller.ts's own enterInlineTextEdit() doc for
   why this deliberately doesn't try to match the real curated font (only
   font-weight/font-style, which need no font file). Position/size are set
   inline (repositionInlineTextEdit()), computed from the live object's own
   mm bounding box + viewport pan/zoom - this rule only covers the parts
   that never change per-object. Same '#22d3ee' guide-blue accent as the
   drag-to-draw preview outline (DragTextDrawState's own doc) so the two
   halves of the same gesture (draw the box, then type into it) read as
   one continuous piece of UI chrome.

   The overlay's own TEXT is deliberately invisible (`color: transparent`)
   - the WebGPU canvas underneath already re-renders the real object live
   on every keystroke (see inlineTextEditInput()'s own doc), so a real,
   native-rendered `<textarea>` sitting exactly on top of it would show
   BOTH renderings at once, in two different fonts, visibly clashing (a
   real bug this fixed - confirmed live: the canvas's own glyphs plus the
   textarea's own native text, and its native blue selection highlight
   right after `.select()` on entry, all overlapping). `caret-color` is
   set explicitly since it would otherwise inherit from (and vanish with)
   `color` - this keeps the blinking cursor visible even though the text
   itself isn't. `::selection` gets its own explicit, app-consistent tint
   for the same reason - the browser's own default selection color varies
   and would otherwise still show as stray colored text-shaped patches
   with `color: transparent` alone (the highlight box itself isn't
   affected by `color`, only the glyphs drawn on top of it are). */
.pattern-editor-inline-text-edit {
    position: absolute;
    left: 0;
    top: 0;
    resize: none;
    overflow: hidden;
    padding: 0;
    margin: 0;
    border: 1px dashed #22d3ee;
    background: transparent;
    color: transparent;
    -webkit-text-fill-color: transparent;
    caret-color: #1a1a1a;
    /* text.rs's own layout_line_resolved() is a naive per-character
       advance-width sum, no kerning/ligature application - matching
       that here (rather than leaving the browser's own default shaping
       on) keeps the overlay's invisible layout, and therefore its
       native caret/selection/wrap positioning, as close as possible to
       what actually renders on canvas (see
       pattern_editor_canvas_controller.ts's own syncInlineTextEditDomFont()
       doc for the rest of that fix). */
    font-kerning: none;
    font-variant-ligatures: none;
    line-height: 1.2;
    outline: none;
    z-index: 5;
}

.pattern-editor-inline-text-edit::selection {
    background: rgba(34, 211, 238, 0.35);
}

/* Chain-link group indicator (see the Twig template's own doc, and
   updateGroupIndicatorOverlay() in pattern_editor_canvas_controller.ts) -
   a real icon-font glyph, not a WebGPU marker, so its own size is a
   plain fixed CSS value rather than a world-mm one; only its POSITION
   (left/top, set inline by the controller) tracks the live selection in
   world space. A white drop-shadow keeps it legible over both light
   pattern fills and this app's own dark canvas background alike, the
   same problem every marker drawn straight over arbitrary document
   content has - the WebGPU markers solve it with an opaque halo shape
   (makeHandleMarkerHalo()), this is that same idea via a CSS filter
   since there's no separate halo element to add here. pointer-events:
   none so it never intercepts a click meant for the canvas beneath it -
   same convention .editor-brand-watermark already uses. */
.pattern-editor-group-indicator {
    position: absolute;
    left: 0;
    top: 0;
    transform: translate(-50%, -50%);
    font-size: 1.3rem;
    color: #2d8cff;
    filter: drop-shadow(0 0 2px #fff) drop-shadow(0 0 2px #fff);
    pointer-events: none;
    z-index: 6;
}

.tooltip-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.05rem;
    height: 1.05rem;
    flex-shrink: 0;
    border-radius: 50%;
    border: 1px solid var(--border-strong);
    color: var(--text-muted);
    font-size: 0.65rem;
    font-weight: 700;
    line-height: 1;
    cursor: help;
}

.tooltip-icon:hover,
.tooltip-icon:focus-visible {
    border-color: var(--accent-500);
    color: var(--accent-400);
    outline: none;
}

/* ==========================================================================
   Dropzone (upload page)
   ========================================================================== */

.dropzone {
    position: relative;
    border: 2px dashed var(--border-strong);
    border-radius: var(--bs-border-radius-lg);
    padding: 3rem 1.5rem;
    text-align: center;
    cursor: pointer;
    background: var(--surface-1);
    transition: border-color 0.15s ease, background-color 0.15s ease;
}

.dropzone:hover {
    border-color: var(--accent-600);
}

.dropzone.is-dragover {
    border-color: var(--accent-500);
    background: var(--bs-primary-bg-subtle);
}

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

.dropzone-icon {
    font-size: 2.25rem;
    line-height: 1;
    margin-bottom: 0.75rem;
}

.dropzone-browse {
    color: var(--accent-400);
    text-decoration: underline;
}

.dropzone-preview {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;
}

.dropzone-thumb {
    width: 3.5rem;
    height: 3.5rem;
    object-fit: cover;
    border-radius: var(--bs-border-radius);
    border: 1px solid var(--border-subtle);
    background: var(--surface-2);
}

/* ==========================================================================
   Footer
   ========================================================================== */

.footer-app {
    border-top: 1px solid var(--border-subtle);
    padding: 2rem 0;
    color: var(--text-muted);
    font-size: 0.875rem;
}

.footer-app a {
    color: var(--text-muted);
}

.footer-app a:hover {
    color: var(--text-primary);
}

/* ==========================================================================
   Misc
   ========================================================================== */

.text-muted-app {
    color: var(--text-muted) !important;
}

.mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-variant-numeric: tabular-nums;
}

.table {
    --bs-table-bg: transparent;
    --bs-table-color: var(--text-primary);
    --bs-table-border-color: var(--border-subtle);
}

/* Strip the default disclosure triangle so <summary> reads as a plain
   icon-button trigger for the inline rename form on my_projects. */
.rename-toggle {
    display: inline-block;
    cursor: pointer;
    list-style: none;
    color: var(--text-muted);
}

.rename-toggle:hover {
    color: var(--accent-400);
}

.rename-toggle::-webkit-details-marker {
    display: none;
}

.calibration-tips summary {
    cursor: pointer;
    list-style: none;
}

.calibration-tips summary:hover {
    color: var(--text-primary);
}

.calibration-tips summary::-webkit-details-marker {
    display: none;
}

.calibration-tips ul {
    padding-left: 1.25rem;
}

.project-thumbnail {
    width: 48px;
    height: 48px;
    object-fit: cover;
    border-radius: 6px;
    background-color: var(--border-subtle);
}

.project-thumbnail-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.25rem;
    color: var(--text-muted);
}

.project-thumbnail-placeholder img {
    width: 1.75rem;
    height: 1.75rem;
}

/* Small per-row indicator on my_projects distinguishing which CosFaro tool
   a project belongs to (Poster Maker vs Prop Scaler) - deliberately just
   an icon, not a text label, to keep the row compact; the tooltip carries
   the actual name. */
.tool-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 0.5rem;
    background: var(--bs-primary-bg-subtle);
    color: var(--accent-400);
    font-size: 0.9rem;
    flex-shrink: 0;
}

.tool-badge img {
    width: 1.5rem;
    height: 1.5rem;
}

/* Icon-only row actions (View/Continue/Duplicate/Delete) on my_projects -
   same muted-until-hover treatment as .rename-toggle/.nudge-btn, so every
   icon-button across the app behaves consistently. .text-danger on the
   delete action overrides the hover color too, via the more specific
   selector below, so it stays reachable as "danger" red rather than
   flipping to the accent color like the others. */
.icon-action {
    color: var(--text-muted);
    font-size: 1.1rem;
    line-height: 1;
    text-decoration: none;
}

.icon-action:hover {
    color: var(--accent-400);
}

.icon-action.text-danger:hover {
    color: var(--bs-danger) !important;
}

/* Bootstrap's modal ships light-theme defaults; remapped onto this
   site's own tokens. Unscoped (not tied to one modal id) - used by
   every "resume a local project" landing-page list's delete-confirm
   modal (Pattern Editor/Poster Maker/Unfolder/Prop Scaler landing
   pages), and available for any future modal without needing its own
   copy of this override. */
.modal-content {
    background: var(--surface-1);
    border: 1px solid var(--border-subtle);
    color: var(--text-primary);
}
.modal-header,
.modal-footer {
    border-color: var(--border-subtle);
}
.btn-close {
    filter: invert(1) grayscale(1) brightness(1.5);
}

/* Settings-row layout for modal-based preference panels (Unfolder's own
   Preferences modal today - unscoped so any future settings modal can
   reuse it without its own copy). Replaces the sidebar-era pattern of
   cramming a whole section's controls into one flex-wrap row, which
   reflows unpredictably once a conditional field (a material preset) or a
   longer translated label changes how much fits per line - one row per
   setting reads the same regardless. */
.settings-section + .settings-section {
    margin-top: 1.25rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--border-subtle);
}

.settings-section-header {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    margin-bottom: 0.15rem;
}

.settings-section-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.6rem;
    height: 1.6rem;
    border-radius: var(--bs-border-radius-sm);
    background: rgba(var(--accent-rgb), 0.14);
    color: var(--accent-400);
    font-size: 0.8rem;
    flex-shrink: 0;
}

.settings-section-title {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-primary);
}

.settings-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.4rem 1rem;
    padding: 0.6rem 0 0.6rem 2.15rem;
}

.settings-row + .settings-row {
    border-top: 1px solid var(--border-subtle);
}

/* A row that only exists beneath its parent setting (a material preset/
   thickness field shown only for the matching mode) - sits closer to the
   row above with no divider of its own, so it reads as refining that
   setting rather than as a new, equally-weighted one. Declared after the
   adjacent-sibling rule above so it wins at equal specificity. */
.settings-row.settings-row-sub {
    padding-top: 0.4rem;
    padding-left: 3.15rem;
    border-top: none;
}

/* Each row's label is a real <label for="...">, not just adjacent text -
   clicking it focuses (or, for the export-tabs checkbox, toggles) its
   control, the same affordance a native form gives for free. */
.settings-row-label {
    font-size: 0.8rem;
    color: var(--text-primary);
    flex: 1 1 auto;
    min-width: 8rem;
    cursor: pointer;
}

.settings-row-control {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex: 0 0 auto;
}

/* ==========================================================================
   Editor shell (shared by Unfolder, Pattern Editor, and any future tool
   using the same menu-bar/tool-rail/canvas/side-panel chrome)
   ========================================================================== */

/* Every tool's fullscreenRoot div carries this class alongside its own
   Stimulus data-target (data-unfolder-target="fullscreenRoot",
   data-pattern-editor-canvas-target="fullscreenRoot", ...) - the class
   drives shared styling, the data-target stays per-controller for
   toggleFullscreen()/handleFullscreenChange() to find it. One shared rule
   here replaces what used to be two near-identical [data-X-target=
   "fullscreenRoot"]:fullscreen blocks (#1). Each tool's own canvas/sidebar
   height budget still varies (how many persistent rows sit above the
   canvas differs per tool), so that part stays a per-tool rule below. */
.editor-fullscreen-root {
    position: relative;
}

.editor-fullscreen-root:fullscreen {
    background: var(--bg-page, #0b0b0c);
    padding: 1rem;
    overflow: auto;
}

/* Full-screen editors (Unfolder, Pattern Editor): local-storage durability
   overlay - see editor_storage_overlay_controller.ts's own doc for why
   this shows by default over the whole editor (menu bar included) on
   every fresh visit rather than being a one-time-forever dismiss.
   Positioned absolute against .editor-fullscreen-root's own
   position:relative above (same technique as .editor-brand-watermark)
   rather than a page-level Bootstrap .modal, so it renders identically
   in windowed and native-fullscreen mode - Bootstrap's own modal injects
   its backdrop onto <body>, which sits outside the fullscreen element
   and simply wouldn't paint while fullscreen is active. */
.editor-storage-overlay {
    position: absolute;
    inset: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    background: rgba(10, 10, 12, 0.72);
}

.editor-storage-overlay-card {
    max-width: 30rem;
    width: 100%;
    background: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--bs-border-radius-lg);
    padding: 1.25rem 1.5rem;
    box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.5);
}

/* Reopens .editor-storage-overlay above once dismissed - anchored to the
   bottom-right corner of the canvas/viewport itself (each canvas holder
   already has position:relative - see the inline style on
   viewportHolder/layoutHolder/canvasHolder), not the menu bar, so it sits
   right where the user is actually looking rather than up in the toolbar
   row. Sized and colored to read as a floating status badge rather than
   another toolbar button - deliberately larger than the menu bar's own
   .btn-sm icon buttons (2.75rem vs. ~2rem) and circular so the shape
   itself signals "this is a HUD element, not part of the button row." */
.editor-canvas-storage-trigger {
    position: absolute;
    bottom: 0.75rem;
    right: 0.75rem;
    z-index: 10;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--surface-1);
    border: 1px solid var(--accent-600);
    color: var(--accent-400);
    font-size: 1.3rem;
    line-height: 1;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
    transition: background-color 0.15s ease, transform 0.15s ease;
}

.editor-canvas-storage-trigger:hover,
.editor-canvas-storage-trigger:focus-visible {
    background: var(--surface-2);
    transform: translateY(-1px);
}

/* Unfolder: tabs-only header above the canvas (8rem budget). */
[data-unfolder-target="fullscreenRoot"]:fullscreen [data-unfolder-target="viewportHolder"],
[data-unfolder-target="fullscreenRoot"]:fullscreen [data-unfolder-target="layoutHolder"],
[data-unfolder-target="fullscreenRoot"]:fullscreen [data-unfolder-target="outlinerPanel"],
[data-unfolder-target="fullscreenRoot"]:fullscreen .unfolder-layout-sidebar {
    height: calc(100vh - 8rem) !important;
}

/* Pattern Editor: unlike Unfolder's single fixed-height header above, the
   number of persistent rows above the canvas varies (0 or 1 options-bar
   row depending on tool/selection - see .pattern-editor-options-bar-slot's
   own doc), so a fixed rem budget here would either leave dead space (bar
   hidden) or clip/scroll (bar wrapped to 2 lines on a narrow window) - a
   real, reported bug (2026-09). Flexbox measures the actual remaining
   space instead: canvas-pane and main-row both grow/shrink to fill
   whatever their own fixed-height siblings (menu bar, options-bar slot,
   the status/hint paragraphs below the canvas) don't use, then the tool
   rail/canvas/side panel stretch to fill THAT exactly via height: 100%.
   min-height: 0 overrides flex items' own default min-height:auto (which
   would otherwise refuse to shrink below their content's natural size and
   defeat the whole "fill exactly what's left" point). Scoped to this
   tool's own fullscreenRoot attribute selector, not the shared
   .editor-fullscreen-root class, so Unfolder's own budget-based rule
   above is untouched. */
[data-pattern-editor-canvas-target="fullscreenRoot"]:fullscreen {
    display: flex;
    flex-direction: column;
}

[data-pattern-editor-canvas-target="fullscreenRoot"]:fullscreen .pattern-editor-canvas-pane {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

[data-pattern-editor-canvas-target="fullscreenRoot"]:fullscreen .pattern-editor-main-row {
    flex: 1 1 auto;
    min-height: 0;
}

[data-pattern-editor-canvas-target="fullscreenRoot"]:fullscreen [data-pattern-editor-canvas-target="canvasHolder"],
[data-pattern-editor-canvas-target="fullscreenRoot"]:fullscreen .editor-side-panel,
[data-pattern-editor-canvas-target="fullscreenRoot"]:fullscreen .editor-tool-rail {
    height: 100% !important;
}

/* Illustrator-style corner brand tag (see
   templates/_editor_brand_watermark.html.twig) - a small solid footer
   chip, not a translucent floating watermark: opaque surface color, a
   border on its top/left edges the way a corner-docked panel would have
   one, anchored to the bottom-right corner of .editor-fullscreen-root's
   own position:relative above. Sits in the same DOM node in both normal
   layout and native fullscreen, so it's visible either way.
   pointer-events:none so it never intercepts clicks meant for the canvas
   beneath it. */
.editor-brand-watermark {
    position: absolute;
    bottom: 0;
    right: 0;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.3rem 0.6rem;
    border-top: 1px solid var(--border-subtle);
    border-left: 1px solid var(--border-subtle);
    border-top-left-radius: var(--bs-border-radius);
    background: var(--surface-1);
    pointer-events: none;
    z-index: 5;
}

.editor-brand-watermark-icon {
    width: 1rem;
    height: 1rem;
    border-radius: 0.25rem;
}

.editor-brand-watermark-text {
    font-weight: 700;
    font-size: 0.65rem;
    letter-spacing: -0.01em;
}

/* Fullscreen toggle for tools with no menu bar to park it in (see
   _editor_shell.html.twig's own editorShowMenuBar doc) - top-right corner
   of .editor-fullscreen-root, mirroring the watermark's own bottom-right
   placement so the two don't compete for the same corner. */
.editor-fullscreen-toggle-standalone {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    z-index: 5;
}

/* Shared box chrome for Unfolder's Blender-style side panels - the 3D
   viewport tab's Parts/Properties sidebar (.unfolder-outliner) and the
   Pattern layout tab's always-visible settings sidebar
   (.unfolder-layout-sidebar) both compose this rather than duplicating
   the same background/border/radius declarations. */
.editor-side-panel {
    display: flex;
    flex-direction: column;
    background: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--bs-border-radius);
    overflow: hidden;
}

/* Pattern Editor's own side panel: unlike the single shared scroll region
   it used to be (Layers list + Transform/Path Tools/Appearance all one
   flex-column overflow-y:auto), a long Layers list would push Appearance
   further down and out of view before scrolling reached it - the same
   "everything crowds into one column" complaint the panel got generally.
   Split into 3 independently-scrolling regions instead, the same
   Outliner-over-Properties split .unfolder-outliner-parts/-properties
   already use for Unfolder's own sidebar (see that pair's own doc) -
   Layers keeps a capped share of the height so it can't starve
   Properties, Properties gets the rest, and the status/hint footer stays
   pinned below both rather than scrolling away with either. The 38% cap
   below is only the DEFAULT, unset share - a reported follow-up ("the
   space for layers is getting rather small") added the drag handle just
   below this rule, which pins an explicit pixel height via inline style
   (higher specificity than this class, so it wins once set) instead. */
.pattern-editor-side-panel-layers {
    /* flex-basis:auto (content height), NOT a fixed % - a short layer
       list (the common case) should sit at its own natural height, not
       reserve a third of the panel as dead space; max-height is what
       actually stops a LONG list from starving Properties below. */
    flex: 0 1 auto;
    max-height: 38%;
    min-height: 0;
    overflow-y: auto;
    padding: 0.6rem 0.6rem 0;
}

/* Drag handle between Layers and Properties (layersPanelResizePointerDown()'s
   own doc, pattern_editor_canvas_controller.ts) - a thin hit-target (taller
   than the visible line via negative margin, same "generous hit area,
   thin visible mark" idea a browser's own column-resize handles use) so
   it's easy to grab without feeling fat at rest. Replaces the plain
   border-top .pattern-editor-side-panel-properties used to carry - this
   IS that divider line now, just interactive. */
.pattern-editor-side-panel-resize-handle {
    flex-shrink: 0;
    height: 9px;
    margin: -4px 0;
    position: relative;
    z-index: 2;
    cursor: row-resize;
    touch-action: none;
}

.pattern-editor-side-panel-resize-handle::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 4px;
    height: 1px;
    background: var(--border-subtle);
}

.pattern-editor-side-panel-resize-handle:hover::after,
.pattern-editor-side-panel-resize-handle:focus-visible::after,
.pattern-editor-side-panel-resize-handle.pe-resizing::after {
    top: 3px;
    height: 3px;
    background: var(--accent-500);
    border-radius: 2px;
}

.pattern-editor-side-panel-resize-handle:focus-visible {
    outline: none;
}

.pattern-editor-side-panel-properties {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 0.6rem;
}

.pattern-editor-side-panel-footer {
    flex-shrink: 0;
    padding: 0.5rem 0.6rem 0.6rem;
    border-top: 1px solid var(--border-subtle);
}

/* Small-caps section label matching .editor-section-header's own
   typography (uppercase, letter-spaced, muted) - for editor-side-panel
   content that doesn't need a full collapsible section (Poster Maker/Prop
   Scaler's own sidebars have too few controls to warrant an accordion),
   just the same "this is a tool panel" visual language. */
.editor-side-panel-title {
    font-size: 0.65rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--text-muted);
    margin-bottom: 0.75rem;
}

/* Dark work-surface a preview image/canvas sits centered on, the same
   "canvas surrounded by tool chrome" composition Unfolder's own
   viewportHolder and Pattern Editor's own canvasHolder use (fixed height,
   dark surface, centered content) - see poster_local/calibrate.html.twig
   and friends. Unlike those two, the content here (.preview-frame) keeps
   its own natural, aspect-ratio-preserving size rather than filling the
   holder - it's a photo being calibrated/positioned, not a pannable/
   zoomable canvas, so it should just sit centered and shrink to fit
   rather than stretching to fill. */
.editor-canvas-holder {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 520px;
    background: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--bs-border-radius);
    padding: 1rem;
    overflow: hidden;
}

.editor-canvas-holder .preview-frame {
    max-width: 100%;
    max-height: 100%;
}

/* Only the base photo (always the frame's own first, always-present
   .d-block img - see calibration_controller.js/tiling_preview_controller.js/
   prop_scaler_fit_controller.js's own templates) is height-capped here,
   never Prop Scaler fit's own .prop-scaler-overlay-img: that one is
   deliberately unconstrained (max-width: none, see its own CSS doc) since
   its rendered size is driven by a JS scale transform, not intrinsic
   layout - capping its height here would shrink it before that transform
   multiplies it, double-shrinking it. */
.editor-canvas-holder .preview-frame > img.d-block {
    max-height: calc(520px - 2rem);
    width: auto;
}

.editor-fullscreen-root:fullscreen .editor-canvas-holder {
    height: calc(100vh - 12rem);
}

.editor-fullscreen-root:fullscreen .editor-canvas-holder .preview-frame > img.d-block {
    max-height: calc(100vh - 14rem);
}

/* Outliner (Unfolder's part list + active tool's own options) - modelled
   on Blender's own Outliner-over-Properties sidebar pairing (a slim
   header bar over a dense, flush-edge, flat row list, then a properties
   area below reflecting whatever's currently active) but built from this
   app's own tokens throughout (--surface, --accent, --text custom
   properties), not Blender's own colors. */
.unfolder-outliner-parts {
    flex: 1 1 45%;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
}

.unfolder-outliner-properties {
    flex: 1 1 55%;
    min-height: 0;
    overflow-y: auto;
    padding: 0.55rem;
    border-top: 1px solid var(--border-subtle);
}

/* Always-visible pattern/export settings sidebar for the Layout tab - no
   per-tool gating like the viewport tab's own sidebar, since Page setup/
   Fold display/Glue tabs/Material etc. are always-relevant pattern
   settings, not per-tool panels. */
.unfolder-layout-sidebar {
    overflow-y: auto;
}

@media (max-width: 991.98px) {
    /* Below the flex-lg-row breakpoint the canvas+sidebar stack instead of
       sitting side by side - the canvas keeps its full 620px (still needs
       to be usable for viewing/editing), but the sidebar (always visible,
       unlike the viewport tab's conditional one) would otherwise
       unconditionally add another 620px before any of its content is
       reachable. Its own overflow-y:auto still reaches every option-group
       within this shorter height. */
    .unfolder-layout-sidebar {
        height: 360px !important;
    }
}

.unfolder-outliner-header {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.3rem 0.5rem;
    flex-shrink: 0;
    font-size: 0.6rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
    border-bottom: 1px solid var(--border-subtle);
}

.unfolder-outliner-header i {
    font-size: 0.7rem;
}

.unfolder-outliner-list {
    flex: 1 1 auto;
    overflow-y: auto;
    margin: 0;
    padding: 0.25rem;
    list-style: none;
}

/* Full-row hover/selected treatment (Blender's own outliner highlights the
   whole row, not just the label) - a thin accent-colored left border on
   selection reads as "selected" without needing a solid fill the way
   Blender's default blue selection does, closer to this app's own subtle,
   translucent-accent language used elsewhere (e.g. active nav states). */
.unfolder-outliner-row {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.15rem 0.35rem;
    margin: 0 0 1px;
    border-radius: var(--bs-border-radius-sm);
    border-left: 2px solid transparent;
    font-size: 0.7rem;
    transition: background-color 0.1s ease, border-color 0.1s ease;
}

.unfolder-outliner-row:hover {
    background: var(--surface-2);
}

.unfolder-outliner-row-selected,
.unfolder-outliner-row-selected:hover {
    background: rgba(var(--accent-rgb), 0.14);
    border-left-color: var(--accent-500);
}

/* Shared by both Outliner toggle icons (eye = viewport visibility,
   file = pattern inclusion) - Blender's own eye/camera pair gets the same
   dim/bright "on" treatment for both, so one rule pair covers both
   independent toggles rather than duplicating it per-icon. */
.unfolder-outliner-row-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.15rem;
    height: 1.15rem;
    flex-shrink: 0;
    padding: 0;
    border: none;
    border-radius: var(--bs-border-radius-sm);
    background: transparent;
    color: var(--text-muted);
    font-size: 0.68rem;
    line-height: 1;
}

.unfolder-outliner-row-toggle:hover {
    background: var(--surface-2);
    color: var(--accent-400);
}

/* "On" state (visible / included) gets the brighter icon color; "off"
   stays at the button's own default muted color - mirrors Blender's own
   dimmed-eye idiom, without a second color needing definition. */
.unfolder-outliner-row-toggle-on {
    color: var(--text-primary);
}

.unfolder-outliner-row-label {
    flex: 1 1 auto;
    min-width: 0;
    cursor: pointer;
}

.unfolder-outliner-row-count {
    flex-shrink: 0;
    min-width: 1.3rem;
    padding: 0.05rem 0.3rem;
    border-radius: 999px;
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: 0.6rem;
    text-align: center;
}

/* Pattern Editor's parts-list "Print" button (window.print()) - the
   standard "isolate one element for printing" trick: hide everything on
   the page, then re-reveal only .pattern-editor-print-area (and its own
   descendants) and pull it out of the page's normal flow so it prints
   starting at the top of the page regardless of its on-screen scroll
   position. .pattern-editor-print-hide additionally hides the CSV/Print
   buttons themselves (and anything else print-irrelevant) even though
   they're inside the parts-list pane, not just the app chrome outside
   it. Only fires under @media print, so plain on-screen viewing is
   completely unaffected. */
@media print {
    /* `body *` below hides every DESCENDANT of body, but not body/html
       themselves - without this, the site's own dark theme background
       still paints the full page behind the isolated print area (visible
       as a black band below the table, confirmed by hand with a print-
       media screenshot before adding this). */
    html,
    body {
        background: #fff !important;
    }
    body * {
        visibility: hidden;
    }
    .pattern-editor-print-area,
    .pattern-editor-print-area * {
        visibility: visible;
    }
    .pattern-editor-print-area {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        /* The site's own dark theme (light text on a near-black
           background) would otherwise carry straight into the printed
           page - technically isolated correctly, but wrong for paper:
           wastes ink even with "print background colors" enabled, and
           some printers/browsers ignore that setting and print
           near-invisible light-on-white text instead. Force plain black-
           on-white regardless of the site's active theme or the user's
           print-background setting. */
        color: #000;
        background: #fff;
    }
    .pattern-editor-print-area th,
    .pattern-editor-print-area td {
        color: #000;
        border-color: #999 !important;
    }
    .pattern-editor-print-hide {
        display: none !important;
    }
}

/* ==========================================================================
   Admin dashboard
   ========================================================================== */

/* Stat-tile trend sparkline (admin/dashboard.html.twig's sparkline()
   macro) - just spacing; the SVG carries its own colors via var(...) on
   its stroke/fill presentation attributes. */
.stat-sparkline {
    margin-top: 0.5rem;
}

/* Compact variants of the homepage's .feature-card/.feature-icon (see
   "Cards / surfaces" above) for the admin dashboard's "Admin sections"
   grid - a secondary quick-nav surface, not the page's hero content, so
   it uses the same idiom at a tighter density rather than the homepage's
   full padding/icon size. */
.feature-card--compact {
    padding: 1.1rem;
}

.feature-icon-sm {
    width: 2.25rem;
    height: 2.25rem;
    font-size: 1.1rem;
    margin-bottom: 0.5rem;
}

/* The System Health panel's aggregate status pill - a calm static dot
   when everything's fine, and the ONLY thing on the page that moves when
   it isn't (see --attention below). Real signal, not decoration. */
.health-dot {
    display: inline-block;
    width: 0.65rem;
    height: 0.65rem;
    border-radius: 50%;
    background: var(--bs-success);
    flex-shrink: 0;
}

.health-dot--attention {
    background: var(--bs-danger);
    box-shadow: 0 0 0 0 rgba(var(--bs-danger-rgb), 0.6);
}

@media (prefers-reduced-motion: no-preference) {
    .health-dot--attention {
        animation: health-pulse 2s ease-out infinite;
    }
}

@keyframes health-pulse {
    0% {
        box-shadow: 0 0 0 0 rgba(var(--bs-danger-rgb), 0.55);
    }
    100% {
        box-shadow: 0 0 0 0.6rem rgba(var(--bs-danger-rgb), 0);
    }
}
