/* public/css/cs-demo.css — ColorSmith It! button, §7.4 disclosure, and processing UI (P2-T2).

   Scoped to sit inside .cc-root's DOM subtree (see index.html: .cs-panel lives inside
   .cc-results) so it inherits the counter's --cc-* custom properties defined on .cc-root in
   color-counter.css -- this file never edits color-counter.js/.css, which stay frozen (product
   spec §3). Fallback values are supplied on every var() in case this file is ever loaded outside
   that subtree.

   Result panel (wipe viewer / palette / score / slider) is out of scope here -- it lands in
   P2-T3/T4. This file covers only the [ColorSmith It!] button, the honest downscale disclosure,
   and the real-progress processing state (product spec §13.2/§13.3, §7.4).
*/

/* Single-stage takeover + mobile-first layout (owner ruling 2026-08-12; sizing revised same day
   12:28 per owner live-testing feedback -- see the two numbered points below). Overrides
   color-counter.css's wide/two-column .cc-root sizing -- this file loads AFTER color-counter.css
   in index.html, so an equal-specificity single-class rule here wins by source order.
   color-counter.js/.css stay untouched (§3 frozen counter logic); only the layout is amended.

   Round 2, point 2 ("increase the overall image size... let the viewer scale up"): .cc-root is
   now the OUTER hero bound -- min(92vw, 1100px), not a flat 420px -- so the source-image preview
   (.cc-image-pane, full width of .cc-root) and the wipe viewer (.cs-wipe, capped separately
   below to the same bound) can both grow to hero size on desktop. On a narrow viewport this
   formula degrades to roughly the viewport width itself (e.g. 92% of 400px ~= 368px), so "small
   screens keep the current single-column behavior" falls out of the SAME min() with no separate
   breakpoint needed. Non-image "controls" (header, results text, actions, and everything inside
   #cs-panel except the wipe viewer itself -- button/processing/counts/palette/slider/notes)
   get their OWN narrower cap below (--cs-control-width) so they don't stretch edge-to-edge at
   hero width; most of #cs-panel's children already carried their own individual max-width caps
   (.cs-processing/.cs-palette/.cs-slider/etc.) from the original P2-T2/T3/T4 work and needed no
   change. */
:root {
    --cs-hero-width: min(92vw, 1100px);
    --cs-control-width: 480px;
}

.cc-root {
    max-width: var(--cs-hero-width);
    width: 100%;
    margin: 0 auto;
}

/* Round-3 re-review fix (owner second addendum, 2026-08-12): these three selectors must match
   this file's own established `.cc-root .cc-X` compound pattern (see `.cc-root .cc-preview`,
   `.cc-root .cc-hint[hidden]` above) to reliably beat color-counter.css's specificity, not just
   its source order. A bare `.cc-header` (specificity 0,1,0) loses a same-property tie against
   color-counter.css's `.cc-root .cc-header` (0,2,0) regardless of which file loads second --
   only equal-or-higher specificity plus later source order is a guaranteed win. Caught via
   screenshot review: .cc-header was rendering at full hero width instead of the intended 480px
   control column. */
.cc-root .cc-header,
.cc-root .cc-results,
.cc-root .cc-actions {
    max-width: var(--cs-control-width);
    margin-left: auto;
    margin-right: auto;
}

/* Overrides color-counter.css's `.cc-root .cc-preview { max-height: clamp(200px, 58vh, 860px) }`
   -- same 80vh ceiling as the post-result wipe viewer below, so the pre-result source preview
   doesn't feel small relative to the hero-size result comparison that follows it. Same selector,
   same specificity, later in source order (this file loads after color-counter.css) -- everything
   else about .cc-preview (object-fit, border-radius, width:auto/height:auto) is left as-is. */
.cc-root .cc-preview {
    max-height: 80vh;
}

/* Pre-existing bug fix, discovered while verifying round 2's "keep the source image visible"
   requirement (it was directly hiding the fix's own result): color-counter.css's
   `.cc-root .cc-hint { display: flex; ... }` has no `:not([hidden])`/`[hidden]` guard, so it's
   the SAME cross-origin cascade gotcha the CRITICAL comment below (`.cs-panel[hidden]`) already
   documents for this file's own elements -- an author `display` rule always wins over the UA
   stylesheet's `[hidden] { display:none }`, REGARDLESS of the `hidden` attribute actually being
   present, because origin (author > UA) outranks specificity. color-counter.js's showPreview()
   DOES set `hint.hidden = true` once an image loads, but that attribute toggle was silently a
   no-op visually -- the "Drag an image here / PNG, GIF, JPG, WEBP / or click to browse" hint text
   kept rendering underneath the actual preview image forever. Not introduced by this session (a
   pre-session screenshot shows the same defect); fixed here rather than in color-counter.css
   itself, matching this file's established policy of never editing that file directly. */
.cc-root .cc-hint[hidden] {
    display: none;
}

/* #cs-stage holds the drag hint banner, the counter's own dropzone (.cc-image-pane), and the
   demo's button/processing/result UI (#cs-panel) as top-to-bottom siblings -- "REPLACES the
   drag-image box in place" per the original ruling, not a second box stacked below the counter's
   results row (.cc-results, a sibling BELOW #cs-stage). is-drag-hover is toggled by cs-demo.js's
   whole-stage drag-anywhere handler; it forwards dropped files to the counter's own
   <input class="cc-file"> so color-counter.js's OWN change listener does the actual counting --
   no counter logic duplicated here. */
.cs-stage {
    display: flex;
    flex-direction: column;
    gap: 16px;
    margin-bottom: 24px;
    border-radius: 12px;
    transition: outline-color 0.15s ease;
    outline: 2px dashed transparent;
    outline-offset: 4px;
}

.cs-stage.is-drag-hover {
    outline-color: var(--cc-accent, #f4ff9e);
}

/* Owner round 3 (2026-08-12 13:00; revised 13:28 per live feedback -- "the button should stay in
   the same position and not cause the image to jump above/below it"). #cs-stage-frame is the ONE
   box that holds the counted-state source preview (.cc-image-pane), the result-state wipe viewer
   (#cs-wipe), AND the result-state side-by-side view (#cs-side-by-side) as mutually-exclusive
   children -- unstyled/full-width by default (matches the old .cc-image-pane default exactly, so
   the pre-image dropzone box is unaffected), then cs-demo.js's applyStageFrameSize() sets an
   EXPLICIT inline pixel width plus a `--cs-frame-h` custom property (computed once per image from
   the source's natural aspect at the same min(92vw,1100px)/80vh hero constraints the CSS used to
   apply) and adds `.is-sized`.

   13:28 revision -- height depends on which mode is active:
   - Wipe/image-pane modes (".is-sized" without ".is-side-mode"): `height: var(--cs-frame-h)` --
     exactly the single-image fit, hard-locked, children fill it at width/height 100% (unchanged
     from the original round-3 design).
   - Side-by-side mode (".is-sized.is-side-mode"): `height: auto` -- ROUND 8 (owner ruling
     2026-08-12 18:31, item 1) REMOVES the `min-height: var(--cs-frame-h)` floor this used to
     carry: "when side by side, the slider and other data ends up way below the images if they
     were tall. It should move up to the same spacing." A TALL (portrait) image's side-by-side
     panes, at half the frame's width each, render considerably SHORTER than that same image's own
     single-pane Wipe height -- the floor was reserving Wipe's full height as empty dead space
     below the (shorter) side-by-side content, pushing the wipe-range twin/toggle/count/palette
     controls far down the page for no visual reason. Owner's own resolution of the earlier
     anti-jump ruling this floor came from: "the anti-jump ruling was about the image jumping
     around the toggle, not about preserving empty space" -- i.e. growth-downward-only still
     applies WITHIN a mode (content added while already in Side-by-side never yanks the box
     shorter out from under something rendered lower), but a MODE SWITCH is now free to resize the
     frame (grow OR shrink) to fit that mode's own actual content -- switching to Side-by-side on a
     tall image can and should shrink the frame from Wipe's taller reservation.
   `display:flex; flex-direction:column; justify-content:center` centers whichever child is
   shorter than the box within any leftover height (a no-op for the 100%-height wipe/image-pane
   children now that Side-by-side no longer carries a height floor to leave leftover space in).
   Margin auto centers a narrower-than-column fixed-width box horizontally (flexbox auto-margin
   centering, not text-align). */
.cs-stage-frame {
    width: 100%;
    margin: 0 auto;
    /* Owner ruling (2026-08-12 14:12/14:21, §16): defeats mobile long-press "Save Image"/"Copy
       Image" (the touch analogue of desktop right-click) across the whole viewer -- source pane,
       Wipe, and Side-by-side alike -- and stops the underlying pixels from being selected/dragged
       as content. Casual-access removal only, per the plan-doc amendment; not DRM. */
    -webkit-touch-callout: none;
    user-select: none;
}

.cs-stage-frame.is-sized {
    display: flex;
    flex-direction: column;
    justify-content: center;
    height: var(--cs-frame-h);
}

.cs-stage-frame.is-sized.is-side-mode {
    /* Round 8 (owner ruling 2026-08-12 18:31, item 1): no min-height floor anymore -- sizes purely
       to its own actual content (the two panes), tightening the spacing below it. See the doc
       comment above for the full before/after rationale. */
    height: auto;
    /* Owner ruling (2026-08-12 14:57): "the side-by-side can use the total column width (the gray
       column) rather than just the original image width." Wipe/image-pane keep the per-image
       hero width set inline by applyStageFrameSize() (cs-demo.js) -- unchanged. Side-by-side
       instead fills 100% of #cs-stage (the padded gray column inside .counter-shell), which
       needs `!important` here because it's overriding that same inline `style.width` (inline
       styles beat any class selector's specificity). #cs-stage-frame keeps `margin:0 auto`
       (above) and #cs-compare-toggle is centered on #cs-stage's own width independently of the
       frame's width (align-self:center on a sibling flex item, not relative to the frame) -- so
       the toggle's position and the frame's own left edge/top don't move when this widens; only
       the content region grows outward, symmetrically, within the already-full-width column. */
    width: 100% !important;
}

/* Portrait-image fix (owner live-testing screenshot, 2026-08-12 13:28: "this is not the same
   hero image" -- a portrait upload rendered tiny inside a huge dropzone box). Root cause:
   color-counter.css's @media(min-width:700px) block carries TWO leftover rules from the old
   two-column layout that still fire on a portrait upload --
   `.cc-root[data-orientation="portrait"]:not([data-palette="shown"]) .cc-image-pane { width:42%;
   max-width:42%; flex:0 0 auto; }` (specificity 0,4,0: .cc-root + [data-orientation] +
   [data-palette] inside :not() + .cc-image-pane) and a `[data-palette="shown"]` variant
   (max-width:58%). color-counter.js (frozen) still sets `data-orientation`/`data-palette` on
   #cc-main even though the single-stage takeover no longer uses that two-column layout. A plain
   `.cs-stage-frame.is-sized > .cc-image-pane` selector (0,3,0) LOSES that specificity fight --
   even an inline `width` (or `!important` on width alone) doesn't help, because `max-width` is a
   SEPARATE property that clamps the used width regardless of what `width` says. Fix: anchor on
   `#cs-stage-frame` (an ID beats any number of classes/attributes, no specificity arithmetic to
   get wrong) and explicitly reset `max-width`/`flex` too, not just `width`/`height`. */
#cs-stage-frame.is-sized > .cc-image-pane,
#cs-stage-frame.is-sized > .cc-image-pane .cc-dropzone,
#cs-stage-frame.is-sized > .cs-wipe,
#cs-stage-frame.is-sized > .cs-wipe .cs-wipe-frame {
    width: 100%;
    max-width: 100%;
    height: 100%;
    flex: 1 1 auto;
}

/* Defensive: color-counter.css's `.cc-dropzone { min-height: 220px }` would otherwise force the
   dropzone taller than a genuinely tiny source image's own computed frame (a small image is never
   upscaled -- computeStageFrameSize() caps at the natural size), overflowing the frame box. Once
   sized, the frame itself is the sizing authority. */
#cs-stage-frame.is-sized > .cc-image-pane .cc-dropzone {
    min-height: 0;
}

#cs-stage-frame.is-sized > .cc-image-pane .cc-preview {
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

/* Round 2, point 1 ("just add that to the top and don't consume the source image"): a slim
   FULL-WIDTH banner at the very top of #cs-stage (see index.html -- it's the stage's first
   child, above .cc-image-pane), not a box that swaps in over the image. Visible only in the
   counted/result states (cs-demo.js's syncStageChrome()); hidden during processing and never
   shown before the first count (the dropzone's own hint text already covers that state). No
   explicit `display` set here, so the UA `[hidden] { display:none }` rule applies unaided --
   same established pattern as .cs-button/.cs-disclosure/.cs-note noted in the CRITICAL comment
   below. */
.cs-drag-hint {
    width: 100%;
    margin: 0;
    padding: 8px 12px;
    border: 1px dashed var(--cc-border-solid, #3a3d44);
    border-radius: 8px;
    color: var(--cc-muted, #9aa0a6);
    font-size: 12px;
    text-align: center;
}

.cs-panel {
    margin-top: 0;
    padding-top: 20px;
    border-top: 1px solid var(--cc-border-solid, #3a3d44);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    text-align: center;
}

/* CRITICAL fix (review round 1): an author rule that sets `display` on an element that also
   carries the native `hidden` attribute defeats the UA stylesheet's `[hidden] { display: none }`
   -- author rules win over UA rules regardless of the `hidden` attribute's presence. Any element
   toggled via `.hidden = true/false` in cs-demo.js that ALSO has an explicit `display` value here
   needs its own `[hidden]` override, or it renders even while "hidden". `.cs-button`,
   `.cs-disclosure`, and `.cs-note` don't set `display` at all, so the UA rule already wins for
   them unaided -- only `.cs-panel` and `.cs-processing` (both `display: flex`) needed this. */
.cs-panel[hidden],
.cs-processing[hidden] {
    display: none;
}

.cs-button {
    background: var(--cc-accent, #f4ff9e);
    color: var(--cc-on-accent, #000);
    border: none;
    border-radius: 8px;
    padding: 12px 28px;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s ease, opacity 0.15s ease;
}

.cs-button:hover:not(:disabled) {
    background: var(--cc-accent-hover, #fbffc4);
}

.cs-button:disabled {
    opacity: 0.55;
    cursor: default;
}

/* Round 10 (launch-checklist P0 #3a): outcome-focused line beside [ColorSmith It!]. No `display`
   set here, so the UA [hidden] rule applies unaided (same established pattern as .cs-button/
   .cs-disclosure/.cs-note -- see the CRITICAL comment above .cs-panel[hidden]). */
.cs-button-note {
    max-width: 32em;
    margin: -4px 0 0;
    color: var(--cc-muted, #9aa0a6);
    font-size: 13px;
    line-height: 1.4;
}

/* §7.4 disclosure -- unobtrusive note, never a modal; only shown when downscaling actually
   occurred (truthful-by-construction per §7.3's threshold = target). */
.cs-disclosure {
    max-width: 46em;
    margin: 0;
    padding: 10px 14px;
    border: 1px solid var(--cc-border-solid, #3a3d44);
    border-radius: 8px;
    background: var(--cc-panel, #26282d);
    color: var(--cc-muted, #9aa0a6);
    font-size: 12px;
    line-height: 1.5;
}

.cs-disclosure p {
    margin: 0;
}

.cs-disclosure p + p {
    margin-top: 4px;
}

.cs-disclosure-res {
    color: var(--cc-text, #e6e6e6);
}

/* §13.3 processing state -- real stage line + wall-clock elapsed seconds; the progress bar
   only ever reflects an actually-reported fraction from the engine's progress callback (never
   synthetic motion during the callback-silent auto-N search). */
.cs-processing {
    width: 100%;
    max-width: 360px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}

.cs-processing-heading {
    margin: 0;
    font-size: 16px;
    font-weight: 600;
}

.cs-processing-stage {
    margin: 0;
    color: var(--cc-muted, #9aa0a6);
    font-size: 13px;
    font-variant-numeric: tabular-nums;
}

.cs-progress-track {
    width: 100%;
    height: 6px;
    border-radius: 999px;
    background: var(--cc-panel-hover, #2c3138);
    overflow: hidden;
}

.cs-progress-bar {
    height: 100%;
    width: 0%;
    background: var(--cc-accent, #f4ff9e);
    /* Transitions between real reported fractions only -- does not advance on its own, so it
       stays motionless (not fake-animated) during the silent auto-N stretch. */
    transition: width 0.15s ease;
}

.cs-note {
    max-width: 46em;
    margin: 12px auto 0;
    color: #ff7b7b;
    font-size: 13px;
    text-align: center;
}

/* Non-error variant: transitional status (e.g. "finishing the previous run") rather than a
   failure -- reuses the muted counter palette instead of the error red. */
.cs-note.is-info {
    color: var(--cc-muted, #9aa0a6);
}

/* -------------------------------------------------------------------------------------------
   P2-T3: result panel -- wipe viewer (§14), counts row + reduction % (§13.4), §7.5 processed-
   resolution block, timing-honesty line, beta quality score (§12), palette grid (§15).

   Same [hidden]-defeat gotcha as above: `.cs-result` and `.cs-resolution-block` both set an
   explicit `display` value when visible, so both need their own `[hidden] { display: none; }`
   override -- see the CRITICAL comment above `.cs-panel[hidden]`.
   ------------------------------------------------------------------------------------------- */

.cs-result {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 18px;
}

.cs-result[hidden] {
    display: none;
}

/* Round 10 (launch-checklist P0 #3b): "this is a demo, nothing is downloadable" one-liner -- static
   markup, no JS wiring (shows/hides with #cs-result itself, see index.html's own comment beside it). */
.cs-no-export {
    max-width: 32em;
    margin: 0;
    color: var(--cc-muted, #9aa0a6);
    font-size: 12px;
    line-height: 1.5;
    text-align: center;
}

/* Wipe viewer (§14): stacked canvases, the result canvas clipped from the left up to the divider
   position so the original shows through on the right.

   Round 3 (owner ruling 2026-08-12 13:00) supersedes round 2's "shrink-wrap the frame around the
   original canvas's own intrinsic size" model: sizing authority now belongs to #cs-stage-frame
   (see the comment above it) -- #cs-wipe/.cs-wipe-frame/both canvases simply fill 100% of
   whatever box the frame (a sibling-level concern, computed once per image from the SAME
   hero constraints this rule used to apply directly: min(92vw,1100px) wide, 80vh tall) has
   already been sized to. The pre-#cs-stage-frame defaults below (width:100%, no fixed height)
   only matter for the brief window before applyStageFrameSize() has run (there is no visible
   flash in practice -- the frame is sized off the SAME <img> load event that makes the
   [ColorSmith It!] button reachable, well before a result can exist to show #cs-wipe at all). */
.cs-wipe {
    width: 100%;
    margin: 0 auto;
}

.cs-wipe-frame {
    position: relative;
    display: flex;
    width: 100%;
    overflow: hidden;
    border-radius: 8px;
    background: var(--cc-panel-hover, #2c3138);
    touch-action: none; /* the frame owns horizontal drag/tap gestures */
    cursor: ew-resize;
    user-select: none;
}

.cs-wipe-canvas {
    display: block;
}

.cs-wipe-canvas-original,
.cs-wipe-canvas-result {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

.cs-wipe-divider {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 3px;
    margin-left: -1.5px;
    background: var(--cc-accent, #f4ff9e);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);
    pointer-events: none;
}

/* Round-3 handle polish (owner ruling 2026-08-12 13:00): the old `left:50%` + fixed
   `margin:-13px 0 0 -13px` centering derived the circle's position from the 3px-wide DIVIDER's
   own box (50% of 3px = 1.5px) then subtracted a hand-computed half-width offset -- two
   independently-rounded numbers that can drift a sub-pixel apart at some sizes/zoom levels
   ("currently slightly off", owner). `transform: translate(-50%, -50%)` centers the circle
   against its OWN rendered box instead -- a single, size-independent calculation that's exactly
   centered on the divider line at any width/zoom, no separate margin arithmetic to keep in sync.

   Second owner pass on the same live-tested page ("Centering is good. Arrows are too small. I did
   not mean literal < and > characters, I meant side arrows"): the text-glyph chevrons ("‹ ›") are
   replaced with two SOLID drawn triangles via an inline SVG background-image -- font glyphs can't
   be sized/weighted independently of their box the way a drawn shape can, and the owner explicitly
   asked for real triangles, not characters. The SVG draws ONLY the two triangles (transparent
   background) at roughly 33% of the 30px circle's diameter each (a left-pointing tip at x=8 and a
   right-pointing tip at x=22, 2px gap between their inner edges at the true center) -- the
   accent-colored circle itself stays a normal CSS background/border on the same box, unchanged
   from the centering fix above. `pointer-events: none` is stated explicitly here too (not just
   inherited from .cs-wipe-divider) per the owner's own ask, so the arrows can never intercept the
   drag gesture the handle itself exists to indicate. */
.cs-wipe-divider::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 30px;
    height: 30px;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background-color: var(--cc-accent, #f4ff9e);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3E%3Cpolygon points='8,15 14,10 14,20' fill='%23000'/%3E%3Cpolygon points='22,15 16,10 16,20' fill='%23000'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center;
    background-size: 100% 100%;
    border: 2px solid var(--cc-panel, #26282d);
    pointer-events: none;
}

.cs-wipe-label {
    position: absolute;
    top: 8px;
    padding: 3px 8px;
    border-radius: 4px;
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
    pointer-events: none;
}

.cs-wipe-label-result {
    left: 8px;
}

.cs-wipe-label-original {
    right: 8px;
}

/* Same [hidden]-defeat gotcha as the CRITICAL comment above: cs-demo.js's applyCompareModeVisibility()/
   hideResultPanel() toggle this control's `hidden` attribute directly (visible only in Wipe
   compare mode), and it sets `display` explicitly, so it needs its own override.

   Owner round 7 (2026-08-12 16:18, item 3): relocated to be a direct sibling of #cs-stage-frame
   (index.html), immediately under it, rather than buried inside #cs-result below the whole panel.
   No `margin-top` needed anymore -- #cs-stage's own `gap: 16px` (see .cs-stage above) already
   spaces it from the frame above it, same as every other top-level stage child. */
.cs-wipe-range[hidden] {
    display: none;
}

.cs-wipe-range {
    display: block;
    width: 100%;
    margin: 0;
    accent-color: var(--cc-accent, #f4ff9e);
}

/* Owner round 3 (2026-08-12 13:00; relocated 13:28 per live feedback): Wipe | Side-by-side
   compare-mode toggle. Same [hidden]-defeat gotcha -- explicit `display` needs its own override
   (see the CRITICAL comment above). A direct child of #cs-stage (a FIXED slot right after
   #cs-stage-frame -- and, as of owner round 7 item 3, after #cs-wipe-range too, see index.html),
   not #cs-result -- #cs-stage has no align-items set (default stretch, needed so #cs-panel below
   still spans full width), so `align-self: center` overrides stretch for JUST this item to keep it
   shrink-to-fit + centered like it was under #cs-result's own align-items:center before the move. */
.cs-compare-toggle[hidden] {
    display: none;
}

.cs-compare-toggle {
    display: inline-flex;
    align-self: center;
    gap: 4px;
    padding: 3px;
    border-radius: 999px;
    background: var(--cc-panel, #26282d);
    border: 1px solid var(--cc-border-solid, #3a3d44);
}

.cs-compare-btn {
    border: none;
    background: transparent;
    color: var(--cc-muted, #9aa0a6);
    font-size: 12px;
    font-weight: 600;
    padding: 6px 14px;
    border-radius: 999px;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}

.cs-compare-btn:hover {
    color: var(--cc-text, #e6e6e6);
}

.cs-compare-btn.is-active {
    background: var(--cc-accent, #f4ff9e);
    color: var(--cc-on-accent, #000);
}

.cs-compare-btn.is-active:hover {
    color: var(--cc-on-accent, #000);
}

/* Side-by-side compare mode (owner round 3; moved INSIDE #cs-stage-frame 13:28 per live feedback).
   Original + ColorSmith panes, sized against the FRAME's own width (not a separate hero-width cap
   of its own -- the frame is already <= hero width by construction, see its own doc comment).

   Round 7 (owner ruling 2026-08-12 16:17, item 2) REPLACES the old VIEWPORT-width media query
   entirely: orientation now derives from the SOURCE IMAGE's own aspect ratio, applied via
   cs-demo.js's applyCompareModeVisibility() toggling `.is-portrait-source` off
   csDemo.downscale.srcWidth/srcHeight (isPortraitSource()). Landscape/square (W>=H, the default
   below, no modifier class needed) stacks VERTICALLY -- each pane gets the FULL frame width, which
   maximizes pane size for a wide image (a wide image squeezed to half-width, as the old default
   did, wasted the frame's own width). Portrait (W<H, `.is-portrait-source`) stacks HORIZONTALLY --
   each pane keeps its full height side by side, which maximizes pane size for a tall image (a tall
   image stacked top-to-bottom, as the old narrow-viewport case did, wasted the frame's own height).
   This is genuinely viewport-INDEPENDENT now: a landscape image stays stacked vertically even at a
   huge desktop width, and a portrait image stays side-by-side even at a narrow phone width --
   "maximize each pane's dimension for THIS image's shape" doesn't care how wide the window is. */
.cs-side-by-side[hidden] {
    display: none;
}

.cs-side-by-side {
    display: flex;
    flex-direction: column;
    gap: 12px;
    justify-content: center;
    align-items: center;
    width: 100%;
}

.cs-side-pane {
    flex: 1 1 100%;
    max-width: 100%;
    width: 100%;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
}

.cs-side-by-side.is-portrait-source {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
}

.cs-side-by-side.is-portrait-source .cs-side-pane {
    flex: 1 1 calc(50% - 6px);
    max-width: calc(50% - 6px);
    width: auto;
}

.cs-side-label {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--cc-muted, #9aa0a6);
}

/* Round 9 (owner bug report, 2026-08-12 live-testing: "Very high vertical aspect ratio images
   get stretched out to display side by side"). Root cause: `width: 100%` forced a FIXED width
   while `height: auto` derived the height from it -- for a replaced element (canvas) that's
   `height = width / intrinsicRatio`, which for an extreme-portrait source at the pane's full
   width vastly exceeds `max-height: 60vh`. The used height then clamps to that cap, but because
   `width` was a specified length (not `auto`), the browser has no reason to shrink it back down
   to match -- the canvas paints at full pane width but clamped/squashed height, i.e. distorted.
   Switching BOTH `width` and `height` to `auto` (kept constrained only by `max-width`/
   `max-height`) puts the browser on the CSS2.1 §10.4 replaced-element sizing path used when both
   dimensions are auto and both max- constraints are set: it solves for the largest box that fits
   within max-width AND max-height while preserving the intrinsic ratio -- the standard pure-CSS
   "letterbox/contain" trick, no `object-fit` needed (this predates `object-fit` support
   requirements and needs no separate stacking/box changes). For any image whose aspect ratio
   already fits comfortably under the height cap at full pane width (every existing fixture:
   small.png, large.png, portrait.png), this resolves to the exact same rendered size as before
   -- max-width alone still binds first, so none of the round 3/7/8 pane-geometry assertions
   change. `margin: 0 auto` centers the canvas horizontally when it renders narrower than its
   frame (only possible now, only for extreme aspect ratios) -- the frame itself
   (.cs-side-canvas-frame) still spans the pane's full width for the save-guard overlay
   (§16) to cover, that is unaffected. */
.cs-side-canvas {
    display: block;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 60vh;
    margin: 0 auto;
    border-radius: 8px;
    background: var(--cc-panel-hover, #2c3138);
}

/* Owner ruling (2026-08-12 14:12/14:21, §16): result-pane save guard (index.html has the full
   rationale comment beside the markup). The frame is a plain non-positioned wrapper that shrinks
   to the canvas's own rendered box (canvas is its only in-flow child, so the frame's height
   matches it exactly even though the canvas's height is intrinsic/`auto`); the guard is an
   absolutely-positioned transparent sheet stacked above the canvas (positioned elements always
   paint after static ones in the same stacking context, so no z-index is needed) that intercepts
   all pointer targeting -- right-click, drag-out, long-press -- so it, not the canvas, is what
   any image-save path would land on. */
.cs-side-canvas-frame {
    position: relative;
    width: 100%;
}

.cs-side-canvas-guard {
    position: absolute;
    inset: 0;
    background: transparent;
}

/* Counts row + reduction % (§13.4). */
.cs-counts {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 28px;
}

.cs-count {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
}

.cs-count-label {
    color: var(--cc-muted, #9aa0a6);
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.cs-count-value {
    font-size: 18px;
    font-weight: 600;
}

.cs-reduction {
    margin: 0;
    font-size: 15px;
    font-weight: 600;
    color: var(--cc-accent, #f4ff9e);
    text-align: center;
}

/* §7.5 processed-resolution block -- gated in JS (renderResolutionBlock) on downscaling having
   actually occurred; never appears for a full-resolution result. */
.cs-resolution-block {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 20px;
    max-width: 46em;
    margin: 0;
    padding: 10px 14px;
    border: 1px solid var(--cc-border-solid, #3a3d44);
    border-radius: 8px;
    background: var(--cc-panel, #26282d);
}

.cs-resolution-block[hidden] {
    display: none;
}

.cs-res-group {
    text-align: center;
    min-width: 8em;
}

.cs-res-title {
    margin: 0 0 2px;
    color: var(--cc-muted, #9aa0a6);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.cs-res-value {
    margin: 0;
    font-size: 13px;
    color: var(--cc-text, #e6e6e6);
}

/* Timing-honesty line (owner ruling 2026-08-11) -- measured wall-clock time, never an estimate.
   Round 9 (owner ruling 2026-08-12 20:00-20:01): now a wrapper that can hold ONE line (normal
   path, unchanged) or TWO (a >6MP lift-back result -- analysis/requantize phase + full-res apply
   phase, each its own honest measurement, never a combined total). */
.cs-timing {
    margin: 0;
    color: var(--cc-muted, #9aa0a6);
    font-size: 12px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.cs-timing-line {
    margin: 0;
}

.cs-timing-line + .cs-timing-line {
    margin-top: 2px;
}

/* §12 quality score -- secondary placement: small, muted, no letter grades. Round 10 (launch-
   checklist P0 #3c): now a two-line wrapper (see renderScore(), cs-demo.js) -- the "N/100" value
   line plus an always-visible one-line plain-language meaning underneath, same [hidden]-defeat
   gotcha as #cs-timing/#cs-resolution-block above (this element sets no `display` of its own, so
   the UA rule applies unaided -- no override needed here, unlike those two). */
.cs-score {
    margin: 0;
    text-align: center;
}

.cs-score-value {
    margin: 0;
    color: var(--cc-text, #e6e6e6);
    font-size: 12px;
    font-weight: 600;
}

.cs-score-meaning {
    margin: 2px 0 0;
    color: var(--cc-muted, #9aa0a6);
    font-size: 11px;
}

/* §15 palette -- swatch grid, chromatic-first order preserved from the engine, count caption,
   no hover metadata, no edit affordances. */
.cs-palette {
    width: 100%;
    max-width: 480px;
    margin: 0 auto;
    text-align: center;
}

.cs-palette-caption {
    margin: 0 0 8px;
    color: var(--cc-muted, #9aa0a6);
    font-size: 12px;
}

.cs-palette-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(24px, 1fr));
    gap: 6px;
}

.cs-palette-swatch {
    display: block;
    aspect-ratio: 1;
    border-radius: 4px;
    border: 1px solid rgba(0, 0, 0, 0.33);
}

/* Round-4 owner ruling: a full-width rule between the chromatic and achromatic swatch groups
   (see cs-demo.js renderPalette()). Spans every column of the auto-fill grid regardless of the
   column count, and only appears when both groups are present (JS only inserts the element then). */
/* Owner ruling (2026-08-12 15:02, "the line break is nearly invisible on the dark theme"): the
   original 1px var(--cc-border-solid, #3a3d44) is too close to the swatch grid's dark surrounding
   background to actually read as a break. 2px + var(--cc-muted, #9aa0a6) -- already used
   elsewhere for secondary text on this same dark panel -- gives it real contrast without
   introducing a new color. */
.cs-palette-divider {
    grid-column: 1 / -1;
    height: 2px;
    margin: 5px 0;
    background: var(--cc-muted, #9aa0a6);
}

/* Round 9 (owner ruling 2026-08-12 19:52, revised 19:56-19:57) -- §16 pre-release pointer. Was
   the full CTA sentence as plain text; now a short link down to the canonical "Coming to
   HueForge" section (index.html's #coming-to-hueforge, styled in site.css) -- same secondary/
   muted treatment as .cs-score/.cs-timing above it (small, quiet) so it still reads as a closing
   note under the result, not a call-to-action competing visually with the palette. */
.cs-cta {
    margin: 4px 0 0;
    color: var(--cc-muted, #9aa0a6);
    font-size: 12px;
    text-align: center;
}

.cs-cta a {
    color: inherit;
    text-decoration: underline;
}

.cs-cta a:hover {
    color: var(--cc-accent, #f4ff9e);
}

/* -------------------------------------------------------------------------------------------
   P2-T4: "explore other color counts" slider (engine design §D) -- range 8-64 (per-population
   budget, cs_requantize's `n`; preset v2, owner ruling 2026-08-12), default mark, 2x-swatches
   note, lightweight inline busy
   state (NOT the full .cs-processing takeover -- requantize is sub-second typical).
   ------------------------------------------------------------------------------------------- */
.cs-slider {
    width: 100%;
    max-width: 420px;
    margin: 0 auto;
    padding-top: 18px;
    border-top: 1px solid var(--cc-border-solid, #3a3d44);
    text-align: center;
}

.cs-slider-top {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 6px;
}

.cs-slider-label {
    color: var(--cc-text, #e6e6e6);
    font-size: 13px;
    font-weight: 600;
}

.cs-slider-value {
    color: var(--cc-muted, #9aa0a6);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}

.cs-slider-range {
    display: block;
    width: 100%;
    accent-color: var(--cc-accent, #f4ff9e);
}

.cs-slider-bottom {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    min-height: 16px;
    margin-top: 6px;
}

/* Subtle by default; .is-active (slider sitting exactly on the fixed initial N=32 default,
   preset v2) switches to the accent color + visible bullet -- "sliding back to chosenN shows the
   default mark again". */
.cs-slider-auto {
    color: var(--cc-muted, #9aa0a6);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}

.cs-slider-auto.is-active {
    color: var(--cc-accent, #f4ff9e);
    font-weight: 600;
}

/* Lightweight inline busy indicator -- small spinner + wall-clock elapsed, never a fake percent
   (same timing-honesty rule as .cs-processing, just without the full takeover). */
.cs-slider-busy {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--cc-muted, #9aa0a6);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
}

.cs-slider-busy[hidden] {
    display: none;
}

.cs-slider-busy::before {
    content: "";
    width: 10px;
    height: 10px;
    border-radius: 50%;
    border: 2px solid var(--cc-border-solid, #3a3d44);
    border-top-color: var(--cc-accent, #f4ff9e);
    animation: cs-slider-spin 0.7s linear infinite;
}

@keyframes cs-slider-spin {
    to {
        transform: rotate(360deg);
    }
}

.cs-slider-note {
    margin: 8px 0 0;
    color: var(--cc-muted, #9aa0a6);
    font-size: 11px;
}

@media (max-width: 768px) {
    .cs-button {
        width: 100%;
    }

    .cs-counts {
        gap: 20px;
    }

    .cs-resolution-block {
        gap: 14px;
        padding: 8px 10px;
    }

    .cs-res-group {
        min-width: 40%;
    }
}

/* Finding 6 (final review): prefers-reduced-motion guards -- the progress bar's width transition
   and the slider-busy spinner are the only two continuously-animating pieces of UI on this page.
   Neither carries information that's lost by going static: the progress bar still jumps straight
   to the current aria-valuenow's position, and the spinner's ::before circle is still visible
   (just motionless) as a busy indicator. */
@media (prefers-reduced-motion: reduce) {
    .cs-progress-bar {
        transition: none;
    }

    .cs-slider-busy::before {
        animation: none;
    }
}

/* -------------------------------------------------------------------------------------------
   Round 10 (owner ruling 21:14): leave-guard confirmation dialog (back-nav / accidental-loss
   protection). Same [hidden]-defeat gotcha as the CRITICAL comment above .cs-panel[hidden] --
   .cs-leave-guard-backdrop sets `display: flex` (for centering), so it needs its own override.
   ------------------------------------------------------------------------------------------- */
.cs-leave-guard-backdrop[hidden] {
    display: none;
}

.cs-leave-guard-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: rgba(0, 0, 0, 0.6);
}

.cs-leave-guard-dialog {
    width: 100%;
    max-width: 360px;
    padding: 20px;
    border: 1px solid var(--cc-border-solid, #3a3d44);
    border-radius: 12px;
    background: var(--cc-panel, #26282d);
    color: var(--cc-text, #e6e6e6);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}

.cs-leave-guard-title {
    margin: 0 0 6px;
    font-size: 16px;
    font-weight: 600;
}

.cs-leave-guard-desc {
    margin: 0 0 14px;
    color: var(--cc-muted, #9aa0a6);
    font-size: 13px;
    line-height: 1.5;
}

.cs-leave-guard-checkbox-row {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 18px;
    color: var(--cc-muted, #9aa0a6);
    font-size: 13px;
    cursor: pointer;
}

.cs-leave-guard-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

.cs-leave-guard-btn {
    border: 1px solid var(--cc-border-solid, #3a3d44);
    border-radius: 8px;
    padding: 8px 16px;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    background: transparent;
    color: var(--cc-text, #e6e6e6);
}

.cs-leave-guard-btn-stay {
    background: var(--cc-accent, #f4ff9e);
    color: var(--cc-on-accent, #000);
    border-color: transparent;
}

.cs-leave-guard-btn-leave:hover {
    border-color: var(--cc-muted, #9aa0a6);
}
