/* Generated from marketing/brand/tokens.json by scripts/build-site-css.mjs.
   Do not edit by hand: run the script. */
:root {
  --ld-colors-primary-light: #6d28d9;
  --ld-colors-primary-dark: #c4b5fd;
  --ld-colors-primary-hover-light: #5b21b6;
  --ld-colors-primary-hover-dark: #ddd6fe;
  --ld-colors-background-gradient-light: #fee368, #ae92f4;
  --ld-colors-background-gradient-dark: #231a00, #100825;
  --ld-colors-text-body-light: #3c1a8e;
  --ld-colors-text-body-dark: #ede9fe;
  --ld-colors-text-muted-light: rgba(60, 26, 142, 0.75);
  --ld-colors-text-muted-dark: rgba(237, 233, 254, 0.75);
  --ld-colors-surface-card-light: rgba(255, 255, 255, 0.55);
  --ld-colors-surface-card-dark: rgba(0, 0, 0, 0.40);
  --ld-colors-input-border-light: rgba(91, 33, 182, 0.20);
  --ld-colors-input-border-dark: rgba(196, 181, 253, 0.25);
  --ld-colors-input-focus-ring-light: rgba(109, 40, 217, 0.20);
  --ld-colors-input-focus-ring-dark: rgba(196, 181, 253, 0.25);
  --ld-colors-feedback-success-light: #166534;
  --ld-colors-feedback-success-dark: #86efac;
  --ld-colors-feedback-warning-light: #854d0e;
  --ld-colors-feedback-warning-dark: #fde68a;
  --ld-colors-feedback-danger-light: #991b1b;
  --ld-colors-feedback-danger-dark: #fecaca;
  --ld-typography-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --ld-typography-scale-h1: 2rem;
  --ld-typography-scale-h2: 1.25rem;
  --ld-typography-scale-body: 1.05rem;
  --ld-typography-scale-small: 0.9rem;
  --ld-typography-scale-xs: 0.85rem;
  --ld-typography-weights-body: 400;
  --ld-typography-weights-bold: 700;
  --ld-typography-tracking-headings: -0.01em;
  --ld-spacing-3xs: 0.25rem;
  --ld-spacing-2xs: 0.375rem;
  --ld-spacing-xs: 0.5rem;
  --ld-spacing-sm: 0.75rem;
  --ld-spacing-md: 1rem;
  --ld-spacing-lg: 1.5rem;
  --ld-spacing-xl: 2rem;
  --ld-spacing-2xl: 3rem;
  --ld-spacing-3xl: 4rem;
  --ld-spacing-4xl: 6rem;
  --ld-measure-prose: 44rem;
  --ld-measure-page: 68rem;
  --ld-radius-card: 24px;
  --ld-radius-input: 12px;
  --ld-radius-pill: 999px;
  --ld-radius-alert: 16px;
  --ld-shadow-card: 0 20px 50px rgba(91, 33, 182, 0.15);
  --ld-shadow-card-dark: 0 20px 50px rgba(0, 0, 0, 0.50);
  --ld-shadow-cta: 0 10px 24px rgba(109, 40, 217, 0.35);
  --ld-shadow-icon: 0 18px 40px rgba(91, 33, 182, 0.25);
  --ld-motion-ease: 120ms ease;
}
/* LIP-064: the shared stylesheet for every public Blade page on both hosts.

   Was inline in layouts/app.blade.php, which meant every page re-shipped it
   and nothing was cached between pages. Moved verbatim: this file is the same
   CSS in the same order, so the cascade is unchanged.

   The `:root` token block is prepended at build time from
   marketing/brand/tokens.json. See scripts/build-site-css.mjs. */

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

/* Tells the browser which scheme the page is in, so it paints the
   scrollbars, the caret and any native control to match. Without it
   a dark page still gets a light scrollbar, which shows up as a pale
   bar down the right of every dark surface.

   Three rules rather than one, because the attribute is set by the
   theme script at the end of the body: until it runs there is no
   attribute at all, and the media query is what covers that first
   paint for somebody whose system is dark. The explicit values come
   last so a reader who has chosen a theme wins over their system in
   both directions.

   It inherits, so anything opaque inside the page (the filter
   drawer, the fine-print sheet) gets the right scrollbar without
   declaring its own. */
:root { color-scheme: light; }
@media (prefers-color-scheme: dark) {
  :root:not([data-bs-theme="light"]) { color-scheme: dark; }
}
:root[data-bs-theme="light"] { color-scheme: light; }
:root[data-bs-theme="dark"] { color-scheme: dark; }

body {
  /* The shell's own two numbers, named because three things have to agree
     about them: `main`'s padding, the header's, and the width each caps its
     content at. The header sits outside `main` and so pads the viewport on
     its own account, and while these were written twice it capped padding and
     content together and came out narrower than the page beneath it.

     960px, between the prose measure the documents had and the 1200px /live
     uses: the narrow one left the header and the page floating in the middle
     of a large screen, and the full width was too much page for a column of
     text. /live keeps its own wider frame, which is a map and wants it.

     A surface or a page that wants a different one overrides it: cabinsizer's
     documents keep the prose measure below, since that host has no header to
     agree with and its legal pages were already the width they want. */
  --ld-shell-pad: 1.25rem;
  --ld-shell-measure: 60rem;
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  /* The drift wall is the width of the viewport inside a shell that is not,
     and 100vw counts the scrollbar the shell does not. `clip` rather than
     `hidden` because hidden would make the body a scroll container and take
     every sticky element in the sheets with it. */
  overflow-x: clip;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  color: #3c1a8e;
  background: linear-gradient(135deg, #fee368 0%, #ae92f4 100%);
}
[data-bs-theme="dark"] body {
  color: #ede9fe;
  background: linear-gradient(135deg, #231a00 0%, #100825 100%);
}

/* cabinsizer has no header to agree with, and its three legal documents were
   already the width a legal document wants. */
body.ld-cabinsizer { --ld-shell-measure: var(--ld-measure-prose); }

/* LIP-064: the shell no longer centres by default.

   It used to, because the first pages through it were sign in forms and a
   short card centred in the viewport is right for those. Everything added
   since has had to opt out, one page at a time: a long document centred
   vertically pushes its own first line off the top, and anything placed
   beside the card becomes a row sibling floating at its middle.

   So the default is now what most pages want, and the screens that want
   centring ask for it with .ld-centred. That is the same change either way;
   what differs is which pages have to know about it, and there are four of
   those and twenty of the others. */
main {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--ld-spacing-xl) var(--ld-shell-pad);
}

/* A single short card, centred in whatever height is left. The auth screens
   and the error pages, which is where this shell came from. */
main.ld-centred {
  flex-direction: row;
  justify-content: center;
}

/* Glass card */
.ld-card {
  width: 100%;
  max-width: 28rem;
  padding: 2rem;
  border-radius: 24px;
  background: rgba(255, 255, 255, 0.55);
  -webkit-backdrop-filter: blur(24px);
  backdrop-filter: blur(24px);
  box-shadow: 0 20px 50px rgba(91, 33, 182, 0.15);
}
[data-bs-theme="dark"] .ld-card {
  background: rgba(0, 0, 0, 0.4);
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
}

.ld-card h1,
.ld-card h2 { color: inherit; font-weight: 700; letter-spacing: -0.01em; margin: 0 0 0.5rem; }
.ld-card h1 { font-size: 1.75rem; }
.ld-card h2 { font-size: 1.25rem; }
.ld-card p { color: rgba(60, 26, 142, 0.75); margin: 0 0 1rem; line-height: 1.5; }
.ld-card p:last-child { margin-bottom: 0; }
[data-bs-theme="dark"] .ld-card p { color: rgba(237, 233, 254, 0.75); }

/* Form */
.ld-field { margin-bottom: 1rem; }
.ld-label {
  display: block;
  font-size: 0.9rem;
  font-weight: 600;
  margin-bottom: 0.35rem;
}
.ld-input {
  width: 100%;
  padding: 0.75rem 1rem;
  border-radius: 12px;
  border: 1px solid rgba(91, 33, 182, 0.2);
  background: rgba(255, 255, 255, 0.7);
  font-size: 1rem;
  color: inherit;
  outline: none;
  transition: border-color 120ms ease, box-shadow 120ms ease;
}
.ld-input:focus {
  border-color: #6d28d9;
  box-shadow: 0 0 0 3px rgba(109, 40, 217, 0.2);
}
[data-bs-theme="dark"] .ld-input {
  background: rgba(255, 255, 255, 0.1);
  border-color: rgba(196, 181, 253, 0.25);
  color: #ede9fe;
}
[data-bs-theme="dark"] .ld-input:focus {
  border-color: #c4b5fd;
  box-shadow: 0 0 0 3px rgba(196, 181, 253, 0.25);
}
/* A field whose label sits inside it until there is something to say
   ------------------------------------------------------------------------
   The label is a real label, never a placeholder: a placeholder disappears
   the moment somebody starts typing, which is exactly when they want to check
   what the box was for, and assistive tech does not treat one as a name.

   No script. `:placeholder-shown` is the browser telling us the field is
   empty, and `:focus` is it telling us somebody is in it. Together those are
   the whole of the behaviour, which is why the markup carries a placeholder
   of one space: without it the state never occurs. */
/* One rhythm through the form: the same gap above the first field as between
   any two of them, and the same again before the button.
   The intro sat closer to the first field than the fields sat to each other,
   because the prose ends on a zero margin and only the fields carried one. */
.ld-page form { margin-top: var(--ld-spacing-lg); }
.ld-page form > p:last-child { margin: var(--ld-spacing-lg) 0 0; }

.ld-float {
  position: relative;
  margin-bottom: var(--ld-spacing-lg);
  /* The fields are what wants a measure on a page-width panel; a text box as
     wide as the site is a text box nobody can scan. */
  max-width: var(--ld-measure-prose);
}
/* The last field already carries the gap the button needs. */
.ld-float:last-of-type { margin-bottom: 0; }
.ld-float .ld-input {
  /* Room at the top for the label to rise into. */
  padding-top: 1.4rem;
  padding-bottom: 0.5rem;
}
.ld-float textarea.ld-input { padding-top: 1.6rem; }
.ld-float .ld-label {
  position: absolute;
  top: 0.95rem;
  left: 1rem;
  margin: 0;
  font-size: 1rem;
  font-weight: 400;
  color: rgba(60, 26, 142, 0.6);
  /* Clicks go to the field under it, which is what a label in this position
     looks like it should do. */
  pointer-events: none;
  transform-origin: left top;
  transition: transform 120ms ease, color 120ms ease;
}
[data-bs-theme="dark"] .ld-float .ld-label { color: rgba(237, 233, 254, 0.55); }

/* Up and smaller once the field has focus or content. Scaled rather than
   resized, so the movement is one transform on one layer and the text does
   not reflow as it goes. */
.ld-float .ld-input:focus + .ld-label,
.ld-float .ld-input:not(:placeholder-shown) + .ld-label {
  transform: translateY(-0.7rem) scale(0.75);
}
.ld-float .ld-input:focus + .ld-label { color: #6d28d9; }
[data-bs-theme="dark"] .ld-float .ld-input:focus + .ld-label { color: #c4b5fd; }

@media (prefers-reduced-motion: reduce) {
  .ld-float .ld-label { transition: none; }
}

.ld-field-error {
  color: #b91c1c;
  font-size: 0.85rem;
  margin-top: 0.35rem;
}
[data-bs-theme="dark"] /* A field whose label sits inside it until there is something to say
   ------------------------------------------------------------------------
   The label is a real label, never a placeholder: a placeholder disappears
   the moment somebody starts typing, which is exactly when they want to check
   what the box was for, and assistive tech does not treat one as a name.

   No script. `:placeholder-shown` is the browser telling us the field is
   empty, and `:focus` is it telling us somebody is in it. Together those are
   the whole of the behaviour, which is why the markup carries a placeholder
   of one space: without it the state never occurs. */
/* One rhythm through the form: the same gap above the first field as between
   any two of them, and the same again before the button.
   The intro sat closer to the first field than the fields sat to each other,
   because the prose ends on a zero margin and only the fields carried one. */
.ld-page form { margin-top: var(--ld-spacing-lg); }
.ld-page form > p:last-child { margin: var(--ld-spacing-lg) 0 0; }

.ld-float {
  position: relative;
  margin-bottom: var(--ld-spacing-lg);
  /* The fields are what wants a measure on a page-width panel; a text box as
     wide as the site is a text box nobody can scan. */
  max-width: var(--ld-measure-prose);
}
/* The last field already carries the gap the button needs. */
.ld-float:last-of-type { margin-bottom: 0; }
.ld-float .ld-input {
  /* Room at the top for the label to rise into. */
  padding-top: 1.4rem;
  padding-bottom: 0.5rem;
}
.ld-float textarea.ld-input { padding-top: 1.6rem; }
.ld-float .ld-label {
  position: absolute;
  top: 0.95rem;
  left: 1rem;
  margin: 0;
  font-size: 1rem;
  font-weight: 400;
  color: rgba(60, 26, 142, 0.6);
  /* Clicks go to the field under it, which is what a label in this position
     looks like it should do. */
  pointer-events: none;
  transform-origin: left top;
  transition: transform 120ms ease, color 120ms ease;
}
[data-bs-theme="dark"] .ld-float .ld-label { color: rgba(237, 233, 254, 0.55); }

/* Up and smaller once the field has focus or content. Scaled rather than
   resized, so the movement is one transform on one layer and the text does
   not reflow as it goes. */
.ld-float .ld-input:focus + .ld-label,
.ld-float .ld-input:not(:placeholder-shown) + .ld-label {
  transform: translateY(-0.7rem) scale(0.75);
}
.ld-float .ld-input:focus + .ld-label { color: #6d28d9; }
[data-bs-theme="dark"] .ld-float .ld-input:focus + .ld-label { color: #c4b5fd; }

@media (prefers-reduced-motion: reduce) {
  .ld-float .ld-label { transition: none; }
}

.ld-field-error { color: #fca5a5; }

/* CTA pill */
.ld-cta,
.ld-prose .ld-actions a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.85rem 1.6rem;
  border-radius: 999px;
  background: #6d28d9;
  color: #fff;
  font-weight: 600;
  font-size: 1rem;
  border: 0;
  cursor: pointer;
  text-decoration: none;
  box-shadow: 0 10px 24px rgba(109, 40, 217, 0.35);
  transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
}
.ld-cta:hover,
.ld-cta:focus,
.ld-prose .ld-actions a:hover,
.ld-prose .ld-actions a:focus {
  background: #5b21b6;
  color: #fff;
  transform: translateY(-1px);
  box-shadow: 0 14px 30px rgba(109, 40, 217, 0.4);
}
[data-bs-theme="dark"] .ld-cta,
[data-bs-theme="dark"] .ld-prose .ld-actions a {
  background: #c4b5fd;
  color: #2e1065;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.45);
}
[data-bs-theme="dark"] .ld-cta:hover,
[data-bs-theme="dark"] .ld-cta:focus,
[data-bs-theme="dark"] .ld-prose .ld-actions a:hover,
[data-bs-theme="dark"] .ld-prose .ld-actions a:focus {
  background: #ddd6fe;
  color: #2e1065;
}
.ld-cta-block { width: 100%; padding: 0.95rem 1.6rem; }

/* A button in a body is still a button. `.ld-prose a` underlines every link
   there and is one specificity step above `.ld-cta`, so without this every
   call to action inside written content comes out underlined. */
.ld-prose a.ld-cta { text-decoration: none; }

/* Alerts */
.ld-alert {
  padding: 1rem 1.2rem;
  border-radius: 16px;
  margin-bottom: 1rem;
}
.ld-alert h2 { margin-top: 0; }
.ld-alert-success { background: rgba(34, 197, 94, 0.15); color: #166534; }
.ld-alert-warning { background: rgba(250, 204, 21, 0.25); color: #854d0e; }
.ld-alert-danger  { background: rgba(239, 68, 68, 0.18); color: #991b1b; }
[data-bs-theme="dark"] .ld-alert-success { background: rgba(34, 197, 94, 0.2); color: #86efac; }
[data-bs-theme="dark"] .ld-alert-warning { background: rgba(250, 204, 21, 0.2); color: #fde68a; }
[data-bs-theme="dark"] .ld-alert-danger  { background: rgba(239, 68, 68, 0.25); color: #fecaca; }

/* Theme and motion switchers
   ------------------------------------------------------------------------
   One look, two pills. They answer different questions, so they are not one
   segmented group; they sit together in the footer, and a reader should not
   have to work out that they are the same kind of control. */
.ld-footer-switches {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}
.ld-theme-switch,
.ld-motion-switch {
  display: inline-flex;
  gap: 0.375rem;
  padding: 0.375rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.25);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}
[data-bs-theme="dark"] .ld-theme-switch,
[data-bs-theme="dark"] .ld-motion-switch { background: rgba(255, 255, 255, 0.08); }
.ld-theme-switch button,
.ld-motion-switch button {
  width: 44px;
  height: 44px;
  /* These hold an icon and nothing else, and the markup puts the svg on its
     own line, so each carries whitespace text nodes either side of it. A
     space is about four pixels wide, and any text decoration reaching this
     far paints a four by two dash across it in the corner of the button. No
     text box, nothing to decorate, and the icon is sized in pixels so none of
     this costs anything. */
  font-size: 0;
  line-height: 0;
  text-decoration: none;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  color: #5b21b6;
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
  /* Its own compositing layer, so the round background paints and is clipped
     cleanly while it fades. These sit inside a pill carrying a backdrop
     filter, and a background transitioning on a rounded box inside one of
     those leaves a few stale pixels on the circle's edge as the transition
     runs out. It shows as a small dash near the bottom of the button, on the
     way out rather than on the way in, and it shows on the motion pill first
     because a pill holding one button is itself a circle. */
  transform: translateZ(0);
}
.ld-theme-switch button svg,
.ld-motion-switch button svg {
  width: 18px;
  height: 18px;
  display: block;
}
[data-bs-theme="dark"] .ld-theme-switch button,
[data-bs-theme="dark"] .ld-motion-switch button { color: #c4b5fd; }
.ld-theme-switch button:hover,
.ld-motion-switch button:hover { background: rgba(139, 92, 246, 0.12); }
.ld-theme-switch button.active,
.ld-theme-switch button.active:hover,
.ld-motion-switch button.active,
.ld-motion-switch button.active:hover { background: #8b5cf6; color: #fff; }

/* Somebody has asked for things to stop moving.

   Blunt, and `!important` on purpose. It is a preference, so it outranks
   whatever any component decided about its own animation, and one rule cannot
   fall out of step the way a companion selector on each of a dozen
   `prefers-reduced-motion` blocks would.

   Near zero rather than `none`, with one iteration, so anything mid-loop
   lands on its end state instead of snapping back to its start: the drift
   wall's tracks end exactly one copy along, which is where a seamless loop is
   supposed to be anyway.

   The operating system's own setting is handled where each animation is
   declared and is untouched by this. There is no third state that forces
   motion back on over it, deliberately. */
:root[data-ld-motion="reduced"] *,
:root[data-ld-motion="reduced"] *::before,
:root[data-ld-motion="reduced"] *::after {
  animation-duration: 0.001s !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001s !important;
}

/* A control for something that is not running is a dead button. */
:root[data-ld-motion="reduced"] .ld-carousel-toggle { display: none; }
@media (prefers-reduced-motion: reduce) {
  .ld-carousel-toggle { display: none; }
}

/* Three cells: what the page puts on the left, the theme switch, what
   it puts on the right. A grid rather than a flex row because the
   switch has to sit at the middle of the page and not at the middle
   of whatever happens to be beside it; equal `1fr` sides are what
   guarantee that however long the two labels are.

   The side cells are always rendered, so a page that yields nothing
   still gets a centered switch, which is what every page had before
   this existed. */
footer {
  /* Air above, not below: the footer is the end of the page, and it
     should read as separated from what it follows rather than as the
     last row of it. */
  padding: 3.5rem 1.5rem 1.5rem;
  /* Held to the width of the widest content above it, so the two
     ends sit under the page rather than out at the corners of a wide
     screen, where they stop reading as one row and stop belonging to
     anything. Matches `.co-page`. */
  width: 100%;
  max-width: 1100px;
  margin-inline: auto;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.75rem 1.5rem;
}
/* Spans the three columns and sits under them, so it reads as the
   last line of the page rather than as a fourth thing competing
   with the switch. Quiet on purpose: it is a place to look when you
   need it, not something to draw the eye. */
.ld-legal {
  grid-column: 1 / -1;
  justify-self: center;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.35rem 1rem;
  margin-top: 0.5rem;
  font-size: 0.8125rem;
  opacity: 0.6;
}
    /* `span.cs-mark`, not `.cs-mark`: the mark also carries
       `ld-floating-app-icon` for the float animation, and that rule sets
       `display: block`. The element selector wins on specificity, which is
       what keeps the glyph centred in its tile rather than in the corner.

       These live here rather than in carry-on-styles because the mark is no
       longer only on the baggage pages: the legal pages wear it as their way
       home, and they load none of that stylesheet. */
    span.cs-mark {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      flex: none;
      background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 55%, #4c1d95 100%);
      color: #fff;
      box-shadow: 0 12px 30px rgba(76, 29, 149, 0.35);
    }
    .cs-mark-glyph { display: inline-flex; }
    /* The mark wrapped as a link home. It inherits the page's ink and refuses
       the underline: the mark is a picture, not a word to be decorated. */
    .cs-home { display: inline-flex; flex: none; color: inherit; text-decoration: none; }
    .cs-mark-svg { width: 100%; height: 100%; display: block; }

    /* Mark and word as one unit, for the places that need to say the name:
       the landing hero and the home link on a solo page. */
    .cs-lockup { display: inline-flex; align-items: center; gap: 0.6rem; }
    .cs-wordmark {
      font-weight: 800;
      letter-spacing: -0.03em;
      line-height: 1;
      /* Lowercase like `littledots`, and for the same reason: it is a name, not
         a sentence, and it never takes a capital. */
      text-transform: lowercase;
    }

/* A document page: the mark, then the card, stacked and starting at
   the top.

   `main` is a shell that centres a single short card, which is right
   for the auth screens it was built for and wrong for a page of
   prose: a long document centred vertically pushes its own first
   line off the top, and anything placed beside the card becomes a
   row sibling floating at its vertical middle.

   `align-self: stretch` opts one child out of that centring without
   touching the shell, so the auth screens keep the behaviour they
   were designed around. */
/* Follows the shell rather than carrying its own number, or the header and
   the page under it disagree about how wide the site is. Text inside keeps
   its own measure: a wide surface is good and a wide line of prose is not. */
.ld-doc {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  width: 100%;
  max-width: var(--ld-shell-measure);
  margin-inline: auto;
}

/* The wrapper governs the width and the card fills it. `.ld-card`
   caps itself at 28rem for the auth screens it was built for, which
   on a document reads as a narrow column, and under
   `align-items: stretch` a capped item is placed at the start rather
   than centred, so it also drifts left. */
.ld-doc .ld-card { max-width: none; }

/* The way back, at the top of a page that is otherwise only words.
   Centred over the card rather than tucked against its left edge:
   on a page whose whole body is one column of prose, the mark reads
   as the page's own heading, and a heading sits over the middle of
   what it heads. */
.ld-sitemark {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  color: inherit;
  text-decoration: none;
  margin-bottom: 1.75rem;
  align-self: center;
}
.ld-sitemark:hover { opacity: 0.85; }
.ld-sitemark-word { font-weight: 800; letter-spacing: -0.03em; text-transform: lowercase; font-size: 1.5rem; }

.ld-legal a { color: inherit; text-decoration: none; }
/* A menu item marked "not on narrow": the same menu, minus what
   does not fit. One list, one place to edit it. */
@media (max-width: 560px) { .is-wide-only { display: none; } }
.ld-legal a:hover { text-decoration: underline; opacity: 1; }
.ld-footer-start { justify-self: start; }
.ld-footer-end { justify-self: end; }
/* Stacked, and centered with it: at this width the row cannot hold
   three things, and a left-aligned line under a centered switch
   reads as a mistake rather than as an arrangement. */
@media (max-width: 700px) {
  footer { grid-template-columns: 1fr; justify-items: center; }
  .ld-footer-start, .ld-footer-end { justify-self: center; }
}

/* Reusable floating app icon. Mirrors app/components/FloatingAppIcon.tsx
   (web variant) so Blade-rendered pages get the same hover. */
@keyframes app-icon-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}
.ld-floating-app-icon {
  display: block;
  object-fit: contain;
  animation: app-icon-float 3.6s ease-in-out infinite;
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .ld-floating-app-icon { animation: none; }
}

/* Prose
   ------------------------------------------------------------------------
   The body of every page and article on both hosts, and until LIP-064 step 4
   it was defined nowhere at all: the class was on the wrapper, no rule
   matched it, and every migrated document rendered as browser default with no
   measure, no heading rhythm and no list or link styling. That is most of why
   the content pages read as bare.

   Spacing comes from the scale rather than from values chosen per rule, which
   is the other half of the same problem. */
.ld-prose {
  line-height: 1.65;
  font-size: var(--ld-typography-scale-body);
}

/* Vertical rhythm is set by the space ABOVE a block, so a heading stays with
   what follows it rather than drifting toward what it left. The first child
   never gets it, or every card opens with a gap. */
/* The panel is as wide as the site; a line of text is not.

   Capped on the children and not on `.ld-prose` itself, which is the whole
   point: a cap on the wrapper is a cap on everything inside it, and the strip
   and the badge row are meant to run the panel's full width. A child cannot
   be wider than its parent, so the parent has to stay wide and the text has
   to say how far it goes.

   The line the exceptions draw is between running text and structure. A
   paragraph, a heading, a list: those are read along, and a measure is what
   keeps them readable. A panel, a grid of cards, a strip of pictures, a row
   of buttons: those are arrangements, and a container held to the width of a
   sentence reads as a page that failed to fill itself. */
.ld-prose > * {
  margin-top: var(--ld-spacing-lg);
  margin-bottom: 0;
  max-width: var(--ld-measure-prose);
}
.ld-prose > p:has(> a > img[src*="badge-"]),
.ld-prose > .ld-notice > p:has(> a > img[src*="badge-"]),
.ld-prose > .ld-panel,
.ld-prose > .ld-products,
.ld-prose > .ld-bookmarklet,
/* A code block is structure too. Held to the reading measure it scrolls
   sideways to show a line that would have fitted, and code is the one thing
   on a page you cannot skim past the end of. */
.ld-prose > pre { max-width: none; }

/* And inside a panel the measure applies again, for the same reason it
   applies outside one. */
.ld-prose .ld-panel > * { max-width: var(--ld-measure-prose); }
/* Except a badge row, which is the block's own call to action and spans it. */
.ld-prose .ld-notice > p:has(> a > img[src*="badge-"]) { max-width: none; }
.ld-prose > :first-child { margin-top: 0; }

.ld-prose h1,
.ld-prose h2,
.ld-prose h3,
.ld-prose h4 {
  margin-top: var(--ld-spacing-2xl);
  margin-bottom: var(--ld-spacing-xs);
  font-weight: var(--ld-typography-weights-bold);
  letter-spacing: var(--ld-typography-tracking-headings);
  line-height: 1.25;
}
.ld-prose > h1:first-child,
.ld-prose > h2:first-child,
.ld-prose > h3:first-child { margin-top: 0; }

.ld-prose h1 { font-size: var(--ld-typography-scale-h1); }
.ld-prose h2 { font-size: var(--ld-typography-scale-h2); }
.ld-prose h3 { font-size: 1.05rem; }
.ld-prose h4 { font-size: 0.95rem; opacity: 0.85; }

/* A heading immediately after another is a section title over a subtitle, not
   two sections, so it loses the gap that would separate them. */
.ld-prose h2 + h3,
.ld-prose h3 + h4 { margin-top: var(--ld-spacing-md); }

.ld-prose a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
}
.ld-prose a:hover { text-decoration-color: currentColor; }

.ld-prose ul,
.ld-prose ol { padding-left: var(--ld-spacing-lg); }
.ld-prose li + li { margin-top: var(--ld-spacing-xs); }
.ld-prose li > ul,
.ld-prose li > ol { margin-top: var(--ld-spacing-xs); }

.ld-prose strong { font-weight: var(--ld-typography-weights-bold); }

/* Its own space above and below, rather than the paragraph gap it inherited.
   A quotation carries a rule beside it, so it reads as a block with visual
   mass, and the same gap that separates two paragraphs leaves the next one
   sitting against it. The inner padding is what stops the first and last
   lines touching the ends of that rule. */
.ld-prose blockquote {
  margin-block: var(--ld-spacing-xl);
  margin-inline: 0;
  padding: var(--ld-spacing-2xs) 0 var(--ld-spacing-2xs) var(--ld-spacing-lg);
  border-left: 2px solid color-mix(in srgb, currentColor 25%, transparent);
  opacity: 0.85;
}
.ld-prose blockquote > * { margin-top: var(--ld-spacing-md); }
.ld-prose blockquote > :first-child { margin-top: 0; }
.ld-prose blockquote > :last-child { margin-bottom: 0; }

.ld-prose code {
  font-size: 0.9em;
  padding: 0.12em 0.36em;
  border-radius: 6px;
  background: color-mix(in srgb, currentColor 10%, transparent);
}

/* Scrolls rather than overflowing its card: a command is one long line and
   the page must not gain a horizontal scrollbar because of it. */
.ld-prose pre {
  padding: var(--ld-spacing-md);
  border-radius: var(--ld-radius-input);
  background: color-mix(in srgb, currentColor 8%, transparent);
  overflow-x: auto;
}
.ld-prose pre code { padding: 0; background: none; font-size: 0.85rem; }

.ld-prose hr {
  border: 0;
  border-top: 1px solid color-mix(in srgb, currentColor 20%, transparent);
  margin-top: var(--ld-spacing-2xl);
}

/* Bounded by HEIGHT, not width, because the pictures in these documents are
   mostly phone screenshots: a 471x1024 screen at the column's full width is
   most of a screen tall and pushes the sentence that explains it off the
   page. A height cap sizes a portrait sensibly and still lets a landscape
   shot use the column. Full size is one tap away. */
.ld-prose img {
  max-width: 100%;
  max-height: 22rem;
  width: auto;
  height: auto;
  border-radius: var(--ld-radius-input);
}

/* An embedded picture links to its own full size, so it is a control and
   should look like one. */
/* Every picture link lifts on hover, badges included: that is the control
   saying it is one. Only the cursor differs, because a badge goes to somebody
   else's shop and zoom-in promises a lightbox that never comes. */
.ld-prose a:has(> img) {
  display: inline-block;
  line-height: 0;
  text-decoration: none;
  border-radius: var(--ld-radius-input);
  cursor: zoom-in;
  transition: transform var(--ld-motion-ease), box-shadow var(--ld-motion-ease);
}
.ld-prose a:has(> img):hover {
  transform: translateY(-2px);
  box-shadow: var(--ld-shadow-card);
}
.ld-prose a:has(> img):focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
.ld-prose a:has(> img[src*="badge-"]) { cursor: pointer; }
.ld-prose a:has(> img[src*="badge-"]):hover { box-shadow: none; }

/* The lightbox. One picture, opened from a link that already worked, so with
   no script the link simply loads the image and nothing is lost. */
/* While a viewer is open the page behind it does not move.

   `showModal()` makes the document inert but does not stop it scrolling, and
   because the backdrop covers everything that scrolling is invisible while it
   happens: you close the picture and find yourself somewhere else, having lost
   your place. On touch it is worse, since a swipe over the image drags the
   page underneath instead of doing nothing.

   The padding is set in script to the width the scrollbar was taking, or
   locking would collapse it and shift the whole layout sideways. */
html.ld-scroll-locked,
html.ld-scroll-locked body { overflow: hidden; }

.ld-lightbox {
  border: 0;
  padding: 0;
  max-width: 100vw;
  max-height: 100vh;
  width: 100%;
  height: 100%;
  background: transparent;
  overflow: hidden;
}
.ld-lightbox::backdrop { background: rgba(0, 0, 0, 0.82); backdrop-filter: blur(6px); }
.ld-lightbox-inner {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--ld-spacing-md);
  padding: var(--ld-spacing-3xl) var(--ld-spacing-lg) var(--ld-spacing-xl);
}
.ld-lightbox img {
  max-width: 100%;
  /* Room for the caption and the counter under it, and air above, or a tall
     picture runs to both edges of the screen and the words beneath it are
     crushed against the bottom. */
  max-height: calc(100vh - 11rem);
  width: auto;
  height: auto;
  border-radius: var(--ld-radius-input);
  object-fit: contain;
}
.ld-lightbox-caption {
  color: #fff;
  opacity: 0.8;
  font-size: var(--ld-typography-scale-small);
  text-align: center;
  max-width: 60ch;
}
/* An X here, which the project's dialogs deliberately do not have.
   A viewer is not a dialog: it has no actions to name, the picture is the
   whole point, and a labelled button along the bottom spends a row of a phone
   screen saying what a corner glyph says for nothing. It keeps its accessible
   name, so it reads as "Close" to anything that is not looking at it. */
.ld-lightbox-close {
  position: absolute;
  top: var(--ld-spacing-md);
  right: var(--ld-spacing-md);
  width: 2.5rem;
  height: 2.5rem;
  display: grid;
  place-items: center;
  font: inherit;
  font-size: 1.1rem;
  line-height: 1;
  color: #fff;
  background: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: var(--ld-radius-pill);
  cursor: pointer;
  backdrop-filter: blur(6px);
}
.ld-lightbox-close:hover { background: rgba(255, 255, 255, 0.26); }
.ld-lightbox-close:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* A figure's caption belongs to the picture, so it sits tight under it. */
.ld-prose figure { margin-inline: 0; }
.ld-prose figcaption {
  margin-top: var(--ld-spacing-xs);
  font-size: var(--ld-typography-scale-small);
  opacity: 0.65;
}

.ld-prose table { width: 100%; border-collapse: collapse; }
.ld-prose th,
.ld-prose td {
  text-align: left;
  padding: var(--ld-spacing-xs) var(--ld-spacing-sm);
  border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
}
.ld-prose th { font-weight: var(--ld-typography-weights-bold); }

/* Breadcrumbs
   ------------------------------------------------------------------------ */
.ld-crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ld-spacing-2xs) var(--ld-spacing-xs);
  margin-bottom: var(--ld-spacing-lg);
  font-size: var(--ld-typography-scale-small);
}
.ld-crumb {
  color: inherit;
  text-decoration: none;
  opacity: 0.6;
  border-radius: var(--ld-radius-input);
}
a.ld-crumb:hover { opacity: 1; text-decoration: underline; text-underline-offset: 0.18em; }
a.ld-crumb:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
/* The page you are on is not a link, so it reads as the end of the trail
   rather than as one more thing to press. */
.ld-crumb-current { opacity: 0.95; font-weight: var(--ld-typography-weights-bold); }
.ld-crumb-home { display: inline-flex; }
.ld-crumb-home svg { width: 1.05em; height: 1.05em; display: block; }
.ld-crumb-sep { opacity: 0.35; }

/* The dots
   ------------------------------------------------------------------------
   One piece of artwork, `img/dots.svg`, in several jobs. It was called
   separator.svg on the old site, which named one of them: a rule between
   sections. It is the brand's own scattered blobs, and it also sits over a
   picture, floats above a heading, and marks a page as ours in a way no
   photograph can.

   The float is the same motion the app icon has, so the two read as one
   family rather than as two things that happen to move. */
.ld-dots {
  display: block;
  width: 100%;
  max-width: 22rem;
  margin-inline: auto;
  opacity: 0.9;
}

/* As a rule between sections. Air on both sides, because the point of it is
   the pause rather than the line. */
.ld-divider {
  border: 0;
  padding: 0;
  margin: var(--ld-spacing-3xl) auto;
  background: none;
}
.ld-divider img {
  display: block;
  width: 100%;
  max-width: 16rem;
  margin-inline: auto;
  opacity: 0.75;
  animation: app-icon-float 3.6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .ld-divider img { animation: none; }
}

/* Article cards
   ------------------------------------------------------------------------
   A listing of writing, each with the picture it leads with. The dots sit
   over that picture and fade on hover, which is the whole gesture: the card
   is ours at rest, and the photograph is what you came for once you reach
   for it. */
.ld-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--ld-spacing-lg);
  margin-top: var(--ld-spacing-lg);
}
.ld-article-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: var(--ld-radius-card);
  background: var(--ld-colors-surface-card-light);
  color: inherit;
  text-decoration: none;
  transition: transform var(--ld-motion-ease), box-shadow var(--ld-motion-ease);

  /* Light at rest and only a little heavier under a pointer. The card shadow
     token is tuned for one panel floating on a gradient; a grid of six at
     that weight reads as six things shouting rather than as a list. */
  box-shadow: 0 4px 14px rgba(91, 33, 182, 0.10);
}
[data-bs-theme="dark"] .ld-article-card {
  background: var(--ld-colors-surface-card-dark);
  /* Darker ground, so the same shadow reads far heavier: a black halo on a
     near black page is all anybody sees. */
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);
}
.ld-article-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 10px 26px rgba(91, 33, 182, 0.18);
}
[data-bs-theme="dark"] .ld-article-card:hover { box-shadow: 0 10px 26px rgba(0, 0, 0, 0.42); }
.ld-article-card:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }

.ld-article-card-figure {
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: linear-gradient(135deg, var(--ld-colors-background-gradient-light));
}
.ld-article-card-figure img.ld-article-card-photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Two layers over the photograph: a wash that carries the brand colour, and
   the dots themselves. Both fade together, so what is revealed is the
   picture rather than a picture with dots still on it. */
.ld-article-card-veil {
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, rgba(131, 79, 150, 0.72), rgba(76, 29, 149, 0.62));
  transition: opacity 280ms ease;
}
/* Big, and cropped by the figure. The blobs are the card's face at rest, not
   a badge sitting on it, so they run past the edges the way they did on the
   old site. No float here: a grid of cards all breathing at once is a fidget,
   and the motion belongs to the one mark on a page rather than to six. */
.ld-article-card-dots {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  transition: opacity 280ms ease, transform 280ms ease;
}
/* Zoomed until only five or six blobs are in frame. The artwork is a wide
   strip, 8.7:1, so showing all of it in a 16:10 card leaves a thin band of
   small dots; at this scale each blob is one of the shapes rather than a
   speck, which is what it looked like on the old site. */
.ld-article-card-dots img {
  width: 320%;
  max-width: none;
  flex: none;
}
.ld-article-card:hover .ld-article-card-veil,
.ld-article-card:focus-visible .ld-article-card-veil { opacity: 0; }
.ld-article-card:hover .ld-article-card-dots,
.ld-article-card:focus-visible .ld-article-card-dots { opacity: 0; transform: scale(1.06); }
@media (prefers-reduced-motion: reduce) {
  .ld-article-card-veil,
  .ld-article-card-dots { transition: none; }
}

.ld-article-card-body {
  display: flex;
  flex-direction: column;
  gap: var(--ld-spacing-xs);
  padding: var(--ld-spacing-lg);
  flex: 1;
}
.ld-article-card-title { margin: 0; font-size: 1.15rem; font-weight: var(--ld-typography-weights-bold); line-height: 1.3; }
.ld-article-card-excerpt { margin: 0; opacity: 0.75; font-size: var(--ld-typography-scale-small); line-height: 1.55; }
/* Looks like the button it replaced, without being one: the whole card is
   already the link, and a link inside a link is not a thing. */
.ld-article-card-more {
  margin-top: auto;
  padding-top: var(--ld-spacing-xs);
  font-size: var(--ld-typography-scale-xs);
  font-weight: var(--ld-typography-weights-bold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.8;
}

/* Article hero
   ------------------------------------------------------------------------
   The featured picture at the top of its own page, with the title over it.
   The scrim is what makes the title readable over a photograph nobody chose
   for its contrast. */
.ld-hero-figure {
  position: relative;
  margin: calc(var(--ld-spacing-xl) * -1) calc(var(--ld-spacing-xl) * -1) var(--ld-spacing-lg);
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--ld-radius-card) var(--ld-radius-card) 0 0;
}
.ld-hero-figure img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A light wash over the whole picture, so neither end's gradient has to do
   all the work of making white text readable. */
.ld-hero-scrim {
  position: absolute;
  inset: 0;
  background: rgba(20, 8, 37, 0.22);
}
.ld-hero-text {
  position: absolute;
  inset: 0 0 auto 0;
  padding: var(--ld-spacing-lg);
  background: linear-gradient(to bottom, rgba(20, 8, 37, 0.72), rgba(20, 8, 37, 0));
  color: #fff;
}
.ld-hero-text h1 { margin: 0; color: #fff; }
@media (max-width: 560px) {
  .ld-hero-figure { aspect-ratio: 4 / 3; }
}

/* Store badges
   ------------------------------------------------------------------------
   Apple's and Google's artwork have different intrinsic proportions, so at
   natural size they come out different heights and the row reads as a
   mistake. Height is set and width follows, which is also what both vendors
   ask for.

   Targeted by filename rather than by a class, because these arrive as
   markdown from a content row and markdown cannot carry one. */
.ld-prose a:has(> img[src*="badge-"]) { line-height: 0; }
.ld-prose img[src*="badge-"] {
  height: 2.75rem;
  width: auto;
  max-height: none;
  border-radius: 0;
}

/* All three are the vendors' own artwork, at three different aspect ratios
   (2.99, 3.38 and 2.58 to one), which is exactly why the height is set here
   and the width follows. F-Droid's is the official badge from
   f-droid.org/en/docs/Badges. */

/* The three sit in one paragraph in the content, so they flow as a row and
   wrap together on a phone.

   The room underneath is for the dots, which hang below the row and are a
   background layer: they take no space of their own, so whatever follows
   would otherwise sit in the middle of them.

   A direct child of the prose throughout, which is what a page's own download
   row is. Product cards carry badge rows inside them and dress their own; as
   a descendant rule this reached those too and hung six rems of empty space
   off the bottom of every card. */
.ld-prose > p:has(> a > img[src*="badge-"]),
.ld-prose > .ld-notice > p:has(> a > img[src*="badge-"]) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* Centred, because the accent behind them is centred on the panel and a row
     of badges hard left over a symmetrical band reads as a mistake. */
  justify-content: center;
  gap: var(--ld-spacing-sm);
  margin-bottom: var(--ld-spacing-4xl);
}
/* Inside a notice the same row wants less under it. Out in the prose that
   6rem is a section break *plus* the dots' overhang, because something
   follows and has to clear them. In a notice the row is the last thing in the
   box, so the overhang is all it needs: the notice's own padding is the
   break, and the rest was dead space with the dots floating in the top of
   it. */
.ld-prose > .ld-notice > p:has(> a > img[src*="badge-"]) {
  margin-bottom: 4.75rem;
}

/* A single newline is a line break everywhere else, which is what put these
   here: three badges on three lines of one paragraph. In a flex row each one
   becomes an empty item that only adds a gap, so they go. The break did its
   job in the source by keeping the three readable. */
.ld-prose > p:has(> a > img[src*="badge-"]) > br,
.ld-prose > .ld-notice > p:has(> a > img[src*="badge-"]) > br { display: none; }

/* The dots behind the download buttons
   ------------------------------------------------------------------------
   A playful accent from the old site: the strip sits wider than the row,
   behind the badges, drifting gently up and down. It is a background layer
   rather than an element in the flow, so it cannot push the buttons around
   or be read out to anybody.

   Wider than its container on purpose. The artwork is 8.7:1 and the row is
   short, so at container width the blobs are specks; overflowing it lets them
   be the size they are in the picture, with the ends running past the edges.

   The float is a transform on its own layer, which is what keeps it smooth:
   nothing here reflows, and the compositor can carry it. */
.ld-prose > p:has(> a > img[src*="badge-"]),
.ld-prose > .ld-notice > p:has(> a > img[src*="badge-"]) {
  position: relative;
}
.ld-prose > p:has(> a > img[src*="badge-"])::before,
.ld-prose > .ld-notice > p:has(> a > img[src*="badge-"])::before {
  content: "";
  position: absolute;
  /* Three quarters of the row, but never so narrow that it stops reading as
     the band it is: the aspect ratio is fixed, so a percentage on its own
     means the whole thing halves in height on a phone. The floor holds its
     size down to where the row itself is narrower, and the 100% ceiling is
     what stops a floor wider than the page putting a scrollbar on it.

     Centred with auto margins rather than translateX, which leaves
     `transform` free for the float: a keyframe sets the whole transform, so a
     centring translate written there would have to be repeated in every
     step. */
  width: min(100%, max(75%, 26rem));
  left: 0;
  right: 0;
  margin-inline: auto;
  /* Hung from the top, just under the last row of buttons, rather than from
     the bottom. Anchored below, a band that got shorter left its top drifting
     away from the buttons and opened a gap that widened as the screen
     narrowed. From above it always starts in the same place, just under the
     buttons and overlapping them slightly, which is the arrangement it
     had.

     A direct child of the prose, which is what a page's own download row is.
     The product cards carry badge rows of their own inside them, and an
     accent behind each of those is wallpaper. */
  top: calc(100% - 0.5rem);
  bottom: auto;
  aspect-ratio: 53.66 / 6.16;
  background: url("/img/dots.svg") center / contain no-repeat;
  opacity: 0.85;
  pointer-events: none;
  will-change: transform;
  /* The same float the app icon has, at the old site's own timing: 3.6s
     ease-in-out, six pixels. One motion across the brand rather than two that
     nearly match. */
  animation: app-icon-float 3.6s ease-in-out infinite;
}

/* Only the first row on a page carries it. `/download` lists the companion
   apps with badge rows of their own, and the accent behind every one of them
   reads as wallpaper rather than as a flourish under the thing the page is
   for.

   "The page's own row" means a direct child of the prose, or the row inside
   the one highlight block a page opens with, which is where /roadmap puts it.
   A row inside a product card is neither.

   Said as "any badge row that follows another", which is what makes it the
   first one without a template having to mark it. */
.ld-prose > p:has(> a > img[src*="badge-"]) ~ p:has(> a > img[src*="badge-"])::before {
  content: none;
}

/* The app icons that appear in a body, which are artwork rather than
   screenshots: an SVG with no intrinsic size fills whatever it is given, and
   these were rendering the full width of the column. */
.ld-prose img[src*="app-icon-"] {
  width: 4.5rem;
  height: auto;
}
/* Except in a card, which sizes its own. The rule above is for an icon placed
   in a body by hand, and it is one specificity step above a bare class, so
   without this it silently resizes every card icon on the page. */
.ld-product .ld-product-icon,
.ld-prose .ld-product-icon {
  width: 3.25rem;
  height: 3.25rem;
}
/* The badges sit over it. Without this they share a layer with the accent and
   the paint order is whatever the markup happened to be. */
.ld-prose > p:has(> a > img[src*="badge-"]) > a,
.ld-prose > .ld-notice > p:has(> a > img[src*="badge-"]) > a {
  position: relative;
  z-index: 1;
}
@media (prefers-reduced-motion: reduce) {
  .ld-prose > p:has(> a > img[src*="badge-"])::before,
  .ld-prose > .ld-notice > p:has(> a > img[src*="badge-"])::before { animation: none; }
}


/* The site header
   ------------------------------------------------------------------------
   On every littledots page and absent until LIP-064, which is the largest
   single reason the pages read as bare: they carried no navigation and
   neither call to action.

   Not a bar across the top. It sits on the gradient like everything else and
   is held together by the same glass the page panel uses at the same
   strength, so the page reads as one surface rather than as content under
   chrome. It scrolls away with the page: a document is read top to bottom and
   a strip following the reader down is a strip taking a row off every screen
   for a link they used once.

   The row wraps rather than collapsing to a popup. Three links and one button
   fit on a phone in two rows, and a popup would be a second navigation to
   build, style and keep in step for content that was never too long. */
.ld-header {
  width: 100%;
  /* The measure plus the shell's padding, so the header's *content* is as
     wide as the page's rather than its outer box being as wide as the page's
     content. Everything here is border-box, so the padding has to be added
     back or the header comes out two paddings narrower than what it sits
     over. */
  max-width: calc(var(--ld-shell-measure) + 2 * var(--ld-shell-pad));
  margin: 0 auto;
  padding: var(--ld-spacing-md) var(--ld-shell-pad) 0;
  /* Above the start page's drift wall, which runs up behind it to the top of
     the page. The hero isolates, so without a z-index of its own the header
     would paint under the whole of that stacking context. */
  position: relative;
  z-index: 1;
}
.ld-header-inner {
  display: flex;
  flex-wrap: wrap;
  /* The bar centres what it holds. The baseline that lines the wordmark up
     with the links is one box further in, on .ld-header-brand: a box can
     centre its children or sit them on a shared baseline, not both, and this
     needs both. */
  align-items: center;
  gap: var(--ld-spacing-sm) var(--ld-spacing-lg);
  padding: var(--ld-spacing-xs) var(--ld-spacing-md);
  border-radius: var(--ld-radius-card);
  background: rgba(255, 255, 255, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.4);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
}
[data-bs-theme="dark"] .ld-header-inner {
  background: rgba(0, 0, 0, 0.18);
  border-color: rgba(255, 255, 255, 0.06);
}
/* The mark in the header is a row item rather than the block it is inside a
   document, and it drops the float: one thing bobbing in a line of static
   links reads as a fault rather than as life.

   `inline-block` and not `inline-flex`, and that is the whole of what makes
   the row line up. A flex container hands out the baseline of its first item,
   which here is the icon, and an image has no baseline so its bottom edge is
   used instead: the links would then align to the bottom of the picture. An
   inline-block hands out the baseline of its last line of text, which is the
   word, so the word is what the links meet.

   Extra room on the right on top of the row's gap, because the mark and the
   navigation are two different things and the links are one list. */
.ld-header .ld-sitemark {
  display: inline-block;
  margin: 0 var(--ld-spacing-md) 0 0;
  flex: none;
  /* The document mark centres itself in its own column. Here it has a
     baseline to meet, and an inherited `align-self: center` would quietly
     win against the row's `align-items` and put it back where it started. */
  align-self: baseline;
  text-decoration: none;
  color: inherit;
  white-space: nowrap;
}
.ld-header .ld-sitemark .ld-floating-app-icon {
  /* `.ld-floating-app-icon` is `display: block`, which it wants to be in the
     hero it was written for. Left alone here it takes a line of its own
     inside the mark and drops the word underneath it, which is not a wrapping
     problem and so `white-space` above cannot help.

     Inline-level and vertically centred, it shares the word's line: the line
     box then ends up the height of the icon, its baseline is the word's, and
     the word is centred against the icon exactly as before. */
  display: inline-block;
  vertical-align: middle;
  margin-right: var(--ld-spacing-xs);
  animation: none;
}
.ld-header .ld-sitemark-word { font-size: 1.15rem; font-weight: 700; letter-spacing: -0.01em; }

/* The mark and the links, on one line of text. This is the box that answers
   "why is the wordmark sitting a few pixels below the links": centred boxes
   of different heights put their text wherever their own middles fall, and
   only a shared baseline puts them on the same line.

   It takes the space between itself and the button, so the button ends up
   hard right without anything being pushed there by hand. */
.ld-header-brand {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ld-spacing-sm) var(--ld-spacing-lg);
  margin-right: auto;
}
.ld-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--ld-spacing-md);
}
.ld-nav a {
  color: inherit;
  text-decoration: none;
  font-weight: 600;
  padding: var(--ld-spacing-3xs) 0;
  border-bottom: 2px solid transparent;
  transition: border-color 120ms ease, opacity 120ms ease;
}
.ld-nav a:hover { border-bottom-color: currentColor; }
.ld-nav a[aria-current="page"] { border-bottom-color: currentColor; }

/* The narrow header
   ------------------------------------------------------------------------
   Below the sheet breakpoint the links come out of the bar and into a sheet
   of their own, opened by a burger sitting to the right of Apps.

   The links exist twice in the markup for this, once in the bar and once in
   the sheet, and only one of the two is ever reachable: a closed dialog is
   hidden from the page and from the accessibility tree, so nothing is
   announced or tabbed to twice. Moving one element between the two places
   would need a script to do it on every resize, and the links would vanish
   entirely for anybody without one. */
.ld-header-actions {
  display: flex;
  align-items: center;
  gap: var(--ld-spacing-xs);
  flex: none;
}
.ld-burger {
  display: none;
  align-items: center;
  justify-content: center;
  width: 2.4rem;
  height: 2.4rem;
  padding: 0;
  border-radius: 999px;
  border: 1px solid rgba(109, 40, 217, 0.25);
  background: rgba(255, 255, 255, 0.5);
  color: inherit;
  cursor: pointer;
  transition: background-color 120ms ease;
}
.ld-burger:hover { background: rgba(139, 92, 246, 0.16); }
[data-bs-theme="dark"] .ld-burger {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(196, 181, 253, 0.28);
}
[data-bs-theme="dark"] .ld-burger:hover { background: rgba(196, 181, 253, 0.2); }

@media (max-width: 639px) {
  .ld-header-brand .ld-nav { display: none; }
  .ld-burger { display: inline-flex; }
}

/* The links inside the sheet are a column and read as a list rather than as
   the row they are in the bar. */
.ld-nav-sheet {
  display: flex;
  flex-direction: column;
  gap: var(--ld-spacing-xs);
}
.ld-nav-sheet a {
  display: block;
  padding: var(--ld-spacing-sm) var(--ld-spacing-md);
  border-radius: var(--ld-radius-card);
  background: rgba(255, 255, 255, 0.4);
  color: inherit;
  text-decoration: none;
  font-size: 1.05rem;
  font-weight: 600;
}
[data-bs-theme="dark"] .ld-nav-sheet a { background: rgba(0, 0, 0, 0.25); }
.ld-nav-sheet a:hover { background: rgba(139, 92, 246, 0.2); }


/* Icons in a body
   ------------------------------------------------------------------------
   Placed with `::icon:name::`, and sized in `em` so one sits at the front of
   a heading at that heading's own size rather than at a size chosen here. */
.ld-icon {
  width: 1em;
  height: 1em;
  display: inline-block;
  vertical-align: -0.115em;
  margin-right: 0.35em;
  flex: none;
}
/* Simple Icons are solid single-path marks with no stroke, so they need a
   fill; the heroicons in the same list are outlines and need the opposite.
   Both are drawn in the text's own colour, which is what keeps a row of them
   from reading as a row of stickers. */
.ld-icon { color: inherit; }
.ld-prose h2 .ld-icon,
.ld-prose h3 .ld-icon { opacity: 0.85; }

/* A row of calls to action
   ------------------------------------------------------------------------
   Placed with `::actions::`, and it exists because "a paragraph that contains
   only a link" is not a reliable signal for a button: plenty of paragraphs
   elsewhere are exactly that and want to stay prose. Saying so explicitly
   costs one marker and cannot catch anything by accident.

   The links take the CTA's own rules rather than a copy of them, so a change
   to the button reaches these too. `.ld-prose a` underlines every link in a
   body, which is why the decoration has to be said again here. */
.ld-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ld-spacing-sm);
}
.ld-prose .ld-actions { max-width: none; margin-top: var(--ld-spacing-md); }
.ld-prose .ld-actions a { text-decoration: none; }
.ld-prose .ld-actions p { margin: 0; }

/* The first link is the primary and everything after it is secondary, which
   is how a row of buttons reads anyway and so needs no second marker for the
   page to say it. Two selectors because markdown puts the links in one
   paragraph or in several depending on whether a blank line separates them,
   and which of those an author typed should not decide how the row looks.

   The padding loses the border's width so a secondary sits at exactly the
   same height as the primary beside it. */
.ld-prose .ld-actions > p:not(:first-child) a,
.ld-prose .ld-actions > p a ~ a,
.ld-prose .ld-actions.is-quiet a {
  padding: calc(0.85rem - 1px) calc(1.6rem - 1px);
  background: transparent;
  color: #6d28d9;
  border: 1px solid rgba(109, 40, 217, 0.35);
  box-shadow: none;
}
.ld-prose .ld-actions > p:not(:first-child) a:hover,
.ld-prose .ld-actions > p:not(:first-child) a:focus,
.ld-prose .ld-actions > p a ~ a:hover,
.ld-prose .ld-actions > p a ~ a:focus,
.ld-prose .ld-actions.is-quiet a:hover,
.ld-prose .ld-actions.is-quiet a:focus {
  background: rgba(109, 40, 217, 0.1);
  color: #5b21b6;
  border-color: rgba(109, 40, 217, 0.55);
  transform: translateY(-1px);
  box-shadow: none;
}
[data-bs-theme="dark"] .ld-prose .ld-actions > p:not(:first-child) a,
[data-bs-theme="dark"] .ld-prose .ld-actions > p a ~ a,
[data-bs-theme="dark"] .ld-prose .ld-actions.is-quiet a {
  background: transparent;
  color: #c4b5fd;
  border-color: rgba(196, 181, 253, 0.4);
  box-shadow: none;
}
[data-bs-theme="dark"] .ld-prose .ld-actions > p:not(:first-child) a:hover,
[data-bs-theme="dark"] .ld-prose .ld-actions > p:not(:first-child) a:focus,
[data-bs-theme="dark"] .ld-prose .ld-actions > p a ~ a:hover,
[data-bs-theme="dark"] .ld-prose .ld-actions > p a ~ a:focus,
[data-bs-theme="dark"] .ld-prose .ld-actions.is-quiet a:hover,
[data-bs-theme="dark"] .ld-prose .ld-actions.is-quiet a:focus {
  background: rgba(196, 181, 253, 0.16);
  color: #ddd6fe;
  border-color: rgba(196, 181, 253, 0.6);
}

/* An action that is only an icon is a round button, not a wide pill with a
   glyph rattling about in the middle of it.
   
   Only the inline padding is taken back, so the height still comes from the
   rule above and an icon button lines up with the words beside it. The glyph
   is set a little larger than an `em` to fill the space a line of text would
   have, and loses the gap it carries for the word that usually follows it.

   **Such a link needs a markdown title**: `[::icon:globe::](/live "Live map")`.
   The icons render `aria-hidden`, since normally the heading beside them says
   the same word, so without one the button has no accessible name at all. The
   title is also the tooltip, which an icon on its own wants anyway. */
.ld-prose .ld-actions a:has(> .ld-icon:only-child) {
  padding-inline: calc(0.85rem - 1px);
}
.ld-prose .ld-actions a > .ld-icon:only-child {
  width: 1.3em;
  height: 1.3em;
  margin-right: 0;
}


/* The roadmap
   ------------------------------------------------------------------------
   One column at every width, with the spine down the left and every marker
   sitting on it. The old site put its years and months in a gutter beside the
   spine, which needs a column of width a phone has not got; on the spine they
   read the same in portrait and in a window.

   The spine is drawn on the list rather than on each row, so it is one
   continuous line and cannot show the seams between items. It fades out at
   both ends: a hard stop reads as the story beginning and ending abruptly,
   and neither is true. */
.ld-timeline {
  --ld-tl-spine: 1.35rem;
  position: relative;
  list-style: none;
  margin: var(--ld-spacing-xl) 0 0;
  padding: 0 0 0 calc(var(--ld-tl-spine) * 2 + var(--ld-spacing-md));
}
/* The offsets below are measured against what the browser actually draws,
   which includes `.ld-prose ol`'s own 1.5rem indent winning over this block's
   padding. Change that indent and these move with it. */
.ld-timeline::before {
  content: "";
  position: absolute;
  /* Up into the list's own top margin, so the line meets the panel above it
     rather than starting in mid air below it. */
  top: calc(-1 * var(--ld-spacing-lg));
  bottom: 0.5rem;
  /* Far enough right that a month ends clear of it. */
  left: calc(var(--ld-tl-spine) + 1.9rem);
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(
    to bottom,
    transparent,
    rgba(109, 40, 217, 0.35) 4rem,
    rgba(109, 40, 217, 0.35) calc(100% - 4rem),
    transparent
  );
}
[data-bs-theme="dark"] .ld-timeline::before {
  background: linear-gradient(
    to bottom,
    transparent,
    rgba(196, 181, 253, 0.3) 4rem,
    rgba(196, 181, 253, 0.3) calc(100% - 4rem),
    transparent
  );
}
/* The block sits in from the panel's edge, because the markers hang off the
   left of the list and would otherwise run into the panel's padding. Margin
   and not padding, since the spine and the nodes are placed against the
   list's padding box and would not move with it.

   Kept small because the labels are short: months are abbreviated in the
   rows, so the widest thing hanging off the left is a year pill. */
.ld-prose > .ld-timeline {
  max-width: none;
  margin-left: 1rem;
}

/* Entries
   ------------------------------------------------------------------------ */
/* Qualified with the element, because `.ld-prose li + li` sets the gap
   between any two list rows and is one specificity step above a bare class.
   Written as `.ld-tl-entry` alone this has no effect at all, which is why the
   rows sat half a rem apart however this number was changed. */
.ld-tl-entry { position: relative; }
.ld-prose li.ld-tl-entry { margin-top: var(--ld-spacing-lg); }
.ld-prose li.ld-tl-entry:first-child { margin-top: 0; }
/* Except after a month, which is that entry's own caption rather than the
   end of what came before. The gap belongs above the month, not between the
   two of them. */
.ld-prose li.ld-tl-marker.is-month + li.ld-tl-entry { margin-top: var(--ld-spacing-3xs); }

/* The node sits centred on the spine, which is why its offset is written from
   the same custom property the spine is: two numbers that must agree, said
   once. */
.ld-tl-node {
  position: absolute;
  /* Centred on the line: the spine's middle, less half a node. */
  left: 0.935rem;
  top: 0.1rem;
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(109, 40, 217, 0.3);
  color: #6d28d9;
}
[data-bs-theme="dark"] .ld-tl-node {
  background: rgba(30, 20, 55, 0.95);
  border-color: rgba(196, 181, 253, 0.35);
  color: #c4b5fd;
}
.ld-tl-glyph { width: 1rem; height: 1rem; }

/* Where it started: filled, because it is the one node that is an event
   rather than a thing shipped, and the mark is what it is about. */
.ld-tl-entry.is-inception .ld-tl-node {
  width: 2.25rem;
  height: 2.25rem;
  left: 0.685rem;
  background: #6d28d9;
  border-color: transparent;
  color: #fff;
}
[data-bs-theme="dark"] .ld-tl-entry.is-inception .ld-tl-node { background: #7c3aed; }
.ld-tl-entry.is-inception .ld-tl-glyph { width: 1.35rem; height: 1.35rem; }

/* The body shifts by exactly what the line shifted, so the sub-item dashes
   still meet it: that contact is the thing that makes the list read as hung
   off the timeline rather than sitting beside it. */
.ld-tl-body { padding-left: 1.9rem; }

/* Only the titles step further in. They were sitting against the line, and
   they are the one part of a row that is not attached to it. */
.ld-prose .ld-tl-title {
  padding-left: 1.41rem;
  margin: 0;
  font-size: 1.05rem;
  font-weight: 600;
  line-height: 1.4;
}
.ld-prose .ld-tl-subs {
  list-style: none;
  margin: var(--ld-spacing-xs) 0 0;
  padding: 0;
  font-size: 0.92rem;
  line-height: 1.55;
  opacity: 0.82;
}
/* A dash rather than a tick beside every line. The old page ticked all of
   them, which says "done" 84 times on a page whose nodes already say it. */
.ld-prose .ld-tl-subs li {
  position: relative;
  margin-top: var(--ld-spacing-3xs);
  padding-left: 1rem;
}
.ld-prose .ld-tl-subs li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.72em;
  width: 0.45rem;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  opacity: 0.45;
}
/* A store badge written into an entry, which a few of them carry inline. */
.ld-prose .ld-tl-body img[src*="badge-"] { height: 2rem; width: auto; vertical-align: -0.5rem; }
/* Lined up with the title rather than with the sub-items: it is the row's
   own action, not one of its bullet points. */
.ld-prose .ld-tl-cta {
  margin: var(--ld-spacing-sm) 0 0;
  padding-left: 1.41rem;
}

/* Markers
   ------------------------------------------------------------------------
   A year, a month, a pause, or "what's going on". All four are the same row
   in the table and differ only in weight here. */
.ld-tl-marker {
  position: relative;
  margin-top: var(--ld-spacing-2xl);
}
.ld-tl-marker:first-child { margin-top: 0; }
/* Centred on one another, so a month sits under the middle of a year pill
   however long either word is. Sharing a right edge lined their boxes up and
   still looked wrong, because a pill shows its box and a bare label shows its
   glyphs.

   `right` places the right edge and the half-width translate then puts the
   middle there instead, which is how a fixed centre is expressed without
   knowing how wide the word is. Taken out of flow for it, which is why the
   row carries a min-height: nothing else is in the row to hold it open.

   Not in the hiatus band, where the badge sits in a flex row beside what the
   marker says and has to stay in flow. */
.ld-tl-marker:not(.is-hiatus) .ld-tl-badge {
  position: absolute;
  top: 0;
  right: calc(100% + 1.3rem);
  transform: translateX(50%);
  margin-left: 0;
  white-space: nowrap;
}
.ld-prose li.ld-tl-marker { min-height: 1.45rem; }
.ld-prose li.ld-tl-marker.is-year { min-height: 2.1rem; }

.ld-tl-badge {
  position: relative;
  display: inline-block;
  margin-left: calc(var(--ld-tl-spine) * -2 - var(--ld-spacing-md) + 0.1rem);
  padding: 0.2rem 0.75rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(109, 40, 217, 0.25);
  color: #6d28d9;
}
[data-bs-theme="dark"] .ld-tl-badge {
  background: rgba(30, 20, 55, 0.95);
  border-color: rgba(196, 181, 253, 0.3);
  color: #c4b5fd;
}

/* The year is the one marker that carries the eye down a long page, so it is
   the only one set large. */
.ld-tl-marker.is-year { margin-top: var(--ld-spacing-3xl); }
.ld-tl-marker.is-year .ld-tl-badge {
  padding: 0.3rem 1rem;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
  background: #6d28d9;
  border-color: transparent;
  color: #fff;
}
[data-bs-theme="dark"] .ld-tl-marker.is-year .ld-tl-badge { background: #7c3aed; }

/* Months are wayfinding rather than headings: they say where you are without
   asking to be read. */
.ld-tl-marker.is-month .ld-tl-badge {
  background: transparent;
  border-color: transparent;
  /* No padding at all, not just on the left. With the badge right-aligned,
     padding on the right stops the word short of the edge the year pill's
     background reaches, so the two look misaligned while their boxes are
     not: a pill shows its box and a bare label shows its glyphs. */
  padding: 0;
  opacity: 0.6;
  font-weight: 600;
}

/* A pause in the story, and the one row that is about nothing shipping.
   Its label and what it says sit together on one line inside the band, with
   the dots behind them: there is no list under this one to carry the eye
   across the gap, so the band has to. */
.ld-tl-marker.is-hiatus {
  /* Edge to edge: out through the list's margin, the row indent and the
     panel's own padding, so the band meets the panel's borders on both sides
     rather than stopping at its text. */
  margin: var(--ld-spacing-3xl) calc(-1 * var(--ld-page-pad))
          var(--ld-spacing-3xl) calc(-2.5rem - var(--ld-page-pad));
  /* Tall on purpose. The band is the one row that stands for time passing
     rather than for something shipping, so the space it takes is the point:
     a thin strip reads as another entry, a deep one reads as a gap.

     Padded on the left to the column the entry titles use, so what the marker
     says lines up with them rather than sitting against the line. */
  padding: var(--ld-spacing-4xl) 0 var(--ld-spacing-4xl)
           calc(5.81rem + var(--ld-page-pad));
  border-block: 1px solid rgba(109, 40, 217, 0.18);
  background: linear-gradient(90deg, rgba(139, 92, 246, 0.14), transparent 70%);
}
[data-bs-theme="dark"] .ld-tl-marker.is-hiatus {
  border-block-color: rgba(196, 181, 253, 0.16);
  background: linear-gradient(90deg, rgba(196, 181, 253, 0.12), transparent 70%);
}
/* The line changes character for the length of the band: dashed and dimmer,
   because nothing was shipped across it. It is drawn here rather than on the
   list, which carries one continuous line and cannot vary along its length;
   this one sits over that line, at the same x, for exactly the band's height.

   Offsets inside the band are measured from its own left edge, which is the
   panel's border edge: one panel padding left of where the text starts. */
.ld-tl-marker.is-hiatus::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(4.25rem + var(--ld-page-pad));
  width: 2px;
  background: repeating-linear-gradient(
    to bottom,
    rgba(109, 40, 217, 0.4) 0 0.4rem,
    transparent 0.4rem 0.8rem
  );
}
[data-bs-theme="dark"] .ld-tl-marker.is-hiatus::before {
  background: repeating-linear-gradient(
    to bottom,
    rgba(196, 181, 253, 0.32) 0 0.4rem,
    transparent 0.4rem 0.8rem
  );
}

/* The dots drift, the same float the app icon and the download buttons have.
   They are an element and not a background for exactly that reason: the
   animation moves a transform, and a background image has none to move.

   Centred by arithmetic rather than by translateY, which leaves `transform`
   free for the keyframe: a centring translate written there would have to be
   repeated in every step. */
.ld-tl-marker.is-hiatus::after {
  content: "";
  position: absolute;
  /* Inset from the band's own edge. The band runs to the panel's border now,
     so anchoring at zero put the dots against the wall. */
  right: calc(var(--ld-page-pad) + var(--ld-spacing-md));
  top: calc(50% - 1.125rem);
  height: 2.25rem;
  aspect-ratio: 53.66 / 6.16;
  background: url("/img/dots.svg") center / contain no-repeat;
  opacity: 0.85;
  pointer-events: none;
  will-change: transform;
  animation: app-icon-float 3.6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .ld-tl-marker.is-hiatus::after { animation: none; }
}
/* On a narrow screen there is no room beside the caption, so the dots go
   under it, into space the band already has. No extra padding for them: that
   makes the band lopsided, and the caption flows while the pill and the node
   are placed against the middle, so the three stop lining up.

   Centred with auto margins rather than a translate, which the float keyframe
   owns. */
@media (max-width: 639px) {
  .ld-tl-marker.is-hiatus { padding-block: var(--ld-spacing-3xl); }
  .ld-tl-marker.is-hiatus::after {
    top: auto;
    bottom: var(--ld-spacing-lg);
    left: 0;
    right: 0;
    margin-inline: auto;
    height: 1.5rem;
  }
}
/* Placed rather than flowed, like every other marker's, so the caption's own
   position does not depend on how long the word in the pill is. Where it sat
   before the band grew: out past the text, centred against a band that is
   much taller than it. */
.ld-tl-marker.is-hiatus .ld-tl-badge {
  position: absolute;
  left: calc(-1.1rem + var(--ld-page-pad));
  /* Well above the middle, so it reads as labelling the band rather than as a
     second thing on the caption's line. At a rem and a half the two were
     nearly touching. */
  top: calc(50% - 2.75rem);
  transform: translateY(-50%);
  margin: 0;
  /* Set apart from the node beside it, which wears the same dark plate and
     violet edge, but quietly: this row is a pause, and a filled pill
     announced it like a release. Tinted and borderless, so it reads as part
     of the band rather than as a thing sitting on it. */
  background: rgba(109, 40, 217, 0.16);
  border-color: transparent;
  color: inherit;
  opacity: 0.75;
}
[data-bs-theme="dark"] .ld-tl-marker.is-hiatus .ld-tl-badge {
  background: rgba(196, 181, 253, 0.16);
}

/* The marker's own node, on the line like an entry's, centred in the band. */
.ld-tl-marker.is-hiatus .ld-tl-node {
  left: calc(3.435rem + var(--ld-page-pad));
  top: 50%;
  transform: translateY(-50%);
}

/* What a marker says, beside its own label. */
.ld-tl-aside {
  font-size: 1.05rem;
  font-weight: 600;
  opacity: 0.85;
}

/* Now, at the top: the only marker that is about the present tense.

   One word, so it sits on the axis the years and months share and needs
   nothing said about its width. It was a sentence, which had to wrap to three
   lines to fit there; "Now" is what it meant. */
.ld-tl-marker.is-now .ld-tl-badge {
  background: rgba(139, 92, 246, 0.18);
  border-color: rgba(109, 40, 217, 0.3);
}
[data-bs-theme="dark"] .ld-tl-marker.is-now .ld-tl-badge { background: rgba(196, 181, 253, 0.2); }


/* The start page
   ------------------------------------------------------------------------
   The one page with a hero, and it earns it by being the page with nothing
   else to say. Every other page arrives with a subject already. */
.ld-home {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: var(--ld-shell-measure);
  margin: 0 auto;
}
.ld-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--ld-spacing-lg) var(--ld-spacing-md) 0;
  /* The frame the drift wall is positioned against, and the stacking context
     that lets it sit at z-index -1 without falling behind the page. */
  position: relative;
  isolation: isolate;
}
/* Named for the hero, not `ld-icon`: that class is the inline icon a heading
   carries, and a page-local rule redefining it at 120px would resize every one
   of those on whatever page it was loaded. */
/* Still, and it is the only mark on the site that is. The float is the
   brand's, and everywhere else it is the one thing moving on the page. Here
   the wall behind it is already drifting, so the icon bobbing on top of it
   was a second motion competing with the first over the same few hundred
   pixels. The dots under the badge row came off this hero for the same
   reason, and both keep their float everywhere else. */
.ld-hero-icon {
  width: 120px;
  height: 120px;
  border-radius: 26px;
  box-shadow: 0 18px 40px rgba(91, 33, 182, 0.25);
}
[data-bs-theme="dark"] .ld-hero-icon { box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55); }

.ld-hero h1 {
  margin: var(--ld-spacing-lg) 0 0;
  font-size: clamp(2.25rem, 7vw, 3.25rem);
  font-weight: var(--ld-typography-weights-bold);
  letter-spacing: var(--ld-typography-tracking-headings);
}
/* The hook, set large: it is the one line the page has to land. */
.ld-hero-hook {
  margin: var(--ld-spacing-lg) 0 0;
  max-width: 30rem;
  font-size: 1.2rem;
  line-height: 1.5;
}
.ld-hero-hook strong { font-weight: var(--ld-typography-weights-bold); }
.ld-hero-lede {
  margin: var(--ld-spacing-sm) 0 0;
  max-width: 28rem;
  opacity: 0.78;
}

/* The badge row, wherever it is placed. Carries the dots the download rows
   carry, for the same reason: this is the thing the page is for. */
.ld-badges {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--ld-spacing-sm);
  margin: var(--ld-spacing-xl) 0 var(--ld-spacing-4xl);
}
.ld-badges img { height: 2.75rem; width: auto; max-width: none; border-radius: 0; }
.ld-badges a { line-height: 0; }
.ld-badges a:hover { box-shadow: none; transform: translateY(-2px); }
.ld-badges a { transition: transform 120ms ease; }
.ld-badges::before {
  content: "";
  position: absolute;
  width: 75%;
  left: 0;
  right: 0;
  margin-inline: auto;
  top: calc(100% - 0.5rem);
  aspect-ratio: 53.66 / 6.16;
  background: url("/img/dots.svg") center / contain no-repeat;
  opacity: 0.85;
  pointer-events: none;
  will-change: transform;
  animation: app-icon-float 3.6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .ld-badges::before { animation: none; } }

/* Except on the start page, where the drift wall is already behind them.
   The dots exist to give the badge row some life on a page that has none, and
   the wall does that job; leaving both in put three things bobbing in the same
   stretch of screen, the app icon on the same keyframe as the dots. Scoped to
   the hero rather than dropped, because /download has no wall behind it. */
.ld-hero .ld-badges::before { content: none; }

/* The drift wall
   ------------------------------------------------------------------------
   The start page's hero sits on it: ten columns of photographs, half drifting
   up and half down, at ten durations that never come back into step.

   After React Bits' Drift Wall, with two of its moves dropped. That component
   tilts the whole plane in 3D and lifts whichever tile is under the cursor;
   this does neither, because a hero is looked at rather than used and a
   photograph that answers the pointer invites a click that goes nowhere.

   Dropping the interaction is what removes the JavaScript with it. Each
   column carries its photographs three times and animates by exactly one
   copy, so the loop closes on itself; nothing measures anything, and the
   whole thing is ten composited layers moving at a constant rate. */
.ld-wall {
  /* Capped, because the tiles are drawn from the 320px variant: past about
     11rem a high density screen would be asking for pixels that are not in
     the file. The wall's own width is capped to match, so on a screen wider
     than the columns can fill it fades inside itself rather than ending at a
     line somewhere in the middle of the page. */
  /* Seven columns of these, and the three numbers answer to each other.

     The wall has to be at least as wide as the viewport or its own straight
     edge shows inside the part of the mask that is still opaque, so the
     middle term is at least `100vw / 7`. `.ld-wall` is capped at 7 x the
     ceiling for the same reason, so on a screen wider than the columns can
     fill it fades inside itself rather than stopping at a line.

     The ceiling is a judgement rather than a constraint. These are drawn from
     the 320px variant, so at 16rem a two times screen is being asked for
     about a third again the pixels the file holds. `medium` is the next one up
     and it is 68KB a picture against 11KB, which is 1.7MB of wall on the page
     most people arrive at. Soft loses to that: a tile is at 42% opacity,
     under a mask, and never still. */
  --ld-wall-col: clamp(8.5rem, 14.5vw, 14rem);
  --ld-wall-gap: 0.7rem;
  /* How much of the centre hole is fully clear. Light needs the ground to
     carry down past the store badges, which are dark artwork and were sitting
     in the falloff with photographs coming through behind them. Dark does
     not: the tiles are already at a third of their opacity against a near
     black page, so there is ground behind the badges either way, and holding
     the hole open there takes the wall away for nothing. */
  --ld-wall-clear: 32%;

  position: absolute;
  /* Up past the header to the very top of the page, so the wall is the whole
     of what is behind the nav rather than a band starting under it.

     A flat number and deliberately a generous one: the header's height is not
     knowable from in here, it wraps to two rows on a narrow screen, and
     overshooting costs nothing. The part above the document's own top edge is
     never painted and cannot be scrolled to. */
  top: -9rem;
  bottom: 0;
  left: 50%;
  width: min(100vw, 98rem);
  transform: translateX(-50%);
  overflow: hidden;
  /* Behind the hero's own children, which need no z-index of their own for
     it: .ld-hero isolates, so this cannot fall behind the page either. */
  z-index: -1;
  pointer-events: none;

  /* Two masks, intersected, which is the technique the original uses.

     The first is its vignette: the wall fades out before it reaches an edge,
     so it ends in the page's own gradient rather than at a border. Its centre
     sits near the top rather than in the middle, so the wall is solid from the
     first pixel of the page and fades downward and into the corners. Centred,
     as the original has it, it left a band of bare gradient above the nav.

     The second is centred low for the same reason: it belongs over the words,
     and the words start a header's height down a box that now begins at the
     top of the page. How much of it is fully clear is `--ld-wall-clear`,
     which is the one number here that differs by theme; the reason is on
     the declaration.

     The second is a hole under the middle, where the words are. A scrim would
     have to match a gradient that changes down the page and across it;
     fading the photographs away instead cannot get that colour wrong. */
  -webkit-mask-image:
    radial-gradient(ellipse 78% 100% at 50% 8%, #000 38%, transparent 100%),
    radial-gradient(ellipse 44% 46% at 50% 58%, transparent var(--ld-wall-clear), #000 100%);
  -webkit-mask-composite: source-in;
  mask-image:
    radial-gradient(ellipse 78% 100% at 50% 8%, #000 38%, transparent 100%),
    radial-gradient(ellipse 44% 46% at 50% 58%, transparent var(--ld-wall-clear), #000 100%);
  mask-composite: intersect;
}

/* Centred and free to overflow. The columns are a fixed width, so on a narrow
   screen there are more of them than fit and the ones at the ends run off the
   sides into the part of the mask that is already transparent. That is the
   whole of the responsive behaviour: no breakpoint decides a column count. */
.ld-wall-plane {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: center;
}
.ld-wall-col {
  flex: 0 0 auto;
  width: var(--ld-wall-col);
}
.ld-wall-track {
  display: flex;
  flex-direction: column;
  will-change: transform;
  animation: ld-wall-drift linear infinite;
}
/* Height from the column width rather than an aspect ratio on the image, so
   one copy is exactly `photographs x height` and the keyframe below can be a
   flat third with nothing to measure. The gap is padding inside the tile for
   the same reason: a flex gap would sit between copies too and the loop would
   drift by that much every time round. */
.ld-wall-tile {
  display: block;
  flex: 0 0 auto;
  height: calc(var(--ld-wall-col) * 2 / 3);
  padding: calc(var(--ld-wall-gap) / 2);
}
.ld-wall-tile img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  border-radius: var(--ld-radius-input);
  /* Dimmed rather than covered with an overlay: at this opacity the page's
     own gradient comes through every photograph, which is what keeps a wall
     of other people's holidays reading as our page. */
  opacity: 0.42;
  filter: saturate(0.9);
  -webkit-user-drag: none;
  user-select: none;
}
[data-bs-theme="dark"] .ld-wall { --ld-wall-clear: 10%; }
[data-bs-theme="dark"] .ld-wall-tile img { opacity: 0.32; }

/* One copy of the three. */
@keyframes ld-wall-drift {
  to { transform: translate3d(0, -33.3333%, 0); }
}

/* Alternating direction, and seven durations with no common factor worth
   noticing, so the wall never falls into a pattern. The negative delays start
   each column part way through its own loop, which is what stops all ten
   from lining up on the first frame. */
.ld-wall-col:nth-child(even) .ld-wall-track { animation-direction: reverse; }
.ld-wall-col:nth-child(1) .ld-wall-track { animation-duration: 62s; animation-delay: -11s; }
.ld-wall-col:nth-child(2) .ld-wall-track { animation-duration: 48s; animation-delay: -37s; }
.ld-wall-col:nth-child(3) .ld-wall-track { animation-duration: 74s; animation-delay: -5s; }
.ld-wall-col:nth-child(4) .ld-wall-track { animation-duration: 55s; animation-delay: -52s; }
.ld-wall-col:nth-child(5) .ld-wall-track { animation-duration: 41s; animation-delay: -23s; }
.ld-wall-col:nth-child(6) .ld-wall-track { animation-duration: 68s; animation-delay: -44s; }
.ld-wall-col:nth-child(7) .ld-wall-track { animation-duration: 51s; animation-delay: -17s; }

/* Still a wall, just a still one, and still staggered.
   ------------------------------------------------------------------------
   Stopping the animation is not enough on its own. The keyframe sets no
   fill mode, so a track that has finished reverts to its base style, which
   carries no transform: every column lands on the same line and the wall
   becomes a plain grid, which is the one arrangement it never has while it
   is moving.

   So every second column is nudged down by **half a tile**. Half a copy would
   have been the obvious number and is exactly wrong: a copy is six whole
   tiles, so shifting by three of them shows different photographs in the same
   places and the rows line up again. A fraction of a tile is what breaks the
   line. A track holds three copies of six, so one tile is 5.5556% of it and
   half of one is 2.7778%.

   Both ways of asking for less motion get it: the reader's own switch, and
   the operating system's. */
.ld-wall-col:nth-child(even) .ld-wall-track { --ld-wall-still: -2.7778%; }

@media (prefers-reduced-motion: reduce) {
  .ld-wall-track {
    animation: none;
    transform: translate3d(0, var(--ld-wall-still, 0), 0);
  }
}
:root[data-ld-motion="reduced"] .ld-wall-track {
  transform: translate3d(0, var(--ld-wall-still, 0), 0);
}

/* The block the row writes its photographs in. It draws nothing where it
   stands: HomeController lifts the pictures out of the rendered body and
   hands them to the hero, and this is what is left behind if the marker is
   ever written on a page that has no hero to lift them into. */
.ld-wall-photos { display: none; }


/* A page beside a picture
   ------------------------------------------------------------------------
   `::split::` on /about: the prose in one column and the carousel it holds in
   the other. Two tracks rather than a float, because a float lets the text run
   underneath the moment it is a paragraph longer than the picture, and what
   this arrangement is for is the two reading side by side.

   The aside gets a track wide enough for the front card to be worth looking
   at, which is the whole reason the carousel replaced a strip of thumbnails:
   the picture has to answer the question by itself, without a lightbox. */
.ld-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 26rem);
  gap: var(--ld-spacing-xl);
  /* Both columns start at the top. Centred, the words floated against a
     picture that is taller than they are, which reads as one of the two
     having slipped. */
  align-items: start;
  margin-top: var(--ld-spacing-xl);
}
.ld-prose > .ld-split { max-width: none; }

/* `.ld-prose > *` sets the rhythm and the measure, and it only reaches direct
   children, so a column has to carry both again for what is now a level
   further in. */
.ld-split-body > * {
  margin-top: var(--ld-spacing-lg);
  margin-bottom: 0;
  max-width: var(--ld-measure-prose);
}
.ld-split-body > *:first-child { margin-top: 0; }

/* One column under the other, with the picture first: on a phone the thing
   worth seeing should not be four paragraphs down. */
@media (max-width: 56rem) {
  .ld-split {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--ld-spacing-lg);
  }
  .ld-split > .ld-carousel { order: -1; }
}


/* The depth carousel
   ------------------------------------------------------------------------
   The screenshots on /about, as a stack receding to one side. After React
   Bits' Depth Carousel, and the shape is theirs: each card sits at
   `translateZ(-depth * d)` and `translateX(spread * d)` from the front one,
   turned about Y as it goes back, dimmed and blurred with depth, and the one
   in front of the stack fades as it leaves.

   It replaced a strip that drifted past on a timer. A strip is a thing you
   watch and this is a thing you look through, which is what the screenshots
   wanted: seven phone screens are worth one at a time and large, not seven at
   a glance and small. That is also why nothing here opens a lightbox any
   more. The front card is the size the picture wanted to be.

   No GSAP, and no library at all. What the original needs one for is tweening
   a single number, which is a `requestAnimationFrame` and a cubic.

   **The geometry is derived from the measured card**, not written here as
   pixels: spread and depth are fractions of the card's own width, so the
   stack holds its proportions at every breakpoint and a card size is the only
   thing this stylesheet has to decide. */
.ld-carousel {
  --ld-carousel-card: 14rem;
}

/* Before the script runs, and if it never does: the pictures as they came out
   of the body, at a size worth looking at, each still opening in the
   lightbox. The stack is an enhancement over this and not a replacement for
   it. */
.ld-carousel p {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ld-spacing-sm);
  justify-content: center;
  margin: 0;
}
.ld-carousel img {
  width: var(--ld-carousel-card);
  max-width: 100%;
  height: auto;
  border-radius: var(--ld-radius-input);
}
/* Markdown puts a soft break between images written on their own lines, and
   in a flex row every one of those is an item that pushes the next picture on
   to a line of its own. */
.ld-carousel p > br { display: none; }

/* Once it has. */
.ld-carousel.is-live {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ld-spacing-md);
}
.ld-carousel-stage {
  position: relative;
  width: 100%;
  /* The card's own aspect, plus the room the stack leans into. Set from the
     card width so the stage cannot disagree with what it holds. */
  height: calc(var(--ld-carousel-card) * 1092 / 502);
  perspective: 1200px;
  perspective-origin: 50% 50%;
  transform-style: preserve-3d;
  touch-action: pan-y;
  cursor: grab;
  outline: none;
  user-select: none;
  -webkit-user-select: none;
}
.ld-carousel-stage.is-grabbing { cursor: grabbing; }
.ld-carousel-stage:focus-visible {
  outline: 2px solid #6d28d9;
  outline-offset: 6px;
  border-radius: var(--ld-radius-input);
}
[data-bs-theme="dark"] .ld-carousel-stage:focus-visible { outline-color: #c4b5fd; }

/* No radius, no clip and no box-shadow, and that is the point.

   The screenshots are rounded rectangles on transparency, cornered at about
   70px in a 502px wide file, which is near 30px at the size a card is drawn.
   A card that rounds itself to `--ld-radius-input` is a second, tighter shape
   over the top of that one: the page showed through the artwork's rounder
   corner inside the card's, and the box-shadow traced the card's rectangle
   rather than the picture in it. It read as a notch at each bottom corner,
   and only in the light theme, where the page behind is bright enough to see.

   So the artwork is the shape. Nothing here draws a frame for it, and the
   depth shadow moves to `drop-shadow` on the picture, which follows an alpha
   channel where `box-shadow` can only follow a box. */
.ld-carousel-card {
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--ld-carousel-card);
  aspect-ratio: 502 / 1092;
  margin: 0;
  transform-origin: center center;
  will-change: transform, opacity, filter;
}
.ld-carousel-card img {
  width: 100%;
  height: 100%;
  /* `.ld-prose img` caps a picture at 22rem so a portrait shot cannot take a
     whole screen in a body of text. A card here is taller than that and is
     already the size the page chose, so the cap has to be lifted or the
     picture stops short of the frame it is in. */
  max-height: none;
  /* `contain` rather than `cover`: the card is given the artwork's own
     aspect, so the two agree today, and a screenshot at some other ratio
     should letterbox rather than lose its edges to a crop. */
  object-fit: contain;
  display: block;
  /* The artwork brings its own corners. A radius here would cut into them. */
  border-radius: 0;
  filter: drop-shadow(0 18px 26px rgba(91, 33, 182, 0.4));
  pointer-events: none;
  -webkit-user-drag: none;
}
[data-bs-theme="dark"] .ld-carousel-card img {
  filter: drop-shadow(0 18px 26px rgba(0, 0, 0, 0.6));
}
/* A card behind the front one recedes by going darker and less saturated,
   which is a filter and so respects the picture's alpha. It used to be an
   overlay element laid across the card, and an overlay is a rectangle: on
   artwork this round it painted over the corners the picture does not have.
   Written by the script, since it varies with depth. */

/* What the picture is. The old strip had no room to say it and the lightbox
   said it only once you had opened one. */
.ld-carousel-caption {
  margin: 0;
  font-weight: var(--ld-typography-weights-bold);
  text-align: center;
  min-height: 1.5em;
}

.ld-carousel-controls {
  display: flex;
  align-items: center;
  gap: var(--ld-spacing-sm);
}
/* Under the stage rather than over it. The stack leans right, so an arrow
   pinned inside the frame sits on top of the cards it is there to reach. */
.ld-carousel-step {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border-radius: 999px;
  border: 1px solid rgba(109, 40, 217, 0.35);
  background: transparent;
  color: inherit;
  cursor: pointer;
  transition: background-color 120ms ease, border-color 120ms ease;
}
.ld-carousel-step:hover,
.ld-carousel-step:focus-visible {
  background: rgba(109, 40, 217, 0.1);
  border-color: rgba(109, 40, 217, 0.55);
}
[data-bs-theme="dark"] .ld-carousel-step { border-color: rgba(196, 181, 253, 0.4); }
[data-bs-theme="dark"] .ld-carousel-step:hover,
[data-bs-theme="dark"] .ld-carousel-step:focus-visible {
  background: rgba(196, 181, 253, 0.16);
  border-color: rgba(196, 181, 253, 0.6);
}
.ld-carousel-step svg { width: 1.1rem; height: 1.1rem; }

.ld-carousel-dots {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.ld-carousel-dot {
  width: 0.45rem;
  height: 0.45rem;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: rgba(109, 40, 217, 0.3);
  cursor: pointer;
  transition: width 200ms ease, background-color 200ms ease;
}
.ld-carousel-dot.is-active {
  width: 1.3rem;
  background: #6d28d9;
}
[data-bs-theme="dark"] .ld-carousel-dot { background: rgba(196, 181, 253, 0.32); }
[data-bs-theme="dark"] .ld-carousel-dot.is-active { background: #c4b5fd; }

@media (max-width: 56rem) {
  .ld-carousel { --ld-carousel-card: 12rem; }
}


/* The palette on /brand
   ------------------------------------------------------------------------
   A row per colour, each showing both themes, because every value here is a
   pair and showing one of them is showing half the answer. The chips are set
   inline from the tokens rather than from a class per colour: the page is
   generated from the same file the stylesheet is, so a new token appears here
   without a rule being written for it. */
.ld-palette {
  display: grid;
  gap: var(--ld-spacing-md);
}
.ld-swatch {
  display: flex;
  gap: var(--ld-spacing-md);
  align-items: flex-start;
  padding: var(--ld-spacing-md);
  border-radius: var(--ld-radius-card);
  background: rgba(255, 255, 255, 0.34);
  border: 1px solid rgba(255, 255, 255, 0.42);
}
[data-bs-theme="dark"] .ld-swatch {
  background: rgba(0, 0, 0, 0.22);
  border-color: rgba(255, 255, 255, 0.07);
}
/* The two halves meet, so the pair reads as one object rather than as two
   samples that happen to be adjacent. */
.ld-swatch-chips {
  display: flex;
  flex: none;
  border-radius: var(--ld-radius-input);
  overflow: hidden;
  border: 1px solid rgba(0, 0, 0, 0.12);
}
[data-bs-theme="dark"] .ld-swatch-chips { border-color: rgba(255, 255, 255, 0.16); }
.ld-swatch-chip { display: block; width: 2.75rem; height: 3.5rem; }
.ld-swatch-body { min-width: 0; }
.ld-prose .ld-swatch-body h3 { margin: 0 0 var(--ld-spacing-3xs); font-size: 1rem; }
.ld-prose .ld-swatch-note { margin: 0; font-size: 0.9rem; opacity: 0.78; }
.ld-prose .ld-swatch-values {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ld-spacing-xs) var(--ld-spacing-md);
  margin: var(--ld-spacing-sm) 0 0;
  font-size: 0.85rem;
}
.ld-swatch-values em { font-style: normal; opacity: 0.6; margin-right: 0.35em; }
.ld-prose > .ld-palette { max-width: none; margin-top: var(--ld-spacing-lg); }


/* A notice
   ------------------------------------------------------------------------
   A real one, and not a panel pretending: it is tinted, it is the first thing
   on the page it appears on, and it says something with a date on it rather
   than something the page is about.

   It wears the brand rather than a generic tint. The gradient is the site's
   own two colours at low strength, the dots drift off the right edge behind
   the words, and a violet rule down the left says "read this first" without
   spending a word or an icon on it. The dots are clipped by the corner radius
   rather than sized to fit, which is what keeps them reading as a texture the
   panel was cut from.

   Not dismissible. A notice worth dismissing is worth a control and a place
   to remember the dismissal, and this one is two sentences that go away when
   they stop being true. */
.ld-notice {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  padding: var(--ld-spacing-lg) var(--ld-spacing-xl);
  border-left: 3px solid #6d28d9;
  border-radius: var(--ld-radius-card);
  /* White, and the reason is what it sits on. The page's own background is
     `#fee368` to `#ae92f4`, and this wash used to be those same two colours:
     the page tinted with the page, which at any alpha is a difference in
     degree rather than in kind. Lightening instead is a difference in kind,
     and it is the move `.ld-page` already makes to lift a card off the same
     ground. The hues stay as a warm end and a cool end so the brand is still
     in it, and 245deg runs them across the page's 135 rather than along it.

     Dark has none of this problem. There the bright pair lands on a near
     black page and lifts off it at a third of the alpha, so it keeps them. */
  /* 245deg and not 115: the mirror of it, so the wash runs across the page's
     own rather than along it. The ground is 135deg from yellow to violet, and
     at 115 each end of this sat on the page's matching hue, which is most of
     what made it read as more of the same wash instead of a thing on top of
     it. The dark theme keeps 115, where the page is near black at both ends
     and the direction is not something anybody can see. */
  background:
    linear-gradient(245deg, rgba(255, 252, 235, 0.82), rgba(255, 255, 255, 0.74) 62%, rgba(243, 240, 255, 0.66));
  box-shadow: 0 10px 34px rgba(91, 33, 182, 0.14);
}
[data-bs-theme="dark"] .ld-notice {
  border-left-color: #c4b5fd;
  background:
    linear-gradient(115deg, rgba(254, 227, 104, 0.12), rgba(174, 146, 244, 0.16) 62%, rgba(174, 146, 244, 0.06));
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.28);
}
/* Behind the words and off the edge, so it reads as texture rather than as a
   picture somebody placed. */
.ld-notice::after {
  content: "";
  position: absolute;
  z-index: -1;
  right: -6%;
  bottom: -38%;
  width: 58%;
  aspect-ratio: 53.66 / 6.16;
  background: url("/img/dots.svg") center / contain no-repeat;
  opacity: 0.5;
  pointer-events: none;
}
[data-bs-theme="dark"] .ld-notice::after { opacity: 0.38; }

/* The first line is the headline: it carries the news and the rest explains
   it, so it is set larger rather than merely bold. */
.ld-prose .ld-notice > :first-child { margin-top: 0; }
.ld-prose .ld-notice > * { margin-top: var(--ld-spacing-xs); }
.ld-prose .ld-notice { max-width: none; }
/* A notice is a section of its own rather than another paragraph, and on
   /about it lands under the carousel's controls, where a paragraph's gap read
   as the box having slipped up against them. */
.ld-prose > .ld-notice { margin-top: var(--ld-spacing-2xl); }

/* A notice standing in a row takes the panels' inset rather than its own.
   Its wider one is for a full width band, where the extra room is what stops
   a line running the whole way across; beside a panel it only means the two
   boxes start their headings eight pixels apart, which reads as a mistake
   rather than as the one of them being the highlight. The wash, the border
   and the dots are doing that job already. */
.ld-prose .ld-row > .ld-notice { padding: var(--ld-spacing-lg); }
.ld-prose .ld-notice > h2 {
  display: flex;
  align-items: center;
  margin: 0 0 var(--ld-spacing-2xs);
  font-size: 1.1rem;
}
.ld-prose .ld-notice > h2 .ld-icon,
.ld-notice strong .ld-icon {
  width: 1.35em;
  height: 1.35em;
  margin-right: 0.5em;
  vertical-align: baseline;
  opacity: 1;
  color: #6d28d9;
}
[data-bs-theme="dark"] .ld-prose .ld-notice > h2 .ld-icon,
[data-bs-theme="dark"] .ld-notice strong .ld-icon { color: #c4b5fd; }

.ld-notice strong {
  display: flex;
  align-items: center;
  margin-bottom: var(--ld-spacing-2xs);
  font-size: 1.1rem;
  font-weight: 700;
  letter-spacing: var(--ld-typography-tracking-headings);
}
/* Larger than a heading's icon and in the brand's violet rather than the
   text's colour: this one is the focal point of the block, not a label
   beside a word. Said for both shapes a notice's headline takes, a bold
   first line or a real heading, above. */
.ld-notice p { line-height: 1.55; }


/* A row of panels
   ------------------------------------------------------------------------
   The two-column shape the old site used where two things are alternatives to
   each other: install on a computer beside install on a phone. Below the
   breakpoint they stack, which is the reading order they were written in. */
.ld-row {
  display: grid;
  gap: var(--ld-spacing-md);
  align-items: start;
}
/* As many columns as the panels need, rather than always two: the mark
   families on /brand are three, and the install instructions are two.
   `auto-fit` collapses the tracks nothing lands in, so a pair still splits the
   width in half.

   `minmax(0, ...)` on the inside and not a bare `1fr`, which is
   `minmax(auto, 1fr)`: a column whose content has a wide minimum, a code block
   or a long URL, refuses to go below it and takes its share out of the others.
   That is what left the install column two words wide beside the phone
   column's snippet. `min(18rem, 100%)` keeps the floor from exceeding a narrow
   screen, which would overflow it. */
.ld-row { grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr)); }
/* Same reason, one level in: a grid item's default `min-width: auto` is what
   lets its content push past the track it was given. */
.ld-row > * { min-width: 0; }
/* A picture in a panel is a specimen, not a hero. */
.ld-prose .ld-panel img { max-width: 8rem; height: auto; }

/* The brand marks get a width and not only a cap.
   `max-width` leaves an image at its intrinsic size when that is smaller, and
   these files do not agree about theirs: the plated variants declare 512px
   while the transparent one is cropped to its artwork and declares 54, so it
   drew at a quarter the size of the ones beside it. A specimen is shown at a
   size the page chooses. */
.ld-prose img[src^="/get/brand/"] {
  width: 8rem;
  max-width: 100%;
  height: auto;
}
.ld-prose > .ld-row { max-width: none; margin-top: var(--ld-spacing-lg); }
.ld-prose .ld-row > .ld-panel { margin-top: 0; }


/* The bookmarklet's drag target
   ------------------------------------------------------------------------
   The one genuinely interactive thing on the old site, and the only control
   on this page: you install it by dragging it, so it wears the dots the
   download buttons wear and is the thing the page is arranged around. */
/* The surface, the border and the dots come from `.ld-notice`, which this
   also wears: the drag target is the one thing the page exists for, and the
   quiet glass panel it had read as one more section. Only what the notice has
   no opinion about is set here. */
.ld-bookmarklet {
  text-align: center;
  padding: var(--ld-spacing-xl) var(--ld-spacing-lg);
}
.ld-prose .ld-bookmarklet > * { max-width: var(--ld-measure-prose); margin-inline: auto; }
/* Written against the parent rather than on the class alone, because
   `.ld-prose .ld-notice > *` sets a margin on every child of a notice and this
   is one now: at one class each these lost the spacing they were given and
   the three lines closed up. */
/* Longhands, never the `margin` shorthand. The rule above centres these with
   `margin-inline: auto`, and a shorthand here resets that to 0: the block
   keeps its 44rem measure and sits at the left of the panel while the pill,
   being inline, stays centred. Two things centred on different axes. */
.ld-bookmarklet > .ld-bookmarklet-lead {
  margin-top: 0;
  margin-bottom: var(--ld-spacing-md);
  font-weight: 700;
}
.ld-bookmarklet > .ld-bookmarklet-tip {
  margin-top: var(--ld-spacing-md);
  margin-bottom: 0;
  font-size: 0.85rem;
  opacity: 0.75;
}

/* `grab`, because the gesture is the installation. A pointer would suggest
   clicking it does something, and clicking it on this page files this page. */
.ld-bookmarklet-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--ld-spacing-xs);
  padding: 0.7rem 1.5rem;
  border-radius: 999px;
  background: #6d28d9;
  color: #fff;
  font-weight: 700;
  text-decoration: none;
  cursor: grab;
  box-shadow: 0 10px 24px rgba(109, 40, 217, 0.35);
  transition: transform 120ms ease, box-shadow 120ms ease;
}
.ld-bookmarklet-pill:hover {
  color: #fff;
  transform: translateY(-1px);
  box-shadow: 0 14px 30px rgba(109, 40, 217, 0.45);
}
.ld-bookmarklet-pill:active { cursor: grabbing; }
/* `.ld-prose a` sets `color: inherit`, which at one class and one element
   outranks the pill's own single class: the button has been taking the body's
   text colour all along, which in the light theme is a dark violet on a
   violet button. Said again here, where it wins. */
.ld-prose .ld-bookmarklet-pill,
.ld-prose .ld-bookmarklet-pill:hover,
.ld-prose .ld-bookmarklet-pill:focus { color: #fff; text-decoration: none; }
[data-bs-theme="dark"] .ld-bookmarklet-pill { background: #7c3aed; }

/* The snippet, and the button under it. The block breaks anywhere, because it
   is one long unbroken token and a reader has to be able to see the whole of
   it to check what they are pasting. */
.ld-snippet { display: flex; flex-direction: column; align-items: flex-start; }
.ld-snippet-copy {
  margin-top: var(--ld-spacing-sm);
  padding: 0.45rem 0.9rem;
  border-radius: 999px;
  border: 1px solid rgba(109, 40, 217, 0.25);
  background: rgba(255, 255, 255, 0.5);
  color: inherit;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 120ms ease;
}
.ld-snippet-copy:hover { background: rgba(139, 92, 246, 0.16); }
[data-bs-theme="dark"] .ld-snippet-copy {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(196, 181, 253, 0.28);
}
[data-bs-theme="dark"] .ld-snippet-copy:hover { background: rgba(196, 181, 253, 0.2); }
.ld-prose .ld-snippet { max-width: none; }
.ld-prose .ld-snippet > .ld-bookmarklet-code { width: 100%; margin-top: 0; }

.ld-prose .ld-bookmarklet-code,
.ld-prose .ld-panel > .ld-bookmarklet-code,
.ld-prose .ld-bookmarklet-code code {
  max-width: none;
  white-space: pre-wrap;
  /* `anywhere` rather than `break-all`, because it also lets the box report a
     min-content width of one character. `break-all` wraps the text but leaves
     the box demanding the width of the longest run, which is what pushed the
     column out. */
  overflow-wrap: anywhere;
  word-break: normal;
  /* Nothing left to scroll once it wraps, and a scrollbar under a block a
     reader is meant to select whole is a way to lose half of it. */
  overflow-x: visible;
}


/* A panel a page can put round its own writing
   ------------------------------------------------------------------------
   Placed with `::panel::` and `::/panel::`, so the heading, the line and the
   link inside stay markdown in a row and only the box comes from here. The
   same surface the product cards wear, which is what makes a page of them
   read as one thing.

   Children get their spacing here rather than from `.ld-prose > *`, which
   only reaches the prose's own children and stops at the panel. */
.ld-panel {
  padding: var(--ld-spacing-lg);
  border-radius: var(--ld-radius-card);
  background: rgba(255, 255, 255, 0.34);
  border: 1px solid rgba(255, 255, 255, 0.42);
}
[data-bs-theme="dark"] .ld-panel {
  background: rgba(0, 0, 0, 0.22);
  border-color: rgba(255, 255, 255, 0.07);
}
.ld-prose .ld-panel { margin-top: var(--ld-spacing-md); }
.ld-prose .ld-panel > * { margin-top: var(--ld-spacing-sm); margin-bottom: 0; }
.ld-prose .ld-panel > :first-child { margin-top: 0; }
/* A heading opens a section and wants the break the prose rules would have
   given it. The rule above is one specificity step above `.ld-prose h3`, so
   without this every heading in a panel sits at paragraph spacing. */
.ld-prose .ld-panel > h2,
.ld-prose .ld-panel > h3 { margin-top: var(--ld-spacing-xl); }
.ld-prose .ld-panel > h2:first-child,
.ld-prose .ld-panel > h3:first-child { margin-top: 0; }


/* Product cards in a page
   ------------------------------------------------------------------------
   The same card the switcher uses, laid out down the page rather than down a
   panel, and wearing the stores' own artwork: on a page whose job is getting
   the app, a badge is what a reader is looking for.

   Two columns wherever there is room for them, because the companions are a
   pair and a pair reads as a set side by side and as a queue stacked. */
.ld-products {
  display: grid;
  gap: var(--ld-spacing-md);
  margin-top: var(--ld-spacing-lg);
}
@media (min-width: 720px) {
  .ld-products { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.ld-products > * { min-width: 0; }
/* On the gradient rather than inside a sheet, so it wants the page panel's
   own surface rather than the sheet's lighter fill. */
.ld-products .ld-product {
  background: rgba(255, 255, 255, 0.34);
  border: 1px solid rgba(255, 255, 255, 0.42);
}
[data-bs-theme="dark"] .ld-products .ld-product {
  background: rgba(0, 0, 0, 0.22);
  border-color: rgba(255, 255, 255, 0.07);
}
/* Centred across the card rather than tucked under the text: they are what
   the card is for, and the head above them is a caption. */
.ld-product-badges {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--ld-spacing-xs);
  margin: 0;
}
/* Ours, in the sheet: the one the site you are on is about.

   The wash goes on the other two, not on this one. The other way round read
   backwards: a pale gradient behind ours looked like a card that had been
   faded out, while the plain solid plates beside it looked like the
   substantial ones. So the companions take the wash and recede, and ours
   keeps a solid plate and gains an edge.

   Only in the sheet. On `/download` the cards are the companions, and ours is
   the whole top of the page above them. */
.ld-sheet .ld-product.is-ours {
  background: rgba(255, 255, 255, 0.55);
  border: 1px solid rgba(109, 40, 217, 0.3);
}
[data-bs-theme="dark"] .ld-sheet .ld-product.is-ours {
  background: rgba(0, 0, 0, 0.42);
  border-color: rgba(196, 181, 253, 0.32);
}
.ld-sheet .ld-product.is-ours .ld-product-icon {
  box-shadow: 0 6px 18px rgba(91, 33, 182, 0.28);
}

/* The companions recede, and how you make something recede depends on what it
   is sitting on. The sheet's panel is the brand gradient, so in light a gold
   to lilac wash is that gradient over itself and reads as nothing at all; a
   thinner version of the plate ours wears is what steps back there. In dark
   the panel is nearly black and the wash is the thing that shows. */
.ld-sheet .ld-product:not(.is-ours) {
  background: rgba(255, 255, 255, 0.26);
}
[data-bs-theme="dark"] .ld-sheet .ld-product:not(.is-ours) {
  background: linear-gradient(135deg, rgba(254, 227, 104, 0.07), rgba(174, 146, 244, 0.12));
}

/* The head is a lockup rather than a row spanning the card, so it centres as
   one piece: the icon stays against its own words instead of drifting to the
   far edge of a card sized by the badges under it. */
.ld-products .ld-product-head {
  align-items: center;
  justify-content: center;
}
/* Centred in the card too. The two cards are the same height because the grid
   makes them so, and the shorter one's content sitting at the top of that
   height is the part that reads as a mistake. */
.ld-products .ld-product {
  justify-content: center;
  gap: var(--ld-spacing-lg);
  padding: var(--ld-spacing-lg);
}
.ld-product-badges a { line-height: 0; }
.ld-product-badges img { height: 2.1rem; width: auto; max-width: none; border-radius: 0; }


/* Sheets
   ------------------------------------------------------------------------
   A panel from the right on a wide screen and a sheet from the bottom on a
   phone, which is the shape the filter drawer already established here.

   There are two of them now, the apps and the navigation, so the shell is
   `ld-sheet` and what goes inside is each one's own business. A dialog, so
   the browser owns focus, inertness and Esc; the script adds only the
   opening, the backdrop click and the scroll lock. */
.ld-apps-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--ld-spacing-xs);
  flex: none;
  padding: 0.5rem 0.95rem;
  border-radius: 999px;
  border: 1px solid rgba(109, 40, 217, 0.25);
  background: rgba(255, 255, 255, 0.5);
  color: inherit;
  font: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 120ms ease, border-color 120ms ease, transform 120ms ease;
}
.ld-apps-trigger:hover { background: rgba(139, 92, 246, 0.16); transform: translateY(-1px); }
[data-bs-theme="dark"] .ld-apps-trigger {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(196, 181, 253, 0.28);
}
[data-bs-theme="dark"] .ld-apps-trigger:hover { background: rgba(196, 181, 253, 0.2); }

.ld-sheet {
  border: 0;
  padding: 0;
  background: transparent;
  color: inherit;
  max-width: 100vw;
  max-height: 100vh;
}
.ld-sheet::backdrop { background: rgba(20, 8, 37, 0.55); backdrop-filter: blur(4px); }

/* Wide: a column against the right edge, full height. */
@media (min-width: 640px) {
  .ld-sheet {
    margin: 0 0 0 auto;
    height: 100%;
    max-height: none;
    width: min(24rem, 100vw);
  }
  .ld-sheet-panel { height: 100%; border-radius: 0; }
}
/* Narrow: a sheet up from the bottom, never taller than most of the screen. */
@media (max-width: 639px) {
  .ld-sheet {
    margin: auto auto 0;
    width: 100%;
    max-height: 88vh;
  }
  .ld-sheet-panel { border-radius: var(--ld-radius-card) var(--ld-radius-card) 0 0; }
}

.ld-sheet-panel {
  display: flex;
  flex-direction: column;
  gap: var(--ld-spacing-lg);
  overflow-y: auto;
  /* The bottom padding also clears the phone's home indicator, which would
     otherwise sit over the sticky close. */
  padding: var(--ld-spacing-xl) var(--ld-spacing-lg)
           calc(var(--ld-spacing-xl) + env(safe-area-inset-bottom));
  background: linear-gradient(160deg, #fee368 0%, #ae92f4 100%);
  color: #3c1a8e;

  /* Thin and in the brand's colour rather than the platform's.

     `color-scheme` on :root already gets every scrolling surface here a
     scrollbar in the right theme, which is why nothing else declares one. A
     panel is a small surface though, and the default bar is wide enough on
     Windows to read as a second column beside the cards.

     Both spellings on purpose. Chrome and Firefox take the standard
     properties, and a browser that has them ignores the pseudo-elements
     entirely, so the second block is only ever reached by older WebKit. They
     are kept to the same two colours so the two cannot look like different
     designs. */
  scrollbar-width: thin;
  scrollbar-color: rgba(109, 40, 217, 0.35) transparent;
}
.ld-sheet-panel::-webkit-scrollbar { width: 8px; }
.ld-sheet-panel::-webkit-scrollbar-track { background: transparent; }
.ld-sheet-panel::-webkit-scrollbar-thumb {
  border-radius: 999px;
  /* The border is the gap between the thumb and the panel's edge, and
     `padding-box` is what keeps the fill from painting under it. */
  border: 2px solid transparent;
  background: rgba(109, 40, 217, 0.35);
  background-clip: padding-box;
}
[data-bs-theme="dark"] .ld-sheet-panel {
  scrollbar-color: rgba(196, 181, 253, 0.35) transparent;
}
[data-bs-theme="dark"] .ld-sheet-panel::-webkit-scrollbar-thumb {
  background: rgba(196, 181, 253, 0.35);
  background-clip: padding-box;
}
[data-bs-theme="dark"] .ld-sheet-panel {
  background: linear-gradient(160deg, #231a00 0%, #100825 100%);
  color: #ede9fe;
}
.ld-sheet-head h2 { margin: 0 0 var(--ld-spacing-2xs); font-size: 1.35rem; font-weight: 700; }
.ld-sheet-head p { margin: 0; opacity: 0.75; font-size: 0.9rem; line-height: 1.5; }

/* A product: the icon, the name, the line, and the ways to get it. One card
   in two places, the switcher and `/download`, so the two cannot come to
   disagree about what a product is called or what it says. */
.ld-product {
  display: flex;
  flex-direction: column;
  gap: var(--ld-spacing-md);
  padding: var(--ld-spacing-md);
  border-radius: var(--ld-radius-card);
  background: rgba(255, 255, 255, 0.4);
}
/* The icon beside the name and the line: one row saying what this app is. */
.ld-product-head {
  display: flex;
  gap: var(--ld-spacing-md);
}
[data-bs-theme="dark"] .ld-product { background: rgba(0, 0, 0, 0.25); }
.ld-product-icon { flex: none; width: 52px; height: 52px; border-radius: 14px; }
.ld-product-body { min-width: 0; }
.ld-product-body h3 { margin: 0 0 var(--ld-spacing-3xs); font-size: 1.05rem; font-weight: 700; }
.ld-product-body > p { margin: 0 0 var(--ld-spacing-sm); font-size: 0.85rem; line-height: 1.45; opacity: 0.8; }
/* Smaller than a page's call to action, and resting flat: a card this size
   with a shadow under its button reads as two raised things. */
.ld-product-launch { padding: 0.5rem 1.1rem; font-size: 0.85rem; box-shadow: none; }
/* Which is why the hover is said here. `.ld-cta:hover` lifts the button and
   swaps a resting shadow for a larger one, and this one has no resting shadow
   for that to play against, so the movement had almost nothing to read it by.

   Hover only. Focus gets a ring, not the lift: a dialog puts focus on
   something when it opens, and a lift under it reads as a button already
   being pressed rather than as one waiting to be. */
.ld-product-launch:hover {
  transform: translateY(-1px);
  box-shadow: 0 8px 18px rgba(109, 40, 217, 0.4);
}
[data-bs-theme="dark"] .ld-product-launch:hover {
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.45);
}
.ld-product-launch:focus-visible {
  outline: 2px solid #6d28d9;
  outline-offset: 2px;
}
[data-bs-theme="dark"] .ld-product-launch:focus-visible { outline-color: #c4b5fd; }

/* The panel takes focus when a sheet opens, so it must not draw a ring for it:
   nothing was clicked or tabbed to. */
.ld-sheet-panel:focus { outline: none; }
/* Ours is filled and the others outlined, which is how the old switcher said
   which site you were already on without spending a label on it. */
.ld-product-launch.is-quiet {
  background: transparent;
  color: inherit;
  border: 1px solid currentColor;
}
.ld-product-launch.is-quiet:hover { background: rgba(109, 40, 217, 0.12); color: inherit; }
[data-bs-theme="dark"] .ld-product-launch.is-quiet:hover { background: rgba(196, 181, 253, 0.18); }
/* Qualified with the parent, and that is not decoration: `.ld-product-body > p`
   above sets all four margins and outranks a bare class, so a plain
   `.ld-product-stores` loses its top margin and the links sit against the
   button. */
.ld-product-body > p.ld-product-stores {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ld-spacing-md);
  margin: var(--ld-spacing-md) 0 0;
  font-size: 0.8rem;
}
.ld-product-stores a { color: inherit; opacity: 0.75; }
.ld-product-stores a:hover { opacity: 1; }

/* Sticky rather than scrolled away, which is the filter drawer's answer to
   the same complaint: a panel you have to scroll to leave is the most common
   way this pattern goes wrong. That panel has no X anywhere on it, and this
   one follows.

   Quieter than the drawer's though, and full width rather than a pill in the
   middle. The drawer's foot is filled because it doubles as apply and the
   sentence on it says what leaving gets you. Nothing is applied here, so a
   filled button would be a primary action that is really only a dismissal. */
.ld-sheet-close {
  position: sticky;
  bottom: calc(-1 * var(--ld-spacing-xl));
  margin-top: auto;
  padding: 0.7rem 1.4rem;
  border-radius: 999px;
  border: 1px solid currentColor;
  background: rgba(255, 255, 255, 0.55);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  color: inherit;
  font: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
}
[data-bs-theme="dark"] .ld-sheet-close { background: rgba(20, 12, 45, 0.72); }
.ld-sheet-close:hover { background: rgba(109, 40, 217, 0.12); }
[data-bs-theme="dark"] .ld-sheet-close:hover { background: rgba(196, 181, 253, 0.18); }


/* The page panel
   ------------------------------------------------------------------------
   A quieter relative of `.ld-card`. That one was built for the auth screens,
   where a short form floating on the gradient needs a panel to hold it
   together, and at full page width its weight reads as a box drawn around the
   content rather than a surface under it.

   So this is the same idea at about half the strength: less fill, a softer
   blur, a hairline instead of an edge, and a shadow that lifts the page off
   the gradient without casting onto it. `.ld-card` keeps its own weight where
   it earns it, on the auth screens and the error pages.

   The padding is a custom property because two other things have to escape
   it exactly, and a second copy of the number is how they come to disagree. */
/* The surface, said once. The error card takes it too: it keeps `.ld-card`'s
   shape, being a short centred card and not a page, but it sits under the
   header now and the header wears this. Two panels a screen apart in two
   different glasses is the thing you notice. */
.ld-page,
.ld-card.ld-error-card {
  border-radius: var(--ld-radius-card);
  background: rgba(255, 255, 255, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.4);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  box-shadow: 0 10px 34px rgba(91, 33, 182, 0.06);
}
[data-bs-theme="dark"] .ld-page,
[data-bs-theme="dark"] .ld-card.ld-error-card {
  background: rgba(0, 0, 0, 0.18);
  border-color: rgba(255, 255, 255, 0.06);
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.22);
}
.ld-page {
  --ld-page-pad: var(--ld-spacing-xl);
  width: 100%;
  padding: var(--ld-page-pad);
}
/* The hero runs to the panel's own edges, so it escapes the padding on three
   sides and keeps only the top corners rounded: the bottom two sit against
   the prose rather than against the gradient. */
.ld-page > .ld-hero-figure {
  margin: calc(-1 * var(--ld-page-pad)) calc(-1 * var(--ld-page-pad)) var(--ld-spacing-xl);
  border-radius: var(--ld-radius-card) var(--ld-radius-card) 0 0;
}
/* Headings in a page are the page's own, so the card rule that flattens them
   to `margin: 0 0 .5rem` does not reach here. */
.ld-page > h1 {
  margin: 0 0 var(--ld-spacing-md);
  font-size: var(--ld-typography-scale-h1);
  font-weight: var(--ld-typography-weights-bold);
  letter-spacing: var(--ld-typography-tracking-headings);
}
.ld-page .ld-about-category {
  margin-top: var(--ld-spacing-4xl);
  margin-bottom: var(--ld-spacing-lg);
}
/* The first one used to carry an extra 4.75rem on top of that. /about's prose
   ended in a bare download row, and the dots behind it are a background layer
   with no height that hangs that far below the paragraph, so a plain section
   break put the heading inside them.

   The row is in a notice now, and a notice has `overflow: hidden`: the dots
   are clipped by the box rather than hanging out of the prose, and the row's
   bottom margin no longer collapses out of it either. Nothing is left to
   clear, and the allowance was showing as a hole in the page. */
