/* ==========================================================================
   gallery.css — owner: GALLERY
   Styles for mountGallery() (public/js/gallery.js) and nothing else.
   Every colour, radius, shadow, duration and font comes from tokens.css.
   Light and dark are both carried by the tokens — there is not one raw colour
   literal in this file, so neither theme can drift.

   GEOMETRY IS OWNED BY JS. The wall is virtualised: gallery.js computes the
   column count, column width and row height and writes width/height/transform
   onto each tile, plus --gal-label-h onto .gal-wall. CSS must not try to lay
   the wall out — it only paints. The one exception is .gal-skelgrid, the
   loading placeholder, which has no tiles to virtualise.

   ==========================================================================
   THE PAGE GUTTER

   The gallery and the file browser are TWO MODES OF ONE SCREEN. Toggling
   between them must not move a single pixel sideways, so both resolve their
   horizontal padding from the same shell tokens:

       --gutter-l  /  --gutter-r      (defined once, in app.css)

   which is also what app.css's .view-pad uses for Backup and How-this-works.
   Do not re-derive them as max(var(--gutter), env(…)): above 768px .app has
   already offset the page past an iPhone landscape notch, and that expression
   would pay the left inset twice.
   This file used to write `--sp-4` with a `@media (min-width: 901px)` bump to
   `--sp-6`; --gutter steps 24px → 16px at 640px, so between 641px and 900px
   the gallery sat 8px left of where Backup sat, and switching views slid the
   content sideways. There is no gutter breakpoint in this file any more. If
   the gutter has to change, change --gutter.

   MOUNTED INSIDE THE FILE BROWSER the gutter is already spent by .fb-body —
   files.css zeroes .gal-bar's horizontal padding for exactly that case, and
   the scroller is .fb-body itself, so .gal-scroll never renders at all.
   ========================================================================== */

.gal {
  --gal-gutter-l: var(--gutter-l);
  --gal-gutter-r: var(--gutter-r);

  --gal-radius: var(--radius-lg, 14px);
  --gal-label-h: 30px;
  --gal-hair: var(--hairline, rgba(18, 23, 26, .07));

  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  overflow: hidden;
  color: var(--text);
  font-family: var(--font-sans);
}

.gal *,
.gal *::before,
.gal *::after { box-sizing: border-box; }

/* ==========================================================================
   TOOLBAR
   ========================================================================== */

.gal-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex: 0 0 auto;
  flex-wrap: wrap;
  padding: var(--sp-2-5) var(--gal-gutter-r) var(--sp-2-5) var(--gal-gutter-l);
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

.gal-bar-right {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-left: auto;
}

/* ---------- filter chips ---------- */

.gal-chips {
  display: flex;
  align-items: center;
  gap: var(--sp-1-5);
  flex-wrap: wrap;
  min-width: 0;
}

.gal-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1-5);
  height: 30px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.gal-chip:hover {
  border-color: var(--border-hi);
  color: var(--text);
}

.gal-chip:active { transform: translateY(1px); }

/* --accent is FILL-ONLY. The pressed chip is an amber fill carrying
   --on-accent text; it never renders amber TEXT on a light ground. */
.gal-chip[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
}

.gal-chip-n {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  padding: 2px var(--sp-1-5);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  color: var(--text-mute);
}

.gal-chip[aria-pressed="true"] .gal-chip-n {
  background: color-mix(in srgb, var(--on-accent) 16%, transparent);
  color: var(--on-accent);
}

/* ---------- count ---------- */

.gal-count {
  font-size: 12.5px;
  font-weight: 450;
  line-height: 1.3;
  letter-spacing: .005em;
  color: var(--text-mute);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---------- tile-size control ---------- */

.gal-sizes {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.gal-size {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 26px;
  border-radius: var(--radius-sm);
  color: var(--text-mute);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.gal-size:hover { color: var(--text); background: var(--surface); }

.gal-size[aria-pressed="true"] {
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-1);
}

/* The mark is --n columns: four thin ones for small tiles, two fat ones for
   large. It draws the thing it does — column density — rather than an
   abstract icon, which is the only version Kruno has to read twice. */
.gal-size-mark {
  display: grid;
  grid-template-columns: repeat(var(--n, 3), 1fr);
  gap: 2px;
  width: 15px;
  height: 13px;
}

.gal-size-bar {
  display: block;
  min-width: 1px;
  border-radius: 1.5px;
  background: currentColor;
}

/* ==========================================================================
   SCROLLER
   scrollbar-gutter: stable is not cosmetic — without it the scrollbar
   appearing on the first row of thumbnails narrows the wall, which changes the
   column count, which changes the row count, which can make the scrollbar
   disappear again. Reserve the track once and the geometry is a constant.
   ========================================================================== */

/* Only rendered when the gallery builds its OWN scroller (standalone mount).
   Inside the file browser, opts.scrollParent is .fb-body and this element is
   never created — which is why the two mounts cannot drift apart: the one that
   exists always carries the same gutter expression as .fb-body does.

   The top padding matches .fb-body's --sp-4 exactly, so the first row of
   thumbnails sits at the same height as the first row of the list. */
.gal-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  scrollbar-gutter: stable;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: var(--sp-4) var(--gal-gutter-r)
           calc(var(--sp-8) + env(safe-area-inset-bottom)) var(--gal-gutter-l);
}

.gal-stage { position: relative; }

.gal-wall {
  position: relative;
  width: 100%;
}

.gal-wall:focus { outline: none; }

/* ==========================================================================
   TILE
   Absolutely positioned by JS. `contain: layout` keeps 300 of them from
   dirtying each other's layout; paint containment is deliberately NOT used —
   it would clip the focus ring drawn outside the border box.
   ========================================================================== */

.gal-tile {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  text-align: left;
  cursor: pointer;
  contain: layout;
  border-radius: var(--gal-radius);
  color: var(--text);
  -webkit-tap-highlight-color: transparent;
}

/* ---------- picture frame ---------- */

.gal-frame {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--gal-radius);
  background: var(--surface-2);
  transition: border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              background var(--dur) var(--ease);
}

.gal-tile:hover .gal-frame,
.gal-tile:focus-visible .gal-frame {
  border-color: var(--border-hi);
  box-shadow: var(--shadow-1);
}

.gal-tile:active .gal-frame { box-shadow: none; }

/* A drawing is a page: letterboxing it is right, cropping the title block off
   it is not. Photographs and video stills fill the cell instead. */
.gal-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  background: var(--bg-elev);
  transition: opacity var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease);
}

.gal-img.is-cover { object-fit: cover; background: none; }

.gal-tile.is-img .gal-img { opacity: 1; }
.gal-tile.is-img:hover .gal-img { transform: scale(1.035); }

/* ---------- skeleton shimmer (per tile, while its thumbnail loads) ---------- */

.gal-skel {
  position: absolute;
  inset: 0;
  display: none;
  background: var(--surface-2);
  background-image: linear-gradient(90deg,
      transparent 0%,
      color-mix(in srgb, var(--text) 7%, transparent) 45%,
      transparent 90%);
  background-size: 220% 100%;
  background-repeat: no-repeat;
  animation: gal-sweep 1.35s linear infinite;
}

.gal-tile.is-loading .gal-skel { display: block; }

@keyframes gal-sweep {
  from { background-position: -140% 0; }
  to   { background-position: 240% 0; }
}

/* ---------- type-icon tile (no thumbnail, or the thumbnail failed) ---------- */

.gal-mark {
  position: relative;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  width: 100%;
  height: 100%;
  padding: var(--sp-2);
  color: var(--ft, var(--text-mute));
}

.gal-tile.is-glyph .gal-mark { display: flex; }

/* Faint ruled ground, so a wall of icon tiles still reads as "documents"
   rather than as a wall of holes. */
.gal-tile.is-glyph .gal-frame {
  background:
    repeating-linear-gradient(
      to bottom,
      transparent 0,
      transparent 13px,
      var(--grid) 13px,
      var(--grid) 14px),
    var(--surface);
}

.gal-tile.is-dir.is-glyph .gal-frame {
  background: color-mix(in srgb, var(--ft-dir-c) 8%, var(--surface));
  border-color: color-mix(in srgb, var(--ft-dir-c) 32%, var(--border));
}

.gal-mark .gal-ico {
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: .92;
}

.gal-mark-ext {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: 3px var(--sp-1-5);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, currentColor 12%, transparent);
  color: var(--ft, var(--text-mute));
  max-width: 90%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gal[data-size="s"] .gal-mark-ext { font-size: 10px; letter-spacing: .06em; padding: 2px 5px; }
.gal[data-size="l"] .gal-mark-ext { font-size: 12.5px; padding: 4px var(--sp-2); }

.gal[data-size="s"] .gal-mark { gap: var(--sp-1-5); }

/* file-type colour carriers (same names as the list view) */
.gal-tile.ft-dxf   { --ft: var(--ft-dxf); }
.gal-tile.ft-step  { --ft: var(--ft-step); }
.gal-tile.ft-pdf   { --ft: var(--ft-pdf); }
.gal-tile.ft-sheet { --ft: var(--ft-sheet); }
.gal-tile.ft-doc   { --ft: var(--ft-doc); }
.gal-tile.ft-zip   { --ft: var(--ft-zip); }
.gal-tile.ft-nc    { --ft: var(--ft-nc); }
.gal-tile.ft-img   { --ft: var(--ft-img); }
.gal-tile.ft-txt   { --ft: var(--ft-txt); }
.gal-tile.ft-any   { --ft: var(--text-mute); }
.gal-tile.ft-dir   { --ft: var(--ft-dir-c); }

/* ---------- video badge ---------- */

.gal-badge {
  position: absolute;
  left: var(--sp-2);
  bottom: var(--sp-2);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  border: 1px solid var(--border);
  color: var(--text);
  backdrop-filter: blur(4px);
}

.gal-badge .gal-ico { fill: currentColor; stroke: currentColor; }

.gal[data-size="s"] .gal-badge { width: 22px; height: 22px; left: 5px; bottom: 5px; }

/* ---------- always-visible filename strip ---------- */

.gal-label {
  flex: 0 0 var(--gal-label-h);
  display: flex;
  align-items: center;
  min-width: 0;
  padding: 0 var(--sp-0-5);
}

.gal-label-txt {
  min-width: 0;
  width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  direction: ltr;
  font-size: 12.5px;
  font-weight: 500;
  line-height: 1.25;
  color: var(--text-dim);
}

.gal[data-size="s"] .gal-label-txt { font-size: 11.5px; }
.gal[data-size="l"] .gal-label-txt { font-size: 13.5px; }

.gal-tile:hover .gal-label-txt,
.gal-tile:focus-visible .gal-label-txt { color: var(--text); }

.gal-tile.is-dir .gal-label-txt { font-weight: 600; color: var(--text); }

/* ---------- hover / focus detail overlay ----------
   Sits over the bottom of the picture, not over the filename strip, so the
   name never moves. Pointer-events: none — it must never eat the click. */

.gal-over {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--gal-label-h);
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-5) var(--sp-2) var(--sp-2);
  border-radius: 0 0 var(--gal-radius) var(--gal-radius);
  /* The opaque band has to cover the whole name, not fade through it: the
     name occupies roughly 32–74% of the overlay's height, so the ramp starts
     above that. A weaker gradient let a light drawing's own linework read
     straight through the filename. */
  background: linear-gradient(to top,
      color-mix(in srgb, var(--bg) 96%, transparent) 0%,
      color-mix(in srgb, var(--bg) 94%, transparent) 76%,
      transparent 100%);
  opacity: 0;
  transform: translateY(4px);
  pointer-events: none;
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.gal-tile:hover .gal-over,
.gal-tile:focus-visible .gal-over {
  opacity: 1;
  transform: none;
}

.gal-over-name {
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--text);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow-wrap: anywhere;
}

.gal-over-meta {
  font-size: 11.5px;
  font-weight: 450;
  line-height: 1.25;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Small tiles have no room for two lines of prose. */
.gal[data-size="s"] .gal-over { padding: var(--sp-4) 6px 6px; }
.gal[data-size="s"] .gal-over-name { -webkit-line-clamp: 1; line-clamp: 1; font-size: 11.5px; }
.gal[data-size="s"] .gal-over-meta { font-size: 10.5px; }

/* ==========================================================================
   NARROW VIEWPORTS
   Seven chips wrapped over four lines cost ~500px of an iPhone screen before
   a single thumbnail appeared. One non-wrapping, swipeable row instead, with
   the count and the size control on their own line underneath.
   ========================================================================== */

@media (max-width: 700px) {
  .gal-bar {
    flex-direction: column;
    /* nowrap is load-bearing, not tidiness. In a WRAP column container the
       flex line's cross size is the max-content width of its items, and
       align-items: stretch sizes the items to the LINE — so the non-wrapping
       chip row (892px of chips) stretched itself and .gal-bar-right to 892px
       inside a 390px phone, and .gal's overflow:hidden silently cut the last
       four chips off with no way to reach them. Single-line: the line cross
       size is the container's, the chips get 390px and scroll inside it. */
    flex-wrap: nowrap;
    align-items: stretch;
    gap: var(--sp-2);
  }

  .gal-chips {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    /* Room for the focus ring, which would otherwise be clipped by the
       scroll container. */
    padding: 3px 0;
    margin: -3px 0;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }

  .gal-chips::-webkit-scrollbar { display: none; }
  .gal-chip { flex: 0 0 auto; }

  .gal-bar-right {
    margin-left: 0;
    justify-content: space-between;
  }
}

/* Below ~400px. The gutter is NOT touched — that is the shell's, and moving it
   here is precisely the bug this file was fixed for. Only the bar's own rhythm
   and the empty-state padding give ground, and the empty state keeps the same
   metrics as .fb-state at the same width so toggling modes on an empty folder
   does not resize the card. */
@media (max-width: 400px) {
  .gal-bar { padding-top: var(--sp-2); padding-bottom: var(--sp-2); gap: var(--sp-1-5); }
  .gal-chip { padding: 0 var(--sp-2-5); }
  .gal-state { padding: var(--sp-8) var(--sp-3); }
  .gal-state p { font-size: 13px; }
}

/* Touch: there is no hover, tiles are the tap target, and the filename strip
   already carries identity. Suppress the overlay rather than fake it. */
@media (hover: none) {
  .gal-over { display: none; }
  .gal-tile:hover .gal-frame { border-color: var(--border); box-shadow: none; }
  .gal-tile.is-img:hover .gal-img { transform: none; }
  .gal-tile:active .gal-frame { border-color: var(--accent); }
  /* A finger is not a mouse pointer: give the size control real hit area. */
  .gal-size { width: 38px; height: 34px; }
  .gal-chip { height: 34px; padding: 0 var(--sp-4); }
}

/* ==========================================================================
   LOADING SKELETON (whole view, before the listing arrives)
   The only place CSS lays the grid out: there is nothing to virtualise yet.
   ========================================================================== */

.gal-skelgrid {
  --gal-min: 190px;
  --gal-gap: 14px;
  --gal-ratio: .74;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--gal-min), 1fr));
  gap: var(--gal-gap);
}

.gal-skelcell {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.gal-skelframe {
  aspect-ratio: 1 / var(--gal-ratio);
  border-radius: var(--gal-radius);
  border: 1px solid var(--border);
}

.gal-skelline {
  height: 10px;
  width: 72%;
  border-radius: var(--radius-pill);
}

.gal-skel-anim {
  background: var(--surface-2);
  background-image: linear-gradient(90deg,
      transparent 0%,
      color-mix(in srgb, var(--text) 7%, transparent) 45%,
      transparent 90%);
  background-size: 220% 100%;
  background-repeat: no-repeat;
  animation: gal-sweep 1.35s linear infinite;
}

/* ==========================================================================
   EMPTY / ERROR STATES
   ========================================================================== */

.gal-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2-5);
  min-height: 320px;
  padding: var(--sp-10) var(--sp-6);
  text-align: center;
  animation: gal-fade var(--dur) var(--ease) backwards;
}

.gal-state--skel {
  display: block;
  min-height: 0;
  padding: 0;
  text-align: left;
  animation: none;
}

@keyframes gal-fade { from { opacity: 0; } to { opacity: 1; } }

.gal-state-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 68px;
  height: 68px;
  margin-bottom: var(--sp-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  color: var(--text-mute);
}

.gal-state-mark .gal-ico { fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; }

.gal-state.is-error .gal-state-mark {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 40%, transparent);
  background: var(--danger-soft);
}

.gal-state h3 {
  margin: 0;
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--text);
}

.gal-state p {
  margin: 0;
  max-width: 44ch;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-mute);
}

.gal-state-actions {
  display: flex;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
  flex-wrap: wrap;
  justify-content: center;
}

.gal-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  height: 34px;
  padding: 0 var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-size: 13.5px;
  font-weight: 550;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}

.gal-btn:hover { border-color: var(--border-hi); }

.gal-btn--primary {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
}

.gal-btn--primary:hover { border-color: var(--accent-hi); background: var(--accent-hi); }

.gal-btn .gal-ico { fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; }

/* ==========================================================================
   REDUCED MOTION
   The shimmer is an infinite animation over hundreds of tiles — it is exactly
   the kind of thing the setting exists to stop. Static tints replace it, so
   "still waiting" is still legible.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .gal-skel,
  .gal-skel-anim {
    background-image: none;
    animation: none;
  }
  .gal-img,
  .gal-over,
  .gal-frame,
  .gal-chip,
  .gal-size,
  .gal-btn { transition: none; }
  .gal-tile.is-img:hover .gal-img { transform: none; }
  .gal-over { transform: none; }
  .gal-state { animation: none; }
}

.gal.is-still .gal-skel,
.gal.is-still .gal-skel-anim { background-image: none; animation: none; }
.gal.is-still .gal-img,
.gal.is-still .gal-over,
.gal.is-still .gal-frame { transition: none; }
.gal.is-still .gal-tile.is-img:hover .gal-img { transform: none; }

/* ==========================================================================
   FORCED COLOURS (Windows High Contrast)
   box-shadow is dropped and background-colour is overridden there, so the
   pressed chip and the focused tile would both lose their state entirely.
   ========================================================================== */

@media (forced-colors: active) {
  .gal-chip[aria-pressed="true"],
  .gal-size[aria-pressed="true"] {
    outline: 2px solid Highlight;
    outline-offset: -2px;
  }
  .gal-tile:focus-visible .gal-frame { outline: 3px solid Highlight; outline-offset: 1px; }
  .gal-frame { border-color: CanvasText; }
  .gal-over { background: Canvas; }
}
