/* hall-e.com. A site, not the application's panels on a page: full-bleed bands told apart
   by fill, hairline rules instead of frames, one industrial orange. Square throughout. */

@font-face {
  font-family: "Chakra Petch";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/chakra-petch-latin-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Chakra Petch";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("fonts/chakra-petch-latin-500-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Chakra Petch";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/chakra-petch-latin-600-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Chakra Petch";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/chakra-petch-latin-700-normal.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2");
}

:root {
  --deep: oklch(16% 0 0);
  --body: oklch(20% 0 0);
  --raise: oklch(24% 0 0);
  --line: oklch(29% 0 0);
  --line-strong: oklch(38% 0 0);
  --muted: oklch(70% 0.012 240);
  --text: oklch(93% 0.006 240);
  --accent: oklch(74% 0.18 48);
  /* The desktop's online status dot, to the value: green says working in both places. */
  --ok: oklch(81% 0.15 145);
  --warn: oklch(84% 0.16 85);
  --accent-hi: oklch(80% 0.17 48);
  --ink: oklch(12% 0.005 48);

  --shell: 1180px;
  --gutter: 28px;
  --band: 104px;

  /* Tabler `chevron-down`, the variant the application's dropdowns use, carried as a mask so
     whatever wears it takes its colour from a token rather than from the file. */
  --chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6l6 -6'/%3E%3C/svg%3E");

  color-scheme: dark;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  font-family: "Chakra Petch", sans-serif;
  font-size: 16px;
  line-height: 1.65;
  color: var(--text);
  background: var(--body);
}

h1,
h2,
h3,
h4,
p,
ul,
ol,
dl,
dd,
figure {
  margin: 0;
}

img,
svg {
  max-width: 100%;
}

/* `hidden` is only a user-agent `display: none`, so any explicit display on the element wins
   over it — and the contact form sets `display: grid`, which left it on the page while it was
   marked hidden. This puts the attribute back in charge of everything that carries it. */
[hidden] {
  display: none !important;
}

a {
  color: var(--accent);
  text-decoration: none;
  text-underline-offset: 4px;
}

a:hover {
  text-decoration: underline;
}

a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.mono {
  font-family: "IBM Plex Mono", monospace;
  font-size: 0.92em;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
}

.skip:focus {
  left: 12px;
  top: 12px;
  z-index: 40;
  padding: 10px 14px;
  background: var(--accent);
  color: var(--ink);
}

/* Named for assistive technology, off the page for everyone else. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
}

main {
  flex: 1 0 auto;
}

/* Shell and bands ------------------------------------------------------- */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin: 0 auto;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}

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

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

.masthead {
  position: sticky;
  top: 0;
  z-index: 30;
  background: color-mix(in oklab, var(--deep) 88%, transparent);
  backdrop-filter: saturate(140%) blur(8px);
  border-bottom: 1px solid var(--line);
}

.masthead .shell {
  min-height: 68px;
  display: flex;
  align-items: center;
  gap: 32px;
}

.wordmark {
  font-weight: 700;
  font-size: 27px;
  line-height: 1;
  letter-spacing: -1.1px;
  color: var(--text);
  flex-shrink: 0;
}

.wordmark span {
  color: var(--accent);
}

.wordmark:hover {
  text-decoration: none;
}

.nav {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  overflow-x: auto;
  scrollbar-width: none;
}

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

.nav a {
  position: relative;
  flex-shrink: 0;
  padding: 22px 12px;
  color: var(--muted);
  font-size: 15px;
  font-weight: 500;
}

.nav a:hover {
  color: var(--text);
  text-decoration: none;
}

.nav a.active {
  color: var(--text);
}

.nav a.active::after {
  content: "";
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: -1px;
  height: 2px;
  background: var(--accent);
}

/* The menu button. Present in the markup on every page but displayed only inside the narrow
   breakpoint, where the navigation moves into a panel behind it. It stays hidden without
   scripting, since nav.js is what collapses the navigation in the first place. */
.nav-toggle {
  display: none;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  /* A 26px glyph in a 44px target: the icon matches the wordmark's weight, the box is what
     a thumb actually has to hit. The negative margin pulls the box's padding back so the
     glyph, not the box, lines up with the gutter. */
  width: 44px;
  height: 44px;
  margin-right: -9px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text);
  cursor: pointer;
}

.nav-toggle-glyph {
  display: block;
  width: 26px;
  height: 26px;
}

/* Tabler `menu` — two lines — carried inline rather than as a mask, because each line has to
   be transformed on its own to fold into the X. The stroke matches the chevron above. */
.nav-toggle-glyph path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  transform-box: view-box;
  transform-origin: 12px 12px;
  transition: transform 260ms cubic-bezier(0.2, 0.7, 0.3, 1);
}

/* Each line travels to the centre and then turns about it, which is why the translate is
   written after the rotate: transforms apply right to left, so the line is centred first and
   the rotation leaves it there. Reversed, each line would swing wide of the middle. */
.nav-toggle[aria-expanded="true"] .nav-toggle-top {
  transform: rotate(45deg) translateY(4px);
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bottom {
  transform: rotate(-45deg) translateY(-4px);
}

/* Hero ------------------------------------------------------------------ */

.hero {
  background: var(--deep);
  padding-block: 112px 88px;
}

h1 {
  margin-top: 28px;
  font-size: clamp(38px, 5.4vw, 62px);
  line-height: 1.05;
  font-weight: 600;
  letter-spacing: -1.6px;
  max-width: 17ch;
}

.hero h1,
.hero .lead {
  max-width: none;
}

.hero h1 {
  font-size: clamp(38px, 5.4vw, 58px);
}

.hero .lead {
  margin-top: 26px;
  font-size: 19px;
  color: var(--muted);
}

/* Buttons --------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 13px 24px;
  font-weight: 600;
  font-size: 15px;
  letter-spacing: 0.2px;
  border: 1px solid transparent;
}

.btn:hover {
  text-decoration: none;
}

.btn--accent {
  background: var(--accent);
  color: var(--ink);
}

.btn--accent:hover {
  background: var(--accent-hi);
}

.btn--ghost {
  /* Every other button on the site is an anchor, which has no fill of its own. A real
     `<button>` carries the user agent's grey one, which showed through this outline. */
  background: none;
  border-color: var(--line-strong);
  color: var(--text);
}

.btn--ghost:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* The masthead's Download control. `.nav a` sets a muted colour and a 22px vertical padding
   that fills the 68px bar, and it outranks `.btn--accent` on specificity — one class plus a
   type selector against one class — so the accent fill would have arrived with muted text on
   it. These selectors carry the extra class needed to win, rather than an `!important`. */
.nav .btn.nav-cta {
  flex-shrink: 0;
  width: auto;
  margin-left: 12px;
  padding: 9px 18px;
  font-size: 14px;
  color: var(--ink);
}

.nav .btn.nav-cta:hover {
  background: var(--accent-hi);
  color: var(--ink);
}

/* The active page marker is an underline drawn under a text link. Under a filled button it
   would read as an artefact, so the button says "you are here" by dimming instead. */
.nav .btn.nav-cta.active::after {
  content: none;
}

.nav .btn.nav-cta.active {
  background: color-mix(in oklab, var(--accent) 62%, var(--body));
}

/* Download page ---------------------------------------------------------- */

/* The page is one section long and its only job is the button, so the section fills the
   window rather than sitting in a band at its own content height. That is what removes the
   empty band beneath it -- `.page-head` paints to its content and `main` stretches past it,
   so the remainder reads as a second, empty section. Same reasoning as `.story` on About.
   Filling the window is also what gives the centring something to centre within. */
.main--center {
  display: flex;
  flex-direction: column;
}

.download-head {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: center;
  padding-block: 64px;
}

.download-head h1 {
  margin-top: 0;
  max-width: none;
  font-size: clamp(34px, 4.4vw, 50px);
  letter-spacing: -1.2px;
}

.download-action {
  margin-top: 36px;
}

.btn--wide {
  padding: 16px 34px;
  font-size: 16px;
}

/* Acknowledges the click. There is no browser event for a download starting, so this is a
   timed state set by download.js and not a real progress report — it says "that registered",
   which is the thing a second and third click are asking for. */
.btn--wide[data-downloading] {
  background: color-mix(in oklab, var(--accent) 70%, var(--body));
}

/* Timeline figure ------------------------------------------------------- */

.figure {
  margin-top: 84px;
}

.video-frame {
  position: relative;
}

.figure video {
  width: 100%;
  height: auto;
  display: block;
  background: var(--raise);
  aspect-ratio: 16 / 9;
}

/* Playback position only. It is not a control, and cannot be dragged. */
.video-progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  background: var(--line);
  pointer-events: none;
}

.video-progress span {
  display: block;
  width: 0;
  height: 100%;
  background: var(--accent);
}

/* Section heads --------------------------------------------------------- */

h2 {
  margin-top: 14px;
  font-size: clamp(27px, 3.2vw, 36px);
  line-height: 1.15;
  font-weight: 600;
  letter-spacing: -0.8px;
  max-width: 20ch;
}

/* Split: heading column beside content column --------------------------- */

.split {
  display: grid;
  grid-template-columns: minmax(0, 340px) minmax(0, 1fr);
  gap: 56px 72px;
  align-items: start;
}

/* Rows: hairline-divided content, no frames ----------------------------- */

.rows {
  list-style: none;
  padding: 0;
  display: grid;
}

.rows > li {
  padding: 26px 0;
  border-top: 1px solid var(--line);
  display: grid;
  grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
  gap: 8px 40px;
}

.rows > li:first-child {
  padding-top: 0;
  border-top: 0;
}

.rows > li:last-child {
  padding-bottom: 0;
}

.rows h3 {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.2px;
}

.rows p {
  color: var(--muted);
  max-width: 60ch;
}

/* Stage: the step being described, drawn ------------------------------- */

.stage {
  position: relative;
  margin-top: 48px;
  aspect-ratio: 800 / 220;
  background: var(--deep);
}

.stage .shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  animation: shot-show 35s linear infinite var(--step-delay);
}

/* Each step's place in the cycle. The script rewrites these on a click so
   the sequence restarts from whichever step was picked. */
.stage .shot:nth-child(1) {
  --step-delay: 0s;
}

.stage .shot:nth-child(2) {
  --step-delay: 7s;
}

.stage .shot:nth-child(3) {
  --step-delay: 14s;
}

.stage .shot:nth-child(4) {
  --step-delay: 21s;
}

.stage .shot:nth-child(5) {
  --step-delay: 28s;
}

@keyframes shot-show {
  0%,
  19.99% {
    opacity: 1;
  }

  20%,
  100% {
    opacity: 0;
  }
}

/* Steps ----------------------------------------------------------------- */

.steps {
  list-style: none;
  padding: 0;
  margin-top: 40px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 40px 32px;
}

.steps li {
  position: relative;
  isolation: isolate;
  padding-top: 18px;
  border-top: 2px solid var(--line-strong);
}

.steps li > * {
  position: relative;
}

/* The rule above the step is its progress bar. */
.steps li::before {
  content: "";
  position: absolute;
  top: -2px;
  left: 0;
  width: 0;
  height: 2px;
  background: var(--accent);
  animation: step-fill 35s linear infinite var(--step-delay);
}

/* The step that is running sits on a lifted surface. */
.steps li::after {
  content: "";
  position: absolute;
  z-index: -1;
  top: -2px;
  right: -16px;
  bottom: -16px;
  left: -16px;
  background: var(--raise);
  opacity: 0;
  animation: step-surface 35s linear infinite var(--step-delay);
}

.steps li:nth-child(1) {
  --step-delay: 0s;
}

.steps li:nth-child(2) {
  --step-delay: 7s;
}

.steps li:nth-child(3) {
  --step-delay: 14s;
}

.steps li:nth-child(4) {
  --step-delay: 21s;
}

.steps li:nth-child(5) {
  --step-delay: 28s;
}

/* Picking a step: the whole card is the target, and the text stays selectable
   under it because the button carries no fill of its own. */
.steps li .step-pick {
  position: absolute;
  z-index: 2;
  top: -2px;
  right: -16px;
  bottom: -16px;
  left: -16px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

/* Held for one frame while the cycle is re-pointed at a new step. */
.flow--reset .stage .shot,
.flow--reset .steps li::before,
.flow--reset .steps li::after,
.flow--reset .step-number,
.flow--reset .steps h3,
.flow--reset .steps p {
  animation: none;
}

@keyframes step-fill {
  0% {
    width: 0;
  }

  20% {
    width: 100%;
  }

  20.01%,
  100% {
    width: 0;
  }
}

@keyframes step-surface {
  0%,
  19.99% {
    opacity: 1;
  }

  20%,
  100% {
    opacity: 0;
  }
}

@keyframes step-number-lit {
  0%,
  19.99% {
    color: var(--accent);
  }

  20%,
  100% {
    color: var(--line-strong);
  }
}

@keyframes step-heading-lit {
  0%,
  19.99% {
    color: var(--text);
  }

  20%,
  100% {
    color: var(--muted);
  }
}

@keyframes step-lit {
  0%,
  19.99% {
    color: var(--text);
  }

  20%,
  100% {
    color: var(--muted);
  }
}

.step-number {
  display: block;
  color: var(--line-strong);
  font-family: "IBM Plex Mono", monospace;
  font-size: 13px;
  letter-spacing: 2px;
  animation: step-number-lit 35s linear infinite var(--step-delay);
}

.steps h3 {
  margin-top: 10px;
  color: var(--muted);
  font-size: 18px;
  font-weight: 600;
  animation: step-heading-lit 35s linear infinite var(--step-delay);
}

.steps p {
  margin-top: 8px;
  color: var(--muted);
  font-size: 15px;
  animation: step-lit 35s linear infinite var(--step-delay);
}

/* Questions and answers: a question is a sentence, not a label ---------- */

.faq {
  display: grid;
}

.faq > div {
  padding: 24px 0;
  border-top: 1px solid var(--line);
}

.faq > div:first-child {
  padding-top: 0;
  border-top: 0;
}

.faq > div:last-child {
  padding-bottom: 0;
}

.faq dt {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.2px;
}

.faq dd {
  margin-top: 8px;
  max-width: 60ch;
  color: var(--muted);
}

/* Plain bullet list ----------------------------------------------------- */

.plain {
  list-style: none;
  padding: 0;
  display: grid;
  gap: 10px;
  color: var(--muted);
}

.plain li {
  position: relative;
  padding-left: 20px;
}

.plain li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 11px;
  width: 6px;
  height: 6px;
  background: var(--accent);
}

/* Prose blocks ---------------------------------------------------------- */

.prose p {
  color: var(--muted);
}

.prose p + p {
  margin-top: 16px;
}

.prose--bright p {
  color: var(--text);
}

/* Page head ------------------------------------------------------------- */

.page-head {
  background: var(--deep);
  padding-block: 88px 64px;
}

.page-head h1 {
  margin-top: 16px;
  font-size: clamp(34px, 4.4vw, 50px);
  letter-spacing: -1.2px;
}

.page-head h1,
.page-head .lead {
  max-width: none;
}

/* A page that is one section long: no header band of its own, so nothing
   below it can read as a second, empty one. */
.story {
  padding-block: 56px 96px;
}

.story h1 {
  margin-top: 0;
  max-width: none;
  font-size: clamp(34px, 4.4vw, 50px);
  letter-spacing: -1.2px;
}

.story .prose {
  margin-top: 34px;
}

.page-head .lead {
  margin-top: 22px;
  font-size: 18px;
  color: var(--muted);
}

.effective {
  margin-top: 22px;
  color: var(--muted);
  font-family: "IBM Plex Mono", monospace;
  font-size: 13px;
}

/* Pricing --------------------------------------------------------------- */

/* The page carries no heading band, so this section opens it and takes the top padding a
   `.page-head` would have contributed. Same reasoning as `.story` on About. */
.price {
  padding-block: 88px 104px;
}

/* Two boxes, equal width and equal height. The paid one is not a better tier -- it is what
   happens after the free one -- so they are the same box twice, told apart by their top rule
   and by where the orange lands. */
.plans {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 24px;
}

/* Lifted off the band by fill, not by a frame, like every other surface on this site. The
   column is what puts each button on the box's bottom edge whatever the list above it weighs. */
.plan {
  display: flex;
  flex-direction: column;
  padding: 34px 32px 32px;
  background: var(--raise);
  /* The paid box's rule is the only one. This one is transparent rather than absent, so the
     fill shows through it and the two boxes still start on the same line. */
  border-top: 3px solid transparent;
}

.plan--paid {
  border-top-color: var(--accent);
}

.plan-figure {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 14px;
  margin-top: 0;
}

.plan-figure strong {
  font-size: clamp(40px, 5vw, 58px);
  line-height: 1;
  font-weight: 600;
  letter-spacing: -2px;
}

.plan--paid .plan-figure strong {
  color: var(--accent);
}

/* The qualifier drops to its own line whole rather than breaking mid-phrase. */
.plan-figure span {
  white-space: nowrap;
  color: var(--muted);
  font-size: 16px;
}

/* The free box says its condition in a line rather than a list: one fact does not need a
   mark in front of it, and the orange is what the box has instead of the paid one's figure. */
.plan-copy {
  margin-top: 24px;
  color: var(--accent);
}

.plan-list {
  list-style: none;
  margin-top: 28px;
  padding: 0;
  display: grid;
  gap: 12px;
}

.plan-list li {
  position: relative;
  padding-left: 20px;
  color: var(--muted);
}

.plan-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 11px;
  width: 6px;
  height: 6px;
  background: var(--accent);
}

.plan-action {
  margin-top: auto;
  padding-top: 30px;
}

/* Inside a box the button is the box's own width: at anything narrower it reads as a chip
   floating in the corner rather than as the plan's action. */
.plan-action .btn {
  width: 100%;
}

/* Supported boards ------------------------------------------------------ */

.boards {
  padding-block: 72px 104px;
}

/* Fixed layout so the two columns are set by the table and not by whichever board name happens
   to be the longest, which is what keeps a cell under its own heading. */
.board-table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
  text-align: left;
}

.board-table th,
.board-table td {
  padding: 18px 0;
  font-weight: 400;
}

.board-table thead th {
  padding-top: 0;
  font-family: "IBM Plex Mono", monospace;
  font-size: 12px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--muted);
}

.board-table th:last-child,
.board-table td:last-child {
  width: 180px;
}

.board-table tbody tr {
  border-top: 1px solid var(--line);
}

.board-table tbody th {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.2px;
}

/* The models the brand's row stands for. One row per brand rather than one per board, because
   every model under a brand carries the same state and a column of identical markers says
   nothing the brand row has not already said. */
.board-models {
  display: block;
  margin-top: 4px;
  color: var(--muted);
  font-size: 14px;
  font-weight: 400;
  letter-spacing: 0;
}

/* A state is a value, so it takes the numeric face. Hue tells the four apart, not brightness:
   green works, orange is on its way, and the two neutrals cannot be read today, the quieter of
   them being the one with nothing to act on. All four clear AA on the body. */
.board-status {
  font-family: "IBM Plex Mono", monospace;
  font-size: 12px;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--muted);
}

/* Green says working, the same value the desktop's connection dot uses. */
.board-status[data-state="supported"] {
  color: var(--ok);
}

/* The accent marks what is moving, the way it marks activity everywhere else. */
.board-status[data-state="live"] {
  color: var(--accent);
}

/* Between the two, in the reading colour: not moving, but not a dead end either. */
.board-status[data-state="untested"] {
  color: var(--text);
}

/* The brand name is the link, so it keeps the row's own colour and answers only on hover: a
   column of orange names would fight the states beside them. */
.board-table tbody th a {
  color: inherit;
}

.board-table tbody th a:hover {
  color: var(--accent);
  text-decoration: none;
}

/* A condition on the row it sits in, carried by the mark itself rather than by a note over the
   page: closed it is a round badge, hovered it opens into a pill and says what it is. Round is
   why it may carry a radius at all, and why it can grow without a corner changing shape. */
.warn-pill {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  margin-left: 8px;
  height: 18px;
  border-radius: 9px;
  background: var(--warn);
  color: var(--ink);
  overflow: hidden;
}

.warn-pill-mark {
  flex: none;
}

/* A column of no width rather than a measured one, so the pill opens to exactly the text and
   nothing has to be guessed at or clipped short. */
.warn-pill-text {
  display: grid;
  grid-template-columns: 0fr;
  transition: grid-template-columns 170ms ease;
}

.warn-pill-text > span {
  overflow: hidden;
  /* A grid item will not shrink past its own content without this, which left the badge an
     oval rather than a circle when closed. */
  min-width: 0;
  white-space: nowrap;
  font-size: 12px;
  font-weight: 600;
  /* The row's heading sets both of these and they inherit. */
  letter-spacing: 0;
  text-transform: none;
}

/* The gap before the pill's own edge, set as content rather than padding: padding on the
   clipped item is a floor it cannot shrink below, which left 9px of yellow hanging off the
   badge while it was closed. */
.warn-pill-text > span::after {
  content: "";
  display: inline-block;
  width: 9px;
}

.warn-pill:hover .warn-pill-text {
  grid-template-columns: 1fr;
}

/* Info marker: the treatment the desktop's InfoTip uses, in CSS alone because the site has no
   component layer to put it in. A marker beside the label it qualifies, opening a note anchored
   above it, static, and dismissed on leave -- notes never follow the pointer. */
.info-tip {
  position: relative;
  display: inline-flex;
  align-items: center;
  margin-left: 6px;
  vertical-align: middle;
  opacity: 0.75;
  /* The marker sits inside a label set in uppercase with wide tracking, and both inherit. */
  text-transform: none;
  letter-spacing: normal;
}

.info-tip:hover {
  opacity: 1;
}

.info-tip-text {
  position: absolute;
  bottom: calc(100% + 8px);
  /* Held by its right edge rather than centred on the marker: the column this qualifies sits
     against the page's right margin, so a centred note would hang off the window. */
  right: -9px;
  z-index: 40;
  width: max-content;
  max-width: 240px;
  padding: 7px 10px;
  /* Outlined rather than filled, so it reads as a note about the column. */
  background: oklch(18% 0 0);
  border: 1px solid var(--accent);
  color: var(--accent);
  /* A note is a sentence, so it leaves the numeric face the column header is set in, and a
     popover is never rendered in the label's uppercase and tracking. */
  font-family: "Chakra Petch", sans-serif;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.45;
  text-align: left;
  text-transform: none;
  letter-spacing: normal;
  pointer-events: none;
  visibility: hidden;
  /* One row per state, so a definition wraps under its own label and never under the one
     above it. */
  display: grid;
  gap: 7px;
}

.info-tip-text b {
  font-weight: 600;
}

/* Dropped below the marker when there is no room above it, the way the desktop's InfoTip
   places itself. Only a measurement can tell, so the attribute comes from the page's script. */
.info-tip[data-below="true"] .info-tip-text {
  bottom: auto;
  top: calc(100% + 8px);
}

/* No fade: the note appears immediately, which also leaves nothing for reduced motion to
   turn off. */
.info-tip:hover .info-tip-text {
  visibility: visible;
}

/* Contact --------------------------------------------------------------- */

/* The form leads and the notes sit beside it, in the same proportion `.split` uses the other
   way round: what the page is for takes the wider column. */
.contact-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 360px);
  gap: 56px 72px;
  align-items: start;
}

.contact-aside h2 {
  font-size: 21px;
  letter-spacing: -0.4px;
  max-width: none;
}

.contact-aside .prose {
  margin-top: 20px;
}

.contact-aside .prose .plain {
  margin-top: 16px;
  margin-bottom: 16px;
}

/* Form ------------------------------------------------------------------ */

.form {
  display: grid;
  gap: 24px;
}

.field {
  display: grid;
  gap: 8px;
}

.field label {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Marks a field the form insists on. The control's own `required` is what tells assistive
   technology so; this is the same fact for anyone reading the label. */
.req {
  color: var(--accent);
  font-weight: 600;
}

/* Controls are told apart from the band by fill, like every other surface here, and outlined
   because the reference's one control is outlined. Square, and no radius anywhere. */
.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 12px 14px;
  font-family: inherit;
  font-size: 16px;
  line-height: 1.5;
  color: var(--text);
  background: var(--raise);
  border: 1px solid var(--line-strong);
  border-radius: 0;
  /* iOS zooms a focused control whose text is under 16px and never zooms back out. The size
     above is what stops that; this stops Safari restyling the control around it. */
  -webkit-appearance: none;
  appearance: none;
}

.field textarea {
  resize: vertical;
  min-height: 160px;
}

/* The one control with a menu. Two of them, in fact: the native select below is what a
   visitor gets until contact.js replaces it, so it is styled to match rather than left to the
   browser. `appearance: none` took the native arrow off, so the application's own is drawn
   back on: Tabler's chevron-down at its 1.5px stroke, as a mask, so the accent stays the one
   token above and cannot drift from a hex copy of it baked into a data URI.

   Positioned from the bottom of the field, which is the bottom of the control: a select is
   the last thing in its own `.field`. Add a note under one and this needs an offset. */
.field:has(select)::after {
  content: "";
  position: absolute;
  right: 15px;
  bottom: 16px;
  width: 18px;
  height: 18px;
  background: var(--accent);
  -webkit-mask: var(--chevron-down) center / contain no-repeat;
  mask: var(--chevron-down) center / contain no-repeat;
  /* The arrow is decoration on a control that is already fully operable; a click on it must
     reach the control underneath. */
  pointer-events: none;
  /* The listbox draws its own, so this one goes when the enhancement arrives. */
  display: var(--native-chevron, block);
}

.field:has(select) {
  position: relative;
}

.field select {
  padding-right: 44px;
}

/* Listbox: the application's dropdown, without the framework ------------- */

.listbox {
  position: relative;
  /* The native select is still in here, hidden; its chevron is not wanted over the trigger. */
  --native-chevron: none;
}

.listbox-trigger {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  font-family: inherit;
  font-size: 16px;
  line-height: 1.5;
  text-align: left;
  color: var(--text);
  background: var(--raise);
  border: 1px solid var(--line-strong);
  border-radius: 0;
  cursor: pointer;
}

/* The em dash is the unchosen state. It is set in the muted ink so it reads as the absence of
   a choice rather than as one of the four. */
.listbox-trigger[data-empty="true"] {
  color: var(--muted);
}

.listbox-trigger::after {
  content: "";
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  background: var(--accent);
  -webkit-mask: var(--chevron-down) center / contain no-repeat;
  mask: var(--chevron-down) center / contain no-repeat;
}

.listbox[data-open="true"] .listbox-trigger,
.listbox-trigger:focus-visible {
  border-color: var(--accent);
  outline: none;
}

.listbox-menu {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 4px 0;
  list-style: none;
  max-height: 260px;
  overflow-y: auto;
  background: var(--raise);
  border: 1px solid var(--line-strong);
}

.listbox-option {
  padding: 7px 14px;
  font-size: 15px;
  line-height: 1.5;
  color: var(--text);
  cursor: pointer;
}

/* The highlighted row reads as solid orange with dark text, as the application's menus do.
   Hover is not a second treatment: it moves `data-active`, so there is only ever one. */
.listbox-option[data-active="true"] {
  background: var(--accent);
  color: var(--ink);
}

.listbox-option[aria-selected="true"] {
  color: var(--accent);
}

.listbox-option[aria-selected="true"][data-active="true"] {
  color: var(--ink);
}

/* Submitted, never seen. Not `display: none`: a select that is not rendered cannot be read
   back reliably by every browser, and this one is still the form's value. */
.listbox-native {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* The screen a sent message leaves behind ------------------------------- */

.sent-screen {
  padding: 8px 0;
}

.sent-screen:focus {
  outline: none;
}

/* The heading is the whole screen. Nothing under it, beside it or explaining it. It takes the
   accent, which is what marks a thing that happened everywhere else here. */
.sent-screen h2 {
  margin-top: 0;
  max-width: none;
  color: var(--accent);
}

/* Choice: the one question with two answers ----------------------------- */

.choice {
  margin: 0;
  padding: 0;
  border: 0;
}

.choice legend {
  padding: 0;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.choice-options {
  display: flex;
  gap: 28px;
  margin-top: 12px;
}

/* Carries `.field` so it outranks `.field label`, which is one class plus a type selector and
   would otherwise set these answers in the same small uppercase as the question above them. */
.field .choice-option {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 16px;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
  cursor: pointer;
}

/* Round, because a radio is one of the few things that genuinely is. */
.field .choice-option input {
  appearance: none;
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  margin: 0;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background: var(--raise);
  cursor: pointer;
}

.field .choice-option input:checked {
  border-color: var(--accent);
  /* The dot is the fill itself, inset by the gradient's stop rather than by a pseudo-element
     a radio input cannot carry. */
  background: radial-gradient(circle, var(--accent) 0 4px, var(--raise) 5px);
}

.field .choice-option input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--accent);
}

.field input::placeholder,
.field textarea::placeholder {
  color: var(--muted);
}

/* The browser marks a field invalid from the moment it is empty, so an untouched form would
   render every field in an error colour before anyone has typed. `:user-invalid` waits until
   the person has actually been in the field or tried to submit. */
.field input:user-invalid,
.field textarea:user-invalid {
  border-color: var(--accent-hi);
}

.form-actions {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
}

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

.form-actions .btn:disabled {
  /* The same control, inactive: the accent held at low lightness and chroma, as the
     application's disabled capture button is. Never grey, which reads as a different control. */
  background: color-mix(in oklab, var(--accent) 38%, var(--body));
  color: color-mix(in oklab, var(--ink) 70%, var(--body));
  cursor: default;
}

/* Empty until contact.js writes an outcome into it, and it takes no space until then. */
.form-status {
  font-size: 15px;
  color: var(--muted);
}

.form-status[data-state="sent"] {
  color: var(--accent);
}

.form-status[data-state="failed"] {
  color: var(--text);
}

/* The honeypot. Taken out of the layout without `display: none` or `visibility: hidden`,
   which a crawler checks for and skips: this has to look like a field it can fill in. */
.trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

@media (max-width: 900px) {
  .contact-layout {
    grid-template-columns: minmax(0, 1fr);
    gap: 48px;
  }
}
/* Legal ----------------------------------------------------------------- */

.legal > * {
  max-width: 78ch;
}

.legal h2 {
  margin-top: 48px;
  font-size: 20px;
  letter-spacing: -0.2px;
  max-width: none;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}

.legal h2:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.legal p,
.legal li {
  color: var(--muted);
}

.legal h2 + p {
  margin-top: 14px;
}

.legal p + p,
.legal p + ul,
.legal ul + p {
  margin-top: 14px;
}

.legal ul {
  padding-left: 22px;
  display: grid;
  gap: 6px;
}

.legal strong {
  color: var(--text);
  font-weight: 600;
}

.callout {
  margin-top: 22px;
  padding: 22px 24px;
  border-left: 3px solid var(--accent);
  background: var(--raise);
}

.callout p {
  color: var(--text);
}

/* Footer ---------------------------------------------------------------- */

.site-footer {
  flex-shrink: 0;
  margin-top: auto;
  background: var(--deep);
  border-top: 1px solid var(--line);
}

.footer-inner {
  padding-block: 64px 48px;
  display: grid;
  grid-template-columns: minmax(0, 2fr) repeat(3, minmax(0, 1fr));
  gap: 48px;
}

.footer-brand p {
  margin-top: 16px;
  color: var(--muted);
  font-size: 14px;
  max-width: 34ch;
}

.footer-column h2 {
  margin-top: 0;
  font-family: "IBM Plex Mono", monospace;
  font-size: 12px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 400;
}

.footer-column ul {
  list-style: none;
  padding: 0;
  margin-top: 16px;
  display: grid;
  gap: 10px;
}

.footer-column a {
  color: var(--text);
  font-size: 15px;
  word-break: break-word;
}

.footer-column a:hover {
  color: var(--accent);
  text-decoration: none;
}

.footer-bar {
  padding-block: 20px 24px;
  border-top: 1px solid var(--line);
  display: flex;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  color: var(--muted);
  font-size: 13px;
}

/* Responsive ------------------------------------------------------------ */

@media (max-width: 1000px) {
  :root {
    --band: 80px;
    --gutter: 24px;
  }

  .split {
    grid-template-columns: minmax(0, 1fr);
    gap: 36px;
  }
}

@media (max-width: 860px) {
  /* The diagram's own labels would scale below reading size here. The step
     cards say the same thing in words, so the stage steps aside. */
  .stage {
    display: none;
  }

  .steps {
    margin-top: 56px;
  }

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

  .footer-inner {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 36px;
  }

  .footer-brand {
    grid-column: 1 / -1;
  }
}

@media (max-width: 680px) {
  :root {
    --band: 64px;
    --gutter: 20px;
  }

  body {
    font-size: 15px;
  }

  /* The wordmark and the menu button share one row. Without scripting the navigation is
     still in the flow below them, which is what the wrapping and the stacked rules below
     are for. */
  .masthead .shell {
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0;
    min-height: 60px;
  }

  .wordmark {
    font-size: 24px;
  }

  /* Only once nav.js has marked the document: without it the navigation never collapses, and
     a button that opens nothing is worse than no button. */
  .js .nav-toggle {
    display: inline-flex;
  }

  .nav {
    width: 100%;
    margin-left: -12px;
    margin-right: -12px;
    flex-direction: column;
    align-items: stretch;
    overflow: visible;
  }

  .nav a {
    padding: 14px 12px;
  }

  /* Stacked, the underline that marks the open page would run the width of the row and read
     as a rule between rows. The same 2px accent moves to the leading edge instead. */
  .nav a.active::after {
    left: 0;
    right: auto;
    top: 8px;
    bottom: 8px;
    width: 2px;
    height: auto;
  }

  /* The stacked column stretches it, so it must not also be told to be the full width of its
     container: that width plus these margins is wider than the container itself. */
  .nav .btn.nav-cta {
    width: auto;
    margin: 8px 12px 16px;
  }

  /* With scripting, the navigation leaves the flow and hangs under the masthead as a panel.
     `visibility` rather than opacity alone: a transparent panel would still hold the links
     in the tab order while it is closed. */
  .js .nav {
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    margin: 0;
    padding: 4px var(--gutter) 0;
    background: var(--deep);
    border-bottom: 1px solid var(--line);
    visibility: hidden;
    opacity: 0;
    transform: translateY(-6px);
    transition:
      opacity 200ms ease,
      transform 200ms ease,
      visibility 0s linear 200ms;
  }

  .js .masthead[data-nav-open="true"] .nav {
    visibility: visible;
    opacity: 1;
    transform: none;
    transition:
      opacity 200ms ease,
      transform 200ms ease,
      visibility 0s;
  }

  .hero {
    padding-block: 64px 56px;
  }

  .page-head {
    padding-block: 56px 44px;
  }

  h1 {
    letter-spacing: -1px;
  }

  .rows > li {
    grid-template-columns: minmax(0, 1fr);
    gap: 6px;
  }

  .boards {
    padding-block: 56px 80px;
  }

  /* The status column gives back what the marker does not need, so the board names keep the
     width instead of wrapping beside an empty gutter. */
  .board-table th:last-child,
  .board-table td:last-child {
    width: 124px;
  }

  .btn {
    width: 100%;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    scroll-behavior: auto;
  }

  .warn-pill-text {
    transition: none;
  }

  /* The menu still becomes an X and the panel still opens; neither is animated on the way. */
  .nav-toggle-glyph path,
  .js .nav,
  .js .masthead[data-nav-open="true"] .nav {
    transition: none;
  }

  .stage .shot,
  .steps li::before,
  .steps li::after,
  .step-number,
  .steps h3,
  .steps p {
    animation: none;
  }

  .stage .shot.is-current {
    opacity: 1;
  }

  .steps li.is-current::before {
    width: 100%;
  }

  .steps li.is-current::after {
    opacity: 1;
  }

  .steps li.is-current .step-number {
    color: var(--accent);
  }

  .steps li.is-current h3,
  .steps li.is-current p {
    color: var(--text);
  }
}
