/*
 * One stylesheet, no build step, no framework. The site is one page whose job is to get a person to
 * a download and tell them the truth about it; anything that needs compiling to serve that is
 * overhead the next person has to learn before they can change a sentence.
 *
 * Dark by default with a light mode from the system preference. Both are declared rather than
 * assuming dark: a light-mode visitor on a bright screen otherwise gets a page they have to squint
 * at, and this is a page people are asked to read carefully before running an unsigned binary.
 */

/*
 * THE PALETTE IS THE PRODUCT'S, NOT A TEMPLATE'S. Until 2026-08-27 this page ran GitHub's dark
 * neutrals with a blue accent, which was fine and belonged to nothing: the Maxer identity is a
 * violet-and-cyan gauge on navy, defined in installer/generate-branding.ps1 and drawn in
 * img/app-icon.svg, and no web surface had ever used it. A violet mark on a blue page reads as a
 * logo borrowed from somewhere else.
 *
 * --accent is NOT the brand violet #8A5CFF, and that is deliberate rather than a drift. #8A5CFF on
 * this background is about 4.4:1 - under the 4.5:1 a link needs - and it gives white text on a
 * filled button only about 3.4:1, which passes for large text and fails for everything else. So the
 * violet is lifted for text on dark and darkened for fills and for light mode. The hue is the
 * brand's; the luminance is whatever the contrast requires.
 */
:root {
  color-scheme: dark light;

  --bg: #0a0918;
  --surface: #131127;
  --surface-raised: #1a1733;
  /* 1.76:1 against the background. The old blue palette’s border managed 1.46:1 and the first
     violet pick was 1.39:1 — both fine while a border was decoration, and not fine now that one
     separates two notice cards a person is meant to read as separate things. */
  --border: #3a3270;
  --text: #ecf4ff;
  --text-dim: #a6a2c8;
  --accent: #a98bff;
  --accent-strong: #6c3fe3;

  /* The brand cyan, for rules, focus and the copy-confirmed state. Never for body text: at this
     chroma it is a highlight, and a paragraph set in it is unreadable on either background. */
  --accent-cyan: #22e2ff;

  /* Buy Me a Coffee's own #FFDD00, kept rather than replaced, because for once a brand colour is the
     legible one: 14.6:1 on --bg, where PayPal's navy and Wompi's charcoal both disappear. It is the
     only donate colour taken from a provider, and it survives only on dark — see the light block. */
  --accent-amber: #ffdd00;

  /* The widget marks a reading's low in teal and its high in orange, and the diagrams in "How the
     widget works" draw those marks at the literal values the product uses — a drawing of a thing has
     to be the colour of the thing, so those two hex values stay in the markup and do NOT follow this
     palette. These are for the two WORDS that label them, which are text and have to stay readable:
     #2DD4BF on white is about 1.9:1. Dark mode can use the mark's own colour; light mode cannot. */
  --tick-low-ink: #2dd4bf;
  --tick-high-ink: #fb923c;

  --warn-bg: #2a2015;
  --warn-border: #6e4e22;
  --warn-text: #f2c486;

  --max: 62rem;

  /* How wide a run of prose is allowed to get, independent of the box it sits in. --max is the
     page's column and is right for cards and grids; a paragraph set that wide is 90-odd characters
     a line, and the eye loses the start of the next one. 38rem lands around 75. It is a separate
     number rather than a fraction of --max because it does not scale with the layout — it scales
     with the TYPE, and the two stopped being the same thing when the root size moved. Headings and
     the hero lede set their own, being larger. */
  --measure: 38rem;

  --radius: 12px;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #ffffff;
    --surface: #f7f6fc;
    --surface-raised: #ffffff;
    --border: #dcd8ec;
    --text: #16142a;
    --text-dim: #565273;
    --accent: #5b32c9;
    --accent-strong: #4c28ac;

    /* Darkened hard. #22e2ff on white is about 1.5:1 - invisible - so light mode gets a teal that
       carries the same role at a luminance a person can actually see. */
    --accent-cyan: #0a7e96;

    /* #ffdd00 on white is 1.35:1 — invisible, the same failure its own brand colour saves the other
       two from on dark. Same hue, taken down to 5.4:1. */
    --accent-amber: #7a6a00;
    --tick-low-ink: #0d7d70;
    --tick-high-ink: #b8460d;

    --warn-bg: #fff8e6;
    --warn-border: #e0c384;
    --warn-text: #7a5314;
  }
}

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

/*
 * THE WHOLE PAGE IS SIZED FROM HERE. Nearly every size in this file is a rem, and `rem` resolves
 * against the ROOT element — not against `body`, which is why setting `body { font-size }` never
 * moved the page's scale. The type here runs deliberately quiet: the dominant reading size is
 * 0.85–0.95rem, which at a 16px root is 13.6–15.2px, and on a large screen the whole page read a
 * size smaller than it should.
 *
 * A PERCENTAGE, NOT A PIXEL VALUE, and that is the part worth keeping. 112.5% is 18px against a
 * default browser setting, but it *stays* a proportion: a reader who has set their browser to 20px
 * because they need it gets 22.5px here rather than being overridden back down to 18. `html { font-
 * size: 18px }` would silently throw that preference away, which is the usual way this gets done.
 *
 * Everything else follows from this one number — type, padding, gaps, grid minima and the content
 * width — so the page grows in proportion rather than growing text inside boxes that stayed put.
 */
html {
  font-size: 112.5%;
}

/*
 * The violet bloom behind the hero echoes the radial glow inside the app icon, so the page and the
 * mark are lit the same way.
 *
 * ON body, NOT ON .hero, and that is not a preference. Every `section` is max-width 62rem and
 * centred, so a gradient painted on the hero is clipped to that box — it rendered as a lighter
 * rectangle with hard vertical edges, which reads as a panel someone forgot to align rather than as
 * light. body is the only full-width thing here.
 *
 * `no-repeat` with the size pinned to the top so it does not tile down a long page, and the flat
 * colour underneath so the rest of the page is still var(--bg).
 *
 * STATIC, AND IT MUST STAY STATIC. The reduced-motion block at the end of this file exists because
 * every moving thing here is decorative; a breathing glow would be the first exception and the
 * first thing that has to be remembered.
 */
body {
  margin: 0;
  background:
    radial-gradient(
      60rem 26rem at 50% -6rem,
      color-mix(in srgb, var(--accent) 18%, transparent),
      transparent 70%
    ) no-repeat,
    var(--bg);
  color: var(--text);
  /* 1rem, not 16px: a hardcoded pixel size here would pin the body copy while every rem around it
     scaled off the root, so the text would shrink relative to its own padding. */
  font: 1rem/1.65 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* Half the strength. The same wash that reads as depth on navy reads as a stain on white. */
@media (prefers-color-scheme: light) {
  body {
    background:
      radial-gradient(
        60rem 26rem at 50% -6rem,
        color-mix(in srgb, var(--accent) 9%, transparent),
        transparent 70%
      ) no-repeat,
      var(--bg);
  }
}

h1,
h2,
h3 {
  line-height: 1.2;
  letter-spacing: -0.02em;
  margin: 0 0 0.5em;
}

a {
  color: var(--accent);
}

section {
  max-width: var(--max);
  margin: 0 auto;
  padding: 4rem 1.25rem;
}

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

.site-header {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: center;
  justify-content: space-between;
  max-width: var(--max);
  margin: 0 auto;
  padding: 1.25rem;
}

/*
 * The mark and the wordmark are one control: clicking either goes home, so they are one anchor
 * rather than two things that happen to sit together.
 */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  text-decoration: none;
}

/* Width and height are on the element as attributes too. The icon is the first thing painted, and
   without intrinsic dimensions the header reflows once it arrives. */
/* The simplified mark, not app-icon.svg: the full one has no legible detail at this size. */
.brand img {
  width: 32px;
  height: 32px;
  flex: 0 0 auto;
}

.wordmark {
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--text);
  text-decoration: none;
  letter-spacing: -0.02em;
}

.wordmark span {
  color: var(--text-dim);
  font-weight: 500;
}

.site-header nav {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  align-items: center;
}

.site-header nav a {
  color: var(--text-dim);
  text-decoration: none;
  font-size: 0.95rem;
}

.site-header nav a:hover {
  color: var(--text);
}

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

.hero {
  padding-top: 3rem;
  padding-bottom: 2.5rem;
  text-align: center;
}

.hero h1 {
  font-size: clamp(2rem, 6vw, 3.25rem);
  margin-bottom: 0.6em;
}

.lede {
  max-width: 40rem;
  margin: 0 auto 2.5rem;
  font-size: clamp(1.05rem, 2.5vw, 1.2rem);
  color: var(--text-dim);
}

/*
 * The two ways to get the app, side by side. One works and one does not yet, and showing them
 * together is the point: a visitor can see that the Store is the intended route rather than
 * wondering whether a website download is all there will ever be.
 *
 * DORMANT SINCE 2026-09-04: the Store panel is commented out of index.html, so this row holds one
 * child and centres it. The rules below still size a pair the day it comes back.
 */
.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: stretch;
  justify-content: center;
}

/*
 * THE TWO ROUTES ARE ONE PAIR, so they are sized as a pair rather than each to its own content.
 * (One route at the moment - see .actions above. The shared rule is kept so the pair stays matched
 * if the Store panel is uncommented; alone, .download simply takes the cap.)
 * Equal basis, equal cap, equal padding - the download is not "a button that happens to sit near a
 * panel", it is one of two things you can do, and a reader compares them by looking.
 *
 * NO min-height any more. It was here to stop `stretch` turning a two-line button into a slab of
 * flat violet next to a much taller panel; the compact sizing closed that gap on its own, and a
 * floor set by hand is a number that has to be re-guessed every time either side gains a line.
 * Content decides the height now, and `stretch` makes the shorter one match it.
 */
.download,
.store-status {
  flex: 1 1 17rem;
  max-width: 20rem;
  padding: 1.05rem 1.2rem;
  border-radius: var(--radius);
}

/*
 * The download is the page's whole purpose, so it is the only thing styled as a primary action.
 * Version and size sit inside the control rather than beside it: "how big is this" is asked at the
 * moment of clicking, and an answer that arrives after the click is an answer nobody read.
 */
.download {
  display: inline-flex;
  flex-direction: column;
  /* Centred on both axes: it carried two lines against the panel's four, so anchoring it to the top
     would have left an obvious hole under it. With the panel hidden it is the only child of a
     centred row, and this keeps its own two lines balanced. */
  justify-content: center;
  align-items: center;
  gap: 0.3rem;
  background: var(--accent-strong);
  color: #fff;
  text-align: center;
  text-decoration: none;
  box-shadow: 0 6px 20px color-mix(in srgb, var(--accent-strong) 42%, transparent);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}

.download:hover {
  transform: translateY(-1px);
  box-shadow: 0 10px 26px color-mix(in srgb, var(--accent-strong) 52%, transparent);
}

.download:active {
  transform: translateY(0);
}

.download-main {
  font-size: 1.05rem;
  font-weight: 650;
}

/*
 * Sized in em so it tracks the label rather than needing a second number to keep in step. The nudge
 * on hover is the only motion here that means anything - it points where the click goes - and the
 * reduced-motion block at the end of this file already covers it, since it kills every transition.
 */
.download-icon {
  display: inline-block;
  width: 1.15em;
  height: 1.15em;
  margin-right: 0.45em;
  vertical-align: -0.2em;
  transition: transform 0.12s ease;
}

.download:hover .download-icon {
  transform: translateY(2px);
}

.download-meta {
  font-size: 0.8rem;
  opacity: 0.85;
}

/* ---------------------------------------------------------------------------
   Release loading states

   The page knows no version until the manifest answers, so the download button
   exists before it has a destination. These styles make that legible instead of
   looking like a button that silently does nothing.
   --------------------------------------------------------------------------- */

/* No href while loading, so the cursor must not promise a navigation. */
.download.is-loading,
.download.is-unavailable {
  cursor: default;
  transform: none;
}

.download.is-loading {
  opacity: 0.72;
}

/* A spinner in the button itself rather than a separate overlay: the thing being waited on IS the
   destination of this control, so the feedback belongs where the eye already is. */
.download.is-loading .download-meta::before {
  content: '';
  display: inline-block;
  width: 0.75em;
  height: 0.75em;
  margin-right: 0.5em;
  vertical-align: -0.05em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

.download.is-unavailable {
  opacity: 0.5;
  filter: grayscale(1);
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* Placeholder bars for the digest and the verify command. Deliberately blank rather than filled
   with a plausible-looking fake value — a greyed-out string of hex would read as real. */
.is-skeleton {
  display: block;
  min-height: 1.1em;
  border-radius: 4px;
  background: linear-gradient(
    90deg,
    var(--surface) 25%,
    var(--surface-raised) 37%,
    var(--surface) 63%
  );
  background-size: 400% 100%;
  animation: shimmer 1.4s ease-in-out infinite;
  color: transparent;
}

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

/* Motion here is decorative; the text states carry the same information. */
@media (prefers-reduced-motion: reduce) {
  .download.is-loading .download-meta::before,
  .is-skeleton {
    animation: none;
  }
}

/* --- the Store, and how far the submission has got ---------------------- */

/*
 * DORMANT SINCE 2026-09-04. Nothing in this section is on screen: the markup it styles is
 * commented out in index.html, because the Store submission is not being pursued. It is kept whole
 * so restoring the panel is one uncomment rather than a rewrite.
 *
 * ONE EXCEPTION, and it is live: .visually-hidden, near the end of this section, is used by
 * index.html and support.html. It is not Store markup and must not go with the rest.
 */

/*
 * A status panel, not a control. There is nothing to press: the app is not in the Store, and a
 * disabled button that can never become enabled tells a visitor less than the three stages do.
 *
 * It sits beside the download and matches its height (.actions is align-items: stretch), so the two
 * routes read as a pair — one open, one not yet — rather than as an action with a footnote.
 */
.store-status {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-dim);
  text-align: left;
}

/*
 * THE PILL IS OURS, NOT MICROSOFT'S, AND THE WORDING IS THE WHOLE REASON.
 *
 * Microsoft publishes a badge for this - a dark pill reading "Download from the Microsoft Store",
 * and it is the control this is modelled on. It is not used here because on this page that sentence
 * is not true: the app is not in the Store. Their brand terms also ask that the badge not be altered
 * or dimmed, so "use it but grey it out" is not an option either.
 *
 * So: the same shape, the same logo-left lockup, the same two-line rhythm - and our own words. The
 * four-square rather than the Store bag, so it cannot be taken for the official asset.
 *
 * WHEN THE APP IS ACTUALLY IN THE STORE, throw this away and use Microsoft's badge as a link. It
 * will be true then, and their asset will always look more like their button than ours does.
 */
.store-pill {
  /* It is a <p>, as the header row it replaces was, so its default 1em margins have to go - they
     added 32px to the panel and pushed the pair from 149 to 181. */
  margin: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  width: 168px;
  height: 46px;
  padding: 0 0.7rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface-raised);
}

.store-pill img {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
}

.store-pill-text {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
}

.store-pill-small {
  font-size: 0.62rem;
  color: var(--text-dim);
}

.store-pill-big {
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--text);
}

.store-status {
  justify-content: center;
  gap: 0.95rem;
}

.store-steps-wrap {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
}

/*
 * NOT grayscale() and not a low opacity, which is the reflex for something unavailable. The
 * four-square mark is recognised by its colours, and draining them makes the panel harder to
 * identify rather than softer.
 */
/*
 * Three equal columns with the dot centred in each, so the connectors can be drawn from one dot's
 * centre to the next without measuring anything. An <ol> because these are ordered stages, and a
 * screen reader announcing "list of 3" is exactly right.
 */
.store-steps {
  width: 100%;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  margin: 0;
  padding: 0;
  list-style: none;
}

.store-step {
  position: relative;
  padding-top: 1.3rem;
  text-align: center;
  font-size: 0.72rem;
  line-height: 1.35;
}

/* From the previous dot's centre to this one. -50% of a column is exactly half the gap between two
   centres, which is why the columns have to be equal. */
.store-step + .store-step::before {
  content: '';
  position: absolute;
  top: 0.38rem;
  left: -50%;
  width: 100%;
  border-top: 1px dashed var(--border);
  z-index: 1;
}

.store-dot {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 0.78rem;
  height: 0.78rem;
  border: 2px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  z-index: 2;
}

/*
 * THE STATE WORDS ARE IN THE DOM, not generated by CSS, and that is the point of having them.
 * Colour alone would leave a screen reader with three identical steps — and generated content is
 * not dependably announced. Both spans ship on every step; the attribute below decides which one is
 * shown, and "completed" is shown to assistive technology only because the filled dot already says
 * it to everyone else.
 */
.store-step-state {
  display: none;
  font-style: italic;
}

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

/*
 * data-store-step is the number of stages COMPLETED, so the (n+1)th is the one in process — except
 * at 0, where nothing has started and nothing is in process. That exception is the whole reason
 * this is written out per value rather than as one clever selector.
 */
.store-steps[data-store-step='1'] .store-step:nth-child(-n + 1) .store-dot,
.store-steps[data-store-step='2'] .store-step:nth-child(-n + 2) .store-dot,
.store-steps[data-store-step='3'] .store-step:nth-child(-n + 3) .store-dot {
  border-color: var(--accent);
  background: var(--accent);
}

/* The tick, drawn as two borders of a rotated box so it needs no glyph and no second image. */
.store-steps[data-store-step='1'] .store-step:nth-child(-n + 1) .store-dot::after,
.store-steps[data-store-step='2'] .store-step:nth-child(-n + 2) .store-dot::after,
.store-steps[data-store-step='3'] .store-step:nth-child(-n + 3) .store-dot::after {
  content: '';
  position: absolute;
  top: 0.06rem;
  left: 0.2rem;
  width: 0.2rem;
  height: 0.36rem;
  border: solid var(--bg);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

/* In process: a ring rather than a disc, so it reads as "running" beside a completed tick. */
.store-steps[data-store-step='1'] .store-step:nth-child(2) .store-dot,
.store-steps[data-store-step='2'] .store-step:nth-child(3) .store-dot {
  border-color: var(--accent);
  background: var(--accent);
  box-shadow: inset 0 0 0 3px var(--surface);
}

.store-steps[data-store-step='1'] .store-step:nth-child(-n + 1) [data-when='done'],
.store-steps[data-store-step='2'] .store-step:nth-child(-n + 2) [data-when='done'],
.store-steps[data-store-step='3'] .store-step:nth-child(-n + 3) [data-when='done'] {
  display: block;
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.store-steps[data-store-step='1'] .store-step:nth-child(2) [data-when='current'],
.store-steps[data-store-step='2'] .store-step:nth-child(3) [data-when='current'] {
  display: block;
}

.store-note {
  margin: 1.1rem 0 0;
  font-size: 0.82rem;
  line-height: 1.5;
}

/*
 * The date is in the open because this panel cannot check itself — Partner Center has no public API
 * — so the only honest thing to publish beside a hand-written status is when it was last true.
 */
.store-checked {
  margin: 0.55rem 0 0;
  font-size: 0.75rem;
}

.release-error {
  max-width: 34rem;
  margin: 1rem auto 0;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--warn-border);
  border-radius: var(--radius);
  color: var(--text-dim);
  font-size: 0.9rem;
}

.release-error strong {
  color: var(--text);
}

.release-error button {
  margin-left: 0.5rem;
  padding: 0.3rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.release-error button:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.requirements {
  margin-top: 1rem;
  color: var(--text-dim);
  font-size: 0.9rem;
}

.platform-note {
  max-width: 34rem;
  margin: 1.5rem auto 0;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--warn-border);
  border-radius: var(--radius);
  background: var(--warn-bg);
  color: var(--warn-text);
  font-size: 0.95rem;
  text-align: left;
}

/* --- before you install -------------------------------------------------- */

/*
 * Two notices, equal weight, one section. They were two stacked full-bleed amber panels until
 * 2026-08-28, and the problem was not that either said too much — it was that they said it at
 * identical volume, one after the other, until the pair was louder than the download button above
 * them and neither read as the important one.
 *
 * BOTH STAY PROMINENT, and the second one especially. Someone who meets an unexplained "Windows
 * protected your PC" concludes the download is malware and leaves; telling them first costs one card
 * and saves the install. That is why this is a lighter treatment rather than a quieter one — the
 * amber moved from a filled field to an edge, and nothing was demoted.
 */
/* The same auto-fit idiom as .grid, .steps ol and .widget-block, so the page has one responsive
   pattern rather than four. 20rem is where two columns stop being readable and drop to one.

   min(20rem, 100%) RATHER THAN A BARE 20rem, and every auto-fit grid here is written that way. A
   bare minimum track cannot shrink below itself: at a 112.5% root, 20rem is 360px, so on a 345px
   phone these cards stayed 360px wide and pushed the whole PAGE wider than the viewport — which
   clips every section, not just this one, and reads as the entire site being broken. It is a real
   regression this page has had twice: once when the screenshot grid was written, and again the day
   the root font-size moved and turned every rem minimum into a bigger number. min() lets the track
   collapse to the container instead. */
.notice-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
  gap: 1rem;
}

.notice {
  padding: 1.5rem;
  border: 1px solid var(--warn-border);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--warn-bg) 55%, var(--surface));
  color: var(--warn-text);
}

.notice h3 {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 1.05rem;
  margin-bottom: 0.75em;
  color: var(--warn-text);
}

/* Inline SVG rather than an emoji. The page uses no emoji anywhere else, and an emoji would render
   as a different glyph on every platform and ignore the palette it is sitting in. */
.notice h3 svg {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
}

.notice p {
  margin: 0 0 0.75rem;
  font-size: 0.95rem;
}

.notice p:last-child {
  margin-bottom: 0;
}

.notice-do {
  font-weight: 600;
}

.notice-aside {
  font-size: 0.9rem;
  opacity: 0.85;
}

/* Inherit rather than --accent: a violet link inside an amber card is two accent colours arguing in
   a box whose whole job is to be read once, quickly. */
.notice a {
  color: inherit;
}

/* --- features ----------------------------------------------------------- */

.shots h2,
.features h2,
.notices h2,
.steps h2,
.verify h2,
.privacy h2,
.widget h2 {
  font-size: 1.5rem;
  margin-bottom: 1.5rem;
}

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
  gap: 1rem;
}

.grid article {
  padding: 1.35rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.grid h3 {
  font-size: 1.05rem;
}

.grid p {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.95rem;
}

/* --- steps -------------------------------------------------------------- */

.steps ol {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
  gap: 1rem;
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
}

.steps li {
  position: relative;
  padding: 1.35rem 1.35rem 1.35rem 3.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  counter-increment: step;
}

.steps li::before {
  content: counter(step);
  position: absolute;
  top: 1.35rem;
  left: 1.35rem;
  display: grid;
  place-items: center;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 50%;
  background: var(--accent-strong);
  color: #fff;
  font-size: 0.85rem;
  font-weight: 700;
}

.steps h3 {
  font-size: 1rem;
}

.steps p {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.95rem;
}

/* --- verify ------------------------------------------------------------- */

.verify p {
  color: var(--text-dim);
  max-width: var(--measure);
}

/*
 * The hash must wrap rather than scroll sideways: on a phone a 64-character string in a horizontally
 * scrolling box is effectively unreadable, and this is the one string on the page a person may
 * compare character by character.
 */
.hash {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: center;
  margin: 1.25rem 0;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-raised);
}

.hash code {
  flex: 1 1 20rem;
  overflow-wrap: anywhere;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
  color: var(--text);
}

.hash button {
  flex: 0 0 auto;
  padding: 0.4rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.hash button:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.hash button.is-copied {
  border-color: var(--accent);
  color: var(--accent);
}

pre {
  margin: 0.75rem 0 1rem;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-raised);
  overflow-x: auto;
}

pre code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
  white-space: pre;
}

.verify-note {
  font-size: 0.9rem;
}

/* --- privacy ------------------------------------------------------------ */

.privacy {
  max-width: calc(var(--max) - 8rem);
}

.privacy p {
  color: var(--text-dim);
  max-width: var(--measure);
}

.privacy strong {
  color: var(--text);
}

/* --- support and donations ---------------------------------------------- */

/*
 * SHARED BY TWO PAGES, which is why these live here and not in a page's own <style>. support.html
 * puts this row at the top under its own heading; index.html closes with it. The rules that only
 * make sense inside support.html's prose column stayed there.
 *
 * THE THREE MARKS ARE INLINE SVG DUPLICATED IN BOTH FILES, and nothing keeps them in step. That is
 * forced: an <img> cannot inherit currentColor, and the tint is what makes the marks legible at all
 * (see below), so linking img/paypal.svg would trade a duplicate for an invisible logo. If any of
 * them is ever re-fetched, it changes in index.html AND support.html.
 */

/* Centred, and wrapping rather than shrinking. Two pills at 345px do not fit side by side, let alone
   three, and a row that squeezed them would take the logo down with it — on two of the three the logo
   IS the label, so it has a floor the text does not. */
.donate-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1rem;
  margin: 0 0 1.75rem;
}

/*
 * ONE COLOUR PER BUTTON, CARRIED BY currentColor — border, label and logo all inherit it, so a button
 * is retinted by changing a single declaration and no part of it can fall out of step with the rest.
 *
 * PAYPAL'S AND WOMPI'S are this site's own accents rather than either brand's, and that is forced
 * rather than chosen: Wompi's charcoal #2C2A29 is all but invisible on --bg and PayPal's #003087 is
 * not much better, so their own palettes fail the dark theme — which is this site's default, not its
 * variant.
 *
 * BUY ME A COFFEE IS THE EXCEPTION, AND ONLY ON DARK, where its #FFDD00 gives 14.6:1 and is the most
 * legible of the three. On white the same value is 1.35:1, so light mode carries the hue at the
 * luminance the contrast needs — the treatment the palette note at the top already describes for the
 * brand violet.
 *
 * --accent-cyan is used here for a four-word label. That is the one thing the palette note permits it
 * for; it forbids paragraphs, where at this chroma it stops being readable.
 */
.donate-link {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.75rem 1.5rem;
  border: 2px solid currentColor;
  border-radius: 999px;   /* a pill: any large number resolves to half the height */
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  background: transparent;
}

.donate-paypal { color: var(--accent-cyan); }
.donate-wompi  { color: var(--accent); }
.donate-bmc    { color: var(--accent-amber); }

/* Filled with a wash of the button's own colour rather than a fixed one, so the hover follows
   whatever the button is and works on both themes without a second rule. */
.donate-link:hover {
  background: color-mix(in srgb, currentColor 14%, transparent);
}

.donate-link:focus-visible {
  outline: 2px solid var(--accent-cyan);
  outline-offset: 3px;
}

/* Height, not width. The marks are 170×48, 316×74 and 35×50 — the last portrait where the other two
   are landscape — so equal widths would leave them wildly unmatched. `em` ties them to the label
   beside them, so they scale with a reader's font size instead of drifting away from it. */
.donate-logo {
  height: 1.5em;
  width: auto;
  flex: 0 0 auto;
}

/* Wompi's lockup carries more internal padding than PayPal's, so at a shared height its glyphs read
   smaller. Nudged up until the two wordmarks match by eye rather than by number. */
.donate-logo-wompi { height: 1.15em; }

/* The coffee cup is an icon, not a wordmark, and it is the one portrait mark in the row — at the
   shared 1.5em it reads as a stray glyph beside two lockups. Nudged up by eye, same as Wompi's. */
.donate-logo-bmc { height: 1.6em; }

.donate-note {
  color: var(--text-dim);
  font-size: 0.95rem;
  margin: 0 0 1rem;
  max-width: var(--measure);
}

.donate-note strong { color: var(--text); }

/* The landing page's closing band. It keeps the shared `section` padding — unlike support.html's,
   which has to undo it — because here it is exactly what a full-width band wants. */
.support-cta h2 {
  font-size: 1.5rem;
  margin-bottom: 1.5rem;
}

.support-cta p {
  color: var(--text-dim);
  max-width: var(--measure);
}

.support-cta strong { color: var(--text); }

/* LEFT HERE, CENTRED ON support.html, and the difference is the container rather than a whim. There
   the row is the page's opening block and centres against a prose column it nearly fills. Here the
   band runs the full --max while the paragraphs are capped at --measure, so a centred row sits on a
   different axis from every line above it and reads as nudged rather than placed. */
.support-cta .donate-row {
  justify-content: flex-start;
  margin-top: 1.75rem;
}

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

.site-footer {
  max-width: var(--max);
  margin: 0 auto;
  padding: 2rem 1.25rem 3rem;
  border-top: 1px solid var(--border);
  color: var(--text-dim);
  font-size: 0.9rem;
  text-align: center;
}

/* --- screenshots -------------------------------------------------------- */

.shots-lede {
  color: var(--text-dim);
  margin: -0.75rem 0 1.75rem;
  max-width: var(--measure);
}

.shots-carousel {
  position: relative;
}

/* THE SCROLL SNAP IS THE CAROUSEL. It was a two-column grid of eight figures until it was measured:
   ~2,580px, about 2.8 laptop viewports, sitting between the feature list and the download. Built
   this way the track already swipes, takes a trackpad, scrolls and answers the arrow keys before a
   line of JavaScript runs - js/carousel.js only reveals the buttons and keeps the dots in step. */
.shots-track {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  border-radius: var(--radius);
  /* The scrollbar is redundant next to the dots and the counter, and on a dark card it draws more
     attention than the shot above it. Hidden, not disabled: the track still scrolls every way. */
  scrollbar-width: none;
}

.shots-track::-webkit-scrollbar {
  display: none;
}

.shots-track:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.shot {
  flex: 0 0 100%;
  scroll-snap-align: center;
  margin: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

/* A FIXED STAGE, NOT AN INTRINSIC ONE, and the height is the whole reason this works. The eight
   captures run from a 5.9:1 taskbar strip to a 1.1:1 settings window; a slide sized to each would
   change height on every step and shove the rest of the page up and down as you browsed. So the
   stage holds one height and the image is fitted inside it - wide shots letterbox, tall ones
   pillarbox, against --bg so the padding reads as background rather than as part of the picture.

   clamp() rather than aspect-ratio: this page has no breakpoints on purpose, and clamp() is already
   its fluid idiom (the h1, the lede). A 16:9 ratio would hand a 390px phone a 205px stage; this
   gives it 224px, and stops growing at 34rem so a shot never fills a desktop screen on its own. */
.shot a {
  display: block;
  height: clamp(14rem, 46vw, 34rem);
  background: var(--bg);
  line-height: 0;
  border-bottom: 1px solid var(--border);
}

.shot img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.shot a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.shot figcaption {
  padding: 0.85rem 1.1rem;
  font-size: 0.92rem;
  line-height: 1.5;
  color: var(--text-dim);
}

/* Held to the stage rather than to the card, so the arrows sit on the picture and not beside the
   caption. The same clamp as the stage, halved - it has to track a height that is itself fluid. */
.shots-nav {
  position: absolute;
  top: clamp(7rem, 23vw, 17rem);
  transform: translateY(-50%);
  width: 2.75rem;
  height: 2.75rem;
  padding: 0 0 0.15rem;
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  color: var(--text);
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  border: 1px solid var(--border);
  border-radius: 999px;
  transition: background 0.15s ease;
}

.shots-nav:hover {
  background: var(--surface-raised);
}

.shots-nav:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.shots-prev {
  left: 0.75rem;
}

.shots-next {
  right: 0.75rem;
}

/* Dimmed rather than hidden at either end: a control that vanishes moves the one beside it, and
   the set having a first and a last shot is worth showing. */
.shots-nav[disabled] {
  opacity: 0.3;
  cursor: default;
}

.shots-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 0.9rem;
}

.shots-dots {
  display: flex;
  gap: 0.45rem;
  flex-wrap: wrap;
}

.shots-dot {
  width: 0.6rem;
  height: 0.6rem;
  padding: 0;
  cursor: pointer;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  transition: background 0.15s ease;
}

.shots-dot:hover {
  background: var(--text-dim);
}

.shots-dot[aria-current="true"] {
  background: var(--accent);
  border-color: var(--accent);
}

.shots-dot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Tabular figures so "1 of 8" and "8 of 8" are the same width and the row does not twitch. */
.shots-count {
  font-size: 0.85rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- how the widget works ----------------------------------------------- */

.widget h2 {
  margin-bottom: 1rem;
}

.widget-lede {
  color: var(--text-dim);
  max-width: var(--measure);
  margin-bottom: 2rem;
}

.widget-lede kbd {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85em;
  padding: 0.05em 0.4em;
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 5px;
}

/* The same auto-fit idiom as .grid, .steps ol and .notice-grid, at a wider minimum: each block is a
   paragraph or two beside a drawing, and below about 20rem the drawing stops being readable. Under
   that they stack, which is also the order they read in. A rule between blocks rather than a card
   around each - six bordered boxes in a column is a lot of box for what is really one explanation. */
.widget-block {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
  gap: 1.25rem 2.5rem;
  align-items: center;
  padding: 2rem 0;
  border-top: 1px solid var(--border);
}

.widget-block > div > p {
  color: var(--text-dim);
  font-size: 0.95rem;
}

.widget-block > div > p:last-child {
  margin-bottom: 0;
}

.widget-block b {
  color: var(--text);
  font-weight: 600;
}

.widget-figure {
  margin: 0;
}

.widget-figure a {
  display: block;
  line-height: 0;
}

/* The captures and the diagrams share a frame on purpose: they are the same kind of thing to look
   at, and only the caption says which is which. */
.widget-figure img,
.widget-figure svg {
  display: block;
  width: 100%;
  height: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.widget-figure a:focus-visible img {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.widget-figure figcaption {
  margin-top: 0.65rem;
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--text-dim);
}

/* Every shot on this page is a real capture at its real readings. These three drawings are not, so
   each is labelled, in the one colour the page reserves for a mark rather than for prose. */
.diagram-label {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-cyan);
}

/* --- lightbox ----------------------------------------------------------- */

.lightbox {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  padding: 0;
  max-width: min(92vw, 78rem);
  max-height: 92vh;
  overflow: hidden;
}

.lightbox::backdrop {
  background: rgb(0 0 0 / 72%);
}

.lightbox img {
  display: block;
  max-width: 100%;
  /* Room for the caption and the dialog's own chrome, so a tall shot cannot push either off screen. */
  max-height: calc(92vh - 4.5rem);
  width: auto;
  height: auto;
  margin: 0 auto;
}

.lightbox-caption {
  margin: 0;
  padding: 0.8rem 1.1rem;
  font-size: 0.92rem;
  color: var(--text-dim);
  border-top: 1px solid var(--border);
}

.lightbox-close {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  width: 2.25rem;
  height: 2.25rem;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  color: var(--text);
  background: rgb(0 0 0 / 55%);
  border: 1px solid var(--border);
  border-radius: 999px;
}

.lightbox-close:hover {
  background: rgb(0 0 0 / 75%);
}

/* Honour a reduced-motion preference: the only motion here is decorative, so it simply goes. */
@media (prefers-reduced-motion: reduce) {
  * {
    transition: none !important;
  }

  /* The blanket rule above only reaches `transition`. Smooth scrolling is animation it does not
     catch, so the carousel is turned off by name: it still steps a shot at a time, it just jumps
     there rather than gliding. */
  .shots-track {
    scroll-behavior: auto;
  }
}
