/* ==========================================================================
   My Scruffy Puppy — screen styles

   Type has three roles, and keeping them separate is what holds the page
   together:

     Geist       display and body. Carries every headline and paragraph.
     Geist Mono  utility only. Stickers and the book spec lines, which read
                 as a publisher's colophon rather than decoration.
     LDFundamental  the puppy's own handwriting. A voice, never a display
                 face — it has one weight and no mass, so at headline sizes
                 it reads thin. Used small, inside a speech bubble.

   Contrast: every band pair, and every de-emphasised element that sits on
   one, is held to WCAG AA by tests/ContrastTest.php. Opacity below 1 on
   text is a contrast reduction, so each value used here is asserted rather
   than eyeballed. Anything new needs a matching case in that test.

   Cascade layers keep specificity predictable without a build step.
   ========================================================================== */

@layer reset, tokens, base, layout, components, utilities;

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

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

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

@layer tokens {
  :root {
    --black: #030303;
    --blue: #015ca7;
    --blue-light: #27bbd4;
    --green: #2fb255;
    --ink: #0d0f12;
    --ink-soft: #9ba3ae;
    --lime: #77c232;
    --orange: #ee6632;
    --pink: #e9008a;
    --pink-deep: #dd0083;
    --purple: #6b4096;
    --sky: #16dffd;
    --tan: #e9dbc0;
    --teal: #adf0f2;
    /* Lifted straight out of Puppy_Post_Bath.jpg. The whole left of the
       hero artwork is this one flat value, which is what lets the
       headline print onto it at a contrast figure that can be measured
       (tests/ContrastTest.php) rather than guessed. Re-sample the
       artwork before changing this. */
    --wall: #3fc7f1;
    --white: #fff;
    --font-body: geist, system-ui, -apple-system, "Segoe UI", roboto, sans-serif;
    --font-mono: geistmono, ui-monospace, sfmono-regular, menlo, monospace;
    --font-voice: ldfundamental, "Comic Sans MS", cursive;
    /* Display runs tight, heavy and very large; that density is what reads
       as confident rather than loud. */
    --display: clamp(3rem, 0.6rem + 9vw, 8.5rem);
    --step-2: clamp(2.35rem, 0.9rem + 5.6vw, 5.5rem);
    --step-1: clamp(1.2rem, 1.05rem + 0.6vw, 1.6rem);
    --step-0: clamp(1.0625rem, 1rem + 0.28vw, 1.25rem);
    --step-mono: 0.75rem;
    --gutter: clamp(1.5rem, 5vw, 5rem);
    --band-space: clamp(5.5rem, 11vw, 10rem);
    --measure: 38ch;
    --shell: 78rem;
    --band-bg: var(--white);
    --band-fg: var(--black);
    /* The second clause of a headline. Defaults to the band's own text
       colour; a band may hand it a palette colour instead, but only one
       that clears AA against that band. */
    --accent: currentcolor;
    --scene-position: center;
    /* The colour sweep is measured off the type column, not off the
       viewport. A percentage cannot do this job: the shell stops growing
       at 78rem, so on a wide screen a fixed percentage either abandons
       the column or swallows the drawing. This resolves to the column's
       far edge plus a little air, wherever the column happens to land. */
    --scene-column: 30rem;
    /* Height of the stacked artwork strip on narrow screens. */
    --scene-strip: min(78vw, 28rem);
  }

  @font-face {
    font-display: swap;
    font-family: geist;
    font-style: normal;
    font-weight: 100 900;
    src: url("../fonts/Geist-Variable.woff2") format("woff2");
  }

  @font-face {
    font-display: swap;
    font-family: geistmono;
    font-style: normal;
    font-weight: 100 900;
    src: url("../fonts/GeistMono-Variable.woff2") format("woff2");
  }

  @font-face {
    font-display: swap;
    font-family: ldfundamental;
    font-style: normal;
    font-weight: 400;
    src: url("../fonts/LDFundamental.woff2") format("woff2");
  }
}

@layer base {
  body {
    background: var(--white);
    color: var(--black);
    font-family: var(--font-body);
    font-size: var(--step-0);
    font-synthesis: none;
    line-height: 1.6;
    text-size-adjust: 100%;
  }

  a {
    color: inherit;
  }

  :focus-visible {
    outline: 3px solid currentcolor;
    outline-offset: 3px;
  }
  /* Selection is a surface we did not draw but still ship. Inverting the
     band's own pair keeps it on-palette everywhere and inherits the same
     tested contrast, so no band can produce unreadable selected text. */
  ::selection {
    background: var(--band-fg);
    color: var(--band-bg);
  }
}

@layer layout {
  .band {
    background: var(--band-bg);
    color: var(--band-fg);
    padding-block: var(--band-space);
    padding-inline: var(--gutter);
    position: relative;
  }

  .band__inner {
    margin-inline: auto;
    max-width: var(--shell);
    position: relative;
    z-index: 1;
  }

  .band--blue {
    --accent: var(--teal);
    --band-bg: var(--blue);
    --band-fg: var(--white);
  }

  .band--blue-light {
    --band-bg: var(--blue-light);
    --band-fg: var(--black);
  }

  .band--green {
    --band-bg: var(--green);
    --band-fg: var(--black);
  }

  .band--lime {
    --band-bg: var(--lime);
    --band-fg: var(--black);
  }

  .band--orange {
    --band-bg: var(--orange);
    --band-fg: var(--black);
  }

  .band--pink {
    --band-bg: var(--pink-deep);
    --band-fg: var(--white);
  }

  .band--purple {
    --band-bg: var(--purple);
    --band-fg: var(--white);
  }
  /* Lifted straight out of Puppy_On_Leash.jpg, which is a flat #16dffd
     field behind a cut-out drawing. Matching it exactly means the seam
     between the art panel and the band disappears and the dog reads as
     standing in the band rather than in a box beside it. Re-sample the
     artwork before changing this. */
  .band--sky {
    --band-bg: var(--sky);
    --band-fg: var(--black);
  }

  .band--tan {
    --band-bg: var(--tan);
    --band-fg: var(--black);
  }

  .band--teal {
    --band-bg: var(--teal);
    --band-fg: var(--black);
  }

  .band--ink {
    --band-bg: var(--ink);
    --band-fg: var(--white);
  }
  /* A scene band pairs a drawing with a block of colour, and the two never
     overlap. Narrow screens stack them: the artwork takes a full-width
     strip across the top and the colour block sits under it. Wide screens
     put them side by side (see the 60rem query below). Either way the
     type sits on flat colour, which is why every contrast figure in
     tests/ContrastTest.php is the whole story and not a best case. */
  .band--scene {
    /* Where the type column ends and the artwork may begin, once there is
       room for them to sit side by side. Measured off the column rather
       than as a percentage of the viewport, because the shell stops
       growing at 78rem: a fixed percentage either abandons the column on
       a wide screen or eats the drawing on a narrow one.

       Declared here, not on :root: a custom property's var() references
       are resolved on the element that declares it, so a :root definition
       would have baked in :root's --scene-column and ignored the band's
       own. */
    --scene-edge: calc(max(var(--gutter), 50% - var(--shell) / 2) + var(--scene-column) + var(--gutter));

    overflow: hidden;
    padding-block-start: calc(var(--scene-strip) + var(--band-space));
  }

  #kitty {
    --scene-position: left center;
  }

  #follow {
    --scene-position: right center;
  }
  /* The hero never turns into a side-by-side panel at any width. Its
     artwork is a 2:1 spread with the dog pinned to the right third and a
     flat wall filling the left, so it runs edge to edge at the art's own
     ratio: 50vw is exactly half of 100vw, which means no crop at all
     while it is the smaller term, and the headline is printed onto that
     empty wall. 88vh only takes over on a short window, and it crops the
     top and bottom rather than the sides, so the frame's horizontal
     geometry is fixed everywhere. */
  .hero {
    --scene-strip: min(50vw, 88vh);
  }

  .band__scene {
    block-size: var(--scene-strip);
    inset-block-start: 0;
    inset-inline: 0;
    overflow: hidden;
    position: absolute;
  }

  .band__scene picture {
    block-size: 100%;
    display: block;
  }

  .band__scene img {
    block-size: 100%;
    inline-size: 100%;
    object-fit: cover;
    object-position: var(--scene-position);
  }
  /* The wall goes under the artwork as well as in it. If the image is
     slow, missing, or still decoding, the headline is on the colour its
     contrast was actually measured against rather than on the band's
     blue, where it would be unreadable. */
  .hero .band__scene {
    background: var(--wall);
  }

  .column {
    max-width: min(52rem, 100%);
  }

  .split {
    align-items: center;
    display: grid;
    gap: clamp(2.5rem, 6vw, 5rem);
    grid-template-columns: 1fr;
  }

  @media (width >= 48rem) {
    .split {
      grid-template-columns: 1fr 1fr;
    }

    .split--reverse > :first-child {
      order: 2;
    }
  }

  @media (width >= 60rem) {
    /* Side by side: the drawing gets its own panel beside the colour.
       The hero is excluded and keeps stacking — see the next query. */
    .band--scene:not(:where(.hero)) {
      padding-block-start: var(--band-space);
    }

    .band--scene:not(:where(.hero)) .band__inner {
      min-block-size: min(70vh, 34rem);
    }
    /* :where() keeps the exclusion out of the specificity sum. Written
       as :not(.hero) this rule outweighs .band--scene-end below it and
       flips Puppy's artwork to the wrong side of his band. */
    .band--scene:not(:where(.hero)) .band__scene {
      block-size: auto;
      inset-block: 0;
      inset-inline: var(--scene-edge) 0;
    }

    .band--scene-end .band__scene {
      inset-inline: 0 var(--scene-edge);
    }

    .band--scene-end .column {
      margin-inline-start: auto;
    }

    .band--scene:not(:where(.hero)) .column {
      max-width: var(--scene-column);
    }
  }
  /* The headline block. Below 48rem it stays in the flow under the
     artwork, in the band's own two colours; from 48rem it is lifted onto
     the wall, where there is finally enough of it to hold display type.

     The safe region is measured off the artwork, not guessed. The wall is
     one flat value down to 60.1% of the frame and the dog's leftmost edge
     above the floor is at 65.2% across; the frame is never cropped
     horizontally, so both hold at every width. 64% and 58% are those two
     numbers with a margin, and the checkerboard floor below them — pure
     black against pure white — is the one surface on this page where no
     type would be legible at all. */
  @media (width >= 48rem) {
    .hero__over {
      --band-bg: var(--white);
      --band-fg: var(--ink);

      align-content: center;
      block-size: calc(var(--scene-strip) * 0.58);
      color: var(--ink);
      display: grid;
      inset-block-start: 0;
      inset-inline: clamp(1.25rem, 2.5vw, 3.5rem) 36%;
      justify-items: start;
      position: absolute;
    }
    /* Nothing sits to the right of the copy once the headline has left
       the flow, so it is centred rather than left to trail off. */
    .hero .column {
      margin-inline: auto;
      max-width: 56rem;
      text-align: center;
    }
  }
}

@layer components {
  /* The sticker. A rotated mono chip in the band's own two colours, so it
     can never drop below the band's tested contrast. It replaces the
     hairline-rule eyebrow: this is a picture-book, and a badge stuck on at
     a slight angle says that where a rule says the opposite. */
  .eyebrow,
  .book__index,
  .book__pending {
    background: var(--band-fg);
    border-radius: 999px;
    color: var(--band-bg);
    display: inline-block;
    font-family: var(--font-mono);
    font-size: var(--step-mono);
    font-weight: 600;
    letter-spacing: 0.14em;
    line-height: 1;
    padding: 0.7rem 1.1rem 0.62rem;
    rotate: -1.75deg;
    text-transform: uppercase;
  }

  .eyebrow {
    margin-block-end: clamp(1.75rem, 3.5vw, 2.75rem);
  }

  .display {
    font-family: var(--font-body);
    font-size: var(--display);
    font-weight: 680;
    letter-spacing: -0.038em;
    line-height: 0.96;
    text-wrap: balance;
  }

  .title {
    font-family: var(--font-body);
    font-size: var(--step-2);
    font-weight: 670;
    letter-spacing: -0.034em;
    line-height: 0.99;
    text-wrap: balance;
  }
  /* The headline's second clause, set as its own line rather than as a
     span inside the h1: on wide screens the two halves are on different
     surfaces. It changes colour rather than fading, so it reads as a
     separate thought without losing contrast. */
  /* The greeting above the headline, and the one word on this page that
     is spoken rather than printed — so it is set in the puppy's own hand,
     at the hand's ceiling and no larger. Against a headline four times
     its size it reads as a small note scrawled above a printed name,
     which is the job: this face has one weight, and the moment it is
     scaled up to compete on size its stroke reads thin. Tilted like every
     other applied thing here, so the size reads as chosen rather than as
     something that failed to grow. */
  .hero__intro {
    font-family: var(--font-voice);
    font-size: clamp(1.35rem, 1.1rem + 0.8vw, 1.7rem);
    inline-size: fit-content;
    line-height: 1;
    margin-block-end: 0.4em;
    rotate: -1.75deg;
  }

  .hero__lead {
    color: var(--accent);
    /* The same curve as .title with the ceiling lifted. The hero is the
       one place on the page where the type keeps growing past a laptop. */
    font-size: clamp(2.35rem, 0.9rem + 5.6vw, 7rem);
  }

  .lede {
    font-size: var(--step-1);
    line-height: 1.5;
    margin-block-start: clamp(1.5rem, 3vw, 2.25rem);
    max-width: var(--measure);
  }
  /* The puppy's handwriting, in a bubble he is clearly saying it from.
     White ground under it keeps the one-weight face legible on every band
     and takes the aside out of the band's contrast budget entirely. */
  /* An aside is the dog plus the thing he said, never the bubble alone.
     A tail belongs to whoever it points at, so pointing it down into an
     empty field — or worse, at a sleeping cat — attributes the line to
     the wrong character. The speaker travels with the quote. */
  .says {
    display: grid;
    gap: 0.6rem;
    grid-template-columns: auto minmax(0, 1fr);
    margin-block-start: clamp(2rem, 4vw, 3rem);
    max-width: 34rem;
    place-items: start;
  }

  .says__who {
    display: block;
    inline-size: clamp(3.5rem, 6.5vw, 5.5rem);
  }

  .voice {
    background: var(--white);
    border-radius: 1.5rem;
    color: var(--ink);
    display: inline-block;
    font-family: var(--font-voice);
    font-size: clamp(1.25rem, 1.05rem + 0.9vw, 1.7rem);
    line-height: 1.25;
    max-width: 26ch;
    padding: 1rem 1.4rem 0.9rem;
    position: relative;
    rotate: -1.75deg;
  }
  /* The tail points sideways at the speaker rather than down at nothing.
     border-inline-end is the coloured edge, so the triangle's apex faces
     back toward the dog. */
  .voice::after {
    border: 0.7rem solid transparent;
    border-inline-end-color: var(--white);
    content: "";
    inset-block-start: 1rem;
    inset-inline-end: 100%;
    position: absolute;
  }

  .actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.875rem;
    margin-block-start: clamp(2rem, 4vw, 2.75rem);
  }
  /* Puppy's ball. It rolls along the seam at the foot of his band, inside
     the band's own bottom padding, which is always taller than the ball —
     so it passes below every line of type rather than behind it. Nothing
     on this page sits on top of words, and a moving thing least of all.
     .band--scene clips it, which is what gives it an entrance and an
     exit. */
  .ball {
    --ball: clamp(2.75rem, 5vw, 4.5rem);
    /* The drawn ball fills 325 of its 329px square, so its diameter is
       98.8% of the box the roll is measured against. */
    --ball-ink: 0.988;
    /* turns = travel / (pi * diameter). calc() cannot divide a length by
       a length, but tan(atan2(a, b)) is exactly a / b — which keeps this
       right at every viewport width and every clamped ball size, rather
       than at the one width a hand-tuned constant was picked for. A ball
       whose spin does not match its travel is precisely the thing that
       reads as fake. */
    --roll-turns: calc(
      tan(atan2(calc(100vw + var(--ball)), calc(var(--ball) * var(--ball-ink)))) / pi
    );

    block-size: var(--ball);
    inline-size: var(--ball);
    inset-block-end: 0;
    inset-inline-start: 0;
    position: absolute;
    /* Where it lies when nothing is driving it — reduced motion, or a
       browser without scroll-driven animation. A ball on the ground is a
       finished picture; a ball parked off-screen is a missing one. */
    translate: 12vw 0;
  }

  .ball img {
    inline-size: 100%;
  }

  .button {
    background: var(--band-fg);
    border: 2px solid var(--band-fg);
    border-radius: 999px;
    color: var(--band-bg);
    display: inline-block;
    font-family: var(--font-body);
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    padding: 0.95em 1.75em;
    text-decoration: none;
    transition: opacity 140ms ease-out, translate 140ms ease-out;
  }

  .button:hover {
    opacity: 0.88;
    translate: 0 -2px;
  }

  .button:focus-visible {
    outline-color: var(--band-fg);
  }

  .button--ghost {
    background: transparent;
    color: var(--band-fg);
  }

  .button--ghost:hover {
    background: var(--band-fg);
    color: var(--band-bg);
    opacity: 1;
  }
  /* The colophon: format, extent and ISBN, set the way a book's own
     imprint page sets them. */
  .spec {
    display: flex;
    flex-wrap: wrap;
    font-family: var(--font-mono);
    font-size: var(--step-mono);
    font-weight: 500;
    gap: 0.5rem 0.75rem;
    letter-spacing: 0.1em;
    line-height: 1.4;
    list-style: none;
    margin-block-start: 2rem;
    opacity: 0.82;
    padding: 0;
    text-transform: uppercase;
  }

  .spec li + li::before {
    content: "·";
    margin-inline-end: 0.75rem;
  }

  .books {
    display: grid;
    gap: clamp(4rem, 9vw, 7rem);
    grid-template-columns: 1fr;
    margin-block-start: clamp(3rem, 6vw, 5rem);
  }

  .book {
    align-items: start;
    display: grid;
    gap: clamp(2rem, 5vw, 4rem);
    grid-template-columns: 1fr;
  }

  .book__cover img {
    box-shadow: 0 18px 50px rgb(3 3 3 / 0.28);
  }

  .book__index {
    margin-block-end: 1.25rem;
  }

  .book__title {
    font-family: var(--font-body);
    font-size: clamp(2.1rem, 1.2rem + 3.4vw, 4.25rem);
    font-weight: 670;
    letter-spacing: -0.034em;
    line-height: 0.98;
  }

  .book__title-lead {
    display: block;
    font-size: 0.46em;
    font-weight: 560;
    letter-spacing: -0.01em;
    opacity: 0.82;
  }

  .book__blurb {
    font-size: var(--step-1);
    line-height: 1.5;
    margin-block: 1.25rem 0;
    max-width: 42ch;
  }

  .book__pending {
    margin-block-start: 2rem;
  }

  .videos {
    display: grid;
    gap: clamp(3rem, 7vw, 5.5rem);
    grid-template-columns: 1fr;
    margin-block-start: clamp(3rem, 6vw, 5rem);
  }

  .video__label {
    align-items: baseline;
    display: flex;
    flex-wrap: wrap;
    font-family: var(--font-mono);
    font-size: var(--step-mono);
    gap: 0.75rem;
    justify-content: space-between;
    letter-spacing: 0.14em;
    margin-block-end: 1rem;
    opacity: 0.85;
    text-transform: uppercase;
  }

  .facade {
    background: none;
    border: 0;
    cursor: pointer;
    display: block;
    inline-size: 100%;
    padding: 0;
    position: relative;
  }

  .facade img {
    inline-size: 100%;
  }

  .facade__play {
    align-items: center;
    background: rgb(13 15 18 / 0.62);
    block-size: 6rem;
    border: 2px solid rgb(255 255 255 / 0.7);
    border-radius: 50%;
    color: var(--white);
    display: flex;
    font-size: 1.6rem;
    inline-size: 6rem;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    justify-content: center;
    padding-inline-start: 0.25rem;
    position: absolute;
    transform: translate(-50%, -50%);
    transition: background 160ms ease-out, transform 160ms ease-out;
  }

  .facade:hover .facade__play,
  .facade:focus-visible .facade__play {
    background: rgb(13 15 18 / 0.82);
    transform: translate(-50%, -50%) scale(1.08);
  }

  .video__player {
    inline-size: 100%;
  }

  .about__grid {
    display: grid;
    gap: clamp(2rem, 5vw, 4rem);
    grid-template-columns: 1fr;
    margin-block-start: clamp(2.5rem, 5vw, 3.5rem);
  }

  .about__grid p {
    font-size: var(--step-1);
    line-height: 1.5;
    max-width: 40ch;
  }

  .about__grid p + p {
    margin-block-start: 1.25rem;
  }

  .social {
    display: flex;
    flex-wrap: wrap;
    gap: 0.875rem;
    list-style: none;
    margin-block-start: clamp(2rem, 4vw, 2.75rem);
    padding: 0;
  }
  /* ── The two-beat gag ──────────────────────────────────────────────
     Puppy walks past; arriving at Kitty wakes Kitty up. One authored
     moment spanning two bands, which is why neither band takes the
     site-wide scroll reveal. The keyframes and the feature queries that
     drive it sit at the end of this layer. */
  /* The leash is drawn out to the very edge of its source, so the frame
     cuts it off mid-stroke. Walking the image right exposes that cut end
     in open field. The mask rides with the image, so the stroke always
     dissolves into the band instead of stopping dead — invisible as a
     fade only because what it reveals is the same colour. */
  #puppy .band__scene img {
    mask-image: linear-gradient(to right, transparent 0, #000 14%);
  }

  .kitty {
    display: grid;
    justify-items: start;
    margin-block-start: clamp(2rem, 5vw, 3.5rem);
    max-width: 26rem;
  }

  .kitty__frame {
    grid-area: 1 / 1;
  }

  .kitty__frame--after {
    visibility: hidden;
  }

  .is-awake .kitty__frame {
    visibility: hidden;
  }

  .is-awake .kitty__frame--after {
    visibility: visible;
  }

  .is-awake .kitty {
    animation: kitty-startle 460ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  /* Both asides share one cell, so the punchline cannot push the cat
     down the page. visibility (not opacity) so the inactive line leaves
     the accessibility tree with it. */
  .voice-swap {
    display: grid;
    justify-items: start;
  }

  .voice-swap > .voice {
    grid-area: 1 / 1;
  }

  .voice--after {
    visibility: hidden;
  }
  /* Puppy answers a beat after the cat moves, the way someone speaks
     after the thing they are reacting to. */
  .is-awake .voice-swap > .voice {
    transition: visibility 0s linear 260ms;
    visibility: hidden;
  }
  /* Matched to the rule above, child combinator included. Written as
     `.is-awake .voice--after` it loses on specificity (0,2,0 against
     0,3,0), the setup line hides on schedule and the punchline never
     arrives — a bubble that empties instead of answering. */
  .is-awake .voice-swap > .voice--after {
    visibility: visible;
  }

  .site-footer {
    /* padding-block: clamp(3rem, 6vw, 4.5rem); */
    padding-block: 1rem;
  }

  .site-footer .band__inner {
    align-items: baseline;
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    justify-content: center;
  }

  .footer-meta {
    /* font-family: var(--font-mono); */
    font-size: var(--step-mono);
    font-weight: 700;
    letter-spacing: 0.12em;
    opacity: 0.85;
    text-transform: uppercase;
  }
  /* Puppy's walk works only because #puppy's band colour was sampled
     from its artwork: band and drawing share one exact #16dffd, so
     sliding the image inside its panel reveals nothing at all. The dog
     crosses an unbroken field rather than a moving picture in a box.
     Change either value and the seam appears.

     Travel is bounded by where the ink actually is. In the source the
     dog's ink stops 14.9% short of the right edge, so 9% leaves his nose
     inside the panel with room to spare; the panel clips, and a sliced
     dog is worse than a still one. */
  @keyframes puppy-walk {
    from {
      translate: -3% 0;
    }

    to {
      translate: 9% 0;
    }
  }
  /* The ball turns exactly as far as it travels; see --roll-turns. Held
     linear because it is scroll-driven: the visitor's own scrolling is
     the easing, and a second curve on top of it reads as lag. */
  @keyframes ball-roll {
    from {
      rotate: 0turn;
      translate: calc(var(--ball) * -1) 0;
    }

    to {
      rotate: calc(var(--roll-turns) * 1turn);
      translate: 100vw 0;
    }
  }
  /* Kitty does not fade awake. A dissolve would be a light effect in a
     world that is printed, not lit; a cartoon wakes on one frame. The
     startle is the only motion, and it settles rather than bounces. */
  @keyframes kitty-startle {
    0% {
      rotate: 0deg;
      translate: 0 0;
    }

    32% {
      rotate: -1.6deg;
      translate: 0 -0.6rem;
    }

    100% {
      rotate: 0deg;
      translate: 0 0;
    }
  }

  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      /* One timeline, two subjects: the dog walks and his ball rolls off
         the same pass. That is what keeps this a single authored moment
         rather than two effects that happen to share a band.

         The timeline is named on the section rather than taken with a
         bare view() on either subject. view() binds to the nearest
         ancestor scroll container, and #puppy clips its own children so
         that a walking dog cannot cross into the type column — which
         makes it a scroll container that never scrolls, and an inactive
         timeline. The animation still reports as running; it simply never
         moves. Named on #puppy, the subject is outside the clip, where
         the document is the scroller. */
      #puppy {
        view-timeline-name: --puppy-band;
      }

      #puppy .band__scene img {
        /* Longhands, and animation-duration: auto specifically. The
           `animation` shorthand resets duration to 0s, which is the other
           way to leave a timeline-driven animation attached and inert. */
        animation-duration: auto;
        animation-fill-mode: both;
        animation-name: puppy-walk;
        animation-range: cover 0% cover 100%;
        animation-timeline: --puppy-band;
        animation-timing-function: linear;
      }
      /* Large screens only, and this one is a retreat rather than a
         judgement. Safari runs the translate half of this animation and
         drops the rotate half, so the ball skates across the band without
         turning — the exact reading of "fake" the roll maths was written
         to avoid. Feeding the turn count in by hand changes nothing there,
         which rules out the value and points at the independent rotate
         property itself; no feature query distinguishes that, and CSS
         cannot select a browser. So the roll is held back to screens where
         it is verified, and everywhere else the ball keeps its rest
         position. A ball lying on the ground is a finished picture. A ball
         sliding on its side is a bug.

         Not cover: the ball sits on the band's bottom edge, so it is only
         on screen from the moment that edge clears the foot of the
         viewport until it leaves the top — which is contain 0% to exit
         100% at any band height. Given cover, half the roll would happen
         below the fold and the ball would appear already mid-screen. */
      @media (width >= 60rem) {
        .ball {
          animation-duration: auto;
          animation-fill-mode: both;
          animation-name: ball-roll;
          animation-range: contain 0% exit 100%;
          animation-timeline: --puppy-band;
          animation-timing-function: linear;
        }
      }
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .is-awake .voice-swap > .voice {
      transition-delay: 0s;
    }
  }

  @media (width >= 48rem) {
    /* Tied to the strip's height as well as to the viewport, so the
       headline keeps its proportion of the wall when a short window makes
       88vh the smaller term and the wall shrinks with it. */
    .hero__headline {
      font-size: min(8vw, calc(var(--scene-strip) * 0.16));
    }
    /* The sticker is a separate thought from the greeting and the
       headline, which are one. Tight group, generous separation. */
    .hero__over .eyebrow {
      margin-block-end: clamp(0.75rem, 1.5vw, 1.5rem);
    }
    /* Held to roughly four fifths of the headline: the headline shrinks
       with the strip on a short window and this has to shrink with it, or
       the second clause ends up louder than the first. */
    .hero__lead {
      font-size: min(clamp(2.35rem, 0.9rem + 5.6vw, 7rem), calc(var(--scene-strip) * 0.128));
    }

    .hero .lede {
      font-size: clamp(1.3rem, 1rem + 1.35vw, 2.25rem);
      line-height: 1.2;
      margin-inline: auto;
      max-width: 42ch;
      text-wrap: balance;
    }

    .hero .button {
      font-size: clamp(1rem, 0.85rem + 0.55vw, 1.4rem);
    }

    .hero .actions {
      justify-content: center;
    }
    /* The aside keeps its own alignment inside the centred column. A
       centred speech bubble reads as a caption rather than as something
       the dog said. */
    .hero .says {
      margin-inline: auto;
      text-align: start;
    }

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

    .about__grid {
      grid-template-columns: 1fr 1fr;
    }
  }
}

@layer utilities {
  .visually-hidden {
    block-size: 1px;
    clip-path: inset(50%);
    inline-size: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
  }

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