/* The visual identity's tokens as custom properties.
 *
 * DESIGN.md at the repository root is the normative source for these values and
 * holds the reasoning behind them. This file declares the same values for the
 * browser. There is no build step between the two, so tests/test_design_tokens.py
 * holds them equal; edit both together or the test fails.
 *
 * Two rules, because they are the ones that get broken:
 *
 * A surface and its text color travel as a pair. --color-surface goes with
 * --color-text, and --color-surface-inverse goes with --color-text-inverse.
 * Nothing draws one from each, because the pairs are what carry the measured
 * contrast.
 *
 * A size travels with the line height measured beside it. --font-size-400 goes
 * with --line-height-400. Mixing steps produces spacing the source never
 * rendered.
 */

:root {
  --color-surface: rgb(255, 255, 255);
  --color-surface-inverse: rgb(0, 0, 0);
  --color-text: rgb(0, 0, 0);
  --color-text-inverse: rgb(255, 255, 255);

  /* The fifth color, and the first that is not one of the identity's pair. It
   * is never read as text and never carries meaning on its own: it appears only
   * as the ground of an element that has just changed, and fades to nothing
   * within the second. DESIGN.md has not measured it. */
  --color-just-changed: rgb(255, 243, 191);

  --font-family-base: Helvetica, Helvetica Neue, Arial, sans-serif;
  --font-weight-regular: 400;
  --font-weight-emphasis: 700;

  --font-size-100: 16px;
  --font-size-200: 18px;
  --line-height-200: 25.2px;
  --font-size-300: 20px;
  --line-height-300: 24px;
  --font-size-400: 24px;
  --line-height-400: 33.6px;
  --font-size-500: 34px;
  --line-height-500: 44.2px;
  --font-size-600: 74px;
  --line-height-600: 66.6px;

  --radius-flat: 0px;
  --radius-pill: 50px;

  --space-100: 8px;
  --space-200: 16px;
  --space-300: 24px;
  --space-400: 32px;
  --space-500: 48px;
  --space-600: 64px;
  --measure: 720px;

  /* The room the roll-up holds while it condenses. Sizing to its contents
   * until the script on the result page measures what the full form needs and
   * sets the measurement on the element itself, which is where the number can
   * come from at all. */
  --summary-block-size: auto;

  /* How much room the pinned chrome takes. The roll-up on the result page
   * pins directly beneath it, and CSS cannot ask one sticky element the height
   * of another, so the script on that page measures the chrome and writes the
   * answer here. Nothing before it does, which is why the resting value pins
   * the roll-up to the top of the viewport rather than to a number that would
   * be wrong at some width.  */
  --page-top-block-size: 0px;

  --control-border-width: 1px;
  --table-rule-width: 1px;
  --focus-ring-width: 2px;
  --focus-ring-offset: 2px;
}

/* The margin a browser gives the body by default is dropped. The page is built
 * from full-width bands, and that margin leaves a strip of the page surface
 * outside every one of them: above the chrome it reads as a white line across
 * the top of the window. */
body {
  margin: 0;
  background-color: var(--color-surface);
  color: var(--color-text);
  font-family: var(--font-family-base);
  font-weight: var(--font-weight-regular);
  font-size: var(--font-size-200);
  line-height: var(--line-height-200);
}

/* An inverse band. Sets both halves of the pair, so anything nested inside
 * inherits the correct text color without knowing which surface it sits on. */
.band-inverse {
  background-color: var(--color-surface-inverse);
  color: var(--color-text-inverse);
}

/* The recurring section heading, which is what h1 through h3 get. The 74px step
 * is the hero line of a marketing page and is opt-in through .display only. */
h1,
h2,
h3 {
  font-size: var(--font-size-500);
  line-height: var(--line-height-500);
  font-weight: var(--font-weight-regular);
}

.display {
  font-size: var(--font-size-600);
  line-height: var(--line-height-600);
  font-weight: var(--font-weight-regular);
}

.text-400 {
  font-size: var(--font-size-400);
  line-height: var(--line-height-400);
}

.text-300 {
  font-size: var(--font-size-300);
  line-height: var(--line-height-300);
}

.text-100 {
  font-size: var(--font-size-100);
}

strong,
b {
  font-weight: var(--font-weight-emphasis);
}

/* Square is the default everywhere. The pill is the one exception the identity
 * renders, and it belongs on small controls only. */
/* A pill is one unbroken chip. It is an inline box carrying a border and a
 * radius, so a line break inside it splits that border across two lines and
 * leaves an open edge on each, which reads as a rendering fault rather than as
 * a wrap. Held on one line, it moves to the next line whole instead. */
.pill {
  border-radius: var(--radius-pill);
  display: inline-block;
  white-space: nowrap;
}

/* Components.
 *
 * Every value below is a custom property. DESIGN.md's rule is that a token is
 * applied by name so a change there reaches every screen, and a literal here
 * would be a value that file never saw.
 *
 * No new color. The rings, the rules and the borders are drawn from the four
 * colors above, so each inherits the contrast its surface already records.
 */

.band {
  padding-block: var(--space-600);
}

.container {
  margin-inline: auto;
  max-width: var(--measure);
  padding-inline: var(--space-300);
}

.stack > * + * {
  margin-block-start: var(--space-300);
}

.masthead {
  margin: 0;
}

.nav {
  margin-block-start: var(--space-100);
}

.nav a + a {
  margin-inline-start: var(--space-300);
}

.lede {
  margin-block-end: var(--space-400);
}

/* Django's own field group renders the label, the widget, the help text and the
 * errors, and wires aria-describedby and aria-invalid between them. The
 * stylesheet dresses that output rather than replacing it, because hand-written
 * control markup would drop the wiring silently. */
label {
  display: block;
  margin-block-end: var(--space-100);
}

.helptext {
  display: block;
  margin-block-start: var(--space-100);
  font-size: var(--font-size-100);
}

.errorlist {
  margin-block: var(--space-100);
  padding-inline-start: var(--space-300);
  font-weight: var(--font-weight-emphasis);
}

input,
textarea {
  border: var(--control-border-width) solid var(--color-text);
  border-radius: var(--radius-flat);
  padding: var(--space-100) var(--space-200);
  font-family: var(--font-family-base);
  font-size: var(--font-size-200);
  line-height: var(--line-height-200);
  background-color: var(--color-surface);
  color: var(--color-text);
}

textarea {
  width: 100%;
}

.field-input {
  width: var(--space-600);
}

/* The pointer, because a button that looks clickable and does not say so on
 * hover reads as decoration. A browser gives a link the hand and a button the
 * arrow, and nothing on this page is a link dressed as a button, so the two
 * are made to behave alike. */
.btn {
  cursor: pointer;
  border: var(--control-border-width) solid var(--color-text);
  padding: var(--space-100) var(--space-300);
  background-color: var(--color-surface-inverse);
  color: var(--color-text-inverse);
  font-family: var(--font-family-base);
  font-size: var(--font-size-200);
  line-height: var(--line-height-200);
}

.table {
  border-collapse: collapse;
  width: 100%;
  margin-block-start: var(--space-300);
}

.table caption {
  text-align: start;
  font-size: var(--font-size-100);
  margin-block-end: var(--space-200);
}

.table th,
.table td {
  border-block-end: var(--table-rule-width) solid var(--color-text);
  padding: var(--space-200) var(--space-300);
  text-align: start;
}

.band-inverse .table th,
.band-inverse .table td {
  border-block-end-color: var(--color-text-inverse);
}

.band-inverse input,
.band-inverse .btn {
  border-color: var(--color-text-inverse);
}

/* Fixed proportions rather than columns that size to their contents, so an
 * edit that lengthens a figure does not resize the tile beside it. The first
 * column is wider because the figure in it is a range, which is two numbers
 * and a word where the others are one number. */
.tiles {
  display: grid;
  grid-template-columns: 1.5fr 1fr 1fr;
  gap: var(--space-300);
}

.tile {
  border: var(--control-border-width) solid var(--color-text);
  border-radius: var(--radius-flat);
  padding: var(--space-300);
}

.tile-label,
.tile-sublabel,
.tile-movement,
.tile-value {
  margin: 0;
}

/* The figure is the largest thing in the tile and everything else stands back
 * from it, which is what makes a tile readable at a glance rather than read.
 * A range that runs out of room breaks after the word between its two ends,
 * never inside a figure: a thousands separator is not a break opportunity, so
 * a number stays one number wherever the line ends. */
.tile-value {
  margin-block: var(--space-100);
  font-size: var(--font-size-400);
  line-height: var(--line-height-400);
}

/* How far it moved, then what it moved from, one under the other. Side by
 * side they are two phrases competing for one line, and the line they lose is
 * broken in the middle of a phrase rather than between the two. */
.tile-movement .was {
  display: block;
  padding-inline-start: 0;
}

/* Who you are signed in as, in development. It is scaffolding on every page, so
 * it takes the surface it sits on and separates itself with a rule rather than
 * with anything this file would have to measure. */
.dev-bar {
  padding-block: var(--space-200);
  border-block-end: var(--table-rule-width) solid var(--color-text);
}

/* Who you are and how to stop being them, on one line. A row rather than two
 * blocks, so signing out reads as part of the same statement as being signed
 * in rather than as an action on the page below it. */
.dev-bar .container {
  display: flex;
  align-items: center;
  gap: var(--space-200);
  flex-wrap: wrap;
}

.dev-bar p,
.dev-bar form {
  margin: 0;
}

.dev-bar a {
  padding-inline-start: var(--space-200);
}

/* The chrome that stays: where you can go, and who you are while you go there.
 * Pinned because both answers are ones a reader needs while they are deep in a
 * table rather than only on arrival.
 *
 * It sizes to its contents, and the roll-up below is told how tall it came
 * out: a declared height would be a number that holds at one window width and
 * clips the bars at another, since both wrap.
 *
 * The bands inside lose the page's usual block padding. A masthead set at that
 * rhythm stands 185px tall, which is a fifth of a laptop viewport held there
 * permanently. */
.page-top {
  position: sticky;
  top: 0;
  z-index: 2;
  background-color: var(--color-surface-inverse);
}

/* One line each. The chrome is the thing in front of every page at every
 * scroll position, so it is measured in what it costs rather than in what the
 * page's usual rhythm would give it: the title sits beside the links instead
 * of above them, and the band keeps the smallest step of padding. */
.page-top .band {
  padding-block: var(--space-100);
}

.page-top .container {
  display: flex;
  align-items: center;
  gap: var(--space-300);
  flex-wrap: wrap;
}

.page-top .nav {
  margin-block-start: 0;
}

/* A control in the chrome is not a call to action, it is a switch a developer
 * flips. At the page's button size it is the tallest thing in the bar and sets
 * the bar's height on its own. */
.dev-bar .btn {
  padding: 0 var(--space-100);
  font-size: var(--font-size-100);
}

/* The roll-up, pinned so that it and the table it rolls up can be read at the
 * same time. Opaque for the reason a persistent region has to be: it covers
 * content while it is pinned, and a translucent one over scrolling text is
 * unreadable.
 *
 * container-type is what lets the rules below ask whether it is pinned right
 * now. The browser already tracks that, so asking costs no scroll listener and
 * no script, which matters because ADR-0005 admits script for the recompute
 * swap and nothing else. A container cannot style itself, which is why the
 * visible band is the element inside this one. */
/* The box keeps the room the full form needs, and the bar condenses inside it
 * rather than shrinking it. A sticky element that shrinks as it pins carries
 * every line under it upwards by however much it gave up, in the single scroll
 * step that pins it. Held, the pinned bar paints short and the room it is not
 * using is transparent, so rows scroll through it and go behind the band's own
 * edge.
 *
 * The script on the result page supplies the height, since the room the
 * contents need is not a number a stylesheet can read. Without it the box
 * sizes to its contents, which is the behaviour this replaces rather than a
 * broken page.
 *
 * Nothing catches a click in the room it is not using, which is why the box
 * stops taking pointer events and the band it paints takes them again. */
.summary-sticky {
  position: sticky;
  top: var(--page-top-block-size);
  z-index: 1;
  container-type: scroll-state;
  block-size: var(--summary-block-size);
  pointer-events: none;
}

.summary-band {
  pointer-events: auto;
}

.summary-band {
  background-color: var(--color-surface);
  border-block-end: var(--table-rule-width) solid var(--color-text);
  padding-block: var(--space-300);
}

/* Pinned, it is a different thing from the region a reader scrolled past: it
 * is the only part of the page not moving, and it sits over content rather
 * than above it. So it says so, by taking less room and drawing a harder edge
 * under itself. The tiles lose their own borders in the same move, because
 * three bordered cards inside a bordered bar reads as two frames rather than
 * one, and the figure is what a pinned roll-up is for.
 *
 * The static sublabels go. They say what a figure means, which is worth a line
 * on first read and is not worth the height of the pinned bar on every screen
 * after it. What a figure moved by stays, because that is the thing the pinning
 * exists to keep in view.
 *
 * None of this moves the page, because what shrinks is the band inside the
 * held box rather than the box itself.
 *
 * Where this query is not supported the bar keeps its full form, which is what
 * it had before this rule existed. There is nothing to fall back to. */
@container scroll-state(stuck: top) {
  .summary-band {
    padding-block: var(--space-200);
    border-block-end-width: var(--control-border-width);
  }

  .tile {
    border: 0;
    border-radius: 0;
    padding: 0;
  }

  .tile-value {
    font-size: var(--font-size-300);
    line-height: var(--line-height-300);
    margin-block: 0;
  }

  .tile-sublabel {
    display: none;
  }
}

/* While a recompute is in flight the figures above are the ones the edit has
 * already superseded, so they are held back and the region says it is working.
 * htmx carries the state as a class on the region for exactly as long as the
 * request runs, and the swap that ends the request replaces the element, so
 * nothing has to clear it.
 *
 * Both halves matter. The word is what a reader sees when they are looking at
 * the region, and the hold-back is what catches the reader who is not: a figure
 * at full strength reads as the current one whether or not a word above it says
 * otherwise. */
.tiles-pending {
  margin: 0 0 var(--space-200);
  font-weight: var(--font-weight-emphasis);
  visibility: hidden;
}

.htmx-request .tiles-pending {
  visibility: visible;
}

.htmx-request .tile-value {
  opacity: 0.4;
}

/* The signed movement, beside the value it moved from. The sign carries the
 * direction, not a color: an estimate going up is not worse than one going
 * down, and coloring it would say that it is. */
.delta {
  font-weight: var(--font-weight-emphasis);
}

.delta-money,
.was {
  padding-inline-start: var(--space-200);
}

.line-movement {
  white-space: nowrap;
}

/* The change is reported where the user is not looking, and an instantaneous
 * swap outside the focus of attention is the case change blindness describes.
 * The ground flashes once and fades, which is the oldest remedy for it on the
 * web. It runs on insertion, and only on an element the server marked as
 * changed, so a page with no edit on it never flashes. */
@keyframes just-changed {
  from {
    background-color: var(--color-just-changed);
  }
  to {
    background-color: transparent;
  }
}

.just-changed {
  animation: just-changed 1.2s ease-out;
}

/* A flash is motion, and motion is the thing this query turns off. The value
 * still changes and the row still states its movement in text, so nothing that
 * carries meaning is lost. */
@media (prefers-reduced-motion: reduce) {
  .just-changed {
    animation: none;
  }
}

.flag {
  border: var(--control-border-width) solid var(--color-text);
  padding: 0 var(--space-200);
  font-weight: var(--font-weight-emphasis);
}

.cell-form {
  margin: 0;
}

/* A link takes the text color of the surface it sits on. The identity records
 * four colors and none of them is a link color, so leaving one unset hands the
 * choice to the browser, which supplies its own blue and a second color once
 * visited. Against the inverse surface those measure 2.23:1 and 1.91:1, which
 * is the only place the screens fall under every ratio DESIGN.md records. The
 * underline carries the affordance instead, and the visited state is not
 * distinguished, because telling it apart needs a fifth color this identity
 * has never measured. */
a,
a:visited {
  color: inherit;
  text-decoration: underline;
}

/* The first focus indicator this identity has had. The source rendered none,
 * and keyboard use does not wait for a design decision. Drawn in the text color
 * of whichever surface it sits on, so it inherits that pair's contrast. */
:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-text);
  outline-offset: var(--focus-ring-offset);
}

.band-inverse :focus-visible {
  outline-color: var(--color-text-inverse);
}

.visually-hidden {
  position: absolute;
  width: var(--control-border-width);
  height: var(--control-border-width);
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
