/* Wallgami site.

   Colours are the app's own tokens, from src/Wallgami.App/Theme.cs, so the site
   and the product look like the same thing. Two rules carried over from the app,
   both of which it learned the hard way:

     * --accent is a FILL colour. #D2252B as text on #121114 does not read.
       Accent-coloured text uses --accent-text instead.
     * The app's AccentSoft token is deliberately NOT ported. A dim red fill
       behind lighter red text with a matching border is the washed-out button
       this project does not build.

   The structural idea is folded paper: sections meet at a shallow crease rather
   than sitting in cards.

   The pages move, a good deal since 2026-09-26: the hero's branch and crane,
   the header band and heading cranes on the other pages, the laptop and the
   refused-wallpaper scenes, the margin blossoms, blocks and table rows that
   arrive, creases that catch the light. None of it survives a reduced-motion
   request, loops rest when scrolled away, and nothing moves the layout after
   load. README.md's Motion section says where each piece lives; the motion
   section near the foot of this file has the rules for the oldest of them. */

:root {
  --bg: #121114;
  --panel: #1a181c;
  --card: #211f23;
  --card-hover: #2a272c;
  --fg: #f7f3f1;
  --muted: #aaa2a3;
  --border: #373238;
  --accent: #d2252b;
  /* The app hovers to a LIGHTER red (#E43B40). Here the hover goes darker
     instead: white text on #E43B40 is 4.20:1, under the 4.5:1 needed to read
     comfortably, and this button is white text. tools/check.py enforces it. */
  --accent-hover: #b7181e;
  --accent-text: #f4676c;
  --on-accent: #ffffff;
  --success: #61b977;
  --warn: #f0b84b;
  --crease: #4a434b;
  /* The footer's links go white under the pointer. In the light theme the
     footer panel is near-white, so "white" there would mean invisible: the
     intent is the maximum-contrast colour, and that is what each theme names. */
  --footer-hover: #ffffff;
  /* The light that crosses a fold as it arrives. See .fold.is-lit. */
  --sheen: rgba(255, 255, 255, 0.07);
  --crease-lit: #c9bcc8;

  --measure: 38rem;
  --gap: clamp(3.5rem, 8vw, 6.5rem);
  /* The pinned footer's height. One row of links plus the air around it. */
  --footer-h: 3.2rem;

  color-scheme: dark;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #f4efe9;
    --panel: #fffdfa;
    --card: #ffffff;
    --card-hover: #f6f0ea;
    --fg: #1d1a1c;
    --muted: #655d58;
    --border: #e0d6cd;
    --accent: #c91f26;
    --accent-hover: #b7181e;
    --accent-text: #bc1d24;
    --on-accent: #ffffff;
    --success: #256b36;
    --warn: #7d5700;
    --crease: #c7b9ad;
    --footer-hover: #1d1a1c;
    --sheen: rgba(255, 255, 255, 0.9);
    --crease-lit: #ffffff;

    color-scheme: light;
  }
}

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

html {
  -webkit-text-size-adjust: 100%;
  /* The footer is pinned and its height is measured to pad the body. Without
     this, that measurement chases its own tail: padding the body decides whether
     the page has a scrollbar, the scrollbar decides how wide the page is, and
     the width decides how many lines the footer's trademark notice wraps to.
     Reserving the gutter whether or not it is used breaks that circle -- the
     footer's height stops depending on the padding derived from it. */
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  /* The short pages -- thanks, sorry, 404 -- are shorter than a window, and
     without this the footer stopped partway down and left a band of page
     background beneath it, which reads as the page having failed to finish
     loading. dvh second so mobile browsers measure past their own chrome. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--fg);
  font-family: "Zen Kaku Gothic New", "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

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

a { color: var(--accent-text); text-underline-offset: 3px; }
a:hover { text-decoration-thickness: 2px; }

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

/* This page argues for taking motion sensitivity seriously, so when the machine
   asks for less of it the answer is none rather than less. The script checks the
   same question and declines to start; this is the belt to that pair of braces,
   and it also catches the hover and focus transitions the script knows nothing
   about. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

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

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 10;
  background: var(--accent);
  color: var(--on-accent);
  padding: 0.7rem 1.2rem;
  font-weight: 700;
}
.skip-link:focus { left: 0; }

/* ---- type --------------------------------------------------------------- */

h1, h2, h3 {
  font-family: "Shippori Mincho B1", "Yu Mincho", Georgia, serif;
  font-weight: 700;
  line-height: 1.18;
  letter-spacing: -0.01em;
  margin: 0;
  text-wrap: balance;
}

h1 { font-size: clamp(2.15rem, 5.4vw, 3.6rem); line-height: 1.1; }
h2 { font-size: clamp(1.7rem, 3.6vw, 2.4rem); margin-bottom: 1.1rem; }
h3 {
  font-family: "Zen Kaku Gothic New", "Segoe UI", system-ui, sans-serif;
  font-size: 1.1rem;
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: 0;
}

p { margin: 0 0 1.05rem; max-width: var(--measure); }
p:last-child { margin-bottom: 0; }

.lede {
  font-size: clamp(1.06rem, 1.9vw, 1.24rem);
  line-height: 1.55;
  color: var(--muted);
  max-width: 34rem;
}

.small { font-size: 0.88rem; color: var(--muted); }

strong, b { font-weight: 700; color: var(--fg); }

/* Figures quoted from the app's own meter. Tabular so columns line up. */
.figure {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

/* ---- shell -------------------------------------------------------------- */

.wrap { width: min(72rem, 100% - 2.5rem); margin-inline: auto; }
.wrap-narrow { width: min(46rem, 100% - 2.5rem); margin-inline: auto; }

main { flex: 1 0 auto; }

section { padding-block: var(--gap); }
.panel { background: var(--panel); }

/* The crease. Two page tones meeting along a shallow diagonal with a light line
   on the fold, the way a sheet of paper catches the light where it is bent.

   This was a linear-gradient with hard colour stops tilted 1.3deg off level.
   Browsers do not antialias a hard stop on a near-horizontal gradient, so across
   a desktop width it stair-stepped into a dashed line and read as a bug. The
   same shape as clip-path geometry is antialiased properly at any width.

   --left and --right are where the crease meets each edge, as a share of the
   height; swapping them is what tilts the fold the other way. */
.fold {
  position: relative;
  overflow: hidden;
  height: clamp(26px, 3.6vw, 58px);
  --from: var(--bg);
  --to: var(--panel);
  --left: 22%;
  --right: 78%;
  background: var(--to);
}

/* The sheet lying above the fold. */
.fold::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--from);
  clip-path: polygon(0 0, 100% 0, 100% var(--right), 0 var(--left));
}

/* The light along the fold itself. */
.fold::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--crease);
  clip-path: polygon(
    0 var(--left),
    100% var(--right),
    100% calc(var(--right) + 2px),
    0 calc(var(--left) + 2px)
  );
}

.fold-to-bg { --from: var(--panel); --to: var(--bg); }
.fold-back { --left: 78%; --right: 22%; }

/* Light catching the fold. The first time a crease scrolls into view the
   script adds .is-lit, and a soft band of light crosses the paper on both sides
   of it, brighter along the crease itself, once, left to right. The sheen sits
   in background-image so it rides on each sheet's own clip-path; the plain
   colours above are untouched. Nothing moves without the script, and the
   script does not run under a reduced-motion request. */
.fold,
.fold::before,
.fold::after {
  background-repeat: no-repeat;
  background-size: 300% 100%;
  background-position: 100% 0;
}
.fold,
.fold::before { background-image: linear-gradient(100deg, transparent 42%, var(--sheen) 50%, transparent 58%); }
.fold::after { background-image: linear-gradient(100deg, transparent 40%, var(--crease-lit) 50%, transparent 60%); }
.fold.is-lit,
.fold.is-lit::before,
.fold.is-lit::after { animation: fold-sheen 1.9s cubic-bezier(0.45, 0, 0.25, 1) forwards; }

@keyframes fold-sheen {
  from { background-position: 100% 0; }
  to   { background-position: 0% 0; }
}

/* ---- the editorial split ------------------------------------------------

   Below 64rem this does not exist and every section is the single column it
   always was. Above it, the prose sections split in two: the heading and its
   intro take the left column and stay with you while the content scrolls past
   on the right. That is what stops the page hugging the left edge and leaving
   half a laptop screen empty.

   Three sections opt out. Each is built around something that needs the whole
   measure — two application screenshots and a five-column comparison table —
   and squeezing those into a side column is worse than not laying them out. */

@media (min-width: 64rem) {
  section > .wrap:has(> .section-intro) {
    display: grid;
    grid-template-columns: 22rem minmax(0, 1fr);
    column-gap: clamp(3rem, 4vw, 4.5rem);
    /* Rows must not gap: the heading below spans fifty of them, and most are
       empty. A row gap would be counted fifty times and tear the section open. */
    row-gap: 0;
    align-items: start;
  }

  section > .wrap > .section-intro {
    grid-column: 1;
    /* Spanning the implicit rows is what gives a sticky heading somewhere to
       travel. `1 / -1` does not do it: -1 means the end of the EXPLICIT grid,
       which here is row 1, so the heading would be pinned with no room to move. */
    grid-row: 1 / span 50;
    position: sticky;
    /* Clear of the sticky header rather than behind it. */
    top: 7rem;
    margin-bottom: 0;
  }

  section > .wrap > *:not(.section-intro) { grid-column: 2; }

  #what-it-does > .wrap,
  #access > .wrap,
  #compare > .wrap { display: block; }

  /* Opting out of the two columns has to opt out of the sticking too. The
     heading sticks so that content can scroll past BESIDE it; in a single
     column there is no beside, so the screenshot and the comparison table
     scrolled straight through a pinned, transparent heading and both were
     drawn on top of each other. Here the heading scrolls away with the page,
     exactly as it does below 64rem, and takes back the space under it that the
     grid version gives up. */
  #what-it-does > .wrap > .section-intro,
  #access > .wrap > .section-intro,
  #compare > .wrap > .section-intro {
    position: static;
    margin-bottom: 2.4rem;
  }

  /* These three are full-width grids in the markup, but in the right-hand
     column they have about 38rem to work with rather than 72rem, so they take
     the column count that actually fits it. */
  #cost .meter,
  #rules .triggers,
  #join .columns { grid-template-columns: repeat(2, 1fr); }
}

/* Wide enough that the right column can carry the full count again. */
@media (min-width: 82rem) {
  #cost .meter { grid-template-columns: repeat(4, 1fr); }
  #rules .triggers { grid-template-columns: repeat(3, 1fr); }
}

/* ---- header ------------------------------------------------------------- */

/* The header follows you down the page. It carries its own background, so it
   sits over the dark hero without going transparent, and z-index 20 keeps it
   above the sticky section headings, which travel too. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

.site-header .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.85rem;
  flex-wrap: wrap;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-family: "Shippori Mincho B1", Georgia, serif;
  font-weight: 700;
  font-size: 1.15rem;
  letter-spacing: 0.01em;
  color: var(--fg);
  text-decoration: none;
}
.wordmark img { width: 30px; height: 30px; flex: none; }

.site-nav {
  display: flex;
  align-items: center;
  gap: 1.4rem;
  flex-wrap: wrap;
  font-size: 0.95rem;
  /* At middling widths the whole nav drops onto a second line, and as a lone
     flex item on that line it sat at the far LEFT -- the "Join the list" button
     ending 141px short of the right edge of the page, which the footer's two
     links are flush with. Growing to fill the line and packing its own contents
     to the end keeps the button on the right edge at every width, so the footer
     links stay under it. On a wide screen the nav already sat right, and this
     leaves it exactly where it was. */
  flex: 1 1 auto;
  justify-content: flex-end;
}
.site-nav a { color: var(--muted); text-decoration: none; }
.site-nav a:hover { color: var(--fg); text-decoration: underline; }
.site-nav a[aria-current="page"] { color: var(--fg); text-decoration: underline; }

/* Which page you are on. The build has no conditionals by design, so each page
   states its own name on <body> and the match is made here. That is why the
   visible state needs no JavaScript. The aria-current cue in scripts.html does
   need it, because a stylesheet cannot set an attribute. */
body[data-page="what-it-does"] .site-nav a[href="/what-it-does"],
body[data-page="cost"]         .site-nav a[href="/cost"],
body[data-page="safety"]       .site-nav a[href="/safety"],
body[data-page="compare"]      .site-nav a[href="/compare"],
body[data-page="faq"]          .site-nav a[href="/faq"] {
  color: var(--fg);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 5px;
}
.site-nav .button { color: var(--on-accent); text-decoration: none; }

/* Somebody who has just joined the list does not need to be asked to join the
   list. The other pages keep the button. */
body[data-page="thanks"] .site-nav .button { display: none; }

/* On a phone the six links and the button wrap to two rows and take a fifth of
   the screen before anything is read. The button is the one thing that can go:
   the home page has the form itself a few centimetres below it, and every other
   page carries one at the foot. The links stay -- they are how you get around a
   six-page site, and there is no JavaScript menu here to hide them behind. */
@media (max-width: 34rem) {
  .site-nav .button { display: none; }
  .site-nav { gap: 0.35rem 0.9rem; font-size: 0.86rem; }
  .site-header .wrap { padding-block: 0.55rem; gap: 0.5rem; }

  /* Three stacked rows of links took 108px off the top of a phone before a word
     of the page was read. One row that scrolls sideways inside itself keeps
     every link -- there is no JavaScript menu here to hide them behind -- and
     gives about 60px of it back. The overflow is the nav's own, so the page
     still does not scroll sideways. */
  .site-nav {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .site-nav::-webkit-scrollbar { display: none; }
  .site-nav a { white-space: nowrap; }
  .site-header .wrap { flex-wrap: nowrap; }
  .wordmark { flex: none; }

  /* A row of links stopping dead at the screen edge reads as a broken page
     rather than as something you can push along, so the last few millimetres
     fade out. There are always more links than fit at this width, so the hint
     is always true. */
  .site-nav {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 1.6rem), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 1.6rem), transparent);
  }
}
.site-nav .button:hover { color: var(--on-accent); }

/* ---- buttons ------------------------------------------------------------

   A button is a solid block of colour with solid contrasting text on it. Both
   states stay solid; hover changes the fill, never the solidity. There are no
   outline buttons and no tinted buttons anywhere on this site. */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.85rem 1.6rem;
  border: none;
  border-radius: 2px;
  font-family: inherit;
  font-weight: 700;
  font-size: 1rem;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
}

.button-primary { background: var(--accent); color: var(--on-accent); }
.button-primary:hover { background: var(--accent-hover); color: var(--on-accent); }

.button-secondary { background: var(--fg); color: var(--bg); }
.button-secondary:hover { background: var(--muted); color: var(--bg); }

/* ---- hero ---------------------------------------------------------------

   The hero is the product: the app's own artwork, shown as a wallpaper.

   It used to carry three mock desktop icons in front of the artwork, to make the
   point that your icons stay on top and still work. They were coloured
   rectangles drawn in CSS, and a yellow rectangle does not read as a folder --
   they looked like unexplained blocks rather than a desktop, so they are gone.
   If that point is worth making again it needs real icon shapes, not these. */

.hero { padding: 0; background: #0d0c0f; }

/* The home page is a landing screen rather than the top of a scroll, so the
   hero takes whatever height the header and footer leave instead of a fixed
   78vh. body is already a flex column carrying the footer to the bottom, so
   this is three lines rather than a viewport calculation that would have to
   know the header's height.

   It fills a normal laptop screen. It does not promise never to scroll: the
   field, the consent line and Turnstile's widget are a fixed stack about 300px
   tall, and on a short screen the page scrolls rather than the form shrinking.
   The form is the one thing here that must not get harder to use. */
body[data-page="home"] main { display: flex; flex-direction: column; }
body[data-page="home"] .hero { flex: 1; display: flex; flex-direction: column; }
body[data-page="home"] .hero > .desktop { flex: 1; }

/* The home page is a landing screen, so it has to FIT the window rather than
   merely begin in it. Every vertical measurement in the hero is therefore a
   fraction of the viewport height instead of a fixed size: a short window
   tightens the type and the gaps rather than pushing the form off the bottom.
   The clamps' upper ends are the sizes a roomy screen already had, so nothing
   changes on a large display.

   What does NOT shrink is the form itself -- the field, the button, the
   checkbox and Turnstile's widget keep their sizes at every width, because the
   form is the one thing on this page that must not get harder to use. */
body[data-page="home"] .hero-copy {
  padding-block: clamp(0.6rem, 2.2vh, 4rem);
}
body[data-page="home"] .hero-copy h1 {
  /* Height decides the size, until width would break the longer sentence over
     two lines -- then width decides it. That is what the min() is for.

     The height term is generous because there is room for it: moving the status
     line and the trademark notice to /support took about 70px off the footer,
     and shortening the hero note took more. On a 1366x768 laptop the hero has
     231px of slack, so a heading held down to 30px there was paying for space
     nothing was using. It reaches its old 3.6rem on a large screen again. */
  /* The ceiling is the form, not a round number. The signup row is 34rem wide,
     and "Your laptop, unbothered." -- the longer line -- measures 12.075 times
     the font size in Shippori Mincho. 34rem / 12.1 puts that line's right edge
     on the right edge of the "Email me at release" button, so the headline, the
     lede and the form share one column instead of the headline overhanging the
     rest by up to 151px on a wide screen. */
  font-size: clamp(1.1rem, min(5.4vh + 0.5rem, 6.6vw), calc(34rem / 12.1));
  line-height: 1.12;
}
body[data-page="home"] .hero-copy .lede {
  font-size: clamp(0.92rem, 1.8vh + 0.4rem, 1.24rem);
  line-height: 1.45;
  margin-top: clamp(0.35rem, 1.4vh, 1.1rem);
}
body[data-page="home"] .hero-note {
  margin-top: clamp(0.45rem, 1.3vh, 1.6rem);
  font-size: clamp(0.82rem, 0.9vh + 0.32rem, 0.95rem);
  line-height: 1.42;
}
body[data-page="home"] .privacy-line {
  margin-top: clamp(0.3rem, 0.8vh, 0.85rem);
  line-height: 1.4;
}

/* .desktop carries a min-height so the hero is a picture on a scrolling page.
   On the landing screen it is a flex item that already fills what is left, and
   the floor only ever fights that. */
body[data-page="home"] .desktop { min-height: 0; }

/* The clamps above are floored at sizes that suit a roomy phone. A small one --
   360x640 is still a common Android -- hits those floors and runs off the
   bottom anyway, so on a narrow screen the floors come down and the lines close
   up. Nothing is removed: this is the same page, set tighter. */
@media (max-width: 34rem) {
  body[data-page="home"] .hero-copy { padding-block: clamp(0.4rem, 1.6vh, 4rem); }
  body[data-page="home"] .hero-copy h1 { font-size: clamp(1.1rem, min(2.9vh + 0.5rem, 6.6vw), calc(34rem / 12.1)); }
  body[data-page="home"] .hero-copy .lede {
    font-size: clamp(0.87rem, 1.15vh + 0.42rem, 1.24rem);
    line-height: 1.38;
  }
  body[data-page="home"] .hero-note { font-size: 0.8rem; line-height: 1.35; }
  body[data-page="home"] .privacy-line { font-size: 0.78rem; line-height: 1.32; }
}

.desktop {
  position: relative;
  isolation: isolate;
  min-height: min(78vh, 640px);
  display: flex;
  align-items: flex-end;
  /* clip, not just hidden: an overflow:hidden box can still be scrolled by a link
     to an anchor inside it, and the header's Download (/#download) slid the art up
     by 54px, showing the pale plate under the dark scrim. A clipped box cannot be
     scrolled at all. hidden stays first for browsers without clip. */
  overflow: hidden;
  overflow: clip;
}

/* The artwork frame. It fills the hero, and it is what the parallax script
   moves. */
.desktop-art {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
  container-type: size;
}

/* The artwork is three layers, so it cannot use object-fit: cover, which only
   knows how to place one image. This box does the same arithmetic by hand, in
   container units: as wide as the frame or as wide as 1.5 times its height,
   whichever is more (the art is 3:2), centred across, and 42% of the spare
   height above it, which is what object-position: 50% 42% did. */
.art-stage {
  position: absolute;
  width: max(100cqw, 150cqh);
  aspect-ratio: 3 / 2;
  left: calc((100cqw - max(100cqw, 150cqh)) * 0.5);
  top: calc((100cqh - max(66.667cqw, 100cqh)) * 0.42);
}
.art-stage img { display: block; width: 100%; height: auto; }
.art-plate { position: absolute; inset: 0; }

/* Where tools/images.py cut them from, as fractions of the 1536x1024 art:
   BRANCH_BOX = (931, 0, 1536, 362) and CRANE_BOX = (692, 646, 1016, 920). */
.art-branch,
.art-crane { position: absolute; display: block; }
.art-branch { left: 60.612%; top: 0; width: 39.388%; }
.art-crane { left: 45.052%; top: 63.086%; width: 21.094%; }

/* Keeps the headline legible over artwork, and is the same idea as the app's
   icon-legibility scrim. */
.desktop::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    to bottom,
    rgba(8, 7, 10, 0.35) 0%,
    rgba(8, 7, 10, 0.62) 45%,
    rgba(8, 7, 10, 0.92) 100%
  );
}

/* A flex item will not shrink below the natural width of its contents unless it
   is told it may. Without this the signup row held the hero open wider than a
   phone screen, and because the widest element sets the width of the document,
   every section below it was pushed sideways too. */
.desktop > .wrap { min-width: 0; }

.hero-copy {
  position: relative;
  z-index: 2;
  padding-block: clamp(2.5rem, 6vw, 4rem);
  color: #ffffff;
}
/* No max-width. It used to hold the heading to three ragged lines at 17ch; now
   that each sentence is its own line, a measure narrower than the longer
   sentence is the one thing that can break the shape -- it wraps "unbothered."
   onto a third line. The line length is set by the sentence, and the font size
   is what keeps it on one line at any width. */
.hero-copy h1 { color: #ffffff; max-width: none; }
.hero-copy h1 .hero-line { display: block; }
.hero-copy .lede { color: rgba(255, 255, 255, 0.87); margin-top: 1.1rem; }

.hero-note {
  margin-top: 1.6rem;
  max-width: 34rem;
  color: rgba(255, 255, 255, 0.87);
  font-size: 0.95rem;
}
.hero-note b { color: #ffffff; }

/* ---- the download ----------------------------------------------------------

   The Mac badge is styled after the badges macOS apps are offered with, not
   after this site's own buttons: black in both themes, rounded, a hairline
   border, the system typeface (San Francisco on a Mac), a small line over the
   platform in large type. A visitor recognises it as "the Mac download" before
   reading it. The icon is a plain laptop; Apple's logo is not ours to use. */

.mac-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 1.35rem 0.62rem 1.05rem;
  background: #000000;
  color: #ffffff;
  border: 1px solid #a6a6a6;
  border-radius: 0.85rem;
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", Helvetica, Arial, sans-serif;
  line-height: 1;
  text-decoration: none;
  transition: background-color 0.15s ease, transform 0.15s ease;
}
.mac-badge:hover { background: #1d1d1f; color: #ffffff; }
.mac-badge:active { transform: scale(0.98); }
.mac-badge:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 3px; }
.mac-badge-icon { width: 2.15rem; height: auto; flex: none; }
.mac-badge-text { display: flex; flex-direction: column; gap: 0.2rem; text-align: left; }
.mac-badge-small { font-size: 0.74rem; font-weight: 500; letter-spacing: 0.01em; }
.mac-badge-big { font-size: 1.6rem; font-weight: 600; letter-spacing: -0.02em; }

/* The Windows download: the Mac badge, in Windows' own typeface. */
.win-download { font-family: "Segoe UI Variable Display", "Segoe UI", -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif; }

.downloads { display: flex; flex-wrap: wrap; align-items: center; gap: 0.8rem; }

/* Each platform sees its own download (data-platform, set by head.html before the
   page is drawn). Unknown platforms, no script, and "Show all platforms" see both. */
html[data-platform="mac"] .for-windows,
html[data-platform="windows"] .for-mac { display: none; }
/* The hero says "out now for Mac, Windows soon" once; the Windows-only wording of it
   is for Windows visitors alone. */
html:not([data-platform="windows"]) .hero-note.for-windows { display: none; }

.show-all { display: none; margin-top: 0.55rem; font-size: 0.85rem; }
html[data-platform="mac"] .show-all,
html[data-platform="windows"] .show-all { display: block; }
.link-button {
  all: unset;
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.link-button:hover { color: var(--fg); }
.link-button:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }
.hero .link-button { color: rgba(255, 255, 255, 0.75); }
.hero .link-button:hover { color: #ffffff; }

.hero-download { margin-top: clamp(1rem, 2.4vh, 1.8rem); }
.hero-download .hero-note { margin-top: 1rem; }
.hash { overflow-wrap: anywhere; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 0.92em; }

.get-it { max-width: 40rem; }
.download-meta { margin-top: 0.9rem; font-size: 0.9rem; color: var(--muted); }
.install-steps { margin-top: 1.4rem; padding-left: 1.3rem; }
.install-steps li { margin-top: 0.45rem; }
.install-steps li::marker { color: var(--accent-text); font-weight: 700; }

.privacy-line { margin-top: 0.85rem; font-size: 0.86rem; color: var(--muted); }
.hero .privacy-line { color: rgba(255, 255, 255, 0.75); }
.hero .privacy-line a { color: #ffffff; }

/* ---- generic content blocks ---------------------------------------------- */

.section-intro { max-width: 40rem; margin-bottom: 2.4rem; }

/* A content page's <h1> lives in the 22rem left column of the editorial split,
   not across a hero. At the hero's size a heading like "Some wallpaper apps can
   run programs. Wallgami cannot." came out six lines deep and swallowed the
   page. This keeps it above h2 in the hierarchy and inside its column. */
.section-intro h1 { font-size: clamp(1.75rem, 3vw, 2.3rem); }
.section-intro p { max-width: 38rem; }

.columns {
  display: grid;
  gap: 1.8rem 2.4rem;
  margin-top: 2rem;
}
@media (min-width: 48rem) { .columns { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .columns-three { grid-template-columns: repeat(3, 1fr); } }

.columns h3 { margin-bottom: 0.4rem; }
.columns p { max-width: 30rem; }

.status-list { margin: 0; padding: 0; list-style: none; max-width: 40rem; }
.status-list li {
  display: grid;
  grid-template-columns: 1.6rem 1fr;
  gap: 0.7rem;
  padding-block: 0.6rem;
  border-top: 1px solid var(--border);
}
.status-list li:last-child { border-bottom: 1px solid var(--border); }
.status-list .yes { color: var(--success); font-weight: 700; }
.status-list .no { color: var(--warn); font-weight: 700; }

/* ---- the power rules ----------------------------------------------------

   Nine situations, four possible answers each. This was a table with a nine-row
   rowspan, which put one cell of text beside eight empty ones and read as
   broken. The four actions are the same for every situation, so they are said
   once and the situations are simply listed. */

.actions {
  max-width: 40rem;
  padding: 1.1rem 1.3rem;
  background: var(--card);
  border-left: 3px solid var(--accent);
}
.actions h3 { margin-bottom: 0.3rem; }

.triggers {
  display: grid;
  gap: 1px;
  margin: 2rem 0 0;
  padding: 0;
  list-style: none;
  background: var(--border);
  border: 1px solid var(--border);
}
@media (min-width: 34rem) { .triggers { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 56rem) { .triggers { grid-template-columns: repeat(3, 1fr); } }

.triggers li {
  background: var(--card);
  padding: 0.9rem 1.1rem;
  font-size: 0.95rem;
  line-height: 1.4;
}

/* ---- tables -------------------------------------------------------------- */

.table-scroll { overflow-x: auto; margin-top: 2rem; }

table {
  border-collapse: collapse;
  width: 100%;
  font-size: 0.94rem;
}

caption { text-align: left; color: var(--muted); font-size: 0.88rem; padding-bottom: 0.8rem; }

th, td {
  text-align: left;
  padding: 0.7rem 0.9rem;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

thead th {
  font-weight: 700;
  border-bottom: 2px solid var(--crease);
  white-space: nowrap;
}

tbody th { font-weight: 500; color: var(--fg); }

td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }

.compare td { color: var(--muted); }
.compare .us { color: var(--fg); font-weight: 700; }
.compare thead .us { background: var(--accent); color: var(--on-accent); }

/* Four columns of prose cannot shrink to a phone, so below this width every row
   becomes a small block and each cell shows its column name. The table markup
   and its screen-reader semantics are unchanged. */
@media (max-width: 52rem) {
  .compare thead { display: none; }
  .compare tbody { display: block; }
  .compare tr { display: block; border: 1px solid var(--border); margin-bottom: 1rem; background: var(--card); }
  .compare tbody th { display: block; background: var(--crease); color: var(--fg); font-weight: 700; border-bottom: none; }
  .compare td { display: block; border-bottom: 1px solid var(--border); padding-left: 9.5rem; position: relative; }
  .compare td:last-child { border-bottom: none; }
  .compare td::before {
    content: attr(data-label);
    position: absolute;
    left: 0.9rem;
    width: 8.2rem;
    font-weight: 700;
    color: var(--fg);
  }
}

/* ---- the cost meter -----------------------------------------------------

   Set the way the app's own meter sets it, because the point of the section is
   that these are readings rather than claims. */

.meter {
  display: grid;
  gap: 1px;
  margin-top: 2rem;
  background: var(--border);
  border: 1px solid var(--border);
}
@media (min-width: 40rem) { .meter { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 62rem) { .meter { grid-template-columns: repeat(4, 1fr); } }

.meter-cell { background: var(--card); padding: 1.1rem 1.2rem; margin: 0; }
.meter-cell dt { color: var(--muted); font-size: 0.86rem; line-height: 1.3; margin-bottom: 0.35rem; }
.meter-cell dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.15;
}
.meter-cell dd span { font-size: 0.92rem; font-weight: 500; color: var(--muted); }

/* ---- the safety section -------------------------------------------------- */

.cite {
  margin-top: 1.6rem;
  padding: 1rem 1.2rem;
  border-left: 3px solid var(--accent);
  background: var(--card);
  font-size: 0.92rem;
  color: var(--muted);
  max-width: 40rem;
}
.cite a { color: var(--accent-text); }

.claim {
  margin-top: 1.6rem;
  padding-top: 1.4rem;
  border-top: 1px solid var(--border);
  max-width: 40rem;
}

/* ---- price --------------------------------------------------------------- */

.price-line {
  font-family: "Shippori Mincho B1", Georgia, serif;
  font-size: clamp(1.5rem, 3.4vw, 2.1rem);
  font-weight: 700;
  line-height: 1.3;
  margin-bottom: 1rem;
  max-width: 24ch;
}

/* ---- faq ----------------------------------------------------------------- */

.faq { margin-top: 2rem; }
.faq h3 { margin-bottom: 0.35rem; }
.faq-item { padding-block: 1.1rem; border-top: 1px solid var(--border); max-width: 42rem; }
.faq-item:last-child { border-bottom: 1px solid var(--border); }

/* ---- shots --------------------------------------------------------------- */

figure { margin: 2rem 0 0; }
figure img { border: 1px solid var(--border); width: 100%; }
/* A picture smaller than the column is shown at its own size, not stretched soft. */
.figure-narrow { max-width: 662px; }
figcaption { margin-top: 0.7rem; font-size: 0.88rem; color: var(--muted); max-width: 38rem; }

/* ---- footer -------------------------------------------------------------- */

/* The footer is pinned to the bottom of the window, not parked at the end of the
   page, so the release status and the way to reach a person are always on screen.

   That is only affordable because it no longer repeats the header's six links:
   what is left is two short lines and the trademark notice, about 100px on a
   laptop and 150px on a phone, against 228px and 385px for the old one. body
   carries that height back as bottom padding -- see --footer-h below -- so the
   last line of a page is never underneath it, and so the home page's hero still
   measures itself against the space actually left over. */
.site-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 15;
  height: var(--footer-h);
  border-top: 1px solid var(--border);
  background: var(--panel);
  font-size: 0.92rem;
  color: var(--muted);
}
/* Two links and nothing else. They are set in the accent red rather than the
   muted grey the rest of the site uses for secondary text, because at this size
   -- one short row at the very bottom of the window -- grey-on-panel is the
   first thing a person stops seeing. No underline, and they go white under the
   pointer, so the change of state is the colour rather than a rule appearing. */
/* Pushed to the right, where they land directly under the header's "Join the
   list" button: the header and the footer share the same .wrap, so the right
   edge of one is the right edge of the other at every width. */
.site-footer .wrap {
  display: flex;
  height: 100%;
  align-items: center;
  justify-content: flex-end;
  gap: 1.4rem;
}
.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 1.4rem;
  max-width: none;
}
.site-footer a {
  color: var(--accent-text);
  text-decoration: none;
  font-weight: 500;
}
.site-footer a:hover,
.site-footer a:focus-visible {
  color: var(--footer-hover);
  text-decoration: none;
}

/* Room for the pinned footer, and the same number the footer itself is tall, so
   the two cannot drift apart.

   This used to be measured in the page by a script, because the footer carried
   the release status and a trademark notice that wrapped to a different number
   of lines at every width. Two things went wrong with that and both were real:
   the measurement fed back into its own input and oscillated until the layout
   check timed out, and when the script had not run yet the page fell back to an
   over-generous guess -- which is why the home page scrolled on a small phone
   while every other size fitted.

   The footer is now two links on one line. Its height is a number, not a
   measurement, so it is written down here and nothing has to run to find it.
   tools/check-footer.py is what catches it if that ever stops being true. */
body { padding-bottom: var(--footer-h); }

/* On a phone the footer's three parts each take their own line and the
   trademark notice runs to four, which put it at 165px -- a fifth of the screen,
   permanently. The notice has to stay, so what gives is its size: this brings it
   back to about 120px without dropping a word of it. */
@media (max-width: 30rem) {
  :root { --footer-h: 2.7rem; }
  .site-footer { font-size: 0.9rem; }
  .site-footer .wrap { gap: 1.2rem; }
}

/* ---- short pages --------------------------------------------------------- */

.page-head { padding-block: clamp(2.5rem, 6vw, 4rem) 0; }
.page-body { padding-block: var(--gap); }
.page-body h2 { margin-top: 2.4rem; }
.page-body h2:first-child { margin-top: 0; }
.page-body ul { max-width: var(--measure); padding-left: 1.2rem; }
.page-body li { margin-bottom: 0.5rem; }

.outcome { padding-block: clamp(3rem, 8vw, 5rem); }
.outcome h1 { margin-bottom: 1.2rem; }


/* ---- motion --------------------------------------------------------------

   Read the two rules before changing anything here.

   1. The stylesheet never hides anything on its own. `.reveal` is put on by the
      script at runtime, so with scripting off, or broken, or still loading, the
      page arrives complete and visible. Nothing below is allowed to become the
      reason a paragraph cannot be read.

   2. `prefers-reduced-motion` gets no motion, not less motion. The script
      returns before it touches the document, and the reduce block near the top
      of this file flattens whatever is left. */

/* 14px over half a second was there on 2026-09-26 and nobody could tell it was
   happening. This is enough travel to be seen arriving without being a show. */
.reveal {
  opacity: 0;
  transform: translateY(30px);
  transition:
    opacity 0.8s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 0.8s cubic-bezier(0.22, 0.61, 0.36, 1);
  transition-delay: var(--reveal-delay, 0s);
}
/* Pictures also grow the last few percent into place, like a print laid down. */
figure.reveal { transform: translateY(30px) scale(0.96); }
.reveal.is-in { opacity: 1; transform: none; }

/* The artwork is held slightly larger than its frame so there is somewhere for
   it to travel without uncovering an edge: scale 1.14 leaves 7% of headroom top
   and bottom, and the script never shifts it more than 6%. The translate is
   written before the scale so it is measured in unscaled pixels, which is what
   makes that 6% mean 6%. */
.js-parallax .desktop-art {
  transform: translate3d(0, var(--parallax, 0px), 0) scale(1.14);
  will-change: transform;
}

/* ---- state changes ------------------------------------------------------

   Hover and focus, which the page always had; they simply arrive over 180ms now
   instead of instantly. --card-hover has been sitting in the tokens unused
   since the first commit, and this is the thing it was for. */

.button { transition: background-color 0.18s ease, color 0.18s ease; }
.button:active { transform: translateY(1px); }

.site-nav a,
.site-footer a,
a { transition: color 0.18s ease; }

.triggers li,
.meter-cell { transition: background-color 0.18s ease; }
.triggers li:hover,
.meter-cell:hover { background: var(--card-hover); }

/* The nine situations arrive one after another, and the rule above would
   otherwise replace the reveal's transition with the hover one. Both, then,
   with the run's delay on the arrival only. */
.triggers li.reveal {
  transition-property: opacity, transform, background-color;
  transition-duration: 0.8s, 0.8s, 0.18s;
  transition-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1), cubic-bezier(0.22, 0.61, 0.36, 1), ease;
  transition-delay: var(--reveal-delay, 0s), var(--reveal-delay, 0s), 0s;
}


/* ---- depth, light mode only ----------------------------------------------

   On a dark page a raised surface reads by being lighter than what is under it,
   which the card tokens already do. On a light page nearly everything is light,
   so the same surfaces need a shadow to sit above the sheet rather than lie flat
   on it. Warm-toned rather than grey, to match the paper. */

@media (prefers-color-scheme: light) {
  .actions,
  .cite,
  .meter,
  .triggers {
    box-shadow:
      0 1px 2px rgba(58, 42, 32, 0.05),
      0 6px 16px -8px rgba(58, 42, 32, 0.15);
  }

  figure img {
    box-shadow:
      0 2px 4px rgba(58, 42, 32, 0.06),
      0 14px 30px -14px rgba(58, 42, 32, 0.22);
  }
}

/* ---- petals -------------------------------------------------------------

   The hero artwork is a still image. These are the one thing on the site that
   moves without being asked to, and they are CSS animations on transform and
   opacity rather than video, canvas or WebGL. The reason is not only weight:

     * A sakura video loop is two to five megabytes against the 600 KB budget
       tools/check.py enforces, and autoplaying video is a real draw on a
       battery.
     * Three.js is around 600 KB on its own, and this site's CSP allows scripts
       from 'self' and Turnstile only, so a CDN copy would simply be blocked.
     * Either one would have this page spending a laptop's battery to decorate
       an argument about not spending a laptop's battery.

   Composited transforms cost the main thread nothing per frame, need no
   JavaScript at all -- so they survive a script that never loads -- and the
   reduce-motion block near the top of this file already stops them dead.

   Two animations are needed and they both want `transform`, so the outer
   element carries the fall and its ::before carries the sway. One element
   cannot run both. */

.petals {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}

.petal {
  position: absolute;
  top: 0;
  animation-name: petal-fall;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  will-change: transform;
}

.petal::before {
  content: "";
  display: block;
  width: 100%;
  height: 100%;
  /* Two opposite corners rounded is a petal; a circle is a bubble. */
  border-radius: 100% 0 100% 0;
  background: linear-gradient(140deg, #fcdfe6, #e293a8);
  animation-name: petal-sway;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}

/* Scrolled past the hero there is nothing to look at, and a page that goes on
   animating anyway is this one failing its own argument. The script puts this
   class on when the hero leaves the screen; without a script the petals simply
   keep drifting, which costs a composited transform and nothing else. */
.petals.at-rest .petal,
.petals.at-rest .petal::before { animation-play-state: paused; }

@keyframes petal-fall {
  0%   { transform: translate3d(0, -6vh, 0); opacity: 0; }
  7%   { opacity: var(--peak); }
  88%  { opacity: var(--peak); }
  100% { transform: translate3d(0, 104vh, 0); opacity: 0; }
}

@keyframes petal-sway {
  from { transform: translateX(calc(var(--drift) * -1)) rotate(calc(var(--spin) * -1)); }
  to   { transform: translateX(var(--drift)) rotate(var(--spin)); }
}

/* ---- the branch and the crane -------------------------------------------

   The branch sways from where it enters the picture, the top-right corner, the
   way a real one moves: a slow lean in the wind with a quicker shiver on top of
   it. Two animations that both want `transform`, so the wrapper leans and the
   picture inside it shivers, as with the petals. The periods (11s and 3.7s)
   share no factor, so the two never line up into a visible loop.

   The crane lifts off the paper a little and settles again, tipping forward as
   it rises, like something light in the same breeze. Its painted shadow stays on
   the paper underneath, so the gap it leaves is real.

   None of this is declared unless the machine allows motion. Under a
   reduced-motion request the reduce block at the top of this file would only
   shorten these, which for an endless loop is not the same as stopping it. */

@media (prefers-reduced-motion: no-preference) {
  .art-branch {
    transform-origin: 100% 0;
    animation: branch-lean 11s ease-in-out infinite;
    will-change: transform;
  }
  .art-branch img {
    transform-origin: 100% 0;
    animation: branch-shiver 3.7s ease-in-out infinite alternate;
  }
  .art-crane {
    transform-origin: 50% 95%;
    animation: crane-lift 7.3s ease-in-out infinite;
    will-change: transform;
  }

  /* Off screen there is nothing to look at, so, like the petals, they rest. */
  .hero.at-rest .art-branch,
  .hero.at-rest .art-branch img,
  .hero.at-rest .art-crane { animation-play-state: paused; }
}

@keyframes branch-lean {
  0%   { transform: rotate(0deg); }
  30%  { transform: rotate(-1.4deg); }
  55%  { transform: rotate(-0.5deg); }
  75%  { transform: rotate(-1.1deg); }
  100% { transform: rotate(0deg); }
}

@keyframes branch-shiver {
  from { transform: rotate(0.25deg); }
  to   { transform: rotate(-0.3deg); }
}

/* ---- the pretend laptop on /what-it-does --------------------------------

   The rules list acted out on a laptop: a wallpaper plays, then a game goes
   full screen, the charger comes out, the screen locks, and a notification
   says what Wallgami did about each one. The script moves data-scene along;
   everything that changes is a transition keyed off it, so without the script
   this is the first scene standing still.

   The laptop is an inline-size container and the screen a size container, so
   everything is measured in their own width and height (cqw, cqh) and the
   whole thing scales like a photograph would. The screen is dark in both
   themes, because a desktop is. */

.demo { margin: 0.4rem 0 2.2rem; }
/* The site's figure images get a border, a shadow and full width. None of that
   belongs on the pieces of a drawing. */
.demo img { border: 0; box-shadow: none; }

.demo-laptop { container-type: inline-size; padding-inline: 5%; }

.demo-lid {
  position: relative;
  padding: 2.6cqw 2cqw 2.2cqw;
  background: #0b0b0e;
  border-radius: 2.2cqw 2.2cqw 0.6cqw 0.6cqw;
  box-shadow: 0 0 0 2px #b4b6bd;
}
.demo-cam {
  position: absolute;
  top: 1cqw;
  left: 50%;
  width: max(0.8cqw, 4px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: #24242b;
  transform: translateX(-50%);
}

/* Wider than the lid, silver, with the groove you open it by. */
.demo-base {
  position: relative;
  margin-inline: -5.5%;
  height: max(2.4cqw, 8px);
  background: linear-gradient(180deg, #f2f3f6 0%, #d3d5da 38%, #a2a4ab 100%);
  border-radius: 2px 2px 50% 50% / 2px 2px 100% 100%;
  box-shadow: 0 1.4cqw 1.8cqw -1.2cqw rgba(0, 0, 0, 0.6);
}
.demo-notch {
  position: absolute;
  top: 0;
  left: 50%;
  width: 15%;
  height: 42%;
  background: linear-gradient(180deg, #9a9ca3, #c9cbd1);
  border-radius: 0 0 1cqw 1cqw;
  transform: translateX(-50%);
}

.demo-screen {
  position: relative;
  aspect-ratio: 16 / 10;
  container-type: size;
  overflow: hidden;
  border-radius: 2px;
  background: #000000;
  color: #ffffff;
}

/* -- the wallpaper: a drawn dusk over a lake, with the hero's branch and crane */

.demo-wall { position: absolute; inset: 0; transition: filter 0.7s ease, transform 0.7s ease; }
.dm-scene { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.demo-branch { position: absolute; top: 0; right: -2%; width: 40%; }
.demo-crane { position: absolute; left: 8%; bottom: 15.5%; width: 12%; }
.demo-branch img,
.demo-crane img { display: block; width: 100%; height: auto; }

.demo-petals { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.demo-petals i {
  position: absolute;
  top: -4%;
  width: 1.1cqw;
  aspect-ratio: 1;
  border-radius: 100% 0 100% 0;
  background: linear-gradient(140deg, #fcdfe6, #e293a8);
  opacity: 0;
}
.demo-petals i:nth-child(1) { left: 62%; --fall: 9s; --wait: -1s; }
.demo-petals i:nth-child(2) { left: 74%; --fall: 11s; --wait: -6s; width: 0.9cqw; }
.demo-petals i:nth-child(3) { left: 86%; --fall: 8s; --wait: -3.5s; }
.demo-petals i:nth-child(4) { left: 55%; --fall: 12s; --wait: -9s; width: 0.8cqw; }
.demo-petals i:nth-child(5) { left: 93%; --fall: 10s; --wait: -2s; width: 1.3cqw; }
.demo-petals i:nth-child(6) { left: 68%; --fall: 9.5s; --wait: -7.5s; }

/* -- desktop icons ------------------------------------------------------- */

/* The icons are their own layer, not part of the wallpaper: when the wallpaper
   stops and greys out, or blurs behind the lock screen, the icons do not --
   that is the whole idea of a wallpaper. The lock screen hides them. */
.demo-icons {
  position: absolute;
  z-index: 1;
  left: 1.2cqw;
  top: 2.4cqh;
  transition: opacity 0.4s ease;
  display: grid;
  gap: 1.6cqh;
}
.demo-icon {
  display: grid;
  justify-items: center;
  gap: 0.5cqh;
  width: 9cqw;
}
.demo-icon svg,
.demo-icon img {
  display: block;
  width: 5cqw;
  height: auto;
  aspect-ratio: 1;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.45));
}
.demo-icon > span {
  font-size: clamp(0.42rem, 1.45cqw, 0.72rem);
  line-height: 1.15;
  text-align: center;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
}

/* -- the taskbar, Windows 11 shaped: apps in the middle, tray on the right -- */

.demo-taskbar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  height: max(7.5cqh, 1.5rem);
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(28, 26, 34, 0.84);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  transition: opacity 0.4s ease;
}
.demo-apps { display: flex; align-items: center; gap: 1.3cqw; }
.demo-apps svg,
.demo-running img { display: block; width: max(3cqw, 0.8rem); height: auto; }
.demo-running {
  position: relative;
  padding: 0.35cqw;
  border-radius: 0.6cqw;
  background: rgba(255, 255, 255, 0.1);
}
.demo-running::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -0.1cqw;
  width: 1.4cqw;
  height: 2px;
  border-radius: 2px;
  background: #4aa3ff;
  transform: translateX(-50%);
}
.demo-tray {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 1.4cqw;
  display: flex;
  align-items: center;
  gap: 0.9cqw;
}
.demo-tray svg { display: block; width: max(1.9cqw, 0.55rem); height: auto; }
.demo-tray .demo-power { width: max(2.5cqw, 0.8rem); }
.demo-charge { transform-origin: 2.5px 0; transition: transform 0.6s ease; }
.demo-plug { transition: opacity 0.3s ease; }
.demo-clock {
  font-size: clamp(0.42rem, 1.3cqw, 0.7rem);
  line-height: 1.25;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* -- the full-screen game ------------------------------------------------ */

.demo-game {
  position: absolute;
  /* A pixel past every edge: at a fractional screen width the last column
     otherwise lets the cherry blossoms behind show through. The screen clips it. */
  inset: -1px;
  z-index: 2;
  overflow: hidden;
  background: linear-gradient(180deg, #0b0620 0%, #2a0f4a 42%, #7a1c6c 58%, #0b0620 58%);
  opacity: 0;
  transform: scale(1.04);
  transition: opacity 0.3s ease, transform 0.4s ease;
}
.dg-sun {
  position: absolute;
  left: 50%;
  top: 20%;
  width: 24cqw;
  aspect-ratio: 1;
  border-radius: 50%;
  background: linear-gradient(180deg, #ffe45c, #ff4f93);
  -webkit-mask: linear-gradient(180deg, #000 0 52%, transparent 52% 56%, #000 56% 64%, transparent 64% 67%, #000 67% 74%, transparent 74% 76%, #000 76%);
          mask: linear-gradient(180deg, #000 0 52%, transparent 52% 56%, #000 56% 64%, transparent 64% 67%, #000 67% 74%, transparent 74% 76%, #000 76%);
  transform: translateX(-50%);
}
.dg-floor {
  position: absolute;
  left: -50%;
  right: -50%;
  top: 58%;
  height: 80%;
  background-color: #0b0620;
  background-image:
    linear-gradient(#ff3dbb 2px, transparent 2px),
    linear-gradient(90deg, #ff3dbb 2px, transparent 2px);
  background-size: 5cqw 5cqw;
  transform-origin: 50% 0;
  transform: perspective(30cqh) rotateX(60deg);
}
/* The road is part of the tilted floor, so it narrows to the horizon for
   free: cyan edges, and a yellow centre line that runs at the camera. */
.dg-road {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 17%;
  transform: translateX(-50%);
  background:
    linear-gradient(90deg, #7ff6ff 0 2%, transparent 2% 98%, #7ff6ff 98%) 0 0 / 100% 100% no-repeat,
    linear-gradient(#ffe45c 50%, transparent 50%) 50% 0 / 2% 8cqw repeat-y,
    #120a24;
}

/* Cars are drawn once (the <symbol>) and coloured by currentColor. Each one
   stands on its bottom edge, so scaling it is it coming closer. */
/* Holds the car drawing; takes no room. */
.dg-defs { position: absolute; width: 0; height: 0; }
.dg-car { position: absolute; left: 50%; width: 30cqw; height: auto; transform-origin: 50% 100%; }
.dg-player {
  bottom: 2.5cqh;
  color: #7ff6ff;
  transform: translateX(-50%);
  filter: drop-shadow(0 0 1.2cqw rgba(255, 42, 90, 0.55));
}
.dg-rival { bottom: 42%; opacity: 0; }
.dg-rival1 { color: #ff3dbb; }
.dg-rival2 { color: #ffb347; }

.dg-speed {
  position: absolute;
  right: 2cqw;
  bottom: 3cqh;
  font: 700 clamp(0.5rem, 1.7cqw, 0.85rem) / 1 ui-monospace, "Cascadia Mono", Consolas, monospace;
  letter-spacing: 0.06em;
  color: #7ff6ff;
  text-shadow: 0 0 6px #19e3ff;
}
.dg-speed b { font-size: 1.8em; }

.dg-hud {
  position: absolute;
  left: 2cqw;
  top: 2.4cqh;
  font: 700 clamp(0.5rem, 1.7cqw, 0.85rem) / 1 ui-monospace, "Cascadia Mono", Consolas, monospace;
  letter-spacing: 0.08em;
  color: #7ff6ff;
  text-shadow: 0 0 6px #19e3ff;
}

/* -- the lock screen: the wallpaper, frosted, and the time ------------------ */

.demo-lock {
  position: absolute;
  inset: -1px;
  z-index: 2;
  display: grid;
  justify-content: center;
  align-content: start;
  padding-top: 13cqh;
  text-align: center;
  background: rgba(12, 8, 20, 0.35);
  opacity: 0;
  transition: opacity 0.5s ease;
}
.demo-lock b { font-size: clamp(1.6rem, 12cqw, 5rem); font-weight: 600; line-height: 1; color: #ffffff; }
.demo-lock span { margin-top: 0.3em; font-size: clamp(0.6rem, 2.4cqw, 1.1rem); }

/* -- the scenes ------------------------------------------------------------ */

.demo[data-scene="game"] .demo-game { opacity: 1; transform: none; }
.demo[data-scene="lock"] .demo-lock { opacity: 1; }
.demo[data-scene="lock"] .demo-icons { opacity: 0; }
/* The lock screen's frost is the wallpaper itself blurred, and zoomed a little
   so the blur has no edge to thin out at. A frosted layer on top (backdrop-
   filter) left the sharp picture showing down the right-hand edge. */
.demo[data-scene="lock"] .demo-wall { filter: blur(12px) brightness(0.8); transform: scale(1.12); }
.demo[data-scene="lock"] .demo-taskbar,
.demo[data-scene="game"] .demo-taskbar { opacity: 0; }
.demo[data-scene="battery"] .demo-wall { filter: grayscale(0.75) brightness(0.55); }
.demo[data-scene="battery"] .demo-plug { opacity: 0; }
.demo[data-scene="battery"] .demo-charge { transform: scaleX(0.55); }

/* -- what Wallgami did, under the laptop rather than on its screen -------

   One card, centred beneath it, at every width. */

.demo-body { display: grid; gap: 1.1rem; }
.demo-side { display: flex; justify-content: center; }
.demo-status {
  flex: 0 1 26rem;
  min-width: 0;
  text-align: center;
  padding: 0.85rem 1rem 0.95rem;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 6px;
}
.demo.status-fresh .demo-status { animation: demo-status-in 0.45s cubic-bezier(0.22, 0.61, 0.36, 1); }
.demo-status p { margin: 0; }
.demo-status-app {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.78rem;
  color: var(--muted);
}
.demo .demo-status-app img { width: 1.1rem; height: 1.1rem; }
.demo-status-title {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.35rem;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--fg);
}
.demo-status-word { min-width: 0; }
.demo-status-body { margin-top: 0.2rem; font-size: 0.88rem; line-height: 1.4; color: var(--muted); }
.demo-dot {
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--success);
  flex: none;
}
.demo[data-scene="game"] .demo-dot,
.demo[data-scene="lock"] .demo-dot { background: var(--warn); }
.demo[data-scene="battery"] .demo-dot { background: var(--muted); }

/* Pause / play, as an icon on the screen's top-right corner. It sits on the
   lid rather than inside the screen, because the screen is announced as one
   picture and a picture cannot contain a button. Solid, like every button
   here: a dark block with a white symbol, which reads on any wallpaper. */
.demo-toggle {
  position: absolute;
  top: calc(2.6cqw + 1.4cqw);
  right: calc(2cqw + 1.4cqw);
  z-index: 5;
  display: grid;
  place-items: center;
  width: max(4.4cqw, 2rem);
  height: max(4.4cqw, 2rem);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: #16151a;
  color: #ffffff;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
  transition: background-color 0.18s ease;
}
.demo-toggle:hover { background: #d2252b; color: #ffffff; }
.demo-toggle svg { display: none; width: 55%; height: 55%; fill: currentColor; }
.demo-toggle[data-icon="pause"] .demo-i-pause,
.demo-toggle[data-icon="play"] .demo-i-play,
.demo-toggle[data-icon="next"] .demo-i-next { display: block; }
.demo-lid:hover .demo-toggle { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6); }

.demo figcaption { margin: 1rem auto 0; text-align: center; }

@keyframes demo-status-in {
  from { opacity: 0; transform: translateX(-10px); }
}
/* The hidden attribute has to win over .button's display. */
.demo-toggle[hidden] { display: none; }

/* -- the motion, only where motion is welcome ----------------------------- */

@media (prefers-reduced-motion: no-preference) {
  .dm-tw { animation: dm-twinkle 3.2s ease-in-out infinite alternate; }
  .dm-tw2 { animation-delay: -1.1s; }
  .dm-tw3 { animation-delay: -2.2s; }
  .dm-glow {
    transform-box: fill-box;
    transform-origin: center;
    animation: dm-breathe 6s ease-in-out infinite alternate;
  }
  .dm-cloud { animation: dm-drift 40s ease-in-out infinite alternate; }
  .dm-cloud2 { animation-duration: 54s; animation-direction: alternate-reverse; }
  .dm-cloud3 { animation-duration: 46s; }
  .dm-glint {
    transform-box: fill-box;
    transform-origin: center;
    animation: dm-shimmer 2.4s ease-in-out infinite alternate;
  }
  .dm-glint2 { animation-delay: -0.8s; }
  .dm-glint3 { animation-delay: -1.6s; }
  .demo-branch { transform-origin: 100% 0; animation: branch-lean 11s ease-in-out infinite; }
  .demo-branch img { transform-origin: 100% 0; animation: branch-shiver 3.7s ease-in-out infinite alternate; }
  .demo-crane { transform-origin: 50% 95%; animation: crane-lift 7.3s ease-in-out infinite; }
  .demo-petals i { animation: dm-fall var(--fall) linear var(--wait) infinite; }
  .dg-floor { animation: dg-drive 0.5s linear infinite; }
  .dg-road { animation: dg-dash 0.35s linear infinite; }
  .dg-player { animation: dg-steer 1.7s ease-in-out infinite alternate; }
  .dg-rival1 { animation: dg-pass-left 2.9s cubic-bezier(0.55, 0, 0.9, 0.45) infinite; }
  .dg-rival2 { animation: dg-pass-right 3.7s cubic-bezier(0.55, 0, 0.9, 0.45) -1.7s infinite; }

  /* Whenever it is not playing, the wallpaper in it is actually still -- which
     is the point being made. Paused by the button, or scrolled away, too. */
  .demo:not([data-scene="play"]) .demo-wall *,
  .demo.is-paused .demo-wall *,
  .demo:not([data-scene="game"]) .demo-game *,
  .demo.is-paused .demo-game * { animation-play-state: paused; }
}

@keyframes dm-twinkle { from { opacity: 1; } to { opacity: 0.2; } }
@keyframes dm-breathe {
  from { transform: scale(0.92); opacity: 0.8; }
  to   { transform: scale(1.06); opacity: 1; }
}
@keyframes dm-drift {
  from { transform: translateX(-70px); }
  to   { transform: translateX(90px); }
}
@keyframes dm-shimmer {
  from { transform: scaleX(0.6); opacity: 0.45; }
  to   { transform: scaleX(1.1); opacity: 1; }
}
@keyframes dm-fall {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 0; }
  8%   { opacity: 0.9; }
  90%  { opacity: 0.9; }
  100% { transform: translate(-9cqw, 108cqh) rotate(540deg); opacity: 0; }
}
@keyframes dg-dash {
  from { background-position: 0 0, 50% 0, 0 0; }
  to   { background-position: 0 0, 50% 8cqw, 0 0; }
}
@keyframes dg-steer {
  from { transform: translateX(-53%) rotate(-0.8deg); }
  to   { transform: translateX(-47%) rotate(0.8deg); }
}
@keyframes dg-pass-left {
  0%   { transform: translateX(-50%) translateX(-0.6cqw) scale(0.07); opacity: 0; }
  10%  { opacity: 1; }
  100% { transform: translateX(-50%) translateX(-30cqw) translateY(48cqh) scale(1.2); opacity: 1; }
}
@keyframes dg-pass-right {
  0%   { transform: translateX(-50%) translateX(0.6cqw) scale(0.07); opacity: 0; }
  10%  { opacity: 1; }
  100% { transform: translateX(-50%) translateX(30cqw) translateY(48cqh) scale(1.2); opacity: 1; }
}
@keyframes dg-drive {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 0 5cqw, 0 0; }
}

@keyframes crane-lift {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  45%      { transform: translateY(-5%) rotate(-2.6deg); }
  60%      { transform: translateY(-4.2%) rotate(-2deg); }
}

.petal:nth-child(1) { left: 44.1%; width: 11px; height: 11px;
  --peak: 0.55; --drift: 16px; --spin: -17deg;
  animation-duration: 25.3s; animation-delay: -12.2s; }
.petal:nth-child(1)::before { animation-duration: 5.7s; }
.petal:nth-child(2) { left: 80.0%; width: 10px; height: 10px;
  --peak: 0.38; --drift: 12px; --spin: 28deg;
  animation-duration: 22.6s; animation-delay: -2.5s; }
.petal:nth-child(2)::before { animation-duration: 5.7s; }
.petal:nth-child(3) { left: 80.6%; width: 13px; height: 13px;
  --peak: 0.42; --drift: 15px; --spin: 39deg;
  animation-duration: 22.6s; animation-delay: -10.7s; }
.petal:nth-child(3)::before { animation-duration: 5.1s; }
.petal:nth-child(4) { left: -0.5%; width: 11px; height: 11px;
  --peak: 0.47; --drift: 24px; --spin: 1deg;
  animation-duration: 15.8s; animation-delay: -23.7s; }
.petal:nth-child(4)::before { animation-duration: 3.3s; }
.petal:nth-child(5) { left: 42.9%; width: 8px; height: 8px;
  --peak: 0.49; --drift: 24px; --spin: 43deg;
  animation-duration: 21.2s; animation-delay: -13.5s; }
.petal:nth-child(5)::before { animation-duration: 5.2s; }
.petal:nth-child(6) { left: 26.4%; width: 11px; height: 11px;
  --peak: 0.38; --drift: 17px; --spin: 25deg;
  animation-duration: 26.9s; animation-delay: -19.1s; }
.petal:nth-child(6)::before { animation-duration: 5.9s; }
.petal:nth-child(7) { left: 27.5%; width: 7px; height: 7px;
  --peak: 0.37; --drift: 12px; --spin: -38deg;
  animation-duration: 21.8s; animation-delay: -2.9s; }
.petal:nth-child(7)::before { animation-duration: 3.5s; }
.petal:nth-child(8) { left: 84.4%; width: 7px; height: 7px;
  --peak: 0.4; --drift: 22px; --spin: 13deg;
  animation-duration: 17.6s; animation-delay: -1.4s; }
.petal:nth-child(8)::before { animation-duration: 6.2s; }
.petal:nth-child(9) { left: 5.4%; width: 13px; height: 13px;
  --peak: 0.38; --drift: 10px; --spin: 12deg;
  animation-duration: 17.4s; animation-delay: -9.1s; }
.petal:nth-child(9)::before { animation-duration: 5.4s; }
.petal:nth-child(10) { left: 75.3%; width: 7px; height: 7px;
  --peak: 0.43; --drift: 25px; --spin: -18deg;
  animation-duration: 16.6s; animation-delay: -0.3s; }
.petal:nth-child(10)::before { animation-duration: 5.5s; }
.petal:nth-child(11) { left: 67.6%; width: 8px; height: 8px;
  --peak: 0.32; --drift: 22px; --spin: -26deg;
  animation-duration: 20.4s; animation-delay: -19.8s; }
.petal:nth-child(11)::before { animation-duration: 3.8s; }
.petal:nth-child(12) { left: 38.3%; width: 8px; height: 8px;
  --peak: 0.47; --drift: 10px; --spin: -14deg;
  animation-duration: 15.0s; animation-delay: -26.3s; }
.petal:nth-child(12)::before { animation-duration: 6.0s; }
.petal:nth-child(13) { left: 17.1%; width: 11px; height: 11px;
  --peak: 0.35; --drift: 18px; --spin: -39deg;
  animation-duration: 22.7s; animation-delay: -26.7s; }
.petal:nth-child(13)::before { animation-duration: 3.5s; }
.petal:nth-child(14) { left: 76.8%; width: 9px; height: 9px;
  --peak: 0.35; --drift: 17px; --spin: -39deg;
  animation-duration: 25.0s; animation-delay: -2.0s; }
.petal:nth-child(14)::before { animation-duration: 4.4s; }
.petal:nth-child(15) { left: 59.3%; width: 9px; height: 9px;
  --peak: 0.55; --drift: 14px; --spin: 9deg;
  animation-duration: 22.5s; animation-delay: -15.9s; }
.petal:nth-child(15)::before { animation-duration: 3.6s; }
.petal:nth-child(16) { left: 16.6%; width: 8px; height: 8px;
  --peak: 0.51; --drift: 15px; --spin: 40deg;
  animation-duration: 18.7s; animation-delay: -16.5s; }
.petal:nth-child(16)::before { animation-duration: 3.9s; }


/* ---- the header band on every page but the home page --------------------

   A strip of the home page's artwork across the top of the first section: the
   paper, the branch swaying, clouds drifting, a few petals. It fades out into
   the page by its lower edge, and the section's own padding is set so the text
   starts where the picture is already mostly gone -- the title sits on the
   last of it, and no paragraph ever sits on the picture.

   Like the hero, it is a size container so the stage can do object-fit's
   arithmetic, here showing the top of the art: sky, the branch, Fuji's peak. */

:root { --band-h: clamp(10rem, 22vw, 16rem); }

section:has(> .band) {
  position: relative;
  isolation: isolate;
  padding-top: calc(var(--band-h) * 0.7);
}

.band {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: -1;
  height: var(--band-h);
  overflow: hidden;
  container-type: size;
  pointer-events: none;
  /* Eased, not straight: a straight fade stops abruptly at the bottom and the
     eye draws a line where it ends. */
  -webkit-mask-image: linear-gradient(180deg, #000 25%, rgba(0, 0, 0, 0.62) 52%, rgba(0, 0, 0, 0.3) 72%, rgba(0, 0, 0, 0.1) 87%, rgba(0, 0, 0, 0.02) 96%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 25%, rgba(0, 0, 0, 0.62) 52%, rgba(0, 0, 0, 0.3) 72%, rgba(0, 0, 0, 0.1) 87%, rgba(0, 0, 0, 0.02) 96%, transparent 100%);
}
.band .art-stage { top: calc((100cqh - max(66.667cqw, 100cqh)) * 0.14); }

/* Settles it into the page a little, so it reads as this page's paper rather
   than a photograph pasted on top of it. */
.band::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--bg) 22%, transparent),
    color-mix(in srgb, var(--bg) 55%, transparent));
}

.band-clouds i {
  position: absolute;
  width: 42%;
  height: 26%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0));
}
.band-clouds i:nth-child(1) { left: 4%; top: 14%; }
.band-clouds i:nth-child(2) { left: 38%; top: 38%; width: 34%; height: 20%; }
.band-clouds i:nth-child(3) { left: 62%; top: 6%; width: 30%; }

.band-petals i {
  position: absolute;
  top: -6%;
  width: max(0.7cqw, 7px);
  aspect-ratio: 1;
  border-radius: 100% 0 100% 0;
  background: linear-gradient(140deg, #fcdfe6, #e293a8);
  opacity: 0;
}
.band-petals i:nth-child(1) { left: 66%; --fall: 11s; --wait: -2s; }
.band-petals i:nth-child(2) { left: 78%; --fall: 13s; --wait: -8s; }
.band-petals i:nth-child(3) { left: 88%; --fall: 10s; --wait: -5s; }
.band-petals i:nth-child(4) { left: 72%; --fall: 14s; --wait: -11s; }
.band-petals i:nth-child(5) { left: 94%; --fall: 12s; --wait: -0.5s; }

@media (prefers-reduced-motion: no-preference) {
  .band-clouds i { animation: band-drift 46s ease-in-out infinite alternate; }
  .band-clouds i:nth-child(2) { animation-duration: 60s; animation-direction: alternate-reverse; }
  .band-clouds i:nth-child(3) { animation-duration: 52s; }
  .band-petals i { animation: band-fall var(--fall) linear var(--wait) infinite; }

  /* Scrolled away, it rests: the script puts this on. */
  .band.at-rest *,
  .band.at-rest .art-branch img { animation-play-state: paused; }
}

@keyframes band-drift {
  from { transform: translateX(-14cqw); }
  to   { transform: translateX(14cqw); }
}
@keyframes band-fall {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 0; }
  10%  { opacity: 0.85; }
  85%  { opacity: 0.85; }
  100% { transform: translate(-12cqw, 115cqh) rotate(420deg); opacity: 0; }
}


/* ---- the crane over each section heading ---------------------------------

   In the page from the start (partials/crane.html), drawn. The script rubs out
   the ones still below the screen (.is-waiting) and lets each draw itself when
   it arrives. Every path is pathLength="1", so one dash of length 1 covers it
   exactly and sliding the dash back draws it: the two wings, the body, the
   neck and head, and last the tail. */

.crane-mark {
  display: block;
  width: 3.4rem;
  height: auto;
  margin: 0.2rem 0 0.8rem;
  overflow: visible;
  color: var(--accent);
}
.crane-mark path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linejoin: round;
  stroke-linecap: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
}
.crane-mark.is-waiting path { stroke-dashoffset: 1; }

@media (prefers-reduced-motion: no-preference) {
  /* Only on the way to drawn. Rubbing out at load is instant, so a quick
     scroll never catches half a crane still waiting to be erased. */
  .crane-mark:not(.is-waiting) path { transition: stroke-dashoffset 0.7s cubic-bezier(0.45, 0, 0.25, 1); }
  .crane-mark:not(.is-waiting) path:nth-child(2) { transition-delay: 0.1s; }
  .crane-mark:not(.is-waiting) path:nth-child(3) { transition-delay: 0.45s; transition-duration: 0.8s; }
  .crane-mark:not(.is-waiting) path:nth-child(4) { transition-delay: 1s; }
  .crane-mark:not(.is-waiting) path:nth-child(5) { transition-delay: 1.4s; }
}


/* ---- the Compared table arrives row by row ---------------------------------

   The script puts .reveal on each body row. A row itself cannot be moved
   reliably in every browser, so its cells are: they rise and fade in, in a run
   down the table. The red line down the Wallgami column is the cell's own
   background, grown from the top as each row arrives, so row after row it draws
   one line down the column. Without the script it is simply there. */

.compare tbody .us {
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat: no-repeat;
  background-position: 0 0;
  background-size: 3px 100%;
}
.compare tr.reveal { opacity: 1; transform: none; }
.compare tr.reveal > * {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.6s ease, transform 0.6s cubic-bezier(0.22, 0.61, 0.36, 1);
  transition-delay: var(--reveal-delay, 0s);
}
.compare tr.reveal > .us { background-size: 3px 0; }
.compare tr.reveal.is-in > * { opacity: 1; transform: none; }
.compare tr.reveal.is-in > .us {
  background-size: 3px 100%;
  transition: opacity 0.6s ease, transform 0.6s cubic-bezier(0.22, 0.61, 0.36, 1), background-size 0.45s ease;
  transition-delay: var(--reveal-delay, 0s), var(--reveal-delay, 0s), calc(var(--reveal-delay, 0s) + 0.25s);
}


/* ---- Answers: questions that open ------------------------------------------

   Each answer is a <details>, so it opens with a click, a tap or the keyboard,
   with or without the script. Where the browser can animate to a height of
   "auto" it slides open; where it cannot, it simply opens. The arrow turns. */

.faq-item summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  cursor: pointer;
  list-style: none;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq .faq-item summary h3 { margin: 0; }
.faq-item summary::after {
  content: "";
  flex: none;
  width: 0.6rem;
  height: 0.6rem;
  margin: -0.25rem 0.3rem 0 0;
  border-right: 2px solid var(--accent-text);
  border-bottom: 2px solid var(--accent-text);
  transform: rotate(45deg);
  transition: transform 0.3s ease;
}
.faq-item[open] summary::after { transform: rotate(225deg); margin-top: 0.25rem; }
.faq-item summary:hover h3 { color: var(--accent-text); }
.faq-item > p:first-of-type { margin-top: 0.6rem; }

.faq-item { interpolate-size: allow-keywords; }
.faq-item::details-content {
  height: 0;
  overflow: clip;
  transition: height 0.35s ease, content-visibility 0.35s allow-discrete;
}
.faq-item[open]::details-content { height: auto; }


/* ---- not found: a lost crane -------------------------------------------------

   The logo's crane, wandering slowly about the page behind the words, as if
   looking for the page too. No script on this page; the reduced-motion rule
   leaves it standing still, off to the right. */

section:has(> .lost-crane) { overflow: clip; min-height: 70vh; }
.lost-crane {
  position: absolute;
  z-index: -1;
  left: 72%;
  top: 48%;
  width: clamp(4.5rem, 9vw, 8rem);
  height: auto;
  opacity: 0.9;
  pointer-events: none;
  filter: drop-shadow(0 10px 14px rgba(0, 0, 0, 0.35));
}

@media (prefers-reduced-motion: no-preference) {
  .lost-crane { animation: lost-wander 38s ease-in-out infinite; }
}

@keyframes lost-wander {
  0%   { left: 72%; top: 48%; transform: rotate(-6deg); }
  18%  { left: 58%; top: 70%; transform: rotate(4deg) scale(0.92); }
  36%  { left: 20%; top: 64%; transform: rotate(-3deg) scale(0.86) scaleX(-1); }
  50%  { left: 8%;  top: 38%; transform: rotate(5deg) scale(0.9) scaleX(-1); }
  66%  { left: 34%; top: 80%; transform: rotate(-4deg) scaleX(-1); }
  82%  { left: 80%; top: 76%; transform: rotate(6deg) scale(0.95); }
  100% { left: 72%; top: 48%; transform: rotate(-6deg); }
}


/* ---- Safety: the refused wallpaper -------------------------------------------

   One eight-second timeline shared by every moving part, so they stay in step:
   the file slides in (0-40%), meets the glass (40%), which ripples and glows,
   is stamped and knocked back (42-62%), rests there, refused, and fades before
   the next run. The stage is a size container and everything is placed in its
   own units, so it scales as one picture. Dark in both themes, like the laptop.

   The base styles below ARE the last moment -- refused, bounced, the PC fine --
   so a reduced-motion request, which declares no animation, sees exactly that. */

.refuse { position: relative; margin: 0 0 2rem; max-width: 46rem; }
.refuse img { border: 0; }

.refuse-stage {
  position: relative;
  aspect-ratio: 16 / 7;
  container-type: size;
  overflow: hidden;
  border-radius: 10px;
  background: radial-gradient(ellipse at 60% 40%, #2a2236 0%, #120f18 70%);
  color: #ffffff;
}

.refuse-label {
  position: absolute;
  top: 5cqh;
  left: 59cqw;
  transform: translateX(-50%);
  white-space: nowrap;
  font-size: clamp(0.62rem, 2cqw, 0.95rem);
  font-weight: 700;
  letter-spacing: 0.01em;
  color: #d8f1ff;
}

.refuse-wall {
  position: absolute;
  left: 58cqw;
  top: 20cqh;
  bottom: 9cqh;
  width: 2.2cqw;
  border: 1px solid rgba(190, 235, 255, 0.65);
  border-radius: 4px;
  background: linear-gradient(90deg, rgba(160, 220, 255, 0.12), rgba(210, 243, 255, 0.45), rgba(160, 220, 255, 0.12));
  box-shadow: 0 0 1.6cqw rgba(120, 200, 255, 0.25);
}

.refuse-ripple {
  position: absolute;
  left: 59.1cqw;
  top: 55cqh;
  width: 12cqw;
  aspect-ratio: 1;
  border: 2px solid rgba(170, 225, 255, 0.85);
  border-radius: 50%;
  transform: translate(-50%, -50%) scale(2.4);
  opacity: 0;
}

.refuse-file {
  position: absolute;
  left: 8cqw;
  top: 55cqh;
  width: 13cqw;
  display: grid;
  justify-items: center;
  gap: 1cqh;
  transform: translate(15cqw, -50%) rotate(-10deg);
}
.refuse-file svg { display: block; width: 62%; height: auto; filter: drop-shadow(0 0.6cqw 1cqw rgba(0, 0, 0, 0.45)); }
.refuse-name { font-size: clamp(0.55rem, 1.6cqw, 0.8rem); color: #d8d2d4; white-space: nowrap; }

.refuse-stamp {
  position: absolute;
  top: 26%;
  left: 50%;
  padding: 0.3cqw 1.2cqw;
  border: 2px solid #ff4d57;
  border-radius: 4px;
  background: rgba(18, 15, 24, 0.85);
  color: #ff6b73;
  font-size: clamp(0.6rem, 2.1cqw, 1rem);
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transform: translate(-50%, -50%) rotate(-12deg);
}

.refuse-pc {
  position: absolute;
  left: 76cqw;
  top: 55cqh;
  width: 16cqw;
  display: grid;
  justify-items: center;
  gap: 1cqh;
  transform: translateY(-50%);
}
.refuse-pc > svg { display: block; width: 100%; height: auto; }
.refuse-safe { position: absolute; top: -2cqh; right: -1.5cqw; width: 5cqw; }
.refuse-safe svg { display: block; width: 100%; height: auto; }

/* The pause button: the laptop's, placed in this stage's corner instead. */
.refuse .demo-toggle { top: 0.7rem; right: 0.7rem; width: 2.1rem; height: 2.1rem; }

@media (prefers-reduced-motion: no-preference) {
  .refuse-file   { animation: refuse-go 8s infinite; }
  .refuse-stamp  { animation: refuse-stamp 8s infinite; }
  .refuse-ripple { animation: refuse-ripple 8s infinite; }
  .refuse-wall   { animation: refuse-wall 8s infinite; }
  .refuse-safe   { animation: refuse-safe 8s infinite; }

  .refuse.is-paused .refuse-stage * { animation-play-state: paused; }
}

@keyframes refuse-go {
  0%       { transform: translate(-8cqw, -50%) rotate(0deg); opacity: 0; animation-timing-function: ease-out; }
  8%       { transform: translate(0, -50%) rotate(0deg); opacity: 1; animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.6); }
  40%      { transform: translate(35.5cqw, -50%) rotate(0deg); }
  43%      { transform: translate(34.5cqw, -50%) rotate(2deg) scaleX(0.9); animation-timing-function: cubic-bezier(0.2, 0.7, 0.3, 1); }
  58%      { transform: translate(14cqw, -52%) rotate(-14deg); }
  64%      { transform: translate(15cqw, -50%) rotate(-10deg); }
  88%      { transform: translate(15cqw, -50%) rotate(-10deg); opacity: 1; }
  96%, 100% { transform: translate(15cqw, -50%) rotate(-10deg); opacity: 0; }
}
@keyframes refuse-stamp {
  0%, 43%  { transform: translate(-50%, -50%) rotate(-12deg) scale(1.8); opacity: 0; }
  50%      { transform: translate(-50%, -50%) rotate(-12deg) scale(1); opacity: 1; }
  88%      { opacity: 1; }
  96%, 100% { opacity: 0; }
}
@keyframes refuse-ripple {
  0%, 39%  { transform: translate(-50%, -50%) scale(0.2); opacity: 0; }
  41%      { transform: translate(-50%, -50%) scale(0.3); opacity: 0.9; }
  62%, 100% { transform: translate(-50%, -50%) scale(2.4); opacity: 0; }
}
@keyframes refuse-wall {
  0%, 38%, 70%, 100% { box-shadow: 0 0 1.6cqw rgba(120, 200, 255, 0.25); }
  42%      { box-shadow: 0 0 4cqw rgba(150, 220, 255, 0.75); }
}
@keyframes refuse-safe {
  0%, 44%  { transform: scale(1); }
  50%      { transform: scale(1.25); }
  58%, 100% { transform: scale(1); }
}


/* ---- a large cherry blossom in each side margin -----------------------------

   Built by the script (see sakura()), one per section, alternating sides and
   pink and white. Only shown where the margins beside the 72rem page are wide
   enough to hold one without reaching the text (from 1536px wide); narrower
   than that there is no empty space to put it in. It sits at the top of its
   section, as wide as the margin allows, and when a section is too short it is
   scaled down to fit ("meet"), so it never reaches into the next one.

   --grow runs 0 to 1:
     0.00-0.75  the five petal lines draw in turn (each over 0.35, --from apart)
                while the whole flower turns 30 degrees into place
     0.45-0.80  colour soaks out from the middle to the petal tips
     0.66-0.95  the stamens draw out, one after another, and their dots appear
   Each line is pathLength="1", so one dash of length 1 covers it and sliding
   that dash along draws it. */

.sakura-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.has-sakura { position: relative; }

.sakura-side {
  display: none;
  position: absolute;
  top: 3rem;
  width: min(19rem, calc((100% - 72rem) / 2 - 2.5rem));
  height: calc(100% - 6rem);
  pointer-events: none;
  --grow: 1;
  --ink: #d9577c;
}
.sakura-left { left: 0.5rem; }
.sakura-right { right: 0.5rem; transform: scaleX(-1); }
.sk-white { --ink: #e38aa6; }
section:has(> .band) > .sakura-side { top: calc(var(--band-h) * 0.85); height: calc(100% - var(--band-h) * 0.85 - 3rem); }

@media (prefers-color-scheme: light) {
  .sakura-side { --ink: #c94a70; }
  .sk-white { --ink: #d9779a; }
}

/* From 96rem (1536px) the margin is about 150px wide, enough for a flower you
   can see. */
@media (min-width: 96rem) {
  .sakura-side { display: block; }
}

.sk-flower {
  transform: rotate(calc((1 - var(--grow)) * -30deg)) scale(calc(0.86 + var(--grow) * 0.14));
}
.sk-petal,
.sk-ring,
.sk-stamen {
  stroke: var(--ink);
  stroke-linejoin: round;
  stroke-linecap: round;
  stroke-dasharray: 1;
}
.sk-petal {
  stroke-width: 3.2;
  stroke-dashoffset: calc(1 - clamp(0, (var(--grow) - var(--from)) / 0.35, 1));
}
/* A round cap is drawn even on a line of no length, which left a dot where
   every line was about to start. Each line stays invisible until it begins. */
.sk-petal,
.sk-stamen { stroke-opacity: clamp(0, (var(--grow) - var(--from)) * 60, 1); }
.sk-ring { stroke-opacity: clamp(0, (var(--grow) - 0.62) * 60, 1); }
/* The colour's soft-edged mask circle, growing from the middle to past the
   petal tips (its last third is the fade, hence 150 against petals of 100).
   r as a CSS property is SVG 2; a browser without it keeps the attribute's
   full size, and the flower simply has its colour from the start. */
.sk-fill,
.sk-heart { stroke: none; }
.sk-wash { r: calc(clamp(0, (var(--grow) - 0.45) / 0.35, 1) * 150px); }
.sk-ring {
  fill: none;
  stroke-width: 2;
  stroke-dashoffset: calc(1 - clamp(0, (var(--grow) - 0.62) / 0.1, 1));
}
.sk-stamen {
  fill: none;
  stroke-width: 1.3;
  stroke-dashoffset: calc(1 - clamp(0, (var(--grow) - var(--from)) / 0.12, 1));
}
.sk-dot {
  fill: var(--ink);
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(clamp(0, (var(--grow) - var(--at)) / 0.06, 1));
}

/* Touching a finished flower: it takes the pointer (the rest of its box does
   not), shivers, and petals pop out of it into a fixed layer over the page.
   See shed() in the script. */
.sakura-side .sk-flower { pointer-events: visiblePainted; cursor: pointer; }
/* Nothing here is text, and a click on it must not begin a selection or flash
   a tap highlight. The script also cancels the press itself. */
.sakura-side,
.petal-layer {
  -webkit-user-select: none;
          user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.sakura-side.sk-tickle .sk-flower { animation: sk-tickle 0.6s ease-out; }
@keyframes sk-tickle {
  0%   { transform: rotate(0deg); }
  25%  { transform: rotate(7deg) scale(1.03); }
  55%  { transform: rotate(-5deg); }
  80%  { transform: rotate(2deg); }
  100% { transform: rotate(0deg); }
}

.petal-layer {
  position: fixed;
  inset: 0;
  z-index: 15;
  overflow: hidden;
  pointer-events: none;
}
.pop-petal {
  position: absolute;
  border-radius: 100% 0 100% 0;
  will-change: transform, opacity;
}
.pop-pink { background: linear-gradient(140deg, #fcdfe6, #ec8fac); }
.pop-white { background: linear-gradient(140deg, #ffffff, #f6d3de); }
