/*
  Timeslip, the public page.

  The same visual language as the app, because it is the same product. The
  tokens below are lifted from app/globals.css rather than re-picked, so a
  colour that changes there changes in one place here too.

  WHAT THIS PAGE IS MADE OF, AND WHAT IT DELIBERATELY IS NOT

  Ink, paper, hairlines and one blue. There is no gradient anywhere on the
  page, no glow behind the headline, no grid texture, no blur behind the top
  bar, and no grid of cards with a shadow under each. Those are the defaults a
  generated page arrives with, and a reader who has seen forty of them reads
  them as "nobody made this". A payroll document is not a thing to be casual
  about, so the page is set the way the documents are: a rule between two
  things rather than a box around each, figures in mono, one column of type
  against a narrow column of headings, everything ranged left.

  The one object that carries the identity is the app's own: the block meter,
  a cell for every hour a block asks for. It is the only soft shape and the
  only place blue is used at size. Everything else is text on a rule.

  Colour, in full: ink, paper, the greys between them, the brand blue on the
  meter, the buttons and the links, and green on the meter once the block's
  requirement is met. Nothing else has a colour.

  Type, in full: Bricolage Grotesque at one large size for the headline and
  one smaller size for every section heading; IBM Plex Sans at one size for
  everything a person reads; IBM Plex Mono at one small size for figures and
  for labels. A third text size would be a fourth decision nobody asked for.

  No framework and no build step. One stylesheet, one deferred script
  (app.js), and the page is complete without the script.
*/

/* ------------------------------------------------------------------- fonts */
/*
  Self-hosted, copied from public/fonts. No Google Fonts and no CDN: the page
  claims the site makes no third-party request, and a webfont from somebody
  else's domain would be the first one it made.

  Six files, and each of them is a weight the page actually sets. Plex Sans at
  500 was here and was never fetched by anybody: nothing on the page asks for
  it, and a face nobody requests is a file in the directory pretending to be
  part of the design.
*/
@font-face {
  font-family: "Plex Sans";
  src: url("fonts/ibm-plex-sans-400.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
/* The medium the app sets its labels in; without it a 500 fell back to 400. */
@font-face {
  font-family: "Plex Sans";
  src: url("fonts/ibm-plex-sans-500.woff2") format("woff2");
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: "Plex Sans";
  src: url("fonts/ibm-plex-sans-600.woff2") format("woff2");
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: "Plex Sans";
  src: url("fonts/ibm-plex-sans-700.woff2") format("woff2");
  font-weight: 700;
  font-display: swap;
}
@font-face {
  font-family: "Plex Mono";
  src: url("fonts/ibm-plex-mono-400.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "Plex Mono";
  src: url("fonts/ibm-plex-mono-500.woff2") format("woff2");
  font-weight: 500;
  font-display: swap;
}
/* One file, weight axis inside it: 600 through 800 cost one request. */
@font-face {
  font-family: "Bricolage";
  src: url("fonts/bricolage-grotesque-var.woff2") format("woff2");
  font-weight: 600 800;
  font-display: swap;
}

/* ------------------------------------------------------------------ tokens */
:root {
  color-scheme: light;

  --ink: #14162b;
  --ground: #f4f4f7;
  /*
    Three colours written out rather than derived with rgb(from …): the rule
    through the old way in "What changes", and the hairline and the shadow on
    the photographed screen. Relative colour syntax is new enough that a
    browser a year or two old drops the whole declaration rather than the
    colour, which takes the strike off the ledger and the edge off the
    screen, and neither of those is decoration.
  */
  --strike: rgb(85 89 112 / 0.8);
  --strike-off: rgb(85 89 112 / 0);
  --screen-edge: rgb(20 22 43 / 0.14);
  --screen-shade: rgb(20 22 43 / 0.45);
  --paper: #ffffff;
  --rule: #dcdee7;
  --rule-firm: #b9bdcd;

  --text: #14162b;
  /*
    slate-600, not slate-500. The app measured slate-500 at 4.15:1 on this
    ground, under the line, and this page holds itself to the same 4.5:1.
  */
  --muted: #555970;

  /* Links and the small mono labels on paper. */
  --stamp: #3342cb;

  --leaf: #0e6b44;

  /*
    A button's fill and a logged cell, neither of which moves with the scheme.
    The app makes the same argument in globals.css: white on a primary button
    is right in both, so lifting the blue for the dark scheme would take the
    label with it.
  */
  --accent-solid: #3b4ce1;
  --accent-solid-hover: #2f3ec4;
  /* An outlined control's edge, held to 3:1 on the ground behind it. */
  --edge: #767c93;

  --on-ink: #ffffff;
  --on-ink-muted: rgb(255 255 255 / 0.74);
  /*
    Two edges on ink, because they are doing different jobs. The quiet one is
    a divider; the firm one is the border of a control somebody has to find,
    and it is at 0.45 rather than 0.16 so it clears 3:1 on the ink.
  */
  --on-ink-rule: rgb(255 255 255 / 0.16);
  --on-ink-edge: rgb(255 255 255 / 0.45);

  /*
    The meter, in the app's states.

    The hour that is NEXT is outlined in the app's own mango
    (components/ui/BlockMeter.tsx: ring-2 ring-inset ring-amber-500). This
    page drew it in blue for four passes, on the argument that blue, green and
    the greys were the whole palette and a fourth colour would appear exactly
    once. Two things ended that: the drawn screens already carry the app's
    amber wherever something is flagged, and the photograph in "Nothing to
    install" is a real phone with a real mango cell on it, so blue here was
    the page disagreeing with its own screenshot.
  */
  --cell-logged: #3b4ce1;
  --cell-met: #1fb273;
  --cell-next: #f5892e;
  --cell-empty: rgb(255 255 255 / 0.12);

  /*
    One radius, and it is small enough to read as a cut corner rather than as
    a pill. The meter's cells are the single exception, at 4px, because the
    cell is the thing this page is built around and it is rounded in the app.
  */
  --radius: 3px;

  --measure: 62ch;
  --bar-h: 3.5rem;

  /*
    The two replays. Each is its own sequence plus about three seconds of
    holding still on the finished state, and app.js knows neither number: it
    waits for the fade at the end of one and puts the sequence back to zero.
  */
  --cycle: 8.6s;
  --lead: 1.2s;
  --demo-run: 4.6s;
  --demo-cycle: 8.6s;

  /*
    THE PAGE'S ONE GRID. A margin column carrying a number and a heading, and
    a content column carrying everything a person reads. Every section uses
    it, the band with the story in it included, so there is one left edge on
    the page and one right edge, and no block sits half way across a column
    it does not fill.
  */
  --rail: 18rem;
  --gutter: 4.5rem;

  /*
    THE DRAWN SCREENS' OWN PALETTE.

    Everything under "the screens" further down is a drawing of a real screen
    in the app, so its colours are the app's rather than this page's: the
    calendar's fills, the rhythm grid's blue, the flag panel's mango, the
    office pills. They are named here, once, and swapped in the dark block
    below, so a drawn screen turns with the page instead of staying a white
    rectangle on a dark one. Values are app/globals.css's @theme steps.
  */
  --d-tint: #e4e7fc;        /* blue-100: a day this subject was logged on */
  --d-tint-ring: #c5caf7;   /* blue-200 */
  /*
    The numeral ON that tint. It was --d-blue-deep, which is the right blue on
    paper and measured 4.04:1 on the tint once the scheme turned over: the
    dark tint is a deep navy and the dark blue-deep is a light periwinkle, and
    the pair drifted towards each other. Named separately so each scheme can
    hold its own 4.5.
  */
  --d-tint-ink: #3342cb;
  --d-ground: #f3f4f8;      /* slate-100: the quiet ground inside a screen */
  --d-ground-2: #e3e5ee;    /* slate-200 */
  --d-blue: #3b4ce1;        /* blue-600 */
  --d-blue-deep: #3342cb;   /* blue-700 */
  --d-blue-ring: #6f7cec;   /* blue-400: today's outline */
  --d-green: #1fb273;
  --d-green-ink: #0e6b44;
  --d-amber-fill: #fff4ea;
  --d-amber-rule: #fccfa6;
  --d-amber-ink: #8a4208;
  /*
    The weekend band and the holiday numeral: the app's own --color-sky-band
    and --color-holiday-ink (app/globals.css), copied rather than imported
    for the reason every other --d-* token here is, and equal to the app's
    values in both schemes since the app's own review replaced its old
    hatch with exactly this. See site/README.md, "Where the drawn screens
    agree with the app".
  */
  --d-sky: #e3f0f7;
  --d-teal: #0b6b62;
  /* Two of the app's four subject slots: ELEN2013 falls in 2, ELEN3009 in 4. */
  --sub2: #7a5600;
  --sub4: #5e22a3;
  /* The roll's three pills, in components/Pill.tsx's own tones. */
  --d-mute-fill: #f3f4f8;
  --d-mute-ink: #555970;
  --d-gold-fill: #ffe9d6;
  --d-gold-ink: #8a4208;
  --d-done-fill: #ddf5e8;
  --d-done-ink: #0b5236;
  --d-red: #b42318;
}

/*
  DARK, TWO WAYS IN, AND THE APP DOES THE SAME THING.

  The system's own setting, unless the reader has asked for light on the
  switch in the bar; or an explicit choice of dark from that switch. Written
  twice because a media query and an attribute cannot share one selector, and
  the tokens under both are the same list. app.js puts data-theme on <html>
  from localStorage before anything is painted; with no script at all the
  first block is the whole behaviour and the page simply follows the system.
*/
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    /*
      The greys swap sides and become greys. The light scheme's faintly violet
      paper tint, inverted, is one navy wash over the whole page, which is what
      a template does; the app makes the same argument in globals.css.
    */
    --ink: #0b0d11;
    --ground: #121417;
    --paper: #1a1d23;
    --rule: #2c3038;
    --rule-firm: #3d424c;

    --text: #f1f2f5;
    --muted: #b3b9c4;

    --strike: rgb(179 185 196 / 0.8);
    --strike-off: rgb(179 185 196 / 0);
    --screen-edge: rgb(255 255 255 / 0.16);
    --screen-shade: rgb(0 0 0 / 0.55);

    --stamp: #9aa3f0;
    --leaf: #7fdcaf;

    --edge: #7b8192;

    /* The drawn screens, in the app's own dark values. */
    --d-tint: #1c2550;
    --d-tint-ring: #39457f;
    --d-tint-ink: #aab2f5;
    --d-ground: #22252c;
    --d-ground-2: #2c3038;
    --d-blue: #4f5fe8;
    --d-blue-deep: #6f7cec;
    --d-blue-ring: #6f7cec;
    --d-green: #45c88b;
    --d-green-ink: #7fdcaf;
    --d-amber-fill: #2e1d0c;
    --d-amber-rule: #6b4413;
    --d-amber-ink: #f9b075;
    --d-sky: #203042;
    --d-teal: #4fd8c8;
    --sub2: #d99b00;
    --sub4: #a97cf0;
    --d-mute-fill: #2c3038;
    --d-mute-ink: #b3b9c4;
    --d-gold-fill: #3b2906;
    --d-gold-ink: #f9b075;
    --d-done-fill: #103227;
    --d-done-ink: #7fdcaf;
    --d-red: #f3a29a;
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;

    /*
      The greys swap sides and become greys. The light scheme's faintly violet
      paper tint, inverted, is one navy wash over the whole page, which is what
      a template does; the app makes the same argument in globals.css.
    */
    --ink: #0b0d11;
    --ground: #121417;
    --paper: #1a1d23;
    --rule: #2c3038;
    --rule-firm: #3d424c;

    --text: #f1f2f5;
    --muted: #b3b9c4;

    --strike: rgb(179 185 196 / 0.8);
    --strike-off: rgb(179 185 196 / 0);
    --screen-edge: rgb(255 255 255 / 0.16);
    --screen-shade: rgb(0 0 0 / 0.55);

    --stamp: #9aa3f0;
    --leaf: #7fdcaf;

    --edge: #7b8192;

    /* The drawn screens, in the app's own dark values. */
    --d-tint: #1c2550;
    --d-tint-ring: #39457f;
    --d-tint-ink: #aab2f5;
    --d-ground: #22252c;
    --d-ground-2: #2c3038;
    --d-blue: #4f5fe8;
    --d-blue-deep: #6f7cec;
    --d-blue-ring: #6f7cec;
    --d-green: #45c88b;
    --d-green-ink: #7fdcaf;
    --d-amber-fill: #2e1d0c;
    --d-amber-rule: #6b4413;
    --d-amber-ink: #f9b075;
    --d-sky: #203042;
    --d-teal: #4fd8c8;
    --sub2: #d99b00;
    --sub4: #a97cf0;
    --d-mute-fill: #2c3038;
    --d-mute-ink: #b3b9c4;
    --d-gold-fill: #3b2906;
    --d-gold-ink: #f9b075;
    --d-done-fill: #103227;
    --d-done-ink: #7fdcaf;
    --d-red: #f3a29a;
}

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

html {
  scrollbar-gutter: stable;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--bar-h) + 1rem);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  font-family: "Plex Sans", ui-sans-serif, system-ui, sans-serif;
  font-size: 1.0625rem;
  line-height: 1.6;
  /*
    `clip`, not `hidden`. overflow:hidden makes this element a scroll
    container, and a scroll container between the story's sticky sheet and
    the viewport is what stops the sheet sticking at all: it travelled off
    the top of the section and the left column sat empty for two screens.
    overflow:clip crops the same way and creates no scrollport.
  */
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
}

img {
  max-width: 100%;
  height: auto;
  display: block;
}

a {
  color: var(--stamp);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

h1,
h2 {
  font-family: "Bricolage", "Plex Sans", ui-sans-serif, sans-serif;
  letter-spacing: -0.03em;
  font-weight: 700;
  margin: 0;
}

/*
  A third heading is a heading inside a list, and it is set as the first line
  of its own paragraph rather than as a smaller display size. The list's rule
  and its number are what separate one from the next.
*/
h3 {
  font-family: "Plex Sans", ui-sans-serif, sans-serif;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0;
  margin: 0;
}

p {
  margin: 0;
}

/* Figures are data: mono, tabular, so a column of them reads down. */
.figure {
  font-family: "Plex Mono", ui-monospace, SFMono-Regular, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/*
  The one label style on the page. Small, mono, spaced, ranged left, above
  the thing it names. It is the only place capitals are used.
*/
.label {
  font-family: "Plex Mono", ui-monospace, monospace;
  font-size: 0.8125rem;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}

:focus-visible {
  outline: 2px solid var(--accent-solid);
  outline-offset: 3px;
  border-radius: 2px;
}

.ink :focus-visible,
.hero :focus-visible,
.story :focus-visible,
.topbar :focus-visible {
  outline-color: #fff;
}

::selection {
  background: var(--accent-solid);
  color: #fff;
}

.skip {
  position: fixed;
  left: -9999px;
  top: 0;
  z-index: 60;
  background: var(--paper);
  color: var(--stamp);
  font-weight: 600;
  padding: 0.75rem 1.25rem;
  border: 1px solid var(--rule-firm);
  border-radius: var(--radius);
}
.skip:focus {
  left: 1rem;
  top: 1rem;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ------------------------------------------------------------------ layout */
.inner {
  max-width: 72rem;
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 4vw, 3.5rem);
}

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

/* ------------------------------------------------------------------ topbar */
/*
  Solid, not glass. The hero it starts on is the same ink, so at the top of
  the page there is no bar to see at all; once paper has scrolled under it,
  it is a dark band with a hairline at its foot. A blurred pane would be one
  more surface to look at and would say nothing the solid one does not.
*/
.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--ink);
  border-bottom: 1px solid transparent;
  padding-block: 0.9rem 1.2rem;
}

.topbar-in {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  color: var(--on-ink);
  text-decoration: none;
  font-family: "Bricolage", sans-serif;
  font-weight: 700;
  font-size: 1.1875rem;
  letter-spacing: -0.03em;
  white-space: nowrap;
  min-height: 2.75rem;
  align-items: center;
}
.wordmark svg {
  width: 1.5rem;
  height: 1.5rem;
  flex: none;
}
/*
  Beta, set as an exponent of the name, which is what components/Wordmark.tsx
  does: raised against the cap height, pulled back in under the row's gap, and
  no pill. A chip here was a second object on a bar that is already carrying a
  nav and a button.
*/
.beta {
  align-self: flex-start;
  margin-left: -0.35rem;
  padding-top: 2px;
  font-family: "Plex Sans", sans-serif;
  font-size: 0.5625rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  line-height: 1;
  color: rgb(255 255 255 / 0.7);
}

.topnav {
  display: flex;
  align-items: center;
  gap: 1.75rem;
  font-size: 0.9375rem;
}
.topnav a {
  color: var(--on-ink-muted);
  text-decoration: none;
}
.topnav a:hover {
  color: var(--on-ink);
}
/*
  The fill's label, said again. `.topnav a` is two classes deep and beats
  `.btn-primary`, so without this the one button on the bar is white at 74%
  on blue, which measures 4.23:1.
*/
.topnav .btn {
  min-height: 2.75rem;
  padding: 0.45rem 1rem;
  font-size: 0.9375rem;
  color: #fff;
}
@media (max-width: 54rem) {
  .topnav .nav-link {
    display: none;
  }
}
/*
  A phone: the wordmark, two 44px icon buttons and Sign in share about 360px,
  and at the desktop gap the label broke onto two lines inside its button.
  Tighter gaps and padding keep it on one line; nowrap is the promise.
*/
.topnav .btn {
  white-space: nowrap;
}
@media (max-width: 30rem) {
  .topnav {
    gap: 0.5rem;
  }
  .topnav .btn {
    padding: 0.45rem 0.8rem;
  }
}

/* ----------------------------------------------------------------- buttons */
/*
  A rectangle with a small cut on the corner, a fill or a hairline, and a
  label. No shadow under it, no glow behind it, no lift on hover: the colour
  is the whole signal, and a button that moves when a pointer nears it is
  decoration on a page about a form.
*/
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font: inherit;
  font-weight: 600;
  padding: 0.7rem 1.5rem;
  border-radius: var(--radius);
  text-decoration: none;
  border: 1px solid transparent;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.btn-primary,
.btn-paper {
  background: var(--accent-solid);
  color: #fff;
}
.btn-primary:hover,
.btn-paper:hover {
  background: var(--accent-solid-hover);
}
.btn-ghost {
  border-color: var(--on-ink-edge);
  color: var(--on-ink);
}
.btn-ghost:hover {
  background: rgb(255 255 255 / 0.1);
  border-color: var(--on-ink);
}
.btn-outline {
  border-color: var(--edge);
  color: var(--text);
}
.btn-outline:hover {
  border-color: var(--text);
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/* -------------------------------------------------------------------- hero */
/*
  A slab of ink, flat. The headline is ranged left against the same edge
  every heading below it uses, the drawn log sheet sits in the narrower
  column beside it, and the meter runs the full width under both.
*/
.hero {
  position: relative;
  background: var(--ink);
  color: var(--on-ink);
  overflow: clip;
  padding-bottom: clamp(2rem, 4vw, 3rem);
  margin-top: calc(var(--bar-h) * -1);
  padding-top: var(--bar-h);
}

.hero-grid {
  display: grid;
  gap: clamp(2.5rem, 5vw, 4.5rem);
  align-items: end;
  padding-block: clamp(2.75rem, 6vw, 4.5rem) clamp(2rem, 3.5vw, 3rem);
}
@media (min-width: 62rem) {
  /*
    Uneven on purpose. The words are the larger half and the drawing is the
    smaller one, and neither is a centred column.
  */
  .hero-grid {
    grid-template-columns: minmax(0, 1fr) 20rem;
  }
}

.hero h1 {
  font-size: clamp(2.5rem, 7.4vw, 4.75rem);
  font-weight: 800;
  line-height: 0.94;
  letter-spacing: -0.045em;
  margin-bottom: 1.75rem;
}
/*
  Three lines, each its own block, so the break is the writer's and not the
  container's, and so each can rise out of its own clipped band on load.
*/
.hero h1 .ln {
  display: block;
  overflow: hidden;
  padding-bottom: 0.06em;
}
.hero h1 .ln > span {
  display: block;
}

.hero-lede {
  color: var(--on-ink-muted);
  max-width: 44ch;
  margin-bottom: 1.75rem;
}

/*
  There is no line under the buttons any more. It read "Run by a postgrad at
  Wits. In beta.", and the strip at the foot of this same band now says who
  runs it and what state it is in, at more length and in one place.
*/

/* --------------------------------------------------- the drawn log sheet */
/*
  A faithful drawing of the log sheet rather than a screenshot of it, because
  it has to move: the times are typed the way a student types them and the
  app's own echo resolves underneath.

  It is drawn as what it is, a piece of the form on white paper, and NOT as a
  phone in a bezel. A rendered handset with a glow under it is the single
  most recognisable ornament on a generated product page, and the app's own
  screenshots further down are real phones anyway.
*/
.slip {
  background: #fff;
  color: #14162b;
  border-radius: 1rem;
  padding: 1rem;
  max-width: 24rem;
  /*
    The height of the closed form, and no taller. What the open month pushes
    below that line is clipped, the way a phone's screen clips it, so the
    page under the card holds still while the month opens and closes. Set in
    rem because everything inside is: the card is the same height at every
    width, and the same in every scheme.
  */
  height: var(--slip-h, 28rem);
  overflow: hidden;
  /*
    The card is the app's light form on the hero's ink in both schemes, so
    every token the month inside it reads is pinned to its light value here.
    Without this the month would go dark inside a white card when the page
    does.
  */
  --paper: #ffffff;
  --rule: #dcdee7;
  --muted: #555970;
  --text: #14162b;
  --d-sky: #e3f0f7;
  --d-tint: #e4e7fc;
  --d-tint-ink: #3342cb;
  --d-tint-ring: #c5caf7;
  --d-teal: #0b6b62;
  --d-blue-ring: #6f7cec;
  --d-blue: #3b4ce1;
  --sub2: #7a5600;
  --sub4: #5e22a3;
}
.ph-title {
  font-family: "Bricolage", sans-serif;
  font-weight: 700;
  font-size: 1.0625rem;
  letter-spacing: -0.02em;
}
.ph-sub {
  font-size: 0.8125rem;
  color: #555970;
  margin-top: 0.15rem;
}
.ph-edit {
  font-size: 0.75rem;
  font-weight: 500;
  color: #3342cb;
  margin-top: 0.2rem;
}
/*
  The panel the form sits in: the app's `rounded-xl bg-blue-50/60 p-4 ring-1
  ring-blue-100`, with `space-y-3` between its groups and `mb-1` between a
  label and its field. Those four numbers are the whole of the app's rhythm
  and they are written here once, as the app writes them.
*/
.ph-form {
  margin-top: 0.9rem;
  background: #f5f9ff;
  box-shadow: inset 0 0 0 1px #dbeafe;
  border-radius: 0.75rem;
  padding: 1rem;
  display: grid;
  gap: 0.75rem;
}
.ph-label {
  display: block;
  font-size: 0.875rem;
  font-weight: 500;
  color: #334155;
  margin-bottom: 0.25rem;
}
.ph-field {
  border: 1px solid #64748b;
  border-radius: 0.5rem;
  padding: 0.5rem 0.75rem;
  font-size: 0.875rem;
  line-height: 1.25rem;
  background: #fff;
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
  min-height: 2.375rem;
  white-space: nowrap;
  overflow: hidden;
}
/* The link under the date field, in the app's own words, both of them. */
.ph-link {
  position: relative;
  display: grid;
  width: max-content;
  margin-top: 0.25rem;
  font-size: 0.75rem;
  font-weight: 500;
  color: #1d4ed8;
  text-decoration: underline;
}
.pk-a,
.pk-b {
  grid-area: 1 / 1;
}
.pk-b {
  opacity: 0;
}
/* The press on the link: a ring that lands and lifts. */
.ph-tap {
  position: absolute;
  inset: -4px -6px;
  border-radius: 6px;
  box-shadow: 0 0 0 2px #3b4ce1;
  opacity: 0;
  pointer-events: none;
}
/*
  The month's slot: a grid row that is nothing until the sequence opens it.
  Animating the row rather than a height means the month is never measured,
  and the fields below simply move when it does.
*/
.ph-calslot {
  display: grid;
  grid-template-rows: 0fr;
  overflow: hidden;
}
.ph-calin {
  min-height: 0;
}
.ph-calslot .cal {
  margin-top: 0.5rem;
  padding: 0.75rem;
  border-radius: 0.75rem;
  border-color: #e2e8f0;
}
/* The app's cells are taller than the drawing's, and the dots sit clear of the foot. */
.ph-calslot .cal-d,
.ph-calslot .cal-pad {
  aspect-ratio: 1 / 1;
  padding-bottom: 3px;
}
.ph-calslot .cal-dots {
  margin-top: 3px;
}
/* Friday the 18th, where the hero's tap lands. */
.ph-calslot .cal-tap {
  grid-row: 4 / 5;
  grid-column: 5 / 6;
}
.ph-times {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.75rem;
}
/*
  The duty field and the list under it. The list is the browser's own
  datalist, drawn the plain way every browser draws one: a white sheet under
  the field, a hairline, one row per option. The two lab rows stay when an L
  has been typed; the rest go. It sits over the time boxes rather than
  pushing them, which is what a datalist does.
*/
.ph-duty-g {
  position: relative;
  z-index: 2;
}
.ph-duty {
  position: relative;
}
.ph-duty-raw {
  position: absolute;
  left: 0.75rem;
  top: 0.5rem;
  opacity: 0;
}
.ph-list {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 0.25rem);
  margin: 0;
  padding: 0.25rem 0;
  list-style: none;
  background: #fff;
  border: 1px solid #cbd5e1;
  border-radius: 0.5rem;
  box-shadow: 0 10px 28px rgb(20 22 43 / 0.18);
  opacity: 0;
  transform: translateY(-4px);
}
.ph-list li {
  position: relative;
  padding: 0.4rem 0.75rem;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #14162b;
}
/* The row about to be tapped takes the browser's hover tint as the ring lands. */
.ph-opt-pick {
  background: transparent;
}
.ph-tap-opt {
  inset: 2px 4px;
}

/* Two buttons at the app's `h-11 rounded-lg`, Save taking the room. */
.ph-actions {
  display: flex;
  gap: 0.75rem;
}
.ph-save,
.ph-cancel {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 2.75rem;
  border-radius: 0.5rem;
  font-size: 0.875rem;
  font-weight: 600;
}
.ph-save {
  background: #3b4ce1;
  color: #fff;
  flex: 1;
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
}
.ph-cancel {
  border: 1px solid #64748b;
  color: #334155;
  background: #fff;
  padding: 0 1rem;
}

/* -------------------------------------------------------- the block meter */
/*
  The product's signature, at page scale. One cell per hour the block asks
  for, exactly as components/ui/BlockMeter.tsx draws it: blue as they are
  logged, an outline on the hour that is next, and the whole set green once
  the requirement is met. It is the only blue of any size on the page and the
  only shape on it with a soft corner.
*/
.meter-band {
  padding-top: clamp(1.75rem, 3vw, 2.25rem);
  padding-bottom: 0.25rem;
  border-top: 1px solid var(--on-ink-rule);
}

.meter-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.35rem 1.5rem;
  margin-bottom: 1rem;
}
.meter-head .label {
  color: var(--on-ink-muted);
}
.meter-head .meter-count {
  font-family: "Plex Mono", monospace;
  font-size: 0.8125rem;
  color: var(--on-ink-muted);
}

.meter {
  display: grid;
  grid-template-columns: repeat(21, minmax(0, 1fr));
  gap: clamp(0.2rem, 0.75vw, 0.4rem);
}
@media (min-width: 62rem) {
  /* One row of forty-two, so a cell is taller than it is wide, as in the app. */
  .meter {
    grid-template-columns: repeat(42, minmax(0, 1fr));
  }
}
.cell {
  display: block;
  height: clamp(1.125rem, 3.4vw, 2.5rem);
  border-radius: 4px;
  background: var(--cell-empty);
}
.cell.logged {
  background: var(--cell-logged);
}
/*
  MET, WHICH IS WHERE THE STORY ENDS AND SO IS WHERE THE MARKUP STANDS.

  The app turns the whole set green the moment a block's requirement is met
  (components/ui/BlockMeter.tsx), and that is the state this is drawn in:
  forty-two of forty-two, green, with the sheet stamped beside it. The
  sequence takes it back to twenty-four and plays it forward again.

  `fresh`, `next` and `tail` name the three groups the sequence moves and say
  nothing about how the meter is drawn: with no motion they are forty-two
  cells of one colour.
*/
.met .cell {
  background: var(--cell-met);
}
.met .meter-count {
  color: var(--cell-met);
}

/*
  The figure the meter is counting, with the meter's own state drawn under it
  as a rule. It is blue, not green: twenty-five of forty-two hours is a block
  in progress, and green in this palette means the requirement is met.
  Drawn finished, and counted up by the load sequence where there is one.
*/
.tally {
  position: relative;
  font-weight: 500;
}
.tally::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.3em;
  height: 2px;
  border-radius: 2px;
  background: var(--cell-met);
  transform-origin: left;
}

/*
  The note and the receipt, on one line where there is room for one. The
  receipt is the other end of the hero's story — the sheet those hours go
  onto, with the lecturer's approval stamped across it — so it sits at the
  right-hand end of the band rather than in the reading column.
*/
.meter-foot {
  display: grid;
  gap: 1.25rem;
  align-items: center;
  margin-top: 1.25rem;
}
@media (min-width: 52rem) {
  .meter-foot {
    grid-template-columns: minmax(0, 1fr) auto;
  }
}
.meter-note {
  font-size: 0.8125rem;
  color: var(--on-ink-muted);
  max-width: 68ch;
}

/*
  A slip of paper on the ink, and the one place the stamp lands in the hero.
  The stamp carries its own paper fill: it is pressed onto the slip, and a
  stamp with a transparent middle over anything but plain paper is two
  colours and no words. See the same fix on the lecturer's screen below.
*/
.receipt {
  position: relative;
  display: grid;
  gap: 0.15rem;
  background: #fff;
  color: #14162b;
  border-radius: var(--radius);
  padding: 0.7rem 7.5rem 0.75rem 0.85rem;
  min-width: 0;
}
.rc-name {
  font-weight: 600;
  font-size: 0.875rem;
}
.rc-meta {
  font-size: 0.75rem;
  color: #555970;
}
.rc-stamp {
  position: absolute;
  right: 0.7rem;
  top: 50%;
  /*
    Centred with a margin rather than with translateY, so `transform` is left
    saying one thing only and the press below can animate it without having to
    repeat where the stamp sits.
  */
  margin-top: -1.15rem;
  transform: rotate(-7deg);
  background: #fff;
  border: 2px solid #3342cb;
  color: #3342cb;
  border-radius: var(--radius);
  padding: 0.22rem 0.5rem 0.26rem;
  text-align: center;
}
.rc-stamp b {
  display: block;
  font-family: "Bricolage", sans-serif;
  font-weight: 800;
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  line-height: 1;
}
.rc-stamp span {
  display: block;
  font-size: 0.5rem;
  margin-top: 0.1rem;
}
/* Narrow: the stamp comes off the slip and sits under it, still on the turn. */
@media (max-width: 34rem) {
  .receipt {
    padding-right: 0.85rem;
  }
  .rc-stamp {
    position: static;
    transform: rotate(-7deg);
    justify-self: start;
    margin-top: 0.5rem;
  }
}

/* ---------------------------------------------------------------- sections */
/*
  The page's grid: a narrow column carrying a number and a heading, a wide
  one carrying everything a person reads, and a hairline across the top of
  each. The heading holds its place while its section passes.
*/
main {
  background: var(--ground);
}
/* The skip link lands here; it must not draw a ring around the whole page. */
main:focus {
  outline: none;
}

.section {
  padding-block: clamp(3rem, 6vw, 4.5rem);
  border-top: 1px solid var(--rule);
}

/*
  The grid itself, named once and worn by every section and by the story's
  inner wrapper. Above 64rem the rail is real; below it the two columns
  stack and the rail becomes the line of heading above the block.
*/
.rail {
  display: grid;
  gap: 1.5rem;
}
/* The rule belongs between two sections on paper, not against a band of ink. */
.hero + .section,
.story + .section {
  border-top: none;
}
@media (min-width: 64rem) {
  .rail {
    grid-template-columns: var(--rail) minmax(0, 1fr);
    gap: 0 var(--gutter);
  }
  .rail > .head {
    position: sticky;
    top: calc(var(--bar-h) + 2rem);
  }
}

.head {
  align-self: start;
}
.head .label {
  display: block;
  margin-bottom: 0.75rem;
}
.section h2,
.story h2 {
  font-size: clamp(1.5rem, 2.6vw, 1.9375rem);
  line-height: 1.08;
  max-width: 14ch;
}
.section > .body {
  display: grid;
  gap: 2rem;
  align-content: start;
}

.lede {
  max-width: 48ch;
}
.prose {
  max-width: var(--measure);
  color: var(--muted);
}
.prose + .prose {
  margin-top: -1.25rem;
}
/* The line the four cautions add up to, set as type rather than as a card. */
.care-close {
  color: var(--text);
  max-width: 52ch;
}

/* -------------------------------------------------- before / with Timeslip */
.swap,
.swap-head {
  margin-inline: -0.75rem;
  padding-inline: 0.75rem;
}
/* The line that says which hours this is about, clear of the column heads. */
.swap-lede {
  margin-bottom: 1.5rem;
}
.swap {
  display: grid;
  gap: 0;
  border-top: 1px solid var(--rule);
  margin-block: 0;
}
.swap > div {
  display: grid;
  gap: 0.375rem 2.5rem;
  padding-block: 1.25rem;
  border-bottom: 1px solid var(--rule);
}
@media (min-width: 46rem) {
  .swap > div {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
}
/*
  THE TWO SIDES ARE NOT SET THE SAME, BECAUSE THEY ARE NOT SAYING THE SAME
  THING.

  They were: two columns of body type, the same size and the same ink, and
  the reader's note on the pair was that the right-hand side needs to land.
  So the left is what it is — a difficulty, in grey, at reading weight, with
  a rule drawn through it as the row arrives — and the right is the answer,
  in the display face at the next size up and in the one blue this page uses
  for the things it wants pressed. The eye crosses from a crossed-out problem
  to a blue replacement, which is the whole content of this section.
*/
.swap dt {
  color: var(--muted);
  margin: 0;
  font-weight: 400;
  justify-self: start;
}
/*
  THE LINE THROUGH THE OLD WAY, AND IT IS DRAWN ACROSS THE WORDS.

  It is a rule inside the phrase's own span rather than `text-decoration`,
  because a decoration cannot be drawn: it can only appear. The span shrinks
  to the words, so the line stops where they do, and the row's divider has
  moved to the answer beside it so that shrinking costs the column nothing.

  Below 46rem the two halves of a row stack and a phrase can take two lines,
  which one rule cannot cross. There it is a decoration again, arriving as a
  cut on the same clock. Struck, with the answer beside it, is the default:
  with no script and with reduced motion asked for that is what the page is,
  because this is a section about what has changed already.
*/
.swap dt > span {
  position: relative;
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--strike);
}
@media (min-width: 46rem) {
  .swap dt > span {
    display: inline-block;
    text-decoration: none;
  }
  .swap dt > span::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 0.72em;
    height: 1px;
    background: var(--strike);
    transform-origin: left center;
  }
}
.swap dd {
  margin: 0;
  font-family: "Bricolage", "Plex Sans", ui-sans-serif, sans-serif;
  font-size: 1.1875rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.25;
  color: var(--stamp);
}
.swap-head {
  display: none;
}
@media (min-width: 46rem) {
  .swap-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 2.5rem;
    padding-bottom: 0.6rem;
  }
  /*
    A hairline down the middle, so the two columns read as the two halves of
    one exchange rather than as two lists that happen to be side by side.
  */
  .swap > div > dd {
    border-left: 1px solid var(--rule);
    padding-left: 2.5rem;
    margin-left: -1.25rem;
  }
}


/* ===================================================== the drawn screens ==
  There is not a screenshot on this page any more.

  Every screen below is the app's own markup, rebuilt by hand out of the same
  tokens, so it turns with the colour scheme, stays sharp at any size, weighs
  nothing, and can be animated at the one moment that matters in it. The
  sources are named beside each: the calendar is components/SessionCalendar,
  the queue is components/SessionRhythm inside app/lecturer/ApprovalDashboard,
  the roll is app/admin/timesheets/BlockRollTable with its AttentionPanel.

  Each one is a picture made of text, so it carries role="img" and a label
  that says what it shows, and site/tools/words.py does not count what is
  inside it. Every word in one is the app's own field label.

  THE FINISHED STATE IS WHAT IS WRITTEN HERE. The motion block at the foot of
  this file takes each screen back to its beginning and plays it forward
  against the scroll; with no motion, no script, or reduced motion asked for,
  what a reader gets is the screen at the end of its own story.
*/

/*
  A screen sits in a plain hairline frame at its own proportions, and it is a
  container: everything inside sizes against the frame rather than the
  viewport, so the same drawing works in the body column, in a phone's single
  column, and inside a bento tile.
*/
.frame {
  border: 1px solid var(--rule-firm);
  border-radius: var(--radius);
  background: var(--paper);
  overflow: clip;
  container-type: inline-size;
}
.frame-narrow {
  max-width: 24rem;
}

.demo {
  padding: 0.85rem 0.9rem 1rem;
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--text);
}

/* The app's field label, its input and its text link, at drawing scale. */
.dm-label {
  display: block;
  font-family: "Plex Mono", monospace;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin-bottom: 0.25rem;
}
.dm-field {
  border: 1px solid var(--edge);
  border-radius: 2px;
  padding: 0.4rem 0.55rem;
  min-height: 2rem;
  background: var(--paper);
  font-size: 0.875rem;
}
.dm-link {
  margin-top: 0.4rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--stamp);
}

/* ------------------------------------------------- one: the month as days */
/*
  components/SessionCalendar.tsx. The grid under the date field for the
  student who remembers they demonstrated last Tuesday and not what date that
  was. Weekends and public holidays are shaded and still tappable, which is
  the rule this drawing exists to show: the tap in it lands on a Saturday.
*/
.cal {
  margin-top: 0.5rem;
  border: 1px solid var(--rule);
  border-radius: 0.6rem;
  padding: 0.6rem;
  background: var(--paper);
}
.cal-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.4rem;
}
.cal-month {
  font-size: 0.8125rem;
  font-weight: 600;
}
.cal-arrow {
  color: var(--muted);
  font-size: 1rem;
  line-height: 1;
  padding: 0 0.3rem;
}
.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 3px;
  text-align: center;
  position: relative;
}
.cal-wd {
  font-size: 0.6875rem;
  font-weight: 500;
  color: var(--muted);
  padding-bottom: 2px;
}
.cal-d,
.cal-pad {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1 / 0.88;
  border-radius: 6px;
  font-size: 0.75rem;
}
/* The weekend band. A day the university is shut, and tappable anyway. */
.cal-d.we {
  background: var(--d-sky);
}
/* A day this subject was already logged on. */
.cal-d.has {
  background: var(--d-tint);
  color: var(--d-tint-ink);
  font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--d-tint-ring);
}
/* A public holiday: the numeral in teal, with a diamond over it. */
.cal-d.hol {
  color: var(--d-teal);
  font-weight: 600;
}
.cal-dia {
  position: absolute;
  top: 3px;
  width: 4px;
  height: 4px;
  background: currentColor;
  transform: rotate(45deg);
}
/* Today, ringed. */
.cal-d.tod {
  box-shadow: inset 0 0 0 1.5px var(--d-blue-ring);
}
.cal-d.tod.has {
  box-shadow: inset 0 0 0 1.5px var(--d-blue-ring);
}
/*
  The day the form is holding. Written last, so it wins over all of them.

  --d-blue rather than --d-blue-deep: deep is the darker of the two on paper
  and the LIGHTER of the two in the dark scheme, where white on it measured
  3.64:1. This one carries white in both.
*/
.cal-d.pick {
  background: var(--d-blue);
  color: #fff;
  font-weight: 600;
  box-shadow: none;
}
.cal-dots {
  display: flex;
  gap: 2px;
  margin-top: 2px;
  height: 4px;
}
.sd {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
}
.sd.s2 {
  background: var(--sub2);
}
.sd.s4 {
  background: var(--sub4);
}
.cal-d.pick .sd {
  background: #fff;
}
/*
  The tap. Placed on the cell it lands on by grid position rather than by a
  measurement, so it cannot drift when the grid resizes: row four, column six,
  which is Saturday the 19th.
*/
.cal-tap {
  /*
    Absolutely positioned INSIDE its grid area, which is row four, column six:
    Saturday the 19th. Placed in the flow it would occupy that cell and push
    every day after it along by one, which is exactly what it did, and the
    calendar quietly moved the 19th to a Sunday.
  */
  position: absolute;
  /*
    Both lines of each pair, not just the start. An absolutely
    positioned grid child resolves an `auto` end line to the container's
    padding edge rather than to a span of one, so `grid-row: 4` alone
    drew the tap from Saturday to the bottom right corner of the month.
  */
  grid-row: 4 / 5;
  grid-column: 6 / 7;
  inset: 0;
  border-radius: 8px;
  box-shadow: 0 0 0 2px var(--d-blue);
  opacity: 0;
  pointer-events: none;
}
.cal-key {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.75rem;
  margin: 0.5rem 0 0;
  padding: 0;
  list-style: none;
  font-size: 0.6875rem;
  color: var(--muted);
}
.cal-key li {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}
.cal-hol {
  margin-top: 0.3rem;
  font-size: 0.6875rem;
  color: var(--muted);
}
/* The holiday's mark beside its name, the way each course has its dot. */
.cal-key-dia {
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-right: 0.45rem;
  vertical-align: 1px;
  background: var(--d-teal);
  transform: rotate(45deg);
}
.cal-note {
  margin-top: 0.3rem;
  font-size: 0.6875rem;
  color: var(--muted);
  line-height: 1.4;
}
@container (max-width: 19rem) {
  .cal-key {
    display: none;
  }
}

/* ------------------------------------------- two: the lecturer's one list */
/*
  app/lecturer/ApprovalDashboard.tsx, with components/SessionRhythm.tsx inside
  it. A sheet opens, its sessions read as weeks so the Tuesday lab is a column
  rather than four dates, what is worth checking is said before the button is
  offered, and the button is held for the five seconds components/
  useReviewHold.ts holds it for.
*/
.demo-q {
  padding: 0;
}
.q-row {
  padding: 0.8rem 0.9rem;
}
.q-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
}
.q-name {
  font-weight: 600;
}
.q-no {
  font-weight: 400;
  color: var(--muted);
}
.q-act {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--stamp);
  white-space: nowrap;
}
.q-sub {
  margin-top: 0.15rem;
  color: var(--muted);
}
.q-meta {
  color: var(--muted);
}
.q-meta b {
  color: var(--text);
  font-weight: 600;
}
/*
  The panel the row opens into. It is clipped from the foot rather than
  measured, so opening it costs no layout and the finished state is simply
  the panel, open.
*/
.q-panel {
  position: relative;
  border-top: 1px solid var(--rule);
  padding: 0.8rem 0.9rem 1rem;
  background: var(--ground);
}
.q-cap {
  font-size: 0.6875rem;
  font-weight: 500;
  color: var(--muted);
  margin-bottom: 0.3rem;
}
.rhythm {
  width: 100%;
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: 2px;
  text-align: center;
}
.rhythm th {
  font-size: 0.625rem;
  font-weight: 500;
  color: var(--muted);
  padding-bottom: 1px;
}
.rh {
  border-radius: 4px;
  background: var(--d-ground);
  padding: 3px 1px;
  vertical-align: top;
}
.rh.we {
  background: var(--d-ground-2);
}
.rh-d {
  display: block;
  font-size: 0.625rem;
  line-height: 1.2;
  color: var(--muted);
  font-family: "Plex Mono", monospace;
}
.rh-h {
  display: block;
  font-size: 0.6875rem;
  font-weight: 600;
  line-height: 1.2;
}
/* A day with hours on it: the app's blue, and the label white on top of it. */
.rh.on {
  background: var(--d-blue);
  color: #fff;
}
/*
  Both labels take the cell's own colour, so one animation moves the fill and
  the ink together and they cannot end up white on slate half way through.
*/
.rh.on .rh-d,
.rh.on .rh-h {
  color: inherit;
}
.q-rhnote {
  font-size: 0.625rem;
  color: var(--muted);
  margin-top: 0.2rem;
}
.q-tick {
  display: flex;
  gap: 0.45rem;
  align-items: flex-start;
  margin-top: 0.7rem;
  border-radius: 0.5rem;
  background: var(--d-ground);
  padding: 0.5rem 0.6rem;
  font-size: 0.75rem;
  color: var(--muted);
}
.q-tick b {
  color: var(--text);
}
.tickbox {
  flex: none;
  width: 0.85rem;
  height: 0.85rem;
  margin-top: 0.15rem;
  border-radius: 3px;
  background: var(--d-blue);
  /* The mark is drawn in currentColor so that unticked can hide it. */
  color: #fff;
  position: relative;
}
.tickbox::after {
  content: "";
  position: absolute;
  left: 0.26rem;
  top: 0.1rem;
  width: 0.2rem;
  height: 0.4rem;
  border: solid currentColor;
  border-width: 0 2px 2px 0;
  transform: rotate(42deg);
}
/*
  ONE BUTTON, THREE READINGS, AND THE STAMP IS NOT ON ANY OF THEM.

  Held, it is the app's grey with the seconds counting in it; offered, it is
  the app's blue; pressed, there is no button at all and the row reads the way
  ApprovalDashboard draws a sheet it has already approved, green on its own
  pale fill. Each pair cuts rather than fades, for the reason given over the
  office's pills: a scrub parked half way through a fade is two sets of
  letters at once.

  The stamp used to be pressed into the bottom-right corner of the panel,
  which put APPROVED in blue outline on top of a solid blue button: the two
  most saturated things on the page, one on the other, and the word
  unreadable for the whole of the last beat. It lands on the paper under the
  button now, on a fill of its own, after the button has gone.
*/
.q-button {
  position: relative;
  display: grid;
  margin-top: 0.7rem;
}
.q-button > span {
  grid-area: 1 / 1;
  text-align: center;
  border-radius: 0.5rem;
  padding: 0.55rem;
  font-size: 0.8125rem;
  font-weight: 600;
}
.q-held {
  background: var(--d-ground-2);
  color: var(--muted);
  opacity: 0;
}
.q-go {
  background: var(--d-blue);
  color: #fff;
  opacity: 0;
}
.q-done {
  background: var(--d-done-fill);
  color: var(--d-done-ink);
}
.q-done .figure {
  font-weight: 400;
}
/*
  The approved row is ranged left and keeps its right-hand end clear, because
  that is where the stamp lands: on the row's own pale paper, on a fill of its
  own, covering nothing anybody had to read. Nothing is reserved underneath
  for it — an empty strip held open for a stamp that has not landed yet is a
  hole in the panel for four fifths of the screen's story.
*/
.q-button > .q-done {
  text-align: left;
  padding-right: 6.5rem;
}
.q-end {
  position: relative;
}
.q-stamp {
  position: absolute;
  right: 0.2rem;
  top: 50%;
  margin-top: -1.15rem;
  transform: rotate(-8deg);
  background: var(--paper);
  border: 2.5px solid var(--stamp);
  color: var(--stamp);
  border-radius: var(--radius);
  padding: 0.3rem 0.7rem 0.35rem;
  text-align: center;
}
.q-stamp b {
  display: block;
  font-family: "Bricolage", sans-serif;
  font-weight: 800;
  font-size: 0.9375rem;
  letter-spacing: 0.08em;
  line-height: 1;
}
.q-stamp span {
  display: block;
  font-size: 0.5625rem;
  margin-top: 0.15rem;
}
@container (max-width: 26rem) {
  .rh-d {
    font-size: 0.5625rem;
  }
  .q-top {
    flex-wrap: wrap;
    gap: 0.1rem 0.75rem;
  }
  /*
    Narrow: the approved row is one line of small type and there is no room
    beside it, so the stamp comes off the row and sits under it, ranged right,
    still on the turn.
  */
  .q-button > .q-done {
    padding-right: 0.55rem;
  }
  .q-stamp {
    position: static;
    display: block;
    width: max-content;
    margin: 0.5rem 0.2rem 0 auto;
  }
}

/* --------------------------------------------- three: the office's block */
/*
  app/admin/timesheets: the four figures at the top of the block page, the
  attention panel's one line under them, and BlockRollTable's roster. Watch
  one block finish: the sheets come in, the count reaches its denominator,
  the line clears, and the download appears.
*/
.demo-off {
  padding: 0.8rem 0.9rem 1rem;
}
.off-tiles {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: 0.5rem;
  overflow: clip;
}
.tile {
  background: var(--paper);
  padding: 0.5rem 0.6rem 0.6rem;
}
.tv {
  display: block;
  font-size: 1.125rem;
  font-weight: 500;
  line-height: 1.2;
}
.tile-d {
  display: block;
  font-size: 0.625rem;
  color: var(--muted);
  margin-top: 0.1rem;
}
.off-attn {
  position: relative;
  display: grid;
  margin-top: 0.6rem;
  font-size: 0.75rem;
}
.off-attn > span {
  grid-area: 1 / 1;
  border-radius: 0.5rem;
  padding: 0.45rem 0.6rem;
}
.at-a {
  background: var(--d-amber-fill);
  border: 1px solid var(--d-amber-rule);
  color: var(--d-amber-ink);
  font-weight: 600;
  opacity: 0;
}
.at-b {
  border: 1px solid var(--rule);
  color: var(--muted);
}
.at-b b {
  color: var(--text);
}
.roll {
  width: 100%;
  border-collapse: collapse;
  margin-top: 0.7rem;
  font-size: 0.75rem;
}
.roll th {
  text-align: left;
  font-family: "Plex Mono", monospace;
  font-weight: 400;
  font-size: 0.625rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  border-bottom: 1px solid var(--rule);
  padding: 0 0.4rem 0.3rem 0;
}
.roll td {
  border-bottom: 1px solid var(--rule);
  padding: 0.4rem 0.4rem 0.4rem 0;
  vertical-align: middle;
}
.rr-n {
  font-weight: 500;
  white-space: nowrap;
}
.rr-h {
  color: var(--muted);
  white-space: nowrap;
}
.rr-h b {
  color: var(--d-green-ink);
  font-weight: 500;
}
.bar {
  display: block;
  width: 4.5rem;
  height: 0.375rem;
  border-radius: 999px;
  background: var(--d-ground-2);
  overflow: hidden;
}
.bar i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--d-green);
  transform-origin: left center;
}
.pill-stack {
  display: grid;
  justify-items: start;
}
.pl {
  grid-area: 1 / 1;
  border-radius: 999px;
  padding: 0.1rem 0.45rem;
  font-size: 0.625rem;
  font-weight: 600;
  white-space: nowrap;
  opacity: 0;
}
/* Logging: slate, because nobody is owed anything yet. */
.p0 {
  background: var(--d-mute-fill);
  color: var(--d-mute-ink);
}
/* With lecturers: gold, which in this system means outstanding and nothing else. */
.p1 {
  background: var(--d-gold-fill);
  color: var(--d-gold-ink);
}
/* Approved: green, and it is what the drawing is left showing. */
.p2 {
  background: var(--d-done-fill);
  color: var(--d-done-ink);
  opacity: 1;
}
.off-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 0.5rem;
  font-size: 0.6875rem;
  color: var(--muted);
}
.off-dl {
  background: var(--d-blue);
  color: #fff;
  font-family: "Plex Sans", sans-serif;
  font-size: 0.75rem;
  font-weight: 600;
  border-radius: var(--radius);
  padding: 0.3rem 0.8rem;
}
@container (max-width: 26rem) {
  .off-tiles {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .bar {
    width: 2.75rem;
  }
  .roll {
    font-size: 0.6875rem;
  }
}

/* ------------------------------------------------------------ the steps */
.steps {
  display: grid;
  gap: clamp(2.5rem, 5vw, 3.5rem);
  margin: 0;
  padding: 0;
  list-style: none;
}
.step {
  display: grid;
  gap: 0.6rem;
  border-top: 1px solid var(--rule);
  padding-top: 1.25rem;
}
.step:first-child {
  border-top: none;
  padding-top: 0;
}
.step h3 {
  font-family: "Bricolage", sans-serif;
  font-weight: 700;
  font-size: 1.25rem;
  letter-spacing: -0.02em;
  line-height: 1.2;
  max-width: 26ch;
}
/*
  A CHILD, NOT A DESCENDANT, and every one of these is a child on purpose.

  A drawn screen lives inside a step and inside a bento tile, and it is full
  of paragraphs of its own: the flag panel's mango heading, the button's white
  label, the calendar's note. `.step p` is one step more specific than the
  class each of those sets its own colour with, so written as a descendant it
  quietly repainted half of every screen in the page's muted grey.
*/
.step > p {
  max-width: 62ch;
  color: var(--muted);
}
.step .frame {
  margin-top: 0.5rem;
}
/*
  A STEP IS A ROW, NOT A STACK, ONCE THERE IS ROOM FOR ONE.

  Stacked, each screen sat under its own two lines of caption and the rest of
  the column was empty to the right of them — and the month, which is the
  narrowest drawing on the page, left a third of the row white. Beside each
  other, the caption holds the left of the row and the screen takes about two
  thirds of it, and the three steps alternate so the page does not become a
  ladder. Every drawing in here sizes against its own frame rather than the
  window, so widening one is the whole change.

  The heading in the rail is untouched and still holds its place while the
  three of them pass it.
*/
@media (min-width: 64rem) {
  .step {
    grid-template-columns: minmax(0, 0.52fr) minmax(0, 1fr);
    /*
      Three rows of caption and a fourth that takes the slack, so the label,
      the heading and the line under it stay a block at the top of their
      column instead of being spread down the height of the screen beside
      them.
    */
    grid-template-rows: auto auto auto 1fr;
    column-gap: 2.5rem;
    align-items: start;
  }
  .step > .step-role,
  .step > h3,
  .step > p {
    grid-column: 1;
  }
  .step > .frame {
    grid-column: 2;
    grid-row: 1 / -1;
    margin-top: 0;
  }
  /* The middle one turns over: screen left, caption right. */
  .step:nth-child(2) {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.52fr);
  }
  .step:nth-child(2) > .step-role,
  .step:nth-child(2) > h3,
  .step:nth-child(2) > p {
    grid-column: 2;
  }
  .step:nth-child(2) > .frame {
    grid-column: 1;
  }
  /* The month is no longer the one drawing that has to stay small. */
  .frame-narrow {
    max-width: none;
  }
  /* The caption sits against the top of its screen, not floating above it. */
  .step > .step-role {
    padding-top: 0.15rem;
  }
}

/* ============================================================== the bento ==
  A CLAIM IS A TILE THAT DEMONSTRATES ITSELF.

  What was here was six headings with a sentence under each, read down a
  column, and the reader's note on it was the one everybody has about a list
  of six: the first two are read and the rest are scanned. Every tile below
  carries the thing it claims instead. The duplicate really is flagged, the
  seal really does break and mend, the trail really does write itself, and the
  meter really does fill. Linear, Vercel, Raycast and Stripe all answer list
  fatigue the same way, and it is the right answer.

  It is still not a grid of cards: the gap IS the rule, one pixel of the
  page's own hairline showing between tiles that carry no border, no shadow
  and no lift. Six tiles on three rows of a six-column grid, two columns on a
  tablet, one on a phone, and each tile is its own container so the drawing
  in it sizes against the tile and not against the window.
*/
.bento {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: clip;
}
.bt {
  background: var(--ground);
  padding: 1.4rem 1.25rem 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  container-type: inline-size;
  min-width: 0;
}
.bt-2 {
  grid-column: span 2;
}
.bt-3 {
  grid-column: span 3;
}
.bt-4 {
  grid-column: span 4;
}
/*
  The phone is the one tile with a picture in it, and a picture of a phone is
  twice as tall as anything else here. So it takes two rows on the right of
  the grid instead of standing in a row of its own and leaving the tile beside
  it half empty. That gap was the reader's note on the fifth pass: "large
  empty space inside tiles".
*/
.bt-phone {
  grid-row: span 2;
}
/* The last tile is a list of mails, and a list of mails wants the width. */
.bt-6 {
  grid-column: span 6;
}
.bt h3 {
  font-family: "Bricolage", "Plex Sans", sans-serif;
  font-weight: 700;
  font-size: 1.125rem;
  letter-spacing: -0.02em;
  line-height: 1.2;
}
.bt > p {
  color: var(--muted);
  font-size: 0.9375rem;
  max-width: 44ch;
}
/* The drawing takes whatever is left, and sits at the foot of the tile. */
.bd {
  margin-top: 0.9rem;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: 5.5rem;
}
@media (max-width: 62rem) {
  .bento {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .bt-2,
  .bt-3,
  .bt-4,
  .bt-6 {
    grid-column: auto;
  }
  /* Two columns: every tile is one cell, the phone included. */
  .bt-phone {
    grid-row: auto;
  }
}
@media (max-width: 34rem) {
  .bento {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ----------------------------------------------- one: the duplicate found */
.bd-dup {
  gap: 0.85rem;
  justify-content: center;
}
.dupg {
  display: grid;
  gap: 2px;
}
/* The times, where a check is about them, in the row's own small figures. */
.dup-t {
  color: var(--muted);
  margin-left: 0.35rem;
}
/* On a phone the times take a line of their own rather than wrapping mid-row. */
@media (max-width: 34rem) {
  .dup-t {
    display: block;
    margin-left: 0;
  }
}
.dupr {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr) 1.5rem;
  gap: 0.5rem;
  padding: 0.4rem 0.5rem;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font-size: 0.8125rem;
}
.dupr > :last-child {
  text-align: right;
}
/* The second of the pair, once it has been recognised as the same row. */
.dup2 {
  background: var(--d-amber-fill);
  border-color: var(--d-amber-rule);
  box-shadow: inset 3px 0 0 var(--d-amber-ink);
}
.dupf {
  margin-top: 0.4rem;
  font-size: 0.75rem;
  color: var(--d-amber-ink);
  line-height: 1.4;
}

/* ------------------------------------------------- two: the seal, broken */
.bd-seal {
  gap: 0.35rem;
}
.seal-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  border-top: 2px solid var(--text);
  padding-top: 0.4rem;
  font-size: 0.8125rem;
}
.seal-fig {
  position: relative;
  font-weight: 600;
  font-size: 1rem;
}
/* The figure, in both of its readings. The true one is what is left showing. */
.sf-b {
  position: absolute;
  right: 0;
  top: 0;
  opacity: 0;
  color: var(--d-amber-ink);
}
.seal-hex {
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  color: var(--stamp);
  word-spacing: 0.15em;
}
.seal-say {
  display: grid;
  font-size: 0.75rem;
  font-weight: 600;
}
.seal-say > span {
  grid-area: 1 / 1;
}
.sv-ok {
  color: var(--leaf);
}
.sv-bad {
  color: var(--d-red);
  opacity: 0;
}

/* ------------------------------------------------- three: the audit trail */
.bd-audit {
  gap: 0.45rem;
  justify-content: space-between;
}
/*
  0.75rem, not 0.6875. Eleven pixels of grey on grey was the smallest type on
  the page and the hardest to read in the dark scheme, and the tile it is in
  has the room: it is four lines in a tile two rows tall.
*/
.au {
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--muted);
  border-left: 2px solid var(--rule-firm);
  padding-left: 0.6rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.au b {
  color: var(--text);
  font-weight: 600;
}
/*
  One line each while there is room for one, and wrapped rather than cut once
  there is not: at 360 the tile is the width of the screen and the last word
  of three of these four rows is a person's name, which an ellipsis turned
  into "Dr N. Go...".
*/
@container (max-width: 22rem) {
  .au {
    white-space: normal;
    text-overflow: clip;
  }
}
.au .figure {
  color: var(--stamp);
}

/* ---------------------------------------- four: two databases, and a test */
.bd-db {
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  color: var(--rule-firm);
}
.bd-db svg {
  width: 100%;
  max-width: 13rem;
  height: auto;
}
.db-tick {
  color: var(--leaf);
  stroke: currentColor;
}
.db-tick circle {
  fill: var(--ground);
}
.db-say {
  font-size: 0.6875rem;
  color: var(--muted);
  letter-spacing: 0.04em;
}

/* ------------------------------------------------- five: the block, filled */
/*
  The card at the top of a student's logbook, drawn whole: the figure, the
  meter at the app's own twenty-one cells to a row, and the card's own line
  under it. It was the meter alone before, three quarters of an inch of
  colour floating in the middle of a tile four times its height.
*/
.bmeter {
  display: grid;
  grid-template-columns: repeat(21, minmax(0, 1fr));
  gap: 2px;
}
.bmeter i {
  height: 0.7rem;
  border-radius: 3px;
  background: var(--d-green);
}
@container (min-width: 15rem) {
  .bmeter i {
    height: 0.9rem;
    border-radius: 4px;
  }
}
.bd-meter {
  justify-content: space-between;
  gap: 0.6rem;
}
.bm-top {
  font-size: 1.75rem;
  font-weight: 600;
  line-height: 1;
  color: var(--text);
}
.bm-of {
  font-size: 1rem;
  font-weight: 400;
  color: var(--muted);
}
.bm-say {
  font-size: 0.75rem;
  color: var(--muted);
  line-height: 1.4;
}

/* --------------------------------------------- seven: what it sends, alone */
/*
  The three mails the app sends without being asked, in its own subject
  lines. Drawn as a list of notifications rather than as inboxes: what a
  school is buying here is that nobody has to remember to chase, and the
  proof of that is the words in the subject line.
*/
.bd-note {
  justify-content: center;
  gap: 0.4rem;
}
@container (min-width: 34rem) {
  .bd-note {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.5rem;
    align-items: stretch;
  }
}
.nt {
  display: grid;
  gap: 0.15rem;
  align-content: start;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--d-blue);
  border-radius: var(--radius);
  padding: 0.5rem 0.6rem;
}
.nt-who {
  font-family: "Plex Mono", monospace;
  font-size: 0.625rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.nt-sub {
  font-size: 0.75rem;
  line-height: 1.35;
  color: var(--text);
}

/* ------------------------------------------------ six: nothing to install */
/*
  THE ONE PHOTOGRAPH ON THE PAGE, AND NOTHING DRAWN AROUND IT.

  Everything else here is the app's markup rebuilt by hand, for the four
  reasons in site/README.md, and every one of them holds except for this
  claim. "You install nothing, you open a website" is the one thing a drawing
  cannot say, because a drawn phone is what a reader expects a made-up phone
  to be. So the screen is the app itself, shot from a local build at the
  402 x 874 viewport at deviceScaleFactor 3.

  A phone was drawn around it for one pass — bezel, island, side buttons, and
  a home bar that lay across the app's own tab bar, which is exactly the part
  of the picture that proves the point. It is the screen alone now, which is
  how Linear, Stripe, Apple's own app pages and most of the template shops
  show one: a rounded rectangle at about a tenth of its width, a hairline of
  the page's ink at low alpha, and one soft shadow under it — because a screen
  render is a photograph of an object lying on the page, and it is the only
  thing here that is, which is why it is also the only shadow on the page.

  WHY A BACKGROUND AND NOT AN <img>. The page has two colour schemes and so
  does the app, a light phone screen glowing out of a dark tile is the
  objection that took the screenshots off this page three passes ago, and
  neither <img srcset> nor <picture media> can follow the [data-theme] the
  switch in the bar sets. Two candidates per scheme, at twice and three times
  the 272px the screen is drawn at, WebP at q80, and only the pair that
  matches is ever fetched.
*/
/*
  A SIZE CONTAINER, AND A HEIGHT THAT IS A NUMBER.

  The screen was sized by `aspect-ratio` alone inside a flex item inside a
  grid row whose height comes from the tiles beside it, and inside a grid
  that clips. On this machine that centres to the pixel at every width; on
  another engine the same markup was reported cut off along the bottom, which
  is what an aspect-ratio box gets when the layout resolves its height from
  the flex line instead of from its own width. So the width is a share of
  this container and the height is that width times 874/402, written out, and
  the item is not allowed to flex. The aspect-ratio stays as a second opinion
  and the tile is told it can never be shorter than what is in it.
*/
.bd-phone {
  container-type: inline-size;
  align-items: center;
  justify-content: center;
  /*
    The space under the screen is this element's own padding, not whatever a
    flex line has left over. Reported from another engine as twelve pixels
    where this one measures twenty-four: a centred item in a box that is a
    pixel too short loses half the difference at each end, and the end that
    shows is the one under the app's own tab bar. min-height keeps the box
    from ever being that pixel short, and the padding is there whether or not
    it is.
  */
  min-height: max-content;
  padding-bottom: 1.5rem;
}
/* Which the tile then does not need to pay for twice. */
.bt-phone {
  padding-bottom: 0.5rem;
}
.ip-screen {
  flex: none;
  width: min(17rem, 100%);
  aspect-ratio: 402 / 874;
  border-radius: 9% / 4.14%;
  overflow: hidden;
  background: var(--paper);
  /* The hairline is an outline drawn just inside the edge, over the image. */
  outline: 1px solid var(--screen-edge);
  outline-offset: -1px;
  box-shadow: 0 1.25rem 2.5rem -1.5rem var(--screen-shade);
}
.ip-screen picture,
.ip-screen img {
  display: block;
  width: 100%;
  height: 100%;
}
.ip-screen img {
  object-fit: cover;
}

/* ================================================== careful with, as tiles ==
  Four of them, the same ruled grid as the bento at a quarter of the height:
  a drawn mark, a short claim and one line. The mark is the whole difference
  between this and the four-line list it replaces.
*/
/*
  Two across, not four. The body column is about 680px wide at any desktop
  size this page is read at, and four of these are 170px each: every claim
  wrapped to two lines and every line under it to four. Two is 340px, which
  is a column of type rather than a stack of syllables.
*/
.cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: clip;
}
.cd {
  background: var(--ground);
  padding: 1.25rem 1.1rem 1.4rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.cd-i {
  width: 2rem;
  height: 2rem;
  color: var(--stamp);
  margin-bottom: 0.4rem;
}
.cd h3 {
  font-family: "Bricolage", "Plex Sans", sans-serif;
  font-weight: 700;
  font-size: 1rem;
  letter-spacing: -0.02em;
  line-height: 1.2;
}
.cd > p {
  color: var(--muted);
  font-size: 0.875rem;
}
@media (max-width: 34rem) {
  .cards {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ============================================================= questions ==
  A real <details> each, with the browser's own control on it, so a question
  opens with no script between the press and the answer and keeps working
  where every other thing on this page has been turned off.
*/
.qa details {
  border-top: 1px solid var(--rule);
}
.qa summary {
  position: relative;
  list-style: none;
  cursor: pointer;
  padding: 1.05rem 2.5rem 1.05rem 0.75rem;
  margin-inline: -0.75rem;
  font-family: "Bricolage", "Plex Sans", sans-serif;
  font-weight: 700;
  font-size: 1.0625rem;
  letter-spacing: -0.02em;
}
.qa summary::-webkit-details-marker {
  display: none;
}
.qa summary::after {
  content: "";
  position: absolute;
  right: 1rem;
  top: 1.35rem;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
}
.qa details[open] summary::after {
  transform: rotate(225deg);
  border-color: var(--stamp);
}
.qa-b {
  padding: 0 0.75rem 1.1rem;
  margin-inline: -0.75rem;
  color: var(--muted);
  max-width: 62ch;
}
@media (hover: hover) {
  .qa summary {
    transition: background-color 0.18s ease;
  }
  .qa summary:hover {
    background: rgb(59 76 225 / 0.05);
  }
}

/* ====================================================== the light switch ==
  Sun or moon, whichever one the press would give you. It is hidden in the
  markup and shown by app.js, because with no script there is nothing for it
  to do and the page already follows the system's own setting.
*/
.themer {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 1px solid var(--on-ink-edge);
  border-radius: var(--radius);
  background: transparent;
  color: var(--on-ink);
  cursor: pointer;
  flex: none;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}
.themer:hover {
  background: rgb(255 255 255 / 0.1);
  border-color: var(--on-ink);
}
.themer svg {
  width: 1.125rem;
  height: 1.125rem;
}
.themer .ic-sun {
  display: none;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .themer .ic-sun {
    display: block;
  }
  :root:not([data-theme="light"]) .themer .ic-moon {
    display: none;
  }
}
:root[data-theme="dark"] .themer .ic-sun {
  display: block;
}
.motioner .ic-play {
  display: none;
}
:root[data-motion="still"] .motioner .ic-play {
  display: block;
}
:root[data-motion="still"] .motioner .ic-pause {
  display: none;
}
/*
  PAUSED. Every animation and transition on the page is inside the
  reduced-motion media query, and a media query cannot read an attribute, so
  the pause is one rule over everything instead: with nothing animating, each
  element sits at its base style, and the base style is the finished page.
  app.js also takes off the classes that hide things until they arrive.
*/
:root[data-motion="still"] *,
:root[data-motion="still"] *::before,
:root[data-motion="still"] *::after {
  animation: none !important;
  transition: none !important;
}
:root[data-theme="dark"] .themer .ic-moon {
  display: none;
}
[hidden] {
  display: none !important;
}

/*
  The soft cross-fade the scheme changes with, where the browser has view
  transitions. Everything on the page is a token swap, so one fade of the
  whole root is the whole animation and there is nothing to name.
*/
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.26s;
  animation-timing-function: ease;
  mix-blend-mode: normal;
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
}

/* =================================================== landing on a section ==
  A link from the bar drops you at a heading with no sign that anything moved.
  The section you asked for says so: its number takes the link's own ink, and
  where motion is allowed the rule under it lights once and lets go.
*/
:target {
  scroll-margin-top: calc(var(--bar-h) + 1.5rem);
}
.section:target > .head .label {
  color: var(--stamp);
}
/* =============================================== what it will not do (05) ==
  One refusal set at the size of the answer, the other three ruled against it,
  and the office's own roster underneath with the column a payroll system
  would put money in left empty. Two short lists in a row is what this was,
  and two lists in a row read as filler however true they are.
*/
.refuse {
  gap: 1.75rem;
}
@media (min-width: 52rem) {
  .refuse {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    align-items: start;
    gap: 2rem var(--gutter);
  }
  .refuse-pic {
    grid-column: 1 / -1;
  }
}
.refuse-big {
  font-family: "Bricolage", "Plex Sans", sans-serif;
  font-weight: 700;
  font-size: clamp(1.875rem, 5vw, 3.25rem);
  line-height: 1;
  letter-spacing: -0.04em;
  max-width: 11ch;
  margin-top: -0.12em;
}
.refuse-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--rule);
}
.refuse-list li {
  border-bottom: 1px solid var(--rule);
  padding: 0.85rem 0.75rem;
  margin-inline: -0.75rem;
  color: var(--muted);
  max-width: var(--measure);
}
/* The roster, at three rows, and the empty column in the middle of it. */
.refuse-pic {
  max-width: 34rem;
}
.demo-pay {
  padding: 0.85rem 0.9rem 0.9rem;
}
.roll-pay {
  margin-top: 0;
}
.roll-pay .pay-gap {
  width: 4.5rem;
  padding-right: 0.4rem;
}
/*
  The empty column is labelled "Amount" and each cell says "none": a dashed
  empty box on its own read as a progress bar that had failed to draw, which
  is the opposite of the point (the app holds no rate and no amount).
*/
.roll-pay tbody .pay-gap::after {
  content: "none";
  color: var(--muted);
  font-style: italic;
  font-size: 0.75rem;
}
/* A pill in this drawing is not being animated, so it is simply drawn. */
.pl.lit {
  opacity: 1;
}

/* ------------------------------------------------------------ the closing ==
  A panel of ink at the foot of the page: what to do on the left, what a
  school hands over on the right, and the meter finishing under both. It was
  a lede, a list and two buttons in the reading column, which is how the page
  ended rather than a place it arrived at.
*/
.closing {
  border-radius: var(--radius);
  padding: clamp(1.5rem, 3.5vw, 2.25rem);
}
/*
  The one section whose block crosses the rail. Everywhere else the margin
  column carries the number and the heading and the body column carries what
  is read, and that holds here too — but the panel itself is the end of the
  page and takes the whole width under them, which is the difference between
  a closing and one more section.
*/
@media (min-width: 64rem) {
  .close > .body {
    grid-column: 1 / -1;
    margin-top: 1.5rem;
  }
  /*
    And its heading does not stick. Every other section's heading holds its
    place while a long body passes it; this one has nothing passing it, and
    stuck it sat on the top edge of the panel underneath.
  */
  .close > .head {
    position: static;
  }
}
.closing-grid {
  display: grid;
  gap: 2rem;
  align-items: stretch;
}
/*
  The address sits at the foot of its own column rather than under the
  buttons, so the two halves of the panel end on the same line instead of the
  left one stopping half way down.
*/
.closing-say {
  display: flex;
  flex-direction: column;
}
.closing-ad {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.75rem;
  padding-top: 1.25rem;
}
/*
  Copy the address. A managed laptop often has no mail client, and a mailto
  then does nothing at all; the address is printed as text, and this puts it
  on the clipboard in one press. Shown by app.js only where the clipboard can
  be written.
*/
.copy-ad {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding: 0 0.75rem;
  border: 1px solid var(--on-ink-edge);
  border-radius: var(--radius);
  background: transparent;
  color: var(--on-ink);
  font: inherit;
  font-size: 0.875rem;
  cursor: pointer;
}
.copy-ad:hover {
  background: rgb(255 255 255 / 0.1);
}
@media (min-width: 46rem) {
  .closing-grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 2.5rem;
  }
}
.closing-h {
  font-family: "Bricolage", "Plex Sans", sans-serif;
  font-weight: 700;
  font-size: clamp(1.375rem, 2.6vw, 1.75rem);
  line-height: 1.1;
  letter-spacing: -0.03em;
  margin-bottom: 1.5rem;
  max-width: 18ch;
}
.closing-ad {
  font-size: 0.9375rem;
}
/*
  A link on a line of its own, so it is a target rather than a word in a
  sentence: 19px tall was under the 24 that asks for, and a pointer is not
  the only thing that has to land on it.
*/
.closing-ad a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  color: var(--on-ink);
}
.closing-list .label {
  color: var(--on-ink-muted);
  display: block;
  margin-bottom: 0.35rem;
}

/*
  The six things, with a box beside each that ticks as the panel arrives. The
  tick is drawn, it is not the only carrier of anything, and with no motion
  every one of them is already ticked: the list says what a school hands over
  either way.
*/
.supply {
  margin: 0;
  padding: 0;
  list-style: none;
}
.supply li {
  display: flex;
  align-items: baseline;
  gap: 0.65rem;
  border-top: 1px solid var(--on-ink-rule);
  padding: 0.7rem 0;
  color: var(--on-ink-muted);
  font-size: 0.9375rem;
}
.supply b {
  color: var(--on-ink);
  font-weight: 600;
}
.tk {
  position: relative;
  flex: none;
  align-self: center;
  width: 1.05rem;
  height: 1.05rem;
  border-radius: 3px;
  border: 1px solid var(--on-ink-edge);
  background: var(--cell-met);
}
.tk::after {
  content: "";
  position: absolute;
  left: 0.34rem;
  top: 0.14rem;
  width: 0.25rem;
  height: 0.5rem;
  border: solid #06281b;
  border-width: 0 2px 2px 0;
  transform: rotate(42deg);
}

/*
  The last thing on the page is the meter finishing: it fills as the panel
  arrives and turns green, which is the same event the hero opened with and
  the thing the whole product is for.
*/
.close-act {
  margin-top: 2rem;
  border-top: 1px solid var(--on-ink-rule);
  padding-top: 1.75rem;
}
.close-meter {
  display: flex;
  gap: 5px;
  width: 100%;
}
.close-meter span {
  flex: 1;
  height: 0.75rem;
  border-radius: 4px;
  background: var(--cell-met);
}

/* --------------------------------------------------------- the row hovers */
/*
  A pointer over a row tints it and darkens its rule. Nothing moves and
  nothing lifts: these are rows on paper, and paper does not hover.
*/
@media (hover: hover) {
  .refuse-list li {
    transition: background-color 0.18s ease;
  }
  .refuse-list li:hover {
    background: rgb(59 76 225 / 0.05);
  }
}

/* ------------------------------------------------------------------ footer */
footer.ink {
  padding-block: clamp(2.5rem, 5vw, 3.5rem);
}
.foot-grid {
  display: grid;
  gap: 2rem;
  align-items: start;
  border-top: 1px solid var(--on-ink-rule);
  padding-top: 2rem;
}
@media (min-width: 46rem) {
  .foot-grid {
    grid-template-columns: minmax(0, 1fr) auto;
  }
}
.foot-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  font-size: 0.9375rem;
}
.foot-links a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  color: var(--on-ink-muted);
  text-decoration: none;
}
.foot-note a {
  display: inline-block;
  padding-block: 0.3rem;
  margin-block: -0.3rem;
}
.foot-links a:hover {
  color: var(--on-ink);
  text-decoration: underline;
}
.foot-note {
  color: var(--on-ink-muted);
  font-size: 0.8125rem;
  max-width: 52ch;
  margin-top: 1.25rem;
}
/*
  The first line of the footer is the one a school's own compliance people
  look for, so it is not set at the size of the small print under it.
*/
.foot-first {
  color: var(--on-ink);
  font-size: 0.9375rem;
  font-weight: 600;
}
.foot-note a {
  color: var(--on-ink);
}


/* ================================================================= motion ==
  Everything below this line is enhancement. What is above it is the page
  somebody gets with no script, no scroll timeline, and reduced motion asked
  for, and it is complete: every screen drawn is the screen at the end of its
  own story.
*/
@media (prefers-reduced-motion: no-preference) {
  /* ----------------------------------------------------- the load sequence */
  /*
    ONE sequence rather than an effect per element. The bar, the headline's
    three lines out of their own clipped bands, the lede, the buttons, the
    drawn sheet, and last the meter filling hour by hour. It is finished and
    still by about four seconds and nothing repeats.
  */
  .topbar,
  .hero-lede,
  .hero .actions,
  .slip,
  .meter-band > * {
    animation: riseIn 0.7s cubic-bezier(0.2, 0.75, 0.25, 1) both;
  }
  .topbar {
    animation: fadeIn 0.5s ease both;
  }
  .hero h1 .ln > span {
    animation: lineUp 0.85s cubic-bezier(0.2, 0.75, 0.25, 1) both;
  }
  .hero h1 .ln:nth-child(1) > span {
    animation-delay: 0.1s;
  }
  .hero h1 .ln:nth-child(2) > span {
    animation-delay: 0.19s;
  }
  .hero h1 .ln:nth-child(3) > span {
    animation-delay: 0.28s;
  }
  .hero-lede {
    animation-delay: 0.44s;
  }
  .hero .actions {
    animation-delay: 0.54s;
  }
  /*
    The rise is on the sheet and on the three parts of the band, not on the
    two wrappers around them, because the wrappers carry the replay's fade
    and one element cannot hold two animations of one property without one of
    them quietly winning.
  */
  .slip {
    animation-delay: 0.5s;
    animation-duration: 0.9s;
  }
  .meter-band > * {
    animation-delay: 0.7s;
  }

  /* ------------------------------------------- the hero's own choreography */
  /* hero-timing: generated by site/tools/hero.py from its table of seconds.
     Edit the table and run the tool; do not edit these numbers by hand. */
  /*
    ONE SEQUENCE, AND IT IS THE PRODUCT DOING SOMETHING RATHER THAN A PICTURE
    OF THE PRODUCT. The form is filled in the way a student fills it in, and
    every state it passes through is one the app has. The whole of it, in
    seconds from the start of the cycle (12s, starting 1.2s after load):

         0.00  tap_link
         0.25  cal_open
         0.65  cal_opened
         1.05  tap_day
         1.25  day_picked
         1.55  date_in
         1.95  cal_closed
         2.40  tap_duty
         2.60  list_open
         2.90  list_opened
         3.30  type_l
         3.45  narrowed
         3.95  tap_opt
         4.15  picked
         4.40  list_closed
         4.45  filled
         5.10  press
         5.60  press_up
         5.70  land
         5.75  tally_a
         6.30  tally_b
         6.40  met
         6.80  met_count
         7.00  stamp
         7.70  stamped
        10.60  hold_end
        10.90  faded
        11.00  undo
        11.40  lifted

    The first play is the same animation ending on the hold, by a fractional
    iteration count; while the hero is on the screen the count is infinite.
    Every part shares --cycle and --lead, so none can drift from another, and
    nothing structural takes part: the card, the labels, the fields, the
    thirty-nine logged cells and the receipt are drawn once. No container's
    opacity changes at any point, so the ink behind the card is never exposed.
  */
  .hero {
    --cycle: 12.0s;
    --lead: 1.2s;
    --plays: 0.8833;
  }
  .hero.looping {
    --plays: infinite;
  }
  .ph-tap {
    animation: heroTapLink var(--cycle) ease-out var(--plays) both var(--lead);
  }
  .ph-calslot .cal-tap {
    animation: heroTapDay var(--cycle) ease-out var(--plays) both var(--lead);
  }
  .ph-calslot {
    animation: heroCal var(--cycle) linear var(--plays) both var(--lead);
  }
  .pk-a {
    animation: heroLinkA var(--cycle) linear var(--plays) both var(--lead);
  }
  .pk-b {
    animation: heroLinkB var(--cycle) linear var(--plays) both var(--lead);
  }
  .ph-calslot .cal-d.pick {
    animation: heroPick var(--cycle) linear var(--plays) both var(--lead);
  }
  .ph-date > span {
    animation: heroDate var(--cycle) linear var(--plays) both var(--lead);
  }
  .ph-tap-duty {
    animation: heroTapDuty var(--cycle) ease-out var(--plays) both var(--lead);
  }
  .ph-duty {
    animation: heroFocusDuty var(--cycle) ease var(--plays) both var(--lead);
  }
  .ph-duty-raw {
    animation: heroDutyRaw var(--cycle) linear var(--plays) both var(--lead);
  }
  .ph-duty-val {
    animation: heroDutyVal var(--cycle) linear var(--plays) both var(--lead);
  }
  .ph-list {
    animation: heroList var(--cycle) linear var(--plays) both var(--lead);
  }
  .ph-opt-out {
    animation: heroOptOut var(--cycle) linear var(--plays) both var(--lead);
  }
  .ph-opt-pick {
    animation: heroOptPick var(--cycle) linear var(--plays) both var(--lead);
  }
  .ph-tap-opt {
    animation: heroTapOpt var(--cycle) ease-out var(--plays) both var(--lead);
  }
  .ph-start > span,
  .ph-end > span,
  .ph-hours > span {
    animation: heroValue var(--cycle) linear var(--plays) both var(--lead);
  }
  .ph-save {
    animation: heroPress var(--cycle) ease var(--plays) both var(--lead);
  }
  /*
    THE METER, IN TWO GROUPS. Thirty-nine hours were in before any of this
    started: they fill once, on load, as a wave, and take no further part
    until the whole set turns over. The last three are the lab the sheet has
    just logged, and they land one after another, 120 ms apart.
  */
  .meter .cell {
    animation: cellFill 0.59s ease both calc(0.6s + var(--i, 0) * 20ms),
      heroHeld var(--cycle) steps(1, end) var(--plays) forwards var(--lead);
  }
  .meter .cell.land {
    animation: heroLand0 var(--cycle) ease var(--plays) both var(--lead);
  }
  .meter .cell.land + .cell.land {
    animation-name: heroLand1;
  }
  .meter .cell.land + .cell.land + .cell.land {
    animation-name: heroLand2;
  }
  /*
    The figure counts up to thirty-nine once, on load, under the wave, and the
    sequence takes it from there. `forwards` rather than `both` on the second
    animation: with a backwards fill it would hold thirty-nine from the first
    frame and the count-up would never be seen.
  */
  .tally-n {
    animation: tallyCount 1.2s linear both 0.6s,
      heroTally var(--cycle) linear var(--plays) forwards var(--lead);
  }
  .tally::after {
    animation: tallyFill 1.9s linear both 0.6s,
      heroRule var(--cycle) linear var(--plays) forwards var(--lead);
  }
  .meter-count {
    animation: heroCount var(--cycle) linear var(--plays) both var(--lead);
  }
  .rc-stamp {
    animation: heroStamp var(--cycle) linear var(--plays) both var(--lead);
  }
  @keyframes cellFill {
    0% {
      background: var(--cell-empty);
      box-shadow: inset 0 0 0 0 var(--cell-logged);
      transform: scaleY(0.5);
    }
    38% {
      background: transparent;
      box-shadow: inset 0 0 0 2px var(--cell-logged);
      transform: scaleY(1.06);
    }
    100% {
      background: var(--cell-logged);
      box-shadow: inset 0 0 0 0 var(--cell-logged);
      transform: scaleY(1);
    }
  }
  @keyframes tallyCount {
    from {
      --n: 0;
    }
    to {
      --n: 39;
    }
  }
  @keyframes tallyFill {
    from {
      transform: scaleX(0);
    }
    to {
      transform: scaleX(1);
    }
  }
  @keyframes riseIn {
    from {
      opacity: 0;
      transform: translateY(14px);
    }
    to {
      opacity: 1;
      transform: none;
    }
  }
  @keyframes fadeIn {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }
  @keyframes lineUp {
    from {
      transform: translateY(104%);
    }
    to {
      transform: none;
    }
  }
  @keyframes heroTapLink {
    0%, -0.04% {
      opacity: 0;
      transform: scale(1.6);
    }
    1.25% {
      opacity: 1;
      transform: scale(1);
    }
    2.50% {
      opacity: 1;
      transform: scale(1);
    }
    3.33%, 100% {
      opacity: 0;
      transform: scale(0.9);
    }
  }
  @keyframes heroTapDay {
    0%, 8.71% {
      opacity: 0;
      transform: scale(1.6);
    }
    10.00% {
      opacity: 1;
      transform: scale(1);
    }
    11.25% {
      opacity: 1;
      transform: scale(1);
    }
    12.08%, 100% {
      opacity: 0;
      transform: scale(0.9);
    }
  }
  @keyframes heroTapDuty {
    0%, 19.96% {
      opacity: 0;
      transform: scale(1.6);
    }
    21.25% {
      opacity: 1;
      transform: scale(1);
    }
    22.50% {
      opacity: 1;
      transform: scale(1);
    }
    23.33%, 100% {
      opacity: 0;
      transform: scale(0.9);
    }
  }
  @keyframes heroTapOpt {
    0%, 32.88% {
      opacity: 0;
      transform: scale(1.6);
    }
    34.17% {
      opacity: 1;
      transform: scale(1);
    }
    35.42% {
      opacity: 1;
      transform: scale(1);
    }
    36.25%, 100% {
      opacity: 0;
      transform: scale(0.9);
    }
  }
  @keyframes heroCal {
    0%, 2.08% {
      grid-template-rows: 0fr;
      animation-timing-function: ease-out;
    }
    5.42%, 12.92% {
      grid-template-rows: 1fr;
      animation-timing-function: ease-in-out;
    }
    16.25%, 100% {
      grid-template-rows: 0fr;
    }
  }
  @keyframes heroLinkA {
    0%, 2.04% {
      opacity: 1;
      animation-timing-function: steps(1, end);
    }
    2.08%, 16.21% {
      opacity: 0;
      animation-timing-function: steps(1, end);
    }
    16.25%, 100% {
      opacity: 1;
    }
  }
  @keyframes heroLinkB {
    0%, 2.04% {
      opacity: 0;
      animation-timing-function: steps(1, end);
    }
    2.08%, 16.21% {
      opacity: 1;
      animation-timing-function: steps(1, end);
    }
    16.25%, 100% {
      opacity: 0;
    }
  }
  @keyframes heroPick {
    0%, 10.38% {
      background: transparent;
      color: var(--text);
      font-weight: 400;
      box-shadow: inset 0 0 0 1.5px var(--d-blue-ring);
      animation-timing-function: steps(1, end);
    }
    10.42%, 88.29% {
      background: var(--d-blue);
      color: #fff;
      font-weight: 600;
      box-shadow: none;
      animation-timing-function: steps(1, end);
    }
    88.33%, 100% {
      background: transparent;
      color: var(--text);
      font-weight: 400;
      box-shadow: inset 0 0 0 1.5px var(--d-blue-ring);
      animation-timing-function: steps(1, end);
    }
  }
  @keyframes heroDate {
    0%, 12.88% {
      opacity: 0;
      animation-timing-function: steps(1, end);
    }
    12.92%, 88.33% {
      opacity: 1;
      animation-timing-function: ease;
    }
    90.83%, 100% {
      opacity: 0;
    }
  }
  @keyframes heroDutyVal {
    0%, 34.54% {
      opacity: 0;
      animation-timing-function: steps(1, end);
    }
    34.58%, 88.33% {
      opacity: 1;
      animation-timing-function: ease;
    }
    90.83%, 100% {
      opacity: 0;
    }
  }
  @keyframes heroValue {
    0%, 37.04% {
      opacity: 0;
      animation-timing-function: steps(1, end);
    }
    37.08%, 88.33% {
      opacity: 1;
      animation-timing-function: ease;
    }
    90.83%, 100% {
      opacity: 0;
    }
  }
  @keyframes heroFocusDuty {
    0%, 19.96% {
      border-color: #64748b;
      box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
    }
    20.00%, 34.58% {
      border-color: #3b4ce1;
      box-shadow: 0 0 0 2px #dbeafe;
    }
    37.92%, 100% {
      border-color: #64748b;
      box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
    }
  }
  @keyframes heroDutyRaw {
    0%, 27.46% {
      opacity: 0;
      animation-timing-function: steps(1, end);
    }
    27.50%, 34.54% {
      opacity: 1;
      animation-timing-function: steps(1, end);
    }
    34.58%, 100% {
      opacity: 0;
    }
  }
  @keyframes heroList {
    0%, 21.63% {
      opacity: 0;
      transform: translateY(-4px);
      animation-timing-function: steps(1, end);
    }
    21.67%, 36.62% {
      opacity: 1;
      transform: none;
      animation-timing-function: steps(1, end);
    }
    36.67%, 100% {
      opacity: 0;
      transform: translateY(-4px);
    }
  }
  @keyframes heroOptPick {
    0%, 32.88% {
      background: transparent;
      animation-timing-function: steps(1, end);
    }
    32.92%, 34.54% {
      background: #eef2ff;
      animation-timing-function: steps(1, end);
    }
    34.58%, 100% {
      background: transparent;
    }
  }
  @keyframes heroOptOut {
    0%, 28.71% {
      display: block;
      animation-timing-function: steps(1, end);
    }
    28.75%, 100% {
      display: none;
    }
  }
  @keyframes heroPress {
    0%, 42.50% {
      transform: none;
      background: #3b4ce1;
    }
    43.75%, 45.00% {
      transform: scale(0.97);
      background: #2f3ec4;
    }
    46.67%, 100% {
      transform: none;
      background: #3b4ce1;
    }
  }
  @keyframes heroLand0 {
    0%, 47.50% {
      background: var(--cell-empty);
      box-shadow: inset 0 0 0 0 var(--cell-logged);
      transform: scaleY(0.72);
    }
    48.50% {
      background: transparent;
      box-shadow: inset 0 0 0 2px var(--cell-logged);
      transform: scaleY(1.06);
    }
    50.00%, 53.29% {
      background: var(--cell-logged);
      box-shadow: inset 0 0 0 0 var(--cell-logged);
      transform: none;
    }
    53.33%, 91.62% {
      background: var(--cell-met);
      box-shadow: inset 0 0 0 0 var(--cell-met);
      transform: none;
    }
    91.67%, 100% {
      background: var(--cell-empty);
      box-shadow: inset 0 0 0 0 var(--cell-logged);
      transform: none;
    }
  }
  @keyframes heroLand1 {
    0%, 48.50% {
      background: var(--cell-empty);
      box-shadow: inset 0 0 0 0 var(--cell-logged);
      transform: scaleY(0.72);
    }
    49.50% {
      background: transparent;
      box-shadow: inset 0 0 0 2px var(--cell-logged);
      transform: scaleY(1.06);
    }
    51.00%, 53.29% {
      background: var(--cell-logged);
      box-shadow: inset 0 0 0 0 var(--cell-logged);
      transform: none;
    }
    53.33%, 91.62% {
      background: var(--cell-met);
      box-shadow: inset 0 0 0 0 var(--cell-met);
      transform: none;
    }
    91.67%, 100% {
      background: var(--cell-empty);
      box-shadow: inset 0 0 0 0 var(--cell-logged);
      transform: none;
    }
  }
  @keyframes heroLand2 {
    0%, 49.50% {
      background: var(--cell-empty);
      box-shadow: inset 0 0 0 0 var(--cell-logged);
      transform: scaleY(0.72);
    }
    50.50% {
      background: transparent;
      box-shadow: inset 0 0 0 2px var(--cell-logged);
      transform: scaleY(1.06);
    }
    52.00%, 53.29% {
      background: var(--cell-logged);
      box-shadow: inset 0 0 0 0 var(--cell-logged);
      transform: none;
    }
    53.33%, 91.62% {
      background: var(--cell-met);
      box-shadow: inset 0 0 0 0 var(--cell-met);
      transform: none;
    }
    91.67%, 100% {
      background: var(--cell-empty);
      box-shadow: inset 0 0 0 0 var(--cell-logged);
      transform: none;
    }
  }
  @keyframes heroHeld {
    0%, 53.29% {
      background: var(--cell-logged);
    }
    53.33%, 91.62% {
      background: var(--cell-met);
    }
    91.67%, 100% {
      background: var(--cell-logged);
    }
  }
  @keyframes heroTally {
    0%, 47.92% {
      --n: 39;
      animation-timing-function: linear;
    }
    52.50%, 91.62% {
      --n: 42;
      animation-timing-function: steps(1, end);
    }
    91.67%, 100% {
      --n: 39;
    }
  }
  @keyframes heroCount {
    0%, 53.33% {
      color: var(--on-ink-muted);
    }
    56.67%, 91.67% {
      color: var(--cell-met);
    }
    94.17%, 100% {
      color: var(--on-ink-muted);
    }
  }
  @keyframes heroRule {
    0%, 53.33% {
      background: var(--cell-logged);
    }
    56.67%, 91.67% {
      background: var(--cell-met);
    }
    94.17%, 100% {
      background: var(--cell-logged);
    }
  }
  @keyframes heroStamp {
    0%, 58.33% {
      opacity: 0;
      transform: rotate(-7deg) scale(1.5);
      animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1.1);
    }
    62.50% {
      opacity: 1;
      transform: rotate(-7deg) scale(0.98);
    }
    64.17%, 91.67% {
      opacity: 1;
      transform: rotate(-7deg) scale(1);
      animation-timing-function: ease-out;
    }
    95.00%, 100% {
      opacity: 0;
      transform: rotate(-7deg) scale(1.16);
    }
  }
  /* hero-timing: end */

  /* ===================================== the engine the screens run on ====
    ONE MECHANISM, WRITTEN ONCE, FOR EVERY MOVING PART ON THE PAGE.

    A part that moves carries `m` and names three things on itself: which
    keyframes it plays (--anim), and where in its screen's own story it starts
    and finishes (--s and --e, from 0 to 1). Nothing else. The two blocks
    below are the two ways a browser can play that: scrubbed against the
    screen's passage through the viewport where there are scroll timelines,
    and on a clock started by an observer where there are not. Written any
    other way this file would carry every timing twice and they would drift
    the first time one of them was tuned.

    A screen is its own timeline, so `--d` means "this screen" inside each of
    them and every part reads the nearest one.
  */
  @supports (animation-timeline: view()) {
    .demo,
    .bd {
      view-timeline-name: --d;
    }
    .m {
      animation-name: var(--anim);
      animation-timeline: --d;
      /*
        COVER, NOT ENTRY, and the difference is whether anybody sees the end.

        An entry range is over when the screen's foot reaches the foot of the
        window, which for a screen half the height of the window leaves its
        own foot below the fold at the moment it finishes — and the last beat
        of two of these three screens (the stamp landing, the download
        appearing) is at the foot. A cover range runs from the screen
        appearing to the screen leaving, so 8% to 44% of it (started earlier on 24 Sep 2026: the owner found the drawings slow to begin) finishes with the
        screen sitting square in the window.
      */
      animation-range: cover calc(8% + var(--s, 0) * 36%) cover calc(8% + var(--e, 1) * 36%);
      animation-fill-mode: both;
      animation-timing-function: var(--ease, linear);
    }

    /* The bar tightens as the page moves under it. */
    .topbar {
      animation: fadeIn 0.5s ease both, condense linear both;
      animation-timeline: auto, scroll(root block);
      animation-range: normal, 0 160px;
    }
    @keyframes condense {
      to {
        padding-block: 0.55rem 0.6rem;
        border-bottom-color: var(--on-ink-rule);
      }
    }

    /*
      BLOCKS ARRIVE AS THEY ARE REACHED, AND THEY ARE FULLY IN BY THE TIME
      THE READER IS LOOKING AT THEM.

      The range is written in viewport heights rather than in per cent of the
      element, because per cent of the element means a short row finishes in
      a third of the scroll a tall one takes and neither of them lands where
      a reader's eye is. Thirty viewport-height per cent from first sight is
      the element's top reaching about seven tenths of the way up the screen,
      whatever its own height, and that is where reading starts.
    */
    .reveal {
      animation: reveal linear both;
      animation-timeline: view();
      animation-range: cover 0px cover 30vh;
      animation-timing-function: cubic-bezier(0.2, 0.75, 0.25, 1);
    }
    @keyframes reveal {
      from {
        opacity: 0;
        transform: translateY(16px);
      }
      to {
        opacity: 1;
        transform: none;
      }
    }

  }

  /*
    The same story on a clock. app.js sets `observed` on <html> only where the
    block above cannot run, and then hands each screen `playing` as it
    arrives. --clock is the whole of one screen, 3.4 seconds unless the
    screen says otherwise, and --s and --e cut each part's share out of it,
    so a part that is scrubbed from a fifth to a half of the way through is
    delayed by a fifth of the clock and lasts three tenths of it. Same
    keyframes, same order, same finish.

    The clock is per screen because the screens are not the same length of
    story. The lecturer's has nine beats in a row and the reader's note on it
    at 3.4s was "all at once"; the three checks are three stories told in
    turn. Each of those names its own clock below, next to its beats.
  */
  .observed .playing .m {
    animation-name: var(--anim);
    animation-duration: calc((var(--e, 1) - var(--s, 0)) * var(--clock, 3.4s));
    animation-delay: calc(var(--s, 0) * var(--clock, 3.4s));
    animation-fill-mode: both;
    animation-timing-function: var(--ease, ease);
  }
  .observed .reveal {
    opacity: 0;
    transform: translateY(16px);
    transition: opacity 0.55s ease, transform 0.55s cubic-bezier(0.2, 0.75, 0.25, 1);
  }
  .observed .reveal.seen {
    opacity: 1;
    transform: none;
  }
  /* ---------------------------------------------- the closing strip, whole */
  /*
    THE LAST THING ON THE PAGE PLAYS THROUGH. It was scrubbed against the
    scroll like the rest, and at the foot of a page there is no more scroll:
    the reader's note was that it "awkwardly ends half way". So it is on a
    clock on both paths, started once by app.js when the strip reaches the
    seven-tenths line: the six lines come up a step apart, each box ticks a
    beat after its line, and the meter then fills and turns green, cell by
    cell, which is the same event the hero opened with. With no script
    `will-play` is never set and the strip is simply there, finished.
  */
  .closing.will-play .supply li,
  .closing.will-play .close-act {
    opacity: 0;
    transform: translateY(12px);
  }
  .closing.will-play.seen .supply li {
    opacity: 1;
    transform: none;
    transition: opacity 0.45s ease calc(var(--i, 0) * 0.14s),
      transform 0.45s cubic-bezier(0.2, 0.75, 0.25, 1) calc(var(--i, 0) * 0.14s);
  }
  .closing.will-play .supply .tk {
    background: transparent;
    transition: background-color 0.3s ease calc(var(--i, 0) * 0.14s + 0.35s);
  }
  .closing.will-play .supply .tk::after {
    opacity: 0;
    transition: opacity 0.01s linear calc(var(--i, 0) * 0.14s + 0.5s);
  }
  .closing.will-play.seen .supply .tk {
    background: var(--cell-met);
  }
  .closing.will-play.seen .supply .tk::after {
    opacity: 1;
  }
  .closing.will-play.seen .close-act {
    opacity: 1;
    transform: none;
    transition: opacity 0.45s ease 1.1s, transform 0.45s cubic-bezier(0.2, 0.75, 0.25, 1) 1.1s;
  }
  .closing.will-play .close-meter span {
    transform-origin: left center;
    transform: scaleY(0.25);
    background: var(--rule);
    transition: transform 0.5s cubic-bezier(0.2, 0.75, 0.25, 1) calc(1.3s + var(--i, 0) * 60ms),
      background-color 0.5s ease calc(1.3s + var(--i, 0) * 60ms);
  }
  .closing.will-play.seen .close-meter span {
    transform: none;
    background: var(--cell-met);
  }

  /* -------------------------------------------- one: the month as days */
  .cal-tap {
    --anim: tapDown;
    --s: 0.04;
    --e: 0.3;
    --ease: ease-out;
  }
  .cal-d.pick {
    --anim: dayPick;
    --s: 0.22;
    --e: 0.44;
  }
  .cal-d.pick .sd {
    --anim: dotIn;
    --s: 0.44;
    --e: 0.58;
    --ease: cubic-bezier(0.2, 0.8, 0.3, 1.3);
  }
  .cal-date {
    --anim: keysIn;
    --s: 0.58;
    --e: 0.86;
    --ease: steps(18, end);
  }
  @keyframes tapDown {
    0% {
      opacity: 0;
      transform: scale(1.7);
    }
    35% {
      opacity: 1;
      transform: scale(1);
    }
    75% {
      opacity: 1;
    }
    100% {
      opacity: 0;
      transform: scale(0.9);
    }
  }
  @keyframes dayPick {
    from {
      background: var(--d-sky);
      color: var(--text);
      font-weight: 400;
    }
  }
  @keyframes dotIn {
    from {
      opacity: 0;
      transform: scale(0.1);
    }
  }
  @keyframes keysIn {
    from {
      clip-path: inset(0 100% 0 0);
    }
    to {
      clip-path: inset(0 0 0 0);
    }
  }
  .cal-date {
    display: inline-block;
  }

  /* ------------------------------------ two: the lecturer's one list */
  /*
    BLOCK BY BLOCK. The sheet opens, then the four worked days fill in one
    at a time, then the review line arrives and is ticked, then the button
    is held with the seconds counting, offered, pressed, and the stamp lands.
    Nothing here shares a window with anything else, and on the clock path
    the whole of it takes eight seconds, which is what it takes to read.
  */
  .demo-q {
    --clock: 8s;
  }
  .q-panel {
    --anim: openDown;
    --s: 0;
    --e: 0.1;
    --ease: ease-out;
  }
  .rh.on {
    --anim: cellOn;
    --s: calc(0.12 + var(--k, 0) * 0.07);
    --e: calc(0.17 + var(--k, 0) * 0.07);
  }
  .on0 {
    --k: 0;
  }
  .on1 {
    --k: 1;
  }
  .on2 {
    --k: 2;
  }
  .on3 {
    --k: 3;
  }
  .q-tick {
    --anim: fadeUp;
    --s: 0.42;
    --e: 0.48;
  }
  /* The box is ticked as its own beat, a cut, the way a checkbox is. */
  .tickbox {
    --anim: tickSet;
    --s: 0.5;
    --e: 0.52;
    --ease: steps(1, end);
  }
  /*
    Each reading of the button has a window of its own rather than a share of
    one, so each is on screen long enough to be read: the hold for a quarter
    of the screen's story, the offer for a sixth of it, the approved row for
    the last eighth and for the finished state. The windows abut, they do not
    overlap, and every join is a cut.
  */
  .q-held {
    --anim: holdShow;
    --s: 0.54;
    --e: 0.8;
  }
  .q-go {
    --anim: goShow;
    --s: 0.73;
    --e: 0.9;
  }
  .q-done {
    --anim: doneIn;
    --s: 0.865;
    --e: 1;
  }
  .q-sec {
    --anim: countdown;
    --s: 0.56;
    --e: 0.76;
  }
  /*
    After the cut, never across it. The stamp starts at nothing at 0.86 and
    the button is gone at 0.84, so there is no frame anywhere in this screen's
    scrub with APPROVED over a blue fill.
  */
  .q-stamp {
    --anim: press;
    --s: 0.9;
    --e: 1;
    --ease: cubic-bezier(0.2, 0.8, 0.3, 1.1);
  }
  @keyframes openDown {
    from {
      clip-path: inset(0 0 100% 0);
    }
    to {
      clip-path: inset(0 0 0 0);
    }
  }
  @keyframes cellOn {
    0%,
    49% {
      background: var(--d-ground);
      color: var(--muted);
    }
    50%,
    100% {
      background: var(--d-blue);
      color: #fff;
    }
  }
  @keyframes fadeUp {
    from {
      opacity: 0;
      transform: translateY(8px);
    }
  }
  @keyframes tickSet {
    from {
      background: var(--d-ground);
      box-shadow: inset 0 0 0 1px var(--d-ground-2);
      color: transparent;
    }
  }
  @keyframes holdShow {
    0% {
      opacity: 0;
    }
    2%,
    77% {
      opacity: 1;
    }
    78%,
    100% {
      opacity: 0;
    }
  }
  /*
    Three readings of one control, and two cuts. Held until 90% of the shared
    range, offered from there to just before the end, and the approved line
    for the last of it. Held and offered are the app's two states of the
    button; the third is the row ApprovalDashboard draws once there is nothing
    left to press.
  */
  @keyframes goShow {
    0%,
    4% {
      opacity: 0;
    }
    5%,
    84% {
      opacity: 1;
    }
    85%,
    100% {
      opacity: 0;
    }
  }
  @keyframes doneIn {
    0%,
    9% {
      opacity: 0;
    }
    10%,
    100% {
      opacity: 1;
    }
  }
  @keyframes countdown {
    from {
      --sec: 5;
    }
    to {
      --sec: 0;
    }
  }
  @keyframes press {
    0% {
      opacity: 0;
      transform: rotate(-8deg) scale(1.6);
    }
    72% {
      opacity: 0.96;
      transform: rotate(-8deg) scale(0.97);
    }
    100% {
      opacity: 0.92;
      transform: rotate(-8deg) scale(1);
    }
  }

  /* ------------------------------------------- the office's block, again */
  /*
    The one screen whose story is a thing finishing rather than a state, and
    the one the reader asked to be able to watch twice. While it is on the
    screen it comes off the scroll and on to a clock: the same parts, the
    same order, the same --s and --e, played over --demo-run and held for
    about three seconds before the fade takes it back to the beginning. Every
    other screen on this page is still scrubbed by the scroll, which is what
    makes this one worth reading twice.
  */
  /*
    A clock rather than a fade. The screen itself never changes opacity: this
    animation draws nothing and exists to end, which is the moment app.js
    puts the parts of this one screen back to their own first frame. What a
    reader sees reset is what the screen is about — the count, the pills and
    the bars — inside a frame that has not moved.
  */
  .demo-off.looping {
    animation: offClock var(--demo-cycle) linear both;
  }
  @keyframes offClock {
    from {
      --tick: 0;
    }
    to {
      --tick: 1;
    }
  }
  .demo-off.looping .m {
    animation-name: var(--anim);
    animation-timeline: auto;
    animation-duration: calc((var(--e, 1) - var(--s, 0)) * var(--demo-run));
    animation-delay: calc(var(--s, 0) * var(--demo-run));
    animation-fill-mode: both;
    animation-timing-function: var(--ease, ease);
  }

  /* --------------------------------- three: the office's block, finishing */
  .rr0 {
    --s: 0.06;
    --e: 0.52;
    --from: 0.62;
  }
  .rr1 {
    --s: 0.14;
    --e: 0.62;
    --from: 0.34;
  }
  .rr2 {
    --s: 0.2;
    --e: 0.68;
    --from: 0.78;
  }
  .rr3 {
    --s: 0.3;
    --e: 0.78;
    --from: 0.2;
  }
  .rr4 {
    --s: 0.38;
    --e: 0.86;
    --from: 0.48;
  }
  .bar i {
    --anim: barFill;
    --ease: cubic-bezier(0.2, 0.75, 0.25, 1);
  }
  /*
    The hours count with the bar beside them. Drawn finished the figure reads
    42 of 42, and it used to read that for the whole of the screen's story
    while the bar under it was two thirds along: a row saying "42 / 42" with a
    part-filled bar and a "Logging hours" pill is three answers to one
    question. Each row starts where its own bar starts, which is --from
    expressed as hours. --s and --e are the row's, inherited.
  */
  .rr0 .hc {
    --anim: rowA;
  }
  .rr1 .hc {
    --anim: rowB;
  }
  .rr2 .hc {
    --anim: rowC;
  }
  .rr3 .hc {
    --anim: rowD;
  }
  .rr4 .hc {
    --anim: rowE;
  }
  @keyframes rowA {
    from {
      --n: 26;
    }
    to {
      --n: 42;
    }
  }
  @keyframes rowB {
    from {
      --n: 14;
    }
    to {
      --n: 42;
    }
  }
  @keyframes rowC {
    from {
      --n: 33;
    }
    to {
      --n: 42;
    }
  }
  @keyframes rowD {
    from {
      --n: 8;
    }
    to {
      --n: 42;
    }
  }
  @keyframes rowE {
    from {
      --n: 20;
    }
    to {
      --n: 42;
    }
  }
  .p0 {
    --anim: pillA;
  }
  .p1 {
    --anim: pillB;
  }
  .p2 {
    --anim: pillC;
  }
  .at-a {
    --anim: attnA;
    --s: 0;
    --e: 0.92;
  }
  .at-b {
    --anim: attnB;
    --s: 0;
    --e: 0.92;
  }
  .cnt-in {
    --anim: countIn;
    --s: 0.08;
    --e: 0.88;
  }
  .cnt-look {
    --anim: countLook;
    --s: 0.08;
    --e: 0.7;
  }
  .cnt-new {
    --anim: countNew;
    --s: 0.08;
    --e: 0.5;
  }
  .off-dl {
    --anim: fadeUp;
    --s: 0.88;
    --e: 1;
    --ease: cubic-bezier(0.2, 0.8, 0.3, 1.2);
  }
  @keyframes barFill {
    from {
      transform: scaleX(var(--from, 0.5));
      background: var(--d-blue);
    }
    60% {
      background: var(--d-blue);
    }
    to {
      transform: scaleX(1);
      background: var(--d-green);
    }
  }
  /*
    WORDS OVER WORDS SWAP, THEY DO NOT CROSS-FADE.

    Each of these is two readings of one control, drawn one on top of the
    other, and a scrub can be parked anywhere: half way through a fade the
    reader gets both sets of letters at once, which is unreadable in a way a
    playing animation never shows you. So every pair below is a cut. Fades
    are kept for the things that are not text over text.
  */
  /*
    A CUT IS ONLY A CUT IF NOTHING INTERPOLATES ACROSS IT.

    Every pair below is written as two keyframes one per cent apart, which
    reads as a cut at playing speed and is a one-per-cent ramp when the
    reader is the one moving: park a scrub inside that window and both
    readings are on screen at a third and two thirds. A stepped timing
    function holds each keyframe's value to the end of its own interval, so
    the swap happens between two frames and never inside one. It is the same
    argument as the comment above, one level further down.
  */
  .p0,
  .p1,
  .p2,
  .at-a,
  .at-b,
  .sf-a,
  .sf-b,
  .sv-ok,
  .sv-bad,
  .q-held,
  .q-go,
  .q-done {
    --ease: steps(1, end);
  }
  @keyframes pillA {
    0%,
    46% {
      opacity: 1;
    }
    47%,
    100% {
      opacity: 0;
    }
  }
  @keyframes pillB {
    0%,
    46% {
      opacity: 0;
    }
    47%,
    87% {
      opacity: 1;
    }
    88%,
    100% {
      opacity: 0;
    }
  }
  @keyframes pillC {
    0%,
    87% {
      opacity: 0;
    }
    88%,
    100% {
      opacity: 1;
    }
  }
  @keyframes attnA {
    0%,
    85% {
      opacity: 1;
    }
    86%,
    100% {
      opacity: 0;
    }
  }
  @keyframes attnB {
    0%,
    85% {
      opacity: 0;
    }
    86%,
    100% {
      opacity: 1;
    }
  }
  @keyframes countIn {
    from {
      --n: 2;
    }
    to {
      --n: 5;
    }
  }
  @keyframes countLook {
    from {
      --n: 1;
    }
    to {
      --n: 0;
    }
  }
  @keyframes countNew {
    from {
      --n: 1;
    }
    to {
      --n: 0;
    }
  }

  /* ------------------------------------------------------------ the bento */
  /*
    Three checks, one after another: the duplicate, the overlap, the long
    day. Each takes a third of the screen's story, and each is the same two
    beats — the row that trips the check takes the amber, then the flag line
    comes up under it.
  */
  .bd-dup {
    --clock: 6.5s;
  }
  .dup2 {
    --anim: dupIn;
    --s: calc(0.02 + var(--g, 0) * 0.34);
    --e: calc(0.2 + var(--g, 0) * 0.34);
  }
  .dupf {
    --anim: fadeUp;
    --s: calc(0.21 + var(--g, 0) * 0.34);
    --e: calc(0.29 + var(--g, 0) * 0.34);
  }
  .g0 {
    --g: 0;
  }
  .g1 {
    --g: 1;
  }
  .g2 {
    --g: 2;
  }
  @keyframes dupIn {
    0% {
      opacity: 0;
      transform: translateY(10px);
      background: var(--paper);
      border-color: var(--rule);
      box-shadow: inset 0 0 0 rgb(138 66 8 / 0);
    }
    46% {
      opacity: 1;
      transform: none;
      background: var(--paper);
      border-color: var(--rule);
      box-shadow: inset 0 0 0 rgb(138 66 8 / 0);
    }
    100% {
      opacity: 1;
      transform: none;
    }
  }

  .sf-a {
    --anim: sealA;
    --s: 0.08;
    --e: 0.94;
  }
  .sf-b {
    --anim: sealB;
    --s: 0.08;
    --e: 0.94;
  }
  .seal-hex {
    --anim: sealHex;
    --s: 0.08;
    --e: 0.94;
  }
  .sv-ok {
    --anim: sealOk;
    --s: 0.08;
    --e: 0.94;
  }
  .sv-bad {
    --anim: sealBad;
    --s: 0.08;
    --e: 0.94;
  }
  @keyframes sealA {
    0%,
    16% {
      opacity: 1;
    }
    17%,
    62% {
      opacity: 0;
    }
    63%,
    100% {
      opacity: 1;
    }
  }
  @keyframes sealB {
    0%,
    16% {
      opacity: 0;
    }
    17%,
    62% {
      opacity: 1;
    }
    63%,
    100% {
      opacity: 0;
    }
  }
  @keyframes sealHex {
    0%,
    18% {
      color: var(--stamp);
      letter-spacing: 0.06em;
    }
    24%,
    64% {
      color: var(--d-red);
      letter-spacing: 0.1em;
    }
    76%,
    100% {
      color: var(--stamp);
      letter-spacing: 0.06em;
    }
  }
  @keyframes sealOk {
    0%,
    21% {
      opacity: 1;
    }
    22%,
    72% {
      opacity: 0;
    }
    73%,
    100% {
      opacity: 1;
    }
  }
  @keyframes sealBad {
    0%,
    21% {
      opacity: 0;
    }
    22%,
    72% {
      opacity: 1;
    }
    73%,
    100% {
      opacity: 0;
    }
  }

  .au {
    --anim: keysIn;
    --ease: steps(26, end);
  }
  .au0 {
    --s: 0.05;
    --e: 0.28;
  }
  .au1 {
    --s: 0.26;
    --e: 0.49;
  }
  .au2 {
    --s: 0.47;
    --e: 0.7;
  }
  .au3 {
    --s: 0.68;
    --e: 0.91;
  }

  /*
    The two databases are drawn from the start, so a tile waiting its turn in
    the bento is a picture rather than a heading over nothing; what plays is
    the wall between them, the tick and the line.
  */
  .db-bar {
    --anim: fadeInM;
    --s: 0.34;
    --e: 0.48;
  }
  .db-tick {
    --anim: pop;
    --s: 0.48;
    --e: 0.68;
    --ease: cubic-bezier(0.2, 0.8, 0.3, 1.3);
  }
  .db-say {
    --anim: fadeUp;
    --s: 0.64;
    --e: 0.82;
  }
  /* The three reminders, one after another, the way they are sent. */
  .nt0 {
    --anim: fadeUp;
    --s: 0.08;
    --e: 0.3;
  }
  .nt1 {
    --anim: fadeUp;
    --s: 0.38;
    --e: 0.6;
  }
  .nt2 {
    --anim: fadeUp;
    --s: 0.68;
    --e: 0.9;
  }
  @keyframes fadeInM {
    from {
      opacity: 0;
    }
  }
  @keyframes pop {
    from {
      opacity: 0;
      transform: scale(0.2);
    }
  }
  .db-tick {
    transform-origin: 110px 48px;
  }

  /* Forty-two cells now, so each one's share of the screen's story is smaller. */
  .bd-meter .bmeter i {
    --anim: bcell;
    --s: calc(0.04 + var(--i, 0) * 0.0155);
    --e: calc(0.22 + var(--i, 0) * 0.0155);
  }
  .bm-n {
    --anim: countHours;
    --s: 0.06;
    --e: 0.72;
  }
  @keyframes bcell {
    0% {
      background: var(--d-ground-2);
      transform: scaleY(0.4);
    }
    55% {
      background: var(--d-blue);
      transform: scaleY(1);
    }
    100% {
      background: var(--d-green);
      transform: scaleY(1);
    }
  }
  @keyframes countHours {
    from {
      --n: 0;
    }
    to {
      --n: 42;
    }
  }
  .bmeter i {
    transform-origin: center bottom;
  }



  /* Each counter's resting figure: the one the page reads before and after. */
  .cnt-in {
    --n: 5;
  }
  .cnt-look,
  .cnt-new {
    --n: 0;
  }
  .bm-n {
    --n: 42;
  }
  .tally-n {
    --n: 42;
  }
  .hc {
    --n: 42;
  }
  .q-sec {
    --sec: 0;
  }

  /* ------------------------------------------------------- the counters */
  /*
    A figure that changes is counted by the browser rather than by a script:
    a registered integer custom property is the one kind of custom property
    that interpolates, and a counter is the one way to print one. The markup
    carries the finished figure inside a span, which is what a reader sees
    with no motion, with no script, and in a browser that registers nothing.
  */
  /*
    The span with the finished figure stays in the text, transparent, and the
    counter is painted over it. So the page's own numbers can still be
    selected, found with Ctrl-F, copied into an email and read by a
    translator, which a figure that exists only as a counter cannot be. The
    span reserves the width of the finished figure, and the count grows into
    it. The seconds in the lecturer's held button are the one counter nobody
    copies, and stay a counter alone.
  */
  .cnt,
  .bm-n,
  .tally-n,
  .hc {
    position: relative;
  }
  .cnt > span,
  .bm-n > span,
  .tally-n > span,
  .hc > span {
    color: transparent;
  }
  .q-sec > span {
    display: none;
  }
  .cnt::after,
  .bm-n::after,
  .tally-n::after,
  .hc::after {
    content: counter(n);
    counter-reset: n var(--n);
    position: absolute;
    left: 0;
    top: 0;
  }
  .q-sec::after {
    content: counter(s);
    counter-reset: s var(--sec);
  }

  /* ---------------------------------------------- crossed off, in order */
  /*
    ONE CHAIN, NOT FOUR REVEALS AND FOUR STRIKES.

    The reader's note is the whole specification: it comes up, the first point
    is struck through, its answer comes up, and then it does that for the next
    one. So the four rows are not `reveal` blocks any more and nothing here is
    scrubbed against the scroll — a crossing out that follows a wheel happens
    at whatever speed somebody spins it, and in a browser with no scroll
    timelines it never happened at all, which is the browser this is read in.

    app.js puts `will-strike` on the ledger when it wires the chain up and
    `struck` on it when the ledger reaches the same seven-tenths line every
    other block on this page arrives at. Everything after that is one clock,
    0.85 seconds to the row, which the reader's second note ("slow it down
    so they can appreciate it") set:

        i*0.85   the row comes up, over 0.55s
        i*0.85 + 0.35   its line starts drawing, and takes 600ms
        i*0.85 + 0.85   its answer comes up as the line lands
        i*0.85 + 0.85   the next row starts coming up, as the answer above
                        it arrives

    With no script neither class is ever set and none of this applies: the
    phrases are struck and the answers are there, which is what the markup
    says and what reduced motion gets.
  */
  .r0 {
    --i: 0;
  }
  .r1 {
    --i: 1;
  }
  .r2 {
    --i: 2;
  }
  .r3 {
    --i: 3;
  }
  .swap.will-strike .row {
    opacity: 0;
    transform: translateY(14px);
  }
  .swap.struck .row {
    opacity: 1;
    transform: none;
    transition: opacity 0.55s ease calc(var(--i, 0) * 0.85s),
      transform 0.55s cubic-bezier(0.2, 0.75, 0.25, 1) calc(var(--i, 0) * 0.85s);
  }
  @media (min-width: 46rem) {
    .swap.will-strike dt > span::after {
      transform: scaleX(0);
    }
    .swap.struck dt > span::after {
      transform: none;
      transition: transform 0.6s ease-out calc(var(--i, 0) * 0.85s + 0.35s);
    }
  }
  /*
    Narrow, where a phrase can take two lines and one rule cannot cross them:
    the decoration arrives as a cut, on the same clock, where the drawn line
    would have landed.
  */
  @media (max-width: 45.99rem) {
    .swap.will-strike dt > span {
      text-decoration-color: var(--strike-off);
    }
    .swap.struck dt > span {
      text-decoration-color: var(--strike);
      transition: text-decoration-color 0.01s linear calc(var(--i, 0) * 0.85s + 0.7s);
    }
  }
  .swap.will-strike dd {
    opacity: 0;
  }
  .swap.struck dd {
    opacity: 1;
    transition: opacity 0.45s ease calc(var(--i, 0) * 0.85s + 0.85s);
  }

  /* ------------------------------------------- the bento, one tile at a time */
  /*
    THE TILES ALL ARRIVE, AND THE DRAWINGS TAKE TURNS.

    They were seven reveals on seven view timelines, which on a wide screen
    meant a whole row arriving together and three drawings starting at once;
    the reader's note was that they could not be appreciated, and then, more
    exactly, that every tile should show its first frame and the drawings
    should play one after another, each when the one before it has ended.

    app.js puts `will-play` on the grid when it wires this up and `seen` on
    every tile when the first of them reaches the seven-tenths line, with --k
    the tile's position, so they come up a short step apart, and --start the
    sum of the clocks of the drawings before it. A drawing is on the clock on
    both paths: this is the one place a screen is not scrubbed against the
    scroll, because a scrub cannot be told to wait its turn. Before its turn
    a drawing stands at its own first frame.

    With no script `will-play` is never set and none of this applies.
  */
  .bento.will-play .bt {
    opacity: 0;
    transform: translateY(16px);
  }
  .bento.will-play .bt.seen {
    opacity: 1;
    transform: none;
    transition: opacity 0.55s ease calc(var(--k, 0) * 0.12s),
      transform 0.55s cubic-bezier(0.2, 0.75, 0.25, 1) calc(var(--k, 0) * 0.12s);
  }
  .bento.will-play .bt .m {
    animation-name: none;
    animation-timeline: auto;
    animation-range: normal;
  }
  .bento.will-play .bt.seen .m {
    animation-name: var(--anim);
    animation-duration: calc((var(--e, 1) - var(--s, 0)) * var(--clock, 3.4s));
    animation-delay: calc(var(--s, 0) * var(--clock, 3.4s) + var(--start, 0s) + 0.6s);
    animation-fill-mode: both;
    animation-timing-function: var(--ease, ease);
  }

  /* ------------------------------------------ the four cautions, arriving */
  /*
    Left, right, left, right, a step apart, each ending exactly on the grid
    position it already has. They are four halves of one statement, and an
    alternation reads as one thing being said four ways where four blocks
    rising together reads as a list.

    They keep `reveal` in the markup so the observer path has something to
    watch; these rules are written after it and replace what it does.
  */
  .cd:nth-child(odd) {
    --dir: -1;
  }
  .cd:nth-child(even) {
    --dir: 1;
  }
  .cd:nth-child(1) {
    --i: 0;
  }
  .cd:nth-child(2) {
    --i: 1;
  }
  .cd:nth-child(3) {
    --i: 2;
  }
  .cd:nth-child(4) {
    --i: 3;
  }
  @keyframes cardIn {
    from {
      opacity: 0;
      transform: translateX(calc(var(--dir, -1) * 2.25rem));
    }
    to {
      opacity: 1;
      transform: none;
    }
  }
  @supports (animation-timeline: view()) {
    .cards {
      view-timeline-name: --cards;
    }
    .cards .cd {
      animation: cardIn linear both;
      animation-timeline: --cards;
      animation-range: cover calc(2vh + var(--i, 0) * 4vh) cover calc(30vh + var(--i, 0) * 4vh);
      animation-timing-function: cubic-bezier(0.2, 0.75, 0.25, 1);
    }
  }
  .observed .cards .cd {
    opacity: 0;
    transform: translateX(calc(var(--dir, -1) * 2.25rem));
    transition: opacity 0.55s ease calc(var(--i, 0) * 120ms),
      transform 0.55s cubic-bezier(0.2, 0.75, 0.25, 1) calc(var(--i, 0) * 120ms);
  }
  .observed .cards .cd.seen {
    opacity: 1;
    transform: none;
  }

  /* ----------------------------------------------- landing on a section */
  .section:target {
    animation: landed 1.8s ease both;
  }
  @keyframes landed {
    0%,
    25% {
      background-color: rgb(59 76 225 / 0.07);
    }
    100% {
      background-color: rgb(59 76 225 / 0);
    }
  }

  /* --------------------------------------------------- opening a question */
  @supports (interpolate-size: allow-keywords) {
    :root {
      interpolate-size: allow-keywords;
    }
    .qa details::details-content {
      block-size: 0;
      overflow: clip;
      transition: block-size 0.3s ease, content-visibility 0.3s allow-discrete;
    }
    .qa details[open]::details-content {
      block-size: auto;
    }
  }
  .qa summary::after {
    transition: transform 0.25s ease, border-color 0.25s ease;
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* --------------------------------------------- the properties that count */
/*
  Registered so they interpolate. An unregistered custom property is a token,
  and a token cannot be animated: it would snap from the first figure to the
  last half way through. The initial value of each is the FINISHED figure,
  because that is what the page reads before any of these has been asked to
  run and after every one of them has.
*/
@property --n {
  /*
    INHERITED, and this is the bug the first version of this shipped with.

    The figure is animated on the element and printed by its ::after, and a
    pseudo-element takes its inherited properties from the element it belongs
    to. Registered with inherits: false, ::after saw the initial value instead
    of the animated one, so every counter on the page read its resting figure
    for the whole of its own animation: "0 of 5" while five sheets came in.
  */
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}
@property --sec {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0;
}

/* ------------------------------------------------------------ cell indices */
/*
  Each cell's place in the queue, as a custom property the waves above read.
  It is written here rather than as a style attribute on every span because
  the production policy for this host is style-src 'self', which blocks an
  inline style attribute outright, and because the page has to fill in the
  right order with no script running at all.
*/
.meter .cell:nth-child(1){--i:0}
.meter .cell:nth-child(2){--i:1}
.meter .cell:nth-child(3){--i:2}
.meter .cell:nth-child(4){--i:3}
.meter .cell:nth-child(5){--i:4}
.meter .cell:nth-child(6){--i:5}
.meter .cell:nth-child(7){--i:6}
.meter .cell:nth-child(8){--i:7}
.meter .cell:nth-child(9){--i:8}
.meter .cell:nth-child(10){--i:9}
.meter .cell:nth-child(11){--i:10}
.meter .cell:nth-child(12){--i:11}
.meter .cell:nth-child(13){--i:12}
.meter .cell:nth-child(14){--i:13}
.meter .cell:nth-child(15){--i:14}
.meter .cell:nth-child(16){--i:15}
.meter .cell:nth-child(17){--i:16}
.meter .cell:nth-child(18){--i:17}
.meter .cell:nth-child(19){--i:18}
.meter .cell:nth-child(20){--i:19}
.meter .cell:nth-child(21){--i:20}
.meter .cell:nth-child(22){--i:21}
.meter .cell:nth-child(23){--i:22}
.meter .cell:nth-child(24){--i:23}
.meter .cell:nth-child(25){--i:24}
.meter .cell:nth-child(26){--i:25}
.meter .cell:nth-child(27){--i:26}
.meter .cell:nth-child(28){--i:27}
.meter .cell:nth-child(29){--i:28}
.meter .cell:nth-child(30){--i:29}
.meter .cell:nth-child(31){--i:30}
.meter .cell:nth-child(32){--i:31}
.meter .cell:nth-child(33){--i:32}
.meter .cell:nth-child(34){--i:33}
.meter .cell:nth-child(35){--i:34}
.meter .cell:nth-child(36){--i:35}
.meter .cell:nth-child(37){--i:36}
.meter .cell:nth-child(38){--i:37}
.meter .cell:nth-child(39){--i:38}
.meter .cell:nth-child(40){--i:39}
.meter .cell:nth-child(41){--i:40}
.meter .cell:nth-child(42){--i:41}
.close-meter span:nth-child(1){--i:0}
.close-meter span:nth-child(2){--i:1}
.close-meter span:nth-child(3){--i:2}
.close-meter span:nth-child(4){--i:3}
.close-meter span:nth-child(5){--i:4}
.close-meter span:nth-child(6){--i:5}
.close-meter span:nth-child(7){--i:6}
.close-meter span:nth-child(8){--i:7}
.close-meter span:nth-child(9){--i:8}
.close-meter span:nth-child(10){--i:9}
.close-meter span:nth-child(11){--i:10}
.close-meter span:nth-child(12){--i:11}
.close-meter span:nth-child(13){--i:12}
.close-meter span:nth-child(14){--i:13}
.bmeter i:nth-child(1){--i:0}
.bmeter i:nth-child(2){--i:1}
.bmeter i:nth-child(3){--i:2}
.bmeter i:nth-child(4){--i:3}
.bmeter i:nth-child(5){--i:4}
.bmeter i:nth-child(6){--i:5}
.bmeter i:nth-child(7){--i:6}
.bmeter i:nth-child(8){--i:7}
.bmeter i:nth-child(9){--i:8}
.bmeter i:nth-child(10){--i:9}
.bmeter i:nth-child(11){--i:10}
.bmeter i:nth-child(12){--i:11}
.bmeter i:nth-child(13){--i:12}
.bmeter i:nth-child(14){--i:13}
.bmeter i:nth-child(15){--i:14}
.bmeter i:nth-child(16){--i:15}
.bmeter i:nth-child(17){--i:16}
.bmeter i:nth-child(18){--i:17}
.bmeter i:nth-child(19){--i:18}
.bmeter i:nth-child(20){--i:19}
.bmeter i:nth-child(21){--i:20}
.bmeter i:nth-child(22){--i:21}
.bmeter i:nth-child(23){--i:22}
.bmeter i:nth-child(24){--i:23}
.bmeter i:nth-child(25){--i:24}
.bmeter i:nth-child(26){--i:25}
.bmeter i:nth-child(27){--i:26}
.bmeter i:nth-child(28){--i:27}
.bmeter i:nth-child(29){--i:28}
.bmeter i:nth-child(30){--i:29}
.bmeter i:nth-child(31){--i:30}
.bmeter i:nth-child(32){--i:31}
.bmeter i:nth-child(33){--i:32}
.bmeter i:nth-child(34){--i:33}
.bmeter i:nth-child(35){--i:34}
.bmeter i:nth-child(36){--i:35}
.bmeter i:nth-child(37){--i:36}
.bmeter i:nth-child(38){--i:37}
.bmeter i:nth-child(39){--i:38}
.bmeter i:nth-child(40){--i:39}
.bmeter i:nth-child(41){--i:40}
.bmeter i:nth-child(42){--i:41}
.supply li:nth-child(1){--i:0}
.supply li:nth-child(2){--i:1}
.supply li:nth-child(3){--i:2}
.supply li:nth-child(4){--i:3}
.supply li:nth-child(5){--i:4}
.supply li:nth-child(6){--i:5}
