/* ---------------------------------------------------------
   HUD, toolbar, now-playing bar
   --------------------------------------------------------- */

/* Corner stack: zoom controls + coordinate readout. */
#corner {
  position: fixed;
  z-index: var(--z-chrome);
  right: max(16px, env(safe-area-inset-right));
  bottom: calc(max(14px, env(safe-area-inset-bottom)) + var(--dock));
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  -webkit-user-select: none;
  user-select: none;
  transition: opacity var(--dur-base) var(--ease);
}

#zoom-ctl {
  display: flex;
  align-items: stretch;
  /* The ring is drawn inside the pill, over its segments, and not as a border
     round them: a border takes its width off every segment's height, and each
     segment is meant to be a square glass button. */
  outline: var(--hairline) solid var(--rule-2);
  outline-offset: calc(-1 * var(--hairline));
  border-radius: var(--leaf);
  overflow: hidden;
  background: var(--paper-card);
  box-shadow: var(--shadow-1);
}
#zoom-ctl button {
  display: grid;
  place-items: center;
  /* Every segment the size of every other glass button. */
  width: var(--chrome-button-w);
  height: var(--chrome-button-h);
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-2);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
#zoom-ctl button + button { border-left: 1px solid var(--rule); }
/* The ring is drawn over the two end segments (the outline above), so each
   takes a clear edge of the ring's width on its outer side, as a seam gives
   the others one: the glyph then centres in the part of it you can see. */
#zoom-ctl > button:first-child { border-left: var(--hairline) solid transparent; }
#zoom-ctl > button:last-child { border-right: var(--hairline) solid transparent; }
/* Hover behind a media query; press outside it. Touch :hover sticks. */
@media (hover: hover) {
  #zoom-ctl button:hover { background: var(--paper-2); color: var(--accent-deep); }
}
#zoom-ctl button:active { background: var(--paper-3); }
/* Focus mirrors hover: an outline ring on one segment of a pill looks wrong. */
#zoom-ctl button:focus-visible:not(:disabled) { background: var(--paper-2); color: var(--accent-deep); }
#zoom-ctl button:disabled { color: var(--ink-3); cursor: default; background: none; }
#zoom-ctl svg {
  width: 15px; height: 15px;
  /* Scale the glyph, not the button — avoids dragging hairline dividers. */
  transition: transform var(--dur-base) var(--ease-back);
}
@media (hover: hover) {
  #zoom-ctl button:hover:not(:disabled) svg { transform: scale(var(--btn-grow)) translateY(var(--btn-lift)); }
}
#zoom-ctl button:active:not(:disabled) svg {
  transform: scale(calc(var(--btn-press) - 0.08));
  /* Press: fast ease-in, no overshoot — feedback, not decoration. */
  transition: transform var(--dur-fast) var(--ease-in);
}
/* A segment like the others: 12%–1600% fits in it, so the +/- buttons never shift. */
#zoom-level {
  font-family: var(--font-mono);
  font-size: var(--t-tiny);
  color: var(--ink);
  letter-spacing: calc(0.02em * var(--ls-scale));
}
/* Double rule before the view pair (fit/home) — different verb group. */
#zoom-ctl button[data-zoom="fit"] { border-left-width: 3px; border-left-style: double; }
/* Third group in the pill: the zoom-lock toggle. */
#zoom-lock { border-left-width: 3px; border-left-style: double; }
/* Toggle between two icons — wrappers, not shadow-DOM <use> targets. */
#zoom-lock .lock-shut, #zoom-lock[aria-pressed="true"] .lock-open { display: none; }
#zoom-lock[aria-pressed="true"] .lock-shut { display: inline; }
/* Pressed state: accent ink, no fill (a filled segment reads as a second object). */
#zoom-lock[aria-pressed="true"] { color: var(--accent-deep); }
/* The body wash is in i-lock-shut itself (currentColor 18%). */

/* ---------------------------------------------------------
   The toolbar (ui/toolbar.ts)
   --------------------------------------------------------- */

/* Height = glass button height, published as a token for status.css (#busy). */
:root { --toolbar-h: var(--chrome-button-h); }

#toolbar {
  /* The room the bar is centred in: clear of the menu button at the left and
     of its twin at the right, and of a panel open at either side (below). The
     bar moves over rather than going (research/old/done-2026-09-15.md,
     R11), and ui/toolbar.ts drops the words, then the rarer tools, as the
     room shrinks. */
  --tb-left: calc(max(16px, env(safe-area-inset-left)) + var(--chrome-button-w) + 12px);
  --tb-right: calc(max(16px, env(safe-area-inset-right)) + var(--chrome-button-w) + 12px);
  position: fixed;
  z-index: var(--z-chrome);
  /* Level with an open panel's top (sidebar.css), as the foot row is with its bottom. */
  top: max(var(--panel-inset), env(safe-area-inset-top));
  left: calc((var(--tb-left) + 100vw - var(--tb-right)) / 2);
  max-width: calc(100vw - var(--tb-left) - var(--tb-right));
  translate: -50% 0;
  display: flex;
  align-items: stretch;
  height: var(--toolbar-h);
  /* The ring inside the bar, over its buttons, as on #zoom-ctl: a border would
     take its width off every button's height. */
  outline: var(--hairline) solid var(--rule-2);
  outline-offset: calc(-1 * var(--hairline));
  /* Same corner as the zoom pill — both are segmented pills on glass. */
  border-radius: var(--leaf);
  overflow: hidden;
  background: var(--paper-card);
  /* --shadow-1: same elevation as every other glass control. */
  box-shadow: var(--shadow-1);
  -webkit-user-select: none;
  user-select: none;
  transition: opacity var(--dur-base) var(--ease), visibility 0s, left var(--dur-base) var(--ease);
}
/* Crossing the phone width moves the bar between the top and the foot
   (ui/toolbar.ts sets the class). It fades in where it lands: the `left`
   transition above is for a panel nudging it along its own edge, and at the
   crossing it flew the bar across the window. */
#toolbar.is-relaid {
  transition: none;
  animation: toolbar-relaid var(--dur-base) var(--ease);
}
@keyframes toolbar-relaid { from { opacity: 0; } }
/* A window being resized: the bar follows it exactly (ui/toolbar.ts holdStill). */
#toolbar.is-resizing { transition: none; }
/* A panel takes its side of the room, and the bar centres in what is left. */
#sidebar.is-open ~ #toolbar {
  --tb-left: calc(max(var(--panel-inset), env(safe-area-inset-left)) + min(var(--sidebar-w), 100vw - 28px) + 12px);
}
#header-panel.is-open ~ #toolbar {
  --tb-right: calc(max(var(--panel-inset), env(safe-area-inset-right)) + min(var(--sidebar-w), 100vw - 28px) + 12px);
}

/* The steps ui/toolbar.ts takes as the room shrinks: every tool with its
   word, then icons only, then Colour, Link and Stickers behind More, as the
   phone has them. The move from one to the next is its morph(), in script,
   because the fit has to measure the new step before anything is drawn. */
#toolbar:is([data-fit="icons"], [data-fit="more"]) button span { display: none; }
/* Icons only: each tool one square glass button. */
#toolbar:is([data-fit="icons"], [data-fit="more"]) button {
  flex: none;
  width: var(--chrome-button-w);
  padding-inline: 0;
}
#toolbar[data-fit="more"] [data-more] { display: none; }
#toolbar[data-fit="more"] [data-mobile] { display: flex; }

#toolbar-tools {
  display: flex;
  align-items: stretch;
}

#toolbar button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  /* Fill the bar; the bar carries the height. The 2px under the content lifts
     it 1px over the middle: a word's weight is its lowercase, which sits under
     the middle its capitals are centred on, so a centred row read as low. */
  height: 100%;
  padding: 0 13px 2px;
  border: 0;
  background: none;
  color: var(--ink);
  font-size: var(--t-small);
  letter-spacing: calc(0.02em * var(--ls-scale));
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
/* Seam between adjacent buttons inside the pill. */
#toolbar button + button { border-left: 1px solid var(--rule); }
/* The ring's allowance at the bar's two ends, as on #zoom-ctl. Only where the
   bar is the strip at the top ([data-fit]): on a phone the tools are a tier
   outside the ring. */
#toolbar[data-fit] #toolbar-tools > button:first-child { border-left: var(--hairline) solid transparent; }
#toolbar[data-fit] #toolbar-tools > button:last-child { border-right: var(--hairline) solid transparent; }

/* Two ids to beat the (1,0,1) specificity of `#toolbar button` above. */
#toolbar #toolbar-toggle { display: none; }

/* Mobile-only tool (the More button); hidden on desktop. */
#toolbar [data-mobile] { display: none; }

/* Hover behind media query; press and aria-expanded outside it. */
@media (hover: hover) {
  #toolbar button:hover { background: var(--paper-2); color: var(--accent-deep); }
}
#toolbar button:active { background: var(--paper-3); color: var(--accent-deep); }
/* Focus mirrors hover. */
#toolbar button:focus-visible { background: var(--paper-2); color: var(--accent-deep); }
/* A button whose flyout is down (ui/flyout.ts), held at the hover look while
   the pointer is off it and inside the panel.
   Without this the button goes cold the moment you reach for a row in its own
   menu, and the panel is left hanging off a bar with nothing on it to say which
   segment it belongs to - which on a row of six identically sized segments is
   the only thing that says so.
   #toolbar-toggle wears the same attribute on a phone, and being lit while the
   drawer it opened is out is right there too. */
#toolbar button[aria-expanded="true"] { background: var(--paper-2); color: var(--accent-deep); }
#toolbar svg {
  width: 17px; height: 17px;
  color: var(--accent-text);
  /* The glyph takes the reaction and not the button, for the reason #zoom-ctl
     gives: scaling the button would drag the hairlines between them about. */
  transition: transform var(--dur-base) var(--ease-back);
}
@media (hover: hover) {
  #toolbar button:hover svg { transform: scale(var(--btn-grow)) translateY(var(--btn-lift)); }
}
#toolbar button:active svg {
  transform: scale(calc(var(--btn-press) - 0.08));
  /* The press curve, argued at #zoom-ctl's copy of this line. */
  transition: transform var(--dur-fast) var(--ease-in);
}

/* An armed tool, marked the way #zoom-lock marks a held zoom: the accent at
   full strength rather than a fill behind it, because the bar is one pill on
   glass and a filled segment in the middle of it reads as a second object.
   Nothing sets this until connectors arrive (step 4); it is here beside the
   rest of the bar's dressing rather than three sheets away from it. */
#toolbar button[aria-pressed="true"] {
  color: var(--accent-deep);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* On a phone the bar goes while a panel is open: there the panel is the
   screen, and there is no room beside it to move to. Wider, the bar moves
   over into the room the panel leaves (above), which the maintainer asked for
   in place of the fade it had at every width (R11). The fade was there
   because a centred bar walks inward as it grows while a panel's edge stays,
   and a first tool under the panel is an overlap nobody reports for months.
   Centring in the room left over, and measuring what fits it, answers the
   same overlap without a width to keep in step with the tools.

   visibility carries the delay so it leaves the tab order only once the fade
   has finished, and comes back the instant the panel starts to close. */
@media (max-width: 700px) {
  :is(#sidebar, #header-panel).is-open ~ #toolbar {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity var(--dur-base) var(--ease), visibility 0s var(--dur-base);
  }
}

/* ---------------------------------------------------------
   The now-playing bar (ui/nowplaying.ts)
   --------------------------------------------------------- */

/* ── The gap along the foot, which two bars share ──────────────────────────

   The bottom edge has furniture at both ends - the bin's row on the left, the
   zoom cluster on the right - and the space between them is where anything
   else along that edge has to fit. The player wants it, and so did the
   Timeline before it docked under the whole edge. So the arithmetic is named
   here, once, at the root, rather than computed twice.

   That is not tidiness. The terms below are measurements of *other rules* - the
   bin's row is three chrome buttons and a gap, the zoom cluster is a segmented
   pill whose width has to be stated because it cannot be derived - so a second
   copy would go stale the day a button is added to either cluster, and it would
   go stale in only one of the two bars. One of them would then sit on the
   furniture and the other would not, which reads as a bug in whichever one you
   happened to be looking at.

   Two insets rather than a centre and a width, and that is the whole of how a
   bar stays out of the way: left and right say what it must not sit on top of.
   Narrow the window and the bar shrinks instead of sliding under the furniture;
   open the sidebar and only one number changes. */
:root {
  /* The docked Timeline's height, 0 while it is closed. ui/timeline-view.ts
     writes it; #viewport and everything anchored to the bottom edge add it. */
  --dock: 0px;
  /* The bin row (drawer, undo, redo) and the corner stack: what each inset
     keeps clear, named so a board that does not draw one can say so. */
  --foot-bin-w: calc(3 * var(--chrome-button-w) + 8px);
  --foot-corner-w: 214px;
  --foot-gap: 12px;
  --foot-left: calc(max(16px, env(safe-area-inset-left))
                    + var(--foot-bin-w) + var(--foot-gap));
  --foot-right: calc(max(16px, env(safe-area-inset-right))
                     + var(--foot-corner-w) + var(--foot-gap));
}
/* An inset is the furniture on its side, so it goes where the furniture goes.
   The Mobile board draws no zoom cluster (canvas.css), and the Playlist draws
   no bin row (mobile.css). The gap used to keep room for both anyway, and on
   the Playlist that room was 400px of nothing: it squeezed the player to half
   the list's width and pushed it off the list's centre line. */
:root[data-board-mode="mobile"] { --foot-corner-w: 0px; }
:root[data-board-mode="mobile"][data-feed-lens="playlist"] { --foot-bin-w: 0px; }

/* The now-playing bar in that gap. --np-w caps it - a player is a control and
   does not want to be a metre wide, which is the one way it differs from the
   strip, which does - and it is centred on the *window*, not on the gap.

   The gap is lopsided by design: the bin row is three buttons, the zoom
   cluster is 214px, and a panel opens at one end only. Centring inside it put
   the bar 43px left of the middle on every board, and off the column of the
   Feed and the Playlist, which are centred on the window. So the insets say
   only where the bar must not go: the clamp keeps it on the window's centre
   line while the room allows, and slides it only as far as the furniture on
   one side pushes it.

   Same card as the add bar next door: the same paper, the same hairline, the
   same --leaf corner. It shares an edge with that bar on a phone, and two
   objects on one edge wearing two different corners read as two strips. */
#nowplaying {
  position: fixed;
  z-index: var(--z-chrome);
  bottom: calc(max(14px, env(safe-area-inset-bottom)) + var(--dock));
  /* On the element, so a rule that moves the insets for this bar moves this too. */
  --np-w: min(520px, 100vw - var(--foot-left) - var(--foot-right));
  box-sizing: border-box;
  width: var(--np-w);
  left: clamp(var(--foot-left), 50vw - var(--np-w) / 2, 100vw - var(--foot-right) - var(--np-w));
  /* Two rows and two columns: the transport down the left across both rows, and
     to the right of it the name-and-controls row over the thin seek line.
     A grid rather than a column of flex rows, because the play button belongs to
     both rows at once - it is the bar's one control and it stands beside the
     whole of what it is playing, rather than sitting in the row of small round
     buttons at the far end where it used to be the third of four. */
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  column-gap: 8px;
  row-gap: 4px;
  padding: 7px 12px 8px;
  border: var(--hairline) solid var(--rule-2);
  border-radius: var(--leaf);
  background: var(--paper-card);
  /* --shadow-1, the one every control on the glass wears. See #toolbar. */
  box-shadow: var(--shadow-1);
  -webkit-user-select: none;
  user-select: none;
}
#nowplaying[hidden] { display: none; }
/* And down while a full player that has its own transport is up.

   The bar exists for when you cannot see the thing making the noise. With the
   Desktop window open you can: its play button and seek line are on screen, and
   the bar is a second copy of them laid over the bottom of the window showing the
   first.

   The class ui/playlist.ts writes is `.playlist-transport`, not `.playlist-up`,
   and the difference has earned itself twice. It is on while the window's
   transport drives what is sounding, and off for a video card played beside
   the window, which the window cannot pause - that one gets the bar. The window used to have a
   second body - an album view, a hero and a list with no transport in it at all,
   built to use this bar as its transport - and a flag meaning "the window is open"
   hid the bar there too, leaving that view with no way to play, pause or seek
   anything. The name says the fact this rule depends on rather than the one that
   usually comes with it, which is what makes it safe for the next body.

   The Mobile lens used to be listed here too, on the same reasoning - it carried
   a transport of its own, so `data-feed-lens` stood in for the flag. It does not
   any more: the lens is a header with Play and Shuffle over a track list, and
   this bar is its transport, which is the arrangement the flag's own name was
   written to make room for. The bar is the one player on the phone now.

   It leaves and returns by sliding under the edge rather than blinking, using the
   same four whimsy-tier properties the bar's own entrance uses; `visibility`
   rather than `display` so there is a box for that to happen in. */
:root.playlist-transport #nowplaying {
  visibility: hidden;
  opacity: 0;
  transform: translateY(calc(100% + var(--foot-gap, 12px)));
  transition: opacity var(--dur-base) var(--ease),
              transform var(--dur-base) var(--ease),
              visibility 0s linear var(--dur-base);
}
@media (prefers-reduced-motion: reduce) {
  :root.playlist-transport #nowplaying { transition: none; }
}

/* The lead, down the left of both rows: play/pause, and Next behind it while
   there is a list to step through. ui/nowplaying.ts builds it and prepends it,
   which is why it is the first column without being named in the markup.

   Both rows, explicitly, rather than left to the auto-placement: a Next button
   that comes and goes must not be able to change what row the name is on. */
#nowplaying .np-lead {
  grid-row: 1 / span 2;
  grid-column: 1;
  display: flex;
  align-items: center;
  gap: 2px;
}
#nowplaying .np-top { grid-column: 2; display: flex; align-items: center; gap: 8px; }
#nowplaying .np-seek { grid-column: 2; }

/* Play/pause, at the size a player's play button is.
   Filled in the accent rather than drawn in it, which is the one place on the
   glass that treatment is earned: this is the only control on the bar anybody
   presses without looking for it, and the four in the cluster are ghosts by
   comparison on purpose. Same disc the card's own play button wears (.transport
   .play in media.css), a size up. */
#nowplaying .np-play {
  flex: none;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-fg);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), scale var(--dur-base) var(--ease-back);
}
#nowplaying .np-play svg { width: 16px; height: 16px; }
@media (hover: hover) {
  #nowplaying .np-play:hover { scale: var(--btn-grow); }
}
/* And the press, which this bar had never had.
   The comment above says this is the only control on the glass anybody presses
   without looking for it, which is exactly why the omission mattered: on a phone
   there was no hover to stand in for it, so the most-pressed button in the app
   answered a tap with nothing at all until the audio started. Its own curve, for
   the reason #zoom-ctl's press gives - the transition above settles a disc
   rising to meet a pointer, over --dur-base and with an overshoot, and a press
   wants neither. */
#nowplaying .np-play:active {
  scale: var(--btn-press);
  transition: scale var(--dur-fast) var(--ease-in);
}
/* Next is a passenger: it is only there some of the time, so it is one of the
   quiet round buttons (.np-qbtn, below) rather than a second accent disc. Its
   own rule is further down, next to the class it shares. */

#nowplaying .np-controls { display: flex; align-items: center; gap: 1px; margin-left: auto; }
/* Three things in the cluster, all of them always there: open the playlist, the
   volume, close. Play/pause was the fourth and is the lead now - see the note in
   ui/nowplaying.ts about which cluster a control belongs in. Shuffle, prev and
   repeat used to live here too and be shown or hidden off an .is-queue class;
   they are in the playlist window, beside the list they act on. */

/* Arriving and leaving.
   It comes up from under the edge of the window and goes back down under it.
   Not a nudge: the first version of this rose ten pixels and scaled from 0.98,
   which is an animation by measurement and a pop by eye - a tenth of a second
   of movement smaller than the bar's own padding. A thing pinned to the bottom
   of the screen has an obvious place to come from, and using it is what makes
   the movement legible at all. Hence `100% + 24px` - its own height, and enough
   again to clear the inset it sits on and the shadow it casts.

   Three characters off one pair of rules, because the personality is in the
   tokens and not in the markup: how far under it starts, how much smaller, how
   far tossed, and on what curve. The three tiers set those four at the foot of
   this file; here it is one transform either way.

   Two transitions, deliberately. The rule below is what plays on the way *in* -
   longer, and on --ease-back, because arriving is the app answering something
   and an answer may have some spring in it. The base rule's is what plays on
   the way *out*: quicker and straight, since leaving is only getting out of the
   way and a bar that bounced as it left would be asking to be watched. That is
   the same asymmetry the idle fade makes further down, pointed the other way -
   the furniture drifts off slowly because nothing asked it to go, where this
   goes because somebody closed it.

   Both halves of the exit run for the same --dur-fast on purpose:
   ui/nowplaying.ts ends the exit on the opacity transition, so a transform
   still running when that fires would be cut off by [hidden].

   pointer-events with the opacity, or a bar faded to nothing would still be a
   row of buttons sitting over the board. */
#nowplaying {
  --np-under: calc(100% + 24px);
  --np-shrink: 0.98;
  --np-tilt: 0deg;
  --np-ease: var(--ease-back);

  opacity: 0;
  pointer-events: none;
  transform: translateY(var(--np-under)) scale(var(--np-shrink)) rotate(var(--np-tilt));
  transition: opacity var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
#nowplaying.is-up {
  opacity: 1;
  pointer-events: auto;
  transform: none;
  transition: opacity var(--dur-base) var(--ease),
              transform var(--dur-base) var(--np-ease);
}

/* The sidebar no longer takes it off the screen - it moves over instead. A
   player is the one piece of chrome whose job continues while you are doing
   something else, and hiding it meant the only way to stop a track was to close
   the panel you had just opened. The bin and the add bar still go, because what
   they do is on the board the panel is covering.

   One number: the left gutter clears the panel rather than the trash. The bar
   stays on the window's centre line while that leaves it room, then slides
   right, then loses width - it never slides under the sidebar.

   Both foot bars, because both are in the gap and a panel covers both ends of
   it equally. The general sibling combinator only looks forward, which is why
   #sidebar and #header-panel precede the pair in index.html and why the strip
   sits between #toolbar and #nowplaying rather than anywhere else.

   Below 1080px the bar used to step up over the zoom cluster once a panel
   left it too little room. A bar that jumps a row when a window is resized
   reads as broken, so it stays on the foot and narrows like at any other
   width, and sheds what it can spare as it does (the container rules after
   the bar's parts, below). */
#sidebar.is-open ~ :is(#nowplaying, #timeline-strip) {
  --foot-left: calc(max(var(--panel-inset), env(safe-area-inset-left))
                    + min(var(--sidebar-w), 100vw - 28px) + var(--foot-gap));
}
/* The masthead panel is the same object from the other side. */
#header-panel.is-open ~ :is(#nowplaying, #timeline-strip) {
  --foot-right: calc(max(var(--panel-inset), env(safe-area-inset-right))
                     + min(var(--sidebar-w), 100vw - 28px) + var(--foot-gap));
}

/* At phone width the foot is a flush line of its own - bin, handle and history
   - and both bars stand off it full width rather than sharing it. So the corner
   stack is no longer in the way and the gap is the screen edge and nothing
   else.

   Here rather than in mobile.css, where it was written and where it applied to
   #nowplaying alone. The pair is defined once, in this file, and every rule
   that changes it changes it for both bars: that is the whole of the invariant
   in CLAUDE.md, and a player that had taken a 12px inset while the strip kept
   the 214px one is exactly the drift it names. It costs nothing today only
   because timeline.css hides the strip below 720px - raise that breakpoint and
   the strip lands on the bin row with nothing to catch it. mobile.css keeps the
   half that is about the player alone: how high it stands and how wide it may
   be. */
@media (max-width: 700px) {
  :is(#nowplaying, #timeline-strip) {
    --foot-left: max(12px, env(safe-area-inset-left));
    --foot-right: max(12px, env(safe-area-inset-right));
  }
}

/* The name gets a third of the bar and no more. It is the label on a control,
   not the content - what somebody is looking at while this is up is the
   waveform - so a long filename ellipses rather than pushing the transport
   into a corner. The full one is on the title attribute. */
#nowplaying .np-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--t-small);
  color: var(--ink-2);
}

/* The seek line under the two rows: elapsed, the line, total. Thin - one row and
   a couple of pixels of ink - so it reads as a scrubber, not a second waveform.
   The line is not the measured shape of the sound; a bar is a remote. */
#nowplaying .np-seek { display: flex; align-items: center; gap: 8px; }
#nowplaying .np-time {
  flex: none;
  min-width: 4ch;
  font-size: var(--t-tiny);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
#nowplaying .np-time.np-total { text-align: right; }
#nowplaying .np-line {
  position: relative;
  flex: 1 1 auto;
  height: 14px;          /* the target; the ink is the 2px stroke centred in it */
  cursor: pointer;
  touch-action: none;
}
#nowplaying .np-line-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
}
#nowplaying .np-line-base { stroke: color-mix(in srgb, var(--ink) 22%, transparent); }
/* The base (unplayed) line is clipped to the part *past* the playhead, so the
   grey does not sit under the played wave - the two meet at --np-progress and the
   line reads as wave-then-plain rather than wave-over-grey. The base svg is the
   direct child; the fill's svg is inside .np-line-fill. */
#nowplaying .np-line > .np-line-svg {
  clip-path: inset(0 0 0 calc(var(--np-progress, 0) * 100%));
}
/* The played portion, revealed left-to-right to --np-progress - the same clip the
   card's waveform fill uses. */
#nowplaying .np-line-fill {
  position: absolute;
  inset: 0;
  clip-path: inset(0 calc((1 - var(--np-progress, 0)) * 100%) 0 0);
}
#nowplaying .np-line-fill-line,
#nowplaying .np-line-fill-wave { stroke: var(--accent-text); }
/* Straight at the middle and hard ends of the whimsy axis; a Material-You wave at
   the soft end, the tier that already rounds and softens everything else. */
/* view-box, so the scroll below translates in the wave's own user units (which
   sizeWave has made one-to-one with pixels) rather than in the path's bounding box. */
#nowplaying .np-line-fill-wave { display: none; transform-box: view-box; }
:root[data-whimsy="0"] #nowplaying .np-line-fill-line { display: none; }
:root[data-whimsy="0"] #nowplaying .np-line-fill-wave {
  display: block;
  /* Slowly leftward by exactly one period. WAVE_HALF is 7px and one whole period is
     2 * WAVE_HALF = 14 user units; sizeWave() makes a user unit a pixel and lays the
     path two periods past the edge, so translating by one period and looping is
     seamless - the wave repeats, so the reset is invisible. Keep this 14 in step with
     WAVE_HALF in ui/nowplaying.ts. Slow: a period every few seconds. */
  animation: seek-wave 3s linear infinite;
}
@keyframes seek-wave { to { transform: translateX(-14px); } }
/* Paused: the scroll stops, and the wave flattens all the way to a straight line -
   scaleY(0) about the centre collapses every crest onto the centre line, and the
   non-scaling stroke draws that line at full width. On a transition so it eases both
   ways (flat when paused, swelling back into a wave when it plays again). Not a
   near-zero scale: a sliver of a scale left a shallow ripple standing, which read as
   a paused wave rather than the still line it should be. The scale lives on the
   wrapping group so it does not fight the path's scroll animation. */
#nowplaying .np-line-wave-scale {
  transform-box: view-box;
  transform-origin: 50% 50%;
  transform: scaleY(1);
  transition: transform var(--dur-base) var(--ease);
}
#nowplaying.is-paused .np-line-fill-wave { animation-play-state: paused; }
/* Paused, at the soft end: the wave becomes a plain straight line. Done by swapping
   the wavy fill for the straight one (which is hidden while playing) rather than by
   scaling the wave to nothing - a scaled-to-zero group left no line at all on some
   engines, so the played part of the scrubber vanished on pause. The straight fill
   is the played portion at full ink; the grey base carries the rest. */
:root[data-whimsy="0"] #nowplaying.is-paused .np-line-fill-wave { display: none; }
:root[data-whimsy="0"] #nowplaying.is-paused .np-line-fill-line { display: block; }
@media (prefers-reduced-motion: reduce) {
  :root[data-whimsy="0"] #nowplaying .np-line-fill-wave { animation: none; }
  #nowplaying .np-line-wave-scale { transition: none; }
}
/* Play reads a touch stronger than its neighbours - it is the one reached for. */
#nowplaying .np-play { color: var(--ink); }

/* The volume, and the icon is the label. A word here would be the only text in
   the bar competing with the track's own name, and a speaker is the one glyph
   that needs no gloss. */
#nowplaying .np-volume {
  flex: none;
  display: flex;
  align-items: center;
  gap: 6px;
}
#nowplaying .np-volume svg { width: var(--icon-glyph); height: var(--icon-glyph); color: var(--ink-3); }
#nowplaying .np-volume input { width: 68px; }
#nowplaying .np-close {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--icon-button);
  height: var(--icon-button);
  padding: 0;
  border: 0;
  /* --radius-pill, not 50%, and the shape block in tokens.css is the
     instruction: a 50% that is really a rounded rectangle which happens to be
     square wants the pill. This is a square button with an icon in it, not a
     disc - the play button two rules up is the disc, and it keeps its 50%. The
     difference on screen is one tier: at Harsh, where --radius-pill is 0, this
     squares off with the bar around it instead of being the one thing on a
     board that declares nothing curves still curving. */
  border-radius: var(--radius-pill);
  background: none;
  color: var(--ink-3);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
@media (hover: hover) {
  #nowplaying .np-close:hover { background: var(--paper-2); color: var(--accent-deep); }
}
#nowplaying .np-close:focus-visible { background: var(--paper-2); color: var(--accent-deep); }
#nowplaying .np-close svg { width: var(--icon-glyph); height: var(--icon-glyph); }

/* The right-hand controls: play, and the playlist's shuffle / prev / next /
   repeat. Round icon buttons in the bar's own idiom, like the close - and
   rounded on the same token, so all three follow the corner axis together. */
#nowplaying .np-qbtn {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--icon-button);
  height: var(--icon-button);
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: none;
  color: var(--ink-2);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
@media (hover: hover) {
  #nowplaying .np-qbtn:hover { background: var(--paper-2); color: var(--accent-deep); }
}
#nowplaying .np-qbtn:focus-visible { background: var(--paper-2); color: var(--accent-deep); }
/* The press, for both round buttons at once - the comment above already says the
   close and these are one idiom, and the pressed look is the half of that idiom
   they share exactly. One step further into the paper than the hover, which is
   what #toolbar's press says with the same two tokens.
   No transition of its own, unlike the play disc and the two glyph rules up the
   file: what moves here is a background, and both blocks already run that at
   --dur-fast on --ease. There is nothing to shorten and no overshoot to undo. */
#nowplaying :is(.np-qbtn, .np-close):active { background: var(--paper-3); }
#nowplaying .np-qbtn svg { width: var(--icon-glyph); height: var(--icon-glyph); }
/* An engaged shuffle or repeat reads in the accent. */
#nowplaying .np-qbtn[aria-pressed="true"] { color: var(--accent-text); }
/* And one of them comes and goes: Next, which is only on the bar while a list is
   what is playing (paintNext() in ui/nowplaying.ts). Written here rather than
   left to the browser because `display: grid` above beats the user-agent's
   `[hidden] { display: none }`, so without this the button would be there the
   whole time and merely claim not to be. */
#nowplaying .np-qbtn[hidden] { display: none; }

/* Narrow, it sheds, in the order it can spare things: the volume first (the
   system has one), then the list button and the two times (the line still
   seeks). Play, the name and close stay to the last. It narrows instead of
   stepping up over the zoom cluster - see the panel note above. Measured on
   the bar and not the window, because a panel open at either side takes the
   room just as a narrow window does. */
#nowplaying { container: np / inline-size; }
@container np (max-width: 340px) {
  #nowplaying .np-volume { display: none; }
}
@container np (max-width: 240px) {
  #nowplaying :is(.np-controls .np-qbtn, .np-time) { display: none; }
}

/* ---------------------------------------------------------------------------
   The tour bar (ui/tour.ts)

   The same card as the player next to it - same paper, same hairline, same
   --leaf corner - because on a board that is playing something the two share an
   edge, and two objects on one edge wearing two different corners read as two
   unrelated strips. That is the argument #nowplaying makes about the add bar,
   one object over.

   In the same slot, and lifted clear of the player when there is one. The
   combinator only looks forward, which is why #tour sits *after* #nowplaying in
   index.html while #toolbar sits before it: this rule needs to see the player
   from the tour, and the player's own needs to see the toolbar from the player.
   Both directions are load-bearing and neither is reorderable.

   Not on the idle list below, and that is deliberate rather than an omission. A
   stop is something you look at, and looking at one for fifteen still seconds is
   the normal case rather than the absent one - a bar that faded out while you
   read would take pointer-events with it and leave the tour with no visible way
   on or out. The board's own furniture still goes; this stays, because it is the
   thing the board is currently being read through. */
#tour {
  --tour-gap: 12px;

  position: fixed;
  z-index: var(--z-chrome-over);
  bottom: calc(max(14px, env(safe-area-inset-bottom)) + var(--dock));
  left: max(16px, env(safe-area-inset-left));
  right: max(16px, env(safe-area-inset-right));
  /* A fixed width, so the controls row stays put as names change length. */
  width: min(440px, 100vw - 32px);
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 8px 6px;
  border: var(--hairline) solid var(--rule-2);
  border-radius: var(--leaf);
  background: var(--paper-card);
  box-shadow: var(--shadow-1);
  -webkit-user-select: none;
  user-select: none;
}
#tour[hidden] { display: none; }
/* Above the player rather than over it. The sum is each term named: the bar's
   own height at this padding, plus the gap the player keeps from the edge. */
#nowplaying:not([hidden]) ~ #tour {
  bottom: calc(calc(max(14px, env(safe-area-inset-bottom)) + var(--dock)) + 76px);
}
/* And out of the sidebar's way, on the one side it opens from. The player next
   door loses width here; this one is content-width already, so it moves. */
#sidebar.is-open ~ #tour {
  left: calc(max(var(--panel-inset), env(safe-area-inset-left))
             + min(var(--sidebar-w), 100vw - 28px) + var(--tour-gap));
}
#header-panel.is-open ~ #tour {
  right: calc(max(var(--panel-inset), env(safe-area-inset-right))
              + min(var(--sidebar-w), 100vw - 28px) + var(--tour-gap));
}

/* The entrance, in the player's own idiom: up from under the edge, and back
   under it on the way out. ui/tour.ts unhides a frame before it adds .is-up,
   because an element going straight from display:none to its final state is a
   jump rather than a transition. */
#tour {
  opacity: 0;
  pointer-events: none;
  transform: translateY(calc(100% + 24px)) scale(0.98);
  transition: opacity var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
#tour.is-up {
  opacity: 1;
  pointer-events: auto;
  transform: none;
  transition: opacity var(--dur-base) var(--ease),
              transform var(--dur-base) var(--ease-back);
}
@media (prefers-reduced-motion: reduce) {
  #tour { transition: none; }
  #tour.is-up { transition: none; }
}

/* Two rows. The top one is what this stop is and what is said about it, the
   full width of the bar. The name ellipses rather than widening the bar - the
   same bargain the track name makes in the bar below. */
#tour .tour-where {
  min-width: 0;
  padding: 0 6px 2px;
}
/* The bottom one: back, how far, forward, and out at the far end. Nothing that
   edits the tour - that is the editor's (ui/tour-edit.ts). */
#tour .tour-controls {
  display: flex;
  align-items: center;
  gap: 4px;
}
/* Both sizes here were the scale's own values written as numbers - 13 is
   --t-small and 11 is --t-tiny at the middle tier - so they read as on-scale
   and behaved as off-scale, holding still while every other piece of chrome
   stepped with the axis. Sorted on-scale by the visual audit's item 2. */
#tour .tour-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--t-small);
  color: var(--ink);
}
/* The line somebody wrote for this stop. Between the name and the counter
   because that is the order they are read in - what this is, what is being said
   about it, how far through you are - and in the ink the name is not, so the
   two are told apart at a glance rather than by position alone. Two lines at
   most: a caption is a sentence under a name, and one that ran to five would
   push the counter off a bar this size. */
#tour .tour-caption {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  margin-top: 1px;
  font-size: var(--t-tiny);
  line-height: 1.35;
  color: var(--ink-2);
}
#tour .tour-caption[hidden] { display: none; }
#tour .tour-count {
  flex: none;
  min-width: 4.5em;
  text-align: center;
  font-size: var(--t-tiny);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
/* The three buttons, and there is one rule for all three. Back, forward and out
   are the whole of what this bar does, they sit on one strip within a few pixels
   of each other, and nothing distinguishes them that the glyph does not already
   say - so the close used to carry a divider, a squared-off pair of corners and
   a glyph three pixels smaller than its neighbours', and read as a fourth kind
   of control borrowed from somewhere else. Anything added here goes on
   `#tour button` or it goes on none of them. */
#tour button {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--icon-button);
  height: var(--icon-button);
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: none;
  color: var(--ink-2);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
@media (hover: hover) {
  #tour button:hover:not(:disabled) { background: var(--paper-2); color: var(--accent-deep); }
}
/* :not(:disabled) on the press as well as on the hover. The two at the ends are
   dead by design - see the clamp in stepTour() - and a dead button that darkens
   under a finger is the one saying it will do something. */
#tour button:active:not(:disabled) { background: var(--paper-3); }
/* Greyed at the ends. Clamped rather than wrapped - see stepTour() - so the
   first and last stops each have one dead button, and it has to look dead.

   --ink-3 and nothing else: it is already ink at 38%, and the `opacity: 0.45`
   that used to sit on top of it made these two roughly twice as faint as the
   zoom cluster's dead buttons, which had it right all along. */
#tour button:disabled { color: var(--ink-3); cursor: default; }
#tour button:focus-visible:not(:disabled) { background: var(--paper-2); color: var(--accent-deep); }
#tour button svg { width: var(--icon-glyph); height: var(--icon-glyph); }
/* The one thing the close does not share, and it is space rather than shape:
   stepping and leaving are different errands, so it keeps the far end of the
   strip, where it sat while the stop's actions filled the middle. */
#tour .tour-close { margin-left: auto; }

/* Idle - fifteen seconds of nothing, and the furniture goes.
   ui/idle.ts puts the class on and takes it off; what counts as "something
   happened" is decided there.

   pointer-events with it, and that pairing is the whole safety of this: a
   control faded to nothing that could still be pressed would be a mine, and
   the first tap has to be the one that brings the bar back rather than one
   that lands on an invisible Empty.

   Much slower going than coming, and the gap is the point. Leaving is the app
   getting out of the way: a drift long enough that the eye never catches the
   moment it starts, so the board is simply clear rather than visibly emptied.
   Arriving answers something the user just did, and an answer is late at a
   quarter of a second - the base rules on each element carry that quick half.
   A multiple of --dur-base rather than a fixed time, so the three whimsy tiers
   keep their own tempo instead of all drifting off at the middle one's. */
/* The window's own bar is absent from this list and from the tour's below, and
   the reason is the same for both: it is the frame and not the furniture. No
   window on any desktop empties its caption because nobody moved the mouse, and
   a strip that stayed while the three buttons inside it faded would read as a
   bar that had broken rather than an app being quiet. */
:root.is-idle #menu-btn,
:root.is-idle #mobile-header-edit-btn,
:root.is-idle #mobile-find-btn,
:root.is-idle #drawer-dock,
:root.is-idle #corner,
/* The toolbar goes with the rest, and it is the one entry here worth arguing
   about: everything else on this list reports or controls the view, and this
   one is where things are made. It fades anyway, because a creation surface
   that stayed lit over a board nobody is touching would be the only object on
   screen still talking. If that reads wrong in the hand the answer is a longer
   timer for this one element, not an exemption from fading. */
:root.is-idle #toolbar,
/* The Timeline too, though it is docked: the board takes its band while it is
   gone and is held still as it does (canvas.css, #viewport), so nothing jumps
   either way. Except while Play runs, which is the tour's case -
   a replay is watched without a hand on anything, and the bar is what it is
   watched on. */
:root.is-idle:not(.is-replaying) #timeline-strip,
/* Including the player, and it is worth saying why: something audible with its
   controls faded out sounds like a thing that cannot be stopped. It can - any
   pointer move, key or wheel brings the whole set back inside a quarter of a
   second, which is faster than reaching for it - and exempting it would leave
   one lit panel on an otherwise empty board for as long as a track runs. */
:root.is-idle #nowplaying {
  opacity: 0;
  pointer-events: none;
  transition: opacity calc(var(--dur-base) * 6) var(--ease);
}

/* ---------------------------------------------------------------------------
   Touring - the board being shown rather than worked on. ui/tour.ts puts the
   class on <html> at the first stop and takes it off at the end. And the same
   list again for .is-tour-edit, which ui/tour-edit.ts puts on while the tour is
   being *built*: the mode marks every card on the board and the marks are the
   editor, so a panel over the third of the board they are on is the one thing
   that must not be there. Two classes rather than one, because the two modes
   differ everywhere else - a tour freezes the board (see pressIntent() in
   canvas/input.ts) and building one leaves every gesture alone.

   The idle list above, plus the two panels, and going for a different reason.
   Idle is the app noticing nobody is there and stepping back for a moment;
   this is the app being told to get out of the way for as long as a tour lasts,
   and nothing here comes back on a pointer move - the way back is ending the
   tour. Same pairing of opacity with pointer-events, and for the same safety:
   a faded control that could still be pressed is a mine.

   Quick rather than the idle fade's six beats. A tour starts on a press and the
   first stop is already flying; furniture drifting out over three seconds would
   still be leaving while the camera arrived.

   The two panels are hidden rather than closed, because a panel left open is a
   decision somebody made - it is exactly where they left it when the tour ends.
   #tour itself is not on the list, and neither is #toast: see presentation()
   in ui/tour.ts for both. */
/* .is-cleaning joins the two tour states here rather than repeating the list:
   the cleaning simulator (ui/clean/) hides exactly the same furniture and for
   the same reason. Its own bar is not on the list, so it stays. */
:root:is(.is-touring, .is-tour-edit, .is-cleaning) :is(
  #menu-btn,
  #mobile-header-edit-btn,
  #mobile-find-btn,
  #sidebar,
  #header-panel,
  #drawer-dock,
  #corner,
  #toolbar,
  #timeline-strip,
  #nowplaying
) {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease);
}

/* And the three rules that move the tour bar out of something's way, undone -
   because the thing being got out of the way of is now invisible. Each is the
   base value from #tour's own block restated, at the one extra class of
   specificity it takes to win: an open sidebar under a running tour would
   otherwise hold the bar a sidebar's width off-centre for the whole reading,
   with nothing on screen to explain why. */
:root.is-touring #sidebar.is-open ~ #tour { left: max(16px, env(safe-area-inset-left)); }
:root.is-touring #header-panel.is-open ~ #tour { right: max(16px, env(safe-area-inset-right)); }
:root.is-touring #nowplaying:not([hidden]) ~ #tour {
  bottom: calc(max(14px, env(safe-area-inset-bottom)) + var(--dock));
}

/* ---------------------------------------------------------------------------
   The tour editor's bar (ui/tour-edit.ts)

   The tour bar's twin, in the same slot along the foot and in the same idiom,
   because the two modes are the same shape: the chrome goes, the board fills the
   screen, and one strip says what is being done to it. The tour bar reads a
   tour; this one writes one.

   Built by the module rather than declared in index.html, unlike its twin - see
   initTourEdit() for why. Which means none of the sibling combinators #tour uses
   to dodge the sidebar and the player apply here, and none are needed: this bar
   is only ever up while :root.is-tour-edit is hiding both of them.

   ── One size, always ──

   A **fixed width** and two rows of **fixed height**, and every one of those
   words is load-bearing. This bar is the one surface in the app you drag things
   around inside, and it used to resize constantly while you did it: the width
   came from the content, so a button whose label went from "Select cards, then
   add them" to "Add 3 cards as one stop" moved the whole bar and every chip in
   it, and the caption row appeared out of nothing when a chip was pressed, so
   the bar grew a line taller under the pointer that had just pressed it.

   Nothing here is allowed to move because of something that happened somewhere
   else on it. The width is a number; the rows are heights; the chips scroll
   sideways inside a strip whose left edge never moves; the second row holds the
   caption or the hints, and either way it is the same box at the same height.

   Wider than #tour's 420px: that bar holds a name and a counter, and this one
   holds every stop in the tour. */
#tour-editor {
  position: fixed;
  z-index: var(--z-chrome-over);
  bottom: calc(max(14px, env(safe-area-inset-bottom)) + var(--dock));
  left: max(16px, env(safe-area-inset-left));
  right: max(16px, env(safe-area-inset-right));
  width: min(760px, 100vw - 32px);
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  /* No padding here and no gap: the rows carry their own, so the hairline
     between them runs the whole width of the bar rather than stopping short of
     both ends like an underline under a word. */
  padding: 0;
  overflow: hidden;
  border: var(--hairline) solid var(--rule-2);
  border-radius: var(--leaf);
  background: var(--paper-card);
  box-shadow: var(--shadow-1);
  -webkit-user-select: none;
  user-select: none;
}
#tour-editor[hidden] { display: none; }

/* The entrance, in the tour bar's own idiom: up from under the edge, and back
   under it on the way out. paint() unhides a frame before it adds .is-up. */
#tour-editor {
  opacity: 0;
  pointer-events: none;
  transform: translateY(calc(100% + 24px)) scale(0.98);
  transition: opacity var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
#tour-editor.is-up {
  opacity: 1;
  pointer-events: auto;
  transform: none;
  transition: opacity var(--dur-base) var(--ease),
              transform var(--dur-base) var(--ease-back);
}
@media (prefers-reduced-motion: reduce) {
  #tour-editor { transition: none; }
  #tour-editor.is-up { transition: none; }
}

/* The three rows. Heights and not paddings, so what is in them cannot make them
   taller - see the block at the head of this section. */
#tour-editor .tour-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 0 8px;
}

/* ── The head ── */

/* What this bar is, how much of it there is, and the way out - the Timeline
   strip's head one edge over, and the same three parts in the same order. */
#tour-editor .tour-head-row {
  height: 30px;
  border-bottom: var(--hairline) solid var(--rule);
}
#tour-editor .tour-title {
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--ink-2);
}
#tour-editor .tour-count {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--t-small);
  color: var(--ink-3);
}
/* The cross, at the top right, which is where every closable surface in this app
   puts it: #side-close and #header-close on the two panels, .tl-close on the
   Timeline, #viewer-close on the viewer. It was at the bottom right of this bar,
   on the reasoning that the tour bar's own cross is at the end of its row - true
   of a bar with one row, where the end and the top right are the same place, and
   false the moment there are three.

   .tl-close's measurements, which are now every close's measurements: this
   copied that one faithfully and both were wrong in the same two ways. 24 at
   --radius-sm was a size nobody chose and a corner meant for the seams inside a
   segmented control, and the ink wash was described here as "the hover the rest
   of the app hovers with" when it was the rarest of three. --icon-button,
   --leaf, and the paper step. */
#tour-editor .tour-close {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--icon-button);
  height: var(--icon-button);
  padding: 0;
  border: 0;
  border-radius: var(--leaf);
  background: none;
  color: var(--ink-3);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
#tour-editor .tour-close svg { width: var(--icon-glyph); height: var(--icon-glyph); }
@media (hover: hover) {
  #tour-editor .tour-close:hover { background: var(--paper-2); color: var(--accent-deep); }
}
#tour-editor .tour-close:active { background: var(--paper-3); }
#tour-editor .tour-close:focus-visible { background: var(--paper-2); color: var(--accent-deep); }
/* The strip is the one row that may grow, and it grows *upwards* - the bar is
   pinned to the foot of the window, so a tour long enough to wrap raises the top
   edge and leaves the head, the caption and both verbs exactly where they were. */
#tour-editor .tour-strip-row {
  min-height: 44px;
  padding-block: 7px;
}
#tour-editor .tour-foot-row {
  height: 40px;
  border-top: var(--hairline) solid var(--rule);
}

/* ── The two verbs ── */

/* One pair, and they have to *look* like one pair: the same height, the same
   radius, the same border, differing only in which one is filled. That is
   .btn-row in sidebar.css - the pattern this very feature already uses for its
   two buttons in the Board tab - and it is what makes two controls read as two
   answers to one question rather than as two unrelated things that happen to be
   near each other. They were 30px accent and 24px outline, in different rows,
   and read as exactly that. */
#tour-editor .tour-acts { flex: none; display: flex; gap: 6px; }
#tour-editor .tour-add,
#tour-editor .tour-play {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 26px;
  padding: 0 12px;
  border: var(--hairline) solid var(--rule-2);
  /* --radius-xs: these sit 9px inside the bar's --leaf corner, and a --leaf of
     their own drew a rounder curve than the bar's inside it. */
  border-radius: var(--radius-xs);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-small);
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
#tour-editor .tour-add { min-width: 96px; }

/* The filled one, on .btn-row's own terms - accent ground, accent-deep edge. It
   is Add rather than Take the tour because building a tour is adding stops to it
   over and over, and Take the tour is what you press once at the end. */
#tour-editor .tour-add.is-primary:not(:disabled) {
  background: var(--accent);
  border-color: var(--accent-deep);
  color: var(--accent-fg);
}
/* And the same button pointing the other way: everything selected is already on
   the tour, so the only thing left to offer is taking it off. One button, one
   subject, and which direction it is facing is the label and the colour rather
   than a second control somewhere else. */
#tour-editor .tour-add.is-cut:not(:disabled) {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--paper-card);
}
/* The greying the tour bar's dead buttons wear one strip over, which is now the
   app's single one: --ink-3 and no opacity on top of it. The 0.45 that used to
   be here faded the button's *ground* as well as its ink, so a dead Add cut a
   hole in the row it sits in rather than going quiet in it. */
#tour-editor .tour-add:disabled,
#tour-editor .tour-play:disabled { cursor: default; color: var(--ink-3); }
/* Focus takes the edge and leaves the fill alone, so the three faces this pair
   wears - plain, accent, danger - all show it and none of them lose their
   colour to it. --accent-text rather than --accent-deep because this is an
   *edge* on a filled ground, which is the bound mobile.css argues for. */
#tour-editor .tour-play:not(:disabled):focus-visible,
#tour-editor .tour-add:not(:disabled):focus-visible { border-color: var(--accent-text); }
#tour-editor .tour-play:not(:disabled):focus-visible {
  background: var(--paper-2);
  color: var(--accent-deep);
}
@media (hover: hover) {
  #tour-editor .tour-play:not(:disabled):hover {
    background: var(--paper-2);
    border-color: var(--accent-text);
    color: var(--accent-deep);
  }
  #tour-editor .tour-add.is-primary:not(:disabled):hover { background: var(--accent-deep); }
  /* Toward the ink, the way every other deepened colour in this app is made -
     --accent-deep is the same shape. It was a brightness() filter, which was the
     only one in the whole stylesheet and would have lightened rather than
     deepened the red on a dark palette. */
  #tour-editor .tour-add.is-cut:not(:disabled):hover {
    background: color-mix(in srgb, var(--danger) 86%, var(--ink));
    border-color: color-mix(in srgb, var(--danger) 86%, var(--ink));
  }
}

/* How many cards the press would take. A pill and not part of the sentence, so
   the number can change without the button changing width.

   Outlined in `currentColor` rather than filled with a colour of its own: this
   sits on the accent and on the danger red by turns, and a translucent white
   would have been the one hardcoded colour in the bar and wrong on a light
   palette. The outline is whatever the label is, so it is right on both by
   construction. */
#tour-editor .tour-add-n {
  flex: none;
  display: grid;
  place-items: center;
  min-width: 17px;
  height: 17px;
  padding: 0 4px;
  border-radius: var(--radius-pill);
  box-shadow: inset 0 0 0 var(--hairline) currentColor;
  opacity: 0.8;
  font-family: var(--font-mono);
  font-size: var(--t-tiny);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* ── The foot row ── */

/* Which stop is being captioned. The same square the chip and the card both
   wear, one size down: three drawings of one fact, so the field, the chip and
   the photograph are read as the same thing said three times. */
#tour-editor .tour-cap-wrap {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 6px;
}
#tour-editor .tour-cap-n {
  flex: none;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  border-radius: var(--radius-xs);
  background: var(--accent);
  color: var(--accent-fg);
  font-family: var(--font-mono);
  font-size: var(--t-tiny);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* What is said at this stop. Underlined only when it is being used: a boxed
   field would be the loudest thing on a bar whose whole job is to stay out of
   the way of the board behind it.

   user-select back on, against the bar's own `none`: this is the one thing here
   somebody types in, and a text field you cannot select the text in is a text
   field with half its editing gone. */
#tour-editor .tour-cap {
  flex: 1 1 auto;
  min-width: 0;
  padding: 2px 0;
  border: 0;
  border-bottom: var(--hairline) solid transparent;
  background: none;
  font: inherit;
  font-size: var(--t-small);
  color: var(--ink);
  -webkit-user-select: text;
  user-select: text;
}
#tour-editor .tour-cap::placeholder { color: var(--ink-3); }
@media (hover: hover) {
  #tour-editor .tour-cap:hover { border-bottom-color: var(--rule); }
}
#tour-editor .tour-cap:focus { outline: none; border-bottom-color: var(--accent-text); }

/* What can be done here, in the caption's place while there is no stop being
   captioned. It is the only written record of the three gestures this mode
   hides - the drag, the right-click and the mark on a card - and it is in the
   quietest ink on the bar, because a hint that competes with the controls it is
   about has stopped being a hint. */
#tour-editor .tour-hint {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--t-tiny);
  color: var(--ink-3);
}

/* ── The strip ── */

/* The order. It **wraps**, and that is the whole of it: a strip that scrolled
   sideways hid half the tour behind a gesture, and the one thing this row exists
   for is seeing the order at a glance. A wrapped block shows every stop at once
   and pays for it in height, which is the right way round on a bar that is
   pinned to the bottom of an otherwise empty screen.

   Bottom-up, so the last line of chips is the one that stays put and the block
   grows away from the controls rather than through them.

   The cap is five lines. Past that there is no honest answer left - a tour of
   thirty stops cannot be one glance - and a scroll down a block you can read is
   a far smaller loss than a scroll along a line you cannot. */
#tour-editor .tour-chips {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-content: flex-end;
  gap: 4px;
  max-height: 166px;
  margin: 0;
  /* Room for the stacked square a grouped chip draws above and to the right of
     itself, which the overflow below would otherwise cut off on the top line. */
  padding: 3px 3px 0 0;
  overflow: hidden auto;
  /* The scrollbar is base.css's - see the note in sidebar.css. */
  list-style: none;
}
/* The corner is the chip's, because the only thing this element draws is the
   drop edge below - a wrapper rounded differently from the thing inside it
   would put a second curve a pixel outside the first. */
#tour-editor .tour-chips > li { flex: none; border-radius: var(--leaf); }
/* Where a dragged chip would land. On the side of the neighbour rather than in
   the gap: there is four pixels of gap to draw in, and an edge is what the eye
   reads as "between these two" anyway. */
#tour-editor .tour-chips > li.drop-before {
  box-shadow: inset var(--sel-line) 0 0 var(--select);
}
#tour-editor .tour-chips > li.drop-after {
  box-shadow: inset calc(var(--sel-line) * -1) 0 0 var(--select);
}

/* One stop: a square with the stop's *name* on it, and nothing else.

   The name is the order the stops were added in, not where they are lying - the
   argument is written out at the head of ui/tour-edit.ts and it is the whole
   reason this row can show a reorder having happened. Drawn in the same shape
   and face as the mark a card wears in its own corner, because they are the two
   halves of one editor, and deliberately not in the same colour: the card's is
   accent because it answers "when is this seen", and this one is quiet paper
   until it is the stop being pointed at.

   Chips carrying the cards' names were tried in between and cost the row the
   thing it is for. A named chip is four times as wide, so a dozen stops became a
   block five lines deep, and seeing the whole order at a glance - the one job
   this row has that the board cannot do - was what paid for it. What a square
   cannot say, *which* photograph a stop is, the board says instead and says it
   better: hovering this bar lays every number across the card it belongs to
   (:root.is-tour-peek in item-chrome.css). */
/* Sized and cornered as an icon button, which is what it is: a small square with
   one mark in it. It was 30px at --radius-xs and the timeline's mark was 26px at
   50%, two strips of numbered squares that agreed about nothing - design-audit
   D3. Both are on --icon-button and --leaf now, so the family is one shape at one
   corner and the only difference left is what is inside: a number here, a glyph
   there. */
#tour-editor .tour-chip {
  position: relative;
  display: grid;
  place-items: center;
  width: var(--icon-button);
  height: var(--icon-button);
  padding: 0;
  border: 0;
  border-radius: var(--leaf);
  background: var(--paper-2);
  color: var(--ink-2);
  font-family: var(--font-mono);
  font-size: var(--t-tiny);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  cursor: grab;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
#tour-editor .tour-chip:active { cursor: grabbing; }
/* The stop the foot row is pointed at, in the accent the marks on the cards
   wear - so the chip being pointed at and the cards it is about are one colour. */
#tour-editor .tour-chip.is-current { background: var(--accent); color: var(--accent-fg); }
/* --paper-2, not --paper-3: the hover is one step into the paper and the press
   is two, and this chip is dragged - a grab that started at the press colour has
   nowhere left to go. */
@media (hover: hover) {
  #tour-editor .tour-chip:hover:not(.is-current) { background: var(--paper-2); color: var(--ink); }
}
#tour-editor .tour-chip:active:not(.is-current) { background: var(--paper-3); }
#tour-editor .tour-chip:focus-visible:not(.is-current) { background: var(--paper-2); color: var(--ink); }

/* Several cards framed together, drawn as a second square behind the first.
   A count *inside* a square that already holds a number is two numbers in
   twenty pixels; a stack says "more than one" without asking anybody to read
   anything, and the tooltip carries the figure for when it matters.

   An offset shadow rather than a pseudo-element: it is the chip's own shape,
   automatically, so it stays right if the chip ever changes size or radius -
   and it cannot be caught by the z-order trouble a negative z-index behind a
   button invites. */
#tour-editor .tour-chip.is-group { box-shadow: 3px -3px 0 0 var(--paper-3); }
#tour-editor .tour-chip.is-group.is-current,
#tour-editor .tour-chip.is-group:focus-visible { box-shadow: 3px -3px 0 0 var(--rule-2); }
@media (hover: hover) {
  #tour-editor .tour-chip.is-group:hover { box-shadow: 3px -3px 0 0 var(--rule-2); }
}


/* The scale bar. A label over a stick, both right-aligned, so the stick's right
   end lines up with the edge of the zoom control beneath it - the eye then has
   a straight edge to measure from rather than a floating rule.

   No card, no background: it is a measurement laid on the board, not another
   control sitting on top of it. That also keeps the corner from becoming three
   stacked panels. */
#scale-bar {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  padding: 0 2px 2px;
  -webkit-user-select: none;
  user-select: none;
  pointer-events: none;
}
#scale-bar[hidden] { display: none; }
#scale-bar-label {
  font-family: var(--font-mono);
  font-size: var(--t-tiny);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
  /* Enough of the paper behind it to stay legible over a photograph, without
     drawing a box: the label sits in a wash, the stick does not need one. */
  padding: 0 3px;
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--paper-card) 78%, transparent);
}
/* Serifed at both ends, the way a scale bar on a drawing is: the two tick marks
   are what make it a measurement between two points rather than a line of
   unclear extent. Borders rather than pseudo-elements so the whole thing is one
   element whose width is the only thing the module writes. */
#scale-bar-stick {
  height: 7px;
  border: solid var(--ink-2);
  border-width: 0 var(--hairline);
  position: relative;
}
#scale-bar-stick::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--hairline);
  background: var(--ink-2);
}

#hud {
  display: flex;
  align-items: stretch;
  border: var(--hairline) solid var(--rule-2);
  border-radius: var(--leaf);
  overflow: hidden;
  background: var(--paper-card);
  box-shadow: var(--shadow-1);
  font-family: var(--font-mono);
  font-size: var(--t-tiny);
  color: var(--ink-2);
  pointer-events: none;
}
/* Nowrap because each slot now carries both readings - board units and the real
   measurement - and a pair broken across two lines reads as four numbers. The
   strip grows sideways instead; it is hidden outright on a narrow screen. */
#hud span { padding: 6px 11px; white-space: nowrap; }
#hud span + span { border-left: 1px solid var(--rule); }
#hud-xy { color: var(--accent-deep); }
#hud[hidden] { display: none; }

/* Without color-mix(), for WebKit below 16.2 - section 7 of tokens.css carries
   the argument and declares the channel-form tokens. */
@supports not (color: color-mix(in srgb, red, blue)) {
  #toolbar button[aria-pressed="true"] { background: rgb(var(--accent-c) / 12%); }
  #nowplaying .np-line-base { stroke: rgb(var(--ink-c) / 22%); }
  #scale-bar-label { background: rgb(var(--paper-card-c) / 78%); }
}
