:root {
  --ink: #e9ecf1;
  --muted: #9aa4b2;
  --bg: #0b0d10;
  --panel: #14181e;
  --line: #232a33;
  --accent: #7aa2f7;
  --danger: #f7768e;
  --ok: #9ece6a;
  --font: ui-sans-serif, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.6 var(--font);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); }

/* --- flashes ------------------------------------------------------- */

.flashes {
  position: fixed;
  top: 1rem; left: 50%;
  transform: translateX(-50%);
  z-index: 50;
  margin: 0; padding: 0;
  list-style: none;
  display: grid; gap: .5rem;
  width: min(90vw, 34rem);
}
.flash {
  padding: .6rem .9rem;
  border-radius: .5rem;
  background: var(--panel);
  border: 1px solid var(--line);
  font-size: .9rem;
  box-shadow: 0 8px 24px rgb(0 0 0 / .45);
}
.flash--error { border-color: var(--danger); }
.flash--success { border-color: var(--ok); }

/* --- slideshow ----------------------------------------------------- */

.body--show { overflow: hidden; background: #000; }

.stage {
  position: fixed;
  inset: 0;
  background: #000;
  /* A straightened photo is turned and scaled up past the edges of the stage;
     this is what trims the overhang instead of letting it reach the page. */
  overflow: hidden;
}

/* Everything inside .stage is absolutely positioned, so paint order would
   normally follow the markup and put these overlays above the photograph. It
   does not: `will-change: transform` on the image promotes it to its own
   compositing layer, which then paints above later siblings — the caption, the
   details panel and the controls all disappeared behind the photo, and the
   controls stopped taking clicks.

   The gap matters. `.caption { z-index: 1 }` against `.frame { z-index: 0 }`
   still lost to the composited image; only a clear separation puts the overlays
   above it, so these are spaced rather than consecutive. */
.frame { z-index: 0; }
.caption { z-index: 10; }
.info { z-index: 20; }
.controls { z-index: 30; }

.frame {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  /* Eased both ends: a linear opacity ramp reads as an abrupt start and stop.
     Duration comes from the display setting. */
  transition: opacity var(--fade, 1200ms) cubic-bezier(.37, 0, .28, 1);
}
.frame.is-visible { opacity: 1; }

/* The layer a straightening angle turns, and the scale that puts the corners
   back inside the frame. Separate from the images because the Ken Burns
   animation owns their `transform`, and separate from the frame because the
   frame is what crops the turned picture back to an upright rectangle. */
.frame__turn {
  position: absolute;
  inset: 0;
  transform-origin: center center;
}

/* Used to drop the outgoing layer once it is hidden underneath the incoming
   one — without this it would fade a second time, visibly. */
.frame.is-instant { transition: none; }

.no-motion .frame { transition: none; }

/* The blurred copy sits exactly over the sharp one. Its mask decides where it
   is transparent, and the sharp photo shows through there. `filter` and
   `mask-image` are both set from the photo's settings by blur.js. */
.frame__blur {
  position: absolute;
  inset: 0;
  /* Blur samples beyond the element's edges, which would otherwise leave a
     washed-out border where there is nothing to sample. */
  transform-origin: center center;
  scale: 1.04;
}
.frame__blur[hidden] { display: none; }

.frame img {
  width: 100%;
  height: 100%;
  display: block;
  /* Set from the admin display setting. `cover` fills the screen and crops the
     long edge; `contain` shows the whole frame with bars around it. */
  object-fit: cover;
  transform-origin: center center;
  will-change: transform;
}
.stage[data-fit="contain"] .frame img { object-fit: contain; }

/* --- Ken Burns -------------------------------------------------------
   A slow zoom with a little drift. Four variants so consecutive photos do not
   all move the same way; the JS cycles through them. The animation is applied
   only to the frame that is currently visible, and its duration is set inline
   from the slide interval so the movement never finishes early and freezes. */
@keyframes kb-a { from { transform: scale(1)    translate(0, 0); }
                  to   { transform: scale(1.13) translate(-1.5%, -1%); } }
@keyframes kb-b { from { transform: scale(1.13) translate(1.5%, 1%); }
                  to   { transform: scale(1)    translate(0, 0); } }
@keyframes kb-c { from { transform: scale(1.02) translate(1.5%, -1%); }
                  to   { transform: scale(1.15) translate(-1%, 1%); } }
@keyframes kb-d { from { transform: scale(1.15) translate(-1%, 1%); }
                  to   { transform: scale(1.02) translate(1%, -1.5%); } }

.stage[data-effect="kenburns"] .frame.is-visible img {
  animation-name: var(--kb-name, kb-a);
  animation-duration: var(--kb-duration, 6s);
  animation-timing-function: linear;
  animation-fill-mode: forwards;
}

/* Motion is the thing being opted out of, so the effect goes entirely. */
@media (prefers-reduced-motion: reduce) {
  .stage[data-effect="kenburns"] .frame.is-visible img { animation: none; }
}
.no-motion .frame.is-visible img { animation: none !important; }

.caption {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 4rem 1.5rem 1.5rem;
  display: flex;
  flex-wrap: wrap;
  gap: .25rem 1rem;
  align-items: baseline;
  background: linear-gradient(to top, rgb(0 0 0 / .72), transparent);
  font-size: .95rem;
  pointer-events: none;
}
/* Same trap as .info__row: `display: flex` above would keep an untitled,
   undated photo's empty caption bar — and its gradient — on screen. */
.caption[hidden] { display: none; }
.caption__date { color: var(--muted); font-variant-numeric: tabular-nums; font-size: .85rem; }

.info {
  position: absolute;
  left: 1.5rem; bottom: 5rem;
  max-width: min(22rem, calc(100vw - 3rem));
  padding: 1rem 1.1rem;
  border-radius: .6rem;
  background: rgb(0 0 0 / .62);
  backdrop-filter: blur(10px);
  border: 1px solid rgb(255 255 255 / .12);
  font-size: .85rem;
}
.info dl { margin: 0; display: grid; gap: .45rem; }
/* 7rem, because "Photographer" needs 108px at this size and letter-spacing.
   Each row is its own grid, so the column has to be wide enough for the longest
   label in the set — an `auto` column would size per row and stop lining up. */
.info__row { display: grid; grid-template-columns: 7rem 1fr; gap: .75rem; align-items: baseline; }
/* The script hides rows the photo has no value for. `display: grid` above beats
   the `hidden` attribute's UA rule, so without this a photo with no capture date
   still shows an empty "Taken" line. */
.info__row[hidden] { display: none; }
.info dt { color: var(--muted); font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; }
.info dd { margin: 0; overflow-wrap: anywhere; }

.ctl[disabled] { opacity: .35; cursor: default; }
.ctl[aria-pressed="true"] { background: rgb(255 255 255 / .9); color: #000; }

/* Out of the way by default. The script shows them on arrival, then puts them to
   sleep, and wakes them when the pointer reaches the corner they live in (or on
   a tap, where there is no pointer to hover with). `pointer-events: none` while
   asleep so an invisible button never swallows a click. */
.controls {
  position: absolute;
  right: 1rem; bottom: 1rem;
  display: flex;
  gap: .5rem;
  opacity: 0;
  pointer-events: none;
  transition: opacity 400ms ease;
}
.stage.is-awake .controls,
.controls:focus-within { opacity: 1; pointer-events: auto; }
.no-motion .controls { transition: none; }

.ctl {
  width: 2.75rem; height: 2.75rem;
  border-radius: 50%;
  border: 1px solid rgb(255 255 255 / .18);
  background: rgb(0 0 0 / .45);
  backdrop-filter: blur(6px);
  color: #fff;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}
.ctl:hover { background: rgb(0 0 0 / .7); }
.ctl:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.empty {
  min-height: 100vh;
  display: grid;
  place-content: center;
  text-align: center;
  gap: .5rem;
}
.empty h1 { font-size: clamp(2rem, 7vw, 3rem); margin: 0; letter-spacing: -.02em; }
.empty p { color: var(--muted); margin: 0; }

/* --- forms / admin -------------------------------------------------- */

.wrap { width: min(100% - 2rem, 62rem); margin: 3rem auto; }

.topbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  margin-bottom: 2rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--line);
}
.topbar h1 { font-size: 1.25rem; margin: 0; letter-spacing: -.01em; }

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: .75rem;
  padding: 1.25rem;
  margin-bottom: 1.5rem;
}
.card h2 { margin: 0 0 .75rem; font-size: 1rem; }

label { display: block; font-size: .85rem; color: var(--muted); margin-bottom: .25rem; }

input[type="text"], input[type="password"], input[type="date"],
input[type="time"], input[type="file"] {
  width: 100%;
  padding: .55rem .7rem;
  border-radius: .5rem;
  border: 1px solid var(--line);
  background: #0e1116;
  color: var(--ink);
  font: inherit;
  font-size: .95rem;
}
input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

button, .btn {
  padding: .55rem 1rem;
  border-radius: .5rem;
  border: 1px solid var(--line);
  background: #1c222b;
  color: var(--ink);
  font: inherit;
  font-size: .9rem;
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}
button:hover, .btn:hover { border-color: var(--accent); }
button.primary { background: var(--accent); border-color: var(--accent); color: #0b0d10; font-weight: 600; }
button.danger:hover { border-color: var(--danger); color: var(--danger); }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- thumbnail grid --------------------------------------------------- */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
  gap: .6rem;
  margin-top: 1rem;
}

.tile {
  position: relative;
  margin: 0;
  aspect-ratio: 4 / 3;
  border-radius: .5rem;
  overflow: hidden;
  background: #000;
  border: 1px solid var(--line);
  cursor: grab;
  /* Without this the browser starts a text selection instead of a drag. */
  user-select: none;
  /* And without this iOS offers to save the photograph when a press is held —
     which is the same press that starts the drag. */
  -webkit-touch-callout: none;
}
.tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tile img { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }

.tile.is-dragging { opacity: .35; cursor: grabbing; border-color: var(--accent); }
.tile.is-hidden img { opacity: .4; }

/* For the length of a drag, tiles slide under a cursor that is not moving.
   Anything keyed on :hover would blink on and off as they pass, so hover
   styling is switched off wholesale rather than fought tile by tile. */
.tiles.is-reordering .tile { cursor: grabbing; }
.tiles.is-reordering .tile__gear { opacity: 0; transition: none; pointer-events: none; }

.tile__gear {
  position: absolute;
  top: .35rem; right: .35rem;
  width: 2rem; height: 2rem;
  padding: 0;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid rgb(255 255 255 / .25);
  background: rgb(0 0 0 / .72);
  backdrop-filter: blur(6px);
  color: #fff;
  font-size: 1.05rem;
  line-height: 1;
  opacity: 0;
  transition: opacity 150ms ease, background 150ms ease;
  box-shadow: 0 2px 8px rgb(0 0 0 / .4);
}
.tile__gear:hover { background: #000; border-color: var(--accent); }
.tile:hover .tile__gear,
.tile:focus-within .tile__gear,
.tile__gear:focus-visible { opacity: 1; }
@media (hover: none) { .tile__gear { opacity: 1; } }

/* A quiet mark on the tiles that have HDR files behind them. It says something
   about the photograph rather than offering an action, so it is a label, not a
   button, and it stays visible without hovering — a tile cannot show its own
   dynamic range, since the thumbnail is tone-mapped like every other one.

   Bottom right because the other three corners are taken: blur top left, gear
   top right, eye bottom left. It was put in the blur button's corner to begin
   with, where it was invisible on a phone — the buttons fade in on hover, but a
   touch screen has no hover, so all three are permanently opaque and the badge
   sat underneath one of them. */
.tile__hdr {
  position: absolute;
  bottom: .35rem; right: .35rem;
  padding: .1rem .35rem;
  border-radius: .25rem;
  border: 1px solid rgb(255 255 255 / .25);
  background: rgb(0 0 0 / .72);
  backdrop-filter: blur(6px);
  color: #fff;
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .06em;
  line-height: 1.4;
  pointer-events: none;
}

/* The eye toggles the photo in and out of the slideshow. Unlike the gear it
   stays put on a hidden photo — that is the badge it replaces, and a tile has
   to say it is hidden without being hovered. */
.tile__eye {
  position: absolute;
  left: .35rem; bottom: .35rem;
  width: 2rem; height: 2rem;
  padding: 0;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid rgb(255 255 255 / .25);
  background: rgb(0 0 0 / .72);
  backdrop-filter: blur(6px);
  color: #fff;
  line-height: 1;
  opacity: 0;
  transition: opacity 150ms ease, background 150ms ease, color 150ms ease;
  box-shadow: 0 2px 8px rgb(0 0 0 / .4);
}
.tile:hover .tile__eye,
.tile:focus-within .tile__eye,
.tile__eye:focus-visible,
.tile.is-hidden .tile__eye { opacity: 1; }
@media (hover: none) { .tile__eye { opacity: 1; } }

.tile__eye:hover { background: #000; border-color: var(--accent); }
.tile.is-hidden .tile__eye { color: var(--danger); border-color: rgb(247 118 142 / .4); }

.tile__eye .eye__slash, .tile__eye .eye__cut { display: none; }
.tile.is-hidden .tile__eye .eye__slash,
.tile.is-hidden .tile__eye .eye__cut { display: block; }
/* Drawn under the slash in the button's own colour, so the stroke cuts the eye
   rather than sitting on top of it. */
.tile__eye .eye__cut { stroke: #000; stroke-width: 4.5; }

/* Same chip as the gear, in the other top corner. It stays showing when the
   photo has a blur set, so the grid says which photos carry one. */
.tile__blur {
  position: absolute;
  top: .35rem; left: .35rem;
  width: 2rem; height: 2rem;
  padding: 0;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid rgb(255 255 255 / .25);
  background: rgb(0 0 0 / .72);
  backdrop-filter: blur(6px);
  color: #fff;
  line-height: 1;
  opacity: 0;
  transition: opacity 150ms ease, background 150ms ease;
  box-shadow: 0 2px 8px rgb(0 0 0 / .4);
}
.tile:hover .tile__blur,
.tile:focus-within .tile__blur,
.tile__blur:focus-visible,
.tile__blur.is-on { opacity: 1; }
@media (hover: none) { .tile__blur { opacity: 1; } }
.tile__blur:hover { background: #000; border-color: var(--accent); }
.tile__blur.is-on { color: var(--accent); border-color: rgb(122 162 247 / .45); }
.tiles.is-reordering .tile__blur { opacity: 0; transition: none; pointer-events: none; }

/* The straighten chip sits beside the gear rather than in a corner of its own:
   all four corners were already taken — blur top left, gear top right, eye
   bottom left, HDR badge bottom right. Like the blur chip it stays showing when
   the photo carries an angle, so the grid says which photographs are turned. */
.tile__rotate {
  position: absolute;
  top: .35rem; right: 2.6rem;
  width: 2rem; height: 2rem;
  padding: 0;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid rgb(255 255 255 / .25);
  background: rgb(0 0 0 / .72);
  backdrop-filter: blur(6px);
  color: #fff;
  line-height: 1;
  opacity: 0;
  transition: opacity 150ms ease, background 150ms ease;
  box-shadow: 0 2px 8px rgb(0 0 0 / .4);
}
.tile:hover .tile__rotate,
.tile:focus-within .tile__rotate,
.tile__rotate:focus-visible,
.tile__rotate.is-on { opacity: 1; }
@media (hover: none) { .tile__rotate { opacity: 1; } }
.tile__rotate:hover { background: #000; border-color: var(--accent); }
.tile__rotate.is-on { color: var(--accent); border-color: rgb(122 162 247 / .45); }
.tiles.is-reordering .tile__rotate { opacity: 0; transition: none; pointer-events: none; }

/* --- the straighten dialog -------------------------------------------- */

.rotatepad {
  position: relative;
  width: 100%;
  /* Capped like the blur pad so the slider and the save button stay in view. */
  max-height: 30vh;
  margin: 0 auto .75rem;
  overflow: hidden;                /* the turned picture is cropped, as on stage */
  border-radius: .5rem;
  background: #000;
  user-select: none;
}
.rotatepad img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  transform-origin: center center;
}

/* The guides: two horizontal and two vertical lines on the thirds, and a
   brighter pair through the middle. They stay level while the photograph turns
   underneath them — that is the whole trick, since an eye cannot see a degree
   of tilt without something straight beside it. The lines are drawn as
   gradients rather than elements so there is nothing to lay out. */
.rotatepad__grid {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Six flat gradients: a line down and across the middle, and a pair each way
     on the thirds. Each one is its own 1px-wide layer placed by percentage,
     which is the only way to put a line at 33% of a box without an element. */
  background-image:
    linear-gradient(#fff, #fff), linear-gradient(#fff, #fff),
    linear-gradient(#bbb, #bbb), linear-gradient(#bbb, #bbb),
    linear-gradient(#bbb, #bbb), linear-gradient(#bbb, #bbb);
  background-size: 1px 100%, 100% 1px, 1px 100%, 1px 100%, 100% 1px, 100% 1px;
  background-position:
    50% 0, 0 50%,
    33.333% 0, 66.667% 0,
    0 33.333%, 0 66.667%;
  background-repeat: no-repeat;
  /* A white line vanishes over a bright sky; this keeps it readable over both. */
  mix-blend-mode: difference;
  opacity: .75;
}

.rotateform input[type="range"] { width: 100%; }
.rotateform__nudges { display: flex; gap: .5rem; flex-wrap: wrap; }

/* --- the blur dialog -------------------------------------------------- */

.blurpad {
  position: relative;
  width: 100%;
  /* Capped so the controls and the save button stay in view without scrolling;
     the photo is cropped to fit rather than shrunk, as it is on the stage. */
  max-height: 30vh;
  margin: 0 auto .75rem;
  overflow: hidden;
  border-radius: .5rem;
  background: #000;
  touch-action: none;              /* dragging aims the blur, it does not scroll */
  cursor: crosshair;
  user-select: none;
}
.blurpad img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.blurpad__blur[hidden] { display: none; }
.blurpad__blur { scale: 1.04; }   /* blur samples past the edge; hide the fringe */

/* Shows where the sharp area is: a ring for a point, a band for a line. */
.blurpad__guide {
  position: absolute;
  pointer-events: none;
  border: 1px dashed rgb(255 255 255 / .85);
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgb(0 0 0 / .45), inset 0 0 0 1px rgb(0 0 0 / .45);
}
.blurpad__guide.is-line {
  border-radius: 0;
  border-left: 0; border-right: 0;
}
.blurpad__hint { margin: -.4rem 0 .75rem; }

.blurform input[type="range"] { width: 100%; }
.blurform input[type="number"] {
  width: 100%;
  padding: .55rem .7rem;
  border-radius: .5rem;
  border: 1px solid var(--line);
  background: #0e1116;
  color: var(--ink);
  font: inherit;
  font-size: .95rem;
}
.blurform button[type="submit"] { margin-top: 1rem; }

/* --- photo details ---------------------------------------------------- */

.fields { display: grid; gap: .6rem .75rem; grid-template-columns: 1fr 1fr; }
.fields .span2 { grid-column: 1 / -1; }

.editor {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: .75rem;
  padding: 1.25rem;
  margin-bottom: 1.5rem;
}
.editor__title { margin: 0 0 .75rem; font-size: 1rem; }
.editor__delete { margin-top: 1rem; }

/* With JavaScript the editors live in the modal; the page copies are the
   no-JavaScript fallback. */
.js .editors { display: none; }
.js .modal .editor {
  background: none;
  border: 0;
  border-radius: 0;
  padding: 0;
  margin: 0;
}
.js .modal .editor__title { display: none; }   /* the modal shows it already */

/* --- modal ------------------------------------------------------------ */

body.is-modal { overflow: hidden; }

.modal { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; }
.modal[hidden] { display: none; }
.modal__backdrop { position: absolute; inset: 0; background: rgb(0 0 0 / .6); backdrop-filter: blur(3px); }

.modal__panel {
  position: relative;
  width: min(100% - 2rem, 34rem);
  /* Tall enough for the blur dialog — a preview, six controls and a button —
     to be read without scrolling on a laptop screen. */
  max-height: min(92vh, 54rem);
  overflow-y: auto;
  padding: 1.25rem;
  border-radius: .75rem;
  background: var(--panel);
  border: 1px solid var(--line);
  box-shadow: 0 24px 60px rgb(0 0 0 / .6);
}
.modal__head { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
.modal__head h2 { margin: 0; font-size: 1rem; overflow-wrap: anywhere; }
.modal__close {
  margin-left: auto;
  width: 2rem; height: 2rem;
  padding: 0;
  display: grid; place-items: center;
  border-radius: 50%;
  font-size: 1.2rem;
  line-height: 1;
}

/* --- upload drop zone ------------------------------------------------ */

.dropzone {
  border: 1.5px dashed var(--line);
  border-radius: .75rem;
  background: #0e1116;
  transition: border-color 150ms ease, background 150ms ease;
}

.dropzone__label {
  display: grid;
  justify-items: center;
  gap: .2rem;
  padding: 2.25rem 1rem;
  margin: 0;
  cursor: pointer;
  text-align: center;
}
.dropzone__icon {
  font-size: 1.5rem;
  line-height: 1;
  width: 2.5rem; height: 2.5rem;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1px solid var(--line);
  color: var(--muted);
  margin-bottom: .35rem;
}
.dropzone__title { font-size: .95rem; color: var(--ink); }
.dropzone__hint { font-size: .8rem; color: var(--muted); }

/* The input stays focusable and in the layout — visually hidden, not display:none,
   so keyboard users can still reach it and required validation can focus it. */
.dropzone input[type="file"] {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; border: 0;
  opacity: 0;
  overflow: hidden;
}
.dropzone input[type="file"]:focus-visible + .dropzone__files,
.dropzone:focus-within {
  border-color: var(--accent);
}

.dropzone.is-over {
  border-color: var(--accent);
  background: rgb(122 162 247 / .08);
}
.dropzone.is-over .dropzone__icon { border-color: var(--accent); color: var(--accent); }

.dropzone.has-files .dropzone__label { padding: 1.25rem 1rem; }

.dropzone__files {
  list-style: none;
  margin: 0;
  padding: 0;
}
.dropzone__files:not(:empty) {
  border-top: 1px solid var(--line);
  padding: .5rem .9rem .75rem;
}
.dropzone__files li {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: .3rem 0;
  font-size: .85rem;
  border-bottom: 1px solid rgb(255 255 255 / .04);
}
.dropzone__files li:last-child { border-bottom: 0; }
.dropzone__name { overflow-wrap: anywhere; }
.dropzone__size { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.dropzone__files li.is-bad .dropzone__name { color: var(--danger); }
.dropzone__files li.is-bad .dropzone__size { color: var(--danger); }

.meta-grid {
  display: grid;
  gap: .6rem .75rem;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  margin-top: 1rem;
}

.opt { text-transform: none; letter-spacing: 0; font-style: italic; opacity: .7; }

select {
  width: 100%;
  padding: .55rem .7rem;
  border-radius: .5rem;
  border: 1px solid var(--line);
  background: #0e1116;
  color: var(--ink);
  font: inherit;
  font-size: .95rem;
}
select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.blockrow {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  padding: .6rem 0;
  border-bottom: 1px solid var(--line);
}
.blockrow:last-child { border-bottom: 0; }
.blockrow code { font-size: .9rem; }
.blockrow form { margin-left: auto; }

.checkline { display: flex; align-items: center; gap: .4rem; font-size: .85rem; color: var(--muted); }
.checkline label { margin: 0; }

.login { width: min(100% - 2rem, 22rem); margin: 15vh auto; }
.login h1 { font-size: 1.5rem; margin: 0 0 1.5rem; letter-spacing: -.02em; }
.login button { width: 100%; margin-top: .75rem; }

.note { color: var(--muted); font-size: .85rem; }

.error-page { min-height: 100vh; display: grid; place-content: center; text-align: center; gap: .5rem; }
.error-page h1 { font-size: 4rem; margin: 0; }

@media (max-width: 46rem) {
  /* A narrow window with a mouse can afford smaller tiles: a cursor hits a 2rem
     button at any size. The touch rules below put this back, because a finger
     cannot. */
  .tiles { grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); }
  .fields { grid-template-columns: 1fr; }
  .info { left: 1rem; right: 1rem; bottom: 4.75rem; max-width: none; }
  .info__row { grid-template-columns: 7rem 1fr; }
}

/* --- section nav ------------------------------------------------------ */

/* Sticks to the top so the way back to Upload is always one tap away, however
   far down the grid you have scrolled. */
.cardnav {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  gap: .25rem;
  margin: 1rem 0 0;
  padding: .4rem 0;
  overflow-x: auto;                  /* four sections still fit a narrow phone */
  scrollbar-width: none;
  /* Opaque, or the cards scrolling underneath show through the bar. */
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}
.cardnav::-webkit-scrollbar { display: none; }

.cardnav a {
  flex: 0 0 auto;
  padding: .45rem .8rem;
  border-radius: .5rem;
  color: var(--muted);
  text-decoration: none;
  font-size: .85rem;
  white-space: nowrap;
  transition: background 150ms ease, color 150ms ease;
}
.cardnav a:hover { background: var(--panel); color: var(--ink); }
.cardnav a.is-current { background: var(--panel); color: var(--ink); }

/* Anchors land below the sticky bar rather than under it. */
.card[id] { scroll-margin-top: 3.5rem; }

@media (pointer: coarse) {
  .cardnav a { padding: .6rem .9rem; font-size: .9rem; }
  .card[id] { scroll-margin-top: 4rem; }
}

/* --- touch ------------------------------------------------------------ */

/* Instructions that differ by input device. Both are in the markup and one is
   hidden, rather than chosen in JavaScript, so the right one is there in the
   first paint and is still right with scripting off. */
.only-touch { display: none; }
@media (pointer: coarse) {
  .only-touch { display: inline; }
  .only-pointer { display: none; }
}

/* A finger is not a cursor. Everything below is keyed on the pointer being
   coarse rather than on the screen being narrow, because the two are different
   questions: a tablet is wide and touched, a small window is narrow and
   clicked, and it is the pointer that decides whether a 2rem target is enough. */
@media (pointer: coarse) {
  /* Fewer, larger tiles. Three across a phone leaves about 7rem per tile, and a
     tile has to carry four buttons and a badge without them touching. Two
     across gives half as many photos per screen and controls that can be hit
     without aiming — 11rem rather than the 9.5rem that was enough before the
     straighten button joined the row along the top. */
  .tiles { grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: .6rem; }

  /* 2.75rem is the usual minimum for a touch target. The icons stay the size
     they were; it is the button around them that grows. */
  .tile__gear, .tile__eye, .tile__blur, .tile__rotate { width: 2.75rem; height: 2.75rem; }
  .tile__gear { font-size: 1.2rem; }

  /* Room for the bigger buttons, and clear of the rounded corners. */
  .tile__gear { top: .3rem; right: .3rem; }
  .tile__rotate { top: .3rem; right: 3.35rem; }
  .tile__blur { top: .3rem; left: .3rem; }
  .tile__eye { bottom: .3rem; left: .3rem; }
  .tile__hdr { bottom: .5rem; right: .5rem; }
}
