:root {
    /* Functional Dark Theme */
    --bg-base: #121212;
    --bg-panel: #222222;
    --bg-input: #1a1a1a;
    --border-light: #3a3a3a;
    --border-hover: #555;
    
    --text-main: #eaeaea;
    --text-muted: #999;
    
    /* Restored OG Functional Colors */
    --c-purple: #8e44ad;
    --c-purple-hover: #9b59b6;
    --c-red: #c0392b;
    --c-red-hover: #e74c3c;
    --c-orange: #d35400;
    --c-orange-hover: #e67e22;
    --c-gray: #444;
    --c-gray-hover: #555;

    /* Module accent: Spot Color Vectorizer tan (base #73634E). Same
       three-tier hierarchy as DTF Prep's green, with each tier matched
       to its green counterpart's luminance so contrast against white
       text and the dark panels is unchanged. The --c-green/--c-blue
       names are kept from the shared palette so every existing rule
       re-themes without edits; they no longer hold green or blue.
         --c-green*  bright CTA tier      (DTF #27ae60 / #2ecc71)
         --c-accent  standard tier        (DTF #147343) -- sliders, checkboxes
         --c-blue*   subdued base tier    (DTF #0F5934 / #188D53) */
    --c-green: #B8915F;
    --c-green-hover: #CBAF8A;
    --c-accent: #866C4A;
    --c-blue: #73634E;
    --c-blue-hover: #8A7760;

    --accent-link: #73634E;
}

html, body {
    height: 100%;
}

body {
    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
    padding: 20px;
    background: var(--bg-base);
    color: var(--text-main);
    margin: 0;
    overflow: hidden;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
}

/* --- FULL-BLEED HALFTONE HEADER --- */
.app-header {
    position: relative;
    flex-shrink: 0;
    height: 50px;
    margin: -20px -20px 20px -20px;
    overflow: hidden;
    display: flex;
    align-items: center;
    background-color: #121212; 
    background-image: url('assets/halftone-strip.png');
    background-size: cover; 
    background-position: right center; 
    background-repeat: no-repeat;
    border-bottom: 1px solid var(--border-light); 
}

.brand-block {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: 12px;
    padding-left: 24px;
    height: 100%;
}

/* Fills the header's own fixed content-box height (50px, see
   .app-header) as large as possible WITHOUT growing the header
   itself -- height:100% resolves against .brand-block's own
   height:100% above, max-height is a hard backstop pinned to
   .app-header's height in case anything in that chain ever loses
   its 100%. object-fit:contain (with width:auto as a non-flex
   fallback) preserves the source's ~2.7:1 aspect ratio so it
   scales down cleanly from the native 5636x2076 lockup without
   distortion, clipping, or overlapping the tagline/BETA badge
   next to it. */
.brand-logo {
    display: block;
    height: 100%;
    max-height: 50px;
    width: auto;
    object-fit: contain;
    flex-shrink: 0;
    /* Returns to the dashboard -- see initDashboardLogoLink() in script.js. */
    cursor: pointer;
    filter: drop-shadow(0 0 4px rgba(115, 99, 78, 0.35));
}

/* App title beside the logo -- standardized across DTF Prep, Color
   Merge and the Gang Sheet Builder (.brand-title there): 16.5px bold. */
.tagline {
    font-size: 16.5px;
    font-weight: bold;
    color: var(--text-main);
    text-shadow: 0 1px 4px rgba(0,0,0,0.7);
    white-space: nowrap;
}

.brand-beta-badge {
    font-size: 12px;
    font-weight: normal;
    color: var(--text-muted);
    padding: 2px 8px;
    border: 1px solid #444;
    border-radius: 12px;
    margin-left: 10px;
    text-shadow: none;
}

/* Sits right next to the BETA badge -- deliberately understated
   (transparent, thin border) rather than a solid accent color, so
   it reads as a secondary/ambient action next to the brand block
   instead of competing with the primary toolbar buttons. Needs
   the `button` type selector (not just the class) to out-specify
   the global button:not([class*="cl-"]) reset (padding/border/
   border-radius/color/width) at line ~509 -- see that rule's own
   comment for why a bare type selector reaches every button here. */
button.feedback-header-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    width: auto;
    margin-left: 10px;
    padding: 4px 12px;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-muted);
    background: transparent;
    border: 1px solid #444;
    border-radius: 12px;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
button.feedback-header-btn:hover {
    background: var(--bg-panel);
    border-color: var(--border-hover);
    color: var(--text-main);
}

/* Fullscreen toggle -- global (desktop AND touch). A solid,
   high-contrast pill now (icon + "Fullscreen" label) instead of
   the original transparent icon-only circle, so it reads as a
   real primary action next to Sign In rather than a muted,
   secondary control like Feedback. --c-blue is this app's own
   "actively toggled on" color elsewhere, reused here as the
   brand/primary color the button should always be filled with,
   not just while fullscreen is engaged. Scoped by #id, NOT
   `button.fullscreen-header-btn` -- confirmed live that the
   class+type form (the same pattern .feedback-header-btn itself
   uses) loses this exact fight: the global
   button:not([class*="cl-"]) reset at line ~592 has equal
   specificity and sits LATER in the stylesheet, so it silently
   wins the padding/border-radius/background properties they both
   set regardless of source order up here (this is a pre-existing
   bug on .feedback-header-btn too, out of scope to fix here --
   flagged separately). An #id selector's specificity beats the
   reset outright, independent of where in the file it's
   declared. */
#btnFullscreenToggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: auto;
    /* Confirmed live (back when this was a fixed-width icon
       circle): without this, .header-auth-slot's flex row
       shrinks this button below its own natural size even
       though the row has plenty of unused space and nothing
       else in it is shrinking -- flex's default shrink:1
       otherwise treats a flex item's size as just a suggestion,
       not a floor. Still relevant now that it sizes by padding
       instead of a fixed width. */
    flex-shrink: 0;
    padding: 8px 16px;
    font-size: 13px;
    font-weight: 700;
    color: white;
    background: var(--c-blue);
    border: none;
    border-radius: 20px;
    cursor: pointer;
    transition: background 0.15s ease;
}
#btnFullscreenToggle:hover {
    background: var(--c-blue-hover);
}
/* Toggled by initFullscreenToggle() to reflect actual fullscreen
   state -- including when it changes via the browser's own Esc
   key or native exit control, not just this button's own clicks.
   Reuses the same :hover shade as a simple "currently pressed"
   cue -- the button is already solidly brand-colored at rest, so
   this only needs to read as a subtle shift, not a second color
   language. */
#btnFullscreenToggle.is-fullscreen {
    background: var(--c-blue-hover);
}

/* --- HEADER ACCOUNT SLOT ---
   Fullscreen + the account slot ([data-kittashi-account]) that
   ../assets/kittashi-paywall.js fills with Sign In / Go Pro / PRO and
   Clerk's account menu. Shares .app-header's own fixed 50px height and
   flex row; everything placed here fits inside it, so signing in/out
   never shifts the logo/tagline or changes the header's height. */
.header-auth-slot {
    margin-left: auto;
    padding-right: 24px;
    display: flex;
    align-items: center;
    gap: 10px;
    position: relative;
    z-index: 1;
    height: 100%;
}

/* --- 3-COLUMN LAYOUT --- */
.app-container {
    display: flex;
    gap: 20px;
    align-items: flex-start;
    flex: 1 1 auto;
    min-height: 0;
    width: 100%;
}

.app-footer {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin-top: 10px;
    padding-top: 10px;
    padding-bottom: 4px;
    border-top: 1px solid var(--border-light);
    font-size: 12px;
    color: var(--text-muted);
}

.app-footer a {
    color: var(--accent-link);
    text-decoration: none;
}

.app-footer a:hover {
    text-decoration: underline;
}

/* COLUMN 2: CENTER CANVAS WORKSPACE */
.center-column {
    flex-grow: 1;
    display: flex;
    flex-direction: column;
    gap: 12px;
    min-width: 0;
    height: 100%;
}

.top-action-bar {
    background: var(--bg-panel);
    padding: 12px 20px;
    border-radius: 8px;
    width: 100%;
    box-sizing: border-box;
    border: 1px solid var(--border-light);
    display: flex;
    justify-content: space-between;
    align-items: center;
    box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}

/* COLUMN 3: PIPELINE MODULES (RIGHT) */
.right-column {
    display: flex;
    flex-direction: column;
    gap: 12px;
    width: 420px;
    flex-shrink: 0;
    height: 100%;
}

.sidebar-header {
    background: var(--bg-panel);
    padding: 12px 15px;
    border-radius: 8px;
    border: 1px solid var(--border-light);
    box-sizing: border-box;
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-shrink: 0;
    margin-right: 16px;
    box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}

.sidebar {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 100%;
    flex: 1;
    min-height: 0;
    overflow-y: scroll;
    padding-right: 8px;
    padding-bottom: 20px;
    box-sizing: border-box;
    scrollbar-width: thin;
    scrollbar-color: var(--border-hover) var(--bg-base);
    /* Tells touch browsers this list only ever needs native
       vertical scrolling -- a finger drag scrolls the list the
       same way it would with no touch-action set, but a tap still
       registers as a tap (not a swallowed drag), so module
       headers keep opening on a single tap instead of needing
       touch-action's default to be second-guessed by JS. */
    touch-action: pan-y;
}

.sidebar::-webkit-scrollbar { width: 8px; }
.sidebar::-webkit-scrollbar-track { background: var(--bg-base); border-radius: 4px; }
.sidebar::-webkit-scrollbar-thumb { background: var(--border-hover); border-radius: 4px; }
.sidebar::-webkit-scrollbar-thumb:hover { background: #666; }

/* --- UI COMPONENTS & COLOR CODING --- */
/* :not([class*="cl-"]) excludes Clerk's own rendered buttons --
   Clerk mounts directly into this document (no iframe, no Shadow
   DOM), so a bare `button` type selector here reaches every
   <button> Clerk renders too, including small icon-sized ones like
   its password field's "Show password" toggle. That toggle is
   position:absolute + right:3px/top:3px (Clerk's own intended
   small-icon-in-the-corner placement) but had NO width of its own
   to compete with this rule's width:100% -- so it silently
   stretched to cover its entire parent (confirmed: computed width
   matched the parent's width exactly, both 162px), sitting on top
   of and intercepting every click meant for the actual <input>
   beneath it. That's the confirmed root cause of "the password
   field won't accept typed input" -- not a keyboard listener, not
   a focus trap, just this one rule leaking into Clerk's DOM.
   Every Clerk-rendered element carries a "cl-" prefixed class
   (confirmed: cl-formFieldInputShowPasswordButton, cl-button,
   etc.), so excluding by that prefix is a precise, narrow fix --
   it changes nothing for any of Kittashi's own buttons (none of
   which have a "cl-" class) and doesn't touch Clerk's own styling
   at all, just stops this rule from reaching it in the first
   place. */
button:not([class*="cl-"]) {
    padding: 8px 15px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-weight: 600;
    color: white;
    width: 100%;
    transition: all 0.15s ease;
}

/* Color swatches (Color Replacer, Preview Background) -- the swatch IS
   the whole control, no hex text field beside it. Clicking it opens
   the Coloris popover (bound via Coloris({ el: '.color-swatch' }) in
   script.js), whose own hex field covers typed entry;
   paintColorSwatch() fills it from its value attribute.
   button.color-swatch, not .color-swatch alone, so it outranks the
   button:not([class*="cl-"]) rule above instead of inheriting its
   width:100%/padding. Same rule as DTF Prep's. */
button.color-swatch {
    width: 30px;
    height: 24px;
    flex: 0 0 auto;
    padding: 0;
    border: 1px solid var(--border-light);
    border-radius: 4px;
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
}
button.color-swatch:hover {
    border-color: var(--border-hover);
}

/* Crop Image's "Draw and Adjust Selection Box" cue -- shown only while a
   Manual Crop session is open (startCropping() / closeCropperUI()). */
.crop-draw-hint {
    display: none;
    margin-left: 10px;
    font-size: 12px;
    font-style: italic;
    font-weight: normal;
    color: var(--text-muted);
}
.crop-draw-hint.visible { display: inline; }

/* Functional Button Colors */
#btnCrop, #btnAutoCrop, #btnUpscalerCrop, #btnUpscalerAutoCrop, #btnWand, #btnEraser, #btnColorReplacerRestrictArea, #btnAntiAlias, #btnSmoothing, #btnVectorPreview { background-color: var(--c-purple); }
#btnCrop:hover, #btnAutoCrop:hover, #btnUpscalerCrop:hover, #btnUpscalerAutoCrop:hover, #btnWand:hover, #btnEraser:hover, #btnColorReplacerRestrictArea:hover, #btnAntiAlias:hover, #btnSmoothing:hover, #btnVectorPreview:hover { background-color: var(--c-purple-hover); }

#btnApplyCrop, #upscalerBtnApplyCrop, #btnWandApply, #btnEraserApply, #btnColorReplacerApply, #btnApplyPreAdjust, #btnApplyAntiAlias, #btnApplySmoothing, #btnVectorExport, #downloadBtn { background-color: var(--c-green); }
#btnApplyCrop:hover, #upscalerBtnApplyCrop:hover, #btnWandApply:hover, #btnEraserApply:hover, #btnColorReplacerApply:hover, #btnApplyPreAdjust:hover, #btnApplyAntiAlias:hover, #btnApplySmoothing:hover, #btnVectorExport:hover, #downloadBtn:hover { background-color: var(--c-green-hover); }

#btnCancelCrop, #upscalerBtnCancelCrop, #btnWandClear, #btnEraserClear, #btnResetAll, #btnColorReplacerCancel, #btnResetAntiAlias, #btnResetSmoothing { background-color: var(--c-red); }
#btnCancelCrop:hover, #upscalerBtnCancelCrop:hover, #btnWandClear:hover, #btnEraserClear:hover, #btnResetAll:hover, #btnColorReplacerCancel:hover, #btnResetAntiAlias:hover, #btnResetSmoothing:hover { background-color: var(--c-red-hover); }

#btnUpscaleBasic, #btnResize, #btnResetImage, #btnUpscalerResetImage { background-color: var(--c-orange); }
#btnUpscaleBasic:hover, #btnResize:hover, #btnResetImage:hover, #btnUpscalerResetImage:hover { background-color: var(--c-orange-hover); }

#btnCompare, #btnApplyEdgeCleanup, #btnUndoEraser, #btnUndoWand, #btnUndoColorReplacer, #btnUndoPreAdjust, #btnUndoAntiAlias, #btnUndoSmoothing { background-color: var(--c-blue); }
#btnCompare:hover, #btnApplyEdgeCleanup:hover, #btnUndoEraser:hover, #btnUndoWand:hover, #btnUndoColorReplacer:hover, #btnUndoPreAdjust:hover, #btnUndoAntiAlias:hover, #btnUndoSmoothing:hover { background-color: var(--c-blue-hover); }

/* --- Reset Image confirmation modal -- shared overlay/box shell,
   reused for both State A (no upscale on record) and State B (an
   upscale exists), which only differ in the text and which buttons
   are shown (see #btnResetImage's click handler). Plain
   .style.display toggling, same convention as every other show/hide
   in this app (canvasPlaceholder, upscaleError, etc.), not a CSS
   class. */
#resetImageModal {
    display: none;
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0,0,0,0.6);
    z-index: 1000;
    align-items: center;
    justify-content: center;
}
.modal-box {
    background: var(--bg-panel);
    border: 1px solid var(--border-light);
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.5);
    padding: 24px;
    max-width: 420px;
    width: 90%;
    box-sizing: border-box;
    /* Shared by every modal built on this class (feedback, reset
       image, etc.) --
       a hard cap plus its own scrollbar keeps any of them from
       being cut off top/bottom on a shorter tablet viewport,
       without needing a per-modal override. */
    max-height: 90vh;
    overflow-y: auto;
}
.modal-box p {
    margin: 0 0 20px 0;
    color: var(--text-main);
    font-size: 14px;
    line-height: 1.5;
}
.modal-actions {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
#resetModalCancel { background-color: var(--c-gray); }
#resetModalCancel:hover { background-color: var(--c-gray-hover); }
#resetModalToUpscaled { background-color: var(--c-blue); }
#resetModalToUpscaled:hover { background-color: var(--c-blue-hover); }
#resetModalToOriginal { background-color: var(--c-red); }
#resetModalToOriginal:hover { background-color: var(--c-red-hover); }

/* --- Beta Feedback modal -- same fixed-overlay/.modal-box shell
   as #resetImageModal above,
   just wider (a form needs more room than a confirmation prompt)
   and with its own form-field styling scoped under #feedbackModal
   so it can't leak onto any other select/textarea/input in the
   app. Independent of every other modal here -- see
   initFeedbackModal() near the end of <body> for its open/close/
   submit wiring. */
#feedbackModal {
    display: none;
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0,0,0,0.6);
    z-index: 1000;
    align-items: center;
    justify-content: center;
}
#feedbackModal .modal-box {
    max-width: 460px;
}
#feedbackModal .modal-title {
    margin: 0 0 18px 0;
    font-size: 16px;
    font-weight: 700;
    color: var(--text-main);
}
.feedback-field {
    margin-bottom: 16px;
}
.feedback-field label {
    display: block;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-muted);
    margin-bottom: 6px;
}
#feedbackModal select,
#feedbackModal textarea,
#feedbackModal input[type="email"] {
    width: 100%;
    padding: 8px 10px;
    border: 1px solid var(--border-light);
    background: var(--bg-input);
    color: var(--text-main);
    border-radius: 4px;
    font-family: inherit;
    font-size: 13px;
    box-sizing: border-box;
}
#feedbackModal textarea {
    resize: vertical;
    min-height: 90px;
    line-height: 1.4;
}
#feedbackModal select:focus,
#feedbackModal textarea:focus,
#feedbackModal input[type="email"]:focus {
    outline: none;
    border-color: var(--c-blue);
}
.feedback-actions {
    display: flex;
    gap: 10px;
    margin-top: 4px;
}
.feedback-actions button {
    flex: 1;
    width: auto;
}
#feedbackModalCancel { background-color: var(--c-gray); }
#feedbackModalCancel:hover { background-color: var(--c-gray-hover); }
#feedbackModalSubmit { background-color: var(--c-green); }
#feedbackModalSubmit:hover { background-color: var(--c-green-hover); }
#feedbackModalSubmit:disabled {
    opacity: 0.7;
    cursor: default;
}

/* Coloris renders its popover at a fixed ~200px square, which read
   as small on a large monitor for precision color work -- a
   previous fix enlarged it with `transform: scale(1.5)`, but that
   only stretches the rendered PICTURE. Coloris's own hit-testing
   (colorAreaDims, built from the gradient box's real
   offsetWidth/offsetHeight) has no idea the box is being scaled,
   so it kept mapping clicks against the original untransformed
   200x100 box while the box a user actually sees and clicks is
   300x150 -- confirmed live to clamp roughly the left third of the
   visible saturation/brightness square to zero saturation
   (gray), which is exactly why dragging only the hue slider
   looked "stale": hue is genuinely invisible at S=0. `theme:
   'large'` (passed to Coloris(...) below) is Coloris's own built-in
   enlarged layout (a real 275x150 box, not a stretched one), so
   offsetWidth/offsetHeight and the rendered size stay identical
   and hit-testing is accurate again. */

/* [ Close File ] -- neutral base (the red-x-box below already
   signals "dismiss"; a red button too would visually compete with
   Reset All's already-red, more destructive action). */
.close-file-btn {
    background-color: var(--c-gray);
    display: inline-flex;
    align-items: center;
    gap: 8px;
}
.close-file-btn:hover {
    background-color: var(--c-gray-hover);
}
.close-file-btn .red-x-box {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    background-color: #d93025;
    color: white;
    font-size: 11px;
    font-weight: bold;
    line-height: 1;
    border-radius: 3px;
}

#btnFit, #btnCollapseAll, #btnColorReplacerTargetEyedropper, #btnColorReplacerNewEyedropper { background-color: var(--c-gray); }
#btnFit:hover, #btnCollapseAll:hover, #btnColorReplacerTargetEyedropper:hover, #btnColorReplacerNewEyedropper:hover { background-color: var(--c-gray-hover); }


/* Module 7 (Color Replacer) while a live Color Merge preview is on screen:
   every control is disabled by script, and the note above them says why.
   Dimmed as a whole rather than control by control, so it reads as one
   unavailable tool, not a scatter of greyed-out widgets. */
.module-gate-note {
    margin: 0 0 12px;
    padding: 8px 10px;
    font-size: 12px;
    line-height: 1.4;
    color: var(--text-main);
    background: rgba(230, 126, 34, 0.12);
    border: 1px solid rgba(230, 126, 34, 0.45);
    border-radius: 6px;
}
.module-gate-note[hidden] {
    display: none;
}

/* Module 10 progress / result line under Generate & Download SVG. */
.vector-status {
    margin: 10px 0 0;
    font-size: 12px;
    line-height: 1.4;
    color: var(--text-muted);
}
.vector-status[hidden] {
    display: none;
}
#modColorReplacer.is-gated > :not(.module-gate-note),
#modAntiAlias.is-gated > :not(.module-gate-note),
#modSmoothing.is-gated > :not(.module-gate-note) {
    opacity: 0.45;
    pointer-events: none;
}

/* Live Color Inspector -- a prepress diagnostic readout, not a
   button, so it gets its own quieter style rather than joining
   the --c-gray/--c-purple/etc. button palette above. Monospace
   (a real stack, not just "monospace" alone, for consistent
   glyph metrics across platforms) plus a `ch`-based min-width
   sized to the longest possible readout
   ("R:255 G:255 B:255 | H:360 S:100 L:100%", 39 characters) is
   what actually prevents jitter: every character in a monospace
   font occupies the identical advance width, so reserving that
   many `ch` up front means swapping "0" for "255" can never
   reflow neighboring UI (the Close button beside it, in
   particular) even though the two strings differ in length. */
.color-inspector {
    font-family: 'Consolas', 'Courier New', monospace;
    font-size: 12px;
    color: var(--text-muted);
    background: var(--bg-input);
    border: 1px solid var(--border-light);
    border-radius: 4px;
    padding: 8px 12px;
    min-width: 39ch;
    white-space: pre;
    text-align: center;
    user-select: none;
}

input[type="file"] { font-size: 14px; cursor: pointer; color: var(--text-muted); }
input[type="file"]::file-selector-button {
    background: var(--c-gray);
    color: white;
    border: none;
    padding: 6px 12px;
    border-radius: 4px;
    cursor: pointer;
    margin-right: 10px;
    font-weight: 600;
}
input[type="file"]::file-selector-button:hover { background: var(--c-gray-hover); }

input[type="number"] { 
    width: 60px; 
    padding: 4px; 
    margin-right: 10px; 
    border: 1px solid var(--border-light); 
    background: var(--bg-input); 
    color: white; 
    border-radius: 4px; 
    text-align: center; 
    font-family: monospace; 
}
input[type="number"]:focus {
    outline: none;
    border-color: var(--c-blue);
}

input[type="checkbox"], input[type="radio"] {
    margin-right: 8px;
    transform: scale(1.1);
    cursor: pointer;
    accent-color: var(--c-accent);
}

input[type="range"] {
    flex-grow: 1;
    margin: 0 8px;
    accent-color: var(--c-accent);
}

/* --- PREVIEW AND CANVAS --- */
.preview-controls-row {
    display: flex;
    justify-content: space-between;
    align-items: stretch;
    width: 100%;
    gap: 12px;
}

.bottom-box {
    background: var(--bg-panel);
    padding: 12px 16px;
    border-radius: 8px;
    border: 1px solid var(--border-light);
    box-sizing: border-box;
    box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}

/* Preview Background, inline in the action strip just left of Hold to
   View Original / Fit to Window: one row of radios plus the Custom
   swatch, at the strip buttons' own 30px height. */
.strip-bg-controls {
    display: flex;
    align-items: center;
    gap: 10px;
    flex: 0 0 auto;
    height: 30px;
    font-size: 12px;
    white-space: nowrap;
}
.strip-bg-title {
    font-weight: bold;
    color: var(--text-muted);
}
.strip-bg-controls label {
    display: flex;
    align-items: center;
    cursor: pointer;
}
.strip-bg-controls input[type="radio"] {
    margin: 0 5px 0 0;
}

.bottom-box-header {
    font-size: 13px;
    font-weight: bold;
    margin-bottom: 12px;
    display: flex;
    align-items: center;
    border-bottom: 1px solid var(--border-light);
    padding-bottom: 6px;
}

.bg-checkerboard { background-color: #fff; background-image: linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; }
.bg-black { background: #000; background-image: none; }
.bg-white { background: #fff; background-image: none; }

.canvas-container {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-grow: 1;
    width: 100%;
    /* Root cause of the tablet-landscape collapse: .center-column
       (its flex parent) has min-width:0 so it CAN shrink all the
       way to 0 whenever .right-column's fixed, flex-shrink:0
       width doesn't leave enough room -- which never happened on
       a wide desktop window, but is exactly what a ~800-1024px
       tablet landscape viewport does. A hard floor here means the
       canvas can, at worst, force the row to overflow (visible,
       obviously wrong, easy to spot) instead of silently
       shrinking to nothing (invisible, looks like the canvas
       vanished). The 800-1024px tablet band itself is handled
       properly below (see the dedicated media query), so this
       floor is a backstop, not the primary fix. */
    min-width: 280px;
    min-height: 240px;
    border: 1px solid var(--border-light);
    border-radius: 8px;
    box-shadow: 0 4px 10px rgba(0,0,0,0.3);
    overflow: hidden;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

/* Toggled by mainCanvasContainer's dragenter/dragleave/drop
   listeners while a file is being dragged over the viewport --
   reuses the existing --c-blue accent rather than introducing a
   new color, kept subtle (a border highlight + soft glow) rather
   than a bold overlay banner. */
.canvas-container.drag-active {
    border-color: var(--c-blue);
    box-shadow: 0 0 0 3px rgba(115, 99, 78, 0.35), 0 4px 10px rgba(0,0,0,0.3);
}

/* Ported from Kittana - Template's own #canvasPlaceholder CSS --
   an opaque var(--bg-base) overlay sitting on top of the canvas
   (canvas-container above is already position:relative), covering
   the transparency checkerboard entirely while no image is loaded.
   pointer-events:none is load-bearing, not decorative -- see the
   HTML comment on #canvasPlaceholder. */
.canvas-placeholder {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    background: var(--bg-base);
    color: var(--text-muted);
    text-align: center;
    pointer-events: none;
}
.canvas-placeholder p { margin: 0; font-size: 14px; }
.canvas-placeholder-sub { font-size: 12px; opacity: 0.7; }

canvas {
    display: block;
    cursor: crosshair;
    /* Hands touch gestures over the canvas entirely to our own
       touchstart/touchmove/touchend handlers (see the "Tablet
       touch canvas controls" script section) instead of the
       browser's native scroll/pinch-zoom-page handling. Only
       affects actual finger/touch input -- Apple Pencil on iPad
       Safari already dispatches ordinary mouse events, completely
       separate from touch, so the existing mouse-driven tools
       (Eraser, Wand, panning, etc.) are untouched by this. */
    touch-action: none;
    /* Fill the container box exactly. The render bitmap is sized from this
       same box (see resizeRenderBuffers), so display aspect always equals
       bitmap aspect and nothing is stretched.

       The previous `aspect-ratio: 1200 / 740` was fighting the observer:
       the bitmap got the container's dimensions while CSS forced a 1.62
       display ratio, so the preview was being squeezed on one axis. */
    width: 100%;
    height: 100%;
}

#cropContainer { display: none; width: 100%; flex-grow: 1; border: 1px solid var(--c-purple); border-radius: 8px; overflow: hidden; }
#cropImage { display: block; max-width: 100%; }

/* --- PIPELINE MODULES --- */
.pipeline-module {
    background: var(--bg-panel);
    border-radius: 8px;
    border: 1px solid var(--border-light);
    /* No overflow:hidden here -- it used to clip the accordion's
       square-cornered header/body into the card's rounded shape,
       but that also clipped the info-icon tooltip to this card's
       own (collapsed = short) box height. Corner-rounding is now
       handled directly on .pipeline-header/.module-body below, so
       the tooltip is free to render past the card's edge. */
    flex-shrink: 0;
    box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}

.pipeline-header {
    font-size: 14px;
    font-weight: 600;
    padding: 12px 15px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    cursor: pointer;
    background: #2a2a2a;
    user-select: none;
    border-left: 3px solid transparent;
    border-radius: 8px;
    overflow: visible;
    transition: all 0.2s;
}

/* When the module below is actually open/visible, only round the
   header's TOP corners -- the body picks up the bottom corners
   below -- so the seam between header and body stays flush
   instead of showing a rounded notch. Covers both the normal
   accordion (.module-body.open, toggled by toggleModule()) and
   the AI Upscale view's always-expanded sub-headers (module-body
   with an inline display:block, never using the .open class). */
.pipeline-header:has(+ .module-body.open),
.pipeline-header:has(+ .module-body[style*="display: block"]) {
    border-radius: 8px 8px 0 0;
}

.pipeline-header:hover {
    background: #333;
}

.pipeline-header.active-header {
    border-left: 3px solid var(--c-blue);
    background: #222;
}

/* Active Tool Glow -- premium pulsing indicator for a modal mask
   tool (Eraser/Color Replacer's Restrict Area brush) currently
   toggled ON, applied to that module's own accordion header (see
   updateActiveToolGlow()). Defined AFTER .active-header so its
   border/background win whenever a header is both expanded AND
   the active tool -- the "tool is live" cue takes priority over
   the plain "panel is open" one. Reuses --c-orange, the same
   color every toggleable tool button already turns when active,
   so the glow reads as an extension of that existing convention
   rather than a new, unrelated color language. */
@keyframes activeToolGlowPulse {
    0%, 100% {
        text-shadow: 0 0 4px var(--c-orange), 0 0 8px rgba(211, 84, 0, 0.6);
        box-shadow: 0 0 6px 1px rgba(211, 84, 0, 0.5), inset 0 0 12px rgba(211, 84, 0, 0.15);
    }
    50% {
        text-shadow: 0 0 10px var(--c-orange-hover), 0 0 20px rgba(230, 126, 34, 0.9);
        box-shadow: 0 0 16px 4px rgba(230, 126, 34, 0.8), inset 0 0 20px rgba(230, 126, 34, 0.25);
    }
}

.pipeline-header.active-tool-glow {
    color: #ffd9b3;
    border-left: 3px solid var(--c-orange);
    background: #2a1f14;
    animation: activeToolGlowPulse 1.6s ease-in-out infinite;
}

/* Info icon for each module header. The bubble itself is no
   longer a CSS ::after -- it's a single #global-tooltip div
   appended directly to <body> and positioned in JS via
   getBoundingClientRect() on mouseenter/mouseleave (see the
   initInfoTooltips() script near the end of <body>). That
   sidesteps accordion/sidebar/canvas stacking-context nesting
   entirely: a position:fixed body-level element has nothing
   to escape. */
.info-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 15px;
    height: 15px;
    margin-left: 7px;
    border-radius: 50%;
    background: var(--c-gray);
    color: var(--text-muted);
    font-size: 11px;
    font-weight: bold;
    font-style: italic;
    font-family: Georgia, 'Times New Roman', serif;
    line-height: 1;
    cursor: help;
    position: relative;
    transition: background 0.15s, color 0.15s;
}

.info-icon:hover, .info-icon:focus {
    background: var(--border-hover);
    color: var(--text-main);
    outline: none;
}

#global-tooltip {
    position: fixed;
    z-index: 999999;
    max-width: 280px;
    white-space: normal;
    background: #111;
    color: var(--text-main);
    font-size: 12px;
    font-weight: normal;
    font-style: normal;
    font-family: inherit;
    line-height: 1.5;
    padding: 10px 12px;
    border-radius: 6px;
    border: 1px solid var(--border-light);
    box-shadow: 0 4px 14px rgba(0,0,0,0.45);
    opacity: 0;
    transition: opacity 0.12s ease;
    pointer-events: none;
}

.module-body {
    padding: 15px;
    border-top: 1px solid var(--border-light);
    border-radius: 0 0 8px 8px;
    display: none;
    background: var(--bg-panel);
}

.module-body.open { display: block; }

.chevron { font-size: 11px; color: var(--text-muted); transition: transform 0.2s ease; }
.chevron.open { transform: rotate(180deg); color: white; }

/* --- MODULE 0: AI HD UPSCALE (flashy entry point) ---
   Gradient border via the double-background trick (one opaque
   layer clipped to padding-box for the fill, one gradient layer
   clipped to border-box for the border itself) so .pipeline-module's
   own border-radius/overflow:hidden still apply cleanly. Reuses
   Kittashi's existing blue/purple accents rather than introducing
   new colors -- "subtly flashier", not a new color language. */
.pipeline-module.module-ai-entry {
    border: 2px solid transparent;
    background:
        linear-gradient(var(--bg-panel), var(--bg-panel)) padding-box,
        linear-gradient(135deg, var(--c-blue), var(--c-purple)) border-box;
    box-shadow: 0 0 10px rgba(138, 119, 96, 0.35);
    cursor: pointer;
    margin-bottom: 4px;
    transition: box-shadow 0.2s ease;
}
.pipeline-module.module-ai-entry:hover {
    box-shadow: 0 0 16px rgba(138, 119, 96, 0.6);
}

/* Cross-promo link (Gang Sheet Builder) -- same card shape as a
   collapsed .pipeline-module + .pipeline-header combined into
   one element, since this has no separate header/body to
   accordion. Reuses --c-blue/--c-blue-hover, the SAME variables
   #btnCompare's own "Hold to View Original" button already uses
   -- despite the name, --c-blue holds this module's subdued tan
   accent (see :root); the name is a leftover from an earlier
   palette, not a mismatch with this button's intended color. */
/* AI HD Upscale + the Gang Sheet promo, 50/50 on one row. Both children
   are min-width:0 so a long label ellipsises instead of forcing the row
   wider than the sidebar. */
.sidebar-tool-row {
    display: flex;
    align-items: stretch;
    gap: 10px;
}
.sidebar-tool-row > * {
    /* border-box, or the AI entry's thicker gradient border makes its
       outer width differ from the promo's despite an identical basis. */
    box-sizing: border-box;
    /* An explicit half-minus-half-the-gap, rather than `flex: 1 1 0`:
       these two have different border widths and the equal-growth split
       did not land on a true 50/50. */
    flex: 0 0 calc(50% - 5px);
    min-width: 0;
    margin-bottom: 0;
}
.sidebar-tool-row .gang-sheet-promo {
    /* .gang-sheet-promo sets flex-shrink:0 for the full-width row it used
       to live on, and wins on source order at equal specificity. */
    flex: 0 0 calc(50% - 5px);
    padding: 10px 8px;
    font-size: 13px;
    line-height: 1.2;
}

.gang-sheet-promo {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    padding: 12px 15px;
    border-radius: 8px;
    border: 1px solid var(--border-light);
    box-shadow: 0 2px 6px rgba(0,0,0,0.15);
    background: var(--c-blue);
    color: #fff;
    font-size: 14px;
    font-weight: 600;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.2s, transform 0.2s;
}

.gang-sheet-promo:hover {
    background: var(--c-blue-hover);
    transform: translateY(-1px);
}

.module-ai-entry-header {
    cursor: pointer;
    font-weight: 700;
}

/* --- AI HD UPSCALE VIEW (#upscalerSidebar) --- */
.upscaler-actions {
    display: flex;
    gap: 10px;
    padding: 15px !important;
}
.upscaler-actions .btn-commit { background-color: var(--c-green); }
.upscaler-actions .btn-commit:hover { background-color: var(--c-green-hover); }
/* Disabled until an upscale actually succeeds this visit (see
   setUpscalerCommitEnabled()) -- native :disabled, not a fixed
   .is-disabled class, so keyboard/screen-reader activation is
   blocked too, not just the visual look and mouse clicks a
   pointer-events hack alone would stop. */
.upscaler-actions .btn-commit:disabled,
.upscaler-actions .btn-commit:disabled:hover {
    background-color: var(--c-green);
    opacity: 0.5;
    cursor: not-allowed;
}
.upscaler-actions .btn-cancel { background-color: var(--c-red); }
.upscaler-actions .btn-cancel:hover { background-color: var(--c-red-hover); }

/* --- AI MODEL TILES: ported from the standalone HD Upscaler tool.
   One button+description+divider block per upscale model.
   .model-upscale-btn reused the standalone tool's own
   var(--c-accent) -- that tool's primary button color, which here
   is --c-blue (Kittashi's own --c-accent is the slider/checkbox
   tier), so every such reference below is remapped to --c-blue*.
   Kittashi also has no "--c-blue-active"
   (no :active state existed to port), so is-loading/is-success
   (background-only) needed no such value. */
/* .model-upscale-idle carries the default blue background -- a
   class, not the #btn-upscale-* ID selectors this used to be keyed
   on. An ID selector's specificity (1,0,0) beats a two-class
   selector like .model-upscale-btn.is-success (0,2,0) no matter
   what order they're written in, so that rule below could never
   actually win: JS was toggling the is-success/is-loading classes
   correctly, but the ID rule kept painting blue over them
   regardless. Putting the default state at the SAME two-class
   specificity, mutually exclusive with is-success/is-loading (see
   resetAllUpscaleButtons()/applyUpscale() below, which always
   remove this class when adding either of those, and restore it
   when removing both), is what actually lets the state that's
   supposed to be active win. */
.model-upscale-btn.model-upscale-idle {
    background-color: var(--c-blue);
}
.model-upscale-btn.model-upscale-idle:hover {
    background-color: var(--c-blue-hover);
}

/* Radio + button side by side -- the radio is the "which result is
   currently on screen" selector (see selectCachedModel() in
   script), independent of the button's own idle/loading/success
   color (which just means "has this model ever been run this
   visit", and can be true for several models -- and several radios
   enabled -- at once now that results are cached instead of
   overwriting each other). min-width:0 on the button is required,
   not decorative -- without it a flex item won't shrink below its
   text's natural width no matter how little room .model-radio
   leaves it, which is what "shrink slightly to make room for the
   radio" actually needs here. */
.model-row {
    display: flex;
    align-items: center;
    gap: 10px;
}
.model-row .model-upscale-btn {
    flex: 1 1 auto;
    min-width: 0;
}
.model-radio {
    flex-shrink: 0;
    width: 18px;
    height: 18px;
    margin: 0;
    cursor: pointer;
    accent-color: var(--c-green);
}
.model-radio:disabled {
    cursor: not-allowed;
    opacity: 0.35;
}

/* "Original" -- styled to sit at the same visual weight as the
   model buttons below it (same panel/border/radius/padding as a
   .model-upscale-btn) without actually BEING a button -- clicking
   it only ever swaps which cached image is on screen, never fires
   the API, so a plain radio+label reads more honestly than
   borrowing the button's own "this triggers an action" look. */
.model-block-original {
    margin-bottom: 4px;
}
.model-row-original {
    background-color: var(--bg-panel);
    border: 1px solid var(--border-light);
    border-radius: 4px;
    padding: 8px 15px;
}
.model-original-label {
    flex: 1;
    font-weight: 600;
    font-size: 14px;
    color: var(--text-main);
    cursor: pointer;
    user-select: none;
}

.model-description {
    margin: 6px 0 0;
    font-size: 14px;
    line-height: 1.5;
    color: var(--text-main);
}

.model-divider {
    border: none;
    border-top: 1px solid var(--border-light);
    opacity: 0.5;
    margin: 14px 0;
}

/* "running kitten" loading state -- ported verbatim from the
   standalone tool (animation values/timing unchanged). */
.model-upscale-btn.is-loading,
#btnVectorExport.is-loading,
#btnAntiAlias.is-loading,
#btnSmoothing.is-loading {
    position: relative;
    overflow: hidden;
    cursor: wait;
    background-color: #7ec8e3;
}
.model-upscale-btn.is-loading:hover,
#btnVectorExport.is-loading:hover,
#btnVectorExport.is-loading:disabled,
#btnAntiAlias.is-loading:hover,
#btnSmoothing.is-loading:hover { background-color: #7ec8e3; opacity: 1; }

.model-upscale-btn.is-loading .ai-btn-label,
#btnVectorExport.is-loading .ai-btn-label,
#btnAntiAlias.is-loading .ai-btn-label,
#btnSmoothing.is-loading .ai-btn-label { opacity: 0; }

.model-upscale-btn.is-success { background-color: var(--c-green); }
.model-upscale-btn.is-success:hover { background-color: var(--c-green); }

/* display:none both hides the scene when idle and halts/resets the
   GIF + catPatrol animation; re-adding .is-loading always restarts
   both from a clean state instead of resuming mid-cycle. */
.ai-loading-scene {
    display: none;
    position: absolute;
    inset: 0;
    pointer-events: none;
}
.model-upscale-btn.is-loading .ai-loading-scene,
#btnVectorExport.is-loading .ai-loading-scene,
#btnAntiAlias.is-loading .ai-loading-scene,
#btnSmoothing.is-loading .ai-loading-scene { display: block; }

/* Sibling of the loading button (not a child) -- the button is too
   short to fit both the running kitten and a legible status line, so
   this sits just below it instead. Shown only while its sibling
   button carries .is-loading. */
.ai-status-text {
    display: none;
    text-align: center;
    font-size: 11px;
    font-weight: 700;
    color: var(--text-main);
    margin-top: -2px;
}
.model-upscale-btn.is-loading ~ .ai-status-text { display: block; }

.kitten-gif {
    position: absolute;
    top: 50%;
    left: -80px;
    height: 38px;
    width: auto;
    transform: translateY(-50%) scaleX(1);
    animation: catPatrol 6s linear infinite;
}

@keyframes catPatrol {
    0%   { left: -80px; transform: translateY(-50%) scaleX(1); }
    45%  { left: 100%;  transform: translateY(-50%) scaleX(1); }
    50%  { left: 100%;  transform: translateY(-50%) scaleX(-1); }
    95%  { left: -80px; transform: translateY(-50%) scaleX(-1); }
    100% { left: -80px; transform: translateY(-50%) scaleX(1); }
}

#upscaleError {
    display: none;
    background: rgba(231, 76, 60, 0.15);
    color: #ff6b6b;
    border: 1px solid #e74c3c;
    padding: 8px 10px;
    border-radius: 6px;
    font-size: 12px;
    margin-top: 10px;
    line-height: 1.4;
}

.slider-group { margin-bottom: 12px; display: flex; align-items: center; }
.sidebar label { display: inline-block; width: 140px; font-size: 13px; color: var(--text-muted); }

/* --- ALERTS & MODALS --- */
#alphaWarning {
    display: none;
    background: rgba(231, 76, 60, 0.15);
    color: #ff6b6b;
    border: 1px solid #e74c3c;
    padding: 8px 10px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: bold;
    margin-bottom: 12px;
}

/* AI Upscale's large-image downsampling notice -- informational,
   not a "something is wrong" alert like #alphaWarning's red
   scheme, so it gets its own quieter amber/advisory tone instead
   of reusing that danger palette for a benign FYI. */
.upscaler-info-note {
    background: rgba(243, 156, 18, 0.12);
    color: #f5b041;
    border: 1px solid rgba(243, 156, 18, 0.4);
    padding: 8px 10px;
    border-radius: 4px;
    font-size: 11px;
    line-height: 1.4;
    margin: 0 0 10px 0;
}

/* Pure Black/Color Detection (see processPureBlackCheck(),
   processHalftoneCutoffCheck(), and friends) --
   #pureBlackHeaderWarning is deliberately plain red text with no
   background/border -- "subtle", per spec, next to the collapsed
   module's own header instead of a full alert banner. */
#pureBlackHeaderWarning {
    color: #ff6b6b;
    font-size: 11px;
    font-weight: normal;
    margin-left: 8px;
}
/* Shared by Pre-Processing's Levels Black warning and Halftone
   Knockout's Tolerance warning -- a single fixed-height line,
   ALWAYS present in the layout, with no background/border: plain
   red text only. visibility (not display), so hiding it never
   collapses its box and nothing below it ever shifts when the
   warning appears/disappears -- opacity would do the same job, but
   visibility also correctly drops it from hit-testing/text-
   selection while hidden, not just from view. */
.tolerance-warning-line {
    display: block;
    height: 14px;
    line-height: 14px;
    overflow: hidden;
    color: #ff6b6b;
    font-size: 11px;
    font-weight: bold;
    margin-bottom: 4px;
    visibility: hidden;
}
/* Reusable red-outline state for a slider's paired number input --
   only wired to #preGradeBlackInput in this phase, but not scoped
   to that one ID so a later module can reuse it. The attribute
   selector doubles the specificity of the base input[type="number"]
   rule (see its own definition above) so this reliably wins
   instead of depending on source order alone -- the same class of
   bug the Neural Upscaling buttons hit earlier this session, not
   worth risking again here. */
input[type="number"].input-invalid-warning {
    border: 1px solid #e74c3c;
    outline: 1px solid #e74c3c;
}
input[type="number"].input-invalid-warning:focus {
    border-color: #e74c3c;
}

/* --- Tablet/touch optimizations -----------------------------------
   Additive only. Most of this is gated on (hover: none) and
   (pointer: coarse) -- true for a finger/Apple-Pencil touch
   surface, false for any mouse/trackpad pointer regardless of
   window size or aspect ratio -- specifically so it can never be
   reached by a desktop mouse user. The one width-based query
   below (the 800-1024px column layout fix) is a real layout bug
   independent of input type, so it's keyed on viewport width
   instead; #colorInspector's rule runs the same gate in reverse
   ((hover: hover) and (pointer: fine)) to show it ONLY for a real
   mouse/trackpad. */

#tabletPortraitOverlay {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    z-index: 2000;
    background: var(--bg-base);
    color: var(--text-main);
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    padding: 20px;
    box-sizing: border-box;
    font-size: 18px;
    line-height: 1.5;
}

@media (orientation: portrait) and (hover: none) and (pointer: coarse) {
    #tabletPortraitOverlay { display: flex; }
}

/* Phone-only device gate -- see its own HTML comment. Hidden
   globally by default so it's invisible on desktop and tablet;
   the (max-width: 767px) query below is the only place that ever
   reveals it (and hides the real app alongside it). */
#mobile-device-gate {
    display: none;
}

.mobile-gate-logo {
    max-width: 250px;
    width: 100%;
    height: auto;
    margin-bottom: 20px;
}

#mobile-device-gate h2 {
    margin: 0 0 12px 0;
    color: var(--text-main);
    font-size: 22px;
    font-weight: 700;
}

#mobile-device-gate p {
    margin: 0;
    max-width: 420px;
    color: var(--text-muted);
    font-size: 15px;
    line-height: 1.6;
}

/* Phone gate trigger -- deliberately its own plain (max-width:
   767px) query, no (orientation)/(hover)/(pointer) conditions
   the way every other gate in this file has: a phone is too
   small for this workspace in portrait OR landscape, touch OR
   not, so width alone is the right (and only) signal here. */
@media (max-width: 767px) {
    .app-header,
    .app-container,
    .app-footer {
        display: none !important;
    }
    #mobile-device-gate {
        display: flex !important;
        position: fixed;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
        z-index: 9999;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        text-align: center;
        background-color: var(--bg-input);
        padding: 20px;
        box-sizing: border-box;
    }
}

/* --- Tablet landscape (~800-1024px) column layout -----------------
   .center-column/.canvas-container's min-width:280px floor above
   is a backstop against total collapse, but on this specific
   width band it isn't enough on its own: .right-column's fixed
   420px, plus several rows inside .center-column whose own
   children refuse to shrink below fixed/inline-styled widths,
   together need more room than an 800-1024px viewport has left
   after body's own 40px of padding -- so something has to give,
   and it was always going to be .canvas-container (the only
   child with room to shrink) unless every one of those rows is
   told how to condense too. A few rules below use !important
   specifically to beat an inline `style="..."` attribute in the
   HTML (inline always outranks a stylesheet selector on its own,
   !important is the standard, narrowly-scoped way to still win
   that fight only inside this one media query) -- desktop, which
   never enters this query, keeps those inline values untouched. */
@media (min-width: 800px) and (max-width: 1024px) {
    .app-container {
        gap: 12px;
    }
    .right-column {
        width: 300px;
    }

    /* --- Item 1: hard-clamp the column itself. flex:1 1 0% means
       its share of the row is purely "whatever .right-column
       leaves," never content-driven; min-width:0 removes the
       default shrink floor a flex item otherwise gets from its
       own content; max-width:100% + overflow-x:hidden are the
       backstop for anything nested deeper than a direct flex
       child that might still insist on being wider (min-width:0
       on THIS element can't reach that) -- it gets clipped inside
       its own box instead of bleeding out under the sidebar.
       padding-bottom (item 4) gives the last scrolled module real
       breathing room instead of ending flush at the edge, which
       is what was reading as "cut off." box-sizing:border-box is
       load-bearing here, not decoration -- .center-column has no
       box-sizing of its own (defaults to content-box), and this
       element's height:100% (base CSS) is what makes it match
       .app-container/.right-column's own height exactly. Without
       border-box, this padding would ADD 24px on top of that
       100%, pushing this column's own bottom edge 24px past
       .app-container's -- which is exactly what was making the
       footer below it (positioned off .app-container's box, not
       this one) read as overlapping the last module instead of
       sitting cleanly beneath it (confirmed live: bottom edges
       mismatched by exactly 24px before this). */
    .center-column {
        flex: 1 1 0%;
        min-width: 0;
        max-width: 100%;
        box-sizing: border-box;
        overflow-x: hidden;
        overflow-y: auto;
        padding-bottom: 24px;
    }

    /* --- Item 2: condense the row above the viewport. */
    .top-action-bar {
        flex-wrap: wrap;
        row-gap: 8px;
    }
    /* .top-action-bar's own flex-wrap above only wraps its
       top-level children onto separate lines (the Load Image/
       Download/Reset group and the Close/color-inspector group) -- each of
       THOSE is itself an unwrapped flex row (inline `style` in
       the HTML, not a class, hence the structural :first-child
       selector), and at the low end of this width band the first
       one alone still needs ~540px, well past what's left after
       narrowing .right-column. Confirmed live at 800x600: without
       this, that group overflowed .center-column by ~115px. */
    .top-action-bar > div:first-child {
        flex-wrap: wrap;
        row-gap: 8px;
        gap: 8px !important;
    }
    /* The native file input's own "no file chosen" placeholder is
       what was reading as empty dead space between the filename
       and the Download button -- browsers truncate the filename
       text on their own once the control itself is width-
       constrained, nothing here hides the "Choose File" button. */
    .top-action-bar input[type="file"] {
        max-width: 130px;
    }
    .top-action-bar label {
        margin-right: 6px !important;
    }
    /* --- Item 3: condense the row below the viewport. Too narrow
       for the status line, Background and the three strip buttons on
       one line: let the action strip wrap. */
    .canvas-action-strip {
        flex-wrap: wrap;
    }

    /* Item 4 continued -- force real separation below
       .center-column's own box regardless of any rounding in its
       height, so the copyright line can't read as floating up
       into the last module's own space. */
    .app-footer {
        margin-top: 12px;
        position: relative;
        z-index: 1;
    }
}

/* Live Color Inspector (#colorInspector) is a pure hover readout
   -- see its own "===== Live Color Inspector =====" script
   section -- with no real equivalent on a touch device (a tap
   isn't a hover, and there's no finger-lift "mouseleave" to clear
   a stale reading afterward, so a synthetic touch-compatibility
   mousemove event can otherwise leave a permanently-wrong color
   frozen on screen). Shown only for an actual mouse/trackpad; the
   JS listener has its own matching (hover:hover) and
   (pointer:fine) guard so the underlying pixel sampling never
   even runs on a device this hides it on. */
#colorInspector { display: none; }
@media (hover: hover) and (pointer: fine) {
    #colorInspector { display: block; }
}

.floating-canvas-controls {
    display: none;
    position: absolute;
    left: 8px;
    bottom: 8px;
    z-index: 500;
    flex-direction: column;
    gap: 4px;
    background: var(--bg-panel);
    border: 1px solid var(--border-light);
    border-radius: 6px;
    padding: 4px;
    box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}

/* ~50% of the original 44px touch target -- deliberately smaller
   than the usual 44px touch-target minimum now that these sit
   inside the canvas's own corner instead of floating loose over
   the whole screen (including the left-side menus/toolbar, which
   is the problem this repositioning fixes); a bigger hit target
   here would eat into the canvas itself. */
.floating-canvas-controls button {
    width: 22px;
    height: 22px;
    padding: 0;
    font-size: 11px;
    line-height: 1;
    background-color: var(--c-gray);
}

.floating-canvas-controls button:hover {
    background-color: var(--c-gray-hover);
}

@media (hover: none) and (pointer: coarse) {
    .floating-canvas-controls { display: flex; }
}

/* --- Touch-tablet sidebar/footer swap ------------------------------
   Isolated, standalone query -- doesn't touch .center-column, any
   other existing media query, or any base/global CSS rule.
   (pointer: coarse) is what keeps this off every desktop mouse
   user regardless of window width: a resized desktop browser is
   still (pointer: fine), so it can never match this no matter how
   narrow the width/landscape conditions get -- (hover: none)
   dropped again (was added briefly for older Android) since
   pointer:coarse alone already does that job; the width bumped
   1440->1600px to also catch high-res Android tablets and
   devices with altered display scaling. */
@media (max-width: 1600px) and (orientation: landscape) and (pointer: coarse) {
    .right-column {
        width: 320px !important;
        min-width: 320px !important;
        flex-shrink: 0;
    }
    .app-footer {
        display: none !important;
    }
    .sidebar-footer {
        display: block !important;
        margin-top: 10px;
        padding-top: 10px;
        border-top: 1px solid var(--border-light);
        font-size: 12px;
        color: var(--text-muted);
        text-align: center;
    }
    .sidebar-footer a {
        color: var(--accent-link);
        text-decoration: none;
    }
    .sidebar-footer a:hover {
        text-decoration: underline;
    }

    /* --- Phase 2: top action bar (above the viewport) -------------
       Same query, same rules as Phase 1 -- (pointer: coarse) keeps
       every rule below off any desktop mouse session regardless of
       window width, and nothing here touches .center-column's own
       layout, the canvas/viewport logic, or any JS. */

    /* Item 1 -- pull the Load Image/Download/Reset group tighter.
       .top-action-bar > div:first-child's gap:15px is an inline
       `style="..."` in the HTML, so !important is what's needed to
       win over it here without touching that inline value itself
       (desktop, outside this query, keeps its original 15px). The
       native file input's own "No file chosen" placeholder is what
       actually reads as empty space before the Download button
       even with the gap tightened -- capping its width is what
       closes that gap, not the gap value alone. */
    .top-action-bar > div:first-child {
        gap: 8px !important;
    }
    .top-action-bar input[type="file"] {
        max-width: 130px;
    }

    /* Item 3 -- protect the Close button. .top-action-bar is the
       parent container its children (Load Image group,
       Close/inspector group) sit inside -- max-width:100% +
       box-sizing:border-box is the hard clamp keeping IT from ever
       exceeding .center-column's own width, and flex-wrap:wrap is
       what lets its children actually drop to new lines instead of
       forcing overflow past the right edge when they don't all fit
       on one. */
    .top-action-bar {
        max-width: 100%;
        box-sizing: border-box;
        flex-wrap: wrap;
        row-gap: 8px;
    }

    /* Stop scroll chaining: scrolling the sidebar to its end (or
       start) must never hand the gesture off to the page/canvas
       behind it. .right-column itself doesn't currently scroll --
       .sidebar (its child, #toolModuleSidebar/#upscalerSidebar,
       base CSS's own overflow-y:scroll) is the actual scroll
       container -- so both get overscroll-behavior:contain here:
       .right-column as a belt-and-suspenders in case that ever
       changes, .sidebar because that's where containment actually
       has to apply to do anything. */
    .right-column, .sidebar {
        overscroll-behavior: contain;
    }
}


/* ==========================================================================
   COLOR MERGE
   Everything below is new to Color Merge (not ported from Kittashi).
   Selectors are deliberately two classes deep where they style a <button>
   or <input> -- the base `button:not([class*="cl-"])`,
   `input[type="checkbox"]` and `input[type="range"]` rules above are
   each (0,1,1), so a single class would tie them and win only on source
   order. Swatch colors always come from the --swatch-color custom
   property, set per element by script.js.
   ========================================================================== */

/* --- Sidebar panel (directly below Module 6) --- */
.color-merge-panel {
    display: flex;
    flex-direction: column;
    gap: 12px;
    flex-shrink: 0;
    padding: 15px;
    background: var(--bg-panel);
    border: 1px solid var(--border-light);
    border-radius: 8px;
    box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}

/* --- Grouping Tolerance slider --- */
/* Sits directly above Analyze Colors and reads as part of it: no card of
   its own, just a label row, the track, and the two end labels that say
   which way is which. */
.grouping-tolerance {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.grouping-tolerance-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-main);
    cursor: pointer;
}
.grouping-tolerance-value {
    font-size: 15px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--c-green-hover);
}
/* The base input[type="range"] rule is flex-grow:1 with side margins, for
   the .slider-group rows it was written for -- neither applies here. */
.grouping-tolerance input[type="range"] {
    width: 100%;
    margin: 0;
    flex-grow: 0;
}
.grouping-tolerance-scale {
    display: flex;
    justify-content: space-between;
    font-size: 10px;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.color-merge-panel .btn-analyze-colors {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    /* 12px vertical, down from 16px -- about a quarter off the button's
       height, to buy the row back for Analyzed Colors below. */
    padding: 12px 20px;
    font-size: 17px;
    font-weight: 700;
    letter-spacing: 0.3px;
    border-radius: 8px;
    background: var(--c-green);
    box-shadow: 0 4px 14px rgba(184, 145, 95, 0.35);
}
.color-merge-panel .btn-analyze-colors:not(:disabled):hover {
    background: var(--c-green-hover);
    box-shadow: 0 6px 18px rgba(203, 175, 138, 0.45);
    transform: translateY(-1px);
}
.color-merge-panel .btn-analyze-colors:not(:disabled):active {
    transform: translateY(0);
}
.color-merge-panel .btn-analyze-colors:disabled:not(.is-busy) {
    background: var(--c-gray);
    box-shadow: none;
    color: var(--text-muted);
    cursor: not-allowed;
}

/* While analysis runs: stays green (it's working, not unavailable) with
   the palette icon spinning. */
.color-merge-panel .btn-analyze-colors.is-busy {
    cursor: progress;
}
.color-merge-panel .btn-analyze-colors.is-busy svg {
    animation: color-merge-spin 1s linear infinite;
}
@keyframes color-merge-spin {
    to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
    .color-merge-panel .btn-analyze-colors.is-busy svg {
        animation: none;
    }
}

/* --- Find on Canvas (action strip) --- */
/* Sizing comes from .strip-btn above. The look is its own: a dark,
   desaturated spectrum so it reads as the colour tool it is, next to two
   plain utility buttons -- muted enough that white text holds its contrast
   across the whole sweep. */
.canvas-action-strip .btn-find-color {
    background: linear-gradient(100deg, #5a3548 0%, #4a3f66 32%, #2d5566 64%, #3e5a3a 100%);
    border: 1px solid rgba(255, 255, 255, 0.14);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
    transition: filter 0.15s ease, box-shadow 0.15s ease;
}
.canvas-action-strip .btn-find-color:not(:disabled):hover {
    filter: brightness(1.2) saturate(1.15);
}
.btn-find-color-icon {
    flex: 0 0 auto;
}
/* Active = armed, waiting for a canvas click -- same orange Kittashi
   uses for an active Wand/Eraser. */
.canvas-action-strip .btn-find-color.is-active {
    background: var(--c-orange);
    box-shadow: 0 0 0 2px rgba(230, 126, 34, 0.35);
}
.canvas-action-strip .btn-find-color.is-active:hover {
    background: var(--c-orange-hover);
}
.canvas-action-strip .btn-find-color:disabled {
    opacity: 0.5;
    filter: grayscale(0.6);
    cursor: not-allowed;
}
.finder-status {
    margin: -4px 0 0;
    font-size: 12px;
    line-height: 1.4;
    text-align: center;
    color: var(--text-muted);
}
.finder-status:empty {
    display: none;
}

/* --- Current Colors counter --- */
.palette-color-counter {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 6px;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-muted);
}
.palette-color-counter[hidden] {
    display: none;
}
#currentColorCount {
    font-size: 18px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--text-main);
}

/* --- Flatten Image (sidebar) --- */
/* Destructive commit, so it reads like one: the same solid-purple weight
   Kittashi's own tool buttons carry, not the quiet grey of the Find Color
   toggle directly above it. */
.color-merge-panel .btn-flatten-image {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 11px 14px;
    font-size: 14px;
    font-weight: 700;
    border-radius: 6px;
    background: var(--c-purple);
}
.color-merge-panel .btn-flatten-image:not(:disabled):hover {
    background: var(--c-purple-hover);
}
.color-merge-panel .btn-flatten-image:disabled {
    background: var(--c-gray);
    color: var(--text-muted);
    cursor: not-allowed;
}
/* Mid-bake: stays purple (it's working, not unavailable) with the stack
   icon spinning -- same treatment as the Analyze Colors button. */
.color-merge-panel .btn-flatten-image.is-busy {
    background: var(--c-purple);
    color: #fff;
    cursor: progress;
}
.color-merge-panel .btn-flatten-image.is-busy svg {
    animation: color-merge-spin 1s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
    .color-merge-panel .btn-flatten-image.is-busy svg {
        animation: none;
    }
}

/* --- Action strip: full width, directly under the viewport --- */
.canvas-action-strip {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 6px 10px;
    box-sizing: border-box;
    background: var(--bg-panel);
    border: 1px solid var(--border-light);
    border-radius: 8px;
}
.canvas-action-strip .finder-status {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    text-align: left;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* The three strip buttons share one box model so they sit at exactly the
   same height. Height is pinned rather than left to padding: the Find
   button carries an icon the other two don't, and padding alone would let
   it come out a pixel or two taller. Two classes deep to beat the global
   `button:not([class*="cl-"])` reset (width:100%, 8px 15px padding);
   background is deliberately NOT set here, so #btnFit keeps its grey and
   #btnCompare its accent from their own ID rules. */
.canvas-action-strip .strip-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    flex: 0 0 auto;
    width: auto;
    height: 30px;
    padding: 0 12px;
    font-size: 12px;
    line-height: 1;
    white-space: nowrap;
    border-radius: 6px;
}
/* :empty collapses the status line in the sidebar layout it came from --
   here it has to keep holding the gap open, or the Find button jumps
   left and right as messages come and go. */
.canvas-action-strip .finder-status:empty {
    display: block;
}

/* --- Swatch Bank (bottom row) -- now the Groups strip alone; the raw
   swatches moved to the sidebar (see .ungrouped-pool). --- */
/* Full width, and the full height the Background box used to hold the
   row open at: the Group cards stretch down into it (see
   .color-group-list / .color-group-dropzone below). */
.swatch-bank {
    flex: 1 1 100%;
    min-width: 0;
    min-height: 143px;
    display: flex;
    flex-direction: column;
}
.swatch-bank-header {
    justify-content: space-between;
    gap: 12px;
}

/* --- Analyzed Colors (sidebar) --- */
/* One narrow column with real height, which is what a spectrum of
   swatches actually wants -- it used to share the bottom row with the
   Groups and got about 170px of width for it. */
.ungrouped-pool {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 4px;
    border: 1px solid transparent;
    border-radius: 6px;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.ungrouped-pool.is-drop-target {
    border-color: var(--c-green-hover);
    box-shadow: 0 0 0 2px rgba(203, 175, 138, 0.35);
}
.pool-label {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* The Groups strip: one horizontal row that scrolls sideways (never
   wraps), so adding a Group never grows the panel and squeezes the canvas
   above it -- see scrollIntoContainerX(), which is what brings a new or
   just-found Group back into view. */
.color-group-list {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 0;
    /* Wheel-over-strip is remapped to scrollLeft in script.js; this keeps
       the browser from also claiming the gesture as an overscroll/back
       navigation once an end is reached. */
    overscroll-behavior-x: contain;
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 10px;
    padding: 4px 2px 6px;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
    scrollbar-color: var(--border-hover) var(--bg-base);
}
.color-group-list:empty::before {
    content: "Select swatches on the left, then \201C+ Create Group\201D to merge them into one target color.";
    align-self: stretch;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    padding: 12px;
    border: 1px dashed var(--border-light);
    border-radius: 6px;
    font-size: 12px;
    line-height: 1.4;
    text-align: center;
    color: var(--text-muted);
}

/* One Group = one target color. --group-color (written by
   syncGroupTargetDisplay()) drives the thick border that identifies it at
   a glance, plus the tag and eyedropper accents that pick it up. */
.color-group {
    --group-color: #000000;
    flex: 0 0 auto;
    width: 150px;
    max-width: 150px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 7px;
    background: var(--bg-input);
    border: 4px solid var(--group-color);
    border-radius: 8px;
    /* A light target color on the dark panel would otherwise bleed into
       the background at the border's outer edge. */
    box-shadow: 0 0 0 1px var(--border-light);
    transition: box-shadow 0.15s ease;
}
.color-group.is-drop-target {
    box-shadow: 0 0 0 1px var(--border-light), 0 0 0 3px var(--c-green-hover);
}
/* Armed eyedropper -- the whole card, not just the icon, so it's obvious
   which Group the next canvas click is going to. */
.color-group.is-eyedropping {
    box-shadow: 0 0 0 1px var(--border-light), 0 0 0 3px var(--c-orange);
}

.color-group-tag {
    display: flex;
    align-items: center;
    gap: 4px;
    min-width: 0;
}
/* The hex + its native color input. A real <label> around the input, so
   clicking anywhere on it opens the OS picker with no JS. */
.color-group-pick {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 5px;
    padding: 2px 4px;
    border-radius: 4px;
    cursor: pointer;
}
.color-group-pick:hover {
    background: var(--bg-panel);
}
/* Strip the browser's own chrome off <input type="color"> so it reads as
   a plain swatch chip. The inner ::-webkit-color-swatch is what actually
   carries the color in Chrome/Safari. */
.color-group .color-group-color {
    flex: 0 0 auto;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 1px solid rgba(255, 255, 255, 0.25);
    border-radius: 3px;
    background: none;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
}
.color-group .color-group-color::-webkit-color-swatch-wrapper { padding: 0; }
.color-group .color-group-color::-webkit-color-swatch { border: none; border-radius: 2px; }
.color-group .color-group-color::-moz-color-swatch { border: none; border-radius: 2px; }
.color-group-hex {
    flex: 1 1 auto;
    min-width: 0;
    font-family: monospace;
    font-size: 11px;
    font-weight: 700;
    color: var(--text-main);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.color-group .color-group-eyedropper,
.color-group .color-group-remove {
    flex: 0 0 auto;
    width: 22px;
    height: 22px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 4px;
    background: none;
    color: var(--text-muted);
    line-height: 1;
}
.color-group .color-group-eyedropper:hover {
    color: #fff;
    background: var(--c-orange);
}
.color-group .color-group-eyedropper[aria-pressed="true"] {
    color: #fff;
    background: var(--c-orange);
}
.color-group .color-group-remove {
    font-size: 16px;
}
.color-group .color-group-remove:hover {
    color: #fff;
    background: var(--c-red);
}

.color-group-meta {
    font-size: 10px;
    line-height: 1.3;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.color-group-name {
    font-weight: 700;
    color: var(--text-main);
}
/* Grows into whatever height its card has spare (the cards stretch to
   the Groups strip's height), still capped so a long swatch list scrolls
   rather than growing the strip and squeezing the canvas. */
.color-group-dropzone {
    flex: 1 1 auto;
    min-height: 34px;
    max-height: 92px;
    overflow-y: auto;
    padding: 5px;
    border: 1px dashed var(--border-hover);
    border-radius: 4px;
    scrollbar-width: thin;
    scrollbar-color: var(--border-hover) var(--bg-base);
}
.color-group-dropzone:empty::before {
    content: "Drop swatches here";
    grid-column: 1 / -1;
    align-self: center;
    font-size: 10px;
    text-align: center;
    color: var(--text-muted);
}
.swatch-bank-count {
    font-weight: normal;
    color: var(--text-muted);
}
.swatch-bank .btn-unmerge,
.swatch-bank .btn-create-group {
    flex: 0 0 auto;
    width: auto;
    padding: 4px 10px;
    font-size: 11px;
    border-radius: 4px;
    background: var(--c-gray);
}
.swatch-bank .btn-unmerge:not(:disabled):hover {
    background: var(--c-gray-hover);
}
/* The primary action of the whole panel -- blue, and bolder than its
   neighbour, since everything else down here funnels into it. */
.swatch-bank .btn-create-group {
    font-weight: 700;
    background: var(--c-blue);
}
.swatch-bank .btn-create-group:not(:disabled):hover {
    background: var(--c-blue-hover);
}
.swatch-bank .btn-unmerge:disabled,
.swatch-bank .btn-create-group:disabled {
    background: var(--c-gray);
    opacity: 0.5;
    cursor: not-allowed;
}

/* --- Swatches (shared by the bin and the bucket drop zones) ---
   auto-fill + minmax(22px, 1fr) packs as many ~22px chips per row as
   fit and stretches them to meet the right edge exactly; aspect-ratio
   keeps them square. */
.swatch-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(22px, 1fr));
    gap: 4px;
    /* Shift+Click range-select would otherwise drag a text selection
       across the page along with it. */
    -webkit-user-select: none;
    user-select: none;
}
/* Holds every analyzed color, so it scrolls rather than growing the
   sidebar without limit. Taller than the 128px it had under the viewport
   -- height is the whole reason this moved here. */
.swatch-bank-grid {
    flex: 1 1 auto;
    max-height: 260px;
    overflow-y: auto;
    padding: 2px 4px 2px 2px;
    scrollbar-width: thin;
    scrollbar-color: var(--border-hover) var(--bg-base);
}
.swatch-bank-grid:empty::before {
    content: "Swatches will appear here after analysis.";
    grid-column: 1 / -1;
    padding: 4px 0;
    font-size: 12px;
    color: var(--text-muted);
}

.swatch-grid .swatch-chip {
    width: auto;
    aspect-ratio: 1;
    padding: 0;
    border-radius: 3px;
    background: var(--swatch-color, transparent);
    border: 1px solid rgba(255, 255, 255, 0.12);
    cursor: pointer;
    transition: transform 0.1s ease, box-shadow 0.1s ease, opacity 0.1s ease;
}
.swatch-grid .swatch-chip:hover {
    position: relative;
    z-index: 1;
    transform: scale(1.25);
    box-shadow: 0 2px 8px rgba(0,0,0,0.6);
}
.swatch-grid .swatch-chip:focus-visible {
    outline: 2px solid var(--c-green-hover);
    outline-offset: 1px;
}
/* Selected: a white ring with a dark gap, legible on any swatch color. */
.swatch-grid .swatch-chip.is-selected {
    position: relative;
    z-index: 1;
    box-shadow: 0 0 0 2px var(--bg-panel), 0 0 0 4px #ffffff;
}
.swatch-grid .swatch-chip.is-dragging {
    opacity: 0.3;
}
/* Right-click-and-hold: this swatch's pixels are flooded magenta on the
   canvas right now. Ringed in the same magenta so the chip and the
   canvas read as one gesture. */
.swatch-grid .swatch-chip.is-masking {
    position: relative;
    z-index: 3;
    box-shadow: 0 0 0 2px var(--bg-panel), 0 0 0 4px #ff00ff;
}
/* Find Color on Canvas: the found swatch pulses, then keeps an amber
   ring until the next find. Amber, not white, so it never reads as
   "selected"; a found swatch that IS selected shows both rings. */
@keyframes swatch-target-pulse {
    0%   { transform: scale(1.7); box-shadow: 0 0 0 2px var(--bg-panel), 0 0 0 4px #f5b041, 0 0 18px 4px rgba(245, 176, 65, 0.8); }
    35%  { transform: scale(1.1); }
    65%  { transform: scale(1.5); box-shadow: 0 0 0 2px var(--bg-panel), 0 0 0 4px #f5b041, 0 0 14px 3px rgba(245, 176, 65, 0.7); }
}
.swatch-grid .swatch-chip.is-targeted {
    position: relative;
    z-index: 2;
    box-shadow: 0 0 0 2px var(--bg-panel), 0 0 0 4px #f5b041;
    animation: swatch-target-pulse 1.6s ease-out;
}
.swatch-grid .swatch-chip.is-targeted.is-selected {
    box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px #f5b041;
}
@media (prefers-reduced-motion: reduce) {
    .swatch-grid .swatch-chip.is-targeted {
        animation: none;
    }
}


/* ==========================================================================
   HD UPSCALER SCOPED THEME (base #703D70)
   The embedded AI HD Upscaler wears its own purple identity, separate
   from this app's host accent. Monochromatic tiers, all hue 300:
     --up-bright / -hover   CTA tier: Commit, Apply Crop, success state
                            (white text >= 4.5:1)
     --up-base              standard tier: model buttons, crop buttons,
                            radios, sliders, checkboxes
     --up-dark              hover for the standard tier, active borders
     --up-deep              tracks and subtle fills
   Scoped by redefining the host's own accent variables on the upscaler
   containers ONLY -- custom properties cascade, so every existing rule
   inside them (--c-green CTAs, --c-blue model buttons, --c-purple crop
   buttons, --c-accent sliders) re-themes without edits, while the rest of
   the app keeps its host palette. Red (Cancel) and orange (Reset Image)
   are functional warning colors, not theme, and stay as they are. The
   header strip and logo swap is driven by body.upscaler-active, toggled
   by setUpscalerBranding() alongside the sidebar itself.
   ========================================================================== */
#upscalerSidebar {
    --up-bright: #A65AA6;
    --up-bright-hover: #8F4C8F;
    --up-base: #703D70;
    --up-dark: #552E55;
    --up-deep: #3A203A;

    --c-green: var(--up-bright);
    --c-green-hover: var(--up-bright-hover);
    --c-blue: var(--up-base);
    --c-blue-hover: var(--up-dark);
    --c-purple: var(--up-base);
    --c-purple-hover: var(--up-dark);
    --c-accent: var(--up-base);
    --accent-link: #C58FC5;
}

#upscalerSidebar {
    scrollbar-color: var(--up-dark) var(--up-deep);
}
#upscalerSidebar::-webkit-scrollbar-track { background: var(--up-deep); }
#upscalerSidebar::-webkit-scrollbar-thumb { background: var(--up-dark); }
#upscalerSidebar::-webkit-scrollbar-thumb:hover { background: var(--up-base); }

#upscalerSidebar .pipeline-header {
    border-left-color: var(--up-base);
}
#upscalerSidebar .pipeline-header:hover {
    background: var(--up-deep);
}

#upscalerSidebar input[type="range"],
#upscalerSidebar input[type="checkbox"],
#upscalerSidebar input[type="radio"] {
    accent-color: var(--up-base);
}

/* The running-kitten loading fill was a hardcoded light blue. */
#upscalerSidebar .model-upscale-btn.is-loading,
#upscalerSidebar .model-upscale-btn.is-loading:hover {
    background-color: #D9B8D9;
}

/* Header branding while the upscaler view is open (paths relative to
   this app's folder: the shared assets live one level up). */
body.upscaler-active .app-header {
    background-image: url('../assets/halftone-strip-upscaler.png');
}
body.upscaler-active .brand-logo {
    filter: drop-shadow(0 0 4px rgba(112, 61, 112, 0.45));
}
