/* Concert web app: shared stylesheet for the audience and performer pages. */

/* ----------------------------------------------------------------- Fonts */

/* All faces are under the SIL Open Font License, latin subsets. */
@font-face {
  font-family: "Barlow Condensed";
  font-weight: 600;
  font-display: swap;
  src: url("fonts/barlow-condensed-600-latin.woff2") format("woff2");
}
@font-face {
  font-family: "Barlow Condensed";
  font-weight: 700;
  font-display: swap;
  src: url("fonts/barlow-condensed-700-latin.woff2") format("woff2");
}
@font-face {
  font-family: "Barlow Condensed";
  font-weight: 800;
  font-display: swap;
  src: url("fonts/barlow-condensed-800-latin.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ibm-plex-mono-400-latin.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-weight: 700;
  font-display: swap;
  src: url("fonts/ibm-plex-mono-700-latin.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Sans";
  font-weight: 100 700; /* variable font */
  font-display: swap;
  src: url("fonts/ibm-plex-sans-latin.woff2") format("woff2");
}

/* ---------------------------------------------------------------- Tokens */

:root {
  --backdrop:  #dcdcd6; /* behind the page */
  --page-background:      #217b6e; /* the page */
  --messages-background: #185a50; /* the background of the messages section */
  --ink:       #f3e9c9; /* text and borders */
  /* Derived from the above, so that recolouring the backgrounds recolours these. */
  --ink-faded:     color-mix(in oklab, var(--ink) 55%, var(--page-background));  /* secondary text */
  --divider:       color-mix(in oklab, var(--ink) 30%, var(--page-background));  /* faint separators on the base */
  --divider-messages: color-mix(in oklab, var(--ink) 30%, var(--messages-background)); /* faint separators on the messages background */
  --tint:          color-mix(in oklab, var(--ink) 12%, transparent);  /* a faint background behind a name */
  --attention: #f2c94c; /* action needed: counts, unread markers, the audience member's own request */
  --crest:     #ffe600; /* the top of the wave that crosses a request the concert has taken */
  --attention-faded: color-mix(in oklab, var(--attention) 55%, var(--page-background)); /* the audience member's request, played */
  --shade: rgb(0 0 0 / .6); /* the page, dimmed behind a dialog */

  /* Deliberately separate variables: it makes sense to change them independently. */
  --status:   #c4311c; /* the concert status (ink of a rubber stamp) */
  --playing:  #c4311c; /* the song being played (red recording button) */
  /* The mark on the song being played breathes: a light that is on, rather
     than a spot of ink. One turn of it, and how far down it goes, which is
     far enough to be seen and not so far that the mark reads as going out. */
  --breath:     2.4s;
  --breath-low: .55;

  /* Line weights, thinnest first. */
  --line-thin:  1px;
  --line:       2px;
  --line-bold:  3px;
  --line-heavy: 4px;

  /* 24px, the least WCAG 2.2 AA asks of anything pressed. In rem: a
     fingertip does not shrink with the page. */
  --tap-height: 1.5rem;

  /* On its own, for the unit set inside the minutes field. */
  --box-padding-inline: .8em;

  /* The unit the page is drawn in: display type, spacing and corners are
     multiples of it, so a smaller screen gets the same page smaller rather
     than cut off. Messages and what is typed into fields are in rem
     instead, so that they stay readable however small the page. */
  --unit: clamp(8px, min(100vw / 32, 100dvh / 52), 1rem);

  --page-radius:      calc(1.5 * var(--unit)); /* corner radius of the page's border */
  --page-margin:      calc(.9 * var(--unit));  /* between the page's edge and its border */
  --page-inline-size: min(100vw, 36rem);
  --page-block-size:  min(100dvh, 76rem);
  --section-padding:  calc(.9 * var(--unit));

  /* ------------------------------------------------------------ Spacing */

  /* Every gap, padding and margin between things on the page is one of
     these. Only padding inside a chip or a box is in em: it belongs to the
     type it wraps. */
  --space-hair:  calc(.125 * var(--unit)); /* a line's clearance */
  --space-tight: calc(.3 * var(--unit));   /* within a row */
  --space:       calc(.55 * var(--unit));  /* between rows and lines */
  --space-wide:  calc(.8 * var(--unit));   /* between blocks */
  --space-loose: calc(1.3 * var(--unit));  /* between the parts of a form */
  /* The side padding of list rows, feed items and their headings. */
  --row-padding-inline: calc(var(--section-padding) + var(--space-tight));
  /* Inside a chip: a name, a count, a recipient. */
  --chip-padding: .1em .4em;
  /* Inside a boxed element: a field, an action, the request status. */
  --box-padding: .45em var(--box-padding-inline);

  --font-display: "Barlow Condensed", Impact, sans-serif;
  --font-mono:    "IBM Plex Mono", ui-monospace, Consolas, monospace;
  --font-text:    "IBM Plex Sans", system-ui, sans-serif;

  /* The perforations along the page's sides. */
  --hole-radius:  calc(.44 * var(--unit));
  --hole-spacing: calc(1.38 * var(--unit));

  /* The height of a bar and of a row, which .list-bar and .song-list li
     keep to, so that a section can state its minimum height in rows and
     bars and a squeezed page drops whole rows rather than cutting one. */
  --bar-height: calc(1rem + var(--unit));
  --row-padding-block:    var(--space-tight);
  --row-height:     calc(var(--title-size) * var(--title-line-height) + 2 * var(--row-padding-block) + var(--line-thin));
  /* Between the rule a message is written beside and what is written. */
  --rule-gap: var(--space);
  --title-line-height: 1.2;

  /* The three sizes of type that is read. A label follows the page,
     capped so that it never outgrows a message. A message's size is
     fixed: thirty characters on the narrowest line the page is drawn
     for. TODO: a field under 16px makes iOS zoom the page on focus, and
     nothing in CSS sets a field's size apart from its text. */
  --title-size: calc(1.35 * var(--unit));
  --label-size: min(.75rem, calc(1.1 * var(--unit)));
  --message-text-size:  .85rem;
  /* Fine print a step smaller than the labels: what stands beside a title
     or a message and must not compete with it. */
  --small-label-size: calc(.8 * var(--label-size));
}

/* -------------------------------------------------------------- The page */

[hidden] { display: none !important; }

body {
  margin: 0;
  min-height: 100dvh;
  display: grid;
  place-items: center;
  background: var(--backdrop);
  color: var(--ink);
  font-family: var(--font-text);
  font-size: 1rem;

  /* Attempt to make the scrollbar thin and to use the same colour scheme. */
  scrollbar-width: thin;
  scrollbar-color: var(--ink-faded) transparent;
}

.page {
  box-sizing: border-box;
  inline-size: var(--page-inline-size);
  block-size: var(--page-block-size);
  padding: var(--page-margin);
  background: var(--page-background);
  /* Perforations down both sides: a hole punched every --hole-spacing. Each
     layer cuts one side; intersecting them keeps both. */
  mask:
    radial-gradient(circle at   0 50%, transparent var(--hole-radius), #000 calc(var(--hole-radius) + .5px)) left  / 100% var(--hole-spacing) repeat-y,
    radial-gradient(circle at 100% 50%, transparent var(--hole-radius), #000 calc(var(--hole-radius) + .5px)) right / 100% var(--hole-spacing) repeat-y;
  mask-composite: intersect;
}

.page-inner {
  box-sizing: border-box;
  position: relative; /* a request on its way out travels within the page */
  block-size: 100%;
  border: var(--line-heavy) solid var(--ink);
  border-radius: var(--page-radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* ---------------------------------------------------------- The sections */

/* The page is as tall as the screen and never scrolls: what does not
   fit scrolls inside its section. The header and the controls are never
   squeezed. Of the rest, the messages hold a share of the page and the
   setlist or the requests shrink first, down to their minimum height;
   only then do the messages shrink to theirs. */
.page-inner > * { flex: none; }
/* A column, so that the list inside takes the room left over. */
.setlist-section,
.requests-section,
.messages-section {
  display: flex;
  flex-direction: column;
}
.setlist-section {
  /* Glanced at while the messages are read: never more than a share of
     the page. */
  flex: 0 1 auto;
  min-block-size: var(--row-height);
  max-block-size: 30%;
}
.requests-section {
  /* The shrink factor is what makes it give way long before the messages
     do: shrinking is shared out in proportion to it. */
  flex: 0 1000 auto;
  /* Ensure we show at least 3 rows always. */
  --rows: 3;
  min-block-size: calc(var(--bar-height) + var(--tap-height) + var(--rows) * var(--row-height));
}
.requests-section:has(.open) { --rows: 5; }
.messages-section {
  /* The minimum height is what the tabs, a line of feed and the message
     box need. */
  flex: 1 1 45%;
  min-block-size: min-content;
}

/* ------------------------------------------------------------ Type styles */

/* The display face: uppercase headlines. */
.title-block h1,
.performers,
.song-list .title,
.request-form .title,
.setlist .title,
.setlist .next::after,
.action {
  font-family: var(--font-display);
  text-transform: uppercase;
}

/* The fine print: small, spaced-out mono capitals. */
.set,
.clock,
.header-aside .request-status,
.more-requests,
.tabs,
.requesters,
.send,
.feed .author,
.feed time,
.feed .activity,
.feed .request-entry,
.back,
.age,
.song-list .number,
.song-list .count,
.song-state,
.elapsed,
.order,
.catalogue-toggle,
.how-long,
.section-title,
.thread-name,
.request-form .caption,
.request-form .request-status,
.your-requests .hint,
.pending-requests .retract,
.written .opening,
.recipients {
  font-family: var(--font-mono);
  font-size: var(--label-size);
  letter-spacing: .12em;
  text-transform: uppercase;
}

/* Fine print that annotates the text beside it. */
.feed .about,
.people .sender,
.entry-note {
  color: var(--ink-faded);
  font-family: var(--font-mono);
  font-size: var(--small-label-size);
  letter-spacing: .06em;
  text-transform: uppercase;
}

/* Bars stand back from the rows they head. */
.list-bar > .section-title,
.order,
.tabs { font-size: var(--small-label-size); }

/* On the feed itself, so that its line height is a message's. */
.feed { font-size: var(--message-text-size); }

/* ----------------------------------------------------------------- Header */

.page-header {
  /* Smaller fine print for the header, as it is largely decorative. */
  --label-size: calc(.8 * var(--unit));
  font-size: calc(1.1 * var(--unit));
  border-bottom: var(--line-bold) solid var(--ink);
  padding: var(--space-wide) var(--section-padding) var(--space-wide) var(--space-tight);
  display: grid;
  /* The ticket number, the title, and the side column when the page has one.
     Implicit, so that a page without a side column does not keep a gap for
     the column it hasn't got. */
  grid-auto-flow: column;
  grid-template-columns: auto 1fr;
  /* The ticket number's rule stands in the middle of its own gutter; the side
     column keeps the wider gap, which it asks for itself. */
  column-gap: var(--space-tight);
}

.clock,
.elapsed,
.reading { text-transform: none; } /* "15m", not "15M" */

/* Fine print turned on its side down the header's edge, in the mono face but
   neither spaced out nor capitalised: it is a number read as a number. A size
   container along its inline axis, which sideways-lr makes vertical, so that
   the number can be set from the height the header has. */
.ticket-number {
  writing-mode: sideways-lr;
  container-type: inline-size;
  text-align: center;
  font-family: var(--font-mono);
  padding-right: var(--space-tight);
  border-right: var(--line-thin) solid var(--ink-faded);
  color: var(--ink-faded);
}
/* The fine print size, dropping below it only when the header is too short to
   take the number down its side: nine characters of mono at .6em each come to
   5.4em, and the rest of the divisor is the room kept at either end. A block,
   so that the strip is no thicker than the number: an inline would be laid out
   on a line of the size it inherits, which is the one it shrinks from. */
.ticket-number .figures {
  display: block;
  font-size: min(var(--label-size), 100cqi / 5.8);
  /* Trimmed to the digits themselves: the ascent and descent the font asks
     for either side of them are half again their height, and sideways they
     read as a gap between the number and its rule. */
  text-box: trim-both cap alphabetic;
}

/* A size container, so that the title is set from the width it is given. */
.title-block {
  container-type: inline-size;
  display: grid;
  align-content: start;
  gap: var(--space-tight);
}

.title-block h1 {
  margin: 0;
  font-weight: 800;
  line-height: .9;
  text-wrap: balance;
  overflow-wrap: anywhere;

  /* A short title takes two lines, a longer one widens up to --max-width
     to keep to two, a longer one still takes three, and beyond that the
     font shrinks. --title-chars is set by the server; --em-per-char is
     the display face's average character width with room for word breaks,
     to re-measure if the font changes. Set from the block's own width
     (100cqi), so the title is never more than three lines at --max-size. */
  --em-per-char: .48;
  --max-width:   calc(21 * var(--unit));
  --max-size:    1.55em;
  --title-length:      calc(var(--title-chars, 20) * var(--em-per-char)); /* in em */
  /* Room past the title's end for the concert status to overhang, so it
     never strikes what stands beside the title. */
  --stamp-room:  calc(2 * var(--unit));
  max-inline-size: min(calc(100% - var(--stamp-room)), clamp(calc(16.5 * var(--unit)), calc(var(--title-length) * 1em / 2), var(--max-width)));
  font-size:       min(var(--max-size), calc(3 * 100cqi / var(--title-length)));
}

/* Without a side column (the audience page), the title may use the width,
   and grows a size to fill it. */
.page-header:not(:has(.header-aside)) h1 { --max-width: calc(28 * var(--unit)); --max-size: 1.8em; }
/* The set label, with the clock beside it when there is no side column. */
.set-line { display: flex; justify-content: space-between; }

/* The performers, in the display face one size down, each with their
   instrument. A performer is never broken; wrapping falls between them. */
.performers {
  margin: var(--space-tight) 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  column-gap: 1em; /* the gap alone separates: a separator would start a wrapped line */
  font-weight: 700;
  font-size: calc(.85 * var(--unit));
  letter-spacing: .02em;
  white-space: nowrap;
}
.performers .instrument {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: .6em;
  letter-spacing: normal;
  text-transform: lowercase;
  color: var(--ink-faded);
  margin-inline-start: .3em;
  vertical-align: .15em;
}

/* Position of the stamp: over the end of the title's last line, pulled
   back across the last word by --stamp-overlap and hanging past the line's end
   into --stamp-room. Negative margins, so it takes no room on the line
   and never makes the title wrap. */
.title-block .concert-status {
  --stamp-overlap: .8em;
  margin-inline-start: calc(-1 * var(--stamp-overlap));
  margin-inline-end: calc(-1 * var(--stamp-room));
  vertical-align: .25em;
}

/* Clock and request status, spread evenly down the side. */
.header-aside {
  display: grid;
  align-content: space-between;
  justify-items: end;
  gap: var(--space);
  margin-inline: var(--space-wide) var(--space);
}
.header-aside .request-status { border-width: var(--line-bold); }

/* The concert status, e.g. "LIVE"/"ON BREAK", as a rubber-stamp impression.
   Hidden when the concert is not active. */
.concert-status:empty { display: none; }
.concert-status {
  display: inline-block;
  padding: .05em .33em;
  border: var(--line-bold) solid var(--status);
  border-radius: .25em;
  color: var(--status);
  font-family: var(--font-mono);
  /* Stamped across the title, so it is set from the title: shrink the one
     and the other follows, and the impression stays where it was struck. */
  font-size: .7em;
  font-weight: 800;
  letter-spacing: .15em;
  text-transform: uppercase;
  white-space: nowrap;
  rotate: -14deg;
  /* Worn ink: a noise texture masks out specks. The filter thresholds
     fractal noise into a mostly-opaque speckled tile. */
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='60'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.91' numOctaves='1' seed='2'/%3E%3CfeColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 -2.9 2.2 '/%3E%3C/filter%3E%3Crect width='120' height='60' filter='url(%23g)'/%3E%3C/svg%3E");
  mask-size: 120px 60px;
}

/* The boxed elements: the request status, fields, actions. */
.header-aside .request-status,
.field,
.action {
  display: inline-block;
  padding: var(--box-padding);
  border: var(--line) solid var(--ink);
  border-radius: .5rem;
  background: none;
  color: inherit;
  text-align: center;
}

/* Words that can be pressed but are not in a box: given a tap's height,
   with the word centered in it. */
.back,
.send,
.more-requests {
  display: flex;
  align-items: center;
  min-block-size: var(--tap-height);
}

/* ------------------------------------------------------------- Song lists */

/* Sections whose heading is a watermark: large, faint, behind the rows,
   taking no height. Still a heading in the markup, for screen readers. */
:is(.setlist-section, .messages-section, .controls > .staging-area) {
  position: relative;
  isolation: isolate; /* the watermark goes behind the rows, and no further back */
  overflow: clip;     /* a word longer than its region bleeds off its ends, not into the next */
}
:is(.setlist-section, .messages-section, .controls > .staging-area) > .section-title {
  position: absolute;
  z-index: -1;
  font-family: var(--font-display);
  font-weight: 800;
  line-height: 1;
  color: rgb(from var(--ink) r g b / .1);
  pointer-events: none;
}

/* The watermark of the setlist and the messages runs down the section's
   right edge, bottom to top, as these sections are tall. Physical
   offsets: sideways-lr turns the inline axis vertical, so logical ones
   would name the wrong edges. TODO: a setlist shrunk to a row or two
   clips its word; it could run across the top then, as a staging area's
   does. */
:is(.setlist-section, .messages-section) > .section-title {
  top: 0; bottom: 0; right: 0; left: auto;
  padding: 0 var(--space-hair) 0 0;
  writing-mode: sideways-lr;
  /* justify-content, not align-items: with sideways-lr the main axis runs
     down the page. */
  display: flex;
  justify-content: center;
  font-size: calc(2.7 * var(--unit));
  letter-spacing: .06em;
}

/* The watermark of a staging area runs across its top, as an area is not
   tall. It starts where the note beside the rule does, so the word and the
   rule do not cross. */
.controls > .staging-area > .section-title {
  inset-block-start: calc(-1 * var(--space-hair));
  inset-inline-start: calc(var(--rule-gap) + var(--line-thin));
  padding: 0;
  font-size: calc(3.4 * var(--unit));
  letter-spacing: .03em;
}

/* The heading of a section or a pane. */
.section-title {
  margin: 0;
  padding: var(--space) var(--row-padding-inline) var(--space-tight);
  font-weight: 700;
  letter-spacing: .18em;
}
/* A heading, or the back link, with a control beside it. Everything on a
   bar is one size, so centering the boxes aligns the words; the heading
   keeps no padding of its own, the bar's minimum height being the room. */
.list-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  min-block-size: var(--bar-height);
  padding-inline-end: var(--row-padding-inline);
}
.list-bar > .section-title { padding-block: 0; }

/* One song per row: the title, never broken, then whatever the list shows
   about it (requesters and age, a count, a song state). */
.song-list {
  margin: 0;
  padding: 0;
  list-style: none;
  --row-gap: var(--space-loose);
  /* Four monospace characters, e.g. "1h05". In ch, so that it follows the
     font. */
  --age-width: 4ch;
}
.song-list li {
  container-type: inline-size; /* so that a name can be capped to the row's width */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-hair) var(--row-gap);
  padding: var(--row-padding-block) var(--row-padding-inline);
  border-top: var(--line-thin) solid var(--divider);
}
.song-list .title {
  flex: none;
  max-inline-size: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis; /* only a title wider than the row */
  font-size: var(--title-size);
  line-height: var(--title-line-height);
  font-weight: 700;
}
.song-list .played  .title { color: var(--ink-faded); }
/* A song this audience member requested, wherever it is listed; faded once played. */
.song-list .mine .title        { color: var(--attention); }
.song-list .played.mine .title { color: var(--attention-faded); }
/* Numbered rows, e.g. the setlist. */
.song-list .number { flex: none; color: var(--ink-faded); }
/* How many people requested the song. */
.song-list .count {
  margin-inline-start: auto;
  padding: var(--chip-padding);
  border-radius: .25em;
  background: var(--tint);
}
/* A remark on the song's state, e.g. "played". */
.song-state { margin-inline-start: auto; color: var(--ink-faded); }

/* --------------------------------------------------------------- Requests */

.requests-section {
  min-inline-size: 0; /* never widen the page for a long title: it is cut instead */
}
/* The requests never scroll: the list clips what it has no room for.
   The server sends the rows worth showing (requestsShownInSection in
   Concert.View); static/rows.js hides the last row if it is cut, and
   fills in the count below. */
.requests-section > .list-bar { block-size: var(--bar-height); }
.requests-section .song-list {
  flex: 1 1 auto;
  min-block-size: calc(var(--rows) * var(--row-height));
  overflow: clip;
}
/* The link to the pane listing every request, as tall as a tap. */
.more-requests {
  box-sizing: border-box;
  block-size: var(--tap-height);
  justify-content: center;
  cursor: pointer;
  padding: 0 var(--row-padding-inline);
  border-top: var(--line-thin) solid var(--divider);
  font-size: var(--small-label-size);
}

/* The requesters and the request's age, kept as one unit, beside the
   title when the widest name fits, on a line of its own below it
   otherwise. */
.requested-by {
  flex: 1 1 0; /* zero basis: stays on the title's line whenever its widest name fits */
  display: flex;
  align-items: center;
  gap: var(--row-gap);
}
/* Both the requesters and the request's age are in the fine-print rule,
   which sets their size directly, so the step down goes on each rather
   than on the block they share. */
.requested-by .requesters,
.requested-by .age { font-size: var(--small-label-size); }
/* One chip per name. A name never breaks; wrapping falls between names.
   No letter-spacing: these are people's names, not the page's own words. */
.requesters {
  flex: 1;
  letter-spacing: normal;
  display: flex;
  flex-wrap: wrap;
  justify-content: end;
  gap: var(--space-hair) var(--space-tight);
}
.requesters span {
  white-space: nowrap;
  padding: var(--chip-padding);
  border-radius: .25em;
  background: var(--tint);
  /* Two names and the count of the omitted names fit on one line: no name
     takes more than a share of the row. A longer name is cut. */
  max-inline-size: calc((100cqi - var(--age-width)) / 2.5);
  overflow: hidden;
  text-overflow: ellipsis;
}
.requesters .unread           { color: var(--attention); }
.requesters .more-requesters  { color: var(--ink-faded); background: none; cursor: pointer; }

/* The requesters a row has folded away, and the count they are folded
   behind, which opens them. */
.requesters .folded-requester,
.more-requesters .unfolded { display: none; }
.open .requesters .folded-requester { display: inline; }
.open .more-requesters .folded      { display: none; }
.open .more-requesters .unfolded    { display: inline; }
.open .requested-by { flex-basis: 100%; }
.open .requesters {
  justify-content: start;
  row-gap: var(--space-tight);
}
.open .requesters span { max-inline-size: 100%; }
.age {
  flex: none;
  inline-size: var(--age-width);
  text-align: end;
  text-transform: none; /* "17m", not "17M" */
  color: var(--ink-faded);
}

/* --------------------------------------------------------- Tabs and feeds */

.tabs {
  background: var(--messages-background);
  display: grid;
  grid-auto-columns: 1fr;
  grid-auto-flow: column;
  border-bottom: var(--line-thin) solid var(--divider-messages);
}
.tabs input { display: none; }
.tabs label {
  padding: var(--space) 0 var(--space-hair);
  text-align: center;
  cursor: pointer;
}
.tabs .count { color: var(--attention); margin-inline-start: .3em; font-weight: 700; }
.tabs .count:empty { display: none; }
/* The selected tab is underlined, just above the line under the tabs. */
.tabs label::after {
  content: "";
  display: block;
  inline-size: 60%;
  block-size: var(--line);
  margin: var(--space-tight) auto 0;
}
.tabs input:checked + label        { font-weight: 700; }
.tabs input:checked + label::after { background: var(--ink); }

/* The messages section. */
.messages-section {
  border-top: var(--line) solid var(--ink);
  background: var(--messages-background);
}
/* min-content, so that the section's minimum height counts what the panel
   needs. */
.tab-panels { flex: 1; min-block-size: min-content; display: flex; flex-direction: column; }
.tab-panels > *,
.thread { display: none; }
/* The panel showing is a column, so its message box sits at the foot
   however short the feed. Tabs are radios: switching needs no server.
   A tab's radio is named after the region it shows. */
.page-inner:has(#tab-public-feed:checked) .region-public-feed,
.page-inner:has(#tab-staff-feed:checked)  .region-staff-feed,
.page-inner:has(#tab-own-thread:checked)  .region-own-thread,
.page-inner:has(#tab-people-list:checked):has(.region-private-threads.thread-open) .region-private-threads,
.region-private-threads.thread-open .thread {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-block-size: min-content;
}
/* The people list is a feed and its own panel at once, so it scrolls as a
   panel's feed does rather than holding one. */
.page-inner:has(#tab-people-list:checked):not(:has(.region-private-threads.thread-open)) .region-people-list {
  display: block;
}
/* The feed scrolls. Its minimum height is a line of a message with a
   message's padding. */
.tab-panels .feed {
  flex: 1 1 0;
  min-block-size: calc(1lh + var(--unit));
  overflow-y: auto;
}
/* The thread's bar: the back link, and the audience member's name centered. */
.thread .list-bar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  padding-inline: var(--row-padding-inline);
}
.thread-name { margin: 0; padding-block: var(--space); font-weight: 700; }

/* ------------------------------------------------------------------ Feeds */

/* A feed: messages, with what happened on stage between them. */
.feed {
  margin: 0;
  padding: 0;
  list-style: none;
}
/* A message: who, what, when. */
.feed .message,
.people .row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: var(--space);
  padding: var(--space) var(--row-padding-inline);
}
/* An unread item is marked by colouring its author's name. */
.feed .author {
  padding: var(--chip-padding);
  border-radius: .25em;
  background: var(--tint);
  transition: color .3s ease;
}
.feed .unread .author { color: var(--attention); }
.feed .staff .author { background: none; border: var(--line-thin) solid var(--ink-faded); }
/* Not in the people list: a row there is headed by the audience member,
   who is never the viewer, while the message it quotes may be. */
.feed:not(.people) .self .author { background: var(--ink); border-color: var(--ink); color: var(--page-background); }
.feed time { color: var(--ink-faded); text-transform: none; }
/* A line between messages, with less room than a message. */
.feed .activity,
.feed .request-entry { padding: var(--space-hair) var(--row-padding-inline); text-align: center; }
/* What a message did, printed above what it said. */
.feed .about { display: block; }
.feed .activity { color: var(--ink-faded); }

/* The people list: a row per audience member with their latest message, opening
   their thread. */
.people .message { display: block; padding: 0; }
.people .row { cursor: pointer; }
.people .body {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The reading face is variable, so the weight is a distance to travel and
   not a step. */
.people .said { transition: font-weight .3s ease; }
.people .unread .said { font-weight: 600; }
/* Every staff member gets the same row, so it names the sender both as
   "you" and by name; the client shows the right one. */
.people li:not(.self) .you,
.people li.self .named { display: none; }

/* ------------------------------------------------------------------ Panes */

/* A pane replaces everything between the header and the controls while its
   radio is checked; the radio no pane holds, marked "closed", closes them
   all. Like the tabs, this works without the server.

   Each pane holds the radio that opens it, which is what lets one rule show
   whichever pane is open: a pane is not named here, so there is no name for
   this to disagree with. The radios are inside the panes but outside their
   contents, which are the regions. */
.pane-selector,
.order-selector,
.fold-selector { display: none; }
.pane { display: none; flex-direction: column; min-block-size: 0; }
.page-inner:has(.pane-selector:checked:not(.closed)) > :not(.page-header, .controls, .pane, .pane-selector, .order-selector) { display: none; }
.page-inner > .pane:has(> .pane-selector:checked) { display: flex; flex: 1; }
/* A pane's contents are the part that updates; the bar above them, with the
   search field, is not, so that an update never wipes what was typed. */
.repertoire-contents,
.picker-contents,
.requests-contents {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-block-size: 0;
}
.pane .song-list { flex: 1; overflow-y: auto; }
/* The audience member's own requests, at the head of the repertoire pane,
   do not scroll; the repertoire below them does. */
.your-requests .song-list { flex: none; overflow: visible; }
.pane .song-list li { cursor: pointer; }
/* A pending request is retracted from its own row. The request sits in a
   band as wide as the row, so that RETRACT, past the end of that band, is
   off the row until the row is scrolled sideways to it: a swipe on a touch
   screen, and room made in the row itself where there is a pointer to hover
   with. The hint saying so shows only where hover does not. */
.pending-requests {
  /* The room around the word RETRACT, one em of the type it is set in. */
  --tray-padding: var(--label-size);
  /* Leave a bit showing as a hint. Smaller than the full padding so that it
     only shows colour, not any text. */
  --tray-edge: calc(.8 * var(--tray-padding));
}
.your-requests .hint { margin: 0; padding: var(--space-tight) var(--row-padding-inline) 0; text-transform: none; letter-spacing: normal; color: var(--ink-faded); }
.pending-requests li {
  flex-wrap: nowrap;
  gap: 0;               /* the tray meets the request, and the row's end */
  padding-inline: 0;    /* the request carries it, so that the tray is flush */
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-inline: contain; /* a swipe stays in the row */
  scrollbar-width: none;
}
.pending-requests li::-webkit-scrollbar { display: none; }
.pending-requests .request {
  box-sizing: border-box;
  flex: 0 0 calc(100% - var(--tray-edge));
  display: flex;
  align-items: center;
  gap: var(--row-gap);
  min-inline-size: 0;
  padding-inline: var(--row-padding-inline);
  scroll-snap-align: start;
}
/* The song holds its place while the rest of the row passes under it, so
   that what is being retracted can be read the whole way through. */
.pending-requests .title {
  flex: 0 1 auto;
  min-inline-size: 0;
  position: sticky;
  inset-inline-start: var(--row-padding-inline);
  background: var(--page-background);
}
.pending-requests .age { margin-inline-start: auto; }
/* Printed in reverse, so that it reads as something to press rather than
   as another label on the row. */
.pending-requests .retract {
  display: flex;
  flex: none;
  align-self: stretch;
  align-items: center;
  justify-content: center;
  min-block-size: var(--tap-height);
  padding-inline: var(--tray-padding);
  border: none;
  background: var(--ink);
  color: var(--page-background);
  scroll-snap-align: end;
  cursor: pointer;
}
@media (hover: hover) {
  .your-requests .hint { display: none; }
  /* Room is made for the tray in the row, a pointer having no way to scroll
     the row to it. */
  .pending-requests li:hover .request { flex: 1 1 auto; }
}
.pane-bar {
  display: flex;
  align-items: center;
  gap: var(--space-wide);
  padding: var(--space) var(--row-padding-inline);
}
.pane-bar .field { flex: 1; }
.back { color: var(--ink-faded); cursor: pointer; }
.pane .list-bar { padding-inline-start: var(--row-padding-inline); }

/* --------------------------------------------------------- Request order */

/* Sorting by popularity or by waiting time without the server: every row
   carries both ranks (--popularity, --waiting), and the checked radio
   picks which one is its order. In the requests section a row beyond the
   section's reach in the chosen order is not laid out at all. */
.order { display: flex; gap: var(--space-loose); }
/* An order's name never breaks: the bar it stands in is one line deep. */
.order label { padding-block: var(--space); cursor: pointer; color: var(--ink-faded); white-space: nowrap; }
.order .arrow { display: none; color: var(--ink); font-size: 1.5em; line-height: 0; vertical-align: -.1em; }
.page-inner:has(#order-popularity:checked) [for="order-popularity"],
.page-inner:has(#order-waiting:checked)    [for="order-waiting"] { color: var(--ink); font-weight: 700; }
.page-inner:has(#order-popularity:checked) [for="order-popularity"] .arrow,
.page-inner:has(#order-waiting:checked)    [for="order-waiting"] .arrow { display: inline; }
.ranked { display: flex; flex-direction: column; }
.page-inner:has(#order-popularity:checked) .ranked li { order: var(--popularity); }
.page-inner:has(#order-waiting:checked)    .ranked li { order: var(--waiting); }
.page-inner:has(#order-popularity:checked) .requests-section li:not(.shown-popularity),
.page-inner:has(#order-waiting:checked)    .requests-section li:not(.shown-waiting) { display: none; }

/* ----------------------------------------------------------- Repertoire */

/* Editing the repertoire: a checkbox showing the whole catalogue, each song
   with a switch saying whether it is in the repertoire. */
.catalogue-toggle {
  display: block;
  padding: var(--space-tight) var(--row-padding-inline) var(--space);
  cursor: pointer;
  color: var(--ink-faded);
}
.catalogue-toggle input { accent-color: var(--ink); margin-inline-end: .4em; }
.repertoire-pane:has(#staging-repertoire:checked) .catalogue-toggle { color: var(--ink); }
.song-list .catalogue-only { display: none; }
.repertoire-switch { display: none; }
.repertoire-pane:has(#staging-repertoire:checked) .catalogue-only { display: flex; }
.repertoire-pane:has(#staging-repertoire:checked) .repertoire-switch        { display: block; }
/* The switch: a track with a knob. A button and not a checkbox, since what
   it shows is the change staged rather than an answer of its own. */
.repertoire-switch {
  margin: 0;
  margin-inline-start: auto;
  padding: 0;
  inline-size: 2.4em;
  block-size: 1.3em;
  border: var(--line-thin) solid var(--ink);
  border-radius: 1em;
  background: none;
  position: relative;
  cursor: pointer;
}
.repertoire-switch::before {
  content: "";
  position: absolute;
  inset-block: .15em;
  inset-inline-start: .15em;
  inline-size: calc(1.3em - .3em - 2 * var(--line-thin));
  border-radius: 50%;
  background: var(--ink-faded);
}
li:is(.catalogue-only.staged, :not(.catalogue-only):not(.staged)) .repertoire-switch {
  background: var(--ink);
}
li:is(.catalogue-only.staged, :not(.catalogue-only):not(.staged)) .repertoire-switch::before {
  inset-inline-start: auto;
  inset-inline-end: .15em;
  background: var(--page-background);
}
/* A switch changed from the concert's state, in the attention colour, so
   that changed rows stand out down a long catalogue. */
.staged .repertoire-switch         { border-color: var(--attention); }
.staged .repertoire-switch::before { background: var(--attention); }
.catalogue-only.staged .repertoire-switch         { background: var(--attention); }
.catalogue-only.staged .repertoire-switch::before { background: var(--page-background); }

/* ------------------------------------------------------- Audience setlist */

/* The audience's full setlist: numbered rows, the song being played
   marked by the time it has been running. */
/* At most --setlist-rows rows and at least one, whole, so that scrolled to
   either end no row is cut. The song being played is pinned at the bottom
   edge, in view. */
.setlist-section .song-list {
  --setlist-rows: 6;
  min-block-size: var(--row-height);
  max-block-size: calc(var(--setlist-rows) * var(--row-height));
  overflow-y: auto;
  scrollbar-width: none; /* scrolls by touch or wheel; the bar would cut the dividers */
}
.setlist-section .playing {
  position: sticky;
  bottom: 0;
  background: var(--page-background);
}
/* Number, title and time stay on one line: a long title is cut. */
.setlist-section li { flex-wrap: nowrap; }
.setlist-section .title { flex: 0 1 auto; }

/* --------------------------------------------------------------- Controls */

.controls {
  position: relative; /* for the watermark */
  /* The tear line. Dashes get longer with the width, the only control CSS
     offers over them. */
  border-top: var(--line-bold) dashed var(--ink);
  padding: var(--section-padding);
  display: grid;
  gap: var(--space-wide);
}

/* The setlist: the songs folded away, the fold they are opened by, the last
   songs played, and the slot for the next song. A grid keeps the numbers
   aligned: fold | number | separator | title. */
.region-setlist              { --fold-column: 0; }
.region-setlist:has(.fold)   { --fold-column: 2.9em; }
.setlist {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: [fold] var(--fold-column, 0) [number] auto [separator] auto [title] 1fr [end];
  align-items: center;
  gap: var(--space-tight) var(--space);
  font-size: calc(1.05 * var(--unit));
  --fold-gap: .35em; /* extra room after the fold column */
}
.setlist { --dot: .2em; } /* the separator, and the blank's thickness */
/* Each entry is a row of the grid, so that it can be a tap target. The
   folded group shares its row with the entry after it. */
.setlist li {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: fold / end;
  align-items: center;
}
.setlist .fold + li { grid-column: number / end; }
.setlist .number,
.setlist .next::before       { grid-column: number; }
.setlist .separator          { grid-column: separator; }
.setlist .title              { grid-column: title; font-size: 1.1em; font-weight: 600; }

/* Song numbers. The next song's comes from its data-number attribute, which
   also prints the watermark. */
.setlist .number,
.setlist .fold,
.setlist .next::before {
  font-family: var(--font-mono);
  font-size: .85em;
  color: var(--ink-faded);
}
.setlist .next::before { content: attr(data-number); }
/* The fold: the range the songs folded away are printed as, which opens
   them. The wavy underline is the mark that pressing it does something, as
   on the slot below. */
.setlist .fold {
  grid-column: fold;
  margin-inline-end: var(--fold-gap);
}
.setlist .fold label {
  display: flex;
  align-items: center;
  cursor: pointer;
  text-decoration: underline wavy;
  text-underline-offset: .25em;
  /* A tap's height, taken back by the margin so the row keeps to its line. */
  min-block-size: var(--tap-height);
  margin-block: calc((1lh - var(--tap-height)) / 2);
}

/* The songs folded away, opened over the body: they stand on the tear line,
   so that nothing in the controls moves when they arrive or go. A long
   night is more songs than there is body to stand them in, so they scroll
   past this many. */
.folded-songs { display: none; }
.controls:has(> .fold-selector:checked) .folded-songs {
  display: block;
  position: absolute;
  inset-block-end: 100%;
  inset-inline: 0;
  z-index: 2;
  padding: var(--space-wide) var(--section-padding);
  background: var(--page-background);
  border-block-start: var(--line-thin) solid var(--divider);
}
.folded-songs .setlist {
  --folded-rows: 8;
  max-block-size: calc(var(--folded-rows) * (1lh + var(--space-tight)));
  overflow-y: auto;
}
/* The separator is drawn, so that it and the blank below are centered alike. */
.setlist .separator {
  inline-size: var(--dot);
  block-size: var(--dot);
  margin-inline: .12em; /* a little air on both sides, beyond the column gap */
  border-radius: 50%;
  background: var(--ink-faded);
}

/* An animated indicator of the currently playing song's runtime. */
:is(.setlist, .setlist-section) .playing :is(.elapsed, .song-state) {
  padding: .05em .45em;
  border-radius: .25em;
  background: var(--playing);
  color: var(--ink);
  font-weight: 700;
  animation: breathing var(--breath) ease-in-out infinite;
}
.setlist .playing .elapsed { margin-inline-start: .45em; }
@keyframes breathing {
  0%, 100% { opacity: 1; }
  50%      { opacity: var(--breath-low); }
}

/* The slot for the next song. Its tap target reaches below the line, where
   nothing else is. */
.setlist .next {
  cursor: pointer;
  padding-block-end: var(--space);
  margin-block-end: calc(-1 * var(--space));
}
/* The title is the tap target, blank or not, with padding cancelled by
   margin so the row keeps to its line. */
.setlist .next .title {
  display: flex;
  align-items: center;
  align-self: stretch;
  min-block-size: 1lh;
  padding-block: var(--space-tight);
  margin-block: calc(-1 * var(--space-tight));
}
/* The wavy underline indicates that tapping changes the song, as on the
   audience's candidate. On the title alone, clear of the cross. */
.setlist .next.chosen .lined-up {
  text-decoration: underline wavy var(--ink-faded);
  text-underline-offset: .3em;
}
/* No song chosen: a blank to fill in, as thick as the separator. */
.setlist .next:not(.chosen) .title::before {
  content: "";
  inline-size: 9em;
  block-size: var(--dot);
  border-radius: calc(var(--dot) / 2);
  background: var(--ink-faded);
}
/* Clears the pick: the character alone, in a box big enough to press. */
.clear {
  padding: var(--space-tight) var(--space);
  margin-block: calc(-1 * var(--space-tight));
  border: 0;
  background: none;
  color: var(--ink-faded);
  font: inherit;
  font-size: 1.2em;
  line-height: 1;
  cursor: pointer;
}
.setlist .next:not(.chosen) .clear { display: none; }
/* The next song's number, printed faintly across the controls. */
.setlist .next::after {
  content: attr(data-number);
  position: absolute;
  right: -.7rem; bottom: 2.4rem; /* running off the page's edge */
  font-weight: 800;
  font-size: calc(12 * var(--unit));
  line-height: 1;
  color: rgb(from var(--ink) r g b / .07);
  pointer-events: none;
}

/* -------------------------------------------------------- Join/login pages */

/* The join and login pages: a title and a short form, centered on the page.
   Nothing on them changes while they are read. */
.entry {
  display: grid;
  align-content: center;
  gap: var(--space-loose);
  flex: 1;
  padding: var(--section-padding);
}
.entry-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: calc(2.4 * var(--unit));
  line-height: .9;
  text-transform: uppercase;
  text-wrap: balance;
}
.entry-form { display: grid; gap: var(--space-wide); }
.entry-form .field { padding: var(--space); }
.entry-refusal {
  margin: 0;
  color: var(--attention);
  font-weight: 600;
}
/* What an invitation grants, above the form that accepts it. */
.entry-note { margin: 0; }

/* ------------------------------------------- Staging areas in the controls */

/* A staging area takes the setlist and the stage actions' place: the
   repertoire change while the catalogue shows in its pane, whose switches
   are what stage it; the break and the request status while opened.
   Checkboxes, so this needs no server. */
.staging-selector { display: none; }
.controls > .staging-area { display: none; }
.page-inner:has(#staging-repertoire:checked):has(#pane-repertoire:checked):not(:has(#staging-requests:checked, #staging-leaving:checked)) > .controls > .staging-repertoire,
.page-inner:has(#staging-requests:checked) > .controls > .staging-requests,
.page-inner:has(#staging-leaving:checked) > .controls > .staging-leaving { display: grid; gap: var(--space-wide); }
/* The area showing stands in for the setlist and the stage actions. */
.page-inner:is(:has(#staging-repertoire:checked):has(#pane-repertoire:checked), :has(#staging-requests:checked), :has(#staging-leaving:checked)) > .controls > :is(.setlist, .actions) { display: none; }
/* The cross sits in a corner out of flow, costing no line. The contents
   are inset from that corner, except the closing action, which is let
   back out to stay as wide as the stage buttons. */
.controls > .staging-area {
  --cross-width: 1.7em; /* the corner's width, which the cross fills */
  padding-inline-end: var(--cross-width);
}
.controls > .staging-area > .close {
  position: absolute;
  inset-block-start: 0;
  inset-inline-end: 0;
  inline-size: var(--cross-width);
  text-align: center;
}
.controls > .staging-area .attachment > :is(.action, .action-row) {
  margin-inline-end: calc(-1 * var(--cross-width));
}
/* A cross that closes or clears something: nothing around it. */
.close {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 1.5em;
  line-height: 1;
  cursor: pointer;
}
/* An area's actions are a size smaller than the stage buttons: an area is
   read, not glanced at. */
.controls > .staging-area .action { font-size: calc(1.4 * var(--unit)); }
/* What the areas hold, laid out down the page. */
.leave-stage,
.repertoire-change { display: grid; gap: var(--space-wide); }

/* A refusal, as a dialog over the page. */
.refusal {
  box-sizing: border-box;
  inline-size: min(100vw - 2rem, 24rem);
  padding: var(--space-loose) var(--section-padding);
  border: var(--line-bold) solid var(--attention);
  border-radius: .6rem;
  background: var(--page-background);
  color: var(--attention);
  font-weight: 600;
}
.refusal::backdrop { background: var(--shade); }
.refusal p { margin: 0; padding-inline-end: 1.5rem; }
/* The cross, in the corner of the box. */
.refusal form { position: absolute; inset-block-start: var(--space); inset-inline-end: var(--space); }

.how-long { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space) var(--space-tight); }
.how-long label { display: flex; align-items: baseline; gap: .3em; }
/* color-scheme, so the browser draws its pickers for a dark background. */
.how-long .field { color-scheme: dark; }
/* A time, and the picker the browser draws on it. */
.how-long .back-at .field { inline-size: 5.8em; }
/* Three digits of minutes, with the unit printed inside the field. */
.how-long .back-in { position: relative; }
.how-long .back-in .field {
  inline-size: 3em;
  padding-inline-end: calc(var(--box-padding-inline) + .7em); /* the unit's width */
  text-align: end;
}
.how-long .back-in .field::-webkit-inner-spin-button { appearance: none; }
.how-long .back-in .time-unit {
  position: absolute;
  inset-block: 0;
  inset-inline-end: var(--box-padding-inline);
  display: flex;
  align-items: center;
  color: var(--ink-faded);
  text-transform: lowercase;
  pointer-events: none;
}

/* A text field: a thin-bordered box, tall enough to press. */
.field {
  box-sizing: border-box;
  min-inline-size: 0;
  min-block-size: var(--tap-height);
  border-width: var(--line-thin);
  text-align: start;
  font: inherit;
}
.field::placeholder { color: var(--ink-faded); opacity: 1; }
/* Whoever asked not to be moved is not moved. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 0s !important; animation-duration: 0s !important; }
  .request-form { --sweep: 0s; --launch: 0s; --closing: 0s; }
}

/* No focus ring, as they are drawn by the browser and don't suit the
   theme. TODO: keyboard users get no mark at all; this wants one in the
   theme's colours, set once here. */
:focus, :focus-visible { outline: none; }

/* A required field is bordered like a button; an optional one is faint. */
.field:required { border-width: var(--line); }
.field:optional { border-color: var(--ink-faded); }

/* What is being written sits beside a vertical rule, which grows with it;
   the fields have no border or background of their own. */
.written {
  min-inline-size: 0;
  display: grid;
  gap: var(--space-tight);
  padding-inline-start: var(--rule-gap);
  border-inline-start: var(--line-thin) solid var(--ink-faded);
}
/* What opens the block: who it reaches, or who is writing it. */
.written .opening {
  display: flex;
  align-items: baseline;
  gap: .45em;
  color: var(--ink-faded);
}
.written .opening .cap { flex: none; white-space: nowrap; }
/* The recipient line and the name field are both names and set alike.
   The selector is .opening .name rather than .name because a recipient's
   name is a .name too, in the same block. */
.written .to-whom,
.written .opening .name {
  flex: none;
  min-inline-size: 0;
  padding: var(--chip-padding);
  border: 0;
  border-radius: .25em;
  background: var(--tint);
  color: var(--ink);
  font: inherit;
  font-size: var(--message-text-size);
  letter-spacing: inherit;
  text-transform: inherit;
}
/* The box is the name, so it is exactly as wide as the name and no wider,
   and there is none until something has been typed. */
.written .opening .name {
  field-sizing: content;
  max-inline-size: 100%;
}
/* No name, no box -- but a character's width of room all the same, or the
   field is zero width and the caret has nowhere to stand. */
.written .opening .name:invalid {
  padding: 0;
  background: none;
  min-inline-size: 1ch;
}
/* The line names the field, so pressing anywhere along it writes. */
.written label.opening { cursor: text; }
.written .text {
  inline-size: 100%;
  box-sizing: border-box;
  min-inline-size: 0;
  field-sizing: content;
  min-block-size: var(--tap-height);
  max-block-size: 5lh;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: var(--message-text-size);
  line-height: 1.4;
  resize: none;
}
.written .text::placeholder { color: var(--ink-faded); opacity: 1; }

/* The message box, fixed at the foot while the feed scrolls. What is
   written and SEND share a line until the text outgrows it, and SEND drops
   below; the field's width follows its text (field-sizing), which is what
   makes the line break at the right moment. */
.message-box {
  flex: none;
  margin: var(--space) var(--section-padding);
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  justify-content: end;
  gap: var(--space-tight) var(--space-wide);
}
.message-box > .written { flex: 1 1 auto; }
/* SEND is a word, not a button, at the end of the line. */
.send {
  flex: none;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font-weight: 700;
  cursor: pointer;
}
.send:disabled { color: var(--ink-faded); cursor: default; }

/* The note with a stage action, folded away behind the clip. Folded, the
   recipients and the note are not laid out at all, so nothing between the
   clip and its action can be pressed by mistake. */
.attachment { display: grid; gap: var(--space); }
.attachment:not(.noting) > .written > .recipients,
.attachment:not(.noting) > .written > .note-area { display: none; }
/* The clip keeps a corner of the row its action fills. */
.with-clip { display: grid; grid-template-columns: auto 1fr; gap: var(--space); }
.with-clip .clip { display: grid; place-items: center; inline-size: 2.6rem; padding: 0; }
.with-clip .clip svg { inline-size: 1.1rem; block-size: 1.6rem; }
/* Holding a note: filled in, as the action beside it is. */
.attachment.noting .clip { background: var(--ink); color: var(--page-background); }
/* A minimum height, so there is something to tap into. */
.attachment .note-area { display: block; min-block-size: 2.4lh; cursor: text; }

/* "chosen" is set once a song has been picked. The whole form hides while
   a pane is open, as the pane is where the request is made. */
.request-form {
  display: grid;
  gap: var(--space);
}
.request-form > * { display: none; }
.request-form:not(.chosen) > .browse,
.request-form:not(.chosen) > .open-note,
.request-form > .closed-note,
.request-form.chosen > :is(.candidate-caption, .submit) { display: block; }
.request-form.chosen > .written { display: grid; }
.request-form.chosen > .candidate { display: grid; justify-items: center; }
.request-form .caption { text-align: center; color: var(--ink-faded); }
/* The request status: above the button when open (you may request), below
   it when closed. */
.request-form .request-status {
  text-align: center;
  font-size: .85em;
  font-weight: 700;
}
.request-form > .open-note { order: -1; margin-block: var(--space-tight) var(--space); }
/* The chevron is out of flow, so the word is centered without it. */
.browse { position: relative; }
.browse .chevron {
  position: absolute;
  inset-inline-end: 1.7rem;
  top: 50%;
  translate: 0 -57%; /* the glyph sits low in its box */
  font-size: 1.5em;
  line-height: 1;
}
.page-inner:has(.pane-selector:checked:not(.closed)) .controls:has(.request-form) { display: none; }
.request-form .song {
  margin-block-start: calc(-1 * var(--space-tight));
  padding-block-end: var(--space);
  cursor: pointer;
  text-decoration: underline wavy var(--ink-faded);
  text-underline-offset: .3em;
}
.request-form .song .title { font-size: calc(2 * var(--unit)); font-weight: 800; line-height: 1; }

/* ------------------------------------------------- Request in-flight */

/* Animation for "request in flight" to acknowledge pressing the button until
   the request reaches the server. */
.request-form .submit { transition: color .2s ease, border-color .2s ease; }
.request-form.asking { pointer-events: none; }
.request-form.asking .submit {
  position: relative;
  overflow: hidden;
  color: var(--ink-faded);
  border-color: var(--ink-faded);
}
.request-form.asking .submit::after {
  content: "";
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  inline-size: 35%;
  block-size: var(--line);
  background: var(--attention);
  animation: travelling 1.1s linear infinite;
}
@keyframes travelling {
  from { translate: -100% 0; }
  to   { translate: 300%  0; }
}

/* --------------------------------------------- Request accepted */

.request-form {
  --sweep:   .45s; /* the wave animation over the song title */
  --launch:  .55s; /* the song going away upwards */
  --closing: .34s; /* the area closing down */
}

/* Wave animation for an accepted request */
.request-form.sweeping .candidate .title {
  background-image:
    linear-gradient(100deg,
      var(--ink) 0 34%,
      var(--crest) 50%,
      var(--attention) 66% 100%),
    linear-gradient(#0000 0 100%);
  background-size: 300% 100%, 100% 100%;
  background-repeat: no-repeat;
  -webkit-background-clip: text, border-box;
  background-clip: text, border-box;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: sweeping var(--sweep) ease-out forwards;
}
@keyframes sweeping {
  from { background-position: 0% 0, 0 0; }
  to   { background-position: 100% 0, 0 0; }
}

/* Animation for the request leaving. */
.request-form.copied > .candidate { visibility: hidden; }
.going {
  position: absolute;
  z-index: 2;
  pointer-events: none;
  animation: launching var(--launch) cubic-bezier(.45, .05, .75, .25) both;
}
.going .title { color: var(--attention); }
@keyframes launching {
  0%   { translate: 0 0; opacity: 1; }
  45%  { opacity: 1; }
  100% { translate: 0 -14rem; opacity: 0; }
}

/* Animation for the request area shrinkin back down.  */
.request-form.closing {
  overflow: hidden;
  align-content: end;
  transition: block-size var(--closing) ease;
}
.request-form.closing.chosen > :not(.submit) {
  opacity: 0;
  transition: opacity calc(var(--closing) * .8) ease;
}

/* Animation for the "request" button changing back to "repertoire". */
.request-form.handing-over { position: relative; }
.request-form.handing-over > .submit {
  display: block;
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  animation: leaving calc(var(--closing) * .8) ease both;
}
.request-form.handing-over > .open-note {
  animation: arriving calc(var(--closing) * .8) ease both;
}
/* The count waits until there is something to see it against. */
.request-form.handing-over .count {
  animation: arriving calc(var(--closing) * .5) ease calc(var(--closing) * .5) backwards;
}
@keyframes leaving  { from { opacity: 1; } to { opacity: 0; } }
@keyframes arriving { from { opacity: 0; } to { opacity: 1; } }

/* --------------------------------------------- Control area actions */

/* The main action fills the line; the button beside it (BREAK, END) is as
   wide as its word and level with the action, however tall the note
   above. */
.action-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--space-wide);
  align-items: end;
}
.actions > .attachment,
.actions > .finished,
.actions > .browse { grid-column: 1; }
/* More padding at the sides than above and below: capitals leave the line
   box's ascender and descender room empty, so equal padding would look
   taller than wide. */
.action-row > .leave { grid-column: 2; padding-inline: .4em; }
.actions:not(.chosen) > .attachment { display: none; }
/* One vertical rule for the note and its action together. */
.attachment.noting {
  padding-inline-start: var(--rule-gap);
  border-inline-start: var(--line-thin) solid var(--ink-faded);
}
.attachment > .written { padding-inline-start: 0; border-inline-start: 0; }
/* The recipient line: the caption, the recipients, and at the end the
   count of the rest and the button to add one. Only the names give way. */
.recipients {
  display: flex;
  align-items: baseline;
  gap: .3em;
  min-inline-size: 0;
}
.recipients .caption { color: var(--ink-faded); }
.recipients .whom {
  flex: 0 1 auto;
  min-inline-size: 0;
  display: flex;
  align-items: baseline;
  gap: .3em;
  overflow: hidden;
}
/* Everyone the message may go to reads as a chip of the same size, so that
   each is as easy to press as the next. */
.recipients .scope,
.recipients .recipient,
.recipients .add-recipient {
  padding: var(--chip-padding);
  border-radius: .25em;
  background: var(--tint);
  color: var(--ink-faded);
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
}
.recipients .scope { border: 0; }
/* The way to add one: an empty outline, as against a name already there. */
.recipients .add-recipient {
  border: var(--line-thin) dotted var(--ink-faded);
  background: none;
}
.recipients .recipient { color: var(--ink); }
.recipients .scope.on { background: var(--ink); color: var(--page-background); }
.recipients > * { flex: none; }
/* The names sit on the line with everything else, not in a box of their own. */
.recipients .these { display: contents; }
/* A recipient: their name in full. A slot with no one in it names no one. */
.recipients .recipient:has(.name:empty) { display: none; }
/* Only a name long enough to fill the line on its own is cut. */
.recipients .name {
  display: inline-block;
  max-inline-size: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: bottom;
}
/* A row of the list to choose recipients from. The name takes the slack,
   so the row's remarks (requester, picked) sit flush right. */
.audience-list .title { flex: 1; }
.audience-list li:not(.requester) .song-state { display: none; }
.audience-list .picked .title { font-weight: 700; }
.audience-list .picked::after { content: "✓"; color: var(--ink); }

/* Two long words: a size smaller than the other areas' actions. */
.action.change { font-size: 1.3rem; }
/* Count added to a button (number of pending requests, number of staged
   repertoire changes). */
.action:is(.change, .browse) { position: relative; }
.action.change { padding-inline: 3.5rem; }
.action.browse { padding-inline: 2.6rem; }
.action:is(.change, .browse) .counts {
  position: absolute;
  inset-block: 0;
  display: grid;
  align-content: center;
  gap: var(--space-hair);
}
.action.change .counts { inset-inline-end: var(--space); }
.action.browse .counts { inset-inline-start: var(--space); }
.action:is(.change, .browse) .count {
  padding: var(--chip-padding);
  border-radius: .3em;
  background: var(--page-background);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: .45em;
  font-weight: 700;
  letter-spacing: .06em;
  line-height: 1.3;
}
.action.browse .count { color: var(--attention); }
.action:is(.change, .browse) .count:empty { display: none; }

/* The first action is whichever the concert is waiting for: START while a
   song is lined up, SONG FINISHED while one is being played, and the
   repertoire to pick from otherwise. */
.actions:not(.chosen) > .start,
.actions:is(.chosen, :not(.playing)) > .finished,
.actions:is(.chosen, .playing) > .browse { display: none; }
/* A stage button: START SONG, SONG FINISHED, BREAK, REQUEST. */
.action {
  padding: var(--space-tight);
  border-width: var(--line-bold);
  /* Concentric with the page's corners: its radius less the page's
     border and the controls' padding. */
  border-radius: max(.25rem, calc(var(--page-radius) - var(--line-heavy) - var(--section-padding)));
  font-size: calc(1.8 * var(--unit));
  font-weight: 700;
  cursor: pointer;
}
/* The primary action is printed solid and bold. */
.action.primary {
  background: var(--ink);
  color: var(--page-background);
  font-weight: 800;
}
.action:disabled {
  border-color: var(--ink-faded);
  color: var(--ink-faded);
  cursor: default;
}
