/* African Open Router marketing site — dark, centred, dense.
   Shaped after the category convention (openrouter.ai and peers): near-black
   ground, one high-chroma accent, a centred hero, then a grid of capability
   cards. No build step: Cloudflare Pages serves these bytes as committed. */
/* Dark is the default. The light palette is opt-in via [data-theme="light"] or
   the OS preference, so a reader who has asked their system for light gets it
   without touching anything, and the toggle still wins over both. */
:root {
  --bg: #08090a;
  --bg-2: #0f1113;
  --line: #1e2124;
  --line-2: #2a2e33;
  --ink: #f7f8f8;
  --ink-2: #a8b0b8;
  --ink-3: #6f7780;
  --accent: #c6f24e;
  --accent-ink: #0a0b0c;
  --radius: 12px;
  --wrap: 1200px;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  /* THE HEADER IS A TOKEN NOW, and it is here because it was the one colour on
     this site defined outside the palette. `header` hard-coded
     `rgba(255,255,255,.9)` — the LIGHT value — as its base, while `:root` is
     dark-first. Three of the four scheme states then patched it back:
     [data-theme="light"], [data-theme="dark"], and the system-light media
     query. The fourth had no rule at all.

     That fourth state is SYSTEM DARK WITH NO CHOICE MADE — the default for
     every visitor whose phone is in dark mode who has never touched the
     toggle. They got a white bar carrying `--ink` #f7f8f8 text: the wordmark
     and "تسجيل الدخول" at about 1.03:1 against their own background, which is
     invisible rather than low-contrast. Measured on the live site at 375px,
     both schemes, before this changed.

     `c4dda15` fixed the mirror of this — a dark header in light mode — by
     hard-coding white and adding the two [data-theme] overrides. It fixed the
     direction that was reported and created this one. A token cannot have that
     failure mode: there is one declaration per palette and no default to fall
     through to. */
  --header-bg: rgba(8,9,10,.9);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}
.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 28px; }
a { color: inherit; }

/* announcement */
.banner {
  background: var(--bg-2); border-bottom: 1px solid var(--line);
  text-align: center; padding: 11px 20px; font-size: 14px; color: var(--ink-2);
}
.banner a { color: var(--ink); }

/* header */
header { border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--header-bg); backdrop-filter: blur(8px); z-index: 20; }
.nav { display: flex; align-items: center; gap: 26px; height: 64px; }
.brand { font-weight: 700; letter-spacing: -0.02em; text-decoration: none; font-size: 18px; margin-inline-end: auto; }
.brand span { color: var(--accent); }
.nav a.link { text-decoration: none; color: var(--ink-2); font-size: 14.5px; }
.nav a.link:hover { color: var(--ink); }
.btn {
  display: inline-block; padding: 9px 17px; border-radius: 9px; text-decoration: none;
  font-size: 14.5px; font-weight: 600; border: 1px solid var(--line-2); color: var(--ink);
}
.btn:hover { border-color: var(--ink-3); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.btn.primary:hover { filter: brightness(1.08); }
.btn.lg { padding: 14px 30px; font-size: 16px; }

/* hero */
.hero { padding: 92px 0 60px; text-align: center; }
h1 { font-size: clamp(40px, 7vw, 76px); line-height: 1.03; letter-spacing: -0.035em; margin: 0 auto 20px; max-width: 15ch; font-weight: 700; }
.sub { font-size: clamp(17px, 2vw, 20px); color: var(--ink-2); max-width: 60ch; margin: 0 auto 34px; }
.cta { display: flex; gap: 13px; justify-content: center; flex-wrap: wrap; }

/* stats */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 26px; padding: 60px 0 8px; text-align: center; }
.stat b { display: block; font-size: clamp(24px, 3.4vw, 38px); letter-spacing: -0.03em; font-weight: 700; }
.stat span { color: var(--ink-3); font-size: 13.5px; }

/* sections */
section { padding: 66px 0; }
section + section { border-top: 1px solid var(--line); }
h2 { font-size: clamp(24px, 3.2vw, 34px); letter-spacing: -0.025em; margin: 0 0 12px; font-weight: 700; }
.lede { color: var(--ink-2); max-width: 64ch; margin: 0 0 32px; }
.grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(252px, 1fr)); }
.card { border: 1px solid var(--line); border-radius: var(--radius); padding: 22px; background: var(--bg-2); }
.card h3 { margin: 0 0 8px; font-size: 16.5px; letter-spacing: -0.01em; }
.card p { margin: 0; color: var(--ink-2); font-size: 14.5px; }
.card .k { display: block; font-family: var(--mono); font-size: 12px; color: var(--accent); margin-bottom: 12px; letter-spacing: .04em; }

/* code */
pre {
  background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 20px 22px; overflow-x: auto; font: 13.5px/1.7 var(--mono); margin: 0; color: var(--ink-2);
}
pre .c { color: var(--ink-3); }
pre .s { color: var(--accent); }

/* tables */
table { width: 100%; border-collapse: collapse; font-size: 14.5px; }
th, td { text-align: start; padding: 14px 18px; border-bottom: 1px solid var(--line); }
th { font-weight: 600; color: var(--ink-3); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }
td.num { font-family: var(--mono); white-space: nowrap; }
.scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-2); }
.scroll table { min-width: 560px; }
.scroll tr:last-child td { border-bottom: 0; }

/* legal / docs */
.legal { max-width: 76ch; padding: 54px 0 80px; }
.legal h1 { font-size: clamp(30px, 4vw, 42px); max-width: none; text-align: start; margin-inline-start: 0; }
.legal h2 { font-size: 19px; margin: 36px 0 10px; }
.legal p, .legal li { color: var(--ink-2); }
.legal li { margin-bottom: 8px; }
.legal code { font-family: var(--mono); font-size: 13px; color: var(--ink); }
.meta { color: var(--ink-3); font-size: 14px; margin-top: -6px; }

/* footer */
footer { border-top: 1px solid var(--line); padding: 52px 0 64px; color: var(--ink-3); font-size: 14px; }
.fgrid { display: grid; gap: 30px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); margin-bottom: 36px; }
.fgrid h4 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3); margin: 0 0 12px; }
.fgrid a { display: block; text-decoration: none; color: var(--ink-2); margin-bottom: 9px; font-size: 14px; }
.fgrid a:hover { color: var(--ink); }

/* ── Narrow viewports ────────────────────────────────────────────────────────
   Found by screenshotting production at 390px, not by reading the CSS: the nav
   did not collapse, so brand + four links + the button stayed on one row and
   "Sign in" — the primary conversion control — wrapped to two lines and clipped
   at the right edge. It had been live since the dark rebuild because nothing
   below 1000px had ever been looked at.

   The fix hides the secondary links rather than shrinking them. They all survive
   in the footer, so nothing becomes unreachable, and it needs no JavaScript and
   no menu state. */
@media (max-width: 640px) {
  .wrap { padding: 0 18px; }
  .nav { gap: 14px; height: 58px; }
  .nav a.link { display: none; }
  .brand { font-size: 17px; }
  .btn { padding: 8px 14px; white-space: nowrap; }

  .hero { padding: 52px 0 40px; }
  h1 { max-width: none; }
  .cta { flex-direction: column; align-items: stretch; }
  .cta .btn { text-align: center; }

  .stats { grid-template-columns: repeat(2, 1fr); gap: 22px; padding: 44px 0 4px; }
  section { padding: 46px 0; }
  pre { padding: 16px; font-size: 12.5px; }
  .banner { font-size: 13px; padding: 10px 16px; }
  .legal { padding: 36px 0 56px; }

  /* A PRICE TABLE THAT SCROLLS SIDEWAYS HIDES HALF THE PRICE.
     `.scroll` keeps the page from scrolling horizontally, which is the right
     rule and was already here — but measured at 375px it left MODEL and INPUT
     visible and cut "OUTPUT" off mid-word. Someone comparing models on a phone
     saw the input rate and had to discover a horizontal scrollbar to learn what
     the output costs, and output is the larger number on every row.

     So below 640px the table stops being a table: each row becomes a card, and
     each cell carries its own column name from `data-label`. Both prices land in
     the first viewport, nothing is hidden behind a gesture, and the markup stays
     a real <table> for screen readers and for anyone copying it out. */
  .scroll { overflow-x: visible; border: 0; background: none; }
  .scroll table { min-width: 0; }
  .scroll thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .scroll tbody tr {
    display: block;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-2);
    padding: 6px 2px;
    margin-bottom: 12px;
  }
  .scroll tbody tr:last-child { margin-bottom: 0; }
  .scroll tbody td {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 16px;
    padding: 9px 16px;
    border-bottom: 1px solid var(--line);
  }
  .scroll tbody tr td:last-child { border-bottom: 0; }
  /* The column name, restored per cell. `attr()` keeps it in the markup rather
     than duplicating every heading in CSS, so a renamed column renames here too. */
  .scroll tbody td::before {
    content: attr(data-label);
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-3);
    flex: 0 0 auto;
  }
  /* The model name is the card's heading, not one field among four. */
  .scroll tbody td:first-child {
    font-weight: 600;
    font-size: 16px;
    padding-top: 4px;
  }
  .scroll tbody td:first-child::before { content: none; }
  .scroll tbody td.num { white-space: normal; text-align: end; }
  /* A prose cell — quickstart's "What to do" — needs the whole card width, not
     the right half of a two-column row, or every sentence wraps to three words. */
  .scroll tbody td:not(.num):not(:first-child) {
    display: block;
    text-align: start;
  }
  .scroll tbody td:not(.num):not(:first-child)::before { display: block; margin-bottom: 4px; }
}

@media (max-width: 900px) and (min-width: 641px) {
  .nav { gap: 18px; }
}


/* ── Themes ──────────────────────────────────────────────────────────────────
   Three states, matching the convention: explicit dark, explicit light, and
   "follow the system". The explicit choice is stamped on <html> so it beats the
   media query in both directions — a toggle that only works one way is the
   usual bug here. */
html:root[data-theme="light"] {
  --bg: #ffffff;
  --bg-2: #f7f8fa;
  --line: #e4e7eb;
  --line-2: #d3d8de;
  --ink: #0d0f12;
  --ink-2: #48505a;
  --ink-3: #6f7780;
  --accent: #4d7c0f;
  --accent-ink: #ffffff;
  --header-bg: rgba(255,255,255,.9);
}
@media (prefers-color-scheme: light) {
  html:root:not([data-theme="dark"]):not([data-theme="light"]) {
    --bg: #ffffff;
    --bg-2: #f7f8fa;
    --line: #e4e7eb;
    --line-2: #d3d8de;
    --ink: #0d0f12;
    --ink-2: #48505a;
    --ink-3: #6f7780;
    --accent: #4d7c0f;
    --accent-ink: #ffffff;
    --header-bg: rgba(255,255,255,.9);
  }
}

.themes { display: inline-flex; border: 1px solid var(--line-2); border-radius: 9px; overflow: hidden; }
.themes button {
  background: none; border: 0; color: var(--ink-3); cursor: pointer;
  padding: 7px 10px; font: inherit; font-size: 13px; line-height: 1;
}
.themes button[aria-pressed="true"] { background: var(--bg-2); color: var(--ink); }

/* The nav's control cluster: colour scheme and language, side by side. `.langs`
   mirrors `.themes` on purpose — same border, radius, padding and pressed
   treatment — so the two read as one control strip rather than as a widget and
   a link that happen to be adjacent. */
.navctl { display: inline-flex; align-items: center; gap: 10px; }
.langs { display: inline-flex; border: 1px solid var(--line-2); border-radius: 9px; overflow: hidden; }
.langs a {
  text-decoration: none; color: var(--ink-3); cursor: pointer;
  padding: 7px 10px; font: inherit; font-size: 13px; line-height: 1; font-weight: 600;
  /* Both labels are Latin-or-Arabic tokens, never prose: isolate them so an RTL
     document cannot reorder "EN" against the glyph beside it. */
  direction: ltr; unicode-bidi: isolate;
}
.langs a[aria-current="page"] { background: var(--bg-2); color: var(--ink); }
.langs a:hover { color: var(--ink); }
/* THE TOGGLE STAYS ON A PHONE. It was `display: none` here with no replacement
   anywhere — a dark-first site whose light/dark/system control the phone could
   not reach, which is the viewport where the OS scheme matters most. The
   narrow-viewport block above hides the secondary nav links and defends it by
   saying "they all survive in the footer, so nothing becomes unreachable". This
   control survives nowhere, so the same sentence says keep it.

   Measured at 375px: brand 77 + Sign in 78 + two 14px gaps leaves 156, and
   three buttons at this padding need 113. The padding grows rather than the
   font, so the target is 41px tall instead of 27. */
@media (max-width: 640px) {
  .themes button { padding: 13px 12px; }
  /* The language links sit in the same strip and must be the same height: at the
     base padding they measured 29px against the buttons' 41, which reads as two
     unrelated widgets on one row and gives the smaller one a sub-44px target. */
  .langs a { padding: 13px 12px; }
}

/* REVISITED when the wordmark became "African Open Router".

   The budget above was computed against a 77px mark. The new one needs 158.7px
   on one line at the same weight — measured in the browser, not estimated — so
   the nav no longer holds all three: 158.7 + 120.3 for Sign in + 28 of gap is
   307 of the 339 a 375px viewport leaves inside `.wrap`, and the toggle needs
   74.5. At 320px it is worse. The mark wrapped to two lines and the header grew
   to 110px, which reads as a broken header rather than a renamed one.

   The rule above says do not delete a control that survives nowhere. So it was
   given somewhere to survive instead: the same three buttons render in the
   footer below 640px, and the nav copy is hidden. No JavaScript changed —
   `querySelectorAll(".themes button")` already binds every group on the page
   and stamps `aria-pressed` on all of them, so the two stay in step by
   construction rather than by a second code path.

   Shrinking the mark instead was measured and rejected: keeping all three in
   the nav needs a ~12px wordmark, and a brand small enough to fit is a worse
   answer than a brand that moved one control. */
.ftheme { display: none; }

/* SUPERSEDED, deliberately, and the arithmetic above is still correct — a third
   control does not fit on that line at 375px and never will.

   What changed is the ask: both controls must be visible, and the language
   switcher is a second one, so the footer trick does not scale. The row does.
   Below 640px the nav wraps and the cluster takes a centred row of its own:
   brand and Sign in keep their line at full size, `.navctl` gets the next one,
   and nothing is hidden at any width.

   This is NOT the failure the note above rejected. That one was the WORDMARK
   wrapping mid-phrase into a header that read as broken. This is a second row
   the layout asks for, with its own content, centred — and the header is 104px
   instead of 58px because it now carries four controls instead of two. */
@media (max-width: 640px) {
  .nav { flex-wrap: wrap; height: auto; padding-block: 9px; row-gap: 9px; }
  .nav .brand { order: 1; }
  .nav .btn { order: 2; }
  .navctl { order: 3; width: 100%; justify-content: center; gap: 12px; }
}

/* Moving the toggle out bought enough room at 375px and NOT at 320px, which is
   the width this was nearly shipped wrong at: the header looked correct on the
   phone size everyone tests and still wrapped on the smallest one.

   The arithmetic, measured in the browser rather than estimated — the mark needs
   158.7px on one line, Sign in 120.3, the gap 14, and `.wrap` spends 36 on
   padding. So the two share a line only above 329px of viewport. 320px is the
   iPhone SE and the narrowest width worth supporting, so the mark steps down
   there instead of wrapping: at 15px it needs 142.8 and 284 are available. */
@media (max-width: 340px) {
  .brand { font-size: 15px; }
}


html:root[data-theme="dark"] {
  --bg: #08090a; --bg-2: #0f1113; --line: #1e2124; --line-2: #2a2e33;
  --ink: #f7f8f8; --ink-2: #a8b0b8; --ink-3: #6f7780;
  --accent: #c6f24e; --accent-ink: #0a0b0c;
  --header-bg: rgba(8,9,10,.9);
}

/* ── Card illustrations ──────────────────────────────────────────────────────
   Inline SVG, never an <img> or a CDN sprite: a Cloudflare Pages site with a
   strict CSP makes an external asset a silent blank box, and a diagram that
   fails to load is worse than none. Drawn from the product's own vocabulary —
   a hold becoming a charge, a ledger line, scoped keys, a sweep — rather than
   decorative icons. */
.card figure { margin: 0 0 16px; padding: 0; }
/* THE GEOMETRY IS LTR AND THE DOCUMENT IS RTL, and every label paid for it.

   These are hand-placed coordinates: `<text x="8">محجوز</text>` means "8px from
   the left of the bar it labels". Under `dir="rtl"` the inherited direction
   makes the default `text-anchor: start` resolve to the RIGHT edge, so the run
   is drawn LEFTWARD from x=8 — off the box and clipped by the svg's own
   viewport. Measured on the live site: 8 of the labels across the four cards
   were drawn outside their own <svg> box, at 375px AND at 1440px.

   What a reader saw was not an empty label. It was "مح" where "محجوز" belongs,
   and `models:invoke` struck through the key glyph beside it — a diagram that
   reads as a rendering fault rather than as a diagram.

   `direction: ltr` restores the authored frame. The Arabic inside each label
   still shapes and orders right-to-left, because bidi resolves that per run;
   only the base direction the coordinates are measured against changes. The
   flow arrow keeps pointing right, which is deliberate — these diagrams carry
   Latin identifiers and numbers, and mirroring the geometry is a redraw, not a
   fix for text nobody can read. */
.card figure svg { direction: ltr; }
.card svg { display: block; width: 100%; height: auto; }
.card svg .bg { fill: var(--bg); }
.card svg .ln { stroke: var(--line-2); stroke-width: 1.5; fill: none; }
.card svg .ac { stroke: var(--accent); stroke-width: 1.5; fill: none; }
.card svg .acf { fill: var(--accent); }
.card svg .mut { fill: var(--ink-3); }
.card svg .inkf { fill: var(--ink-2); }

/* Featured-model cards borrow the table type without the table chrome. */
.card table { font-size: 13.5px; }
.card td { border: 0; }
.card .mut { color: var(--ink-3); }

/* ── Search ──────────────────────────────────────────────────────────────────
   Real, not decorative. A search box that opens nothing is worse than no search
   box: it is a promise the page does not keep. This one filters the models and
   pages that actually exist, entirely client-side — the site is five static
   files, so there is nothing to query and no endpoint to be down. */
.search { position: relative; flex: 0 1 320px; margin-inline-end: auto; }
.search input {
  width: 100%; background: var(--bg-2); border: 1px solid var(--line);
  border-radius: 9px; color: var(--ink); font: inherit; font-size: 14px;
  padding: 8px 46px 8px 32px;
}
.search input::placeholder { color: var(--ink-3); }
.search input:focus { outline: none; border-color: var(--line-2); }
.search .mag { position: absolute; inset-inline-start: 11px; top: 50%; transform: translateY(-50%); color: var(--ink-3); font-size: 13px; pointer-events: none; }
.search kbd {
  position: absolute; inset-inline-end: 9px; top: 50%; transform: translateY(-50%);
  border: 1px solid var(--line-2); border-radius: 5px; padding: 2px 6px;
  font: inherit; font-size: 11px; color: var(--ink-3); pointer-events: none;
}
.results {
  position: absolute; inset-inline: 0; top: calc(100% + 8px);
  background: var(--bg-2); border: 1px solid var(--line-2); border-radius: 10px;
  padding: 6px; display: none; max-height: 340px; overflow-y: auto; z-index: 40;
}
.results.open { display: block; }
.results a { display: block; padding: 9px 11px; border-radius: 7px; text-decoration: none; font-size: 14px; }
.results a:hover, .results a.on { background: var(--bg); }
.results .t { color: var(--ink); }
.results .d { color: var(--ink-3); font-size: 12.5px; }
.results .none { padding: 12px; color: var(--ink-3); font-size: 13.5px; }
@media (max-width: 900px) { .search { display: none; } }


/* ── Direction ───────────────────────────────────────────────────────────────
   The document is Arabic and RTL. These are the islands that must not turn.

   `pre` is the one that matters: a shell or Python line inside an RTL block gets
   its leading `#`, `$` and quotes reordered by the bidi algorithm, so the line
   reads correctly character by character and wrongly as a command. Same for a
   model id, a price and a URL — they are identifiers, not prose.

   `unicode-bidi: isolate` and not just `direction: ltr`: without isolation the
   surrounding RTL paragraph still reorders the run as a whole. */
pre, code, kbd, .mono, .num, .k,
[dir="ltr"] {
  direction: ltr;
  unicode-bidi: isolate;
  text-align: left;
}

/* The brand is a Latin wordmark and stays one. */
.brand { direction: ltr; unicode-bidi: isolate; }
