/* GALLERY — Chain skin (page-specific layer for gallery.html ONLY).

   /css/chain.css is the design language; this file is the composition on top of it. Every
   colour, radius, motion and type value below resolves to a --cn-* token from chain.css —
   no literals are re-declared here except the three the token set genuinely has no entry for
   (the destructive-action red, the reject red, and the thumb letterbox, which reuses
   .cn-strip-img's #0A1213).

   Structural components reused verbatim from chain.css rather than restyled here:
     .cn-shell .cn-grain .cn-bar .cn-cmdgrp .cn-cmdbtn .cn-vrule .cn-wordmark
     .cn-status .cn-nav .cn-cta .cn-body .cn-stagehead .cn-ghost .cn-action

   ── THE LIGHT TABLE REBUILD ───────────────────────────────────────────────
   The previous composition was a uniform CSS grid of 184px cells, each one a fixed 4/3 box
   with object-fit:cover. Measured on the live page at 1920x1080, that spent 19.2% of the
   screen on photographs and threw away 29.6% of the average frame to the crop — a 2:3
   portrait showed exactly half of itself. It also could not grow: auto-fill meant a wider
   display bought MORE 196px thumbnails, never bigger ones.

   This layer is built for culling instead of for browsing a folder:
     · rows are justified in JS (.cn-gal-row), so every card carries its photo's OWN aspect
       ratio and the crop is structurally zero — cover and contain are the same picture;
     · a row's target height is a size STEP the user drives (- / + keys, head control), so
       screen real estate converts into image size on demand;
     · card chrome is an overlay on the photo, not a strip beneath it, so the cell is the
       photograph;
     · the loupe (.cn-gal-loupe) is a real full-screen viewer with 1:1 zoom — the thing that
       makes a selection a decision rather than a guess.                                    */

/* ── shell composition ─────────────────────────────────────────────────── */
/* .cn-body is a row in Studio (rail | left | right). The Gallery is one full-width column:
   a head row, the scrolling well, and the selection action bar. */
.cn-gal-body{ flex-direction:column; gap:var(--cn-body-gap); }

/* chain.css sets `html.cn-app a{ color:var(--cn-amber) }` for prose links — specific enough
   (0,1,2) to beat a bare component class, so the anchors that are *controls* here (wordmark,
   icon links, the action bar's room buttons) re-assert their component colour at equal weight
   instead of coming through amber. */
html.cn-app a.cn-wordmark{ color:var(--cn-ink); }
html.cn-app a.cn-gal-icon{ color:var(--cn-t-45); }
html.cn-app a.cn-gal-icon:hover{ color:var(--cn-amber); }
html.cn-app .cn-gal-actionbar a.cn-ghost{ color:var(--cn-t-85); }

/* Studio's command labels are all single words, so .cn-cmdbtn is a fixed 52px column. Two of
   this page's are not — the button grows with its label rather than wrapping onto a second
   line and pushing the 62px bar out of spec. */
.cn-bar .cn-cmdbtn{ width:auto; min-width:52px; padding:6px 8px; }
.cn-bar .cn-cmdbtn span{ white-space:nowrap; }

/* ── head row (extends .cn-stagehead) ──────────────────────────────────── */
.cn-gal-head{ flex-wrap:wrap; gap:14px; row-gap:8px; }
.cn-gal-head h1{ margin:0; font-size:var(--cn-fs-95); font-weight:var(--cn-w-reg);
                 letter-spacing:var(--cn-ls-24); text-transform:uppercase; color:var(--cn-ink); }
/* Import progress — the one place on this page that reports live work, so it borrows the
   status pill's lime the same way Studio's clip readout does. */
.cn-gal-progress{ font-size:var(--cn-fs-9); letter-spacing:var(--cn-ls-14);
                  color:var(--cn-lime); font-variant-numeric:tabular-nums; }
.cn-gal-progress:empty{ display:none; }
/* Keycap hints were --cn-t-30 at 8px — 2.52:1 against the well, failing AA badly for the one
   piece of UI whose whole job is to teach the keyboard model. Up two rungs and one size. */
.cn-gal-keys{ display:flex; gap:12px; font-size:var(--cn-fs-9); letter-spacing:var(--cn-ls-14);
              color:var(--cn-t-60); white-space:nowrap; }
.cn-gal-keys b{ font-weight:var(--cn-w-med); color:var(--cn-t-85); }
.cn-gal-count{ font-size:var(--cn-fs-9); letter-spacing:var(--cn-ls-14); color:var(--cn-t-60);
               font-variant-numeric:tabular-nums; white-space:nowrap; }
.cn-gal-count b{ color:var(--cn-ink); font-weight:inherit; }

/* rating filter + sort — native <select> (kept native for keyboard + AT), dressed as Chain pills */
.cn-gal-filter{ display:flex; align-items:center; gap:8px; }
.cn-gal-filter > span{ font-size:var(--cn-fs-85); letter-spacing:var(--cn-ls-20); color:var(--cn-t-60); }
.cn-gal-filter select{ -webkit-appearance:none; appearance:none; font-family:inherit;
  font-size:var(--cn-fs-95); letter-spacing:var(--cn-ls-14); color:var(--cn-t-85);
  padding:5px 26px 5px 12px; border-radius:var(--cn-r-pill); cursor:pointer;
  background-color:var(--cn-surf-btn); border:1px solid var(--cn-line-btn);
  background-repeat:no-repeat; background-position:right 9px center; background-size:9px 9px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%23EEF2F1' stroke-opacity='0.6' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 4.5L6 8l3.5-3.5'/%3E%3C/svg%3E");
  transition:border-color var(--cn-t-state) var(--cn-ease), background-color var(--cn-t-state) var(--cn-ease); }
.cn-gal-filter select:hover{ background-color:var(--cn-hov-14); border-color:var(--cn-line-chip); }
/* the native dropdown itself can't inherit the shell, so its options are painted onto the well */
.cn-gal-filter select option{ background:var(--cn-well); color:var(--cn-ink); }

/* ── thumbnail size control ────────────────────────────────────────────── */
/* The answer to "a wider display should buy me BIGGER photos, not more of them". Four steps,
   also on - / + (and [ / ]), persisted across sessions. */
.cn-gal-zoom{ display:flex; align-items:center; gap:6px; }
.cn-gal-zoom > span{ font-size:var(--cn-fs-85); letter-spacing:var(--cn-ls-20); color:var(--cn-t-60); }
.cn-gal-zoombtn{ font-family:inherit; width:26px; height:26px; flex:none; padding:0; cursor:pointer;
  display:flex; align-items:center; justify-content:center; border-radius:var(--cn-r-pill);
  background:var(--cn-surf-btn); border:1px solid var(--cn-line-btn); color:var(--cn-t-85);
  transition:background-color var(--cn-t-state) var(--cn-ease), color var(--cn-t-state) var(--cn-ease); }
.cn-gal-zoombtn svg{ width:13px; height:13px; fill:none; stroke:currentColor; stroke-width:1.9;
                     stroke-linecap:round; }
.cn-gal-zoombtn:hover{ background:var(--cn-hov-18); }
.cn-gal-zoombtn[disabled]{ opacity:var(--cn-disabled-alpha); cursor:default; }
.cn-gal-zoombtn[disabled]:hover{ background:var(--cn-surf-btn); }
/* four ticks that fill as the step rises — reads as a level, not as a pair of blind buttons */
.cn-gal-zoomsteps{ display:flex; align-items:flex-end; gap:3px; height:14px; }
.cn-gal-zoomsteps i{ display:block; width:3px; border-radius:1px; background:var(--cn-t-22);
                     transition:background-color var(--cn-t-state) var(--cn-ease); }
.cn-gal-zoomsteps i:nth-child(1){ height:5px }
.cn-gal-zoomsteps i:nth-child(2){ height:8px }
.cn-gal-zoomsteps i:nth-child(3){ height:11px }
.cn-gal-zoomsteps i:nth-child(4){ height:14px }
.cn-gal-zoomsteps i.on{ background:var(--cn-amber); }

/* ── the well: same radius/ground/border as Studio's .cn-frame viewer ───── */
.cn-gal-well{ position:relative; flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain;
              border-radius:var(--cn-r-18); padding:14px;
              background:var(--cn-well); border:1px solid var(--cn-line);
              scrollbar-width:thin; scrollbar-color:var(--cn-line-btn) transparent; }
.cn-gal-well::-webkit-scrollbar{ width:6px }
.cn-gal-well::-webkit-scrollbar-thumb{ background:var(--cn-line-btn); border-radius:3px }
.cn-gal-well::-webkit-scrollbar-track{ background:transparent }
/* drop-target state — the empty state invites a batch of photos, so the well must actually
   accept one being dragged onto it rather than only offering a file-picker button. */
.cn-gal-well.dropping{ border-color:var(--cn-amber); background:color-mix(in srgb, var(--cn-amber) 6%, var(--cn-well)); }
.cn-gal-well.dropping::after{ content:'DROP TO IMPORT'; position:absolute; inset:0; z-index:3;
  display:flex; align-items:center; justify-content:center; pointer-events:none;
  font-size:var(--cn-fs-11); letter-spacing:var(--cn-ls-24); color:var(--cn-amber);
  background:color-mix(in srgb, var(--cn-page) 62%, transparent); border-radius:var(--cn-r-18); }

/* ── justified rows ────────────────────────────────────────────────────── */
/* Row heights and card widths are written as inline px by layoutRows() in gallery.html: every
   card in a row shares one height, and its width is that height x the photo's own aspect
   ratio, so a row exactly fills the well's content box. Nothing here may set a width, a
   height or an aspect-ratio on a card — the JS owns that geometry. */
.cn-gal-grid{ display:flex; flex-direction:column; gap:var(--cn-gal-gap, 10px); }
.cn-gal-row{ display:flex; gap:var(--cn-gal-gap, 10px); align-items:flex-start;
  /* off-screen rows skip layout and paint entirely; the inline contain-intrinsic-size that
     layoutRows() writes alongside this keeps the scrollbar honest while they are skipped. */
  content-visibility:auto; }

/* ── card ──────────────────────────────────────────────────────────────── */
/* The card IS the photograph now: no meta strip beneath it, no letterbox around it. Chrome
   rides on top of the image and only when it is wanted. */
/* A container, so the chrome can respond to how wide THIS card actually came out. A justified
   row sizes every card differently — a 1:3 crop and a 3:1 panorama in the same row at the same
   size step — so the size step is the wrong thing to hang "is there room for a filename?" on.
   The card's own width is the only honest answer, and only a container query can ask it. */
.cn-gal-card{ position:relative; flex:none; border-radius:var(--cn-r-10); overflow:hidden;
  container-type:inline-size; container-name:card;
  cursor:pointer; background:#0A1213; border:1px solid var(--cn-line-off);
  transition:transform var(--cn-t-state) var(--cn-ease),
             border-color var(--cn-t-state) var(--cn-ease),
             box-shadow var(--cn-t-state) var(--cn-ease),
             opacity var(--cn-t-state) var(--cn-ease); }
.cn-gal-card:hover{ transform:translateY(-2px); border-color:var(--cn-line-btn); }
.cn-gal-card:active{ transform:scale(0.99); }
/* Selection has to read across a 400-photo wall, not just next to its neighbour: an amber
   rim, an inner amber hairline and the amber glow, so a picked frame is unmistakable at any
   step size — including the smallest, where a 1px border alone disappeared. */
.cn-gal-card[aria-selected="true"]{ border-color:var(--cn-amber);
  box-shadow:inset 0 0 0 2px var(--cn-amber), var(--cn-amber-glow); }
.cn-gal-card:focus-visible{ outline:2px solid var(--cn-ink); outline-offset:2px; }

/* Rejected: still present, still recoverable, but visibly out of the running. Dimmed and
   desaturated rather than hidden, so pressing X is never a decision you can't see. */
.cn-gal-card[data-rejected="1"]{ opacity:0.32; }
.cn-gal-card[data-rejected="1"] img{ filter:saturate(0.25); }
.cn-gal-card[data-rejected="1"]:hover{ opacity:0.72; }

.cn-gal-thumb{ position:relative; display:block; width:100%; height:100%; background:#0A1213; }
/* cover and contain are the same picture here — the cell was built from this photo's own
   aspect ratio — but cover guarantees no sub-pixel letterbox seam on the rounded corners. */
.cn-gal-thumb img{ width:100%; height:100%; object-fit:cover; display:block; }
/* store entries handed over by another room can carry no displayable blob at all */
.cn-gal-thumb.empty::after{ content:'NO PREVIEW'; position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
  font-size:var(--cn-fs-8); letter-spacing:var(--cn-ls-22); color:var(--cn-t-40); }

/* one gradient scrim carrying the bottom chrome — painted only when the chrome is up, so a
   quiet wall of photographs stays a quiet wall of photographs. */
.cn-gal-card::after{ content:''; position:absolute; left:0; right:0; bottom:0; height:52%;
  pointer-events:none; opacity:0;
  background:linear-gradient(to top, rgba(4,8,9,0.86), rgba(4,8,9,0.42) 42%, transparent);
  transition:opacity var(--cn-t-state) var(--cn-ease); }
.cn-gal-card:hover::after,
.cn-gal-card:focus-within::after,
.cn-gal-card[aria-selected="true"]::after,
.cn-gal-card[data-rated="1"]::after{ opacity:1; }

/* selection mark — non-interactive (the whole card is the control). The empty ring used to sit
   on every card at all times, punching a dark disc into 200 photographs at once; now the ring
   only appears on hover/focus and only the *filled* amber tick is permanent. */
.cn-gal-mark{ position:absolute; top:8px; left:8px; width:26px; height:26px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; pointer-events:none;
  background:rgba(7,13,14,0.55); border:1px solid var(--cn-line-knob); color:var(--cn-t-85);
  opacity:0;
  transition:opacity var(--cn-t-state) var(--cn-ease),
             background-color var(--cn-t-state) var(--cn-ease),
             border-color var(--cn-t-state) var(--cn-ease), color var(--cn-t-state) var(--cn-ease); }
.cn-gal-card:hover .cn-gal-mark,
.cn-gal-card:focus-within .cn-gal-mark{ opacity:1; }
.cn-gal-mark svg{ width:14px; height:14px; fill:none; stroke:currentColor; stroke-width:2;
                  stroke-linecap:round; stroke-linejoin:round; }
.cn-gal-mark .tick{ display:none; }
.cn-gal-card[aria-selected="true"] .cn-gal-mark{ opacity:1; background:var(--cn-amber);
  border-color:transparent; color:var(--cn-ink-on-amber); }
.cn-gal-card[aria-selected="true"] .cn-gal-mark .ring{ display:none; }
.cn-gal-card[aria-selected="true"] .cn-gal-mark .tick{ display:block; }

/* reject badge — same corner language as the pick mark, opposite meaning */
.cn-gal-reject{ position:absolute; top:8px; left:8px; width:26px; height:26px; border-radius:50%;
  display:none; align-items:center; justify-content:center; pointer-events:none;
  background:#8C2F2C; border:1px solid transparent; color:var(--cn-ink); }
.cn-gal-reject svg{ width:13px; height:13px; fill:none; stroke:currentColor; stroke-width:2.1;
                    stroke-linecap:round; }
.cn-gal-card[data-rejected="1"] .cn-gal-reject{ display:flex; }
.cn-gal-card[data-rejected="1"] .cn-gal-mark{ display:none; }

.cn-gal-remove{ position:absolute; top:8px; right:8px; width:26px; height:26px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; cursor:pointer; padding:0;
  background:rgba(7,13,14,0.62); border:1px solid var(--cn-line-chip); color:var(--cn-t-85);
  opacity:0; transition:opacity var(--cn-t-state) var(--cn-ease),
                        background-color var(--cn-t-state) var(--cn-ease), color var(--cn-t-state) var(--cn-ease); }
.cn-gal-remove svg{ width:13px; height:13px; fill:none; stroke:currentColor; stroke-width:1.7;
                    stroke-linecap:round; stroke-linejoin:round; }
.cn-gal-card:hover .cn-gal-remove,
.cn-gal-card:focus-within .cn-gal-remove{ opacity:1; }
.cn-gal-remove:hover{ background:#7A2A28; color:var(--cn-ink); }

/* loupe affordance — the whole reason a thumbnail can now be trusted is that it is one key
   away from a full-screen look, so the card says so. */
.cn-gal-loupebtn{ position:absolute; top:8px; right:38px; width:26px; height:26px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; cursor:pointer; padding:0;
  background:rgba(7,13,14,0.62); border:1px solid var(--cn-line-chip); color:var(--cn-t-85);
  opacity:0; transition:opacity var(--cn-t-state) var(--cn-ease),
                        background-color var(--cn-t-state) var(--cn-ease), color var(--cn-t-state) var(--cn-ease); }
.cn-gal-loupebtn svg{ width:13px; height:13px; fill:none; stroke:currentColor; stroke-width:1.7;
                      stroke-linecap:round; stroke-linejoin:round; }
.cn-gal-card:hover .cn-gal-loupebtn,
.cn-gal-card:focus-within .cn-gal-loupebtn{ opacity:1; }
.cn-gal-loupebtn:hover{ background:var(--cn-amber); color:var(--cn-ink-on-amber); border-color:transparent; }

/* ── rating rail ───────────────────────────────────────────────────────── */
/* Rating is a primary culling action, so it gets a real target: 26x26 per star (was 13x15,
   under the 24px WCAG 2.2 AA floor) and an off-state at --cn-t-55 rather than --cn-t-30
   (2.52:1, invisible). Only drawn when the card's chrome is up. */
.cn-gal-stars{ position:absolute; left:6px; bottom:6px; display:flex; gap:0; padding:0;
  opacity:0; transition:opacity var(--cn-t-state) var(--cn-ease); }
.cn-gal-card:hover .cn-gal-stars,
.cn-gal-card:focus-within .cn-gal-stars,
.cn-gal-card[data-rated="1"] .cn-gal-stars{ opacity:1; }
.cn-gal-star{ background:none; border:0; padding:0; cursor:pointer; font-family:inherit;
  width:26px; height:26px; display:flex; align-items:center; justify-content:center;
  font-size:13px; line-height:1; color:var(--cn-t-55);
  text-shadow:0 1px 3px rgba(4,8,9,0.9);
  transition:color var(--cn-t-state) var(--cn-ease); }
.cn-gal-star.on{ color:var(--cn-amber); }
.cn-gal-star:hover{ color:var(--cn-amber-hi); }

/* ── filename / dimensions ─────────────────────────────────────────────── */
/* Was a permanent 34px opaque strip below the photo — 19% of every card given over to a
   filename truncated at the TAIL, which is exactly where a frame number lives. Now: an
   overlay that appears with the rest of the chrome, and a middle ellipsis (built in JS) that
   keeps both ends. */
.cn-gal-meta{ position:absolute; right:8px; bottom:8px; left:calc(6px + 5 * 26px + 6px);
  display:flex; align-items:baseline; justify-content:flex-end; gap:8px;
  opacity:0; pointer-events:none; transition:opacity var(--cn-t-state) var(--cn-ease); }
.cn-gal-card:hover .cn-gal-meta,
.cn-gal-card:focus-within .cn-gal-meta{ opacity:1; }
.cn-gal-name{ min-width:0; font-size:var(--cn-fs-105); color:var(--cn-ink);
              text-shadow:0 1px 3px rgba(4,8,9,0.9);
              overflow:hidden; white-space:nowrap; }
/* dims were --cn-t-42 at 9px — 3.76:1, under AA for small text. On the scrim, at t-75. */
.cn-gal-dims{ flex:none; font-size:var(--cn-fs-9); letter-spacing:var(--cn-ls-10); color:var(--cn-t-75);
              text-shadow:0 1px 3px rgba(4,8,9,0.9);
              font-variant-numeric:tabular-nums; }

/* What fits, by the card's real width. The rating rail is the last thing to go: on a light
   table you can always live without a filename on the frame, never without being able to
   rate it. Below 132px even the shrunk rail would cover the photograph, and at that size the
   keyboard (1-5) and the loupe are the honest way to rate. */
@container card (max-width: 340px){
  .cn-gal-dims{ display:none; }
}
@container card (max-width: 240px){
  .cn-gal-meta{ display:none; }
}
@container card (max-width: 190px){
  .cn-gal-star{ width:20px; height:20px; font-size:11px; }
}
/* Narrower than five usable targets — a portrait sharing a phone-width row with a landscape
   lands here. The rail stops being a control and becomes a readout: the earned stars only, at
   pip size, so you can still SEE the rating at a glance. Setting one moves to the loupe (one
   tap on the button that is still on the card) and to 1-5 on a keyboard. Hiding the rail
   outright would have hidden the rating itself. */
@container card (max-width: 132px){
  .cn-gal-star{ display:none; pointer-events:none; }
  .cn-gal-star.on{ display:flex; width:13px; height:13px; font-size:9px; }
}

/* ── empty state + loading ─────────────────────────────────────────────── */
.cn-gal-note{ display:flex; flex-direction:column; align-items:center; text-align:center;
              padding:min(14vh, 110px) 24px; }
.cn-gal-note-icon{ width:52px; height:52px; border-radius:50%; margin-bottom:16px;
  display:flex; align-items:center; justify-content:center;
  border:1px dashed var(--cn-line-check); color:var(--cn-t-50); }
.cn-gal-note-icon svg{ width:22px; height:22px; fill:none; stroke:currentColor; stroke-width:1.4;
                       stroke-linecap:round; stroke-linejoin:round; }
.cn-gal-note strong{ display:block; font-size:var(--cn-fs-9); font-weight:var(--cn-w-reg);
  letter-spacing:var(--cn-ls-24); text-transform:uppercase; color:var(--cn-t-85); margin-bottom:10px; }
.cn-gal-note span{ display:block; max-width:56ch; font-size:var(--cn-fs-125); line-height:1.55;
                   color:var(--cn-t-60); }
.cn-gal-spinner{ width:18px; height:18px; border-radius:50%; margin-bottom:14px;
  border:2px solid var(--cn-t-16); border-top-color:var(--cn-amber);
  animation:cnGalSpin 0.7s linear infinite; }
@keyframes cnGalSpin{ to{ transform:rotate(360deg) } }

/* ── selection action bar ──────────────────────────────────────────────── */
/* Same geometry family as .cn-chainbar / .cn-exif: a short bordered strip closing the column. */
.cn-gal-actionbar{ flex:none; display:none; align-items:center; gap:10px; flex-wrap:wrap;
  min-height:44px; padding:6px 8px 6px 14px; border-radius:var(--cn-r-14);
  background:var(--cn-surf); border:1px solid var(--cn-line); }
.cn-gal-actionbar.show{ display:flex; }
.cn-gal-actionbar .sp{ flex:1; }
.cn-gal-picked{ display:flex; align-items:center; gap:8px; font-size:var(--cn-fs-9);
                letter-spacing:var(--cn-ls-18); color:var(--cn-t-60); }
.cn-gal-picked i{ width:5px; height:5px; border-radius:50%; display:block; background:var(--cn-amber);
                  box-shadow:var(--cn-amber-glow); }
.cn-gal-picked b{ color:var(--cn-amber); font-weight:inherit; font-variant-numeric:tabular-nums; }
/* chain.css's .cn-ghost is a 30x30 ICON button (width:30px, padding:0). This bar puts 13-15
   character labels in four of them, so every one rendered as a 30px column of wrapped 9.5px
   text overflowing the 44px bar. Labelled ghosts size to their label. */
.cn-gal-actionbar .cn-ghost{ display:inline-flex; align-items:center; gap:7px; margin-left:0;
  width:auto; min-width:30px; height:30px; padding:0 12px; white-space:nowrap; }
.cn-gal-actionbar .cn-ghost svg{ width:13px; height:13px; flex:none; fill:none; stroke:currentColor;
                                 stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
.cn-gal-actionbar .cn-ghost[disabled]{ opacity:var(--cn-disabled-alpha); cursor:default; }
.cn-gal-actionbar .cn-ghost[disabled]:hover{ background:var(--cn-surf-btn); }

/* ── the loupe ─────────────────────────────────────────────────────────── */
/* A cull without a loupe is a guess: at any thumbnail size you cannot see focus, a blink, or
   a micro-expression. Enter / double-click opens this; it reads the 2048px preview derivative
   (falling back to the full-resolution blob), and Z toggles a true 1:1 pixel view with drag
   to pan. Every culling key that works in the grid works in here too. */
.cn-gal-loupe{ position:fixed; inset:0; z-index:600; display:none; flex-direction:column;
  background:color-mix(in srgb, var(--cn-page) 96%, black); }
.cn-gal-loupe.on{ display:flex; }
.cn-gal-loupe-head{ flex:none; display:flex; align-items:center; gap:14px; height:52px;
  padding:0 14px 0 18px; border-bottom:1px solid var(--cn-line); }
/* flex:1 with min-width:0 — the name is the one thing here that can take the leftover room and
   the one thing that can give it back. Without a flex basis of its own it measured zero and
   the header showed an index and dimensions but no filename. */
.cn-gal-loupe-name{ flex:1 1 auto; min-width:0; font-size:var(--cn-fs-13); color:var(--cn-ink);
                    overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.cn-gal-loupe-idx{ flex:none; font-size:var(--cn-fs-9); letter-spacing:var(--cn-ls-18);
                   color:var(--cn-t-60); font-variant-numeric:tabular-nums; }
.cn-gal-loupe-dims{ flex:none; font-size:var(--cn-fs-9); letter-spacing:var(--cn-ls-10);
                    color:var(--cn-t-60); font-variant-numeric:tabular-nums; }
.cn-gal-loupe-head .sp{ flex:1; }
.cn-gal-loupe-flag{ flex:none; display:none; align-items:center; gap:6px; padding:4px 10px;
  border-radius:var(--cn-r-pill); font-size:var(--cn-fs-85); letter-spacing:var(--cn-ls-18); }
.cn-gal-loupe-flag.show{ display:flex; }
.cn-gal-loupe-flag.picked{ background:var(--cn-amber); color:var(--cn-ink-on-amber); }
.cn-gal-loupe-flag.rejected{ background:#8C2F2C; color:var(--cn-ink); }
/* the loupe's own rating rail — same 26px targets and contrast floor as the card's */
.cn-gal-loupe-stars{ display:flex; gap:0; }
.cn-gal-loupe-stars .cn-gal-star{ width:26px; height:26px; font-size:14px; text-shadow:none; }

.cn-gal-loupe-stage{ flex:1; min-height:0; position:relative; overflow:hidden;
  display:flex; align-items:stretch; justify-content:center; background:#050A0B; }

/* ── panes ─────────────────────────────────────────────────────────────
   One pane in single view, two in compare. The pane is a column (viewport + caption) so the
   caption never overlaps the photograph — in a side-by-side you are reading both captions
   while looking at both frames, and a caption floating over the image would be covering the
   very detail you opened compare to inspect. */
.cn-gal-loupe-panes{ flex:1; min-width:0; display:flex; gap:0; }
.cn-gal-loupe-panes.comparing{ gap:2px; background:var(--cn-line); }
.cn-gal-loupe-pane{ flex:1 1 0; min-width:0; display:flex; flex-direction:column;
  background:#050A0B; position:relative; }
.cn-gal-loupe-pane[hidden]{ display:none; }
.cn-gal-loupe-view{ flex:1; min-height:0; position:relative; overflow:hidden; }

/* Fit means fit: the frame fills the viewport in whichever axis binds, scaling UP as well as
   down. A max-width/max-height box left a 560px frame marooned at native size in the middle of
   a 1920px stage — which is the same "photos are too small to judge" problem the grid had. */
.cn-gal-loupe-view img{ display:block; width:100%; height:100%;
  object-fit:contain; user-select:none; -webkit-user-drag:none; }
/* 1:1 — the image leaves the fit box entirely and is positioned by the pan offset */
.cn-gal-loupe-stage.zoomed{ cursor:grab; }
.cn-gal-loupe-stage.zoomed.panning{ cursor:grabbing; }
.cn-gal-loupe-stage.zoomed .cn-gal-loupe-view img{ width:auto; height:auto;
  max-width:none; max-height:none; object-fit:none; position:absolute; }

/* Which pane the next keypress acts on. An amber rail down the pane's own edge rather than a
   full outline: a box drawn around a photograph reads as part of the photograph. */
.cn-gal-loupe-panes.comparing .cn-gal-loupe-pane::before{ content:''; position:absolute;
  left:0; right:0; top:0; height:2px; z-index:2; background:transparent;
  transition:background-color var(--cn-t-state) var(--cn-ease); }
.cn-gal-loupe-panes.comparing .cn-gal-loupe-pane.active::before{ background:var(--cn-amber); }

/* ── pane caption ──────────────────────────────────────────────────────── */
/* Single view has the header for this, so the caption only earns its space in compare. */
.cn-gal-loupe-cap{ display:none; }
.cn-gal-loupe-panes.comparing .cn-gal-loupe-cap{ flex:none; display:flex; align-items:center;
  gap:10px; min-height:34px; padding:0 10px; border-top:1px solid var(--cn-line);
  background:var(--cn-page); }
.cn-gal-loupe-pane.active .cn-gal-loupe-cap{ background:color-mix(in srgb, var(--cn-amber) 8%, var(--cn-page)); }
.cn-gal-loupe-capname{ flex:1; min-width:0; font-size:var(--cn-fs-105); color:var(--cn-t-85);
                       overflow:hidden; white-space:nowrap; }
.cn-gal-loupe-pane.active .cn-gal-loupe-capname{ color:var(--cn-ink); }
.cn-gal-loupe-capstars{ flex:none; display:flex; }
.cn-gal-loupe-capstars .cn-gal-star{ width:22px; height:22px; font-size:12px; text-shadow:none; }
.cn-gal-loupe-capflag{ flex:none; padding:2px 8px; border-radius:var(--cn-r-pill);
  font-size:var(--cn-fs-8); letter-spacing:var(--cn-ls-18); }
.cn-gal-loupe-capflag.picked{ background:var(--cn-amber); color:var(--cn-ink-on-amber); }
.cn-gal-loupe-capflag.rejected{ background:#8C2F2C; color:var(--cn-ink); }

/* The TAB hint is only true while there are two sides to switch between. */
.cn-gal-loupe-cmpkey{ display:none; }
.cn-gal-loupe.comparing .cn-gal-loupe-cmpkey{ display:inline; }
.cn-gal-loupe-busy{ position:absolute; inset:0; display:none; align-items:center; justify-content:center; }
.cn-gal-loupe-busy.on{ display:flex; }

/* prev / next — big, edge-anchored, and out of the photograph's way until wanted */
.cn-gal-loupe-nav{ position:absolute; top:50%; transform:translateY(-50%); z-index:2;
  width:46px; height:46px; border-radius:50%; cursor:pointer; padding:0;
  display:flex; align-items:center; justify-content:center;
  background:rgba(7,13,14,0.7); border:1px solid var(--cn-line-chip); color:var(--cn-t-85);
  opacity:0.22; transition:opacity var(--cn-t-state) var(--cn-ease),
                           background-color var(--cn-t-state) var(--cn-ease); }
.cn-gal-loupe-stage:hover .cn-gal-loupe-nav{ opacity:1; }
.cn-gal-loupe-nav:hover{ background:var(--cn-hov-20); }
.cn-gal-loupe-nav svg{ width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:1.7;
                       stroke-linecap:round; stroke-linejoin:round; }
.cn-gal-loupe-nav.prev{ left:14px } .cn-gal-loupe-nav.next{ right:14px }
.cn-gal-loupe-nav[disabled]{ opacity:0.08; cursor:default; }

.cn-gal-loupe-foot{ flex:none; display:flex; align-items:center; gap:16px; height:38px;
  padding:0 18px; border-top:1px solid var(--cn-line);
  font-size:var(--cn-fs-9); letter-spacing:var(--cn-ls-14); color:var(--cn-t-60); }
.cn-gal-loupe-foot b{ font-weight:var(--cn-w-med); color:var(--cn-t-85); }
.cn-gal-loupe-foot .sp{ flex:1; }

/* ── bar extras ────────────────────────────────────────────────────────── */
/* .cn-cta is display:flex, so "OPEN <b>3</b> IN STUDIO" becomes three flex items and the
   whitespace between them is dropped — the gap puts the word spacing back. */
.cn-cta{ gap:0.32em; }
.cn-cta[disabled]{ opacity:var(--cn-disabled-alpha); cursor:default; box-shadow:none; }
.cn-cta[disabled]:hover{ background:var(--cn-amber); }
.cn-cta b{ font-weight:inherit; font-variant-numeric:tabular-nums; }
.cn-gal-icon{ width:26px; height:26px; border-radius:var(--cn-r-8); flex:none;
  display:flex; align-items:center; justify-content:center; color:var(--cn-t-45);
  transition:color var(--cn-t-state) var(--cn-ease), background-color var(--cn-t-state) var(--cn-ease); }
.cn-gal-icon svg{ width:15px; height:15px; fill:none; stroke:currentColor; stroke-width:1.6;
                  stroke-linecap:round; stroke-linejoin:round; }
.cn-gal-icon:hover{ color:var(--cn-amber); background:var(--cn-hov-10); }
.cn-gal-iconrow{ display:flex; align-items:center; gap:2px; }
input[type=file]{ display:none; }

/* ── in-app confirm modal (never native confirm()) ─────────────────────── */
.cn-gal-modal{ position:fixed; inset:0; z-index:700; display:none;
  align-items:center; justify-content:center; padding:20px;
  background:rgba(4,8,9,0.66); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); }
.cn-gal-modal.on{ display:flex; }
.cn-gal-modalbox{ width:100%; max-width:392px; padding:18px 20px 16px; border-radius:var(--cn-r-16);
  background:color-mix(in srgb, var(--cn-well) 92%, white 8%); border:1px solid var(--cn-line);
  box-shadow:0 18px 50px rgba(0,0,0,0.6);
  animation:cnGalModalIn var(--cn-t-state) var(--cn-ease) both; }
@keyframes cnGalModalIn{ from{ opacity:0; transform:scale(0.97) } to{ opacity:1; transform:scale(1) } }
.cn-gal-modal-kicker{ font-size:var(--cn-fs-85); letter-spacing:var(--cn-ls-24); color:var(--cn-t-55); }
.cn-gal-modal p{ margin:9px 0 18px; font-size:var(--cn-fs-125); line-height:1.5; color:var(--cn-t-90); }
.cn-gal-modal-actions{ display:flex; justify-content:flex-end; gap:8px; }
.cn-gal-modal-actions .cn-action{ width:auto; min-width:96px; }
/* destructive — the one hue with no --cn-* token; kept at the page's existing #ff5c5c family */
.cn-action.danger{ background:transparent; border-color:rgba(255,92,92,0.45); color:#FF6B6B; }
.cn-action.danger:hover{ filter:none; background:rgba(255,92,92,0.12); border-color:rgba(255,92,92,0.7); }

/* ── responsive ────────────────────────────────────────────────────────── */
/* The grid itself needs no breakpoints any more: layoutRows() reads the well's real content
   width on every resize and re-justifies, so the rows fit a phone and a 6K display by the
   same arithmetic. What follows is chrome that has to stand down to make room for them. */
@media (max-width:1180px){
  .cn-gal-keys{ display:none; }
}
@media (max-width:980px){
  /* The bar stops being a fixed-height single row and wraps instead — the shell is a flex
     column, so a taller bar simply gives the well less height. Nothing ever overflows sideways. */
  .cn-bar{ height:auto; min-height:var(--cn-bar-h); flex-wrap:wrap; row-gap:6px;
           padding:8px var(--cn-body-pad); gap:10px; }
  .cn-bar .sp{ flex:1 0 0; }
  .cn-nav{ flex-wrap:wrap; gap:12px; }
}
@media (max-width:720px){
  .cn-gal-well{ padding:10px; }
  .cn-gal-head{ padding:0; }
  .cn-gal-head .rule{ display:none; }
  .cn-gal-count{ order:5; }
  /* the loupe's foot is a keyboard legend, and a phone has no keyboard */
  .cn-gal-loupe-foot{ display:none; }
  .cn-gal-loupe-nav{ opacity:1; }

  /* The loupe header is a single non-wrapping row of nine things, and on a 375px viewport it
     overflowed by 44px — pushing the CLOSE button off the right edge. With no keyboard to
     press Esc with, that left no way out of the viewer at all. Everything below buys that
     button back: tighter gutters, no pixel dimensions, and smaller stars. */
  .cn-gal-loupe-head{ gap:8px; padding:0 10px; }
  .cn-gal-loupe-dims{ display:none; }
  .cn-gal-loupe-head .cn-gal-zoombtn{ flex:none; }
  .cn-gal-loupe-stars .cn-gal-star{ width:22px; height:22px; font-size:12px; }
  /* In compare each pane's caption already carries its own rating rail, so the header's is a
     duplicate — and the room it frees is what lets the filename survive. */
  .cn-gal-loupe.comparing .cn-gal-loupe-stars{ display:none; }
  /* Two frames side by side on a phone are two slivers. Stacked, each still gets the full
     width — which is the axis that matters, since most frames are wider than they are tall. */
  .cn-gal-loupe-panes.comparing{ flex-direction:column; }
}
@media (max-width:460px){
  .cn-bar{ gap:8px; }
  .cn-vrule{ display:none; }
  .cn-status{ display:none; }
  /* the action bar is the one strip that can eat a phone's whole viewport once it wraps */
  .cn-gal-actionbar{ padding:8px; gap:6px; }
  .cn-gal-actionbar .sp{ display:none; }
  .cn-gal-picked{ width:100%; }
  .cn-gal-actionbar .cn-ghost{ padding:0 10px; font-size:var(--cn-fs-9); gap:5px; }
  .cn-gal-actionbar .cn-ghost svg{ width:11px; height:11px; }
}

/* Coarse pointers never get a hover, so chrome that hides until hover would be unreachable:
   on touch the pick mark, rating rail and loupe button are simply always up. */
@media (hover:none){
  .cn-gal-mark, .cn-gal-stars, .cn-gal-loupebtn, .cn-gal-remove{ opacity:1; }
  .cn-gal-card::after{ opacity:1; }
  /* Thumb-sized star targets, but only where the card is wide enough to hold five of them —
     unscoped, this rule sits later in the file than the container queries above and would win,
     pushing a 150px rail back onto a 120px frame. */
  @container card (min-width: 341px){
    .cn-gal-star{ width:30px; height:30px; }
  }
}

@media (prefers-reduced-motion:reduce){
  .cn-gal-card{ transition:none; }
  .cn-gal-card:hover{ transform:none; }
  .cn-gal-modalbox{ animation:none; }
  .cn-gal-spinner{ animation-duration:1.6s; }
}
