/* heart.photos — the public gallery.
   Dark by default because photographs sit better on a dark ground; the light scheme follows the
   viewer's own setting rather than a toggle nobody would find. */
:root {
  --bg: #0b0b0d;
  --panel: #141418;
  --ink: #f2f2f4;
  --muted: #9a9aa6;
  --line: #24242c;
  --accent: #ff5470;
  --shadow: 0 12px 40px rgb(0 0 0 / 0.55);
  --gap: 10px;
  /* The line of text under every photograph on the wall. See .tile-name. */
  --cap-h: 24px;
}
@media (prefers-color-scheme: light) {
  :root {
    --bg: #fbfbfc; --panel: #fff; --ink: #16161a; --muted: #6b6b78;
    --line: #e6e6ec; --shadow: 0 12px 40px rgb(20 20 30 / 0.16);
  }
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  padding: 0 0 64px;
}

/* --- header ------------------------------------------------------------------------------ */
/* Name, search, account — left, middle, right.
   The outer columns take exactly what they hold and the middle takes the rest, with the field
   centred INSIDE it. Equal outer tracks centred the field on the page instead, which is the same
   thing only while the name is narrower than the space left over — and when it was not, the name
   overflowed its track and ran underneath the field. Centred in the room available cannot do that
   however long the name gets. */
/* The block that stays put. Whatever it reserves is taken off the photographs for the whole of
   the rest of the page, so it is kept to the height of the field and the row of words and not a
   pixel more: the padding that used to give the name room to breathe at the top of a page is
   room permanently stolen from the wall once the name never leaves. A hairline marks where it
   ends, because photographs scrolling under an edge with nothing drawn on it look like a bug. */
.topbar {
  position: sticky; top: 0; z-index: 40;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
  margin-bottom: 14px;
}
/* Anything scrolled to lands below the bar rather than under it — a tile tabbed to, above all. */
html { scroll-padding-top: var(--top-h, 96px); }

.top {
  padding: 9px var(--gap);
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: 18px;
}
/* The tagline sits BESIDE the name rather than under it. Under it, it was a second line the bar
   had to be tall enough for, for one sentence that is read once. Beside it, on the baseline, it
   costs nothing — and drops back underneath when the name and it will not fit on a line. */
.top-name {
  justify-self: start; min-width: 0;
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 12px;
}
/* Below this there is not room for three across, and a centred field would be a squeezed one.
   The name takes its line and the search takes the width. */
/* The field SHRINKS, it does not move.
   It used to drop to its own line under 900px, which reflows the whole header at a width somebody
   dragging a window crosses all the time — the name and the account jump, and the wall below jumps
   with them. min() lets it take 620px when there is 620px and whatever is left when there is not,
   all the way down, so the three stay on one line and only one of them changes size. */
@media (max-width: 640px) {
  .top { gap: 10px; }
  .search-go { width: 44px; }
  .search-box { padding-left: 10px; }
}
.brand {
  display: inline-block; color: var(--ink); text-decoration: none;
  font-size: clamp(19px, 2.6vw, 24px); font-weight: 400; letter-spacing: -0.02em;
  line-height: 1.25;
}
.brand span { color: var(--accent); }
.tagline { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.3; }
.tagline a { color: var(--accent); text-decoration: none; }
.tagline a:hover { text-decoration: underline; }

/* The words past the end of the first line. A library of any size has more of them than a line
   has room for, and the bar was wrapping to four and five rows — pushing the photographs, which
   are the point, off the bottom of the window. */
.tagbar-words { position: relative; }
.tag-chip[hidden] { display: none; }
.tag-more { display: inline-flex; align-items: center; gap: 5px; }
.tag-more.on { background: rgb(255 255 255 / .18); }
/* Down for "there is more under here", up for "and this is it". A count alone says how many are
   hidden without saying that pressing is what shows them. */
.tag-caret {
  width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent;
  border-top: 5px solid currentColor; transition: transform .15s ease;
}
.tag-more.up .tag-caret { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .tag-caret { transition: none; } }
.tag-more-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 40;
  max-width: min(560px, 92vw); max-height: 46vh; overflow-y: auto;
  display: flex; flex-wrap: wrap; gap: 6px;
  padding: 8px; border: 1px solid rgb(255 255 255 / .16); border-radius: 14px;
  background: rgb(18 18 22 / .97); box-shadow: 0 14px 38px rgb(0 0 0 / .55);
}
.tag-more-menu[hidden] { display: none; }

/* The wall's own settings, in the same shape as the viewer's. */
.wall-gear-wrap { position: relative; display: inline-flex; }
.wall-gear { display: inline-grid; place-items: center; padding: 5px 9px; }
.wall-menu { top: calc(100% + 6px); right: 0; min-width: 210px; }

/* --- searching the wall -------------------------------------------------------------------- */
.search {
  justify-self: center; width: min(620px, 100%); min-width: 0;
  /* One height for both halves, stated here rather than left to whichever of them happened to be
     taller: the field and the button have to line up top and bottom or they do not read as one
     box, and the button was a couple of pixels shorter than the thing it is attached to. */
  height: 38px;
  display: flex; align-items: stretch; gap: 0;
}
.search-box {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 6px;
  height: 100%; box-sizing: border-box;
  padding: 0 6px 0 14px; border: 1px solid var(--line);
  border-radius: 999px 0 0 999px; background: var(--panel);
  /* Above the button, so the field's own right border is the one that paints at the seam. It was
     underneath: the button's grey left border covered it, and the ring that lights on focus was
     a rectangle with one side missing — open on the right, where the two meet. */
  position: relative; z-index: 1;
  /* Several words asked for at once are wider than a phone: the field scrolls them rather than
     growing and pushing the name off the side. */
  overflow-x: auto; scrollbar-width: none;
}
.search-box::-webkit-scrollbar { display: none; }
/* The ring belongs to the FIELD and closes all the way round it: the box carries its own right
   border (the button's left edge sits beside it), so lighting the box lights a whole rectangle.
   The icon is a way to focus the field, not part of what is focused — it stays quiet. */
.search-box:focus-within { border-color: var(--accent); }
.search-go { margin-left: -1px; }   /* share the seam, so the two greys never double up */
.search-input {
  /* The BOX has no width floor — it shrinks with the header, which is the point — but inside it
     the input keeps a typing sliver, or a long expression squeezed it to zero width and there was
     nothing left to click to type into. The box scrolls; the sliver survives. */
  flex: 1 1 auto; min-width: 64px; font: inherit; font-size: 15px;
  border: 0; background: transparent; color: var(--ink); padding: 3px 0;
}
.search-input:focus { outline: none; }
.search-input::placeholder { color: var(--muted); }
.search-go {
  flex: none; display: grid; place-items: center; width: 60px; height: 100%;
  box-sizing: border-box;
  border: 1px solid var(--line); border-radius: 0 999px 999px 0;
  background: rgb(255 255 255 / .06); color: var(--muted); cursor: pointer;
}
/* No line of its own down the seam. The field draws that border, and when the field lights on
   focus the lit one is what shows through — rather than the button's grey covering it, which is
   what left the ring open on the right. Transparent rather than merely underneath: it does not
   depend on which of the two happens to paint last. */
.search-go { border-left-color: transparent; }
.search-go:hover { color: var(--ink); background: rgb(255 255 255 / .12); }
/* "This clears something" — the same gold he.art uses for Clear filters and its row ×s. */
.search-clear {
  flex: none; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 50%;
  display: inline-grid; place-items: center;
  background: transparent; color: var(--clear-accent, #ffb86b); cursor: pointer; line-height: 1;
}
.search-clear svg { width: 16px; height: 16px; }
.search-clear[hidden] { display: none; }
.search-clear:hover { background: rgb(255 255 255 / .14); }
/* A tag being searched for, as a word you can take back off. It reads as part of what is typed,
   because it IS part of what is being asked for. */
.search-chips { display: flex; align-items: center; gap: 6px; flex: none; }
/* The accent, so it is the same colour as the lit word in the bar it came from — and legible in
   both themes, which a fixed pale blue on a white field is not. */
.search-chip {
  display: inline-flex; align-items: center; gap: 4px; white-space: nowrap;
  padding: 2px 3px 2px 10px; border-radius: 999px; font-size: 13px; font-weight: 500;
  background: var(--accent); color: #fff; border: 1px solid var(--accent);
}
.search-chip-off {
  display: grid; place-items: center;
  width: 17px; height: 17px; padding: 0; border: 0; border-radius: 50%; cursor: pointer;
  background: rgb(255 255 255 / .25); color: #fff; font-size: 10px; line-height: 1;
}
.search-chip-off:hover { background: rgb(255 255 255 / .45); }
/* An excluded word reads as the opposite of an asked-for one, so it does not wear the accent. */
.search-chip.not { background: rgb(128 128 140 / .3); border-color: rgb(128 128 140 / .5); }
.search-chip.not .search-chip-word { text-decoration: line-through; }
/* The word half of a chip is a button: pressing it toggles the word's exclusion. */
.search-chip-word {
  border: 0; background: none; padding: 0; margin: 0; font: inherit; color: inherit;
  cursor: pointer; white-space: nowrap;
}
/* and / or between two chips — a button, because flipping one is how the expression is edited.
   Quiet lower-case words rather than pills: they are the grammar, not the content. */
.search-op {
  flex: none; border: 0; background: none; padding: 0 1px; font: inherit; font-size: 12px;
  color: var(--muted); cursor: pointer; text-transform: lowercase;
}
.search-op:hover { color: var(--accent); text-decoration: underline; }
/* "not" before the excluded words is a statement, not a control. */
.search-op.static { cursor: default; }
.search-op.static:hover { color: var(--muted); text-decoration: none; }
/* With more than one group the parentheses are drawn, not typed: a soft outline around each ANDed
   run, so where one alternative ends and the next begins can be seen without counting. */
.search-group {
  display: inline-flex; align-items: center; gap: 5px; flex: none;
  padding: 3px 5px; border: 1px dashed rgb(128 128 140 / .55); border-radius: 999px;
}
/* Filtered out by what is being typed. A class rather than a rebuild: the wall may be thousands
   of tiles, and every keystroke would otherwise throw all of them away. */
.tile.filtered { display: none; }
/* ...and a group with nothing left in it goes with them. Hidden member by member, the cell itself
   survived as an outlined box of the right size holding no photographs at all. */
.series-cell.filtered { display: none; }
.search-none { text-align: center; color: var(--muted); padding: 40px 0; }

/* --- who is looking ------------------------------------------------------------------------ */
.account { flex: 0 0 auto; margin: 0 0 0 auto; position: relative; }

/* --- the grid ---------------------------------------------------------------------------- */
/* Rows are justified: each row is scaled so it fills the width exactly, the way a contact sheet
   does. Widths come from flex-grow driven by aspect ratio, so a row of wide photos and a row of
   tall ones both end flush without any measuring in JavaScript. */
/* Edge to edge: on a wide screen a centred column left large empty margins either side of the
   photographs, which is the one thing a gallery should not spend space on. */
/* The same gap at the ends of a row as between two photographs in it. The wall ran edge to edge
   while the header and the words above it were already inset by it, so the first photograph of
   every row sat a hair further left than the name of the site — and the seam between two pictures
   was the only spacing on the page the window's own edge did not honour. */
.grid { display: flex; flex-wrap: wrap; gap: var(--gap); padding: 0 var(--gap); }
/* A SERIES: several photographs that are one ordered thing, so one thing on the wall. It takes a
   single place in the row solve — its own --r is its members' shapes added up — and a row can no
   more wrap through the middle of it than through the middle of a photograph. Members keep their
   own shapes inside it, so a tall one among wide ones still stands tall.
   A hairline of the accent around the group is the only mark it wears: the ORDER is the thing, and
   the order is visible in the arrangement itself. */
.series-cell {
  display: flex; gap: 4px; flex-grow: 1; min-width: 0;
  flex-basis: calc(var(--r, 3) * 240px);
  border-radius: 12px; padding: 3px; margin: -3px;
  outline: 1px solid rgb(255 84 112 / .35); outline-offset: -1px;
}
.series-cell .tile {
  flex-grow: var(--r, 1.5); flex-basis: calc(var(--r, 1.5) * 240px); min-width: 0;
  border-radius: 9px;
}
/* The row is solved from --r, and a cell that grew has to hand that growth on in the same
   proportions — growing every member equally would widen a tall one as much as a wide one and
   pull them all away from their own shapes. */
.grid > .series-cell { flex-grow: var(--r, 3); }
/* Being carried across the wall. The group collapses to the one photograph in hand — you took
   hold of a picture and the series came with it — so the rows it passes over re-solve around one
   item rather than around a block as wide as the whole series. It expands again on the drop. */
.series-cell.collapsed { flex-basis: 240px; flex-grow: 1; }
.series-cell .tile.series-hidden { display: none; }
.tile {
  position: relative; display: block; overflow: hidden;
  border-radius: 12px;
  /* The tile holds its place before its picture arrives, so the row never reflows — but the
     panel colour is white in light mode, which reads as a broken image rather than one on its
     way. A tone between the page and the panel says "loading" instead. */
  background: var(--line);
  flex-grow: 1; margin: 0; padding: 0; border: 0; cursor: zoom-in;
  /* --r is the aspect ratio; the row solver below turns it into a width. The PICTURE is one row
     high and the caption sits under it, so the tile is taller than the row by exactly the line of
     text — rather than the picture giving up a band of itself to make room. */
  height: calc(var(--row-h, 240px) + var(--cap-h));
  flex-basis: calc(var(--r) * var(--row-h, 240px));
  transition: transform .18s ease, box-shadow .18s ease;
}
/* The picture's own box: everything that sits ON the photograph — the turned-picture layout, the
   tall-version badge, the tag strip — is measured against this and not against the caption. */
.tile-pic { position: relative; overflow: hidden; height: var(--row-h, 240px); }
.tile:hover, .tile:focus-visible { transform: translateY(-2px); box-shadow: var(--shadow); outline: none; }
/* …but never while something is being carried: a tile lifting under the pointer as the
   carried one passes over it fights the slide it is in the middle of. */
.grid.rearranging .tile:hover { transform: none; box-shadow: none; }
/* And while something is being carried the browser must not hold the wall still on its own
   account. Dropping a picture into a later place takes its height out of everything above the
   anchor, and scroll anchoring compensates by scrolling — so a wheel notch during a drag could
   throw the page back to the top under the reader's hand. They are holding a photograph; the wall
   they are holding it over stays where they put it. */
.grid.rearranging { overflow-anchor: none; }
/* WHERE YOU ARE on this wall. One mark, drawn one way, for both of the things that mean it: the
   photograph you were just looking at, marked when you come back out of the viewer, and the tile
   the keyboard has moved to. They are the same statement, so they are the same ring — and when
   they are on the same tile, which is the ordinary case, that is one ring rather than two.
   Drawn INSIDE the picture. The wall runs edge to edge, so a ring hung outside the tile has its
   left half off the window on the first picture of every row and its right half off on the last:
   a mark that is cut in half at exactly the moments it is most needed.
   An overlay rather than an inset shadow on the tile itself — an inset shadow paints BEHIND the
   tile's own content, and the photograph covers it completely. */
.tile.current::after, .tile:focus-visible::after {
  content: ""; position: absolute; inset: 0; z-index: 4; border-radius: inherit;
  box-shadow: inset 0 0 0 3px var(--accent), inset 0 0 0 4px rgb(255 255 255 / .3);
  pointer-events: none;
}
.tile img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 0; transition: opacity .35s ease;
}
.tile img.on { opacity: 1; }

/* ---- the title under a photograph ---------------------------------------------------------------
   Under every photograph, always, searching or not. A caption belongs under a picture and not on
   it: over the photograph it would have to fight for legibility against whatever is in the bottom
   of the frame, and a scrim dark enough to win that fight takes the bottom of the frame away.
   One line, clipped: the wall is a contact sheet and the rows have to stay level, so a long title
   ends in an ellipsis rather than pushing its neighbours down. */
.tile-name {
  height: var(--cap-h); padding: 0 6px;      /* clear of the tile's rounded bottom corners */
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--muted);
  white-space: nowrap; overflow: hidden;
  /* The page's own colour, not the tile's: the photograph is the card and the title is a label
     under it, the way a print is captioned on the wall beside it rather than across its foot. */
  background: var(--bg);
  user-select: none; cursor: inherit;
}
.tile-name-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* The tag that explains a match the title cannot: the word was found HERE. Set apart from the
   title so it does not read as part of the name. */
.tile-name-why {
  flex: none; max-width: 45%; overflow: hidden; text-overflow: ellipsis;
  padding: 0 6px; border-radius: 999px; font-size: 11px;
  background: var(--line); color: var(--muted);
}
/* Being typed in. The line under the picture IS where the title is written, so the field opens
   there rather than up in the tag strip: what you are changing and the box you change it in are
   the same place on the tile. The line gives up its padding to the box so the text sits where it
   sat a moment ago. */
.tile-name.editing { padding: 0 3px; overflow: visible; }
.tile-name-form { flex: 1 1 auto; min-width: 0; display: flex; }
.tile-title-input {
  flex: 1 1 auto; min-width: 0; font: inherit; font-weight: 600; color: var(--ink);
  height: calc(var(--cap-h) - 4px); padding: 0 5px; border-radius: 5px;
  border: 1px solid var(--line); background: var(--panel);
}
.tile-title-input:focus { outline: none; border-color: var(--accent); }
.tile-title-input::placeholder { color: var(--muted); }

/* The letters that matched. Marked rather than merely coloured: this is the browser's own idea of
   "found", and it survives a reader who cannot tell the two colours apart. */
.tile-name mark.hit {
  /* The tint says WHERE, and the ink says it is worth reading: a caption is set quiet on purpose,
     and letters merely tinted at that weight are easy to miss on a wall of six. */
  background: rgb(255 84 112 / .28); color: var(--ink);
  border-radius: 3px; padding: 0 1px;
}
.tile-tags mark.hit { background: rgb(255 84 112 / .55); color: #fff; border-radius: 3px; padding: 0 1px; }
/* The last row would otherwise stretch a lone photo across the whole width. */
.grid::after { content: ""; flex-grow: 999999; }

/* ---- the words along the bottom of a photograph ------------------------------------------------
   Shown only when asked for (see "Show tags"), and drawn ON the picture rather than under it: the
   justified row solve gives every tile the same height, so a strip that took its own space would
   crop a band off every photograph on the wall whether or not it had anything to say. */
.tile { user-select: none; }             /* it was a <button>, which never selected its own text */
.tile-tags {
  position: absolute; left: 0; right: 0; bottom: var(--cap-h); z-index: 3;
  display: none; align-items: center; gap: 6px;
  padding: 6px 8px; font-size: 12px; line-height: 1.3; text-align: left;
  color: #fff; user-select: text;
  /* A scrim rather than a bar: the bottom of the photograph still shows through it. */
  background: linear-gradient(to top, rgba(0,0,0,.72), rgba(0,0,0,.45) 60%, rgba(0,0,0,0));
}
.grid.tags-on .tile-tags { display: flex; }
.grid.tags-on .tile-tags.bare { display: none; }   /* nothing to say, and no pencil to offer */
.tile-tag-words { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tile-tag-words.none { opacity: .6; font-style: italic; }
.tile-tag-edit {
  flex: none; order: -1;   /* before the words: the way to change them comes first */
  display: inline-grid; place-items: center; width: 22px; height: 22px;
  padding: 0; border: 0; border-radius: 6px; cursor: pointer;
  background: rgba(255,255,255,.14); color: #fff;
}
.tile-tag-edit:hover { background: rgba(255,255,255,.3); }
.tile-tag-edit:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.tile-tag-form { flex: 1 1 auto; min-width: 0; display: flex; }
.tile-tag-input {
  flex: 1 1 auto; min-width: 0; font: inherit; color: #fff;
  padding: 3px 6px; border-radius: 6px;
  border: 1px solid rgba(255,255,255,.45); background: rgba(0,0,0,.55);
}
.tile-tag-input:focus { outline: none; border-color: var(--accent); }
.tile-tag-input::placeholder { color: rgba(255,255,255,.5); }
/* A tile being carried, or one being chosen, is not a tile being written on. */
.grid.rearranging .tile-tags, .grid.picking .tile-tags { display: none; }

/* On a phone a justified row holds one photo anyway, and a fixed row height then crops every
   portrait into a landscape strip. One per row at its own shape shows the photograph instead. */
@media (max-width: 700px) {
  .tile { height: auto; flex: 1 1 100%; }
  .tile-pic { height: auto; aspect-ratio: var(--r); }
  .grid::after { content: none; }
}

.status { text-align: center; color: var(--muted); padding: 28px 0; min-height: 1.5em; font-size: 14px; }
.empty { max-width: 480px; margin: 60px auto; text-align: center; color: var(--muted); }
.empty strong { display: block; color: var(--ink); font-size: 19px; margin-bottom: 6px; }

/* What He.art made of the picture. Set in the panel's own voice rather than the owner's: the
   summary reads as prose, and both are plainly labelled so nobody mistakes them for a caption
   somebody typed. */
.ai-title { font-size: 14px; color: #e9e9f0; }
.ai-summary { font-size: 13.5px; line-height: 1.55; color: #c9c9d4; }
.ai-waiting { color: rgb(255 255 255 / .4); font-style: italic; }
#ai-again-row { margin-top: 10px; }
#ai-again-row[hidden] { display: none; }

/* --- tags ---------------------------------------------------------------------------------- */
/* Every word the wall carries, along the top of it. A chip is a filter and says how many
   photographs it would leave; the one in force says so by being filled in rather than outlined. */
.tagbar {
  display: flex; align-items: flex-start; gap: 12px;
  margin: 0; padding: 0 var(--gap) 8px;
}
.tagbar[hidden] { display: none; }
.tagbar-words { display: flex; flex-wrap: wrap; gap: 7px; flex: 1 1 auto; min-width: 0; }
.tagbar-acts { display: flex; gap: 7px; flex: 0 0 auto; }
.tagbar-acts[hidden] { display: none; }
.tag-chip, .tagbar-acts .ctl-text {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 13px; height: auto; border-radius: 999px;
  border: 1px solid var(--line); background: var(--panel);
  color: var(--ink); font: inherit; font-size: 13px; cursor: pointer;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
/* display above beats the hidden attribute's UA rule, and a control hidden in script would stay
   on screen — which is how Select and Tags… turned up for a signed-out visitor. */
.tagbar-acts .ctl-text[hidden] { display: none; }
/* And | Or | Not sit together as one instrument, a step apart from the wall's own controls. */
.join-modes { display: inline-flex; gap: 4px; margin-right: 10px; }
.join-modes .mode-chip { padding: 5px 11px; }
/* A word being EXCLUDED. Not the accent — that is the colour of what you are looking at, and this
   is the opposite: struck through, in the page's own quieter ink, with the minus said out loud so
   it does not rest on the strike alone. */
.tag-chip.not {
  border-color: var(--line); background: rgb(128 128 140 / .18); color: var(--muted);
}
/* The strike goes on the WORD, not on the chip: a chip-wide line-through is drawn across the minus
   and the count too, and nothing inside can opt out of an ancestor's decoration. */
.tag-chip.not .tag-name { text-decoration: line-through; }
/* A plain hyphen, not U+2212 MINUS SIGN: the wall's typeface has no glyph for that one and drew a
   tofu box. It is also the character the search field uses in -tag:women, so the two agree. */
.tag-chip.not::before { content: "-"; margin-right: 1px; font-weight: 600; }
.tag-chip.not:hover { color: var(--ink); border-color: var(--muted); }
.tag-chip:hover, .tagbar-acts .ctl-text:hover { border-color: var(--accent); color: var(--accent); background: var(--panel); }
.tag-chip .tag-n { color: var(--muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.tag-chip.on, .tagbar-acts .ctl-text.on {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
.tag-chip.on .tag-n { color: rgb(255 255 255 / .8); }
.tag-chip:focus-visible, .tagbar-acts .ctl-text:focus-visible {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px rgb(255 84 112 / .35); color: var(--accent);
}

/* Choosing photographs. The tiles stop opening and start being picked, and the bar that does the
   picking sits over the wall so the wall stays in view while it is used. */
.grid.picking .tile { cursor: copy; }
.grid.picking .tile:hover { transform: none; box-shadow: none; }
.tile.picked img { opacity: .55; }
.tile.picked::after {
  content: '✓'; position: absolute; inset: 0 0 var(--cap-h) 0; z-index: 2;
  display: grid; place-items: center;
  background: rgb(255 77 109 / .28); box-shadow: inset 0 0 0 3px var(--accent);
  color: #fff; font-size: 34px; text-shadow: 0 1px 6px rgb(0 0 0 / .6);
  pointer-events: none;
}
/* Under the button that opens it, not along the foot of the window. It was pinned bottom-centre,
   a long way from the press that summoned it and from the photographs being chosen — far enough
   that a reader could turn Multiselect on and not notice anything had happened. */
.selbar {
  margin: 0 var(--gap) 8px auto; width: max-content;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  max-width: min(94vw, 860px); padding: 10px 14px; border-radius: 999px;
  border: 1px solid rgb(255 255 255 / .16); background: rgb(18 18 22 / .96);
  box-shadow: 0 6px 18px rgb(0 0 0 / .35);
}
@supports (backdrop-filter: blur(1px)) { .selbar { background: rgb(18 18 22 / .84); backdrop-filter: blur(14px); } }
.selbar[hidden] { display: none; }
.selbar-count { font-size: 13px; color: #fff; white-space: nowrap; font-variant-numeric: tabular-nums; }
.selbar-gap { flex: 1 1 auto; min-width: 0; }
.selbar-say { font-size: 12.5px; color: rgb(255 255 255 / .55); white-space: nowrap; }
/* Fields inside the dark surfaces (the bar over the wall, the panel, the editor dialog). */
.selbar-input, .ptag-input, .tagedit-search, .tagedit-name {
  border-radius: 999px; border: 1px solid rgb(255 255 255 / .18);
  background: rgb(255 255 255 / .06); color: #fff;
  font: inherit; font-size: 13px; padding: 6px 13px;
}
.selbar-input { width: 150px; }
.selbar-input::placeholder, .ptag-input::placeholder, .tagedit-search::placeholder { color: rgb(255 255 255 / .38); }
.ctl-text.sel-go { background: var(--accent); border-color: var(--accent); color: #fff; }
.ctl-text.sel-go:hover { background: var(--accent); filter: brightness(1.12); }
.ctl-text[disabled] { opacity: .4; cursor: default; pointer-events: none; }

/* This photograph's own words, in the panel. Its owner can take one off; anyone can follow one
   back to the rest of the wall filed under it. */
.ptags { display: flex; flex-wrap: wrap; gap: 6px; }
.ptags:empty { display: none; }
.ptag {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 5px 3px 11px; border-radius: 999px;
  border: 1px solid rgb(255 255 255 / .16); background: rgb(255 255 255 / .06);
  font-size: 12.5px; color: #e9e9f0;
}
.ptag.plain { padding: 3px 11px; }
.ptag-go { background: none; border: 0; padding: 0; color: inherit; font: inherit; cursor: pointer; }
.ptag-go:hover { color: #fff; text-decoration: underline; }
.ptag-off {
  display: grid; place-items: center; width: 17px; height: 17px; padding: 0;
  border: 0; border-radius: 999px; background: rgb(255 255 255 / .1);
  color: rgb(255 255 255 / .7); font-size: 12px; line-height: 1; cursor: pointer;
}
.ptag-off:hover { background: rgb(255 77 109 / .5); color: #fff; }
.ptag-add { margin-top: 8px; }
.ptag-add[hidden] { display: none; }
.ptag-input { width: 100%; box-sizing: border-box; }
.ptags-none { color: rgb(255 255 255 / .4); font-size: 12.5px; }

/* The words themselves. A search that narrows as it is typed, because the whole difficulty with a
   vocabulary that has grown is finding the one you meant. */
.tagedit { max-width: min(560px, 94vw); }
.tagedit-search { width: 100%; box-sizing: border-box; margin: 4px 0 12px; }
.tagedit-list { max-height: min(52vh, 460px); overflow: auto; }
.tagedit-row {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 2px; border-bottom: 1px solid rgb(255 255 255 / .07);
}
.tagedit-row:last-child { border-bottom: 0; }
.tagedit-name { flex: 1 1 auto; min-width: 0; }
.tagedit-n {
  flex: 0 0 auto; min-width: 8ch; text-align: right;
  color: rgb(255 255 255 / .45); font-size: 12px; font-variant-numeric: tabular-nums;
}
.tagedit-row .ctl-text { flex: 0 0 auto; }
.tagedit-del:hover { background: rgb(255 77 109 / .3); color: #fff; }
.tagedit-empty { padding: 18px 2px; color: rgb(255 255 255 / .45); font-size: 13px; }
@media (max-width: 700px) {
  .tagbar { flex-direction: column; gap: 8px; }
  .selbar { left: 8px; right: 8px; transform: none; max-width: none; border-radius: 16px; }
  .selbar-input { width: 110px; }
}

/* --- viewer ------------------------------------------------------------------------------ */
.viewer {
  border: 0; padding: 0; margin: 0; width: 100%; height: 100%; max-width: 100%; max-height: 100%;
  background: rgb(6 6 8 / 0.96); color: #fff;
  display: none; align-items: center; justify-content: center;
}
.viewer[open] { display: flex; }
.viewer::backdrop { background: rgb(6 6 8 / 0.9); }
.stage {
  /* No gap. The row carries its own six pixels on every side, and that is the whole of the space
     around it — see .stage > .controls. A gap here as well made the space ABOVE the row fourteen
     pixels and the space below it six, which is not a row sitting between two things. */
  margin: 0; display: flex; flex-direction: column; align-items: center; gap: 0;
  width: 100%; max-width: 100%; height: 100%; box-sizing: border-box;
  /* Nothing of its own. The photograph runs to the glass at its end, and the row carries its
     own margin at the other. */
  padding: 0;
}
/* The box the chosen component draws in. It has a definite height, which is what lets a
   canvas-style component fit the photo to the room that is actually left over. */
.pane {
  flex: 1 1 auto; min-height: 0; width: 100%;
  display: flex; align-items: center; justify-content: center;
}
/* One row under the thumbnails, holding everything you reach for with a picture in front of you:
   leave it, move between them, turn a paired one over, size it, show or hide the thumbnails, fill
   the screen, open its settings. Everything that DESCRIBES the file is in the sidebar.

   It wraps only as a last resort. There is one growable thing in it (the zoom slider) and one
   growable gap, so on any ordinary window the row absorbs the spare width instead of using it —
   and where even the natural widths will not fit, wrapping costs the photograph a little height
   rather than pushing a control off the end of the line. */
/* ONE group, centred. The row was two: everything about the photograph pushed to the left edge
   and everything about the window pushed to the right, with a stretching cell of nothing between
   them. That is the shape of a title bar, and this is not one — the photograph is centred and so
   are the thumbnails, and controls pinned to the two far corners of a wide screen ask the eye to
   travel the width of the glass and back to use two of them in a row.
   Centred, they sit under the middle of the picture they are about, and the distance from any one
   to any other is as short as the row can make it. */
.controls {
  display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap;
  width: 100%; padding: 0 10px; box-sizing: border-box;
}
/* What used to hold the two halves apart. Nothing does now, so it takes no room — kept as an
   element rather than removed because it is one line of CSS against a change to the markup and to
   every test that counts what is in the row. */
.ctl-spacer { display: none; }
.ctl {
  /* 32, not 34. The row carries one more control than it was designed around — the settings gear
     — and with the panel open beside it there was no width left for it. Two pixels off each
     round button is a thing nobody sees; a control coming out of the row is a thing everybody
     does. */
  flex: 0 0 auto; width: 32px; height: 32px; padding: 0;
  background: rgb(255 255 255 / .08); color: #fff; border: 0;
  border-radius: 999px; line-height: 1; cursor: pointer;
  display: grid; place-items: center; font-size: 20px;
  transition: background .15s ease;
}
.ctl:hover { background: rgb(255 255 255 / .2); }
/* A ring, not a fill. Closing the panel with the I key hands focus back to the button that opens
   it — which is right — and a focus that paints the same background as `.on` then said the panel
   was still open. Being able to see where the keyboard is must not look like being switched on. */
.ctl:focus-visible { outline: none; }
.ctl.close { font-size: 15px; }
.sides { flex: 0 0 auto; }
/* It used to hold a place on a photograph with no tall half, hidden but measured, so nothing moved
   as the arrows walked from a paired photograph to an unpaired one. There is nothing to reserve
   now: the switch says what you PREFER, which is a thing you have asked for wherever you are, so
   it is on screen and live on every photograph. */
/* Which photograph this is, between the two arrows that change it.
   The width is RESERVED, so the arrows do not shuffle sideways as the number climbs — and it is
   reserved by holding the widest string this gallery can produce in a hidden twin rather than by
   a number of ch. 7ch was a guess, and a guess is wrong in both directions: " of " is three
   glyphs narrower than three digits, so it over-reserved for "1 of 58" and still came up short
   for "10 of 58", which is the shift you could see. A twin cannot be wrong — it is the string. */
.ctl-count {
  flex: 0 0 auto; padding: 0 2px; text-align: center;
  display: inline-grid; grid-template-areas: "n"; align-items: center; justify-items: center;
  color: rgb(255 255 255 / .62); font-size: 12.5px; font-variant-numeric: tabular-nums;
  white-space: nowrap; user-select: none;
}
.ctl-count-wide, .ctl-count-now { grid-area: n; }
/* Present for its width and for nothing else. visibility rather than display, which would take
   its width with it, and rather than opacity, which would leave it selectable. */
.ctl-count-wide { visibility: hidden; }
.ctl-count:has(.ctl-count-now:empty) { display: none; }
@media (max-width: 700px) {
  .ctl { width: 30px; height: 30px; font-size: 18px; }
  .controls { gap: 7px; }
}
@media (prefers-reduced-motion: reduce) {
  .tile, .tile img { transition: none; }
}

/* Marks a wallpaper that also exists tall. Deliberately quiet: it is a note about the photo,
   not a control competing with it. */
/* Off unless asked for: see "Show paired icon". A class on the wall rather than on each tile, so
   turning it on and off is one write and never a rebuild.

   Everything about the badge EXCEPT whether it is there. Its own display lived down here, after
   the rule that hides it and at the same weight, so the later declaration won and the badge was
   on every wallpaper whatever the switch said. There is exactly one place that decides now, and
   it is the line below this block. */
.pair-badge {
  position: absolute; right: 8px; bottom: calc(8px + var(--cap-h));
  min-width: 22px; height: 22px; padding: 0 6px;
  place-items: center;
  border-radius: 6px; font-size: 13px; line-height: 1;
  background: rgb(0 0 0 / 0.55); color: #fff; backdrop-filter: blur(4px);
  pointer-events: none;
}
.pair-badge { display: none; }
.grid.pairs-shown .pair-badge { display: grid; }
.pair-switch { display: inline-flex; gap: 0; margin-top: 6px; border: 1px solid rgb(255 255 255 / 0.25); border-radius: 999px; overflow: hidden; }
.pair-switch button {
  background: transparent; color: #cfcfd8; border: 0; cursor: pointer;
  font: inherit; font-size: 12px; padding: 4px 14px;
}
.pair-switch button.on { background: rgb(255 255 255 / 0.16); color: #fff; }
.pair-switch button:hover { color: #fff; }

/* While a photo is open, the page behind it must not scroll. The padding replaces the width the
   scrollbar was occupying, so the grid does not jump sideways as the viewer opens and closes. */
html.viewer-open { overflow: hidden; }
html.viewer-open body { padding-right: var(--sbw, 0px); }

/* The filmstrip inside the viewer. It sits under the photo, scrolls on its own, and never grows
   tall enough to compete with the photograph. */
/* Along the TOP, centred, and big enough to recognise a photograph in. It only fills the width
   when there are more than fit, which is when centring stops mattering anyway. */
.viewer {
  --strip-h: 176px;
  /* Named because a turned thumbnail is laid out in the same box with its two axes swapped, and
     two numbers that must add up cannot be written down twice. */
  --strip-w: 256px;
  /* THE seam. Every place two things meet in here is this wide: between one thumbnail and the
     next, between the thumbnails and the photograph, between the row of controls and either of
     them. It was three numbers — a hairline between thumbnails, ten pixels of stage gap, six of
     row padding — so the viewer had three different ideas about how close two things sit, and the
     photograph was the one paying for the largest of them. */
  --seam: 3px;
}
@media (max-width: 700px) { .viewer { --strip-h: 120px; --strip-w: 176px; } }
.strip {
  /* Anchored to the viewer, not to the viewport. The viewer is the dialog, which fills the window
     windowed and fills the screen in fullscreen — one box, the same in both, whichever element the
     browser ended up making fullscreen. Fixed positioning resolves against whatever the browser
     decides the containing block is, and in fullscreen that is not always what it is here. */
  /* Along the FOOT. It was along the top, above the controls, which put the thing you glance
     at between you and the row you press — and left the photograph hanging below both. The
     picture now starts where the window does. */
  position: absolute; left: 0; right: 0; bottom: 0;
  /* Stated, not derived from the content: a row whose height is whatever its tallest child happens
     to measure can be a scrollbar short of a thumbnail for one layout, and the thumbnails spend
     the rest of the session pushed down inside it with their tops cut off. */
  height: var(--strip-h);
  box-sizing: border-box;
  /* A hairline between the photographs — enough that two similar ones do not read as one picture,
     not so much that the black becomes part of the design. No padding at the ends: the row still
     runs the full width of the window. */
  display: flex; gap: var(--seam); padding: 0;
  justify-content: safe center;
  /* Pinned to the top of the row, so nothing can push the thumbnails down inside it. */
  align-items: flex-start;
  /* Scrollable, and with nothing drawn to say so. The bar was a fourteen-pixel band of chrome
     under every thumbnail, reserved out of the photograph's height whether or not the row was
     long enough to need it — and the row already says where it is with an arrow at each end. The
     overflow stays AUTO rather than hidden: hidden throws away a smooth programmatic scroll, and
     the arrows scroll the row that way. */
  overflow-x: auto; overflow-y: hidden;
  background: linear-gradient(to bottom, rgb(0 0 0 / .78), rgb(0 0 0 / 0));
  scrollbar-width: none;
}
.strip::-webkit-scrollbar { display: none; }
.strip:empty { display: none; }
.strip-thumb {
  position: relative;
  flex: 0 0 auto; width: var(--strip-w); height: var(--strip-h); padding: 0;
  /* Square, and marked from the INSIDE: a border would push its neighbours apart by its own
     width, which is the gap this row is meant not to have. */
  border: 0; border-radius: 0; background: #1a1a20;
  cursor: pointer; overflow: hidden; transition: box-shadow .15s ease;
}
.strip-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The mark goes on an overlay, not on the button: an inset shadow paints BEHIND the button's
   own content, so the photograph covered it completely. */
.strip-thumb::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  box-shadow: inset 0 0 0 0 transparent; transition: box-shadow .15s ease;
}
.strip-thumb:hover::after { box-shadow: inset 0 0 0 2px rgb(255 255 255 / .7); }
.strip-thumb:focus-visible::after { box-shadow: inset 0 0 0 1px #fff; }
/* One pixel, not two. It is a mark on a photograph, and two pixels of red around a small picture
   is a frame the eye reads before the picture inside it. */
.strip-thumb.on::after { box-shadow: inset 0 0 0 1px var(--accent); }
/* A series in the row. It is not a box of its own here — the row is flat, and a series is simply
   a run of neighbours — but the rules for dragging one are the wall's, and a rule you cannot see
   the edge of is a rule that reads as the row misbehaving. A hairline along the top and bottom of
   its members says where the group ends, quietly enough not to compete with the photographs. */
.strip-thumb[data-series]::before {
  content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none;
  border-top: 2px solid rgb(255 84 112 / .55);
  border-bottom: 2px solid rgb(255 84 112 / .55);
}
.strip-thumb:focus-visible { outline: none; }
/* Reading IN from the edge of the window: the thumbnails, then the row, then the photograph —
   at whichever end the thumbnails are. The row belongs beside the picture because it is what you
   press while looking at it; the thumbnails belong at the edge because they are the thing you
   scroll along, and a row of buttons between the glass and the strip put a hand reaching for the
   strip across the buttons on its way.
   And the photograph takes the far end ENTIRELY. Eight pixels of dialog between a picture and the
   edge of the screen is eight pixels the picture is not using and a hairline of not-quite-black
   that reads as a rendering seam. */
.stage > #pane { order: 1; }
.stage > .controls { order: 2; }
.viewer.strip-top .stage > .controls { order: 1; }
.viewer.strip-top .stage > #pane { order: 2; }
/* The row's own space, on every side: the same seam as everything else. */
.stage > .controls { padding-top: var(--seam); padding-bottom: var(--seam); }
/* Room for the strip, at whichever end it is, so a tall photograph is never behind it.
   The seam belongs to whatever is NEXT to the thumbnails, and which thing that is depends on
   where the row went. With the row in the flow it is the row, and it carries the seam in its own
   padding — so the stage holds back the thumbnails' height and nothing more, or the two would add
   up and the row would sit twice as far from the thumbnails as the photograph does from the row.
   Overlaid, the row is not in the flow at all and the photograph runs down to the thumbnails, so
   the stage holds back the seam as well. */
.viewer[open]:not(.no-strip) .stage { padding-bottom: var(--strip-h); }
.viewer.overlay-bar[open]:not(.no-strip) .stage,
.viewer.no-bar[open]:not(.no-strip) .stage {
  padding-bottom: calc(var(--strip-h) + var(--seam));
}
.viewer.strip-top[open]:not(.no-strip) .stage {
  padding-bottom: 0;
  padding-top: var(--strip-h);
}
.viewer.overlay-bar.strip-top[open]:not(.no-strip) .stage,
.viewer.no-bar.strip-top[open]:not(.no-strip) .stage {
  padding-bottom: 0;
  padding-top: calc(var(--strip-h) + var(--seam));
}
.viewer.strip-top .strip { top: 0; bottom: auto; }
/* The paging arrows are ON the thumbnails, so they go where the thumbnails go. They are 58 wide
   and a whole row tall and they sit over whatever is behind them — parked at the wrong end they
   covered the corner the info button is in, and the press never reached it. */
.viewer.strip-top .strip-page { top: 0; bottom: auto; }
.viewer:not(.strip-top) .strip-page { bottom: 0; top: auto; }
/* No thumbnails, nothing to page through. */
.viewer.no-strip .strip-page { display: none; }
/* The panel begins past the thumbnails AND past the row, so it is level with the photograph
   rather than with the buttons — which run the full width and would be behind it. */
.viewer.strip-top[open]:not(.no-strip) .sidebar {
  top: calc(var(--ctl-h, 48px) + var(--strip-h)); bottom: 0;
}
.orig-line { color: #b9b9c4; font-size: 12px; }
.orig-line a { color: #fff; }
@media (max-width: 700px) {
  /* The strip's room is reserved above, at whichever end it is — repeating it here without the
     strip-top case would have put a phone's picture behind its own thumbnails. */
  .close { top: 12px; right: 12px; }
}

/* --- a photograph somebody has turned -------------------------------------------------------- */
/* A quarter turn is a fact about the picture, so it has to be true everywhere the picture is
   drawn — the grid and the filmstrip as much as the big view. In both places the BOX is already
   the turned shape (the tile's aspect ratio comes from the displayed size, and a thumbnail's box
   is fixed), so what is left is to turn the picture inside it.

   Half a turn is free: the box does not change shape, so the picture only has to spin.
   A quarter turn needs the picture laid out with its width and height swapped, which is what the
   container units below do. Without them the fallback turns the cover-crop and grows it enough to
   still fill the box: it crops harder than it needs to, and it is never on its side. */
.tile[data-rot="90"] img,
.tile[data-rot="270"] img {
  transform: rotate(var(--rot, 0deg)) scale(max(var(--r, 1), calc(1 / var(--r, 1))));
}
.tile[data-rot="180"] img { transform: rotate(180deg); }
@supports (width: 1cqh) {
  .tile[data-rot="90"] .tile-pic, .tile[data-rot="270"] .tile-pic { container-type: size; }
  .tile[data-rot="90"] img,
  .tile[data-rot="270"] img {
    position: absolute; top: 50%; left: 50%;
    width: 100cqh; height: 100cqw;
    transform: translate(-50%, -50%) rotate(var(--rot, 0deg));
  }
}
/* The filmstrip's boxes have a size written down, so no measuring is needed: swap the two. */
.strip-thumb[data-rot="90"] img,
.strip-thumb[data-rot="270"] img {
  position: absolute; top: 50%; left: 50%;
  width: var(--strip-h); height: var(--strip-w);
  transform: translate(-50%, -50%) rotate(var(--rot, 0deg));
}
.strip-thumb[data-rot="180"] img { transform: rotate(180deg); }

/* Keyboard selection in the grid. The browser's own ring, replaced by the one above — which is
   the same one the wall uses to say where you got to, because they say the same thing. */
.tile:focus-visible { outline: none; }

.cmp-btn {
  font: inherit; font-size: 11px; padding: 2px 10px; margin-right: 2px;
  border: 1px solid rgb(255 255 255 / .3); border-radius: 999px;
  background: transparent; color: #cfcfd8; cursor: pointer;
}
.cmp-btn:hover { color: #fff; border-color: rgb(255 255 255 / .55); }
.cmp-btn.on { background: rgb(255 255 255 / .18); color: #fff; border-color: transparent; }
/* Compare is the one ACTION in a panel of facts, so it gets a line to itself rather than sitting
   inline among the file sizes where it read as more caption. */
/* The saving, said in colour: green when the AVIF is smaller than what it was made from, red on
   the rare occasion it is not. */
.cap-cut { font-variant-numeric: tabular-nums; }
.cap-cut.ok { color: #4ade80; }
.cap-cut.bad { color: #f87171; }
/* The same air above and below: it sits BETWEEN the two files it compares, and a button with a
   gap over it and none under it belongs to the thing above rather than to both. */
.cmp-line { display: block; margin: 10px 0; }
.cmp-line .cmp-btn { margin-right: 0; padding: 4px 14px; }

/* Everything the viewer can be SET to. The row above the picture is what you reach for while
   looking; this is what you decide once, so it is a menu rather than more buttons in the row. */
.gear-wrap { position: relative; display: inline-flex; }
.gear-menu {
  /* Flush against the row it hangs from, with nothing between them. Eight pixels of air left a
     sliver of the photograph showing between the gear and the panel it had just opened, which
     reads as two things that happen to be near each other rather than one that came out of the
     other. */
  position: absolute; top: 100%; right: 0; z-index: 40; min-width: 268px;
  padding: 5px; border: 1px solid rgb(255 255 255 / .16); border-radius: 12px;
  background: rgb(18 18 22 / .97); box-shadow: 0 12px 34px rgb(0 0 0 / .55);
  backdrop-filter: blur(8px);
}
.gear-menu[hidden] { display: none; }
/* Upwards when the row it hangs from is along the FOOT — which is wherever the thumbnails are,
   and they are at the foot by default. Opening downwards from there put the menu below the window:
   eighty pixels of it with the thumbnails up, and the whole of it with them away, at which point
   pressing the gear looked like a button that did nothing. */
.viewer:not(.strip-top) .gear-menu { top: auto; bottom: 100%; }
.gear-row {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 7px 10px; border: 0; border-radius: 8px; background: none; cursor: pointer;
  font: inherit; font-size: 13px; color: #cfcfd8; text-align: left;
}
.gear-row:hover:not(:disabled) { background: rgb(255 255 255 / .08); color: #fff; }
.gear-row:disabled { opacity: .38; cursor: default; }
.gear-row.on { color: #fff; }
.gear-name { flex: 1 1 auto; }
/* The key, written down. They existed and were said nowhere, so they were a thing you knew or a
   thing you did not. Empty for the settings that have none — a chip with nothing in it would be
   a promise of a key that is not there. */
.gear-key {
  flex: 0 0 auto; min-width: 18px; padding: 1px 5px; border-radius: 5px; text-align: center;
  font-size: 11px; color: rgb(255 255 255 / .62); background: rgb(255 255 255 / .1);
}
.gear-key:empty { background: none; }
/* What the setting is set TO. A switch you can read at a glance without reading a word: the
   knob is on the left for off and the right for on, and the track lights when it is on. */
.gear-pill {
  flex: 0 0 auto; display: inline-flex; align-items: center;
  width: 34px; height: 19px; padding: 2px; border-radius: 999px;
  background: rgb(255 255 255 / .13); transition: background .15s ease;
}
.gear-knob {
  width: 15px; height: 15px; border-radius: 50%; background: rgb(255 255 255 / .6);
  transition: transform .15s ease, background .15s ease;
}
.gear-pill.on { background: var(--accent, #8a7dff); }
.gear-pill.on .gear-knob { transform: translateX(15px); background: #fff; }
/* The same switch, out in the control row rather than down a menu: a word and the pill that
   says what it is set to. It replaced a pair of buttons reading "Pagination on | off", which
   named the mechanism and named it the wrong way round — the state a reader wants is the one
   that pair called off. */
.browse-switch { display: inline-flex; align-items: center; gap: 7px; }
.browse-switch .gear-pill { pointer-events: none; }
.browse-name { line-height: 1; }

/* The one setting that is not a yes and a no says which of its two it is instead. */
.gear-pill.two {
  width: auto; height: auto; padding: 2px; gap: 2px; background: rgb(255 255 255 / .1);
}
.gear-half {
  padding: 2px 8px; border-radius: 999px; font-size: 11px; line-height: 1.4;
  color: rgb(255 255 255 / .6);
}
.gear-half.on { background: rgb(255 255 255 / .2); color: #fff; }
@media (prefers-reduced-motion: reduce) {
  .gear-pill, .gear-knob { transition: none; }
}
/* The original is the file that was never converted, so it is the big one: a second or two of
   nothing, and the button looks broken. It says it is working instead, and goes on saying it until
   the bytes are decoded rather than merely fetched. */
.cmp-btn.loading { opacity: .75; cursor: progress; }
.cmp-spin {
  display: inline-block; width: 11px; height: 11px; margin-right: 7px; vertical-align: -1px;
  border: 2px solid rgb(255 255 255 / .3); border-top-color: #fff; border-radius: 50%;
  animation: cmp-spin .7s linear infinite;
}
@keyframes cmp-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .cmp-spin { animation-duration: 2.4s; } }

/* HOW the row moves, now that nothing is drawn under it to move it with.
   While BROWSING the pointer moves it, and instantly — a row that eased after the hand would
   never catch up with it. Paginating, the arrows move it a screenful at a time and smooth is
   what makes that read as a step along one row rather than a cut to another. */
.viewer.paginating .strip { scroll-behavior: smooth; }
.viewer.can-hover:not(.paginating) .strip { scroll-behavior: auto; }
.viewer.hover-mode .strip-thumb { cursor: pointer; }

/* The row of controls OVER the photograph rather than under it — the default, see "Overlay".
   It is eight small things on one line, and the band of window it held open came out of the
   picture's height on every photograph whether anybody was reaching for the row or not. Out of the
   flow, the picture has the whole of the glass; the row is laid on top of it.

   Where it lands is the place it occupied in the flow: against the thumbnails at whichever end
   they are, and against the glass when they are away. */
/* Out of the flow and AS WIDE AS ITS BUTTONS, rather than a band across the window. The shading
   has to be there — white controls on an unknown photograph are white controls on a white sky —
   and a full-width wash hid a stripe of every picture to say something about eight small things
   in the middle of it. A capsule around the buttons says it where it is needed and leaves the
   rest of the photograph alone.

   The vertical padding is the SAME six pixels the row carries in the flow, so switching Overlay
   on does not move a single button: the box is in the same place, and only what is painted behind
   it changes. (The horizontal padding is the capsule's own room; the group stays centred either
   way, so it moves nothing.) */
.viewer.overlay-bar .controls,
.viewer.no-bar .controls {
  position: absolute; z-index: 6;
  /* Centred with AUTO MARGINS, not with a transform. A transformed element becomes the containing
     block for `position: fixed` inside it — and the settings menu is pinned to the window that way
     while it is open, so a transform here quietly moved the menu by half the row's width and put
     it off the bottom of the screen. Both offsets zero and the width set to its content does the
     same centring with nothing to trip over. */
  left: 0; right: 0; margin-left: auto; margin-right: auto;
  width: max-content; max-width: calc(100% - 20px);
  /* Against the thumbnails, with the seam coming from the row's own padding — exactly where the
     row sits when it is in the flow, so turning Overlay on moves nothing. */
  bottom: var(--strip-h); top: auto;
}
.viewer.overlay-bar .controls { padding: var(--seam) 14px; border-radius: 999px; }
/* The capsule itself, on a pseudo-element rather than on the row.
   backdrop-filter makes an element the containing block for `position: fixed` inside it, exactly
   as a transform does — and the settings menu is pinned to the window that way while it is open,
   so a blur on the row put the menu a quarter of a screen from where it belonged. On a child of
   the row it blurs the same pixels and traps nothing. */
.viewer.overlay-bar .controls::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit; background: rgb(10 10 14 / .55);
}
@supports (backdrop-filter: blur(1px)) {
  .viewer.overlay-bar .controls::before { background: rgb(10 10 14 / .42); backdrop-filter: blur(12px); }
}
.viewer.overlay-bar.no-strip .controls,
.viewer.no-bar.no-strip .controls { bottom: 0; }
.viewer.overlay-bar.strip-top .controls,
.viewer.no-bar.strip-top .controls { top: var(--strip-h); bottom: auto; }
.viewer.overlay-bar.strip-top.no-strip .controls,
.viewer.no-bar.strip-top.no-strip .controls { top: 0; }
/* The stage no longer holds a place for it, so the photograph gets that height back. */
.viewer.overlay-bar .stage > .controls { order: 0; }

/* ...and the row that is not there at all — see "Hide button bar". Placed exactly as the overlay
   places it and then made invisible, rather than removed: the settings menu hangs off the gear,
   and a menu with nothing to hang from opens in the corner of the window. Invisible, the row is
   still a place, so Ctrl+, opens the menu where the gear would have been. */
.viewer.no-bar .controls { background: none; visibility: hidden; pointer-events: none; }
/* The one thing in the hidden row that can still be on screen. */
.viewer.no-bar .gear-menu:not([hidden]) { visibility: visible; pointer-events: auto; }

/* Zooming must not put a scrollbar on the viewer — panning is the way around a zoomed photo,
   exactly as in the converter. */
.viewer { overflow: hidden; }

/* One size for everything on the file line, and links that brighten rather than underline. */
.fileinfo { color: #b9b9c4; font-size: 13px; line-height: 1.55; }
.fileinfo a { color: #fff; }
.orig-line { font-size: 13px; }
.pair-switch button { font-size: 13px; }
.cmp-btn { font-size: 13px; }
#caption a, .orig-line a {
  color: #8ab4ff; text-decoration: none;
  transition: color .18s ease;
}
#caption a:hover, .orig-line a:hover, #caption a:focus-visible, .orig-line a:focus-visible {
  color: #cfe0ff; text-decoration: none;
}
.ext-mark { font-size: .82em; margin-left: 3px; opacity: .85; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.controls .pair-switch { margin-top: 0; }
.controls .pair-switch button { padding: 4px 11px; }
/* The two that only ever hold a sign, in a group with a slider between them: a little smaller
   than the round buttons that carry the row, which buys the width the gear needed. */
.controls .vb-zoom .ctl { width: 30px; height: 30px; font-size: 18px; }

/* Rearranging: the dragged picture follows the pointer with no easing (it must not lag behind
   the hand), while everything else slides out of its way. */
.strip-thumb.shifting { transition: transform .16s cubic-bezier(.2, .7, .3, 1); }
.strip-thumb.dragging {
  transition: none; z-index: 3; position: relative;
  box-shadow: 0 10px 26px rgb(0 0 0 / .6); cursor: grabbing;
}
.strip-thumb { touch-action: none; }
@media (hover: none), (pointer: coarse) {
  /* Give the swipe back. Dragging a thumbnail to rearrange it is a mouse gesture; on a touch
     screen the same movement means "pan this row", which is what every other row of pictures on
     a phone does — and without it the strip cannot be moved at all. */
  .strip-thumb { touch-action: pan-x; }
  .tile { touch-action: pan-y; }
}


/* --- what gives up the width ---------------------------------------------------------------- */
/* Everything in the row is its own natural size and stays it — a button that resizes with the
   window is a button you have to look for. The two exceptions are the zoom slider, which is
   useless when short, and the empty cell between the photograph's controls and the window's. */
.controls > * { flex: 0 0 auto; }
.controls > .ctl-spacer { display: none; }
/* The zoom group does not stretch and does not squeeze: a slider that changes width as the row
   fills up is a slider whose middle means something different on every photograph. 100px of
   track, and the row finds its slack in the empty cell instead. */
.controls > .vb-zoom { flex: 0 0 auto; }
.controls .vb-zoom .sb-slider { flex: 0 0 100px; width: 100px; min-width: 100px; max-width: 100px; }
@media (max-width: 700px) {
  .pair-switch button { padding: 4px 10px; }
}

/* --- the settings sidebar ------------------------------------------------------------------- */
/* Everything about the photograph, and everything that decides how it is drawn. It lies OVER the
   picture on the right rather than beside it, so the photograph keeps the full width of the
   window and Fit goes on describing what actually fits. */
.viewer { --sb-w: 370px; }
.sidebar {
  position: absolute; top: 0; right: 0; bottom: 0; z-index: 8;
  width: min(var(--sb-w), 100vw);
  display: flex; flex-direction: column;
  background: rgb(12 12 16 / .95); color: #e9e9f0;
  border-left: 1px solid rgb(255 255 255 / .12);
  box-shadow: -18px 0 44px rgb(0 0 0 / .45);
  overscroll-behavior: contain;
}
@supports (backdrop-filter: blur(1px)) {
  .sidebar { background: rgb(12 12 16 / .82); backdrop-filter: blur(14px); }
}
/* The panel is hidden with the attribute, so say what hidden means for a flex box. */
.sidebar[hidden] { display: none; }
/* The thumbnails are above it rather than behind it: covering the right end of the row would
   hide the very photographs the row exists to offer. */
.viewer:not(.strip-top)[open]:not(.no-strip) .sidebar {
  bottom: calc(var(--ctl-h, 48px) + var(--strip-h));
}
/* The photograph steps aside for it, and nothing else does. Leaving the picture at full width
   behind the panel would make Fit a lie: it would fit the window, and a strip of it down the right
   would be under the panel, unreachable and unpannable. So the pane shrinks and the component
   re-fits itself to the room genuinely left.
   The row does not shrink with it. The panel starts below the thumbnails, or above them — either
   way it is nowhere near the row, so squeezing the row was moving buttons out from under nothing:
   open the panel and every control shuffled left, and the row wrapped onto a second line when it
   ran out of width it did not need to give up. */
@media (min-width: 701px) {
  .viewer.sidebar-open .stage > #pane { padding-right: var(--sb-w); }
}
/* On a phone there is no width to share: the panel takes the window, and its own close button is
   the way back to the photograph. A sliver of picture down one side would be neither. */
@media (max-width: 700px) {
  .viewer { --sb-w: 100vw; }
  .sb-body { padding-bottom: 40px; }
}

.sb-head {
  display: flex; align-items: center; gap: 12px; flex: 0 0 auto;
  padding: 12px 12px 10px 16px; border-bottom: 1px solid rgb(255 255 255 / .1);
}
.sb-title { margin: 0; flex: 1 1 auto; font-size: 14px; font-weight: 600; letter-spacing: .01em; }
.sb-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 4px 16px 24px; }
.sb-sec { display: flex; flex-direction: column; gap: 8px; padding: 14px 0; border-bottom: 1px solid rgb(255 255 255 / .07); }
.sb-sec:last-child { border-bottom: 0; }
.sb-sec[hidden] { display: none; }
.sb-label {
  font-size: 11px; letter-spacing: .09em; text-transform: uppercase;
  color: #8a8a98;
}
.sb-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.sb-note { color: #8a8a98; font-size: 12px; }
.sidebar .pair-switch { margin-top: 0; align-self: flex-start; }
/* A control that cannot act on anything yet says so rather than answering with nothing. */
.sidebar .ctl[disabled], .sidebar .ctl-text[disabled], .sidebar .sb-slider[disabled] {
  opacity: .4; cursor: default; pointer-events: none;
}

/* The photo's own name. For its owner this is a text field that saves itself; for everyone else
   it is simply the name, and it must look like the same thing either way. */
.pname { display: flex; align-items: center; gap: 8px; min-width: 0; }
.pname-text {
  color: #e8e8ef; font-size: 14px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pname-input {
  font: inherit; font-size: 14px; color: #fff; width: 100%; min-width: 0;
  background: rgb(255 255 255 / .06); border: 1px solid rgb(255 255 255 / .18);
  border-radius: 999px; padding: 6px 12px;
  transition: border-color .18s ease, background .18s ease;
}
.pname-input::placeholder { color: #85858f; }
.pname-input:hover { border-color: rgb(255 255 255 / .35); }
.pname-input:focus-visible, .pname-input:focus {
  outline: none; border-color: rgb(255 255 255 / .6); background: rgb(255 255 255 / .1);
}
/* Saved, and not saved: a border is enough of an answer for something that saves itself. */
.pname.saved .pname-input { border-color: rgb(120 220 160 / .8); }
.pname.bad .pname-input { border-color: var(--accent); }

/* The description. The same field, given room to be a sentence — so it is a box rather than a
   pill, and Enter inside it makes a new line rather than saving. */
.pdesc { display: block; min-width: 0; }
.pdesc-text { margin: 0; color: #d6d6e0; font-size: 13.5px; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }
.pdesc-input {
  font: inherit; font-size: 13.5px; line-height: 1.5; color: #fff;
  width: 100%; min-height: 88px; resize: vertical;
  background: rgb(255 255 255 / .06); border: 1px solid rgb(255 255 255 / .18);
  border-radius: 12px; padding: 8px 11px;
  transition: border-color .18s ease, background .18s ease;
}
.pdesc-input::placeholder { color: #85858f; }
.pdesc-input:hover { border-color: rgb(255 255 255 / .35); }
.pdesc-input:focus-visible, .pdesc-input:focus {
  outline: none; border-color: rgb(255 255 255 / .6); background: rgb(255 255 255 / .1);
}
.pdesc.saved .pdesc-input { border-color: rgb(120 220 160 / .8); }
.pdesc.bad .pdesc-input { border-color: var(--accent); }

/* Keeping the file rather than looking at it. A link, because that is what it is — but one that
   fetches the bytes itself, so a photograph served from another host still saves rather than
   opening in a tab. */
.dl-btn { text-decoration: none; display: inline-flex; align-items: center; }
.dl-btn[hidden] { display: none; }
.dl-btn.working { opacity: .55; }

/* Zoom: two buttons and the range between them, which is the only control here that has to grow
   with the room it is given. */
.sb-slider {
  flex: 1 1 auto; min-width: 0; margin: 0;
  accent-color: var(--accent); color-scheme: dark; cursor: pointer;
}

/* ---- Zoom, and the thumbnail controls -------------------------------------------------------
   These had a bar of their own under the photograph. They are in the row above it now, where the
   rest of the controls are; what is left here is how they group together inside it. */
.vb-group { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* The zoom group is the only part that wants the spare width — the slider is useless when short.
   Half the width it used to take: the range it covers is the same, and the room it was using is
   worth more to the photograph's own controls than to a finer grip on the same 0-1000. */
.vb-zoom { flex: 1 1 130px; min-width: 100px; max-width: 230px; }
/* Stacked: the number goes UNDER the slider rather than beside it, and the pair fits in the row's
   own height. The slider is the widest thing in this group and the number is the narrowest, so
   side by side the number costs the slider a fifth of its travel for two lines of nothing. The
   minus and plus span both rows and stay centred against the pair.
   A grid rather than a wrapper element: the four controls are siblings and the reader can switch
   this off, so moving them in the DOM for one of the two arrangements would leave the other
   holding a box it does not want. */
.viewer.zoom-stacked .vb-zoom {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: auto auto; align-items: center; column-gap: 8px; row-gap: 0;
}
.viewer.zoom-stacked .vb-zoom > #zoom-out { grid-area: 1 / 1 / 3 / 2; }
.viewer.zoom-stacked .vb-zoom > .sb-slider { grid-area: 1 / 2 / 2 / 3; align-self: end; }
.viewer.zoom-stacked .vb-zoom > #zoom-in { grid-area: 1 / 3 / 3 / 4; }
.viewer.zoom-stacked .vb-zoom > .vb-pct {
  grid-area: 2 / 2 / 3 / 3; justify-self: center; text-align: center;
  /* Tight against the slider: the row has one control's worth of height and this has to live in
     what the slider is not using. */
  line-height: 1.1; margin-top: -1px;
}
/* The number the slider is showing. Tabular figures and a reserved width so the controls beside
   it hold still as it counts through 100 %, 1000 %, and back. */
.vb-pct {
  /* Room for the widest thing it can ever say, so the row does not shift as the number changes.
     "9999%" is five characters; tabular figures make every digit the same width, so 5ch is the
     honest reservation rather than a guess that holds until someone zooms past 999%. */
  /* A FIXED width, not a floor: 9999% is four tabular digits and a per-cent sign, and the sign
     is wider than a digit — so 5ch was three pixels short and the row shifted at four figures. */
  flex: 0 0 auto; width: 5.75ch; text-align: right;
  color: #c9c9d4; font-size: 12.5px; font-variant-numeric: tabular-nums;
}

/* A control that cannot act on anything yet says so rather than answering with nothing. */
.controls .ctl[disabled], .controls .ctl-text[disabled], .controls .sb-slider[disabled] {
  opacity: .4; cursor: default; pointer-events: none;
}
/* With the thumbnails put away there is nothing for these two to say. */
/* Nothing to browse: browsing is what the pointer does ALONG the thumbnail row, and with the row
   away there is no row. (This named #modeswitch, the pair Browse replaced — so the control quietly
   stopped being hidden when the pair stopped existing.) */
.viewer.no-strip #btn-browse { display: none; }
/* On a phone the row cannot be one row: the zoom takes a line of its own and the rest follows
   underneath it, which is the shape the lower bar used to have anyway. The gap between the two
   halves has nothing to separate once they are on different lines. */
@media (max-width: 700px) {
  .controls { padding: 0 12px; }
  /* Nothing left to hold apart once the row is on two lines — and the gap would strand the second
     line's controls against the right edge, away from the ones they follow. */
  .controls > .ctl-spacer { display: none; }
}

/* The sign-in button: an outlined pill with a face on it, the shape everyone already reads as
   "become someone here". */
.signin-link {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  padding: 7px 14px 7px 11px; border-radius: 999px; font-size: 14px; font-weight: 500;
  border: 1px solid rgb(138 180 255 / .45); color: #8ab4ff; text-decoration: none;
  background: transparent; cursor: pointer;
}
.signin-link:hover { color: #cfe0ff; border-color: rgb(207 224 255 / .7); background: rgb(138 180 255 / .12); }
.account a { color: #8ab4ff; text-decoration: none; font-size: 13px; }

/* Signed in, it is a face and nothing else — the name and the address are one press away, where
   they do not spend header room saying what you already know. */
.avatar-btn {
  width: 34px; height: 34px; padding: 0; border-radius: 50%; cursor: pointer;
  border: 1px solid var(--line); background: var(--accent); color: #fff; overflow: hidden;
  display: grid; place-items: center; font: inherit; font-size: 14px; font-weight: 600;
}
.avatar-btn img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar-btn:hover { border-color: var(--accent); }
.avatar-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.acct-menu {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 40;
  /* As wide as the name and the address need and no wider — a fixed width leaves a short address
     sitting in a panel of empty. The cap is so a long one wraps rather than running off the page. */
  width: max-content; max-width: min(320px, calc(100vw - 2 * var(--gap)));
  padding: 14px; border: 1px solid var(--line); border-radius: 12px;
  background: var(--panel); box-shadow: var(--shadow); text-align: left;
}
.acct-menu[hidden] { display: none; }
.acct-name { color: var(--ink); font-size: 15px; font-weight: 600; }
.acct-mail { color: var(--muted); font-size: 13px; margin-top: 2px; word-break: break-all; }
.acct-menu .signout-btn { margin: 12px 0 0; font-size: 13px; padding: 6px 14px; }
.account:empty { display: none; }

/* Fit / 1:1 and Hover / Click: pairs that say which one is true as much as they set it. They
   borrow the shape of the Wide/Tall switch, because they are the same kind of thing. */
.zoom-switch, .mode-switch { margin-top: 0; }

/* A line that says what just happened. Inside the dialog when the viewer is open, because a
   modal's backdrop covers everything that is not. */
.note {
  position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%);
  z-index: 20; max-width: min(92vw, 520px);
  padding: 9px 16px; border-radius: 999px;
  background: rgb(20 20 26 / .94); color: #f2f2f4; font-size: 13px;
  box-shadow: var(--shadow); border: 1px solid rgb(255 255 255 / .12);
}

/* Rearranging the grid: the tile being moved lifts off the sheet, and the rows re-solve around
   it. Nothing else has to move by hand — the layout already knows how. */
.tile-dragging {
  cursor: grabbing; z-index: 4;
  /* No easing on the one the hand is holding: it must be exactly where the pointer is, this
     frame, not on its way there. Everything else animates; this is the thing they animate
     around. */
  transition: none;
  box-shadow: 0 14px 34px rgb(0 0 0 / .55);
  outline: 2px solid var(--accent); outline-offset: -2px;
  will-change: transform;
  /* So the tile under the pointer is the one being passed OVER, not the one being carried. */
  pointer-events: none;
}
.tile-dragging:hover { transform: none; }
@media (prefers-reduced-motion: reduce) { .tile-dragging { transition: none; } }

/* Signing out of this host, which keeps its own session: quiet, but present. */
.signout-btn {
  font: inherit; font-size: 12px; padding: 1px 8px; margin-left: 2px;
  border: 1px solid rgb(255 255 255 / .22); border-radius: 999px;
  background: transparent; color: #b9b9c4; cursor: pointer;
  transition: color .15s ease, border-color .15s ease;
}
.signout-btn:hover, .signout-btn:focus-visible { color: #fff; border-color: rgb(255 255 255 / .5); outline: none; }
.signout-btn[disabled] { opacity: .5; cursor: default; }

/* Putting the thumbnails away gives their height back to the photograph, so the stage stops
   reserving the room the fixed strip used to occupy. */
.viewer.no-strip .strip { display: none; }
/* Nothing down there any more, so nothing should be holding the photograph off the glass but
   the row's own margin. The room kept for the thumbnails was kept whether they were there or
   not, so putting them away left a strip's worth of black at the bottom of every picture. */
.viewer.no-strip .stage,
.viewer.no-strip[open] .stage,
.viewer.no-strip.strip-top[open] .stage { padding: 0; }
.viewer.no-strip[open] .sidebar { top: 0; bottom: 0; }
@media (max-width: 700px) {
  .viewer.no-strip[open] .stage { padding-top: 12px; }
}

/* A control that is a word rather than a symbol: same shape as the round ones, wide enough to
   read. */
.ctl-text {
  flex: 0 0 auto; height: 34px; padding: 0 12px;
  background: rgb(255 255 255 / .08); color: #cfcfd8; border: 0;
  border-radius: 999px; line-height: 1; cursor: pointer; font: inherit; font-size: 13px;
  transition: background .15s ease, color .15s ease;
}
.ctl-text:hover, .ctl-text:focus-visible { background: rgb(255 255 255 / .2); color: #fff; outline: none; }
.ctl-text.on { background: rgb(255 255 255 / .22); color: #fff; }
/* Browse and Clickless carry a PILL, and the pill is what says on or off. The grey capsule behind
   the word was a second switch-shaped thing around the first, and a switch drawn inside a switch
   reads as one control that cannot make up its mind — worse when it filled in on its own as well,
   so the word had a lit background and a pill saying the opposite. The word stands on the black.
   The hover wash stays: it is not state, it is the row answering the pointer. */
.controls .browse-switch, .controls .browse-switch.on { background: transparent; }
.controls .browse-switch:hover, .controls .browse-switch:focus-visible {
  background: rgb(255 255 255 / .2); color: #fff;
}
/* The way back to something that is missing, in a row of controls for things that are not.
   Grey on grey it was the last thing found in a row a reader was not looking at — so it takes a
   wash of the accent instead: enough to be picked out of the row at a glance, well short of the
   solid fill the accent is used for when something is SELECTED, which this is not. */
.ctl-text.lead {
  background: rgb(255 84 112 / .16); color: #ffc6d0;
  box-shadow: inset 0 0 0 1px rgb(255 84 112 / .38);
}
.ctl-text.lead:hover, .ctl-text.lead:focus-visible {
  background: rgb(255 84 112 / .28); color: #fff;
}
.ctl.on { background: rgb(255 255 255 / .28); color: #fff; }
.ctl svg { display: block; }
@media (max-width: 700px) {
  .ctl-text { height: 30px; padding: 0 9px; font-size: 12px; }
}

/* NOTHING here takes a focus mark. Not the buttons, not the switches, not the slider, not the
   fields.
   The buttons have had both kinds. A fill, in the same white that "on" is drawn in — so a button
   merely holding focus was indistinguishable from one switched on, and putting the info panel away
   with i left its button still looking as though the panel were there. Then a ring, which is
   honest about which is which and is still a mark drawn around a button nobody asked to have
   marked: press i twice and the button you pressed is left circled in accent red over the
   photograph.
   The fields kept a soft accent ring on the argument that "typing goes here" is information a
   reader cannot get any other way. Over a photograph it is a red rectangle around a control, which
   is the thing this row exists not to do — and for a text field the caret already says it. The
   slider had one too, and a slider is not somewhere anybody types.
   The thumbnails keep theirs: a thumbnail is a photograph rather than a control, and the mark
   there is also how a reader following the row with the keyboard knows where they are. */
.controls button:focus-visible,
.controls select:focus-visible,
.controls input:focus-visible,
.sidebar button:focus-visible,
.sidebar a:focus-visible,
.sidebar select:focus-visible,
.sidebar input:focus-visible,
.sidebar textarea:focus-visible,
.pair-switch button:focus-visible {
  outline: none;
  box-shadow: none;
}

/* --- dropdowns, everywhere, forever --------------------------------------------------------- */
/* The list a <select> drops open is drawn by the browser, not by this stylesheet, and it takes
   its colours from the element's colour SCHEME rather than from its background. A dark control
   with no scheme declared therefore opens a white list. Every select on this surface is dark, so
   say it once, here, rather than remembering it at each new one — and paint the options too,
   because a browser that ignores the scheme still honours these. */
select {
  color-scheme: dark;
  accent-color: var(--accent);
}
select option, select optgroup {
  background-color: #16161a;
  color: #e9e9f0;
}

/* What the keys do. A panel rather than a page: it is read once, in the middle of looking at
   something else, and then dismissed with the same key that summoned it. */
.viewer, :root { --keys-w: 340px; }
.keys {
  position: fixed; z-index: 30; top: 50%; left: 50%; transform: translate(-50%, -50%);
  margin: 0;
  width: min(560px, calc(100vw - 32px)); max-height: min(80vh, 720px); overflow: auto;
  background: #16161a; color: #e9e9f0; border: 1px solid rgb(255 255 255 / .14);
  border-radius: 14px; box-shadow: 0 24px 60px rgb(0 0 0 / .6); padding: 18px 20px 20px;
}
.keys:not([open]) { display: none; }
/* Nothing is dimmed while it is up. It used to be shown modally, which dims the page by default —
   and told the truth while it did, because a modal page cannot be used. It can be used now.
   The settings menu passes ABOVE it (z-index 40 against this 30): the menu is something you opened
   on purpose and want to read, and this is a reference you left lying open. */
.keys::backdrop { background: none; }

/* PINNED: a column down the left, the side the info panel is not on, with nothing behind it. */
#keys.pinned {
  top: 0; bottom: 0; left: 0; right: auto; transform: none;
  width: min(var(--keys-w), 100vw); max-height: none; height: auto;
  border-radius: 0; border-left: 0; border-top: 0; border-bottom: 0;
  border-right: 1px solid rgb(255 255 255 / .12);
  box-shadow: 18px 0 44px rgb(0 0 0 / .45);
  padding: 14px 16px 18px;
}
@supports (backdrop-filter: blur(1px)) {
  #keys.pinned { background: rgb(12 12 16 / .82); backdrop-filter: blur(14px); }
}
/* Pinned, it is furniture rather than a window: nothing to drag it by, and no cursor saying there
   is. */
#keys.pinned .keys-head { cursor: default; }
/* In the viewer, EVERYTHING gives up the width — not only the photograph. The info panel on the
   other side can get away with shrinking the picture alone because it stops above the thumbnails
   and is nowhere near the row; this one is a full-height column, so a panel that only the picture
   made room for would sit on the left end of the row and on the first few thumbnails. Nothing is
   behind it: the stage moves over, the thumbnails start where it ends, and so does the arrow that
   pages them. */
@media (min-width: 701px) {
  .viewer.keys-open .stage { padding-left: var(--keys-w); }
  .viewer.keys-open .strip { left: var(--keys-w); }
  .viewer.keys-open .strip-page.prev { left: var(--keys-w); }
  /* The row is placed against the VIEWER rather than inside the stage when it is overlaid or
     hidden, so the stage's padding does not reach it: it centred itself across the whole window
     and put its left-hand buttons behind the panel. */
  .viewer.keys-open.overlay-bar .controls,
  .viewer.keys-open.no-bar .controls { left: var(--keys-w); }
}
/* ...and on the wall, the wall does. */
@media (min-width: 701px) {
  html.keys-pinned body { padding-left: var(--keys-w); }
}
/* On a phone there is no width to share, so a pinned panel would BE the page. It floats there. */
@media (max-width: 700px) {
  #keys.pinned {
    top: 50%; bottom: auto; left: 50%; transform: translate(-50%, -50%);
    width: min(560px, calc(100vw - 32px)); max-height: min(80vh, 720px);
    border: 1px solid rgb(255 255 255 / .14); border-radius: 14px;
    box-shadow: 0 24px 60px rgb(0 0 0 / .6); padding: 18px 20px 20px;
  }
  html.keys-pinned body { padding-left: 0; }
  .viewer.keys-open .stage { padding-left: 0; }
  .viewer.keys-open .strip { left: 0; }
  .viewer.keys-open .strip-page.prev { left: 0; }
  .viewer.keys-open.overlay-bar .controls,
  .viewer.keys-open.no-bar .controls { left: 0; }
}
.keys-head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
/* The handle the shortcuts panel is moved by, so it says so. Only that one: the tag editor wears
   the same class for its looks and does not move, and a cursor promising otherwise is a lie the
   pointer tells before the hand finds out. */
#keys .keys-head { cursor: move; user-select: none; }
#keys .keys-head button { cursor: pointer; }
.keys-head h2 { font-size: 15px; font-weight: 600; margin: 0; flex: 1 1 auto; }
.keys-list { margin: 0; display: grid; gap: 8px; }
.keys-list > div { display: flex; align-items: baseline; gap: 12px; }
.keys-list dt { flex: 0 0 auto; min-width: 168px; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.keys-list dd { margin: 0; color: #b9b9c4; font-size: 13px; }
.keys .or { color: #7a7a88; font-size: 11px; }
.keys kbd {
  font: inherit; font-size: 11px; line-height: 1; padding: 4px 7px;
  background: rgb(255 255 255 / .1); border: 1px solid rgb(255 255 255 / .16);
  border-bottom-width: 2px; border-radius: 6px; color: #fff; white-space: nowrap;
}
.keys-foot { margin: 14px 0 0; color: #8a8a98; font-size: 12px; }

/* In fullscreen the photograph is the only thing there is. This matters most when the browser
   refused to make the dialog itself fullscreen and the PAGE is the fullscreen element instead:
   the page is then the topmost thing there is, and without this the grid paints over the viewer. */
html.is-fullscreen body > *:not(#viewer):not(#keys) { display: none !important; }
/* ...and for the same reason, <html> must not be given a background of its own while it is the
   fullscreen element. Hiding the page's CONTENT was only half of it: the root element paints too,
   and up there it painted its background OVER the viewer — a band of near-black across the top of
   the screen, sitting on the thumbnails, as tall as whatever box the root had left.
   The body is not in the top layer, so its background reaches the canvas the ordinary way and the
   page behind is still dark; and the viewer is made properly opaque rather than nearly so, since
   in fullscreen it is the only thing there is. */
html.is-fullscreen body { background: #060608; }
html.is-fullscreen #viewer[open] { background: #060608; }
/* ...and the viewer covers the screen, said outright rather than inherited.
   A modal dialog cannot BE the fullscreen element in Chrome — the request falls through to the
   page — so what is fullscreen is <html>, and the dialog is a top-layer box over a page whose
   viewport has just changed size underneath it. Everything here is what the layout already works
   out for itself when it works; writing it down is what stops it being worked out from a
   containing block that has moved. */
html.is-fullscreen #viewer[open] {
  position: fixed; inset: 0; margin: 0;
  width: 100vw; height: 100vh; max-width: none; max-height: none;
}

/* Moving along the row when it is not moving by itself. Only in View mode, only when there is
   somewhere to go, and narrow enough that the thumbnail underneath is still a picture. */
.strip-page {
  position: absolute; bottom: 0; z-index: 3; width: 58px; height: var(--strip-h, 176px);
  border: 0; padding: 0; cursor: pointer; color: #fff; font-size: 24px; line-height: 1;
  display: grid; place-items: center;
  background: linear-gradient(to right, rgb(0 0 0 / .75), rgb(0 0 0 / 0));
  transition: opacity .15s ease; opacity: .92;
}
/* A disc under the chevron, so it reads as a button rather than as a shadow on the photograph. */
.strip-page::before {
  content: ''; position: absolute; z-index: -1;
  width: 34px; height: 34px; border-radius: 999px;
  background: rgb(18 18 22 / .82); border: 1px solid rgb(255 255 255 / .22);
}
.strip-page:hover::before { background: rgb(30 30 36 / .92); border-color: rgb(255 255 255 / .5); }
.strip-page.next { background: linear-gradient(to left, rgb(0 0 0 / .72), rgb(0 0 0 / 0)); }
.strip-page.prev { left: 0; }
.strip-page.next { right: 0; }
.strip-page:hover, .strip-page:focus-visible { opacity: 1; outline: none; }
.strip-page:focus-visible { box-shadow: inset 0 0 0 1px #fff; }
.strip-page[hidden] { display: none; }
