:root {
    --bg: #EEF2F4;
    --paper: #FFFFFF;
    --ink: #1B2A34;
    --muted: #5E6E79;
    --line: #D5DDE3;
    /* The food safety box's own green: muted, so it reads as official rather
       than as a warning or an advert, and far enough from the page grey that
       the box's header and footer no longer blend into the background. */
    --safe-bg: #EEF5F0;
    --safe-line: #D2E3D7;
    --safe-ink: #2D6A47;
    --gut: 20px;
    --hover: rgba(255, 255, 255, .55);
    --fill: #CFDAE1;
    --warn: #F2DFAF;
    --beep: #FFC531;
    --beep-deep: #E0A400;
    --on-beep: #1B2A34;
    --radius: 14px;
    --maxw: 1080px;
    /* The rectangular blocks under the clock all sit on this. 728 is not
       arbitrary: it is the IAB leaderboard width, so the ad slot cannot be
       anything else, and having the settings bar and the text columns agree
       with it is cheaper than three near-identical numbers - 700, 720, 728 -
       that read as a mistake rather than a choice. */
    --col: 728px;
    /* Every control in the settings bar. Three different paddings used to
       produce three different heights. */
    --ctl-h: 38px;
    color-scheme: only light;
  }

  * { box-sizing: border-box }

  html {
    margin: 0;
    height: 100%;
    background: var(--bg);
    color-scheme: only light;
    -webkit-text-size-adjust: 100%;
  }

  body {
    margin: 0;
    min-height: 100%;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    font-family: "Manrope", system-ui, -apple-system, "Segoe UI", sans-serif;
    font-size: 17px;
    line-height: 1.5;
    color: var(--ink);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
    transition: background-color .4s ease;
  }

  a { color: inherit; text-decoration: none }
  a:hover { text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 3px }
  button { font: inherit; touch-action: manipulation; -webkit-tap-highlight-color: transparent }
  :focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; border-radius: 6px }

  .wrap { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gut) }

  /* main and the footer sit in their own stacking contexts at 1, and they come
     after the header in the DOM, so anything inside the header - the slide-in
     nav - was painted underneath them. The header has to outrank both. */
  main, .site-footer { position: relative; z-index: 1 }
  .site-header { position: relative; z-index: 60 }

  /* ---------- Header ---------- */
  .site-header { padding: 18px 0 }
  .site-header .wrap { display: flex; align-items: center; justify-content: space-between; gap: 20px }

  .brand { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 20px; letter-spacing: -.01em }
  .brand:hover { text-decoration: none }
  .brand svg { width: 28px; height: 28px; flex: none }

  .header-right { display: flex; align-items: center; gap: 8px; min-width: 0 }
  .site-nav { display: flex; gap: 6px; flex-wrap: wrap }
  .site-nav a { padding: 8px 12px; border-radius: 999px; font-weight: 600; font-size: 15px; color: var(--muted) }
  .site-nav a:hover { background: var(--paper); color: var(--ink); text-decoration: none }
  .site-nav a[aria-current] { background: var(--paper); color: var(--ink) }

  /* ---------- Confirm dialog ---------- */
  .ask {
    width: min(400px, calc(100vw - 32px));
    padding: 0; border: 0;
    background: transparent;
    font: inherit;
  }
  .ask::backdrop { background: rgba(27, 42, 52, .45) }
  .ask-card {
    padding: 22px;
    border: 1px solid var(--line); border-radius: 16px;
    background: var(--paper); color: var(--ink);
    box-shadow: 0 24px 60px rgba(27, 42, 52, .22);
  }
  .ask-title { margin: 0 0 6px; font-size: 19px; font-weight: 800; letter-spacing: -.01em; color: var(--ink) }
  .ask p { margin: 0 0 18px; font-size: 15px; line-height: 1.45; color: var(--muted) }
  .ask-actions { display: flex; gap: 8px; justify-content: flex-end }

  @media (prefers-reduced-motion: no-preference) {
    .ask[open] { animation: ask-in .18s cubic-bezier(.4, 0, .2, 1) both }
    @keyframes ask-in { from { opacity: 0; transform: translateY(8px) scale(.97) } }
  }

  .burger {
    display: none;
    width: 44px; height: 44px;
    border: 1px solid var(--line); border-radius: 12px;
    background: var(--paper); color: var(--ink);
    cursor: pointer; place-items: center; flex: none;
  }
  .burger svg { width: 22px; height: 22px }
  .burger .x { display: none }
  body.menu-open .burger .x { display: block }
  body.menu-open .burger .bars { display: none }

  .scrim {
    position: fixed; inset: 0; z-index: 50;
    background: rgba(27, 42, 52, .45);
    opacity: 0; visibility: hidden;
    transition: opacity .25s ease, visibility .25s;
  }
  body.menu-open .scrim { opacity: 1; visibility: visible }

  /* ---------- Timer ---------- */
  main { flex: 1; display: flex; flex-direction: column }
  .timer { padding: 52px 0 56px; text-align: center }
  .timer h1 { margin: 0 0 8px; font-size: clamp(26px, 3.4vw, 34px); font-weight: 800; letter-spacing: -.02em }
  .lede { max-width: 74ch; margin: 0 auto 26px; font-size: 17px; color: var(--muted) }

  .clock {
    position: relative;
    display: inline-block;
    max-width: 100%;
  }

  .display {
    position: relative;
    display: block;
    overflow: hidden;
    /* em-based side padding, held to a 22px floor: the caret needs about
       .105em to its right, and reserving it at every size is what stops the
       box changing width the moment you click into it. */
    padding: 10px max(22px, .11em);
    --r: 24px; --bw: 3px;
    /* One pixel of the border is drawn on top of the fill instead of around
       it, see .display::after. The declared border carries the rest, so the
       total weight is unchanged. */
    border: calc(var(--bw) - 1px) solid var(--ink);
    border-radius: var(--r);
    background: var(--paper);
    font-size: clamp(92px, 22vw, 280px);
    font-weight: 800; line-height: 1; letter-spacing: -.03em;
    font-variant-numeric: tabular-nums;
    cursor: text; user-select: none;
    transition: font-size .25s ease, background-color .2s ease, box-shadow .2s ease, opacity .3s ease;
  }
  .display.hours { font-size: clamp(64px, 16vw, 224px) }
  .display:hover { box-shadow: inset 0 0 0 4px var(--ink) }
  body.running .display, body.paused .display { cursor: pointer }
  body.running .display:hover, body.done .display:hover { box-shadow: none }
  .display.editing, .display.editing:hover { background: var(--paper); box-shadow: inset 0 0 0 4px var(--ink) }

  /* the digits are drawn twice: normal, then inverted and clipped to progress.
     the overlay is a pixel-identical copy of the whole field, so the two layers
     can never drift apart - only the clip moves. */
  .digits { position: relative; z-index: 1; display: block; white-space: nowrap }
  .display .digits { display: inline-block }

  /* The seam. The fill is a dark curve drawn over the paper background, so its
     anti-aliased edge is a blend of ink and white - lighter than the ink on
     both sides of it, which reads as a hairline gap in each corner. Matching
     radii cannot remove it; the blend is intrinsic.

     So the last pixel of the border is painted here, above the fill, exactly
     where the blend lands. The declared border is one pixel thinner to
     compensate, so nothing about the design changes. */
  .display::after {
    content: '';
    position: absolute; inset: 0;
    z-index: 5;
    pointer-events: none;
    border: 1px solid var(--ink);
    border-radius: calc(var(--r) - var(--bw) + 1px);
  }
  body.done .display::after { border-color: var(--on-beep) }

  .display .fill {
    position: absolute; inset: 0;
    z-index: 2;
    padding: inherit;
    /* The fill's own corners, matched to the inside of the border. Without
       this it is a square-cornered rectangle relying entirely on the parent's
       rounded overflow clip - and a clip applied to a composited layer is
       anti-aliased against the paper underneath, which left a hairline of
       white in each corner. Its own rounded edge removes the dependency. */
    border-radius: calc(var(--r) - var(--bw));
    background: var(--ink);
    color: var(--paper);
    clip-path: inset(0 100% 0 0);
    transition: background-color .6s ease;
    /* No will-change. It promoted the fill to its own layer, which is what
       caused the seam, and it bought nothing: Chrome does not run clip-path
       animations on the compositor anyway, so the sweep was always on the
       main thread. */
  }
  /* the fill runs as one linear animation for the whole duration, so the
     compositor owns it instead of a requestAnimationFrame loop. the implicit
     start frame is whatever clip-path the inline style holds, which is how
     resume picks up where pause left off. */
  @keyframes fill-up { to { clip-path: inset(0 0 0 0) } }
  body.warning .display .fill { background: var(--beep-deep) }
  body.paused .display { opacity: .55 }
  body.done .display .fill { background: var(--on-beep) }
  body.done .display { border-color: var(--on-beep) }

  .caret {
    position: absolute;
    top: 50%; right: -.075em;
    display: none;
    width: .038em; height: .84em;
    transform: translateY(-50%);
    background: var(--ink);
    border-radius: .012em;
  }
  .display.editing .caret { display: block; animation: caret 1.1s step-end infinite }
  .display .fill .caret { background: var(--paper) }
  @keyframes caret { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }
  @media (prefers-reduced-motion: reduce) { .display.editing .caret { animation: none } }

  .display .dim { color: var(--line) }
  .display .fill .dim { color: rgba(255, 255, 255, .3) }
  /* Manrope sets the colon .09em below the figure centre, which reads as
     sagging against 280px digits. Measured off the glyph, not guessed. */
  .display .sep { display: inline-block; transform: translateY(-.09em) }

  /* Dimmed rather than dropped: seconds ticking should be noticed only when
     looked for. Mostly on, so the dim reads as a tick and not a strobe. The
     digits are rewritten once a second, which restarts this animation, so the
     tick stays in phase with the clock for free. */
  body.running .display .sep { animation: blink 1s step-end infinite }
  @keyframes blink { 0% { opacity: 1 } 62% { opacity: .48 } }

  /* The finished state reuses the box the digits lived in: the fill has just
     run to 100%, so the slab is already solid ink. The digits fade out and the
     words take their place. Sized in em, so it tracks the display's own clamp
     and needs no separate breakpoints. */
  .done-face {
    position: absolute; inset: 0;
    z-index: 3;
    display: none; place-items: center;
    font-size: .5em; letter-spacing: -.02em;
    color: var(--beep);
    white-space: nowrap;
  }
  body.done .done-face { display: grid }
  .display .digits { transition: opacity .25s ease }
  body.done .display .digits { opacity: 0 }

  /* The cue face: SHAKE or TURN across the clock for the first seconds of a
     phase that has a cue. The same slot as the done face, in ink rather than
     the alarm colour - it is an instruction, not the end - and the digits
     underneath keep counting, so the time comes back where it should be.

     It carries its own fill, built the way the clock's is: the word twice, ink
     on paper underneath and paper on ink on top, with the top layer revealed
     left to right over the cue's seconds. So the face says how long it will
     stay, in the same language the clock uses for how long is left. */
  .cue-face {
    position: absolute; inset: 0;
    z-index: 3;
    display: none;
    /* its own ground: the clock's fill is ink, and an ink word over it
       disappeared into it */
    background: var(--paper); border-radius: inherit;
    font-size: .55em; font-weight: 800; letter-spacing: .02em;
    text-transform: uppercase;
    white-space: nowrap;
  }
  .cue-face .cue-word { display: grid; place-items: center; position: absolute; inset: 0; color: var(--ink) }
  .cue-face .cue-fill {
    position: absolute; inset: 0;
    border-radius: calc(var(--r) - var(--bw));
    background: var(--ink);
    clip-path: inset(0 100% 0 0);
  }
  .cue-face .cue-fill .cue-word { color: var(--paper) }
  body.cueing .cue-face { display: block }
  body.cueing .display .digits { opacity: 0 }

  .edit-pencil {
    position: absolute;
    right: -46px; top: 50%;
    width: 38px; height: 38px;
    transform: translateY(-50%);
    display: grid; place-items: center;
    border: 1px solid var(--line); border-radius: 50%;
    background: var(--paper); color: var(--muted);
    cursor: pointer;
    transition: color .2s ease, border-color .2s ease, opacity .2s ease, visibility .2s;
  }
  .edit-pencil:hover { color: var(--ink); border-color: var(--ink) }
  .edit-pencil svg { width: 17px; height: 17px }
  /* Gone whenever the clock is not idle, which now includes paused. The
     presets already give way to the +1/-1 row once it is running, so leaving
     the edit affordance up was the one control that did not follow the state.

     visibility, not just opacity: an opacity:0 button is still focusable and
     still announced, so a keyboard user could tab to a pencil that is not
     there - the same mistake the mobile menu had. */
  body.running .edit-pencil,
  body.paused .edit-pencil,
  body.done .edit-pencil { opacity: 0; visibility: hidden; pointer-events: none }
  /* Once you are editing, the pencil has done its job. It also stops the focus
     ring appearing round it: clicking it leaves it focused, and the first
     keystroke flips the browser into :focus-visible. */
  .display.editing + .edit-pencil { opacity: 0; visibility: hidden; pointer-events: none }

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

  /* off screen rather than aria-hidden: it still gets programmatic focus on
     touch to raise the numeric keyboard, and focusing a hidden-from-AT element
     is a violation. 16px stops iOS zooming when it takes focus. */
  .keypad-input {
    position: absolute; left: 0; top: 0;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%); overflow: hidden;
    font-size: 16px;
  }

  .hint {
    margin: 10px 0 0; height: 23px; overflow: hidden;
    font-size: 15px; line-height: 23px; color: var(--muted);
    white-space: nowrap; text-overflow: ellipsis;
  }

  /* only rendered for multi-segment sequences, so a plain countdown page never
     shows either of these */


  .seg-label {
    margin: 0 0 10px;
    font-size: 15px; font-weight: 700; letter-spacing: .02em;
    color: var(--muted);
  }
  body.done .seg-label { visibility: hidden }

  .dots { display: flex; justify-content: center; gap: 7px; margin: 16px 0 0; padding: 0; list-style: none }
  .dots li {
    width: 9px; height: 9px;
    border: 1.5px solid var(--fill); border-radius: 50%;
  }
  .dots li.past { background: var(--fill); border-color: var(--fill) }
  .dots li.now { background: var(--ink); border-color: var(--ink); transform: scale(1.25) }

  /* The phase strip, for a short named sequence. Two dots said "one of two"
     and nothing else; rice needs to say that the second one is ten minutes of
     resting, because that is the step people skip. Written out, the whole
     sequence is readable before it starts - and while it runs, the one in
     progress is the filled one. */
  .phases { gap: 0; flex-wrap: nowrap; margin-top: 18px }
  .phases li {
    position: relative;
    display: flex; align-items: center; gap: 9px;
    width: auto; height: auto;
    padding: 9px 15px 9px 12px;
    border: 1px solid var(--line); border-radius: 999px;
    background: var(--paper);
    transform: none;
    color: var(--muted);
    transition: color .2s ease, border-color .2s ease, background-color .2s ease;
  }

  /* The rail. Two pills with air between them read as two options - pick one -
     which is the opposite of what a rice timer means: the second phase is not
     an alternative to the first, it is what happens next. A line joining them
     says "and then", and it costs nothing but a pseudo-element. */
  .phases li + li { margin-left: 24px }
  .phases li + li::before {
    content: '';
    position: absolute; top: 50%; right: 100%;
    width: 24px; height: 3px;
    transform: translateY(-1.5px);
    /* --fill was too close to the page to read as a join at all. This is the
       body text colour held back, which is visible against the background
       without competing with the pills it connects. */
    background: var(--muted); opacity: .5;
  }
  /* Once the rail has been crossed it is as solid as the phases it joins. */
  .phases li.now::before, .phases li.past::before { background: var(--ink); opacity: 1 }

  /* The cue on the rail, when a segment has one: "Shake" between the two air
     fryer halves. Under the line rather than on it, so the row keeps its
     width - the pills already use nearly all of a phone - and centred on the
     24px rail, overflowing it evenly on both sides. em, so full screen scales
     it with the pills. */
  .phases li .cue {
    position: absolute; right: 100%; top: calc(50% + 1.5px);
    width: 24px;
    display: flex; flex-direction: column; align-items: center;
    font-size: .8em; font-weight: 700; white-space: nowrap;
    /* ink, not muted: it is an instruction, the one thing in the row that
       tells you to do something, and grey read as a footnote */
    color: var(--ink);
    pointer-events: none;
  }
  /* The stem: from the middle of the rail down to the word, so the cue reads
     as belonging to that join rather than floating under the row. It starts
     just below the rail's 3px line and is drawn in the rail's own colour, and
     turns solid with it once the first phase is done. 22px is half a pill
     plus the gap - the pill's height is fixed by its padding and 20px icon,
     so this is a measured number rather than a guess. */
  .phases li .cue::before {
    content: '';
    width: 2px; height: 22px; margin-bottom: 2px;
    background: var(--muted); opacity: .5;
  }
  .phases li.now .cue::before, .phases li.past .cue::before { background: var(--ink); opacity: 1 }
  /* On a projector the pills scale with the font and a fixed stem would no
     longer reach; the word alone is still clear there. */
  body.fs .phases li .cue::before { display: none }
  body.fs .phases li .cue { top: calc(100% + .3em) }
  .phases:has(.cue) { padding-bottom: 20px }

  /* Three or more phases on a phone: numbers only.

     Two pills with a name and a time each just fit a 375px screen. Four do
     not come close - the corn grill's Side 1 to Side 4 ran 150px off each
     edge. So from three phases up, below 640px, each pill keeps its number or
     icon and drops the words: the running phase's time is the big clock right
     above it, the cues still hang under the rails, and the buttons keep their
     full aria-label, so nothing is lost to a screen reader. */
  @media (max-width: 640px) {
    .phases:has(li:nth-child(3)) li { padding: 8px 10px; gap: 0 }
    .phases:has(li:nth-child(3)) li .t,
    .phases:has(li:nth-child(3)) li .d { display: none }
  }

  .phases li .ico {
    position: relative;
    display: grid; place-items: center;
    width: 20px; height: 20px; flex: none;
  }
  .phases li .ico > * { grid-area: 1 / 1 }
  .phases li img { width: 20px; height: 20px; opacity: .55 }
  .phases li .n {
    display: grid; place-items: center;
    width: 20px; height: 20px;
    border-radius: 999px;
    background: var(--fill); color: var(--ink);
    font-size: 12px; font-weight: 800;
  }

  /* Only on a phase that is over, and quiet enough that the eye finds the
     RUNNING one first - the tick is a receipt, not an announcement. */
  .phases li .tick { width: 18px; height: 18px; opacity: 0; color: var(--ink) }
  .phases li.past .tick { opacity: .75 }
  .phases li.past img, .phases li.past .n { opacity: 0 }
  .phases li .t { font-size: 15px; font-weight: 700; color: var(--ink) }
  .phases li .d { font-size: 15px; font-variant-numeric: tabular-nums }

  /* Running: the one in progress is the only filled thing in the row. */
  .phases li.now {
    background: var(--ink); border-color: var(--ink);
    color: rgba(255, 255, 255, .75);
    transform: none;
  }
  .phases li.now .t { color: var(--paper) }
  .phases li.now img { opacity: 1; filter: invert(1) }
  .phases li.now .n { background: var(--paper); color: var(--ink) }

  /* Not before it starts. On an idle clock the first phase is "now" only in
     the sense that it is next, and drawn filled it looked like the thing to
     press - a dark pill beside a yellow Start button pulls the eye the wrong
     way. So until the clock has run, every phase is drawn the same, and the
     filled one appears the moment there is something in progress to mark. */
  body:not(.running):not(.paused):not(.done) .phases li.now {
    background: var(--paper); border-color: var(--line); color: var(--muted);
  }
  body:not(.running):not(.paused):not(.done) .phases li.now .t { color: var(--ink) }
  body:not(.running):not(.paused):not(.done) .phases li.now img { opacity: .55; filter: none }
  body:not(.running):not(.paused):not(.done) .phases li.now .n { background: var(--fill); color: var(--ink) }

  /* Done: still readable, because "twelve minutes of simmering happened" is
     part of what the page is telling you. Dimmed as a whole rather than
     greyed piece by piece, so the tick fades with it instead of floating. */
  .phases li.past { background: var(--bg); border-color: var(--line); opacity: .65 }
  .phases li.past .t { color: var(--ink) }

  /* The caption over the preset row, where the row picks a thing rather than a
     length of time. */
  .presets-label {
    margin: 24px 0 0;
    font-size: 17px; font-weight: 800; letter-spacing: -.01em;
    color: var(--ink);
  }
  body.done .dots:not(.phases) li { background: var(--on-beep); border-color: var(--on-beep) }
  /* Phases on the yellow screen. The dot rule above filled them solid too,
     and on a pill that also carries .past - itself dimmed to 65% - that came
     out as a muddy brown block with its label gone into it. Done means every
     phase is finished, so every pill shows finished: outlined in ink on the
     yellow, with the tick in place of its icon. */
  body.done .phases li,
  body.done .phases li.now,
  body.done .phases li.past {
    background: transparent; border-color: var(--on-beep);
    color: var(--on-beep); opacity: 1;
  }
  body.done .phases li .t { color: var(--on-beep) }
  body.done .phases li img, body.done .phases li .n { opacity: 0 }
  body.done .phases li .tick { opacity: 1; color: var(--on-beep) }
  body.done .phases li + li::before { background: var(--on-beep); opacity: 1 }
  body.done .phases li .cue { color: var(--on-beep) }
  body.done .phases li .cue::before { background: var(--on-beep); opacity: 1 }

  /* the honest line about what this browser cannot do, shown at the one moment
     it can still change what the visitor does */
  .note {
    max-width: 46ch; margin: 8px auto 0;
    font-size: 14px; line-height: 1.4; color: var(--muted);
  }
  body.done .note { color: var(--on-beep) }

  /* sequence pages have their times from config, so the digit entry goes away
     rather than sitting there doing nothing */
  body.no-edit .edit-pencil { display: none }
  body.no-edit .display { cursor: default }
  body.no-edit .display:hover { box-shadow: none }
  /* A preset that sets one duration has nothing useful to say to a sequence -
     it could only overwrite the first segment - so the row goes. A preset that
     carries a whole sequence is the opposite: on the rice page it IS how you
     choose, and hiding it left the page with a caption and no choices. */
  body.no-edit .presets:not(.has-seq) { display: none }
  /* and the caption goes with it, or the page is left with a question and no
     answers underneath */
  body.no-edit .presets-label:has(+ .presets:not(.has-seq)) { display: none }

  /* A phase page is a sequence page whose phases can each be set, so the
     pencil and the clickable clock come back - they edit the phase on the
     clock - while the rule above still hides single-duration presets, which
     have nothing to say to a sequence. */
  body.no-edit.phase-edit .edit-pencil { display: grid }
  body.no-edit.phase-edit .display { cursor: pointer }

  /* The pill is the button. It inherits everything from the li so that the
     running, past and now states keep drawing on the li as before. */
  .phases li .ph {
    display: flex; align-items: center; gap: inherit;
    margin: 0; padding: 0; border: 0; background: none;
    font: inherit; color: inherit; cursor: pointer;
  }
  .phases li:has(.ph) { cursor: pointer }
  .phases li:has(.ph):hover { border-color: var(--ink) }
  /* The phase the keypad is typing into. The same ring the display wears
     while editing, so the two read as one action. */
  .phases li.edit { border-color: var(--ink); box-shadow: inset 0 0 0 2px var(--ink) }

  .presets, .adjust { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin: 14px 0 28px }
  .presets button, .adjust button {
    padding: 9px 16px;
    border: 1px solid var(--line); border-radius: 999px;
    background: var(--paper); color: var(--ink);
    font-size: 15px; font-weight: 600; cursor: pointer;
  }
  .presets button:hover, .adjust button:hover { border-color: var(--ink) }

  /* The one you picked. Two pixels of ink rather than a tint, because these sit
     on white cards on a near-white page and a background wash barely registers
     - and because on the egg row the picture is the content, so the marker has
     to be around it rather than behind it. The extra pixel is taken out of the
     padding, so nothing moves when the selection changes. */
  .presets button[aria-pressed="true"] {
    border-width: 2px; border-color: var(--ink);
    padding: 8px 15px;
    color: var(--ink);
  }
  .presets button[aria-pressed="true"] small { color: var(--ink) }
  .presets.has-images button[aria-pressed="true"] { padding: 11px 9px 13px }
  /* A named preset carries its duration as well - "Soft" means nothing without
     "6 min" next to it, and the number is derived from the seconds the button
     sets, so the two cannot drift apart. */
  .presets button small { margin-left: 7px; font-weight: 500; color: var(--muted) }
  .presets button:hover small { color: var(--ink) }

  /* A preset with a picture. The pill becomes a card, and the row says three
     ways an egg can come out rather than three durations - which is the choice
     being made on that page. The duration stays, because "Jammy" without
     "8 min" is a mood, not a timer. */
  .presets.has-images { gap: 12px; margin: 18px 0 30px }
  .presets.has-images button {
    display: flex; flex-direction: column; align-items: center;
    width: 124px; padding: 12px 10px 14px; gap: 1px;
    border-radius: var(--radius);
  }
  .presets.has-images button img {
    width: 84px; height: 84px; margin-bottom: 6px;
    object-fit: contain;
    /* the cut-outs have no background of their own, so without this they float
       against the card instead of sitting on it */
    filter: drop-shadow(0 5px 7px rgba(27, 42, 52, .17));
  }
  .presets.has-images button small { margin-left: 0 }

  /* ---------- The second preset row ----------
     For a page whose choice has two axes: the shape row sets the time and this
     shifts it. It carries .presets as well as .mods, so every rule that hides a
     row of choices - a sequence page, a running clock, a sounding alarm -
     already covers it and none of them is repeated here.

     What is here is the difference in rank, and nothing else. The buttons are
     smaller, they never take pictures, and the caption is lighter than the one
     above it; two rows of equal weight would read as two unrelated questions
     rather than as one answer and its adjustment. */
  .mods-label { margin-top: 18px; font-size: 15px; font-weight: 700; color: var(--muted) }
  .mods { gap: 8px; margin: 10px 0 26px }
  .mods button { padding: 7px 15px; font-size: 14px }
  /* the two pixels of border the selected state adds come out of the padding,
     so choosing one does not nudge the other along the row */
  .mods button[aria-pressed="true"] { padding: 6px 14px }

  @media (prefers-reduced-motion: no-preference) {
    .presets.has-images button img { transition: transform .18s ease }
    .presets.has-images button:hover img { transform: scale(1.06) }
  }

  .adjust button { min-width: 104px; font-variant-numeric: tabular-nums }
  .adjust button[disabled] { opacity: .4; cursor: default }
  .adjust button[disabled]:hover { border-color: var(--line) }

  /* one row, two states: setup shows the presets, a live clock shows the nudges */
  .adjust { display: none }
  body.running .presets, body.paused .presets { display: none }
  /* The caption is a question about a choice - "How do you like it?" - and the
     choice is made. Leaving it above the +1 min row asks something the page is
     no longer offering to answer. */
  body.running .presets-label, body.paused .presets-label { display: none }
  body.running .adjust, body.paused .adjust { display: flex }
  body.done .adjust { display: none }

  /* asked for only once the timer has actually been left in a background tab,
     and only on a real click, so the browser never sees a gesture-less request */
  .notify-ask {
    display: none;
    align-items: center; gap: 10px;
    max-width: var(--col); margin: 14px auto 0; padding: 10px 14px;
    border: 1px solid var(--line); border-radius: 12px;
    background: var(--paper);
    font-size: 15px; color: var(--muted); text-align: left;
  }
  .notify-ask.show { display: flex }
  .notify-ask p { margin: 0; flex: 1 }
  .notify-ask button {
    flex: none; min-height: 40px; padding: 8px 14px;
    border: 1px solid var(--ink); border-radius: 8px;
    background: var(--paper); color: var(--ink);
    font-size: 15px; font-weight: 600; cursor: pointer;
  }
  .notify-ask button:hover { background: var(--ink); color: var(--paper) }
  .notify-ask .dismiss {
    min-height: 40px; padding: 8px 10px;
    border: 0; background: none; color: var(--muted); font-weight: 600;
  }
  .notify-ask .dismiss:hover { background: none; color: var(--ink); text-decoration: underline }
  body.done .notify-ask, body.fs .notify-ask { display: none }

  .controls {
    position: relative;
    display: flex; flex-direction: column; align-items: center; gap: 10px;
  }
  /* Reset does nothing to an idle timer, so it is not there. It arrives at the
     same moment the presets give way to the +1/-1 row - one state change
     rather than two - and it is display:none rather than merely invisible so
     it leaves the tab order too.

     It stays away in the finished state: the Start button reads "Stop" there
     and calls the same reset, so a second control would be a duplicate.

     display:none does shift the page on mobile, where the button is in the
     flow. That shift lands within 500ms of the Start press that caused it, so
     it is excluded from CLS - and the button just pressed does not move. */
  .controls .btn.quiet {
    position: absolute;
    left: 50%; top: 0;
    margin-left: 140px;
    height: 68px;
    display: none; align-items: center;
  }
  body.running .controls .btn.quiet,
  body.paused .controls .btn.quiet { display: inline-flex }

  .btn {
    padding: 16px 34px;
    border: 0; border-radius: 999px;
    background: var(--paper); color: var(--ink);
    box-shadow: 0 1px 0 var(--line);
    font-size: 18px; font-weight: 800; cursor: pointer;
  }
  .btn:hover { box-shadow: 0 0 0 2px var(--ink) inset }
  .btn.primary {
    min-width: 240px; min-height: 68px;
    padding: 18px 44px;
    font-size: 21px;
    background: var(--beep); color: var(--on-beep);
    box-shadow: none;
  }
  .btn.primary:active { transform: translateY(1px) }
  .btn.primary:hover { background: var(--beep-deep); box-shadow: none }
  .btn.quiet {
    padding: 10px 18px;
    background: transparent; box-shadow: none;
    font-size: 16px; font-weight: 600; color: var(--muted);
  }
  .btn.quiet:hover { color: var(--ink); box-shadow: none; text-decoration: underline; text-underline-offset: 4px }
  /* A second scale. The default .btn IS the hero Start button - 240px wide,
     68px tall - and inheriting that into a dialog is what made "Reset the
     timer" as loud as "Pause". Compact resets every dimension the hero sets
     rather than overriding some of them and leaving min-width behind. */
  .btn.compact {
    min-width: 0; min-height: 40px;
    padding: 9px 18px;
    box-shadow: 0 0 0 1px var(--line) inset;
    font-size: 15px; font-weight: 700;
  }
  .btn.compact:hover { box-shadow: 0 0 0 1px var(--ink) inset }

  /* Ink, not yellow. On this site yellow means start, and it means time's up.
     Lending it to a destructive confirm reads as encouragement to press it. */
  .btn.compact.solid { background: var(--ink); color: var(--paper); box-shadow: none }
  .btn.compact.solid:hover { background: #2A3D49; box-shadow: none }

  .btn[disabled] { opacity: .4; cursor: default; box-shadow: none }

  .fs-exit {
    display: none;
    align-items: center; gap: 8px;
    padding: 11px 20px;
    border: 1px solid var(--line);
    background: var(--paper); color: var(--muted);
    font-size: 15px; font-weight: 600; box-shadow: none;
  }
  .fs-exit:hover { color: var(--ink); border-color: var(--ink); box-shadow: none }
  .fs-exit svg { width: 16px; height: 16px }
  .fs-tip { display: none; margin: 2px 0 0; font-size: 14px; color: var(--muted) }
  body.fs .fs-exit { display: inline-flex }
  body.fs .fs-tip { display: block }
  body.done .fs-exit, body.done .fs-tip { display: none }

  /* ---------- Settings ---------- */
  .settings {
    display: grid;
    grid-template-columns: auto auto 1fr;
    align-items: center;
    gap: 16px;
    max-width: var(--col); margin: 32px auto 0; padding: 14px 18px;
    border: 1px solid var(--line); border-radius: 12px;
    background: var(--paper);
    font-size: 15px; color: var(--muted);
    text-align: left;
  }
  .settings label { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; white-space: nowrap }
  .settings label:hover { color: var(--ink) }
  .settings label.disabled { opacity: .4; cursor: not-allowed }
  .settings label.disabled:hover { color: var(--muted) }
  .settings .field { display: inline-flex; align-items: center; gap: 9px }
  .settings select {
    height: var(--ctl-h);
    padding: 0 11px;
    border: 1px solid var(--line); border-radius: 8px;
    background: var(--bg); color: var(--ink);
    font: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
  }
  .settings select:hover { border-color: var(--ink) }
  .settings input[type=checkbox] { width: 17px; height: 17px; accent-color: var(--ink); flex: none }
  .settings .fs-btn {
    justify-self: end;
    display: inline-flex; align-items: center; gap: 8px;
    height: var(--ctl-h); padding: 0 16px;
    border: 1px solid var(--line); border-radius: 8px;
    background: var(--bg); color: var(--ink);
    font-size: 15px; font-weight: 600; white-space: nowrap; cursor: pointer;
  }
  .settings .fs-btn:hover { border-color: var(--ink) }

  /* the waves and the word say the same thing, so it reads at a glance and is
     still unambiguous when you are choosing rather than recognising */
  .settings .field-controls { display: inline-flex; align-items: center; gap: 8px; min-width: 0 }
  .settings .vol {
    display: inline-flex; align-items: center; gap: 7px; flex: none;
    height: var(--ctl-h); padding: 0 11px;
    border: 1px solid var(--line); border-radius: 8px;
    background: var(--bg); color: var(--ink);
    font-size: 14px; font-weight: 600; cursor: pointer;
  }
  .settings .vol:hover { border-color: var(--ink) }
  .settings .vol svg { width: 18px; height: 18px; flex: none }
  /* fixed slot: the icon stays put and the button keeps one width, so cycling
     the level does not shuffle the rest of the bar sideways */
  .settings .vol span { display: inline-block; width: 34px; text-align: left }
  .settings .vol[disabled] { opacity: .4; cursor: not-allowed }
  .settings .vol[disabled]:hover { border-color: var(--line) }
  .settings .vol .w1, .settings .vol .w2, .settings .vol .w3 { opacity: .2 }
  .settings .vol[data-vol="low"] .w1,
  .settings .vol[data-vol="mid"] .w1, .settings .vol[data-vol="mid"] .w2,
  .settings .vol[data-vol="high"] .w1, .settings .vol[data-vol="high"] .w2, .settings .vol[data-vol="high"] .w3 { opacity: 1 }
  .settings .fs-btn svg { width: 16px; height: 16px }
  .settings .fs-btn .exit { display: none }
  body.fs-active .fs-btn .enter { display: none }
  body.fs-active .fs-btn .exit { display: block }

  /* ---------- Done ---------- */
  body.done { background: var(--beep); color: var(--on-beep) }
  body.done .hint, body.done .brand { color: var(--on-beep) }
  body.done .presets, body.done .presets-label, body.done .settings { visibility: hidden }
  body.done .site-nav a { color: var(--on-beep) }
  body.done .burger { background: transparent; border-color: rgba(27, 42, 52, .3); color: var(--on-beep) }
  body.done .btn.primary { background: var(--on-beep); color: var(--beep) }
  body.done .btn.quiet { color: var(--on-beep) }

  /* which timer finished carries the detail now, so it gets the size to match */
  body.done .hint { height: auto; margin-top: 18px; font-size: 18px; font-weight: 600 }

  @media (prefers-reduced-motion: no-preference) {
    body.done .clock { animation: pulse 1.6s ease-in-out infinite }
    body.done.flash { animation: flash .5s steps(1) 6 }
    @keyframes pulse { 0%, 100% { transform: scale(1) } 50% { transform: scale(1.02) } }
    @keyframes flash { 0%, 50% { background: var(--beep) } 51%, 100% { background: #FFF1C9 } }
  }
  @media (prefers-reduced-motion: reduce) {
    body.running .display .sep { animation: none }
  }

  /* ---------- Full screen ---------- */
  body.fs .site-header,
  body.fs .site-footer,
  body.fs .definition,
  body.fs .lookup,
  body.fs .facts,
  body.fs .steps,
  body.fs .intro,
  body.fs .about,
  body.fs .uses,
  body.fs .faq,
  body.fs .more,
  body.fs .presets,
  body.fs .presets-label,
  body.fs .adjust,
  body.fs .settings,
  body.fs .edit-pencil,
  body.fs .timer h1,
  body.fs .lede,
  body.fs .note,
  body.fs .hint { display: none }
  body.fs main { justify-content: center; min-height: 100dvh }
  body.fs .timer { display: flex; flex-direction: column; justify-content: center; padding: 0 }
  body.fs .wrap { max-width: none; padding: 0 3vw }
  body.fs .clock { display: block; width: 100% }
  /* the label and the round dots stay: on a projector they are the only thing
     telling the room which part of the sequence it is in */
  body.fs .seg-label { font-size: clamp(18px, 2.4vw, 30px); margin-bottom: 2vh }
  body.fs .dots { margin-top: 2.5vh }
  /* Round dots only. This rule used to hit every child of .dots, and once the
     phase strip started borrowing that element it squashed each named phase
     into a 14px square - the flame survived, "Simmer 12:00" did not. */
  body.fs .dots:not(.phases) li { width: 14px; height: 14px }

  /* The phase strip on a projector. Everything scales off the font size, so
     one line sets how big the whole strip is at the back of a room. */
  body.fs .phases { font-size: clamp(16px, 1.7vw, 26px) }
  body.fs .phases li { padding: .5em .9em .5em .65em; gap: .5em }
  body.fs .phases li .t, body.fs .phases li .d { font-size: 1em }
  body.fs .phases li .ico, body.fs .phases li img { width: 1.3em; height: 1.3em }
  body.fs .phases li .n { width: 1.3em; height: 1.3em; font-size: .7em }
  body.fs .phases li .tick { width: 1.15em; height: 1.15em }
  body.fs .phases li + li { margin-left: 1.5em }
  body.fs .phases li + li::before { width: 1.5em; height: .16em; transform: translateY(-.08em) }
  body.fs .display {
    padding: .06em .11em;
    --r: .1em; --bw: 4px;
    border-width: var(--bw); border-radius: var(--r);
    font-size: min(29vw, 46vh);
  }
  body.fs .display.hours { font-size: min(23vw, 34vh) }
  body.fs .controls { margin-top: 4vh }

  /* ---------- Ad slots ---------- */
  /* Boxed rather than bare: an empty slot then reads as space held for
     something, instead of looking like a hole in the layout. */
  .ad {
    display: flex; align-items: center; justify-content: center;
    margin: 0 auto;
    border: 1px solid var(--line); border-radius: 12px;
    background: var(--paper);
    color: var(--line); font-size: 12px; letter-spacing: .08em; text-transform: uppercase;
    overflow: hidden;
  }
  .ad-leaderboard { width: 100%; max-width: var(--col); min-height: 90px; margin: 34px auto 0 }
  .ad-inline { width: 100%; max-width: var(--col); min-height: 90px; margin: 8px auto 40px }
  body.fs .ad { display: none }
  body.done .ad { visibility: hidden }

  /* ---------- Lookup pages ----------
     The egg, pasta and tea pages share a shape: a short timer with named
     presets, and a table that is the actual reason to visit. Shared chrome,
     not page decoration - every one of those pages uses it.
     (body.fs .lookup is in the full screen hide list above.) */
  .lookup { padding: 0 0 8px }
  .lookup > .wrap { position: relative; padding-top: 40px; max-width: var(--col) }
  .lookup > .wrap::before {
    content: '';
    position: absolute; top: 0;
    left: var(--gut); right: var(--gut);
    height: 1px; background: var(--line);
  }
  .lookup h2 { margin: 0 0 10px; font-size: 22px; font-weight: 800; letter-spacing: -.01em }
  .lookup-note { margin: 0 0 18px; max-width: 64ch; font-size: 16px; line-height: 1.5; color: var(--muted) }
  .lookup-note:last-child { margin: 18px 0 0 }

  /* What the official guidance says - inc/facts.php.

     Built to read like a reference card rather than a tip: a header that
     names what it is, each rule on its own row with the subject in a column
     of its own, the source under every rule rather than in a footnote, and a
     dated line at the foot saying it was checked. Neutral lines only - an
     accent colour made it look like a promotion, which is the opposite of
     what a box of official figures is for. */
  /* room on both sides: it is its own block, not the tail of the table */
  .facts { padding: 48px 0 40px }
  /* The full width of the page, the same as the three columns below it, and
     the width spent on columns of rules rather than on longer lines: in the
     table's narrow column the box was one long list and the tallest thing
     on the page after the table itself. */
  .facts-card {
    background: var(--paper);
    border: 1px solid var(--safe-line);
    border-radius: 14px;
    overflow: hidden;
  }
  .facts-head {
    display: flex; align-items: center; gap: 14px;
    padding: 16px 24px;
    background: var(--safe-bg);
    border-bottom: 1px solid var(--safe-line);
  }
  .facts-icon { width: 30px; height: 30px; flex: none; color: var(--safe-ink) }
  .facts-kicker {
    margin: 0 0 1px;
    font-size: 13px; font-weight: 700;
    color: var(--safe-ink);
  }
  .facts h2 { margin: 0; font-size: 18px; font-weight: 800; letter-spacing: -.01em }
  .facts-list {
    display: grid; grid-template-columns: repeat(2, 1fr);
    margin: 0; padding: 0;
  }
  /* A grid of rules, two across. The lines between them are each row's top
     and left edge, drawn only where there is a neighbour, so the grid has
     inner rules and no doubled border against the card's own. */
  .facts-row { padding: 16px 24px 18px; border-top: 1px solid var(--line) }
  .facts-row:nth-child(-n+2) { border-top: 0 }
  .facts-row:nth-child(2n) { border-left: 1px solid var(--line) }
  /* an odd one out takes the whole last row rather than leaving a hole */
  .facts-row:last-child:nth-child(odd) { grid-column: 1 / -1 }
  .facts-row dt { margin: 0 0 4px; font-size: 15px; font-weight: 800; line-height: 1.4; color: var(--ink) }
  .facts-row dd { margin: 0 }
  .facts-row dd p { margin: 0; font-size: 15px; line-height: 1.55; color: var(--ink) }
  .facts-src {
    display: inline-block; margin-top: 5px;
    font-size: 13px; font-weight: 600; color: var(--muted);
    text-decoration: none;
  }
  .facts-src:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px }
  .facts-foot {
    display: flex; align-items: center; gap: 8px;
    margin: 0; padding: 11px 24px;
    background: var(--safe-bg);
    border-top: 1px solid var(--safe-line);
    font-size: 13px; color: var(--safe-ink);
  }
  .facts-foot svg { width: 15px; height: 15px; flex: none; color: var(--safe-ink) }
  .facts-foot a { color: inherit; font-weight: 700; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px }
  @media (max-width: 640px) {
    .facts { padding: 32px 0 28px }
    .facts-head { padding: 14px 16px; gap: 12px }
    .facts-list { grid-template-columns: 1fr }
    .facts-row { padding: 13px 16px }
    .facts-row:nth-child(2) { border-top: 1px solid var(--line) }
    .facts-row:nth-child(2n) { border-left: 0 }
    .facts-foot { padding: 10px 16px }
  }
  /* A note is one paragraph on most pages and :last-child gives it its top
     margin. Broken into several it is no longer last, so the first one lands
     flush against the table and reads as a caption on the bottom row rather
     than as the prose after it. */
  .times + .lookup-note { margin-top: 18px }
  .lookup-note b { color: var(--ink); font-weight: 700 }

  /* The picture strip. Six eggs, five pasta shapes, four cuts of steak: a
     result you recognise, next to the number that produced it. The column
     count comes from the partial as --cols, so a strip of five needs no rule
     of its own; below 800px the captions start wrapping to three lines at six
     across, and rows of three read better than six squeezed ones. */
  .strip {
    list-style: none;
    margin: 22px 0 30px;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(var(--cols, 6), 1fr);
    gap: 18px 12px;
  }

  .strip li { text-align: center }

  .strip img {
    display: block;
    width: 100%;
    max-width: 124px;
    height: auto;
    margin: 0 auto 9px;
    /* these are subjects masked out of a photograph, with no ground of their
       own; without a shadow they hover, and six hovering eggs read as clip-art */
    filter: drop-shadow(0 4px 7px rgba(27, 42, 52, .16));
  }

  .strip b {
    display: block;
    font-size: 15px;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
  }

  .strip span {
    display: block;
    margin-top: 1px;
    font-size: 13.5px;
    line-height: 1.35;
    color: var(--muted);
  }

  .times {
    width: 100%;
    border-collapse: separate; border-spacing: 0;
    border: 1px solid var(--line); border-radius: 12px;
    background: var(--paper);
    overflow: hidden;
    font-size: 16px; text-align: left;
  }
  .times th, .times td { padding: 12px 16px; vertical-align: baseline }
  .times thead th {
    font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: var(--muted);
    border-bottom: 1px solid var(--line);
  }
  .times tbody th {
    width: 1%; white-space: nowrap;
    font-weight: 800; font-variant-numeric: tabular-nums;
  }
  /* A row that shows what it is about. The picture sits in front of the name in
     the row header rather than in a column of its own, because a column would
     want a heading and there is no honest word for it that is not the row
     header again. */
  .times tbody th img {
    width: 58px; height: 36px;
    object-fit: contain;
    vertical-align: middle;
    margin: -2px 12px 0 0;
  }
  .times tbody td { color: var(--muted) }
  .times tbody td b { color: var(--ink); font-weight: 700 }
  .times tbody tr + tr th, .times tbody tr + tr td { border-top: 1px solid var(--line) }

  /* ---------- Method steps ----------
     Three drawings and three sentences, for the part of a page that is a
     procedure rather than a time. Numbered by the counter rather than by <ol>'s
     own markers, so the number sits on the picture instead of outside it. */
  /* Full page width, not the --col reading measure the table and the questions
     use. Those are text you read down; this is a row of three pictures you
     compare across, and the same block that holds the prose columns further
     down the page - so the two three-column sections agreeing matters more
     than this one agreeing with the paragraphs. It also buys each drawing
     about a hundred pixels, which is the difference between a pan you can see
     and a pan you can identify. */
  /* 56 at the bottom is the timer section's own figure, and the divider that
     opens the next block sits 40 below whatever precedes it - so this is the
     same 96px of air every other section boundary on the page has. At 10 the
     line landed almost against the last line of step three and read as an
     underline on the text rather than the start of the table. */
  .steps { padding: 34px 0 56px }
  .steps h2 { margin: 0 0 22px; font-size: 22px; font-weight: 800; letter-spacing: -.01em; text-align: center }
  .steps ol {
    counter-reset: step;
    list-style: none;
    margin: 0; padding: 0;
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 26px 40px;
  }
  .steps li { counter-increment: step; position: relative }
  .steps img {
    display: block;
    width: 100%; max-width: 184px; height: auto;
    /* centred over its own column rather than flush left: the drawings are
       round-ish objects on transparent ground, and left-aligning three of them
       lines up their bounding boxes while the pans they contain sit at three
       different distances from the edge */
    margin: 0 auto 12px;
    /* A corner, for the pages whose steps are photographs rather than cut-out
       artwork. It lived in pasta-timer/page.css, then in tea-timer/page.css as
       well, and a third copy for steak is where duplicating one cosmetic line
       stops being cheaper than the version bump that moving it costs.

       Harmless on the pages that do not need it: a radius on a cut-out with
       transparent corners rounds nothing, because there is no edge there to
       round. So it applies to every steps block and only shows on three. */
    border-radius: 14px;
  }
  .steps h3 {
    display: flex; align-items: center; gap: 9px;
    margin: 0 0 6px;
    font-size: 17px; font-weight: 800; letter-spacing: -.01em;
  }
  .steps h3::before {
    content: counter(step);
    flex: none;
    width: 22px; height: 22px;
    border-radius: 999px;
    background: var(--ink); color: var(--paper);
    font-size: 13px; font-weight: 800;
    display: grid; place-items: center;
  }
  .steps p { margin: 0; font-size: 16px; line-height: 1.5; color: var(--muted) }

  /* ---------- Content ---------- */
  /* Section dividers line up with the columns rather than running to the
     viewport edge: the rule is drawn inside the wrap's gutter, so it starts and
     stops exactly where the text does at every breakpoint. */
  .definition > .wrap::before, .site-footer > .wrap::before {
    content: '';
    position: absolute; top: 0;
    left: var(--gut); right: var(--gut);
    height: 1px; background: var(--line);
  }
  .about > .wrap { padding-top: 24px }
  .site-footer > .wrap { position: relative; padding-top: 40px }


  /* A short factual definition rather than a marketing paragraph. It exists
     because an answer engine otherwise has to assemble what this product IS
     from six scattered sections. */
  .definition { padding: 0 0 4px }
  .definition > .wrap { position: relative; padding-top: 40px }
  .definition h2 { margin: 0 0 10px; font-size: 22px; font-weight: 800 }
  .definition p { margin: 0; max-width: 68ch; font-size: 17px; color: var(--muted) }

  .about { padding: 0 0 48px }
  .about .wrap { display: grid; grid-template-columns: repeat(3, 1fr); gap: 40px }
  .about h2 { margin: 0 0 8px; font-size: 18px; font-weight: 800 }
  .about p { margin: 0 0 12px; max-width: 36ch; font-size: 16px; color: var(--muted) }
  .about p:last-child { margin-bottom: 0 }

  kbd {
    padding: 1px 6px;
    border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px;
    background: var(--paper); color: var(--ink);
    font: inherit; font-size: 13px; font-weight: 700;
  }

  .uses { padding: 0 0 48px }
  .uses .wrap { max-width: var(--col) }
  .uses h2 { margin: 0 0 12px; font-size: 22px; font-weight: 800 }
  .uses p { margin: 0 0 14px; max-width: 66ch; color: var(--muted) }

  .faq { padding: 0 0 48px }
  .faq .wrap { max-width: var(--col) }
  .faq h2 { margin: 0 0 18px; font-size: 22px; font-weight: 800 }
  .faq details { padding: 14px 0; border-top: 1px solid var(--line) }
  .faq details:last-of-type { border-bottom: 1px solid var(--line) }
  .faq summary {
    position: relative;
    padding-right: 38px;
    list-style: none; font-weight: 700; cursor: pointer;
  }
  .faq summary::-webkit-details-marker { display: none }

  /* Two bars rather than a swapped "+" and "–" glyph: the vertical one rotates
     flat, so the plus becomes a minus as a movement instead of a jump cut. */
  .faq summary::before, .faq summary::after {
    content: '';
    position: absolute; right: 5px; top: 50%;
    width: 15px; height: 2.5px; border-radius: 2px;
    background: var(--muted);
  }
  .faq summary::before { transform: translateY(-50%) }
  .faq summary::after { transform: translateY(-50%) rotate(90deg) }
  .faq details[open] summary::after { transform: translateY(-50%) rotate(0deg) }
  /* the marker darkens on hover, the heading never changes: going lighter on
     hover reduces the affordance instead of raising it */
  .faq summary:hover::before, .faq summary:hover::after { background: var(--ink) }

  .faq details p { margin: 10px 0 0; max-width: 62ch; color: var(--muted) }

  @media (prefers-reduced-motion: no-preference) {
    .faq summary::after { transition: transform .28s cubic-bezier(.4, 0, .2, 1) }
    /* the answer eases in on open; there is no matching close animation
       because details collapses instantly and faking it needs JS */
    .faq details[open] p { animation: reveal .26s ease-out both }
    @keyframes reveal { from { opacity: 0; transform: translateY(-6px) } }
  }

  /* A hub's own heading block, for the pages that have no clock under it. */
  .intro { padding: 48px 0 0; text-align: center }
  .intro h1 { margin: 0 0 8px; font-size: clamp(26px, 3.4vw, 34px); font-weight: 800; letter-spacing: -.02em }
  .intro .lede { margin-bottom: 0 }

  .more { padding: 0 0 64px }
  /* A hub's grid is the page rather than a footnote to it: three across leaves
     two clean rows for six cards, where four across leaves a ragged one. */
  .more.hub { padding: 32px 0 56px }
  .more.hub a { padding: 14px 16px }
  .more h2 { margin: 0 0 14px; font-size: 22px; font-weight: 800 }
  /* Three across, on the hub and at the foot of a page alike: with a thumb in
     front of the label, four across left "Air fryer chicken breast" on three
     lines in a card a quarter of the width. */
  .more ul { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 0; padding: 0; list-style: none }
  .more a {
    display: block; padding: 16px 18px;
    border: 1px solid transparent; border-radius: var(--radius);
    background: var(--paper); font-weight: 600;
  }
  .more a:hover { border-color: var(--ink); text-decoration: none }
  .more a small { display: block; margin-top: 2px; font-size: 14px; font-weight: 500; color: var(--muted) }
  /* The thumb sits left of the text and the card keeps its own padding, so a
     card without one (interval, the /#5m kind) lines up with the rest by its
     text box rather than by a gap where a picture would be. */
  .more a.has-thumb { display: flex; align-items: center; gap: 14px; padding-top: 10px; padding-bottom: 10px; padding-left: 12px }
  .more a.has-thumb img { flex: none; width: 56px; height: 56px; object-fit: contain }
  .more a.has-thumb span { min-width: 0 }

  /* ---------- Footer ---------- */
  .site-footer { padding: 0 0 32px; font-size: 14px; color: var(--muted) }
  .site-footer .wrap { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap }
  .site-footer nav { display: flex; gap: 18px }

  /* ---------- Responsive ---------- */
  @media (max-width: 820px) {
    .about .wrap { grid-template-columns: 1fr; gap: 24px }
    .more ul { grid-template-columns: repeat(2, 1fr) }
  }

  /* 800 rather than 820: the strip is inside .lookup, which is narrower than
     the page, so it survives slightly further down than the column grid. */
  @media (max-width: 800px) {
    .strip { grid-template-columns: repeat(3, 1fr); gap: 20px 14px }
  }

  @media (max-width: 640px) {
    /* one step at a time, picture beside the words rather than above them:
       three stacked drawings at full width is a lot of scrolling for three
       sentences */
    .steps ol { grid-template-columns: 1fr; gap: 20px }
    .steps li { display: grid; grid-template-columns: 76px 1fr; gap: 0 16px; align-items: start }
    .steps img { grid-row: span 2; margin: 0; max-width: 76px }
  }

  @media (max-width: 640px) {
    body { font-size: 16px }
    :root { --gut: 16px; --ctl-h: 46px }

    .site-header { padding: 12px 0 }
    .site-header .wrap { gap: 10px }
    .brand { font-size: 18px }
    .brand svg { width: 24px; height: 24px }
    .burger { display: grid }

    .site-nav {
      position: fixed; inset: 0 0 0 auto; z-index: 50;
      width: min(78vw, 300px);
      flex-direction: column; gap: 4px;
      padding: calc(16px + env(safe-area-inset-top)) 16px calc(16px + env(safe-area-inset-bottom));
      background: var(--paper);
      box-shadow: -8px 0 30px rgba(27, 42, 52, .16);
      transform: translateX(100%);
      transition: transform .28s cubic-bezier(.4, 0, .2, 1);
      overflow-y: auto;
    }
    body.menu-open .site-nav { transform: translateX(0) }
    .site-nav a {
      padding: 15px 14px;
      border-radius: 12px;
      font-size: 17px; color: var(--ink);
    }
    .site-nav a[aria-current] { background: var(--bg); font-weight: 800 }

    .timer { padding: 28px 0 36px }
    /* ---------- Getting Start above the fold ----------

       A kitchen timer is used on a phone, standing up, with one hand and
       something boiling. The measurement that prompted this: on a 375x812
       viewport the Start button's top edge sat at 744px, and a real phone
       spends about 100px on browser chrome - so the one control the whole
       page exists for was over a hundred pixels below the fold, on every
       timer page.

       Nothing is hidden to fix it, and that is deliberate rather than
       cautious. Google indexes the MOBILE rendering, so a display:none on the
       h1 or the lede is not a heading that still counts in the source - it is
       the page's primary heading absent from the version being judged. They
       shrink instead, which costs nothing anywhere. */
    .timer h1 { margin-bottom: 4px; font-size: 21px; line-height: 1.2 }
    .lede { margin-bottom: 12px; font-size: 14px; line-height: 1.45 }
    .display:hover { background: var(--paper); box-shadow: none }
    /* "00:00" measures 2.903em wide in Manrope ExtraBold at -.03em tracking.
       With a 16px gutter and .11em side padding that leaves room for about
       29vw on every phone from 320px up; 27vw keeps a few px of margin. The
       old 22vw was leaving 74-97px of screen unused. */
    .display {
      --r: 18px; --bw: 2px;
      padding: 10px max(12px, .11em);
      border-width: var(--bw); border-radius: var(--r);
      font-size: clamp(56px, 27vw, 150px);
    }
    .edit-pencil { right: -6px; top: -10px; transform: none; width: 34px; height: 34px }
    .display.hours { font-size: clamp(46px, 22vw, 128px) }
    .hint { margin-top: 10px; height: 21px; font-size: 14px; line-height: 21px }
    body.done .hint { margin-top: 14px; font-size: 16px }

    .presets {
      flex-wrap: nowrap; justify-content: flex-start;
      margin: 14px -16px 20px; padding: 2px 16px;
      overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
      /* the row scrolls past the edge, so fade the cut instead of letting a
         button look chopped in half */
      -webkit-mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 30px), transparent 100%);
      mask-image: linear-gradient(90deg, #000 0, #000 calc(100% - 30px), transparent 100%);
    }
    .presets::-webkit-scrollbar { display: none }
    .presets button { flex: none; min-height: 44px; padding: 10px 16px }
    /* the row already scrolls sideways here, so the cards keep their shape and
       the third one peeking past the fade is what says it scrolls */
    /* On a phone the picture presets stop being cards and become a list you
       swipe through.

       Wrapped cards fitted - five rices went to 3 + 2 with nothing clipped -
       but they cost about 280px of height, and everything that matters after
       them went below the fold: the Start button, the sound picker, the first
       row of the table. A list item is half the height of a card because the
       picture sits beside the name instead of above it, and the box shows
       three of them at a time.

       overscroll-behavior is the load-bearing line. A scrolling box inside a
       scrolling page hands its momentum to the page the moment it hits its
       own end, so a flick meant for the rice carries on into the article -
       `contain` stops the chain at this box.

       A page with three presets, like the egg timer, has a list shorter than
       the cap and simply never scrolls. */
    .presets.has-images {
      flex-direction: column;
      flex-wrap: nowrap;
      justify-content: flex-start;
      gap: 8px;
      margin: 14px 0 20px;
      padding: 0;
      /* Exactly three items (62px) and their two gaps, plus 2px so a rounding
         difference cannot turn a list of three into a six-pixel scroll with a
         fade over the last egg. Five rices overflow this and scroll; three eggs
         fill it and never do. */
      max-height: 204px;
      overflow-y: auto;
      overscroll-behavior: contain;
      -webkit-overflow-scrolling: touch;
      scrollbar-width: none;
      scroll-snap-type: y proximity;
      /* the cut at the bottom fades, so a half-visible item reads as "there is
         more" rather than as a card that got chopped */
      -webkit-mask-image: linear-gradient(180deg, #000 0, #000 calc(100% - 26px), transparent 100%);
      mask-image: linear-gradient(180deg, #000 0, #000 calc(100% - 26px), transparent 100%);
    }
    .presets.has-images button {
      flex-direction: row;
      align-items: center;
      width: 100%;
      padding: 8px 14px 8px 8px;
      gap: 12px;
      text-align: left;
      scroll-snap-align: start;
    }
    .presets.has-images button[aria-pressed="true"] { padding: 7px 13px 7px 7px }
    .presets.has-images button img { width: 56px; height: 44px; margin: 0; flex: none }
    .presets.has-images button .label { flex: 1; min-width: 0 }
    .presets.has-images button small { margin: 0; white-space: nowrap }
    /* The dead band under the clock. Three margins and one bug, and together
       they were 46px of nothing on every page.

       The bug: .dots sets display:flex, which beats the hidden attribute, so
       the empty round counter held 12px of margin on every page that has no
       rounds - which is all of them except interval and pomodoro. */
    /* The clock fills the column, so the phase row lines up with it.

       The clock is an inline-block sized by its own digits - 300px at this
       width - while the phase pills below it are a flex row filling the 343px
       column. Both were correctly centred, on exactly the same axis, and that
       is precisely why it looked wrong: the pills overhang the clock card by
       21px on each side, and two centred things of different widths read as
       two things that failed to line up.

       Widening the clock rather than narrowing the pills, because the pills
       are as small as they can be while still holding a phase name and a
       time, and because the digits are the thing this page is for. */
    .clock { width: 100% }

    /* The pencil keeps its 34px circle and gets a 46px target.

       It is the only control on a timer page under the 44px a fingertip
       needs, and it sits half off the corner of the clock, so a miss lands on
       the clock instead. The pseudo-element grows the hit area without moving
       or resizing anything that is drawn. */
    .edit-pencil::after { content: ''; position: absolute; inset: -6px }

    .dots[hidden] { display: none }
    /* The hint is empty until the clock has been started once - it fills with
       "14 min countdown, ends 11.11" on the first Start and stays filled - so
       before that it is an empty line box holding 27px open. Collapsing it
       costs a reflow at the moment it fills, and that moment is already the
       one where the whole preset row disappears, so the jump lands inside a
       reflow the page was going to do anyway. */
    .hint:has(#hintText:empty) { display: none }
    .hint { margin-top: 6px }

    /* 12px of padding above and below a 28px mark is desktop air on a screen
       that has none to spare. 8 keeps the tap target over 44. */
    .site-header { padding: 8px 0 }
    .presets-label { margin-top: 14px }
    .presets { margin: 10px 0 14px }

    .times { font-size: 15px }
    .times th, .times td { padding: 10px 12px }
    /* The row header stops refusing to wrap. It is nowrap on the desktop so a
       time like "10 min" cannot break in half, but on a phone the longest NAME
       - "Parboiled, easy cook" - then sets the column width, and with a
       thumbnail in front of it there was no room left for the fourth column:
       .times clips rather than scrolls, so Rest simply vanished off the edge. */
    .times tbody th { white-space: normal; min-width: 0 }
    .times tbody th img {
      width: 38px; height: 24px;
      margin: 0 8px 0 0;
      float: left;   /* so a name that wraps runs beside the picture, not under it */
    }
    .times th, .times td { padding: 10px 9px }
    .times tbody th { padding-left: 12px }
    .times thead { display: none }
    .lookup > .wrap { padding-top: 32px }
    .presets button:active { background: var(--bg) }
    .adjust { justify-content: center; margin: 14px 0 20px }
    .adjust button { flex: 1; min-height: 48px; min-width: 0 }
    .dots { margin-top: 12px }
    .seg-label { margin-bottom: 8px; font-size: 14px }
    .note { font-size: 13px }

    .controls { gap: 6px }
    .controls .btn.quiet { position: static; margin-left: 0; height: auto }
    .btn.primary { width: 100%; min-width: 0; min-height: 66px; font-size: 21px }
    .btn.quiet { min-height: 44px }

    .settings {
      grid-template-columns: 1fr;
      gap: 2px;
      margin-top: 20px; padding: 6px;
      border-radius: 12px;
    }
    .settings label, .settings .field {
      min-height: 48px; padding: 6px 10px;
      border-radius: 8px; font-size: 16px;
    }
    /* The label goes on mobile: each setting already gets its own line, the
       speaker button next to it says "audio", and the value reads "Beep". On
       the desktop bar it stays, where the row is horizontal and needs the
       scanning anchor. Clipped rather than display:none, so it is still the
       select's accessible name. */
    .settings .field > label {
      position: absolute;
      width: 1px; height: 1px;
      margin: -1px; padding: 0; border: 0;
      clip-path: inset(50%); overflow: hidden;
      white-space: nowrap;
    }
    .ask-card { padding: 20px }
    .ask-actions { flex-direction: column-reverse; gap: 10px }
    .ask-actions .btn { width: 100%; min-height: 46px; justify-content: center }

    .settings .field { justify-content: stretch }
    .settings .field-controls { flex: 1; gap: 8px }
    .settings .field-controls select { flex: 1; min-width: 0 }
    .settings select { font-size: 16px }
    .settings .vol { padding: 0 10px; font-size: 15px }
    .settings input[type=checkbox] { width: 21px; height: 21px }
    .ad-leaderboard { min-height: 100px; margin-top: 26px }
    .ad-inline { min-height: 250px; margin-bottom: 32px }
    /* The rows above sit at 16px from the box edge: 6px of box padding plus
       10px of their own. The button is a direct grid child, so it only got the
       6px and ran 10px wider on each side. The margin makes up the difference,
       and 6px at the bottom mirrors the 12px above the first row. */
    .settings .fs-btn {
      justify-self: stretch; justify-content: center;
      margin: 6px 10px; font-size: 16px;
    }

    .notify-ask { flex-wrap: wrap; gap: 6px }
    .notify-ask p { flex-basis: 100%; margin-bottom: 4px }
    .notify-ask button { flex: 1; min-height: 44px; justify-content: center }

    body.fs .display { border-width: 3px; font-size: min(26vw, 34vh) }
    body.fs .display.hours { font-size: min(16vw, 22vh) }

    .about { padding: 20px 0 32px }
    .about p { max-width: none }
    .uses, .faq { padding-bottom: 32px }
    .uses h2, .faq h2, .more h2 { font-size: 19px }
    .faq summary { min-height: 44px; align-items: center }
    .more { padding-bottom: 40px }
    .more ul { grid-template-columns: 1fr }
    .more a { min-height: 56px }

    .site-footer { padding-bottom: calc(24px + env(safe-area-inset-bottom)) }
    .site-footer .wrap { flex-direction: column; gap: 10px }
  }

  @media (max-height: 520px) and (orientation: landscape) {
    .site-header { padding: 8px 0 }
    .timer { padding: 8px 0 20px }
    .timer h1, .hint { display: none }
    .display { font-size: clamp(52px, 13vh, 96px) }
    .presets { margin: 10px -16px 12px }
  }

  @media (max-width: 380px) {
    .brand span { display: none }
    .strip { grid-template-columns: repeat(2, 1fr) }
    .strip img { max-width: 108px }
  }

  /* A touch device reports a permanent hover on the last thing tapped, so every
     :hover rule stuck after a tap - which is why Start sat in its darker press
     colour instead of returning to yellow. */
  @media (hover: none) {
    .btn:hover { box-shadow: 0 1px 0 var(--line) }
    .btn.primary:hover { background: var(--beep); box-shadow: none }
    .btn.quiet:hover { color: var(--muted); text-decoration: none }
    /* Cancel the hover highlight on touch - but never on the button that is
       actually chosen.

       iOS keeps :hover on whatever you last tapped until you tap something
       else, so the just-picked preset matched this rule. It has exactly the
       same specificity as `.presets button[aria-pressed="true"]` and sits
       ~870 lines later in the file, so it won, and the selected card lost its
       dark border the instant it was selected. The one preset that kept its
       border was the default, because nobody had touched it. */
    .presets button:not([aria-pressed="true"]):hover,
    .adjust button:hover { border-color: var(--line) }
    .settings select:hover, .settings .fs-btn:hover, .settings .vol:hover { border-color: var(--line) }
    .settings label:hover { color: var(--muted) }
    .more a:hover { border-color: transparent }
    .edit-pencil:hover { color: var(--muted); border-color: var(--line) }
    .site-nav a:hover { background: none }
    .display:hover { box-shadow: none }
  }
