/* The editors (Pattern Editor, Reference Photo, Unfolder) and the shared
   editor shell: menu bar, tool rail, options bar, side panels, context
   menus, layers, rulers, float window, command palette. */

/* 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. Shared
   with .editor-context-menu below, so the right-click menu can't drift
   from the menu bar's own dropdowns. */
.editor-menu-bar .dropdown-menu-dark,
.editor-context-menu {
    --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;
}

/* Pattern Editor + Reference Photo right-click menu - hand-built, and
   deliberately NOT the real `.dropdown-menu` class (Bootstrap's global
   dropdown keydown handler crashes on an instance-less menu - see
   openContextMenu()'s doc in either controller), so the layout
   variables and chrome `.dropdown-menu` would have supplied are defined
   here instead. Without them `.dropdown-item` padding and
   `.dropdown-divider` margin resolve to nothing. The one deliberate
   difference from the menu bar's dropdowns is the shadow: this menu
   floats over the canvas's own content rather than hanging off a bar. */
.editor-context-menu {
    --bs-dropdown-item-padding-x: 1rem;
    --bs-dropdown-item-padding-y: 0.25rem;
    --bs-dropdown-divider-margin-y: 0.5rem;
    display: block;
    min-width: 10rem;
    margin: 0;
    padding: 0.5rem 0;
    list-style: none;
    color: var(--text-primary);
    background-color: var(--bs-dropdown-bg);
    border: 1px solid var(--bs-dropdown-border-color);
    border-radius: var(--bs-border-radius);
    box-shadow: 0 0.35rem 1rem rgba(0, 0, 0, 0.35);
}

.editor-context-menu .dropdown-item {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    cursor: pointer;
}

.editor-context-menu .dropdown-item:hover,
.editor-context-menu .dropdown-item:focus-visible {
    background-color: var(--bs-dropdown-link-hover-bg);
    color: var(--bs-dropdown-link-hover-color);
}

/* Fixed-width icon gutter (empty on rows without an icon) so every
   label starts at the same x, and the shortcut hint pushed flush right. */
.editor-context-menu .context-menu-icon {
    flex: 0 0 1rem;
    text-align: center;
}

/* A menu-bar row's own leading icon (`.menu-icon`) - the same 1rem
   gutter as the context menu's, and in any dropdown where at least one
   row has an icon, icon-less rows are indented by that gutter so their
   labels still line up. */
.editor-menu-bar .menu-icon,
.editor-context-menu .menu-icon {
    display: inline-block;
    width: 1rem;
    text-align: center;
    color: var(--text-muted);
}

.editor-menu-bar .menu-icon {
    margin-right: 0.5rem;
}

/* Label + smaller shortcut hint on one baseline (flex's default stretch
   top-aligns them, leaving the hint visibly riding high). */
.editor-menu-bar .dropdown-item.justify-content-between {
    align-items: baseline;
}

.editor-menu-bar .dropdown-menu:has(> li > .dropdown-item .menu-icon) > li > .dropdown-item:not(:has(.menu-icon)) {
    padding-left: calc(var(--bs-dropdown-item-padding-x) + 1.5rem);
}

.editor-context-menu .text-muted-app {
    margin-left: auto;
    padding-left: 1rem;
}

/* 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);
}

/* The options bar above an editor's board (Pattern Editor, Reference Photo):
   one slot holding mutually-exclusive rows, at most one shown at a time,
   chosen from the active tool or the selection. The slot has a FIXED
   height and its rows never wrap, so switching rows - or a long German
   label, or a narrow window - can never change it and shove the board
   below up or down (a real, reported bug, twice). A row that's too long
   for the window scrolls sideways instead of growing. Every control in a
   row is the same height, so no row ends up taller than the others. */
.editor-options-bar-slot {
    height: 2.15rem;
    overflow: hidden;
}

.editor-options-bar-slot > .editor-options-bar {
    height: 100%;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    white-space: nowrap;
    scrollbar-width: thin;
}

.editor-options-bar-slot > .editor-options-bar > * {
    flex-shrink: 0;
}

.editor-options-bar :is(.form-control, .form-select, .btn, input[type='color']) {
    height: 1.6rem;
}

.editor-options-bar .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding-block: 0;
}

/* 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 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/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 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()), 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);
}

/* Text on a path (Parity 2.19): the textarea's caret and selection would
   run straight across the text's box, so both are hidden and the canvas
   draws them along the path instead (InlineTextEditor.pathCaret()). */
.pattern-editor-inline-text-edit--on-path {
    caret-color: transparent;
}

.pattern-editor-inline-text-edit--on-path::selection {
    background: transparent;
    color: transparent;
}

/* Reference Photo's own inline note-text editor (reference_photo_canvas_controller.ts's
   enterInlineTextEdit()) - unlike Pattern Editor's invisible/WASM-synced overlay above,
   this one is genuinely VISIBLE: RP has no competing canvas glyph pipeline to double-
   render against (render()'s own visibleItems filter just skips drawing this one note's
   fillText for the duration of the edit instead), so there's nothing to hide. `color` is
   set inline per-note by the controller (matching that note's own `color` field);
   background stays a translucent dark panel so the text stays legible over light photos
   underneath regardless of the note's own color. */
.reference-photo-inline-note-edit {
    position: absolute;
    left: 0;
    top: 0;
    resize: none;
    overflow: hidden;
    padding: 0.25em;
    margin: 0;
    border: 1px dashed #2d8cff;
    background: rgba(0, 0, 0, 0.55);
    line-height: 1.2;
    outline: none;
    z-index: 5;
}

/* Reference Photo's Background section (side panel): style toggles + a row of
   round color swatches + the native picker for anything else. The swatch's own
   fill is set inline from its color; `.active` (toggled by
   renderBackgroundControls()) rings whichever one matches the board. The
   "Default" swatch (no fill - the theme background shows through) is drawn as
   a slashed circle, the usual "none" glyph. */
.reference-photo-bg-swatches {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    flex-wrap: wrap;
}

/* Reference Photo: the stage is everything that moves into the float window
   (canvas + its overlays) - see openFloatWindow() in
   reference_photo_canvas_controller.ts. */
.reference-photo-stage {
    position: absolute;
    inset: 0;
}

/* Fullscreen: the canvas and side panel fill whatever the menu bar leaves,
   instead of keeping their fixed windowed 620px - same flex chain as Pattern
   Editor's own fullscreen block further down. */
[data-reference-photo-canvas-target="fullscreenRoot"]:fullscreen,
[data-reference-photo-canvas-target="fullscreenRoot"].is-window-filled {
    display: flex;
    flex-direction: column;
}

[data-reference-photo-canvas-target="fullscreenRoot"]:fullscreen .reference-photo-main-row,
[data-reference-photo-canvas-target="fullscreenRoot"].is-window-filled .reference-photo-main-row {
    flex: 1 1 auto;
    min-height: 0;
}

[data-reference-photo-canvas-target="fullscreenRoot"]:fullscreen [data-reference-photo-canvas-target="canvasHolder"],
[data-reference-photo-canvas-target="fullscreenRoot"]:fullscreen .editor-side-panel,
[data-reference-photo-canvas-target="fullscreenRoot"].is-window-filled [data-reference-photo-canvas-target="canvasHolder"],
[data-reference-photo-canvas-target="fullscreenRoot"].is-window-filled .editor-side-panel {
    height: 100% !important;
}

/* With the side panel hidden (View > Side Panel, P) the canvas reaches the
   shell's corner brand chip, so the storage-warning button moves up above it. */
.reference-photo-main-row:has(> .editor-side-panel.d-none) .editor-canvas-storage-trigger {
    bottom: 2.5rem;
}

/* Nested rows in a Layers panel tree (Reference Photo groups, Pattern Editor
   folders/layer objects). Every row starts with a fixed-width twisty column
   (the expand chevron, or an empty .layer-tree-twisty spacer), and each level
   indents by exactly that column plus the row gap, so a child's own twisty
   sits under its parent's icon - clearly to the right of the parent's
   chevron. A guide line per ancestor level runs down from that ancestor's
   chevron, so you can see where a group's contents end. The row sets
   --layer-depth inline; each editor tunes the other variables on its list. */
.layer-tree {
    --layer-tree-twisty: 1.25rem;
    --layer-tree-gap: 0.25rem;
    --layer-tree-pad: 0.25rem;
    --layer-tree-row-gap: 0px;
    --layer-tree-indent: calc(var(--layer-tree-twisty) + var(--layer-tree-gap));
}

.layer-tree-row {
    position: relative;
    padding-left: calc(var(--layer-tree-pad) + var(--layer-depth, 0) * var(--layer-tree-indent));
}

.layer-tree-row::before {
    content: '';
    position: absolute;
    /* Reaches up through the list's own row spacing so the guide stays one
       unbroken line from the group row down to its last child. */
    top: calc(-1 * var(--layer-tree-row-gap));
    bottom: 0;
    left: calc(var(--layer-tree-pad) + var(--layer-tree-twisty) / 2);
    width: calc(var(--layer-depth, 0) * var(--layer-tree-indent));
    background: repeating-linear-gradient(to right, var(--text-muted) 0 1px, transparent 1px var(--layer-tree-indent));
    opacity: 0.45;
    pointer-events: none;
}

.layer-tree-twisty {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--layer-tree-twisty);
}

/* A hidden item's row (Reference Photo) - faded on its contents rather than the
   row itself, so the guide lines in ::before keep their full strength. */
.layer-tree-row-hidden > * {
    opacity: 0.5;
}

/* Pattern Editor's rows are spaced with mb-1 and start flush. (Its drag-over
   indicator's 2px top border nudges a row's guide down by 2px, but that
   border is drawn right across the gap, so the line still reads unbroken.) */
.pattern-editor-layers-list {
    --layer-tree-pad: 0px;
    --layer-tree-row-gap: 0.25rem;
}

/* A swatch's color next to its palette icon in the Layers panel - bordered so
   it stays visible whatever the color. */
.reference-photo-layer-chip {
    display: inline-block;
    flex-shrink: 0;
    width: 0.8rem;
    height: 0.8rem;
    border: 1px solid var(--text-muted);
    border-radius: 2px;
}

.reference-photo-float-placeholder {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    color: var(--text-muted);
    text-align: center;
    padding: 1rem;
}

/* Float window top pill - only ever shown inside the float window
   (.is-floating), faded in on pointer movement by showFloatToolbar()
   together with the tool rail. Follows the theme's surface colors (it
   hosts the page's own options bar, whose inputs are theme-styled), with a
   shadow to lift it off the photos underneath. */
.reference-photo-float-toolbar {
    display: none;
    position: absolute;
    top: 8px;
    left: 50%;
    transform: translate(-50%, -6px);
    z-index: 7;
    align-items: center;
    gap: 2px;
    padding: 3px 3px 3px 8px;
    border: 1px solid var(--border-subtle);
    border-radius: 0.6rem;
    background: var(--surface-1);
    color: var(--text-primary);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease, transform 0.18s ease;
    max-width: calc(100% - 16px);
    flex-wrap: nowrap;
    white-space: nowrap;
}

/* The page's options bar while it's inside the pill: no underline or bottom
   gap, and it scrolls sideways (its own rule) rather than widening the pill
   past the window. The "nothing to set" hint stays out of the way here. */
.reference-photo-float-options {
    display: flex;
    min-width: 0;
    flex: 0 1 auto;
}

.reference-photo-float-options .editor-options-bar-slot {
    margin-bottom: 0 !important;
    min-width: 0;
}

.reference-photo-float-options .editor-options-bar {
    border-bottom: 0;
    padding-bottom: 0;
}

.reference-photo-float-options [data-reference-photo-canvas-target="optionsEmptyRow"] {
    display: none !important;
}

.reference-photo-float-options:not(:has(.editor-options-bar:not(.d-none):not([data-reference-photo-canvas-target="optionsEmptyRow"]))) + .reference-photo-float-toolbar-divider {
    display: none;
}

/* The page's tool rail while floating - down the window's left edge, over
   the photos, fading in and out together with the top pill. Always a
   column here, whatever the float window's width. */
.reference-photo-stage.is-floating > .editor-tool-rail {
    position: absolute;
    left: 8px;
    top: 52px;
    z-index: 7;
    flex-direction: column !important;
    flex-wrap: nowrap;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
}

.reference-photo-stage.is-floating.float-ui-visible > .editor-tool-rail {
    opacity: 1;
    pointer-events: auto;
}

.reference-photo-stage.is-floating .editor-tool-rail-divider {
    width: 100%;
    height: auto;
    margin: 0.3rem 0;
}

/* The rail's View adjustments popover (toggleViewAdjustPopover()) - hand-built
   so it also opens inside the float window, opening to the button's right. */
.reference-photo-popover-anchor {
    position: relative;
}

.reference-photo-popover {
    position: absolute;
    left: calc(100% + 0.6rem);
    top: 0;
    z-index: 20;
    padding: 0.5rem;
    background: var(--surface-1);
    color: var(--text-primary);
    border: 1px solid var(--border-subtle);
    border-radius: var(--bs-border-radius);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}

.reference-photo-stage.is-floating .reference-photo-float-toolbar {
    display: flex;
}

.reference-photo-stage.float-ui-visible .reference-photo-float-toolbar {
    opacity: 1;
    transform: translate(-50%, 0);
    pointer-events: auto;
}

.reference-photo-float-toolbar > button {
    width: 30px;
    height: 30px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 0.4rem;
    background: transparent;
    color: var(--text-primary);
    font-size: 15px;
    cursor: pointer;
}

.reference-photo-float-toolbar > button:hover:not(:disabled) {
    background: var(--surface-2);
}

.reference-photo-float-toolbar > button.active {
    background: var(--bs-primary);
    color: #111;
}

.reference-photo-float-toolbar > button:disabled {
    opacity: 0.35;
    cursor: default;
}

.reference-photo-float-toolbar-divider {
    width: 1px;
    height: 18px;
    margin: 0 3px;
    flex-shrink: 0;
    background: var(--border-subtle);
}

.reference-photo-stage-toast {
    position: absolute;
    left: 50%;
    bottom: 12px;
    transform: translateX(-50%);
    z-index: 7;
    max-width: calc(100% - 24px);
    padding: 0.4rem 0.75rem;
    border-radius: 0.5rem;
    background: rgba(18, 18, 20, 0.88);
    color: #f0f0f0;
    font-size: 0.8rem;
    text-align: center;
    pointer-events: none;
}

/* View > Lock Board: the chip that says so (and unlocks on click), top center
   over the board; the side panel greyed out, since it can't be used. */
.reference-photo-lock-chip {
    position: absolute;
    top: 10px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 7;
    display: flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.25rem 0.65rem;
    border: 1px solid var(--border-subtle);
    border-radius: 999px;
    background: var(--surface-1);
    color: var(--text-primary);
    font-size: 0.8rem;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
}

.reference-photo-lock-chip:hover {
    background: var(--surface-2);
}

.editor-side-panel.is-locked {
    opacity: 0.5;
}

/* View > Slideshow's controls - a pill at the bottom center, above the
   stage toast's spot. */
.reference-photo-slideshow-bar {
    position: absolute;
    left: 50%;
    bottom: 12px;
    transform: translateX(-50%);
    z-index: 7;
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 3px;
    max-width: calc(100% - 24px);
    border: 1px solid var(--border-subtle);
    border-radius: 0.6rem;
    background: var(--surface-1);
    color: var(--text-primary);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
    white-space: nowrap;
}

.reference-photo-slideshow-bar.d-none {
    display: none !important;
}

.reference-photo-slideshow-bar > button {
    border: 0;
    border-radius: 0.4rem;
    background: transparent;
    color: inherit;
    padding: 0.2rem 0.45rem;
}

.reference-photo-slideshow-bar > button:hover {
    background: var(--border-subtle);
}

.reference-photo-slideshow-bar > select {
    width: auto;
    margin-left: 4px;
    padding: 0.15rem 1.6rem 0.15rem 0.5rem;
    background-position: right 0.4rem center;
    font-size: 0.8rem;
}

.reference-photo-slideshow-counter {
    min-width: 4.5em;
    padding: 0 0.4rem;
    font-size: 0.8rem;
    font-variant-numeric: tabular-nums;
    text-align: center;
}

/* While a slideshow runs, the stage toast moves up out of the bar's way. */
.reference-photo-stage:has(.reference-photo-slideshow-bar:not(.d-none)) .reference-photo-stage-toast {
    bottom: 56px;
}

/* The command palette (Ctrl+K) - a dimmed layer over the stage with the box
   near the top, so it also works inside the float window. */
.reference-photo-palette {
    position: absolute;
    inset: 0;
    z-index: 9;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    padding-top: 48px;
    background: rgba(0, 0, 0, 0.3);
}

.reference-photo-palette.d-none {
    display: none !important;
}

/* The editors' command palette - Reference Photo's Ctrl+K (inside the
   overlay above) and Pattern Editor's S (.editor-palette-floating below).
   Rows are built by editor-shared/palette-view.ts. Kept in the same
   vocabulary as the menu bar's dropdowns (surface-1 sheet, surface-2 for
   the hovered/active row) so it reads as one more editor panel; the accent
   is spent in one place only: the rail on the active row, which the
   letters the search matched pick up. */
.editor-palette {
    display: flex;
    flex-direction: column;
    width: min(34rem, calc(100% - 24px));
    max-height: min(28rem, calc(100% - 72px));
    overflow: hidden;
    border: 1px solid var(--border-strong);
    border-radius: var(--bs-border-radius-lg);
    background: var(--surface-1);
    color: var(--text-primary);
    font-size: 0.85rem;
    box-shadow: 0 1.25rem 3rem rgba(0, 0, 0, 0.55);
}

.editor-palette.d-none {
    display: none !important;
}

@media (prefers-reduced-motion: no-preference) {
    .editor-palette {
        animation: editor-palette-in 0.12s ease-out;
    }
}

@keyframes editor-palette-in {
    from {
        opacity: 0;
        translate: 0 -6px;
    }
}

.editor-palette-search {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.65rem 0.9rem;
    border-bottom: 1px solid var(--border-subtle);
    background: var(--surface-2);
    color: var(--text-muted);
}

.editor-palette-input {
    flex: 1 1 auto;
    min-width: 0;
    padding: 0;
    border: 0;
    outline: none;
    background: transparent;
    color: var(--text-primary);
    font-size: 0.95rem;
}

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

.editor-palette-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 0.35rem;
}

.editor-palette-group {
    padding: 0.55rem 0.6rem 0.25rem;
    color: var(--text-muted);
    font-size: 0.72rem;
    font-weight: 600;
}

.editor-palette-group:first-child {
    padding-top: 0.25rem;
}

/* Icon gutter | label | where it lives | shortcut keycaps. */
.editor-palette-row {
    position: relative;
    display: grid;
    grid-template-columns: 1.1rem minmax(0, 1fr) auto auto;
    align-items: center;
    column-gap: 0.65rem;
    width: 100%;
    padding: 0.38rem 0.6rem;
    border: 0;
    border-radius: 0.4rem;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.editor-palette-row:hover {
    background: rgba(255, 255, 255, 0.04);
}

.editor-palette-row.active {
    background: var(--surface-2);
}

.editor-palette-row.active::before {
    content: '';
    position: absolute;
    top: 0.3rem;
    bottom: 0.3rem;
    left: 0;
    width: 2px;
    border-radius: 2px;
    background: var(--accent-500);
}

.editor-palette-row:disabled {
    opacity: 0.4;
    cursor: default;
}

.editor-palette-icon {
    color: var(--text-muted);
    font-size: 0.9rem;
    text-align: center;
}

.editor-palette-row.active .editor-palette-icon {
    color: var(--accent-400);
}

.editor-palette-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.editor-palette-label mark {
    padding: 0;
    background: transparent;
    color: var(--accent-400);
    font-weight: 600;
}

.editor-palette-section {
    color: var(--text-muted);
    font-size: 0.75rem;
    white-space: nowrap;
}

.editor-palette-keys {
    display: inline-flex;
    gap: 0.2rem;
}

.editor-palette kbd {
    min-width: 1.4em;
    padding: 0.05rem 0.3rem;
    border: 1px solid var(--border-strong);
    border-bottom-width: 2px;
    border-radius: 0.3rem;
    background: var(--surface-0);
    color: var(--text-muted);
    font: 500 0.7rem/1.25 var(--bs-font-sans-serif);
    text-align: center;
    box-shadow: none;
}

.editor-palette-row.active kbd {
    color: var(--text-primary);
}

.editor-palette-empty {
    margin: 0;
    padding: 1.4rem 1rem;
    color: var(--text-muted);
    text-align: center;
}

.editor-palette-footer {
    display: flex;
    gap: 1rem;
    padding: 0.45rem 0.9rem;
    border-top: 1px solid var(--border-subtle);
    color: var(--text-muted);
    font-size: 0.72rem;
}

.editor-palette-footer span {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}

@media (max-width: 576px) {
    .editor-palette-section,
    .editor-palette-footer {
        display: none;
    }
}

/* One selected photo's file facts in the side panel (renderImageInfo()). */
.reference-photo-image-info {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.1rem 0.6rem;
}

.reference-photo-image-info dt {
    font-weight: normal;
    color: var(--text-muted);
}

.reference-photo-image-info dd {
    margin: 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* Reference Photo's mini-map (drawMinimap()) - bottom-left, clear of the
   storage-warning button and brand chip on the right. Sized inline by the
   controller to the board's own aspect ratio. */
.reference-photo-minimap {
    position: absolute;
    left: 10px;
    bottom: 10px;
    z-index: 6;
    border: 1px solid rgba(255, 255, 255, 0.35);
    border-radius: 0.35rem;
    background: rgba(18, 18, 20, 0.72);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
    cursor: pointer;
    touch-action: none;
}

/* Small square native color pickers in the side panel (note text/background, line color). */
.reference-photo-color-input {
    width: 1.9rem;
    height: 1.9rem;
    padding: 0.1rem;
    border: 1px solid var(--border-subtle);
    border-radius: var(--bs-border-radius-sm);
    background: none;
    cursor: pointer;
}

/* A key button in Preferences > Keyboard Shortcuts - .active while it waits for a key press. */
.reference-photo-shortcut-key {
    min-width: 6.5rem;
    font-family: ui-monospace, monospace;
    font-size: 0.75rem;
}

/* Reference Photo's color row in the options bar - see .editor-options-bar-slot. */
.editor-options-bar-slot .reference-photo-color-value {
    width: auto;
    min-width: 7.5rem;
}

.editor-options-bar-slot .reference-photo-color-preview {
    width: 1.6rem;
    height: 1.6rem;
}

/* The rail's color stack (Photoshop's foreground/background layout, no labels):
   front square top-left = line color for new drawings, back square bottom-right =
   last picked color, corner arrow top-right = "draw with the picked color". Exactly
   one rail button wide, so the rail stays compact in every language. The surface-
   colored ring around the front square separates the two where they overlap. */
.reference-photo-color-stack {
    position: relative;
    width: 2rem;
    height: 2rem;
    flex-shrink: 0;
}

.reference-photo-color-stack-front,
.reference-photo-color-stack-back {
    position: absolute;
    width: 1.3rem;
    height: 1.3rem;
    padding: 0;
    border: 1px solid var(--text-muted);
    border-radius: 0.2rem;
    cursor: pointer;
}

.reference-photo-color-stack-front {
    left: 0;
    top: 0;
    z-index: 2;
    background: none;
    box-shadow: 0 0 0 2px var(--surface-1);
}

.reference-photo-color-stack-front::-webkit-color-swatch-wrapper {
    padding: 0;
}

.reference-photo-color-stack-front::-webkit-color-swatch {
    border: 0;
    border-radius: 0.15rem;
}

.reference-photo-color-stack-front::-moz-color-swatch {
    border: 0;
    border-radius: 0.15rem;
}

.reference-photo-color-stack-back {
    right: 0;
    bottom: 0;
    z-index: 1;
    background: var(--chip-color, transparent);
}

/* Nothing picked yet - an empty, hatched square rather than a misleading color. */
.reference-photo-color-stack-back:disabled {
    cursor: default;
    background: repeating-linear-gradient(135deg, transparent 0 3px, var(--border-subtle) 3px 4px);
}

.reference-photo-color-stack-use {
    position: absolute;
    right: -0.1rem;
    top: -0.15rem;
    width: 0.75rem;
    height: 0.75rem;
    padding: 0;
    border: 0;
    background: none;
    color: var(--text-muted);
    font-size: 0.6rem;
    line-height: 1;
    cursor: pointer;
}

.reference-photo-color-stack-use:hover:not(:disabled) {
    color: var(--text-primary);
}

.reference-photo-color-stack-use:disabled {
    visibility: hidden;
}

.reference-photo-color-stack-front:focus-visible,
.reference-photo-color-stack-back:focus-visible,
.reference-photo-color-stack-use:focus-visible {
    outline: 2px solid var(--bs-primary);
    outline-offset: 1px;
}

/* View adjustments button: a dot while the view is adjusted, so a board that's
   quietly inverted, mirrored or darkened is never a mystery. */
.reference-photo-view-adjust-button {
    position: relative;
}

.reference-photo-view-adjust-button.has-adjustments::after {
    content: '';
    position: absolute;
    top: 3px;
    right: 3px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--bs-primary);
}

.reference-photo-view-adjust-menu {
    width: 17rem;
    font-size: 0.75rem;
}

/* Inside the rail, but not a rail tool - undo .editor-tool-rail .btn's fixed 2rem square. */
.editor-tool-rail .reference-photo-view-adjust-menu .btn {
    width: auto;
    height: auto;
    padding: 0.15rem 0.5rem;
}

/* Right-click submenus (Reference Photo's showContextMenuAt()) - open on hover or
   .open (keyboard/click), to the right, or to the left with .flip-left near the
   window edge. */
.editor-context-submenu {
    position: relative;
}

.editor-context-submenu > .dropdown-item {
    align-items: center;
}

.editor-context-submenu > .editor-context-menu {
    display: none;
    position: absolute;
    top: -0.5rem;
    left: 100%;
    z-index: 1;
}

.editor-context-submenu > .editor-context-menu.flip-left {
    left: auto;
    right: 100%;
}

.editor-context-submenu.open > .editor-context-menu {
    display: block;
}

.editor-context-submenu.open > .dropdown-item {
    background-color: var(--bs-dropdown-link-hover-bg);
}

/* Eyedropper's live readout next to the cursor. */
.reference-photo-pick-chip {
    position: absolute;
    z-index: 6;
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.2rem 0.45rem 0.2rem 0.2rem;
    border-radius: 0.4rem;
    background: rgba(20, 20, 20, 0.85);
    color: #fff;
    font: 0.75rem ui-monospace, monospace;
    pointer-events: none;
    white-space: nowrap;
}

.reference-photo-pick-chip-swatch {
    width: 1.4rem;
    height: 1.4rem;
    border-radius: 0.25rem;
    border: 1px solid rgba(255, 255, 255, 0.6);
}

.reference-photo-color-preview {
    width: 2.6rem;
    height: 2.6rem;
    flex-shrink: 0;
    border-radius: var(--bs-border-radius);
    border: 1px solid var(--border-subtle);
}

/* Nothing picked yet - a diagonal strike, same idea as the background
   modal's "Default" swatch. */
.reference-photo-color-preview-empty {
    background: linear-gradient(to top right, transparent calc(50% - 1px), var(--border-subtle) calc(50% - 1px), var(--border-subtle) calc(50% + 1px), transparent calc(50% + 1px)) !important;
}

.reference-photo-color-value {
    display: flex;
    justify-content: space-between;
    gap: 0.5rem;
    width: 100%;
    padding: 0.05rem 0.3rem;
    border: 0;
    border-radius: 0.25rem;
    background: transparent;
    color: inherit;
    font: 0.72rem ui-monospace, monospace;
    text-align: left;
}

.reference-photo-color-value:hover:not(:disabled) {
    background: var(--border-subtle);
}

.reference-photo-color-value.copied {
    color: var(--bs-success);
}

.reference-photo-bg-swatch {
    width: 1.35rem;
    height: 1.35rem;
    padding: 0;
    border-radius: 50%;
    border: 1px solid var(--border-subtle);
    cursor: pointer;
    position: relative;
    flex-shrink: 0;
}

/* A box-shadow ring rather than `outline` - the modal's own button styling
   resets outline, which silently hid the active state. Inner ring in the
   modal's background color fakes the outline-offset gap. */
.reference-photo-bg-swatch.active {
    box-shadow: 0 0 0 2px var(--bs-modal-bg, var(--bs-body-bg)), 0 0 0 4px var(--bs-primary);
}

.reference-photo-bg-swatch-default {
    background: transparent;
    overflow: hidden;
}

.reference-photo-bg-swatch-default::after {
    content: '';
    position: absolute;
    left: 50%;
    top: -15%;
    width: 1px;
    height: 130%;
    background: var(--bs-danger);
    transform: rotate(45deg);
}

.reference-photo-bg-swatches 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;
}

/* Chain-link group indicator (see the Twig template's 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;
}

/* Follows the cursor while hovering an avatar "Show measurements" overlay
   line (unfolder_controller.ts's handleAvatarViewportPointerMove()) -
   positioned via inline left/top (absolute against viewportHolder's own
   position:relative), this stylesheet only owns its chrome. pointer-events:
   none so it never itself blocks the hover/raycast it's reporting on. */
.unfolder-measurement-tooltip {
    position: absolute;
    z-index: 5;
    pointer-events: none;
    transform: translate(12px, -50%);
    background: var(--surface-2);
    border: 1px solid var(--border-strong);
    border-radius: var(--bs-border-radius, 0.375rem);
    padding: 0.35rem 0.6rem;
    font-size: 0.75rem;
    white-space: nowrap;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

.unfolder-measurement-tooltip .name {
    color: var(--text-muted);
    margin-right: 0.4em;
}

.unfolder-measurement-tooltip .value {
    color: var(--accent-400);
    font-weight: 650;
}

/* Bottom-right corner of the 3D viewport (position:relative via the
   shared .editor-canvas-viewport rule) - stacks the body-transparency
   slider directly above the .editor-canvas-storage-trigger warning badge
   (edit.html.twig wraps both in this div, viewportHolder only). Anchored
   by `bottom`/`right` rather than a fixed height, so when the trigger is
   ever absent the slider - now the stack's only/last child - lands
   exactly on the corner itself instead of leaving a gap. Top-right was
   tried first but that's Viewport3D's own ViewHelper axis gimbal
   (viewport3d.js's `this.viewHelper.location = { top: 0, right: 0 }`),
   and top-left read as a stray, unanchored control with nothing else
   up there once the "Loading model…" status text hides itself. */
.unfolder-viewport-bottom-right-stack {
    position: absolute;
    bottom: 0.75rem;
    right: 0.75rem;
    z-index: 10;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.5rem;
}

/* Takes the trigger out of its own normal position:absolute (see that
   rule below) so it becomes a plain flex item in the stack above instead
   of jumping to the bottom-right corner on top of the slider - scoped to
   this wrapper only, every other .editor-canvas-storage-trigger
   (Unfolder's layoutHolder pane, Pattern Editor, Prop Scaler) is
   untouched. */
.unfolder-viewport-bottom-right-stack .editor-canvas-storage-trigger {
    position: static;
}

.unfolder-viewport-opacity-control {
    background: var(--surface-2);
    border: 1px solid var(--border-strong);
    border-radius: var(--bs-border-radius, 0.375rem);
    padding: 0.35rem 0.6rem;
    color: var(--text-muted);
    font-size: 0.9rem;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

.unfolder-viewport-opacity-control .form-range {
    width: 7rem;
}

.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;
}

/* ==========================================================================
   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,
.editor-fullscreen-root.is-window-filled {
    background: var(--bg-page, #0b0b0c);
    padding: 1rem;
    overflow: auto;
}

/* Fill window (editor_window_fill_controller.ts): the same layout as native
   fullscreen above, but pinned over the browser tab instead of the screen.
   Every per-tool :fullscreen rule in this file has an .is-window-filled
   twin, so the flex chains/height budgets behave identically. z-index sits
   above the sticky site header (1020/1030) and below Bootstrap's modal
   backdrop (1050), so body-level modals, tooltips and toasts still paint
   over the filled editor. The body class stops the page behind it from
   scrolling under the wheel. */
.editor-fullscreen-root.is-window-filled {
    position: fixed;
    inset: 0;
    z-index: 1040;
}

body.editor-window-filled {
    overflow: hidden;
}

/* Full-screen editors (Unfolder, Pattern Editor): local-storage durability
   overlay - see editor_storage_overlay_controller.ts 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,
[data-unfolder-target="fullscreenRoot"].is-window-filled [data-unfolder-target="viewportHolder"],
[data-unfolder-target="fullscreenRoot"].is-window-filled [data-unfolder-target="layoutHolder"],
[data-unfolder-target="fullscreenRoot"].is-window-filled [data-unfolder-target="outlinerPanel"],
[data-unfolder-target="fullscreenRoot"].is-window-filled .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 .editor-options-bar-slot's
   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,
[data-pattern-editor-canvas-target="fullscreenRoot"].is-window-filled {
    display: flex;
    flex-direction: column;
}

[data-pattern-editor-canvas-target="fullscreenRoot"]:fullscreen .pattern-editor-canvas-pane,
[data-pattern-editor-canvas-target="fullscreenRoot"].is-window-filled .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,
[data-pattern-editor-canvas-target="fullscreenRoot"].is-window-filled .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,
[data-pattern-editor-canvas-target="fullscreenRoot"].is-window-filled [data-pattern-editor-canvas-target="canvasHolder"],
[data-pattern-editor-canvas-target="fullscreenRoot"].is-window-filled .editor-side-panel,
[data-pattern-editor-canvas-target="fullscreenRoot"].is-window-filled .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 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
   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/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,
.editor-fullscreen-root.is-window-filled .editor-canvas-holder {
    height: calc(100vh - 12rem);
}

.editor-fullscreen-root:fullscreen .editor-canvas-holder .preview-frame > img.d-block,
.editor-fullscreen-root.is-window-filled .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;
    }
}

/* The windowed editors' fixed-size layout pieces (were inline styles):
   the canvas box, the canvas element itself, and the side column next to
   it. Last in this file so they win like the inline styles did; the
   fullscreen rules above override the 620px with !important. */
.editor-canvas-box {
    min-width: 0;
    height: 620px;
    border: 1px solid var(--border-subtle);
    border-radius: var(--bs-border-radius);
    position: relative;
}

.editor-canvas {
    width: 100%;
    height: 100%;
    touch-action: none;
    display: block;
    outline: none;
}

.editor-side-column {
    width: 280px;
    flex-shrink: 0;
    height: 620px;
}

.editor-side-column-wide {
    width: 300px;
}

/* The settings panel on the Poster / Prop Scaler step pages. */
.editor-side-panel-padded {
    padding: 1rem;
}

/* The right-click menu: positioned at the click point by the controller. */
.editor-floating-menu {
    position: fixed;
    z-index: 1080;
}

/* Pattern Editor's S-key palette opens on a bare keypress, anchored to no
   button: pinned near the top of the viewport (it's inside the editor
   root, so it stays visible in fullscreen and fill-window mode too). */
.editor-palette-floating {
    position: fixed;
    top: 14vh;
    left: 50%;
    z-index: 1080;
    max-height: min(30rem, 72vh);
    transform: translateX(-50%);
}

/* Step and Repeat, Trace Bitmap: no backdrop and docked to the right on wide screens,
   so its live preview on the canvas stays in view. The modal's own full-
   screen layer lets pointer events through to the canvas (which only pans
   while it's open); the dialog itself still takes them. */
.pe-modal-aside {
    pointer-events: none;
}
.pe-modal-aside .modal-content {
    box-shadow: 0 0.75rem 2.5rem rgba(0, 0, 0, 0.45);
}
@media (min-width: 992px) {
    .pe-modal-aside .modal-dialog {
        margin: 4.5rem 1rem 1rem auto;
    }
}

/* Pattern Editor's Transform dialog. Its tab panes lay out as a column
   only while shown: a Bootstrap d-flex (!important) would show all four. */
.pe-transform-pane.active {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

/* The 3x3 reference-point grid: which point of the bounding box a rotate,
   scale or skew pivots on. Each cell is a radio styled as a small square. */
.pe-transform-reference {
    display: grid;
    grid-template-columns: repeat(3, 1.5rem);
    gap: 0.25rem;
}

.pe-transform-reference .btn {
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
}

.pe-transform-pivot:disabled .pe-transform-reference,
.pe-transform-pivot:disabled .form-check {
    opacity: 0.5;
}
