/* ==========================================================================
   The photo room.

   The shell, the head, the idle bar, the faces, the buttons and every colour
   and duration come from elsewhere -- `styles.css` for the components,
   `themes.css` for what this room is made of -- because this is the same
   product as the converter and not a second one that resembles it.

   **Nothing shared is redefined here.** That line was in this file before it
   was true of it. An early draft restyled `.stage` and `.panel__head`; those
   went, and `.crop`, `.crop__box`, `.crop__grip` and `.crop__edge` stayed,
   which was worse, because the converter crops a video with those same four
   names and this stylesheet loads second. A room's file was restyling another
   room's crop. They are scoped to this room's own face now, and `npm run check`
   refuses a bare class selector that is also a component in `styles.css`, so
   the rule is enforced rather than merely written at the top of the file.

   What is left is what only this room has: the well the picture sits in, the
   crop over it, and the adjustments in their groups.
   ========================================================================== */

/* --- the shell, while this room is working --------------------------------- */

/* Wider than the converter's console, because a photograph is the thing being
   looked at rather than a name and a row of buttons. The shared stylesheet
   already transitions the shell's width, so this widening is part of the same
   morph rather than a jump after it: the console grows outwards and downwards
   together. It goes back to the shared width on the idle face without being
   told, because that is where the rule stops applying. */
/* The face is `photo-edit`, not `edit`. Face names are namespaced per room --
   one shell holds every room's faces now -- and this rule was written before
   they were, so it matched nothing and the editing face was laid out in the
   620px console meant for a bar with one button in it. */
.stage[data-state="photo-edit"] .shell {
  width: min(100%, 1120px);
}

/* --- the editing face's two columns --------------------------------------- */

/* The picture takes the width it can get; the adjustments take a fixed column
   beside it and scroll within the picture's height rather than lengthening the
   shell. Stacked instead, eight sliders under a 400px picture made the shell
   1369px tall on an 860px screen -- taller than the window, which is not a
   shell any more but a page. */
.edit {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 280px;
  gap: var(--s4);
  align-items: start;
}

/* The picture and the controls for looking at it, as one column. */
.edit__picture { min-width: 0; }

/* The adjustments and the notice that there are more of them, as one column.
   The notice is positioned against this rather than against the list, because
   a notice that there is more below must not itself be below the fold. */
.edit__controls { position: relative; min-width: 0; }


/* The adjustments scroll within the picture's height, and the bar they scroll
   with is this room's rather than the operating system's.

   Left alone, macOS draws its overlay scrollbar *on top of* the controls --
   over the right-hand end of every slider -- and then, the moment the pointer
   is anywhere near, swells it into a wide grey-and-white slab that belongs to
   no part of this design.

   So: a gutter is reserved, which is what stops the overlap, and the bar is
   drawn as a hairline in the room's own ink. It widens a little under the
   pointer, because a scrollbar you cannot grab is worse than an ugly one, but
   it stays the room's colour while it does. */
.edit .sliders {
  max-height: clamp(220px, 46vh, 560px);
  overflow-y: auto;
  min-width: 0;

  /* Space for the bar, kept whether or not it is showing, so nothing shifts
     sideways when a group is opened and the list becomes long enough. */
  scrollbar-gutter: stable;
  padding-right: var(--s3);

  /* And four pixels on the other side, which is not for a scrollbar.

     A column that scrolls clips whatever is drawn outside it, and several
     things in here are drawn outside themselves: a chosen pill rings itself
     three pixels out under the pointer, and anything focused by keyboard gets
     a two-pixel outline two pixels clear of its edge. Flush against the left
     of the column, the left half of every one of those was cut off -- so a
     ring that should say "this one" instead drew three sides of a box and
     looked like a rendering fault.

     Four pixels is exactly the furthest anything reaches. */
  padding-left: var(--s1);

  /* Firefox and anything else following the standard property. */
  scrollbar-width: thin;
  scrollbar-color: var(--film-rule) transparent;
}

.edit .sliders::-webkit-scrollbar { width: 6px; }
.edit .sliders::-webkit-scrollbar-track { background: transparent; }
.edit .sliders::-webkit-scrollbar-thumb {
  background: var(--film-rule);
  border-radius: var(--radius-control);
}
.edit .sliders:hover::-webkit-scrollbar-thumb { background: var(--film-ink-3); }

/* Side by side, the column is exactly as tall as the picture beside it --
   the well and the tray under it together -- and the list fills it. It used
   to stop at the well's height, a tray's height short of the column next to
   it; that was also a different arrangement from the big view's, which
   fills, so coming back from the big view the list jumped at the end of the
   move. One arrangement now, at every size.

   `contain: size` is what lets it fill without deciding the height: the
   column counts for nothing when the row is measured, so the row is the
   picture's, and then the column is stretched to it. Without it the list's
   full length -- every group, open or not -- would be the row's height. */
@media (min-width: 761px) {
  .edit { align-items: stretch; }
  .edit__controls {
    display: flex;
    flex-direction: column;
    contain: size;
  }
  /* After the list's own height above, which it replaces here. */
  .edit .sliders { flex: 1; min-height: 0; max-height: none; }
}

@media (max-width: 760px) {
  .edit { grid-template-columns: minmax(0, 1fr); }
  .edit .sliders { max-height: 34vh; }

  /* The picture as big as a phone allows. Stacked, it is held by the width
     of the screen, so the margins round it are what it is short of: the
     page's and the face's both come in to the smallest the rest of the face
     can live with. And the well is the photograph's own shape rather than a
     fixed band, so none of that width is spent on empty well above and
     below it -- up to most of the screen's height, for one taller than it
     is wide. `room.js` writes the shape. */
  main:has(.stage[data-state="photo-edit"]) { padding-inline: var(--s2); }
  [data-face="photo-edit"].face { padding-inline: var(--s3); }
  /* Scoped to the face so it outranks the well's own height further down. */
  [data-face="photo-edit"] .well {
    height: auto;
    aspect-ratio: var(--picture-aspect, 4 / 3);
    min-height: 160px;
    max-height: 72vh;
    max-height: 72svh;
  }
}

/* --- the well the picture sits in ----------------------------------------- */

/* A recess cut into the console, the same idea as the converter's preview
   island: the picture is set into the surface rather than floating on it. */
.well {
  position: relative;
  width: 100%;
  /* Tall enough to work in, never so tall the controls leave the screen --
     and, side by side, whatever height brings the page to the window's
     (--well-fit, worked out in room.js: fitWell). */
  height: var(--well-fit, clamp(220px, 46vh, 560px));
  margin: 0;
  border-radius: calc(var(--radius-shell-open) - 12px);
  overflow: hidden;
  /* Cut to its own rounded corner, so the picture settling into focus --
     scaled a little past the edge as it starts -- is never drawn outside it. */
  clip-path: inset(0 round calc(var(--radius-shell-open) - 12px));
  background: var(--film-well);
  box-shadow: inset 0 1px 0 var(--film-raise), inset 0 0 0 var(--hairline) var(--film-rule);
}

.well__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  touch-action: none;
  cursor: grab;
}
.well__canvas[data-dragging="true"] { cursor: grabbing; }

/* A raw's first look while it develops (room.js: showFirstLook): over the
   picture on its way out, under the band of light that says the room is
   still working. It comes into focus as a picture does when the well opens
   (`well-resolve`, with the light passing over it once: `data-glance`), and
   is faded from over the developed picture once that is drawn, which does
   not come into focus again. Never touched: the picture under it is the one
   the controls belong to. */
.well__look {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  background: var(--film-well);
  opacity: 0;
  transition: opacity 400ms var(--ease-soft);
}
/* Not held at its end (`backwards`, not `both`): an animation holding the
   opacity would stop the fade from over the developed picture. */
.well__look[data-shown] {
  opacity: 1;
  animation: well-resolve 900ms cubic-bezier(.3, 0, .2, 1) backwards;
}
@media (prefers-reduced-motion: reduce) {
  .well__look { transition: none; }
  .well__look[data-shown] { animation: none; }
}

/* --- the whole window ------------------------------------------------------

   Asked for at the island's own top-right corner, which is the one place on
   it that nothing else uses. Not a button drawn on the island but the island's
   edge, lit, in this room's two colours -- the canyon's gold and the blue the
   light splits into, sampled from the film -- and nothing else: no violet,
   no rose, because there is none in the stone.

   The light knows where the pointer is (`room.js` writes it): far off, a
   small glint in the corner; closer, it grows, slides along the edge towards
   the pointer, and burns brighter at the spot nearest it. The hit area is
   the corner and does not move -- only the light does. The well is left to
   the photograph and the tray to the things done to it.

   The ring is the shell's refraction ring again, a conic gradient masked to
   its border, with a round mask on top that is the light itself: its centre
   is where along the edge the light is, its radius how much of it there is.
   A blurred copy underneath is the glow, and only the near half of it. */
.shell__corner {
  display: none;
  position: absolute;
  top: 0;
  right: 0;
  z-index: 5;
  width: 72px;
  height: 72px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  border-top-right-radius: var(--radius-shell-open);
}
.shell__corner:focus-visible { outline: none; }

.shell__glow {
  --glow-r: 28px;
  --glow-x: 0px;
  --glow-y: 0px;
  --glow-n: 0;
  display: none;
  position: absolute;
  top: 0;
  right: 0;
  z-index: 4;
  width: 160px;
  height: 160px;
  pointer-events: none;
  border-top-right-radius: var(--radius-shell-open);
  -webkit-mask: radial-gradient(circle var(--glow-r) at calc(100% - var(--glow-x)) var(--glow-y),
    #000 20%, rgb(0 0 0 / 0.5) 55%, transparent 100%);
  mask: radial-gradient(circle var(--glow-r) at calc(100% - var(--glow-x)) var(--glow-y),
    #000 20%, rgb(0 0 0 / 0.5) 55%, transparent 100%);
  opacity: calc(0.5 + var(--glow-n) * 0.4);
}
.stage[data-state="photo-edit"] .shell__corner,
.stage[data-state="photo-edit"] .shell__glow { display: block; }
/* Lit as the island is: after the shell has begun to open, over as long as
   its refraction ring takes. `backwards`, so once it has finished the
   pointer is what decides how bright it is. */
.stage[data-state="photo-edit"] .shell__glow {
  animation: corner-in 1250ms var(--ease-soft) backwards,
             glow-drift 6s ease-in-out infinite alternate;
}
/* The light is never quite still, as light through moving air is not: the
   colours drift a few degrees round the corner and back. The same drift,
   three and a half times as far, when the pointer is right by it -- which is
   the `--glow-n` in the gradient's angle, not a second animation. */
@property --glow-turn {
  syntax: '<angle>';
  inherits: true;
  initial-value: 0deg;
}
@keyframes glow-drift {
  from { --glow-turn: -5deg; }
  to   { --glow-turn: 5deg; }
}
@keyframes corner-in {
  0%, 30% { opacity: 0; }
  100%    { opacity: 0.5; }
}

.shell__glow::before,
.shell__glow::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  /* Thicker where the pointer is close: a line far off, a band near. */
  padding: calc(1.5px + var(--glow-n) * 1px);
  /* Round the middle of the box, so the colour changes along the edge and
     not only round the curve: gold along the top, brightening towards the
     corner, the split at the corner itself, and blue down the side. The top
     edge the light can reach is 0 to 45 degrees from the middle of this
     box, the side 45 to 90, which is where these stops are put. */
  background: conic-gradient(from calc(300deg + var(--glow-turn) * (1 + var(--glow-n) * 2.5)) at 50% 50%,
    #B8832A 0deg,
    #D8A23C 60deg,
    #E9BE6A 86deg,
    #8FD6E2 106deg,
    #6FB6CE 124deg,
    #316E7F 190deg,
    #B8832A 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
}
/* The glow, which is the "stronger where the pointer is": nothing far off,
   a bloom round the nearest stretch of edge when the pointer is right by it. */
.shell__glow::after {
  filter: blur(5px);
  padding: calc(2px + var(--glow-n) * 3px);
  opacity: calc(var(--glow-n) * var(--glow-n) * 0.7);
}
.shell__corner:hover + .shell__glow,
.shell__corner:focus-visible + .shell__glow { opacity: 1; }
.shell__corner:focus-visible + .shell__glow::after { opacity: 1; }

/* A finger never comes near without pressing, so on a touch screen the
   light rests as it would with a pointer half-way to it: big enough to be
   found, and still only the corner. `room.js` leaves these alone for touch,
   so nothing written inline overrides them. */
@media (hover: none) {
  .shell__glow { --glow-r: 58px; --glow-n: 0.6; }
}

@media (prefers-reduced-motion: reduce) {
  .stage[data-state="photo-edit"] .shell__glow { animation: none; }
}

/* The shell, as big as the window: laid over the page rather than grown
   inside it, so the top bar and the strip of other photographs are covered
   rather than pushed. Its corners, lean and edge go, because a surface the
   size of the window has no edge to show -- the edge as a colour and not as
   a width, so that the inside is the same size as it is at rest, which is
   what lets the move back land without anything inside it shifting. Everything in it keeps its place --
   the head, the panel, the foot -- and the well takes whatever height is left,
   which is the point. The canvas follows on its own: the well is watched. */
.stage[data-state="photo-edit"] .shell[data-big] {
  position: fixed;
  inset: 0;
  z-index: 100;
  width: auto;
  height: auto;
  border-color: transparent;
  border-radius: 0;
  background: var(--film-shell-solid);
  transform: none;
}
/* The picture while the shell moves: a still of it, with the framed picture
   at the fitted size -- 92% of the well's width or of its height, whichever
   is the tighter, which is what `View.fitScale` does -- centred, and whatever
   was drawn round the frame round it.
   Only during the move; the canvas and everything laid over it are hidden
   meanwhile, and come back when the move lands. */
.well__still { display: none; }
.shell.is-morphing .well { container-type: size; }
.shell.is-morphing .well__still {
  /* The fitted frame's width on this frame of the move. */
  --still-frame: min(92cqw, 92cqh * var(--still-shape, 1.5));
  display: block;
  position: absolute;
  left: 50%;
  top: 50%;
  /* The whole well as it was taken, grown or shrunk by as much as the frame
     in the middle of it -- so the frame is where the fit puts it and what
     was drawn round it stays round it. */
  width: calc(var(--still-frame) * var(--still-wide, 1));
  height: calc(var(--still-frame) / var(--still-shape, 1.5) * var(--still-tall, 1));
  transform: translate(-50%, -50%);
  pointer-events: none;
}
/* All but the words saying what the well is waiting on, which are not a
   picture and cannot be stretched: arriving from the bar with a raw, they are
   what the well says from the moment it appears. */
.shell.is-morphing .well > :not(.well__still):not(.well__wait) { visibility: hidden; }
/* The picture's column, cut at its own edge while the shell moves. A tray
   button going out where it stood can be overtaken by the adjustments beside
   it as the column narrows; cut here, it goes behind the column's edge
   instead of over the sliders, whatever the size of the window. */
.shell.is-morphing .edit__picture { overflow: clip; }

/* Holding the shell's place on the deck while it is laid over the window,
   so the page does not close up behind it -- `room.js` sizes it. */
.shell-stand { flex: 0 0 auto; }
/* While `room.js` moves the shell in or out, its own transitions are off: a
   transition outranks the move and would draw its corner over it. All but
   the lean, which the move leaves alone and which this eases out as the big
   view starts and back in as the page returns. */
.shell.is-sizing { transition: transform 420ms var(--ease-soft) !important; }
.shell[data-big] .shell__faces { height: 100%; }
.shell[data-big] [data-face="photo-edit"].is-active {
  height: 100%;
  display: flex;
  flex-direction: column;
}
.shell[data-big] .edit {
  flex: 1;
  min-height: 0;
  grid-template-rows: minmax(0, 1fr);
  align-items: stretch;
}
.shell[data-big] .edit__picture {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.shell[data-big] .well { flex: 1; height: auto; min-height: 0; }

/* Not on a phone. Stacked, the picture already has the whole width of the
   screen, and a photograph wider than it is tall is held by that width and
   nothing else -- filling the window gave it no more room, only pushed the
   panel further down. A tablet has the columns side by side and gains from
   it, so there the corner stays. */
@media (max-width: 760px) {
  .stage[data-state="photo-edit"] .shell__corner,
  .stage[data-state="photo-edit"] .shell__glow { display: none; }
}

/* --- the tray under the picture -------------------------------------------

   Where the view controls and the crop ratios live. They used to float inside
   the well, one pinned to its top and one to its bottom, which was wrong twice
   over: they stood on the photograph -- the one thing in this room that is
   meant to be looked at -- and they ran into the crop box's own grips whenever
   it was dragged near an edge.

   Under it, they cannot cover anything and cannot collide. The row wraps, so
   six ratio buttons on a narrow screen become two lines rather than a
   horizontal scroll or an overlap. */
.tray {
  /* What the buttons in it are measured against when the big view carries
     them to their new places (`room.js`). */
  position: relative;
  display: flex;
  /* Lines packed to the top. The default spreads them over whatever height
     the tray has, and while the big view moves its height between two
     sizes it is often taller than its lines -- which pushed every button
     down by the difference. */
  align-content: flex-start;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  margin-top: var(--s2);
  min-height: 30px;
}
.tray__set {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
}
.tray__label {
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--film-ink-3);
}

/* --- the crop --------------------------------------------------------------

   Every selector here is inside this room's own face, and that is not tidiness.

   The converter has a crop too -- for trimming the frame of a video -- and it
   got to `.crop`, `.crop__box`, `.crop__grip` and `.crop__edge` first, in
   styles.css. This stylesheet loads second, so an unscoped copy of those four
   names was quietly restyling the converter's crop from inside the photo
   room's file: exactly the mistake this file's own header warns about, made in
   the file that warns about it.

   The room's faces are namespaced; its components can be too, by living inside
   them. Outside `photo-edit` these rules do not exist. */

[data-face="photo-edit"] .crop { position: absolute; inset: 0; z-index: 3; }

/* Everything outside the crop, dimmed. Drawn as a ring of shadow around the
   box rather than as four panels, so there is nothing to keep in step. */
[data-face="photo-edit"] .crop__box {
  position: absolute;
  box-shadow: 0 0 0 9999px rgb(var(--shade-rgb) / 0.62);
  outline: 1px solid rgba(255, 255, 255, 0.9);
  cursor: move;
}

/* Thirds, which is the only guide worth having by default. */
[data-face="photo-edit"] .crop__box::before,
[data-face="photo-edit"] .crop__box::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-repeat: no-repeat;
}
[data-face="photo-edit"] .crop__box::before {
  background-image:
    linear-gradient(90deg, rgba(255,255,255,.34) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,.34) 1px, transparent 1px);
  background-size: 1px 100%, 1px 100%;
  background-position: 33.333% 0, 66.666% 0;
}
[data-face="photo-edit"] .crop__box::after {
  background-image:
    linear-gradient(180deg, rgba(255,255,255,.34) 1px, transparent 1px),
    linear-gradient(180deg, rgba(255,255,255,.34) 1px, transparent 1px);
  background-size: 100% 1px, 100% 1px;
  background-position: 0 33.333%, 0 66.666%;
}

/* Corners are drawn; edges are invisible strips that widen what can be
   grabbed, because a one-pixel line is not a target. */
[data-face="photo-edit"] .crop__grip {
  position: absolute;
  width: 18px;
  height: 18px;
  border: 2px solid #fff;
  background: transparent;
}
[data-face="photo-edit"] .crop__grip[data-edge="-1,-1"] { left: -2px;  top: -2px;    border-right: 0; border-bottom: 0; cursor: nwse-resize; }
[data-face="photo-edit"] .crop__grip[data-edge="1,-1"] { right: -2px; top: -2px;    border-left: 0;  border-bottom: 0; cursor: nesw-resize; }
[data-face="photo-edit"] .crop__grip[data-edge="1,1"] { right: -2px; bottom: -2px; border-left: 0;  border-top: 0;    cursor: nwse-resize; }
[data-face="photo-edit"] .crop__grip[data-edge="-1,1"] { left: -2px;  bottom: -2px; border-right: 0; border-top: 0;    cursor: nesw-resize; }

[data-face="photo-edit"] .crop__edge { position: absolute; }
[data-face="photo-edit"] .crop__edge[data-edge="0,-1"] { left: 18px; right: 18px; top: -6px;    height: 12px; cursor: ns-resize; }
[data-face="photo-edit"] .crop__edge[data-edge="0,1"] { left: 18px; right: 18px; bottom: -6px; height: 12px; cursor: ns-resize; }
[data-face="photo-edit"] .crop__edge[data-edge="-1,0"] { top: 18px; bottom: 18px; left: -6px;   width: 12px;  cursor: ew-resize; }
[data-face="photo-edit"] .crop__edge[data-edge="1,0"] { top: 18px; bottom: 18px; right: -6px;  width: 12px;  cursor: ew-resize; }


/* --- the adjustments ------------------------------------------------------- */

.adj { padding: var(--s2) 0; }

.adj__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  margin-bottom: var(--s1);
}

.adj__name { font-size: var(--t-sm); color: var(--film-ink-2); }

.adj__value {
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  color: var(--film-ink-3);
  font-variant-numeric: tabular-nums;
}
.adj[data-touched="true"] .adj__name { color: var(--film-ink); font-weight: 600; }
.adj[data-touched="true"] .adj__value { color: var(--film-ink); }

/* The track carries the value: a bar grows from wherever the adjustment does
   nothing out to where it stands, so a glance says which way and how far
   without reading the number. `--from` and `--to` are set as the slider moves.

   Colour sliders are the exception. Their track is the colour they mean, which
   says the direction faster than a bar could. Muted on purpose -- these sit
   beside a photograph, and a bright rainbow next to it would fight the only
   thing in the room that is meant to have colour in it. */
.adj__slider {
  --rule: var(--film-rule);
  --fill: rgba(238, 241, 240, 0.72);
  --track: linear-gradient(90deg,
    var(--rule) 0, var(--rule) var(--from, 50%),
    var(--fill) var(--from, 50%), var(--fill) var(--to, 50%),
    var(--rule) var(--to, 50%), var(--rule) 100%);

  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 20px;
  background: transparent;
  cursor: ew-resize;
  margin: 0;
}

/* Read off the reference developer's newer edition, and then muted to this
   room's rail.

   Twice these were guessed and twice they were wrong, so the third time they
   were looked at: the reference developer's Temp runs a desaturated steel blue to a
   desaturated olive gold, its Tint a soft green to a soft orchid, and both
   pass through the panel's own track grey in the middle rather than through a
   colour. Vibrance and Saturation are almost the same track as each other --
   grey at the left, drifting slate, then green, olive, amber and a dull red --
   which is nothing like the rainbow that was here.

   The middle is var(--film-rule) rather than a grey of its own, which is the
   only adjustment to the room: the part of the track that means "nothing is
   happening" is the same colour as every other track's whole length. */
.adj[data-hue="temperature"] .adj__slider,
.adj[data-hue="kelvin"] .adj__slider {
  --track: linear-gradient(90deg, #3F5E8C 0%, var(--film-rule) 50%, #A5A34E 100%);
}
.adj[data-hue="tint"] .adj__slider {
  --track: linear-gradient(90deg, #4E7F4E 0%, var(--film-rule) 50%, #A45BA4 100%);
}
.adj[data-hue="saturation"] .adj__slider {
  --track: linear-gradient(90deg,
    var(--film-rule) 0%, #5E6B7E 38%, #6E8A6B 56%,
    #9A9A5E 72%, #B08A52 86%, #B85F45 100%);
}
/* Vibrance's is Saturation's, a shade quieter, which is what the slider is. */
.adj[data-hue="vibrance"] .adj__slider {
  --track: linear-gradient(90deg,
    var(--film-rule) 0%, #636D7A 40%, #6F8570 58%,
    #91915F 73%, #A18457 87%, #A96450 100%);
}

.adj__slider::-webkit-slider-runnable-track {
  height: 3px;
  border-radius: 3px;
  background: var(--track);
  transition: filter var(--dur-state) var(--ease);
}
.adj__slider::-moz-range-track {
  height: 3px;
  border-radius: 3px;
  background: var(--track);
}
.adj__slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 13px;
  height: 13px;
  margin-top: -5px;
  border-radius: 50%;
  background: var(--knob-face);
  border: none;
  transition: background var(--dur-state) var(--ease), transform var(--dur-state) var(--ease);
}
.adj__slider::-moz-range-thumb {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--knob-face);
  border: none;
}
.adj__slider:hover::-webkit-slider-thumb { background: var(--knob-face-hi); transform: scale(1.12); }
/* What a slider does while it is being moved is in styles.css with the other
   one, because it is the framework's reaction and not this room's -- the room
   supplies only the character, in `themes.css`. */
.adj__slider:focus-visible { outline: 2px solid var(--film-ink); outline-offset: 4px; border-radius: 4px; }

@media (prefers-reduced-motion: reduce) {
  .adj__slider::-webkit-slider-thumb { transition: none; }
}

/* --- the adjustment groups, which open and close ---------------------------

   DESIGN.md §1.6. Nine sliders in one column was a wall: you had to scroll it
   to learn that a vignette was even on offer. Four headings you can read at a
   glance, any one of which opens, says more in less space.

   Closed is not hidden. A shut group still carries its name, how many
   adjustments are inside it, and a mark if any of them has been used -- so
   nothing about the picture is ever being changed by something you cannot
   see. */

.group + .group { border-top: var(--hairline) solid var(--film-rule); }

.group__head {
  display: flex;
  align-items: center;
  gap: var(--s2);
  width: 100%;
  padding: var(--s3) 0;
  background: none;
  border: 0;
  cursor: pointer;
  color: inherit;
  text-align: left;
}
.group__head:focus-visible { outline: 2px solid var(--film-ink); outline-offset: 2px; }

.group__name {
  flex: 1 1 auto;
  font-family: var(--font-ui);
  font-size: var(--t-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--film-ink-2);
  margin: 0;
}
.group__head:hover .group__name { color: var(--film-ink); }

/* How many adjustments are in there, so a shut group still says what it is
   worth opening for. */
.group__count {
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  color: var(--film-ink-3);
}

/* Something in here has been changed. The one place in this room the accent is
   used, because it is the one thing worth finding when it is out of sight. */
.group__mark {
  width: 5px; height: 5px;
  border-radius: var(--radius-control);
  background: var(--signal);
  opacity: 0;
  transition: opacity var(--dur-state) var(--ease);
}
/* Its own head, and only its own.

   A descendant selector here reached into the panels that live *inside* a
   group -- the Curve inside Light, the Mix, Point and Grade inside Colour --
   because those are `.group` elements in the body of the group above them, so
   their marks are descendants of it. Moving Exposure lit the dot on Light,
   correctly, and on Curve as well, which says a tone curve has been drawn
   when none has. The same reading was what sent somebody looking for a
   highlights setting that was not there.

   The child combinator says what was always meant: a group answers for
   itself. The dot inside the Curve panel is set by the Curve panel's own
   `data-touched` and by nothing above it. */
.group[data-touched="true"] > .group__head > .group__mark { opacity: 1; }

.group__chev {
  width: 16px; height: 16px;
  fill: none;
  stroke: var(--film-ink-3);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform var(--dur-state) var(--ease);
}
/* Scoped the same way and for the same reason: open Light and every nested
   panel's chevron turned over too, so a shut Curve panel drew itself as
   though it were open. */
.group[data-open="true"] > .group__head > .group__chev { transform: rotate(180deg); }

.group__body { padding-bottom: var(--s2); }

@media (prefers-reduced-motion: reduce) {
  .group__chev, .group__mark { transition: none; }
}

/* --- the slider that lives in a tray ---------------------------------------
   The adjustments have their own full-width rows; a quality slider sits inline
   beside the format it belongs to, so it is short and has its number next to
   it rather than above. */
.tray__range {
  -webkit-appearance: none;
  appearance: none;
  width: 120px;
  height: 20px;
  background: none;
  cursor: ew-resize;
  margin: 0;
}
.tray__range::-webkit-slider-runnable-track {
  height: 3px; border-radius: 3px; background: var(--film-raise-hi);
}
.tray__range::-moz-range-track {
  height: 3px; border-radius: 3px; background: var(--film-raise-hi);
}
.tray__range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 13px; height: 13px; margin-top: -5px;
  border-radius: 50%; border: none; background: var(--knob-face);
}
.tray__range::-moz-range-thumb {
  width: 13px; height: 13px; border-radius: 50%; border: none; background: var(--knob-face);
}
.tray__range:focus-visible { outline: 2px solid var(--film-ink); outline-offset: 4px; border-radius: 4px; }

/* --- the foot, which has two faces -----------------------------------------

   What you can do, and what you are doing. Exporting is a decision with three
   or four parts to it, and leaving them all on screen the whole time put a row
   of controls between the picture and the buttons that nobody was reading.
   Pressing Export swaps the row and the shell grows around it, so it is the
   same movement the rest of the site makes rather than a panel appearing.

   DESIGN.md §1.6 permits this and is worth checking against: collapsing is
   allowed, hiding the *existence* of the thing is not. The button says Export
   and says which format it will use, so what is behind it is never a guess --
   and it opens on a press, never on a hover.

   Not `panel__go`, which belongs to the building and is a row of buttons at the
   foot of a face. This is a row of buttons that changes what it is. */
.foot { margin-top: var(--s5); }

.foot__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
}

/* The format the button will use, said on the button itself. */
.foot__which {
  margin-left: var(--s3);
  padding-left: var(--s3);
  border-left: var(--hairline) solid rgb(0 0 0 / 0.2);
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  opacity: 0.7;
}

/* Open, the row is the export: a way back, the choices, what will come out,
   and the one button worth pressing at the end of it -- which sits at the far
   end, where the eye finishes. */
.foot__row--export { gap: var(--s4); }
.foot__row--export > .action { margin-left: auto; }

/* "Saved" on the Export button for a moment after a save (room.js:
   sayDoneOnExport): the button's own words go clear and the word is laid
   over them, so the button keeps its place and its width exactly. */
#photo-export-btn { position: relative; }
#photo-export-btn[data-done],
#photo-export-btn[data-done] .foot__which { color: transparent; border-left-color: transparent; }
#photo-export-btn::after {
  content: attr(data-done);
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--knob-ink);
  opacity: 0;
  transition: opacity 200ms var(--ease);
  pointer-events: none;
}
#photo-export-btn[data-done]::after { opacity: 1; }

/* Narrow: the choices stay on one line and the button takes its own, rather
   than four things each claiming a line of their own. */
@media (max-width: 760px) {
  .foot__row--export { gap: var(--s3); }
  .foot__row--export > .action { margin-left: 0; width: 100%; }
}

/* --- the tone curve --------------------------------------------------------

   The one control in this room that is a drawing rather than a number. Square,
   because both axes are the same thing -- the value that went in against the
   value that comes out -- and a curve drawn on a rectangle lies about its own
   slope. */
.curve {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  margin: var(--s3) 0 var(--s2);
  border-radius: calc(var(--radius-shell-open) - 22px);
  background: var(--film-well);
  box-shadow: inset 0 0 0 var(--hairline) var(--film-rule);
  cursor: crosshair;
  touch-action: none;
}
.curve:focus-visible { outline: 2px solid var(--film-ink); outline-offset: 2px; }

.curve__hint {
  margin: 0 0 var(--s2);
  font-size: var(--t-xs);
  color: var(--film-ink-3);
  line-height: var(--lh-prose);
}

/* A word inside a hint that does something. Not a pill: it is a sentence with
   an action at the end of it, and a pill in the middle of a sentence reads as
   a label rather than as something to press. */
.curve__hint .linkish {
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  color: var(--film-ink-2);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.curve__hint .linkish:hover { color: var(--film-ink); }

/* --- the eight bands of hue ------------------------------------------------
   Each slider carries the colour it is about, so "Aqua" is not the only way to
   know which one you are on. The track is that colour at low strength rather
   than at full, because eight saturated rails beside a photograph is a
   fairground. */
.bands { margin-top: var(--s2); }
.adj__slider--band { --track: color-mix(in srgb, var(--band) 34%, transparent); }
.adj__slider--band::-webkit-slider-runnable-track { background: var(--track); }
.adj__slider--band::-moz-range-track { background: var(--track); }
.bands .adj[data-touched="true"] .adj__name { color: var(--band); }

/* A look is chosen rather than dialled, so the pills wrap into a block instead
   of running off the side of a 280px column. */
.looks { flex-wrap: wrap; margin-top: var(--s2); }

/* --- where a mask is ------------------------------------------------------

   Drawn over the picture as elements rather than into it, so a mask can be
   dragged and focused like anything else on the page -- the same decision the
   crop box was built on, and for the same reason. */
[data-face="photo-edit"] .masks { position: absolute; inset: 0; z-index: 2; pointer-events: none; }

/* The chosen layer, outlined.

   A dashed edge rather than a solid one, and no fill at all: whatever the
   layer is has to stay readable through its own outline, and a caption behind
   a translucent panel is a caption you cannot judge. */
[data-face="photo-edit"] .layerbox {
  position: absolute;
  z-index: 3;
  cursor: grab;
  touch-action: none;
  outline: 1px dashed rgb(255 255 255 / 0.75);
  outline-offset: 0;
  box-shadow: 0 0 0 1px rgb(0 0 0 / 0.45);
}
[data-face="photo-edit"] .layerbox[data-dragging] { cursor: grabbing; }

/* One grip, at the bottom right, and one only.

   Four would be tidier and would also cover a caption's corners with things
   that are not the caption. A layer is moved far more often than it is
   resized, so the box itself is the big target and the grip is the small
   one. */
[data-face="photo-edit"] .layerbox__grip {
  position: absolute;
  right: -6px;
  bottom: -6px;
  width: 12px;
  height: 12px;
  border-radius: 3px;
  background: var(--film-ink);
  box-shadow: 0 0 0 1px rgb(0 0 0 / 0.5);
  cursor: nwse-resize;
}

/* A colour, and a row of choices, inside the tight rhythm. Both borrow the
   grid from `.adj__tight .adj` and simply put something else in the middle
   column, so a panel of sliders and a panel of choices line up. */
/* The doubled class is not a tic: these rows live inside `.adj__tight`, whose
   own rule is three columns -- a name, a track and a reading -- and at equal
   specificity the one further down the file wins. These are further up, so
   they lost, and a two-column row was quietly being given three: the choices
   were squeezed into the track's share and the reading's column sat empty
   beside them. */
[data-face="photo-edit"] .adj.adj--colour,
[data-face="photo-edit"] .adj.adj--pick {
  display: grid;
  grid-template-columns: minmax(0, 5em) minmax(0, 1fr);
  align-items: center;
  gap: var(--s2);
  min-height: 21px;
}

/* The colour field (js/ui/colour.js): a full-width button in the panel, as
   tall as the sliders' rows, the hex beside its swatch. */
[data-face="photo-edit"] .uc-colour.adj__colour {
  width: 100%;
  min-height: 21px;
}

/* The row of choices wraps rather than scrolls: five faces in a column this
   narrow is two lines, and two lines you can read beat one line you cannot. */
[data-face="photo-edit"] .adj--pick .tray__set {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
}

/* Except where there are few enough to sit on one line, where they should.

   Three choices stacked into two rows reads as two settings rather than one,
   and the eye has to work out which of the five things above and below the
   break belong together. On one line there is nothing to work out. The pills
   share the width equally and give up their padding before they give up the
   line, so the longest word decides the size of all three rather than the row
   deciding to wrap. */
/* And the name gives up more of its column here than elsewhere, because three
   words across the rest of it is what decides whether "As shot" and "Rebuild"
   are readable or are "As sh..." and "Rebui...". */
/* And a third arrangement, for a setting whose name will not fit beside it.

   The label column is five characters wide, which is right for Blend, Weight
   and Align and hopeless for "Lens correction". Squeezing it wraps the label
   into two lines beside a single row of buttons, which looks like a mistake;
   abbreviating it to fit means calling it something it is not. So the name
   goes on its own line and the switches sit under it, full width, wrapping as
   they need to -- which also means one more kind of correction can be added
   without the row having to be redesigned around it. */
[data-face="photo-edit"] .adj.adj--stack {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s1);
  align-items: start;
}
[data-face="photo-edit"] .adj--stack .tray__set { flex-wrap: wrap; gap: 3px; }
[data-face="photo-edit"] .adj--stack .pill { flex: 0 1 auto; }

[data-face="photo-edit"] .adj--pick .pill {
  min-height: 19px;
  padding: 0 var(--s2);
  font-size: 10px;
}

/* A list of choices: the building's drop-down (<uc-select>, js/ui/dropdown.js)
   in this room's clothes -- the panel's raise, rule and ink, a control as
   tall as the sliders' rows, the type of the labels beside it, and the list
   opening in the canyon's shell rather than the operating system's grey.
   The browser's own select is gone: its open list could not be styled. */
[data-face="photo-edit"] .adj__choose {
  --uc-dd-pad: 0 var(--s1) 0 var(--s2);
  --uc-dd-chev: 12px;
  --uc-dd-radius: calc(var(--radius-shell-open) - 12px);
  width: 100%;
  min-height: 21px;
  font-size: var(--t-xs);
  border-radius: var(--radius-control);
  background-color: var(--film-raise);
  transition: background-color var(--dur-state) var(--ease),
              border-color var(--dur-state) var(--ease);
}
[data-face="photo-edit"] .adj__choose:hover,
[data-face="photo-edit"] .adj__choose[data-open] {
  background-color: var(--film-raise-hi);
  border-color: var(--film-ink-3);
}
[data-face="photo-edit"] .adj__choose:focus-visible {
  outline: 2px solid var(--film-ink);
  outline-offset: 1px;
}
/* The list's rows at the panel's scale, not the dialog-sized default. */
[data-face="photo-edit"] .adj__choose::part(list) {
  font-size: var(--t-xs);
}

/* The words themselves. A textarea rather than a single line, because a
   caption on a photograph is often two. */
[data-face="photo-edit"] .layer__words {
  width: 100%;
  margin-bottom: var(--s2);
  /* The words sit in the middle of the bubble.

     This is a capsule -- `--radius-control` is 999px -- and the text started
     in its top left corner, which is the one part of a capsule that is not
     there: the curve cuts across the first characters and the line runs out
     towards a side that is closing in on it. It read as text that had been
     dropped in rather than text that belongs in the box.

     Centred both ways, and the side padding is wide enough to clear the
     curve at its narrowest rather than merely wide enough to look even.
     `align-content` does the vertical half where it is supported; where it is
     not, the even padding leaves a single line only a few pixels high. */
  padding: var(--s3) var(--s5);
  text-align: center;
  align-content: center;
  font: inherit;
  font-size: var(--t-xs);
  line-height: 1.4;
  /* No native grip. The browser's resize corner is three grey diagonals drawn
     by the operating system, and it is the only thing on this panel that is.
     Two lines is what a caption is; a third wraps. */
  resize: none;
  border: var(--hairline) solid var(--film-rule);
  border-radius: var(--radius-control);
  background: var(--film-raise);
  color: var(--film-ink);
}
[data-face="photo-edit"] .layer__words:focus-visible {
  outline: 2px solid var(--film-ink);
  outline-offset: 1px;
}

/* Where the brush is painted.

   Above the masks, and taking every press while it is open: healing is a
   gesture on the whole photograph, and a mask's handle underneath would take
   presses meant for the picture. The system cursor is taken away and the ring
   put in its place, because the useful thing to know about a brush is how big
   it is, and an arrow does not say. */
[data-face="photo-edit"] .heal {
  position: absolute;
  inset: 0;
  z-index: 3;
  cursor: none;
  touch-action: none;
}

/* Two rings, light over dark, so it reads on a white sky and on a black
   doorway without either being chosen in advance. */
[data-face="photo-edit"] .heal__brush {
  position: absolute;
  pointer-events: none;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  border: 1px solid rgb(255 255 255 / 0.85);
  box-shadow: 0 0 0 1px rgb(0 0 0 / 0.55), inset 0 0 0 1px rgb(0 0 0 / 0.35);
}

[data-face="photo-edit"] .mask {
  position: absolute;
  pointer-events: auto;
  cursor: grab;
  transform: translate(-50%, -50%);
}
[data-face="photo-edit"] .mask[data-dragging] { cursor: grabbing; }

/* A radial mask: a ring at the edge of its reach, and a dot at its middle to
   take hold of. The ring is dashed so it never reads as part of the
   photograph. */
/* One dashed line and nothing else. It used to carry a dark rim on both
   sides, on the theory that a white line needs backing to stay visible over a
   pale photograph -- but the line is dashed, so the photograph shows through
   it anyway, and all the rim did was draw a second, heavier oval around the
   first. A dashed white line over a scrim reads perfectly well on its own. */
[data-face="photo-edit"] .mask--radial {
  border: 1.5px dashed rgba(255, 255, 255, 0.8);
  border-radius: 50%;
}

/* A graduated mask: the line it fades across. */
[data-face="photo-edit"] .mask--linear {
  height: 2px;
  background: rgba(255, 255, 255, 0.72);
  box-shadow: 0 0 0 1px rgb(var(--shade-rgb) / 0.5);
}

/* --- which way a graduated mask works ----------------------------------------

   A line says where the boundary is and nothing about which half of the
   photograph is being changed, which is half of what a graduated mask is for.
   Two things say it: a band on the affected side, as deep as the fade actually
   carries, and an arrow pointing into it.

   Both are children of the line, so they turn with it -- the arrow drawn
   pointing "up" in the element's own frame comes out pointing into the
   affected side at every angle, which is exactly the relationship the shader
   has between the angle and its normal. */
[data-face="photo-edit"] .mask__fade {
  position: absolute;
  left: 0;
  right: 0;
  height: var(--reach, 40px);
  pointer-events: none;
}
[data-face="photo-edit"] .mask[data-side="over"] .mask__fade {
  bottom: 100%;
  background: linear-gradient(to top,
    rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0));
}
[data-face="photo-edit"] .mask[data-side="under"] .mask__fade {
  top: 100%;
  background: linear-gradient(to bottom,
    rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0));
}

/* The arrow: a short stem with a head on it, beside the middle handle rather
   than on top of it, so the thing you grab and the thing that tells you which
   way it goes are never the same pixel. */
[data-face="photo-edit"] .mask__way {
  position: absolute;
  left: 50%;
  width: 0;
  height: 0;
  margin-left: 26px;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  pointer-events: none;
  filter: drop-shadow(0 0 1px rgb(var(--shade-rgb) / 0.7));
}
[data-face="photo-edit"] .mask[data-side="over"] .mask__way {
  bottom: 9px;
  border-bottom: 7px solid rgba(255, 255, 255, 0.82);
}
[data-face="photo-edit"] .mask[data-side="under"] .mask__way {
  top: 9px;
  border-top: 7px solid rgba(255, 255, 255, 0.82);
}

[data-face="photo-edit"] .mask__grip {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 16px;
  height: 16px;
  margin: -8px 0 0 -8px;
  border-radius: 50%;
  background: var(--knob-face);
  box-shadow: 0 0 0 2px rgb(var(--shade-rgb) / 0.55);
}
[data-face="photo-edit"] .mask[data-chosen="true"] .mask__grip { background: var(--signal); }

/* The chosen mask is on top of the others and drawn at full strength; the rest
   stand back. Where two overlap, the one being worked on has to be the one
   whose handles are reachable -- otherwise the way to get at a mask underneath
   is to move the one on top out of the way first, which is a fine way to lose
   a framing you had already settled. */
[data-face="photo-edit"] .mask[data-chosen="true"] { z-index: 2; }
[data-face="photo-edit"] .mask[data-chosen="false"] { z-index: 1; opacity: 0.55; }
[data-face="photo-edit"] .mask[data-chosen="false"]:hover { opacity: 0.8; }

/* The three handles that shape the oval: one at the side for its width, one
   underneath for its height, one above to turn it. On the edges, where the
   thing each one changes is. Smaller than the middle handle, because moving a
   mask is the commoner action and the two should not read as equals. */
[data-face="photo-edit"] .mask__pull {
  position: absolute;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--knob-face);
  box-shadow: 0 0 0 2px rgb(var(--shade-rgb) / 0.55);
}
[data-face="photo-edit"] .mask__pull--wide { right: 0; top: 50%; margin: -5.5px -5.5px 0 0; cursor: ew-resize; }
[data-face="photo-edit"] .mask__pull--tall { bottom: 0; left: 50%; margin: 0 0 -5.5px -5.5px; cursor: ns-resize; }

/* The turn handle stands off the top of the oval on a short stem, so it is
   never confused with the one that changes the height. */
[data-face="photo-edit"] .mask__pull--spin {
  top: 0;
  left: 50%;
  margin: -24px 0 0 -5.5px;
  cursor: grab;
  background: var(--knob-face);
}
[data-face="photo-edit"] .mask__pull--spin::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 11px;
  width: 1.5px;
  height: 13px;
  margin-left: -0.75px;
  background: rgba(255, 255, 255, 0.55);
}

[data-face="photo-edit"] .mask[data-chosen="true"] .mask__pull { background: var(--signal); }

/* The toggle that takes the outlines off, at the end of the row of ways to
   add a mask. */
.masklist__look { margin-left: auto; }

/* The list of masks in the column. */
.masklist { margin-top: var(--s2); }
.masklist__row { display: flex; align-items: center; gap: var(--s2); margin-bottom: var(--s2); }
/* --- what you can add, against what is already there -------------------------

   Two different kinds of thing were wearing the same clothes in the same row:
   four buttons that *make* a mask, and a list of the masks that exist. Pressed
   one after another they behave completely differently, and nothing said so.

   So the offers are outlined and carry a plus, and they sit above a heading;
   the masks themselves are solid rows on a surface of their own, and the one
   being edited is marked down its leading edge. An offer looks like a gap
   waiting to be filled; a mask looks like an object. */
[data-face="photo-edit"] .pill--offer {
  background: none;
  border-style: dashed;
  border-color: var(--film-rule);
  color: var(--film-ink-3);
  gap: var(--s2);
}
[data-face="photo-edit"] .pill--offer:hover:not(:disabled) {
  background: var(--film-raise);
  border-style: solid;
  color: var(--film-ink);
}
[data-face="photo-edit"] .pill__plus {
  font-size: var(--t-sm);
  line-height: 1;
  opacity: 0.75;
}

.masklist__add { margin-bottom: var(--s2); }

/* Each mask, as an object with a surface under it. */
.masklist__row {
  padding: var(--s1);
  border-radius: var(--radius-control);
  background: rgb(var(--shade-rgb) / 0.35);
  border-left: 2px solid transparent;
}
/* The one the sliders below are about. */
.masklist__row[data-chosen="true"] {
  background: var(--film-raise);
  border-left-color: var(--signal);
}

/* --- the layers, which have an order ----------------------------------------

   Masks do not have one: they are all applied and the order changes nothing,
   so a list of them is a set. Layers are a stack, the order is most of what
   the panel is for, and a list that does not show one is hiding the thing it
   exists to show.

   So the layer list gets a handle, a drop line, and a colour of its own.

   The colour is the cool end of this room's own light. Gold is `--signal` and
   `--signal` means *the thing you are adjusting* -- the pressed pill, the
   filled part of a slider, the chosen mask. Order is not an adjustment; it is
   structure, and it was competing with every warm control on the panel for
   the same meaning. This blue is already in the room: it is the cold end of
   the temperature slider's own gradient, forty lines up this file, which is
   the canyon's light going cool at the edges. Nothing new was invented to
   have it. */
.masklist--ordered { --order: #6E93B8; }
.masklist--ordered .masklist__row[data-chosen="true"] { border-left-color: var(--order); }

/* The handle. Two lines and nothing else: it has to read as somewhere to take
   hold of rather than as a third button in a row that already has two. */
.masklist__grip {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  align-self: stretch;
  padding: 0;
  border: 0;
  background: none;
  border-radius: var(--radius-control);
  cursor: grab;
  color: var(--film-ink-3);
}
.masklist__grip:hover { color: var(--film-ink); }
.masklist__grip:focus-visible { outline: 2px solid var(--order); outline-offset: 1px; }
.masklist__grip:active { cursor: grabbing; }
.masklist__grip span {
  width: 9px;
  height: 9px;
  border-top: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
}

/* The one being carried, and where it would land. The line is drawn on the
   row's own edge rather than as an element between rows, so there is nothing
   to insert, measure or take away again. */
.masklist__row[data-carrying="true"] { opacity: 0.4; }
.masklist__row[data-over="above"] { box-shadow: 0 -2px 0 -0.5px var(--order); }
.masklist__row[data-over="below"] { box-shadow: 0 2px 0 -0.5px var(--order); }

/* The name takes whatever room is going; the two controls beside it take only
   what they need. This was one rule for every pill in the row, which stretched
   the remove button to a third of the panel and left its cross sitting in the
   corner of all that space. */
.masklist__row .pill { flex: 0 0 auto; justify-content: center; }

.masklist__row .masklist__name {
  flex: 1 1 auto;
  min-width: 0;
  justify-content: flex-start;
  overflow: hidden;
  text-overflow: ellipsis;
  display: block;
  line-height: 28px;
  text-align: left;
}

/* Square, and the cross in the middle of it. */
.masklist__row .masklist__drop {
  width: 30px;
  padding: 0;
  font-size: 15px;
}

/* --- the picture arriving ---------------------------------------------------

   The edit face appears the moment the shell begins to morph, and the
   photograph can only be drawn once the morph has finished and the well has
   stopped moving. What the well shows in between is the room working, and
   then the picture comes into focus in it.

   It used to be shut in between -- clipped to a line across its middle -- and
   then opened from that line, the way the converter's preview island opens.
   On a photograph that read wrong twice. Shut, the well was a hole in the
   panel for the length of the morph. Opening, every frame of the first half
   second was the picture as a wide, short, blurred strip, which looks exactly
   like the photograph squashed flat. So the well stays open, as the recess it
   always is, and the light that crosses a working button crosses it while it
   waits; the picture is never clipped to anything but its own frame, and
   never drawn at any shape but its own -- it is only scaled, evenly, as it
   settles. */

/* Hidden while it is waiting to be drawn, not shut away with the well round
   it. The canvas is the only thing that goes. */
.well[data-arriving] .well__canvas { opacity: 0; }

/* The room working, in the well: the same band of light, in the same film
   colours, that crosses a pill while it works (see `work-sweep`), slower and
   softer because it is crossing a much larger surface. Moved with a transform
   so it goes on moving whatever the page is busy with.

   Always running and only seen while the well waits. Stopping it as the
   picture lands would snap the band back off the edge mid-crossing; instead
   it fades out on its way, which is the picture taking over from it. */
.well::before {
  content: "";
  position: absolute;
  z-index: 1;
  top: 0;
  bottom: 0;
  left: -55%;
  width: 55%;
  pointer-events: none;
  background-image: linear-gradient(100deg,
    transparent 0,
    hsl(30 94% 80% / 0.10) 18%,
    hsl(30 94% 80% / 0.28) 34%,
    hsl(20 60% 74% / 0.30) 46%,
    hsl(180 45% 66% / 0.26) 62%,
    hsl(210 55% 64% / 0.15) 80%,
    transparent 100%);
  filter: blur(16px);
  opacity: 0;
  transition: opacity 320ms var(--ease-soft);
  will-change: transform, opacity;
}
@media (prefers-reduced-motion: no-preference) {
  .well[data-arriving]::before,
  .well[data-busy]::before,
  .well[data-opening]::before {
    animation: well-sweep 2200ms linear infinite;
  }
}
.well[data-arriving]::before,
.well[data-busy]::before { opacity: 1; }
/* Not over a raw's first look (room.js: showFirstLook): a picture is
   showing, and the wait is said at the foot of the well, as it is over any
   picture still being worked on. Crossing the look's surround, the band
   read as a blurred slab beside the photograph. */
.well[data-glance]::before { opacity: 0; }

/* What the room is waiting on, in words (room.js: sayWorking), over the band
   in the middle of the well -- small, grey, and in the same type as the line
   under the tray that used to carry it. Faded rather than switched, so it
   goes as the picture arrives rather than a frame before. */
.well__wait {
  position: absolute;
  z-index: 3;
  left: 50%;
  top: 50%;
  max-width: calc(100% - 2 * var(--s4));
  margin: 0;
  transform: translate(-50%, -50%);
  font-size: var(--t-sm);
  line-height: 1.4;
  color: var(--film-ink-2);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  /* Legible over the photograph that is waiting to be replaced, which stays
     in the well, dimmed, until the next one is ready. */
  text-shadow: 0 1px 10px rgb(0 0 0 / 0.55);
  opacity: 0;
  visibility: hidden;
  transition: opacity 280ms var(--ease-soft), visibility 0s linear 280ms;
}
.well__wait[data-on] {
  opacity: 1;
  visibility: visible;
  /* A beat late, so a wait that is over almost at once -- a small picture,
     a photograph already developed behind -- never flashes a sentence. */
  transition: opacity 280ms var(--ease-soft) 200ms, visibility 0s;
}
/* With the picture still showing -- a second development after a develop
   setting moves, the distances worked out for Dehaze, a raw's first look
   while it develops -- it is said at the
   foot of the well on a plate of the well's own colour, so it is legible
   over a bright sky and does not sit on whatever is in the middle of the
   photograph. */
.well:not([data-busy]):not([data-arriving]):not([data-opening]) .well__wait,
.well[data-glance] .well__wait {
  top: auto;
  bottom: var(--s3);
  transform: translateX(-50%);
  padding: 2px var(--s3);
  border-radius: var(--radius-control);
  background: color-mix(in srgb, var(--film-shell-solid) 84%, transparent);
}

/* What has just been done (room.js: sayDone): at the foot of the well on the
   same plate, in the room's confirming colour, for a few seconds. */
.well__note {
  position: absolute;
  z-index: 3;
  left: 50%;
  bottom: var(--s3);
  max-width: calc(100% - 2 * var(--s4));
  margin: 0;
  transform: translateX(-50%);
  padding: 2px var(--s3);
  border-radius: var(--radius-control);
  background: color-mix(in srgb, var(--film-shell-solid) 84%, transparent);
  font-size: var(--t-sm);
  line-height: 1.4;
  color: var(--film-ok);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 280ms var(--ease-soft), visibility 0s linear 280ms;
}
.well__note[data-on] {
  opacity: 1;
  visibility: visible;
  transition: opacity 280ms var(--ease-soft), visibility 0s;
}

/* The band's own width plus the well's: 155% of the well, which is 282% of
   the band. A constant rate and no landing, for the reason `work-sweep`
   gives. */
@keyframes well-sweep {
  from { transform: translateX(0); }
  to   { transform: translateX(282%); }
}

/* The picture arrives out of focus and settles. Evenly scaled, so its shape
   is its own on every frame, and from nothing rather than from a third: the
   well is already open, so a blurred picture appearing at a third of its
   strength would be the first thing in the opening to jump. */
.well[data-opening] .well__canvas {
  animation: well-resolve 900ms cubic-bezier(.3, 0, .2, 1) both;
}
@keyframes well-resolve {
  from { filter: blur(14px); transform: scale(1.04); opacity: 0; }
  35%  { filter: blur(9px); opacity: 0.8; }
  to   { filter: blur(0); transform: none; opacity: 1; }
}

/* And light passing over it once as it lands, in the room's two colours --
   the canyon's gold and the blue the light splits into -- and nothing else. */
.well[data-opening]::after,
.well[data-glance]::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(100deg,
    transparent 38%,
    hsl(36 94% 74% / 0.24) 46%,
    hsl(186 70% 74% / 0.22) 54%,
    hsl(210 70% 76% / 0.2) 60%,
    transparent 67%);
  background-size: 260% 100%;
  animation: island-sheen 860ms cubic-bezier(.3, 0, .25, 1) both;
}

/* Somebody who asked for no motion gets the picture, immediately. The room
   never sets these attributes in that case; this is the belt to that braces. */
@media (prefers-reduced-motion: reduce) {
  .well[data-arriving] .well__canvas { opacity: 1; }
  /* The band still says the room is working, standing still in the middle
     of the well rather than crossing it. */
  .well::before { left: 22%; width: 56%; }
  .well[data-opening] .well__canvas,
  .well[data-opening]::after,
  .well[data-glance]::after { animation: none; }
}

/* --- saying the column runs on ----------------------------------------------

   A list that scrolls has to say so. The scrollbar is the browser's answer and
   it is not enough here: it is a hairline in the room's own ink, deliberately
   quiet, and on macOS it is not drawn at all until something moves.

   Two things say it instead. The edge the list runs past is faded rather than
   cut, which reads as the list continuing under it -- the whole point of the
   soft edge is that a hard one looks like the end. And while there is more
   below and nobody has gone looking yet, a button says how many more there are
   and takes you to them, so the rest of the panel is not merely implied: it is
   one press away. That is DESIGN's rule -- every path visible from the start,
   or reachable from it.

   The fade is the building's scroll cue (js/ui/scroll-cue.js), which is
   this room's soft edge made shared: a mask on the column's own box, so the
   edge stays put while the list moves underneath it. What is this room's
   own is the length of each edge and the clear strip under the button. */
.edit .sliders { --cue-size: 26px; }

/* The foot fades further than the head when the button is standing on it,
   because a label over a legible slider is two things in one place. The
   head has nothing on it and only has to stop looking cut.

   "Below" alone is the one state the button is showing in (at the top, with
   more underneath), so the list is faded out *above* it and is clear under
   it: a group's row half-readable behind the button read as the button
   sitting on top of it. The clear strip is the button's height and its gap. */
.edit .sliders[data-more="below"] {
  --cue-size-bottom: 52px;
  --cue-clear-bottom: 42px;
}

/* Over the fade at the foot of the column, centred on the list rather than on
   the column, so it does not sit under the scrollbar's gutter. */
.edit__controls > .more {
  position: absolute;
  left: 50%;
  bottom: var(--s2);
  transform: translate(-50%, 0);
  gap: var(--s2);
  padding-right: var(--s2);
  /* Above the list, and readable over whatever the fade has left behind. */
  z-index: 2;
  background: var(--film-raise-hi);
  color: var(--film-ink);
  box-shadow: 0 6px 18px rgb(0 0 0 / 0.28);
  animation: more-appear 320ms var(--ease-soft) both;
}

.edit__controls > .more svg {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor; stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round;
  animation: more-nudge 2.4s var(--ease-soft) infinite;
}

.edit__controls > .more:hover { transform: translate(-50%, 1px); }
.edit__controls > .more:active { transform: translate(-50%, 0) scale(var(--react-press)); }

@keyframes more-appear {
  from { opacity: 0; transform: translate(-50%, 6px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

/* One small fall, then a long wait. A chevron that bounces without stopping is
   a thing you end up reading past; one that moves every couple of seconds is
   still noticed and never becomes wallpaper. */
@keyframes more-nudge {
  0%, 72%, 100% { transform: translateY(0); }
  80%           { transform: translateY(3px); }
  88%           { transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .edit__controls > .more,
  .edit__controls > .more svg { animation: none; }
}

/* --- a mask's own adjustments ------------------------------------------------

   A mask carries everything the picture does, which is twelve sliders, and
   twelve sliders at the panel's usual rhythm is a column longer than the
   photograph beside it. The groups exist precisely to stop that happening, so
   a mask panel that reintroduces it defeats the thing it lives inside.

   Tighter rather than fewer, and tighter rather than hidden: the label and the
   reading share a line with the track instead of standing above it, which is
   the same three things in two thirds of the height. Nothing is collapsed and
   nothing is behind a menu -- every one of the twelve is on screen the moment
   a mask is chosen, which is the rule this room is built on. */
.adj__tight { margin-bottom: var(--s2); }

.adj__tight .adj {
  display: grid;
  /* The name gives way before the track does. A track under about a hundred
     pixels stops being a thing you can place a value on and becomes a thing
     you nudge at. */
  grid-template-columns: minmax(0, 5em) minmax(90px, 1fr) 2.4em;
  align-items: center;
  gap: var(--s2);
  padding: 1px 0;
}

/* The row's two halves are pulled apart and put either side of the track.

   Each one is placed by column rather than left to fall where the markup puts
   it. `display: contents` hands the three of them to the grid in document
   order -- name, reading, track -- which put the track in the last column and
   gave it 42 pixels to be a slider in. They are named instead, and the markup
   can say what it likes. */
.adj__tight .adj__row {
  display: contents;
  margin: 0;
}

.adj__tight .adj__name {
  grid-column: 1;
  grid-row: 1;
  font-size: var(--t-xs);
  /* A long name gives way rather than pushing the track narrower. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.adj__tight .adj__value { grid-column: 3; grid-row: 1; text-align: right; }

/* Shorter, because it no longer has to leave room for a line of text above
   it. The thumb keeps its size: a control you cannot get hold of is not a
   smaller control, it is a worse one. */
.adj__tight .adj__slider { grid-column: 2; grid-row: 1; height: 16px; }

/* The word over each run. Quieter than a group's name, because a run is a
   division inside one group rather than another group. */
.tray__label--run {
  margin: var(--s3) 0 var(--s1);
  font-size: var(--t-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--film-ink-3);
}

/* Four ways to add a mask will not sit on one line in a 280px column. */
.tray__set--wrap { flex-wrap: wrap; }

/* --- while the model is working ---------------------------------------------

   The denoiser has the machine for as long as it runs. A slider dragged
   against it moves in jerks while the picture behind it does not redraw, and
   a control that answers late is worse than one that says it is not available
   -- so the room goes out of reach and says so, the same way the converter's
   face does while a conversion runs.

   Everything except the way out. Stopping has to stay possible at exactly the
   moment everything else is impossible, or the only way out of a long job is
   to close the tab. */
/* Nothing greys out any more.

   The whole face used to, and that was right while the model had the main
   thread: a slider dragged against it moved in jerks. Both models have threads
   of their own now, and both are asked of the photograph as it arrived rather
   than of the recipe -- so nothing changed while one runs can reach it, and
   the two can run at the same time without either having to be redone. The
   matte says where the subject is; the denoiser says what the pixels should
   have been; the shader puts them together at the end. */

/* Light passing over a working button.

   Over the whole surface rather than through the letters. Clipped to the
   text it was a thing you had to be looking at the words to notice, and the
   point of it is to catch the eye of somebody who has looked away: the button
   is the shape that moves, and the words on it stay plainly legible.

   The hues are the room's own -- the amber, green, cyan and violet the shell
   passes over a picture as it opens, and the film casts behind the console. */
[data-face="photo-edit"] .pill[data-working] {
  position: relative;
  overflow: hidden;
  color: var(--film-ink);
}

[data-face="photo-edit"] .pill[data-working]::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -85%;
  width: 85%;
  pointer-events: none;

  /* The crystal's own colours, read off the room's film rather than guessed
     at: warm amber through the glass, teal in its thickness, cold blue in the
     shadow behind it. Sampled over a dozen frames, the film's hues come out at
     30, 180 and 210 degrees, in that order of quantity.

     Wide and soft. It was a narrow band at a third of this width, and a narrow
     band of colour crossing a dark pill does not read as light passing over a
     surface -- it reads as a coloured line going past, which is what it
     literally was. The blur is what turns the one into the other; it costs
     nothing per frame because the element is on its own layer for the
     transform anyway. */
  background-image: linear-gradient(100deg,
    transparent 0,
    hsl(30 94% 80% / 0.14) 16%,
    hsl(30 94% 80% / 0.42) 32%,
    hsl(20 60% 74% / 0.46) 44%,
    hsl(180 45% 66% / 0.40) 60%,
    hsl(210 55% 64% / 0.24) 78%,
    transparent 100%);
  /* Soft, not faint. The first soft version dropped the strength along with
     the hard edge and disappeared into the pill; the blur is what stops it
     reading as a line, and the strength is what stops it reading as nothing. */
  filter: blur(4px);

  /* Moved with a transform, not with a background position.

     A background position is a paint: the page has to run the style and paint
     it again on every frame, on the same thread that was already busy -- which
     is how a loading indicator ends up hanging at exactly the moment it is
     most needed. A transform is handed to the compositor and goes on moving
     smoothly whatever else the page is doing. The work moved to its own
     thread as well, but this one should hold up even when something else does
     not. */
  animation: work-sweep 1800ms linear infinite;
  will-change: transform;

  /* A constant rate and no landing. The converter's easing accelerates
     towards the end of a pass, which suits a readout that resolves to a
     number; looped forever it leaves a beat at the join where nothing moves,
     and a loading indicator that stops for a moment looks like one that has
     stopped. */
}

/* The band is 85% of the button wide and starts entirely off its left edge, so
   its own width plus the button's -- 185% of the button, 218% of the band --
   carries it right across and out the other side. */
@keyframes work-sweep {
  from { transform: translateX(0); }
  to   { transform: translateX(218%); }
}

/* Somebody who asked for less movement keeps the words and the percentage,
   which is the part carrying the information. */
@media (prefers-reduced-motion: reduce) {
  [data-face="photo-edit"] .pill[data-working]::after { animation: none; content: none; }
}

/* Auto, apart from the looks it sits beside.

   Every other pill on that rail is an opinion that means the same thing
   whatever it is pointed at; this one measures the photograph, and it should
   be findable without being shouted.

   It was an accent-coloured outline and that was wrong twice. It fixed the
   text colour, so the pill kept light text when it was pressed and went light
   underneath -- white on white. And a coloured border on one pill in a row of
   pills whose own borders come up on hover put two lines a couple of pixels
   apart, which reads as a mistake rather than as emphasis.

   So no border of its own, and no opinion about the text. What marks it is
   light: a highlight where light would catch a rounded thing, and a refraction
   going slowly round inside it. Round rather than across, because across is
   what the denoiser's button does while the model runs, and anything that
   crosses a button from one side to the other means loading no matter how
   slowly it travels. */
[data-face="photo-edit"] .pill--auto {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

/* Where the light sits on it: bright at the top left, a soft bounce at the
   bottom right, which is how a rounded surface reads at any size. */
[data-face="photo-edit"] .pill--auto::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  background:
    radial-gradient(120% 90% at 26% 14%, rgba(255, 255, 255, 0.20), transparent 58%),
    radial-gradient(100% 80% at 80% 96%, rgba(255, 255, 255, 0.09), transparent 62%);
}

/* The refraction, turning rather than travelling.

   A band crossing the face is the gesture this room uses for *working* -- it
   is what the denoiser's button does while the model runs -- so a second one
   here said "loading" however slowly it moved. Light going round says
   something else entirely: that the thing has depth and is catching what is
   around it. It is also the shell's own idiom, the conic refraction that runs
   the rim while the console changes shape, borrowed here and slowed to a walk.

   Filling the pill rather than edging it, because an edge is a border, and a
   border on one pill in a row of pills was the problem that started this. */
[data-face="photo-edit"] .pill--auto::after {
  content: "";
  position: absolute;
  /* Overspilling its own box, so no edge of the gradient ever shows. */
  inset: -40%;
  z-index: -1;
  pointer-events: none;
  background: conic-gradient(from var(--refract-angle),
    hsl(42 92% 72% / 0) 0deg,
    hsl(42 92% 72% / 0.30) 52deg,
    hsl(96 70% 70% / 0) 104deg,
    hsl(178 82% 72% / 0.26) 156deg,
    hsl(214 88% 76% / 0) 212deg,
    hsl(286 72% 76% / 0.24) 268deg,
    hsl(332 80% 74% / 0) 324deg,
    hsl(42 92% 72% / 0) 360deg);
  filter: blur(7px);
  animation: auto-refract 9s linear infinite;
}

@keyframes auto-refract { to { --refract-angle: 360deg; } }

/* Where a turning angle cannot be animated, the colours are simply there and
   still -- which is a jewel that is not catching the light rather than no
   jewel at all. */
@media (prefers-reduced-motion: reduce) {
  [data-face="photo-edit"] .pill--auto::after { animation: none; }
}

/* A reading you can drag. The cursor says so before the hand tries it, and the
   text does not select under the drag -- a number that highlights halfway
   through being changed reads as a mistake. */
[data-face="photo-edit"] .pill--scrub {
  cursor: ns-resize;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}
[data-face="photo-edit"] .pill--scrub[data-scrubbing] { color: var(--film-ink); }
/* As wide at 4% as at 100%, with figures of one width, so opening a photograph
   at another size does not slide every button after it along the tray. */
[data-face="photo-edit"] #photo-zoom {
  font-variant-numeric: tabular-nums;
  min-width: calc(4.7ch + 2 * var(--s3));
}

/* A track that is the whole colour wheel: for choosing a hue, and for turning
   one. The muting is the same judgement as the warmth and tint tracks -- these
   sit beside a photograph, and a full-strength rainbow next to it would fight
   the only thing in the room that is meant to have colour in it. */
[data-face="photo-edit"] .adj[data-wheel] .adj__slider {
  --track: linear-gradient(90deg,
    hsl(0 48% 58%), hsl(60 48% 58%), hsl(120 48% 58%),
    hsl(180 48% 58%), hsl(240 48% 58%), hsl(300 48% 58%), hsl(360 48% 58%));
}

/* A run inside the mask panel.

   The same kind of thing as the picture's own groups, one level in, and it has
   to read that way: these sit *inside* a group and opening one must not look
   like opening another of the outer ones. So they are indented behind a rule
   that runs the height of the section, which is the ordinary way of saying
   "this belongs to the thing above it" and costs no colour, no weight and no
   second idea.

   Their contents are laid out exactly like the picture's adjustments -- the
   name above the track -- because a mask's sliders are the same instruments
   and should not look like a denser species of them. */
[data-face="photo-edit"] .group--run {
  /* Indented by the rule and its padding alone. A margin outside that as well
     took sixteen pixels off every track in the run, and the track is the part
     doing the work -- the rule says "this belongs to the thing above" just as
     clearly from where it is. */
  margin: var(--s2) 0;
  padding-left: var(--s3);
  border-left: var(--hairline) solid var(--film-rule);
  border-top: 0;
}
[data-face="photo-edit"] .group--run > .group__head { padding: var(--s2) 0; }

/* Its name is quieter than an outer group's, and it carries a mark of the
   nesting rather than shouting the difference. */
[data-face="photo-edit"] .group--run > .group__head .group__name {
  font-size: var(--t-xs);
  color: var(--film-ink-2);
  letter-spacing: 0.04em;
}
[data-face="photo-edit"] .group--run[data-open="true"] { border-left-color: var(--film-ink-3); }
[data-face="photo-edit"] .group--run[data-open="true"] > .group__head .group__name { color: var(--film-ink); }

/* That a photograph came back with the changes somebody last made to it.

   Beside Reset, because Reset is what the sentence makes you think about, and
   in the room's own confirming colour -- the one `panel__alert[data-kind]`
   uses -- because this is a thing that went right. Quiet, small, and no
   button: the room does not need permission to give back work that was
   already done, it only needs to say that it did. */
[data-face="photo-edit"] .kept {
  align-self: center;
  margin-left: var(--s2);
  font-size: var(--t-xs, 11px);
  letter-spacing: 0.02em;
  color: var(--film-ok);
  white-space: nowrap;
}

/* A number you take hold of.

   The export size is dragged rather than chosen from a list, so it has to
   look like something that can be dragged before anybody tries. The cursor
   says which way, the grab state says it is happening, and neither is
   decoration: a control whose only affordance is that somebody guessed is a
   control nobody finds. */
[data-face="photo-edit"] .pull {
  position: relative;
  display: inline-flex;
  align-items: center;
  cursor: ns-resize;
  user-select: none;
  touch-action: none;
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease);
}
[data-face="photo-edit"] .pull:hover,
[data-face="photo-edit"] .pull:focus-visible {
  color: var(--film-ink);
}
[data-face="photo-edit"] .pull[data-pulling] {
  cursor: ns-resize;
  color: var(--film-ink);
  background: var(--film-hover, rgb(255 255 255 / 0.08));
}

/* The two arrows that say the size can be pulled.

   Above the number and below it, on the centre line, because that is the
   direction they are asking for. Beside it they were a pair of marks at the
   end of a label; over and under they are the gesture drawn out -- and a
   number that can be dragged and does not say so is a number nobody drags.

   Outside the pill, not inside it. Sitting in the padding they were within
   the bubble that lights up under the pointer, which made them read as part
   of the label rather than as something the pill does -- and crowded the
   number besides. Above and below its edge they belong to the gesture and
   leave the text alone.

   Taken out of the flow either way, so the pill stays the width of its text
   and the arrows hang off it rather than pushing it about.

   Quiet at rest and awake under the pointer: the affordance only has to be
   there at the moment somebody wonders.

   Drawn rather than typed. The characters that look like this sit at a
   different height in every font the room might fall back to, and two
   triangles that do not line up read as a mistake. */
[data-face="photo-edit"] .pull__arrow {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: 8px;
  height: 5px;
  fill: currentColor;
  opacity: 0.42;
  pointer-events: none;
  transition: opacity var(--fast) var(--ease);
}
[data-face="photo-edit"] .pull__arrow--up { top: -8px; }
[data-face="photo-edit"] .pull__arrow--down { bottom: -8px; }

[data-face="photo-edit"] .pull:hover .pull__arrow,
[data-face="photo-edit"] .pull:focus-visible .pull__arrow,
[data-face="photo-edit"] .pull[data-pulling] .pull__arrow { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  [data-face="photo-edit"] .pull,
  [data-face="photo-edit"] .pull__arrow { transition: none; }
}

/* --- looks somebody saved ---------------------------------------------------

   The same pill as the eight the room comes with, because a look you saved is
   a look. What it has on top is a way to get rid of it: a cross in the corner
   that appears when the pointer is on the pill, so that deleting is something
   you can see yourself about to do rather than a gesture you have to know.

   Red only once it is the thing under the pointer. A permanently red mark on
   every saved look would make the rail look like a list of problems. */
/* A look somebody made, told apart from the eight the room came with.

   A dot in the room's own accent, before the name. Not a different shape or
   a different colour of pill: those would say it was a different *kind* of
   control, and it is not -- it does exactly what the built-in ones do. The
   dot says where it came from and nothing else.

   The accent rather than a neutral grey, because this rail already has one
   pill wearing the room's light -- Auto, which is the loudest thing on it --
   and a saved look is the other thing here that belongs to you rather than
   to the room. Quiet at rest and full strength when the look is on, so the
   mark follows the pill instead of competing with it.

   `position: relative` stays for the delete cross in the corner. */
[data-face="photo-edit"] .pill--mine {
  position: relative;
  gap: 6px;
}

[data-face="photo-edit"] .pill--mine::before {
  content: "";
  width: 5px;
  height: 5px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--room-photo-accent);
  opacity: 0.6;
  transition: opacity var(--dur-state) var(--ease);
}

[data-face="photo-edit"] .pill--mine:hover::before { opacity: 0.85; }
[data-face="photo-edit"] .pill--mine[aria-pressed="true"]::before { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  [data-face="photo-edit"] .pill--mine::before { transition: none; }
}

[data-face="photo-edit"] .pill__bin {
  position: absolute;
  top: -5px;
  right: -5px;
  /* Sixteen and not twelve. It is a small target on a small pill and the
     thing it does cannot be undone, so it should not need aiming at. */
  width: 16px;
  height: 16px;
  box-sizing: border-box;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--film-face, #1b1f24);
  border: 1px solid var(--film-rule, rgb(255 255 255 / 0.16));
  color: var(--film-ink);
  font-size: 10px;
  line-height: 1;
  opacity: 0;
  transform: scale(0.8);
  transition: opacity var(--fast) var(--ease), transform var(--fast) var(--ease),
    background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
[data-face="photo-edit"] .pill--mine:hover .pill__bin,
[data-face="photo-edit"] .pill--mine:focus-within .pill__bin {
  opacity: 1;
  transform: scale(1);
}
[data-face="photo-edit"] .pill__bin:hover,
[data-face="photo-edit"] .pill__bin:focus-visible {
  background: var(--film-bad, #C0514B);
  border-color: var(--film-bad, #C0514B);
  color: #fff;
}

/* Naming one, in the place the button was. */
[data-face="photo-edit"] .pill--naming {
  min-width: 9em;
  font: inherit;
  color: var(--film-ink);
  background: var(--film-face, #1b1f24);
}

@media (prefers-reduced-motion: reduce) {
  [data-face="photo-edit"] .pill__bin { transition: none; }
}

/* The batch bar, while several photographs are being exported.

   `bar-progress` is the building's, from `styles.css`, and is sized for the
   converter's panel where it sits under everything with air above it. In this
   row it is one control among several, so it loses the margin and takes the
   width that is going. Scoped to this room's face: the converter's own bar
   must keep the shape it was designed with.

   While it is there the rest of the row is not. Everything in it is something
   you cannot do in the middle of an export, and a row of disabled buttons is
   a row of small refusals to read. */
[data-face="photo-edit"] .foot__row--export .bar-progress {
  margin-top: 0;
  height: 34px;
  min-width: 0;
  /* No width at all until it is wanted, rather than no display.

     A thing that is `display: none` has nothing to be transitioned out of, so
     showing it is a jump however long the duration says -- the same fact that
     kept the well from animating shut. Here it is always in the row at no
     width, and grows across it. */
  flex: 0 1 0;
  opacity: 0;
  overflow: hidden;
  transition: flex-basis var(--dur-morph) var(--ease-soft),
              opacity var(--dur-state) var(--ease);
}
[data-face="photo-edit"] .foot__row--export[data-busy] .bar-progress {
  flex-basis: 100%;
  opacity: 1;
}

/* Everything else, still there and plainly not available.

   Greyed rather than taken away. A control that vanishes while something is
   happening leaves you working out what went where; one that dims says both
   that it exists and that it is not yours for the moment. And when the row
   is the thing being replaced, taking its contents out would collapse it to
   nothing and back, which is a flinch rather than a transition.

   Named by their containers rather than one by one. The first version
   listed the view bar and the crop bar, and left the zoom, the turn, Remove
   background, Smart denoise, Upscale and Heal at full brightness and fully
   pressable -- because those are five more sets in the same bar, and a list
   of controls is a list that goes out of date the next time one is added.
   The bar, the column, the head, the strip and the rows: five containers
   that between them are everything except the picture itself and the bar
   saying how far along it is. */
/* While one photograph is on its way the head's back arrow stays live: it
   is the way out of the wait, and calls the opening off (room.js:
   callOff). A batch keeps the whole head. */
.stage[data-busy]:not([data-busy="picture"]) .panel__head,
.stage[data-busy="picture"] .panel__head > :not(.back),
.stage[data-busy] .edit__controls,
.stage[data-busy] .tray,
.stage[data-busy] .foot__row:not(.foot__row--export),
.stage[data-busy] .foot__row--export > *:not(.bar-progress) {
  opacity: 0.32;
  pointer-events: none;
}

/* The strip is the exception, and only while one photograph is being got
   ready.

   Everything above acts on the photograph in the room, and while the next one
   is arriving the one in the room is on its way out -- a slider moved now is
   moved on a picture that is about to be replaced, and the change goes with
   it. The strip is not that. Pressing another frame is changing your mind,
   which is a thing somebody is entitled to do in the second and a half a raw
   takes, and the room already handles it: the later press wins and the
   earlier opening stands down where it finds it has been overtaken.

   A batch is different again, and takes the strip with it: there the answer
   to "which photographs" has been given and acted on, and changing it halfway
   through is not changing your mind, it is changing the thing being done. */
.stage[data-busy="all"] .roll {
  opacity: 0.32;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .stage .panel__head,
  .stage .panel__head > *,
  .stage .edit__controls,
  .stage .tray,
  .stage .roll,
  .stage .foot__row,
  .stage .foot__row--export > * {
    transition: opacity var(--dur-state) var(--ease);
  }
}

/* --- a line about something under way -------------------------------------

   Grey, because it is not a failure and not a result: it is the room saying
   what it is doing while it does it. The panel's alert is shared with the
   converter, so this is scoped to this room's face -- one stylesheet must not
   restyle another room's messages.

   Three dots that arrive one after another and then begin again. A still
   ellipsis says no more than the sentence does; a moving one says the room is
   still there. */
[data-face="photo-edit"] .panel__alert[data-kind="working"] {
  color: var(--film-ink-2);
}

.alert__dots {
  display: inline-flex;
  align-items: flex-end;
  gap: 3px;
  /* Against the sentence's baseline rather than the middle of the line, so
     the dots sit where a full stop would. */
  margin-left: 4px;
  padding-bottom: 0.18em;
}
.alert__dots i {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.22;
  animation: alert-dot 1.4s var(--ease) infinite;
}
.alert__dots i:nth-child(2) { animation-delay: 0.18s; }
.alert__dots i:nth-child(3) { animation-delay: 0.36s; }

/* Up quickly, down slowly, and a long rest before the round starts again --
   which is what makes it read as three dots arriving rather than as three
   dots flickering. */
@keyframes alert-dot {
  0%   { opacity: 0.22; }
  14%  { opacity: 1; }
  44%  { opacity: 0.22; }
  100% { opacity: 0.22; }
}

/* Still there, still grey, just not moving. */
@media (prefers-reduced-motion: reduce) {
  .alert__dots i { animation: none; opacity: 0.6; }
}

/* The picture on its way out, while the next one is being got ready.

   It goes out of focus and dims, and stays there, dimmed, for as long as the
   next one takes -- sixteen seconds for a hundred-megapixel raw in Safari --
   with the room's working light crossing it (see `.well::before`). Only once
   the next photograph is in hand does it fade the rest of the way (the
   arriving state), and the new one comes into focus in the same place. It
   used to fade out completely at the press, which on a long development read
   as the photograph vanishing and the panel sitting on an empty dark well.

   It used to shut: the well closed from both edges to a line across its
   middle and opened out of it again. Shut, it was an empty hole in the panel
   for as long as the next photograph took to develop, and opening, the
   photograph was a squashed strip.

   The pointer goes with it, because nothing in there answers to anything
   while it waits. */
.well[data-busy] { pointer-events: none; }
.well[data-busy] .well__canvas { opacity: 0.32; filter: blur(10px); }
/* Several being exported: each is drawn here on its way through, so none is shown. */
.well[data-busy="all"] .well__canvas { opacity: 0; }

@media (prefers-reduced-motion: no-preference) {
  .well:not([data-opening]) .well__canvas {
    transition: opacity 280ms cubic-bezier(.4, 0, .2, 1), filter 280ms cubic-bezier(.4, 0, .2, 1);
  }
}

/* Handed over from a first look (room.js: dropFirstLook): the developed
   picture is already sharp and whole under the look, and the look alone
   fades. Coming into focus as well, it went sharp, soft, sharp again. */
.well[data-handover] .well__canvas { transition: none; animation: none; }

/* --- the other photographs -------------------------------------------------

   A second island on the deck, out of the left side of the one the picture is
   in. Not a column inside it: the shell clips whatever is drawn outside its
   edge -- that is what gives every room its rounded corner -- so a strip
   coming out of the side has to be its own island or it has to be inside, and
   inside means taking width off the photograph.

   It is made of what the shell is made of, because it is the same kind of
   thing: the room's raised surface, the room's hairline, the room's edge
   light, the room's corner. `styles.css` owns the row they sit in.

   The previews are square-cornered. A rounded thumbnail is a rounded picture,
   and a photograph does not have rounded corners -- the radius belongs to the
   island holding them, which is why the island keeps its and they have none. */
.roll {
  /* Set from one place: the previews, the padding and the island's width are
     the same measurement said three times if they are written separately. */
  --shot-w: 160px;
  --shot-h: 120px;
  position: relative;
  /* The island is as wide as a preview plus what surrounds it -- the padding
     and its own hairline. The border is in the sum because the box is sized
     border-box: leave it out and the list is two pixels narrower than the
     previews it holds, which is two pixels of sideways scrolling that makes
     the strip believe it is the horizontal one. */
  width: calc(var(--shot-w) + var(--s3) * 2 + var(--hairline) * 2);
  flex: 0 0 auto;
  /* To the left of the island it belongs to, which is where it was asked to
     be -- and above it when the deck wraps. It is last in the source because
     the picture is the thing you came for; reaching the strip is one press,
     not one per photograph. See the roving focus in `roll.js`. */
  order: -1;
  border-radius: var(--radius-shell-open);
  background: var(--film-raise);
  border: var(--hairline) solid var(--shell-edge, var(--film-rule));
  box-shadow: var(--react-edge);
  overflow: hidden;
}
.roll[hidden] { display: none; }

/* Everything in the island, lifted out of the island's own height.

   The island stands beside the shell in a row that stretches both to the
   taller of them. Left in normal flow, twenty-two thumbnails are two and a
   half thousand pixels of content, so the row became that tall and took the
   shell with it -- an editing island four times the height of the window
   because there were photographs in the strip beside it.

   Positioned out, it contributes nothing, the row is as tall as the shell,
   and this fills whatever that turns out to be. No measurement, no number
   that has to be kept in step with anything. */
.roll__body {
  position: absolute;
  inset: var(--s3);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}

@supports (backdrop-filter: blur(1px)) {
  .roll {
    backdrop-filter: blur(var(--blur-shell)) saturate(1.12);
    -webkit-backdrop-filter: blur(var(--blur-shell)) saturate(1.12);
  }
}

/* The deck belongs to the building and every room stands on it, so an island
   left on it would follow you into the converter. It shows on this room's
   editing face and nowhere else. */
.stage:not([data-state="photo-edit"]) .roll { display: none; }

.roll__list {
  /* `position: relative` is not decoration: the frames' `offsetTop` is
     measured against this, and that is how the chosen one is scrolled to. */
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* Said, not left to the default. When one axis is not `visible` the other
     quietly becomes `auto`, and an auto axis that never scrolls still answers
     questions about how far it could -- which is what the arrows ask. */
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  /* The arrows are the way through it, so the bar is not also needed -- and a
     system scrollbar inside a column this wide is a tenth of the column. */
  scrollbar-width: none;
}
.roll__list::-webkit-scrollbar { display: none; }

/* Sized in pixels rather than by `aspect-ratio`.

   This was four-by-three as a ratio, and on Safari the strip did not appear
   at all: WebKit does not apply `aspect-ratio` to a `<button>`, the only
   thing inside each one is positioned, and so every frame was a hundred and
   sixty pixels wide and none tall. Two numbers instead of one ratio, held
   together by the custom properties above. */
.roll__shot {
  position: relative;
  flex: 0 0 auto;
  width: var(--shot-w);
  /* Never wider than the column it is in, whatever the column turns out to
     be. On its side there is no such limit and the width stands. */
  max-width: 100%;
  height: var(--shot-h);
  padding: 0;
  overflow: hidden;
  border: 0;
  border-radius: 0;
  background: rgb(0 0 0 / 0.25);
  cursor: pointer;
  opacity: 0.58;
  transition: opacity var(--dur-state) var(--ease),
    box-shadow var(--dur-state) var(--ease);
}
.roll__shot:hover { opacity: 0.88; }

/* Which one is in the room.

   The ring is drawn by a layer of its own, over the thumbnail. It was an
   inset `box-shadow` on the frame, and a box-shadow -- inset or not -- paints
   on the element's own background, which is *behind* the picture: the
   thumbnail covers the frame edge to edge, so the ring was visible on exactly
   the frames whose preview had failed to load and on no others.

   Inset rather than around, so nothing moves as you step through the strip;
   forty photographs twitching by two pixels each is the wrong answer to
   "this is the one". The dark line outside the accent is what keeps it
   readable over a light photograph, which a single gold line on a bright sky
   is not. */
.roll__shot[aria-current="true"] { opacity: 1; }
.roll__shot[aria-current="true"]::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 0 2px var(--accent), inset 0 0 0 3px rgb(0 0 0 / 0.45);
}


/* There is no second mark for "being opened". The ring moves the instant a
   frame is pressed -- before the photograph has developed -- so it already
   says which one was asked for, and the well beside it says that the asking
   is still going on. */

/* Picked out for something that will act on several at once.

   A different mark from the ring, because it answers a different question:
   the ring says which photograph is in the room, and this says which ones a
   batch will take. They overlap constantly -- opening one picks it out -- and
   two marks that mean different things must not look the same.

   So the ring stays a ring, and this is the frame coming forward: full
   brightness, and a bar along the foot in the room's own accent. */
.roll__shot[data-chosen="true"] { opacity: 1; }
.roll__shot[data-chosen="true"]::before {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 3px;
  background: var(--accent);
  pointer-events: none;
  z-index: 1;
}

.roll__thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The name, which is what there is to show until the picture of it arrives --
   and all there is for a file that turns out to have no preview in it. */
.roll__name {
  position: absolute;
  inset: auto 0 0 0;
  padding: 4px 6px;
  font-size: 10px;
  line-height: 1.25;
  text-align: left;
  color: var(--film-ink-2);
  background: linear-gradient(to top, rgb(0 0 0 / 0.72), transparent);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: opacity var(--dur-state) var(--ease);
}
.roll__shot[data-ready="true"] .roll__name { opacity: 0; }
.roll__shot[data-ready="true"]:hover .roll__name { opacity: 1; }

/* A frame whose picture has not arrived yet.

   It was a dark rectangle at a quarter opacity with nine-point type on it,
   over a dark backdrop, which is very nearly nothing: a folder of forty read
   as forty empty places where photographs were going to be rather than as
   forty photographs being fetched. There was nothing wrong with it, and no
   way to tell that from the outside, which is the whole fault.

   So it is a surface rather than an absence -- lighter than the island it
   sits on, at full opacity -- and it breathes, slowly, so that "coming" and
   "failed" do not look the same. Both stop the moment the picture lands. */
.roll__shot[data-ready="false"] {
  opacity: 1;
  background: var(--film-raise);
  animation: roll-waiting 1.8s var(--ease) infinite;
}
.roll__shot[data-ready="false"] .roll__name {
  opacity: 1;
  color: var(--film-ink-2);
  background: none;
}

@keyframes roll-waiting {
  0%, 100% { background-color: rgb(255 255 255 / 0.06); }
  50%      { background-color: rgb(255 255 255 / 0.13); }
}

@media (prefers-reduced-motion: reduce) {
  .roll__shot[data-ready="false"] { animation: none; }
}

.roll__arrow {
  flex: 0 0 auto;
  height: 22px;
  display: grid;
  place-items: center;
  border: var(--hairline) solid var(--film-rule);
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--film-ink-2);
  cursor: pointer;
  transition: opacity var(--dur-state) var(--ease), color var(--dur-state) var(--ease);
}
.roll__arrow svg { width: 12px; height: 6px; fill: currentColor; }
.roll__arrow:hover { color: var(--film-ink); }
.roll__arrow:disabled { opacity: 0.22; cursor: default; }

/* Narrow enough that an island beside the picture would leave the picture a
   sliver. The deck wraps, the strip lies on its side above the shell -- the
   same island and the same frames, turned.

   Above and not below, for the same reason it is to the left and not
   underneath on a wide screen: a strip under a seven-hundred-pixel island is
   a strip below the fold, which is a strip nobody knows is there.

   A thousand and not the seven-sixty this room stacks its own columns at.
   They are different questions -- that one asks whether the picture and its
   adjustments fit side by side, this one whether a second island fits beside
   both -- and a shared number would be a coincidence, not a rule. */
@media (max-width: 1000px) {
  /* Scoped to this room's face: the deck is the building's and every room
     stands on it, and only this one ever puts a second island down. */
  .stage[data-state="photo-edit"] .deck { flex-wrap: wrap; }
  .roll {
    width: 100%;
    /* On its side there is no shell beside it to take a height from, so it
       says its own: one preview and what surrounds it. Same three terms as
       the width above, turned. */
    height: calc(var(--shot-h) + var(--s3) * 2 + var(--hairline) * 2);
  }
  .roll__body { flex-direction: row; }
  .roll__list {
    flex-direction: row;
    overflow-x: auto;
    overflow-y: hidden;
  }
  .roll__arrow { height: auto; width: 22px; }
  .roll__arrow svg { transform: rotate(-90deg); }
}

@media (prefers-reduced-motion: reduce) {
  .roll__shot, .roll__arrow, .roll__name { transition: none; }
}

/* A panel that lives inside another one: the tone curve inside Light, the
   colour mix inside Colour, which is where the reference developer's newer
   edition keeps them.

   Built on the same rule the mask panel's runs use, and for the same reason:
   these sit *inside* a group, and opening one must not look like opening
   another of the outer ones. The indent behind a rule that runs the height of
   the section is the ordinary way of saying "this belongs to the thing above
   it", and it costs no colour, no weight and no second idea.

   Tighter than a mask run, and not as tight: a mask run is one of several
   stacked inside its panel and has to give them room, where this is the only
   thing under its parent's sliders and can afford to sit closer to them. */
.group--inside {
  margin: var(--s2) 0 0;
  padding-left: var(--s3);
  border-left: var(--hairline) solid var(--film-rule);
}
.group--inside > .group__head { padding: var(--s2) 0; }
.group--inside > .group__body { padding-bottom: var(--s1); }
.group--inside > .group__head .group__name {
  font-size: var(--t-xs);
  color: var(--film-ink-2);
  letter-spacing: 0.04em;
}
.group--inside[data-open="true"] { border-left-color: var(--film-ink-3); }
.group--inside[data-open="true"] > .group__head .group__name { color: var(--film-ink); }
.group--inside .group__count { opacity: 0.55; }

/* And the colour mix a little tighter still. It carries eight tracks behind
   three pills, so the rows it shows at once is what decides whether the panel
   is usable without scrolling -- but not as tight as a mask run, whose rows
   are a denser species on purpose. */
#photo-hsl-group .adj { margin-bottom: var(--s1); }
#photo-hsl-group .adj__row { margin-bottom: 0; }
#photo-hsl-group .adj__slider { height: 16px; }

/* The colour mix's tracks say which of the three things is being moved, the
   way the reference developer's do: a hue slides between its neighbours on the wheel, a
   saturation runs from grey to the band's own colour, and a lightness runs
   from dark to light. One track that never changed could only ever tell you
   which band you were on, never which question you were answering. */
#photo-hsl-group[data-band-mode="shift"] .adj__slider--band {
  --track: linear-gradient(90deg,
    var(--band-before, var(--band)), var(--band) 50%, var(--band-after, var(--band)));
}
#photo-hsl-group[data-band-mode="sat"] .adj__slider--band {
  --track: linear-gradient(90deg, #8C9093 0%, #A9ADB0 45%, var(--band) 100%);
}
#photo-hsl-group[data-band-mode="lum"] .adj__slider--band {
  --track: linear-gradient(90deg,
    color-mix(in srgb, var(--band) 55%, #101214) 0%,
    var(--band) 50%,
    color-mix(in srgb, var(--band) 45%, #FFFFFF) 100%);
}

/* The curve's five buttons carry their channel whether they are chosen or
   not, which is how the reference developer's row of rings reads: the colour is what the
   button *is*, not a way of saying it is selected. Dimmed when it is not. */
#photo-curve-body .pill[data-curve="r"] { color: #D1483C; }
#photo-curve-body .pill[data-curve="g"] { color: #3FA95C; }
#photo-curve-body .pill[data-curve="b"] { color: #4C7FD6; }
#photo-curve-body .pill[aria-checked="false"] { opacity: 0.62; }
#photo-curve-body .pill[aria-checked="true"] { opacity: 1; }

/* A slider that is made of other sliders: the vignette's shape, the grain's
   character. The reference developer keeps them behind a mark on the row itself, which is
   the right place -- they are not six more things in the panel, they are four
   answers about one thing and two about another.

   The mark sits beside the name, quiet until the row is hovered or it is
   open, because a panel of ten rows with ten chevrons down the side reads as
   ten things to do rather than as ten sliders. It takes up the slack in the
   row (margin-right: auto), so the value stays at the right-hand end where
   every other slider's value is. */
.adj__more {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  width: 18px; height: 18px;
  margin-left: calc(var(--s1) - var(--s3));
  margin-right: auto;
  padding: 0;
  border: 0;
  background: none;
  color: var(--film-ink-3);
  opacity: 0.45;
  cursor: pointer;
  transition: opacity var(--dur-state) var(--ease), color var(--dur-state) var(--ease);
}
.adj:hover .adj__more,
.adj[data-open="true"] .adj__more { opacity: 1; color: var(--film-ink-2); }
.adj__more:focus-visible { outline: 2px solid var(--film-ink); outline-offset: 2px; opacity: 1; }
.adj__more svg {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform var(--dur-state) var(--ease);
}
.adj[data-open="true"] .adj__more svg { transform: rotate(180deg); }

/* And what is behind it, set in behind a rule like every other nested thing
   in this room, so one idea covers the mask runs, the panels inside panels
   and these. */
.adj__sub {
  margin: var(--s2) 0 var(--s1) var(--s2);
  padding-left: var(--s3);
  border-left: var(--hairline) solid var(--film-rule);
}
.adj__sub .adj { margin-bottom: var(--s1); }
.adj__sub .adj__name { font-size: var(--t-xs); color: var(--film-ink-2); }

@media (prefers-reduced-motion: reduce) {
  .adj__more, .adj__more svg { transition: none; }
}

/* And colour grading, which is the same kind of panel as the colour mix: a
   row of pills over sliders that answer for whichever one is chosen. Tightened
   the same amount, so the two read as a pair rather than as two designs that
   happened to end up next to each other. */
#photo-grade-group .adj { margin-bottom: var(--s1); }
#photo-grade-group .adj__row { margin-bottom: 0; }
#photo-grade-group .adj__slider { height: 16px; }

/* The saturation track runs from grey to the hue that is actually chosen, so
   the slider shows what it is about to lay down. `--grade-ink` is set on the
   panel from the hue slider, which is the only place that knows it. */
#photo-grade-group .adj[data-part="Sat"] .adj__slider {
  --track: linear-gradient(90deg, #8C9093 0%, #A9ADB0 40%,
    var(--grade-ink, #A9ADB0) 100%);
}
/* And the luminance track from dark to light through that same colour, which
   is what the slider does. */
#photo-grade-group .adj[data-part="Lum"] .adj__slider {
  --track: linear-gradient(90deg,
    color-mix(in srgb, var(--grade-ink, #9AA0A4) 50%, #101214) 0%,
    var(--grade-ink, #9AA0A4) 50%,
    color-mix(in srgb, var(--grade-ink, #9AA0A4) 45%, #FFFFFF) 100%);
}

/* The views. The 3-way one shows the three range wheels and nothing else;
   the single one shows one wheel, large, with its three sliders. */
#photo-grade-group[data-range="Three"] .grade__single { display: none; }
#photo-grade-group:not([data-range="Three"]) .grade__three { display: none; }

/* Midtones above, shadows and highlights side by side below -- dark to
   light reading left to right, the reference developer's arrangement. */
#photo-grade-group .grade__three {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--s3);
  row-gap: var(--s2);
  margin-top: var(--s2);
}
#photo-grade-group .grade__cell[data-range="Midtone"] {
  grid-column: 1 / -1;
  justify-self: center;
  width: calc(50% - var(--s3) / 2);
}
#photo-grade-group .grade__cellhead {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s2);
}
#photo-grade-group .grade__cellname { font-size: var(--t-xs); color: var(--film-ink-2); }
#photo-grade-group .gwheel__read {
  white-space: nowrap;
  font-size: var(--t-xs);
  color: var(--film-ink-3);
  font-variant-numeric: tabular-nums;
}
#photo-grade-group .grade__single { margin-top: var(--s2); }

/* The wheel: the hues round the edge, running anticlockwise from red at the
   right, and grey at the middle where nothing is laid down. Muted the same
   way as the hue tracks, for the same reason -- it sits beside a photograph.
   The whole disc is the target; the point on it only says where you are. */
#photo-grade-group .gwheel {
  --wheel: 104px;
  --wheel-hue: 48%;
  --wheel-light: 58%;
  position: relative;
  width: var(--wheel);
  aspect-ratio: 1;
  margin: var(--s1) auto var(--s2);
  border-radius: 50%;
  background:
    radial-gradient(circle closest-side,
      hsl(200 4% var(--wheel-light)) 0%, hsl(200 4% var(--wheel-light) / 0) 100%),
    conic-gradient(from 90deg,
      hsl(0 var(--wheel-hue) var(--wheel-light)), hsl(330 var(--wheel-hue) var(--wheel-light)),
      hsl(300 var(--wheel-hue) var(--wheel-light)), hsl(270 var(--wheel-hue) var(--wheel-light)),
      hsl(240 var(--wheel-hue) var(--wheel-light)), hsl(210 var(--wheel-hue) var(--wheel-light)),
      hsl(180 var(--wheel-hue) var(--wheel-light)), hsl(150 var(--wheel-hue) var(--wheel-light)),
      hsl(120 var(--wheel-hue) var(--wheel-light)), hsl(90 var(--wheel-hue) var(--wheel-light)),
      hsl(60 var(--wheel-hue) var(--wheel-light)), hsl(30 var(--wheel-hue) var(--wheel-light)),
      hsl(0 var(--wheel-hue) var(--wheel-light)));
  box-shadow: inset 0 0 0 1px var(--film-line);
  cursor: crosshair;
  touch-action: none;
}
#photo-grade-group .gwheel--big { --wheel: 176px; }
#photo-grade-group .gwheel:focus-visible { outline: 2px solid var(--film-ink); outline-offset: 3px; }
#photo-grade-group .gwheel__puck {
  position: absolute;
  left: var(--puck-x, 50%);
  top: var(--puck-y, 50%);
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  background: var(--grade-ink, var(--film-ink-3));
  border: 2px solid var(--film-ink);
  box-shadow: 0 0 0 1px var(--film-shell-solid);
  pointer-events: none;
}
/* At rest the point is only a small mark at the middle, so an untouched
   wheel reads as untouched. */
#photo-grade-group .gwheel[data-touched="false"] .gwheel__puck {
  width: 8px;
  height: 8px;
  margin: -4px 0 0 -4px;
  background: transparent;
  border-width: 1px;
}
#photo-grade-group .grade__lum .adj__slider {
  --track: linear-gradient(90deg,
    color-mix(in srgb, var(--grade-ink, #9AA0A4) 50%, #101214) 0%,
    var(--grade-ink, #9AA0A4) 50%,
    color-mix(in srgb, var(--grade-ink, #9AA0A4) 45%, #FFFFFF) 100%);
}

/* Blending and Balance are about how the wheels share the picture rather than
   about any one of them, so they sit below a rule and outside the pills --
   which is where the reference developer keeps them too. */
#photo-grade-group .grade__share {
  margin-top: var(--s2);
  padding-top: var(--s2);
  border-top: 1px solid var(--film-line);
}

/* --- point colour ----------------------------------------------------------
   The same tightening as the colour mix and colour grading, so the three
   panels inside Colour read as one family. */
#photo-point-group .adj { margin-bottom: var(--s1); }
#photo-point-group .adj__row { margin-bottom: 0; }
#photo-point-group .adj__slider { height: 16px; }

/* The eyedropper and the colours already taken, on one row. */
.point__top {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-bottom: var(--s2);
  flex-wrap: wrap;
}
.point__swatches { display: flex; gap: 6px; flex-wrap: wrap; }

/* A picked colour, with a way to forget it. The cross only shows on the one
   being pointed at: eight crosses in a row is a row of crosses, not a row of
   colours. */
.point__swatch { position: relative; display: inline-flex; }
.point__dot {
  width: 20px;
  height: 20px;
  border-radius: 4px;
  border: 1px solid var(--film-line);
  padding: 0;
  cursor: pointer;
}
.point__swatch[data-chosen="true"] .point__dot {
  border-color: var(--film-ink);
  box-shadow: 0 0 0 1px var(--film-ink);
}
.point__drop {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 0;
  padding: 0;
  line-height: 13px;
  font-size: 10px;
  background: var(--film-ink-3);
  color: var(--film-back);
  cursor: pointer;
  opacity: 0;
}
.point__swatch:hover .point__drop,
.point__drop:focus-visible { opacity: 1; }

/* The three shift tracks carry the colour they are about, the way the colour
   mix's do -- so the slider shows which colour is being moved. */
#photo-point-group .adj[data-part="sat"] .adj__slider {
  --track: linear-gradient(90deg, #8C9093 0%, #A9ADB0 45%, var(--point-ink, #A9ADB0) 100%);
}
#photo-point-group .adj[data-part="lum"] .adj__slider {
  --track: linear-gradient(90deg,
    color-mix(in srgb, var(--point-ink, #9AA0A4) 55%, #101214) 0%,
    var(--point-ink, #9AA0A4) 50%,
    color-mix(in srgb, var(--point-ink, #9AA0A4) 45%, #FFFFFF) 100%);
}

.point__show {
  margin-top: var(--s2);
}

/* While a colour is being taken, the pointer says so over the photograph. */
[data-face="photo-edit"] .well__canvas[data-picking="true"] { cursor: crosshair; }

/* Undo, redo and the two turns: a drawn icon in a pill of its own height,
   square-ish rather than the width of a character and its padding, so the
   icon is the size of the words beside it and sits in the middle. */
[data-face="photo-edit"] :is(#photo-undo, #photo-redo, #photo-turn-left, #photo-turn-right) {
  width: 36px;
  padding: 0;
}
[data-face="photo-edit"] .pill__icon {
  display: block;
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* --- a finger instead of a pointer -------------------------------------------

   The building's reach (styles.css, "A finger's reach") lends every pill
   here 44 px each way without changing how it looks. What this room adds is
   room for it: rows of pills that wrap stand far enough apart that two
   reaches never meet, and the two things that are whole lines of the panel
   -- a group's head and a slider -- are a finger tall themselves. */
@media (pointer: coarse) {
  .tray, .tray__set,
  [data-face="photo-edit"] .adj--pick .tray__set,
  [data-face="photo-edit"] .adj--stack .tray__set { row-gap: calc(var(--hit) - 30px); }
  .group__head { min-height: var(--hit); }
  /* Auto has both pseudo-elements and a clip of its own (its light), so it
     is a finger tall itself. */
  [data-face="photo-edit"] .pill--auto { min-height: var(--hit); }
  /* The small "what is it made of" button lends the building's reach. */
  :where(.adj__more) { position: relative; }
  :where(.adj__more)::after { content: ""; position: absolute; inset: var(--hit-reach); }
  /* An input lends no pseudo-element: the slider itself is the height, its
     track still drawn in its middle, and the margin gives back what it took. */
  .adj__slider { height: var(--hit); margin-block: calc((20px - var(--hit)) / 2); }
}
