:root {
  --rim: 24px;
  --gap: 0px;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --dur: 420ms;
  --bg: #ffffff;
  --footer-h: 48px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  width: 100%;
  background: var(--bg);
  overflow: hidden;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  color: #111;
  -webkit-font-smoothing: antialiased;
}

body[data-level="2"] .tile {
  cursor: pointer;
}

.frame {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: var(--footer-h);
  padding: var(--rim);
  display: block;
}

/* In L2, the frame scrolls vertically — the grid grows as more rows load.
   L1 and zoom mode keep the original fixed-height behaviour. */
body[data-level="2"]:not([data-zoom="on"]) .frame {
  overflow-y: auto;
  overscroll-behavior: contain;
}
body[data-level="2"]:not([data-zoom="on"]) .frame::-webkit-scrollbar { width: 8px; }
body[data-level="2"]:not([data-zoom="on"]) .frame::-webkit-scrollbar-thumb {
  background: rgba(0, 0, 0, 0.18);
  border-radius: 4px;
}
body[data-level="2"]:not([data-zoom="on"]) .frame::-webkit-scrollbar-thumb:hover {
  background: rgba(0, 0, 0, 0.30);
}

/* === Footer === */
.footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--footer-h);
  background: #000;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--rim);
  z-index: 50;
  font-family: "JetBrains Mono", "SF Mono", ui-monospace, Menlo, Consolas, monospace;
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  user-select: none;
}

.footer-brand {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
  text-align: left;
  transition: opacity 180ms ease;
}
.footer-brand:hover {
  opacity: 0.6;
  outline: none;
}
.footer-brand:focus-visible {
  opacity: 1;
  outline: 2px solid currentColor;
  outline-offset: 4px;
  border-radius: 2px;
}

.footer-menu-wrap {
  position: relative;
  display: flex;
  align-items: center;
}

.footer-menu-btn {
  width: 32px;
  height: 32px;
  background: transparent;
  border: 0;
  color: #fff;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  margin: 0;
  border-radius: 2px;
  transition: background 180ms ease;
}
.footer-menu-btn:hover {
  background: rgba(255, 255, 255, 0.08);
  outline: none;
}
.footer-menu-btn:focus-visible {
  background: rgba(255, 255, 255, 0.08);
  outline: 2px solid currentColor;
  outline-offset: 3px;
}
.footer-menu-btn .icon-hamburger,
.footer-menu-btn .icon-close {
  display: block;
}
.footer-menu-btn .icon-close { display: none; }
.footer-menu-wrap.is-open .footer-menu-btn .icon-hamburger { display: none; }
.footer-menu-wrap.is-open .footer-menu-btn .icon-close { display: block; }

.footer-menu {
  position: absolute;
  bottom: calc(100% + 10px);
  right: 0;
  min-width: 240px;
  background: #000;
  color: #fff;
  padding: 8px 0;
  display: flex;
  flex-direction: column;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 2px;
  opacity: 0;
  visibility: hidden;
  transform: translateY(6px);
  transition: opacity 200ms ease, transform 220ms var(--ease), visibility 220ms;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
}
.footer-menu-wrap.is-open .footer-menu {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.footer-menu a {
  display: block;
  padding: 10px 18px;
  color: #fff;
  text-decoration: none;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: background 160ms ease, color 160ms ease;
}
.footer-menu a:hover {
  background: rgba(255, 255, 255, 0.08);
  outline: none;
}
.footer-menu a:focus-visible {
  background: rgba(255, 255, 255, 0.08);
  outline: 2px solid currentColor;
  outline-offset: -2px;
}

@media (max-width: 560px) {
  .footer { padding: 0 var(--rim); font-size: 10px; letter-spacing: 0.14em; }
  .footer-menu { min-width: 200px; }
}

.grid {
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr;
  grid-template-rows: 1fr 1fr 1fr;
  gap: var(--gap);
  transition: grid-template-columns var(--dur) var(--ease),
              grid-template-rows var(--dur) var(--ease),
              opacity 220ms ease;
  /* Any subpixel gap exposed during the grid template transition shows this
     dark colour instead of white — invisible during hover squeeze. */
  background: #0c0c0c;
  /* Allow absolutely-positioned zoom overlay to be placed inside the grid */
  position: relative;
}

/* Level 2: 6 columns, rows grow as photos are appended. Tile aspect-ratio
   drives row height so we don't depend on viewport height. */
body[data-level="2"] .grid {
  grid-template-columns: repeat(6, 1fr);
  grid-template-rows: none;
  grid-auto-rows: auto;
  height: auto;
  min-height: 100%;
}
body[data-level="2"]:not([data-zoom="on"]) .tile {
  aspect-ratio: 4 / 3;
}

/* During an L2 hover squeeze, the JS sets explicit row heights — drop
   aspect-ratio so each tile fills its cell (otherwise the implicit height
   from aspect-ratio fights the row track and we get black gaps). */
body[data-level="2"]:not([data-zoom="on"]) .grid[data-l2-hover="on"] .tile {
  aspect-ratio: auto;
  height: 100%;
}

/* In zoom mode the grid switches back to absolute positioning + fixed
   viewport-sized container — covered by .grid rules in zoom block. */
body[data-zoom="on"] .grid {
  height: 100%;
  min-height: 0;
}

.grid.is-leaving { opacity: 0; }
.grid.is-entering { opacity: 0; }

.tile {
  position: relative;
  overflow: hidden;
  background: #f3f3f3;
  cursor: pointer;
  outline: none;
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
  min-height: 0;
  transform-origin: center center;
  will-change: transform;
  transition: transform 520ms var(--ease), z-index 0s;
}
/* Keyboard focus — high-contrast inset ring so it reads on any photo and
   doesn't get clipped by .tile { overflow: hidden }. Sits above the tile's
   own ::after dim overlay (z-index 1) and the zoom thumbs but below the
   zoomed-centre tile (z-index 5). */
.tile:focus-visible {
  outline: none;
}
.tile:focus-visible::before {
  content: "";
  position: absolute;
  inset: 0;
  border: 3px solid #ffffff;
  box-shadow:
    inset 0 0 0 2px #000,
    0 0 0 1px rgba(0, 0, 0, 0.5);
  pointer-events: none;
  z-index: 3;
}

/* Zoom mode: grid becomes a block container with absolutely positioned thumbs + center */
body[data-zoom="on"] .grid {
  display: block;
  position: relative;
  grid-template-columns: none;
  grid-template-rows: none;
}
body[data-zoom="on"] .hint { opacity: 0; }
body[data-zoom="on"] .tile {
  position: absolute;
  cursor: zoom-in;
  transition: opacity 260ms ease;
}
body[data-zoom="on"] .tile.is-zoomed-center {
  cursor: zoom-out;
  z-index: 5;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.35);
}
body[data-zoom="on"] .tile.is-thumb {
  cursor: zoom-in;
  z-index: 2;
}
body[data-zoom="on"] .tile.is-thumb img { filter: saturate(0.9) contrast(1.0); }
/* Always-present overlay on every tile, invisible by default. The zoom mode
   raises it to 50% so the overlay fades in instead of popping. */
.tile::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0);
  pointer-events: none;
  transition: background 380ms ease;
  z-index: 1;
}
body[data-zoom="on"] .tile.is-thumb::after { background: rgba(0, 0, 0, 0.5); }
/* Hover (or keyboard focus) reveals the photo underneath. focus-visible
   AND .is-focused are mirrored so tab/arrow nav gets the same brightness
   lift as the cursor, even when the browser doesn't classify a JS .focus()
   call as keyboard-induced. */
body[data-zoom="on"] .tile.is-thumb:hover::after,
body[data-zoom="on"] .tile.is-thumb:focus-visible::after,
body[data-zoom="on"] .tile.is-thumb.is-focused::after { background: rgba(0, 0, 0, 0); }
body[data-zoom="on"] .tile.is-thumb:hover img,
body[data-zoom="on"] .tile.is-thumb:focus-visible img,
body[data-zoom="on"] .tile.is-thumb.is-focused img { filter: saturate(1.05) contrast(1.05); }
body[data-zoom="on"] .tile:hover img { transform: none; }

.tile img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
  transition: filter var(--dur) var(--ease), transform 600ms var(--ease);
  filter: saturate(0.92) contrast(1.02);
}

.grid:hover .tile:not(:hover) img { filter: saturate(0.75) contrast(1.0) brightness(0.96); }
.tile:hover img { filter: saturate(1.05) contrast(1.04); transform: scale(1.02); }

.tile .label {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.96);
  padding: 10px 18px 10px 20px;
  color: #fff;
  font-family: "JetBrains Mono", "SF Mono", ui-monospace, Menlo, Consolas, monospace;
  font-size: clamp(12px, 1.15vw, 16px);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 700;
  white-space: nowrap;
  opacity: 0;
  background: rgba(0, 0, 0, 0.28);
  backdrop-filter: blur(10px) saturate(140%);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 2px;
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.45);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
  transition: opacity 200ms var(--ease), transform 320ms var(--ease);
  pointer-events: none;
  z-index: 2;
}
.tile .label::after {
  content: "";
  display: inline-block;
  width: 0.6em;
  height: 1em;
  margin-left: 4px;
  vertical-align: -0.12em;
  background: currentColor;
  opacity: 0.9;
  animation: caret 1s steps(2, end) infinite;
}
.tile .label.is-done::after {
  animation: none;
  opacity: 0;
  width: 0;
  margin-left: 0;
  transition: opacity 220ms ease, width 220ms ease;
}

@keyframes caret {
  0%, 49% { opacity: 0.9; }
  50%, 100% { opacity: 0; }
}

.tile:hover .label,
.tile:focus-visible .label,
.tile.is-focused .label {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

body[data-level="2"] .tile .label { display: none; }

/* Back tile: full-bleed category cover with a hover arrow overlay */
.tile.is-back::before {
  content: "←";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.92);
  color: #fff;
  font-size: clamp(22px, 3.4vw, 44px);
  font-weight: 700;
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.75);
  opacity: 0;
  transition: opacity 240ms ease, transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
  pointer-events: none;
  z-index: 3;
}
.tile.is-back:hover::before,
.tile.is-back:focus-visible::before,
.tile.is-back.is-focused::before {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}
/* On touch devices (no hover) the arrow is always subtly visible */
@media (hover: none) {
  .tile.is-back::before {
    opacity: 0.7;
    transform: translate(-50%, -50%) scale(0.92);
    font-size: clamp(18px, 4vw, 28px);
  }
}
/* The back tile never gets the 50% dim overlay, even when it sits in the perimeter */
.tile.is-back::after { display: none; }
body[data-zoom="on"] .tile.is-back.is-thumb::after { display: none; }

/* Zoom mode left/right navigation arrows. Just the icon (no chip / circle);
   sit inside the scaled image with breathing room from the edge. The first
   time the user navigates they glide outward toward the image edge while
   fading out, so the hide doubles as a reveal that the gesture was received. */
.zoom-nav-arrow {
  position: fixed;
  top: 50%;
  width: 44px;
  height: 44px;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: transparent;
  color: #fff;
  border: 0;
  cursor: pointer;
  z-index: 100;
  opacity: 0.85;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.55));
  transform: translate(0, -50%);
  transition:
    opacity 520ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 520ms cubic-bezier(0.22, 1, 0.36, 1);
}
body[data-zoom="on"] .zoom-nav-arrow { display: flex; }
.zoom-nav-arrow.is-left  { left: 10%; }
.zoom-nav-arrow.is-right { right: 10%; }
.zoom-nav-arrow svg {
  width: 100%;
  height: 100%;
  stroke: currentColor;
  stroke-width: 1.6;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke-width 200ms ease;
}
.zoom-nav-arrow:hover { opacity: 1; }
.zoom-nav-arrow:hover svg { stroke-width: 3.2; }
.zoom-nav-arrow:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.7);
  outline-offset: 4px;
  border-radius: 2px;
}
/* Hide animation: glide outward toward the edge of the large image, fading
   out at the same time so the arrow reaches transparency just as it hits
   the image's outer edge. */
.zoom-nav-arrow.is-left.is-hidden  { transform: translate(-60px, -50%); opacity: 0; pointer-events: none; }
.zoom-nav-arrow.is-right.is-hidden { transform: translate( 60px, -50%); opacity: 0; pointer-events: none; }

@media (max-width: 900px) {
  .zoom-nav-arrow { width: 38px; height: 38px; }
  .zoom-nav-arrow.is-left  { left: 8%; }
  .zoom-nav-arrow.is-right { right: 8%; }
}
@media (max-width: 560px) {
  .zoom-nav-arrow { width: 34px; height: 34px; }
  .zoom-nav-arrow.is-left  { left: 6%; }
  .zoom-nav-arrow.is-right { right: 6%; }
  .zoom-nav-arrow.is-left.is-hidden  { transform: translate(-40px, -50%); opacity: 0; }
  .zoom-nav-arrow.is-right.is-hidden { transform: translate( 40px, -50%); opacity: 0; }
}
/* Disable browser horizontal-swipe nav while zoomed so the gesture maps to
   image switching instead of triggering page back/forward on iOS / trackpad. */
body[data-zoom="on"] {
  overscroll-behavior-x: contain;
  touch-action: pan-y;
}

@media (max-width: 900px) {
  :root { --rim: 16px; }
  body[data-level="1"] .grid {
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-rows: 1fr 1fr 1fr 1fr;
  }
  body[data-level="2"] .grid {
    grid-template-columns: repeat(4, 1fr);
    grid-template-rows: repeat(6, 1fr);
  }
}

@media (max-width: 560px) {
  :root { --rim: 12px; }
  body[data-level="1"] .grid {
    grid-template-columns: 1fr 1fr;
    grid-template-rows: repeat(6, 1fr);
  }
  body[data-level="2"] .grid {
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(8, 1fr);
  }
  /* Mobile L1: hide the centre typewriter labels entirely. The hover/focus
     reveal pattern doesn't translate to touch (sticky-hover, no focus ring
     on tap), so the labels would either never appear or leave empty padded
     strips behind after navigating L2→L1. The category cover image alone
     is the affordance on mobile. */
  body[data-level="1"] .tile .label { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .grid, .tile img { transition: none; }
}

/* Photo metadata bar — overlays the bottom edge of the zoomed centre tile.
   Same footprint as the demo footer (48px, near-black, white type) so the
   eye reads them as a unified caption strip. pointer-events: none keeps the
   click-to-close behaviour on the photo itself. */
.photo-meta {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 20px;
  background: rgba(0, 0, 0, 0.55);
  color: #ffffff;
  font-size: 11px;
  letter-spacing: 0.04em;
  pointer-events: none;
  opacity: 0;
  transition: opacity 320ms ease;
  z-index: 6;
}
.photo-meta.is-visible { opacity: 1; }
.photo-meta .pm-left,
.photo-meta .pm-right {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.photo-meta .pm-right { opacity: 0.82; }
.photo-meta .pm-title {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 40vw;
}
.photo-meta .pm-desc {
  font-style: italic;
  opacity: 0.75;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.photo-meta .pm-sep { opacity: 0.4; }
@media (max-width: 720px) {
  .photo-meta { font-size: 10px; padding: 0 14px; gap: 8px; }
  .photo-meta .pm-title { font-size: 11px; max-width: 50vw; }
  .photo-meta .pm-desc { display: none; }
}
