/*
| Workout — the three screens with a shape of their own: the exercise picker,
| the training builder and the session runner. The shell, the palette and the
| base controls come from app.css.
*/

/* Exercise picker ------------------------------------------------------- */
/*
| A bottom sheet rather than a full page, so the builder stays visible behind
| it and picking an exercise never loses the scroll position of the list you
| were editing.
*/
.sheet[hidden] {
  display: none;
}

.sheet {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  background: rgba(0, 0, 0, 0.6);
}

.sheet-panel {
  display: flex;
  flex-direction: column;
  max-height: 92vh;
  background: var(--bg);
  border-top: 1px solid var(--line-strong);
  border-radius: 18px 18px 0 0;
  padding-bottom: env(safe-area-inset-bottom);
}

/*
| A sheet that takes the whole screen instead of the bottom of it.
|
| The others sit over the page they were opened from on purpose: the picker
| keeps the builder in view behind it, so choosing an exercise never loses your
| place in the list you were editing. A conversation has nothing to keep in
| view — the transcript is the whole of it, the answers run long, and the strip
| of dimmed page above was only ever costing lines to read them in. Growing to
| fill the sheet is what does that: there is then no free space left for the
| `justify-content: flex-end` above to push anything to the bottom with.
|
| `min-height: 0` is load-bearing, and dropping it is not cosmetic. A flex item
| refuses by default to shrink below its own content, and this panel is the one
| in the chain with no `overflow` of its own to waive that. The bottom sheet
| never met the rule because `max-height: 92vh` forced the shrink; with the cap
| gone, a transcript longer than the screen grew the panel rather than
| scrolling inside it, and carried the input box off the bottom of the
| viewport — thirty turns put it 1130px below the fold.
*/
.sheet-full > .sheet-panel {
  flex: 1 1 auto;
  min-height: 0;
  max-height: none;
  border-top: 0;
  border-radius: 0;
  padding-top: env(safe-area-inset-top);

  /*
  | The whole height, but not the whole width: every other screen in the app is
  | a 640px column down the middle, and a conversation run edge to edge on a
  | monitor is a line of text nobody can follow back to the start of. On a
  | phone the cap is never reached and this is the full screen it says it is.
  */
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
}

/*
| A column, so the body can hand what the heading and the input leave to the
| transcript inside it — which is a scroller, and so shrinks without being
| asked to.
*/
.sheet-full > .sheet-panel > .sheet-body {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.sheet-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 12px 10px 16px;
  border-bottom: 1px solid var(--line);
}

.sheet-title {
  flex: 1 1 auto;
  font-size: 15px;
  font-weight: 600;
}

.sheet-body {
  flex: 1 1 auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 12px 16px 20px;
}

.sheet-search {
  position: sticky;
  top: 0;
  z-index: 1;
  padding-bottom: 10px;
  background: var(--bg);
}

/*
| The way out of a sheet that asks for something rather than offering a list.
| Pinned under the fields — the panel's own bottom padding already clears the
| home indicator — because a configuration with a set varied on every row is
| longer than the screen, and the way back to the session must not be
| something you scroll to find.
*/
.sheet-foot {
  flex: none;
  padding: 10px 16px 12px;
  border-top: 1px solid var(--line);
  background: var(--bg);
}

/*
| The picker's, which carries two: the way back to the list and the way out
| with what was chosen. The way out takes the rest of the row, so it is the
| wide target whether or not the other one is there.
*/
.sheet-foot-row {
  display: flex;
  gap: 8px;
}

/*
| [hidden] alone loses to the `display` above. Without this the row is on
| screen for a swap too, where one tap answers the sheet and there is nothing
| to commit — as a full-width button with no label, because the wording is
| written by the code that only runs when several are being chosen.
*/
.sheet-foot-row[hidden] {
  display: none;
}

.sheet-foot-row > .btn-primary {
  flex: 1 1 auto;
}

/*
| Both ways into the catalogue are always on screen: the search field for
| someone who knows the names, the body map for someone who would rather point
| at what they want to train. They are not alternative modes to switch between.
*/
.picker-hint {
  font-size: 12px;
  color: var(--ink-faint);
  margin: 2px 0 10px;
}

/* Body map -------------------------------------------------------------- */
.bodymap {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-bottom: 12px;
}

.bodymap-figure {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 8px 4px 4px;
  text-align: center;
}

.bodymap-caption {
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 2px;
}

.bodymap svg {
  display: block;
  width: 100%;
  height: auto;
  max-height: 38vh;
}

/* Holds the shared silhouette definition; nothing of it is ever painted. */
.bodymap-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* The body itself: present for context, never interactive. */
.body-silhouette {
  fill: #0f1114;
  stroke: var(--line-strong);
  stroke-width: 1.2;
}

/*
| Hair and face. These carry no muscle and do nothing when tapped — they exist
| so that "this is the back" registers before you have read the caption.
*/
.body-hair {
  fill: var(--line-strong);
}

.body-face {
  fill: var(--ink-dim);
}

.body-line,
.body-parting {
  fill: none;
  stroke: var(--ink-faint);
  stroke-width: 1;
  stroke-linecap: round;
}

.body-parting {
  stroke: var(--surface-2);
  stroke-width: 1.2;
}

/*
| One region per muscle, keyed to #services/muscles by its data-muscle.
|
| The outline is faint: with the muscles now packed against each other inside
| the body, a heavier one would read as a wireframe rather than as anatomy.
*/
.region {
  /* Translucent rather than a flat colour, so it reads as a lit muscle. */
  fill: rgba(255, 255, 255, 0.13);
  stroke: rgba(255, 255, 255, 0.26);
  stroke-width: 0.6;
  cursor: pointer;
  transition: fill 0.12s ease;
  /* Tapping quickly must not select the body outline as text. */
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

.region:hover {
  fill: rgba(255, 255, 255, 0.26);
}

/*
| The fibre lines inside each belly: the direction the muscle pulls. They are
| what stops a quadriceps reading as one grey slab, and they are drawn as
| strokes over the fill rather than as gaps in it, so a muscle stays a single
| tappable shape.
*/
.region .fiber {
  fill: none;
  stroke: rgba(255, 255, 255, 0.28);
  stroke-width: 0.7;
  stroke-linecap: round;
  pointer-events: none;
}

.region:hover .fiber {
  stroke: rgba(255, 255, 255, 0.45);
}

/* Dark on light once the muscle is selected, or the lines would disappear. */
.region.is-active .fiber {
  stroke: rgba(0, 0, 0, 0.28);
}

.region.is-active {
  fill: var(--ink);
  stroke: var(--ink);
}

/*
| Each muscle is a real button now — role="button", tabindex — so Tab and
| Enter/Space reach it. The default SVG focus ring is invisible against a dark
| fill, so it is drawn by hand instead.
*/
.region:focus-visible {
  outline: none;
  stroke: var(--ink);
  stroke-width: 1.5;
}

/*
| The strip under the figure: what is selected, and a way to drop it. It is
| empty and takes no space until a muscle is chosen — the figure is the only
| way to add a filter now, so there is nothing to show before that.
*/
.chip-remove {
  opacity: 0.6;
  font-size: 10px;
}

.chip.is-active:hover .chip-remove {
  opacity: 1;
}

.chip-clear {
  color: var(--ink-faint);
  background: none;
  border-style: dashed;
}

.chip-clear:hover {
  color: var(--fail);
  border-color: rgba(229, 72, 77, 0.4);
}

/*
| The equipment row, under the figure and whatever muscles it has selected:
| drawn as the settings screen draws the language — a label over a radio
| group of chips.
*/
.picker-filter {
  margin-bottom: 14px;
}

.chip-row:not(:empty) + .picker-filter {
  margin-top: 12px;
}

/* Results --------------------------------------------------------------- */
.exercise-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.exercise-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 11px 12px;
  text-align: left;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font: inherit;
}

.exercise-row:hover {
  background: var(--surface-2);
}

/*
| Ticked, while several are being chosen at once. The whole row is marked and
| not just the box at its end: a column of rows is read by its edges, and a
| tick 300px away from the name is not what tells you the name is chosen.
*/
.exercise-row.is-selected {
  background: var(--surface-2);
  border-color: var(--ink);
}

/*
| Listed, and not available. Only the switch draws these: an exercise that
| cannot inherit the numbers already written on the slot.
|
| Dimmed rather than removed, because the reason it is still here is that
| somebody is looking for it — see picker.js. Everything that identifies it
| stays legible; what goes is the affordance, and in its place a line saying
| what stands in the way.
*/
.exercise-row.is-blocked {
  cursor: default;
  opacity: 0.55;
}

.exercise-row.is-blocked:hover {
  background: var(--surface);
}

.exercise-row-blocked {
  font-size: 12px;
  color: var(--fail);
  margin-top: 2px;
}

.exercise-row-main {
  flex: 1 1 auto;
  min-width: 0;
}

.exercise-row-name {
  font-size: 14.5px;
  font-weight: 500;
}

.exercise-row-meta {
  font-size: 12px;
  color: var(--ink-dim);
  margin-top: 1px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The tick itself, at the end of the row it answers for. */
.row-check {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  font-size: 12px;
  line-height: 1;
  border: 1px solid var(--line-strong);
  border-radius: 50%;

  /* Empty until it is ticked, rather than absent: the row says it can be. */
  color: transparent;
}

.exercise-row.is-selected .row-check {
  color: #0b0c0e;
  background: var(--ink);
  border-color: var(--ink);
}

.result-count {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 12px 0 8px;
}

/*
| A heading over a run of results. Ruled off, because the first group in the
| swap sheet is the handful of exercises the training itself holds and the
| second is the rest of the catalogue — a difference in kind, not in ranking,
| and it has to survive being read at arm's length between sets.
*/
.result-group {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin: 18px 0 8px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

/* Nothing to rule off against at the top of the list. */
.result-group:first-child {
  margin-top: 4px;
  padding-top: 0;
  border-top: none;
}

/* Training builder ------------------------------------------------------ */
.slot {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;

  /* Clears the fixed app bar when a panel is scrolled into view on opening. */
  scroll-margin-top: calc(var(--appbar-h) + env(safe-area-inset-top) + 10px);
}

/*
| Only one exercise is unfolded at a time — see builder.js — so the open panel
| is marked out rather than left to be inferred from its height.
*/
.slot.is-open {
  border-color: var(--line-strong);
}

.slot + .slot {
  margin-top: 10px;
}

.slot-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 10px 12px 14px;
}

.slot-index {
  flex: none;
  width: 24px;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
}

.slot-head-main,
.alt-head-main {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  padding: 0;
}

.head-text {
  flex: 1 1 auto;
  min-width: 0;
}

/* Points right when the panel is folded, down when it is open. */
.caret {
  flex: none;
  font-size: 17px;
  line-height: 1;
  color: var(--ink-faint);
  transition: transform 0.15s ease;
}

.is-open > .slot-head > .slot-head-main > .caret,
.is-open > .alt-head > .alt-head-main > .caret {
  transform: rotate(90deg);
}

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

.slot-name {
  font-size: 15px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.slot-summary {
  font-size: 12.5px;
  color: var(--ink-dim);
  margin-top: 2px;
}

/*
| The exercise's comment, under the summary. Quieter than the summary and in
| italics, because it is somebody's words rather than the plan's numbers. In
| full on a shared page; one line in the builder's folded row, where the panel
| holds the rest.
*/
.slot-comment {
  margin-top: 3px;
  font-size: 12.5px;
  font-style: italic;
  line-height: 1.35;
  color: var(--ink-faint);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.slot-comment.is-clamped {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A coach's comment, in the panel of the week they sent: read, not typed. */
.slot-comment-said {
  margin: 0;
  font-size: 14px;
  line-height: 1.45;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.slot-tools {
  display: flex;
  gap: 4px;
  flex: none;
}

/*
| A row carrying a picture stacks: the square on its own line, the name and the
| summary under it.
|
| Beside the name, at this size, there is no name. The row was already carrying
| an index, a chevron, a drag handle and a delete button across a phone's
| width, and a square that wide leaves the text about nothing — the name
| truncates to a syllable and the summary breaks one word to a line. Stacking
| gives both of them the width of the row back, at the cost of a row about as
| tall as two of the old ones.
|
| `:has()` rather than a class from the three files that build these rows,
| because the condition is exactly "this row has a picture in it" and the
| layout is the only thing that cares. A row without art is left alone, which
| is what stops a list holding both from reading as two lists.
*/
.slot-head:has(.movement-thumb) {
  align-items: flex-start;
}

.slot-head-main:has(.movement-thumb) {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    'picture picture'
    'text caret';
  align-items: center;
  row-gap: 10px;
}

.slot-head-main:has(.movement-thumb) .movement-thumb {
  grid-area: picture;
}

.slot-head-main:has(.movement-thumb) .head-text {
  grid-area: text;
}

.slot-head-main:has(.movement-thumb) .caret {
  grid-area: caret;
}

/*
| Supersets, in the builder ------------------------------------------------
|
| A superset is a run of consecutive exercises on the same flat list — no
| container, no nesting — so what marks it is a rail down the left of its
| members and a tag across the top of the first. The rail is the only thing
| binding them, and it is enough: the eye reads a continuous edge as one
| block, and the drag never has to know.
|
| Blue, the colour the app already uses for structure rather than for outcome
| — the same one a moved target is printed in. Green, amber and red are
| spoken for by how a set went, and a superset is not a verdict.
*/
.slot.is-superset {
  border-left: 3px solid var(--rise);
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

/*
| One block: the members close up against each other, corners and all.
|
| Keyed off "this exercise *continues* a group" rather than off "both of these
| are in some group", which is what it used to say — and what made two
| supersets back to back read as one long block, since the last member of the
| first and the first member of the second are both superset members. An
| exercise that starts a superset starts a block, so it keeps the same gap
| above it that every other exercise on the list gets.
*/
.slot.is-superset:not(.is-superset-first) {
  margin-top: 0;
  border-top: none;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

.slot.is-superset:not(.is-superset-last) {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

/*
| The bar across the top of a superset, and the one place its rounds are set.
|
| The rounds sit here rather than in each member's panel, where they were one
| editable copy per exercise of a number they all share. On the tag they are a
| property of the group, they are on screen without opening anything, and there
| is only ever one of them.
*/
.slot-superset {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 12px 6px 14px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--rise);
  background: rgba(76, 159, 240, 0.09);
}

.slot-superset-label {
  min-width: 0;
}

/*
| The same bar on the read-only page a shared link opens, where the rounds are
| a sentence rather than a field — nothing there is typed into. It keeps the
| tag's own letterspacing and case, so the two halves of the strip read as one
| line and not as a label with a number bolted to it.
*/
.slot-superset-rounds {
  flex: none;
  font-variant-numeric: tabular-nums;
}

/*
| The rounds field on that bar: label and box side by side rather than stacked,
| because the bar is a strip and the field has to live inside its height.
*/
.superset-rounds {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
  margin: 0;
}

.superset-rounds .form-label {
  margin: 0;
  white-space: nowrap;
}

.superset-rounds .form-control {
  width: 56px;
  padding: 4px 8px;
  text-align: center;
  font-size: 15px;
}

/*
| The line under the locked rounds in a member's panel, saying where the number
| is set. Pulled up under the row it belongs to, which the field grid has
| already put a margin below.
*/
.locked-note {
  margin: -6px 0 12px;
}

/*
| Making a superset, at the top of an exercise's panel: two buttons on the
| quiet row the switch already sat on, and nothing around them.
|
| This was a bordered block with a heading and a line explaining what a
| superset is, put there because the feature was being missed outright when it
| was a single grey link saying only "with the exercise above". What fixed that
| was the labels — "Superset with above" and "Superset with below" say what
| they do and there are two of them — so the box around them was furniture on
| every panel forever after.
|
| The offer is tinted; the way back out is an ordinary button.
*/
.superset-toggle {
  color: var(--rise);
  border-color: rgba(76, 159, 240, 0.35);
}

/*
| The direction, and the only thing telling the two join buttons apart. Given
| its own weight and a little air, because at 13px an arrow set in the body
| weight beside a capital S disappears into it.
*/
.superset-arrow {
  margin-right: 6px;
  font-weight: 700;
}

.slot-body {
  border-top: 1px solid var(--line);
  padding: 14px;
  background: var(--surface-2);
}

.slot-body[hidden] {
  display: none;
}

/*
| The row at the top of a panel: the superset controls, and the switch.
|
| Above the fields because both decide what those fields are *for* — one what
| the exercise is run with, one what it is — and a control you meet after the
| numbers it reinterprets is one you have to scroll back up to use.
|
| To the right because neither is usually why the panel was opened, and all of
| one weight because they are all ordinary controls: the switch was drawn quiet
| when it was the only thing here, and next to two buttons that look like
| buttons it read as disabled rather than as understated.
*/
.switch-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 6px;
  margin: -4px 0 10px;
}

.subsection {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 18px 0 8px;
}

/*
| Per-set overrides: a compact table of the sets that break the pattern, folded
| away behind the switch that offers it. Indented under that switch so the
| table reads as its answer and not as another section of the panel.
*/
.override-list {
  margin: 2px 0 14px 14px;
  padding-left: 14px;
  border-left: 1px solid var(--line);
}

.override-row {
  display: grid;
  grid-template-columns: 42px repeat(2, minmax(0, 1fr)) 36px;
  gap: 6px;
  align-items: center;
  margin-bottom: 6px;
}

/*
| No load column on a bodyweight exercise — see overridesSection in
| exercise_config.js. The target column is always drawn, holding a locked "max"
| where there is no number to vary.
*/
.override-row.is-unweighted {
  grid-template-columns: 42px minmax(0, 1fr) 36px;
}

.override-index {
  font-size: 12px;
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}

.override-row .form-control {
  min-height: 38px;
  font-size: 14px;
  padding: 6px 8px;
}

/* Alternatives are drawn as an indented sub-list of the slot they replace. */
.alt {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  margin-bottom: 8px;
  overflow: hidden;
  scroll-margin-top: calc(var(--appbar-h) + env(safe-area-inset-top) + 10px);
}

.alt.is-open {
  border-color: var(--line-strong);
}

.alt-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 8px 9px 12px;
}

.alt-name {
  font-size: 13.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.alt-body {
  border-top: 1px solid var(--line);
  padding: 12px;
  background: var(--surface-2);
}

.alt-body[hidden] {
  display: none;
}

/* Sticky save bar at the foot of the builder. */
.form-actions {
  position: sticky;
  bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  display: flex;
  gap: 8px;
  padding: 12px 0 4px;
  background: linear-gradient(to top, var(--bg) 62%, rgba(11, 12, 14, 0));
}

.form-actions .btn {
  flex: 1 1 0;
}

/*
| The settings page hides its bar once the script that saves for it has
| loaded. Spelled out because `display: flex` above beats the `hidden`
| attribute's own rule, and the bar would otherwise stay put.
*/
.form-actions[hidden] {
  display: none;
}

/*
| Page N of M, between its two arrows. Not sticky and not full-width, unlike
| the save bar above: it is a place in a list, not an action to commit.
*/
.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 16px;
}

.pager-page {
  color: var(--muted);
  font-size: 13px;
}

/*
| "More", under the history and the gymbros feed.
|
| Not a pager: both screens arrive a week at a time and the week before is
| appended under it, so there is one direction and no page number to report.
| Quiet and full-width — it is the end of what has been read rather than an
| action to commit, but it is also the only thing down there to aim a thumb at.
*/
.more {
  margin-top: 16px;
}

/* Rest-time picker -------------------------------------------------------- */
/*
| Two wheels in a sheet, for the two rest lengths an exercise has.
|
| Everything here is one number: `--wheel-row` is the height of a row, and the
| whole control is sized in multiples of it. The wheels are read back by
| dividing scroll position by that height — see public/duration.js — so the
| stylesheet and the script agree by measurement rather than by both being told
| the same figure twice.
*/
.wheels {
  --wheel-row: 44px;
  --wheel-rows: 5;

  position: relative;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  height: calc(var(--wheel-row) * var(--wheel-rows));
}

.wheel-labels {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  margin-bottom: 6px;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-dim);
  text-align: center;
}

.wheel {
  /*
  | Above the band, which is absolutely positioned and would otherwise paint
  | its background over the very numbers it is meant to frame.
  */
  position: relative;
  z-index: 1;

  height: 100%;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;

  /*
  | Half the wheel, less half a row, at each end — which is what lets the first
  | and last numbers reach the middle. Padding rather than spacer elements: a
  | listbox may only contain options.
  */
  padding: calc((var(--wheel-row) * (var(--wheel-rows) - 1)) / 2) 0;

  /* The band below marks the selection; a scrollbar would only compete. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.wheel::-webkit-scrollbar {
  display: none;
}

.wheel:focus-visible {
  outline: none;
}

.wheel-option {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--wheel-row);
  scroll-snap-align: center;
  font-size: 21px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
  transition: color 0.15s ease;
}

/*
| The chosen row, and only it, is at full strength. The rest stay legible —
| this is a list you steer by reading what is coming, not a single value with
| decoration either side of it.
*/
.wheel-option.is-selected {
  color: var(--ink);
  font-size: 24px;
}

/*
| The band frames the middle row across both wheels at once, so the pair reads
| as one time rather than as two independent numbers.
*/
.wheel-band {
  position: absolute;
  left: 0;
  right: 0;
  top: calc((var(--wheel-row) * (var(--wheel-rows) - 1)) / 2);
  height: var(--wheel-row);
  border-top: 1px solid var(--line-strong);
  border-bottom: 1px solid var(--line-strong);
  background: var(--surface-2);
  border-radius: var(--radius-sm);
  pointer-events: none;
}

/* Focus lands on a wheel, which has no border of its own to light up. */
.wheels:focus-within .wheel-band {
  background: var(--surface-3);
}

.wheel-readout {
  margin-top: 14px;
  text-align: center;
}

/*
| The field that opens the sheet. A button wearing a text box's clothes: it
| holds a value and sits in the same grid as the number boxes around it, so it
| should not look like a different kind of thing.
*/
.duration-control {
  display: flex;
  align-items: center;
  text-align: left;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

/*
| An override that inherits shows the length it would take rather than a blank
| button, faded to the same grey a placeholder gets in a box beside it — the
| value is there to read, it is just not this set's own.
*/
.duration-control.is-placeholder {
  color: var(--ink-faint);
}

/* Session runner --------------------------------------------------------- */
/*
| One exercise per screen, with every one of its sets on it at once.
|
| The set list is the whole interface: each row shows what you managed last
| session, what you are about to log, and — once logged — whether you hit the
| target. Colour is the status: green made it, red fell short (by how much),
| neutral still to do. Every row stays editable, so correcting set 1 after
| finishing set 3 is two taps and no navigation.
*/
body.runner-body {
  padding-bottom: env(safe-area-inset-bottom);
}

/*
| No padding at the top: the header is stuck up there and has to reach the app
| bar, so the gap under the bar belongs to the header's own first line instead.
*/
.runner {
  max-width: 560px;
  margin: 0 auto;
  padding: 0 16px calc(20px + env(safe-area-inset-bottom));

  /*
  | Down to the bottom of the screen even when what is on it does not reach
  | there. A warm-up screen, or a station of two sets, leaves a stretch of empty
  | page below it — and empty page is exactly where a thumb comes to rest, so a
  | swipe made there was landing on the document and reaching no listener at
  | all. The gesture has to be available everywhere the screen looks like the
  | session, which means the session has to be the whole screen.
  |
  | The subtraction is the one in app.css that gives the body its padding: the
  | fixed bar at the top, the banner under it when there is one, and the insets
  | at both ends.
  */
  min-height: calc(
    100dvh - var(--appbar-h) - env(safe-area-inset-top) - env(safe-area-inset-bottom)
  );
}

.has-release .runner {
  min-height: calc(
    100dvh - var(--appbar-h) - var(--release-h) - env(safe-area-inset-top) -
      env(safe-area-inset-bottom)
  );
}

/* Stepping between stations ------------------------------------------------ */
/*
| The station on screen leaves the way the thumb went, and the next one follows
| it in. Drawn by the browser from two snapshots of the page taken either side
| of the redraw — `stepScreen` in public/runner.js starts it and says which way
| it is going, which is the one thing the browser cannot work out for itself.
|
| The bar at the top is lifted out of the picture by a name of its own: it
| carries the training's name and the way out of the runner, and neither of
| those is stepping anywhere. The release banner goes with it for the same
| reason. Everything left unnamed is the session, and the session is what
| slides.
|
| The page's own background is in neither snapshot — it is painted on the
| canvas underneath and simply stays there — which is what makes two pages
| sliding across it read as one page turning rather than as two pictures
| chasing each other.
*/
.runner-body .app-bar {
  view-transition-name: app-bar;
}

.runner-body .release-bar {
  view-transition-name: release-bar;
}

@keyframes step-out-left {
  to {
    transform: translateX(-100%);
  }
}

@keyframes step-in-right {
  from {
    transform: translateX(100%);
  }
}

@keyframes step-out-right {
  to {
    transform: translateX(100%);
  }
}

@keyframes step-in-left {
  from {
    transform: translateX(-100%);
  }
}

/*
| `mix-blend-mode` is the line that has to be written: left alone, the browser
| cross-fades the two snapshots into each other, and two pages sliding past one
| another are not fading into anything — one is simply in front of the other.
*/
::view-transition-old(root),
::view-transition-new(root) {
  /* Long enough to read as a movement, short enough never to be waited on. */
  animation-duration: 260ms;
  animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
  animation-fill-mode: both;
  mix-blend-mode: normal;
}

html[data-step='forward']::view-transition-old(root) {
  animation-name: step-out-left;
}

html[data-step='forward']::view-transition-new(root) {
  animation-name: step-in-right;
}

html[data-step='back']::view-transition-old(root) {
  animation-name: step-out-right;
}

html[data-step='back']::view-transition-new(root) {
  animation-name: step-in-left;
}

/*
| The wall at the end. A drag past the last station has nowhere to go, because
| finishing is a button and no gesture reaches it — so the page gives as far as
| it can and comes back, which is the difference between a screen refusing and
| a screen that never noticed.
|
| Sticky rather than fixed is what makes this possible: the head and the rest
| band travel with the transform instead of standing still while the list slides
| out from under them.
*/
@keyframes runner-bump-forward {
  35% {
    transform: translateX(-22px);
  }

  100% {
    transform: translateX(0);
  }
}

@keyframes runner-bump-back {
  35% {
    transform: translateX(22px);
  }

  100% {
    transform: translateX(0);
  }
}

.runner.is-bumped-forward {
  animation: runner-bump-forward 280ms cubic-bezier(0.32, 0.72, 0, 1);
}

.runner.is-bumped-back {
  animation: runner-bump-back 280ms cubic-bezier(0.32, 0.72, 0, 1);
}

@media (prefers-reduced-motion: reduce) {
  .runner.is-bumped-forward,
  .runner.is-bumped-back {
    animation: none;
  }
}

.runner-progress {
  padding-top: 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/*
| The path ------------------------------------------------------------------
|
| The session as a row of rounded steps under its own title: the warm-up, one
| per exercise, the finish, in the order they are performed and each one a tap
| away.
|
| It takes the whole width where the session is short enough to. Two or three
| exercises share the row out between their steps rather than lining up on the
| left and stopping where they run out — what is left of the row is not room
| for more exercises, it is the rest of this one's. Anything longer holds the
| floor below and scrolls, which is the trade set out there.
|
| The steps touch, and each one ends in a point sunk into the next. That is the
| half of the strip that says "and then": a row of separate pills is a set of
| choices, and this is an order of events.
|
| The triangle is filled, and a hairline runs down its two edges so that two
| steps wearing the same colour can still be told apart — without it the
| untouched half of a session is one long bar with numbers floating in it.
|
| A hairline and not the band this had twice before. The first go was a rotated
| square with a radius on the tip, the second a whole second triangle 2px behind
| in the page's own colour, and both read as an arrow *drawn* between the steps
| rather than as one step ending. At 1px it is the edge of the step rather than
| a thing sitting on the strip.
|
| Blue, and its own colour rather than one borrowed from `--rise` or `--mine`.
| Those two are the same value today and mean particular things — a target that
| has moved, the training being edited — and a value two meanings share is a
| value that gets changed for one of them and quietly moves the other.
|
| Cut rather than turned, too. A rotated box is antialiased by the compositor
| and stairs down a 45° edge; a `clip-path` triangle is one straight edge each
| way, cut cleanly, and the pseudo-element it is cut from has no radius or
| border of its own to lose.
|
| Only the two ends of the row are rounded. The corners inside it are where the
| points go.
|
| Bled to the edges of the header. Past about two exercises the steps hit the
| width below and the strip scrolls sideways rather than wrapping to a second
| row, which would cost the set list a line of height on every screen of every
| session. public/runner.js centres the step you are on whenever it redraws, so
| the scroll is for reaching the far end, not for finding where you are.
|
| Colour is the state: solid green once every row of an exercise is logged,
| amber while some of it is, neutral until it has been touched. The one you are
| standing on is filled whatever it holds, and carries a bar under its number.
*/
.runner-path {
  /* The hairline down the edge of each step; see the note above. */
  --seam: #4c9ff0;

  /* The step centring in runner.js measures against this. */
  position: relative;

  display: flex;

  /*
  | Four points more below than above, to sit the same distance from both.
  | Above the strip is a line of 11px capitals, and a line of type carries its
  | leading with it — the ink stops about four points short of the bottom of
  | its own box. Below is a picture or a title, which starts where its box
  | does. Equal margins measured the same distance and looked wrong.
  */
  margin: 8px -16px 12px;

  padding: 0 16px;
  overflow-x: auto;

  /* A scrollbar under the strip would read as a second progress line. */
  scrollbar-width: none;
}

.runner-path::-webkit-scrollbar {
  display: none;
}

.path-step {
  /*
  | Share the row, down to a floor: 85px. A step is read as a chevron and then a
  | cell — `>` then `1` — so the floor is the chevron's 21 points plus a cell of
  | 64: half as wide again as the strip is tall, and six times the width of the
  | digit standing in the middle of it.
  |
  | Below the floor the steps stop shrinking and the strip scrolls instead,
  | which past two exercises it always will. That is the trade taken here, twice
  | over: a cell that can be read and hit at arm's length between sets is worth
  | more than the whole session being on screen at once, and the far end of a
  | long one was always going to be a drag away regardless.
  */
  flex: 1 1 auto;
  min-width: 85px;

  position: relative;
  height: 42px;
  display: inline-flex;
  align-items: center;
  justify-content: center;

  /*
  | The chevron's own width, kept clear on the left. What is left of the step is
  | the cell the number is centred in — see `.path-mark`.
  */
  padding: 0 0 0 21px;

  border: 0;
  font: inherit;
  font-size: 15px;
  font-weight: 700;

  /* So 1 and 11 sit at the same width. */
  font-variant-numeric: tabular-nums;

  color: var(--ink-dim);
  background: var(--surface-2);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/*
| The point, and the line down its edge.
|
| Full height and half as wide, which puts a right angle at the tip and 45° down
| each edge. The fill is `inherit`, so it is whatever the step it grows out of is
| currently wearing; the line is the same triangle in blue, sitting 1.5px to its
| right and painted underneath, so what shows of it is the 1px that the fill
| leaves uncovered along the two leading edges.
|
| Both are lifted above the step that follows them. Without that the next step —
| later in the source, and so painted later — would cover the point of the one
| before it.
*/
.path-step::before,
.path-step::after {
  content: '';
  position: absolute;
  z-index: 1;
  top: 0;

  /* Half the step's height of point, and 1px of base standing inside the step
     it grows out of: two boxes meeting exactly edge to edge leave a hairline of
     whatever is behind them on a screen that does not land on whole pixels. */
  width: 22px;
  height: 100%;

  clip-path: polygon(0 0, 100% 50%, 0 100%);
}

/* 1.5px to the right of the fill, which leaves 1px of it showing down the
   edge. A fixed offset and not a share of the width: the line is a line at any
   size the strip is drawn at. */
.path-step::before {
  right: -22.5px;
  background: var(--seam);
}

.path-step::after {
  right: -21px;
  background: inherit;
}

/* The two ends of the row: rounded, and the last one points at nothing. */
/* Nothing comes into it, so the whole of it is cell. */
.path-step:first-child {
  border-radius: 12px 0 0 12px;
  padding-left: 0;
}

.path-step:last-child {
  border-radius: 0 12px 12px 0;
}

.path-step:last-child::before,
.path-step:last-child::after {
  content: none;
}

/*
| The number, squarely in the middle of its cell.
|
| The strip reads as chevron, cell, chevron, cell — `> 1 > 2 >` — and the number
| belongs to the cell. So the chevron coming in is padded off above and the
| number is centred in what remains, which puts it the same distance from the
| tip on its left as from the foot of the one on its right, and never inside
| either of them. Two earlier goes at this had it a few points off centre and
| then, correcting for that, sitting over the chevron itself; a number on a
| chevron belongs to neither of the steps the chevron divides.
*/
.path-mark {
  /* The full height of the step, so the bar under the one you are on (below)
     sits at the same height under a digit as under an icon. */
  position: relative;
  align-self: stretch;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/*
| The tint, laid on the page's own colour rather than left to fall on whatever
| is behind it.
|
| `--partial-bg` is 14% of a hue and 86% nothing, which is what every other
| tinted thing in the app wants. Here it is wrong twice over: the point is drawn
| on top of the step that follows it, so a translucent one shows that step
| through itself, and the hairline sits directly behind the point, so a
| translucent point turns the whole wedge blue. The flat gradient is the tint as
| a layer, which leaves `background-color` free to be the page underneath — and
| `background: inherit` on the point copies both.
*/
.path-step.is-part {
  color: var(--partial);
  background: linear-gradient(var(--partial-bg), var(--partial-bg)) var(--bg);
}

/*
| Where you are: filled rather than tinted, in ink until something is logged and
| in amber while some of it is.
*/
.path-step.is-current {
  color: var(--bg);
  background: var(--ink);
}

.path-step.is-current.is-part {
  background: var(--partial);
}

/*
| Finished: the full green, whether you are on it or not.
|
| It used to be the tint once you moved on, keeping the fill for the step you
| were on — which dimmed an exercise at the very moment it was finished, and
| turned the green you had just earned into the faintest thing on the strip.
| The green now stays as loud as the set rows that earned it; after the rule
| above, so it wins over the ink of the step you are on.
*/
.path-step.is-done {
  color: var(--bg);
  background: var(--ok);
}

/*
| The bar under the step you are on. A finished step and the finished step you
| are standing on are now the same green, so fill alone no longer says where you
| are; the bar does, in whatever colour the number is, on every state. On a
| superset it is under every member, as the fill is.
*/
.path-step.is-current .path-mark::after {
  content: '';
  position: absolute;
  bottom: 6px;
  left: 50%;
  width: 16px;
  height: 3px;
  margin-left: -8px;
  border-radius: 2px;
  background: currentColor;
}

.path-step:focus-visible {
  outline: 2px solid var(--line-strong);
  outline-offset: 2px;
}

/* Warm-up ----------------------------------------------------------------- */
/*
| The screen the session opens on: how the day feels on the left, the clock on
| the right.
|
| One third and two thirds, and it does not stack. A phone held in one hand is
| 360-odd points wide, which leaves the question about 110 of them — enough for
| a column of faces with a word under each, and not enough for anything else,
| which is exactly why the question is asked in faces. The clock keeps the
| other two thirds because it is the half read from across the room.
|
| The clock borrows the rest timer's language — green while it runs, amber once
| it is over — because it is making the same promise: this is the time you
| asked for, and you will be told when it is up.
*/
.warmup-split {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: 16px;
  margin-top: 12px;
}

/*
| The timer column fills its half of the row rather than ending where its
| content happens to end: six faces stacked up are taller than a clock and two
| rows of buttons, and the difference was dead screen between the minutes and
| the way out. The lengths take it — see below.
*/
.warmup-timer {
  display: flex;
  flex-direction: column;
}

/*
| The clock, its note and the lengths are one block, and the block sits in the
| middle of what the column has left under the heading — the auto margin here
| and the one on the lengths below split the spare height between them.
|
| Nine answers stacked up on the left are taller than a clock and two rows of
| buttons, and that difference has to go somewhere. Above and below the whole
| group is where it reads as a screen laid out on purpose; all of it under the
| number left the lengths on the floor of the column, and none of it at all
| left a dead stretch between them and the way out.
*/
.warmup-clock {
  margin: auto 0 22px;
  text-align: center;
}

/*
| Big, and as big as the column allows rather than as big as it used to be: at
| two thirds of a narrow phone, 76px of tabular digits would set "10:00" wider
| than the space it has.
*/
.warmup-value {
  font-size: clamp(42px, 13vw, 76px);
  font-weight: 200;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.03em;
  line-height: 1;
  color: var(--ok);
}

/* Nothing picked yet: a clock, but not a running one. */
.warmup-clock.is-idle .warmup-value {
  color: var(--ink-faint);
}

.warmup-clock.is-over .warmup-value {
  color: var(--partial);
}

.warmup-bar {
  height: 3px;
  max-width: 240px;
  border-radius: 2px;
  background: var(--surface-3);
  overflow: hidden;
  margin: 20px auto 0;
}

.warmup-bar-fill {
  width: 0;
  height: 100%;
  background: var(--ok);
  transition: width 0.3s linear;
}

.warmup-clock.is-over .warmup-bar-fill {
  background: var(--partial);
}

/*
| One per column, and the same size in both: they are two headings of equal
| standing, not a heading and a subheading. Full ink, above what they name, so
| neither column can be read as the other's caption.
*/
.warmup-title {
  font-size: 15px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
  margin: 0;
  text-align: center;
}

/*
| `--ink-dim` because `--ink-muted`, which this asked for until now, is not a
| token this app has: the invalid value fell through to inherited full ink, and
| the one line on the screen that is meant to be quiet was the brightest thing
| under the clock.
*/
.warmup-note {
  font-size: 12px;
  color: var(--ink-dim);
  margin: 0 0 12px;
  text-align: center;
}

/*
| Three across, which keeps every target a thumb wide on the narrowest phone
| and still leaves the clock above the fold.
|
| Shorter than they were: they had the whole width to themselves once, and at
| two thirds of it a 92px row would push the way out of the screen under the
| fold. 68px is still twice a list row and still hittable without looking,
| which is what the height was for.
*/
.warmup-options {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;

  /** The other half of the centring; see `.warmup-clock`. */
  margin-bottom: auto;
}

/*
| Not quite the 92px they had when this screen was one column wide, because at
| two thirds of a narrow phone that is a column of tall rectangles. Still twice
| a list row, which is what the height was for: these are tapped standing up,
| one-handed, by somebody about to pick something heavy up.
*/
.warmup-options .btn {
  min-height: 80px;
  padding: 10px 4px;
}

.warmup-next {
  margin-top: 16px;
}

/* How do you feel --------------------------------------------------------- */
/*
| Nine faces in a column, best at the top: three feelings, a rule, and six
| reasons.
|
| Read as a dial rather than word by word: the order is the whole scale, so
| the answer is a tap at a height rather than a sentence anybody has to finish
| reading. The word under each face is what makes it unambiguous — and what a
| screen reader gets, since the face itself is hidden from it.
|
| Tighter than a row of buttons anywhere else in the app, because nine of them
| have to stand in a third of a phone without pushing the way out of the
| session below the fold. Still 44px of thumb, which is the floor that matters.
|
| The chosen one wears the app's green, like the warm-up length already
| running and the session already under way on the trainings list: it reports
| a state rather than offering one.
*/
.feeling-options {
  display: grid;
  gap: 3px;
}

.feeling-option {
  flex-direction: column;
  gap: 2px;
  min-height: 0;
  padding: 5px 3px;
}

.feeling-face {
  font-size: 18px;
  line-height: 1.1;
}

.feeling-label {
  font-size: 10px;
  line-height: 1.15;
  letter-spacing: 0;
  color: var(--ink-dim);
  text-wrap: balance;
}

/*
| Where the feelings end and the reasons begin. A hairline and no caption: the
| two halves are obvious once they are apart, and a heading in a column this
| narrow would cost more height than the answers under it are worth.
*/
.feeling-rule {
  height: 1px;
  margin: 3px 10px;
  background: var(--line);
}

.feeling-option.is-chosen {
  border-color: var(--ok);
  background: var(--ok-bg);
}

.feeling-option.is-chosen .feeling-label {
  color: var(--ok);
  font-weight: 600;
}

/*
| The opposite of the warm-up's note, and the reason both are on the screen:
| the minutes are not kept and this answer is.
*/
.feeling-note {
  font-size: 11px;
  line-height: 1.3;
  text-align: center;
  margin: 6px 0 0;
}

/* Exercise header --------------------------------------------------------- */
/*
| Stuck under the app bar for the whole of the exercise.
|
| Every set row looks like every other one, and a list of them is long enough to
| push its own title off the top of the screen — which is the moment a load gets
| typed into the wrong exercise. So which exercise, and where in the session it
| is, are simply never off screen.
|
| Opaque and bled to the edges, like the rest band at the other end: the set
| list runs underneath it rather than beside it.
*/
.runner-head {
  position: sticky;
  top: calc(var(--appbar-h) + env(safe-area-inset-top));
  z-index: 10;
  margin: 0 -16px;
  padding: 0 16px 9px;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}

/*
| Picture and title side by side. Centred on each other rather than aligned to
| the top, because an exercise whose name wraps to two lines would otherwise
| leave the square hanging off the first one.
*/
.runner-title {
  display: flex;
  align-items: center;
  gap: 11px;
}

/*
| An exercise with no drawing starts the line with its own name, and a line of
| type carries about six points of empty box above its first capital where a
| picture starts where its box does. Trimmed back off, so the path above sits
| the same distance from a title as it does from a picture.
*/
.runner-title.is-bare {
  margin-top: -6px;
}

.runner-title-text {
  min-width: 0;
}

.runner-exercise {
  font-size: 26px;
  line-height: 1.15;
  font-weight: 600;
  letter-spacing: -0.02em;
}

/*
| The same trim again, for the superset screen — there the path is followed by
| this line rather than by a title, and it has the same leading to give back.
*/
.runner-head > .runner-meta {
  margin-top: -5px;
}

.runner-meta {
  margin-top: 3px;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/*
| "Last time 😖 Still sore" — see `lastFeelingView` in runner.js. The lead and
| the answer wrap as two pieces, and the face is drawn larger than the capitals
| round it: at 11px it is a coloured dot, and the face is the part read first.
*/
.runner-last-feeling {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 0.6em;
}

.runner-last-face {
  font-size: 14px;
  letter-spacing: 0;
}

.runner-load {
  font-size: 15px;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-dim);
}

.runner-load b {
  color: var(--ink);
  font-weight: 600;
}

/* Movement illustrations --------------------------------------------------- */
/*
| The start and the finish of one rep, from the exercise dataset. SPIKE — two
| exercises are wired up; see public/exercise_art.js.
|
| The art arrives as black line work on a white ground, which on a screen this
| dark is a pair of lit windows. It is left that way regardless, because the
| drawings carry their anatomy in grey shading and inverting them crushes the
| greys into the background — the figure survives, the muscle being worked does
| not, and that is the half worth looking at. `filter: invert(1)
| hue-rotate(180deg)` here is the whole of the darker treatment if the glare
| turns out to matter more than the detail.
|
| One square holding both poses and cross-fading between them, at one size
| wherever it appears — the picker, the runner's header, and the lists a person
| wrote themselves. It was three sizes, each argued from what its screen could
| spare, and the argument was wrong in the same direction every time: a picture
| you have to squint at is worse than none, and every square small enough to be
| free was one nobody looked at. So the screens give way to the square instead.
|
| Two of them had to change shape to do it, and the change is the honest half
| of this. The picker's rows are now tall enough that about three fit a phone
| screen. The builder's rows stack — see `.slot-head-main:has()` — because at
| this size a picture beside a name leaves the name nothing. The runner's
| header is the one that took it unaltered, with the title wrapping beside the
| square; it is also the one paying most, since it is sticky and every pixel of
| it is one the set list loses on every screen of every set.
|
| They do not all run at the same speed, because they are not looked at the
| same way. In the picker you are scanning a list and each square gets a
| glance, so it holds each pose about three quarters of a second — slower and
| you would scroll past before it had shown you the second half of the
| movement. Everywhere else nobody is scanning: the picture sits beside a name
| that is already chosen, and anything brisk enough to be useful in a list is a
| flicker at the edge of vision while you are trying to read a load off the row
| below it. So those hold each pose for two seconds instead. That, and nothing
| about size, is all `.is-header` and `.is-compact` mark now.
|
| The cross-fade itself stays short in both. Held long, two line drawings at
| half opacity read as one double-exposed figure rather than as a movement.
*/
.movement-thumb {
  position: relative;
  flex: 0 0 auto;
  width: 176px;
  height: 176px;
  border-radius: 14px;
  overflow: hidden;
  background: #fff;
}

/*
| A photo an admin took, rather than a drawing. The white above is the
| drawings' own ground carried to the square's edge, so it never shows as an
| edge; a photo brings no ground of its own, and the room either side of one
| that is not square is black — the app's background — rather than a white bar
| belonging to nothing.
*/
.movement-thumb.is-photo {
  background: #000;
}

.movement-thumb-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/*
| Only the finishing pose animates: it sits over the starting one and fades
| itself in and out, so the two never both fade at once and the white ground
| never shows through between them.
*/
.movement-thumb-frame.is-end {
  opacity: 0;
  animation: movement-cycle 1.8s infinite;
}

/*
| The slow cycle, for every square outside the picker's list — which is the
| whole of what these two classes still say, now that the size is shared. It
| needs percentages of its own rather than just a longer duration on the one
| above: stretching those to four and a half seconds would stretch the
| cross-fade with them, to something like four tenths of a second of two
| half-transparent figures overlapping.
*/
.movement-thumb.is-header .movement-thumb-frame.is-end,
.movement-thumb.is-compact .movement-thumb-frame.is-end {
  animation-name: movement-cycle-slow;
  animation-duration: 4.5s;
}

@keyframes movement-cycle {
  0%,
  40% {
    opacity: 0;
  }
  48%,
  90% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

/*
| Two seconds on each pose either side of a fifth-of-a-second cross-fade. The
| percentages look lopsided because they are: the fade back at the end of the
| cycle has no hold after it, so it takes the whole of the remainder.
*/
@keyframes movement-cycle-slow {
  0%,
  46% {
    opacity: 0;
  }
  50.5%,
  96% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

/*
| Left on the setup pose. A list of exercises quietly flexing is exactly the
| kind of motion this setting is asked for.
*/
@media (prefers-reduced-motion: reduce) {
  .movement-thumb-frame.is-end {
    animation: none;
  }
}

/* Set list ----------------------------------------------------------------- */
/*
| A row is a line of history, then a line for each quantity the set is made of:
| what you managed on this set last time, then the reps, then the load, then
| the button that logs the lot.
|
| Last session gets its own line rather than a column because it is a whole
| sentence — "6 reps of 70 kg" — and squeezing both numbers into a cell would
| lose the weight, which is half of what you are trying to beat.
|
| The set itself is one line, written the way it is said: "12 reps × 60 kg".
| Side by side under a pair of headings the two figures were not tellable
| apart — two numbers of the same size with a small word under each, and the
| headings scrolled off by the third set. Each number carries its unit against
| its own digits now, and the × that was always implied is on the screen.
|
| A per-side exercise gets *two rows per set*, one for each arm or leg, so
| every ± belongs to exactly one load. A single row with two weights behind one
| button could never say which side it was about to change.
*/
/*
| The comment on the exercise, between the sticky header and the first set:
| read once on arriving, and free to scroll away with the list. A rule down
| its side rather than a box, so it reads as a quotation and not as one more
| card to press.
*/
.runner-comments {
  display: grid;
  gap: 8px;
  margin-top: 14px;
}

.runner-comment {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0;
  padding: 2px 0 2px 11px;
  border-left: 2px solid var(--line-strong);
  font-size: 14.5px;
  line-height: 1.45;
  color: var(--ink-dim);
}

/* In a superset, the rule takes the member's colour, beside its letter. */
.runner-comment[class*='is-member-'] {
  border-left-color: var(--member);
}

.runner-comment .member-badge {
  margin-top: 1px;
}

.runner-comment-text {
  min-width: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.set-list {
  margin: 18px 0 8px;
}

/*
| One card is one set. On a per-side exercise it holds two rows — a left and a
| right — divided by a hairline, so the pair reads as the single set it is
| while each half keeps its own numbers, its own load and its own outcome.
*/
.set-card {
  margin-bottom: 8px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* Clip the rows' status tints to the rounded corners. */
  overflow: hidden;
}

.set-card.is-current {
  border-color: var(--line-strong);
}

/* The card takes the colour of the set as a whole; each half keeps its own. */
.set-card.is-ok {
  border-color: rgba(63, 191, 127, 0.4);
}

.set-card.is-fail {
  border-color: rgba(229, 72, 77, 0.4);
}

/* Matched last time exactly: not a loss, not yet a gain. */
.set-card.is-even {
  border-color: rgba(226, 160, 63, 0.4);
}

/*
| The set on the clock. The band at the foot of the screen names it in words;
| this is so it can be found in the list without reading them, and it outranks
| the tints above because a hold under way is the only thing on this screen
| that is happening rather than recorded.
*/
.set-card.is-holding {
  border-color: var(--ink-faint);
  box-shadow: 0 0 0 1px var(--line);
}

.set-row + .set-row {
  border-top: 1px solid var(--line);
}

/*
| The figures on the left, the buttons that end the set on the right, full
| height beside them: one set is one thing to log, however many numbers went
| into it.
*/
.set-body {
  display: flex;
  align-items: stretch;
  gap: 8px;
}

/*
| The set on one line — the ↓, the count, the ×, the load — and under each
| number what the plan asked of it.
|
| A grid rather than a row of stacks, because the two figures have to share a
| baseline and only one of them has anything under it: laid out as two columns
| of their own, the load floated half a line away from the number it is
| multiplied by.
|
| The button has a column to itself rather than a place inside the count
| figure, because the figure is allowed to wrap — see `.set-row .set-figure` —
| and inside it the button was the first thing the wrap took, leaving an arrow
| stranded on a line above the number it belongs to.
|
| Centred rather than spread, so a set of one figure and a set of two are the
| same object in the middle of the row instead of two different shapes.
*/
.set-quantities {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  grid-template-areas:
    'nudge count times load '
    '.     cnote .     lnote';
  grid-template-columns: auto auto auto auto;
  justify-content: center;
  align-items: center;
  align-content: center;
  column-gap: 7px;
  row-gap: 4px;
}

/* Nothing to multiply by: the count is the whole of the set. */
.set-quantities.is-unweighted {
  grid-template-areas:
    'nudge count'
    '.     cnote';
  grid-template-columns: auto auto;
}

/*
| A hold has no button to lead it — it is set on the wheels — and the column
| goes with it rather than standing empty, or the gap beside nothing would pull
| the whole line off centre.
*/
.set-quantities.is-timed {
  grid-template-areas:
    'count times load '
    'cnote .     lnote';
  grid-template-columns: auto auto auto;
}

.set-quantities.is-timed.is-unweighted {
  grid-template-areas:
    'count'
    'cnote';
  grid-template-columns: auto;
}

.set-figure.is-count {
  grid-area: count;
}

.set-figure.is-load {
  grid-area: load;
}

.set-note.is-count {
  grid-area: cnote;
}

.set-note.is-load {
  grid-area: lnote;
}

/*
| The × that joins them. Smaller than either figure — it is the punctuation and
| they are the sentence — but in the row's own ink, because it is the thing that
| says the two numbers are one set and not a list.
*/
.set-times {
  grid-area: times;
  font-size: 17px;
  line-height: 1;
  color: var(--ink);
}

/*
| Under one of the numbers: what the plan asked of it, and what today made of
| it. Under the figure rather than at the end of the row, because a pair of
| bare deltas sat together would need decoding all over again — which is the
| thing this row was rebuilt to stop.
*/
.set-note {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 5px;
  line-height: 1.2;
}

/* The target, where the figure has been moved off it. */
.set-aim {
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
  white-space: nowrap;
}

/*
| What this session has managed on this axis, at the end of the axis it is
| about. Progress is as often a heavier bar as an extra rep, and each line
| reports its own.
|
| Green for a number that reached its target, amber for one that is short of
| it without having gone backwards, red for one that is short of it and down
| on last session. It carries the same three colours as the tint behind the
| row, and for the same reason: green is arriving, and a set that gained on
| last time while missing what it was set has not arrived.
*/
.set-delta {
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The buttons that end the row, as tall as the lines they sit beside. */
.set-actions {
  flex: none;
  display: flex;
  align-items: stretch;
  gap: 8px;
}

.set-delta.is-hit {
  color: var(--ok);
}

.set-delta.is-partial {
  color: var(--partial);
}

.set-delta.is-loss {
  color: var(--fail);
}

/*
| The same cell in the "next time" prompt, where it is reporting a change to a
| target rather than a result, and up is simply good.
*/
.set-delta.is-up {
  color: var(--ok);
}

.set-delta.is-down {
  color: var(--fail);
}

.set-delta.is-flat {
  color: var(--ink-faint);
  font-weight: 400;
}

/*
| Last session and what today asks of it, on their own line above the controls.
*/
.set-previous {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-bottom: 8px;
  font-size: 12.5px;
  color: var(--ink-faint);
}

/*
| The sentence wraps inside its own column rather than the line wrapping
| between them. A per-side row carrying two bracketed figures is long enough to
| need a second line, and letting the flex line break instead left the divider
| hanging off the end of the first one with nothing after it.
*/
.set-previous > :last-child {
  min-width: 0;
}

/*
| The numbers are what the line is for, so they carry the weight and the prose
| around them stays quiet.
*/
.set-previous b {
  font-weight: 700;
  color: var(--ink-dim);
}

/*
| How far the plan has moved since that session — "(+1)", "(−2.5)".
|
| Blue up, amber down, and never green or red: this is the brief, not the
| result, and the two lines have to stay tellable apart at a glance.
*/
.set-shift {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.set-shift.is-up {
  color: var(--rise);
}

.set-shift.is-down {
  color: var(--partial);
}

.set-number {
  flex: none;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}

.set-row.is-ok .set-number {
  color: var(--ok);
}

.set-row.is-fail .set-number {
  color: var(--fail);
}

.set-row.is-even .set-number {
  color: var(--partial);
}

/* Separates the set number, the side and the history without boxing them. */
.set-divider {
  flex: none;
  color: var(--ink-faint);
  opacity: 0.45;
}

/*
| Which arm or leg, sat next to the set number rather than in a column of its
| own: a whole column to hold four letters cost more width than the controls
| could spare.
*/
.set-side {
  flex: none;
  font-weight: 700;
  color: var(--ink-dim);
}

.set-row.is-ok .set-side {
  color: var(--ok);
}

.set-row.is-fail .set-side {
  color: var(--fail);
}

.set-row.is-even .set-side {
  color: var(--partial);
}

.set-row {
  padding: 10px 12px;
}

/* Still to do: no colour at all, so the eye goes to what is left. */
.set-row.is-pending {
  color: var(--ink);
}

.set-row.is-current {
  background: var(--surface-2);
}

.set-row.is-ok {
  background: var(--ok-bg);
}

.set-row.is-fail {
  background: var(--fail-bg);
}

.set-row.is-even {
  background: var(--partial-bg);
}

/*
| One stepper: − the number + on one line, and the caption on its own line
| under all three. Under the number alone it had about 55px to live in, which
| is not a sentence; spanning the cell it gets the whole column.
|
| The progression prompt's shape. A set row uses the figure without the cell
| around it and writes its unit on the line instead; see `.set-unit`.
*/
.set-cell {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.set-figure {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: 0;
}

/*
| Tighter on a set row, where the unit is one of the things in the line — and
| able to wrap, which is how the worst line on this screen stays readable: a
| per-side load inside a superset of four, on a 320px phone, writes "137.5"
| and "kg/side" into about 90px. The unit drops under its own number there
| rather than the number being shaved to fit beside it. Clipping a figure is
| the one thing this row may never do.
*/
.set-row .set-figure {
  gap: 5px;
  flex-wrap: wrap;
}

/*
| Leading the count: ↓ 12 reps × 60 kg, all on one line.
|
| It was a − on a line of its own under the number, which cost every row about
| fifty pixels of height and still did not line up with the box it moves — it
| was centred under "12 reps", the number and its unit together, and so sat off
| to the right of the number. Against the box there is nothing left to align.
|
| Narrower than the 44px square it replaced, because width here comes out of
| the figures beside it, and taller than it is wide: this is the one control on
| the screen aimed at without looking, by a hand that is still shaking, and the
| line it is on is the easiest thing on the row to find.
*/
.set-row .step-btn {
  grid-area: nudge;
  width: 34px;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/*
| The unit, against the digits: "12 reps", "60 kg". This is the half of the
| figure that says what the figure is, and the number without it is a number
| that could have been either of them.
|
| Legible at arm's length, and in the row's own ink rather than the number's —
| a load that has turned green did so because the set went well, and "kg" is
| not a result.
*/
.set-unit {
  flex: none;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.1;
  color: var(--ink);
  white-space: nowrap;
}

.step-btn {
  width: 40px;
  height: 44px;
  flex: none;
  padding: 0;
  min-height: 0;
  font-size: 20px;
  font-weight: 300;
  line-height: 1;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--ink);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.step-btn:hover {
  background: var(--surface-3);
}

.step-btn:disabled {
  opacity: 0.28;
  pointer-events: none;
}

/*
| The line is a number, its unit, a ×, another number and its unit, and on a
| phone this narrow there is not room for all five at full size: a superset of
| four leaves them about 180px to divide between them, and a per-side load
| writes "kg/side" where the others write "kg".
|
| So everything in the line comes down a size together, rather than one part of
| it being sacrificed to the rest. It is the same row, smaller.
*/
@media (max-width: 400px) {
  .step-btn {
    width: 28px;
  }

  .set-row .step-btn {
    width: 30px;
    height: 38px;
  }

  .set-body,
  .set-actions {
    gap: 6px;
  }

  .set-row .set-figure {
    gap: 3px;
  }

  .set-quantities {
    column-gap: 5px;
  }

  .set-row .set-value {
    font-size: 22px;
  }

  .set-unit {
    font-size: 13px;
  }

  .set-log,
  .set-run {
    width: 52px;
  }
}

.set-row.is-ok .step-btn,
.set-row.is-fail .step-btn,
.set-row.is-even .step-btn {
  background: rgba(255, 255, 255, 0.04);
}

/*
| The number is an input, not a label: nine taps on − to correct a badly
| misjudged set is nine taps too many.
|
| Boxed, in the same hairline the buttons beside it are drawn in. An underline
| was the whole of the chrome before, and under a figure this large it read as
| a rule rather than as a field — on a row where the only boxed things were the
| buttons, the number between them looked like the one part of the set that
| could not be touched by hand.
*/
.set-value {
  /*
  | The border and the padding, as one number, because runner.js has to add it
  | to the width it measures in `ch`: the box is border-box, so a bare `ch`
  | width would take this out of the digits rather than out of the line —
  | which is the clipping that measurement exists to stop. Declared here, next
  | to the padding it describes, so the two cannot drift apart.
  */
  --set-chrome: 12px;

  /* Takes what the button beside it leaves, and shrinks rather than pushes. */
  flex: 1 1 auto;
  width: 100%;
  min-width: 0;
  padding: 2px 5px 3px;
  font-family: inherit;
  /* Comfortably over the 16px below which iOS zooms the page on focus. */
  font-size: 25px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  text-align: center;
  color: inherit;
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  -webkit-appearance: none;
  appearance: none;
}

.set-value:focus {
  outline: none;
  border-color: var(--ink);
  background: rgba(255, 255, 255, 0.06);
}

/*
| On a set row the box is cut to the digits in it — runner.js sets the width in
| `ch` — so that the unit beside it is against the number rather than adrift at
| the end of a full-width box.
|
| It is also what stopped the count being clipped: taking whatever the two
| buttons left it, the figure had 27px in the narrowest column on this screen
| and drew "15" as "1".
*/
.set-row .set-value {
  /* Never shrinks: the line gives way around it, see `.set-row .set-figure`. */
  flex: 0 0 auto;
  width: auto;
}

/*
| The same figure, tapped rather than typed: it opens the wheels. It takes the
| whole of `.set-value` above — size, underline, the colour a good set turns —
| and only has to say that it is a button.
*/
.set-duration {
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/*
| Not dimmed, because nothing is unavailable: the clock is writing this figure
| and it is the liveliest thing on the screen. It simply is not a control for
| as long as that is true.
*/
.set-duration:disabled {
  cursor: default;
}

.set-caption {
  font-size: 9px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  white-space: nowrap;
  line-height: 1;
  text-align: center;
}

.set-row.is-ok .set-value {
  color: var(--ok);
}

.set-row.is-fail .set-value {
  color: var(--fail);
}

.set-row.is-even .set-value {
  color: var(--partial);
}

/*
| The log button doubles as the status: an empty ring while the set is still
| to do, a tick once it is made, and the shortfall — "-2" — when it was not.
| It is the biggest target in the row, because it is the one thing every set
| ends with and the one tap made with a hand that is still shaking.
|
| It takes its size in height rather than in width, because width here comes
| straight out of the number beside it: the reps column is what gives, and a
| count squeezed between its two buttons is a worse trade than a narrow button.
*/
.set-log {
  width: 60px;
  /*
  | As tall as the lines beside it — two on a weighted set, one on a bodyweight
  | one — with a floor under it, so the row that has least to say still ends in
  | a button worth aiming at.
  */
  align-self: stretch;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  min-height: 58px;
  font-size: 20px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--ink-faint);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.set-log:hover {
  background: var(--surface-3);
}

.set-row.is-ok .set-log {
  color: var(--ok);
  border-color: rgba(63, 191, 127, 0.45);
  background: rgba(63, 191, 127, 0.16);
}

.set-row.is-fail .set-log {
  color: var(--fail);
  border-color: rgba(229, 72, 77, 0.45);
  background: rgba(229, 72, 77, 0.16);
}

.set-row.is-even .set-log {
  color: var(--partial);
  border-color: rgba(226, 160, 63, 0.45);
  background: rgba(226, 160, 63, 0.16);
}

.set-ring {
  display: block;
  width: 26px;
  height: 26px;
  border: 2px solid var(--line-strong);
  border-radius: 50%;
}

/*
| ► to start the hold, ■ to end it — the log button's box exactly, immediately
| to its left. The two are the whole of a timed set and are aimed at with the
| same thumb from the same distance, so neither may be the smaller target.
|
| Blue to start and red to stop: the one pair of colours nobody has to be
| taught, and the reason this button may borrow them is that it is the only
| control in the row that is not reporting a result. The ✓ beside it owns
| green, amber and red as verdicts on the set — this one only ever says which
| of two things the next tap does.
*/
.set-run {
  width: 60px;
  align-self: stretch;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  min-height: 58px;
  line-height: 0;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--surface-2);
  /*
  | The blue the app already owns. It is the colour of a target that has moved
  | — a brief rather than a verdict — which is exactly the register this button
  | speaks in, so it is the same ink rather than a seventh one.
  */
  color: var(--rise);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.set-run:hover {
  background: var(--surface-3);
}

/* Nothing to count: a set already performed, or one with no length set. */
.set-run:disabled {
  opacity: 0.28;
  pointer-events: none;
}

/*
| Running. The tinted box is the log button's own recipe for a lit state, so
| the two sit beside each other as one pair rather than two designs.
*/
.set-run.is-holding {
  color: var(--fail);
  border-color: rgba(229, 72, 77, 0.45);
  background: rgba(229, 72, 77, 0.16);
}

/*
| Adding a set on the day.
|
| Under the last card, because it makes another one of those — and full width
| across the list, so it is found by a thumb that is not being aimed. Whether
| the training keeps the set is asked on the spot, over the session; see
| partials/keep_prompt.edge.
*/
.set-extra {
  margin-top: 10px;
}

.set-extra-actions {
  display: flex;
  gap: 8px;
}

.set-extra-actions .btn {
  flex: 1 1 0;
}

/*
| Supersets, in the session ------------------------------------------------
|
| One screen for the whole superset: every round of it, and inside each round
| every exercise in turn. Which exercise a row is for is the one thing this
| screen has to say that the single-exercise screen never had to — there, the
| answer was written once at the top and held for the page.
|
| So each exercise of the superset gets a letter and a colour, learned once
| from the chips in the header and then repeated above every one of its rows.
| A colour alone would not do it — it has to survive being read at arm's
| length by someone who is out of breath, and it has to work for a reader who
| cannot tell two of them apart — so the letter is the label and the colour is
| what makes it findable without reading.
|
| Four tints, cycled. None of them is green, amber or red: those three belong
| to how a set went, and they are the colours this screen is already covered
| in.
*/
.is-member-1 {
  --member: #4c9ff0;
}

.is-member-2 {
  --member: #a78bfa;
}

.is-member-3 {
  --member: #2dd4bf;
}

.is-member-4 {
  --member: #f472b6;
}

/* The exercises this station is made of, across the sticky header. */
.superset-members {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 6px;
}

.superset-member {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  padding: 5px 10px 5px 6px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  font-size: 14px;
  font-weight: 600;
}

.member-badge {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 6px;
  background: var(--member);
  color: #0b0c0e;
  font-size: 11px;
  font-weight: 800;
  line-height: 1;
}

.member-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The word itself, in the meta line, wearing the same blue the builder uses. */
.superset-mark {
  color: var(--rise);
  font-weight: 700;
}

/*
| One card is one round. It is the same card a single set gets, with a head on
| it: a round holds several exercises, and unlike a left and a right they are
| not two halves of one thing that could go without saying.
*/
.round-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 12px 8px;
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.round-name {
  font-weight: 700;
  color: var(--ink-dim);
}

.round-done {
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
}

.round-member + .round-member {
  border-top: 1px solid var(--line);
}

/*
| Which exercise the rows belong to: the letter, in a gutter down the left.
|
| It was the letter *and* the exercise's name, written above every round. The
| name is on screen either way — the chips naming A, B and C are in the sticky
| header, which does not scroll away — so repeating it per round bought nothing
| and cost a line of height in every card. On a superset of three over four
| rounds that is twelve lines of the screen saying what the top of it already
| said.
|
| Centred against the exercise's rows rather than sat on the first, so a
| per-side exercise reads as one lettered thing with a left and a right in it.
*/
.round-member {
  position: relative;
  border-left: 3px solid var(--member);
}

/*
| Floated over the rows rather than sat in a column beside them, so that the
| tint a row carries — the grey of the set being worked on, the green or red of
| one already logged — runs the full width of the card instead of stopping at a
| gutter and leaving a seam down the left.
|
| Centred against the exercise's rows rather than sat on the first, so a
| per-side exercise reads as one lettered thing with a left and a right in it.
*/
.round-member > .member-badge {
  position: absolute;
  top: 50%;
  left: 9px;
  z-index: 1;
  transform: translateY(-50%);
}

/* Which is the room the badge needs; the rest of the padding is the row's. */
.round-member .set-row {
  padding-left: 40px;
}

/* A swap is per exercise here, so each button wears the exercise's colour. */
.superset-swap {
  border-left: 3px solid var(--member);
}

/*
| The two answers to that question — the set's, and the added exercise's, which
| is the same question and the same sheet — sharing the row equally. Neither is
| the primary: one keeps the plan as written and one changes it, and the app
| has no opinion about which of those today was.
*/
.keep-prompt-answers {
  display: flex;
  gap: 8px;
}

.keep-prompt-answers .btn {
  flex: 1 1 0;
}

.set-note {
  font-size: 12px;
  color: var(--ink-faint);
  margin: 2px 0 16px;
}

/* Progression prompt --------------------------------------------------------- */
/*
| Shown once an exercise is finished, while its rest runs. It is the only
| place the app writes back to the *plan* rather than the log: what you just
| managed is the best evidence for what next week's target should be, and
| asking here costs one tap instead of a trip back to the builder.
|
| Doing nothing is a valid answer, and the default.
*/
.progression {
  margin: 16px 0 4px;
  padding: 14px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
}

.progression-label {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* Which exercise the prompt is about, when a superset stacks two or three. */
.progression-member {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.progression-member-name {
  font-size: 12px;
  letter-spacing: 0.04em;
  text-transform: none;
  font-weight: 600;
  color: var(--ink-dim);
}

/* The same rail its rows carry, so the prompt and the card read as one thing. */
.progression.is-member {
  border-left: 3px solid var(--member);
}

/*
| A split exercise gets one group per side, each with its own fields: raising
| the left arm's target should not oblige you to raise the right arm's in the
| same gesture.
*/
.progression-group + .progression-group {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

.progression-text {
  font-size: 14px;
  line-height: 1.45;
  margin: 5px 0 12px;
}

.progression-group + .progression-group .progression-text {
  margin-top: 0;
}

.progression-text b {
  font-weight: 600;
}

.progression-text .ok {
  color: var(--ok);
}

.progression-text .fail {
  color: var(--fail);
}

/*
| Next time's target, as fields rather than as a menu — they open on today's
| numbers and nothing moves until you move it. Same controls as the set rows
| above, so the numbers you type here look like the numbers you typed a moment
| ago.
|
| Two columns. A per-side exercise puts the shared rep target across both and
| gives each side's load a column of its own underneath, since the left and the
| right are separate figures on the training and can move in opposite
| directions.
*/
.progression-fields {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px 10px;
}

.progression-field.is-wide {
  grid-column: 1 / -1;
}

.progression-field-label {
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  text-align: center;
  margin-bottom: 6px;
}

/* The change against the current target, inside the field that carries it. */
.progression-field .set-delta {
  display: block;
  margin-top: 7px;
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/*
| Where the two chips were. Leaving a target alone is the default here, so the
| pair was a question that did not need asking — and the one marked "Keep as
| is" was pressed for no better reason than that it looked pressable. This says
| which way the fields came out, and nothing about it invites a tap.
|
| Both states are coloured, and neither is greyed out. This is the only report
| of what the prompt is about to do to next week's plan, so the answer nobody
| chose deliberately still has to be read at arm's length: in faint ink it was
| missed entirely.
*/
.progression-verdict {
  margin: 16px 0 0;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-align: center;
  /* Blue for the plan as it stands, green for the plan once it has moved. */
  color: var(--rise);
}

.progression-verdict.is-moved {
  color: var(--ok);
}

/* Rest timer --------------------------------------------------------------- */
/*
| A band rather than a takeover: the set list has to stay visible and editable
| while the clock runs, which is the point of putting every set on one screen.
*/
.rest-band {
  position: sticky;
  bottom: 0;
  z-index: 15;
  margin: 8px -16px 0;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
  background: rgba(11, 12, 14, 0.94);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-top: 1px solid var(--line);
}

.rest-band[hidden] {
  display: none;
}

.rest-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.rest-label {
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.rest-value {
  font-size: 40px;
  font-weight: 200;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--ok);
}

.rest-band.is-over .rest-value {
  color: var(--partial);
}

.rest-bar {
  height: 3px;
  border-radius: 2px;
  background: var(--surface-3);
  overflow: hidden;
  margin: 10px 0;
}

.rest-bar-fill {
  height: 100%;
  background: var(--ok);
  transition: width 0.3s linear;
}

.rest-band.is-over .rest-bar-fill {
  background: var(--partial);
}

.rest-actions {
  display: flex;
  gap: 8px;
}

.rest-actions .btn {
  flex: 1 1 0;
  min-height: 40px;
  font-size: 13px;
}

/* Navigation between exercises ----------------------------------------------- */
/*
| Two arrows, each half the width of the screen. Nothing is written on them —
| see `STEP_ICONS` — and the room the words used to take is left to the target
| instead: between sets this is aimed at with a thumb, one-handed, without
| looking, and the arrow is only ever as easy to hit as the button under it is
| wide. A drag across the screen does the same thing; see `watchSwipes`.
*/
.runner-nav {
  display: flex;
  gap: 8px;
  margin-top: 18px;
}

.runner-nav .btn {
  flex: 1 1 0;
}

/* Taller than a button of text, since it is the tap and not the reading. */
.runner-step {
  min-height: 52px;
  padding: 0;
}

/*
| What can be done to the shape of the session from this screen: swap this
| exercise, put one after it, and — on one added today — take it back out.
| Small buttons under the navigation rather than beside it: none of them is a
| way through the session, and all are the kind of thing looked for rather than
| aimed at.
|
| Two of them share the row; a third wraps onto its own line rather than
| squeezing all three into a phone's width. That is what the basis is for — at
| 140px, two fit across 375px and three do not.
|
| The labels wrap rather than truncate. "Add an exercise after this" is a long
| thing to say and longer in most other languages, and half a sentence in a
| button is worse than two lines of one.
*/
.runner-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

.runner-tools .btn {
  flex: 1 1 140px;
}

/* Finish -------------------------------------------------------------------- */
.runner-finish {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}

/*
| Beside finishing, but not its equal: one ends the session, the other throws
| it away. Sized to its own label so the button you actually came for keeps the
| rest of the row.
*/
/* Matches the specificity of `.btn-row > .btn`, which shares the row equally. */
.btn-row > .runner-cancel {
  flex: 0 0 auto;
}

.runner-summary {
  text-align: center;
  padding: 12vh 0 0;
}

.runner-summary-title {
  font-size: 28px;
  font-weight: 600;
  letter-spacing: -0.02em;
}

.runner-summary-gains {
  margin-top: 10px;
  font-size: 15px;
  font-weight: 600;
  color: var(--ok);
}

/*
| The finish screen carries the whole session now, so it starts near the top of
| the page. The other screens wearing `.runner-summary` — the session already
| running, the one that cannot be read — hold four lines and still belong in
| the middle of it.
*/
.runner-summary.is-complete {
  padding-top: 6vh;
}

/*
| The two ways out of a finished session, side by side and twice the usual
| height: they are the only thing left to tap on the screen, and at that size
| neither is a target you have to aim for at the end of a workout.
*/
.runner-summary-ends {
  margin-top: 22px;
}

/*
| The icon over the label rather than beside it: at this height there is room
| for it, and the two labels are long enough in most languages that a picture
| on the same line would push them into a third line.
*/
.runner-summary-ends > .btn {
  flex-direction: column;
  gap: 7px;
  min-height: 92px;
  padding-inline: 10px;
}

/*
| Half the width of the way out, which is what the row is really for. Written
| to the specificity of `.btn-row > .btn`, the rule it has to outrank.
*/
.runner-summary-ends > .runner-summary-reopen {
  flex: 0.5 1 0;
}

/*
| The session, as the card it is about to become — the history's own
| `.session-entry`, not a copy of one, so it needs nothing here but a place to
| stand. It brings its own surface, its own head and its own share button, and
| it is read rather than announced, so it is left-aligned inside a screen that
| centres everything above it.
*/
.runner-summary .session-entry {
  margin-top: 24px;
  text-align: left;
}

/* Recent sessions -------------------------------------------------------- */
/*
| The row above the trainings list: the last session you and each gymbro
| finished today or yesterday, as the timeline's own cards — see
| partials/recent_sessions.edge. Everything a card looks like is
| `.timeline-card`'s; all this owes it is the row.
*/
.recent {
  margin-bottom: 12px;
}

/*
| Side by side, and sideways past the edge rather than wrapping: a second line
| of cards would push the trainings further down for the sake of older news,
| and a row read left to right from the freshest is the one order that needs
| no explaining.
|
| Equal shares with a floor, as the timeline's columns have — so two sessions
| split the width between them, and from the third on the next card shows its
| edge, which is how the row says there is more of it. The floor is wider than
| the timeline's 86px because there is no gutter of dates beside this one, and
| set where a gymbro's name and "Yesterday" still fit on the card's first line
| — a caption broken after its dot reads as two labels.
*/
.recent-cards {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;

  /* Room for a desktop scrollbar under the cards rather than across them. */
  padding-bottom: 6px;
}

/*
| One height for the whole row, so it reads as a row — and a column inside
| each card, because a button stretched taller than its content centres that
| content, and a one-line name would float halfway down beside a two-line one.
*/
.recent-cards > .timeline-card {
  flex: 1 0 144px;
  display: flex;
  flex-direction: column;
  width: auto;
  scroll-snap-align: start;
}

/* Admin: an exercise's picture ------------------------------------------ */
/*
| The two poses side by side, each drawn the way `.movement-thumb` draws the
| pair — white ground, the picture fitted whole — and never larger than it, so
| a square here is the square the picker and the runner will show. On a phone
| the two of them are a little narrower than that, which is the only
| difference.
*/
.art-poses {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 176px));
  gap: 12px;
}

.art-pose {
  position: relative;
  display: grid;
  gap: 6px;
  cursor: pointer;
}

.art-pose-square {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  border-radius: 14px;
  overflow: hidden;
  background: #fff;
}

/* A photo's ground, as in `.movement-thumb.is-photo`. */
.art-pose-square.is-photo {
  background: #000;
}

/*
| An empty pose is a gap in the page's own colours rather than a white square:
| white is what a picture sits on, and an empty white square reads as a
| picture that failed to load.
*/
.art-pose-square:has(> .art-pose-image[hidden]) {
  background: var(--surface);
  border: 1px dashed var(--line-strong);
}

.art-pose-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.art-pose-image[hidden],
.art-pose-empty[hidden] {
  display: none;
}

.art-pose-empty {
  padding: 12px;
  font-size: 13px;
  text-align: center;
  color: var(--ink-dim);
}

.art-pose-label {
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/*
| The input is the square's label's, and invisible: the square is what is
| pressed. Kept in the page rather than `display: none` so it can still be
| reached from a keyboard, which is when the ring below shows.
*/
.art-pose-input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.art-pose:has(.art-pose-input:focus-visible) .art-pose-square {
  outline: 2px solid var(--rise);
  outline-offset: 2px;
}

.art-error {
  color: var(--fail);
}

/*
| The figure and the equipment chips under the catalogue's search, as in the
| picker. The lit muscles are links there rather than buttons, so they lose
| the underline a link would otherwise bring.
*/
.catalogue-filter {
  margin-top: 14px;
}

.catalogue-filter .bodymap {
  margin-bottom: 0;
}

.catalogue-muscles {
  margin-top: 10px;
}

.catalogue-filter a.chip {
  text-decoration: none;
}

/*
| The catalogue list: each exercise's square on the left of its name, small
| enough that sixty rows are still a list, big enough to tell a wrong machine
| from the right one.
*/
.admin-exercise-link {
  display: flex;
  align-items: center;
  gap: 12px;
}

.admin-exercise-text {
  flex: 1 1 auto;
  min-width: 0;
}

.movement-thumb.is-admin {
  width: 72px;
  height: 72px;
  border-radius: 10px;
}

/*
| Nothing to show: a gap in the page's colours, as an empty pose is above,
| with a crossed-out picture in it so it reads as "none" rather than as one
| still loading.
*/
.movement-thumb.is-empty {
  display: grid;
  place-items: center;
  background: var(--surface);
  border: 1px dashed var(--line-strong);
  color: var(--ink-faint);
}

.movement-thumb.is-empty svg {
  width: 28px;
  height: 28px;
}

/* Admin users ------------------------------------------------------------ */
/*
| One row per account, with the labels along the top instead of repeated down
| every card. The columns are the same eleven questions asked of everybody,
| which is what a table is for — and what makes a card each read like eleven
| headings answered eleven times.
|
| Eleven columns do not fit a phone, so the table scrolls inside its own box
| rather than stretching the page under it. The scrollbar is left visible here,
| unlike the tab strip in app.css where it is hidden on purpose: there the tabs
| themselves are cut off mid-word and say there is more, and here every column
| ends cleanly at the edge, so the bar is the only thing that does.
*/
.table-scroll {
  overflow-x: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/*
| Nothing wraps. A cell two lines deep in one column drags the whole row taller
| and the eye loses the line it was reading across — which is the one thing a
| table has over the cards this replaced.
*/
.user-table {
  width: 100%;
  font-size: 13px;
  white-space: nowrap;

  /*
  | Separate, not collapsed. A collapsed table hands its borders to the table
  | itself rather than to the cells, and the sticky first column below then
  | scrolls out from under its own right-hand line.
  */
  border-collapse: separate;
  border-spacing: 0;
}

.user-table th,
.user-table td {
  padding: 9px 12px;
  text-align: left;
  vertical-align: middle;
  font-weight: 400;
}

.user-table thead th {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  border-bottom: 1px solid var(--line);
}

/* Between the rows only: the head has its own line, and the last row ends. */
.user-table tbody tr + tr > * {
  border-top: 1px solid var(--line);
}

/* Everything but the account itself is secondary to it. */
.user-table tbody td {
  color: var(--ink-dim);
}

/*
| Counts, read down the column rather than across the row, so they line up on
| the digit and a busy month is visible without reading a number.
*/
.user-table .is-number {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/*
| The account stays put while the rest of the row scrolls under it. Without
| this, a table scrolled to the right is a line of "On", "Off" and "2 months
| ago" belonging to nobody — the one column that says whose row it is would be
| off the left edge, which on a phone is most of the time.
|
| Its own background, because a sticky cell is transparent by default and the
| row would slide through it.
*/
.user-table thead th:first-child,
.user-table tbody th {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--surface);
  border-right: 1px solid var(--line);
}

/*
| The account. A flex box *inside* the cell rather than the cell itself: a
| table cell told to be flex leaves the table layout, and the column it was
| holding stops lining up with its heading.
*/
.user-cell {
  display: flex;
  align-items: center;
  gap: 10px;
}

/*
| Google's picture, the one thing the app has always stored and never drawn.
| A fixed box, so a slow or blocked fetch does not resize the row around it.
*/
.user-avatar {
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 999px;
  object-fit: cover;
  background: var(--surface-2);
}

/* No picture — an account made before Google had one, or by /dev/login. */
.user-avatar-blank {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.user-name {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink);
}

/*
| The name is what gives way when the column is capped on a phone, not the tags
| beside it. Cut the other way round, the row that says ADMIN would be exactly
| the row wide enough to lose the word.
*/
.user-name-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.user-name .tag {
  flex: none;
}

.user-email {
  font-size: 12px;
  color: var(--ink-dim);
}

/*
| A phone leaves 343px of table box, and the pinned column was taking 293 of
| them — so scrolling right revealed an inch and a half of the columns it is
| pinned beside, which is not a table. Capped, with the two lines clipped
| rather than wrapped: a name that grows a second line takes the row's whole
| height with it, and rows of different heights are what the cards this
| replaced already did.
|
| The full name and address are still on the row at any width the account is
| actually administered from; this is the size where something has to give, and
| the first few characters of a name are enough to know whose row you are on.
*/
@media (max-width: 560px) {
  .user-table thead th:first-child,
  .user-table tbody th {
    max-width: 195px;
  }

  /*
  | Two tags, a 32px picture and a name do not share 170px, and the flex box
  | resolves that by giving the name nothing — a row reading "ADMIN YOU" and no
  | account. "You" is the tag to drop: the Admin column already shows a dash
  | where your own button would be, and the note under the table says why.
  */
  .user-tag-you {
    display: none;
  }

  .user-ident {
    min-width: 0;
  }

  .user-email {
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* Timeline ---------------------------------------------------------------- */
/*
| A week of sessions, as a calendar rather than as a list — the shape of both
| the history and the gymbros feed.
|
| A row per day and a column per person inside it, which is the whole design.
| Stacked as a list, sessions answer "what happened, most recent first", a
| question you can only ask one session at a time. Laid out as a week they
| answer "how did the week go": three sessions or one, Monday and Thursday or
| four days running, and on the feed, who else was in on the same day. The days
| nothing happened on are drawn too — an empty Wednesday is part of that
| answer, and closing the gaps up would report four sessions in four days.
|
| Nothing on a card but who, what and how it started. Everything that was
| actually lifted is in the sheet it opens, which is the same .session-entry
| this screen used to stack, unchanged — including for the camera that
| photographs it; see public/session_image.js.
*/
/*
| Whose sessions are on it: everybody, you alone, or one gymbro alone.
|
| A picker rather than a row of pills, and the reason is the list's length: it
| is as long as the reader has gymbros, and a row of buttons would either wrap
| into a second line of chrome above the week or scroll sideways past the edge,
| where the name being looked for is the one that cannot be seen. A <select>
| hands the problem to the platform, which already owns a control for "one of
| these, however many there are" and draws it as a wheel or a sheet on a phone.
|
| One line, shared with its label, because the top of this screen belongs to
| the week rather than to the chrome above it.
*/
.who {
  display: flex;
  align-items: center;
  gap: 10px;

  min-width: 0;
}

/*
| The picker and the way to add a gymbro, on one row above the week: both are
| about who is on it. The picker takes what room there is and gives way first
| on a narrow phone; the button keeps its words.
*/
.who-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin: 4px 0 14px;
}

.who-row > #share-invite {
  flex: none;
}

/*
| On a phone the label goes — from the eye, not from a screen reader: "Whose
| sessions" beside "Everyone" and "Become gymbros" leaves the picker no room to
| say who it is set to, and its value is the label anybody actually reads.
*/
@media (max-width: 480px) {
  .who-row .who-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
}

.who-label {
  flex: none;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/*
| A pill with a chevron, rather than the app's .form-control — this is not a
| field in a form being filled in, it is the state of the screen, and it reads
| beside the week's own heading rather than above a row of inputs.
|
| `color-scheme` is load-bearing and not decoration: without it the browser
| draws the open list for a light page, which on this surface is a white sheet
| dropped over a black app. The same reason the release field carries it; see
| app.css.
|
| The chevron is a background image because a data URI cannot inherit
| `currentColor` — so the colour is written out. The palette is dark only, so
| there is one value to write.
*/
.who-select {
  flex: 0 1 auto;
  min-width: 0;
  padding: 7px 32px 7px 13px;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  background-color: var(--surface);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23f2f3f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  border: 1px solid var(--line);
  border-radius: 999px;
  appearance: none;
  -webkit-appearance: none;
  color-scheme: dark;
  cursor: pointer;
}

.who-select:hover {
  border-color: var(--line-strong);
}

.who-select:focus-visible {
  outline: 2px solid var(--line-strong);
  outline-offset: 2px;
}

/* Only ever on screen with scripting off, where it is the way to submit. */
.who noscript {
  flex: none;
}

.timeline-week + .timeline-week {
  margin-top: 26px;
}

/*
| Named where it has a name — this week, last week — and dated always, because
| the week before those two has no name and "3 weeks ago" is not a week, it is
| an approximation of one.
*/
.timeline-week-name {
  display: flex;
  align-items: baseline;
  gap: 7px;
  margin: 4px 0 8px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.timeline-week-dates {
  font-weight: 400;
}

/*
| Quieter behind a name, because there it is the gloss rather than the heading.
| The separator exists for the same reason: only when there is something in
| front of it to separate.
*/
.timeline-week-now + .timeline-week-dates {
  color: var(--ink-faint);
}

.timeline-week-now + .timeline-week-dates::before {
  content: '·';
  margin-right: 7px;
}

.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
}

/*
| The date in a fixed gutter and the day's cards beside it. Fixed, so every
| day of every week lines its cards up at the same left edge and the column of
| dates reads down the page as one thing.
*/
.timeline-day {
  display: grid;
  grid-template-columns: 50px 1fr;
}

/*
| Wide enough for the weekday and the date on one line, because that is what a
| rest day collapses to below — and a gutter that fitted them only when they
| were stacked would wrap "SUN 13" on every day nothing happened.
*/
.timeline-mark {
  padding: 10px 10px 10px 0;
  text-align: right;
  white-space: nowrap;
}

.timeline-dow {
  display: block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.timeline-dom {
  display: block;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}

.timeline-day.is-today .timeline-dow,
.timeline-day.is-today .timeline-dom {
  color: var(--ink);
}

/*
| The rail. It is a border on the cards rather than a line drawn down the list,
| so it is made of one segment per day and cannot come apart from the rows it
| is measuring — a day that grows because three people trained grows its own
| piece of it.
|
| It scrolls sideways rather than wrapping when several people share a day:
| wrapping would put the fourth person under the first, which is the one thing
| a column is there to prevent. The sheets are not in here for that very
| reason — see partials/timeline_week.edge.
*/
.timeline-cards {
  position: relative;
  display: flex;
  gap: 8px;
  padding: 7px 0 7px 13px;
  border-left: 1px solid var(--line);
  overflow-x: auto;
}

/* A mark on the rail for a day something happened on, and none for a rest. */
.timeline-day:not(.is-rest) .timeline-cards::before {
  content: '';
  position: absolute;
  left: -4px;
  top: 18px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ink-faint);
}

.timeline-day.is-today:not(.is-rest) .timeline-cards::before {
  background: var(--rise);
}

/*
| A day with nothing on it collapses to one quiet line. It is still a row —
| the week has to keep its shape — but it is not an absence worth the height
| of a card, and seven of them on a rested week would be a screen of nothing.
*/
.timeline-day.is-rest .timeline-mark {
  padding: 3px 10px 3px 0;
}

.timeline-day.is-rest .timeline-cards {
  padding: 3px 0 3px 13px;
}

.timeline-day.is-rest .timeline-dow,
.timeline-day.is-rest .timeline-dom {
  display: inline;
  font-size: 10.5px;
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
}

/*
| One person's day. Equal shares of the row, so two people who trained on the
| same Tuesday are the same size on it — and a floor under that share, because
| a training's name needs a width to be a name rather than a column of
| syllables.
|
| The floor is set at three columns across the narrowest phone this app is used
| on, because three gymbros on one Saturday is the case the columns exist for
| and a third one off the right edge would miss it. A fourth scrolls, which is
| the honest thing for a row that genuinely has more in it than there is width.
*/
.timeline-column {
  flex: 1 1 0;
  min-width: 86px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.timeline-card {
  display: block;
  width: 100%;
  text-align: left;
  padding: 9px 11px;
  font: inherit;
  color: inherit;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.timeline-card:hover {
  border-color: var(--line-strong);
  background: var(--surface-2);
}

/*
| Yours: blue, so it is found across a row before any caption is read. Always,
| rather than only once a gymbro's card is beside it — a card that turned blue
| when somebody else trained would look like it had changed rather than like
| the one it always was. See partials/timeline_card.edge.
*/
.timeline-card.is-own {
  background: var(--own-bg);
  border-color: var(--own-line);
}

.timeline-card.is-own:hover {
  background: var(--own-bg-hover);
  border-color: var(--own);
}

.timeline-card.is-own .timeline-card-who {
  color: var(--own);
}

/*
| Whose session it is, above the name and in the same quiet capitals the card
| in the sheet uses for it. When the week holds several people it is the first
| thing being scanned for, yours included — "You" rather than a blank, because
| an unlabelled column beside two labelled ones reads as a label that failed to
| render. Gone entirely once the timeline has been filtered to one person.
*/
.timeline-card-who {
  display: block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin-bottom: 2px;
}

/*
| `anywhere` rather than a clamp: a card is 104px at its narrowest and plenty
| of trainings are named something longer than that. Better three lines of the
| real name than one line of an abbreviated one — the card is what you are
| aiming at, and the wrong half of "Upper body — heavy" is no target at all.
*/
.timeline-card-name {
  display: block;
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

/*
| What came of the session: the tally and the length, on one line under the
| name.
|
| Compact rather than labelled, because the card it is on is 86px wide when
| three gymbros share a day and the words would cost it three lines — see
| partials/timeline_week.edge. Tabular figures so the line under one card sits
| over the line under the next, which is the whole reason a column of them is
| readable at a glance.
*/
.timeline-card-stats {
  display: block;
  margin-top: 3px;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}

/*
| The score is the sheet's own — the green-or-amber verdict on the numerator
| comes with it, so the two screens cannot come to draw a missed set
| differently. All the card owes it is its own size.
*/
.timeline-card-stats .score {
  font-size: inherit;
}

.timeline-card-feeling {
  display: block;
  margin-top: 3px;
  font-size: 11.5px;
  color: var(--ink-dim);
}

/*
| The session itself, in the sheet a card opens. The entry brings its own
| border and padding — it is the card the picture is taken of — so all this
| owes it is a little air under the heading.
*/
.timeline-sheet .session-entry {
  margin-top: 2px;
}

/* History ---------------------------------------------------------------- */
/*
| A summary row. The history used to be built from these; it is a timeline now
| — see above — and this is left to the gymbros list, which really is a list of
| summaries.
*/
.session-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.session-row-main {
  flex: 1 1 auto;
  min-width: 0;
  text-decoration: none;
  color: inherit;
}

.session-row-name {
  font-size: 15px;
  font-weight: 600;
}

.session-row-meta {
  font-size: 12.5px;
  color: var(--ink-dim);
  margin-top: 2px;
}

.score {
  flex: none;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}

.score b {
  color: var(--ok);
}

.score.has-misses b {
  color: var(--partial);
}

.score-label {
  color: var(--ink-dim);
}

/*
| Quiet until it is reached for: a row of red bins down the history would read
| as a warning about the sessions rather than an action on them.
|
| Sized past the 36px of a small button because it is the one thing on this
| screen you must not hit by accident, and a thumb on a phone is about 44px
| wide. The entry is several lines tall, so it costs nothing.
*/
.session-row-delete {
  width: 46px;
  min-height: 46px;
  color: var(--ink-dim);
  background: none;
  border-color: transparent;
}

.session-row-delete:hover {
  color: var(--fail);
  background: var(--fail-bg);
}

/*
| One whole session — the heading and every set performed under it.
|
| The card is what a timeline card opens, and it is still a card rather than a
| page: the padding is the row's, the exercises sit inside it rather than in a
| .panel of their own, and nothing in it is a link, because there is nowhere
| left to go. That it stayed a card is also what lets it be photographed — see
| public/session_image.js.
*/
.session-entry {
  padding: 13px 14px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.session-entry-head {
  display: flex;
  align-items: center;
  gap: 12px;
}

.session-entry-title {
  flex: 1 1 auto;
  min-width: 0;
}

/*
| The two things you can do to a card, side by side and out of the way. A
| tighter gap than the head's own: they are one cluster, not two items in the
| heading, and the heading's 12px between them would read as the second being
| about something else.
*/
.session-entry-actions {
  flex: none;
  display: flex;
  align-items: center;
  gap: 2px;
}

/*
| Level with the face, and sized to sit beside it: next to a 72px picture a
| 46px button with a 19px glyph read as an afterthought. Both buttons grow
| together, for the reason they were always the same size — see the note
| below — and the drawing inside each is scaled to the box.
*/
.session-entry-actions > .btn-icon,
.session-entry-actions > form > .btn-icon {
  width: 56px;
  min-height: 56px;
}

.session-entry-actions svg {
  width: 26px;
  height: 26px;
}

/*
| Neutral, unlike the bin beside it. Sharing is not a warning, and the two
| buttons are the same size because they are the same kind of target — see the
| note on .session-row-delete for why that size is 46px.
*/
.session-entry-share {
  width: 46px;
  min-height: 46px;
  color: var(--ink-dim);
  background: none;
  border-color: transparent;
}

.session-entry-share:hover {
  color: var(--ink);
  background: var(--surface-2);
}

/*
| Whose session this is, above the name. Absent on your own — which is also
| what tells the bin beside it whether to be drawn, since somebody else's
| session is not yours to remove; see partials/session_entry.edge.
*/
.session-entry-owner {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin-bottom: 3px;
}

.session-entry-name {
  font-size: 15px;
  font-weight: 600;
}

.session-entry-meta {
  font-size: 12.5px;
  color: var(--ink-dim);
  margin-top: 2px;
}

/*
| How the session started, as the last item of the meta line. Kept in one piece
| when the line wraps: a face on the end of one line and its word at the start
| of the next reads as two facts.
*/
.session-feeling {
  white-space: nowrap;
}

.session-entry-title .score {
  margin-top: 5px;
}

/*
| A rule under the heading rather than a gap: the sets belong to the session
| above them, and on a page of stacked cards the reader needs to see that at a
| glance rather than infer it from spacing.
*/
.session-entry-body {
  margin-top: 10px;
  border-top: 1px solid var(--line);
}

.session-entry-body > .history-exercise:last-child,
.session-entry-body > .history-superset:last-child {
  padding-bottom: 0;
}

/*
| A superset, as it was performed: its exercises drawn together under one rail
| and named once.
|
| The same blue rail the builder marks a superset with, for the same reason —
| green, amber and red are spoken for by how the sets went, and a superset is
| not a verdict. It is read off the logs rather than off the training, so a
| group broken up since is still drawn here the way the session happened.
*/
.history-superset {
  padding: 12px 0 12px 11px;
  border-left: 3px solid var(--rise);
  border-bottom: 1px solid var(--line);
}

.history-superset:last-child {
  border-bottom: none;
}

.history-superset-mark {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--rise);
}

/*
| Inside, the exercises are already inside a block that has ruled itself off,
| so they lose their own divider and close up.
*/
.history-superset .history-exercise {
  padding: 9px 0 0;
  border-bottom: none;
}

.session-entry-empty {
  margin-top: 10px;
  font-size: 13px;
  color: var(--ink-faint);
}

/*
| A session's sets, and how each one moved.
|
| A column per set rather than rows of numbers, and that is the whole design.
| Laid out as rows — pills on one, deltas on the next — the two line up only
| until the strip wraps, and from there every delta sits under the wrong set.
| Stacking each set with its own deltas makes the alignment structural: a wrap
| moves a set and its numbers together because they are one box.
|
| The columns are separate flex boxes, so nothing makes their rows agree except
| the heights below. Every cell is fixed, and the first cell of every column —
| the pill, the side letter, the load — is fixed to the taller pill row. Let one
| of them size to its content and that column alone slides out of line.
*/
.set-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 5px;
  margin-top: 8px;
}

.set-col {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.set-col > * {
  height: 16px;
  line-height: 16px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.set-col > :first-child {
  height: 24px;
  line-height: 22px;
}

/*
| What each row of deltas is measured against: the session before this one, and
| the first there ever was. Right-aligned, so the words end where the numbers
| begin rather than trailing off towards them.
*/
.set-col-legend {
  margin-right: 3px;
}

.set-col-legend > * {
  text-align: right;
  color: var(--ink-faint);
  white-space: nowrap;
}

.set-legend {
  font-size: 10px;
}

/* The load sits a little clear of the last set: it is not one of them. */
.set-col-load {
  margin-left: 5px;
}

.set-col-load > * {
  text-align: left;
}

.set-load {
  font-size: 12.5px;
  color: var(--ink-dim);
}

/*
| Green up, red down, and a set that stood still prints nothing — the cell is
| still drawn, so the sets either side of it stay over their own numbers.
*/
.set-move {
  font-weight: 600;
}

.set-move.is-up {
  color: var(--ok);
}

.set-move.is-down {
  color: var(--fail);
}

.set-pill {
  min-width: 34px;
  padding: 0 8px;
  font-size: 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--ink-dim);
}

.set-pill.is-ok {
  color: var(--ok);
  background: var(--ok-bg);
  border-color: rgba(63, 191, 127, 0.3);
}

/*
| Short of the target, but level with last session or better. The same amber
| the runner tints such a row with while you are standing over it, and the same
| the history's tally uses for a session that missed something: a set moving the
| right way on a target it has not reached yet is neither of the other two.
*/
.set-pill.is-even {
  color: var(--partial);
  background: var(--partial-bg);
  border-color: rgba(226, 160, 63, 0.3);
}

.set-pill.is-fail {
  color: var(--fail);
  background: var(--fail-bg);
  border-color: rgba(229, 72, 77, 0.3);
}

.set-pill.is-skip {
  color: var(--ink-faint);
}

.history-exercise {
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
}

.history-exercise:last-child {
  border-bottom: none;
}

.history-exercise-name {
  font-size: 14.5px;
  font-weight: 500;
}

/* One line per side on a per-side exercise, otherwise a single line. */
.set-line {
  margin-top: 8px;
}

.set-line .set-strip {
  margin-top: 0;
}

/* The L or R, sitting on the pill row so the letter names the strip beside it. */
.set-line-label {
  font-size: 11px;
  font-weight: 600;
  color: var(--ink-faint);
}

/* Offline banner --------------------------------------------------------- */
/*
| Shown by store.js when the app is running from its cache, or when there are
| changes still waiting to be pushed.
*/
.offline-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  z-index: 25;
  padding: 7px 16px;
  font-size: 12px;
  text-align: center;
  color: var(--partial);
  background: rgba(226, 160, 63, 0.14);
  border-top: 1px solid rgba(226, 160, 63, 0.3);
}

.offline-bar[hidden] {
  display: none;
}

@media (min-width: 768px) {
  .offline-bar {
    bottom: 0;
  }

  .form-actions {
    bottom: 0;
  }

  .bodymap svg {
    max-height: 44vh;
  }
}

/* Sharing a session ----------------------------------------------------- */
/*
| The card, on its way out of the app as a picture.
|
| Two of these rules are not decoration: .share-stage and .share-frame are what
| public/session_image.js photographs. It clones the entry into the frame,
| leaves it in the document long enough to be laid out — off to the left, where
| `hidden` would not do, because a hidden element has no layout to measure —
| and rasterises the result.
*/
.share-stage {
  position: fixed;
  top: 0;
  left: -10000px;
  z-index: -1;
  pointer-events: none;
}

/*
| The picture itself: the card on the app's own background, with a little room
| around it so it does not read as a screenshot cropped too tight.
|
| It carries the type styles that everything else on a page inherits from
| `body`, because inside the <svg> there is no body — see the note on svgFor in
| public/session_image.js. Sized by its contents, so the card keeps the exact
| width it has on screen and the set strips wrap where they wrapped.
*/
.share-frame {
  display: inline-block;
  padding: 18px;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--bg);
}

/*
| Whose app this is, under the card. Faint and small: it is a signature on a
| picture that will be looked at in a chat thread, not a banner.
*/
.share-frame-mark {
  margin-top: 12px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  text-align: center;
}

/* And the sheet the picture is shown in before it is sent. */
.share-session-note {
  padding: 28px 0;
  font-size: 13px;
  color: var(--ink-dim);
  text-align: center;
}

.share-session-note[hidden] {
  display: none;
}

/*
| Shown at whatever width the sheet has. The picture is several times that in
| pixels, which is the point of it — a chat app that blows it up finds detail
| rather than a scaled screenshot.
*/
.share-session-image {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

.share-session-image[hidden] {
  display: none;
}

/* [hidden] alone loses to the `display` .btn sets. */
#share-session-send[hidden],
#share-session-save[hidden] {
  display: none;
}

/* A shared training ------------------------------------------------------ */
/*
| The page a share link opens. It borrows the builder's list wholesale — the
| rows, the rail, the collapsed summaries — so almost nothing is needed here:
| only the two places where a row that cannot be opened has to stop looking
| like one that can, and the shell it sits in.
*/

/* No tab bar on this page, so nothing has to be kept clear of one. */
body.shared-body {
  padding-bottom: env(safe-area-inset-bottom);
}

/*
| A div where the builder has a button. It inherits the layout and gives back
| the hand cursor: a row that invites a press and then does nothing reads as
| broken, which is worse than one that never asked.
*/
.slot-head-main.is-static {
  cursor: default;
}

/* A shared program ------------------------------------------------------- */
/*
| The page a week's share link opens: a card per training, folded shut over
| the builder's list.
|
| The fold is the whole design. A week is three to six trainings and each is a
| screenful of exercises, so showing them all at once buries the thing the page
| is opened to answer — what shape is this week — under the answer to a
| question nobody has asked yet. Shut, the cards are a week you can read in one
| look; open, each is the same list the single-training page shows.
|
| Built on `.training-card`'s surface rather than sharing its class: these do
| not link anywhere and hold nothing to press, so what they borrow is the look
| and not the behaviour.
*/
.shared-day {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px;
}

/*
| The default marker is a filled triangle that sits differently in every
| browser, so it is taken off and drawn as the chevron the balance help uses.
| `list-style` removes it in Firefox and current Safari; the WebKit pseudo is
| for the older engine an installed app may still be running.
*/
.shared-day-summary {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  list-style: none;
}

.shared-day-summary::-webkit-details-marker {
  display: none;
}

.shared-day-summary::after {
  content: '';
  flex: 0 0 auto;
  margin-left: auto;
  width: 6px;
  height: 6px;
  border-right: 1.5px solid var(--ink-dim);
  border-bottom: 1.5px solid var(--ink-dim);
  transform: rotate(-45deg);
  transform-origin: 60% 60%;
  transition: transform 120ms ease;
}

.shared-day[open] .shared-day-summary::after {
  transform: rotate(45deg);
}

.shared-day-text {
  min-width: 0;
}

.shared-day-name {
  display: block;
  font-weight: 600;
  font-size: 15px;
}

/* The count and the days, on one line: what this day of the week is. */
.shared-day-meta {
  display: block;
  margin-top: 2px;
  font-size: 12.5px;
  color: var(--ink-dim);
}

/*
| The list, once the fold is open. Ruled off from the summary so the exercises
| read as the inside of the card rather than as more of its heading.
*/
.shared-day-body {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}

.shared-day-notes {
  margin: 12px 0 0;
  font-size: 13.5px;
  color: var(--ink-dim);
  white-space: pre-wrap;
}

/* Assistant panel --------------------------------------------------------- */

/*
| The transcript. It takes whatever the heading and the input box leave rather
| than a share of the viewport, because on a screen of its own there is nothing
| else to divide the height with — and it is the scroller, which is what lets
| the module put the newest turn in view by setting `scrollTop` on it.
*/
.assistant-log {
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/*
| A turn. Both sides are the same shape and differ only in weight and where
| they sit, because this is a conversation between two parties and not a
| messaging app — bubbles with tails would be borrowing a visual language for
| something that has no third participant and no delivery state.
*/
.assistant-turn {
  margin: 0;
  padding: 8px 11px;
  border-radius: 12px;
  font-size: 14px;
  line-height: 1.45;
  white-space: pre-wrap;
}

.assistant-user {
  align-self: flex-end;
  max-width: 85%;
  background: var(--surface-3);
  color: var(--ink);
}

.assistant-assistant {
  align-self: flex-start;
  background: var(--surface);
  color: var(--ink);
}

/*
| Ways in, as chips.
|
| They were cards with an example under each, and the examples were the
| problem: five two-line blocks is a page to read before you may ask a
| question, on a panel whose whole promise is that you can just say what you
| want. These are the same five offers with the explaining taken out.
|
| Shaped like the turns above them rather than like buttons, because that is
| what they become — tapping one sends it, and it lands in the log as the
| message it already looked like.
*/
.assistant-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  margin-top: 10px;
}

.assistant-chip {
  padding: 7px 13px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-dim);
  font-family: inherit;
  font-size: 13px;
  line-height: 1.3;
  text-align: left;
  cursor: pointer;
}

/*
| Hover is a pointer's business. On a phone it sticks after the tap, which
| would leave one chip lit for the rest of the sitting — except the whole block
| is gone by then, so what it would really do is light the chip during the
| moment it is being sent, which reads as an error.
*/
@media (hover: hover) {
  .assistant-chip:hover {
    border-color: var(--line-strong);
    color: var(--ink);
  }
}

.assistant-chip:active {
  background: var(--surface-2);
}

/*
| Waiting for an answer.
|
| It used to be a dimmed word, on the reasoning that a sheet which has already
| disabled its own input has said everything a spinner would. That held while
| the wait was one call. It is not one call — it is up to eight, each running
| tools, and eight seconds of a motionless label is what a hung panel looks
| like. So the line moves, and it says which part of the work is happening.
*/
.assistant-turn.is-waiting {
  display: flex;
  align-items: center;
  gap: 7px;
  color: var(--ink-faint);
}

.assistant-doing {
  font-size: 13.5px;
}

/*
| Three dots that rise in turn, the way a typing indicator does in any chat
| app. Borrowed on purpose: it is the one piece of visual language everybody
| already reads as "the other end is working on it", and inventing a private
| version of it would be making the reader learn something they know.
*/
.assistant-dots {
  display: inline-flex;
  align-items: flex-end;
  gap: 3px;
  padding-bottom: 2px;
}

.assistant-dots > i {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
  animation: assistant-dot 1.25s ease-in-out infinite;
}

.assistant-dots > i:nth-child(2) {
  animation-delay: 0.16s;
}

.assistant-dots > i:nth-child(3) {
  animation-delay: 0.32s;
}

@keyframes assistant-dot {
  0%,
  70%,
  100% {
    opacity: 0.35;
    transform: translateY(0);
  }

  35% {
    opacity: 1;
    transform: translateY(-3px);
  }
}

/*
| The answer as it is being typed, with a block where the next character will
| land. It is an `::after` on the paragraph rather than a character in it, so
| nothing here can end up in the text the reader copies.
*/
.assistant-turn.is-streaming::after {
  content: '';
  display: inline-block;
  width: 2px;
  height: 0.95em;
  margin-left: 2px;
  vertical-align: text-bottom;
  background: var(--ink-dim);
  animation: assistant-caret 1s step-end infinite;
}

/*
| An empty streaming line has no text to sit beside, and a caret on its own
| above the dots reads as a second thing happening. There is only one.
*/
.assistant-turn.is-streaming:empty {
  display: none;
}

@keyframes assistant-caret {
  50% {
    opacity: 0;
  }
}

/*
| Movement is the point above, so this is not a case where honouring the
| setting means removing it: someone who has asked for less motion still needs
| to know the panel is alive. The dots stay and stop travelling, the caret
| stops blinking, and both keep saying the same thing more quietly.
*/
@media (prefers-reduced-motion: reduce) {
  .assistant-dots > i {
    animation: assistant-dot-still 1.25s ease-in-out infinite;
  }

  .assistant-turn.is-streaming::after {
    animation: none;
  }

  @keyframes assistant-dot-still {
    0%,
    70%,
    100% {
      opacity: 0.35;
    }

    35% {
      opacity: 1;
    }
  }
}

.assistant-turn.is-error {
  background: var(--fail-bg);
  color: var(--ink);
}

/*
| Something changed behind the sheet, with the way to catch the page up beside
| it. Not an error and not an answer, so it carries neither's colour.
*/
.assistant-turn.is-note {
  background: transparent;
  padding-left: 0;
  color: var(--ink-dim);
  font-size: 13px;
}

/*
| The buttons sit at the foot of the row, so a box grown to several lines keeps
| send where the thumb left it rather than floating it up the middle.
*/
.assistant-ask {
  display: flex;
  align-items: flex-end;
  gap: 8px;
}

/*
| Grown by the module to fit what is written, up to about six lines and then
| scrolled. No drag handle: the height is already the text's, and a corner to
| pull on is one more thing to catch with a thumb.
*/
.assistant-ask > .form-control {
  flex: 1 1 auto;
  min-width: 0;
  max-height: 170px;
  resize: none;
}

.assistant-ask > .btn {
  flex: none;
}

/*
| The microphone, beside the box it fills rather than beside send: it is
| another way of writing the question, and the button that spends a request
| stays last, where it has always been.
|
| Square, because the icon is the label. The row already carries one word on
| send, and a second would leave the box with nothing to be wide in.
*/
.assistant-ask > .assistant-mic {
  width: 46px;
  padding: 0;
  color: var(--ink-dim);
}

/*
| Listening. Colour is the whole of the signal — a microphone that is on and
| one that is off are the same drawing otherwise, and there is no room on this
| row for a line of text saying which.
*/
.assistant-mic.is-listening {
  color: var(--ok);
  border-color: var(--ok);
  animation: assistant-listening 1.4s ease-in-out infinite;
}

@keyframes assistant-listening {
  50% {
    opacity: 0.55;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The colour already says it; the breathing was only ever emphasis. */
  .assistant-mic.is-listening {
    animation: none;
  }
}

/* Coaching --------------------------------------------------------------- */
/*
| A card that is a link as a whole — a trainee, a template, a week — rather
| than a card with a link and a button in it. The same card as the trainings
| list draws, so the Coach tab reads as part of the same app.
*/
a.training-card {
  color: inherit;
  text-decoration: none;
}

/*
| What is fixed rather than chosen: the week a coach's builder writes into,
| and the days of a week a coach sent. Text where a control would be, at the
| size the control's value would have been.
*/
.locked-value {
  margin: 0;
  font-size: 15px;
  color: var(--ink);
}

/*
| Whose a week is, beside its name on the trainings list. Quieter than the
| name, and not shouted in capitals like the heading it sits in.
*/
.section-title-note {
  margin-left: 6px;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--ink-faint);
}

/* A week a coach sent, on the programs screen: a name, not a field. */
.program-card-name {
  font-size: 16px;
  font-weight: 600;
}

.program-card-from {
  margin: 2px 0 0;
  font-size: 12.5px;
}

/* The public page's address on the Coach tab: something to read and copy. */
.coach-page-address {
  margin: 8px 0 0;
  font-size: 14px;
  color: var(--ink);
  overflow-wrap: anywhere;
}

/* The Coach tab's share line: nothing until a share or a copy fills it. */
.share-status:empty {
  display: none;
}

/*
| A section's heading with the one action that belongs to it beside it — the
| Coach tab's "Add a trainee". The heading keeps its own look; the row only
| puts the two on one line and takes over the spacing the heading had.
*/
.section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 4px 0 12px;
}

.section-head > .section-title {
  margin: 0;
}

/* Two panels stacked, for a browser that never folded them into tabs. */
.section-panel:not([hidden]) + .section-panel:not([hidden]) {
  margin-top: 26px;
}

/* An account's face ------------------------------------------------------ */
/*
| The picture somebody chose, or the first letter of their name where they did
| not. A fixed box either way, so a slow picture does not move the row around
| it, and a circle because a face is the one thing nobody needs framed.
*/
.avatar {
  flex: none;
  display: block;
  border-radius: 999px;
  object-fit: cover;
  background: var(--surface-2);
}

.avatar-md {
  width: 40px;
  height: 40px;
  font-size: 16px;
}

.avatar-lg {
  width: 72px;
  height: 72px;
  font-size: 24px;
}

.avatar-blank {
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* A name with its face beside it, at the head of a page or a panel. */
.person-head {
  display: flex;
  align-items: center;
  gap: 14px;
}

/* The picture row on the settings screen: the face, then what to do with it. */
.picture-row {
  display: flex;
  align-items: center;
  gap: 12px;
}

.picture-row form {
  margin: 0;
}

/*
| On a session card, beside its three lines of name, numbers and feeling: about
| as tall as the three of them, so the face reads as the card's subject rather
| than as a bullet in front of its first line.
*/
.avatar-card {
  width: 44px;
  height: 44px;
  font-size: 15px;
}

/*
| In the sheet a card opens, beside whose it is, the training's name, its date
| line and its score — about as tall as those four lines.
*/
.avatar-sheet {
  width: 72px;
  height: 72px;
  font-size: 24px;
}

/*
| A card with a face: the picture on the left, and the name, the numbers and
| the feeling stacked beside it. The column may shrink to nothing rather than
| push the picture out of a narrow card — the name already wraps anywhere.
*/
/*
| Measured by its own width rather than the screen's: the same card is 300px
| wide alone on a day and 90px wide as one of three on a phone, and only the
| card knows which it is.
*/
.timeline-card {
  container-type: inline-size;
}

.timeline-card.has-face .timeline-card-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

/*
| Too narrow for a face and a column of text beside it — what is left over
| would be a word a line, or a letter. The face goes on top, and the text has
| the card's whole width under it.
*/
@container (max-width: 140px) {
  .timeline-card.has-face .timeline-card-row {
    flex-direction: column;
    align-items: flex-start;

    /* Clear of the initials' pill, which hangs below the circle. */
    gap: 12px;
  }
}

/*
| The card is a box and this button is all of it: unstyled, and stretched over
| the whole card by its `::after`, so a press anywhere opens the session — and
| the face, which is above it, can be pressed on its own. See
| partials/timeline_card.edge for why the card stopped being the button.
*/
.timeline-card {
  position: relative;
}

.timeline-card-open {
  display: block;
  flex: 1 1 auto;
  min-width: 0;
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.timeline-card-open::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

.timeline-card-open:focus-visible {
  outline: none;
}

.timeline-card-open:focus-visible::after {
  outline: 2px solid var(--line-strong);
  outline-offset: 2px;
}

/*
| Your own face, with no picture yet: your initials, and a camera in the
| corner saying that pressing it chooses one. Above the stretched button, so
| the press lands here rather than opening the session.
*/
.avatar-choose {
  position: relative;
  z-index: 1;
  flex: none;
  cursor: pointer;
}

/*
| Your own face with no picture yet: a camera in the circle, in a light tone,
| and your initials in a small circle centred under it, across the bottom
| edge — the face says it can be changed, and the pill still says whose it is.
*/
.avatar-choose-camera {
  color: rgba(255, 255, 255, 0.7);
}

.avatar-choose-camera svg {
  width: 48%;
  height: 48%;
}

.avatar-choose:hover .avatar-choose-camera {
  color: #fff;
}

.avatar-choose-initials {
  position: absolute;
  left: 50%;
  bottom: -4px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 15px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--own);
  color: var(--bg);
  border: 1.5px solid var(--own-bg);
  font-size: 7.5px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  white-space: nowrap;
}

.avatar-choose.is-sheet .avatar-choose-initials {
  min-width: 22px;
  height: 18px;
  font-size: 9px;
}

/* Read aloud, never drawn. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* "You", above your own session — in your colour, as your card is. */
.session-entry-owner.is-own {
  color: var(--own);
}

/*
| An exercise in a session's sheet with its picture: the two poses on the
| left, as the session's face is on the left of the heading above, and the
| name and the sets beside it. Big enough to see the movement in, which a
| thumbnail the size of a face was not.
*/
.history-exercise.has-art {
  display: flex;
  align-items: center;
  gap: 12px;
}

.history-exercise-body {
  flex: 1 1 auto;
  min-width: 0;
}

.movement-thumb.is-history {
  width: 96px;
  height: 96px;
  border-radius: 12px;
}

/*
| The face above them stays a face's size, centred in a slot as wide as the
| pictures, so the heading's text starts where every exercise's does.
*/
.session-entry-head > .avatar,
.session-entry-head > .avatar-choose {
  margin-inline: 12px;
}
