/* ==========================================================================
   Kittashi AI HD Upscaler -- standalone
   Built from DTF Prep's own stylesheet (the embedded upscaler it shipped
   with), keeping only the shared shell -- header, modals, canvas
   viewport -- plus the upscaler's own panel. Sign-in, Pricing and the
   Fork prompt are styled by ../assets/kittashi-paywall.css. The upscaler's
   purple identity (base #703D70) is the whole page's theme here, set once
   on :root, so every shared rule that reads --c-green/--c-blue/--c-purple
   picks it up without edits.
     --up-bright / -hover   CTA tier: Download, Apply Crop, success state
                            (white text >= 4.5:1)
     --up-base              standard tier: model buttons, crop buttons,
                            radios, focus rings
     --up-dark              hover for the standard tier, scrollbar thumb
     --up-deep              tracks and subtle fills
   Red (Cancel Crop) and orange (Reset Image) are functional warning
   colors, not theme, and stay as they are in the rest of the suite.
   ========================================================================== */
: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;

    /* HD Upscaler purple tiers */
    --up-bright: #A65AA6;
    --up-bright-hover: #8F4C8F;
    --up-base: #703D70;
    --up-dark: #552E55;
    --up-deep: #3A203A;

    /* Shared functional names, mapped onto the tiers above */
    --c-purple: var(--up-base);
    --c-purple-hover: var(--up-dark);
    --c-green: var(--up-bright);
    --c-green-hover: var(--up-bright-hover);
    --c-blue: var(--up-base);
    --c-blue-hover: var(--up-dark);
    --c-red: #c0392b;
    --c-red-hover: #e74c3c;
    --c-orange: #d35400;
    --c-orange-hover: #e67e22;
    --c-gray: #444;
    --c-gray-hover: #555;

    --accent-link: #C58FC5;
}

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-upscaler.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(). */
    cursor: pointer;
    filter: drop-shadow(0 0 4px rgba(112, 61, 112, 0.45));
}

/* App title beside the logo -- standardized across the suite (DTF Prep,
   Color Merge, the Gang Sheet Builder): 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. Filled with --c-blue (the
   upscaler's base purple here), always, 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, 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 swatch (Preview Background's Custom Color) -- 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. */
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);
}

/* Functional Button Colors */
/* 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 */
#btnUpscalerCrop, #btnUpscalerAutoCrop { background-color: var(--c-purple); }
#btnUpscalerCrop:hover, #btnUpscalerAutoCrop:hover { background-color: var(--c-purple-hover); }

#upscalerBtnApplyCrop { background-color: var(--c-green); }
#upscalerBtnApplyCrop:hover { background-color: var(--c-green-hover); }

#upscalerBtnCancelCrop { background-color: var(--c-red); }
#upscalerBtnCancelCrop:hover { background-color: var(--c-red-hover); }

#btnUpscalerResetImage { background-color: var(--c-orange); }
#btnUpscalerResetImage:hover { background-color: var(--c-orange-hover); }

#btnCompare { background-color: var(--c-blue); }
#btnCompare:hover { background-color: var(--c-blue-hover); }

#btnFit { background-color: var(--c-gray); }
#btnFit:hover { background-color: var(--c-gray-hover); }

/* Shared modal box -- the overlays below (feedback) are a fixed
   backdrop around one of these. */
.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;
}

/* --- Beta Feedback modal -- same fixed-overlay/.modal-box shell
   as the shared .modal-box 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() in script.js 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;
}

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

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

.fine-num-input { width: 65px !important; }

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

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

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

.preview-buttons-column {
    display: flex;
    flex-direction: column;
    gap: 8px;
    justify-content: flex-start;
    width: 180px;
    flex-shrink: 0;
}

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

.bg-box { width: 280px; flex-shrink: 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(112, 61, 112, 0.45), 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;
}

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

/* --- 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) -- Kittashi has no such variable, its equivalent
   primary accent is --c-blue, so every --c-accent* 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 {
    position: relative;
    overflow: hidden;
    cursor: wait;
    background-color: #D9B8D9;
}
.model-upscale-btn.is-loading:hover { background-color: #D9B8D9; }

.model-upscale-btn.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 { 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); }

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

/* --- 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. */

#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 group
       and Close) -- 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. .bg-box
       gets flex-grow:0 (unlike .adjustments-box/
       .preview-buttons-column) so it only ever claims the narrow
       width its own content -- 4 radio options and a color
       swatch -- actually needs, leaving the space it used to
       reserve at a flat 280px for the sliders and buttons
       instead. */
    .bg-box {
        width: auto;
        flex: 0 1 130px;
        min-width: 0;
    }
    .preview-buttons-column {
        width: auto;
        flex: 1 1 140px;
        min-width: 0;
    }

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

.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 both its children (the Load Image group and
       Close) 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;
    }
}

/* --- Upscaler purple accents on the shared shell --- */
.sidebar {
    scrollbar-color: var(--up-dark) var(--up-deep);
}
.sidebar::-webkit-scrollbar-track { background: var(--up-deep); }
.sidebar::-webkit-scrollbar-thumb { background: var(--up-dark); }
.sidebar::-webkit-scrollbar-thumb:hover { background: var(--up-base); }

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

/* Fit / Hold to View Original sit right beside Preview Background --
   there's no Post-Processing panel here to fill the row between them. */
.preview-controls-row {
    justify-content: flex-start;
}

