/* heart.photos — the public gallery.
   Dark for everybody, whatever the browser says (George, 17 October 2026: "always dark on heart.photos for
   everyone, ignoring the browser's setting"), because photographs sit better on a dark ground. The light
   colors are the reader's Mode (gear.js, wallmenu.js, layout.js setMode): Light, or Auto in a browser set to
   light. html[data-mode] is put on by the head of gallery.html before the first paint. */
: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 seam between two photographs on the wall — and the same four pixels the other wall, the
     one in the app at he.art/photos, keeps between its own. It is far short of the margin the
     wall keeps at its ends (see .grid): the ends line the first picture of every row up with the
     name of the site, and between two pictures there is nothing to line up with, so the space
     there is only doing the work of telling them apart. */
  --tile-gap: 4px;
  /* The line of text under every photograph on the wall. See .tile-name. */
  --cap-h: 24px;
  /* The band of words ACROSS the bottom of a photograph, when the wall is showing them. One line
     of 12px text beside a 22px button, with 6px of air above and below: 34. */
  --tags-h: 34px;
  /* ...and the room the strip leaves at its right-hand end for the rating, which lives in that
     same corner. The mark is 5px of padding, its marks, and 5px again — 23px with one, 37px with
     two — then 6px from the edge of the picture and 8px of air between the two. Written down
     because it is arithmetic about another rule's box, and the two must not drift apart. */
  --mark-clear: 37px;
  --mark-clear-2: 51px;
}
:root { color-scheme: dark; }
:root[data-mode="light"] {
  color-scheme: light;
  --bg: #fbfbfc; --panel: #fff; --ink: #16161a; --muted: #6b6b78;
  --line: #e6e6ec; --shadow: 0 12px 40px rgb(20 20 30 / 0.16);
}
@media (prefers-color-scheme: light) {
  :root[data-mode="auto"] {
    color-scheme: light;
    --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;
  /* A hand's depth of nothing under the last row and no more. The 64px this was is left from when
     the multiselect bar was pinned across the bottom of the window; nothing has been pinned there
     for a long time, and what it left behind was a wall that ended in a screenful of black.
     The inset is for a phone whose home bar would otherwise sit over the last line. */
  padding: 0 0 max(10px, env(safe-area-inset-bottom));
}

/* --- 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. */
/* The block itself does not stay: the WORDS do not follow you down the page.
   The chips are the wall's own controls and there are a hundred and fifty of them behind a More —
   a row that deep is not something to give a permanent band of a window to, and scrolled past it
   is out of the way of the photographs, which are the point. What stays is the line above it: the
   name, the way to the galleries, the search, and who you are. */
.topbar {
  background: var(--bg);
  margin-bottom: 14px;
}
/* No line under it: the search and the line of words below stand apart by their spacing (George, 10 October
   2026: "Remove this horizontal line below the search box"). */
.top {
  position: sticky; top: 0; z-index: 40;
  background: var(--bg);
}
/* Anything scrolled to lands below the bar rather than under it — a tile tabbed to, above all.
   The bar being only the name's line now, this is that line and not the words below it. */
html { scroll-padding-top: var(--head-h, 56px); }

.top {
  padding: 9px var(--gap);
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: 18px;
}
/* On a phone nothing at all stays: there is little height to begin with, and a bar pinned to the
   top of it spends a fifth of the screen saying the same three things all the way down a wall. */
html.mobile .top { position: static; border-bottom: 0; }
/* ...but on a desktop the line of words stays too, just under the name's line (George, 27 September
   2026: "keep the line starting with All, Latest sticky when scrolling down on desktop"). It can:
   since the words past the first line went behind More it is one line, not the band of a hundred
   and fifty it was when it was let go (above). A hairline under it only while photographs pass
   beneath (layout.js adds .stuck). Beside an open column of galleries it starts where the wall
   does, or its ground would be drawn over the top of the column. What a tile scrolled to has to
   clear is then both lines (--top-h), not the name's alone. */
html:not(.mobile) .topbar {
  position: sticky; top: var(--head-h, 56px); z-index: 39;
  border-bottom: 1px solid transparent;
  /* ...and a band of the page's own ground above it, under the name's line (which is drawn over
     it, z-index 40), so no sliver of the wall shows between the two where the header's height is
     a fraction of a pixel the screen rounds the other way (George, 27 September 2026). */
  box-shadow: 0 -3px 0 var(--bg);
}
html:not(.mobile) .topbar.stuck { border-bottom-color: var(--line); }
html:not(.mobile).galleries-open .topbar { margin-left: var(--gal-w, 232px); }
html:not(.mobile).galleries-open .tagbar { margin-left: 0; }
html:not(.mobile) { scroll-padding-top: var(--top-h, 96px); }
.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;
}
/* The name's ".photos" wears the logo's own color whatever the theme: Rose, #ff5470, Logo 1's (George, 10
   October 2026: "change the ".photos" color of "Heart.photos" to the same base color as the new logo"); it was
   Red #e62117 from 8 October ("Use the "Red" theme color #E62117 for the Heart.photos logo and favicon"). The heart became Red, #e62117, on
   17 October 2026 ("use #e62117"), and the word with it (George, 17 October 2026: "Photos" should match the same color as the heart). */
.brand span { color: #e62117; }
/* The mark before the name, as tall as its capitals and on its line (George, 10 October 2026: "Add the
   appropriate sized logo to the left of "Heart.photos""): Logo 1 unless the reader chose Logo 2
   (layout.js applyLogo). */
.brand-logo { display: inline-block; width: 1em; height: 1em; margin-right: .3em; vertical-align: -0.14em; }
html[data-logo="2"] .brand .logo-1, html:not([data-logo="2"]) .brand .logo-2 { display: none; }
/* The first line of the column: where else to go on the left, and the one thing that is not a
   place to go on the right — see gallery.html. */
/* The name, the pencil and the chooser, on one line — see gallery.html. */
.collection-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; min-width: 0; }
.collection-head .collection-title { margin: 0; min-width: 0; }
.collection-edit {
  flex: 0 0 auto; display: grid; place-items: center; width: 26px; height: 26px; padding: 0;
  border: 0; border-radius: 999px; cursor: pointer;
  background: rgb(255 255 255 / .07); color: var(--muted);
  transition: background .12s ease, color .12s ease;
}
.collection-edit:hover { background: rgb(255 255 255 / .16); color: var(--ink); }
/* A display of its own beats the browser's [hidden], so every one of these says it again. */
.collection-edit[hidden], .collection-head[hidden] { display: none; }
/* Empty trash, over the Trash: the one button there, and one that cannot be taken back. */
.trash-empty[hidden] { display: none; }
.ctl-text.trash-empty:hover:not(:disabled), .ctl-text.trash-forever:hover:not(:disabled),
.ctl-text.trash-empty:focus-visible, .ctl-text.trash-forever:focus-visible {
  background: rgb(255 84 112 / .28); color: #fff; }
.ctl-text.trash-empty:disabled { opacity: .45; cursor: default; }
/* Upload, beside it: photos from this computer into the gallery (upload.js). */
.gal-upload[hidden] { display: none; }
/* Delete gallery, over a gallery of your own: into the Trash, so it is not the red of Empty trash
   until the pointer is on it. */
.gal-delete[hidden] { display: none; }
.ctl-text.gal-delete:hover, .ctl-text.gal-delete:focus-visible { background: rgb(255 84 112 / .28); color: #fff; }
/* A gallery of your own in the Trash, opened at its address: a line saying so, with Restore. */
.collection-trashed {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; width: fit-content; max-width: 100%;
  margin: 10px 0 0; padding: 8px 10px 8px 14px; border-radius: 12px;
  background: rgb(255 84 112 / .12); border: 1px solid rgb(255 84 112 / .35); color: var(--ink);
}
.collection-trashed[hidden] { display: none; }
/* The galleries in the Trash, over its photographs: a name, how many photos, and the two ways out. */
.trash-galleries { display: flex; flex-direction: column; gap: 6px; margin: 10px 0 2px; }
.trash-galleries[hidden] { display: none; }
.trash-galleries-head { margin: 0; font-size: 13px; font-weight: 600; color: var(--muted); }
.trash-gal { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.trash-gal-name { font-weight: 500; color: var(--ink); }
.trash-gal-n { font-size: 13px; color: var(--muted); }
/* Editing in place: the name on its own line and the description under it, both as wide as the
   heading they replace, so what you are typing is the size of what it will become. */
.collection-form { display: flex; flex-direction: column; gap: 8px; margin: 10px 0 0; max-width: 640px; }
.collection-input {
  width: 100%; box-sizing: border-box; font: inherit; color: var(--ink);
  background: rgb(255 255 255 / .06); border: 1px solid var(--line); border-radius: 9px;
  padding: 8px 10px;
}
.collection-input:focus { outline: none; border-color: rgb(255 84 112 / .6); }
#collection-name { font-size: 20px; font-weight: 600; }
.collection-about { resize: vertical; min-height: 56px; line-height: 1.4; }
.collection-save { display: flex; gap: 8px; }

/* The gallery in hand, up in the line that stays — see gallery.html. Capped, because the wordmark
   and the search field share this line with it and a gallery can be called anything. */
.top-gallery { display: flex; align-items: center; gap: 6px; min-width: 0; }
.top-gallery:empty { display: none; }
.top-gallery .tag-chip { max-width: min(34vw, 260px); overflow: hidden; white-space: nowrap; }
/* One line, however long the gallery is called. The name inside the chip is what gives way, with an
   ellipsis, because it is a name somebody wrote; wrapped, a long one made the chip three lines tall
   and pushed everything else on the line off the edge of a phone. The button beside it keeps all
   of its words. */
.top-gallery .tag-gallery { min-width: 0; }
.top-gallery .tag-gallery .tag-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.top-gallery .tag-add { flex: 0 0 auto; max-width: none; }

.gal-top {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  margin: 0 0 8px; padding: 2px 4px 2px 6px;
}
.gal-links {
  margin: 0; color: var(--muted); font-size: 13px; line-height: 1.3;
  display: flex; align-items: baseline; gap: 6px;
}
.gal-links a { color: var(--accent); text-decoration: none; }
.gal-links 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; }
/* Below the gear, said with enough specificity to survive .gear-menu — which comes later in this
   file and sets a top of its own out of `var(--seam)`. --seam is declared on .viewer, and this
   menu hangs off the wall's tagbar, outside it: the variable is not there, the calc is invalid,
   the declaration is dropped at computed-value time, and `top` falls back to auto. A box with
   auto top and no bottom sits at its static position — which is exactly where the button is, so
   the menu opened ON the icon that opened it. */
.wall-gear-wrap .wall-menu { top: calc(100% + 6px); bottom: auto; 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: var(--on-accent); 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. */
/* !important since 16 October 2026: Wall theme 2 lays its tiles out with `display: grid` under a longer selector
   (html.wall-2 .grid > .tile), which outweighed this one, so a search marked every other photo filtered and left it on
   screen (George: "search doesn't work"). Hidden is hidden, in every theme. */
.tile.filtered { display: none !important; }
/* ...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; }
/* The galleries a search finds, in a line over the photographs it finds (search.js; George, 2 October
   2026: "when searching at heart.photos also include gallery names in the search results"). Each a
   pill like the photo panel's, with how many photos it holds. */
.search-gals { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 2px var(--gap) 12px; }
.search-gals-head { margin-right: 4px; font-size: 12.5px; color: var(--muted); }
.search-gal {
  display: inline-flex; align-items: center; gap: 7px; padding: 3px 11px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--panel);
  font-size: 12.5px; color: var(--ink); text-decoration: none;
}
.search-gal:hover, .search-gal:focus-visible { border-color: var(--accent); outline: none; }
.search-gal-n { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.search-gals-more { font-size: 12px; color: var(--muted); }

/* --- who is looking ------------------------------------------------------------------------ */
.account { flex: 0 0 auto; margin: 0 0 0 auto; position: relative; }
/* Multiselect and who you are, together at the end of the line that stays. */
.top-end { display: flex; align-items: center; justify-content: flex-end; gap: 10px; min-width: 0; }
/* Pricing and Multiselect, one group (gallery.html). Pricing is a plain word in the header's quiet
   color that answers the pointer the way the name's own links do. */
.top-acts { display: flex; align-items: center; gap: 12px; }
/* Multiselect's white, and Multiselect's hover color (George, 30 September 2026: "Make "Pricing" in
   the header at heart.photos the same white as "Multiselect" then the same hover color as
   "Multiselect"). */
.top-link { color: var(--ink); text-decoration: none; font-size: 14px; white-space: nowrap; transition: color .15s ease; }
.top-link:hover, .top-link:focus-visible { color: var(--accent); }
/* Pricing beside the name, and on a phone beside Multiselect instead (George, 2 October 2026). */
.top-name .top-pricing { align-self: center; }
/* ...as far from the name as Create image is when Create image is not there (George, 17 October 2026: ""Pricing" is too
   close to "Photos" logo when "Create image" isn't there, "Pricing" should be the same distance as "Create image""). */
.top-name .top-create[hidden] + .top-pricing { margin-left: 16px; }
html:not(.mobile) .top-pricing-phone, html.mobile .top .top-pricing { display: none; }
/* ...and Create a new image before it, in both places (George, 8 October 2026), a button that reads as
   the link beside it. */
/* The page's line height too, which a button does not take by itself: with its own ("normal", 18.7px
   against Pricing's 21px) its words sat half a pixel higher, a whole pixel on a screen at 150% (George,
   9 October 2026: "the "Create a new image" is not aligned with "Pricing""). */
button.top-link { border: 0; background: transparent; padding: 0; font-family: inherit; line-height: inherit; cursor: pointer; }
/* ...with room before it and between it and Pricing (George, 10 October 2026: "Add more whitespace before "Create
   image" to give it more breathing room ... and add more space between "Create image" and "Pricing""). */
.top-name .top-create { align-self: center; margin: 0 12px 0 16px; }
/* Get extension, after Pricing (getext.js). */
.top-name .top-getext { align-self: center; margin: 0 0 0 16px; }
.top-getext[hidden] { display: none; }
html:not(.mobile) .top-create-phone, html.mobile .top .top-create { display: none; }
/* Create image's AI sparkle (George, 10 October 2026: "with the "by AI" icon"), on the words' line and in their
   color, the accent on hover as they are. Set by its size and a nudge down, not by making the button a flex
   box: a display rule on the button would show it to somebody signed out, over its hidden. */
.top-link .ai-mark { width: 16px; height: 16px; margin-right: 6px; vertical-align: -3px; }
/* ...and what Multiselect does, on a line of its own under that one, under the button. */
.top .selbar { grid-column: 1 / -1; margin: 0 0 0 auto; }

/* --- 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 ENDS of a row are inset by --gap, which is what the header and the words above it are inset
   by: the first photograph of every row starts where the name of the site does, rather than a hair
   to the left of it.
   The seam BETWEEN two photographs is far less. The two used to be one number, on the reasoning
   that a wall should space itself evenly — but they are not doing the same job. The margin holds
   the wall off the edge of the window; the seam only has to say "two pictures, not one", and at
   the margin's width it was spending the wall's own room saying it. */
.grid { display: flex; flex-wrap: wrap; gap: var(--tile-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); }
/* A PAIR side by side (George, 7 October 2026: "show paired photos ... in the thumbnails with a
   connected icon like we use at he.art/photos"): the two halves in one cell, as a series is, with no
   outline, and the chain in the seam between them at the middle of the pictures. --seamf is the wide
   one's share of the cell, set by wall.js pairCell; the cell's padding is 3px and its gap 4px. */
.series-cell.pair-cell { position: relative; outline: none; }
.pair-link {
  position: absolute; z-index: 4; pointer-events: none;
  left: calc(3px + var(--seamf, 0.6) * (100% - 10px) + 2px);
  top: calc(3px + var(--row-h, 240px) / 2);
  transform: translate(-50%, -50%);
  width: 30px; height: 30px; border-radius: 999px; display: grid; place-items: center;
  background: #8a7dff; color: #fff; box-shadow: 0 0 0 3px #0b0b0d;
}
.pair-link svg { width: 15px; height: 15px; }
.series-cell.pair-cell.collapsed .pair-link { display: none; }
/* ...and in the viewer's row of thumbnails, on the tall half's thumbnail, straddling the gap to its wide
   one (strip.js): that thumbnail lets it out of its box, the picture still filling the box exactly. */
.strip-thumb.pair-tall { overflow: visible; }
.strip-pair-link {
  position: absolute; z-index: 3; pointer-events: none;
  left: calc(var(--seam, 2px) / -2); top: 50%; transform: translate(-50%, -50%);
  width: 22px; height: 22px; border-radius: 999px; display: grid; place-items: center;
  background: #8a7dff; color: #fff; box-shadow: 0 0 0 2px #0b0b0d;
}
.strip-pair-link svg { width: 12px; height: 12px; }
.viewer.strip-left .strip-pair-link, .viewer.strip-right .strip-pair-link { left: 50%; top: calc(var(--seam, 2px) / -2); }
/* 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; }
/* ...and it is drawn as the STACK it is: two cards behind the photograph in hand, tipped a degree
   apart, and the number of photographs on the corner. Collapsed to one picture it looked exactly
   like a single tile being carried, so a reader moving four of them had nothing to say the other
   three were coming — until the drop, where four photographs landed somewhere they had chosen for
   one. The cards are drawn rather than real tiles: the rows re-solve around the cell as it is
   carried, and three tiles' worth of box would re-solve them around a block. */
.series-cell.collapsed.tile-dragging { overflow: visible; }
.series-cell.collapsed.tile-dragging::before,
.series-cell.collapsed.tile-dragging::after {
  content: ''; position: absolute; inset: 3px; z-index: -1; pointer-events: none;
  border-radius: 10px; background: #1b1b21;
  outline: 1px solid rgb(255 84 112 / .45); outline-offset: -1px;
  box-shadow: 0 8px 20px rgb(0 0 0 / .45);
}
.series-cell.collapsed.tile-dragging::before { transform: translate(11px, 11px) rotate(2deg); }
.series-cell.collapsed.tile-dragging::after { transform: translate(5px, 5px) rotate(-1.2deg); }
/* How many are coming, said in words on the corner of the one in hand. */
.series-cell.collapsed.tile-dragging > .tile::before {
  content: attr(data-carrying) " photos"; position: absolute; z-index: 5; top: 6px; left: 6px;
  padding: 2px 8px; border-radius: 999px; pointer-events: none;
  background: var(--accent, #ff4d6d); color: #1a0a10;
  font-size: 11px; line-height: 1.5; font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
  .series-cell.collapsed.tile-dragging::before,
  .series-cell.collapsed.tile-dragging::after { transform: translate(8px, 8px); }
}
.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));
  /* ...and above 100% Thumbnail size, no more than a tenth wider than its own shape (--grow-max,
     wall.js). A row that holds one or two big photographs gave them the whole width and cut half of
     each one away; at 100% and below nothing caps it and every row still ends flush. */
  max-width: calc(var(--r) * var(--row-h, 240px) * var(--grow-max, 1000));
  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); }
/* Play on a tile that plays, as the viewer's row has it (George, 10 October 2026: "yes please also on the
   wall"); pressed, the photograph opens playing (wall.js). In the top-left corner since that evening, a small
   disc like the mark's, and always there on a tile whose photograph plays by itself ("move the video icon to the
   top-left corner of thumbnails that can play as a video and always show it not just on hover on both the wall
   and gallery"; .plays-own, wall.js); in a gallery that zooms, where every tile plays, under the pointer. Twice
   the size since later that evening ("double the size of the play/pause button in the thumbnails"). At the
   middle of the left edge since the next day (George, 11 October 2026: "move the Play icon to the left center of
   the thumbnail instead of the top-left"), which gives the mark its corner back. */
.tile-play {
  position: absolute; left: calc(50% - 24px); top: calc(50% - 24px); z-index: 3; width: 48px; height: 48px;
  display: flex; align-items: center; justify-content: center; border-radius: 50%;
  background: rgb(12 12 16 / .62); color: #fff; opacity: 0; pointer-events: none; cursor: pointer;
  transition: opacity .15s ease, background .15s ease;
}
.tile.plays-own .tile-play, .tile.plays:hover .tile-play, .tile.plays:focus-visible .tile-play { opacity: 1; pointer-events: auto; }
.tile-play:hover { background: rgb(0 0 0 / .8); }
/* IN THE MIDDLE (George, 12 October 2026: "When "Video tag" is off ... show the circle with a triangle play/pause
   icon in the center of thumbnails not the left center", and "When "Video tag" is on ... show the circle ... in
   the center of thumbnails only on hover"). It was at the middle of the left edge for a day, and in the top-left
   corner before that. With Video tag off (html.video-disc) it is always there on a tile that plays by itself;
   with it on, the word says so and the disc comes under the pointer alone, on any tile that plays. */
html:not(.video-disc) .tile.plays-own:not(:hover):not(:focus-visible) .tile-play { opacity: 0; pointer-events: none; }
.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; }
/* While a photograph is carried the rows are PINNED (see pinRows in wall-drag.js): a break after the
   last photograph of each row holds every row to the photographs already on it, so a change of
   places can never send a picture to the row above just because it now fits there. The break IS the
   gap between the rows — the rows close up to nothing and it stands in for them — and a break with
   nothing before it, an emptied row, gives no gap at all rather than two. */
.grid.pinned { row-gap: 0; }
.row-break { flex: 0 0 100%; height: var(--tile-gap); }
.grid.pinned > .row-break:first-child,
.grid.pinned > .row-break:last-child,
.row-break + .row-break { display: 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 — or the POINTER has, which moves the selection exactly as the keys do.
   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.
   On :focus and not on :focus-visible. Selection here IS focus, and a focus arrived at by pointer
   is deliberately not focus-visible in any browser — quite rightly, since that pseudo-class exists
   to draw the BROWSER's ring for a keyboard user. This is the wall's own mark and means the same
   thing whichever way the reader got there; under focus-visible the selection moved under the hand
   with nothing on screen saying so. It is not on :hover either, which would paint a second mark on
   whatever a still hand happened to be resting over — see the pointermove in wall.js.
   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::after {
  content: ""; position: absolute; inset: 0; z-index: 4; border-radius: inherit;
  /* A hairline. At three pixels the mark was a frame, and a frame around a photograph reads as
     part of the photograph — it took a corner of every picture it was on. One pixel of the accent
     with one of white outside it is as findable on a wall of thumbnails and takes nothing. */
  box-shadow: inset 0 0 0 1px var(--accent), inset 0 0 0 2px rgb(255 255 255 / .3);
  pointer-events: none;
}
/* ...but while a photograph is being CARRIED, the only red rectangle on the wall is the one in the
   hand. The carried picture draws its own outline (see .tile-dragging), and this mark — the
   photograph the viewer was last on, or whichever tile holds focus, which after a press is the one
   being dragged — put a second identical rectangle somewhere else at the same time. Two identical
   marks say two different things and the screen does not say which is which; during a drag there is
   only one thing worth marking, and it is the one under the hand. */
.grid.rearranging .tile.current::after,
.grid.rearranging .tile:focus::after { content: 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); }

/* Hidden means not there. The buttons in this panel's title bar are given a display of their own,
   which outranks the browser's own rule for [hidden] — so the pin, hidden because there is no side
   to pin to, stayed on screen and pinned things anyway. */
#keys-pin[hidden] { display: none !important; }

/* A rating, as a chip. The same icons the mark on a photograph is made of, at the same size, so
   the chip along the top and the badge in the corner are plainly one thing said twice. */
.tag-chip.tag-rating .tag-name { display: inline-flex; align-items: center; gap: 1px; }
/* A standing chip with nothing behind it yet: its place is kept, quietly, so the mark is where it
   was the last time anybody looked (George, 19 September 2026). */
.tag-chip.tag-rating.tag-empty:not(.on) { opacity: .55; }
.tag-chip.tag-rating svg { display: block; width: 14px; height: 14px; }
.tag-chip.tag-rating.rating-3 svg, .tag-chip.tag-rating.rating-4 svg { color: var(--accent); }
/* The chips for the bottom of the scale carry the same marks the photographs do. */
.tag-chip.tag-rating .like-emo { display: inline-block; font-size: 11px; line-height: 1; }
/* Lit, the chip is the accent and the hearts on it would vanish into their own background. */
.tag-chip.tag-rating.on svg { color: currentColor; }
.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;
  box-sizing: border-box; min-height: var(--tags-h);
  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; }
/* With nothing left to say it takes no room. It is the sentinel infinite scroll watches, so it
   stays IN the page — but a wall that has reached its end was ending in a hand's width of nothing,
   and the line under it made that two hands. */
.status:empty { padding: 4px 0; min-height: 0; margin: 0; }
/* Load more, every hundred photographs (feed.js). */
.wall-more { text-align: center; margin: 10px 0 26px; }
.wall-more[hidden] { display: none; }
/* The page bar past a page of two hundred (feed.js PAGE): Previous page, where it is, Next page. */
.wall-more { display: flex; justify-content: center; align-items: center; gap: 14px; }
.wall-more [hidden] { display: none; }
.wall-page { font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }
.wall-more .ctl-text { padding: 0 22px; color: var(--ink); background: var(--panel); border: 1px solid var(--line); }
.wall-more .ctl-text:hover, .wall-more .ctl-text:focus-visible { border-color: var(--accent); color: var(--accent); background: var(--panel); }
html.galleries-open .wall-more { margin-left: var(--gal-w, 232px); }
html.mobile.galleries-open .wall-more { margin-left: 0; }

/* Back to top (George, 27 September 2026), Vi.ki's control: the bottom-right corner once the wall
   has been scrolled (totop.js adds .on), standing above whatever the corner dock holds (--dock-h).
   visibility rather than display, so it fades, and leaves the tab order while it is not offered. */
.to-top {
  position: fixed; right: 14px; bottom: calc(14px + var(--dock-h, 0px) + var(--heart-ai-launcher, 0px)); z-index: 50;
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: 999px; box-sizing: border-box;
  background: var(--bg); color: var(--ink); border: 1px solid var(--line);
  box-shadow: 0 8px 24px rgb(0 0 0 / .35);
  opacity: 0; visibility: hidden; transform: translateY(6px);
  transition: opacity .18s ease, transform .18s ease, visibility 0s linear .18s;
}
.to-top.on { opacity: 1; visibility: visible; transform: none; transition-delay: 0s; }
.to-top:hover, .to-top:focus-visible { color: var(--accent); border-color: var(--accent); outline: none; }
.to-top svg { display: block; }
@media (prefers-reduced-motion: reduce) { .to-top, .to-top.on { transition: none; } }
/* Which build is on screen: a footnote, and sized like one. It had no rule at all, so it was a
   paragraph — full leading, an em of margin above and below — for four small words. */
.build { margin: 2px 0 8px; padding: 0 12px; font-size: 11px; color: var(--muted); opacity: .7; }
/* The privacy and copyright policies beside it, in the same small print until they are pointed at. */
.build a { color: inherit; text-decoration: none; }
.build a:hover { color: var(--ink); text-decoration: underline; }
.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; }
/* The way out of Only my content, under what it says (feed.js showEmpty). */
.empty .empty-all { display: block; margin: 14px auto 0; }

/* 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. */
/* One size for the words in the panel, 13px, the size of the File and Source lines (George,
   25 September 2026): the labels above them are the only smaller type, and the photo's own name at
   the top the only bigger. */
.ai-title { font-size: 13px; color: #e9e9f0; }
.ai-summary { font-size: 13px; line-height: 1.55; color: #c9c9d4; }
/* What drew a picture. The words are the long part and read as a quotation of the ask; the model
   and the seed are short facts, the seed in the same monospace the file's own numbers use. Each
   line opens with its name in the panel's quiet label color. */
.pmade-line { font-size: 13px; }
.pmade-model { font-size: 13px; color: #e8e8ef; }
.pmade-prompt { margin: 4px 0 0; font-size: 13px; line-height: 1.5; color: #c9c9d4; }
.pmade-seed { margin-top: 4px; font-size: 13px; color: #9a9aa8; }
.pmade-num { font-family: ui-monospace, Consolas, monospace; }
.pmade-key { color: #8a8a98; }
.pmade-link { color: #8ab4ff; text-decoration: none; transition: color .18s ease; }
.pmade-link:hover, .pmade-link:focus-visible { color: #cfe0ff; }
.pmade-line[hidden], .pmade-prompt[hidden] { display: none; }
/* A Sharpen pair's Shown first choice (twins.go): the visibility chips' own look, in a row. */
.pmade-first { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
.pfirst-chips { display: inline-flex; gap: 6px; }
/* The copy buttons after the prompt and the seed (George, 28 September 2026): quiet until pointed
   at, and a tick in the accent for a moment once the words are on the clipboard. */
.pmade-copy {
  display: inline-flex; align-items: center; justify-content: center; vertical-align: middle;
  width: 22px; height: 22px; margin: -3px 0 -2px 1px; padding: 0;
  background: none; border: 0; border-radius: 6px; color: #8a8a98; cursor: pointer;
  transition: color .15s ease, background .15s ease;
}
.pmade-copy:hover { color: #fff; background: rgb(255 255 255 / .1); }
.pmade-copy svg { display: block; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.pmade-copy .pc-done, .pmade-copy.done .pc-copy { display: none; }
.pmade-copy.done .pc-done { display: inline; }
.pmade-copy.done { color: var(--accent); }
.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, .top .top-select {
  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], .top .top-select[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, .top .top-select: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, .top .top-select.on {
  background: var(--accent); border-color: var(--accent); color: var(--on-accent);
}
.tag-chip.on .tag-n { color: color-mix(in srgb, var(--on-accent) 78%, transparent); }
/* The gallery in hand, beside All. A collection is not a word — pressing it goes somewhere rather
   than narrowing what is here — so it says so with a mark of its own rather than by being a chip
   that happens to be first. */
.tag-chip.tag-gallery::before {
  content: ""; width: 11px; height: 9px; flex: 0 0 auto; margin-right: 1px;
  border: 1.5px solid currentColor; border-radius: 2px; border-top-width: 3px;
  opacity: .85;
}
/* Putting photographs in it, which is a thing you DO rather than a place you are: an outline, and
   the accent only while it is on — the wall is a set of switches while that is true, and a control
   that turns the wall into something else should be lit while it has. */
.tag-chip.tag-add { border-style: dashed; }
.tag-chip.tag-add.on { border-style: solid; }
/* Only my content: its words and the on/off pill after them (tagbar.js mineChip; George, 8 October
   2026: "Make "Only my content" on heart.photos a pill not a toggle button"), as Browse is in the control
   row: the words never fill with the accent, and the pill alone says whether it is on. */
.tag-chip.tag-mine { gap: 8px; padding-left: 6px; padding-right: 4px; border-color: transparent; background: transparent; }
.tag-chip.tag-mine:hover { border-color: transparent; background: transparent; color: var(--accent); }
.tag-chip.tag-mine .gear-pill { pointer-events: none; }
.tag-chip:focus-visible, .tagbar-acts .ctl-text:focus-visible, .top .top-select: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;
  /* 10px on the left, where the round x now starts the bar: its circle sits in the pill's own
     rounded end, the same distance from the edge all the way round. */
  max-width: min(94vw, 920px); padding: 10px 14px 10px 10px; 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; }
/* With nothing to say, neither takes a place in the row: two empty slots and their gaps were
   enough to push the bar onto a second line (George, 30 September 2026). The spacer goes; the
   message stays in the page, as a live region must to be heard when it speaks, and hands its gap
   back. */
.selbar-gap:has(+ .selbar-say:empty) { display: none; }
.selbar-say:empty { margin-left: -8px; }
/* 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); }
/* Who can see the chosen photographs: a chooser dressed as the field beside it. */
.selbar-vis {
  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 10px; cursor: pointer;
}
.selbar-vis:focus { outline: none; border-color: var(--accent); }
.ctl-text.sel-go { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.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; }
/* The word the row is narrowed to, pressed in the panel (toggleWordHere): lit as the gallery the photo
   is open in is, and pressed again it lets go. */
.ptag.on {
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.ptag.on .ptag-go { color: var(--accent); font-weight: 600; }
.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; }
/* The viewer is exactly as tall as the screen IS, which is not the same as 100%.
   A modal dialog lives in the top layer, so its percentage height resolves against the initial
   containing block — the viewport as it would be with the browser's own bars out of the way. On a
   phone with the address bar showing, that is taller than anything you can see, and everything
   follows from it: the picture is fitted to a pane whose foot is under the chrome and so is drawn
   too big, which in landscape makes it too WIDE as well and runs it off the side; and the row of
   controls, sitting at the bottom of that box — the way out, the pair switch, the "i" — is below
   the fold entirely. dvh is the viewport as it actually is at this moment, bars and all. */
@supports (height: 100dvh) {
  .viewer[open] { height: 100dvh; max-height: 100dvh; }
}
/* ...and as WIDE as the screen is, for the same reason and only on a phone. The width is measured
   the same way and can be wrong the same way — a viewer wider than the screen centres the
   photograph on a middle that is off the side of it, which is a picture pushed into the right-hand
   edge with black where it should have been. Left alone elsewhere: on a desktop the difference is
   a scrollbar, and 100dvw would count it in. */
@supports (width: 100dvw) {
  html.mobile .viewer[open] { width: 100dvw; max-width: 100dvw; }
}
.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;
}
/* How much of the window is NOT the photograph, on each side: the panels. The row is what you press
   while looking at the picture, so it belongs under the picture — it keeps inside these, whether it
   is in the flow (padding) or laid on the photograph (insets), and the panels then have the full
   height beside it instead of stopping short of a row that ran the whole width of the window.
   Nought unless a panel is open, and nought on a phone at any time: there a panel IS the window. */
.viewer { --bar-l: 0px; --bar-r: 0px; }
@media (min-width: 701px) {
  .viewer.sidebar-open { --bar-r: var(--sb-w); }
  .viewer.sidebar-open.strip-right { --bar-r: calc(var(--sb-w) + var(--strip-w)); }
  .viewer.keys-open { --bar-l: var(--keys-w); }
}
.stage > .controls { padding-left: calc(var(--bar-l) + 10px); padding-right: calc(var(--bar-r) + 10px); }
/* 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; }
/* Holding its place on a photograph with no tall half. visibility rather than display, because the
   whole point is that the width stays: hidden it still measures, so nothing either side of it
   moves as the arrows walk from a paired photograph to an unpaired one. */
.sides.reserved { visibility: hidden; pointer-events: none; }
/* 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; }
/* The arrows either side of it only when Next/prev buttons is on (setNavShow), as on Vi.ki: the arrow
   keys, a swipe and the thumbnails all step without them (George, 1 October 2026, "default off"). */
.viewer:not(.nav-on) #prev, .viewer:not(.nav-on) #next { 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-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 {
  /* Times THUMBNAIL SIZE (--thumb-scale, wall.js), the one setting the wall's rows answer to as
     well (George, 25 September 2026), and never more than two fifths of the window: past that the
     row is taking the photograph's room rather than showing it. Every other size below is written
     the same way. */
  --strip-h: min(calc(176px * var(--thumb-scale, 1)), 40vh);
  /* 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: min(calc(256px * var(--thumb-scale, 1)), 40vw);
  /* 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: min(calc(120px * var(--thumb-scale, 1)), 40vh);
    --strip-w: min(calc(176px * var(--thumb-scale, 1)), 40vw);
  }
}
.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;
  /* In its photograph's own shape (--r, strip.js applyThumbShape) at the row's thickness, held
     between half as wide as tall and 2.4 times as wide (George, 1 October 2026: "Thumbnails should be
     in the same aspect ratio as the image ... a reasonable minimum width and height"): a 9:16
     portrait and a 21:9 frame are drawn whole, anything thinner or wider is cropped to that, and
     one with no shape known keeps the old box. Down the side, the column's width is the thickness. */
  flex: 0 0 auto; height: var(--strip-h); padding: 0;
  width: calc(var(--strip-h) * clamp(.5, var(--r, 1.4545), 2.4));
  /* 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;
}
/* The thumbnail on screen, and the one under the pointer, are marked by a THICK BAR along the edge
   that faces the big picture, in the colors the thin frame had (George, 3 October 2026: "instead of
   a border around the thumbnail currently visible in the gallery let's instead use a thick line
   covering the space between the thumbnail and the big image ... because the current thin border is
   hard to see on some thumbnails"). A one-pixel frame vanished into a red painting's own edge; a bar
   is read as "this one is up there". --bar is that edge as an inset shadow's offset: the top with
   the row along the foot, the bottom with it along the top, the inner side with it down a side.
   One pixel since the same evening (George: "change the current thumbnail indicator to 1px thick
   instead of 4px thick"), then two (George, same evening: "change the current thumbnail indicator to
   2px thick instead of 1px thick"): on the one edge facing the picture, not round the whole
   thumbnail. */
.strip-thumb { --bar: 0 2px; }
.viewer.strip-top .strip-thumb { --bar: 0 -2px; }
.viewer.strip-left .strip-thumb { --bar: -2px 0; }
.viewer.strip-right .strip-thumb { --bar: 2px 0; }
.strip-thumb:hover::after { box-shadow: inset var(--bar) 0 0 rgb(255 255 255 / .7); }
.strip-thumb:focus-visible::after { box-shadow: inset 0 0 0 1px #fff; }
.strip-thumb.on::after { box-shadow: inset var(--bar) 0 0 var(--accent); }
/* A thumbnail that plays shows Play under the pointer (George, 10 October 2026: "add a play icon as a hover
   effect over thumbnails that are youtube videos or infinite zoom galleries or animated gifs"): a video's
   photograph, an animated one or a frame of one (.plays, strip.js), and every photograph of a gallery that
   zooms, whose seek row is up. The one on screen shows nothing while it plays (the same day: "on thumbnails
   only show the play button and, once the video starts playing, hide the pause button. Only show the play
   button on hover when the video isn't playing"). Pressing the one on screen is pressing the photograph
   (strip.js). Whether it plays is read from the Play buttons themselves, so nothing keeps it in step. */
/* In the thumbnail's top-left corner since that evening, a small disc like the mark's, and always there on one
   that plays by itself (George: "move the video icon to the top-left corner of thumbnails that can play as a
   video and always show it not just on hover on both the wall and gallery and change it to a pause while the
   video is playing because now that it's not in the center it isn't hiding important parts of the thumbnail");
   in a gallery that zooms, where every one plays, under the pointer. Pause on the one playing. Twice the size
   since later that evening ("double the size of the play/pause button in the thumbnails"). At the middle of the
   left edge since the next day (George, 11 October 2026: "move the Play icon to the left center of the thumbnail
   instead of the top-left"), which gives the mark its corner back. */
.strip-play {
  position: absolute; left: calc(50% - 18px); top: calc(50% - 18px); z-index: 3; width: 36px; height: 36px;
  display: none; align-items: center; justify-content: center; border-radius: 50%;
  background: rgb(12 12 16 / .62); color: #fff; pointer-events: none;
}
.strip-play .sp-pause { display: none; }
.strip-thumb.plays .strip-play,
.viewer:has(#zq-seek-row:not([hidden])) .strip-thumb:hover .strip-play { display: flex; }
/* ...and, after the arrow keys, the one on screen in a gallery that zooms (George, the same day: "if navigating
   with arrow keys then show the play icon on the thumbnail when the current image can be played and is
   paused"; .keyed, keys.js, gone at the next press of a pointer). */
.viewer.keyed:has(#zq-seek-row:not([hidden])) .strip-thumb.on .strip-play { display: flex; }
.viewer:has(#mv-row:not([hidden]) #btn-mplay[aria-pressed="true"], #an-seek-row:not([hidden]) #btn-aplay[aria-pressed="true"],
  #zq-seek-row:not([hidden]) #btn-zplay[aria-pressed="true"]) .strip-thumb.on .strip-play { display: flex; }
.viewer:has(#mv-row:not([hidden]) #btn-mplay[aria-pressed="true"], #an-seek-row:not([hidden]) #btn-aplay[aria-pressed="true"],
  #zq-seek-row:not([hidden]) #btn-zplay[aria-pressed="true"]) .strip-thumb.on .strip-play .sp-play { display: none; }
.viewer:has(#mv-row:not([hidden]) #btn-mplay[aria-pressed="true"], #an-seek-row:not([hidden]) #btn-aplay[aria-pressed="true"],
  #zq-seek-row:not([hidden]) #btn-zplay[aria-pressed="true"]) .strip-thumb.on .strip-play .sp-pause { display: block; }
/* In the middle, as on the wall, and with Video tag on only under the pointer (George, 12 October 2026). */
html:not(.video-disc) .strip-thumb:not(:hover) .strip-play { display: none !important; }
/* VIDEO, in a word (George, 11 October 2026: "Add a new setting "Video tag" default On in the settings menu
   on the heart.photos homepage that shows a clear "video" tag in the top-left corner of a thumbnail with a
   video instead of the triangle in a circle, when Off then show the triangle in a circle as we currently
   do"): on a thumbnail whose photograph has a video (.has-video; wall.js, strip.js), on the wall and in the
   row, with no disc on it. Video tag off is html.video-disc (wallmenu.js applyThumbSigns): the word goes and
   the disc is back. An animated photograph and a gallery that zooms keep the disc either way. A solid dark
   plate under white letters with an edge of its own, so it reads over any photograph. The mark (marks.js)
   has the corner, and the word stands just right of it. */
.video-tag {
  position: absolute; left: 6px; top: 6px; z-index: 3; pointer-events: none;
  padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; line-height: 1.4; letter-spacing: .02em;
  background: rgb(12 12 16 / .82); color: #fff;
  box-shadow: 0 1px 4px rgb(0 0 0 / .5), inset 0 0 0 1px rgb(255 255 255 / .3);
}
.tile-pic:has(> .mark-badge) .video-tag { left: 34px; }
.strip-thumb .video-tag { left: 4px; top: 4px; padding: 1px 6px; font-size: 10px; }
.strip-thumb:has(> .mark-badge) .video-tag { left: 26px; }
html.video-disc .video-tag { display: none; }
/* Where the zoom is, on the photograph's own thumbnail (zoommark.js): the part of the picture on
   screen, as a box. White with a dark edge, so it reads on a sky and on a night alike; the
   thumbnail's own box cuts off whatever of it falls outside. */
.strip-zoom {
  position: absolute; z-index: 1; pointer-events: none; box-sizing: border-box;
  border: 1.5px solid #fff; border-radius: 1px;
  box-shadow: 0 0 0 1px rgb(0 0 0 / .55), inset 0 0 0 1px rgb(0 0 0 / .35);
}
/* 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; }
/* A place in the row for a photograph on its way. A link into the middle of a wall opens on its
   photograph before the wall has reached it, and the row beside it used to show the wall's FIRST
   photographs until then. It shows these instead: the size of a thumbnail, so nothing moves when
   the real one takes its place, and turning, so the row reads as filling rather than as empty. */
.strip-wait {
  position: relative; flex: 0 0 auto;
  width: var(--strip-w); height: var(--strip-h); background: #1a1a20;
}
.strip-wait::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 16px; height: 16px; margin: -8px 0 0 -8px; box-sizing: border-box;
  border: 2px solid rgb(255 255 255 / .16); border-top-color: rgb(255 255 255 / .6);
  border-radius: 50%; animation: cmp-spin .9s linear infinite, spin-late var(--spin-wait, 2s) step-end;
}
/* EVERY SPINNER WAITS TWO SECONDS (George, 15 October 2026: "don't show the loading spinner when pressing R unless it is taking over 2 seconds to start playing, same rule for all spinners only show them if the action is taking 2+ seconds"): a spinner drawn by the stylesheet alone is out of sight for
   --spin-wait from when it appears (util.js spinWait, set by gallery.js), so a place filled sooner never shows one. */
@keyframes spin-late { from, to { visibility: hidden; } }
/* ...the image component's ring and its "Loading the original…" too, from here: the component is the player's
   (player_lock_test.go) and its own sheet shows them at once, so the page's outweighs it on the pane. */
#pane .hic.loading .hic-spin { animation: spin-late var(--spin-wait, 2s) step-end; }
/* The photographs beside it that ARE known while the wall is still being walked to them: there to
   be seen, and opened once the wall has them. */
.strip[data-waiting] .strip-thumb:not(.on) { cursor: default; }
/* 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; }
/* The bar is at the top for every edge but the foot (bar-top, layout.js). */
.viewer.bar-top .stage > .controls { order: 1; }
.viewer.bar-top .stage > #pane { order: 2; }
/* The row's own space above and below the buttons: twice the seam. The seam is what separates two
   things that are next to each other; this is what separates a control from the edge of the thing
   it is laid on, and a control wants more air than a joint does. Both the flow and the overlay use
   it, or pressing Overlay would move every button by the difference. */
.stage > .controls { padding-top: calc(var(--seam) * 2); padding-bottom: calc(var(--seam) * 2); }
/* 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.strip-bottom .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 that is all it holds back. It used to clear the row as
   well — the row ran the full width of the window, so a panel level with it would have had buttons
   behind it — and the band that bought was a strip of window nobody could use: too short for
   anything, and exactly where a panel wants its last line. The ROW gives way instead now: it stays
   inside the photograph (see below), and the panels have the full height beside it. */
.viewer.strip-top[open]:not(.no-strip) .sidebar {
  top: var(--strip-h); bottom: 0;
}
/* EXTEND SIDEBAR (George, 25 September 2026; off unless turned on): the panel the whole height of the
   window beside a row along the top or the foot, and the ROW stopping at the panel's edge, with
   its arrows, so nothing in it is under the panel. Not on a phone, where the panel is the window. */
@media (min-width: 701px) {
  .viewer.sb-extend.strip-top[open]:not(.no-strip) .sidebar,
  .viewer.sb-extend.strip-bottom[open]:not(.no-strip) .sidebar { top: 0; bottom: 0; }
  .viewer.sb-extend.sidebar-open.strip-top .strip,
  .viewer.sb-extend.sidebar-open.strip-bottom .strip { right: var(--sb-w); }
  .viewer.sb-extend.sidebar-open.strip-top .strip-page.next,
  .viewer.sb-extend.sidebar-open.strip-bottom .strip-page.next { right: var(--sb-w); }
}
.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 are each their photograph's shape, so they are measured as the tile's are. */
.strip-thumb[data-rot="90"], .strip-thumb[data-rot="270"] { container-type: size; }
.strip-thumb[data-rot="90"] img,
.strip-thumb[data-rot="270"] img {
  position: absolute; top: 50%; left: 50%;
  width: 100cqh; height: 100cqw;
  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 {
  /* It starts where the PHOTOGRAPH does, which is the row's outer edge — not where the gear's own
     button ends, which is a few pixels inside the row and reads as a panel stuck to an icon.
     Between the two is the row's own padding: furniture against furniture, with no sliver of
     picture showing through, which is what eight pixels of air used to leave. */
  position: absolute; top: calc(100% + var(--seam) * 2); right: 0; z-index: 40; min-width: 268px;
  /* Never taller than the window. The menu grows a row whenever the viewer learns a new setting,
     and on a short screen the last of them went off the bottom — where a reader cannot reach it and
     cannot tell it is there. It scrolls inside itself instead. */
  max-height: calc(100dvh - 24px); overflow-y: auto; overscroll-behavior: contain;
  /* Lifted off the dark page (George, 17 October 2026: "Settings menu is hard to see in dark mode, can we make it a
     little more contrast from the dark background?"): a lighter surface than the page, a brighter edge and a
     deeper shadow, the way the extension's menus already stand off their panel (#26262f). */
  padding: 5px; border: 1px solid rgb(255 255 255 / .26); border-radius: 12px;
  background: rgb(36 36 44 / .98); box-shadow: 0 14px 40px rgb(0 0 0 / .7);
  backdrop-filter: blur(8px);
}
.gear-menu[hidden] { display: none; }
/* The funnel's menu hangs from a button in the MIDDLE of the row rather than at its end, so it is
   centered on its own button instead of pinned to the row's edge -- and it is wider than the gear's,
   because its two rows each say every one of their answers at once rather than yes or no. */
.sift-menu { right: auto; left: 50%; transform: translateX(-50%); min-width: min(380px, calc(100vw - 24px)); }
/* Named answers in one row are wider than the menu, so they wrap rather than pushing the menu
   sideways. The row has to be ALLOWED to shrink for that: every other pill is `flex: 0 0 auto`,
   which held Show at the full width of its six answers and gave the menu a scrollbar instead. Each
   answer stays in one piece.

   And each answer is a CHIP, with its own background, and the row has none (George, 25 September
   2026). One background behind a row that wraps keeps the row's full width, so it ran on past the
   chips of its shorter lines, which read as a mistake. */
.sift-menu .gear-pill.many {
  flex: 1 1 auto; min-width: 0; flex-wrap: wrap; justify-content: flex-start;
  gap: 4px; padding: 0; border-radius: 0; background: none;
}
/* The name, and its answers straight after it (George, 25 September 2026). The name took all the
   room between them and pushed the answers to the far side, which left a column of nothing down
   the middle of the menu. As wide as the longer of the two names, so both rows start together. */
.sift-menu .gear-name { flex: 0 0 4em; }
.sift-menu .gear-pill.many .gear-half { white-space: nowrap; background: rgb(255 255 255 / .1); }
.sift-menu .gear-pill.many .gear-half:hover:not(:disabled) { background: rgb(255 255 255 / .16); }
.sift-menu .gear-pill.many .gear-half.on { background: rgb(255 255 255 / .3); color: #fff; }
.sift-menu .gear-pill.many .gear-half:disabled { opacity: .45; }
.sift-menu .gear-row { align-items: flex-start; }
/* Lit while the wall is narrowed or sorted: a reader who left a sort on and came back deserves to
   be able to see WHY the wall is not the wall they remember. */
#btn-sift.on { color: var(--accent, #8a7dff); }
/* THE HEIGHT OF WHAT IT STANDS BESIDE. .ctl is a 32px circle, which is right for the round buttons
   at the ends of the row and three pixels proud of the rating pill at top and bottom -- and the
   rating pill is exactly what this button is about (George, 23 September 2026). So it takes that
   pill's height and sits flush with the three marks it reads the wall back by. */
.controls #btn-sift { width: 25px; height: 25px; }
.controls #btn-sift svg { width: 15px; height: 15px; }
/* 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(.bar-top) .gear-menu { top: auto; bottom: calc(100% + var(--seam) * 2); }

/* A row that is a number rather than a switch: the label, and the slider taking the rest. */
.gear-row-slider { cursor: default; gap: 10px; }
.gear-row-slider:hover { background: none; }
/* The name on one line: the slider is what gives up width, not the words. */
.gear-row-slider .gear-name { flex: 0 0 auto; white-space: nowrap; }
.gear-slider { flex: 1 1 auto; min-width: 90px; accent-color: var(--accent); cursor: pointer; }
/* ...and where it is set to, as a per-cent, where the other rows have their key. */
.gear-val {
  flex: 0 0 auto; min-width: 36px; text-align: right; font-size: 11px;
  color: rgb(255 255 255 / .62); font-variant-numeric: tabular-nums;
}

/* Everything back the way it came, at the foot of the menu: CENTRED, and dressed as the button it
   is -- the same violet pill the private-photo page offers "All photos" with (George, 21 September
   2026). It does not answer a question the way every row above it does, it acts, so it is the
   width of its own words and set apart from them.
   Disabled rather than taken away when there is nothing to put back: a button that comes and goes
   moves everything under it and leaves a reader hunting for it. */
.gear-reset {
  display: flex; align-items: center; width: fit-content; margin: 10px auto 4px;
  padding: 7px 16px; border-radius: 999px;
  background: rgb(138 125 255 / .3); border: 1px solid rgb(138 125 255 / .7); color: #e3e3e3;
  cursor: pointer; font: inherit; font-size: 13px;
}
.gear-reset:hover:not(:disabled) { background: rgb(138 125 255 / .42); border-color: rgb(138 125 255 / .9); color: #fff; }
.gear-reset:disabled { opacity: .38; cursor: default; }

.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; }
/* A display of its own beats the browser's [hidden] (see the note at the head of this file), and
   the switch is taken off the row on a gallery whose thumbnails all fit (strip.js). */
.browse-switch[hidden] { display: none; }
.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; }
/* Several named places rather than two halves of one switch — each pressed for itself, so each is
   a button and has to be told it is not a browser's idea of one. */
.gear-pill.many .gear-half {
  border: 0; background: none; font: inherit; font-size: 11px; cursor: pointer;
}
.gear-pill.many .gear-half:hover:not(:disabled) { background: rgb(255 255 255 / .12); color: #fff; }
.gear-pill.many .gear-half.on { background: rgb(255 255 255 / .2); color: #fff; }
.gear-pill.many .gear-half:disabled { cursor: default; }
/* A whole row of them that cannot be pressed — the thumbnails are away, so where they sit is not a
   question. Readable, so it still says what it is set to; the same dimming a disabled row has. */
.gear-row-off { opacity: .38; }
.gear-row-off:hover { background: none; color: #cfcfd8; }
@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. A band across the whole window darkens a stripe
   of every photograph in order to say something about eight small things in the middle of it; this
   says it where it is needed and leaves the rest of the picture alone.

   Centred with AUTO MARGINS, never a transform — and nothing here may filter, either. Both make an
   element the containing block for `position: fixed` inside it, and the settings menu is pinned to
   the window that way while it is open: each of them, in its turn, moved the menu a quarter of a
   screen from where it belonged.

   The vertical padding is the SAME seam 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 fade's own room comes from the pseudo-element below, which takes no part in the
   layout — it used to come from padding here, and padding moved every button. */
.viewer.overlay-bar .controls,
.viewer.no-bar .controls {
  position: absolute; z-index: 6;
  /* Between the photograph's own edges, not the window's — see --bar-l/--bar-r. The max-width has
     to come off the same two, or an over-constrained box gives up its auto margins and the row
     centres on the window and runs under the panel. */
  left: var(--bar-l); right: var(--bar-r); margin-left: auto; margin-right: auto;
  width: max-content; max-width: calc(100% - var(--bar-l) - var(--bar-r) - 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: calc(var(--seam) * 2) 14px; }
/* The seek lines take the width of the buttons and never give the row any (George, 10 October 2026: "make
   the header shading only apply to the header width not the full width of the video/image frame"). A seek
   row is a flex line of 100%, and fitSeekRow measures its line's offset from the row's own left edge: once
   the row was wide, that offset kept it wide, so a video's row held the whole bar, and the wash behind it,
   at the width of the frame. Contained, the row counts for nothing in the bar's width and fills what the
   buttons make. */
.viewer.overlay-bar .controls > .zq-seek-row { contain: inline-size; }
/* Above the hotkeys panel for as long as the settings are open — see setGear. The menu hangs
   inside this row and the row is its own stacking context, so the menu's own z-index is spent in
   here and cannot lift it past a panel outside. AFTER the rules that set this row's z-index, or it
   would lose the tie to them and change nothing. */
.viewer.gear-open .controls { z-index: 41; }
/* The wash, on a pseudo-element rather than on the row itself.
   Two reasons, both learnt the hard way. It reaches PAST the row's box, which is what gives the
   fade somewhere to happen — that room used to come from padding on the row, and padding moved
   every button the moment Overlay was pressed. And anything that filters (a backdrop blur, say)
   becomes the containing block for `position: fixed` inside it, which put the pinned settings
   menu a quarter of a screen from where it belonged; on a child of the row it traps nothing. */
.viewer.overlay-bar .controls::before {
  content: ''; position: absolute; z-index: -1;
  /* It STOPS where the bar stops, on the side the thumbnails are on: past that edge is the seam and
     then the thumbnails, and a wash reaching into them darkens pictures rather than furniture. It
     reaches out over the PHOTOGRAPH instead, which is the side that needs it and the only side
     with room to fade in.
     Rounded on that side alone. A corner is only a corner where something ends against something
     else: against the picture it is an edge of its own and wants rounding, and against the
     thumbnails it is a joint between two pieces of furniture, which wants a right angle. */
  /* Half the run it had. The fade was reaching a third of the way up the photograph before it
     was gone, which is a lot of picture spent on a gradient nobody is looking at. */
  inset: -17px 0 0;
  border-radius: 16px 16px 0 0;
  /* Two stops, so the darkening really is even from nothing to full. It had a third at 55 %, which
     held it near full for most of the run and then dropped it away — a curve, read as a band with
     a soft edge rather than as light falling off. */
  /* ...and half again as dark where it is darkest, since it now has half the distance to say the
     same thing in. */
  background: linear-gradient(to top, rgb(0 0 0 / .95), rgb(0 0 0 / 0));
}
/* Along the top, everything is the other way up: the photograph is below the bar. */
.viewer.overlay-bar.bar-top .controls::before {
  inset: 0 0 -17px;
  border-radius: 0 0 16px 16px;
  background: linear-gradient(to bottom, rgb(0 0 0 / .95), rgb(0 0 0 / 0));
}
.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.bar-top.no-strip .controls,
.viewer.no-bar.bar-top.no-strip .controls { top: 0; bottom: auto; }
/* 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; }
/* A link that leaves the page, wherever it is put. The look used to belong to the CAPTION — the
   two "Open" links beside the files — so the same helper, used in the panel for a photograph's
   source, produced the browser's own underlined purple instead. It belongs to the class the helper
   adds, so anything it makes looks like the others by being the same thing. */
.ext-link, #caption a, .orig-line a {
  color: #8ab4ff; text-decoration: none; font-size: 13px;
  transition: color .18s ease;
}
.ext-link:hover, .ext-link:focus-visible,
#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; }
/* The rating buttons carry a drawing where the others carry a word, so they are centred on it and
   are not asked to be as wide as the word "Fit". */
/* A pill that is not being offered is not there at all: .pair-switch sets its own display, which
   beats the browser's rule for [hidden] and left the rating buttons taking up the row while they
   were meant to be gone (19 September 2026). */
.pair-switch[hidden] { display: none; }
.controls .rate-switch button { display: inline-flex; align-items: center; justify-content: center; padding: 4px 8px; }
.controls .rate-switch svg { display: block; }
.controls .pair-switch button { padding: 4px 11px; }
/* Zoom is a place you get to, not one you press: unlit it is the one button in the row that does
   nothing, and it looks it. Lit, it is simply the lit one. */
.size-switch button[aria-disabled="true"]:not(.on) { color: rgb(255 255 255 / .38); cursor: default; }
.size-switch button[aria-disabled="true"]:not(.on):hover { color: rgb(255 255 255 / .38); }

/* 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; }
  /* ...and along whichever axis the row actually runs. Down a side it is a COLUMN, and pan-x
     tells the browser to refuse the one gesture that moves it: a finger dragged up the thumbnails
     did nothing at all, on the layout a phone held sideways gets by default. */
  .viewer.strip-left .strip-thumb, .viewer.strip-right .strip-thumb { touch-action: pan-y; }
  .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. 52px of
   track (100px until 12 October 2026, when George sent YouTube's volume slider: "make the zoom slider and
   volume slider the same length slider as the screenshot slider length"; YouTube's is 52px, and the
   volume's below is the same), 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 52px; width: 52px; min-width: 52px; max-width: 52px; }
@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;
  /* The settings menu's ground (.gear-menu) since 17 October 2026 (George, 17 October 2026: "on all vi.ki sites use the same background color for the dropdown from the logo and the avatar menu as for the settings dropdown and on heart.photos avatar dropdown and viewer info sidebar"); it was rgb(12 12 16 / .95). */
  background: rgb(36 36 44 / .98); 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; }

/* WHO CAN SEE THIS PHOTOGRAPH: the three chips He.art has always put beside one, dressed the way
   He.art dresses them (George, 23 September 2026). They arrived here with He.art's class names and
   none of its rules, which left three plain browser buttons in a panel that has no others.
   A pill with a hairline round it, the chosen one filled. The fill is THIS site's accent rather
   than He.art's violet: everything else lit in this panel is that colour, and one control in
   somebody else's brand reads as a control from somewhere else. */
.sb-chips { display: flex; flex-wrap: wrap; gap: 5px; }
.sb-chips-inline { flex-wrap: nowrap; }
.sb-chip {
  background: rgb(255 255 255 / .05); border: 1px solid rgb(255 255 255 / .14);
  color: rgb(233 233 240 / .75); border-radius: 999px; padding: 3px 11px;
  font: inherit; font-size: 12px; line-height: 1.45; cursor: pointer;
}
.sb-chip:hover { background: rgb(255 255 255 / .12); color: #fff; }
.sb-chip.sel {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  color: #fff;
}
.sb-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The thumbnails are above it rather than behind it: covering the end of the row of photographs
   would hide the very things it exists to offer. The button bar is not held back — it keeps to the
   photograph's own width now, so there is nothing of it out here to be covered. */
.viewer.strip-bottom[open]:not(.no-strip) .sidebar {
  bottom: 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); }
  /* ...and the ROW keeps inside the picture with it — said once, as --bar-l/--bar-r, where the row
     itself is laid out. */
}
/* 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; }
}

/* No lines between the parts of the panel (George, 24 September 2026): the space between them is
   enough to tell one from the next. */
/* Two columns, the x in the top right: the gallery the photo is open in, when there is one, on the first
   line and the photo's title under it; otherwise the title on the first line (sidebar.js
   renderGalleryHead). */
.sb-head {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: 12px; row-gap: 6px;
  flex: 0 0 auto; padding: 12px 12px 6px 16px;
}
.sb-head > #sb-close { grid-column: 2; grid-row: 1; }
/* ...at the top, level with the gallery's name, when the gallery's block is there above the title. */
.sb-head:has(> .sb-gal:not([hidden])) > #sb-close { align-self: start; }
.sb-gal { grid-column: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 3px; }
.sb-gal[hidden] { display: none; }
.sb-gal-title {
  max-width: 100%; color: #f2f2f7; font-size: 15px; font-weight: 700; line-height: 1.3; text-decoration: none;
  overflow-wrap: anywhere;
}
.sb-gal-title:hover, .sb-gal-title:focus-visible { color: var(--accent); text-decoration: underline; }
.sb-gal-desc {
  margin: 0; color: #a9a9b6; font-size: 12.5px; line-height: 1.45; white-space: pre-line; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.sb-gal-desc.open { display: block; overflow: visible; }
.sb-gal-desc[hidden], .sb-gal-more[hidden] { display: none; }
.sb-gal-more { font-size: 12px; padding: 0; }
/* + Moment (moment.js syncNow): the video's time to the thousandth and the button, one line between the gallery's
   block and the photo's title. The digits keep their width, so the line does not shake while it plays. */
.sb-head > .pmoment-now { grid-column: 1; min-width: 0; display: flex; align-items: center; gap: 10px; }
.pmoment-now[hidden] { display: none; }
.pmoment-now-time { font-variant-numeric: tabular-nums; color: #f2f2f7; font-size: 13px; letter-spacing: .02em; }
.pmoment-add { font-size: 12.5px; padding: 0; white-space: nowrap; }
.pmoment-add:disabled { opacity: .5; cursor: default; }
/* The temporary timestamps (moment.js drawMarks; George, 17 October 2026: "Pressing E create a temporary timestamp that shows as a selected chip on its own row below +Moment, clicking any other temporary timestamp chip seeks to that moment and pauses the video, pressing play plays from the currently selected temporary timestamp, pressing , or . while playing nudges the currently selected temporary timestamp rewinds to it and plays again so users can find the perfect audio start point then clicking +Moment with a selected temporary timestamp creates the moment at that timestamp and clicking a selected timestamp toggles it"): a row of their own under + Moment's,
   the chosen one lit. */
.sb-head > .pmoment-marks { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }
.pmoment-marks[hidden] { display: none; }
.pmoment-mark { flex: 0 0 auto; height: 24px; padding: 0 10px; margin: 0; border: 1px solid rgb(255 255 255 / .2);
  border-radius: 999px; background: none; color: #cfcfd8; font: inherit; font-size: 12px; letter-spacing: .02em;
  font-variant-numeric: tabular-nums; cursor: pointer; }
.pmoment-mark:hover, .pmoment-mark:focus-visible { border-color: var(--accent); color: #fff; outline: none; }
.pmoment-mark[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
/* The skip buttons (moment.js drawNudge), in one pill as the Heartbeat extension draws them: Play or Pause, the
   start, then a second, a tenth and a frame, each back and on, a thin line between the pairs. */
.sb-head > .pmoment-nudge { grid-column: 1 / -1; }
/* It fits the panel: the frame number sits under the time, the buttons are 28px, and on a panel narrower still the
   pill wraps rather than cutting off the last button (George, 18 October 2026: "Nudge buttons are cut off can't see the last nudge button"). */
.pmoment-nudge { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 1px; max-width: 100%; box-sizing: border-box;
  padding: 3px 6px; border: 1px solid rgb(255 255 255 / .16); border-radius: 20px; background: rgb(0 0 0 / .35); justify-self: start; }
.pmoment-nudge[hidden] { display: none; }
.pmoment-nudge button { flex: none; display: grid; place-items: center; width: 28px; height: 30px; padding: 0; margin: 0; border: 0;
  border-radius: 50%; background: none; color: #e9e9f0; cursor: pointer; }
.pmoment-nudge button:hover, .pmoment-nudge button:focus-visible { background: rgb(255 255 255 / .12); outline: none; }
.pmoment-nudge .nudge-time { display: inline-flex; flex-direction: column; line-height: 1.15; padding: 0 6px 0 2px; font-variant-numeric: tabular-nums;
  font-size: 14px; font-weight: 600; color: #f2f2f7; white-space: nowrap; }
/* ...a button that goes to that time (moment.js, 'at'): as wide as its words, a soft box on hover. */
.pmoment-nudge button.nudge-time { width: auto; height: auto; min-height: 30px; padding: 2px 8px; border-radius: 8px; align-items: flex-start;
  justify-content: center; font-family: inherit; text-align: left; }
/* Whitespace between the pairs, no bar (George, 19 October 2026: "Draw our icons using SVG to make them smooth at any resolution currently the circles look pixelated / Also remove our vertical separators, just whitespace is enough"). */
.pmoment-nudge .nudge-gap { width: 6px; flex: none; }
.pmoment-nudge svg text { font: 500 7px/1 Poppins, system-ui, sans-serif; fill: currentColor; }
/* The moment's start before the skip buttons moved it (moment.js revertStart): dashed, a way back. */
.pmoment-mark.pmoment-was { border-style: dashed; color: #9a9aa6; }
.pmoment-nudge .nudge-frame { font-size: 11px; font-weight: 400; color: #9a9aa6; }
/* An x on each one E added, on hover, as the extension's color chips have (.swatch-x), and Clear at the row's right
   end (George, 17 October 2026: "add an "x" icon to the temporary timestamps so they can be removed and a "Clear" link on the right edge as we have in the extension for clearing user-generated chips to clear all temporary timestamps / use the selected photo's moment start time as the first temporary timestamp that can't be removed (no "x" icon and clicking "Clear" doesn't remove it) and only show "Clear" when there is something to clear / pressing R while paused stays paused / selecting a temporary timestamp while playing keeps playing after jumping to that timestamp and if paused it stays paused"). */
.pmoment-chip { position: relative; flex: 0 0 auto; display: inline-flex; }
.pmoment-mark-x { position: absolute; top: -6px; right: -6px; width: 17px; height: 17px; padding: 0; margin: 0;
  border: 1px solid rgb(255 255 255 / .25); border-radius: 50%; background: #3a3a44; color: #fff; display: grid;
  place-items: center; cursor: pointer; opacity: 0; }
.pmoment-chip:hover .pmoment-mark-x, .pmoment-chip:has(:focus-visible) .pmoment-mark-x { opacity: 1; }
.pmoment-mark-x:hover { background: #d83a4a; }
.pmoment-clear { margin: 0 0 0 auto; padding: 0 2px; height: 24px; border: 0; background: none; color: #cfcfd8;
  font: inherit; font-size: 12.5px; cursor: pointer; }
.pmoment-clear:hover, .pmoment-clear:focus-visible { color: #fff; text-decoration: underline; outline: none; }
/* A frame of an animated photo (framewall.js) is no photo: the panel says what it can of it and offers
   to save it, and nothing that would change or file the photo it is a frame of. */
.sidebar.frame-entry .sb-body > .sb-sec:not(#pframe-sec):not(#pmeta-sec):not(#pauto-sec) { display: none; }
.sb-title { margin: 0; flex: 1 1 auto; min-width: 0; font-size: 14px; font-weight: 600; letter-spacing: .01em; }
/* ...where the title is the heading: the field takes the line, up to the x. */
.sb-head .pname { font-weight: 400; }
/* The title on a line of its own, the whole width (George, 17 October 2026: "use the pencil icon not in a circle and put the title on its own line it's still on the same line as +Moment and video time"):
   placed by itself it took the free cell beside + Moment's line, where the x is on the first line. */
.sb-head > .pname { grid-column: 1 / -1; }
.sb-head .pname-text { font-weight: 600; }
.sb-head .pname-text.untitled { color: #85858f; font-weight: 400; }
/* The pencil beside a title its reader may change (sidebar.js renderName): quiet until the pointer is on it. */
/* Plain, larger, and the same box whatever the pointer does (George, 17 October 2026: "remove the circular border behind the pencil and make it larger and stop the shifting on hover it currently has and group the photo title with the creator right now there is a large vertical space between the creator and the title"): it wore .ctl-text, whose pill showed behind it
   and whose padding gave way to this rule's on hover, so it moved under the pointer. */
.pname-edit { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; margin: 0; padding: 2px;
  line-height: 0; color: #85858f; background: none; border: 0; border-radius: 0; box-shadow: none; cursor: pointer;
  font: inherit; transition: color .15s ease; }
.pname-edit:hover, .pname-edit:focus-visible { color: #f2f2f7; background: none; outline: none; }
/* + Moment's question (moment.js askMoment): the title field the width of the box. */
.pmoment-dlg-in { width: 100%; box-sizing: border-box; margin: 4px 0 2px; }
/* Unlisted or Private, under the title (sidebar.js renderVisWord): a quiet pill in the panel's own
   colors (it stays dark in every theme, as .sb-chip does), Private a shade stronger. */
.pvis-words { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 6px; margin: 0 16px 4px; }
.pvis-words[hidden] { display: none; }
.padmin-note { flex: 0 0 auto; margin: 0 16px 6px; }
.padmin-note[hidden] { display: none; }
.pvis-word {
  flex: 0 0 auto; align-self: flex-start; margin: 0; padding: 1px 9px;
  border: 1px solid rgb(255 255 255 / .18); border-radius: 999px;
  background: rgb(255 255 255 / .07);
  color: #e9e9f0; font: inherit; font-size: 12px; font-weight: 600; line-height: 18px; letter-spacing: .01em;
  cursor: pointer;
}
.pvis-word[hidden] { display: none; }
.pvis-word[data-vis="private"], .pvis-word[data-vis="trash"] { background: rgb(255 255 255 / .16); border-color: rgb(255 255 255 / .28); }
/* Only where a press opens something (Trash, and Private on your own); a word otherwise. */
.pvis-word:disabled { cursor: default; }
.pvis-word:not(:disabled):hover { background: rgb(255 255 255 / .24); }
.sb-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 4px 16px 24px; }
/* Who made it right under the title, one group with it (George, 17 October 2026: "remove the circular border behind the pencil and make it larger and stop the shifting on hover it currently has and group the photo title with the creator right now there is a large vertical space between the creator and the title"). */
.sb-body > .sb-meta:first-child { padding-top: 2px; }
.sb-sec { display: flex; flex-direction: column; gap: 8px; padding: 12px 0; }
/* The size and the moment it was made: two quiet lines, read rather than worked. */
.sb-meta { gap: 3px; }
/* Who can see it: its chips straight under Created, as part of the same few lines. */
.sb-meta:not([hidden]) + #pvis-sec { padding-top: 0; margin-top: -3px; }
.pmeta { color: #a9a9b6; font-size: 13px; font-variant-numeric: tabular-nums; }
.pmeta[hidden] { display: none; }
/* ...and who made it, on the line under: their picture and their name, a link to their page, and Follow
   (George, 12 October 2026; sidebar.js renderCreator). */
.pby-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin-bottom: 4px; }
/* Their picture and name are one pill, the picture at its head, which lights under the pointer: the pill fills,
   the picture grows a little inside a ring of the accent, and the name brightens (George, 12 October 2026: "add
   a nice hover effect over the avatar and name"). Only where it goes somewhere (a[href]). */
.pby { display: inline-flex; align-items: center; gap: 8px; margin-left: -3px; padding: 3px 12px 3px 3px;
  color: var(--ink); text-decoration: none; border-radius: 999px;
  transition: background .15s ease, color .15s ease, box-shadow .15s ease; }
.pby[hidden], .pby-img[hidden] { display: none; }
.pby-img { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; flex: none;
  transition: transform .15s ease, box-shadow .15s ease; }
a.pby[href]:hover, a.pby[href]:focus-visible { background: rgb(255 255 255 / .1); color: #fff; outline: none;
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .16); }
a.pby[href]:hover .pby-img, a.pby[href]:focus-visible .pby-img { transform: scale(1.12); box-shadow: 0 0 0 2px var(--accent); }
a.pby[href]:active { background: rgb(255 255 255 / .16); }
@media (prefers-reduced-motion: reduce) { .pby, .pby-img { transition: none; } a.pby[href]:hover .pby-img { transform: none; } }
.pfollow { padding: 2px 12px; font-size: 12px; }
.pfollow[aria-pressed="true"] { background: none; color: var(--muted); }
/* The size, how long ago, and when: three parts with the same dot, the same distance either side, between each
   two that are there (renderMeta; George, 12 October 2026: "use the same separator between "hours ago" and "date
   time created" as you use between resolution and "hours ago""). */
#pdims:not([hidden]) ~ #pago:not([hidden])::before,
#pdims:not([hidden]) ~ #pcreated:not([hidden])::before,
#pago:not([hidden]) ~ #pcreated:not([hidden])::before { content: '\00b7'; margin-right: 8px; }
/* ...and between a gallery's, in its panel (sidebar.js renderNestPanel). */
#pnest-count ~ #pnest-ago:not([hidden])::before, #pnest-count ~ #pnest-created:not([hidden])::before { content: '\00b7'; margin-right: 8px; }
/* A creator's page (creators.go): Follow beside their name at the head of the wall (galleries.js). */
.head-follow { margin-left: 10px; vertical-align: middle; }
/* The size, the other resolutions the image is kept at, and All, on one line (same.js). */
.pdims-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }
.pdims-also { color: #8b8b98; }
.pdims-all { padding: 1px 8px; font-size: 12px; }
.pdims-all[hidden] { display: none; }
.sb-sec[hidden] { display: none; }
/* A section's heading: in the theme's own pink, in sentence case as it is written, the size of
   the words under it and a weight heavier (George, 25 September 2026). It was small gray capitals,
   which did not stand out from what they head. */
.sb-label { font-size: 13px; font-weight: 600; color: var(--accent); }
.sb-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.sb-row[hidden] { display: none; }
/* The layer editor (layers.js; George, 3 October 2026): over the whole viewer, the bar along the top
   and the photo fitted in the room below it, each layer a box over it, the chosen one framed in the
   accent with a grip at its corner. */
.layered {
  position: absolute; inset: 0; z-index: 90; display: flex; flex-direction: column;
  background: rgb(10 10 14 / .96); color: var(--ink); outline: none;
}
.layered-bar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 10px 14px; border-bottom: 1px solid rgb(255 255 255 / .1);
}
.layered-say { color: var(--muted); font-size: 13px; margin-right: 6px; }
/* The second line along the top: Size, X and Y for the chosen layer (George, 4 October 2026). */
.layered-sliders { padding-top: 6px; padding-bottom: 8px; gap: 18px; }
.layered-slide { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); }
/* Half as wide as they were (George, 4 October 2026: "Make the sliders half as wide"). */
.layered-slide input[type="range"] { width: clamp(45px, 7vw, 90px); accent-color: var(--accent); }
.layered-slide input:disabled { opacity: .4; }
/* The percentage, a field to type into or turn the wheel over, looking like the words around it. */
.layered-num { display: inline-flex; align-items: center; color: var(--ink); font-variant-numeric: tabular-nums; }
.layered-num input {
  width: 3em; padding: 1px 1px 1px 3px; border: 1px solid transparent; border-radius: 5px; background: transparent;
  color: inherit; font: inherit; text-align: right; -moz-appearance: textfield; appearance: textfield;
}
.layered-num input::-webkit-inner-spin-button, .layered-num input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.layered-num input:hover, .layered-num input:focus { border-color: rgb(255 255 255 / .28); background: rgb(255 255 255 / .06); outline: none; }
/* 100%, beside the field it sets: as small as the field, close to it. */
.layered-sliders .layered-100 { margin-left: -10px; padding: 2px 9px; font-size: 12px; }
.layered-pickwrap { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); }
.layered-pickwrap[hidden] { display: none; }
.layered-pick { max-width: 240px; font: inherit; color: var(--ink); background: rgb(255 255 255 / .06);
  border: 1px solid rgb(255 255 255 / .16); border-radius: 8px; padding: 4px 8px; }
.layered-gap { flex: 1 1 auto; }
.layered-bar .layered-save:not(:disabled) { background: var(--accent); color: #1a0a10; border-color: var(--accent); }
.layered-room { position: relative; flex: 1 1 auto; min-height: 0; display: grid; place-items: center; overflow: hidden; }
/* Publish under way (layers.js syncBar): "Creating image" over the picture, which takes no presses
   until it is done. */
.layered-busy {
  position: absolute; inset: 0; z-index: 5; display: grid; place-items: center;
  background: rgb(0 0 0 / .45); cursor: progress;
}
.layered-busy[hidden] { display: none; }
.layered-busy-say {
  display: inline-flex; align-items: center; padding: 10px 16px; border-radius: 12px;
  background: rgb(20 20 24 / .92); color: #fff; font: 600 15px/1.4 system-ui, sans-serif;
  box-shadow: 0 6px 24px rgb(0 0 0 / .45);
}
.layered-busy .cmp-spin { width: 16px; height: 16px; margin-right: 10px; vertical-align: 0; }
.layered-stage {
  position: relative; overflow: hidden; touch-action: none; user-select: none;
  background: repeating-conic-gradient(rgb(255 255 255 / .06) 0 25%, transparent 0 50%) 0 0 / 20px 20px;
  box-shadow: 0 10px 40px rgb(0 0 0 / .6);
}
.layered-base, .layered-layer { position: absolute; }
.layered-base { inset: 0; pointer-events: none; }
.layered-stage img { position: absolute; max-width: none; transform-origin: center; pointer-events: none; }
/* A layer shows only its part: the whole picture inside, cut to the box (layers.js place). */
.layered-clip { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.layered-part { position: absolute; }
.layered-layer { cursor: move; outline: 1px dashed rgb(255 255 255 / .35); outline-offset: 0; }
.layered-layer.on { outline: 2px solid var(--accent); }
.layered-grip {
  position: absolute; right: -7px; bottom: -7px; width: 14px; height: 14px; border-radius: 4px;
  background: var(--accent); border: 2px solid #1a0a10; cursor: nwse-resize; display: none;
}
.layered-layer.on .layered-grip { display: block; }

/* The Studio's edits (edits.js): a wrapping row of the panel's small buttons, the one being made
   breathing while the library makes it and the rest held still until it is done. */
.pedit-row { gap: 6px; }
.pedit-row .ctl-text.pedit-busy { animation: pedit-busy .9s ease-in-out var(--spin-wait, 2s) infinite alternate; }
@keyframes pedit-busy { from { opacity: 1; } to { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .pedit-row .ctl-text.pedit-busy { animation: none; opacity: .6; } }
/* The galleries a photo is in: pills, as its tags are (.ptag; George, 1 October 2026: "Use the same
   style as tag for listing the galleries a photo belongs to in the sidebar"), the ones the library
   made quieter, each with the tags' x where it can be taken out of it, and the gallery it is open
   in lit in the accent. */
.pgal-in { display: flex; flex-wrap: wrap; gap: 6px; }
.pgal-in[hidden] { display: none; }
.pgal-link { text-decoration: none; }
.pgal-link.auto { color: #a9a9b6; }
.ptag.pgal-item.here {
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.pgal-item.here .pgal-link { color: var(--accent); font-weight: 600; }
.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; position: relative; }
.pdesc-text { margin: 0; color: #d6d6e0; font-size: 13px; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }
/* A description's markdown, rendered (markdown.js), with room on the right for its keeper's pencil. */
.pdesc-md { white-space: normal; }
.pdesc[data-editable] .pdesc-md { padding-right: 26px; }
.pdesc-md p { margin: 0 0 7px; }
.pdesc-md p:last-child, .pdesc-md ul:last-child, .pdesc-md ol:last-child { margin-bottom: 0; }
.pdesc-md strong { color: #fff; font-weight: 600; }
.pdesc-md .md-h { color: #fff; font-weight: 600; font-size: 13.5px; }
.pdesc-md ul, .pdesc-md ol { margin: 0 0 7px; padding-left: 18px; }
.pdesc-md li { margin: 2px 0; }
.pdesc-md a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.pdesc-md code { font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: rgb(255 255 255 / .08); border-radius: 5px; padding: 1px 5px; }
.pdesc-edit {
  position: absolute; top: -3px; right: -4px; width: 26px; height: 26px; display: grid; place-items: center;
  border: 0; border-radius: 8px; background: transparent; color: #9a9aa6; cursor: pointer;
}
.pdesc-edit:hover, .pdesc-edit:focus-visible { background: rgb(255 255 255 / .1); color: #fff; outline: none; }
.pdesc-input {
  font: inherit; font-size: 13px; line-height: 1.5; color: #fff;
  /* As tall as its text, or its placeholder when it has none, and no taller than half the window:
     past that it scrolls rather than pushing everything below it out of the panel. */
  width: 100%; field-sizing: content; min-height: 0; max-height: 50vh; resize: none;
  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; }

/* Where the picture came from. The field and the way to check it sit on one line: pasting an
   address and then following it is one movement, and a link on its own row below would put the
   two halves of that movement in different places. */
.psrc-row { display: flex; align-items: center; gap: 10px; }
.psrc-input {
  flex: 1 1 auto; min-width: 0;
  font: inherit; font-size: 13px; color: #e9e9f0;
  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;
}
.psrc-input::placeholder { color: #85858f; }
.psrc-input:hover { border-color: rgb(255 255 255 / .35); }
.psrc-input:focus-visible, .psrc-input:focus {
  outline: none; border-color: rgb(255 255 255 / .6); background: rgb(255 255 255 / .1);
}
/* Saved, and not saved — the same answer the name and the description give. */
.psrc.saved .psrc-input { border-color: rgb(120 220 160 / .8); }
.psrc.bad .psrc-input { border-color: var(--accent); }
.psrc-open { flex: 0 0 auto; }
/* Read-only, it is the address itself, and a long one must not push the panel wider. */
.psrc .ext-link { overflow-wrap: anywhere; }

/* Credit, license, alt text and the tick (rights.js). One line each, label above the field, so a
   long license name and a long credit do not have to share a row. The tick is the one control in
   the panel that says something about the world rather than about the file, so it is spelled out
   in a sentence under it. */
.prights { display: flex; flex-direction: column; gap: 10px; }
.pr-line { display: flex; flex-direction: column; gap: 4px; }
.pr-label { color: #a7a7b2; font-size: 12px; letter-spacing: .02em; }
.pr-value { color: #e9e9f0; font-size: 13px; overflow-wrap: anywhere; }
.pr-input {
  font: inherit; font-size: 13px; color: #e9e9f0;
  background: rgb(255 255 255 / .06); border: 1px solid rgb(255 255 255 / .18);
  border-radius: 999px; padding: 6px 12px; min-width: 0;
  transition: border-color .18s ease, background .18s ease;
}
.pr-input::placeholder { color: #85858f; }
.pr-input:hover { border-color: rgb(255 255 255 / .35); }
.pr-input:focus-visible, .pr-input:focus {
  outline: none; border-color: rgb(255 255 255 / .6); background: rgb(255 255 255 / .1);
}
/* A list to choose from, and it says so: a small arrow down at its right (George, 24 September
   2026). Drawn with two gradients rather than an image, so no stylesheet rule about where images
   may come from can take it away, and set again on focus, whose background shorthand would. */
select.pr-input, select.pr-input:focus {
  appearance: none; cursor: pointer; padding-right: 30px;
  background-image: linear-gradient(45deg, transparent 50%, #b8b8c4 50%),
    linear-gradient(135deg, #b8b8c4 50%, transparent 50%);
  background-position: calc(100% - 17px) 55%, calc(100% - 12px) 55%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.pr-input.saved { border-color: rgb(120 220 160 / .8); }
.pr-input.bad { border-color: var(--accent); }
.pr-own { display: flex; align-items: center; gap: 8px; color: #e9e9f0; font-size: 13px; cursor: pointer; }
.pr-own input { accent-color: #7c9cff; width: 16px; height: 16px; cursor: pointer; }
.pr-own input.saved { outline: 2px solid rgb(120 220 160 / .8); outline-offset: 2px; }
.pr-own input.bad { outline: 2px solid var(--accent); outline-offset: 2px; }
.pr-why { margin: 0; color: #a7a7b2; font-size: 12px; line-height: 1.45; }
.psrc-text { color: #b9b9c4; font-size: 13px; overflow-wrap: anywhere; }
.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
   A grid rather than a wrapper element: the two 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: minmax(0, 1fr);
  grid-template-rows: auto auto; align-items: center; column-gap: 8px; row-gap: 0;
}
.viewer.zoom-stacked .vb-zoom > .sb-slider { grid-area: 1 / 1 / 2 / 2; align-self: end; }
.viewer.zoom-stacked .vb-zoom > .vb-pct {
  grid-area: 2 / 1 / 3 / 2; 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 %, 500 %, 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". As tall as the search box beside it in the header row, 38px (George,
   26 September 2026: it was 35, and the two did not line up). */
.signin-link {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  height: 38px; box-sizing: border-box;
  padding: 0 14px 0 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;
}
/* Under the pointer it fills, and the letters and the border stay as they are (George,
   26 September 2026: they used to brighten, which read as the button fading rather than
   answering). */
.signin-link:hover, .signin-link:focus-visible { background: rgb(138 180 255 / .22); }
.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. Behind it, the page's own background, not
   the theme's color (George, 10 October 2026: "Don't use the theme color behind the avatar keep it the
   background color behind the avatar"), and no ring until the pointer is on it (the same day: "Remove the
   circle around the avatar but show the hover circle on hover"). */
.avatar-btn {
  width: 34px; height: 34px; padding: 0; border-radius: 50%; cursor: pointer;
  border: 1px solid transparent; background: var(--bg); color: var(--ink); 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;
}
/* The settings menu's ground, edge and shadow (.gear-menu) on the dark page (George, 17 October 2026: "on all vi.ki sites use the same background color for the dropdown from the logo and the avatar menu as for the settings dropdown and on heart.photos avatar dropdown and viewer info sidebar"); the light page keeps
   its own white panel. */
:root:not([data-mode="light"]) .acct-menu { background: rgb(36 36 44 / .98); border-color: rgb(255 255 255 / .26); box-shadow: 0 14px 40px rgb(0 0 0 / .7); }
@media (prefers-color-scheme: light) {
  :root[data-mode="auto"] .acct-menu { background: var(--panel); border-color: var(--line); box-shadow: var(--shadow); }
}
.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-link { display: block; margin-top: 10px; font-size: 14px; color: inherit; text-decoration: underline; text-underline-offset: 3px; }
/* "Don't wrap "Sign out"" (George, 17 October 2026): a menu's words stay on one line. */
.acct-menu .signout-btn { margin: 12px 0 0; font-size: 13px; padding: 6px 14px; }
.acct-menu .acct-link, .acct-menu .signout-btn { white-space: nowrap; }
/* The other accounts signed in on this browser (account.js otherAccountRow): a face, the name and the
   address on one line each; a long one ends in an ellipsis rather than wrapping. */
.acct-menu .acct-other { display: flex; align-items: center; gap: 9px; text-decoration: none; min-width: 0; }
.acct-other img, .acct-other .acct-other-i { width: 24px; height: 24px; border-radius: 50%; flex: 0 0 24px; object-fit: cover; }
.acct-other .acct-other-i { display: grid; place-items: center; background: var(--accent); color: var(--bg); font-size: 12px; font-weight: 600; }
.acct-other-who { min-width: 0; display: flex; flex-direction: column; }
.acct-other-who span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-other-who span + span { color: var(--muted); font-size: 12px; }
.acct-menu .signout-btn + .signout-btn { margin-left: 6px; }
.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. */
/* A line that says what just happened, at the TOP of the screen.
   It sat at the foot, which on a phone is the corner of the glass a hand is over and the eye is
   furthest from — "Sign in to rate photos" went by unread while somebody pressed keys down a whole
   wall believing every one of them was being kept. At the top it is where a page is read from, and
   it is coloured rather than dark-on-dark: this is the one piece of text on the page that exists
   because something did NOT happen, and it has to win a glance it was never given. */
.note {
  position: fixed; left: 50%; top: 18px; bottom: auto; transform: translateX(-50%);
  z-index: 60; max-width: min(92vw, 520px);
  display: flex; align-items: center; gap: 10px;
  padding: 10px 18px; border-radius: 999px;
  background: var(--accent); color: #1a0a10; font-size: 14px; font-weight: 500;
  box-shadow: 0 10px 30px rgb(0 0 0 / .5), 0 0 0 1px rgb(0 0 0 / .25);
  border: 0;
  animation: note-in .18s ease-out;
}
@keyframes note-in {
  from { opacity: 0; transform: translateX(-50%) translateY(-8px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
/* Inside the viewer it is placed against the same top edge, over the photograph. */
.viewer .note { top: 18px; }
/* The wait for the end of the wall (Ctrl+End; longwait.js): a ring and how far the feed has got,
   shown only after a second, at the foot of the window and in the page's own dark rather than the
   accent, because it is not something that went wrong. Esc stops it. */
.long-wait {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 60;
  display: inline-flex; align-items: center; gap: 2px;
  padding: 10px 18px; border-radius: 999px;
  background: rgb(18 18 23 / .95); color: var(--ink); font-size: 14px;
  border: 1px solid rgb(255 255 255 / .14); box-shadow: 0 10px 30px rgb(0 0 0 / .5);
}
.long-wait[hidden] { display: none; }
.viewer .long-wait { z-index: 75; }
/* A note that asks: its two answers sit at the end of the line (askNote). */
.note-ask { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: center; }
.note-acts { display: inline-flex; gap: 8px; }
.note-btn { font: inherit; font-size: 13px; font-weight: 600; padding: 4px 14px; border-radius: 999px;
  cursor: pointer; border: 1px solid rgb(26 10 16 / .45); background: transparent; color: #1a0a10; }
.note-btn.note-yes { background: #1a0a10; color: var(--accent); border-color: #1a0a10; }
.note-btn:hover, .note-btn:focus-visible { filter: brightness(1.15); }
/* The x every note ends in (note's noteClose): the words, then the way to put them away now. */
.note-x { flex: none; display: inline-grid; place-items: center; width: 24px; height: 24px;
  margin: -4px -10px -4px 0; padding: 0; border: 0; border-radius: 999px; cursor: pointer;
  background: transparent; color: #1a0a10; opacity: .7; }
.note-x:hover, .note-x:focus-visible { opacity: 1; background: rgb(26 10 16 / .14); }
/* How long the note has left (api.js noteTime; George, 3 October 2026: "add a little timer bar that
   shows how long the notice will stay visible"): a hairline along its foot, inside the pill's flat
   part, that runs out from the right as the note's own clock does. */
.note-time {
  position: absolute; left: 18px; right: 18px; bottom: 4px; height: 2px; border-radius: 2px;
  background: rgb(26 10 16 / .5); transform-origin: left center; pointer-events: none;
  animation: note-time linear forwards;
}
@keyframes note-time { from { transform: scaleX(1); } to { transform: scaleX(0); } }

/* 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; } }
/* ...and it is put DOWN rather than switched off: for the length of the slide home it keeps the
   height it was carried at. The lift comes off the instant the hand lets go, so underneath this
   the photograph would spend its landing BEHIND the one it has just changed places with — dropped
   into the wall rather than set down on it. The shadow goes with it, and fades as it settles. */
.tile-landing { z-index: 4; box-shadow: 0 14px 34px rgb(0 0 0 / .55); }

/* Signing out of this host, which keeps its own session: quiet, but present. */
.signout-btn {
  white-space: nowrap;
  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; }
/* Full screen's two icons, one at a time (layout.js syncFullscreen). */
.full-btn svg[hidden] { display: none; }
@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);
  /* Named, because the title bar below cancels exactly this much to reach the panel's edges. */
  --keys-pad-x: 20px; --keys-pad-y: 18px;
  padding: var(--keys-pad-y) var(--keys-pad-x) 20px;
}
.keys:not([open]) { display: none; }

/* ---- the Log --------------------------------------------------------------------------------- */
/* Wider than the hotkeys, because a line here is five things across: which photograph, what was
   changed, what it went from and to, when, and the way back. */
.keys.log { width: min(760px, calc(100vw - 32px)); }
.log-list { display: flex; flex-direction: column; gap: 2px; }
.log-empty-line { color: var(--muted); margin: 10px 2px; font-size: 14px; }
.log-row {
  display: grid; align-items: center; gap: 10px;
  grid-template-columns: minmax(0, 1.6fr) auto minmax(0, 1.4fr) auto auto;
  padding: 7px 8px; border-radius: 9px; font-size: 13px;
}
.log-row:nth-child(odd) { background: rgb(255 255 255 / .035); }
/* Put back: the line stays, and says so by going quiet. Struck through would say it never
   happened; it did happen, and it happened twice if you count the undoing. */
.log-row.undone { opacity: .55; }
.log-photo {
  display: flex; align-items: center; gap: 8px; min-width: 0;
  background: none; border: 0; padding: 0; color: inherit; font: inherit; cursor: pointer;
  text-align: left;
}
.log-photo img { width: 42px; height: 30px; object-fit: cover; border-radius: 4px; flex: 0 0 auto; }
.log-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.log-photo:hover .log-name { text-decoration: underline; }
.log-photo:disabled { cursor: default; opacity: .6; }
.log-photo:disabled:hover .log-name { text-decoration: none; }
.log-what { color: var(--muted); white-space: nowrap; }
.log-change { display: flex; align-items: center; gap: 6px; min-width: 0; }
.log-value { display: inline-flex; align-items: center; gap: 1px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.log-value svg { display: block; width: 13px; height: 13px; }
/* Nothing, said as nothing rather than left blank: a gap in a row of values reads as a fault. */
.log-empty { color: var(--muted); font-style: italic; }
/* A word taken OFF, which is a value like any other and needs saying as one. */
.log-off { text-decoration: line-through; color: var(--muted); }
.log-arrow { color: var(--muted); flex: 0 0 auto; }
.log-when { color: var(--muted); white-space: nowrap; }
.log-undo {
  background: rgb(255 255 255 / .09); color: #e9e9f0; border: 1px solid rgb(255 255 255 / .16);
  border-radius: 999px; padding: 3px 12px; font: inherit; font-size: 12px; cursor: pointer;
  white-space: nowrap;
}
.log-undo:hover { background: rgb(255 255 255 / .16); }
.log-undo:disabled { opacity: .45; cursor: default; }
/* On a phone the five columns become two lines: the photograph and the way back on the first,
   what changed underneath it. */
@media (max-width: 700px) {
  .log-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 4px; }
  .log-photo { grid-column: 1; }
  .log-undo { grid-column: 2; grid-row: 1; }
  .log-what, .log-change, .log-when { grid-column: 1 / -1; }
  .log-when { font-size: 12px; }
}
/* 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 —
   and starting and stopping exactly where that panel does, so the two read as a pair. */
#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);
  --keys-pad-x: 16px; --keys-pad-y: 14px;
  padding: var(--keys-pad-y) var(--keys-pad-x) 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; }
/* It begins and ends where the info panel does: past the thumbnails at whichever end they are, and
   nothing else. Both panels used to stop short of the button bar as well, because the bar ran the
   whole width of the window; the bar keeps to the photograph now (see below), so there is nothing
   out here for a panel to sit on. */
.viewer:not(.strip-top)[open]:not(.no-strip) #keys.pinned {
  bottom: var(--strip-h);
}
.viewer.strip-top[open]:not(.no-strip) #keys.pinned {
  top: var(--strip-h); bottom: 0;
}

/* ...so what gives up the width is the photograph, and only the photograph — the same bargain the
   info panel strikes on the other side, and for the same reason: a picture behind a panel makes
   Fit a lie. */
@media (min-width: 701px) {
  .viewer.keys-open .stage > #pane { padding-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);
    --keys-pad-x: 20px; --keys-pad-y: 18px;
    padding: var(--keys-pad-y) var(--keys-pad-x) 20px;
  }
  html.keys-pinned body { padding-left: 0; }
  .viewer.keys-open .stage > #pane { padding-left: 0; }
}
/* The title bar reaches the panel's own EDGES: it cancels the padding with negative margins and
   takes it back as padding of its own, so the band across the top — the strip above the words and
   the strips beside them — IS the bar rather than the panel showing through around it. Which
   matters because the bar is the handle: a hand aiming at the top corner of a window is aiming at
   the handle, and the padding is most of what it hits. */
.keys-head {
  display: flex; align-items: center; gap: 12px;
  margin: calc(var(--keys-pad-y) * -1) calc(var(--keys-pad-x) * -1) 12px;
  padding: var(--keys-pad-y) var(--keys-pad-x) 0;
  /* ...and it STAYS as the list scrolls under it, the x with it (George, 24 September 2026): a
     long list scrolled down to its end had no way out but Escape. Opaque, so the rows pass under
     it rather than through it, and its foot pads the rows off it. */
  position: sticky; top: calc(var(--keys-pad-y) * -1); z-index: 2;
  background: #16161a; padding-bottom: 8px; margin-bottom: 4px;
}
#keys.pinned .keys-head { background: rgb(12 12 16 / .97); }
/* 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; }
/* What it does on the left, the keys that do it on the right — the way a menu lists its shortcuts,
   and the way somebody reading this is looking: they know what they want and are after the key. */
.keys-list > div { display: flex; align-items: baseline; gap: 12px; }
.keys-list dd { margin: 0; order: 0; flex: 1 1 auto; color: #b9b9c4; font-size: 13px; }
.keys-list dt {
  order: 1; flex: 0 0 auto; margin-left: auto;
  display: flex; flex-direction: column; align-items: flex-end; gap: 3px;
}
/* One WAY of doing it per line, with the "or" that introduces it. Strung along one line they
   wrapped wherever the width ran out, so "or" could end a line and its keys begin the next. */
.keys-list .alt { display: flex; align-items: center; gap: 4px; }
.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):not(heart-ai-chat) { 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; }

/* --- galleries ------------------------------------------------------------------------------ */
/* A column down the left of the wall, listing the collections: the ones somebody made by hand, and
   under them the words the wall is filed under — which are collections too, in the sense that
   matters to a reader. It is a sibling of the wall rather than an overlay, so the photographs give
   up the width and nothing is drawn over them. */
/* In the page's own ink, so it can be seen in either scheme: the buttons it borrows its shape from sit
   over photographs and are drawn light, which on the light scheme's ground is nearly nothing. */
.gal-toggle { flex: 0 0 auto; margin-right: 2px; color: var(--ink); }
.gal-toggle.on { background: rgb(128 128 140 / .2); }
/* ...and no disc behind it while the column is shut (George, 12 October 2026: "when the sidebar is closed at
   heart.photos wall don't show a highlight circle background, only show it when the sidebar is open"): the three
   lines alone, lit under the pointer as any control is. */
.gal-toggle:not(.on):not(:hover):not(:focus-visible) { background: none; box-shadow: none; }

.galleries {
  /* From the bottom of the line that stays — the same line the button that opens it is on. The
     words below that line scroll away; the column does not, so it cannot hang from them. */
  position: fixed; left: 0; top: var(--head-h, 56px); bottom: 0; z-index: 12;
  width: var(--gal-w, 232px); overflow-y: auto; overscroll-behavior: contain;
  padding: 10px 10px 24px; box-sizing: border-box;
  /* The page's own ground and line, so the names on it are in ink that can be read on it in either
     scheme. A fixed near-black under the light scheme's near-black ink was a column of nothing. */
  background: var(--bg); background: color-mix(in srgb, var(--bg) 96%, transparent);
  border-right: 1px solid var(--line);
}
.galleries[hidden] { display: none; }
/* The wall moves over rather than being covered: a list you opened to USE is not a thing to read
   through a hole in. The words along the top move with it — they are the wall's own controls, and
   a row of them running under the column is a row half of which cannot be pressed.
   ALL of the wall, the small print at its foot included (George, 21 September 2026): the column is
   fixed and full height, so anything left at the page's own left edge sits behind it, and the line
   that says which build is on screen was readable from the last two letters of "Copyright". */
html.galleries-open .grid,
html.galleries-open .collection,
html.galleries-open .r2trash,
html.galleries-open .status,
html.galleries-open .build,
html.galleries-open .tagbar { margin-left: var(--gal-w, 232px); }
/* On a phone the column is half the screen and the words are one line of chips: moved over by
   half a phone there would be nothing left of them. */
html.mobile.galleries-open .tagbar { margin-left: 0; }

/* Narrowing the column, and the order it is read in. Both at the top, because they are questions
   about the whole column rather than about either list in it. */
.gal-find { display: flex; gap: 6px; padding: 2px 4px 8px; }
/* Only where the button that opened it has scrolled away — see the phone rules at the foot. */
.gal-close { display: none; }
html.mobile .gal-close {
  display: grid; place-items: center; flex: 0 0 auto; width: 32px; height: 32px;
  background: rgb(128 128 140 / .1); color: var(--muted); cursor: pointer;
  border: 1px solid var(--line); border-radius: 8px;
}
html.mobile .gal-close:hover { color: var(--ink); }
.gal-search {
  flex: 1 1 auto; min-width: 0;
  background: rgb(128 128 140 / .1); color: var(--ink);
  border: 1px solid var(--line); border-radius: 8px;
  padding: 6px 9px; font: inherit; font-size: 13px;
}
.gal-search:focus { outline: none; border-color: var(--accent); }
.gal-search::placeholder { color: var(--muted); }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* A heading over a column of rows, and drawn as one: a band of its own with the two things it can
   be ordered by sitting in it. It used to be bare small-caps floating over the list, which reads
   as a label somebody left there rather than as the head of the column beneath it.
   Not shouted, either. LETTERS SPACED OUT IN CAPITALS is a way of saying "this is a heading" when
   there is nothing else to say it with; a band and a place say it without raising the voice. */
.gal-head {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 8px; border-radius: 8px;
  background: rgb(128 128 140 / .1);
}
.gal-head h2 { margin: 0; font-size: 12.5px; font-weight: 600; letter-spacing: 0;
  color: var(--muted); flex: 1 1 auto; }
/* The head of a list is how it is sorted. Two of them — what it is called, and how much is in it —
   and the arrow says which is in force and which way it points, so the answer to "how is this
   ordered" is in the same place as the way to change it. */
.gal-h { margin: 0; flex: 1 1 auto; min-width: 0; }
.gal-by {
  margin: 0; padding: 2px 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: 12.5px; font-weight: 600; letter-spacing: 0;
  color: var(--muted); text-align: left;
}
.gal-by[data-by="title"] { width: 100%; }
.gal-by[data-by="count"] { flex: 0 0 auto; }
.gal-by:hover { color: var(--ink); }
/* The head in force is the bright one. It used to carry an arrow as well, which said a third time
   what the list under it is already saying: a column read A to Z looks like a column read A to Z,
   and a glyph on top of that is a mark to skim past on every one of three headings. Which way it
   points is still there for anyone who wants it — the data-dir attribute, and the title on hover:
   "By name, A to Z — click to turn it round". */
.gal-by.on { color: var(--ink); }
/* The NAME of each section (Galleries, Auto-generated, Tags, Public galleries) in the theme's own
   color and the size of the rows under it, as the info panel's headings are (George, 25 September
   2026): gray on gray, it did not stand out from what it heads. Count beside it still says, by
   lighting up, which of the two the list is ordered by. */
.gal-by[data-by="title"], .gal-by[data-by="title"].on { color: var(--accent); font-size: 13px; }
.gal-by[data-by="title"]:hover { color: var(--accent); filter: brightness(1.15); }
.gal-by:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
/* Making one. A button, and drawn as one — filled, with an edge and a lit state. It used to be a
   dashed outline the width of the column, which is the shape of a place to DROP something rather
   than the shape of a thing to press; and it wore a + at a moment when the words beside it said
   exactly what it does. */
.gal-new {
  flex: 0 0 auto; padding: 5px 12px; cursor: pointer;
  background: rgb(128 128 140 / .14); color: var(--ink);
  border: 1px solid var(--line); border-radius: 999px;
  font: inherit; font-size: 12.5px; line-height: 1.4; white-space: nowrap;
}
.gal-new:hover { background: rgb(255 255 255 / .18); border-color: var(--accent); }
.gal-new:active { background: rgb(255 255 255 / .24); }
.gal-new:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gal-head-tags, .gal-head-pub, .gal-head-auto { margin-top: 14px; }
.gal-head-pub[hidden], #gal-public[hidden], .gal-head-auto[hidden], #gal-auto[hidden] { display: none; }
/* The fold on the Auto-generated section: pointing down while it is open, to the side when not. */
.gal-fold {
  flex: 0 0 auto; width: 18px; height: 18px; display: grid; place-items: center; padding: 0;
  border: 0; border-radius: 4px; background: none; color: var(--muted); cursor: pointer;
}
.gal-fold svg { width: 10px; height: 10px; transition: transform .15s ease; }
.gal-fold[aria-expanded="false"] svg { transform: rotate(-90deg); }
.gal-fold:hover { color: var(--ink); }
.gal-fold:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.gal-list { display: flex; flex-direction: column; gap: 1px; }
/* display above beats the hidden attribute's own rule, so a list hidden in script would stay on
   screen — which is how an empty Galleries section kept its space. */
.gal-list[hidden], .gal-head[hidden] { display: none; }
.gal-empty { color: var(--muted); font-size: 13px; margin: 4px 6px; }
.gal-item {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px; border-radius: 8px; color: var(--ink); text-decoration: none; font-size: 14px;
}
.gal-item:hover { background: rgb(128 128 140 / .14); }
.gal-item.on { background: rgb(128 128 140 / .22); }
/* The one being added to, marked while it is: every click on the wall is going into THIS. */
.gal-item.adding { box-shadow: inset 0 0 0 1px var(--accent); }
.gal-name { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gal-n { color: var(--muted); font-size: 12px; flex: 0 0 auto; }
/* The Trash, first in the column: a line like a gallery's, with its can in front. */
.gal-trash { margin: 0 0 6px; }
.gal-trash[hidden] { display: none; }
.gal-trash svg { flex: 0 0 auto; color: var(--muted); }
.gal-trash.on svg, .gal-trash:hover svg { color: var(--ink); }
/* Every look-alike at once, under the Trash (lookalikes.go): lines like it, the whole library's
   one for an admin alone. */
.gal-looks { display: flex; flex-direction: column; gap: 1px; margin: 0 0 6px; }
.gal-looks[hidden], .gal-look[hidden] { display: none; }
.gal-look svg { flex: 0 0 auto; color: var(--muted); }
.gal-look.on svg, .gal-look:hover svg { color: var(--ink); }

/* What a collection is called, over the photographs in it. */
.collection { padding: 14px 16px 4px; }
.collection[hidden] { display: none; }
.collection-title { margin: 0 0 4px; font-size: 22px; font-weight: 500; }
.collection-desc[hidden], .collection-form[hidden] { display: none; }
/* Whose it is, after its name: quieter and smaller, because the name of the gallery is the thing
   being read and this is the answer to a second question. */
.collection-by { font-size: 14px; font-weight: 400; color: var(--muted); margin-left: 8px; }
.collection-by[hidden] { display: none; }
.collection-desc { margin: 0 0 8px; color: var(--muted); font-size: 14px; max-width: 70ch; }
.collection-vis { display: inline-flex; }
.collection-vis[hidden] { display: none; }
.collection-vis select {
  background: rgb(255 255 255 / .07); color: var(--ink);
  border: 1px solid rgb(255 255 255 / .14); border-radius: 999px;
  padding: 5px 8px; font: inherit; font-size: 13px; cursor: pointer;
}
.collection-vis select:focus { outline: none; border-color: var(--accent); }

/* Choosing what goes in. Every tile is a switch while it is on, so the wall says so plainly: the
   ones already in wear the accent, and the pointer says a click will do something. */
.grid.gal-adding .tile { cursor: copy; }
.grid.gal-adding .tile .tile-pic { outline: 2px solid transparent; outline-offset: -2px; }
.grid.gal-adding .tile.in-gallery .tile-pic { outline-color: var(--accent); }
.grid.gal-adding .tile.in-gallery::after {
  content: '✓'; position: absolute; top: 6px; left: 6px; z-index: 3;
  width: 22px; height: 22px; border-radius: 999px;
  display: grid; place-items: center; font-size: 13px;
  background: var(--accent); color: #1a0a10; box-shadow: 0 1px 4px rgb(0 0 0 / .5);
}

/* On a phone the column takes HALF the width and no more.
   It used to take all of it, on the grounds that a list of collections is a thing you use and then
   leave — but a column that hides the wall hides the answer to the question it is being used to
   ask. Which gallery is this? The one whose photographs you can see beside the name. Half is
   enough for either: the wall is one column of pictures on a phone, and it keeps its half.
   And it hangs from the top of the SCREEN rather than from under the header, because on a phone
   the header is not pinned there — see below. Which is why it carries its own way out. */
@media (max-width: 700px) {
  .galleries { width: min(50vw, 260px); }
  html.galleries-open .grid, html.galleries-open .collection, html.galleries-open .r2trash { margin-left: min(50vw, 260px); }
}
html.mobile .galleries { top: 0; }

/* Two panes, on a phone with the column open.
   The column and the wall are two things to move through, and on a phone they were sharing one
   scroll: the wall is the PAGE, so a swipe anywhere moved the page under a column that stayed
   still — and a column that stays still while everything else moves is a column that reads as
   stuck rather than as fixed. Given the column, they become two scrollers side by side, each
   answering the finger that is on it and neither reaching the other.
   The header stays put above both while this is on. It is not the sticky header the wall scrolls
   under — nothing scrolls here but the two panes — and with the column drawn from the top of the
   screen it is the only way the name and the search stay reachable.
   #wall is `display: contents` everywhere else, so none of this exists until it is needed. */
.wall { display: contents; }
html.mobile.galleries-open, html.mobile.galleries-open body { height: 100%; overflow: hidden; }
html.mobile.galleries-open .topbar { margin-bottom: 0; }
html.mobile.galleries-open .galleries { top: var(--top-h, 96px); }
html.mobile.galleries-open .wall {
  display: block; position: fixed; z-index: 1;
  top: var(--top-h, 96px); left: min(50vw, 260px); right: 0; bottom: 0;
  overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
}
/* The pane already stands where the column is not, so the wall inside it takes the whole of it. */
html.mobile.galleries-open .grid, html.mobile.galleries-open .collection, html.mobile.galleries-open .r2trash,
html.mobile.galleries-open .status, html.mobile.galleries-open .build { margin-left: 0; }

/* --- a phone ------------------------------------------------------------------------------- */
/* html.mobile is put on by mobile.js — a coarse pointer on a window under 950px — with portrait
   or landscape beside it for which way up the phone is held. What follows is everything a phone
   does differently, in one place, keyed off those classes rather than off a media query repeated
   a dozen times with a dozen chances to disagree. */

/* Nothing up here follows you down the page on a phone — see the rule beside .top for why. */
html.mobile { scroll-padding-top: 0; }
/* The name and the way to the galleries stay on ONE line. The column that holds them can be
   squeezed to nothing by the search field beside it, and wrapping put the name under its own
   button — a header two lines tall to say what one line said. The name gives way instead: it is
   already cut with an ellipsis when there is not room for it. */
html.mobile .top-name {
  flex-wrap: nowrap; align-items: center; gap: 0 6px;
  /* Filling its column rather than being sized by its content: left to start, a grid item is as
     wide as what is in it and simply overflows the track — which put the name under the search
     field instead of beside it, the moment the name was long enough. */
  justify-self: stretch; max-width: 100%;
}
/* ...and the name is the part that gives. Without a floor of its own a flex item is as wide as its
   text, so a name that would not fit simply pushed the row wider than its column and slid under
   the field beside it. */
html.mobile .top .brand { flex: 0 1 auto; min-width: 0; }
/* ...on the name's own line, between the name and the account. It had the tagline's line to
   itself, which read as a second header: a band of chrome above every wall, on the screen with
   the least height to give it. Three things on one line fit because the tagline is gone — the
   field keeps everything the name and the avatar do not need, and min-width:0 is what lets it
   give way rather than push the avatar off the edge. */
html.mobile .top {
  /* The NAME takes what it needs and the field takes the rest.
     The field used to be guaranteed half the header, on the grounds that it is the one of the
     three actually being used — but half a phone is more than the name had, and the name is not
     a thing that can be given "most of": cut, it is the site's own name spelt wrong at the top of
     every page. It is a dozen fixed characters. The field is what stretches. */
  grid-template-columns: auto minmax(0, 1fr) auto; grid-template-rows: auto; gap: 8px 10px;
}
/* The wrapper that holds the way to the galleries, the name and the gallery in hand steps aside on a
   phone, so the three can take places of their own: the button and the name start the first line,
   and the gallery in hand gets the second line to itself. Beside the name, on a phone, it wrapped to
   three lines, squeezed the search to nothing and pushed the avatar and its own button off the
   edge. */
html.mobile .top { grid-template-columns: auto auto minmax(0, 1fr) auto; }
html.mobile .top .top-name { display: contents; }
html.mobile .top #btn-galleries { grid-column: 1; grid-row: 1; }
html.mobile .top .top-gallery { grid-column: 1 / -1; grid-row: 2; }
html.mobile .top .top-gallery .tag-gallery { max-width: none; flex: 0 1 auto; }
html.mobile .top .brand {
  grid-column: 2; grid-row: 1;
  font-size: 17px; max-width: 100%;
  /* Cut, not abbreviated. An ellipsis is for a title somebody wrote — it says "there is more of
     this name, and you can go and read it". A wordmark has no more of it to read: "Heart.pho…"
     is not a shortening of the name, it is the name misspelt. Where there is no room for the
     whole of it, what fits is shown and the rest simply is not. */
  overflow: hidden; text-overflow: clip; white-space: nowrap;
}
html.mobile .top .search { grid-column: 3; grid-row: 1; width: 100%; min-width: 0; justify-self: stretch; }
html.mobile .top .account { grid-column: 4; grid-row: 1; justify-self: end; }
/* Multiselect on a phone: the end of the second line, beside the gallery in hand, because the
   first line has the name, the field and the avatar and no room for a word. The wrapper that holds
   it with the avatar on a desktop steps aside so each can take its own place. */
html.mobile .top .top-end { display: contents; }
html.mobile .top .top-acts { grid-column: 3 / -1; grid-row: 2; justify-self: end; }
/* Spanning the field's column as well as the avatar's, so the avatar's column stays the avatar's
   width: in that column alone the word made it wider and took the room out of the search field.
   The gallery in hand keeps the whole line and leaves room at its end for the word. */
html.mobile .top .top-gallery { padding-right: 70px; }
html.mobile .top:has(.top-select:not([hidden])) .top-gallery { padding-right: 186px; }
html.mobile .top .selbar { grid-row: 3; margin: 0; }
/* ...and while choosing, the header stays on screen after all: the controls it holds are the ones
   being used, all the way down the wall. */
html.mobile .top.selecting { position: sticky; }
/* A visitor's way to sign in is its person alone on a phone: the words took the room the search needs,
   and the field was left saying "Sea". The link is still called Sign in to anybody hearing the page
   read out, and the person is the mark that says so on screen. */
html.mobile .account .signin-link { font-size: 0; gap: 0; padding: 0; width: 38px; justify-content: center; }
html.mobile .account .signin-link svg { width: 18px; height: 18px; }
/* Whose gallery it is, kept in one piece: "by George" at the end of one line and "Vanous" at the start
   of the next reads as two names. */
html.mobile .collection-by { white-space: nowrap; }

/* A gallery's own heading on a phone: the pencil and the name share the first line, the name
   wrapping beside the pencil rather than dropping under it, and who may see it takes the line
   under them. With the column open the wall has half a phone, and the name is set smaller to fit
   in it. */
html.mobile .collection-head .collection-title { flex: 1 1 0; min-width: 0; font-size: 20px; overflow-wrap: anywhere; }
html.mobile .collection-head .collection-vis { flex-basis: 100%; }
html.mobile.galleries-open .collection { padding: 10px 10px 4px; }
html.mobile.galleries-open .collection-head .collection-title { font-size: 17px; }
/* ...and the first line of the column wraps rather than running out of it: New gallery, and the
   two links after it when there is room, under it when there is not. */
html.mobile .gal-top { flex-wrap: wrap; row-gap: 6px; }
html.mobile .gal-new { padding: 4px 10px; font-size: 12px; }
html.mobile .gal-links { font-size: 12px; }

/* The words along the top, alone. And / Or / Not / Multiselect and the gear build and tend an
   expression, and there is no room to build one in — a word is pressed on its own (tagbar.js).
   The chip that would repeat the lit word inside the field is not drawn either. */
html.mobile .tagbar-acts { display: none !important; }
html.mobile .search-chips { display: none; }

/* The wall, edge to edge: one photograph to a row held upright, two on its side. The justified
   row solve is for a wall with room to solve across; here every photograph has the width. */
html.mobile .grid { display: grid; grid-template-columns: 1fr; gap: 1px; padding: 0; }
html.mobile.landscape .grid { grid-template-columns: 1fr 1fr; }
html.mobile .grid > .tile, html.mobile .series-cell .tile {
  height: auto; width: 100%; flex: none; border-radius: 0;
}
html.mobile .tile-pic { height: auto; aspect-ratio: var(--r); }
/* A thumbnail is something to CHOOSE from, and no single photograph should take the screen to be
   chosen. Held upright a 9:16 ran seven hundred pixels down a 412-wide phone — one picture, one
   screenful, with the wall below it out of sight; on its side the same shape filled the whole of a
   390-tall window. So the height is capped and the picture cropped from the middle, which it
   already is everywhere else on the wall (object-fit: cover). The width is left alone: the columns
   decide that, and a wide photograph is short by its own arithmetic. */
html.mobile.portrait .tile-pic { max-height: 62vh; }
html.mobile.landscape .tile-pic { max-height: 58vh; }
html.mobile .grid::after { content: none; }
/* A series takes the row, its members side by side in it. */
html.mobile .series-cell { grid-column: 1 / -1; flex-basis: auto; margin: 0; padding: 0; border-radius: 0; }

/* The bar over the photograph holds three things: the way out at the left edge, the pair switch
   in the middle, the info button at the right edge. Everything else the bar does — moving between
   photographs, sizing, the thumbnails, full screen, the settings — a phone does by swiping,
   pinching and turning over, and a row of small buttons over a small picture is what those
   gestures exist to replace. Full width, so the edges are the edges of the screen. */
html.mobile .viewer .controls {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  /* auto, not 100%. Overlaid, this row is positioned by its two edges, and down a side the near
     edge is the width of the thumbnail column in from the frame — so a width of 100% pushed the
     far edge that same distance PAST the screen, taking the "i" off the right of a sideways phone
     entirely and shifting the pair switch off the centre of the picture with it. Left to auto, the
     two edges decide the width, which is what they are for. */
  width: auto; max-width: 100%;
}
/* No wash under the row on a phone.
   It is three small buttons with the whole width of the picture between them, and the gradient
   that makes eight or nine of them legible on a laptop is, here, a band of darkness laid over the
   bottom of every photograph to serve almost nothing. The buttons carry their own edge. */
html.mobile .viewer.overlay-bar .controls::before { content: none; }
html.mobile .viewer .controls > * { display: none; }
html.mobile .viewer .controls > #close { display: inline-flex; align-items: center; justify-content: center; grid-column: 1; justify-self: start; }
html.mobile .viewer .controls > #sides { display: inline-flex; grid-column: 2; justify-self: center; }
/* Something behind the two words. The bar carries no wash on a phone — three small buttons with
   the whole width of a photograph between them do not earn a band of darkness across it — and the
   two round buttons at the ends have a fill of their own to be read against. This one did not: on
   a sky, on a sunrise, on anything pale, "Wide | Tall" was white text on white.
   The switch takes the same fill the round buttons have, so the three of them read as one row. */
html.mobile .viewer .controls > #sides .pair-switch {
  margin-top: 0;
  background: rgb(0 0 0 / .45);
  border-color: rgb(255 255 255 / .35);
}
@supports (backdrop-filter: blur(1px)) {
  html.mobile .viewer .controls > #sides .pair-switch { background: rgb(0 0 0 / .32); backdrop-filter: blur(8px); }
}
/* ...and the words themselves a shade brighter, since they are now on a ground of their own. */
html.mobile .viewer .controls > #sides .pair-switch button { color: #e8e8ef; }
html.mobile .viewer .controls > #sides .pair-switch button.on { background: rgb(255 255 255 / .22); color: #fff; }
/* The same ground behind the two round ones. Theirs was a fill of WHITE at eight per cent, which
   on a bright photograph is a white glyph on white — the fill was doing nothing but adding a
   little more white to it. A dark disc reads on anything, and the three controls then look like
   one row rather than two kinds of button.
   An inset shadow rather than a border: these are exactly 32px across and a border would make
   them 34, which moves everything the row is centred on. */
html.mobile .viewer .controls > .ctl {
  background: rgb(0 0 0 / .45);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .35);
}
html.mobile .viewer .controls > .ctl:hover { background: rgb(0 0 0 / .6); }
@supports (backdrop-filter: blur(1px)) {
  html.mobile .viewer .controls > .ctl { background: rgb(0 0 0 / .32); backdrop-filter: blur(8px); }
}
html.mobile .viewer .controls > #btn-settings { display: inline-flex; align-items: center; justify-content: center; grid-column: 3; justify-self: end; }
html.mobile .viewer.no-bar .controls { display: none; }

/* On its side the window is short, and the row of thumbnails takes half the height it would:
   the photograph is the thing, and there is not much of it to spare. */
html.mobile.landscape .viewer {
  --strip-h: min(calc(88px * var(--thumb-scale, 1)), 40vh);
  --strip-w: min(calc(128px * var(--thumb-scale, 1)), 40vw);
}
/* ...and the whole row goes to the TOP of the picture.
   Along the foot it sat in the band of a sideways phone that the hand holding it is already over —
   under the thumb rather than beside it — which is why the way out was lifted up there first, on
   its own. One button at the top and two at the bottom is worse than either: they are one row,
   they are read as one row, and a reader who has found the × has found where the other two are. */
html.mobile.landscape .viewer.overlay-bar .controls {
  top: 8px; bottom: auto;
}
/* Under the thumbnails when the thumbnails are up there too. */
html.mobile.landscape .viewer.strip-top:not(.no-strip).overlay-bar .controls {
  top: calc(var(--strip-h) + 8px);
}
/* The row is panned by swiping it. The arrows that page it for a mouse sit OVER its two ends,
   and on a phone they were the thing a thumb landed on when it aimed at the thumbnail underneath. */
html.mobile .strip-page { display: none; }

/* --- the thumbnails down a side ------------------------------------------------------------- */
/* Four sides, not two ends. Q walks them clockwise.
   A window is wider than it is tall and a photograph usually is too, so along an end the row and
   the picture are competing for the one thing they both want — height. Down a side they are not:
   a column of thumbnails takes width, which a wide window has to spare and which a photograph
   fitted to the height was not using. It is the same row, stood on its end — the same thumbnails
   at the same size, scrolled and panned and dragged along the way it now runs (strip.js).

   --strip-w is a thumbnail's length ALONG the row and --strip-h its thickness ACROSS it, so on a
   side the two swap roles and the thumbnails themselves need no rule of their own: the column is
   --strip-w across and each thumbnail is the same box it always was.
   The SAME size down the left as along the top (George, 24 September 2026). The side had smaller
   ones of its own, 176 by 121 against 256 by 176, which made the same photographs harder to tell
   apart for no better reason than where the row was. */
.viewer.strip-left .strip, .viewer.strip-right .strip {
  top: 0; bottom: 0; height: auto; width: var(--strip-w);
  flex-direction: column;
  overflow-x: hidden; overflow-y: auto;
  /* The wash runs in from the edge the row is on, the way it runs up from the foot along an end. */
  background: linear-gradient(to right, rgb(0 0 0 / .78), rgb(0 0 0 / 0));
}
.viewer.strip-left .strip-thumb, .viewer.strip-right .strip-thumb {
  width: var(--strip-w);
  height: calc(var(--strip-w) / clamp(.5, var(--r, 1.4545), 2.4));
}
.viewer.strip-left .strip { left: 0; right: auto; }
.viewer.strip-right .strip { right: 0; left: auto;
  background: linear-gradient(to left, rgb(0 0 0 / .78), rgb(0 0 0 / 0)); }
/* The photograph keeps the whole height and gives up the column's width. */
.viewer.strip-left[open]:not(.no-strip) .stage { padding: 0 0 0 var(--strip-w); }
.viewer.strip-right[open]:not(.no-strip) .stage { padding: 0 var(--strip-w) 0 0; }
.viewer.strip-left.overlay-bar[open]:not(.no-strip) .stage,
.viewer.strip-left.no-bar[open]:not(.no-strip) .stage {
  padding: 0 0 0 calc(var(--strip-w) + var(--seam));
}
.viewer.strip-right.overlay-bar[open]:not(.no-strip) .stage,
.viewer.strip-right.no-bar[open]:not(.no-strip) .stage {
  padding: 0 calc(var(--strip-w) + var(--seam)) 0 0;
}
/* The row of controls sits at the TOP of the photograph with the thumbnails down a side (George, 24
   September 2026). Overlaid it is centred on the PICTURE rather than on the window, or a column
   down one side would push it off centre by half its own width. */
/* ...and between the column and an open panel, as along an end (George, 10 October 2026: "when thumbnails
   are on the left of a gallery on heart.photos the button bars don't change when the info sidebar opens
   (they should)"): --bar-l and --bar-r grow as a panel opens, and these edges ignored them. On the right,
   --bar-r already counts the column while the info panel is open beside it. */
.viewer.strip-left.overlay-bar .controls, .viewer.strip-left.no-bar .controls {
  top: 0; bottom: auto; left: calc(var(--strip-w) + var(--bar-l)); right: var(--bar-r);
  max-width: calc(100% - var(--strip-w) - var(--bar-l) - var(--bar-r) - 20px);
}
.viewer.strip-right.overlay-bar .controls, .viewer.strip-right.no-bar .controls {
  top: 0; bottom: auto; left: var(--bar-l); right: max(var(--strip-w), var(--bar-r));
  max-width: calc(100% - var(--bar-l) - max(var(--strip-w), var(--bar-r)) - 20px);
}
/* The paging arrows are ON the thumbnails, so on a side they are at the two ENDS of the column:
   a wide, short button at the top and another at the foot, with the chevrons turned to match. */
.viewer.strip-left .strip-page, .viewer.strip-right .strip-page {
  width: var(--strip-w); height: 58px; left: auto; right: auto;
  background: linear-gradient(to bottom, rgb(0 0 0 / .75), rgb(0 0 0 / 0));
}
.viewer.strip-left .strip-page.prev, .viewer.strip-right .strip-page.prev { top: 0; bottom: auto; }
.viewer.strip-left .strip-page.next, .viewer.strip-right .strip-page.next {
  bottom: 0; top: auto;
  background: linear-gradient(to top, rgb(0 0 0 / .75), rgb(0 0 0 / 0));
}
.viewer.strip-left .strip-page svg, .viewer.strip-right .strip-page svg { transform: rotate(90deg); }
.viewer.strip-left .strip-page.prev { left: 0; }
.viewer.strip-right .strip-page.prev { right: 0; }
.viewer.strip-left .strip-page.next { left: 0; }
.viewer.strip-right .strip-page.next { right: 0; }
/* The panel takes the full height beside the column, and stops short of it where the two would
   be on the same edge. */
.viewer.strip-left[open]:not(.no-strip) .sidebar { top: 0; bottom: 0; }
.viewer.strip-right[open]:not(.no-strip) .sidebar { top: 0; bottom: 0; right: var(--strip-w); }
/* A series is a RUN of neighbouring thumbnails, marked along the two sides it does not touch —
   which on a column is the left and the right rather than the top and the bottom. */
.viewer.strip-left .strip-thumb[data-series]::before,
.viewer.strip-right .strip-thumb[data-series]::before {
  border-top: 0; border-bottom: 0;
  border-left: 2px solid rgb(255 84 112 / .55);
  border-right: 2px solid rgb(255 84 112 / .55);
}
/* A phone on its side has the height to spare least of all, which is where a column earns the
   most: the thumbnails are narrower there, and the picture keeps the whole of the glass. */
html.mobile .viewer.strip-left, html.mobile .viewer.strip-right {
  --strip-w: min(calc(116px * var(--thumb-scale, 1)), 40vw);
  --strip-h: min(calc(80px * var(--thumb-scale, 1)), 40vh);
}

/* --- what the keeper of the wall thinks of a photograph ------------------------------------- */
/* A thumb, two thumbs, or a heart, in the corner of the picture — and for the photographs nobody
   has said anything about, nothing at all. Most of a wall is nought, and a badge on every one of
   them saying "no opinion" is a wall of badges.
   In the bottom-right corner of the PICTURE, which on a tile is the box above the caption: a mark
   over the words would be a mark over the title. */
.like-badge {
  /* Above the band of words, which is a scrim across the whole width of the picture and passes
     behind the mark: sharing a layer with it, the mark was painted THROUGH the dark end of that
     gradient. The words themselves stop before it (see .tile-tags). */
  position: absolute; right: 6px; bottom: 6px; z-index: 4; pointer-events: none;
  display: inline-flex; align-items: center; gap: 1px;
  padding: 3px 5px; border-radius: 999px;
  background: rgb(12 12 16 / .62); color: #fff;
  /* The photographs underneath are anything at all, so the mark carries its own edge rather than
     trusting the picture to be dark enough for white to read on. */
  box-shadow: 0 1px 4px rgb(0 0 0 / .5), inset 0 0 0 1px rgb(255 255 255 / .22);
}
@supports (backdrop-filter: blur(1px)) {
  .like-badge { background: rgb(12 12 16 / .42); backdrop-filter: blur(6px); }
}
.like-badge svg { display: block; width: 13px; height: 13px; }
/* The hearts wear the wall's own red — the colour that already means "this one" on it. */
.like-badge.like-3, .like-badge.like-4 { color: var(--accent); }
/* The bottom two steps are the emoji, which brings its own colour and needs no help from us. It is
   text among drawings, so it is sized to sit on the same line as them rather than to its own. */
/* Sized to the icons it stands among, so a badge is 23px wide with one mark and 37px with two
   whatever it is drawn from — which is what makes the room the words leave for it exact. */
.like-emo { display: block; width: 13px; font-size: 12px; line-height: 13px; text-align: center; }
.strip-thumb .like-emo { font-size: 10px; line-height: 11px; }
/* A thumb down is a plain white thumb, turned over. It is not red: red is the wall's word for
   "this one", and the whole point of the bottom half of the scale is that these are the ones it
   is not. */
/* Said, but not KEPT: a rating by somebody the server would not take it from, which lasts as long
   as the tab does. It must not look like one that is saved — it used to look identical, and the
   only thing telling anybody apart was one line of text shown once per visit. So the mark goes
   hollow: the shape is still there, plainly said, and plainly not filled in. */
.like-badge.like-unsaved {
  background: none; backdrop-filter: none;
  color: rgb(255 255 255 / .85);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .55), 0 1px 4px rgb(0 0 0 / .5);
}
.like-badge.like-unsaved svg path { fill: none; stroke: currentColor; stroke-width: 1.6; }
.like-badge.like-unsaved.like-3, .like-badge.like-unsaved.like-4 { color: var(--accent); }
/* An emoji cannot go hollow — it is a glyph, not a shape we draw — so an unsaved one says it the
   only way left: quietly. The badge's own outline is still there to say the rest. */
.like-badge.like-unsaved .like-emo { opacity: .75; }

/* The words stop before the rating, which keeps the corner it has everywhere.
   The strip is a band across the WHOLE width of the picture and the mark sits in the bottom-right
   of it, so a photograph with enough tags to reach the edge had its own rating written over — and
   the rating is what the wall is sorted by. The strip gives up the end of itself instead: the tags
   were already cut with an ellipsis at whatever width they had, so this costs a few characters of
   something already abridged and nothing at all on the photographs that have room.
   Only where there IS a mark: an unrated photograph has the whole width, which is most of a wall. */
.grid.tags-on .tile:has(.like-badge) .tile-tags { padding-right: var(--mark-clear); }
/* Two marks are wider than one. The four ratings that draw two of anything say so in their class. */
.grid.tags-on .tile:has(.like-2, .like-4, .like-n2, .like-n4) .tile-tags {
  padding-right: var(--mark-clear-2);
}

/* No opinion: the sideways thumb. Quieter than the marks that mean something — it is the mark for
   having nothing to say — but plainly THERE, because the whole of its job is to tell a photograph
   somebody has been past from one nobody has reached. */
.like-badge.like-0 { color: rgb(255 255 255 / .78); }
.tag-chip.tag-rating.rating-0 svg { opacity: .8; }

/* A photograph only its owner is seeing here — see tile() in wall.js. Top right, which nothing
   else on a tile uses; in words, because "this is not public" is a sentence and not an icon
   anybody would read correctly the first time. Quiet, like the rating: a note about the photo,
   not a control competing with it. */
.vis-mark {
  position: absolute; top: 6px; right: 6px; z-index: 4; pointer-events: auto;
  padding: 2px 7px; border-radius: 999px; font-size: 11px; line-height: 1.4;
  background: rgb(12 12 16 / .62); color: #fff;
  box-shadow: 0 1px 4px rgb(0 0 0 / .5), inset 0 0 0 1px rgb(255 255 255 / .22);
}
@supports (backdrop-filter: blur(1px)) {
  .vis-mark { background: rgb(12 12 16 / .42); backdrop-filter: blur(6px); }
}
.tile.vis-private .vis-mark { color: #ffd479; }

/* The row's thumbnails are smaller than the wall's, so the mark is too. */
.strip-thumb .like-badge { right: 4px; bottom: 4px; padding: 2px 4px; }
/* Show rating off (George, 11 October 2026: "Add a new setting "Show rating" default On after "Show tags" that
   shows or hides the rating icon in the bottom-right corner of thumbnails"; wallmenu.js applyThumbSigns): no
   thumbnail wears it, on the wall or in the row, and the words under a tile have their whole width back. */
html.no-ratings .tile .like-badge, html.no-ratings .strip-thumb .like-badge { display: none; }
html.no-ratings .grid.tags-on .tile .tile-tags { padding-right: 8px; }
.strip-thumb .like-badge svg { width: 11px; height: 11px; }

/* --- dropping photographs on the wall ---------------------------------------------------------- */
/* The veil says the drop will be caught. A page that accepts a file and looks exactly like a page
   that does not is a page nobody tries it on. Over everything, because the drop is over everything
   — and pointer-events: none so it cannot itself swallow the drop it is advertising. */
.dropveil {
  position: fixed; inset: 0; z-index: 60; pointer-events: none;
  display: grid; place-items: center;
  background: rgb(10 10 14 / .72);
  box-shadow: inset 0 0 0 3px var(--accent);
}
.dropveil[hidden] { display: none; }
/* ...over the viewer too, which it is moved into while a photograph is open (upload.js veilOn). */
.viewer .dropveil { z-index: 80; }
.dropveil-say {
  display: grid; place-items: center; gap: 10px;
  color: var(--ink); font-size: 17px; text-align: center;
  padding: 26px 34px; border-radius: 16px;
  background: rgb(20 20 26 / .9); border: 1px solid rgb(255 255 255 / .14);
}
.dropveil-say p { margin: 0; }
.dropveil-say svg { color: var(--accent); }

/* What is going up, and what is being made (create.js). Bottom-right, narrow, and never over the
   middle of the wall: both are things you set going and then carry on around. One stack, the dock,
   so the two never sit on top of each other; inside the viewer while a photograph is open, clear of
   the info panel and of a row of thumbnails along the foot. */
/* ...and above the He.art AI chat's button, which keeps the very corner (--heart-ai-launcher, set by
   photoweb/aichat/aichat.js while the button shows), so neither hides the other. */
.dock {
  position: fixed; right: 14px; bottom: calc(14px + var(--heart-ai-launcher, 0px)); z-index: 55;
  width: min(340px, calc(100vw - 28px));
  display: flex; flex-direction: column; gap: 10px;
  pointer-events: none;
}
.dock > * { pointer-events: auto; }
.viewer .dock { z-index: 70; }
.viewer.sidebar-open .dock { right: calc(var(--sb-w) + 14px); }
.viewer[open].strip-bottom:not(.no-strip) .dock { bottom: calc(var(--strip-h) + 14px + var(--heart-ai-launcher, 0px)); }
.uploads {
  background: rgb(18 18 23 / .97); color: var(--ink);
  border: 1px solid rgb(255 255 255 / .12); border-radius: 12px;
  box-shadow: 0 10px 30px rgb(0 0 0 / .5);
  overflow: hidden;
}
.uploads[hidden] { display: none; }
/* Something has just gone into the queue in the corner (create.js glowQueue; George, 3 October 2026:
   "visually highlight the queue in the corner with an animated glow to catch user's attention", in
   place of a note saying where it went): a ring of the accent breathes out three times over the
   panel's own shadow and is gone. Where motion is reduced, the ring holds still for that time. */
.creations.cr-glow { animation: cr-glow 1.2s ease-in-out 3; }
@keyframes cr-glow {
  0%, 100% { box-shadow: 0 10px 30px rgb(0 0 0 / .5), 0 0 0 1px var(--accent), 0 0 0 0 transparent; }
  50% { box-shadow: 0 10px 30px rgb(0 0 0 / .5), 0 0 0 2px var(--accent), 0 0 26px 6px color-mix(in srgb, var(--accent) 60%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .creations.cr-glow { animation: none; box-shadow: 0 10px 30px rgb(0 0 0 / .5), 0 0 0 2px var(--accent); }
}
.up-head {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 8px 9px 12px; border-bottom: 1px solid rgb(255 255 255 / .08);
  background: rgb(255 255 255 / .04);
}
.up-say { flex: 1 1 auto; font-size: 13px; }
/* Which way a list runs, after what it says: quiet, since it is a note about the list and not news of it
   (George, 12 October 2026: "say "latest first" in any popup where the latest is first"). */
.up-order { flex: none; font-size: 11px; font-weight: 400; color: var(--muted); white-space: nowrap; }
.log-order { margin-left: 8px; }
.up-x {
  flex: 0 0 auto; display: grid; place-items: center; width: 26px; height: 26px;
  background: none; border: 0; border-radius: 7px; color: var(--muted); cursor: pointer;
}
.up-x:hover { background: rgb(255 255 255 / .1); color: var(--ink); }
.up-list { max-height: min(46vh, 340px); overflow-y: auto; }
/* A row is: what it is called, how it is getting on, the one button that applies, and the progress
   under all three. The bar is a line along the bottom of the row rather than a gauge of its own —
   it is the row that is making progress. */
.up-row {
  position: relative; display: flex; align-items: center; gap: 8px;
  padding: 9px 10px 10px 12px; border-bottom: 1px solid rgb(255 255 255 / .05); font-size: 13px;
}
.up-row:last-child { border-bottom: 0; }
.up-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.up-like { color: #8ab4ff; text-decoration: none; }
.up-like:hover, .up-like:focus-visible { color: #cfe0ff; }
.up-state { flex: 0 0 auto; color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.up-act {
  flex: 0 0 auto; background: none; color: var(--muted); cursor: pointer;
  border: 1px solid rgb(255 255 255 / .16); border-radius: 999px;
  padding: 3px 9px; font: inherit; font-size: 12px;
}
.up-act:hover { color: var(--ink); border-color: var(--accent); }
.up-act.up-go { color: var(--accent); border-color: rgb(255 84 112 / .5); }
.up-act[hidden] { display: none; }
.up-bar {
  position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: rgb(255 255 255 / .07);
}
.up-fill { display: block; height: 100%; width: 0; background: var(--accent); transition: width .18s ease; }
.up-row[data-state="done"] .up-fill { background: rgb(120 200 140); }
.up-row[data-state="failed"] .up-fill, .up-row[data-state="stopped"] .up-fill { background: rgb(150 150 160); }
.up-row[data-state="stopped"] .up-name, .up-row[data-state="failed"] .up-name { color: var(--muted); }

/* On a phone the corner is the whole width, and the panel is shorter: the wall behind it is the
   point, and a panel taking half the screen is a panel in the way. */
@media (max-width: 700px) {
  .dock, .viewer.sidebar-open .dock { right: 8px; left: 8px; bottom: 8px; width: auto; }
  .up-list { max-height: 34vh; }
}
/* A picture being made: its buttons, side by side where an upload has its one. */
.cr-acts { flex: 0 0 auto; display: inline-flex; gap: 4px; }
/* Pause, Resume and Remove as shapes (create.js iconButton), as small as the words beside them. */
.up-act.up-icon { display: inline-flex; align-items: center; justify-content: center; padding: 3px 7px; line-height: 0; }
.up-act.up-icon svg { display: block; }
.cr-row[data-state="held"] .up-name { color: var(--muted); }
/* The marked photos (marks.js): a small picture of each, its name to open it, and the way to unmark it. */
.mk-row { padding-top: 7px; padding-bottom: 7px; }
.mk-thumb { flex: 0 0 auto; width: 36px; height: 36px; border-radius: 6px; object-fit: cover; background: rgb(255 255 255 / .06); }
.mk-name { background: none; border: 0; padding: 0; color: inherit; font: inherit; text-align: left; cursor: pointer; }
.mk-name:hover { color: #fff; text-decoration: underline; }
/* ...and its head lets a button go to a second line rather than out of sight under the panel's edge,
   now that Pair or Unpair can stand beside View all, Move photos and Clear (George, 3 October 2026). */
.marks .up-head { flex-wrap: wrap; row-gap: 6px; }
/* ...and the mark on a marked photograph's tile and thumbnail, in the corner nothing else wears. */
.mark-badge {
  position: absolute; top: 6px; left: 6px; z-index: 4; pointer-events: auto;
  display: grid; place-items: center; width: 22px; height: 22px; border-radius: 999px;
  background: rgb(12 12 16 / .62); color: var(--accent);
  box-shadow: 0 1px 4px rgb(0 0 0 / .5), inset 0 0 0 1px rgb(255 255 255 / .22);
}
.mark-badge svg { display: block; fill: currentColor; }
.strip-thumb .mark-badge { top: 4px; left: 4px; width: 18px; height: 18px; }
.strip-thumb .mark-badge svg { width: 10px; height: 10px; }
.grid.gal-adding .mark-badge { display: none; }
.cr-row[data-state="running"] .up-fill { transition: width 1s linear; }

/* Create, in the info panel: a box that grows with the words, and the two buttons under it. */
.pcreate { display: flex; flex-direction: column; gap: 8px; }
.pcreate-prompt {
  width: 100%; box-sizing: border-box; min-height: 2.4em; resize: none; overflow-y: hidden;
  padding: 7px 12px; border-radius: 12px; border: 1px solid var(--line);
  background: rgb(255 255 255 / .05); color: var(--ink);
  font: inherit; font-size: 13px; line-height: 1.45;
}
.pcreate-prompt::placeholder { color: #8a8a98; }
.pcreate-prompt:focus { outline: none; border-color: var(--accent); }
/* The seed, a short number under the words (create.js). */
.pcreate-seed {
  width: 11em; max-width: 100%; box-sizing: border-box; padding: 6px 12px; border-radius: 12px;
  border: 1px solid var(--line); background: rgb(255 255 255 / .05); color: var(--ink);
  font: inherit; font-size: 13px; font-variant-numeric: tabular-nums;
}
.pcreate-seed::placeholder { color: #8a8a98; }
.pcreate-seed:focus { outline: none; border-color: var(--accent); }
/* Select regions carries a small arrow: down while the tools are put away (pressing it opens a row of
   them below), turning to point up while they are out. */
#pregions { display: inline-flex; align-items: center; gap: 6px; }
.rgn-caret { flex: 0 0 auto; transition: transform .2s ease; }
#pregions.on .rgn-caret { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .rgn-caret { transition: none; } }
.ctl-text.pcreate-go { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.ctl-text.pcreate-go:not(:disabled):hover, .ctl-text.pcreate-go:not(:disabled):focus-visible { background: var(--accent); filter: brightness(1.12); }
/* Nothing to create yet: the button waits, greyed, until "What to change" says something. The
   pointer still reaches it (unlike .ctl-text[disabled]), so its tooltip can say what is missing. */
.ctl-text.pcreate-go:disabled { opacity: .45; cursor: default; pointer-events: auto; }

/* REGIONS drawn over the photograph for Create to keep to (regions.js; George, 28 September 2026):
   a layer over the picture in the photograph's own stage, so the wheel still zooms through it. It
   takes the pointer only while Select regions is pressed. Every line is drawn twice, dark under
   light, so a region shows on a white sky and a black night alike. */
.rgn-layer { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 5; pointer-events: none; overflow: visible; }
.rgn-layer.editing { pointer-events: auto; cursor: crosshair; }
.rgn-layer * { pointer-events: none; }
.rgn-under { fill: none; stroke: rgb(0 0 0 / .55); stroke-width: 4; stroke-linejoin: round; }
.rgn-shape { fill: rgb(255 84 112 / .14); stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; }
.rgn-shape.chosen { fill: rgb(255 84 112 / .26); }
.rgn-saved { fill: rgb(255 255 255 / .1); stroke: #fff; stroke-width: 2; stroke-dasharray: 7 5; stroke-linejoin: round; }
.rgn-bounds { fill: none; stroke: rgb(255 255 255 / .8); stroke-width: 1; stroke-dasharray: 3 3; }
.rgn-handle { fill: #fff; stroke: var(--accent); stroke-width: 1.5; }
.rgn-del circle { fill: var(--accent); stroke: #fff; stroke-width: 1.5; }
.rgn-del path { fill: none; stroke: #fff; stroke-width: 2; stroke-linecap: round; }
.rgn-layer.editing .rgn-shape { pointer-events: all; cursor: move; }
.rgn-layer.editing .rgn-handle, .rgn-layer.editing .rgn-del, .rgn-layer.editing .rgn-del * { pointer-events: all; }
.rgn-layer.editing .rgn-del { cursor: pointer; }
.rgn-handle.h-nw, .rgn-handle.h-se { cursor: nwse-resize; }
.rgn-handle.h-ne, .rgn-handle.h-sw { cursor: nesw-resize; }
.rgn-handle.h-n, .rgn-handle.h-s { cursor: ns-resize; }
.rgn-handle.h-e, .rgn-handle.h-w { cursor: ew-resize; }
/* The row of controls lies over the top of the photograph, and a region is often drawn right there
   (a game's text boxes sit in its top corner): while regions are being drawn, a press between its
   buttons goes through to the photograph. */
.viewer.regions-on .stage > .controls { pointer-events: none; }
.viewer.regions-on .stage > .controls > * { pointer-events: auto; }
.viewer.regions-on .stage > .controls > .sides.reserved { pointer-events: none; }

/* --- the Rating and Visibility lists along the bar (wallfilters.js) ---------------------------- */
/* A chip that opens a list rather than narrowing the wall itself: the same dress as the words, and
   the caret that More already uses to say "there is more under here". */
.tag-drop .tag-name { display: inline-flex; align-items: center; gap: 5px; }
.tag-drop .drop-eye { display: inline-flex; opacity: .8; }
/* THEMES (George, 25 September 2026): He.art's four, each its two colors, over Heart.photos' own
   pink and amber, which is what an unthemed page wears. The accent is everything that is ON (a
   switch, a chosen chip, a heading in the panel), and the clear accent the buttons that take
   something away. */
html[data-theme="violet"] { --accent: #8a7dff; --clear-accent: #ffb86b; }
/* Red, before Rose (George, 8 October 2026: a "Red" theme "that uses #E62117"), with Rose's amber. */
html[data-theme="red"] { --accent: #e62117; --clear-accent: #ffb86b; }
html[data-theme="gold"] { --accent: #ffb86b; --clear-accent: rgb(244 241 234 / .85); }
/* The words on anything the accent fills: white on the dark accents, near-black on the two light ones
   (George, 25 September 2026: black, not white, on the Gold buttons). */
:root { --on-accent: #fff; }
html[data-theme="gold"], html[data-theme="mono"], html[data-theme="teal"] { --on-accent: #16161a; }
html[data-theme="mono"] { --accent: #c8c8d2; --clear-accent: #c8c8d2; }
/* Teal is TEAL, with rose for the clear buttons (George, 4 October 2026: "clicking Pink and Teal look
   exactly the same, are they the same theme?"). It was He.art's theme 4 to the letter, a rose accent
   with teal Clear filters, and that rose (#ff4d6d) is Heart.photos' own pink to the eye, so the two
   differed only on the rare clear button. */
html[data-theme="teal"] { --accent: rgb(94 214 199); --clear-accent: #ff4d6d; }
/* ...and a light accent keeps a switch's knob readable: dark on it, not white on near-white. */
html[data-theme="mono"] .gear-pill.on .gear-knob, html[data-theme="gold"] .gear-pill.on .gear-knob,
html[data-theme="teal"] .gear-pill.on .gear-knob { background: #16161a; }
/* Six chips in the wall's menu wrap rather than widen it. */
.wall-menu .gear-row-choices { align-items: flex-start; }
.wall-menu .gear-row-choices .gear-pill.many { flex: 0 1 auto; min-width: 0; flex-wrap: wrap; justify-content: flex-end; border-radius: 12px; }
/* ...each answer a chip of its own and nothing behind the row, as in the filter menu (George,
   25 September 2026): one background behind chips that wrap onto two lines was a grey box with a
   ragged edge. And the name on one line. */
.wall-menu .gear-row-choices .gear-pill.many { gap: 4px; padding: 0; border-radius: 0; background: none; }
.wall-menu .gear-row-choices .gear-pill.many .gear-half { white-space: nowrap; background: rgb(255 255 255 / .1); }
.wall-menu .gear-row-choices .gear-pill.many .gear-half:hover { background: rgb(255 255 255 / .16); }
.wall-menu .gear-row-choices .gear-pill.many .gear-half.on { background: rgb(255 255 255 / .3); color: #fff; }
.wall-menu .gear-row-choices .gear-name { white-space: nowrap; }

/* CHECKERED BACKGROUND (George, 25 September 2026; on unless turned off): a photograph whose
   background was taken off (a cut-out, made_from) shows a checkerboard where the background was,
   the way every editor says "nothing here". Behind the picture only, in its own box: on the wall's
   tile, the row's thumbnail and the picture in the viewer. */
html.checker .tile.cutout .tile-pic img,
html.checker .strip-thumb.cutout img,
html.checker .viewer.is-cutout .hic-layer img {
  background: repeating-conic-gradient(#3b3b43 0 25%, #2c2c33 0 50%) 0 0 / 16px 16px;
}
/* ...or ONE COLOR there instead, chosen on the slider (Cut-out background: Solid; --cut-color is
   set by layout.js from the slider's place). */
html.cutsolid .tile.cutout .tile-pic img,
html.cutsolid .strip-thumb.cutout img,
html.cutsolid .viewer.is-cutout .hic-layer img {
  background: var(--cut-color, #fff);
}
/* ...and the same behind an animated cut-out's frames, drawn over it (animate.js). */
html.checker .viewer.is-cutout .hic-layer canvas.hic-frames {
  background: repeating-conic-gradient(#3b3b43 0 25%, #2c2c33 0 50%) 0 0 / 16px 16px;
}
html.cutsolid .viewer.is-cutout .hic-layer canvas.hic-frames { background: var(--cut-color, #fff); }
/* The color slider: its track is every color it can be, black at the left, every major color, and
   white at the right (cutColorOf in state.js), and its knob wears the color it is on. */
.gear-slider.gear-hue {
  -webkit-appearance: none; appearance: none; height: 12px; margin: 0; padding: 0; border-radius: 999px;
  background: linear-gradient(to right, #000 0%, hsl(0 80% 50%) 10%, hsl(30 80% 50%) 18%,
    hsl(60 80% 50%) 26%, hsl(120 80% 50%) 42%, hsl(180 80% 50%) 58%, hsl(240 80% 50%) 74%,
    hsl(270 80% 50%) 82%, hsl(300 80% 50%) 90%, #fff 100%);
}
.gear-slider.gear-hue::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%;
  background: var(--thumb, #fff); border: 2px solid #fff; box-shadow: 0 0 0 1px rgb(0 0 0 / .6); cursor: pointer;
}
.gear-slider.gear-hue::-moz-range-thumb {
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--thumb, #fff); border: 2px solid #fff; box-shadow: 0 0 0 1px rgb(0 0 0 / .6); cursor: pointer;
}
.gear-slider.gear-hue::-moz-range-track { background: transparent; }
.gear-val.gear-swatch {
  flex: 0 0 18px; min-width: 18px; width: 18px; height: 18px; border-radius: 5px;
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .3);
}

/* Tags on galleries (George, 25 September 2026): a row of them over the column's list to narrow
   it, and a gallery's own under its description. An internal one (put on by Heart.photos) wears
   the accent's outline and no x. */
.gal-tagfilter { display: flex; flex-wrap: wrap; gap: 5px; padding: 2px 12px 8px; }
.gal-tagfilter[hidden] { display: none; }
.gal-tagchip {
  font: inherit; font-size: 11.5px; padding: 2px 9px; border-radius: 999px; cursor: pointer;
  border: 1px solid rgb(255 255 255 / .16); background: none; color: var(--muted);
}
.gal-tagchip:hover { color: var(--ink); border-color: rgb(255 255 255 / .32); }
.gal-tagchip.internal { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.gal-tagchip.on { background: color-mix(in srgb, var(--accent) 22%, transparent); border-color: var(--accent); color: var(--ink); }
.collection-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 6px 0 2px; }
.collection-tags[hidden] { display: none; }
.coll-tag { display: inline-flex; align-items: center; border: 1px solid rgb(255 255 255 / .18); border-radius: 999px; }
.coll-tag.internal { border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
.coll-tag-word, .coll-tag-x {
  font: inherit; font-size: 12px; border: 0; background: none; color: var(--muted); cursor: pointer; padding: 2px 9px;
}
.coll-tag-word:hover { color: var(--ink); }
.coll-tag-x { padding: 2px 8px 2px 0; }
.coll-tag-x:hover { color: var(--accent); }
.coll-tag-add {
  font: inherit; font-size: 12px; width: 8.5em; padding: 2px 9px; border-radius: 999px;
  border: 1px dashed rgb(255 255 255 / .22); background: none; color: var(--ink);
}
.coll-tag-add:focus { outline: none; border-style: solid; border-color: var(--accent); }

.drop-menu {
  position: absolute; z-index: 45; min-width: 200px; max-width: min(320px, calc(100vw - 16px));
  max-height: min(70vh, 460px); overflow-y: auto; overscroll-behavior: contain;
  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);
}
.drop-menu[hidden] { display: none; }
.drop-row {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 7px 10px; border: 0; border-radius: 8px; background: none;
  color: #cfcfd8; font: inherit; font-size: 13px; text-align: left; cursor: pointer;
}
.drop-row:hover:not(:disabled), .drop-row:focus-visible { background: rgb(255 255 255 / .1); color: #fff; outline: none; }
.drop-row.on { color: #fff; }
.drop-row:disabled { cursor: default; opacity: .6; }
.drop-word { display: inline-flex; align-items: center; gap: 6px; flex: 1 1 auto; min-width: 0; }
.drop-n { color: rgb(255 255 255 / .45); font-size: 11.5px; font-variant-numeric: tabular-nums; }
/* The answer in force has a tick beside it. */
.drop-tick { flex: 0 0 auto; width: 14px; height: 14px; display: grid; place-items: center; }
.drop-row.on .drop-tick::before { content: "\2713"; color: var(--accent); font-size: 13px; }
/* The rating's own marks, at the size the chips drew them: the same icons the badge on a photograph
   is made of, in the button and down the list. */
.tag-rating-drop .tag-name svg, .drop-word svg { display: block; flex: 0 0 auto; width: 14px; height: 14px; }
.tag-rating-drop .like-emo, .drop-word .like-emo { display: inline-block; font-size: 11px; line-height: 1; }
.drop-word svg + svg { margin-left: -5px; }
/* The top two ratings are hearts, and hearts are red: on the photographs, on the chips they
   replaced, and down this list (George, 23 September 2026). Lit, the button is the accent itself and
   its hearts are the button's own white. */
.drop-row.drop-hearts .drop-word svg { color: var(--accent); }

/* --- Multiselect: closing it, and putting the chosen photographs into galleries (select.js) --- */
.sel-close {
  flex: 0 0 auto; display: grid; place-items: center; width: 32px; height: 32px; padding: 0;
  border: 0; border-radius: 999px; background: rgb(255 255 255 / .08); color: #cfcfd8; cursor: pointer;
}
.sel-close:hover, .sel-close:focus-visible { background: rgb(255 255 255 / .2); color: #fff; outline: none; }
/* The list hangs from the header like the header's own controls, so it does not scroll away. */
.gal-pick { position: fixed; width: min(320px, calc(100vw - 16px)); display: flex; flex-direction: column; gap: 6px; }
.gal-pick-find {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 13px; color: #fff;
  background: rgb(255 255 255 / .06); border: 1px solid rgb(255 255 255 / .18); border-radius: 999px;
  padding: 6px 12px;
}
.gal-pick-find:focus { outline: none; border-color: var(--accent); }
.gal-pick-list { max-height: min(50vh, 340px); overflow-y: auto; overscroll-behavior: contain; }
.gal-pick-box { border: 1.5px solid rgb(255 255 255 / .45); border-radius: 3px; }
.gal-pick-row.on .gal-pick-box { border-color: var(--accent); background: var(--accent); }
/* The galleries the photo is in already come first, set off from the rest by a hairline. */
.gal-pick-in + .gal-pick-row:not(.gal-pick-in) { border-top: 1px solid var(--line); }
.gal-pick-row.on .gal-pick-box::before { content: "\2713"; color: #fff; font-size: 11px; }
/* ...and a dash where some of the chosen photos are in it and some are not (George, 2 October 2026). */
.gal-pick-row.some .gal-pick-box { border-color: var(--accent); }
.gal-pick-row.some .gal-pick-box::before { content: "\2013"; color: var(--accent); font-size: 12px; font-weight: 700; line-height: 1; }
.gal-pick-vis { flex: 0 0 auto; font-size: 11px; color: rgb(255 255 255 / .5); }
.gal-pick-empty { margin: 6px 8px; font-size: 12.5px; color: rgb(255 255 255 / .55); }
.gal-pick-foot { display: flex; align-items: center; gap: 10px; padding: 4px 4px 2px; border-top: 1px solid rgb(255 255 255 / .1); }
.gal-pick-say { flex: 1 1 auto; font-size: 12px; color: rgb(255 255 255 / .6); }
/* A Shift+click while a gallery is being added to selects no text, as Multiselect's does not. */
.grid.gal-adding .tile { user-select: none; }
/* A gallery made from inside the list: the plus where a tick would be, in the accent. */
.gal-pick-new { color: var(--accent); }
.gal-pick-new .drop-tick { font-size: 15px; font-weight: 600; line-height: 1; }
.gal-pick-new:hover:not(:disabled) { color: var(--accent); }


/* ---- galleries inside galleries (nest.js; George, 1 October 2026) ------------------------------
   A gallery standing in another one's wall is its cover drawn as a STACK, two cards peeking above
   it so it reads as several photographs and not as one, with "+12" on its corner for how many are
   in it ("a "+#" on the thumbnail for how many photos are in that gallery"). */
.tile.nest { background: transparent; }
.tile.nest .tile-pic { background: transparent; }
.tile.nest .tile-pic > img { position: absolute; left: 0; right: 0; bottom: 0; height: calc(100% - 8px); border-radius: 10px 10px 0 0; }
.tile.nest .tile-pic::before, .tile.nest .tile-pic::after {
  content: ""; position: absolute; height: 4px; border-radius: 4px 4px 0 0; pointer-events: none;
}
.tile.nest .tile-pic::before { top: 0; left: 18px; right: 18px; background: rgb(255 255 255 / .2); }
.tile.nest .tile-pic::after { top: 4px; left: 9px; right: 9px; background: rgb(255 255 255 / .36); }
.nest-n {
  position: absolute; right: 8px; bottom: 8px; z-index: 3; pointer-events: none;
  padding: 1px 9px; border-radius: 999px; font-size: 13px; font-weight: 700; line-height: 1.6;
  font-variant-numeric: tabular-nums; color: #fff; background: rgb(0 0 0 / .62);
  box-shadow: 0 0 0 1px rgb(255 255 255 / .38);
}
.strip-thumb.nest .nest-n { right: 4px; bottom: 4px; padding: 0 6px; font-size: 11px; }
.strip-thumb.nest::before {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  box-shadow: inset 0 3px 0 rgb(255 255 255 / .35);
}
.nest-thumb .nest-n { right: 5px; bottom: 5px; padding: 0 7px; font-size: 12px; }

/* ...and opened in the viewer, ITS wall where the photograph would be ("shows the wall in the space
   the big image normally is"). It takes the photograph's place in the stage, in the same order the
   picture has, and the picture's own box steps aside while it is up. */
.stage > #nestwall { order: 1; }
.viewer.bar-top .stage > #nestwall { order: 2; }
.viewer.on-nest:not(.nest-zooming) .stage > #pane { display: none; }
.nestwall {
  flex: 1 1 auto; min-height: 0; width: 100%; box-sizing: border-box; overflow-y: auto;
  overscroll-behavior: contain; padding: 14px 18px 18px; color: #fff;
  display: flex; flex-direction: column; gap: 12px;
}
.nestwall[hidden] { display: none; }
/* Infinite zoom (zoomplay.js): its layer, laid over the pane at the pane's place (set by the script),
   the photographs clipped to the frame the first of them fills; and its seek bar over the button bar. */
.zq { position: fixed; z-index: 1; overflow: hidden; pointer-events: none; }
.zq[hidden] { display: none; }
.zq-box { position: absolute; overflow: hidden; }
.zq-box img {
  position: absolute; left: 50%; top: 50%; width: 100%; height: 100%; transform-origin: 50% 50%;
  will-change: transform; user-select: none; -webkit-user-drag: none; pointer-events: none;
}
.zq-box img[hidden] { display: none; }
/* Numbered frames (zoomplay.js): each frame's place in the gallery at its bottom-left corner, one size
   whatever the zoom, in the big area and on every thumbnail of the row. */
.zq-num {
  position: absolute; z-index: 50; transform: translateY(-100%); margin: -4px 0 0 4px;
  padding: 1px 7px; border-radius: 6px; background: rgb(0 0 0 / .72); color: #fff;
  font: 600 13px/1.45 system-ui, sans-serif; font-variant-numeric: tabular-nums;
  pointer-events: none; white-space: nowrap;
}
.zq-num[hidden] { display: none; }
.zq-num-rest { position: fixed; z-index: 2; }
.strip-thumb .frame-num {
  position: absolute; left: 3px; bottom: 3px; z-index: 2; padding: 0 4px; border-radius: 4px;
  background: rgb(0 0 0 / .72); color: #fff; font: 600 11px/1.4 system-ui, sans-serif;
  font-variant-numeric: tabular-nums; pointer-events: none;
}
.zq-seek-row { display: flex; flex: 1 0 100%; box-sizing: border-box; }
/* The seek lines (Infinite zoom's, an animation's) go between the buttons and the photograph: under
   the buttons where the bar is at the top, over them where the thumbnails, and so the bar, are at the
   foot (George, 4 October 2026: "Move the seek bar below the button bar", then "but when thumbnails
   are at the bottom move the seek bar above the button bar"). First in the markup, so the foot's is
   the order they are written in. */
.viewer.bar-top .controls > .zq-seek-row { order: 1; }
/* As wide as the buttons under it and over them (fitSeek sets both): Play, the line, the time. */
.zq-seek-in { display: flex; align-items: center; gap: 10px; min-width: 0; max-width: 100%; }
.zq-seek-row[hidden] { display: none; }
.zq-seek { flex: 1 1 auto; min-width: 0; accent-color: var(--accent); cursor: pointer; }
.zq-time { flex: none; font-size: 12px; color: rgb(255 255 255 / .7); font-variant-numeric: tabular-nums; }
.zplay-btn { flex: none; }
.full-btn { flex: none; }
/* Too narrow for its buttons and a line worth dragging (fitSeekRow): the line on a line of its own, nearest
   the photograph (under the buttons with the bar at the top, over them at the foot), the buttons wrapping. */
.zq-seek-in.stacked { flex-wrap: wrap; justify-content: center; row-gap: 4px; }
.zq-seek-in.stacked .zq-seek { flex: 1 0 100%; order: -1; }
.viewer.bar-top .zq-seek-in.stacked .zq-seek { order: 1; }
/* The speed just before Full screen in every row (speed.js; George, 10 October 2026: "Keep button positions
   consistent"): a small, quiet button, as the time is, and the row of chips it opens over the photograph, leftward
   from its right edge so they stay on the screen: above the bar, or below it where the bar is at the top. */
.speed-wrap { position: relative; flex: none; display: inline-flex; }
.speed-wrap[hidden] { display: none; }
/* The button, the chips and the panel they sit on are YouTube's rounded ones (George, 10 October 2026:
   "Use this kind of rounded background for the playback speed", showing youtube.com's speed panel): pills of
   faint white with no edge, the one chosen brighter, on a rounded dark panel; the button a pill as Play's
   and the volume's discs are. */
.speed-btn {
  height: 32px; padding: 0 12px; border: 0; border-radius: 999px; background: rgb(255 255 255 / .08);
  color: #fff; font: 600 13px/1 system-ui, sans-serif; font-variant-numeric: tabular-nums;
  white-space: nowrap; cursor: pointer; transition: background .15s ease;
}
.speed-btn:hover, .speed-btn:focus-visible, .speed-btn[aria-expanded="true"] { background: rgb(255 255 255 / .2); }
.speed-pop {
  position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 40;
  display: flex; flex-wrap: nowrap; align-items: center; gap: 8px; padding: 10px;
  border: 0; border-radius: 14px; background: rgb(28 28 28 / .92);
  box-shadow: 0 6px 24px rgb(0 0 0 / .45); white-space: nowrap;
}
.speed-pop[hidden] { display: none; }
/* ...within the window's width, on more rows where one would not fit (George, 12 October 2026: "Fit the popups
   to the screen"; speed.js fitPop moves them sideways onto the screen). */
.speed-pop { flex-wrap: wrap; width: max-content; max-width: calc(100vw - 16px); box-sizing: border-box; }
.viewer.bar-top .speed-pop { bottom: auto; top: calc(100% + 8px); }
.speed-chip {
  min-width: 52px; padding: 9px 14px; border: 0; border-radius: 999px; background: rgb(255 255 255 / .1);
  color: #fff; font: 500 13px/1 system-ui, sans-serif; font-variant-numeric: tabular-nums;
  cursor: pointer; transition: background .15s ease;
}
.speed-chip:hover, .speed-chip:focus-visible { background: rgb(255 255 255 / .2); }
.speed-chip[aria-pressed="true"] { background: rgb(255 255 255 / .34); font-weight: 700; }
/* The resolution's chips (moment.js syncRes): the one playing lit, as the speed chosen is. Without the
   Heart.photos extension they are a list, not a choice, and say so by not answering the pointer. */
.res-pop.res-list .res-chip:not([aria-pressed="true"]) { cursor: default; color: rgb(255 255 255 / .72); }
.res-pop.res-list .res-chip:not([aria-pressed="true"]):hover { background: rgb(255 255 255 / .1); }
/* Always max quality: a pill of the list that is always pressable, lit while on. */
.res-pop.res-list .res-chip.res-mode, .res-pop .res-chip.res-mode { cursor: pointer; color: #fff; }
.psrc-cap { margin: 0 0 6px; }
/* Where a saved photo or gallery came from, to an admin (savecopy.js). */
.psrc-saved, .sb-gal-saved { margin: 6px 0 0; }
.res-tag { font-size: .58em; font-weight: 700; line-height: 1; vertical-align: super; margin-left: 2px; opacity: .85; }
.res-note { flex-basis: 100%; margin: 0; padding: 2px 4px 0; color: rgb(255 255 255 / .7); font: 400 12px/1.35 system-ui, sans-serif; white-space: normal; max-width: 420px; }
/* A rectangle, with the extension as without (George, 12 October 2026: "Why did it change to one row, keep it a
   rectangle so we can comfortably read everything and see all the buttons"): the wrap was the plain list's
   alone, so with the extension the chips and the note ran along one line and over one another. */
.res-pop { flex-wrap: wrap; width: max-content; max-width: min(460px, calc(100vw - 24px)); }
.res-pop .res-chip { flex: none; }
/* THE VIDEO'S GEAR (George, 12 October 2026: "Create Settings gear and put Quality and Playback speed into it
   like YouTube"): a round button as Full screen's beside it, its panel a line for the speed and a line for the
   quality, each under its name. The gear turns a twelfth of a turn as the panel opens and back as it closes,
   YouTube's two positions ("Spin the gear icon when it opens the settings menu and spin again in reverse when
   it closes ... rotate smoothly between the two positions"); the HD or 4K it wears stays upright. */
.set-btn { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 32px; padding: 0; }
.set-gear { display: block; width: 18px; height: 18px; transition: rotate .25s ease; }
.set-btn[aria-expanded="true"] .set-gear { rotate: 30deg; }
@media (prefers-reduced-motion: reduce) { .set-gear { transition: none; } }
.set-tag { position: absolute; top: 3px; right: 1px; padding: 1px 2px; border-radius: 2px; background: #f03; color: #fff; font: 700 8px/1 system-ui, sans-serif; letter-spacing: .02em; pointer-events: none; }
.set-tag[hidden] { display: none; }
.speed-pop.set-pop { flex-direction: column; flex-wrap: nowrap; align-items: stretch; gap: 12px; max-width: min(460px, calc(100vw - 24px)); }
.set-line { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.set-name { padding: 0 4px; color: rgb(255 255 255 / .7); font: 600 12px/1 system-ui, sans-serif; }
.set-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.set-pop .res-pop { width: auto; max-width: none; }
/* On a phone the row of chips wraps rather than running off the screen. */
html.mobile .speed-pop { flex-wrap: wrap; width: max-content; max-width: calc(100vw - 24px); }
.zplay-btn svg[hidden] { display: none; }
.zq-seek:disabled { cursor: default; opacity: .45; }
/* An animated photograph's seek row (animate.js) is Infinite zoom's line, and a frame back and a frame on
   beside Play on a phone, which has no , and . keys, only while it stands still. Where the <img> plays it
   (no frames), Play alone. */
.an-step { display: none; flex: none; }
html.mobile .an-seek-row.paused:not(.plain) .an-step { display: inline-flex; align-items: center; justify-content: center; }
.an-seek-row.plain .zq-seek, .an-seek-row.plain .zq-time { display: none; }
/* On a phone the bar shows only the way out, the pair switch and the settings (html.mobile .controls),
   and a seek line is a line of its own across the top of them. */
html.mobile .viewer .controls > .zq-seek-row:not([hidden]) { display: flex; grid-column: 1 / -1; }
/* ...the bar the whole width of it, on a line of its own, with Play, the steps and the time under it:
   beside them a phone left the bar a stub, too short to drag through anything. */
html.mobile .zq-seek-in { flex-wrap: wrap; row-gap: 4px; }
html.mobile .zq-seek-in .zq-seek { flex: 1 0 100%; order: -1; }
.collection-zoom { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 14px; font-size: 14px; }
.collection-zoom[hidden] { display: none; }
.collection-zoom label { display: inline-flex; align-items: center; gap: 6px; }
.collection-zoom-pct { width: 64px; }
.gal-zoom.on { color: var(--accent); border-color: var(--accent); }
@media (min-width: 701px) {
  .viewer.keys-open .stage > #nestwall { padding-left: calc(var(--keys-w) + 18px); }
}
.nest-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
.nest-title { margin: 0; font-size: 18px; font-weight: 650; text-wrap: balance; }
.nest-by { font-size: 14px; font-weight: 400; color: rgb(255 255 255 / .6); }
.nest-by[hidden] { display: none; }
.nest-count { font-size: 13px; color: rgb(255 255 255 / .65); font-variant-numeric: tabular-nums; }
.nest-desc { flex-basis: 100%; margin: 0; max-width: 70ch; font-size: 13px; color: rgb(255 255 255 / .75); }
.nest-desc[hidden] { display: none; }
/* A gallery that zooms is played in the viewer's own pane, with the image component in it, and its wall goes:
   no heading over it and no thumbnails under it (George, 15 October 2026: "remove this", and "Why are we showing
   the frames below the video player they shouldn't be shown below the video"), and the wheel zooms it as it zooms
   the frames ("allow zoom in with mousewheel when playing an infinite scroll gallery in a gallery"; nestzoom.js,
   zoomplay.js nestImage). */
.viewer.on-nest.nest-zooming .stage > #nestwall { display: none; }
.nest-grid { display: flex; flex-wrap: wrap; gap: 6px; }
/* ...rearranged by hand (nest.js): the thumbnail in hand marked while it takes each new place. */
.nest-grid.nest-arranging, .nest-grid.nest-arranging .nest-thumb { cursor: grabbing; }
/* ...smoothly, as the wall is (.tile-dragging): the others slide (the transition, NEST_SETTLE_MS), and the
   one in hand has none, since it must be exactly under the pointer, lifted over the rest. */
.nest-grid .nest-thumb { transition: transform .19s ease; }
.nest-thumb.nest-dragging {
  z-index: 4; transition: none; will-change: transform; pointer-events: none;
  box-shadow: 0 14px 34px rgb(0 0 0 / .55); outline: 2px solid var(--accent); outline-offset: -2px;
}
.nest-grid.nest-arranging .nest-thumb:hover { outline: none; }
.nest-grid::after { content: ""; flex-grow: 999999; }
.nest-grid[aria-busy="true"]:empty { min-height: 120px; }
.nest-thumb {
  position: relative; height: var(--nest-h, 170px); padding: 0; border: 0; border-radius: 8px;
  overflow: hidden; cursor: pointer; background: rgb(255 255 255 / .06);
  flex-grow: var(--r, 1.5); flex-basis: calc(var(--r, 1.5) * var(--nest-h, 170px));
  max-width: calc(var(--r, 1.5) * var(--nest-h, 170px) * 1.6);
}
.nest-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.nest-thumb[data-rot="180"] img { transform: rotate(180deg); }
.nest-thumb[data-rot="90"] img, .nest-thumb[data-rot="270"] img {
  position: absolute; top: 50%; left: 50%; width: var(--nest-h, 170px); height: calc(var(--nest-h, 170px) * var(--r, 1.5));
  transform: translate(-50%, -50%) rotate(var(--rot, 0deg));
}
.nest-thumb:hover, .nest-thumb:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.nest-thumb.cutout { background: repeating-conic-gradient(#2a2a32 0% 25%, #1c1c22 0% 50%) 50% / 16px 16px; }
.nest-empty-say { margin: 8px 0; color: rgb(255 255 255 / .6); font-size: 14px; }
.nest-more { margin: 0; display: flex; justify-content: center; }
.nest-more[hidden] { display: none; }
@media (max-width: 700px) { .nestwall { --nest-h: 120px; padding: 10px 10px 14px; } }
/* Nothing on screen is a photograph: the sizes, the slider and the download have nothing to act on,
   and keep their places so the row does not shift as the arrows walk on and off a gallery. */
.viewer.on-nest:not(.nest-zooming) #sizes, .viewer.on-nest:not(.nest-zooming) .vb-zoom, .viewer.on-nest #btn-download { visibility: hidden; }

/* Add gallery, over a gallery of the reader's own, and the list it opens. */
.nest-add { position: relative; display: inline-flex; gap: 10px; }   /* Add gallery, Add tag */
.nest-pick {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 30;
  width: min(300px, calc(100vw - 24px)); max-height: min(60vh, 380px);
  display: flex; flex-direction: column; gap: 6px; padding: 8px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--panel); box-shadow: var(--shadow);
}
.nest-pick[hidden] { display: none; }
.nest-pick .gal-search { width: 100%; box-sizing: border-box; }
.nest-pick-list { overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 1px; }
.nest-pick-item {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 6px 8px; border: 0; border-radius: 8px; background: transparent; color: var(--ink);
  font: inherit; font-size: 13.5px; text-align: left; cursor: pointer;
}
.nest-pick-item .gal-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nest-pick-item .gal-n { flex: 0 0 auto; color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.nest-pick-item:hover, .nest-pick-item:focus-visible { background: var(--line); outline: none; }
.nest-pick-say { margin: 4px 8px; font-size: 13px; color: var(--muted); }
/* ...clear of the row of controls where it floats over the picture (Overlay): on the side the
   thumbnails are on, where the row sits, so the gallery's name and its first row are not under it. */
.viewer.overlay-bar:not(.no-bar) .stage > #nestwall { padding-bottom: calc(var(--ctl-h, 56px) + 12px); }
.viewer.overlay-bar.strip-top:not(.no-bar) .stage > #nestwall,
.viewer.overlay-bar.bar-top:not(.no-bar) .stage > #nestwall { padding-top: calc(var(--ctl-h, 56px) + 8px); padding-bottom: 18px; }
/* A gallery with nothing to show the reader yet: the stack, and the mark of a gallery in it. */
.tile.nest-bare .tile-pic, .strip-thumb.nest-bare, .nest-thumb.nest-bare { background: rgb(255 255 255 / .05); }
.tile.nest-bare .tile-pic > img, .strip-thumb.nest-bare > img, .nest-thumb.nest-bare > img { visibility: hidden; }
.tile.nest-bare .tile-pic { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239a9aa6' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='6' width='18' height='14' rx='2'/%3E%3Cpath d='M7 3h10'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: center 58%; background-size: 44px; }

/* ---- Create a new image (newimage.js; George, 1 October 2026) ---------------------------------
   Opened from the header since 8 October 2026 (.top-create, above). */
/* Photo studio's form in this site's own dialog: the viewer's dark glass, a column of choices. */
.cnew {
  width: min(560px, calc(100vw - 24px)); max-height: calc(100dvh - 32px); padding: 0;
  border: 1px solid rgb(255 255 255 / .14); border-radius: 16px;
  background: rgb(18 18 22 / .98); color: #f2f2f4; box-shadow: 0 18px 50px rgb(0 0 0 / .6);
}
.cnew::backdrop { background: rgb(6 6 8 / .72); }
.cnew-form { display: flex; flex-direction: column; gap: 12px; padding: 16px 18px 18px; overflow-y: auto; max-height: calc(100dvh - 34px); box-sizing: border-box; }
.cnew-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.cnew-head h2 { margin: 0; font-size: 18px; font-weight: 650; }
.cnew-sub { margin: -4px 0 0; font-size: 13px; color: rgb(255 255 255 / .62); }
.cnew-label { font-size: 13px; font-weight: 600; color: rgb(255 255 255 / .85); }
.cnew-small { font-size: 12px; font-weight: 400; color: rgb(255 255 255 / .55); }
.cnew-prompt, .cnew-field input {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 14px; color: #fff;
  background: rgb(255 255 255 / .06); border: 1px solid rgb(255 255 255 / .18); border-radius: 10px; padding: 9px 11px;
}
.cnew-prompt { resize: vertical; min-height: 64px; }
.cnew-prompt:focus, .cnew-field input:focus { outline: none; border-color: var(--accent); }
.cnew-set { margin: 0; padding: 0; border: 0; display: flex; flex-direction: column; gap: 6px; }
.cnew-set[hidden] { display: none; }
.cnew-set legend { padding: 0; margin-bottom: 6px; font-size: 13px; font-weight: 600; color: rgb(255 255 255 / .85); }
.cnew-choices { display: flex; flex-wrap: wrap; gap: 6px; }
.cnew-choice {
  display: inline-flex; align-items: baseline; gap: 6px; padding: 6px 11px; border-radius: 999px; cursor: pointer;
  border: 1px solid rgb(255 255 255 / .16); background: rgb(255 255 255 / .04); font-size: 13px;
}
.cnew-choice input { accent-color: var(--accent); margin: 0; align-self: center; }
.cnew-choice:has(input:checked) { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }
.cnew-choice.off { opacity: .45; cursor: not-allowed; }
.cnew-two { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
@media (max-width: 520px) { .cnew-two { grid-template-columns: 1fr; } }
.cnew-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.cnew-into { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: rgb(255 255 255 / .85); cursor: pointer; }
.cnew-into[hidden] { display: none; }
.cnew-into input { accent-color: var(--accent); }
.cnew-go { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.cnew-go button:disabled { opacity: .5; cursor: not-allowed; }
.cnew-hand { display: flex; flex-direction: column; gap: 6px; }
.cnew-hand[hidden] { display: none; }
.cnew-hand-list { display: flex; flex-wrap: wrap; gap: 6px; }
.cnew-mark {
  width: 64px; height: 64px; padding: 0; border: 0; border-radius: 8px; overflow: hidden; cursor: pointer;
  background: rgb(255 255 255 / .06); outline: 2px solid transparent; outline-offset: -2px; opacity: .6;
}
.cnew-mark img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cnew-mark.on { outline-color: var(--accent); opacity: 1; }
.cnew-mark:hover, .cnew-mark:focus-visible { opacity: 1; }
.cnew-hand .cnew-small { margin: 0; }
/* Choose model (editmodel.js; George, 3 October 2026): in the photo panel the button, and beside it the
   model Create uses; over the viewer, the models that can change a photo, one press each, the one in
   use marked as Create a new image marks its choice. */
.pmodel-name { min-width: 0; font-size: 12px; color: #a3a3b2; overflow-wrap: anywhere; }
.pmodel-dlg { width: min(480px, calc(100vw - 24px)); }
/* Are you sure, before a source is changed (sidebar.js askSourceChange). */
.psrc-dlg { width: min(420px, calc(100vw - 24px)); }
.psrc-dlg-acts { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
.psrc-dlg-acts .psrc-dlg-yes { background: var(--accent); color: var(--on-accent, #16161a); border-color: var(--accent); }
/* Delete gallery's one choice (galleries.js deleteGallery). */
.gdel-photos { display: flex; align-items: center; gap: 9px; font-size: 14px; cursor: pointer; }
.gdel-photos input { width: 16px; height: 16px; margin: 0; accent-color: var(--accent); cursor: pointer; }
.gdel-dlg #gdel-hint { margin: -6px 0 0 25px; }
/* Restore and Delete forever over a photograph in the Trash (trash.js trashTile), and Delete forever over a file in
   the Trash on R2 (r2trash.js): in the middle of the picture, seen on hover or focus, always where nothing hovers;
   never while choosing in Multiselect. */
.tile-acts { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 3;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  opacity: 0; pointer-events: none; transition: opacity .12s; }
.tile-restore, .tile-forever { padding: 7px 18px; border: 0; border-radius: 999px; white-space: nowrap;
  font: inherit; font-size: 13px; font-weight: 650; cursor: pointer; box-shadow: 0 4px 14px rgb(0 0 0 / .45); }
.tile-restore { background: var(--accent); color: var(--on-accent, #16161a); }
.tile-forever { background: rgb(20 20 24 / .86); color: #fff; outline: 1px solid rgb(255 84 112 / .7); }
.tile-forever:hover, .tile-forever:focus-visible { background: rgb(190 40 64); }
.tile:hover .tile-acts, .tile:focus .tile-acts, .tile:focus-within .tile-acts,
.r2tile:hover .tile-acts, .r2tile:focus-within .tile-acts { opacity: 1; pointer-events: auto; }
@media (hover: none) { .tile-acts { opacity: 1; pointer-events: auto; } }
body:has(header.selecting) .tile-acts { display: none; }
/* THE TRASH ON R2 (r2trash.js): the bucket's trashed files, each a tile with what it was. */
.r2trash { margin: 4px 0 24px; padding: 0 var(--gap); }
.r2trash[hidden] { display: none; }
.r2trash-sum { margin: 0 0 12px; font-size: 13px; color: var(--muted); }
.r2trash-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
.r2tile { position: relative; display: flex; flex-direction: column; border-radius: 10px; overflow: hidden;
  background: rgb(255 255 255 / .04); outline: none; }
.r2tile:focus-visible { box-shadow: 0 0 0 2px var(--accent); }
.r2tile-pic { position: relative; aspect-ratio: 4 / 3; background: #000; display: flex; align-items: center; justify-content: center; }
.r2tile-pic img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; }
.r2tile-pic .r2-noimg { font-size: 12px; color: var(--muted); padding: 8px; text-align: center; }
.r2trash-cap { padding: 8px 10px 10px; font-size: 12px; line-height: 1.45; color: var(--muted); overflow-wrap: anywhere; }
.r2trash-cap .r2-what { color: var(--ink); font-weight: 600; font-size: 13px; }
.r2trash-cap a { color: inherit; }
.r2trash-cap .r2-kept { color: #f87171; }
.r2tile.r2-going { opacity: .4; pointer-events: none; }
/* A source written down is held until Are you sure? lets it be changed. */
.psrc-input[readonly] { cursor: text; }
.pmodel-list { display: flex; flex-direction: column; gap: 6px; }
.pmodel-choice {
  display: flex; flex-direction: column; align-items: flex-start; gap: 3px; width: 100%; box-sizing: border-box;
  padding: 9px 12px; border-radius: 12px; cursor: pointer; text-align: left; font: inherit; color: inherit;
  border: 1px solid rgb(255 255 255 / .16); background: rgb(255 255 255 / .04);
}
.pmodel-choice:hover, .pmodel-choice:focus-visible { border-color: rgb(255 255 255 / .42); outline: none; }
.pmodel-choice.on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); }
.pmodel-choice-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.pmodel-choice-name { font-size: 14px; font-weight: 600; }
.pmodel-tag { font-size: 11px; font-weight: 600; padding: 1px 7px; border-radius: 999px;
  border: 1px solid rgb(255 255 255 / .25); color: rgb(255 255 255 / .75); }
/* A gallery open in the viewer has a panel of its own (George, 15 October 2026: "also show all the other gallery
   metadata like title in the sidebar when viewing galleries inside a gallery"; sidebar.js renderNestPanel): the
   gallery it is in at the top, then only the gallery's own sections. Until then the panel stepped aside. */
.viewer.on-nest #sidebar .sb-body > :not(.pnest), .viewer.on-nest #sidebar #pname,
.viewer.on-nest #sidebar #pvis-words { display: none; }
.pnest-title { display: block; color: #fff; font-size: 15px; font-weight: 650; text-decoration: none; text-wrap: balance; }
.pnest-title:hover, .pnest-title:focus-visible { text-decoration: underline; }
/* ...and the gallery's box keeps clear of it, as the photograph's pane does. */
@media (min-width: 701px) {
  .viewer.on-nest.sidebar-open .stage > #nestwall { padding-right: calc(var(--sb-w) + 18px); }
}
#pnest-sec { display: flex; flex-direction: column; gap: 4px; }
#pnest-sec[hidden], #pnest-src-sec[hidden] { display: none; }
.pnest-desc { margin: 4px 0 0; font-size: 13px; color: rgb(255 255 255 / .8); white-space: pre-wrap; overflow-wrap: anywhere; }
.pnest-desc[hidden] { display: none; }
#pnest-tags { margin-top: 4px; }
.psrc-frames { margin-bottom: 6px; }

/* --- the He.art AI chat (photoweb/aichat/aichat.js) ------------------------------------------------ */
/* George, 3 October 2026: "clicking the chat icon opens a sidebar from top-to-bottom taking 25% of the
   browser width that is completely separate from the existing UI (treat it like a Chrome DevTools
   sidebar)". Docked, the component puts .heart-ai-docked and --heart-ai-dock (its width) on <html> and
   fires a resize: the page then ENDS at its edge rather than running on under it, the body and every
   layer pinned to the window alike, the photo viewer included (it is a modal in the top layer, sized
   against the whole window, so it is narrowed by hand). */
html.heart-ai-docked body { margin-right: var(--heart-ai-dock); }
/* In vw, not %, so the width is a number of pixels wherever it is read: a page snapshot copies the
   viewer alone, and a percentage there is of the copy (the page has no scrollbar while it is open). */
html.heart-ai-docked .viewer[open] { width: calc(100vw - var(--heart-ai-dock)); max-width: none; }
html.heart-ai-docked.is-fullscreen #viewer[open] { width: calc(100vw - var(--heart-ai-dock)); }
html.heart-ai-docked .to-top { right: calc(14px + var(--heart-ai-dock)); }
html.heart-ai-docked .dock { right: calc(14px + var(--heart-ai-dock)); }
html.heart-ai-docked .viewer.sidebar-open .dock { right: calc(var(--sb-w) + 14px + var(--heart-ai-dock)); }
html.heart-ai-docked .note,
html.heart-ai-docked .long-wait,
html.heart-ai-docked .keys:not(.pinned) { left: calc((100% - var(--heart-ai-dock)) / 2); }
html.heart-ai-docked .dropveil { right: var(--heart-ai-dock); }
html.heart-ai-docked.mobile.galleries-open .wall { right: var(--heart-ai-dock); }
html.heart-ai-docked .cnew[open] { right: var(--heart-ai-dock); }
/* Its button keeps the corner the dock's panels keep: clear of the photo panel and of a row of
   thumbnails along the foot while a photograph is open (it is carried into the viewer with it). */
.viewer.sidebar-open heart-ai-chat { --ai-launcher-right: calc(var(--sb-w) + 14px); }
.viewer[open].strip-bottom:not(.no-strip) heart-ai-chat { --ai-launcher-bottom: calc(var(--strip-h) + 14px); }
/* ...and wears the page's accent, whichever theme is on. */
heart-ai-chat { --ai-accent: var(--accent); --ai-accent-ink: var(--on-accent); }

/* ---- A photograph that comes alive (moment.js; George, 9 October 2026) ---------------------------
   YouTube's player laid over the photograph at its size, out of sight until it plays from the
   photograph's own second, so the photograph turns into the video with no jump; and out of sight
   again, slowly, when it ends. Clicks go through it to the photograph, which plays and pauses. In sight at
   once, with no fade, on the photograph's very frame (George, 10 October 2026: "there shouldn't be any
   transition the static photo is the exact video frame"): a fade in mixed the still photograph with the
   moving video. */
.mv-box { position: absolute; z-index: 2; pointer-events: none; opacity: 0; transition: opacity .3s ease; overflow: hidden; }
.mv-box.on { opacity: 1; transition: none; }
/* The photograph is out of sight while its video is over it (George, 12 October 2026: "The photo is visible in
   the bottom pixel below the video, better to hide the photo with visibility:hidden css to avoid seeing this
   sliver"): the video's box and the photograph do not always end on the same pixel. It keeps its place, so
   nothing moves, and is back the moment the box is no longer the one in sight. Not under the frame shown for
   a save (.mv-shot), which is a picture of the photograph's own place. */
:has(> .mv-box.on:not(.mv-shot)) > :not(.mv-box) img, :has(> .mv-box.on:not(.mv-shot)) > img { visibility: hidden; }
/* ...the player taller than the box by a band each way (moment.js place), where YouTube draws its title,
   logo and link button, and the box cuts the bands off: the picture alone shows (George, 9 October 2026:
   "move the youtube branding out of the player"). */
.mv-box iframe { position: absolute; left: 0; top: 0; display: block; width: 100%; height: 100%; border: 0; }
/* ...and its row's way to the video on YouTube, at the second playing (as Vi.ki's player has): YouTube's
   own logo, its 2024 drawing (Wikimedia Commons, File:YouTube_2024.svg), so it shows over any video, light
   or dark (George, 10 October 2026: "Can't see YouTube logo in this video, make it look exactly like the
   YouTube logo so it is visible in all backgrounds"). With no white pill behind it (the same day: "I don't
   like the white border around the youtube logo, remove it"), it is YouTube's logo for dark grounds, the
   wordmark white, on the header's own shading, with no shadow of its own (the same day: "remove the black
   shadow from the YouTube logo"). */
.mv-yt { flex: none; display: inline-flex; align-items: center; height: 30px; margin-left: 10px; text-decoration: none; }
.mv-yt:hover, .mv-yt:focus-visible { opacity: .85; }
.mv-yt svg { flex: none; display: block; }
/* ...the time, with room on each side as if it sat in a pill of its own as YouTube's does, but with no pill
   (George, 10 October 2026: "add more space for the time as if it was in its own bubble like youtube ... but
   don't show a bubble"), a button that shows the time left (moment.js) with no look of one but its pointer. */
.mv-time { height: 32px; padding: 0 10px; border: 0; border-radius: 999px; background: none;
  color: rgb(255 255 255 / .7); font: 12px/1 system-ui, sans-serif; font-variant-numeric: tabular-nums;
  white-space: nowrap; cursor: pointer; }
.mv-time:hover, .mv-time:focus-visible { color: #fff; }
/* ...CC, as Vi.ki's player has it (the same day: "add CC before the playback speed on heart.photos as we have
   it on vi.ki"), in the speed button's pill (the same day: "on heart.photos use the same treatment for the CC
   as for the playback speed"): faint white, brighter under the pointer, the letters dim while off. On, as
   YouTube marks it: the letters white and a short line of the accent under them, nothing filled (the same
   day: "Our CC button when enabled is too visually distracting please make it less distracting for its "On"
   state"). */
.mv-cc {
  position: relative; flex: none; height: 32px; padding: 0 12px; border: 0; border-radius: 999px; background: rgb(255 255 255 / .08);
  color: rgb(255 255 255 / .6); font: 600 13px/1 system-ui, sans-serif; white-space: nowrap; cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.mv-cc:hover, .mv-cc:focus-visible { background: rgb(255 255 255 / .2); color: #fff; }
.mv-cc[aria-pressed="true"] { color: #fff; }
.mv-cc[aria-pressed="true"]::after {
  content: ""; position: absolute; left: 30%; right: 30%; bottom: 6px; height: 2px; border-radius: 1px; background: var(--accent);
}
/* ...and its volume after Play (George, 9 October 2026: "also add volume control after the play button"),
   one control as YouTube's is (the same day: "the volume is one control not two as they currently are"): the
   speaker and its slider in one pill that lights as a whole, the speaker no circle of its own. */
.mv-vol-wrap { flex: none; display: inline-flex; align-items: center; border-radius: 999px;
  background: rgb(255 255 255 / .08); transition: background .15s ease; }
.mv-vol-wrap:hover, .mv-vol-wrap:has(:focus-visible) { background: rgb(255 255 255 / .2); }
.mv-vol-wrap .mv-vol-btn, .mv-vol-wrap .mv-vol-btn:hover { flex: none; background: none; }
.mv-vol-btn svg[hidden] { display: none; }

/* The speaker's waves shrink into it on mute and grow out of it on unmute, the small one first, as YouTube's do
   (George, 10 October 2026: "... when clicking mute then unmute just as YouTube has"; moment.js showLook). */
.mv-vol-btn .mv-wave { transform-box: fill-box; transform-origin: left center; }
.mv-vol-btn.waves-in .mv-wave { animation: mv-wave-in .22s ease-out both; }
.mv-vol-btn.waves-in .mv-wave-2 { animation-delay: .05s; }
.mv-vol-btn.waves-out .mv-wave { animation: mv-wave-out .17s ease-in both; }
.mv-vol-btn.big-in .mv-wave-2 { animation: mv-wave-in .2s ease-out both; }
.mv-vol-btn.big-out .mv-wave-2 { animation: mv-wave-out .15s ease-in both; }
@keyframes mv-wave-in { from { transform: scale(0); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes mv-wave-out { from { transform: scale(1); opacity: 1; } to { transform: scale(0); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .mv-vol-btn .mv-wave { animation: none !important; } }
/* The slider white and as thin as YouTube's (same day: "where it's currently red make it white and
   thinner"): a 3px line, white as far as the volume goes and faint after it (--mv-vol, which moment.js
   keeps), and a 12px white knob. */
.mv-vol { flex: none; width: 64px; height: 20px; margin: 0 12px 0 0; background: none; cursor: pointer;
  -webkit-appearance: none; appearance: none;
  transition: width .2s ease, margin .2s ease, opacity .2s ease, visibility 0s; }
.mv-vol::-webkit-slider-runnable-track { height: 2px; border-radius: 1px;
  background: linear-gradient(to right, #fff var(--mv-vol, 100%), rgb(80 80 80 / .72) var(--mv-vol, 100%)); }
.mv-vol::-moz-range-track { height: 2px; border-radius: 1px;
  background: linear-gradient(to right, #fff var(--mv-vol, 100%), rgb(80 80 80 / .72) var(--mv-vol, 100%)); }
/* Thinner than the seek lines (George, 10 October 2026: "reduce the thickness of the volume slider by
   2px", then "increase the volume slider height by 1px"): a 2px line, the knob centered on it. */
.mv-vol::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; margin-top: -5px; border-radius: 50%; background: #fff; border: 0; }
.mv-vol::-moz-range-thumb { width: 12px; height: 12px; border-radius: 50%; background: #fff; border: 0; }
/* Folded away until the speaker is pointed at, then opened smoothly; open while the pointer is in the row,
   folded away smoothly when it leaves (same day: "hide the bar until user hovers over the volume icon then
   smoothly open it, keep it open if the mouse stays in that row ... and smoothly close it when the mouse
   leaves that row"; moment.js keeps .vol-open on the row). */
.mv-row:not(.vol-open) .mv-vol { width: 0; margin-right: 0; opacity: 0; visibility: hidden;
  transition: width .2s ease, margin .2s ease, opacity .15s ease, visibility 0s .2s; }

/* THE VIDEO ROW AT YOUTUBE'S SIZE (George, 10 October 2026: "Propertionally increase all our video control font
   sizes to match YouTube's"). Measured that day on youtube.com at 1920 by 1080 (a 1258px player): round buttons
   of 40px, Play 18px and the speaker 20px tall, the time in 14px at weight 500. The row was a fifth smaller
   than that (32px buttons, 12px time), so everything in it is a quarter bigger: the buttons, the pills, the
   text, the logo and the slider (the icons' own sizes are in gallery.html). The viewer's other rows keep 32px. */
.mv-row .ctl { width: 40px; height: 40px; }
.mv-row .zq-seek-in { gap: 12px; }
.mv-row .mv-time { height: 40px; padding: 0 12px; font-size: 14px; font-weight: 500; }
.mv-row .mv-cc, .mv-row .speed-btn { height: 40px; padding: 0 15px; font-size: 16px; }
.mv-row .set-btn { width: 40px; padding: 0; }
.mv-row .set-gear { width: 24px; height: 24px; }
.mv-row .mv-yt { height: 38px; }
.mv-row .mv-vol { width: 52px; margin-right: 15px; }
.mv-row .mv-cc[aria-pressed="true"]::after { bottom: 8px; }
/* ...unless Larger video controls is off (George, 10 October 2026: "when off use the previous sizes that match the
   size of our other icons like the "Back (Esc)" arrow"; moment.js setVideoLarge): the row as it was before, its
   round buttons 32px as the viewer's others, the time 12px, the icons 16px (full screen 18). */
html.video-small .mv-row .ctl { width: 32px; height: 32px; }
html.video-small .mv-row .zq-seek-in { gap: 10px; }
html.video-small .mv-row .mv-time { height: 32px; padding: 0 10px; font-size: 12px; font-weight: 400; }
html.video-small .mv-row .mv-cc, html.video-small .mv-row .speed-btn { height: 32px; padding: 0 12px; font-size: 13px; }
html.video-small .mv-row .set-btn { width: 32px; padding: 0; }
html.video-small .mv-row .set-gear { width: 18px; height: 18px; }
html.video-small .mv-row .mv-cc[aria-pressed="true"]::after { bottom: 6px; }
html.video-small .mv-row .mv-yt { height: 30px; }
html.video-small .mv-row .mv-yt svg { width: 75px; height: 16px; }
html.video-small .mv-row.vol-open .mv-vol { width: 52px; margin-right: 12px; }
html.video-small .mv-row .zp-play, html.video-small .mv-row .zp-pause,
html.video-small .mv-row .mv-vol-btn svg { width: 16px; height: 16px; }
html.video-small .mv-row .full-btn svg { width: 18px; height: 18px; }
/* ...and the animation's and Infinite zoom's rows the same, on and off (George, 10 October 2026: "when "Larger video
   controls" is on, also use the same size for animated gif video controls and for infinite zoom library video
   controls"): YouTube's Play and Pause at 22px (gallery.html), 40px buttons, the time 14px at weight 500; off,
   their earlier sizes, Play and Pause as tall as they were (10px of YouTube's shape is the old 15px of the old one). */
.an-seek-row .ctl, #zq-seek-row .ctl { width: 40px; height: 40px; }
.an-seek-row .zq-seek-in, #zq-seek-row .zq-seek-in { gap: 12px; }
.an-seek-row .zq-time, #zq-seek-row .zq-time { font-size: 14px; font-weight: 500; }
.an-seek-row .speed-btn, #zq-seek-row .speed-btn { height: 40px; padding: 0 15px; font-size: 16px; }
html.video-small .an-seek-row .ctl, html.video-small #zq-seek-row .ctl { width: 32px; height: 32px; }
html.video-small .an-seek-row .zq-seek-in, html.video-small #zq-seek-row .zq-seek-in { gap: 10px; }
html.video-small .an-seek-row .zq-time, html.video-small #zq-seek-row .zq-time { font-size: 12px; font-weight: 400; }
html.video-small .an-seek-row .speed-btn, html.video-small #zq-seek-row .speed-btn { height: 32px; padding: 0 12px; font-size: 13px; }
html.video-small .an-seek-row .zp-play, html.video-small .an-seek-row .zp-pause,
html.video-small #zq-seek-row .zp-play, html.video-small #zq-seek-row .zp-pause { width: 10px; height: 10px; }
html.video-small .an-seek-row .full-btn svg, html.video-small #zq-seek-row .full-btn svg { width: 18px; height: 18px; }
/* The panel's Video section: what it plays, and the way to name a moment. */
.pmoment-say a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.pmoment-form { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* The box and its Open on one line, as Source's are (.psrc-row), and Open Heartbeat under them. */
.pmoment-row { flex: 1 0 100%; display: flex; align-items: center; gap: 10px; min-width: 0; }
.pmoment-in { flex: 1 1 auto; min-width: 0; box-sizing: border-box; height: 30px; padding: 4px 10px; font: inherit; font-size: 13px;
  color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
.pmoment-in:focus { outline: none; border-color: var(--accent); }
/* A link Heartbeat refused, after the box saved itself (moment.js saveLink); the words are its title. */
.pmoment-in.bad { border-color: #e5484d; }
/* Under the box, Open Heartbeat (George, 10 October 2026: "add a button "Open h.ki" below the input", renamed the
   same day), a button; the box has the line to itself while it is empty, so its example shows whole. */
.pmoment-hki { display: inline-flex; align-items: center; text-decoration: none; }

/* Every slider a thin line (George, 10 October 2026: "change the thickness of the seek bar to match
   youtube's thickness ... and use the same thickness for sliders like zoom on both sites", then "use the
   same thickness as the volume slider for all the other sliders except the video seek slider and the
   background color picker slider in settings"): Zoom, the settings' sliders and a layer's are a 2px line,
   as the volume is, and the seek lines (an animation's, Infinite zoom's, a video's) keep YouTube's 3px.
   Each is the accent as far as the knob and faint after it, with a 12px knob. The line is the knob's
   border-image reaching out across the slider, which hides what passes its ends, so it follows the knob
   wherever a script or a drag puts it, with nothing to keep in step. The color slider (.gear-hue) keeps
   its band of every color, which a thin line would hide. */
.zq-seek,
.sb-slider,
.gear-slider:not(.gear-hue),
.layered-slide input[type="range"] {
  --line: var(--accent); --rest: rgb(255 255 255 / .3); --thick: 2px;
  height: 12px; margin: 0; padding: 0; background: none; overflow: hidden; cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.zq-seek { --thick: 3px; }
/* ...and past the knob, the lines in the viewer's bar (the seek lines, Zoom, and the volume below) a dark grey, as
   YouTube's (George, 10 October 2026: "Use this dark grey for the seek bar that is ahead of the current position so
   it is more visible on white backgrounds", showing youtube.com's player, and of ours: "Can't see our current light
   grey seek bar color on white background"). Measured on that screenshot, YouTube's is about half grey over white
   and a little darker than a dark frame; this one is grey at .72, about the same over white (129) and still a
   line over the viewer's black (60), where YouTube's would vanish. The settings' and a layer's sliders sit on dark
   panels and keep the faint white. */
/* For an afternoon of 11 October 2026 they were white in a dark casing, a pixel taller each way, so as to read
   over a dark photograph too; George the same day: "revert the scrollbar color" and "use the same thickness for
   the zoom slider as the volume slider" (the casing made Zoom's line look twice the volume's). The grey is back,
   and nothing lies behind the line. */
.zq-seek, .controls .sb-slider { --rest: rgb(80 80 80 / .72); }
.zq-seek::-webkit-slider-thumb,
.sb-slider::-webkit-slider-thumb,
.gear-slider:not(.gear-hue)::-webkit-slider-thumb,
.layered-slide input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 12px; height: 12px; border-radius: 50%; background: var(--line);
  border-image: linear-gradient(90deg, var(--line) 50%, var(--rest) 0) 0 1 / calc(50% - var(--thick) / 2) 100vw / 0 100vw;
}
.zq-seek::-moz-range-thumb,
.sb-slider::-moz-range-thumb,
.gear-slider:not(.gear-hue)::-moz-range-thumb,
.layered-slide input[type="range"]::-moz-range-thumb {
  width: 12px; height: 12px; border: none; border-radius: 50%; background: var(--line);
  border-image: linear-gradient(90deg, var(--line) 50%, var(--rest) 0) 0 1 / calc(50% - var(--thick) / 2) 100vw / 0 100vw;
}
.zq-seek::-moz-range-track,
.sb-slider::-moz-range-track,
.gear-slider:not(.gear-hue)::-moz-range-track,
.layered-slide input[type="range"]::-moz-range-track,
.zq-seek::-moz-range-progress,
.sb-slider::-moz-range-progress,
.gear-slider:not(.gear-hue)::-moz-range-progress,
.layered-slide input[type="range"]::-moz-range-progress { background: none; }

/* Save frame as new image (moment.js captureBox): while the tab is captured and the video cut out of it,
   nothing that can lie over the video is drawn, for that moment. */
html.frame-capture .viewer .controls,
html.frame-capture .viewer .strip,
html.frame-capture #dock,
html.frame-capture heart-ai-chat,
html.frame-capture #note { visibility: hidden !important; }
/* ...and no pointer in it (George, 10 October 2026: "it captured the YouTube Play button and the mouse
   cursor which is bad"): a captured tab draws the pointer as the page shows it. The second player a frame
   is taken from stands over the paused one (moment.js THE SECOND PLAYER). */
html.frame-capture, html.frame-capture * { cursor: none !important; }
.mv-box.mv-shot { z-index: 3; }

/* TEST MODE (testmode.js; George, 15 October 2026: "when on show the 3 embedded players at the top of the viewer
   sidebar ... label the players 1 2 3"), for admins alone: the players out of sight, moved whole into the top of
   the photo panel, each at 16:9 with its number over it and what it holds under it. A waiting player is hidden
   (display none) over the picture; here it is shown, at the panel's width, as it is, and never as "on". */
/* The panel is a flex column that lays its children out at their own width, and a frame of width 100% has none of its
   own, so the section came out 28 px wide and every frame 0 x 0, which George saw as black squares (15 October 2026:
   "currently I also see black squares for "Player 1" "Player 2" and "Player 3""): the section takes the panel's width. */
.tp { flex: none; align-self: stretch; width: 100%; box-sizing: border-box; padding: 10px 14px 6px; border-bottom: 1px solid rgb(255 255 255 / .08); }
.tp-head { margin: 0 0 6px; font-size: 12px; color: #9a9aa6; }
.tp-slot { margin: 0 0 10px; }
.tp-frame { position: relative; width: 100%; aspect-ratio: 16 / 9; background: #000; border-radius: 6px; overflow: hidden; }
.tp-still { position: absolute; z-index: 1; inset: 0; width: 100%; height: 100%; object-fit: contain; background: #000; }
.tp-label { position: absolute; z-index: 2; left: 6px; top: 6px; padding: 2px 8px; border-radius: 999px;
  background: rgb(0 0 0 / .72); color: #fff; font-size: 12px; font-weight: 600; line-height: 1.4; pointer-events: none; }
.tp-info { margin: 4px 0 0; font-size: 11.5px; line-height: 1.4; color: #b9b9c4; overflow-wrap: anywhere; }

/* WAITING, as YouTube shows it (moment.js syncSpin; George, 12 October 2026: "Show a loading spinner while waiting
   for a video to start playing exactly as YouTube's"): its ring, 64 px, white with a soft dark edge so it reads
   over a bright frame, an arc that grows and shrinks as it turns, in the middle of the video. */
.mv-spin { position: absolute; z-index: 5; width: 64px; height: 64px; margin: -32px 0 0 -32px; pointer-events: none;
  opacity: 0; transition: opacity .15s ease; }
.mv-spin.on { opacity: 1; }
/* THE BIG PLAY over a photograph that has a video, as YouTube's embedded player shows it before it starts (moment.js
   syncBigPlay; George, 17 October 2026): its shape and size, dark until the pointer is on it, red then, and gone with
   the button bar and the row (bar-idle). */
.mv-bigplay { position: absolute; z-index: 5; width: 68px; height: 48px; margin: -24px 0 0 -34px; padding: 0; border: 0;
  background: none; cursor: default; opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .3s ease, visibility 0s linear .3s; }
.mv-bigplay.on { opacity: 1; visibility: visible; pointer-events: auto; transition: opacity .15s ease; }
#viewer.bar-idle .mv-bigplay.on { opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .3s ease, visibility 0s linear .3s; }
/* The arrow, not a hand, over a photograph with a video and over its big Play, as YouTube's player shows (George, 17
   October 2026: "use the pointer not the hand mouse cursor over the video same as youtube uses"; moment.js syncRow).
   A pointer at rest still goes (pointer-idle, !important). */
#viewer.has-video .hic, #viewer.has-video .hic:active { cursor: default; }
/* THE BEZEL: Pause or Play just pressed, in the middle of the video, as YouTube shows it (moment.js bezel; George,
   17 October 2026): fades in, stands a second, fades out. */
.mv-bezel { position: absolute; z-index: 6; width: 64px; height: 64px; margin: -32px 0 0 -32px; border-radius: 50%;
  background: rgb(0 0 0 / .55); pointer-events: none; opacity: 0; }
.mv-bezel svg { display: block; width: 100%; height: 100%; padding: 10px; box-sizing: border-box; fill: #fff; }
/* ...standing half a second since the same evening (George, 17 October 2026: "change the timer to hide the pause/play icon to 0.5 seconds instead of 1 second when pause/play video, show the loading spinner when playing a new video not in the round robin instead of waiting 1 second before showing it and change the 1-second rule to 0.75 seconds when opening a video in the round robin"): a quarter of a second in, half
   a second there, four tenths out. */
.mv-bezel.on { animation: mv-bezel-flash 1.15s ease both; }
@keyframes mv-bezel-flash {
  0% { opacity: 0; transform: scale(.85); }
  22% { opacity: 1; transform: scale(1); }
  65% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.1); }
}
@media (prefers-reduced-motion: reduce) { .mv-bezel.on { animation-name: mv-bezel-fade; } }
@keyframes mv-bezel-fade { 0%, 100% { opacity: 0; } 22%, 65% { opacity: 1; } }
.mv-bigplay svg { display: block; width: 100%; height: 100%; filter: drop-shadow(0 0 2px rgb(0 0 0 / .4)); }
.mv-bigplay-bg { fill: #212121; fill-opacity: .8; transition: fill .1s cubic-bezier(.4, 0, 1, 1), fill-opacity .1s cubic-bezier(.4, 0, 1, 1); }
.mv-bigplay:hover .mv-bigplay-bg, .mv-bigplay:focus-visible .mv-bigplay-bg { fill: #f00; fill-opacity: 1; }
.mv-bigplay:focus-visible { outline: 2px solid #fff; outline-offset: 2px; border-radius: 14px; }
/* ...and Infinite zoom's while it buffers (zoomplay.js startPlay), over every photograph of its layer. */
.zq-spin { z-index: 20; }
.mv-spin svg { display: block; width: 100%; height: 100%; animation: mv-spin-turn 1.6s linear infinite;
  filter: drop-shadow(0 0 2px rgb(0 0 0 / .55)); }
.mv-spin:not(.on) svg, .mv-spin:not(.on) circle { animation-play-state: paused; }
.mv-spin circle { fill: none; stroke: #fff; stroke-width: 6; stroke-linecap: round; transform-origin: 50% 50%;
  stroke-dasharray: 1 200; stroke-dashoffset: 0; animation: mv-spin-arc 1.6s ease-in-out infinite; }
@keyframes mv-spin-turn { to { transform: rotate(360deg); } }
@keyframes mv-spin-arc {
  0% { stroke-dasharray: 1 200; stroke-dashoffset: 0; }
  50% { stroke-dasharray: 120 200; stroke-dashoffset: -40px; }
  100% { stroke-dasharray: 120 200; stroke-dashoffset: -180px; }
}
@media (prefers-reduced-motion: reduce) { .mv-spin svg { animation-duration: 3.2s; } .mv-spin circle { animation: none; stroke-dasharray: 90 200; } }

/* The second player a frame is read in (moment.js readerFrame; George, 12 October 2026): the extension's script
   reads the video's own pixels inside it, so it is never looked at. A real size, since YouTube lays its player
   out by it, and out of sight and out of the pointer's way. */
.mv-reader { position: fixed; left: 0; top: 0; z-index: -1; width: 640px; height: 360px; border: 0; opacity: 0; pointer-events: none; }

/* THE VIDEO'S FULL SCREEN, as Vi.ki's player's (George, 10 October 2026: "as on vi.ki the full screen icon for
   the video on heart.photos makes the video full screen and nothing else comes with it just the video", then
   "why not use the same implementation of the youtube video full screen control bar from vi.ki on
   heart.photos"): the viewer's stage alone has the screen (layout.js toggleVideoFullscreen), the picture
   and YouTube's player filling it, and the bar lies over its foot on a shade, edge to edge, with the video's
   row alone and its line as long as the screen allows, as Vi.ki's .hb-stage:fullscreen .hb-bar does; it
   goes with the pointer (moment.js, bar-idle). */
#viewer .stage:fullscreen { padding: 0 !important; background: #000; }
#viewer .stage:fullscreen > #pane { padding: 0 !important; }
/* The shade is only as tall as the row (George, 10 October 2026: "Shading is too tall in full screen, should be
   only as tall as needed to fit the controls"): it was 3rem over the row, about 90 px. Short, it darkens
   quickly so the white buttons stay legible over a bright picture. */
#viewer .stage:fullscreen > .controls {
  position: absolute; z-index: 6; left: 0; right: 0; top: auto; bottom: 0; margin: 0;
  width: auto; max-width: none; padding: 10px 1rem 8px;
  background: linear-gradient(to bottom, rgb(0 0 0 / 0), rgb(0 0 0 / .6) 45%, rgb(0 0 0 / .8)); transition: opacity .2s ease;
}
#viewer .stage:fullscreen > .controls::before { content: none; }
/* ...and Infinite zoom's or an animation's row the same way (George, 10 October 2026: "add the full screen icon
   and hotkey for infinite zoom galleries that behaves the same as youtube videos with the same layout except no
   YouTube logo and no volume controls and no CC", then "same for animated gifs"): whichever row plays alone in
   the bar, in its own order, which is the video's (the speed after the line, Full screen last); and no bar on a
   photograph that plays nothing, gone to with an arrow. */
#viewer .stage:fullscreen > .controls > :not(.zq-seek-row) { display: none !important; }
#viewer .stage:fullscreen > .controls > .zq-seek-row { contain: none; order: 0; }
#viewer .stage:fullscreen > .controls:not(:has(> .zq-seek-row:not([hidden]))) { display: none !important; }
#viewer .stage:fullscreen .zq-seek-in { width: 100% !important; margin-left: 0 !important; }
/* The chips open above the bar there, wherever the bar is outside full screen (George, 10 October 2026: "Keep
   the playback speed buttons onscreen"). */
#viewer .stage:fullscreen .speed-pop { top: auto; bottom: calc(100% + 8px); }
#viewer.bar-idle .stage:fullscreen > .controls { opacity: 0; pointer-events: none; transition: opacity .3s ease; }
#viewer.pointer-idle .stage:fullscreen, #viewer.pointer-idle .stage:fullscreen * { cursor: none !important; }

/* THE VIDEO'S CONTROLS ALONG THE FOOT OF THE VIDEO (George, 12 October 2026: "When the thumbnails are at any edge,
   move the video controls to the bottom and make them as wide as the video and autohide them the same as when
   the video is full screen and keep the thumbnail button bar at the top even when the thumbnails are at the
   bottom"). Outside full screen too, the video's row leaves the button bar and lies over the foot of the picture,
   exactly as wide as the picture is on screen, on the same shade as in full screen, and goes with the pointer
   by the same rules (moment.js placeRow and bar-idle). It is the row itself that moves, fixed to the window
   where the script says the picture's foot is (nothing between it and the window filters, see the note on the
   overlaid bar), so the bar it came from closes up around what is left. Not on a phone, where the bar is the
   phone's own. An animated photograph's row and Infinite zoom's go the same way since 15 October 2026 (George: "for
   animated gifs move the video controls below the video just as we do for embedded youtube videos and do the same
   for infinite zoom galleries"): the row placed is marked .row-under (moment.js playRowOn).
   ALONG THE FOOT OF THE BIG IMAGE AREA since 17 October 2026 (George: "move the video controls to the bottom of the
   big image area not just below the video to balance the position of the button bar which is at the top of the big
   image area"): the pane cut to the photo panel, edge to edge (moment.js placeRow, pictureRect), whatever the video's
   shape. The rules that laid it below the video where there was room, with no shade (video-below), went then. */
#viewer.video-under .zq-seek-row.row-under {
  position: fixed; z-index: 7; contain: none; order: 0; margin: 0; box-sizing: border-box; padding: 22px 12px 8px;
  background: linear-gradient(to bottom, rgb(0 0 0 / 0), rgb(0 0 0 / .6) 45%, rgb(0 0 0 / .8)); transition: opacity .3s ease;
}
#viewer.video-under .row-under .zq-seek-in { width: 100% !important; margin-left: 0 !important; }
#viewer.video-under .row-under .speed-pop { top: auto; bottom: calc(100% + 8px); }
#viewer.video-under.bar-idle .row-under { opacity: 0; pointer-events: none; }
/* ...and the pointer with it, over the picture (George, 15 October 2026: "In all cases also hide the mouse cursor
   if it is touching the video so it isn't covering any portion of the video"), as it goes in full screen above:
   the video, an animation and Infinite zoom alike. YouTube's player takes no pointer (.mv-box), so the pointer
   over the video is the page's to hide. Since 17 October 2026 over every picture, and only while the pointer stands
   still on it (George: "when the mouse stops moving inside the big image then hide the button bar and video controls
   and mouse cursor after 1.5 seconds"; moment.js settle, pointer-idle): never outside the picture, where the
   controls go at once but the pointer stays. */
#viewer.pointer-idle #pane, #viewer.pointer-idle #pane * { cursor: none !important; }
/* B puts the button bar away, not the video's row (George, 12 October 2026: "The B hotkey only applies to the
   thumbnail button bar not the video control bar because it auto hides when it overlaps the video"): the row
   is the bar's child, so it is shown again inside the hidden bar. */
#viewer.no-bar.video-under .row-under { visibility: visible; }
#viewer.no-bar.video-under:not(.bar-idle) .row-under { pointer-events: auto; }
/* ...and the button bar goes by itself while the video plays, back when it is paused (George, 12 October 2026:
   "when playing a video hide the button bar, restore it when video is paused"; moment.js syncRow): hidden as B
   hides it, keeping its place, the video's row and an open settings menu left in sight. */
/* An animated photograph and Infinite zoom the same way since 15 October 2026 (George: "as we hide the button bar
   when the video starts playing for youtube videos so should we hide the button bar in the same way for animated
   gifs and infinite zooms"; moment.js syncPlays): the row left in sight is whichever plays, .row-under. */
/* ...and since 17 October 2026 it goes and comes back as the video's row does (George: "apply the same autohide logic to
   the button bar as we have for the video controls on the viewer"): once something has played on the photograph, it
   shows while the pointer moves or a key is pressed and goes BAR_IDLE_MS after (bar-idle, moment.js), playing or
   paused, never while the pointer is on it (restBar), and not at all while B keeps it (.bar-kept). It was away for
   the whole of a play until then, and back only on pause. */
/* ...and on every photograph, played or not, by where the pointer is, since the same evening (George, 17 October
   2026: "when the mouse leaves the big image then immediately autohide the button bar and video controls", "when
   the mouse moves inside the big image then immediately show the button bar and video controls", and the rest in
   moment.js, pointerAt): bar-idle is set by the pointer's place, not by what plays, so these rules no longer wait
   for .video-under. */
/* FADED, NOT SWITCHED (George, 17 October 2026: "instead of a sudden on/off let's make the button bar and video controls
   fade away and fade in smoothly"): the bar fades out over .3s and only then stops being there (visibility after the
   fade, so it cannot be pressed while invisible), and fades back in from the moment it is wanted (the rule below). */
#viewer .stage > .controls { transition: opacity .3s ease, visibility 0s linear 0s; }
#viewer.bar-idle:not(.bar-kept) .controls { opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .3s ease, visibility 0s linear .3s; }
#viewer.bar-idle:not(.bar-kept) .gear-menu:not([hidden]) { visibility: visible; pointer-events: auto; }
html:not(.bar-shade) #viewer.video-under .row-under { background: none; }
/* ...and the button bar at the top with the thumbnails along the foot, as it is with them down a side. */
.viewer.overlay-bar.bar-top.strip-bottom .controls,
.viewer.no-bar.bar-top.strip-bottom .controls { top: 0; bottom: auto; }

/* LARGER CONTROLS (George, 10 October 2026: "Rename "Larger video controls" to "Larger controls" and have it apply
   to all our icons and buttons like the "Back (Esc)" button that goes from the gallery to the wall for consistent
   sizes"): while it is on, everything else in the viewer's bar is drawn a quarter larger, as the rows that play
   already are (40px buttons where they were 32), so Back, the switches, the pills, the counter, the zoom and the
   buttons at the end stand as tall as Play; and the thumbnails' arrows with them. zoom scales a control whole (its
   box, its drawing, its words) and keeps its proportions; the rows that play have their own sizes above, and the
   menus hang beside the buttons, not in them, so both keep theirs. Off (html.video-small, the setting's first
   name), the sizes they had. */
html:not(.video-small) .viewer .controls > :is(.ctl, .ctl-text, .pair-switch, .ctl-count, .sides, .vb-group),
html:not(.video-small) .viewer .controls > .gear-wrap > .ctl { zoom: 1.25; }
html:not(.video-small) .viewer .controls { gap: 10px; }
html:not(.video-small) .strip-page::before { width: 40px; height: 40px; }
html:not(.video-small) .strip-page svg { width: 20px; height: 20px; }
/* BUTTON BAR SHADING, off unless turned on (George, 10 October 2026: "add a new setting below "Larger video
   controls" called "Button bar shading" default Off which removes the shading behind our buttons bars so it looks
   like vi.ki photo viewer and like YouTube ... and when "Button bar shading" is turned on we add back the shading
   we currently have", showing youtube.com's player: "Like YouTube, remove the background shading and simply shade
   the buttons but don't shade the seek bar"). Off: no wash behind the bar, over the photograph, at the foot of a
   video over the whole viewer or along the stage's own full screen; each control on a shade of its own instead,
   Vi.ki's viewer's (wall.css .v-btn: black, darker under the pointer; here at .5 rather than .45, so white words
   hold on a white photograph), the round buttons as discs and the rest as pills, and the seek lines bare. A lit
   control keeps its light, and the speaker keeps no disc inside its pill. On (html.bar-shade, layout.js
   setBarShade), the rules before these, unchanged. */
html:not(.bar-shade) .viewer.overlay-bar .controls::before { content: none; }
html:not(.bar-shade) #viewer .stage:fullscreen > .controls { background: none; }
html:not(.bar-shade) .viewer .controls .ctl:not(.on):not(.mv-vol-btn),
html:not(.bar-shade) .viewer .controls .pair-switch,
html:not(.bar-shade) .viewer .controls .browse-switch,
html:not(.bar-shade) .viewer .controls .ctl-count,
html:not(.bar-shade) .viewer .controls .vb-zoom,
html:not(.bar-shade) .viewer .controls .zq-time,
html:not(.bar-shade) .viewer .controls .mv-yt,
html:not(.bar-shade) .viewer .controls .mv-cc,
html:not(.bar-shade) .viewer .controls .speed-btn,
html:not(.bar-shade) .viewer .controls .mv-vol-wrap { background: rgb(0 0 0 / .5); }
html:not(.bar-shade) .viewer .controls .ctl:not(.on):not(.mv-vol-btn):hover,
html:not(.bar-shade) .viewer .controls .browse-switch:is(:hover, :focus-visible),
html:not(.bar-shade) .viewer .controls .mv-time:is(:hover, :focus-visible),
html:not(.bar-shade) .viewer .controls .mv-yt:is(:hover, :focus-visible),
html:not(.bar-shade) .viewer .controls .mv-cc:is(:hover, :focus-visible),
html:not(.bar-shade) .viewer .controls .speed-btn:is(:hover, :focus-visible, [aria-expanded="true"]),
html:not(.bar-shade) .viewer .controls .mv-vol-wrap:is(:hover, :has(:focus-visible)) { background: rgb(0 0 0 / .75); }
/* ...each a pill: the pairs lose their outline, and what had no shape of its own (the count, the zoom, the time
   beside Infinite zoom's and an animation's line, YouTube's logo) gets one, as tall as the buttons beside it. */
html:not(.bar-shade) .viewer .controls .pair-switch { border-color: transparent; }
html:not(.bar-shade) .viewer .controls :is(.ctl-count, .vb-zoom) { height: 32px; padding: 0 12px; border-radius: 999px; box-sizing: border-box; }
html:not(.bar-shade) .viewer .controls .zq-time:not(.mv-time) { height: 40px; line-height: 40px; padding: 0 12px; border-radius: 999px; }
html.video-small:not(.bar-shade) .viewer .controls .zq-time:not(.mv-time) { height: 32px; line-height: 32px; padding: 0 10px; }
html:not(.bar-shade) .viewer .controls .zq-time:empty { background: none; padding: 0; }
html:not(.bar-shade) .viewer .controls .mv-yt { padding: 0 10px; border-radius: 999px; }

/* ---- WALL THEME 2 ---------------------------------------------------------------------------------
   George, 13 October 2026: "Create "Wall theme: value "2" (default for everyone) so the same wall looks like
   https://h.ki/ligaatrhk", then "In "Wall theme: 2" add the avatar of the creator, make the title more
   visible, add "time ago" line, don't show view count". h.ki's cards, measured there: a 16:9 picture with
   8px corners and 8px of card around it, a 36px avatar beside the title (16px, medium weight, two lines at
   most), then the creator's name and how long ago (12px, the quiet color). As many columns of at least
   300px times Thumbnail size as fit, every picture that shape and cropped to it. Theme 1, the justified
   rows, is everything above, untouched: html.wall-2 is wallmenu.js applyWallTheme, the line under the
   title captions.js tileBy. Last in the file, so it wins over the phone's own wall. */
.tile-by { display: none; }
html.wall-2 .grid {
  display: grid; gap: 6px 0; padding: 0 calc(var(--gap) - 8px);
  /* At least 375px a column, a quarter more than the 300px it began with (George, 13 October 2026: "make thumbnails 25%
     larger on the wall in Wall theme 2"). */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, calc(375px * var(--thumb-scale, 1))), 1fr));
}
html.wall-2 .grid::after { content: none; }
/* A pair or a series is its photographs, each a card, one after another. */
html.wall-2 .grid > .series-cell:not(.filtered) { display: contents; }
html.wall-2 .pair-link { display: none; }
html.wall-2 .row-break { grid-column: 1 / -1; }
html.wall-2 .grid > .tile, html.wall-2 .series-cell .tile {
  display: grid; grid-template-columns: 36px minmax(0, 1fr); grid-template-rows: auto auto auto 1fr;
  column-gap: 12px; align-content: start;
  width: auto; height: auto; max-width: none; flex: none; margin: 0;
  padding: 8px 8px 12px; border-radius: 14px; background: transparent;
  /* The picture's width, for the tag strip laid over its foot (100cqw below). */
  container-type: inline-size;
}
html.wall-2 .tile:hover, html.wall-2 .tile:focus-visible {
  transform: none; box-shadow: none; background: color-mix(in srgb, var(--ink) 7%, transparent);
}
html.wall-2 .tile-pic {
  grid-column: 1 / -1; grid-row: 1; height: auto; max-height: none; aspect-ratio: 16 / 9;
  border-radius: 10px; background: var(--line); margin-bottom: 12px;
}
html.wall-2 .tile-name {
  grid-column: 2; grid-row: 2; display: block; height: auto; padding: 0; background: none;
  white-space: normal; overflow: visible; font-size: 16px; line-height: 1.4; font-weight: 500; color: var(--ink);
}
html.wall-2 .tile-name-text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
html.wall-2 .tile-name-why { display: inline-block; margin: 4px 4px 0 0; font-weight: 400; vertical-align: top; }
html.wall-2 .tile-by { display: contents; }
html.wall-2 .tile-by-pic {
  grid-column: 1; grid-row: 2 / span 3; align-self: start; width: 36px; height: 36px; border-radius: 50%;
  overflow: hidden; display: grid; place-items: center; background: var(--line);
  color: var(--ink); font-size: 15px; font-weight: 600; text-decoration: none;
}
/* Not the thumbnail's own rules (faded in, turned with the photo, picked): an avatar is just a picture. */
html.wall-2 .tile .tile-by-pic img {
  position: static; width: 100%; height: 100%; object-fit: cover; opacity: 1; transform: none; transition: none;
}
html.wall-2 .tile-by-name, html.wall-2 .tile-by-ago {
  grid-column: 2; justify-self: start; max-width: 100%; min-width: 0;
  font-size: 12.5px; line-height: 1.4; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-decoration: none;
}
html.wall-2 .tile-by-name { grid-row: 3; margin-top: 4px; }
html.wall-2 .tile-by-name[href]:hover { color: var(--ink); }
html.wall-2 .tile-by-ago { grid-row: 4; }
/* The tag strip on the picture's foot: the card is the picture's width plus 8px each side. */
html.wall-2 .tile-tags {
  left: 8px; right: 8px; bottom: auto; top: calc(8px + 100cqw * 9 / 16); transform: translateY(-100%);
  border-radius: 0 0 10px 10px;
}
html.wall-2 .tile.picked::after { inset: 8px 8px auto 8px; aspect-ratio: 16 / 9; border-radius: 10px; }

/* A note listing the photos another official gallery holds (api.js officialNote): the words, then each
   photo as a small thumbnail and a link that opens it in that gallery. A card rather than a pill, as
   a list does not fit on one line. */
.note.note-list { flex-wrap: wrap; border-radius: 14px; align-items: flex-start; padding-bottom: 14px; }
.note-clashes { order: 2; flex: 1 0 100%; list-style: none; margin: 6px 0 0; padding: 0;
  display: grid; gap: 6px; max-height: 50vh; overflow: auto; }
.note-clashes a { display: flex; align-items: center; gap: 10px; color: #1a0a10; text-decoration: underline;
  text-underline-offset: 2px; font-size: 13px; }
.note-clashes img { flex: none; width: 40px; height: 40px; object-fit: cover; border-radius: 6px;
  background: rgb(26 10 16 / .15); }
.note.note-list .note-x { order: 1; margin-left: auto; }
/* An official gallery says so, quietly, after its name (galleries.js renderHeading), and so does a
   photo's list of galleries (sidebar.js). */
.collection-official { font-size: 12px; font-weight: 500; color: var(--muted); margin-left: 8px;
  padding: 1px 8px; border: 1px solid currentColor; border-radius: 999px; vertical-align: middle; }
.collection-official[hidden] { display: none; }
.pgal-official { opacity: .7; font-size: .9em; margin-left: 4px; }

/* The photo's own image and its video frame side by side in the Video section (moment.js vframePair; George, 16
   October 2026: "sure, do as you suggest to compare them visually"). */
.pmoment-vpair { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 6px; }
.pmoment-vpair.one { grid-template-columns: 1fr; }
.pmoment-vpair.one .pmoment-vcap { white-space: normal; }
.pmoment-vfig { display: flex; flex-direction: column; gap: 3px; color: var(--muted); text-decoration: none; min-width: 0; }
.pmoment-vfig img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 6px; background: #000; display: block; }
.pmoment-vfig:hover .pmoment-vcap { color: var(--ink); }
.pmoment-vcap { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Change thumbnail (moment.js renderThumbChange): a small heading, the two buttons, the link box, the earlier ones. */
.pthumb { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
/* Hidden means hidden: the display above overrode the attribute, so Change thumbnail showed on photos with no video
   (George, 19 October 2026, a screenshot of one; moment.js renderThumbChange decides). */
.pthumb[hidden] { display: none; }
.pthumb-head { font-size: 12px; font-weight: 600; color: var(--muted); }
.pthumb-how { margin: 0; }
.pthumb-row { display: flex; flex-wrap: wrap; gap: 6px; }
.pthumb-link { flex: none; }
.pthumb-earlier { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); }
.pthumb.drop { outline: 2px dashed var(--accent); outline-offset: 4px; border-radius: 6px; }

/* An earlier thumbnail beside its Restore button (moment.js drawEarlier; George, 16 October 2026: "where can I see the
   previous thumbnail?"). */
.thumb-earlier-line { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
.thumb-earlier-look { flex: none; display: block; width: 96px; }
.thumb-earlier-look img { width: 96px; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 5px; background: #000; display: block; }
