/* 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);
}

/* A table in a guide is a lookup, read across a row and down a column, so
   the rules run between rows and the answer columns sit centered under their
   headings. Every column after the first is an answer; the first is the
   question, and wraps rather than squeezing them on a phone. */
.ld-prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95em;
  line-height: 1.4;
}
.ld-prose th,
.ld-prose td {
  padding: var(--ld-spacing-xs) var(--ld-spacing-sm);
  border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
  text-align: left;
  vertical-align: top;
}
.ld-prose th {
  font-weight: var(--ld-typography-weights-bold);
  border-bottom-color: color-mix(in srgb, currentColor 35%, transparent);
}
.ld-prose th:not(:first-child),
.ld-prose td:not(:first-child) { text-align: center; }
.ld-prose th:first-child,
.ld-prose td:first-child { padding-left: 0; }
.ld-prose tbody tr:last-child td { border-bottom: 0; }

/* 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);
}
/* The listing used to render outside `.ld-prose`, on a page of its own. As a
   `::categories::` marker it renders inside one, where `.ld-prose > *` caps
   every child at the reading measure and `.ld-prose a` underlines every link.
   That took the grid from three columns to two and drew a line under each
   card. Neither rule is wrong; the listing is simply not prose. */
.ld-prose .ld-cards { max-width: none; }
.ld-prose .ld-article-card,
.ld-prose .ld-article-card:hover { text-decoration: none; }

.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; }

/* The third cell in a category's row: two pieces and the way to the rest.
   
   Deliberately not a card. It carries no picture and stands for a place rather
   than a thing to read, so it is drawn as an outline where a card is a solid,
   and it lifts on hover the same way so the row still reads as one set. */
.ld-card-more {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: var(--ld-spacing-2xs);
  padding: var(--ld-spacing-lg);
  border-radius: var(--ld-radius-card);
  border: 1px dashed rgba(109, 40, 217, 0.35);
  color: inherit;
  text-decoration: none;
  transition: transform var(--ld-motion-ease), border-color var(--ld-motion-ease), background var(--ld-motion-ease);
}
[data-bs-theme="dark"] .ld-card-more { border-color: rgba(196, 181, 253, 0.28); }
.ld-card-more:hover {
  transform: translateY(-3px);
  border-color: rgba(109, 40, 217, 0.6);
  background: rgba(109, 40, 217, 0.05);
}
[data-bs-theme="dark"] .ld-card-more:hover {
  border-color: rgba(196, 181, 253, 0.55);
  background: rgba(196, 181, 253, 0.07);
}
.ld-card-more:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
.ld-card-more-label { font-weight: var(--ld-typography-weights-bold); }
.ld-card-more-count { font-size: var(--ld-typography-scale-small); opacity: 0.65; }
.ld-card-more svg { width: 1.4em; height: 1.4em; margin-top: var(--ld-spacing-2xs); }
.ld-prose .ld-card-more,
.ld-prose .ld-card-more:hover { text-decoration: none; }

.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; }

/* The trail, at the foot of the picture. It has to be told so.
   
   Undefined, it stayed in normal flow after an image that already fills the
   figure, and a figure with a fixed aspect ratio and `overflow: hidden` simply
   clipped it away. Every article with a hero lost its breadcrumbs completely
   while the ones without kept theirs, which is what made it look arbitrary
   rather than like one missing rule. Every published article has a hero.
   
   The gradient runs the other way from `.ld-hero-text`, for the same job: the
   words are white and the photograph underneath them was not chosen for its
   contrast. `margin-bottom` is cleared because the trail is the last thing in
   the box and the padding is already the space. */
.ld-hero-crumbs {
  position: absolute;
  inset: auto 0 0 0;
  padding: var(--ld-spacing-lg);
  background: linear-gradient(to top, rgba(20, 8, 37, 0.72), rgba(20, 8, 37, 0));
  color: #fff;
}
.ld-hero-crumbs .ld-crumbs { margin-bottom: 0; }

@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; }

/* Join, beside the controls rather than among the sections. A nav lists
   places to go and this is the thing the site is asking for, so it is drawn
   as the filled action the start page's own first button is. */
.ld-header-join {
  display: inline-flex;
  align-items: center;
  padding: var(--ld-spacing-3xs) var(--ld-spacing-md);
  border-radius: 999px;
  font-weight: 600;
  text-decoration: none;
  background: #6d28d9;
  color: #fff;
  box-shadow: 0 6px 16px rgba(109, 40, 217, 0.3);
  /* The same lift every other button here makes, and the same distance: the
     cards travel 3px because they are large, and a control in a row travels 1. */
  transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
}
[data-bs-theme="dark"] .ld-header-join { background: #c4b5fd; color: #2b1264; box-shadow: none; }
.ld-header-join:hover,
.ld-header-join:focus {
  background: #5b21b6;
  color: #fff;
  transform: translateY(-1px);
  box-shadow: 0 10px 24px rgba(109, 40, 217, 0.38);
}
[data-bs-theme="dark"] .ld-header-join:hover,
[data-bs-theme="dark"] .ld-header-join:focus {
  background: #ddd6fe;
  color: #2b1264;
  transform: translateY(-1px);
  box-shadow: none;
}
.ld-header-join:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }

/* 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;
}
/* Every control in the bar is the same height, set here rather than left to
   whatever each one's padding worked out to. Join was 27px beside an Apps
   button of 35, which reads as a mistake because it is one. */
.ld-header-actions > .ld-header-join,
.ld-header-actions > .ld-apps-trigger,
.ld-header-actions > .ld-search-trigger,
.ld-header-actions > .ld-burger {
  height: 2.4rem;
  padding-top: 0;
  padding-bottom: 0;
}
.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; }
  /* Beside Apps and the burger there is no room for it on a phone, and the
     controls wrapped onto a row of their own. It leads the sheet instead. */
  .ld-header-join { display: none; }
}

/* 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); }

/* Join and the sections as one list. The panel spaces its own children a
   section apart, so without a group of their own Join would sit that far
   above the links it leads. */
.ld-nav-sheet-group {
  display: flex;
  flex-direction: column;
  gap: var(--ld-spacing-xs);
}
.ld-sheet-join {
  display: block;
  padding: var(--ld-spacing-sm) var(--ld-spacing-md);
  border-radius: var(--ld-radius-card);
  background: #6d28d9;
  color: #fff;
  text-decoration: none;
  font-size: 1.05rem;
  font-weight: 600;
}
.ld-sheet-join:hover,
.ld-sheet-join:focus { background: #5b21b6; color: #fff; }
[data-bs-theme="dark"] .ld-sheet-join { background: #c4b5fd; color: #2b1264; }
[data-bs-theme="dark"] .ld-sheet-join:hover,
[data-bs-theme="dark"] .ld-sheet-join:focus { background: #ddd6fe; color: #2b1264; }


/* 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; }

/* A split that follows a row of panels is a change of subject, not the next
   paragraph, so it gets the break a section deserves. Scoped to that pairing
   rather than raised on `.ld-split` itself: on /about the split is the first
   thing on the page and has nothing above it to be spaced from. */
.ld-row + .ld-split { margin-top: var(--ld-spacing-4xl); }

/* The same break in the other direction: the start page closes on a row after
   its split, and at the prose rhythm the panel sat against the paragraph above
   it as though it belonged to that section. Scoped under `.ld-prose` because
   `.ld-prose > .ld-row` sets the rhythm further down the file, and at equal
   specificity the later rule wins. */
.ld-prose > .ld-split + .ld-row { margin-top: var(--ld-spacing-4xl); }

/* And its words line up with the panel's. A panel is a box with padding, so
   its heading starts inset, while prose below starts flush against the
   container and the two read as misaligned even though both are correct.
   Matching the panel's own padding is the alignment.
   
   On the body rather than on the split, because padding the split would take
   the carousel's width with it, and the carousel is not what looks wrong. */
.ld-row + .ld-split .ld-split-body { padding-left: var(--ld-spacing-lg); }
/* A notice is inset further than a panel and carries a 3px rule down its left,
   so the words below have further to travel to line up with it. */
.ld-row:has(> .ld-notice) + .ld-split .ld-split-body {
  padding-left: calc(var(--ld-spacing-xl) + 3px);
}

/* In a box, the first action is the one the box is asking for and the rest are
   somewhere else to go, so they group at the far end rather than crowding the
   ask. Every other in-box action row on the site holds a single link, which is
   why this needs no marker of its own: with one child there is nothing to
   push, and the rule is a no-op wherever the arrangement does not arise.

   Only when the links are separate paragraphs. Written without blank lines
   between them markdown emits one paragraph, which is a single flex child with
   nothing to separate. */
.ld-panel .ld-actions > p:nth-child(2),
.ld-notice .ld-actions > p:nth-child(2) { margin-left: auto; }

/* `.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; }
  /* Flush again once the columns stack. The indent is there to match a box
     beside it; on a phone there is no box to match and it only eats measure. */
  .ld-row + .ld-split .ld-split-body { padding-left: 0; }
}


/* A video, once somebody asks for it
   ------------------------------------------------------------------------
   Built by site.js from a YouTube link the body wrote. With no script the
   link is what the page shows, so nothing here has a server side.

   Half width where there is room, because a video in a body of text is an
   illustration rather than the thing you came for, and full width once the
   column is narrow enough that half of it is too small to watch. */
.ld-video {
  margin-top: var(--ld-spacing-lg);
  max-width: 100%;
}
@media (min-width: 56rem) {
  .ld-video { max-width: 50%; }
}

.ld-video iframe,
.ld-video-start {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  border-radius: var(--ld-radius-card);
}

/* The placeholder says what pressing it does. A click is only consent if it
   is informed, and this is the one thing on the page that reaches somebody
   else's server. */
.ld-video-start {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--ld-spacing-2xs);
  padding: var(--ld-spacing-lg);
  cursor: pointer;
  color: inherit;
  text-align: center;
  background: rgba(109, 40, 217, 0.08);
  border: 1px dashed rgba(109, 40, 217, 0.35);
  transition: background var(--ld-motion-ease), border-color var(--ld-motion-ease);
}
[data-bs-theme="dark"] .ld-video-start {
  background: rgba(196, 181, 253, 0.07);
  border-color: rgba(196, 181, 253, 0.28);
}
.ld-video-start:hover { background: rgba(109, 40, 217, 0.14); border-color: rgba(109, 40, 217, 0.6); }
[data-bs-theme="dark"] .ld-video-start:hover { background: rgba(196, 181, 253, 0.12); border-color: rgba(196, 181, 253, 0.55); }
.ld-video-start:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }

.ld-video-play {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: 50%;
  background: #6d28d9;
  color: #fff;
}
.ld-video-play svg { width: 1.75rem; height: 1.75rem; margin-left: 0.15rem; }
/* Says what the box is before it says what pressing it does. */
.ld-video-title {
  font-size: var(--ld-typography-scale-small);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  opacity: 0.65;
  margin-bottom: var(--ld-spacing-2xs);
}
.ld-video-label { font-weight: var(--ld-typography-weights-bold); }
.ld-video-note { font-size: var(--ld-typography-scale-small); opacity: 0.7; max-width: 22rem; }

/* 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. */
/* LIP-069: the live journal on the public-journal guide.

   Centred, because the embed caps itself at 680px and prose here is narrower
   than that on a wide screen, so left aligned it sits off to one side of a
   column it is wider than. `margin-inline` on the wrapper rather than on the
   frame, since the frame is created by the embed script and this stylesheet
   never sees it.

   The block breaks out of the prose measure on purpose: it is a picture of a
   whole page, and squeezing it into a reading column makes the trip list
   unreadable for no gain. */
.ld-journal-embed {
  display: flex;
  justify-content: center;
  width: 100%;
  max-width: 680px;
  margin: var(--ld-spacing-lg) auto;
}

.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.

     LIP-069: 1260 by 2640, which is the capture viewport in
     marketing/website-screens/scenes.mjs at its scale factor. It was
     1092 / 502, the shape of the WordPress era mockups these replaced.
     Change the viewport there and this has to move with it. */
  height: calc(var(--ld-carousel-card) * 2640 / 1260);
  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: 1260 / 2640;
  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;
  /* LIP-069: the artwork used to bring its own corners, being a mockup with
     a phone bezel drawn into the picture, and a radius here would have cut
     into them. A capture is a bare rectangle of app content, so the corner is
     the stylesheet's job again. Card radius rather than input: this is a
     screen, and it reads as one at 14rem. */
  border-radius: var(--ld-radius-card);
  overflow: hidden;
  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);
  /* Boxes in a row are the same height, whatever each one holds. `start` sized
     every box to its own content, so two panels whose copy differed by a line
     ended at different heights and the row read as ragged rather than as a
     set. Once the columns collapse each box is its own track and this says
     nothing either way. */
  align-items: stretch;
}
/* 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-3xl);
  margin-bottom: var(--ld-spacing-lg);
}
/* Except when it opens the page. `.ld-prose > :first-child` already says this
   and loses: the two have the same specificity and the rule above comes later
   in the file. On /about a category heading always had prose above it; where
   the listing is the first thing on the page, that break lands under the title
   as a hole. */
.ld-prose > .ld-about-category:first-child { margin-top: 0; }

/* A row of boxes after the listing is a change of subject, not the next
   paragraph, so it takes the same break a category heading takes rather than
   the prose rhythm's. */
.ld-cards + .ld-row { margin-top: var(--ld-spacing-4xl); }
/* 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. */

/* LIP-067: the form ticket's honeypot field.

   Off-screen rather than `display: none` or the `hidden` attribute, and that
   is the load-bearing part. A field that is not rendered is one some browsers
   decline to submit, and the rule checking it requires it to be *present and
   empty*: absence is what the old honeypot treated as a pass, which is why it
   never fired. So it has to exist, occupy layout, and simply be somewhere
   nobody looks.

   `left: -9999px` and not `clip-path` or a 1px box, because a parser deciding
   what is visible is exactly who this is hiding from, and a field sitting at
   the origin with a zero size reads as hidden to anything that bothers to
   look. This reads as content that happens to be placed badly.

   The class is on the wrapper, never the input: the input's name rotates per
   ticket, so a selector naming the field would have to know all six. */
.ld-ticket-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* The proof-of-work widget. It ships its own shadow-DOM styling, so this only
   places it: a break above matching the fields it follows, and the brand's
   own accent so it does not read as a foreign control dropped into the form.
   Both are custom properties the widget reads, not overrides of its internals,
   which would break on any upgrade. */
.ld-altcha {
  display: block;
  margin-top: 1.25rem;
  max-width: 22rem;
  --altcha-border-radius: 12px;
  --altcha-color-border: rgba(91, 33, 182, 0.2);
  --altcha-color-base: rgba(255, 255, 255, 0.7);
}
[data-bs-theme="dark"] .ld-altcha {
  --altcha-color-border: rgba(196, 181, 253, 0.25);
  --altcha-color-base: rgba(255, 255, 255, 0.1);
}

/* The sparkle, inline in a sentence
   ------------------------------------------------------------------------
   An article can name an icon, and a reader who has never opened the app
   still does not know what to look for. This draws the app's own glyph at
   the size of the words around it, as a button that says what it means.

   Sized in `em` throughout, so it rides with whatever text it sits in
   rather than needing a rule per place it is used. */
.ld-sparkle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Deliberately taller than the line it sits in. The point of it is that
     somebody who has never opened the app can see the shape, and at the
     size of the surrounding words a four-pointed star is a smudge.

     Being taller, it grows the line box whatever we do, and the baseline is
     what decides which way. Sitting just above it, in the descender space
     the line already reserves, sends the extra height upward into the gap
     above the paragraph rather than down into the next line of prose. */
  vertical-align: -0.2em;
  width: 2.5em;
  height: 2.5em;
  margin: 0 0.25em;
  padding: 0;
  border: 1px solid rgba(109, 40, 217, 0.28);
  border-radius: 999px;
  background: rgba(139, 92, 246, 0.12);
  color: var(--ld-colors-primary-light);
  cursor: pointer;
  transition: background-color 120ms ease, border-color 120ms ease, transform 160ms ease;
}
.ld-sparkle svg { width: 1.5em; height: 1.5em; }
.ld-sparkle:hover,
.ld-sparkle:focus-visible {
  background: rgba(139, 92, 246, 0.22);
  border-color: rgba(109, 40, 217, 0.45);
  transform: rotate(-12deg) scale(1.06);
}
[data-bs-theme="dark"] .ld-sparkle {
  border-color: rgba(196, 181, 253, 0.3);
  background: rgba(196, 181, 253, 0.16);
  color: var(--ld-colors-primary-dark);
}
[data-bs-theme="dark"] .ld-sparkle:hover,
[data-bs-theme="dark"] .ld-sparkle:focus-visible {
  background: rgba(196, 181, 253, 0.28);
  border-color: rgba(196, 181, 253, 0.5);
}

/* A sheet holding one line of copy is a lot of room around very little, so
   this one lands as a small card in the middle instead.

   Two classes, so these win over `.ld-sheet` inside both of its media
   queries without either being repeated here: specificity outranks source
   order, and a panel from the side is the wrong shape at either width. */
.ld-sheet.is-mini {
  margin: auto;
  width: min(23rem, calc(100vw - 2rem));
  height: auto;
  max-height: calc(100vh - 3rem);
}
.ld-sheet.is-mini .ld-sheet-panel {
  height: auto;
  border-radius: var(--ld-radius-card);
  gap: var(--ld-spacing-md);
  padding: var(--ld-spacing-lg);
}
.ld-sheet.is-mini .ld-sheet-head h2 { font-size: 1.15rem; }
.ld-sheet.is-mini p { margin: 0 0 var(--ld-spacing-sm); font-size: 0.95rem; line-height: 1.55; }
.ld-sheet.is-mini p:last-of-type { margin-bottom: 0; }

/* A path through the app's menus
   ------------------------------------------------------------------------
   Placed with `::path:Settings > Privacy > Magic helpers::`. A guide says
   where a thing is on nearly every page, and as prose that is three bold
   words and a preposition, which reads as three separate things somebody
   emphasised rather than as one route. */
.ld-path {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.3em;
  padding: 0.12em 0.5em;
  border-radius: 0.6em;
  border: 1px solid rgba(109, 40, 217, 0.18);
  background: rgba(139, 92, 246, 0.09);
  /* The line is already tight in prose, so the trail keeps the paragraph's
     own leading rather than pushing its neighbours apart. */
  line-height: 1.35;
}
.ld-path-step { font-weight: 600; }
.ld-path-sep { opacity: 0.5; font-weight: 400; }
[data-bs-theme="dark"] .ld-path {
  border-color: rgba(196, 181, 253, 0.22);
  background: rgba(196, 181, 253, 0.12);
}

/* An icon named inside a sentence, rather than leading a heading, which is
   what `.ld-icon` was built for and why it carries a trailing margin. */
.ld-prose p .ld-icon,
.ld-prose li .ld-icon,
.ld-prose td .ld-icon {
  width: 1.15em;
  height: 1.15em;
  margin-right: 0;
  vertical-align: -0.22em;
}

/* A path directly under a heading is a locator, not a sentence: the heading
   says what the section is about and the trail says where in the app to find
   it. So it sits tight under the heading rather than a paragraph's distance
   away, and drops back in size so it reads as a label on the heading instead
   of as the section's first line.

   Matched on the paragraph holding nothing else, because a trail written
   inside a sentence is prose and is left alone. */
.ld-prose h2 + p:has(> .ld-path:only-child),
.ld-prose h3 + p:has(> .ld-path:only-child) {
  margin-top: calc(var(--ld-spacing-sm) * -0.5);
  margin-bottom: var(--ld-spacing-md);
  font-size: 0.9em;
}

/* A picture in an article sits on a plinth
   ------------------------------------------------------------------------
   App screens are portrait and prose caps a picture at 22rem tall, so a
   phone shot renders about 170px wide. Left aligned in a column three times
   that, it read as something that had failed to load rather than as a
   deliberately small picture.

   So the paragraph holding it becomes the frame: the picture is centred and
   the space beside it is a panel, which is the same surface `.ld-panel`
   already uses. A landscape shot fills the column and gains a margin of
   padding, which is the same gesture at a different width, so both kinds
   read as one thing.

   Scoped to a direct child of `.ld-prose` and to a media link, which is what
   keeps it away from the store badges, the app icons, and anything a
   carousel or a split has already boxed. */
.ld-prose .ld-figure {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ld-spacing-sm);
  margin: var(--ld-spacing-lg) 0;
  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-prose .ld-figure {
  background: rgba(0, 0, 0, 0.22);
  border-color: rgba(255, 255, 255, 0.07);
}
/* The link is the flex item, so it has to stop filling the row or the
   picture is centred inside a box that is still the column's full width. */
.ld-prose .ld-figure > a {
  display: block;
  max-width: 100%;
}
/* The caption says what the picture shows, so it reads as a note on the
   picture rather than as another line of the article: smaller, quieter, and
   centred under it however wide the picture turned out. */
.ld-prose .ld-figure figcaption {
  max-width: 34rem;
  font-size: 0.85rem;
  line-height: 1.5;
  text-align: center;
  opacity: 0.68;
}

/* Apps moves into the navigation sheet on the narrowest screens
   ------------------------------------------------------------------------
   The bar holds the mark, Apps and the burger down to about 400px in a
   headless browser, and it was still breaking above that in a real one:
   a scrollbar, a wider system font or a zoom level all take from the same
   row. So the switch is at 420, bought a little early on purpose, since
   the cost of moving Apps into the sheet twenty pixels sooner is nothing
   and the cost of a wrapped header is a broken looking page.

   Two copies of the control, one in each place, which is what the links and
   Join already do here: a closed dialog is hidden from the page and from the
   accessibility tree, so only one is ever announced or tabbed to. Moving a
   single element between the two would need a script on every resize, and it
   would leave the control missing for anybody without one. */
.ld-apps-trigger.is-in-sheet { display: none; }
.ld-nav-sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ld-spacing-sm);
}
.ld-nav-sheet-head h2 { margin-bottom: 0; }

@media (max-width: 479px) {
  .ld-header-actions .ld-apps-trigger { display: none; }
  .ld-apps-trigger.is-in-sheet { display: inline-flex; }
}

/* Search
   ------------------------------------------------------------------------
   A button dressed as a field in the bar, and a dialog that holds the real
   one. Quiet at rest: the site is read rather than queried, so the control
   should be findable without competing with the sections beside it. */
.ld-search-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--ld-spacing-xs);
  flex: none;
  min-width: 11rem;
  padding: 0.45rem 0.7rem;
  border-radius: 999px;
  border: 1px solid rgba(109, 40, 217, 0.18);
  background: rgba(255, 255, 255, 0.4);
  color: inherit;
  font: inherit;
  font-size: 0.85rem;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 120ms ease, border-color 120ms ease;
}
.ld-search-trigger:hover {
  background: rgba(255, 255, 255, 0.72);
  border-color: rgba(109, 40, 217, 0.32);
}
.ld-search-trigger svg { flex: none; opacity: 0.6; }
.ld-search-trigger-label { flex: 1; opacity: 0.6; text-align: left; }
/* The shortcut, drawn as the key it is. */
.ld-search-trigger-key {
  flex: none;
  padding: 0.05rem 0.4rem;
  border-radius: 0.35rem;
  border: 1px solid rgba(109, 40, 217, 0.2);
  font-size: 0.75rem;
  opacity: 0.55;
}
[data-bs-theme="dark"] .ld-search-trigger {
  border-color: rgba(196, 181, 253, 0.2);
  background: rgba(255, 255, 255, 0.06);
}
[data-bs-theme="dark"] .ld-search-trigger:hover {
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(196, 181, 253, 0.35);
}
[data-bs-theme="dark"] .ld-search-trigger-key { border-color: rgba(196, 181, 253, 0.25); }

/* The sheet's copy is a row like the links beside it, not a pill. */
.ld-search-trigger.is-in-sheet {
  display: none;
  width: 100%;
  min-width: 0;
  border-radius: var(--ld-radius-input);
  padding: 0.7rem 0.9rem;
  font-size: 0.95rem;
}
.ld-search-trigger.is-in-sheet .ld-search-trigger-key { display: none; }

/* Measured rather than guessed: with the field in it the bar wrapped onto a
   second row between 640 and 700, because the sections are still in the bar
   there and the field asks for eleven rem of its own. The icon alone fits
   all the way down. */
@media (max-width: 767px) {
  .ld-header-actions .ld-search-trigger {
    min-width: 0;
    width: 2.4rem;
    height: 2.4rem;
    padding: 0;
    justify-content: center;
  }
  .ld-header-actions .ld-search-trigger-label,
  .ld-header-actions .ld-search-trigger-key { display: none; }
}

/* And below 480 it follows Apps into the sheet. Search, Apps and the burger
   beside the mark is three controls on a phone, which fits by arithmetic and
   reads as a toolbar rather than a header. */
@media (max-width: 479px) {
  .ld-header-actions .ld-search-trigger { display: none; }
  .ld-search-trigger.is-in-sheet { display: inline-flex; }
}

.ld-search-dialog {
  border: 0;
  padding: 0;
  background: transparent;
  color: inherit;
  width: min(40rem, calc(100vw - 2rem));
  max-width: 100vw;
  /* Top anchored: results grow downward, and a panel that grows from the
     middle moves what somebody is reading while they read it. */
  margin: 4rem auto auto;
  max-height: calc(100vh - 6rem);
}
.ld-search-dialog::backdrop { background: rgba(20, 8, 37, 0.55); backdrop-filter: blur(4px); }
@media (max-width: 639px) {
  .ld-search-dialog { margin: 0 auto auto; width: 100vw; max-height: 100vh; }
  .ld-search-panel { border-radius: 0 0 var(--ld-radius-card) var(--ld-radius-card); }
}

.ld-search-panel {
  display: flex;
  flex-direction: column;
  max-height: inherit;
  overflow: hidden;
  border-radius: var(--ld-radius-card);
  background: var(--ld-surface-raised, rgba(255, 255, 255, 0.92));
  box-shadow: 0 24px 60px rgba(20, 8, 37, 0.28);
}
[data-bs-theme="dark"] .ld-search-panel {
  background: #1b1033;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
}
.ld-search-panel:focus { outline: none; }

.ld-search-bar {
  display: flex;
  align-items: center;
  gap: var(--ld-spacing-sm);
  flex: none;
  padding: var(--ld-spacing-md) var(--ld-spacing-lg);
  border-bottom: 1px solid rgba(109, 40, 217, 0.12);
}
[data-bs-theme="dark"] .ld-search-bar { border-bottom-color: rgba(255, 255, 255, 0.08); }
.ld-search-bar svg { flex: none; opacity: 0.5; }
.ld-search-field {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 1.05rem;
  padding: 0.2rem 0;
}
.ld-search-field:focus { outline: none; }
.ld-search-field::-webkit-search-cancel-button { display: none; }
.ld-search-close {
  flex: none;
  padding: 0.2rem 0.5rem;
  border-radius: 0.4rem;
  border: 1px solid rgba(109, 40, 217, 0.2);
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 0.75rem;
  opacity: 0.6;
  cursor: pointer;
}
.ld-search-close:hover { opacity: 1; }
[data-bs-theme="dark"] .ld-search-close { border-color: rgba(196, 181, 253, 0.25); }

.ld-search-results {
  overflow-y: auto;
  padding: var(--ld-spacing-sm) var(--ld-spacing-sm) var(--ld-spacing-md);
}
.ld-search-hint,
.ld-search-empty {
  margin: 0;
  padding: var(--ld-spacing-md) var(--ld-spacing-md) var(--ld-spacing-lg);
  opacity: 0.6;
  font-size: 0.9rem;
}

.ld-search-group + .ld-search-group { margin-top: var(--ld-spacing-sm); }
.ld-search-group-title {
  margin: 0;
  padding: var(--ld-spacing-sm) var(--ld-spacing-md) var(--ld-spacing-2xs);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.5;
}
.ld-search-list { margin: 0; padding: 0; list-style: none; }

.ld-search-hit {
  display: flex;
  align-items: flex-start;
  gap: var(--ld-spacing-sm);
  padding: var(--ld-spacing-sm) var(--ld-spacing-md);
  border-radius: var(--ld-radius-input);
  color: inherit;
  text-decoration: none;
}
.ld-search-hit:hover,
.ld-search-hit.is-active,
.ld-search-hit:focus-visible {
  background: rgba(139, 92, 246, 0.12);
  outline: none;
}
[data-bs-theme="dark"] .ld-search-hit:hover,
[data-bs-theme="dark"] .ld-search-hit.is-active,
[data-bs-theme="dark"] .ld-search-hit:focus-visible { background: rgba(196, 181, 253, 0.14); }

/* The text takes what is left, and `min-width` is what lets it: a flex item
   will not shrink below its content otherwise, and a long title would push
   the row wider than the panel. */
.ld-search-hit-body { flex: 1; min-width: 0; }
/* 4:3, the shape of the cards these pictures already draw elsewhere. */
.ld-search-hit-thumb {
  flex: none;
  width: 3.6rem;
  height: 2.7rem;
  margin-top: 0.15rem;
  border-radius: var(--ld-radius-input);
  object-fit: cover;
}
.ld-search-hit-head {
  display: flex;
  align-items: baseline;
  gap: var(--ld-spacing-xs);
  flex-wrap: wrap;
}
.ld-search-hit-title { font-weight: 600; }
.ld-search-hit-meta { font-size: 0.75rem; opacity: 0.55; }
.ld-search-hit-snippet {
  display: block;
  margin-top: 0.15rem;
  font-size: 0.85rem;
  line-height: 1.5;
  opacity: 0.72;
  /* Two lines: enough to show the sentence the match is in, and short
     enough that five results still fit without scrolling. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Where a search result lands, which the browser highlights itself.
   
   `::target-text` is the run a `#:~:text=` directive matched, and it is drawn
   by the browser in its own pale yellow or lavender unless a page says
   otherwise. The same browsers that implement text directives implement this,
   so the pair always agree: arriving from a result looks like the result did.

   The accent and white, in both themes. Everything else brand coloured here
   flips in the dark, because the dark accent is a light lavender and white on
   it cannot be read. A highlight is the exception on purpose: it marks one
   run of words inside a paragraph rather than filling a control, and the
   whole job is to be the thing you see first, which the deep violet does in
   either theme. */
::target-text {
  background: var(--ld-colors-primary-light);
  color: #fff;
}

/* The same pair the landing highlight uses, so a result and the place it
   sends you look like one thing. */
.ld-search-hit mark {
  background: var(--ld-colors-primary-light);
  color: #fff;
  border-radius: 0.25rem;
  padding: 0 0.15em;
}

/* The page at /search, which the form falls back to and the modal sends
   anybody pressing Enter without choosing a result. */
.ld-search-form {
  display: flex;
  gap: var(--ld-spacing-sm);
  margin: var(--ld-spacing-md) 0 var(--ld-spacing-lg);
}
.ld-search-input {
  flex: 1;
  min-width: 0;
  padding: 0.7rem 0.9rem;
  border-radius: var(--ld-radius-input);
  border: 1px solid rgba(109, 40, 217, 0.22);
  background: rgba(255, 255, 255, 0.55);
  color: inherit;
  font: inherit;
}
[data-bs-theme="dark"] .ld-search-input {
  background: rgba(255, 255, 255, 0.06);
  border-color: rgba(196, 181, 253, 0.25);
}
.ld-search-submit {
  flex: none;
  padding: 0.7rem 1.1rem;
  border-radius: var(--ld-radius-input);
  border: 0;
  background: var(--ld-colors-primary-light);
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
[data-bs-theme="dark"] .ld-search-submit { background: var(--ld-colors-primary-dark); color: #2b1264; }
.ld-search-count { margin: 0 0 var(--ld-spacing-md); opacity: 0.7; font-size: 0.9rem; }

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

/* A line under a page's opening saying what is on it. Centred and quiet: it
   is a label for the page rather than part of its argument, and a reader
   should be able to skip it in one glance. */
.ld-prose .ld-lede {
  max-width: 34rem;
  margin: var(--ld-spacing-md) auto var(--ld-spacing-lg);
  text-align: center;
  font-size: 0.95rem;
  opacity: 0.7;
}
.ld-prose .ld-lede p { margin: 0; }
