/* Shashin — public site.
   Dark, quiet chrome so the photographs carry the page. No build step, no
   framework: modern baseline CSS (color-mix, light-dark, dvh, :has, grid) with
   graceful fallbacks. */

@layer tokens, base, layout, grid, viewer, bits, responsive;

@layer tokens {
  :root {
    color-scheme: dark;

    --bg: #0a0b0d;
    --bg-raise: #101216;
    --ink: #f2f4f6;
    --ink-2: rgba(242, 244, 246, 0.66);
    --ink-3: rgba(242, 244, 246, 0.4);
    --line: rgba(242, 244, 246, 0.11);
    --line-strong: rgba(242, 244, 246, 0.22);
    --accent: #7fc4ff;
    --scrim: rgba(6, 7, 9, 0.72);

    --pad: clamp(16px, 4vw, 48px);
    --maxw: 1900px;
    --mast-h: 56px;
    --tile-ar: 3 / 2; /* keep in sync with SHASHIN_THUMB_* cover cropping */
    --radius: 6px;

    --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
    --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
      "Noto Sans", sans-serif;
    --font-serif: ui-serif, "Iowan Old Style", "Palatino Linotype", Georgia, serif;
  }

  /* Placeholder tints, picked from the average color of each photo server side
     so a loading frame already reads as the right kind of scene. */
  .sw-0 { --sw: #14171a; }
  .sw-1 { --sw: #3c3f42; }
  .sw-2 { --sw: #3a2a22; }
  .sw-3 { --sw: #3b3421; }
  .sw-4 { --sw: #263325; }
  .sw-5 { --sw: #1e3336; }
  .sw-6 { --sw: #1e2a3d; }
  .sw-7 { --sw: #2e2338; }
}

@layer base {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  html {
    background: var(--bg);
    -webkit-text-size-adjust: 100%;
  }

  html.viewer-open {
    overflow: hidden;
    overscroll-behavior: none;
  }

  body {
    margin: 0;
    min-height: 100dvh;
    font-family: var(--font);
    font-size: 15px;
    line-height: 1.5;
    color: var(--ink);
    background:
      radial-gradient(120% 55% at 50% -10%, rgba(127, 196, 255, 0.05), transparent 60%),
      var(--bg);
    text-rendering: optimizeLegibility;
    -webkit-font-smoothing: antialiased;
  }

  img {
    display: block;
    max-width: 100%;
  }

  a {
    color: inherit;
    text-decoration: none;
  }

  h1,
  h2,
  h3,
  p,
  dl,
  dd,
  figure {
    margin: 0;
  }

  ul {
    margin: 0;
    padding: 0;
    list-style: none;
  }

  :focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: 3px;
  }

  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  ::selection {
    background: color-mix(in oklab, var(--accent) 45%, transparent);
  }
}

@layer layout {
  .masthead {
    position: fixed;
    inset: 0 0 auto;
    z-index: 20;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    height: var(--mast-h);
    padding: 0 var(--pad);
    border-bottom: 1px solid var(--line);
    background: color-mix(in oklab, var(--bg) 82%, transparent);
    backdrop-filter: blur(14px) saturate(1.15);
  }

  /* The two ways out of the top-right, kept in one cluster so the wordmark on the
     left and the actions on the right are the only two things on the bar. */
  .mast-actions {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  /* Both are the same micro-label pill; the RSS one earns its icon, which is the
     one mark readers are expected to recognise without the word. */
  .mast-rss,
  .mast-top {
    display: inline-flex;
    gap: 6px;
    align-items: center;
    padding: 6px 11px;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 10.5px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-2);
    transition: border-color 0.18s var(--ease), color 0.18s var(--ease);
  }

  .mast-rss svg {
    width: 13px;
    height: 13px;
    fill: currentColor;
    stroke: currentColor;
    stroke-width: 2;
    /* The dot is filled, the waves are not — the only difference between this and an
       outline icon, and the reason both properties are set. */
    stroke-linecap: round;
  }

  .mast-rss svg circle {
    stroke: none;
  }

  .mast-rss svg path {
    fill: none;
  }

  .mast-rss:hover,
  .mast-rss:focus-visible {
    border-color: var(--accent);
    color: var(--ink);
  }

  @media (max-width: 520px) {
    /* Two labels plus an icon would crowd a phone bar; the RSS mark stays, because
       the icon is the whole message. */
    .mast-rss span {
      position: absolute;
      width: 1px;
      height: 1px;
      overflow: hidden;
      clip-path: inset(50%);
    }
  }

  .mast-top {
    display: inline-flex;
    gap: 6px;
    align-items: center;
    padding: 6px 11px;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 10.5px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-2);
    transition: border-color 0.18s var(--ease), color 0.18s var(--ease);
  }

  .mast-top svg {
    width: 13px;
    height: 13px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .mast-top:hover,
  .mast-top:focus-visible {
    border-color: var(--accent);
    color: var(--ink);
  }

  /* The bar repeats what each page already says in its own heading, so
     with script on it stays out of the way until the visitor has scrolled into
     the grid (`nav-up` in app.js), and comes back the moment it is focused. */
  html.js .masthead {
    transform: translateY(-100%);
    visibility: hidden;
    transition:
      transform 0.32s var(--ease),
      visibility 0s linear 0.32s;
  }

  html.js.nav-up .masthead,
  html.js .masthead:has(a:focus-visible) {
    transform: none;
    visibility: visible;
    transition:
      transform 0.32s var(--ease),
      visibility 0s;
  }

  /* A full-page photograph gets the whole frame to itself. */
  html.js.viewer-open .masthead {
    transform: translateY(-100%);
    visibility: hidden;
  }

  /* With script off the bar is the only navigation there is. */
  html:not(.js) body {
    padding-top: var(--mast-h);
  }

  /* The same face as the page title below it, just sized to sit in a 56px bar: the
     site is called the same thing in both places, so it should look the same. */
  .wordmark {
    font-family: var(--font-serif);
    font-size: clamp(17px, 2.4vw, 21px);
    font-weight: 500;
    letter-spacing: -0.02em;
    /* Two things are wrong with a line box here. The box is far taller than the
       letters, and a serif's ascent outruns its descent, so the capital band lands
       above the middle of the bar: measured ink centre 26.3px against a bar centre of
       28px. A line box the height of the cap band tightens the box (and the focus ring
       around it), and the translate puts the letters on the bar's optical centre. Both
       are tuned against rendered pixels; another serif may be a pixel out either way,
       which is closer than trusting its metrics ever was. */
    line-height: 0.72;
    transform: translateY(1px);
    color: var(--ink);
  }




  .gallery {
    width: 100%;
    max-width: var(--maxw);
    margin: 0 auto;
    padding: 0 var(--pad) 40px;
  }

  .hero {
    padding: clamp(38px, 7vw, 92px) 0 clamp(18px, 3vw, 30px);
    border-bottom: 1px solid var(--line);
    margin-bottom: clamp(18px, 3vw, 30px);
  }

  .hero-title {
    margin: 10px 0 12px;
    font-family: var(--font-serif);
    font-size: clamp(30px, 6.4vw, 58px);
    font-weight: 500;
    letter-spacing: -0.02em;
    line-height: 1.02;
  }

  .hero-lede {
    max-width: 58ch;
    font-size: 14.5px;
    color: var(--ink-2);
  }

  .notice {
    margin-bottom: 18px;
    padding: 10px 14px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--bg-raise);
    color: var(--ink-2);
    font-size: 13.5px;
  }

  /* ------------------------------------------------------------------ *
   * Sort and tag controls under the masthead. Chips are links, not      *
   * buttons, so the whole thing works before (and without) JavaScript.  *
   * ------------------------------------------------------------------ */
  .filters {
    display: flex;
    flex-wrap: wrap;
    gap: 14px 28px;
    align-items: flex-start;
    margin-bottom: clamp(18px, 3vw, 28px);
    padding-bottom: clamp(14px, 2vw, 20px);
    border-bottom: 1px solid var(--line);
  }

  /* Label above its control, so the two groups line up on the same two rows:
     TAGS beside ORDER, then the chips beside the segmented control. */
  .fgroup {
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: flex-start;
    min-width: 0;
  }

  /* The tag row takes whatever width the ordering leaves and wraps within it. */
  .ftags {
    flex: 1 1 320px;
  }

  .forder {
    flex: 0 0 auto;
  }

  .flabel {
    font-size: 10.5px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-3);
  }

  .seg {
    display: inline-flex;
    padding: 2px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--bg-raise);
  }

  .seg-item {
    padding: 5px 13px;
    border-radius: 999px;
    font-size: 12px;
    color: var(--ink-2);
    white-space: nowrap;
    transition: background-color 0.18s var(--ease), color 0.18s var(--ease);
  }

  .seg-item:hover {
    color: var(--ink);
  }

  .seg-item.is-on {
    background: var(--ink);
    color: var(--bg);
    font-weight: 550;
  }

  .chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .chip {
    display: inline-flex;
    gap: 6px;
    align-items: center;
    padding: 4px 10px;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 12px;
    color: var(--ink-2);
    transition: border-color 0.18s var(--ease), color 0.18s var(--ease),
      background-color 0.18s var(--ease);
  }

  .chip:hover {
    border-color: var(--line-strong);
    color: var(--ink);
  }

  /* A chosen tag is filled rather than merely outlined: the grid behind it has
     already changed, so the chip should read as state, not as a link. */
  .chip.is-on {
    border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 16%, transparent);
    color: var(--ink);
  }

  .chip-x {
    margin-right: -2px;
    color: var(--accent);
    font-size: 13px;
    line-height: 1;
  }

  .chip-n {
    font-size: 10.5px;
    color: var(--ink-3);
    font-variant-numeric: tabular-nums;
  }

  .chip.is-on .chip-n {
    color: var(--ink-2);
  }

  .fmore {
    font-size: 11.5px;
    color: var(--ink-3);
  }

  .fmore summary {
    padding: 4px 8px;
    border: 1px dashed var(--line);
    border-radius: 999px;
    cursor: pointer;
    list-style: none;
  }

  .fmore summary::-webkit-details-marker {
    display: none;
  }

  .fmore summary:hover {
    border-color: var(--line-strong);
    color: var(--ink-2);
  }

  .fmore[open] summary {
    border-style: solid;
    color: var(--ink-2);
  }

  .fmore .chips {
    margin-top: 8px;
  }

  .colophon {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: center;
    padding: 26px var(--pad) calc(30px + env(safe-area-inset-bottom));
    border-top: 1px solid var(--line);
    font-size: 11.5px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-3);
  }

  .colophon a:hover {
    color: var(--ink);
  }

  .colophon-sep {
    color: var(--line-strong);
  }

  .notice-page {
    max-width: 620px;
    margin: 0 auto;
    padding: clamp(70px, 16vh, 160px) var(--pad) 80px;
    text-align: center;
  }

  .notice-code {
    font-family: var(--font-serif);
    font-size: clamp(58px, 14vw, 110px);
    line-height: 1;
    color: var(--ink-3);
  }

  .notice-title {
    margin: 12px 0 10px;
    font-size: 21px;
    font-weight: 600;
  }

  .notice-body {
    color: var(--ink-2);
  }

  .button {
    display: inline-block;
    margin-top: 22px;
    padding: 11px 20px;
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    font-size: 12px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    transition: background 0.18s var(--ease), border-color 0.18s var(--ease);
  }

  .button:hover {
    background: var(--bg-raise);
    border-color: var(--accent);
  }
}

@layer grid {
  .month {
    margin-bottom: clamp(26px, 4vw, 46px);
  }

  .month-head {
    position: sticky;
    top: var(--mast-h);
    z-index: 10;
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin: 0 0 10px;
    padding: 9px 2px 7px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ink-2);
    background: linear-gradient(
      to bottom,
      color-mix(in oklab, var(--bg) 92%, transparent) 60%,
      transparent
    );
    backdrop-filter: blur(6px);
  }

  .month-count {
    padding: 1px 7px;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 10.5px;
    letter-spacing: 0.06em;
    color: var(--ink-3);
  }

  .tiles {
    display: grid;
    gap: 10px;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
  }

  .tile {
    position: relative;
    display: block;
    aspect-ratio: var(--tile-ar);
    overflow: hidden;
    border-radius: var(--radius);
    background: var(--sw, #14171a);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
    isolation: isolate;
  }

  .tile-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.62s var(--ease), filter 0.4s var(--ease);
  }

  .tile-veil {
    position: absolute;
    inset: 0;
    display: block;
    background: linear-gradient(
      to top,
      rgba(4, 5, 7, 0.86) 0%,
      rgba(4, 5, 7, 0.34) 38%,
      rgba(4, 5, 7, 0) 72%
    );
    opacity: 0;
    transition: opacity 0.3s var(--ease);
  }

  .tile-meta {
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    display: grid;
    gap: 3px;
    padding: 12px 13px 12px;
    transform: translateY(8px);
    opacity: 0;
    transition: transform 0.34s var(--ease), opacity 0.28s var(--ease);
  }

  .tile-title,
  .tile-tags,
  .tile-sub {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    mask-image: linear-gradient(to left, transparent 0, #000 14px);
    -webkit-mask-image: linear-gradient(to left, transparent 0, #000 14px);
  }

  .tile-title {
    font-size: 14px;
    font-weight: 600;
    letter-spacing: -0.005em;
    color: #fff;
  }

  .tile-title.is-anon {
    font-weight: 400;
    font-style: italic;
    color: rgba(255, 255, 255, 0.72);
  }

  .tile-tags {
    font-size: 11.5px;
    color: rgba(255, 255, 255, 0.66);
  }

  .tile-tag + .tile-tag::before {
    content: " · ";
    color: rgba(255, 255, 255, 0.35);
  }

  .tile-tag::before {
    content: "#";
    color: rgba(255, 255, 255, 0.4);
  }

  .tile-sub {
    font-size: 10.5px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.5);
  }

  .tile-dot {
    padding: 0 5px;
    color: rgba(255, 255, 255, 0.3);
  }

  /* Hover-capable devices get the reveal; touch devices always see the caption. */
  @media (hover: hover) and (pointer: fine) {
    .tile:hover .tile-img,
    .tile:focus-visible .tile-img {
      transform: scale(1.045);
    }

    .tile:hover .tile-veil,
    .tile:focus-visible .tile-veil {
      opacity: 1;
    }

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

  /* A phone has no hover, and a caption on every tile is a wall of text over the
     photographs. So the caption is hidden, and `hold-caption` in app.js raises the
     one a finger is resting on — holding is what a finger does before it commits to
     a tap, which makes it the touch equivalent of hovering on the way in. */
  @media (hover: none) {
    .tile.is-caption .tile-meta {
      transform: none;
      opacity: 1;
    }

    .tile.is-caption .tile-veil {
      opacity: 0.72;
    }

    .tile-sub {
      display: none;
    }
  }

  .gridfoot {
    padding: 8px 0 26px;
    text-align: center;
  }

  .sentinel {
    height: 1px;
  }

  .loader,
  .gridend,
  .morelink,
  .backlink {
    margin-top: 18px;
    font-size: 11.5px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-3);
  }

  .morelink a,
  .backlink a {
    display: inline-block;
    padding: 10px 18px;
    border: 1px solid var(--line);
    border-radius: 999px;
    color: var(--ink-2);
    transition: border-color 0.18s var(--ease), color 0.18s var(--ease);
  }

  .morelink a:hover,
  .backlink a:hover {
    border-color: var(--accent);
    color: var(--ink);
  }

  .spinner {
    display: inline-block;
    width: 11px;
    height: 11px;
    margin-right: 8px;
    border: 1.5px solid var(--line-strong);
    border-top-color: var(--accent);
    border-radius: 50%;
    vertical-align: -1px;
    animation: spin 0.8s linear infinite;
  }

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

@layer viewer {
  .viewer-root:has(.viewer) {
    display: block;
  }

  .viewer {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: block;
    height: 100dvh;
    background: #06070a;
    color: #fff;
    animation: viewer-fade 0.24s var(--ease) both;
  }

  /* Replacing one photo with the next must not fade the whole overlay up from
     transparent: the grid sits behind it and would show through. */
  .viewer[data-swap] {
    animation: none;
  }

  @keyframes viewer-fade {
    from {
      opacity: 0;
    }
  }

  .viewer-bg {
    position: absolute;
    inset: 0;
    background:
      radial-gradient(80% 60% at 50% 42%, color-mix(in oklab, var(--sw) 55%, transparent), transparent 70%),
      radial-gradient(120% 120% at 50% 50%, rgba(8, 9, 12, 0.5), #05060880 85%),
      #06070a;
  }

  .viewer-frame {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    padding: max(14px, env(safe-area-inset-top)) max(14px, env(safe-area-inset-right))
      max(14px, env(safe-area-inset-bottom)) max(14px, env(safe-area-inset-left));
    pointer-events: none;
  }

  /* Wheel zoom. 1 is the size the photo is fitted to when it opens, so it can
     only ever be magnified, never shrunk into the middle of the screen. The plain
     arrow cursor stays everywhere it was: the backdrop closing the viewer is not
     worth a magnifier icon. */
  .viewer.is-zoomed .viewer-img {
    /* Only worth the extra layer once there is something to drag. */
    will-change: transform;
    cursor: grab;
    /* While magnified the finger drags the picture instead of turning pages. */
    touch-action: none;
  }

  /* Double-click zoom is a gesture, so it is eased. The wheel and dragging are
     direct manipulation and must track the input frame by frame, so the
     transition only runs while app.js marks the change as one. Keep the duration
     in step with ANIM_MS in assets/app.js. */
  .viewer.is-animating .viewer-img {
    transition: transform 0.24s var(--ease);
  }

  .viewer.is-panning {
    cursor: grabbing;
  }

  .viewer.is-panning .viewer-img {
    cursor: grabbing;
  }

  .viewer-img {
    /* Grid items refuse to shrink below their intrinsic size unless told so. */
    min-width: 0;
    min-height: 0;
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
    pointer-events: auto;
    border-radius: 3px;
    background: var(--sw, #101216);
    box-shadow: 0 40px 90px -30px rgba(0, 0, 0, 0.9), 0 0 0 1px rgba(255, 255, 255, 0.06);
    /* No fill mode on purpose: with `both` the finished animation keeps applying
       its final keyframe, which outranks transitions and inline styles, so the
       double-click zoom below could not animate the transform. */
    animation: img-in 0.3s var(--ease);
  }

  @keyframes img-in {
    from {
      opacity: 0;
      transform: scale(0.985);
    }
  }

  .viewer-bar {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 12px max(14px, env(safe-area-inset-right)) 26px max(14px, env(safe-area-inset-left));
    background: linear-gradient(to bottom, rgba(4, 5, 7, 0.62), rgba(4, 5, 7, 0));
    pointer-events: none;
    transition: opacity 0.24s var(--ease), transform 0.24s var(--ease);
  }

  .viewer-tools {
    display: flex;
    gap: 8px;
    pointer-events: auto;
  }

  .viewer-counter {
    pointer-events: none;
    padding: 7px 12px;
    border: 1px solid rgba(255, 255, 255, 0.13);
    border-radius: 999px;
    background: rgba(10, 12, 15, 0.5);
    backdrop-filter: blur(10px);
    font-size: 11.5px;
    letter-spacing: 0.12em;
    font-variant-numeric: tabular-nums;
    color: rgba(255, 255, 255, 0.72);
  }

  .viewer-counter b {
    color: #fff;
    font-weight: 600;
  }

  .vbtn {
    position: relative;
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 999px;
    background: rgba(12, 14, 18, 0.5);
    backdrop-filter: blur(12px);
    color: rgba(255, 255, 255, 0.86);
    cursor: pointer;
    transition: background 0.18s var(--ease), color 0.18s var(--ease),
      border-color 0.18s var(--ease), transform 0.18s var(--ease);
  }

  .vbtn:hover {
    background: rgba(255, 255, 255, 0.16);
    border-color: rgba(255, 255, 255, 0.34);
    color: #fff;
  }

  .vbtn:active {
    transform: scale(0.94);
  }

  .vbtn svg {
    width: 18px;
    height: 18px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .vbtn-close:hover {
    background: #d9483c;
    border-color: #d9483c;
  }

  .vbtn-tip {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    padding: 4px 8px;
    border-radius: 5px;
    background: rgba(8, 9, 12, 0.92);
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
    font-size: 10.5px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    white-space: nowrap;
    opacity: 0;
    transform: translateY(-3px);
    transition: opacity 0.16s var(--ease), transform 0.16s var(--ease);
    pointer-events: none;
  }

  .vbtn:hover .vbtn-tip,
  .vbtn[data-copied] .vbtn-tip {
    opacity: 1;
    transform: none;
  }

  .vbtn[data-copied] .vbtn-tip {
    color: #9fe6b0;
  }

  .viewer-nav {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    pointer-events: none;
    transition: opacity 0.24s var(--ease);
  }

  .vnav {
    position: absolute;
    top: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 46px;
    height: 76px;
    margin: 0;
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 12px;
    background: rgba(10, 12, 15, 0.34);
    backdrop-filter: blur(8px);
    color: rgba(255, 255, 255, 0.82);
    cursor: pointer;
    pointer-events: auto;
    transform: translateY(-50%);
    transition: background 0.18s var(--ease), opacity 0.18s var(--ease),
      transform 0.18s var(--ease), border-color 0.18s var(--ease);
  }

  .vnav-prev {
    left: max(12px, env(safe-area-inset-left));
  }

  .vnav-next {
    right: max(12px, env(safe-area-inset-right));
  }

  .vnav svg {
    width: 26px;
    height: 26px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .vnav-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }

  .vnav:hover:not(:disabled) {
    background: rgba(255, 255, 255, 0.16);
    border-color: rgba(255, 255, 255, 0.3);
  }

  .vnav-prev:hover:not(:disabled) {
    transform: translateY(-50%) translateX(-3px);
  }

  .vnav-next:hover:not(:disabled) {
    transform: translateY(-50%) translateX(3px);
  }

  .vnav:disabled {
    opacity: 0.22;
    cursor: default;
  }

  .viewer-card {
    position: absolute;
    bottom: clamp(16px, 3vh, 30px);
    left: clamp(16px, 2.4vw, 30px);
    width: min(430px, calc(100vw - 132px));
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 14px;
    background: color-mix(in oklab, #0a0c10 76%, transparent);
    backdrop-filter: blur(20px) saturate(1.3);
    box-shadow: 0 24px 60px -28px rgba(0, 0, 0, 0.9);
    transition: opacity 0.26s var(--ease), transform 0.26s var(--ease);
  }

  .viewer-card-scroll {
    max-height: min(56dvh, 460px);
    overflow: auto;
    overscroll-behavior: contain;
    padding: 16px 18px 15px;
  }

  .vc-title {
    font-family: var(--font-serif);
    font-size: 21px;
    font-weight: 500;
    letter-spacing: -0.01em;
    line-height: 1.22;
    text-wrap: balance;
  }

  .vc-title.is-anon {
    font-style: italic;
    font-weight: 400;
    color: rgba(255, 255, 255, 0.6);
  }

  .vc-desc {
    margin-top: 11px;
    font-size: 13.5px;
    line-height: 1.58;
    color: rgba(255, 255, 255, 0.76);
    white-space: pre-line;
  }

  .vc-camera {
    display: flex;
    align-items: center;
    gap: 7px;
    margin-top: 13px;
    font-size: 12px;
    color: rgba(255, 255, 255, 0.7);
  }

  .vc-camera svg {
    flex: none;
    width: 15px;
    height: 15px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linejoin: round;
    opacity: 0.72;
  }

  .vc-specs {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
    gap: 10px 14px;
    margin-top: 14px;
    padding-top: 13px;
    border-top: 1px solid rgba(255, 255, 255, 0.11);
  }

  .vc-spec dt {
    font-size: 9.5px;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.42);
  }

  .vc-spec dd {
    margin: 2px 0 0;
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    color: rgba(255, 255, 255, 0.92);
  }

  .vc-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 14px;
  }

  .vtag {
    padding: 3px 9px;
    border: 1px solid rgba(255, 255, 255, 0.16);
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.05);
    font-size: 11px;
    color: rgba(255, 255, 255, 0.78);
  }

  .vtag::before {
    content: "#";
    margin-right: 1px;
    color: rgba(255, 255, 255, 0.38);
  }

  .vc-note {
    margin-top: 14px;
    font-size: 10.5px;
    letter-spacing: 0.04em;
    color: rgba(255, 255, 255, 0.36);
  }

  /* Only the details panel comes and goes — the counter, tools and arrows keep
     their place, so hiding the text never moves anything around. */
  .viewer.no-card .viewer-card {
    opacity: 0;
    pointer-events: none;
    transform: translateY(10px);
  }

  /* A click on the photograph hides the whole gallery, panel included. Nothing
     moves: the bar and the arrows only fade, so the picture they sit against never
     shifts when they come back. `inert` in app.js takes them out of the tab order
     while they are invisible. */
  .viewer.no-chrome .viewer-bar,
  .viewer.no-chrome .viewer-nav,
  .viewer.no-chrome .viewer-card {
    opacity: 0;
  }

  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      transition-duration: 0.01ms !important;
    }
  }
}

@layer bits {
  /* Same-document view transitions if the browser has them. */
  @supports (view-transition-name: root) {
    ::view-transition-old(root),
    ::view-transition-new(root) {
      animation-duration: 0.22s;
      animation-timing-function: var(--ease);
    }
  }
}

@layer responsive {
  /* Docked details sheet. Keep this breakpoint in sync with SHEET_LAYOUT in
     assets/app.js: the panel only stays hidden across photos when it covers the
     picture rather than sitting next to it. */
  @media (max-width: 760px) {
    :root {
      --mast-h: 52px;
    }

    .hero-title {
      font-size: clamp(28px, 9vw, 40px);
    }

    .tiles {
      gap: 6px;
      grid-template-columns: repeat(auto-fill, minmax(min(100%, 160px), 1fr));
    }

    .tile {
      border-radius: 4px;
    }

    .viewer-frame {
      padding: 8px;
      transition: padding 0.26s var(--ease);
    }

    /* The details sheet docks at the bottom and takes about a third of the
       screen — but only while it is actually on screen, so hiding it gives the
       whole page back to the photograph. */
    .viewer:not(.no-card) .viewer-frame {
      padding-bottom: 34dvh;
    }

    .viewer-img {
      box-shadow: 0 20px 50px -28px rgba(0, 0, 0, 0.9);
    }

    .viewer-card {
      right: 0;
      bottom: 0;
      left: 0;
      width: auto;
      border-right: 0;
      border-bottom: 0;
      border-left: 0;
      border-radius: 16px 16px 0 0;
      padding-bottom: env(safe-area-inset-bottom);
    }

    .viewer-card-scroll {
      max-height: 32dvh;
      padding: 14px 18px 16px;
    }

    .vc-title {
      font-size: 18px;
    }

    /* Keep the arrows clear of the bottom sheet… */
    .vnav {
      top: auto;
      bottom: calc(36dvh + 12px);
      transform: none;
      width: 40px;
      height: 58px;
    }

    /* …and put them back at the bottom edge once the sheet is hidden. */
    .viewer.no-card .vnav {
      bottom: calc(18px + env(safe-area-inset-bottom));
    }

    .vnav-prev:hover:not(:disabled),
    .vnav-next:hover:not(:disabled) {
      transform: none;
    }

    .viewer-counter {
      font-size: 10.5px;
    }

    .vbtn {
      width: 34px;
      height: 34px;
    }
  }

  @media (max-width: 420px) {
  }
}
