  /* Two themes, one set of names.

     The accent cannot simply be reused. #00ff88 on white is about 1.4:1, which
     is not a colour, it is a rumour -- so light mode gets a genuinely darker
     green, and the primary button's near-black label has to turn white to stay
     on the right side of it. That is why --on-accent exists rather than the
     button hardcoding a colour that only works against one of the two.

     --accent-rgb carries the same colour as bare components, because roughly
     twenty rules tint with it at some alpha and rgba() cannot read a hex
     variable. rgb(var(--accent-rgb) / .12) can. */
  :root {
    color-scheme: dark;

    --bg: #0a0a0a;
    --bg-raised: #141414;
    --bg-inset: #0b0b0b;
    --line: #292929;
    --text: #e8eef5;
    --muted: #8b9bad;
    --placeholder: #5c5c5c;
    --dot: #171717;

    --accent: #00ff88;
    --accent-rgb: 0 255 136;
    --accent-dim: #00d976;
    --on-accent: #04140a;
    /* On black the accent is perfectly legible as lettering too, so here the
       two are the same colour. They only part company in light mode. */
    --accent-text: #00ff88;

    --danger: #ff8080;
    --blue: #00d4ff;

    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
    --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
  }

  /* Surfaces are the app's own light theme, copied from lib/theme/
     app_colors.dart so the site and the product agree rather than merely
     rhyming. If those ever move, that file is the source of truth.

     The green is copied too, now that there are two of them. */
  :root[data-theme="light"] {
    color-scheme: light;

    --bg: #f5f7fb;          /* AppColors.lightBackground */
    --bg-raised: #ffffff;   /* AppColors.lightCard       */
    --bg-inset: #eef2f7;
    --line: #e2e8f0;        /* AppColors.lightBorder     */
    --text: #0f172a;        /* AppColors.lightText       */
    /* Not AppColors.lightTextSecondary, though it started there. #64748B is
       right where the app puts it -- 4.76 on a white card -- but this page has
       a tinted background and a fixed wash travelling over it, and the wash is
       what sets the floor: it is fixed, so every paragraph on the site passes
       underneath it and any of this text can end up on its darkest point.

       Tuned against that rather than against the flat page, which is why it is
       darker than it looks like it needs to be. 5.20 on the wash, 6.82 off it.
       Raising the wash's alpha means darkening this again. */
    --muted: #4d5766;
    --placeholder: #94a3b8;
    --dot: #e4eaf2;

    /* The app's own success green, at last. It was too dark before because one
       token was doing two jobs: a green that has to be legible at 12px on white
       has to be a forest green, and that made every button and every tint look
       heavy with it.

       Split in two, both can be right. This one is the shape colour -- buttons,
       tints, borders, icons -- and it is #22C55E straight out of
       app_colors.dart, so the page and the product finally use the same green.
       Its label goes dark rather than white, which is 8.31 and is also exactly
       what the Get Started button in the app does. */
    --accent: #22c55e;        /* AppColors.success */
    --accent-rgb: 34 197 94;
    --accent-dim: #16a34a;
    --on-accent: #04140a;

    /* And this one is for lettering. #22C55E as 12px text on this background is
       2.12, which is not readable by any measure, so small green words take a
       deeper green -- and deeper again than the flat page needs, for the same
       reason as the grey above. 5.41 on the wash, 7.10 off it.

       Only small green words use it. The buttons and tints keep the bright
       green above, which is the green anyone actually notices, so none of this
       makes the page look darker. */
    --accent-text: #0f6127;

    --danger: #ef4444;      /* AppColors.danger */
    --blue: #0e7490;
  }

  /* A terminal is dark in every theme ever designed, so this one stays put.
     Re-declaring the tokens rather than hardcoding inside each rule means
     everything within keeps working, including its green, which would
     otherwise turn dark green against a dark shell and vanish. */
  .terminal {
    --bg-raised: #141414;
    --line: #292929;
    --text: #e8eef5;
    --muted: #8b9bad;
    --accent: #00ff88;
    --accent-rgb: 0 255 136;
    --accent-text: #00ff88;
    --accent-dim: #00d976;
    --on-accent: #04140a;
  }

  /* Both phones hold real captures now, so the glass carries its own colours
     and the screens need no tokens at all. The whole --screen-* set went out
     with the drawings it was built for, along with the status bar, the chips,
     the icon, the rule and the call to action that used to be assembled here.

     The body does not change with the theme either. That is hardware: a black
     phone around a white screen is what a real one looks like, and so are the
     island and the bezel.

     What is left inside is the halo behind the glass and the hairline on the
     body, and those keep the bright green in both themes. They are decoration
     rather than text, and a dark green glow around a dark phone on a light page
     would simply not be there. */
  .phone-stage,
  .app-shot {
    --accent: #00ff88;
    --accent-rgb: 0 255 136;
    --accent-text: #00ff88;
  }

  * { box-sizing: border-box; }

  html { -webkit-text-size-adjust: 100%; }

  body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--sans);
    line-height: 1.6;
    /* The dotted grid from the original design, drawn rather than shipped. */
    background-image: radial-gradient(circle, var(--dot) 1px, transparent 1px);
    background-size: 34px 34px;
  }

  a { color: inherit; }

  .wrap {
    width: 100%;
    max-width: 1060px;
    margin: 0 auto;
    padding: 0 24px;
  }

  .label {
    font-family: var(--mono);
    font-size: 12px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--accent-text);
    margin: 0 0 18px;
  }

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

  header {
    padding: 26px 0 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    flex-wrap: wrap;
  }

  .nav {
    display: flex;
    align-items: center;
    gap: 22px;
    font-size: 14px;
  }

  /* :not(.btn) matters more than it looks. Without it this rule also catches
     the Get the App button, and it beats .btn-primary on specificity -- one
     class and one element against one class -- so the button's label was
     painted --muted instead of --on-accent. Grey lettering on a green button,
     in both themes, while the identical button in the hero got it right for no
     reason other than not living inside .nav. */
  .nav > a:not(.btn) { text-decoration: none; color: var(--muted); transition: color .15s ease; }
  .nav > a:not(.btn):hover { color: var(--text); }

  /* Every page already says which one it is with aria-current, but nothing
     was painting it, so all four links looked identical and the nav never
     told you where you were.

     --accent-text rather than --accent, for the reason set out where those
     two part company: the button green is 2.12 against the light wash and
     unreadable at 14px. This is lettering, so it takes the lettering green.

     Placed after :hover deliberately -- the two have identical specificity,
     so source order decides, and the current page should stay green while
     the pointer is over it rather than dropping back to plain text. */
  .nav > a:not(.btn)[aria-current="page"] {
    color: var(--accent-text);
    font-weight: 600;
  }

  .langs {
    display: inline-flex;
    gap: 2px;
    font-family: var(--mono);
    font-size: 11px;
    border: 1px solid var(--line);
    border-radius: 8px;
    padding: 3px;
  }

  .lang { padding: 3px 7px; border-radius: 5px; color: var(--muted); }
  .lang.is-on { background: rgb(var(--accent-rgb) / .12); color: var(--accent-text); }

  /* Shows the theme you would get by pressing it, not the one you are in --
     a sun while the page is dark. The label says so in words for anyone who
     cannot see which of the two icons is showing. */
  .theme-toggle {
    display: inline-grid;
    place-items: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border-radius: 8px;
    border: 1px solid var(--line);
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    transition: color .15s ease, border-color .15s ease;
  }

  .theme-toggle:hover { color: var(--accent-text); border-color: var(--accent-text); }
  .theme-toggle svg { width: 15px; height: 15px; display: block; }

  /* Back to top. Borrows the theme toggle's language -- same border, same
     muted-to-accent hover -- so it reads as part of the furniture rather than
     something bolted on.

     inset-inline-end rather than right, because the Arabic page is dir="rtl"
     and a button hard-pinned to the right would sit under the scrollbar side
     of a layout that runs the other way.

     visibility, not just opacity: a transparent button is still in the tab
     order, so a keyboard reader would land on an invisible control at the top
     of the page where it does nothing. */
  .to-top {
    position: fixed;
    inset-block-end: 22px;
    inset-inline-end: 22px;
    z-index: 40;
    display: inline-grid;
    place-items: center;
    width: 42px;
    height: 42px;
    padding: 0;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--bg-raised);
    color: var(--muted);
    cursor: pointer;
    opacity: 0;
    visibility: hidden;
    transform: translateY(8px);
    transition: opacity .2s ease, transform .2s ease, visibility .2s ease,
                color .15s ease, border-color .15s ease;
  }

  .to-top svg {
    width: 17px;
    height: 17px;
    display: block;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .to-top:hover { color: var(--accent-text); border-color: var(--accent-text); }

  .to-top.is-visible { opacity: 1; visibility: visible; transform: none; }

  @media (prefers-reduced-motion: reduce) {
    .to-top { transition: none; transform: none; }
  }
  .theme-toggle .icon-moon { display: none; }

  :root[data-theme="light"] .theme-toggle .icon-sun { display: none; }
  :root[data-theme="light"] .theme-toggle .icon-moon { display: block; }

  /* .btn-sm lives further down, immediately after .btn. It used to sit here,
     where it did nothing at all: same specificity, declared 77 lines earlier,
     so every one of its three properties lost to .btn and the header button
     rendered full size on every screen. */

  /* These links used to be hidden on a phone, on the grounds that they were
     decoration: every one pointed at a section of this same page, which a
     visitor reaches by scrolling anyway.

     That stopped being true when About and Contact became pages. Hiding them
     left a phone with no route to either -- and, because the rule matched every
     anchor in the nav, it was hiding the Get the App button too. The one thing
     still visible was the language switcher.

     So the nav stays and wraps instead. The header is not sticky, so the extra
     rows are paid for once and scroll away. */
  @media (max-width: 780px) {
    header {
      justify-content: center;
      gap: 8px;
      padding-top: 16px;
    }

    /* This was 25px, chosen to keep the header short because the wordmark is
       the tallest thing in it and the one element nobody needs to tap. That
       reasoning held while the desktop mark was 40px; against the 52-76px it
       is now, 25 read as a different logo rather than a smaller one.

       The nav wraps onto its own row here anyway, so a taller mark costs the
       header about twenty pixels and nothing else. Scaled rather than fixed so
       it grows with the phone: 42px on a 375-wide screen, 52 on a large one.

       Qualified with header so it outranks the base rule rather than merely
       arriving before it. This media block sits above the rules it overrides,
       so a bare .wordmark here loses on source order -- which is exactly how
       .btn-sm spent its whole life doing nothing. */
    header .wordmark { height: clamp(23px, 5.9vw, 28px); }

    .nav {
      width: 100%;
      flex-wrap: wrap;
      justify-content: center;
      gap: 2px 16px;
    }

    /* Vertical padding rather than a bigger font: it keeps the type at the size
       the design uses while giving a thumb something to actually land on.
       38px is below the usual 44 guideline, but the gaps either side are dead
       space rather than a neighbouring target, so a near miss costs nothing. */
    .nav > a:not(.btn) {
      font-size: 14px;
      padding: 8px 2px;
    }

    .nav .btn-sm { padding: 10px 18px; }

    .lang { padding: 8px 12px; font-size: 12px; }
  }

  /* Dominique's light-on-dark export, as vector. It replaces a 350x75 PNG of
     navy artwork drawn for a white header, which had to be inverted and
     hue-rotated in CSS to become readable here. The filter is gone with it:
     the artwork is the right colour to begin with, and a vector stays sharp
     at any pixel density where the PNG was soft on every modern phone.

     The export ships a solid #000000 background rectangle, which was removed
     — this page is #0a0a0a over a dotted grid, so a black box would sit
     visibly behind the letters. */
  /* Two inks, one mark.

     The export is light-on-dark artwork, which is right on this page's black
     and wrong the moment the page turns to paper: the green loop measures 1.39
     against #F5F7FB, so it does not read as faint, it reads as missing. The
     light variant is the same file with the two inks carried down until they
     hold -- blue to #0A63D8 at 5.17, green to #0D842D at 4.50 -- rather than a
     CSS filter, which would have dragged the gradient in the dash along with
     them and dulled the hue.

     Carried as a background rather than an <img> for the reason the screenshots
     are: each file is 60KB, and a browser fetches only the background a rule
     applies. Two <img> tags with one hidden would have pulled down both. The
     anchor already had aria-label="Deve-Loops", so nothing was lost with alt. */
  .wordmark {
    display: block;
    /* These heights look small because the mark is now 5.6:1, where the old
       one was 3:1. They are chosen so the logo occupies the same WIDTH in the
       header as it always has -- at the previous heights the new mark would
       be ~424px across and overflow a phone. Change aspect-ratio and these
       two clamps together or the header breaks. */
    height: clamp(28px, 4vw, 41px);
    aspect-ratio: 1200 / 215;
    background: url(/deve-loops-wordmark.png) left center / contain no-repeat;
  }

  :root[data-theme="light"] .wordmark {
    background-image: url(/deve-loops-wordmark-light.png);
  }

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

  .hero { padding: 88px 0 96px; }

  h1 {
    font-size: clamp(38px, 8vw, 78px);
    line-height: 1.03;
    letter-spacing: -.03em;
    font-weight: 800;
    margin: 0 0 26px;
    max-width: 15ch;
  }

  h1 .accent { color: var(--accent-text); }
  h1 .heart { color: var(--accent-text); }

  .lede {
    font-size: clamp(16px, 2.2vw, 19px);
    color: var(--muted);
    max-width: 56ch;
    margin: 0 0 36px;
  }

  .buttons { display: flex; flex-wrap: wrap; gap: 14px; }

  /* The two hero buttons sit side by side and read as one pair, so they
     should be one width rather than two.

     A fixed width cannot do that here. "See what we've built" measures
     185px, but the French "Voir ce que nous avons créé" measures 235 and
     the Arabic 132 -- a width taken from the English label would clip one
     language and leave the other floating in space. Equal `1fr` tracks in
     a grid sized to its content give both buttons the width of whichever
     label is longer, in whatever language is showing. */
  .hero .buttons {
    display: inline-grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
  }

  /* The grid stretches each button past its text, so the label has to be
     told to sit in the middle of the space rather than at its start. */
  .hero .buttons .btn { justify-content: center; }

  /* Narrow enough and two equal columns squeeze the longer label onto a
     second line, which makes the pair 78px tall instead of 54. Stacked,
     each keeps one line and they stay the same width as each other. */
  @media (max-width: 560px) {
    .hero .buttons { grid-auto-flow: row; }
  }

  .btn {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    /* 10px puts these at 46 tall. Slimmer than the 54 they were, and still
       clear of the 44px minimum touch target in Apple's guidelines and
       WCAG -- going to 9 would hit 44 exactly but also collapse the
       difference between this and .btn-sm, which is already 9. */
    padding: 10px 22px;
    border-radius: 12px;
    font-weight: 600;
    font-size: 15px;
    text-decoration: none;
    border: 1px solid var(--line);
    background: var(--bg-raised);
    transition: border-color .18s ease, transform .18s ease, box-shadow .18s ease;
  }

  /* After .btn, not before it. Both are one class, so the later one wins and
     order is the whole difference between this working and being ignored. */
  .btn-sm { padding: 9px 16px; font-size: 13.5px; border-radius: 10px; }

  /* The dark buttons carry a green edge that glows, so they read as a set
     with the filled green one rather than as empty boxes beside it.

     Two shadows, not one: the tight 1px ring sharpens the border into a
     real stroke, and the wide soft one is the glow. Built from
     --accent-rgb at low alpha so it follows the theme -- in light mode the
     accent is a darker green (#22c55e) and the same rule lands as a soft
     tint instead of neon, which is what that mode wants.

     .btn-primary is excluded: it is already solid green, and a green glow
     around a green button just makes it look blurry. */
  .btn:not(.btn-primary) {
    border-color: rgb(var(--accent-rgb) / .42);
    box-shadow:
      0 0 0 1px rgb(var(--accent-rgb) / .16),
      0 0 18px rgb(var(--accent-rgb) / .20);
  }

  .btn:hover { border-color: var(--accent-text); transform: translateY(-1px); }

  .btn:not(.btn-primary):hover {
    border-color: rgb(var(--accent-rgb) / .85);
    box-shadow:
      0 0 0 1px rgb(var(--accent-rgb) / .3),
      0 0 26px rgb(var(--accent-rgb) / .36);
  }

  .btn-primary {
    background: var(--accent);
    border-color: var(--accent-text);
    color: var(--on-accent);
  }

  .btn-primary:hover { background: var(--accent-dim); border-color: var(--accent-dim); }

  /* The App Store link carries Apple's mark rather than the site green.
     Their badge is not a coloured button -- it inverts with the page it
     sits on, black on light and white on dark -- so this is built from
     --text and --bg rather than a fixed pair, and follows the theme
     toggle without a second rule. */
  /* Apple's supplied badge, not a button we style. Their marketing
     guidelines require the artwork unaltered -- no recolouring, no
     redrawing, no cropping -- so there is deliberately nothing here that
     touches how it looks. It replaces a lockup this file used to draw by
     hand, which was against those guidelines even though it looked right.

     The file is 40 tall natively and is scaled to 46 so it lines up with
     the .btn beside it. Scaling in proportion is allowed; altering it is
     not, which is why the height is set and the width left to follow. */
  .appstore-badge-link {
    display: inline-flex;
    text-decoration: none;
    transition: opacity .18s ease, transform .18s ease;
  }

  .appstore-badge-link:hover { opacity: .88; transform: translateY(-1px); }

  .appstore-badge { display: block; height: 46px; width: auto; }

  /* Taller than Apple's on purpose. Google's supplied artwork carries its own
     clear space inside the file, so at a matching 46px the drawn button comes
     out about a third smaller than Apple's and the pair looks misaligned.
     This height makes the two buttons read as the same size, which is what a
     visitor actually compares. */
  .googleplay-badge { display: block; height: 68px; width: auto; margin: -11px 0; }

  .btn[aria-disabled="true"] {
    opacity: .5;
    pointer-events: none;
  }

  .btn small {
    font-weight: 400;
    font-size: 12px;
    color: var(--muted);
  }

  /* ---------- phone mockup ---------- */

  .hero-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 64px;
    align-items: center;
  }

  @media (min-width: 900px) {
    .hero-grid { grid-template-columns: 1.05fr .95fr; gap: 48px; }
  }

  .phone-stage { display: flex; justify-content: center; }

  /* Two layered drop-shadows, green core over a cyan halo, on a wrapper that
     floats. Copied from the original design rather than approximated. */
  .phone-glow {
    filter:
      drop-shadow(0 0 24px rgb(var(--accent-rgb) / .35))
      drop-shadow(0 0 48px rgba(0, 212, 255, .2));
    animation: float 6s ease-in-out infinite;
  }

  @keyframes float {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-12px); }
  }

  /* 1284x2778 is the screenshot size in website/screens, so the mockup, the
     rotating box and the real device all agree. Declared as a ratio rather
     than a computed height, so a narrow screen shrinks both dimensions
     together instead of squashing one -- the old fixed height would have gone
     on holding 520 while max-width pulled the body in.

     Width came down from 260 rather than the height going up: 240 wide leaves
     the body 519.3 tall against the 520 it was, so the hero does not move. */
  .phone {
    position: relative;
    width: 240px;
    aspect-ratio: 1284 / 2778;
    max-width: 82vw;
    border-radius: 40px;
    padding: 10px;
    background: #1a1a1a;
    border: 1.6px solid #292929;
    box-shadow:
      0 0 0 1px rgb(var(--accent-rgb) / .1),
      inset 0 0 0 1px rgba(245, 245, 245, .05);
  }

  .phone-screen {
    height: 100%;
    border-radius: 32px;
    overflow: hidden;
  }

  /* The real welcome screen, one capture per theme. No padding: a capture
     already contains its own status bar and chrome, so it fills the glass the
     way it does on the device. Anchored to the top, because the screen is a
     hair narrower in proportion than the capture and the crop is better taken
     off the foot than off the app icon. */
  .phone-screen-shot {
    background: url(/screens/getstarted.jpg) center top / cover no-repeat;
  }

  :root[data-theme="light"] .phone-screen-shot {
    background-image: url(/screens/getstarted-light.jpg);
  }

  /* The island. It was the only difference between this mockup and the mini
     phone in the featured card, and it is the single feature that makes a
     rounded rectangle read as an iPhone rather than as a generic slab.

     Every number is .mini-notch scaled by the ratio of the two bodies,
     260/216, so the two read as one device at two sizes rather than as two
     different phones.

     No extra room is made for it. The mini phone pads its screen down by 44px
     to clear the island, but this one has a status bar with the time on the
     left and indicators on the right, so a centred island drops into the gap
     between them -- which is exactly where a real one sits. */
  .phone-notch {
    position: absolute;
    /* Put on the same centre line as the clock beside it, the way a real one
       sits, rather than simply near the top. Measured against the clock rather
       than derived: the body has a 1.6px border, so an offset that looks right
       in the arithmetic lands a couple of pixels out on the screen. */
    top: 24px;
    left: 50%;
    transform: translateX(-50%);
    /* Came down with the body, 260 to 240, so it keeps its share of the width
       rather than growing into it. 0.325 is about what a real island takes. */
    width: 78px;
    height: 18px;
    border-radius: 11px;
    background: #05080c;
    z-index: 2;
  }









  /* ---------- card icons ---------- */

  .card-icon {
    width: 42px; height: 42px;
    border-radius: 13px;
    display: grid;
    place-items: center;
    margin-bottom: 16px;
    background: rgb(var(--accent-rgb) / .1);
    border: 1px solid rgb(var(--accent-rgb) / .26);
  }

  .card-icon svg { width: 20px; height: 20px; stroke: var(--accent-text); fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }

  /* ---------- sections ---------- */

  section { padding: 80px 0; border-top: 1px solid var(--line); }

  h2 {
    font-size: clamp(26px, 4.4vw, 40px);
    line-height: 1.12;
    letter-spacing: -.02em;
    font-weight: 800;
    margin: 0 0 18px;
  }

  /* ---------- featured app ---------- */

  .app-card {
    background: var(--bg-raised);
    border: 1px solid var(--line);
    border-radius: 20px;
    padding: clamp(24px, 4vw, 40px);
  }

  .tags { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 22px; }

  .tag {
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 5px 11px;
  }

  .app-name { font-size: clamp(24px, 4vw, 34px); font-weight: 800; margin: 0 0 8px; letter-spacing: -.02em; }
  .app-tagline { color: var(--accent-text); font-weight: 600; margin: 0 0 16px; }
  .app-body { color: var(--muted); margin: 0 0 28px; max-width: 62ch; }

  .facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 18px;
    padding: 24px 0 28px;
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line);
    margin-bottom: 28px;
  }

  /* These values are sentences, not the two-character numbers the reference
     design carried, so they wrap. Pushing the label to the bottom of each
     cell keeps the row of labels level however many lines the value takes. */
  .facts > div {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 6px;
  }

  .fact-value {
    font-size: clamp(15px, 1.7vw, 17px);
    font-weight: 800;
    color: var(--accent-text);
    letter-spacing: -.01em;
    font-variant-numeric: tabular-nums;
    text-wrap: balance;
  }
  .fact-label { font-family: var(--mono); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }

  /* The card runs as two columns once there is room for them: a look at the
     app beside a description of it. Below that width the screenshot is the
     first thing to go — on a phone the reader already holds the real thing. */
  @media (min-width: 900px) {
    .app-card {
      display: grid;
      grid-template-columns: 250px minmax(0, 1fr);
      gap: clamp(30px, 4vw, 52px);
      align-items: center;
    }
  }

  .app-shot { display: flex; justify-content: center; margin-bottom: 30px; }
  @media (min-width: 900px) { .app-shot { margin-bottom: 0; } }

  .app-shot-glow {
    filter: drop-shadow(0 0 24px rgb(var(--accent-rgb) / .26))
            drop-shadow(0 0 52px rgba(0, 212, 255, .15));
  }

  /* The same device as the hero, at four fifths the size. A second screen
     rather than a repeat of the welcome panel, so the two phones on the page
     show different parts of the app. */
  /* Same true ratio as the hero phone. 200 wide leaves it 432.7 tall against
     the 432 it was, so the featured card does not move either. */
  .mini-phone {
    position: relative;
    width: 200px;
    aspect-ratio: 1284 / 2778;
    max-width: 72vw;
    border-radius: 34px;
    padding: 9px;
    background: #1a1a1a;
    border: 1.6px solid #292929;
    box-shadow:
      0 0 0 1px rgb(var(--accent-rgb) / .1),
      inset 0 0 0 1px rgba(245, 245, 245, .05);
  }

  /* Came down with its body too, 216 to 200, for the same reason. */
  .mini-notch {
    position: absolute;
    top: 14px;
    left: 50%;
    transform: translateX(-50%);
    width: 65px;
    height: 16px;
    border-radius: 9px;
    background: #05080c;
    z-index: 2;
  }

  .mini-screen {
    height: 100%;
    border-radius: 27px;
    padding: 44px 15px 18px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    overflow: hidden;
  }

  /* A real capture behind the glass instead of a drawing of one. No padding:
     the shot already contains its own status bar and chrome, so it fills the
     screen the way it would on the device. Anchored to the top, because the
     screen is a hair narrower in proportion than the capture and the crop
     should come off the bottom rather than the heading. */
  .mini-screen-shot {
    padding: 0;
    background: url(/screens/analytics.jpg) center top / cover no-repeat;
  }

  :root[data-theme="light"] .mini-screen-shot {
    background-image: url(/screens/analytics-light.jpg);
  }


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

  .grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 20px;
    margin-top: 40px;
  }

  /* ---------- screens ---------- */

  /* Four screens on the four sides of a turning box.

     Each face is rotated a further quarter turn and then pushed out along its
     own new Z. The push is half a face width, because on a four-sided box the
     distance from the axis to a face is exactly half its width -- which is what
     makes the four meet at the corners and close, rather than leaving gaps or
     overlapping through each other. */
  .shots {
    --face-w: 220px;
    --face-h: calc(var(--face-w) * 2778 / 1284);
    --face-z: calc(var(--face-w) / 2);

    /* A box in perspective does not stay inside its own box. The near face is
       magnified -- 1100 / (1100 - 110), about a ninth larger -- so it stands
       taller than the element that holds it, and a corner swung round to 45
       degrees reaches higher still: 39px above the top on a 476px face, which
       is how the heading was being sat on. This is that overhang, kept as a
       share of the face so it stays right at any size, plus a gap you can see. */
    --face-bleed: calc(var(--face-h) * .085);

    height: var(--face-h);
    margin: calc(var(--face-bleed) + 30px) auto calc(var(--face-bleed) + 10px);
    /* Far enough back that the box turns rather than lunges at the reader. */
    perspective: 1100px;
  }

  .shots-cube {
    position: relative;
    width: var(--face-w);
    height: var(--face-h);
    margin: 0 auto;
    transform-style: preserve-3d;
    animation: cube-spin 22s linear infinite;
  }

  /* Negative, so the face in front travels left: the direction the strip slid.
     Arabic reverses it, exactly as the strip did. */
  @keyframes cube-spin {
    from { transform: rotateY(0deg); }
    to   { transform: rotateY(-360deg); }
  }

  [dir="rtl"] .shots-cube { animation-direction: reverse; }

  .shot {
    position: absolute;
    inset: 0;
    margin: 0;
    border-radius: 18px;
    overflow: hidden;
    background: var(--bg-raised);
    border: 1px solid var(--line);
    /* The back of a face is the inside of the box. Without this you catch a
       screenshot mirrored, read from behind, through the turn. */
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
  }

  .shot:nth-child(1) { transform: rotateY(0deg)   translateZ(var(--face-z)); }
  .shot:nth-child(2) { transform: rotateY(90deg)  translateZ(var(--face-z)); }
  .shot:nth-child(3) { transform: rotateY(180deg) translateZ(var(--face-z)); }
  .shot:nth-child(4) { transform: rotateY(270deg) translateZ(var(--face-z)); }

  /* Carried as backgrounds rather than <img>, because there are two of each --
     the dark-mode capture and the light one, which are the same moment shot
     twice: same clock, same battery, same figures. A browser fetches only the
     background a rule actually applies, so a visitor downloads the four for
     their own theme instead of all eight. Two <img> tags with one hidden would
     have pulled down both sets, since display:none does not stop a fetch.

     The cost is that alt has to become aria-label on a role="img", which the
     language engine now translates alongside alt and placeholder. */
  .shot {
    background-size: cover;
    background-position: center top;
    background-repeat: no-repeat;
  }

  .shot-home      { background-image: url(/screens/home.jpg); }
  .shot-ai        { background-image: url(/screens/ai.jpg); }
  .shot-savings   { background-image: url(/screens/savings.jpg); }
  .shot-analytics { background-image: url(/screens/analytics.jpg); }

  :root[data-theme="light"] .shot-home      { background-image: url(/screens/home-light.jpg); }
  :root[data-theme="light"] .shot-ai        { background-image: url(/screens/ai-light.jpg); }
  :root[data-theme="light"] .shot-savings   { background-image: url(/screens/savings-light.jpg); }
  :root[data-theme="light"] .shot-analytics { background-image: url(/screens/analytics-light.jpg); }

  /* A finger cannot hover, and on a phone this would fire mid-scroll. Stopping
     it matters: a screenshot that will not hold still cannot be read. */
  @media (hover: hover) and (pointer: fine) {
    .shots:hover .shots-cube { animation-play-state: paused; }
  }

  @media (max-width: 780px) {
    .shots { --face-w: 168px; perspective: 820px; }
  }

  /* Motion that starts by itself and never stops is precisely what this setting
     is for -- and a turning box shows one screen at a time, so waiting is the
     only way through it. It unfolds into a flat row the reader pushes instead. */
  @media (prefers-reduced-motion: reduce) {
    /* Flat, so there is no overhang to leave room for. */
    .shots { height: auto; margin: 0 auto; perspective: none; overflow-x: auto; }

    .shots-cube {
      width: auto;
      height: auto;
      display: flex;
      gap: 14px;
      transform-style: flat;
      animation: none;
    }

    .shot {
      position: static;
      flex: 0 0 var(--face-w);
      aspect-ratio: 1284 / 2778;
      transform: none !important;
    }
  }

  /* Only the heading centres — the cards keep their own start alignment so
     the icons stay on the leading edge in both directions. */
  .section-centered > .label,
  .section-centered > h2 { text-align: center; }

  .section-centered > h2 {
    max-width: 34ch;
    margin-left: auto;
    margin-right: auto;
    text-wrap: balance;
  }

  /* "Why people love it" is not a uniform grid in Dominique's design. The four
     cards are 542 and 447 wide on the first row, 412 and 573 on the second,
     19 apart, each 185 tall. CSS grid cannot vary column widths per row, so
     this one row is flex instead, every card claiming its share of the width
     left once the single gap is removed:

       row 1   542/989 and 447/989      row 2   412/985 and 573/985

     Shares, not pixels, so the proportions survive any container width — the
     wrap measures 1012 here against the roughly 1006 the design was drawn at.
     Each pair is shaved to .999 of the row rather than a clean 1.0: at exactly
     100% a sub-pixel rounding error is enough to wrap the second card onto a
     line of its own, and 0.1% of a 993px row is under a pixel of slack.

     Height 185 comes out of the padding rather than being declared. The stack
     is 20 + icon 42 + 16 + heading 27 + 10 + two 24px lines + 20, plus the 1px
     border top and bottom: 185 exactly. At the 26px padding the other cards
     use it lands on 197, which is what gave the design away — these cards are
     padded 20. min-height then holds the floor for the cards whose copy runs
     to one line instead of two, English "Private by Design" and three of the
     four in Arabic. Nothing measured exceeds 185 in any of the three, so the
     row is exactly 185 everywhere and longer copy would grow rather than
     clip. */
  @media (min-width: 900px) {
    .grid-mosaic {
      display: flex;
      flex-wrap: wrap;
      gap: 19px;
    }

    .grid-mosaic > .card {
      min-height: 185px;
      padding: 20px;
    }

    .grid-mosaic > .card:nth-child(1) { flex: 0 0 calc((100% - 19px) * .5475); }
    .grid-mosaic > .card:nth-child(2) { flex: 0 0 calc((100% - 19px) * .4515); }
    .grid-mosaic > .card:nth-child(3) { flex: 0 0 calc((100% - 19px) * .4177); }
    .grid-mosaic > .card:nth-child(4) { flex: 0 0 calc((100% - 19px) * .5813); }
  }

  .card {
    background: var(--bg-raised);
    border: 1px solid var(--line);
    border-radius: 16px;
    padding: 26px;
  }

  .card h3 { font-size: 17px; margin: 0 0 10px; font-weight: 700; }
  .card p { margin: 0; color: var(--muted); font-size: 15px; }

  /* ---------- about ---------- */

  .about p { color: var(--muted); max-width: 62ch; }
  .about strong { color: var(--text); font-weight: 600; }

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

  footer {
    border-top: 1px solid var(--line);
    padding: 40px 0 56px;
    font-size: 14px;
    color: var(--muted);
  }

  .footer-links { display: flex; flex-wrap: wrap; gap: 22px; margin-bottom: 20px; }
  .footer-links a { text-decoration: none; }
  .footer-links a:hover { color: var(--accent-text); }

  /* Instagram and TikTok, on every page from 2026-09-04. They sit on their own
     line rather than joining .footer-links because those are obligations --
     privacy, terms, support, an address for complaints -- and an invitation to
     follow us reads oddly in that company. Icon-only for the same reason: the
     row above already carries five links, and two more handles spelled out
     would tip the footer into a list nobody reads.

     The reduced-motion rule further down already kills every transition with a
     universal selector, so these need no guard of their own. */
  .footer-social { display: flex; gap: 10px; margin-bottom: 20px; }

  .footer-social a {
    width: 34px;
    height: 34px;
    border-radius: 9px;
    display: grid;
    place-items: center;
    color: var(--muted);
    border: 1px solid var(--line);
    transition: color .18s ease, border-color .18s ease, background .18s ease;
  }

  .footer-social a:hover {
    color: var(--accent-text);
    border-color: rgb(var(--accent-rgb) / .45);
    background: rgb(var(--accent-rgb) / .07);
  }

  .footer-social a:focus-visible {
    outline: 2px solid rgb(var(--accent-rgb) / .85);
    outline-offset: 2px;
  }

  .footer-social svg { width: 17px; height: 17px; }

  @media (prefers-reduced-motion: reduce) {
    * { transition: none !important; }

    /* transition alone was never the whole story. These two run on animation
       and so walked straight past the rule above -- the glow behind the hero
       phone bobbing every six seconds, and the two background washes drifting
       for twenty-four and thirty.

       Gentler than the loop mark on the about page, but both are on the page
       everyone lands on, and both are decoration with nothing to say. The
       aurora keeps its colour and the glow keeps its light; they simply stop
       moving.

       The blinking caret in the terminal is deliberately left alone. It is
       not decoration -- it is what says that block is a terminal -- and a
       cursor that blinks is what a cursor does. */
    .phone-glow { animation: none; }
    .aurora .a1,
    .aurora .a2 { animation: none; }
  }

  /* The language control is a real button now, so it needs the browser's
     button chrome stripped back to what the span used to look like. */
  .lang {
    appearance: none;
    background: none;
    border: 0;
    font: inherit;
    cursor: pointer;
  }
  .lang:hover { color: var(--text); }
  .lang:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

  /* Arabic. The layout is built from flex and grid, which mirror themselves
     when dir flips, so only the handful of things that cannot are listed. */
  [dir="rtl"] .label { letter-spacing: 0; }
  [dir="rtl"] body { font-family: "Segoe UI", Tahoma, var(--sans); }
  [dir="rtl"] .fact-value, [dir="rtl"] .tag { font-family: var(--sans); }

  /* ---------- depth ---------- */

  /* Everything below is additive. The palette, spacing and type above are the
     original design and are left alone; this only adds depth on top, and
     switches itself off for reduced motion, coarse pointers and no-JS. */

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

  .aurora span {
    position: absolute;
    display: block;
    border-radius: 50%;
    filter: blur(80px);
  }

  /* Both blooms were turned up on 2026-09-04, and both drift faster than they
     did. On black there is no contrast cost to answer for -- white type over a
     blurred wash at these alphas stays far above any threshold -- so the only
     question was taste, and they were quiet enough that most visitors never
     registered them moving at all. */
  .aurora .a1 {
    width: 46vw; height: 46vw;
    min-width: 300px; min-height: 300px;
    top: -12vh; left: 50%;
    background: radial-gradient(circle, rgb(var(--accent-rgb) / .30), transparent 68%);
    animation: drift-a 20s ease-in-out infinite;
  }

  .aurora .a2 {
    width: 40vw; height: 40vw;
    min-width: 260px; min-height: 260px;
    top: 4vh; left: 6%;
    background: radial-gradient(circle, rgba(0, 212, 255, .24), transparent 68%);
    animation: drift-b 26s ease-in-out infinite;
  }

  /* The two blooms were tuned behind the home page's hero, where a phone
     mockup, a heading and two buttons sit on top of them and they read as
     depth. A subpage hero is a few lines of centred type on empty ground, so
     the same blooms have nothing to sit behind and read as a stain across the
     page -- green down the right, blue up the left.

     The light stays, because the pages should still look related. It is dimmer,
     smaller and moved to the middle, so it falls behind the title the way the
     design has it, and the blue one goes entirely: with nothing to balance it
     against on a centred layout it only ever pulled the eye left. */
  .page-sub .aurora .a1 {
    width: 36vw; height: 36vw;
    top: -10vh; left: 32%;
    background: radial-gradient(circle, rgb(var(--accent-rgb) / .15), transparent 70%);
  }

  /* Still hidden. The reasoning above holds whatever the alpha is: the second
     bloom has nothing to balance against on a centred layout and only ever
     pulled the eye left. Raising the first one is a brightness decision;
     bringing this one back would be a layout one. */
  .page-sub .aurora .a2 { display: none; }

  /* On black these are light. On paper they have to be ink, because nothing
     glows brighter than white -- so light mode tints rather than glows, and
     that changes which colour to use as well as how much of it.

     It goes blue rather than green. Green under green buttons muddies them;
     a cool wash sits behind and lets them come forward. And blue is not
     borrowed from anywhere: it is half the wordmark, so the page picks up the
     colour of its own logo. The green stays as a whisper on the second bloom
     so the pair still reads as the mark rather than as plain blue.

     Both are stronger than the dark ones look, in alpha terms, because a tint
     on near-white has far less room to show than a glow on black. */
  /* Stays fixed, like the dark one, so it travels with the reader instead of
     scrolling away at the top of the page.

     That is the expensive choice and it is worth knowing why. Fixed means every
     paragraph on the site passes underneath the wash, so the wash sets the
     floor for how dark the text has to be -- there is no such thing as a tint
     you can see that does not darken what sits on it. At this alpha the blend
     is #C1DCFC against a #F5F7FB page, fifty-odd points apart and plainly
     blue, and the two text colours below were moved down to carry it: the grey
     reads 5.20 on the wash and the green label 5.41.

     Wider and further left than the dark one, because at this size it sweeps
     across the top rather than pooling in the corner. */
  /* Raised from .22 to .28 on 2026-09-04, and the contrast was recomputed
     rather than assumed, because this is the one bloom that can actually make
     text unreadable.

     At .28 the blend over the #F5F7FB page is #B3D5FC, luminance .6406. The
     body grey still reads 4.81 against it and the green label 5.00, both above
     the 4.5 that AA asks for on normal text. That is the worst case -- the
     dead centre of the gradient -- and most type sits further out where the
     wash has already fallen away. There is not much room left above this: .32
     would put the grey under 4.5 and the page would fail. */
  :root[data-theme="light"] .aurora .a1 {
    width: 70vw; height: 70vw;
    top: -18vh; left: 28%;
    background: radial-gradient(circle, rgba(8, 125, 255, .28), transparent 72%);
  }

  :root[data-theme="light"] .aurora .a2 {
    background: radial-gradient(circle, rgba(34, 197, 94, .21), transparent 70%);
  }

  :root[data-theme="light"] .page-sub .aurora .a1 {
    background: radial-gradient(circle, rgba(8, 125, 255, .21), transparent 74%);
  }

  /* Four steps rather than two, from 2026-09-04. The old pair went out and
     came straight back along the same line, and a wash retracing its own path
     reads as a pulse rather than as movement -- the eye reads the reversal and
     stops seeing it. Travelling a loop and arriving back where it started
     never repeats a position, so it keeps looking alive at the same speed.

     Transform only, so both stay on the compositor and neither costs a layout
     or a repaint however large the blur is. */
  @keyframes drift-a {
    0%   { transform: translate3d(0, 0, 0) scale(1); }
    25%  { transform: translate3d(-5vw, 3vh, 0) scale(1.10); }
    50%  { transform: translate3d(-2vw, 7vh, 0) scale(1.18); }
    75%  { transform: translate3d(4vw, 4vh, 0) scale(1.08); }
    100% { transform: translate3d(0, 0, 0) scale(1); }
  }

  @keyframes drift-b {
    0%   { transform: translate3d(0, 0, 0) scale(1.06); }
    25%  { transform: translate3d(5vw, -3vh, 0) scale(.96); }
    50%  { transform: translate3d(7vw, 3vh, 0) scale(1.12); }
    75%  { transform: translate3d(2vw, 5vh, 0) scale(1.02); }
    100% { transform: translate3d(0, 0, 0) scale(1.06); }
  }

  /* Phones got no movement at all before this, which is where most of the
     traffic is.

     The amplitudes are deliberately larger than the desktop ones, which reads
     backwards until you do the arithmetic: vw is a percentage of the viewport,
     so 3vw is 43px on a 1440px monitor and 12px on a 390px phone. Matching the
     desktop numbers produces motion nobody can see. These are roughly triple,
     which lands back at a comparable distance in actual pixels, and the scale
     step does most of the visible work at this size anyway.

     Transform-only keeps both on the compositor, and nothing here declares
     will-change -- promoting two blurred elements this large is how you spend
     a phone's memory rather than save its frames. */
  @keyframes drift-a-cal {
    0%   { transform: translate3d(0, 0, 0) scale(1); }
    33%  { transform: translate3d(-9vw, 5vh, 0) scale(1.16); }
    66%  { transform: translate3d(7vw, 8vh, 0) scale(1.08); }
    100% { transform: translate3d(0, 0, 0) scale(1); }
  }

  @keyframes drift-b-cal {
    0%   { transform: translate3d(0, 0, 0) scale(1.05); }
    33%  { transform: translate3d(10vw, -5vh, 0) scale(.94); }
    66%  { transform: translate3d(6vw, 7vh, 0) scale(1.18); }
    100% { transform: translate3d(0, 0, 0) scale(1.05); }
  }

  /* The phone turns to follow the pointer. .phone-glow already owns a
     transform for the float, so the rotation needs its own element or the
     two overwrite each other. */
  .phone-tilt {
    transform:
      perspective(1200px)
      rotateX(var(--tilt-x, 0deg))
      rotateY(var(--tilt-y, 0deg));
    transition: transform .55s cubic-bezier(.22, .61, .36, 1);
    will-change: transform;
  }

  .phone-stage.is-tilting .phone-tilt { transition-duration: .14s; }

  /* Driven by the scroll, or by the device's own gyroscope. Both already supply
     the continuity a transition would otherwise provide, and both update every
     frame -- leaving the .55s ease in place would put the phone permanently a
     beat behind the hand holding it. */
  .phone-stage.is-scroll-tilt .phone-tilt,
  .phone-stage.is-gyro-tilt .phone-tilt { transition: none; }

  /* Glass. A highlight tracking the pointer sells the tilt better than
     parallax inside the screen would, and the screen cannot hold depth
     anyway: overflow:hidden forces preserve-3d back to flat. */
  .phone::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 40px;
    pointer-events: none;
    background: radial-gradient(
      340px circle at var(--sheen-x, 50%) var(--sheen-y, 0%),
      rgba(255, 255, 255, .14),
      rgba(255, 255, 255, .04) 40%,
      transparent 64%);
    opacity: 0;
    transition: opacity .4s ease;
  }

  .phone-stage.is-tilting .phone::after { opacity: 1; }

  /* The cards take the same idea, much quieter. */
  .card, .app-card {
    position: relative;
    isolation: isolate;
  }

  .js-motion .card {
    transform:
      translateY(var(--reveal-y, 0px))
      perspective(900px)
      rotateX(var(--tilt-x, 0deg))
      rotateY(var(--tilt-y, 0deg));
  }

  .card {
    transition:
      transform .45s cubic-bezier(.22, .61, .36, 1),
      opacity .75s cubic-bezier(.22, .61, .36, 1),
      border-color .25s ease;
  }

  .card.is-tilting { transition-duration: .1s; }
  .card:hover { border-color: rgb(var(--accent-rgb) / .34); }

  .card::before,
  .app-card::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    pointer-events: none;
    background: radial-gradient(
      var(--sheen-size, 240px) circle at var(--sheen-x, 50%) var(--sheen-y, 50%),
      rgb(var(--accent-rgb) / .10), transparent 72%);
    opacity: 0;
    transition: opacity .3s ease;
  }

  .card:hover::before,
  .app-card:hover::before { opacity: 1; }

  .app-card { --sheen-size: 420px; }

  @media (max-width: 780px) {
    .aurora span { filter: blur(60px); }
    .aurora .a1 { animation: drift-a-cal 24s ease-in-out infinite; }
    .aurora .a2 { animation: drift-b-cal 28s ease-in-out infinite; }
  }

  /* Arriving on scroll. Cards fold their offset into the transform above
     rather than declaring a second one, which would cancel the tilt. */
  .js-motion .reveal {
    opacity: 0;
    --reveal-y: 22px;
    transition:
      opacity .75s cubic-bezier(.22, .61, .36, 1),
      transform .75s cubic-bezier(.22, .61, .36, 1);
  }

  .js-motion .reveal:not(.card) { transform: translateY(var(--reveal-y, 0px)); }
  .js-motion .reveal.is-in { opacity: 1; --reveal-y: 0px; }

  .js-motion .reveal[data-delay="1"] { transition-delay: .07s; }
  .js-motion .reveal[data-delay="2"] { transition-delay: .14s; }
  .js-motion .reveal[data-delay="3"] { transition-delay: .21s; }
  .js-motion .reveal[data-delay="4"] { transition-delay: .28s; }
  .js-motion .reveal[data-delay="5"] { transition-delay: .35s; }

  /* A section landing flush against the top of the window reads as though the
     page was cut off. This keeps a little air above the heading, for the eased
     scroll below and equally for the browser's own jump when someone opens
     /#features directly. */
  section[id] { scroll-margin-top: 28px; }

  /* The eased scroll parks focus on the section it just delivered, so keyboard
     and screen-reader users arrive where the eye does. That focus is given, not
     taken, so it should not draw a ring around the whole section. */
  section[id]:focus { outline: none; }

  @media (prefers-reduced-motion: reduce) {
    * { animation: none !important; }

    /* `.aurora { display: none }` stood here and contradicted the block
       further up this file, which states in as many words that "the aurora
       keeps its colour ... they simply stop moving". This rule came later and
       won the cascade, so the documented intent never actually shipped: asking
       for less motion removed the background entirely and left a flat page.

       Reduced motion is a request about movement, not about colour. The
       universal rule above already stops the drift, which is the whole of what
       was being asked for. */
    .phone-tilt, .card { transform: none !important; }
    .js-motion .reveal { opacity: 1 !important; transform: none !important; }
  }

  /* ============================================================
     SUBPAGES (about, contact)
     ============================================================ */

  /* A subpage opens on its own title rather than a product shot, so the hero
     is centred type and nothing else. */
  .page-hero {
    text-align: center;
    max-width: 760px;
    margin: 0 auto;
    padding: 54px 0 20px;
  }

  /* The global h1 is capped at 15ch, which is right for the home page's four
     short words at 78px and wrong for a sentence. At 58px it works out near
     435px and forces this headline into five lines; the design sets it in
     three. Overridden here rather than loosened globally, because the home
     page's line breaks depend on that cap. */
  .page-hero h1 {
    font-size: clamp(34px, 5.4vw, 58px);
    line-height: 1.08;
    letter-spacing: -.02em;
    margin: 0 0 18px;
    max-width: 21ch;
    margin-left: auto;
    margin-right: auto;
  }

  .page-hero .lede { max-width: 54ch; margin-left: auto; margin-right: auto; }

  .label-blue { color: var(--accent-2, #4da3ff); }

  /* ---------- about: mission ---------- */

  .split {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 46px;
    align-items: center;
    margin: 40px 0;
  }

  @media (max-width: 860px) {
    .split { grid-template-columns: 1fr; gap: 28px; }
  }

  /* A terminal, because the studio is named after a loop and the mission reads
     better as something you could actually run. */
  .terminal {
    background: #0d0d0d;
    border: 1px solid var(--line);
    border-radius: 14px;
    overflow: hidden;
    font-family: var(--mono);
    font-size: 12.5px;
    line-height: 1.85;
  }

  .terminal-bar {
    display: flex;
    gap: 7px;
    padding: 13px 15px;
    border-bottom: 1px solid var(--line);
  }

  .terminal-bar i {
    width: 11px;
    height: 11px;
    border-radius: 50%;
    display: block;
  }

  .terminal-bar i:nth-child(1) { background: #ff5f57; }
  .terminal-bar i:nth-child(2) { background: #febc2e; }
  .terminal-bar i:nth-child(3) { background: #28c840; }

  .terminal-body { padding: 16px 18px 22px; }
  .terminal-body p { margin: 0; color: var(--muted); }
  .terminal-cmd { color: var(--accent-text); font-weight: 700; }
  .terminal-key { color: #4da3ff; font-weight: 700; }

  /* The cursor is the only thing on the page that blinks. It stops when the
     reader has asked for less motion, like everything else here. */
  .terminal-caret {
    display: inline-block;
    width: 8px;
    height: 15px;
    background: var(--accent);
    vertical-align: -2px;
    animation: caret 1.1s steps(1) infinite;
  }

  @keyframes caret { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }

  /* ---------- about: the developer ---------- */

  .dev-card {
    background: var(--bg-raised);
    border: 1px solid var(--line);
    border-radius: 20px;
    padding: 34px;
    display: grid;
    grid-template-columns: 1.35fr 1fr;
    gap: 40px;
    align-items: center;
    margin: 40px 0;
  }

  @media (max-width: 860px) {
    .dev-card { grid-template-columns: 1fr; gap: 26px; padding: 26px; }
  }

  .dev-aside { display: flex; flex-direction: column; align-items: center; gap: 20px; }

  .dev-mark {
    width: 116px;
    height: 116px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    border: 1px solid rgb(var(--accent-rgb) / .45);
    background: radial-gradient(circle at 50% 50%, rgb(var(--accent-rgb) / .13), transparent 70%);
    /* Five and a half against the loop's nine, so the ring and the mark never
       settle into the same beat and the pair keeps drifting out of step. */
    /* Without this the turn is an orthographic squash: the mark just gets
       narrower and wider again. Perspective is what makes the near edge grow
       as the far edge shrinks, which is what reads as turning. */
    perspective: 340px;
    animation: loop-halo 5.5s ease-in-out infinite;
  }

  @keyframes loop-halo {
    0%, 100% { box-shadow: 0 0 0 0 rgb(var(--accent-rgb) / 0); }
    50%      { box-shadow: 0 0 30px 3px rgb(var(--accent-rgb) / .26); }
  }

  /* A full turn, which the video does not actually contain.

     Surveying all ten seconds of it frame by frame: the ribbon tilts to
     edge-on at about 3.4s and again at 8.4s, and comes back the way it went.
     There is no 360 in there to extract, so no window of it was ever going to
     give one -- it has to be made.

     Made from the artwork, which fixes the other complaint at the same time.
     The sprite animated background-position, and background is painted, not
     composited: every step repainted an 83KB image rescaled into a 72px box,
     which is what the stutter was. A transform is handed to the compositor and
     costs nothing per frame.

     The file is now the green-to-blue loop, matching the new wordmark. It is a
     56KB PNG rather than the 4.5KB vector it replaced, because Dominique's
     Loop.svg is not really a vector -- it wraps two 2269x2563 rasters and
     weighs 661KB, which is not something to load six times for a 64px
     element. Rendering it down once, at 640px, keeps it sharp to 3x and costs
     a twelfth of the source. Replace with a true vector if one ever exists.

     Going edge-on at 90 degrees is not a flaw here, it is the same thing the
     video does at 3.4s -- a flat ribbon seen side-on has no width. The
     difference is that this one keeps going round. */
  .dev-loop {
    display: block;
    width: 64px;
    /* Matches the new artwork. The old mark was exactly 2:1; this one is
       1.61:1, and forcing the old box letterboxed it into a smaller loop. */
    aspect-ratio: 640 / 398;
    position: relative;
    background: url(/loop.png) center / contain no-repeat;
    transform-style: preserve-3d;
    animation: loop-turn 6s linear infinite;
  }

  @keyframes loop-turn {
    from { transform: rotateY(0deg); }
    to   { transform: rotateY(360deg); }
  }

  /* Thickness.

     rotateY on a background image is a cutout turning. At 90 degrees a cutout
     has no width, so the mark vanished entirely twice a turn, and the rest of
     the time it read as paper rather than as an object.

     A slab fixes both. The element's own background stays the front face;
     four masked silhouettes sit behind it just over a pixel apart as the
     body, each darker than the last; and a second copy of the artwork closes
     the back. Past 90 degrees you are looking at that back face with the body
     behind it, which is what a real ribbon does -- so the turn now has two
     sides instead of one side and a disappearance.

     Masked silhouettes rather than more copies of the file, because the
     artwork carries its own gradients and highlights. Stacking those would
     stack the light too, and the body would glow instead of reading as the
     shadowed edge it is meant to be. */
  .dev-depth,
  .dev-face-back {
    position: absolute;
    inset: 0;
    pointer-events: none;
  }

  .dev-depth {
    -webkit-mask: url(/loop.png) center / contain no-repeat;
    mask: url(/loop.png) center / contain no-repeat;
  }

  /* Deep greens rather than greys or a translucent tint: the card behind is
     white in light mode and near-black in dark, and anything see-through
     would be a shadow in one and a highlight in the other. These are opaque,
     so the edge reads the same on both. */
  .dev-depth:nth-of-type(1) { transform: translateZ(-1.2px); background: #12613a; }
  .dev-depth:nth-of-type(2) { transform: translateZ(-2.4px); background: #0f4f2f; }
  .dev-depth:nth-of-type(3) { transform: translateZ(-3.6px); background: #0c3f26; }
  .dev-depth:nth-of-type(4) { transform: translateZ(-4.8px); background: #0a331f; }

  .dev-face-back {
    transform: translateZ(-6px);
    background: url(/loop.png) center / contain no-repeat;
  }

  /* The shine, kept from the video's own vocabulary. It sits inside the loop
     so it turns with it rather than sliding across a shape that is moving
     underneath it, and it is masked by the same file that draws the shape, so
     it can only ever light the ribbon. */
  .dev-shine {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
    -webkit-mask: url(/loop.png) center / contain no-repeat;
    mask: url(/loop.png) center / contain no-repeat;
  }

  /* Where masking is not available the shine would be a white bar sweeping a
     rectangle instead of lighting the ribbon, which is worse than no shine. */
  @supports not ((mask-image: url(/loop.png)) or (-webkit-mask-image: url(/loop.png))) {
    .dev-shine { display: none; }

    /* Same reasoning for the body: unmasked it is four dark rectangles
       stacked behind the mark, which is worse than a flat mark. The back face
       is drawn with a background rather than a mask, so it can stay and the
       shape keeps both its sides. */
    .dev-depth { display: none; }
  }

  .dev-shine::before {
    content: "";
    position: absolute;
    top: -30%;
    bottom: -30%;
    left: 0;
    width: 26%;
    background: linear-gradient(
      90deg,
      rgb(255 255 255 / 0),
      rgb(255 255 255 / .8),
      rgb(255 255 255 / 0)
    );
    animation: loop-shine 6s cubic-bezier(.45, 0, .5, 1) infinite;
  }

  /* Sweeps in the first third, then waits out the rest of the turn. A shine
     with no pause in it reads as a barber's pole rather than a highlight. */
  @keyframes loop-shine {
    0%        { transform: translateX(-120%) skewX(-16deg); }
    36%, 100% { transform: translateX(500%) skewX(-16deg); }
  }

  /* Everything above this line moves forever, which is the one thing someone
     who has asked for less motion cannot get away from by scrolling past it.
     A mark that turns continuously is close to the worst case for the
     vestibular disorders the setting exists for.

     The site already switches off transitions and the scroll reveal for these
     visitors, but those rules only reach transition, and all three of these
     are animation.

     Parked rather than removed: transform:none leaves the loop face-on, which
     is the angle the artwork was drawn at, so it reads as a logo that happens
     not to spin rather than as something that failed to load. The slab stays
     -- depth is not motion, and face-on it is what gives the mark its edge. */
  @media (prefers-reduced-motion: reduce) {
    .dev-mark { animation: none; }

    .dev-loop {
      animation: none;
      transform: none;
    }

    /* A sweep frozen halfway across would read as a stripe painted on the
       ribbon, so the highlight goes rather than stops. */
    .dev-shine { display: none; }
  }

  .pills { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }

  .pill {
    font-size: 11.5px;
    padding: 6px 12px;
    border-radius: 999px;
    color: var(--accent-text);
    border: 1px solid rgb(var(--accent-rgb) / .32);
    white-space: nowrap;
  }

  /* ---------- contact ---------- */

  .contact-grid {
    display: grid;
    grid-template-columns: 1.5fr 1fr;
    gap: 24px;
    align-items: start;
    margin: 30px 0 10px;
  }

  @media (max-width: 860px) {
    .contact-grid { grid-template-columns: 1fr; }
  }

  .panel {
    background: var(--bg-raised);
    border: 1px solid var(--line);
    border-radius: 18px;
    padding: 26px;
  }

  .panel h2 { font-size: 20px; margin: 0 0 20px; }

  .field { margin-bottom: 16px; }
  .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }

  @media (max-width: 560px) {
    .field-row { grid-template-columns: 1fr; }
  }

  .field label {
    display: block;
    font-size: 13px;
    font-weight: 600;
    margin-bottom: 7px;
  }

  .field input,
  .field select,
  .field textarea {
    width: 100%;
    background: var(--bg-inset);
    border: 1px solid var(--line);
    border-radius: 10px;
    padding: 12px 14px;
    color: var(--text);
    font: inherit;
    font-size: 14px;
    transition: border-color .18s ease;
  }

  .field textarea { min-height: 128px; resize: vertical; }

  .field input:focus,
  .field select:focus,
  .field textarea:focus {
    outline: none;
    border-color: rgb(var(--accent-rgb) / .5);
  }

  .field input::placeholder,
  .field textarea::placeholder { color: var(--placeholder); }

  .btn-block { width: 100%; justify-content: center; }

  /* The form says what happened in words, in the reader's own language, rather
     than leaving them guessing whether a press did anything. */
  .form-note {
    margin: 14px 0 0;
    font-size: 13.5px;
    line-height: 1.6;
    min-height: 1.2em;
  }

  .form-note.is-ok { color: var(--accent-text); }
  .form-note.is-bad { color: var(--danger); }

  .link-rows { display: flex; flex-direction: column; gap: 10px; }

  .link-row {
    display: flex;
    align-items: center;
    gap: 13px;
    padding: 12px 14px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--bg-inset);
  }

  /* The social rows became real anchors once the accounts existed. They keep
     the row's own look rather than the site's link styling -- an underlined
     blue-ish handle inside a bordered card reads as a mistake -- so the hover
     state is what tells a visitor these are clickable at all. */
  a.link-row {
    color: inherit;
    text-decoration: none;
    transition: border-color .18s ease, background .18s ease;
  }

  a.link-row:hover {
    border-color: rgb(var(--accent-rgb) / .45);
    background: rgb(var(--accent-rgb) / .07);
  }

  a.link-row:focus-visible {
    outline: 2px solid rgb(var(--accent-rgb) / .85);
    outline-offset: 2px;
  }

  @media (prefers-reduced-motion: reduce) {
    a.link-row { transition: none; }
  }

  .link-row svg { width: 17px; height: 17px; }

  .link-icon {
    width: 34px;
    height: 34px;
    border-radius: 9px;
    display: grid;
    place-items: center;
    flex: none;
    color: var(--accent-text);
    background: rgb(var(--accent-rgb) / .1);
  }

  .link-meta { display: flex; flex-direction: column; line-height: 1.35; min-width: 0; }
  .link-meta small { font-size: 11.5px; color: var(--muted); }
  .link-meta b { font-size: 14px; font-weight: 700; }

  .note-card {
    margin-top: 18px;
    border: 1px solid rgb(var(--accent-rgb) / .28);
    background: rgb(var(--accent-rgb) / .05);
    border-radius: 16px;
    padding: 20px 22px;
  }

  .note-card h3 {
    display: flex;
    align-items: center;
    gap: 9px;
    font-size: 14px;
    margin: 0 0 9px;
    color: var(--accent-text);
  }

  .note-card h3::before {
    content: "";
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--accent);
    flex: none;
  }

  .note-card p { margin: 0; font-size: 13.5px; line-height: 1.65; }

  @media (prefers-reduced-motion: reduce) {
    .terminal-caret { animation: none; }

    /* Stopped, the sprite would freeze on whichever of the 24 frames it
       happened to be showing -- several of which are the ribbon caught
       edge-on, mid-turn, which reads as a broken image rather than a logo.
       The blanket rule stops the turn, which leaves the mark flat and facing
       forward, which is the right still. The shine is the one thing that has
       to go rather than stop: frozen it is a white bar lying across the
       ribbon, and light that is not moving is not light. */
    .dev-shine { display: none; }
  }

  /* ---- Documents: privacy, terms, support -------------------------------

     The one part of this site where reading beats looking. Narrow measure,
     even rhythm, and no reveal animation: a policy that fades in as you
     scroll is a policy that is harder to read, and these are the pages a
     reviewer or a worried user opens when they want an answer, not a show.

     The prose carries its own lang and dir. Switching the site to Arabic
     flips the chrome around it and leaves the document alone, because these
     texts exist in English only -- a mistranslated privacy policy is worse
     than an untranslated one, and Apple reviewed this wording. */
  .doc { max-width: 720px; margin: 0 auto; }

  /* Between the three documents. */
  .doc-nav {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    margin-top: 26px;
  }

  .doc-nav a {
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 8px 15px;
    background: var(--bg-raised);
    color: var(--muted);
    font-size: 13px;
    font-weight: 600;
    text-decoration: none;
    transition: color .15s ease, border-color .15s ease;
  }

  .doc-nav a:hover { color: var(--text); border-color: var(--accent); }

  .doc-nav a[aria-current="page"] {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--on-accent);
  }

  /* Says which language the text below is in, in the language being read. */
  .doc-note {
    max-width: 720px;
    margin: 34px auto 0;
    padding: 12px 16px;
    border: 1px solid var(--line);
    border-inline-start: 3px solid var(--accent);
    border-radius: 10px;
    background: var(--bg-raised);
    color: var(--muted);
    font-size: 13px;
    line-height: 1.6;
  }

  /* A twelve-section policy earns a contents list, and in-page links here
     inherit the site's eased scrolling without asking for anything. */
  .doc-toc {
    margin: 30px 0 0;
    padding: 20px 22px;
    border: 1px solid var(--line);
    border-radius: 14px;
    background: var(--bg-raised);
  }

  .doc-toc h2 {
    font-size: 12px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
    margin: 0 0 12px;
    font-weight: 700;
  }

  .doc-toc ol {
    margin: 0;
    padding: 0;
    list-style: none;
    columns: 2;
    column-gap: 26px;
  }

  @media (max-width: 620px) { .doc-toc ol { columns: 1; } }

  .doc-toc li { margin: 0 0 8px; break-inside: avoid; }

  .doc-toc a {
    color: var(--muted);
    text-decoration: none;
    font-size: 13.5px;
    line-height: 1.5;
    transition: color .15s ease;
  }

  .doc-toc a:hover { color: var(--accent-text); }

  /* The global section rule rules a full-width page; inside a document the
     dividers belong between the sections, not above the first one. */
  .doc section { padding: 30px 0 0; border-top: 0; }

  .doc section + section {
    margin-top: 30px;
    border-top: 1px solid var(--line);
  }

  .doc h2 {
    font-size: clamp(17px, 2.6vw, 20px);
    line-height: 1.35;
    letter-spacing: -.01em;
    margin: 0 0 11px;
  }

  .doc p {
    margin: 0 0 14px;
    color: var(--muted);
    font-size: 15px;
    line-height: 1.75;
  }

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

  /* Scoped to the prose. A bare `.doc a` also caught the contents list, at
     equal specificity and later in the file, so those links were painted
     green and their hover changed nothing. */
  .doc section a { color: var(--accent-text); }

  /* ---- 404 -------------------------------------------------------------
     Cloudflare Pages serves this file, with a real 404 status, for any path
     that does not exist. Without it every typo returned 200 and the home
     page, which tells a visitor -- and a crawler -- that a wrong address is
     a real one. */
  .notfound-code {
    font-family: var(--mono);
    font-size: clamp(64px, 16vw, 132px);
    line-height: 1;
    font-weight: 800;
    letter-spacing: -.04em;
    color: var(--accent);
    margin: 0 0 6px;
  }

  /* ---- FAQ ---------------------------------------------------------------

     Built on <details> rather than a scripted accordion. It opens with no
     JavaScript, it is keyboard operable and announced correctly without any
     ARIA, and the browser's own find-in-page reaches answers that are still
     shut -- which a div-and-click version silently breaks.

     The name attribute makes them exclusive: opening one closes the rest, so
     the page never grows into a wall while someone is reading a single
     answer. */
  .faq { max-width: 720px; margin: 0 auto; }

  .faq details {
    border: 1px solid var(--line);
    border-radius: 14px;
    background: var(--bg-raised);
    margin-bottom: 10px;
    transition: border-color .15s ease;
  }

  .faq details[open] { border-color: rgb(var(--accent-rgb) / .45); }

  .faq summary {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 16px 18px;
    cursor: pointer;
    font-size: 15.5px;
    font-weight: 700;
    color: var(--text);
    list-style: none;
    /* Or a long question on a phone gets selected instead of opened. */
    -webkit-tap-highlight-color: transparent;
  }

  /* Safari draws its own triangle through ::-webkit-details-marker, which
     list-style alone does not remove. */
  .faq summary::-webkit-details-marker { display: none; }

  .faq summary:hover { color: var(--accent-text); }

  .faq summary:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 14px;
  }

  /* The marker is drawn rather than typed: a glyph would need a font that has
     it in all three languages, and would sit on the wrong side in Arabic.
     A rotated chevron flips with the writing direction for free. */
  .faq summary::before {
    content: "";
    flex: none;
    width: 9px;
    height: 9px;
    border-right: 2px solid var(--accent);
    border-bottom: 2px solid var(--accent);
    transform: rotate(-45deg);
    transition: transform .2s ease;
  }

  [dir="rtl"] .faq summary::before { transform: rotate(-135deg); }

  .faq details[open] summary::before { transform: rotate(45deg); }

  [dir="rtl"] .faq details[open] summary::before { transform: rotate(45deg); }

  .faq details p {
    margin: 0;
    padding: 0 18px 18px 39px;
    color: var(--muted);
    font-size: 14.5px;
    line-height: 1.7;
  }

  [dir="rtl"] .faq details p { padding: 0 39px 18px 18px; }

  .faq-more {
    max-width: 720px;
    margin: 26px auto 0;
    text-align: center;
    color: var(--muted);
    font-size: 14px;
  }

  .faq-more a { color: var(--accent-text); }

  @media (prefers-reduced-motion: reduce) {
    .faq summary::before { transition: none; }
  }
