/*
| Workout — application styles.
|
| Same structural language as the JIE app it grew out of: a fixed blurred top
| bar, a bottom tab bar that folds into pills on a wide screen, rounded panels
| on a flat background. The palette is where they part company. Everything here
| is neutral — near-black, greys, white — and colour is spent only where it
| carries meaning: a set succeeded, a set was missed, a timer is running.
|
| Mobile-first. The desktop overrides live at the bottom of each section.
*/

:root {
  --appbar-h: 52px;
  --tabbar-h: 58px;
  --release-h: 26px;

  --bg: #0b0c0e;
  --surface: #141619;
  --surface-2: #1c1f23;
  --surface-3: #24282d;

  --line: rgba(255, 255, 255, 0.1);
  --line-strong: rgba(255, 255, 255, 0.22);

  --ink: #f2f3f5;
  --ink-dim: rgba(242, 243, 245, 0.56);
  --ink-faint: rgba(242, 243, 245, 0.32);

  /* The only saturated colours in the app. */
  --ok: #3fbf7f;
  --ok-bg: rgba(63, 191, 127, 0.14);
  --fail: #e5484d;
  --fail-bg: rgba(229, 72, 77, 0.14);
  --partial: #e2a03f;
  --partial-bg: rgba(226, 160, 63, 0.14);

  /*
  | A target that has moved since last session — the brief for the set, not a
  | verdict on it. It sits outside the green/amber/red scale deliberately: a
  | plan asking for more is neither good news nor bad, and any of those three
  | would have been read as a result before the set was even done.
  */
  --rise: #4c9ff0;

  /*
  | The balance wheel's ramp: how much of a program a muscle group gets. One
  | step per ring of the wheel — see #services/balance_chart.
  |
  | A heat scale, cool green through gold and orange to red. Blue to red was
  | the first one and it read as two scales rather than one: blue is already
  | spoken for above, and a blue wedge next to a red one looks like two kinds
  | of thing rather than two ends of the same measurement. The heat ramp is one
  | idea the whole way along, and it is the one everybody already knows how to
  | read.
  |
  | It is a scale and not a verdict, which is the thing to hold on to about it:
  | red is the group a program gives the most to, and every program has one,
  | including a perfectly balanced program. Whether a group is in the right
  | place is said in words beside it. Which is why these are their own colours
  | rather than the --ok / --partial / --fail three above, which mean a set
  | went well or badly everywhere else in the app — near enough to be a family,
  | deliberately not the same values.
  */
  --load-1: #52b87c;
  --load-2: #9cc44f;
  --load-3: #dcb63f;
  --load-4: #e8913f;
  --load-5: #e2564b;

  /*
  | The same wheel, read the other way: one training's part of the program it
  | is in, drawn on the builder under the form.
  |
  | There the ramp above is gone entirely and the wheel is two colours — the
  | program in white, this training's share of it in blue. Which group is the
  | big one is the program screen's question; the builder's is "what does this
  | training cover", and one wedge cannot answer both at once without becoming
  | two charts in one picture.
  |
  | The same blue as --rise, and deliberately not the same variable: that one
  | is a target that has gone up since last session, this one is the training
  | you are editing. A value two meanings share is a value that gets changed
  | for one of them and quietly moves the other.
  */
  --mine: #4c9ff0;
  --mine-ground: rgba(242, 243, 245, 0.86);

  /*
  | Your own sessions, on the cards of the home screen's row and the History
  | tab's timeline. A ground and an edge rather than a mark, so yours can be
  | found across a row at a glance without reading a single caption.
  |
  | The same blue again, and its own variables for the reason --mine gives:
  | "yours among your gymbros'" is a third meaning, and it must be free to move
  | without taking the builder's wheel or a raised target with it.
  */
  --own: #4c9ff0;
  --own-bg: rgba(76, 159, 240, 0.14);
  --own-bg-hover: rgba(76, 159, 240, 0.2);
  --own-line: rgba(76, 159, 240, 0.45);

  /*
  | The system stack rather than a web font. A font fetched from a CDN is a
  | network dependency, and this app is expected to work in a basement gym
  | with no signal.
  |
  | A variable rather than one declaration on `body`, because the share picture
  | is laid out where there is no body to inherit from — inside an <svg>, by
  | public/session_image.js — and the type there has to be this type or the
  | picture is not the card.
  */
  --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;

  --radius: 14px;
  --radius-sm: 10px;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  height: auto;
  min-height: 100%;
}

/*
| No pinch-zoom, anywhere.
|
| `pan-x pan-y` is every gesture this app has a use for and none of the ones
| it does not: the page scrolls, a row still drags, and neither two fingers
| spreading apart nor a double-tap does anything at all. Both of those matter
| — `pan-x pan-y` is stricter than `manipulation`, which keeps pinch-zoom, and
| stricter than `auto`, which keeps the double-tap as well.
|
| On `html` because the effective touch-action of an element is narrowed by
| its ancestors and never widened: a descendant cannot hand either gesture
| back, whatever it declares for itself.
|
| This is Chromium's half, and Chromium is the Pixel. WebKit reads
| touch-action from the element a touch lands on and is unreliable about
| taking a pan restriction from the root, which is why the viewport meta and
| the `gesture*` listener in partials/head.edge are not belt-and-braces for
| this rule but the parts that carry iOS.
|
| It is the whole app on purpose. There is nothing here to magnify — no body
| text set small, no photographs, no tables wider than the screen — and the
| layout is bars pinned to the edges with sheets that fill it, which a zoom
| does not enlarge so much as derange. The gesture's only regular appearance
| was by accident, from a hand reaching to scroll mid-set.
|
| Reading small text is still answered, one level down, by the platform's own
| accessibility zoom, which none of these three touch.
*/
html {
  touch-action: pan-x pan-y;
}

body {
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--bg);
  overflow-x: hidden;
  -webkit-text-size-adjust: 100%;
  padding-top: calc(var(--appbar-h) + env(safe-area-inset-top));
  padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
}

/*
| The deploy banner is fixed under the app bar, so it takes its height out of
| the page rather than out of the first thing on it. Set by the shell and by
| the runner — see resources/views/partials/release_banner.edge.
*/
body.has-release {
  padding-top: calc(var(--appbar-h) + var(--release-h) + env(safe-area-inset-top));
}

a {
  color: inherit;
}

/*
| A second quick tap on a control is a second press, not a zoom.
|
| Safari on iOS holds every tap for about 300ms in case it turns out to be the
| first half of a double-tap, which it would answer by zooming in. Two fast
| taps on the runner's + then magnify the page instead of adding two reps —
| exactly the gesture somebody makes when the set went better than planned.
|
| `manipulation` opts these elements out of that one gesture and keeps the
| panning. It reads as redundant now that `html` is `pan-x pan-y` above, and
| on Chromium it is: that rule already covers the whole page. It is still here
| because WebKit is the browser this was written for, and WebKit takes
| touch-action from the element the touch landed on rather than from the root
| — so on iOS this is the declaration that actually answers a fast second tap
| on the runner's +, and the root rule is the one that might not.
|
| Which leaves the double-tap alive on iOS on everything that is *not* one of
| these — a card, a heading, blank space — in a Safari tab, where the viewport
| meta is ignored too. An installed app honours the meta, that is how this app
| is used, and widening this list to cover the rest was judged not worth what
| it costs to reason about later.
|
| `.drag-handle` sets `touch-action: none` of its own; a class outranks these
| bare element selectors, so the grip keeps it.
*/
button,
a,
label,
summary {
  touch-action: manipulation;
}

/* Top bar --------------------------------------------------------------- */
/*
| The safe-area inset is 0 while a browser toolbar is in the way and only
| grows once the app is installed, where the page runs under the status bar.
*/
.app-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  height: calc(var(--appbar-h) + env(safe-area-inset-top));
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: env(safe-area-inset-top) 8px 0 16px;
  background: rgba(11, 12, 14, 0.78);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}

.app-mark {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
}

/*
| The screen's name, beside the mark.
|
| `margin-right: auto` rather than a wrapper around the two: an auto margin
| takes the bar's free space before `justify-content` sees any, so the name
| stays against the mark and the icons stay against the far edge — which is the
| arrangement the bar already had with two children, kept with three.
|
| Lighter and unspaced where the mark is bold and letter-spaced, so the pair
| reads as a name and where you are in it rather than as two labels. Truncated
| rather than wrapped: "Einstellungen" beside four icons on a narrow phone is
| the case that runs out of room, and a bar that grew a second line would move
| every screen down to say something it has already mostly said.
*/
.app-bar-title {
  min-width: 0;
  margin-right: auto;
  padding-left: 9px;
  font-size: 12.5px;
  font-weight: 400;
  letter-spacing: 0.01em;
  color: var(--ink-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/** The trailing icons, kept together so the mark can sit alone on the left. */
.app-bar-actions {
  display: flex;
  align-items: center;
}

.app-bar-form {
  margin: 0;
  display: flex;
}

.app-bar-action {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  color: var(--ink);
  opacity: 0.6;
  text-decoration: none;
  background: none;
  border: none;
  cursor: pointer;
}

.app-bar-action:hover {
  opacity: 1;
}

/*
| The notification bell, and the line through it.
|
| Slash or no slash is the whole signal: the two states are "you will be told"
| and "you will not", and neither is a result, so neither gets a colour. Green
| for on would read as a set that went well.
|
| The line is a child of the same drawing rather than a second icon, so it can
| simply go when there is nothing to strike out.
*/
.push-bell[data-push='on'] {
  opacity: 1;
}

.push-bell[data-push='on'] .push-bell-slash {
  display: none;
}

/*
| Blocked is not a switch. The browser has remembered a refusal and script
| cannot ask again, so the bell reports the state and stops there — fainter
| than off, and with the hover and the pointer taken away so it promises
| nothing. What to do about it is on the settings screen, in words.
*/
.push-bell[data-push='blocked'],
.push-bell[data-push='blocked']:hover {
  opacity: 0.28;
  cursor: default;
}

/* Mid-flight. Subscribing is a round trip, and on a gym's signal a slow one. */
.push-bell[data-busy],
.push-bell[data-busy]:hover {
  opacity: 0.28;
}

/* Tabs ------------------------------------------------------------------ */
/*
| Kept outside .app-bar: backdrop-filter creates a containing block, which
| would anchor this fixed element to the header instead of the viewport.
*/
.tabbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  display: flex;
  height: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
  /*
  | A shade above the page rather than the same near-black.
  |
  | Installed on an iPhone the inset below is the home indicator's, ~34px of
  | bar with nothing drawn in it. Painted in the page's own colour that band
  | reads as a gap under the tabs — the bar looks like it has come unstuck from
  | the bottom, when it is in fact flush against it. One step lighter, the same
  | step the cards take, and the whole block reads as the bar it is.
  */
  background: rgba(20, 22, 25, 0.94);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-top: 1px solid var(--line);
}

.tab {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  color: var(--ink);
  opacity: 0.45;
  text-decoration: none;
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.tab:hover {
  opacity: 0.75;
}

.tab.is-active {
  opacity: 1;
}

.tab svg {
  display: block;
}

/* Release banner --------------------------------------------------------- */
/*
| "The app goes down at six on Saturday", carried on every screen until it
| does. Fixed directly under the app bar, which is the only place a notice
| about the whole app can sit without belonging to one screen or another.
|
| Under the bar rather than over it: the app bar is where you are, and a strip
| that pushed it off the top of the screen would move the one landmark that
| never moves. The z-index keeps it below the bar for the same reason — the
| two never overlap, but if a safe-area inset ever made them, the bar wins.
*/
.release-bar {
  position: fixed;
  top: calc(var(--appbar-h) + env(safe-area-inset-top));
  left: 0;
  right: 0;
  z-index: 19;
  height: var(--release-h);
  display: flex;
  align-items: center;
  overflow: hidden;
  font-size: 12px;
  line-height: 1;
  border-bottom: 1px solid;
}

/*
| The marquee. One strip of two identical copies, slid by half its own width,
| so the copy behind is always covering the gap the copy in front leaves —
| there is no seam and no moment where the strip is empty.
|
| It travels leftwards, so the sentence arrives head first: the opening word
| comes in at the right edge and the rest follows it across, which is the order
| you would read it in standing still. Running the other way puts the full stop
| on screen first and asks you to read the thing backwards.
|
| `width: max-content` is what makes the halves equal: the track is as wide as
| its two copies want to be, so -50% is exactly one copy every time, whatever
| the sentence came out as in this language.
*/
.release-track {
  display: flex;
  width: max-content;
  will-change: transform;
  animation: release-scroll var(--release-scroll, 20s) linear infinite;
}

/*
| At least a viewport wide, whatever the sentence needs.
|
| Without the floor a short one — Chinese is about half the length of German —
| makes a strip narrower than a desktop window, and the translation above then
| slides a bare stretch of colour through the middle of it. The two copies
| cover the screen between them at every point of the pass only if each one is
| at least a screen; the padding is the gap between repeats when the sentence
| is the wider of the two, which on a phone it always is.
*/
.release-text {
  /*
  | Never shrunk to fit. `min-width` below is what allows a flex item to be
  | squeezed under its content, and squeezing is precisely what must not
  | happen here — the strip is wider than the window on purpose.
  */
  flex: none;
  min-width: 100vw;
  padding: 0 24px;
  white-space: nowrap;
}

/*
| Not moving, because it does not have to: the sentence fits, so the strip is
| a line of text rather than a marquee. Put there by the script in the partial,
| which is the only thing that can compare the two.
*/
.release-bar.is-still .release-track {
  animation: none;
  width: 100%;
  justify-content: center;
}

.release-bar.is-still .release-text {
  min-width: 0;
}

.release-bar.is-still .release-text[aria-hidden='true'] {
  display: none;
}

@keyframes release-scroll {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

/*
| Green well ahead, amber as it comes into the week, red once it is about to
| happen — the thresholds are in #services/release_notices. The same three
| colours the set pills use, and read the same way: how much room is left.
*/
.release-calm {
  color: var(--ok);
  background: var(--ok-bg);
  border-bottom-color: rgba(63, 191, 127, 0.3);
}

.release-soon {
  color: var(--partial);
  background: var(--partial-bg);
  border-bottom-color: rgba(226, 160, 63, 0.3);
}

.release-imminent {
  color: var(--fail);
  background: var(--fail-bg);
  border-bottom-color: rgba(229, 72, 77, 0.3);
}

/* Content --------------------------------------------------------------- */
.main {
  max-width: 640px;
  margin: 0 auto;
  padding: 18px 16px 36px;
}

.main-wide {
  max-width: 860px;
}

.section-title {
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin: 26px 0 10px;
}

.section-title:first-child {
  margin-top: 4px;
}

.page-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}

.page-title {
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

/*
| A control that belongs to the heading rather than to the page: the builder's
| Share, beside the name of the training it sends.
|
| `margin-right: auto` is what keeps it there. The head is a space-between row,
| so a second child would otherwise be thrown to the far margin — which is the
| right place for a page's own actions and the wrong one for this: out there it
| would read as "share this screen", and the thing being shared is the training
| the heading names.
|
| Centred against the heading rather than sitting on its baseline, which the
| head aligns everything else to. A button with nothing in it but a glyph has
| its baseline at the bottom of the box, so on the row's baseline it hangs
| below the words it stands beside.
*/
.page-title-action {
  margin-right: auto;
  align-self: center;
}

/*
| And the line under that heading: what sharing does, and where the desktop
| fallback says the link has been copied. Pulled up under the title, since the
| head's own margin is sized for a heading with nothing after it.
*/
.page-head-hint {
  margin: -8px 0 16px;
}

/*
| More than one thing to do from a page's heading, kept on one line.
|
| `margin-left: auto` rather than leaning on the head's `space-between`, which
| only pushed this right while there was a heading beside it to be pushed away
| from. The trainings list has moved its heading into the app bar and the row
| is on its own now — and New belongs at the far right whether or not anything
| shares the line with it.
*/
.page-head-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
  margin-left: auto;
}

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

.faint {
  color: var(--ink-faint);
}

.empty {
  color: var(--ink-dim);
  text-align: center;
  padding: 40px 12px;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
}

.stack > * + * {
  margin-top: 10px;
}

/* A line above a list, explaining why its cards are behaving unusually. */
.list-note {
  margin-bottom: 10px;
  font-size: 13px;
}

/*
| A standing aside — something true about the screen rather than a response to
| anything done on it. Quieter than a flash message, which is why it is not
| one: this stays put, and a notification that never goes away stops being read.
*/
.notice {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 8px;
  margin-bottom: 14px;
  padding: 10px 13px;
  font-size: 13px;
  color: var(--ink-dim);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

.notice a {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Panels and cards ------------------------------------------------------ */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px;
}

.training-card {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 14px 14px 16px;
}

.training-card-body {
  flex: 1 1 auto;
  min-width: 0;
  text-decoration: none;
  color: inherit;
}

.training-card-name {
  font-size: 16px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
| A name with a tag after it — "Draft", "Public", "Hidden". Ellipsized as one
| line, a long name pushed the tag past the edge, so on a phone the tag was
| the first thing cut. Here the name is the part that gives way. `:has()`
| because the tag is conditional in two of the three places that draw one.
*/
.training-card-name:has(> .tag) {
  display: flex;
  align-items: baseline;
  gap: 6px;
}

.training-card-name:has(> .tag) > .training-card-name-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.training-card-name > .tag {
  flex: none;
}

.training-card-meta {
  margin-top: 3px;
  font-size: 12.5px;
  color: var(--ink-dim);
}

/* The days a training is meant to be done on, on its card. -------------- */
/*
| Under the meta line rather than in it. The line above says how big the
| training is and when it was last done, both of which are about the training
| itself; this is about when it is meant to happen, and running the two
| together made a sentence that changed subject halfway.
|
| A label and nothing more. The Start button beside it works whatever day it
| is — this screen is a plan, not a calendar that locks — so the days are set
| in the quietest type on the card rather than as a badge that looks like a
| gate.
*/
.training-card-days {
  margin-top: 4px;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The days a training is done on, in the builder. ----------------------- */
/*
| Seven toggles that have to fit the narrowest phone the app runs on, so they
| share the width rather than taking what they need — `1 1 0` and a minimum of
| nothing, since "Wed" and "Sat" are not the same width in every language.
*/
.day-chips {
  display: flex;
  gap: 6px;
}

.day-chip {
  flex: 1 1 0;
  min-width: 0;
  min-height: 40px;
  padding: 8px 4px;
  font: inherit;
  font-size: 13px;
  color: var(--ink-dim);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.day-chip[aria-pressed='true'] {
  color: var(--ink);
  background: var(--surface-3);
  border-color: var(--line-strong);
  font-weight: 600;
}

/* Programs -------------------------------------------------------------- */
/*
| A program is a name, so its screen is a column of name fields. The field is
| the control: there is one thing to change about a program, and a row that
| showed it as text with an edit button beside it would be a press away from
| the same box.
|
| Each of those rows is a .field-row — see Forms above. The gap between the
| naming field and the list under it is the only thing this screen adds.
*/
.program-new {
  margin-bottom: 4px;
}

.program-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px;
}

.program-card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 8px;
  font-size: 12.5px;
}

/*
| Share and Delete, kept together at the far end so the count stays alone at
| the near one. Two buttons about the program rather than one button and a
| number.
*/
.program-card-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

/*
| What a press of Share did, which on a desktop is the only sign it did
| anything — a phone opens its share sheet and this stays empty. Collapsed
| while it is, so a card carries no blank line waiting for a message that may
| never come.
*/
.share-note {
  margin: 8px 0 0;
  font-size: 12.5px;
  color: var(--ink-dim);
}

.share-note:empty {
  display: none;
}

/* The balance report ----------------------------------------------------- */
/*
| A wheel per program, on the program's own card: eight sectors around a hub,
| one per muscle group, with the group named in a ring of labels around it.
|
| It replaced eight stacked bars on a tab of its own. The bars were exact and
| read like a table — eight lengths compared one at a time down a column — and
| the question this report is actually asked is about the shape of a week,
| which a wheel answers in one look. Eight rows is also most of a phone screen,
| and the report had to fit on a card under a program's name without turning
| the list into something nobody scrolls to the end of.
|
| The sectors are coloured by how much of the program a group gets — blue at
| the first ring, red at the last — which is the same reading as the length,
| said twice. Eight wedges of one grey is a chart that has to be read one wedge
| at a time before the big one can be found, and the first thing anybody wants
| off this card is which way their week leans.
|
| So colour here is a scale and not a verdict, which is the opposite of what it
| means everywhere else in the app: a red sector is the group trained most, and
| every program has one. What is *wrong* is said in words — the number beside
| the wheel turns amber and carries "light", "heavy" or "none" — and marked on
| the sector itself at the end of the range it crossed.
*/
/*
| Held off the name field above it by a rule, so the card reads as a heading
| and then a report on it rather than as one block of mixed furniture.
*/
.balance {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}

.balance-meta {
  font-size: 12.5px;
}

/*
| The one line that says whether there is anything to do about this program.
| Above the wheel rather than below it: a verdict you have to scroll past the
| evidence to reach is one nobody reads.
*/
.balance-verdict {
  font-size: 13px;
  margin-top: 2px;
}

.balance-verdict.is-ok {
  color: var(--ink-dim);
}

.balance-verdict.is-off {
  color: var(--partial);
}

/*
| The key to the builder's two-colour wheel, and the one thing on that panel
| the drawing cannot say: how much of the whole program this training is.
|
| Above the wheel, under the verdict. A legend below the picture it explains is
| read after the picture has already been misread once.
*/
.balance-legend {
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--ink-dim);
  margin-top: 6px;
}

/*
| The wheel, and the eight names pinned around it.
|
| The names sit on their own sector's centre line, just outside the rim, each
| one hung off that pin by half of its own size along the same radius — so the
| name at the top is centred above the wheel, the one at three o'clock sits
| wholly to its right, and the four on the diagonals lean the way their sector
| points. One rule, eight positions; the angles come from the drawing itself
| (see #services/balance_chart) so a name cannot drift off the wedge it names.
|
| They were laid out in a three-by-three grid first, a cell above and a cell to
| each side. Nothing ever collided and nothing was ever where it belonged: the
| short names sat out at the edge of the card, and the four corner ones shared
| a line with the top and the bottom instead of standing on their own diagonal.
|
| The padding is the room the ring of names needs: the drawing is the plot
| below, and every name hangs outside it.
*/
.balance-wheel {
  padding: 40px 0;
  margin: 2px 0;
}

.balance-wheel-plot {
  position: relative;
  width: clamp(128px, 36vw, 190px);
  aspect-ratio: 1;
  margin: 0 auto;
}

.balance-wheel-chart {
  display: block;
  width: 100%;
  height: 100%;
}

/* Hub to rim, whatever the share: the length every sector is read against. */
.wheel-tracks path {
  fill: var(--surface-2);
}

.wheel-fills path.is-load-1 {
  fill: var(--load-1);
}

.wheel-fills path.is-load-2 {
  fill: var(--load-2);
}

.wheel-fills path.is-load-3 {
  fill: var(--load-3);
}

.wheel-fills path.is-load-4 {
  fill: var(--load-4);
}

.wheel-fills path.is-load-5 {
  fill: var(--load-5);
}

/*
| The builder's wheel: the program in white, and the part of it the training
| being edited puts there in blue.
|
| The heat ramp is dropped rather than kept alongside, which is what the
| override above it is doing. Five load colours plus a blue is six colours in
| one drawing saying two different things, and the reading this wheel is for —
| which groups does this training cover, and how much of each — is a
| comparison between two lengths in the same wedge. Two colours, one question.
|
| White for the program because it is the ground here rather than the subject:
| a little under full ink, since eight solid wedges at the body text's own
| white is a lamp on a dark card.
*/
.balance-wheel.is-mine .wheel-fills path {
  fill: var(--mine-ground);
}

.wheel-mine path {
  fill: var(--mine);
}

/*
| And the out-of-range mark turns amber on this wheel, where the white one
| would be invisible against the wedge it is drawn across. Amber is free here —
| there is no amber wedge for it to disappear into, as there is on the ramp —
| and it is the colour this app already uses for a group outside its range, on
| the number and the word beside this very drawing.
*/
.balance-wheel.is-mine .wheel-marks path {
  stroke: var(--partial);
  stroke-width: 1.6;
}

/*
| The rings, in the card's own colour: they are gaps cut across the sectors
| rather than lines drawn on them. A sector divided into bands reads as a
| length to be counted, where the same sector drawn solid reads as a blob —
| the markings on a ruler do the same work.
*/
.wheel-rings circle {
  fill: none;
  stroke: var(--surface);
  stroke-width: 2.5;
}

/*
| The mark on a sector that is out of range: the end of the range it crossed.
| Drawn last so the ring gaps above do not break it up, and in white rather
| than in amber — over an amber wedge, amber is not a mark.
|
| Only the sectors that are off carry one. Both ends of every band was the
| first drawing, and sixteen hairlines through a picture made of arcs is a
| chart that has to be explained before it can be read.
*/
.wheel-marks path {
  fill: none;
  stroke: rgba(255, 255, 255, 0.72);
  stroke-width: 1.4;
}

/*
| A name, its share, and — only when there is something to say — the one word
| for which way it is off. The name is the fainter of the two lines: the
| number is what is being looked up, and the name is how you find it.
*/
/*
| Placed by the `left`, `top` and `transform` the template writes on each one.
| The width is capped rather than fixed: a one-word name takes the width of the
| word, and the two long ones wrap onto a second line instead of reaching for
| the edge of the card. In vw, because the room a name has is the card's margin
| and the card is the width of the screen.
*/
.balance-label {
  position: absolute;
  width: max-content;
  max-width: clamp(70px, 20vw, 130px);
  font-size: 11.5px;
  line-height: 1.3;
}

.balance-label-name {
  display: block;
  color: var(--ink-faint);
}

.balance-label-value {
  display: block;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.balance-label.is-off .balance-label-value {
  color: var(--partial);
}

.balance-label-tag {
  font-size: 9.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--partial);
}

/*
| This training's share, beside the program's own — the number the blue wedge
| draws. In the wedge's colour, so the two are read as one thing, and after the
| program's share rather than instead of it: the range and the verdict beside
| it are the program's, and a number in front of them that was not the one they
| judge would make both of them wrong.
*/
.balance-label-mine {
  color: var(--mine);
}

.balance-label-mine::before {
  content: '·';
  margin: 0 1px 0 0;
  color: var(--ink-faint);
}

/*
| Which way a wrapped name reads: outwards, away from the wheel. The three on
| the left end against it, the three on the right begin against it, and the two
| it stands over are centred on it. Only the wrapped ones show the difference —
| which is both of the long ones, in every language that has them.
*/
.balance-label.is-n,
.balance-label.is-s {
  text-align: center;
}

.balance-label.is-nw,
.balance-label.is-w,
.balance-label.is-sw {
  text-align: right;
}

.balance-note {
  font-size: 12px;
  margin-top: 4px;
}

/* How the report is worked out, folded shut ----------------------------- */
/*
| Under the wheel it explains, on every card that draws one. Shut by default:
| the chart is what the card is for, and an explanation that has to be
| scrolled past on every visit is one that stops being read on the second.
|
| Under rather than over, which is the whole reason it is a row of its own and
| not a button in the corner of the drawing. An explanation that opened above
| the wheel would push the wheel down the page at the moment somebody wanted
| to read the two against each other.
|
| The fold and its chevron are the shared week's, deliberately — there are two
| disclosures in this app and they should not open differently. What is added
| is the mark: a bordered `i` that says the row is an explanation rather than
| more of the report.
*/
.balance-help {
  margin-top: 2px;
}

.balance-help-summary {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 2px;
  font-size: 12.5px;
  color: var(--ink-dim);
  cursor: pointer;
  list-style: none;
}

.balance-help-summary::-webkit-details-marker {
  display: none;
}

.balance-help-mark {
  flex: none;
  width: 17px;
  height: 17px;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  font-size: 11px;
  font-style: italic;
  line-height: 15px;
  text-align: center;
}

/* The chevron, after the label rather than before it: the mark leads the row. */
.balance-help-summary::after {
  content: '';
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transform-origin: 60% 60%;
  transition: transform 120ms ease;
}

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

/*
| Set in from the summary and against a rule, so an open panel reads as a
| passage held apart from the report rather than as the top of it.
*/
.balance-help-panel {
  margin: 2px 0 6px;
  padding: 2px 0 4px 13px;
  border-left: 1px solid var(--line);
  font-size: 13px;
  line-height: 1.55;
  color: var(--ink-dim);
}

.balance-help-panel p {
  margin-bottom: 12px;
}

.balance-help-title {
  margin-bottom: 5px;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.balance-help-title + p {
  margin-top: 0;
}

/* The worked example, marked as one: it is the same explanation in numbers. */
.balance-help-example {
  color: var(--ink-faint);
}

/*
| The eight groups. A name and its range on one line, the muscles under it —
| which is the shape of the question being asked, "what counts as Back, and how
| much of it is expected".
*/
.balance-help-groups {
  margin-bottom: 14px;
}

.balance-help-groups dt {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-top: 9px;
  color: var(--ink);
}

/*
| Laid out as a flex row rather than as a run of text, so the spacing is this
| stylesheet's and not the template's. Between two inline spans the browser
| collapses the newline in the markup to a space, which put a gap before each
| separator and none after it — "Lats ·Upper back". Flex drops that whitespace
| altogether and the gap below is the only thing setting the rhythm.
*/
.balance-help-groups dd {
  display: flex;
  flex-wrap: wrap;
  column-gap: 5px;
  color: var(--ink-faint);
  font-size: 12px;
}

/* Between the muscles, never before the first or after the last. */
.balance-help-groups dd span + span::before {
  content: '·';
  margin-right: 5px;
  color: var(--line-strong);
}

.balance-help-band {
  flex: none;
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}

/* Trainings that have been put away -------------------------------------- */
/*
| Nothing of the archive is drawn on the trainings list — not the cards it used
| to fold away at the bottom, and not a line pointing at them either. That
| screen is what you run today, and everything on it should be startable.
|
| The cards below are the Archive panel's, on the settings screen.
*/

/*
| Hollow where a live card is solid. An archived training is still readable at
| a glance — the name is what somebody is scanning for — but the card it sits
| on is plainly not one of the ones being offered to run.
|
| A column rather than the row a live card is, because this one carries two
| controls instead of a single Start. Side by side with the name they would
| leave it a strip too narrow to read; under it they have a line of their own
| and the name has the width of the card.
*/
.training-card.is-archived {
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  background: transparent;
  border-style: dashed;
}

/*
| Restore and Delete, right-aligned so the row reads as a pair of controls
| under the card rather than as two more lines of it. Not stretched to the full
| width: they are not equals — one puts the training back and the other is the
| end of it — and giving them the same block would say they were.
*/
.training-card-actions {
  display: flex;
  justify-content: flex-end;
  gap: 6px;
}

/* Panels behind a strip of tabs ------------------------------------------ */
/*
| One screen cut into parts you choose between, rather than scroll through.
| Used by the settings screen, which had grown a language picker, two switches,
| a browser permission, two links to other screens, a date field, some credits
| and a version number into a single column — and by the Coach tab, which is
| trainees on one side and programs on the other.
|
| The page is rendered whole and folded by public/section_tabs.js — the strip is
| drawn `hidden` and shown only once that module has claimed it, so a browser
| that never runs it gets every panel, one after another, under the headings
| the tabs would have replaced. Nothing is ever hidden behind a control that is
| not working.
*/
.section-tabs {
  display: flex;
  gap: 2px;

  margin: 2px 0 14px;
  border-bottom: 1px solid var(--line);

  /*
  | Five tabs and a long word in some languages — "Préférences", "Einstellungen"
  | — will not always divide 343px of phone. The strip scrolls rather than
  | wrapping to a second row: a tab bar two lines deep stops reading as one
  | control. The bar is hidden because a scrollbar drawn across the underline
  | doubles it.
  */
  overflow-x: auto;
  scrollbar-width: none;
}

/*
| Up against the app bar when it is the first thing on the screen, which since
| the heading moved into that bar it usually is.
|
| The negative margin pulls back most of `.main`'s 18px — a strip of tabs is
| chrome belonging to the bar above it rather than the first item of content,
| and a screen that opened on a band of empty page was the thing moving the
| heading was supposed to fix.
|
| Scoped to `:first-child` so it never eats the space under a flash message,
| which renders above it and only sometimes.
*/
.section-tabs:first-child {
  margin-top: -10px;
}

.section-tabs::-webkit-scrollbar {
  display: none;
}

/* [hidden] alone loses to `display: flex`. */
.section-tabs[hidden] {
  display: none;
}

.section-tab {
  flex: 1 0 auto;
  padding: 9px 14px;
  font-size: 13px;
  letter-spacing: 0.01em;
  white-space: nowrap;
  text-align: center;
  text-decoration: none;
  color: var(--ink-dim);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;

  /* Over the container's own line, so the two never draw as a step. */
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.section-tab:hover {
  color: var(--ink);
}

.section-tab.is-active {
  color: var(--ink);
  border-bottom-color: var(--ink);
}

/*
| The heading each panel carries for the page that has no tabs, which is the
| page as it is rendered: public/section_tabs.js removes them when it puts the
| strip up, so this only ever applies where the panels are stacked. Without it
| the `:first-child` rule above collapses the gap between two panels to 4px and
| the next heading reads as part of the section it is following.
*/
.section-panel > .section-title:first-child {
  margin-top: 26px;
}

.section-tab:focus-visible {
  outline: 2px solid var(--line-strong);
  outline-offset: -2px;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

/*
| Rows that go somewhere else, where a stack of block buttons used to be.
|
| A block button is something the screen does; these are other screens. Naming
| the destination and pointing at it says that without the label having to be
| read as a verb — which is what "Manage exercises" and "See users" were doing,
| one under the other, in a column of things that were not links at all.
*/
.link-rows {
  display: grid;
  gap: 8px;
}

.link-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  color: var(--ink);
  text-decoration: none;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

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

.link-row-text {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.link-row-title {
  font-size: 15px;
  font-weight: 600;
}

.link-row-hint {
  font-size: 12.5px;
  color: var(--ink-dim);
}

.link-row-chevron {
  flex: none;
  margin-left: auto;
  color: var(--ink-faint);
}

/* Arranging a list ------------------------------------------------------- */
/*
| Shared by the trainings list and the builder's exercises — see public/drag.js.
| On the trainings list the grip is shown only while the list is being arranged,
| in the Start button's place; in the builder it is always there.
|
| `touch-action` is the whole reason a drag starts here and not anywhere on the
| item: it hands this one small square to the script, and leaves every other
| pixel of the list scrolling normally under a finger.
*/
.drag-handle {
  touch-action: none;
  color: var(--ink-dim);
  cursor: grab;
}

.drag-handle:active {
  cursor: grabbing;
}

/*
| Lifted off the list while it is in the air, and above its neighbours so the
| shadow falls on them rather than under them.
*/
.training-card.is-dragging,
.slot.is-dragging {
  position: relative;
  z-index: 2;
  border-color: var(--line-strong);
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.5);
}

/* A drag across a name would otherwise select it on the way past. */
#trainings-list.is-dragging,
#slots.is-dragging {
  user-select: none;
}

/* The install offer ------------------------------------------------------ */
/*
| A card at the top of the trainings list rather than something floating over
| it: the bottom of the screen already belongs to the tab bar and the offline
| notice, and an offer worth refusing should scroll away like anything else.
*/
.install-card {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
  padding: 14px 16px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/* [hidden] alone loses to `display: flex`. */
.install-card[hidden] {
  display: none;
}

.install-mark {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  color: var(--ink-dim);
  background: var(--surface-2);
  border-radius: 11px;
}

.install-text {
  /* Wraps to its own line before the words get too narrow to read. */
  flex: 1 1 190px;
  min-width: 0;
}

.install-title {
  font-size: 15px;
  font-weight: 600;
}

.install-body {
  margin-top: 3px;
  font-size: 12.5px;
  color: var(--ink-dim);
}

.install-actions {
  display: flex;
  gap: 8px;
  width: 100%;
}

.install-actions .btn {
  flex: 1 1 auto;
}

.install-steps {
  display: grid;
  gap: 12px;
  margin-top: 14px;
  padding-left: 22px;
  font-size: 14px;
}

.install-step-icon {
  vertical-align: -3px;
  color: var(--ink);
}

/* Buttons --------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 46px;
  padding: 10px 18px;
  font: inherit;
  font-size: 15px;
  letter-spacing: 0.02em;
  text-align: center;
  text-decoration: none;
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

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

/* The display above outranks the `hidden` attribute; this puts it back. */
.btn[hidden] {
  display: none;
}

.btn:disabled,
.btn[aria-disabled='true'] {
  opacity: 0.4;
  pointer-events: none;
}

.btn-block {
  display: flex;
  width: 100%;
}

.btn-primary {
  color: #0b0c0e;
  background: var(--ink);
  border-color: var(--ink);
  font-weight: 600;
}

.btn-primary:hover {
  background: #ffffff;
}

/*
| Something already under way: the training whose session is open, the warm-up
| length currently counting down. Green because it is a state and not an
| invitation — the button is reporting, not asking.
*/
.btn-running {
  color: #0b0c0e;
  background: var(--ok);
  border-color: var(--ok);
  font-weight: 600;
}

.btn-running:hover {
  background: #56ce97;
}

.btn-quiet {
  background: none;
  border-color: transparent;
  color: var(--ink-dim);
}

.btn-quiet:hover {
  background: var(--surface-2);
  color: var(--ink);
}

.btn-sm {
  min-height: 36px;
  font-size: 13px;
  padding: 6px 12px;
}

.btn-danger {
  color: var(--fail);
  border-color: rgba(229, 72, 77, 0.35);
  background: var(--fail-bg);
}

.btn-danger:hover {
  background: rgba(229, 72, 77, 0.22);
}

.btn-row {
  display: flex;
  gap: 8px;
}

.btn-row > .btn {
  flex: 1 1 0;
}

/* Icon-only square button, used for the slot controls. */
.btn-icon {
  min-height: 36px;
  width: 36px;
  padding: 0;
  flex: 0 0 auto;
}

/* Forms ----------------------------------------------------------------- */
.form-group {
  margin-bottom: 12px;
}

.form-label {
  display: block;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin-bottom: 6px;
}

.form-control {
  display: block;
  width: 100%;
  min-height: 46px;
  padding: 10px 14px;
  font: inherit;
  /* 16px keeps iOS from zooming the page in when a field takes focus. */
  font-size: 16px;
  line-height: 1.4;
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

.form-control:focus {
  outline: none;
  border-color: var(--line-strong);
  background: var(--surface-3);
}

.form-control::placeholder {
  color: var(--ink-faint);
}

/*
| A field showing a value that cannot be typed into — the rep target of an
| exercise taken to failure, which reads "max" and is moved by the switch under
| it and nothing else.
|
| Dimmed and flattened rather than hidden: the reading is worth having, and the
| difference between this and the box beside it should be obvious at a glance
| and not only on being tapped. `opacity` is left alone, because the browser's
| own disabled fading takes the text below the contrast the rest of the panel
| holds to.
*/
.form-control.is-locked {
  background: transparent;
  border-style: dashed;
  color: var(--ink-dim);
  opacity: 1;
  cursor: default;
  /* Safari fades disabled fields on its own, whatever the colour above says. */
  -webkit-text-fill-color: var(--ink-dim);
}

textarea.form-control {
  resize: vertical;
  line-height: 1.55;
}

/*
| A field and the button that commits it, on one line.
|
| For a value that is typed rather than chosen: the button is what says the
| typing is finished. Beside the box rather than under it, because a short name
| and a short verb fit on a line together and a block button under a single
| field reads as the end of a form rather than the end of a field.
*/
.field-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.field-row .btn {
  flex: 0 0 auto;
}

/*
| The one native picker in the app, on the admin's release field.
|
| `color-scheme` is declared here rather than on :root because it is the only
| thing that needs it: without it the browser draws its calendar icon and its
| dropdown for a light page, which on this surface is a dark-grey glyph on
| dark grey and a white sheet over a black app. Scoped to the field so the
| rest of the page keeps whatever the browser was already doing.
*/
input[type='datetime-local'].form-control {
  color-scheme: dark;
}

select.form-control {
  appearance: none;
  -webkit-appearance: none;
}

.field-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.field-grid-3 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

/*
| For a row pairing a short caption with one long enough to wrap — "Sets"
| beside "Rest between sets". Left to itself the grid tops the two columns out
| level, which puts the second line of one caption where the other column's box
| already is, and the two boxes end up on different lines for no reason a reader
| can see. Aligning on the bottom lines the boxes up and lets the captions take
| whatever height they need above them.
*/
.field-grid-end {
  align-items: end;
}

/* Checkbox / toggle row */
.check {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  cursor: pointer;
  font-size: 14px;
}

.check input {
  width: 20px;
  height: 20px;
  accent-color: var(--ink);
  flex: none;
}

/* Check row -------------------------------------------------------------- */
/*
| A tick box down the left edge, its caption to the right, and the caption is
| not part of the target: these sit in a column of number boxes that is taller
| than the screen, and a whole tappable line is something a thumb on its way
| past can flip without ever meaning to. The box is 30px of paint inside a
| 44px-tall target, which is a deliberate press and nothing else.
|
| The gap between them is the box's own right padding rather than the row's, so
| the target reaches into the space instead of stopping at the paint.
*/
.check-row {
  display: flex;
  align-items: center;
  min-height: 44px;
  font-size: 14px;
}

.check-box {
  position: relative;
  display: flex;
  align-items: center;
  flex: none;
  /* Padding, not size: the target grows around the box without moving it. */
  padding: 7px 12px 7px 0;
  cursor: pointer;
}

/* Over the box rather than hidden, so the tap lands on the input itself. */
.check-box input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  margin: 0;
  cursor: pointer;
}

/* Paint only. The tap goes through it to the input lying underneath. */
.check-mark {
  pointer-events: none;
  position: relative;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  background: var(--surface-3);
  border: 1px solid var(--line);
  transition:
    background 0.15s ease,
    border-color 0.15s ease;
}

/*
| The tick: two borders of a box, turned on the diagonal. Centred by `margin:
| auto` against `inset: 0` so it stays put whatever size the box is given, then
| lifted a pixel — a checkmark sitting on its true centre reads as low.
*/
.check-mark::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 7px;
  height: 13px;
  border: solid var(--bg);
  border-width: 0 2px 2px 0;
  opacity: 0;
  transform: translateY(-1px) rotate(45deg) scale(0.6);
  transition:
    opacity 0.12s ease,
    transform 0.12s ease;
}

.check-box input:checked + .check-mark {
  background: var(--ink);
  border-color: var(--ink);
}

.check-box input:checked + .check-mark::after {
  opacity: 1;
  transform: translateY(-1px) rotate(45deg) scale(1);
}

/* Keyboard only: a pointer press should not leave a ring behind it. */
.check-box input:focus-visible + .check-mark {
  outline: 2px solid var(--line-strong);
  outline-offset: 2px;
}

/* Flash messages -------------------------------------------------------- */
.flash {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  margin-bottom: 14px;
  font-size: 14px;
}

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

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

/* Chips ----------------------------------------------------------------- */
.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 4px 11px;
  font-size: 12.5px;
  color: var(--ink-dim);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.chip:hover {
  color: var(--ink);
}

.chip.is-active,
.chip:has(input[type='radio']:checked) {
  color: #0b0c0e;
  background: var(--ink);
  border-color: var(--ink);
  font-weight: 600;
}

/**
 * A chip that is really a radio: the input still exists for the keyboard and
 * for assistive tech, it just does not draw itself.
 */
.chip input[type='radio'] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.chip:has(input[type='radio']:focus-visible) {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.form-hint {
  margin-top: 6px;
  font-size: 11.5px;
  color: var(--ink-dim);
}

/*
| What became of a setting that saved itself, on the line under it. Sized like
| a hint and coloured unlike one: the hint is always there and is read once,
| this appears in answer to something you just did.
|
| Hidden while it is empty rather than by an attribute, so it can stay in the
| document as a live region — one that is added and removed is not reliably
| read out when it fills.
*/
.form-note {
  margin-top: 6px;
  font-size: 11.5px;
  color: var(--ok);
}

.form-note.is-error {
  color: var(--fail);
}

.form-note:empty {
  display: none;
}

/*
| The last line on the settings page. Quiet on purpose: it is there to be read
| out when something has gone wrong, not to be noticed the rest of the time.
*/
/*
| The credit links under the settings page's illustration notice. Stacked
| rather than run together on one line: they are whole URLs, and two of those
| side by side wrap into an unreadable knot on a phone.
*/
.credits-links {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  margin-top: 8px;
  font-size: 11.5px;
}

.credits-links a {
  color: var(--ink-dim);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.credits-links a:hover {
  color: var(--ink);
}

.version-note {
  margin-top: 30px;
  font-size: 11.5px;
  text-align: center;
  color: var(--ink-faint);
}

.tag {
  display: inline-block;
  padding: 2px 8px;
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-dim);
  border: 1px solid var(--line);
  border-radius: 999px;
}

/* Login ----------------------------------------------------------------- */
body.auth-body {
  padding-top: 0;
  padding-bottom: 0;
}

.auth-main {
  max-width: 400px;
  margin: 0 auto;
  padding: 16vh 16px 32px;
}

.auth-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 26px 22px;
  text-align: center;
}

.auth-mark {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  margin-bottom: 6px;
}

.auth-tagline {
  color: var(--ink-dim);
  font-size: 13.5px;
  margin-bottom: 22px;
}

.auth-note {
  margin-top: 16px;
  font-size: 12px;
  color: var(--ink-faint);
}

/*
| The welcome screen, on the sign-in page's card. Its head is centred like
| the card it borrows; the form under it is not, because a label centred over
| a full-width field reads as a heading rather than as the field's name.
*/
.welcome-card {
  text-align: left;
}

.welcome-card .auth-mark,
.welcome-card .auth-tagline,
.welcome-title {
  text-align: center;
}

.welcome-title {
  margin: 14px 0 6px;
  font-size: 19px;
  font-weight: 600;
}

.welcome-card .form-group {
  margin-bottom: 20px;
}

/*
| The picture, at the head of the form and on its own line: the camera circle
| is the whole control, so it is centred like the card's head above it, and a
| little larger than a page's head draws a face — it is the first thing the
| form asks, and the one press here that is not a field.
*/
.welcome-picture {
  text-align: center;
}

.welcome-picture label {
  position: relative;
  display: inline-block;
  vertical-align: top;
  border-radius: 999px;
  cursor: pointer;
}

.welcome-picture .avatar {
  width: 88px;
  height: 88px;
}

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

.welcome-picture label:focus-within {
  outline: 2px solid var(--line-strong);
  outline-offset: 3px;
}

/*
| The name under it, centred on the same line the circle is: with no label
| above it, the field reads as the caption of the face rather than as the
| first row of a form.
*/
.welcome-name {
  text-align: center;
}

.welcome-submit {
  margin-top: 24px;
}

.welcome-logout {
  margin-top: 8px;
}

/* Error pages ----------------------------------------------------------- */
.error-card {
  text-align: center;
}

.error-title {
  font-size: 18px;
  margin: 4px 0 6px;
}

.error-text {
  font-size: 14px;
  color: var(--ink-dim);
  margin-bottom: 20px;
}

/* Desktop: fold the tabs up into the top bar ---------------------------- */
@media (min-width: 768px) {
  body {
    padding-bottom: 32px;
  }

  .tabbar {
    top: 0;
    bottom: auto;
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    z-index: 21;
    height: var(--appbar-h);
    align-items: center;
    padding-bottom: 0;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-top: none;
    gap: 6px;
  }

  .tab {
    flex: 0 0 auto;
    flex-direction: row;
    gap: 8px;
    height: 34px;
    padding: 0 14px;
    border-radius: 999px;
    font-size: 11.5px;
  }

  .tab svg {
    width: 16px;
    height: 16px;
  }

  .tab.is-active {
    background: rgba(255, 255, 255, 0.1);
  }

  /* Room enough for the buttons to sit beside the text rather than under it. */
  .install-actions {
    width: auto;
    margin-left: auto;
  }

  .install-actions .btn {
    flex: 0 0 auto;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }

  /*
  | The rule above stops the marquee, which would otherwise leave the notice
  | clipped at whatever the viewport happens to be — so the strip becomes
  | something to swipe instead of something that moves. The second copy goes
  | with it: it exists to hide the seam in an animation that is no longer
  | running, and repeated text in a strip you scroll by hand reads as a fault.
  */
  .release-bar {
    overflow-x: auto;
    scrollbar-width: none;
  }

  .release-bar::-webkit-scrollbar {
    display: none;
  }

  .release-track {
    animation: none;
    transform: none !important;
  }

  .release-text[aria-hidden='true'] {
    display: none;
  }
}
