/*
  The shell (sign-in, left rail, view frame) and the components views share.
  Only tokens.css values are used here — no raw colours.
*/

/* ------------------------------------------------------------- sign-in */

.gate {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--sp-5);
  z-index: 50;
  overflow: auto;
}

/* A soft wash behind the card so the first screen of the product doesn't
   look like a flat error page. Purely decorative. */
.gate-art {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(900px 500px at 50% -20%, rgba(124, 92, 255, .22), transparent 70%),
    radial-gradient(600px 400px at 85% 110%, rgba(52, 211, 153, .08), transparent 70%);
  pointer-events: none;
}

.gate-card {
  position: relative;
  width: 100%;
  max-width: 400px;
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-5);
  background: var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-3);
}

.gate-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.gate-logo {
  width: 42px;
  height: 42px;
  flex: none;
  padding: 5px;
  color: #fff;
  background: linear-gradient(140deg, var(--accent), var(--accent-2));
  border-radius: var(--r-md);
}

.gate-brand h1 {
  font-size: 21px;
}

/* The name set as the logo: tighter than body text, with the AI carried in the
   accent so the two halves read as one word rather than a sentence. */
.wordmark {
  font-weight: 650;
  letter-spacing: -.02em;
}

.wordmark span {
  color: var(--accent-2);
}

.gate-seg {
  align-self: stretch;
}

.gate-seg .seg-btn {
  flex: 1;
}

.gate-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

/* ---------------------------------------------------------------- shell */

.shell {
  display: grid;
  grid-template-columns: var(--rail-w) 1fr;
  height: 100%;
}

.rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-3) 0;
  background: var(--s1);
  border-right: 1px solid var(--border);
  overflow-y: auto;
  overflow-x: hidden;
}

.rail-brand {
  width: 38px;
  height: 38px;
  flex: none;
  margin-bottom: var(--sp-3);
  padding: 5px;
  color: #fff;
  background: linear-gradient(140deg, var(--accent), var(--accent-2));
  border-radius: var(--r-md);
}

.rail-items {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  align-items: center;
}

.rail-foot {
  margin-top: auto;
  padding-top: var(--sp-3);
}

/* Icon over a small label, like the reference tools. The label is what makes
   the rail legible without a hover; the icon is what makes it scannable. */
.rail-item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 68px;
  padding: 8px 2px 6px;
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  color: var(--fg-3);
  font-size: 10.5px;
  font-weight: 550;
  text-align: center;
  cursor: pointer;
  transition: color var(--t-fast), background var(--t-fast);
}

.rail-item svg {
  width: 21px;
  height: 21px;
}

.rail-item span {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rail-item:hover {
  color: var(--fg);
  background: var(--s2);
}

.rail-item.active {
  color: var(--fg);
  background: var(--accent-soft);
}

/* The marker that says "you are here", anchored to the rail edge. */
.rail-item.active::before {
  content: "";
  position: absolute;
  left: -8px;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 22px;
  border-radius: var(--r-pill);
  background: var(--accent);
}

.rail-sep {
  width: 44px;
  height: 1px;
  margin: var(--sp-2) 0;
  background: var(--border);
}

/* The rail has room for every section, so the phone-only overflow entry is
   not wanted here. */
.rail-more,
/* For a control that only makes sense once the layout is one column at a
   time — a way to another tab, where a wide screen already shows both. */
.narrow-only {
  display: none;
}

/* A switch, not a destination, so it never takes the active marker — its state
   is carried by colour instead. */
.nsfw-toggle.on {
  color: var(--nsfw);
}

.nsfw-toggle.on:hover {
  color: var(--nsfw);
  background: rgba(244, 114, 182, .12);
}

.view {
  overflow-y: auto;
  overflow-x: hidden;
  position: relative;
}

.view:focus {
  outline: none;
}

/* -------------------------------------------------- page scaffolding */

/* Every view opens with this header so switching sections feels like the
   same product rather than a different page. */
.page-head {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 64px;
  padding: var(--sp-3) var(--sp-5);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
}

.page-head h1 {
  font-size: 20px;
}

/* The view's own controls, grouped so a narrow screen can move them together
   onto a row of their own instead of squeezing them beside the title. */
.head-tools {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
}

/* The trail in the page head. It carries the h1, so the current folder is the
   heading rather than a decoration next to one. */
.crumbs {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

.crumbs h1 {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
}

.crumbs h1 span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.crumbs h1 svg {
  flex: none;
  width: 17px;
  height: 17px;
  color: var(--fg-3);
}

/* An ancestor: a step back up, so it reads and behaves as a link. */
.crumb {
  padding: 2px 0;
  border: 0;
  background: none;
  color: var(--fg-3);
  font-size: 15px;
  font-weight: 550;
  cursor: pointer;
  transition: color var(--t-fast);
}

.crumb:hover {
  color: var(--fg);
  text-decoration: underline;
}

.crumb-sep {
  color: var(--fg-3);
  font-size: 15px;
  opacity: .6;
}

.page-body {
  padding: var(--sp-5);
  max-width: 1500px;
}

.page-body.narrow {
  max-width: 780px;
}

.section {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  margin-bottom: var(--sp-6);
}

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
}

/* Search box with a leading glyph, used in several views. */
.search {
  position: relative;
  flex: 1;
  max-width: 340px;
}

.search svg {
  position: absolute;
  left: 11px;
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  color: var(--fg-3);
  pointer-events: none;
}

.search input {
  padding-left: 34px;
  border-radius: var(--r-pill);
}

/* ------------------------------------------------------------ home view */

.hero {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-7) var(--sp-5);
  margin-bottom: var(--sp-6);
  text-align: center;
  background: var(--s1);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
}

.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(700px 320px at 50% -30%, rgba(124, 92, 255, .3), transparent 70%);
  pointer-events: none;
}

.hero h1 {
  position: relative;
  font-size: 32px;
}

.hero p {
  position: relative;
  max-width: 520px;
}

/* The quick-start tiles under the hero. */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--min, 210px), 1fr));
  gap: var(--sp-3);
}

.tile {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-4);
  background: var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--t-fast), transform var(--t-fast),
    background var(--t-fast);
}

.tile:hover {
  background: var(--s3);
  border-color: var(--border-2);
  transform: translateY(-2px);
}

.tile-icon {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: var(--r-md);
  background: var(--accent-soft);
  color: var(--accent-2);
}

.tile-icon svg {
  width: 20px;
  height: 20px;
}

.tile h3 {
  margin-bottom: 2px;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* Something that cannot be done right now, shown anyway so the reason can be.
   Dimmed rather than removed: a card that says what is missing is worth more
   than a gap where it would have been. */
.tile-off {
  opacity: 0.55;
  cursor: not-allowed;
}

.tile-off:hover {
  background: var(--s2);
  border-color: var(--border);
  transform: none;
}

/* The Stage's chosen file: big enough to recognise, small enough that what can
   be done with it is on the same screen. */
.stage-asset {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  align-items: flex-start;
  padding: var(--sp-4);
  background: var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}

.stage-preview {
  width: min(340px, 100%);
  max-height: 340px;
  object-fit: contain;
  border-radius: var(--r-md);
  background: var(--s1);
}

.stage-side {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 220px;
  flex: 1;
}

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--sp-3);
}

.stat {
  padding: var(--sp-4);
  background: var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}

.stat-value {
  font-size: 24px;
  font-weight: 650;
  letter-spacing: -.02em;
}

/* Inline result lines — a connection test, a saved-state note. Colour is the
   whole message here, so these are text rather than badges. */
.ok-text {
  color: var(--ok);
}

.err-text {
  color: var(--danger);
}

/* --------------------------------------------------------- viewing as */

/* Pinned to the bottom rather than the top: the top of every page is a header
   someone is reading, and the bottom is where the phone bar already lives, so
   this reads as part of the frame rather than as content. */
.viewas-bar {
  position: fixed;
  z-index: 60;
  right: var(--sp-4);
  bottom: var(--sp-4);
  left: auto;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  max-width: min(520px, calc(100vw - var(--sp-5)));
  padding: 10px var(--sp-3) 10px var(--sp-4);
  border: 1px solid var(--nsfw);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--nsfw) 18%, var(--s2));
  box-shadow: var(--shadow-2);
  font-size: 13px;
}

.viewas-bar svg {
  flex: none;
  width: 18px;
  height: 18px;
}

/* On a phone the bottom bar is already there, so this sits above it and spans
   the width — a floating pill would cover the navigation. */
@media (max-width: 760px) {
  .viewas-bar {
    right: var(--sp-3);
    left: var(--sp-3);
    bottom: calc(var(--rail-h, 64px) + var(--sp-3));
    max-width: none;
    border-radius: var(--r-md);
  }
}

/* A tail of someone else's log — ComfyUI's console, a training run. Fixed
   height and scrolling, because the interesting line is usually the last one
   and an unbounded block would push everything else off the page. */
.log-tail {
  margin: 12px 0 0;
  padding: 10px 12px;
  max-height: 260px;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--bg);
  color: var(--fg-2);
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}

/* --------------------------------------------------------- media grid */

/* Shared by the library and by any view that shows results. The tile size
   is a CSS variable so a zoom control can drive it. */
.media-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--tile, 200px), 1fr));
  gap: var(--sp-3);
}

.media {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1;
  padding: 0;
  overflow: hidden;
  background: var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  cursor: pointer;
  /* Holding a tile starts a selection, so the hold must not also raise the
     platform's own "save image" sheet or start selecting text. */
  user-select: none;
  -webkit-touch-callout: none;
  transition: border-color var(--t-fast), transform var(--t-fast),
    opacity var(--t-fast);
}

.media:hover {
  border-color: var(--accent-line);
  transform: translateY(-2px);
}

.media img,
.media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: opacity var(--t-med);
}

.media img.loading {
  opacity: 0;
}

/* Caption on hover only, so the grid stays a wall of images at rest. */
.media-info {
  position: absolute;
  inset: auto 0 0 0;
  padding: 20px 10px 8px;
  background: linear-gradient(transparent, rgba(0, 0, 0, .85));
  font-size: 11.5px;
  text-align: left;
  opacity: 0;
  transition: opacity var(--t-fast);
}

.media:hover .media-info,
.media:focus-visible .media-info {
  opacity: 1;
}

.media-tags {
  position: absolute;
  top: 6px;
  left: 6px;
  display: flex;
  gap: 4px;
}

/* Multi-select. The tick only appears once selecting has been turned on, so
   the grid at rest is still a wall of images. */
.media-pick {
  position: absolute;
  top: 6px;
  right: 6px;
  display: none;
  place-items: center;
  width: 22px;
  height: 22px;
  color: transparent;
  background: rgba(0, 0, 0, .5);
  border: 2px solid rgba(255, 255, 255, .7);
  border-radius: 50%;
}

.media-grid.picking .media-pick {
  display: grid;
}

/* The star sits in the same corner, and two badges fighting over it reads as
   a rendering bug. */
.media-grid.picking .media-star {
  display: none;
}

.media-pick svg {
  width: 13px;
  height: 13px;
}

.media.picked {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 2px var(--accent);
}

.media.picked .media-pick {
  color: #fff;
  background: var(--accent);
  border-color: var(--accent);
}

/* Dragging tiles onto a folder. The tile fades so it reads as picked up, and
   the label under the pointer says how many are coming with it. */
.media.dragging {
  opacity: .45;
}

body.dragging-media {
  cursor: grabbing;
  user-select: none;
}

.drag-ghost {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: 60vw;
  padding: 6px 11px;
  background: var(--accent);
  border-radius: var(--r-pill);
  color: #fff;
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-shadow: var(--shadow-3);
  /* Never the answer to elementFromPoint: the chip underneath is. */
  pointer-events: none;
}

.drag-ghost svg {
  width: 14px;
  height: 14px;
  flex: none;
}

/* Sticks to the bottom of the scroller while there is a selection to act on. */
.sel-bar {
  position: sticky;
  bottom: var(--sp-4);
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-4);
  padding: var(--sp-3);
  background: var(--s2);
  border: 1px solid var(--border-2);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
}

/* A list of choices in a modal, each its own button. */
.pick-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 46vh;
  overflow-y: auto;
}

.pick-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 9px 11px;
  background: var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  color: var(--fg);
  font-size: 13.5px;
  text-align: left;
  cursor: pointer;
}

.pick-row:hover:not(:disabled) {
  border-color: var(--accent-line);
}

.pick-row:disabled {
  color: var(--fg-3);
  cursor: not-allowed;
}

.pick-row.current {
  border-color: var(--accent-line);
}

/* Library picker ------------------------------------------------------- */

.pick-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--pick-tile, 150px), 1fr));
  gap: 8px;
  overflow-y: auto;
  flex: 1;
  min-height: 200px;
  padding: 2px;
}

/*
  A square tile built from padding rather than aspect-ratio, with the picture
  taken out of flow inside it. Both details matter: as an in-flow child the
  image's own height decided the tile's, which collapsed the grid into
  overlapping strips of different heights.
*/
.pick-cell {
  position: relative;
  height: 0;
  padding: 100% 0 0;
  border: 2px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--s1);
  cursor: pointer;
}

/* Contained, not cropped: these are pictures being chosen on their detail, and
   a square crop is exactly what hides the part that decides the answer. */
.pick-cell img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  transition: opacity var(--t-fast);
}

.pick-cell:hover {
  border-color: var(--accent-line);
}

.pick-cell.on {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 2px var(--accent);
}

/* The tick only appears once picked: an unchecked box on every tile turns the
   grid into a form, and the pictures are the point. */
.pick-tick {
  position: absolute;
  top: 5px;
  right: 5px;
  z-index: 1;
  display: none;
  padding: 3px;
  border-radius: var(--r-pill);
  background: var(--accent);
  color: #fff;
}

.pick-cell.on .pick-tick {
  display: inline-flex;
}

/* The file name, so two near-identical frames can still be told apart. */
.pick-name {
  position: absolute;
  inset: auto 0 0 0;
  z-index: 1;
  padding: 14px 6px 4px;
  background: linear-gradient(transparent, rgba(0, 0, 0, .85));
  font-size: 10.5px;
  color: var(--fg-2);
  text-align: left;
  opacity: 0;
  transition: opacity var(--t-fast);
}

.pick-cell:hover .pick-name,
.pick-cell.on .pick-name {
  opacity: 1;
}

.media-star {
  position: absolute;
  top: 6px;
  right: 6px;
  color: var(--warn);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .8));
}

.media-star svg {
  width: 16px;
  height: 16px;
  fill: var(--warn);
}

/* Marks a clip in a grid of stills. */
.media-play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.media-play span {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: rgba(0, 0, 0, .55);
  backdrop-filter: blur(3px);
}

.media-play svg {
  width: 18px;
  height: 18px;
  color: #fff;
}

/* ------------------------------------------------------------- folders */

.folder-strip {
  display: flex;
  gap: var(--sp-2);
  overflow-x: auto;
  padding-bottom: var(--sp-2);
}

.folder-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  flex: none;
  background: var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  color: var(--fg-2);
  font-size: 13px;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast),
    border-color var(--t-fast);
}

.folder-chip svg {
  width: 15px;
  height: 15px;
}

.folder-chip:hover {
  background: var(--s3);
  color: var(--fg);
}

.folder-chip.active {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--fg);
}

.folder-chip .count {
  color: var(--fg-3);
  font-size: 11.5px;
}

/* Under a drag. Bigger than a hover, because on a phone the finger is over
   the chip and this edge is the only part still visible. */
.folder-chip.drop-target {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  transform: scale(1.06);
}

.folder-chip.drop-target .count {
  color: rgba(255, 255, 255, .8);
}

/* Not a folder yet: dashed, so it does not read as one of the real ones. */
.folder-chip.new {
  border-style: dashed;
  color: var(--fg-3);
}

.folder-chip.new:hover {
  border-color: var(--accent-line);
  color: var(--fg);
}

/* --------------------------------------------------------------- uploads */

/* Between the folder strip and the grid, where the files are about to land. */
.upload-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
  padding: 10px var(--sp-3);
  background: var(--s2);
  border: 1px solid var(--border-2);
  border-radius: var(--r-lg);
}

.spinner.sm {
  width: 14px;
  height: 14px;
  border-width: 2px;
  flex: none;
}

/*
  Files dragged in from the desktop. The hint names the folder they will land
  in, which the drag itself cannot say — and it must not take pointer events,
  or it would swallow the drop it is advertising.
*/
.drop-files::after {
  content: attr(data-drop-hint);
  position: fixed;
  inset: var(--sp-3);
  z-index: 70;
  display: grid;
  place-items: center;
  padding: var(--sp-5);
  background: rgba(6, 7, 10, .72);
  border: 2px dashed var(--accent-line);
  border-radius: var(--r-xl);
  backdrop-filter: blur(2px);
  color: var(--fg);
  font-size: 16px;
  font-weight: 600;
  text-align: center;
  pointer-events: none;
  animation: fade var(--t-fast);
}

/* -------------------------------------------------------------- viewer */

.viewer {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: grid;
  grid-template-columns: 1fr minmax(280px, 340px);
  background: rgba(6, 7, 10, .93);
  backdrop-filter: blur(6px);
  animation: fade var(--t-med);
}

@keyframes fade {
  from {
    opacity: 0;
  }
}

.viewer-stage {
  display: grid;
  place-items: center;
  padding: var(--sp-5);
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  /* The stage owns every gesture over it: pinch zooms the picture rather than
     the page, and a drag pans instead of scrolling something behind it. */
  touch-action: none;
}

/* Zoom and pan are one transform on this wrapper. */
.viewer-media {
  display: grid;
  place-items: center;
  min-width: 0;
  min-height: 0;
  will-change: transform;
}

.viewer-media img,
.viewer-media video {
  max-width: 100%;
  max-height: calc(100vh - 2 * var(--sp-5));
  object-fit: contain;
  border-radius: var(--r-md);
  box-shadow: var(--shadow-3);
  -webkit-user-select: none;
  user-select: none;
}

.viewer.zoomed .viewer-media {
  cursor: grab;
}

/* ------------------------------------------------------- before and after */

/* The result sets the size of the box and the input is laid over it, clipped
   at the divider. Both are `contain`, so a before and an after that were not
   the same shape still line up rather than one being stretched to match. */
.cmp {
  position: relative;
  display: inline-block;
  max-width: 100%;
  line-height: 0;
  touch-action: none;
  cursor: ew-resize;
}

.cmp .cmp-after {
  display: block;
  max-width: 100%;
  max-height: calc(100vh - 2 * var(--sp-5));
  object-fit: contain;
}

/* Over the result, at exactly its size, and without the shadow every other
   picture in the viewer gets — here it would draw a line along the clip. */
.cmp .cmp-before {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: contain;
  box-shadow: none;
}

.cmp-line {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: #fff;
  box-shadow: 0 0 10px rgba(0, 0, 0, .6);
  pointer-events: none;
}

.cmp-grip {
  position: absolute;
  top: 50%;
  left: 50%;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  transform: translate(-50%, -50%);
  background: #fff;
  border-radius: 50%;
  color: #111;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .5);
}

.cmp-grip svg {
  width: 20px;
  height: 20px;
}

/* Which side is which. Without these the comparison is ambiguous the moment
   the two pictures are similar, which is exactly when it is being used. */
.cmp-tag {
  position: absolute;
  top: 8px;
  padding: 3px 9px;
  background: rgba(10, 12, 18, .72);
  border-radius: var(--r-pill);
  color: #fff;
  font-size: 11px;
  line-height: 1.6;
  letter-spacing: .02em;
  pointer-events: none;
}

.cmp-in {
  left: 8px;
}

.cmp-out {
  right: 8px;
}

.viewer-side {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-5);
  background: var(--s1);
  border-left: 1px solid var(--border);
  overflow-y: auto;
}

.viewer-meta {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/* A wide screen has the panel and its own close button; this one is for the
   phone layout, where the panel is folded down to two rows. */
.viewer-close {
  display: none;
  position: fixed;
  top: calc(var(--sp-3) + env(safe-area-inset-top));
  right: var(--sp-3);
  z-index: 81;
  width: 42px;
  height: 42px;
  padding: 0;
  border-radius: 50%;
  background: rgba(10, 12, 18, .72);
  backdrop-filter: blur(6px);
}

/* Label/value pairs for the metadata panel. */
.kv {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.kv dt {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--fg-3);
  text-transform: uppercase;
  letter-spacing: .04em;
}

.kv dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.prompt-box {
  padding: var(--sp-3);
  background: var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  font-size: 13px;
  line-height: 1.5;
  max-height: 220px;
  overflow-y: auto;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

/* --------------------------------------------------------------- table */

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.table th {
  padding: 8px 10px;
  text-align: left;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--fg-3);
  text-transform: uppercase;
  letter-spacing: .04em;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.table td {
  padding: 10px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

.table tr:last-child td {
  border-bottom: 0;
}

.table tbody tr:hover {
  background: var(--s2);
}

.table .right {
  text-align: right;
}

/* --------------------------------------------------------------- modal */

.modal {
  position: fixed;
  inset: 0;
  z-index: 85;
  display: grid;
  place-items: center;
  padding: var(--sp-4);
  background: rgba(6, 7, 10, .7);
  backdrop-filter: blur(4px);
  animation: fade var(--t-fast);
}

.modal-card {
  width: 100%;
  max-width: 440px;
  max-height: calc(100vh - 2 * var(--sp-5));
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-5);
  background: var(--s2);
  border: 1px solid var(--border-2);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-2);
}

/* A short list of things to pick between, each its own row: big enough for a
   thumb, with room for a line saying what the choice means. */
.choose-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.choose-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: flex-start;
  width: 100%;
  padding: var(--sp-3) var(--sp-4);
  text-align: left;
  color: var(--fg-1);
  background: var(--s3);
  border: 1px solid var(--border-2);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}

.choose-item:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
}

/*
  The library picker is a grid of photographs, so it takes the room it can get.
  It has to be stated after .modal-card and not with the picker's other rules:
  a variant's `max-width` only beats the 440px cap above by coming later, and
  the phone rules further down turn every dialog into a full-width sheet, which
  they can only do if they come later still.
*/
.pick-card {
  gap: var(--sp-3);
  max-width: min(1100px, 96vw);
  max-height: 90vh;
}

/*
  Only the grid gives way when the dialog runs out of height. In a column flex
  box, flex-shrink works on the vertical axis, so on a phone the folder chips
  and the search row were being squashed to a sliver instead.
*/
.pick-card > *:not(.pick-grid) {
  flex: none;
}

/* ---------------------------------------------------------------- sheet */

/* A list of destinations or actions, in a card that becomes a bottom sheet on
   a phone. Rows rather than a menu of small buttons, so every one is a target
   a thumb can hit without aiming. */
.sheet-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0 calc(-1 * var(--sp-2));
}

.sheet-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 13px var(--sp-2);
  border: 0;
  border-radius: var(--r-md);
  background: transparent;
  color: var(--fg);
  font-size: 15px;
  font-weight: 550;
  text-align: left;
  cursor: pointer;
}

.sheet-row:hover {
  background: var(--s3);
}

.sheet-row.current {
  background: var(--accent-soft);
}

.sheet-row svg {
  width: 20px;
  height: 20px;
  flex: none;
  color: var(--fg-3);
}

.sheet-row.current svg {
  color: var(--accent-2);
}

.sheet-row.danger,
.sheet-row.danger svg {
  color: var(--danger);
}

.sheet-row.danger:hover {
  background: var(--danger-soft);
}

/* ------------------------------------------------------------ responsive */

/*
  The rail becomes a bottom bar on a narrow screen. The mobile app is the real
  phone client, but the site should still be usable on a phone — and "usable"
  here means the whole app, not a subset: every section is reachable, nothing
  is clipped off the right edge, and nothing sits under the home indicator.
*/
@media (max-width: 860px) {
  .shell {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, 1fr) auto;
  }

  .rail {
    grid-row: 2;
    flex-direction: row;
    justify-content: space-between;
    gap: 0;
    padding: 0 var(--sp-1) env(safe-area-inset-bottom);
    border-right: 0;
    border-top: 1px solid var(--border);
    overflow: visible;
  }

  .rail-brand,
  .rail-sep,
  .desk-only {
    display: none;
  }

  .rail-items {
    flex-direction: row;
    flex: 1;
    width: auto;
    justify-content: space-between;
  }

  .rail-foot {
    flex: none;
    margin-top: 0;
    padding-top: 0;
  }

  /* Five across, each its own share of the width, so the bar never scrolls
     and every label stays readable. */
  .rail-item {
    flex: 1;
    width: auto;
    min-width: 0;
    min-height: 52px;
    padding: 8px 2px;
  }

  .rail-more {
    display: flex;
  }

  .narrow-only {
    display: inline-flex;
  }

  /* Its counterpart: a control that exists only because the wide layout has no
     tab bar to do the same job. */
  .wide-only {
    display: none;
  }

  /*
    The picture gets the screen. The panel below it is the actions and the way
    between images; the metadata behind Details, because a prompt is often
    longer than everything else on this screen put together.
  */
  .viewer {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, 1fr) auto;
  }

  .viewer-stage {
    padding: var(--sp-2);
  }

  .viewer-media img,
  .viewer-media video {
    max-height: 100%;
  }

  .viewer-side {
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-3) calc(var(--sp-3) + env(safe-area-inset-bottom));
    border-left: 0;
    border-top: 1px solid var(--border);
    max-height: 62vh;
  }

  .viewer-meta {
    display: none;
  }

  .viewer.info-open .viewer-meta {
    display: flex;
  }

  .viewer-close {
    display: grid;
    place-items: center;
  }

  .rail-item.active::before {
    left: 50%;
    top: 0;
    transform: translateX(-50%);
    width: 20px;
    height: 3px;
  }

  .page-body,
  .page-head {
    padding-left: var(--sp-4);
    padding-right: var(--sp-4);
  }

  .page-body {
    padding-bottom: var(--sp-6);
  }

  /*
    Heads carry a title and a variable number of controls. Wrapping them is
    what keeps a long folder name from squeezing the buttons into slivers — and
    a head that wraps to three rows has no business staying pinned to the top
    of a 390px screen, so it scrolls away with the content instead.
  */
  .page-head {
    position: static;
    flex-wrap: wrap;
    min-height: 56px;
    gap: var(--sp-2);
  }

  .page-head h1 {
    font-size: 18px;
  }

  .page-head .head-tools {
    flex-wrap: wrap;
  }

  /* Free to fill the row, but not squeezed down to a magnifying glass and one
     letter by whatever else is sharing it. Small enough a basis that it stays
     on the row instead of wrapping and claiming a whole one for itself. */
  .page-head .search {
    max-width: none;
    min-width: 108px;
  }

  .lib-head .spacer {
    display: none;
  }

  /* 16px is the threshold under which mobile Safari zooms the page in on
     focus, which then leaves the layout scrolled sideways. */
  input[type=text],
  input[type=password],
  input[type=number],
  input[type=search],
  select,
  textarea {
    font-size: 16px;
  }

  /* A 30px button is a fine mouse target and a poor thumb one. */
  .btn.sm {
    height: 34px;
    padding: 0 12px;
  }

  .btn.sm.icon-only {
    width: 34px;
  }

  .check input {
    width: 18px;
    height: 18px;
  }

  /* Two up rather than one enormous tile: a phone screen holds a grid of
     thumbnails perfectly well, and one image per row means endless scrolling. */
  .media-grid {
    grid-template-columns: repeat(auto-fill, minmax(min(var(--tile, 200px), 44vw), 1fr));
    gap: var(--sp-2);
  }

  /* Nothing hovers on a touchscreen, so a caption that only appears on hover
     is a caption nobody ever sees. */
  @media (hover: none) {
    .media-info {
      opacity: 1;
      background: linear-gradient(transparent, rgba(0, 0, 0, .78));
    }
  }

  .folder-strip {
    /* Kinetic scrolling that lands on a chip rather than between two. */
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
  }

  .folder-chip {
    scroll-snap-align: center;
    min-height: 38px;
  }

  /* A table of six columns cannot be made narrow, so it keeps its width and
     scrolls inside its own box rather than being clipped by the view. */
  .table-scroll .table {
    min-width: 540px;
  }

  /* Dialogs become bottom sheets: anchored to the thumb, full width, and tall
     enough to show a list without becoming a scroll box inside a scroll box. */
  .modal {
    align-items: end;
    padding: 0;
  }

  .modal-card {
    max-width: none;
    max-height: 90vh;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    border-bottom: 0;
    padding: var(--sp-4) var(--sp-4) calc(var(--sp-4) + env(safe-area-inset-bottom));
    animation: sheet-up var(--t-med);
  }

  /* Two full-width rows, so neither the confirm nor the cancel is a 60px
     target at the far edge of the screen. */
  .modal-actions {
    flex-direction: column-reverse;
  }

  .modal-actions .btn {
    width: 100%;
    height: 42px;
  }

  .sel-bar {
    bottom: var(--sp-2);
    gap: var(--sp-1);
  }

  .toasts {
    right: var(--sp-3);
    left: var(--sp-3);
    bottom: var(--sp-3);
  }

  .toast {
    max-width: none;
  }
}

@keyframes sheet-up {
  from {
    transform: translateY(14px);
  }
}

/*
  The library head carries more controls than any other page, and on a portrait
  phone they crush each other into an unreadable strip. Three rows instead:
  where you are, what you are looking for, then how it is shown. A landscape
  phone is wide enough for one row and short enough to need the height, so this
  stops well before the tablet range.
*/
@media (max-width: 640px) {
  .lib-head .crumbs,
  .lib-head .search,
  .lib-head .head-tools {
    flex: 1 1 100%;
  }

  .lib-head .search {
    order: 1;
  }

  .lib-head .head-tools {
    order: 2;
  }
}

/* Landscape on a phone: almost no height, so the sticky furniture has to give
   some of it back. */
@media (max-width: 900px) and (max-height: 480px) {
  .rail-item span {
    display: none;
  }

  .rail-item {
    min-height: 40px;
  }

  .page-head {
    min-height: 48px;
    padding-top: var(--sp-2);
    padding-bottom: var(--sp-2);
  }

  .modal-card {
    max-height: 96vh;
  }
}


/* ------------------------------------------------------------- quick edit */

/*
  Paint-and-say, over the picture itself. Full screen on purpose: the frame is
  the working surface, so everything else — the prompt, the brush, the result —
  is arranged around it rather than beside it.
*/
.qe {
  position: fixed;
  inset: 0;
  z-index: 86;
  display: grid;
  grid-template-rows: auto 1fr auto auto;
  background: rgba(6, 7, 10, .96);
  backdrop-filter: blur(6px);
  animation: fade var(--t-med);
}

.qe-head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--border);
}

.qe-stage {
  display: grid;
  place-items: center;
  min-width: 0;
  min-height: 0;
  padding: var(--sp-3);
  overflow: hidden;
}

/* Zoomed in, the frame is larger than the stage, so the stage becomes the
   window onto it. Left-aligned, or a scrolled view snaps back to centre. */
.qe-stage.zoomed {
  place-items: start;
  overflow: auto;
}

/* The picture and the canvas both fill this box, which is what keeps a stroke
   exactly under the finger. */
.qe-frame {
  position: relative;
  line-height: 0;
  transform-origin: top left;
}

.qe-frame img,
.qe-frame canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: var(--r-sm);
}

.qe-frame img {
  position: relative;
  -webkit-user-select: none;
  user-select: none;
}

.qe-frame canvas {
  opacity: .5;
  cursor: crosshair;
  /* This canvas owns every gesture over it: a drag paints instead of scrolling
     the page, and a pinch zooms the frame rather than the document. */
  touch-action: none;
}

/* The finished picture, over the original at exactly its size — so holding the
   compare button is a straight swap with nothing moving or resizing. Absolute
   against the rule above, which makes the first image the one that gives the
   frame its height. */
.qe-frame .qe-result {
  position: absolute;
  inset: 0;
}

/* The brush ring during a right-drag. Two-tone on both edges so it stays
   visible over a white sky and a black shadow alike. */
.qe-ring {
  position: fixed;
  z-index: 88;
  pointer-events: none;
  transform: translate(-50%, -50%);
  border: 1px solid rgb(255 255 255 / 90%);
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgb(0 0 0 / 55%), inset 0 0 0 1px rgb(0 0 0 / 55%);
}

/* Where the soft edge begins: inside this the mask is solid. */
.qe-ring-soft {
  position: absolute;
  border: 1px dashed rgb(255 255 255 / 60%);
  border-radius: 50%;
}

.qe-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  border-top: 1px solid var(--border);
}

/* Naming a thing instead of drawing round it. Its own row, under the brush
   tools: it is a different way to make the same selection, not a brush
   setting, and it is the one most people want first. */
.qe-find {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  border-top: 1px solid var(--border);
}

.qe-find.busy {
  opacity: 0.6;
}

.qe-name {
  flex: 1 1 200px;
  min-width: 140px;
  max-width: 340px;
}

.qe-foot {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
}

.qe-hint {
  margin: 0;
  max-width: 70ch;
}

/* The prompt bar: one pill holding the instruction and the button that acts on
   it, so what to type and what happens next are the same object. */
.qe-bar {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-2);
  padding: 6px 6px 6px var(--sp-3);
  background: var(--s2);
  border: 1px solid var(--border-2);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-3);
}

.qe-prompt {
  flex: 1;
  min-width: 0;
  padding: 8px 0;
  background: none;
  border: 0;
  color: var(--fg);
  font: inherit;
  line-height: 1.45;
  resize: none;
}

.qe-prompt:focus {
  outline: none;
}

.qe-go {
  flex: 0 0 auto;
}

.qe-status {
  padding: var(--sp-2) var(--sp-3);
  background: var(--s1);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.qe-preview {
  width: 84px;
  height: 84px;
  object-fit: cover;
  border-radius: var(--r-sm);
  background: var(--bg);
}

/* A phone has no room for the tool row and the picture both, so the tools
   become a scrolling strip and the picture keeps the height. */
@media (max-width: 640px) {
  .qe-tools {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }

  .qe-tools::-webkit-scrollbar {
    display: none;
  }

  .qe-tools .check span {
    white-space: nowrap;
  }

  .qe-find {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }

  .qe-find::-webkit-scrollbar {
    display: none;
  }

  .qe-foot {
    padding: var(--sp-2) var(--sp-3) var(--sp-3);
  }

  .qe-hint {
    display: none;
  }
}