/*
 * The app frame: app bar, filmstrip, tab bar — docs/UI-LAYOUT.md "Global frame".
 *
 * No colour literal, no spacing literal. Every value comes through a var() from
 * tokens.css, which is the only file allowed to hold one.
 * tests/Feature/DesignTokenTest.php enforces that.
 *
 * Designed at 390 × 844 and allowed to widen. Desktop is a courtesy.
 */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  /* The two together stop iOS inflating text in landscape without disabling
     the user's own zoom, which -webkit-text-size-adjust: none would. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/*
 * `hidden` has to mean hidden.
 *
 * The attribute is styled by the UA stylesheet as a bare `display: none` on the
 * element, so ANY author rule with a class in it wins — `.app-bar__back {
 * display: grid }` silently un-hid the back arrow on the dashboard, where the
 * server had correctly marked it hidden. Found in the browser at 390px, not by
 * reading the CSS.
 *
 * The alternative is a paired `[hidden]` rule beside every display rule, which
 * is a thing to remember for the rest of the project. This is the one place
 * !important is the smaller cost, and it is what normalize.css does for the same
 * reason.
 */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  background: var(--sp-void);
  color: var(--sp-text);
  font-family: var(--sp-font-text);
  font-size: 1rem;
  line-height: 1.5;
  /* The document itself never scrolls: each screen scrolls on its own, which is
     what keeps the app bar and the tab bar in place — PRIOR-ART.md. */
  overflow: hidden;
  overscroll-behavior: none;
}

/* Figures never jitter when they update in place — DESIGN.md §6. */
[data-numeric] {
  font-variant-numeric: tabular-nums;
}

/*
 * Present to a screen reader, absent to everything else.
 *
 * Not `display: none` and not `visibility: hidden`, both of which remove the text
 * from the accessibility tree along with the pixels. The calendar's weekday
 * headings need it: two of the seven Swedish initials are `T`, so the letter is
 * decorative and the whole word is what gets announced.
 */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------- the shell */

.shell {
  display: flex;
  flex-direction: column;
  height: 100vh;
  /* dvh second so a browser that knows it wins: 100vh is wrong on mobile while
     the URL bar is showing. */
  height: 100dvh;
}

/* ------------------------------------------------------------- the app bar */

.app-bar {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--sp-space-2);
  height: var(--sp-appbar-height);
  padding-inline: var(--sp-space-2);
  padding-top: env(safe-area-inset-top);
  /* Not a lightness step: elevation here is a hairline, because Void, Surface
     and Slate are within 1.2:1 and a shadow on near-black does nothing —
     DESIGN.md §9. */
  border-bottom: 1px solid var(--sp-border);
  background: var(--sp-surface);
  box-sizing: content-box;
}

/*
 * Two clusters: the title with the drawer handle beside it, and the three
 * controls on the right. The lead grows and the actions do not, so a long screen
 * title truncates rather than pushing the avatar off the bar.
 */
.app-bar__lead {
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
  align-items: center;
  gap: var(--sp-space-1);
}

.app-bar__actions {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--sp-space-1);
}

.app-bar__title {
  min-width: 0;
  margin: 0;
  overflow: hidden;
  font-family: var(--sp-font-display);
  /* Screen title, 20/28/600, −0.01em — DESIGN.md §6. Measured to fit
     "Tillgänglighet" at 390px with 132px spare. */
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 28px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * Every control on the bar is 44px square — DESIGN.md §13's floor, which is not
 * negotiable and which Jelly's own switch had to be scaled up to meet.
 */
.app-bar__icon {
  position: relative;
  display: grid;
  flex: none;
  place-items: center;
  width: var(--sp-touch-min);
  height: var(--sp-touch-min);
  padding: 0;
  border: 0;
  border-radius: var(--sp-radius-pill);
  background: none;
  color: var(--sp-text);
  cursor: pointer;
}

/*
 * The unread badge. A disc rather than a pill so one digit is centred, sized so
 * that it clears the bell's own strokes without leaving the 44px target.
 *
 * The accent fill with --sp-on-accent on it, which is the pairing DESIGN.md §5.1
 * checked across all five presets and both modes — 4.56:1 at worst. The hairline
 * in --sp-surface is what keeps it legible where it overlaps the glyph.
 */
.app-bar__badge {
  position: absolute;
  top: 6px;
  inset-inline-end: 4px;
  display: grid;
  min-width: 16px;
  height: 16px;
  place-items: center;
  padding-inline: 3px;
  border: 2px solid var(--sp-surface);
  border-radius: var(--sp-radius-pill);
  background: var(--sp-accent);
  color: var(--sp-on-accent);
  font-size: 10px;
  font-weight: 600;
  line-height: 1;
}

/*
 * The mode toggle shows the mode it would switch TO, so the sun appears while
 * the app is dark. Both glyphs are in the markup and CSS picks one: swapping it
 * in JavaScript would mean the icon is built on the client and the server's copy
 * is wrong for a frame on every page load.
 *
 * Keyed on the same root attribute everything else in the palette is keyed on,
 * which theme.js has already set before this element is parsed.
 */
.app-bar__glyph {
  display: none;
  place-items: center;
}

:root[data-jelly-mode="dark"] .app-bar__glyph--light,
:root[data-jelly-mode="light"] .app-bar__glyph--dark {
  display: grid;
}

/*
 * The avatar opens the account menu. A button rather than the span phase 7 had,
 * because it is now a control — and it keeps the 44px square the icons use so the
 * three sit on one optical line.
 */
.app-bar__avatar {
  display: grid;
  flex: none;
  place-items: center;
  width: var(--sp-touch-min);
  height: var(--sp-touch-min);
  padding: 0;
  border: 0;
  border-radius: var(--sp-radius-pill);
  background: var(--sp-slate);
  color: var(--sp-text);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  cursor: pointer;
}

/*
 * The uploaded picture, filling the same circle the initials sit in — stage 2.
 * Every stored avatar is a 256px square, so object-fit is belt and braces for a
 * file that predates AvatarStore or arrives from a stale cache.
 */
.app-bar__face {
  width: 100%;
  height: 100%;
  border-radius: var(--sp-radius-pill);
  object-fit: cover;
}

/* jelly-popover is display:contents-ish on its host; this keeps the trigger
   from stretching the bar. */
.app-bar__pop {
  display: flex;
  flex: none;
  align-items: center;
}

/* ------------------------------------------------------ the two popovers */

/*
 * Shared by the notification dropdown and the account menu. Both are real links
 * and one real form inside jelly-popover's content slot — DESIGN.md §5 has why
 * they are not jelly-menu, and the short version is that jelly-menu rebuilds its
 * items as divs and would have turned every link here into a JavaScript event.
 */
.pop {
  display: flex;
  min-width: 232px;
  max-width: 288px;
  flex-direction: column;
  gap: 2px;
}

.pop__heading {
  margin: 0 0 var(--sp-space-1);
  padding-inline: var(--sp-space-2);
  color: var(--sp-text-muted);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.pop__empty {
  margin: 0;
  padding: var(--sp-space-2);
  color: var(--sp-text-muted);
  font-size: 13px;
}

.pop__list {
  max-height: 60vh;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}

/*
 * A row. 44px minimum in the block direction, because a menu item is a touch
 * target like any other and DESIGN.md §13 does not make exceptions for menus.
 */
.pop__item {
  display: flex;
  width: 100%;
  min-height: var(--sp-touch-min);
  box-sizing: border-box;
  align-items: center;
  gap: var(--sp-space-2);
  padding: var(--sp-space-2);
  border: 0;
  border-radius: var(--sp-radius-cell);
  background: none;
  color: var(--sp-text);
  cursor: pointer;
  font-family: inherit;
  font-size: 15px;
  text-align: start;
  text-decoration: none;
}

.pop__item:hover {
  background: var(--sp-slate);
}

.pop__item--stacked {
  position: relative;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  padding-inline-start: var(--sp-space-6);
}

.pop__item--quiet {
  color: var(--sp-text-muted);
  font-size: 13px;
}

/*
 * The one destructive entry. Signalled by the accent-as-text token rather than
 * by a semantic tone: DESIGN.md §14 question 6 maps the four tones for FILLS,
 * and a rose label on the card surface is a different pairing that nothing has
 * measured. The word "Logga ut" is what carries the meaning, which is what §13
 * asks for — never colour alone.
 */
.pop__item--danger {
  color: var(--sp-accent-text);
}

.pop__form {
  margin: 0;
}

.pop__icon {
  display: grid;
  flex: none;
  place-items: center;
  color: var(--sp-text-muted);
}

.pop__title {
  font-weight: 600;
}

.pop__body,
.pop__time {
  color: var(--sp-text-muted);
  font-size: 13px;
}

/*
 * Unread is a dot AND the title's weight AND the words "Oläst" for a screen
 * reader. DESIGN.md §13 forbids state signalled by colour alone, and a coloured
 * dot on its own is exactly that.
 */
.pop__dot {
  position: absolute;
  top: 18px;
  inset-inline-start: var(--sp-space-2);
  width: 8px;
  height: 8px;
  border-radius: var(--sp-radius-pill);
  background: var(--sp-accent-text);
}

.pop__item--unread .pop__title {
  font-weight: 700;
}

/* ---------------------------------------------------------- the drawer */

/*
 * JELLY'S CLOSE BUTTON IS 30 x 30, AND THE FLOOR IS 44 — phase 14.2.
 *
 * A fourth Jelly default under the touch floor, after the switch (phase 10), the
 * segmented track and the checkbox (stage 3). Measured in a real browser at
 * 390px: `.close` inside jelly-drawer's shadow root, 30 x 30 against
 * docs/DESIGN.md §13's non-negotiable 44.
 *
 * FOUND ON THE EMOJI PICKER AND FIXED FOR EVERY SHEET IN THE APP. It has been
 * true since phase 7 of the app drawer and since phase 8 of the calendar's day
 * sheet; scripts/ui-audit.js reports the element that receives the tap, shadow
 * roots included, which is what surfaced it. It lives here rather than in
 * inventory.css because it is the shell's chrome and there is one of it.
 *
 * ::part(close) and NOT a --jelly-* knob: the component ships no token for this,
 * and an outer-tree ::part rule beats the shadow root's own for the same
 * property, which is exactly the mechanism a provider override does NOT have —
 * docs/DESIGN.md §5 and the three phases that learned it.
 *
 * The artwork is left at its own size and only the box grows, so what changes is
 * what a thumb can hit rather than how the sheet looks. The same fix in kind as
 * .checklist__tick's on jelly-checkbox.
 */
jelly-drawer::part(close),
jelly-dialog::part(close) {
  width: var(--sp-touch-min);
  height: var(--sp-touch-min);
}

.drawer {
  display: flex;
  min-height: 100%;
  flex-direction: column;
  gap: var(--sp-space-2);
}

.drawer__list {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.drawer__item {
  display: flex;
  width: 100%;
  min-height: var(--sp-control-height);
  box-sizing: border-box;
  align-items: center;
  gap: var(--sp-space-3);
  padding: var(--sp-space-2) var(--sp-space-3);
  border: 0;
  border-radius: var(--sp-radius-cell);
  background: none;
  color: var(--sp-text);
  cursor: pointer;
  font-family: inherit;
  font-size: 16px;
  text-align: start;
  text-decoration: none;
}

.drawer__item:hover {
  background: var(--sp-slate);
}

/*
 * The current screen.
 *
 * NO WASH, although the tab bar's active slot has one and this is the same
 * information. DESIGN.md §10 gives the wash to exactly three places and says "and
 * nowhere else"; the active slot in the nav is one of them and a second control
 * showing the same fact is not a fourth. A test counts the wash's uses in this
 * file and caught this being added — the test was right.
 *
 * The rule and the weight carry it instead, which is two channels rather than
 * one and is what §13 asks for regardless.
 */
.drawer__item[aria-current] {
  border-inline-start: 3px solid var(--sp-accent-text);
  font-weight: 600;
}

.drawer__item--danger {
  color: var(--sp-accent-text);
}

.drawer__form {
  margin: 0;
  padding-top: var(--sp-space-2);
  border-top: 1px solid var(--sp-border);
}

.drawer__icon {
  display: grid;
  flex: none;
  place-items: center;
  color: var(--sp-text-muted);
}

.drawer__item[aria-current] .drawer__icon,
.drawer__item--danger .drawer__icon {
  color: inherit;
}

/* ------------------------------------------------- secondary page layout */

.page {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* One row of a settings card: a label and a control, apart. */
.setting {
  display: flex;
  min-height: var(--sp-touch-min);
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-space-4);
  margin-top: var(--sp-space-3);
}

.setting__label {
  font-size: 16px;
}

/*
 * A block inside a settings card. The rule between blocks is what a second card
 * used to do — DESIGN.md §9 again: elevation here is spacing and a hairline, so a
 * nested card was never going to separate two blocks that a line separates for
 * free.
 */
.setting-block + .setting-block {
  margin-top: var(--sp-space-4);
  padding-top: var(--sp-space-4);
  border-top: 1px solid var(--sp-border);
}

.setting-block .screen__heading {
  font-size: 13px;
}

/*
 * Visible to a screen reader and to nothing else. Used for the words that carry
 * a state an icon or a dot is also showing — DESIGN.md §13.
 */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ----------------------------------------------------------- the filmstrip */

.filmstrip {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}

/*
 * Grid rather than flex, and grid-auto-columns rather than min-width: 100vw.
 *
 * PRIOR-ART.md used a flex row of 100vw children. 100vw counts a desktop
 * scrollbar that this layout does not have, and a percentage on a flex item
 * resolves against a track whose own width comes from its children. Auto
 * columns at 100% resolve against the GRID CONTAINER, whose width is the
 * filmstrip's, so every screen is exactly one viewport wide at any width with
 * no circularity and no JavaScript involved in layout.
 */
.filmstrip__track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
  height: 100%;
  /*
   * The declarative version of the thing the original got wrong. It bound
   * @touchmove.prevent, which cancels the browser's own scrolling for every
   * touch on the track — including the vertical ones each screen needs. pan-y
   * hands vertical panning back to the browser and keeps the horizontal axis
   * for us, so no listener has to preventDefault and shell.js can stay passive.
   */
  touch-action: pan-y;
  /*
   * shell.js sets --filmstrip-shift in pixels while dragging and on settle. The
   * fallback is what positions the track with JavaScript off, and --active-index
   * is set server-side on the shell, so a no-JS load opens on the screen the URL
   * named instead of always showing the first one.
   *
   * ONE SCREEN IS 100%, not 100% / --slot-count. The track is a grid whose
   * columns OVERFLOW it: the container stays one viewport wide and the columns
   * sit outside it, so a percentage transform resolves against one viewport
   * rather than against the whole strip. Dividing by the slot count landed the
   * track half a screen off and produced a visible 350ms slide on every page
   * load, because shell.js then corrected it. Measured, not reasoned about —
   * getBoundingClientRect().width on the track is 390, not 780.
   */
  transform: translate3d(var(--filmstrip-shift, calc(-100% * var(--active-index, 0))), 0, 0);
  transition: transform var(--sp-dur-settle) var(--sp-ease-settle);
}

/* While a finger or a mouse is down the track follows it exactly — no easing,
   no duration. PRIOR-ART.md: "transition: none". */
.filmstrip__track[data-dragging] {
  transition: none;
}

.screen {
  height: 100%;
  padding: var(--sp-space-4);
  overflow-x: hidden;
  overflow-y: auto;
  /* A screen's own scroll must not turn into the document's, or the filmstrip
     rubber-band and the browser's would fight. */
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
}

.screen__heading {
  margin: 0 0 var(--sp-space-4);
  font-family: var(--sp-font-display);
  /* Card label, 13/16/600, 0.08em, uppercase — DESIGN.md §6. */
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  line-height: 16px;
  text-transform: uppercase;
  color: var(--sp-text-muted);
}

.screen__body {
  margin: 0;
  color: var(--sp-text);
}

.screen__note {
  margin: var(--sp-space-2) 0 0;
  /* Caption, 13/18/400 — DESIGN.md §6. */
  font-size: 13px;
  line-height: 18px;
  color: var(--sp-text-muted);
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-4);
}

/*
 * A whole card as one target — docs/UI-LAYOUT.md, "every card is tappable and
 * opens its module". A link rather than a click handler, so it works with
 * JavaScript off and gets keyboard activation and a focus ring for free.
 */
.card-link {
  display: block;
  border-radius: var(--sp-radius-card);
  color: inherit;
  text-decoration: none;
}

/* ------------------------------------------------------------ the dashboard */

/*
 * A full-width hero over a two-column grid — docs/UI-LAYOUT.md, carried over from
 * the previous version of the app because it fits more on a phone than a vertical
 * stack without feeling cramped (docs/PRIOR-ART.md).
 *
 * ONE grid, not a hero plus a grid. The hero and any card a module declares as
 * full-width both span both columns, so there is one column definition and one
 * gap rather than two layouts that have to agree about their spacing.
 *
 * The measurement this is built on: at 390px the screen's 16px padding leaves
 * 358px, and two columns with a 16px gap make each cell 171.2px — DESIGN.md §6,
 * which measured every Swedish card label against the 139.2px that leaves inside
 * the card padding.
 */
.dash {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-space-4);
}

.dash__cell {
  min-width: 0;
}

/*
 * Two cards in a row are the same height, and the grid's default `stretch` is
 * what does it — the cell stretches, and the card is told to fill the cell.
 *
 * Not cosmetic. Swedish card values wrap at 22px in a 139.2px box and English
 * ones often do not, so "2 lediga dagar" is two lines beside "2 medlemmar" on
 * one, and top-aligned that reads as a layout that failed rather than as two
 * cards with different amounts to say. Measured at 112px against 84px.
 */
.dash__cell > jelly-card {
  display: block;
  height: 100%;
}

.dash__cell--wide {
  grid-column: 1 / -1;
}

/* Card label, 13/16/600, 0.08em, uppercase — DESIGN.md §6. The copy is written
   in sentence case and the transform does the shouting, so a screen reader is
   never handed an acronym. */
.dash__label {
  margin: 0 0 var(--sp-space-2);
  font-family: var(--sp-font-display);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  line-height: 16px;
  text-transform: uppercase;
  color: var(--sp-text-muted);
}

/* Card value, 22/28/600 — DESIGN.md §6. The one number, or the one sentence. */
.dash__value {
  margin: 0;
  font-family: var(--sp-font-display);
  font-size: 22px;
  font-weight: 600;
  line-height: 28px;
  color: var(--sp-text);
  overflow-wrap: anywhere;
}

/*
 * The hero, and the one thing on this screen that glows.
 *
 * DESIGN.md §10 inherits the concept image's luminous wash in exactly three
 * places, and this is the first of them: "the hero card. The next hangout is lit
 * by the wash. It is the reason the app gets opened, and it is the only thing on
 * the dashboard that glows." A module card must therefore not have it.
 *
 * The wash sits on this element rather than on the jelly-card around it, because
 * a Jelly card paints its surface onto a <canvas> behind its content — a
 * background on the host would be under that canvas and invisible. Zeroing the
 * card's own padding and taking it here puts the wash over the full card instead
 * of inside its padding.
 *
 * The two knobs are set on the jelly-card ELEMENT for the reason DESIGN.md §5
 * records: a component defaults its own --jelly-<component>-* properties in its
 * :host rule, so they are set directly on it and an inherited override loses.
 * tokens.css maps them to --sp-space-4 the same way; this class outranks that on
 * specificity, which is the whole of how the hero opts out.
 */
.dash__hero-card {
  --jelly-card-padding-block: 0;
  --jelly-card-padding-inline: 0;
}

.hero {
  padding: var(--sp-space-4);
  border-radius: var(--sp-radius-card);
  background-image: var(--sp-wash);
}

.hero__heading {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-1);
  margin: 0 0 var(--sp-space-3);
}

/* Same type as a card label — DESIGN.md §6 — and muted, which composites to
   4.82:1 on the wash at its 0.20 ceiling. §10 tabulates that; it is why the
   ceiling is a ceiling. */
.hero__eyebrow {
  font-family: var(--sp-font-display);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  line-height: 16px;
  text-transform: uppercase;
  color: var(--sp-text-muted);
}

/* Hero numeral, 40/44/600, −0.02em — DESIGN.md §6, which measured
   "Fredag 14 aug" at 249.56px in the 358.4px a full-width card leaves. */
.hero__headline {
  font-family: var(--sp-font-display);
  font-size: 40px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 44px;
  color: var(--sp-text);
}

.hero__detail {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-space-2);
  margin: 0;
  color: var(--sp-text);
}

/* Named for what it is rather than for what it contains, so that no class here
   is a prefix of another — a sweep counting hero__dot would otherwise count the
   row itself. */
.hero__attendance {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-space-1);
}

/*
 * 10px, the same size the calendar's state markers settled on — DESIGN.md §14
 * question 4, decided in a browser rather than on paper. These are the same idea
 * at the same size, so they are the same number.
 *
 * FILL AND SHAPE, NEVER COLOUR ALONE: a member who is free is a solid disc, one
 * who is not is a ring. And the sentence beside the dots says it in words, which
 * is why the row itself is aria-hidden — the accessible version is not a
 * translation of this, it is the original.
 *
 * The ring is --sp-text-muted rather than --sp-border-strong. On the card surface
 * §7 clears the strong border at 4.20:1, but these dots sit on the WASH, where it
 * composites to 2.86:1 — under the 3:1 non-text information needs. Muted is
 * 4.79:1 on the same ground. §5's rule, applied: check contrast after the thing
 * underneath changes.
 */
.hero__dot {
  width: 10px;
  height: 10px;
  border: 2px solid var(--sp-text-muted);
  border-radius: var(--sp-radius-pill);
}

/* 6.11:1 against the wash composite — DESIGN.md §7's 9.03:1 is against the card
   surface, and the wash lightens what is under it. */
.hero__dot--free {
  border-color: var(--sp-state-available);
  background: var(--sp-state-available);
}

.member-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* ------------------------------------------------------------- the tab bar */

.tab-bar {
  display: flex;
  flex: none;
  /* Never more than five, and Navigation refuses a sixth rather than dropping
     one silently — docs/UI-LAYOUT.md. */
  align-items: stretch;
  border-top: 1px solid var(--sp-border);
  background: var(--sp-surface);
  /* The whole reason the tab bar is a flex child of the shell rather than
     position: fixed — the inset is padding on a real box, so nothing overlaps
     the screen above it and no screen needs a magic bottom padding. */
  padding-bottom: env(safe-area-inset-bottom);
}

.tab-bar__slot {
  position: relative;
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-space-1);
  /* 390 / 5 = 78px wide, and the floor is cleared on both axes by the icon
     circle alone before the label is counted. */
  min-width: 0;
  min-height: var(--sp-appbar-height);
  padding: var(--sp-space-1) var(--sp-space-1) var(--sp-space-2);
  border: 0;
  background: none;
  color: var(--sp-text-muted);
  font-family: inherit;
  font-size: 11px;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.tab-bar__label {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * The active slot carries THREE cues, because the accessibility floor forbids
 * colour alone and DESIGN.md §10 says the active screen is "lit, not
 * underlined":
 *
 *   colour — the luminous wash, the signature element
 *   shape  — the icon gains a pill that inactive slots do not have
 *   weight — the label goes to 600
 *
 * aria-current carries it a fourth time, for anything not looking at pixels.
 */
.tab-bar__slot[aria-current] {
  color: var(--sp-text);
  font-weight: 600;
}

/*
 * A CIRCLE, AND IT IS CENTRED — the batch's fix 15, *"fix the bg circle that
 * shows the active one (its offcenter)"*.
 *
 * It was 40 × 24: a lozenge wider than the 20px glyph inside it and half its
 * height, so the lit shape sat around the icon rather than on it and read as
 * offset from everything else in the slot. Square and pill-radiused is a circle,
 * and one that is the same distance from the glyph on every side.
 */
.tab-bar__icon {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: var(--sp-radius-pill);
  transition:
    background-color var(--sp-dur-tap) var(--sp-ease-settle),
    transform var(--sp-dur-tap) var(--sp-ease-settle);
}

.tab-bar__slot[aria-current] .tab-bar__icon {
  /*
   * The wash — DESIGN.md §10, "the active screen in the filmstrip nav. Lit, not
   * underlined." Contrast-safe at rest over the app bar surface, and it does not
   * animate, so reduced motion leaves it alone.
   */
  background-image: var(--sp-wash);
}

.tab-bar__icon svg {
  display: block;
  width: 20px;
  height: 20px;
}

/*
 * HOME IS THE CENTRE BUTTON — fix 15, *"make home / dashboard a slight bigger
 * and as the centered button where we then have 2 tab buttons on each side"*.
 *
 * It is already in the middle: `App\Kernel\Shell\Navigation` puts the dashboard
 * at intdiv(count, 2), which is docs/PRIOR-ART.md's own arrangement and gives
 * index 2 of 5 once five things want a tab. Three modules are installed today,
 * so it is one either side rather than two; what this rule adds is the part that
 * was missing, which is that it looks like the centre button.
 *
 * ONLY THE CIRCLE GROWS. The slots stay equal width, because shell.js computes
 * the sliding indicator's travel as rail width ÷ slot count — a wider home slot
 * would put the indicator under the wrong tab, and PRIOR-ART.md is specific that
 * the indicator tracking the drag is most of why the navigation feels connected.
 *
 * The resting treatment is a ring and a fill, so the button reads as a button
 * before it is the active one. When it IS active the wash paints over the fill,
 * exactly as on every other slot — the three cues for the active screen are
 * unchanged and none of them is this.
 *
 * THE RING IS WHAT MAKES IT WORK IN LIGHT MODE, and a real browser is what said
 * so: --sp-slate is DESIGN.md §4's "raised inside a card", and in dark mode it is
 * a visible lift off --sp-surface. In light mode both are Jelly's own near-white,
 * measured at 1.02:1 against the bar — nothing at all. The centre button had no
 * centre button in half the app.
 *
 * --sp-border-strong rather than --sp-border, because §13 says the decorative one
 * may not carry information and "this is the home button" is information. 4.20:1
 * in dark and 7.0:1 on white, against the 3:1 a boundary needs.
 */
.tab-bar__slot--home .tab-bar__icon {
  width: 40px;
  height: 40px;
  background-color: var(--sp-slate);
  box-shadow: inset 0 0 0 1px var(--sp-border-strong);
}

.tab-bar__slot--home .tab-bar__icon svg {
  width: 24px;
  height: 24px;
}

/*
 * THE PRESS RESPONSE — fix 15's *"more jelly coated"*.
 *
 * docs/UI-LAYOUT.md asked for this in the first place: *"Jelly UI's soft-body
 * components make the tab bar an obvious place for a tactile press response —
 * this is the one piece of chrome the user touches constantly."* The bar is one
 * of DESIGN.md §12's three genuinely custom components, so Jelly's own physics
 * cannot be borrowed and this is the smallest thing that reads as a press: the
 * same shape .cal-cell:active uses, on the same token.
 *
 * Collapses to nothing under prefers-reduced-motion, because --sp-dur-tap does —
 * and the transform is cancelled outright as well, since a 0ms transition to a
 * scaled state is still a scaled state.
 */
.tab-bar__slot:active .tab-bar__icon {
  transform: scale(0.88);
}

@media (prefers-reduced-motion: reduce) {
  .tab-bar__slot:active .tab-bar__icon {
    transform: none;
  }
}

/*
 * The sliding indicator, and the detail PRIOR-ART.md singles out as most of why
 * the navigation feels connected: it tracks the drag CONTINUOUSLY, not only on
 * release. shell.js drives --nav-indicator-shift from the same delta as the
 * track, so the two cannot disagree.
 */
.tab-bar__indicator {
  position: absolute;
  bottom: 0;
  left: 0;
  height: 2px;
  width: calc(100% / var(--slot-count, 1));
  /*
   * --sp-accent-text, so the indicator follows the member's own preset — stage 2.
   * It named --sp-indigo-lift, which was the same value while indigo was the only
   * accent there was, and stopped being it when stage 1 added four more: the app
   * repainted around a nav bar that stayed indigo.
   *
   * --sp-accent-text rather than --sp-accent because this is a 2px line read as
   * ink, and that token is already the half of each pair chosen for being legible
   * as one. Measured, both modes, all five: 5.36–7.70:1 on the dark surface and
   * 4.56–5.12:1 on white, against the 3:1 a non-text indicator needs.
   */
  background: var(--sp-accent-text);
  /* Same shape as the track: pixels from shell.js, and a percentage of the
     indicator's own width — one slot — as the no-JS fallback. */
  transform: translate3d(var(--nav-indicator-shift, calc(100% * var(--active-index, 0))), 0, 0);
  transition: transform var(--sp-dur-settle) var(--sp-ease-settle);
  pointer-events: none;
}

.tab-bar__rail {
  position: relative;
  display: flex;
  flex: 1 1 auto;
}

.tab-bar__rail[data-dragging] .tab-bar__indicator {
  transition: none;
}

/* --------------------------------------------------------------- the focus */

/*
 * A single band is enough here: nothing in the shell sits on a saturated fill.
 * The two-band sandwich DESIGN.md §7 specifies is for the calendar grid, where
 * the ring would otherwise be drawn against an availability colour at 1.2:1.
 */
:focus-visible {
  /* Follows the preset, like the indicator above and like --jelly-ring-color,
     which tokens.css already pointed at this token. The three focus indicators
     in the app disagreeing about what the accent is would be the most visible
     way for a preset to look half-applied. */
  outline: 2px solid var(--sp-accent-text);
  outline-offset: 2px;
}

/* --------------------------------------------- the plain layout, for login */

.plain {
  display: grid;
  place-items: center;
  min-height: 100vh;
  min-height: 100dvh;
  /*
   * --sp-space-6 rather than -4, and it is Jelly's geometry that decides it.
   * Every Jelly control paints a soft-body membrane on a <canvas> about 20px
   * wider than the control on each side, absolutely positioned and centred. At
   * 16px of page padding a full-width input's membrane reaches past the viewport
   * and the document grows a horizontal scrollbar — measured at 418px against a
   * 390px viewport before this changed. 24px gives it room.
   */
  padding: var(--sp-space-6);
  padding-top: max(var(--sp-space-6), env(safe-area-inset-top));
  padding-bottom: max(var(--sp-space-6), env(safe-area-inset-bottom));
  /*
   * And the guarantee, because the padding above depends on a number inside
   * somebody else's library. Nothing may scroll the page sideways at 390px.
   */
  overflow-x: hidden;
  overflow-y: auto;
}

/* A label and its control travel together and sit closer than two fields do. */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-1);
}

/*
 * jelly-input is display:inline-block and sizes itself to the inner <input>'s
 * intrinsic width — 280px at 390px, next to a 342px button. Stretching is this
 * layout's business rather than a theme override, so it is set here.
 */
.field jelly-input,
.field jelly-textarea,
.field jelly-select {
  width: 100%;
}

.plain__panel {
  width: 100%;
  max-width: 26rem;
}

.plain__title {
  margin: 0 0 var(--sp-space-2);
  font-family: var(--sp-font-display);
  /* Hero numeral, 40/44/600, −0.02em — DESIGN.md §6. "Squad Party" measures
     well inside the 358px a full-width card leaves at 390px. */
  font-size: 40px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 44px;
}

.plain__lede {
  margin: 0 0 var(--sp-space-6);
  color: var(--sp-text-muted);
}

.form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-4);
}

.form__actions {
  display: flex;
  flex-direction: column;
  gap: var(--sp-space-2);
}

/*
 * A form whose control should not fill the width.
 *
 * Jelly's non-accent button variants select its semantic tones, which tokens.css
 * leaves unmapped, so a secondary action cannot be made quieter with colour here
 * — it has to be made quieter with size. Signing out was otherwise the largest,
 * brightest thing on the squad screen.
 */
.form--start {
  align-items: flex-start;
}

/*
 * Deliberately not a jelly-alert. Jelly's alert tones come from its semantic
 * palette, which tokens.css leaves unmapped, so one here would put a colour on
 * screen that docs/DESIGN.md never sanctioned — and §14 question 6 is still open
 * precisely because this palette has no single ink that stays AA across all four
 * of those tones.
 *
 * The message carries itself: text at full contrast, plus a rule in
 * --sp-border-strong, which at 4.20:1 is the border token §7 permits to carry
 * meaning. Never colour alone.
 *
 * Phase 10's notifications panel shares this rather than styling its own. It is
 * the same object — one sentence saying why something did not happen — and a
 * second rule with the same declarations would be a second thing to keep in step.
 */
.form__error,
.push__problem {
  margin: 0;
  padding-left: var(--sp-space-3);
  border-left: 2px solid var(--sp-border-strong);
  color: var(--sp-text);
}

.form__error code {
  font-family: var(--sp-font-mono);
  font-size: 13px;
  color: var(--sp-text-muted);
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------- notifications */

/*
 * The enable / disable pair on the notifications screen.
 *
 * Exactly one of the two is ever visible — notifications.js unhides the region and
 * picks — so this is a column of one, and the gap is there for the moment it is
 * not. flex-start rather than stretch for the same reason .form--start exists: a
 * secondary action cannot be made quieter with colour while Jelly's semantic tones
 * are unmapped, so it is made quieter with size.
 */
.push__actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-space-2);
  margin-top: var(--sp-space-4);
}

/*
 * The mute form. The switch and its explanation sit together; the submit button is
 * the fallback for JavaScript being off and is pushed away from them so it does not
 * read as part of the sentence.
 */
.push__mute .form__actions {
  align-items: flex-start;
  margin-top: var(--sp-space-2);
}

/* --------------------------------------------------------- reduced motion */

/*
 * tokens.css already retargets --sp-dur-settle to 0ms under the preference, so
 * every transition above collapses on its own. This block covers what a
 * duration cannot: a browser that would still interpolate, and any third-party
 * animation inside a component's shadow root.
 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: var(--sp-dur-instant) !important;
    animation-iteration-count: 1 !important;
    transition-duration: var(--sp-dur-instant) !important;
    scroll-behavior: auto !important;
  }
}
