/* The cleaning simulator (ui/clean/clean.ts): the dirt layer inside #world, the
   sheet of glass that catches every pointer, the tool drawn under the hand, and
   the one bar left standing. Nothing here applies unless :root carries
   .is-cleaning, which only the two ways in put there. Loaded after timeline.css
   and before quality.css, so the quality dial still wins on every animation
   below. */

/* One ease and four durations for the whole mode, written once. Nothing in here
   snaps and nothing bounces: it is a zen thing, and a spring in it would be a
   different feeling arriving in the middle of this one.

   The maintainer sometimes looks at this over Remote Desktop, where animation
   arrives as a jump or not at all. That is the protocol, not the numbers. */
:root {
  --clean-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --clean-travel: 320ms;
  --clean-leave: 450ms;
  --clean-shine: 600ms;
  --clean-sweep: 2200ms;
}

/* ---------------------------------------------------------------------------
   The takeover

   Hiding the rest of the chrome is one word in chrome.css, beside .is-touring,
   which already does exactly this for the tour. What is here is the other half:
   the cards stop answering. Every card, not just the ones that are dirty - the
   board is frozen, and a card that still lifts under the pointer while its
   neighbour is being washed is the mode leaking.
   --------------------------------------------------------------------------- */

:root.is-cleaning .item,
:root.is-cleaning .item *,
:root.is-cleaning .item-shadow { pointer-events: none; }

/* A board bigger than the game (TUNING.maxCards): the cards it left out go out
   of sight while it is up, written by setCleanPlay() in canvas/items.ts, and
   the connection lines go with them - a line to a hidden card points at
   nothing, and the lines are routed as one layer. */
.item.is-clean-out,
.item-shadow.is-clean-out,
:root.is-clean-culled #web { visibility: hidden; }

/* The paper texture stays, quieter: it is the surface being cleaned. */
:root.is-cleaning #grain { opacity: 0.4; }

/* A dirty GIF holds still, and starts again the moment its card comes clean.
   A card under an inch of mud carrying on as though nothing had happened to it
   is the one thing on the board not taking part, and the frame where it picks
   up again is worth more than the loop nobody could see under the crust.

   The same twin <img> and the same swap canvas/stills.ts already uses for the
   zoom rung and for the quality dial - clean.ts writes the class per card and
   asks that module to shoot the frame. `is-ready` is on the twin only once its
   picture has loaded, so a capture that never arrived leaves the GIF playing
   rather than blanking the card. */
.item.is-clean-held .item-body img[data-gif] + img.still.is-ready { display: block; }
.item.is-clean-held .item-body img[data-gif]:has(+ .still.is-ready) { display: none; }

/* The same class drains a dirty card's colour, and the colour comes back when
   the last of its mess is gone - so the board regains its colour card by card.
   On .item-body, which is where the content is: .item's own background is the
   neutral card stock, and its transition list is a shorthand this sheet must
   not replace. The card's own picture adjustments go first and still apply.
   The same slow pace both ways - the board drains as the mess arrives, and a
   card coming clean blooms back rather than switching on. */
:root.is-cleaning .item-body { transition: filter 1.2s var(--clean-ease); }
.item.is-clean-held .item-body { filter: var(--item-filter, ) saturate(0.15); }

/* ---------------------------------------------------------------------------
   The dirt

   A zero-sized box inside #world whose children are placed by the same
   arithmetic as the cards. It costs nothing in layout and inherits the world
   transform, which is the whole reason pan and zoom keep working.
   --------------------------------------------------------------------------- */

/* Addressed by class, not by the `dirt` id it also carries: the element is made
   when the mode opens and is not in index.html, and tests/element-ids.test.js
   holds every id a stylesheet names to one that is. The id is for the console. */
.clean-layer {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
  overflow: visible;
  /* The glass above catches everything. Nothing in here is ever a target, which
     is also why hit-testing is arithmetic rather than elementFromPoint(). */
  pointer-events: none;
}

/* Held back until the intro's black is solid, so nothing of the mess is seen
   arriving. No transition either way: there is a black sheet over it when it
   goes, and the crusts have their own fade once they are painted. See
   showMess() in ui/clean/clean.ts. */
.clean-layer.is-waiting { opacity: 0; }

/* The corner is written on from clean.ts, copied off the card underneath - a
   card's radius is per item type and capped at half its shorter side, so no
   token here can name it. This is the fallback for the frame or two before that
   read lands, and for a card that is culled and has no node to read. */
.clean-card {
  position: absolute;
  overflow: hidden;
  border-radius: min(var(--radius), var(--half-min, 9999px));
}

.clean-grime,
.clean-run,
.clean-wet,
.clean-bits {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* The crust is generated small and drawn up, so it is always being scaled.
   Smooth is right for dirt; a crisp edge would read as a sticker rather than as
   something settled on the card. It is also painted a frame or two after the
   mode opens (see drainPaint), which the fade turns from a pop into the mess
   arriving. */
.clean-grime {
  image-rendering: auto;
  opacity: 0;
}

/* Whole here: how see-through a crust is lives in its pixels (LOOK's `cover`,
   art.ts, four fifths by default), so it can differ card by card.

   That is per canvas, and a layered card has two of them stacked - so where
   both remain the card is nearly hidden, and where only the under-layer is left
   it is not. That difference is the whole point of layering: the reveal happens
   twice. clean.ts appends the fields bottom first, which is what puts the top
   layer on top. */
.clean-grime.clean-settled {
  opacity: 1;
  transition: opacity 260ms var(--clean-ease);
}

/* The sheen the jet leaves. Above the crust and below the debris, opacity
   driven per frame by clean.ts rather than by a transition, because it has to
   restart from full every time the jet passes over again. */
.clean-wet {
  opacity: 0;
  mix-blend-mode: screen;
  pointer-events: none;
}

/* Dirty water running off. Over the crust and under the sheen, normal blend, so
   a rivulet crosses grime it has not cleaned yet - which is the point of it.
   No opacity of its own: art.ts paints every wash at its own alpha and the
   canvas is faded down each frame, so each trail dies of its own age. */
.clean-run { pointer-events: none; }

.clean-bits { overflow: visible; }

/* A piece of debris. The outer group carries where it is, as a transform the
   skid and the tug both ease along; the inner carries how it is turned, as an
   attribute nothing animates. See debrisNode(). */
.clean-bit {
  transition: transform 300ms var(--clean-ease);
}

/* A piece's shadow is its own drawing again in currentColor (art.ts), and this
   is what currentColor is: the palette's ink, the way --item-shadow is. */
.clean-bit-shade { color: var(--ink); }

/* Gum that has been pulled at and has not come off yet. Slower on the way back
   than a snap would be - it is rubbery, not springy. */
.clean-bit.clean-resist {
  transition: transform 260ms var(--clean-ease);
}

/* Debris tipped out onto the bare board. One little SVG per piece, placed in
   the dirt layer's own coordinates. */
.clean-loose {
  position: absolute;
  width: 100px;
  height: 100px;
  overflow: visible;
  pointer-events: none;
}

/* ---------------------------------------------------------------------------
   The badge

   What the tool in hand still owes this card, at its top-right, counter-scaled
   by clean.ts so it is one size on screen at any zoom. A red disc and a bang
   while there is work, the arc closing as it comes off, a check when there is
   none - and nothing at all while no tool is held, which is the state the mode
   opens in and returns to. That last rule is what keeps it from being a
   dashboard: the badges are an answer to picking a tool up, not furniture.

   The disc carries the red and the glyph stays the near-white every mark on
   this layer is drawn in. The other way round - a dark bang on the dark disc -
   was tried on paper and does not read at 24px over grime.
   --------------------------------------------------------------------------- */

.clean-ring {
  position: absolute;
  right: 3px;
  top: 3px;
  width: 24px;
  height: 24px;
  overflow: visible;
  transform-origin: 100% 0;
  opacity: 0;
  transition: opacity 260ms var(--clean-ease);
  pointer-events: none;
}

.clean-ring.is-up { opacity: 1; }

.clean-ring .ring-bg {
  fill: rgb(20 20 18 / 0.35);
  stroke: rgb(250 250 248 / 0.3);
  stroke-width: 2;
  transition: fill 260ms var(--clean-ease);
}

.clean-ring .ring-fg {
  fill: none;
  stroke: rgb(250 250 248 / 0.92);
  stroke-width: 2.4;
  stroke-linecap: round;
  transform: rotate(-90deg);
  transform-origin: 12px 12px;
  transition: stroke-dashoffset 160ms linear;
}

/* Both glyphs are held back by their own length and drawn on, rather than
   appearing - which is why each carries a dasharray and neither is hidden. The
   bang's is the stem, the gap and the dot together. */
.clean-ring .ring-tick,
.clean-ring .ring-bang {
  fill: none;
  stroke: rgb(250 250 248 / 0.95);
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.clean-ring .ring-tick {
  stroke-dasharray: 14;
  stroke-dashoffset: 14;
}

.clean-ring .ring-bang {
  stroke-dasharray: 7;
  stroke-dashoffset: 7;
}

/* Wanted here: the disc goes red and the bang draws on. The arc stays white
   and keeps closing, so the circle completing is legible against it. */
.clean-ring.is-want .ring-bg { fill: var(--danger); }
.clean-ring.is-want .ring-bang {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset var(--clean-travel) var(--clean-ease);
}

/* Nothing left: the ring closes into a check. */
.clean-ring.is-done .ring-fg { opacity: 0; }
.clean-ring.is-done .ring-tick {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset var(--clean-travel) var(--clean-ease);
}

.clean-webs {
  position: absolute;
  overflow: visible;
}

/* A web is pale and so is the board: without a shadow under it, it is a web
   only over a dark card and nothing at all over the paper between them, which
   is exactly where webs hang. Its own path again, dropped down the screen
   (art.ts, webNode()) rather than a drop-shadow filter over the whole sheet -
   the sheet is board-sized and a filter re-rasterised all of it at every zoom
   step. Wider and fainter than the strand, which is what the blur was doing. */
.clean-web-shade {
  fill: none;
  stroke: rgb(30 26 20 / 0.3);
  stroke-width: 2.1;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.clean-web {
  fill: none;
  stroke: rgb(250 249 245 / 0.86);
  stroke-width: 1.1;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The tenant of the biggest web. Placed in the dirt layer's own coordinates and
   turned by clean.ts, which is also what drives the drop and the walk - this
   sheet owns the leg shuffle and nothing else.

   Its own shadow rather than the webs' sheet: it leaves that sheet behind the
   moment the web is swept, and walks off across the board on its own. */
.clean-spider {
  position: absolute;
  overflow: visible;
  pointer-events: none;
  filter: drop-shadow(0 1px 1px rgb(30 26 20 / 0.4));
}

/* Two teams of four - the first and third leg on one side with the second and
   fourth on the other, which is the diagonal gait a spider actually walks. Half
   a cycle apart, so one team is always down. */
.clean-spider-legs {
  transform-box: view-box;
  transform-origin: center;
  animation: clean-shuffle 2.4s ease-in-out infinite;
}

.clean-spider-legs.is-b { animation-delay: -1.2s; }

/* A stroll is the same shuffle, quick. */
.clean-spider.is-walking .clean-spider-legs { animation-duration: 0.5s; }

/* The tier where nothing moves on its own. It sits still and goes with its web -
   see dropSpider(). */
.clean-spider.is-still .clean-spider-legs { animation: none; }

/* Two keyframes, which is all a shuffle is: the legs gather and reach again. A
   rotation rather than a translation, so the far side of the body swings more
   than the near one and the whole thing reads as pushing rather than sliding. */
@keyframes clean-shuffle {
  0%, 100% { transform: rotate(-3deg) translateY(0.3px); }
  50% { transform: rotate(3deg) translateY(-0.3px); }
}

/* The robot vacuum. Placed by clean.ts, which turns only the body inside it:
   the shadow is on this box, which never turns, so it always falls down the
   screen - see robotNode(). Above the webs and the prints, because it drives
   over both. */
.clean-robot {
  position: absolute;
  pointer-events: none;
  filter: drop-shadow(0 2px 2px rgb(30 26 20 / 0.45));
}

.clean-robot svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* The mop pad, out of sight until it is fitted, then set down under it. */
.clean-robot-mop {
  opacity: 0;
  transform: scale(0.6);
  transform-box: view-box;
  transform-origin: center;
  transition: opacity 900ms var(--clean-ease), transform 900ms var(--clean-ease);
}

.clean-robot.has-mop .clean-robot-mop {
  opacity: 1;
  transform: none;
}

/* The one light, breathing slowly: it is on and it is working. quality.css's
   animation switch stills it on the low tier with everything else. */
.clean-robot-lamp { animation: clean-lamp 2.4s ease-in-out infinite; }

@keyframes clean-lamp {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

/* The thread it comes down on, tied where the strand was. A hairline in the
   layer rather than a path in the web sheet, because the web sheet is the thing
   that has just been swept away. */
.clean-thread {
  position: absolute;
  width: 1px;
  pointer-events: none;
  background: rgb(250 249 245 / 0.8);
}

/* The trail across the floor. Absolutely placed in the dirt layer like a card
   box, but owned by no card - it sits after them in document order, because
   whoever walked through did it once everything else was already on the board,
   and a print under a card is a print nobody can wash.

   Smooth scaling for the same reason the crust has it: a print is generated at
   three pixels to the world unit and drawn up. */
.clean-track {
  position: absolute;
  image-rendering: auto;
  pointer-events: none;
  opacity: 0.86;
}

/* What leaving looks like, for a washed-off crust, a swept web and a washed
   print alike. 450ms is what clean.ts waits before it removes the node. */
.clean-gone {
  animation: clean-gone var(--clean-leave) var(--clean-ease) forwards;
  pointer-events: none;
}

/* Fades where it lies. It used to swell on the way out, which is right for a
   bubble and wrong for a crust: the last of the mud growing as it leaves reads
   as the mess coming back. */
@keyframes clean-gone {
  to { opacity: 0; }
}

/* A card that just came clean. The light is the smaller half of it now - the
   card itself waking up is the event, and a full-strength sweep over a card
   that is also moving is two things happening to one card. */
.clean-card.is-clean::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg,
    transparent 38%,
    rgb(255 255 255 / 0.35) 50%,
    transparent 62%);
  animation: clean-shine var(--clean-shine) var(--clean-ease) forwards;
  pointer-events: none;
}

/* The card wakes up: the real item does exactly what it does under the pointer,
   and the dirt layer's box - which is carrying the light above - goes with it,
   or the light slides across a card that is lifting away from underneath it.

   The tokens and not a transform of our own, so the whimsy dial decides how much
   a card moves. At Harsh --lift-hover is 0 and --grow-hover is 1 and nothing
   moves at all, which is that tier working rather than this failing.
   --lift-hover unmultiplied is the number items.css arrives at: its --lift is
   divided by --iz and then multiplied by it again in `translate`. */
:root.is-cleaning .item.is-woken {
  --lift: calc(var(--lift-hover) / var(--iz));
  --grow: var(--grow-hover);
}

.clean-card {
  transition: translate var(--dur-base) var(--ease-back),
    scale var(--dur-base) var(--ease-back);
}

.clean-card.is-woken {
  translate: 0 var(--lift-hover);
  scale: var(--grow-hover);
}

@keyframes clean-shine {
  from { transform: translateX(-120%); }
  to { transform: translateX(120%); opacity: 0; }
}

/* ---------------------------------------------------------------------------
   The glass

   A sheet inside the viewport element that every pointer lands on. Inside it,
   rather than over the whole page, so a wheel this does not handle still
   bubbles to canvas/input.ts and zooms - the camera stays free.
   --------------------------------------------------------------------------- */

.clean-catch {
  position: absolute;
  inset: 0;
  z-index: var(--z-overlay);
  /* One finger cleans; the browser must not also decide it was a scroll. */
  touch-action: none;
  /* With a tool in hand the tool is drawn under the pointer instead. With
     nothing in hand the ordinary grab cursor is right, because with nothing in
     hand the only thing a press can do is move the board. */
  cursor: grab;
}

/* The room goes a little quiet around the edges while the mode is up. It is the
   only thing here that says "somewhere else" rather than "the app, with dirt on
   it", and it costs one gradient that never repaints. */
.clean-catch::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 46%,
    transparent 55%,
    color-mix(in srgb, var(--ink) 16%, transparent) 100%);
  pointer-events: none;
  animation: clean-dusk 900ms var(--clean-ease) both;
}

@keyframes clean-dusk {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Nothing in hand: the ordinary grab, and grabbing while the board moves. */
.clean-catch.can-pan { cursor: grab; }
.clean-catch.is-panning { cursor: grabbing; }

/* A tool is out: the drawing is the pointer, everywhere and all the time - over
   the bars and their buttons, which each set a pointer of their own (hence the
   !important rather than a selector for every one), and while the board is
   dragged, when the tool simply rides along. */
:root.is-clean-holding,
:root.is-clean-holding * { cursor: none !important; }

/* The jet's mist and the flecks it throws off. One canvas the size of the
   viewport, inside the glass so it pans with nothing and stays where the hand
   is - the particles are in screen space by design. */
.clean-mist {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* ---------------------------------------------------------------------------
   The tool in hand

   On the body rather than inside the glass: the glass opens a stacking context
   at --z-overlay, and a tool carried over the bar has to be drawn on top of the
   bar, which is two rungs above that. 56px, and its own illustration - a 16px
   glyph blown up three and a half times is why the first pass never felt held.
   --------------------------------------------------------------------------- */

.clean-cursor {
  position: fixed;
  left: var(--clean-x, -200px);
  top: var(--clean-y, -200px);
  /* CURSOR_PX in clean.ts is this number. */
  width: 64px;
  height: 64px;
  /* Centred on the point with a margin and not with translate(-50%, -50%),
     because `transform` is the one slot the flights animate. A keyframe replaces
     the whole property, so a base transform here means every keyframe has to
     repeat it exactly - and the one that did not is why a tool flying home
     landed beside its slot rather than in it. Half of 64. */
  margin: -32px 0 0 -32px;
  /* Two shadows: a tight one that seats the tool and a wide soft one that lifts
     it off the board. One alone reads as a sticker or as a smudge.

     The far one grows with the hand's speed - 8px down to 16px, and its blur
     with it - because a tool swept fast is a tool lifted. --clean-lift is 0..1,
     eased and written by clean.ts (paintLift()); unset, it is a still hand. */
  filter: drop-shadow(0 1px 1px color-mix(in srgb, var(--ink) 30%, transparent))
    drop-shadow(0 calc(8px + 8px * var(--clean-lift, 0)) calc(10px + 10px * var(--clean-lift, 0))
      color-mix(in srgb, var(--ink) 24%, transparent));
  transition: opacity 160ms var(--clean-ease);
  pointer-events: none;
  z-index: calc(var(--z-overlay) + 3);
}

.clean-cursor[hidden] { display: none; }

/* Shifted so the tool's working end is under the pointer and the rest of it
   trails down and to the right, the way a hand holds it. clean.ts writes the two
   offsets per tool (TIP there). */
.clean-cursor svg {
  width: 100%;
  height: 100%;
  overflow: visible;
  transform: translate(var(--clean-tip-x, 0px), var(--clean-tip-y, 0px));
  transition: transform var(--clean-travel) var(--clean-ease);
}

/* Under a finger the tool is not drawn at all.

   It used to be drawn offset up and left of the point so the hand would not
   cover it, which put the picture of the tool a centimetre from the place it
   bites - on a board where a press that lands off its mess pans instead of
   working. Aiming by the drawing therefore missed, every time, and the tools read
   as broken rather than as displaced. The tray says which tool is in hand (a lit
   slot, .is-in-hand below) and the finger says where it is, which between them is
   everything the drawing was carrying. The host stays - the washer's gauge is a
   reading and has to be somewhere - and see showCursor() in clean.ts, which stops
   flying it in and out. */
.clean-cursor.is-touch svg { display: none; }

/* And the two dimmings go with it: they dimmed a drawing, and all that is left
   here is the gauge. A tank that fades because the tool is over nothing would be
   a reading that comes and goes for a reason that has nothing to do with it. */
.clean-cursor.is-touch:is(.is-idle, .is-faded) { opacity: 1; }

/* A tool flying home is centred on its path, not hung from its tip. */
.clean-cursor.is-ghost svg { transform: none; }

/* Over something this tool cannot do anything with. Dimmed rather than swapped
   for a "no" symbol: the tool is still the tool, it just has nothing here. */
.clean-cursor.is-idle { opacity: 0.4; }
/* Space is held: the next press moves the board rather than cleaning, said
   before the drag rather than during it, which is the whole point of a held
   modifier. Faded, not gone - the tool is the only pointer there is. */
.clean-cursor.is-faded { opacity: 0.4; }

/* The tilt. clean.ts writes the rotation as an attribute on this group; the
   transition is what turns a jump per pointer event into weight. */
.clean-spin { transition: transform 180ms var(--clean-ease); }

/* ---------------------------------------------------------------------------
   The tank

   A bar standing beside the washer, and only beside the washer. It was a slot
   drawn into the gun's body to begin with, which was wrong twice over: it turned
   with the tool's tilt, so the level had to be read at an angle, and it was six
   pixels of a drawing somebody is holding while looking at the card under it.
   Outside the SVG, so the lean does not reach it.
   --------------------------------------------------------------------------- */

/* To the *right* of the gun. The lance points up and to the left from the
   nozzle under the pointer, so the right is the side with nothing on it, and a
   bar on the left sat between the hand and the lance it was reporting on.

   It follows the drawing rather than the box. The tool is hung from its working
   end (--clean-tip-*, written per tool in clean.ts), so what is drawn sits well
   off-centre inside the 64px host: the washer is pushed some seventeen pixels
   right and twenty-four down, and the svg draws past its box. A bar placed
   against the box was therefore placed against nothing in particular, and for
   the one tool shifted furthest it landed straight on the lance. Tracking the
   same two offsets keeps the gap to the drawing the same whatever is held. */
.clean-tank {
  position: absolute;
  /* Hung from the nozzle, the drawing ends about fifteen pixels past the host's
     right edge, and the translate below moves this bar seventeen: so 10px of
     margin leaves about 12 between the gun and the bar. */
  left: 100%;
  top: 50%;
  width: 8px;
  height: 46px;
  margin: -23px 0 0 10px;
  translate: var(--clean-tip-x, 0px) var(--clean-tip-y, 0px);
  transition: translate var(--clean-travel) var(--clean-ease);
  border-radius: 4px;
  background: var(--paper-3);
  box-shadow: 0 0 0 1px rgb(255 255 255 / 0.3), 0 1px 3px rgb(0 0 0 / 0.35);
  overflow: hidden;
  display: none;
}

.clean-cursor[data-tool="washer"] .clean-tank { display: block; }

/* The washer's own pigment, not a water blue: blue is a colour this app has no
   token for, and a level reads as a level in any colour. Scaled rather than
   sized, so the sixty writes a second it takes are the compositor's. */
/* The level is one number written by paintGauge() (--clean-tank, 0..1) and the
   axis it fills along is this sheet's, because the gauge stands upright beside a
   cursor and lies flat above a finger. Filling up from the foot. */
.clean-tank-fill {
  position: absolute;
  inset: 0;
  border-radius: 4px;
  background: var(--accent);
  transform: scaleY(var(--clean-tank, 1));
  transform-origin: 50% 100%;
  transition: transform 120ms linear;
}

/* Above the finger, and lying down.

   Nothing else is drawn under a finger (see the svg rule above), so the gauge is
   on its own and does not have to keep clear of a lance any more - which is what
   put it out to the right. Above rather than beside: to the right of the point it
   is under the hand of anybody right-handed and behind the wrist of anybody left-
   handed, while above it is clear of both, and horizontal is the shape a reading
   takes when it is not standing next to something. Far enough up to clear a
   fingertip: the host is 64px and centred on the point, so its top edge is 32px
   clear, and the 10px puts the bar at about 45. */
.clean-cursor.is-touch .clean-tank {
  left: 50%;
  top: auto;
  bottom: calc(100% + 10px);
  width: 62px;
  height: 9px;
  margin: 0;
  translate: -50% 0;
}

/* Filling from the left, which is the same gauge turned a quarter. */
.clean-cursor.is-touch .clean-tank-fill {
  transform: scaleX(var(--clean-tank, 1));
  transform-origin: 0 50%;
}

/* Full, with the finger off the glass: the bar goes.

   Under a finger this is the only thing the mode draws on the board, and once
   the tank has filled it is a bar hanging where a hand used to be, saying that
   the washer is as ready as it was before anybody touched anything. So it waits
   out the refill - which is the one thing it is there to show - and then fades.
   Touch alone: beside a drawn tool the bar is part of the tool's own picture,
   and a gun that keeps losing its gauge would read as the drawing flickering.

   Out slowly and back at once: the return is the answer to a press, and an
   answer 300ms late is a control that feels slow. Each direction takes its
   duration from the state it is going *to*, which is why there are two. */
.clean-cursor.is-touch .clean-tank { transition: opacity 90ms var(--clean-ease); }
.clean-cursor.is-touch.is-tank-full .clean-tank {
  opacity: 0;
  transition: opacity 300ms var(--clean-ease);
}

/* There is no footprint ring. The tool's own drawing is where it bites, and a
   circle around it said the same thing twice while sitting between the hand and
   the grime it was there to reveal. */

/* A piece on its way into the bin. Its own fixed node, so it can cross from the
   hand to the bar without belonging to either. */
.clean-fly {
  position: fixed;
  width: 56px;
  height: 56px;
  overflow: visible;
  pointer-events: none;
  z-index: calc(var(--z-overlay) + 3);
}

/* The ending's card on its way down: a copy built by the card's own renderer
   (canvas/spotless.ts, dressed by items.css), over the middle of the screen,
   held above the board, then falling onto it at the size it will have there.
   On the body, not in the glass, because the mode has already gone when it
   lands. Placed by its centre from clean.ts; the fall is a Web Animation there.
   The corner is the one the board gives a card. */
.clean-drop {
  position: fixed;
  z-index: calc(var(--z-overlay) + 3);
  border-radius: var(--radius);
  pointer-events: none;
  transform: translate(-50%, -50%);
}

/* The real card, already on the board, under the copy that is still falling
   onto it. Shown the moment the copy lands - see dealCard(). */
.item.is-clean-arriving { visibility: hidden; }

/* And the light that goes across the whole board behind it. */
:root.is-clean-done .clean-catch::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg,
    transparent 40%,
    rgb(255 255 255 / 0.4) 50%,
    transparent 60%);
  animation: clean-sweep var(--clean-sweep) var(--clean-ease) forwards;
  pointer-events: none;
}

@keyframes clean-sweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(100%); }
}

/* ---------------------------------------------------------------------------
   The opening (ui/clean/intro.ts)

   Black over everything, the bar and the tool included, until a star opens it.
   The black is an SVG rect with a star-shaped hole masked out of it, which is
   the only way to cut a shape *out* of a sheet; the date and the bubble sit on
   top. The black is warm - the palette's ink taken most of the way down - so
   the board's own colours are what the star opens onto, not a cold void.
   --------------------------------------------------------------------------- */

.clean-intro {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-overlay) + 6);
  opacity: 0;
  transition: opacity 600ms var(--clean-ease);
  cursor: default;
  user-select: none;
}

.clean-intro.is-in { opacity: 1; }
.clean-intro.is-fading { opacity: 0; transition-duration: 450ms; }

.clean-intro-sheet {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.clean-intro-dark { fill: color-mix(in srgb, var(--ink) 22%, #000); }
.clean-intro-stage {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  transition: opacity 260ms var(--clean-ease);
}

.clean-intro.is-leaving .clean-intro-stage { opacity: 0; }

.clean-intro-date {
  font-family: var(--font-display);
  font-size: clamp(40px, 9vw, 96px);
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: 0.02em;
  line-height: 1;
  color: var(--paper);
  opacity: 0;
  transition: opacity 600ms var(--clean-ease);
}

.clean-intro-date.is-shown { opacity: 1; }

/* Each digit is a window one digit tall onto a reel of them, which the script
   scrolls. The window's top and bottom are feathered, so a digit rolls in and
   out of sight rather than being sliced off by an edge. */
.clean-intro-cell {
  display: inline-block;
  height: 1em;
  overflow: hidden;
  vertical-align: top;
  mask-image: linear-gradient(transparent, #000 14%, #000 86%, transparent);
}

.clean-intro-reel {
  display: flex;
  flex-direction: column;
  will-change: transform;
}

/* The per-reel motion blurs live in here and nothing is drawn from it - a
   filter referenced from HTML is measured in the referring element's own
   pixels, so this sheet needs no size of its own. See playIntro(). */
.clean-intro-fx {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.clean-intro-reel > span {
  display: block;
  height: 1em;
}

/* Lost: one jolt, the way a needle lands after spinning off the dial. */
.clean-intro-date.is-lost { animation: clean-intro-lost 420ms var(--clean-ease); }

@keyframes clean-intro-lost {
  0% { transform: scale(1.06); filter: blur(3px); }
  30% { transform: translateX(-6px); }
  60% { transform: translateX(4px); }
  100% { transform: none; filter: none; }
}

/* The bubble keeps its place from the start, empty and invisible, so the date
   above it does not jump when it arrives. */
.clean-intro-say {
  position: relative;
  box-sizing: border-box;
  width: min(440px, calc(100vw - 32px));
  min-height: 4.2em;
  padding: 12px 34px 12px 16px;
  border-radius: calc(var(--leaf) * 1.4);
  background: var(--paper);
  border: 1px solid var(--rule);
  box-shadow: 0 14px 30px -10px color-mix(in srgb, #000 50%, transparent);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.45;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 320ms var(--clean-ease), transform 320ms var(--clean-ease);
}

.clean-intro-say.is-shown { opacity: 1; transform: none; }
.clean-intro-say p { margin: 0; }

/* The whole line, for a screen reader only: the typed copy is aria-hidden. */
.clean-intro-heard {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* "Go on": a small down-pointing triangle, bobbing, once the line is all out. */
.clean-intro-next {
  position: absolute;
  right: 12px;
  bottom: 10px;
  width: 10px;
  height: 8px;
  background: var(--accent);
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  opacity: 0;
  transition: opacity 160ms var(--clean-ease);
}

.clean-intro-say.is-ready .clean-intro-next {
  opacity: 1;
  animation: clean-intro-bob 900ms var(--clean-ease) infinite alternate;
}

@keyframes clean-intro-bob {
  from { transform: translateY(-2px); }
  to { transform: translateY(2px); }
}

/* ---------------------------------------------------------------------------
   The bar

   Centred on the window rather than inside --foot-left / --foot-right. Those
   insets exist to keep a bar clear of the bin row and the zoom cluster along the
   foot, and neither is down there while this is up: the bin row is hidden with
   the rest of the chrome, and the zoom cluster - the one piece the mode keeps -
   is pinned to the top left instead (see "The camera, left standing" below).
   Reserving room for furniture that is not drawn would push the bar off-centre
   for nothing.
   --------------------------------------------------------------------------- */

/* Above the glass, not at --z-chrome with the rest of the furniture. #viewport
   is fixed with z-index:auto, so it opens no stacking context and the sheet
   inside it at --z-overlay competes directly with anything on the page below
   that. At chrome height the glass covered the bar and swallowed every press on
   it - the bar looked right and did nothing. */
#clean-bar {
  position: fixed;
  z-index: calc(var(--z-overlay) + 2);
  bottom: max(18px, env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-radius: calc(var(--leaf) * 1.4);
  background: var(--paper);
  border: 1px solid var(--rule);
  /* A tray the tools rest in, so it sits a little heavier on the board than a
     toolbar does: a contact shadow and a long soft one. */
  box-shadow: 0 1px 1px color-mix(in srgb, var(--ink) 12%, transparent),
    0 14px 30px -10px color-mix(in srgb, var(--ink) 36%, transparent);
  max-width: calc(100vw - 24px);
  animation: clean-bar-in 520ms var(--clean-ease) both;
}

#clean-bar[hidden] { display: none; }

/* It rises into place when the mode opens. `display` flipping from none is
   what restarts it, so this costs no script. */
@keyframes clean-bar-in {
  from { opacity: 0; transform: translate(-50%, 18px); }
  to { opacity: 1; transform: translate(-50%, 0); }
}

#clean-tools,
#clean-end {
  display: flex;
  align-items: center;
  gap: 6px;
}

#clean-tools[hidden],
#clean-confirm[hidden],
#clean-end[hidden] { display: none; }

/* The same reactions as #toolbar's buttons - paper-2 on hover, paper-3 pressed -
   because this is the app's toolbar for the length of the mode and a second
   vocabulary of hovers would say it is something else.
   A tool is a column: illustration over name. */
#clean-bar button {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 76px;
  padding: 6px 2px 6px;
  border: 0;
  border-radius: var(--leaf);
  background: none;
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  transition: background-color 200ms var(--clean-ease);
}

/* The tools and the bin are drawn in the stickers' line language: a paper body,
   an inked outline, one band of pigment. A <use> renders in a shadow tree no
   rule can reach, so the three colours go in as the three paints that inherit
   through one - `fill` is the body, `color` is the line (the symbols say
   currentColor), and `stroke` is the pigment, taken by the few shapes in each
   symbol that set no stroke of their own. All three are palette tokens, so the
   tools change with the board's palette the way its stickers do. */
#clean-bar .ico,
.clean-cursor svg {
  fill: var(--sticker-body);
  color: var(--ink);
  stroke: var(--accent);
}

#clean-bar [data-clean="glove"] .ico,
.clean-cursor[data-tool="glove"] svg { stroke: var(--accent-warm); }

#clean-bar [data-clean="duster"] .ico,
.clean-cursor[data-tool="duster"] svg { stroke: var(--accent-deep); }

#clean-can .ico { stroke: var(--rule-2); }

/* A tool lifts a little towards the hand that is about to take it. */
#clean-tools button[aria-pressed] .ico {
  transition: transform var(--clean-travel) var(--clean-ease), opacity var(--clean-travel) var(--clean-ease),
    filter var(--clean-travel) var(--clean-ease);
  filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--ink) 22%, transparent));
}

#clean-tools button[aria-pressed]:not(.is-out):hover .ico { transform: translateY(-3px) rotate(-4deg); }

#clean-bar button[hidden] { display: none; }

#clean-bar button span {
  font-size: 0.68rem;
  line-height: 1;
  letter-spacing: 0.01em;
}

#clean-bar button:hover,
#clean-bar button:focus-visible { background: var(--paper-2); color: var(--accent-deep); }
#clean-bar button:active { background: var(--paper-3); color: var(--accent-deep); }

/* A tool that is in the hand. The slot keeps its width - the row must not jump
   when a tool leaves it - and shows where the tool went from: a dashed outline
   of the space it was in, and nothing else. */
#clean-bar button.is-out {
  background: none;
  cursor: default;
}

/* What is left in the slot is the tool's own outline pressed into the tray - the
   drawing itself, flattened to a faint grey - so the slot says which tool goes
   back there. A dashed box said only that something was missing. */
#clean-bar button.is-out .ico {
  opacity: 0.16;
  filter: grayscale(1) brightness(0.4);
}

#clean-bar button.is-out span { opacity: 0.45; }

#clean-bar button.is-out {
  background: var(--paper-2);
  box-shadow: inset 0 1px 3px color-mix(in srgb, var(--ink) 14%, transparent);
}

/* The same tool in hand, under a finger: lit rather than emptied.

   Nothing is drawn over the board there (.clean-cursor.is-touch above), so this
   slot is the only picture of the tool and the outline .is-out leaves would be
   saying it is nowhere. The selection is the app's own "something is in hand"
   mark, the one .sticker-tile.is-armed wears for the same sentence, and it is
   loud on purpose: on a touchscreen it is the only thing on screen that knows
   which tool a press is about to use. */
#clean-bar button.is-in-hand {
  background: var(--select-fill);
  box-shadow: inset 0 0 0 2px var(--select);
}

/* And the glyph keeps its colours and stands a little out of the tray, the way
   the drawing used to stand off the board. */
#clean-bar button.is-in-hand .ico { transform: translateY(-2px); }

#clean-bar .ico { width: 42px; height: 42px; overflow: visible; }

/* The count and the way out, in the top corner, away from the tray.
   The tray at the foot is for what is in the hand; these two are the session's
   and not the hand's, and a way out standing in the row of tools read as a
   fourth tool. The ✕ is the app's ordinary button - .btn-row's paper, hairline
   and leaf, and its lift, grow and press off the whimsy dial - because leaving
   is an ordinary thing to do and the mode has no other buttons like it. */
#clean-top {
  position: fixed;
  z-index: calc(var(--z-overlay) + 2);
  top: max(14px, env(safe-area-inset-top));
  right: max(14px, env(safe-area-inset-right));
  display: flex;
  align-items: center;
  gap: 8px;
  animation: clean-top-in 520ms var(--clean-ease) both;
}

#clean-top[hidden] { display: none; }

/* Down from the edge it is pinned to, the way the bar comes up from its own. */
@keyframes clean-top-in {
  from { opacity: 0; translate: 0 -12px; }
}

#clean-count {
  display: inline-flex;
  align-items: center;
  /* One height for the whole top row, the zoom cluster in the other corner
     included: --chrome-button-h is a segment of that pill, and it grows to 45px
     under a finger. A plate of its own size stood a dozen pixels short of the
     pill across the screen from it, on the axis the eye reads first. */
  min-height: var(--chrome-button-h);
  min-width: 5ch;
  padding: 0 10px;
  justify-content: center;
  border: var(--hairline) solid var(--rule);
  border-radius: var(--leaf);
  background: var(--paper);
  box-shadow: 0 1px 1px color-mix(in srgb, var(--ink) 12%, transparent);
  font-variant-numeric: tabular-nums;
  font-size: 0.85rem;
  color: var(--ink-2);
  /* The spaces between the three spans collapse in a flex row; this is them. */
  gap: 4px;
}

/* "3 of 12": the two numbers in the display face, a touch larger, and the word
   between them quiet, at the count's own size. The spans are clean.ts's; the
   text they add up to is the aria-live reading, unchanged. */
#clean-count .count-n {
  font-family: var(--font-display, serif);
  font-size: 1.05rem;
  color: var(--ink);
}

#clean-count .count-of { color: var(--ink-3); }

#clean-top .clean-x {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: var(--chrome-button-h);
  padding: 0 12px;
  border: var(--hairline) solid var(--rule-2);
  border-radius: var(--leaf);
  background: var(--paper);
  box-shadow: 0 1px 1px color-mix(in srgb, var(--ink) 12%, transparent);
  color: var(--ink);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    transform var(--dur-base) var(--ease-back),
    color var(--dur-fast) var(--ease);
}

@media (hover: hover) {
  #clean-top .clean-x:hover {
    background: var(--paper-2);
    border-color: var(--accent-text);
    color: var(--accent-deep);
    transform: translateY(var(--btn-lift)) scale(var(--btn-grow));
  }
}

#clean-top .clean-x:focus-visible { background: var(--paper-2); color: var(--accent-deep); }

#clean-top .clean-x:active {
  transform: scale(var(--btn-press));
  background: var(--paper-3);
  /* Faster curve on press - the hover's spring is too slow for a click. */
  transition: transform var(--dur-fast) var(--ease-in), background var(--dur-fast) var(--ease);
}

/* The close glyph is one of the app's own icons and is drawn as they are. */
#clean-top .clean-x .ico {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
}

/* The bin, immediately left of the glove, and only there while the glove is in
   hand - a jet and a duster have nothing to throw away. Bigger than a button's
   glyph because it is a target you throw at with a full hand.

   In the bar's flow, at the left end, and the bar grows to take it: 200ms of
   width and light together, so the plate arrives by being made room for rather
   than by appearing over something. It cannot use `hidden` - display:none has
   nothing to animate from - so the empty state is a zero width the glyph is
   clipped to, and the negative margin cancels the flex gap a zero-width item
   would still be holding open. `inert` is what keeps a button nobody can see
   out of the tab order; clean.ts sets both together.

   Every declaration here is under `#clean-bar #clean-can` and not `#clean-can`
   alone: the tool rule above is `#clean-bar button`, which carries an element
   name and so outranks a lone id. The bin spent a version reading
   `position: relative` off that rule while claiming to be absolute, and landed
   a bar-width off the screen. */
#clean-bar #clean-can {
  width: 0;
  min-width: 0;
  margin: 0 -8px 0 0;
  padding: 4px 0;
  overflow: hidden;
  opacity: 0;
  transition: width 200ms var(--clean-ease), margin-right 200ms var(--clean-ease),
    opacity 200ms var(--clean-ease), transform 140ms var(--clean-ease),
    background-color 200ms var(--clean-ease);
}

/* The rule between the bin and the tools. Its own mark rather than the button's
   right border, because a border forced the button square: a rounded corner
   bends its border with it, so the bin was the one plate in the bar whose hover
   and press ignored the whimsy tier's --leaf. Centred in the gap to the glove -
   the flex gap plus the 2px margin the shown bin keeps. */
#clean-bar #clean-can::after {
  content: '';
  position: absolute;
  top: 6px;
  bottom: 6px;
  right: -5px;
  width: var(--hairline);
  background: var(--rule);
}

/* Shown, and no longer clipped. The hidden overflow above is what holds the
   glyph inside a zero-width slot while the bin is away; once it is here it is a
   54px drawing in a 72px button whose lid *swings out of that box* when it
   opens, and the clip took the corner off both the lid and the rim. The arrival
   is carried by the scale below rather than by the clip, which is what lets this
   be dropped the moment there is room. */
#clean-bar #clean-can.is-shown {
  width: 72px;
  margin-right: 2px;
  opacity: 1;
  overflow: visible;
}

#clean-can .ico { width: 54px; height: 54px; overflow: visible; }

/* The drawing grows with the room it is being given, rather than being revealed
   through a widening slot. A 54px bin centred in a 20px box is clipped to a
   vertical slice of itself, and a slice of a bin looks like a drawing that has
   gone wrong rather than like a bin arriving. */
#clean-bar #clean-can .ico { transition: scale 200ms var(--clean-ease); }
#clean-bar #clean-can:not(.is-shown) .ico { scale: 0.4; }

/* Hinged at the left end of the rim, in the symbol's own 48 box. */
#clean-can .can-lid {
  transform-origin: 8px 19px;
  transition: transform var(--clean-travel) var(--clean-ease);
}

#clean-can.is-open .can-lid { transform: rotate(-54deg) translate(-1px, -1px); }

/* Something landed in it. A pixel or two of settle, and no more. The transition
   that carries it is in the block above, with the rest of them. */
#clean-can.is-hit { transform: translateY(1.5px); }

/* A hand that cannot let go asked what to do with what is in it. This is the
   answer, and there is no other: the bin glows for a second. */
#clean-can.is-asking {
  background: var(--paper-2);
  /* clean.ts holds the class for a second; this ends just inside that. */
  animation: clean-ask 960ms var(--clean-ease);
}

/* One slow breath of light, in and out. A hard ring that snaps on is an alarm. */
@keyframes clean-ask {
  0%, 100% { box-shadow: 0 0 0 0 transparent; }
  45% {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-warm) 55%, transparent),
      0 0 22px color-mix(in srgb, var(--accent-warm) 45%, transparent);
  }
}

/* The ✕ asks before it acts, in its own place in the corner rather than in a
   dialog: a dialog would take the top layer and put a scrim over the board
   somebody is looking at, and asking in the tray put the question across the
   screen from the button that raised it. One plate, the ✕'s paper and
   hairline, with the two answers inside it as the tray's paper-2 plates.
   Exactly the ✕'s height, border included, so the corner does not grow a few
   pixels when the question opens: the answers take whatever height is left. */
#clean-confirm {
  display: flex;
  align-items: stretch;
  gap: 2px;
  height: var(--chrome-button-h);
  padding: 3px;
  border: var(--hairline) solid var(--rule-2);
  border-radius: var(--leaf);
  background: var(--paper);
  box-shadow: 0 1px 1px color-mix(in srgb, var(--ink) 12%, transparent),
    0 8px 20px -8px color-mix(in srgb, var(--ink) 30%, transparent);
  animation: clean-ask-in 200ms var(--clean-ease) both;
}

#clean-top .clean-x[hidden] { display: none; }

@keyframes clean-ask-in {
  from { opacity: 0; translate: 0 -4px; }
}

#clean-confirm span {
  align-self: center;
  padding: 0 6px;
  font-size: 0.85rem;
  color: var(--ink);
  white-space: nowrap;
}

#clean-confirm button {
  padding: 0 12px;
  border: 0;
  /* --radius-xs, as the tour bar's pair: 3px inside the plate's --leaf, a
     --leaf of their own draws a rounder curve than the one around them. */
  border-radius: var(--radius-xs);
  background: var(--paper-2);
  color: var(--ink);
  font: inherit;
  font-size: 0.85rem;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 200ms var(--clean-ease), color 200ms var(--clean-ease);
}

#clean-confirm button:hover,
#clean-confirm button:focus-visible { background: var(--paper-3); color: var(--accent-deep); }

/* Done is the app's primary button and nothing of the mode's own: .btn-row's
   .primary in sidebar.css - accent ground, accent-deep hairline, --leaf corner,
   the body face, and the lift, grow and press off the whimsy dial. It is the
   one thing left to do and it is a verb like any other verb in the app; a 60px
   paper plate in the display face read as a leftover tool slot. The tray's own
   padding shrinks to fit it rather than standing round it as a tall box. */
:root.is-clean-done #clean-bar { padding: 6px; }

#clean-end button {
  width: auto;
  min-width: 112px;
  height: 38px;
  padding: 0 22px;
  flex-direction: row;
  border: var(--hairline) solid var(--accent-deep);
  border-radius: var(--leaf);
  background: var(--accent);
  color: var(--accent-fg);
  font: inherit;
  font-size: 0.9rem;
  letter-spacing: calc(0.03em * var(--ls-scale));
  transition: background var(--dur-fast) var(--ease),
    transform var(--dur-base) var(--ease-back);
}

/* At Harsh the accent can sit close to the paper - the ink edge .primary
   takes there, for the same reason. */
:root[data-whimsy="2"] #clean-end button { border-color: var(--ink); }

@media (hover: hover) {
  #clean-end button:hover {
    background: var(--accent-deep);
    color: var(--accent-fg);
    transform: translateY(var(--btn-lift)) scale(var(--btn-grow));
  }
}

#clean-end button:focus-visible { background: var(--accent-deep); color: var(--accent-fg); }

#clean-end button:active {
  background: var(--accent-deep);
  color: var(--accent-fg);
  transform: scale(var(--btn-press));
  transition: transform var(--dur-fast) var(--ease-in), background var(--dur-fast) var(--ease);
}

/* The bin, gone for good at the end. Hidden it is still a zero-width slot a
   tool high with its divider drawn beside it, and that is what was holding the
   tray open as a tall box round a small button. */
:root.is-clean-done #clean-bar #clean-can { display: none; }

/* ---------------------------------------------------------------------------
   The camera, left standing

   Every other piece of furniture goes out with .is-cleaning - one word in
   chrome.css, beside the two tour states. The zoom cluster comes back, and it is
   the only one that does. The tools are aimed at cards at whatever size the
   board was fitted to, and on a phone there is no wheel, no keys of its own and
   - until this - no way to make a card bigger than a thumbnail. That read as
   tools that could not be used at all rather than as tools that are hard to aim,
   because a press that misses its mess does not do nothing: it pans the board.

   Three things have to be undone to draw it. The opacity and pointer-events
   that rule sets. The z-index, because #corner sits at --z-chrome and the glass
   above it at --z-overlay would swallow every press aimed at it - which is the
   bug #clean-bar's own block already records. And, on a phone, the corner it is
   pinned to: mobile.css sends it to the *top* right there, which in here is the
   count and the way out.

   Top left, on every width and either kind of device rather than a desktop's
   corner and a phone's: it is the one corner the mode claims nothing in - the
   count and the way out have the other one, the tray has the foot - so there is
   one placement to keep clear of instead of two, and the cluster never has to
   stand over the tray to stay out of its way.
   Gated on .is-clean-chrome and not .is-cleaning so it arrives with the tray when
   the star opens (showBar() in clean.ts) - a zoom control over the black would
   otherwise be the only thing on screen for the whole of the opening. */
:root.is-cleaning.is-clean-chrome #corner {
  z-index: calc(var(--z-overlay) + 2);
  top: max(14px, env(safe-area-inset-top));
  bottom: auto;
  left: max(14px, env(safe-area-inset-left));
  right: auto;
  align-items: flex-start;
  opacity: 1;
  pointer-events: auto;
  /* In from the edge it is pinned to, as #clean-top comes in from its own. */
  animation: clean-top-in 520ms var(--clean-ease) both;
}

/* Out, in, and how far out you are - and nothing else.
   Fit, back-to-0,0 and the lock are journeys and a setting, and this is a game
   played with one hand on the board: three more segments of answers to questions
   nobody asks while scrubbing a card, and on a phone they were half the width of
   the only control the mode leaves standing. The lock goes further than hidden -
   startClean() takes it off and stopClean() puts it back, because a held
   magnification is exactly what makes the tools impossible to aim.
   The ring round the pill is an outline drawn over its segments, and each end
   segment clears it with a transparent border of its own; `+` is the last one
   there is now, so it takes the clearance the lock used to. */
:root.is-cleaning.is-clean-chrome #zoom-ctl
  > :is([data-zoom="fit"], [data-zoom="home"], #zoom-lock) { display: none; }
:root.is-cleaning.is-clean-chrome #zoom-ctl > [data-zoom="in"] {
  border-right: var(--hairline) solid transparent;
}

/* While the ✕ is asking, the camera stands down. "Leave it dirty?" is the only
   thing on screen with an answer in it, and the pill across the top from it is
   an invitation to do something else with the same hand. Faded rather than
   removed, over the 200ms the question arrives in, and on #zoom-ctl rather than
   on #corner: the corner's own opacity belongs to clean-top-in, which holds it
   under the animation for as long as the mode is open. */
:root.is-cleaning.is-clean-chrome #zoom-ctl {
  transition: opacity 200ms var(--clean-ease);
}
:root.is-cleaning.is-clean-chrome:has(#clean-confirm:not([hidden])) #zoom-ctl {
  opacity: 0;
  pointer-events: none;
}

/* The ruler and the coordinate readout are not the camera, and they are not
   here: one measures a board nobody is measuring, the other reports a pointer
   that has been taken away and replaced with a tool. The pill on its own. */
:root.is-cleaning :is(#scale-bar, #hud) { display: none; }

/* A narrow screen. Three tools fit where six did not, so there is one
   breakpoint rather than two and the column never has to shrink to a glyph. */
@media (width < 560px) {
  #clean-bar { gap: 2px; padding: 5px 6px; }
  #clean-bar button { width: 60px; }
  #clean-bar button span { font-size: 0.62rem; }
  #clean-bar .ico { width: 34px; height: 34px; }
  #clean-can .ico { width: 44px; height: 44px; }
  /* The gap is 2px here, so that is what the empty bin has to give back. */
  #clean-bar #clean-can { margin-right: -2px; }
  #clean-bar #clean-can::after { right: -2px; }
  #clean-bar #clean-can.is-shown { width: 56px; margin-right: 2px; }
  #clean-top {
    top: max(10px, env(safe-area-inset-top));
    right: max(10px, env(safe-area-inset-right));
    gap: 6px;
  }
  /* The glyph alone. The button keeps its aria-label, so the name is still
     there for anything that reads rather than looks. */
  #clean-top .clean-x span { display: none; }
  #clean-top .clean-x { padding: 0 8px; }
  #clean-count { padding: 0 8px; font-size: 0.75rem; }
  /* The question and both answers have to fit beside the count on a phone. */
  #clean-confirm span { padding: 0 4px; font-size: 0.75rem; }
  #clean-confirm button { padding: 0 8px; font-size: 0.75rem; }
  /* The cluster keeps the mode's own 10px inset at this width, as the count and
     the way out do - both insets, so the pill and the count sit on one line
     across the top. Never `right` here: the corner is pinned left in the mode,
     and a left and a right together stretch its box the width of the screen,
     where it stands over the board catching presses meant for a card. */
  :root.is-cleaning.is-clean-chrome #corner {
    top: max(10px, env(safe-area-inset-top));
    left: max(10px, env(safe-area-inset-left));
  }
}

/* A browser with no color-mix() drops a whole declaration that uses it, which
   here would be every shadow in the mode. The same shadows again, from the
   channel tokens. */
@supports not (color: color-mix(in srgb, red, blue)) {
  .clean-cursor {
    filter: drop-shadow(0 1px 1px rgb(var(--ink-c) / 30%))
      drop-shadow(0 calc(8px + 8px * var(--clean-lift, 0)) calc(10px + 10px * var(--clean-lift, 0)) rgb(var(--ink-c) / 24%));
  }
  .clean-catch::before {
    background: radial-gradient(ellipse at 50% 46%, transparent 55%, rgb(var(--ink-c) / 16%) 100%);
  }
  #clean-bar {
    box-shadow: 0 1px 1px rgb(var(--ink-c) / 12%), 0 14px 30px -10px rgb(var(--ink-c) / 36%);
  }
  #clean-tools button[aria-pressed] .ico { filter: drop-shadow(0 2px 2px rgb(var(--ink-c) / 22%)); }
  #clean-bar button.is-out { box-shadow: inset 0 1px 3px rgb(var(--ink-c) / 14%); }
  #clean-confirm {
    box-shadow: 0 1px 1px rgb(var(--ink-c) / 12%), 0 8px 20px -8px rgb(var(--ink-c) / 30%);
  }
  /* The breath of light is a keyframe, and a keyframe cannot be conditional on
     its own contents: a steady ring for the second instead. */
  #clean-can.is-asking {
    animation: none;
    box-shadow: 0 0 0 3px rgb(var(--accent-warm-c) / 55%);
  }
}
