/* ==========================================================================
   DataSouq — design tokens

   Built the way the Supabase design system builds them: each theme sets a
   handful of scalars, and every colour is DERIVED from those in OKLCH.
   Nothing below the knobs is hand-picked.

   TWO APPS SHIP THIS SYSTEM AND THEY DISAGREE. apps/design-system is Studio —
   dense panels inside an application. apps/www is supabase.com — a landing page.
   Where they differ, this page follows apps/www, because that is what it is.
   That fork has been taken for the type scale (--text-sm 14px, not Studio's 13),
   the container ladder, the hero band, the section rhythm, card padding, the
   card title weight, the section header's internal gaps and the whole footer.
   apps/www does not import typography.css, so where an element role is in
   question apps/www wins there too.

   Traced to:
     packages/ui/build/css/source/semantic.css         derivation chain
     packages/ui/build/css/source/compat.css           the border/foreground aliases
     packages/ui/build/css/source/global.css           card padding tokens
     packages/ui/build/css/themes/light.css            light knobs + brand scale
     packages/ui/build/css/themes/classic-dark.css     dark knobs + brand scale
     packages/config/css/theme.css                     --radius-panel: 6px, font tokens
     packages/config/css/utilities.css                 focus-ring, focus-inset
     packages/config/typography.css                    element type roles
     packages/ui/src/components/Button/Button.tsx      variants
     packages/ui/src/lib/constants.ts                  size scale
     packages/ui/src/components/shadcn/ui/card.tsx     Card and its sections
     packages/ui-patterns/src/PageSection|PageHeader|PageContainer
     packages/ui-patterns/src/MetricCard                the metric row's pairing
     packages/ui-patterns/src/EmptyStatePresentational   the panel
     packages/ui-patterns/src/SkipToContent              the skip link's contract
     apps/design-system/styles/globals.css             the Inter-tuned scale
     apps/design-system/content/docs/accessibility.mdx focus, tabindex, sr-only
     apps/design-system/content/docs/icons.mdx         Lucide, tints, custom SVGs
     apps/www/styles/globals.css                       supabase.com's own scale
     apps/www/components/Nav, Footer, UI/SectionHeader
     apps/www/app/(home)/_components/Hero, OpenSourceSection
     apps/www/app/(home)/_components  the product-card row, measured live:
       grid-cols-12 gap-3, col-span-6 xl:col-span-3, sm:h-[400px], rounded-xl,
       a p-px gradient border, 24px/16px padding, an items-center gap-2 title
       row with an 18px icon, Manrope 600 16/24, and a 14/20 body whose strong
       lifts the colour only
     supabase.com/features  the feature-card family, measured live across all
       81 cards: a p-px gradient border, an inner rounded-[11px] flex column at
       gap-2 p-2, then a tinted aspect-square panel 80px tall (140px from md)
       holding a 20px Lucide mark, and BELOW it a flex-col gap-1.5 text column
       with the heading at text-sm md:text-base leading-5. Not one of the 81
       puts its icon on the title's line.
     apps/www/app/(products)/…/_components               bordered card precedents

   Every deviation, and why. Nothing deviates without a reason that changes the
   outcome — and nothing here is left asserted only at the rule it governs:

     1. IBM Plex Sans Arabic is appended to both font stacks, and
        :root[lang="ar"] drops --font-weight-normal to 400. Inter ships a real
        450; Plex Arabic ships whole hundreds, and CSS font matching resolves
        450 upward onto the 500 face — measured, 450 and 500 set the same string
        at 498.00px against 400's 485.81px. Left at 450 the Arabic ran a full
        step heavier than the English beside it and `strong`, being font-medium
        at 500, became the same face as its own paragraph.

     2. CardTitle is `text-xs font-mono uppercase` upstream, and CardHeader /
        CardContent / CardFooter split with an internal border. Both are dense
        Studio idioms: uppercase carries no meaning in Arabic, Plex Arabic has
        no mono companion, and the three small cards hold a title and two lines,
        so there is nothing to divide. The dataset card does divide, once, above
        its metric row.

     3. The section eyebrow keeps 14px sentence-case sans in --brand-text where
        every apps/www definition of it — SectionHeader's subtitle, the `.label`
        utility, typography.css's heading-meta — is 12px mono uppercase
        tracking-wider in --foreground-lighter. Three of those five properties
        cannot come along: uppercase means nothing in Arabic, there is no Arabic
        mono, and positive tracking breaks Arabic joins. The size and the colour
        are what compensate for their loss; without them nothing would mark the
        line as a label. Reasoned in full at .head__label.

     4. Every body paragraph keeps --muted-foreground where upstream uses
        --foreground-lighter. On --card that token measures 4.27:1 in dark and
        fails AA outright. On --background it measures 5.42:1 light and 4.59:1
        dark — passing, but by 0.09 at 16px/450, about 2% of headroom, on a
        colour that moves with any knob edit. --muted-foreground is 9.29:1 and
        8.22:1. Two surfaces, one answer, for two different reasons.

     5. The brand icon plate (.card__icon) has no upstream source AS A BRAND
        FILL. A plate as such is not unprecedented, and this said so too
        absolutely until the verification pass caught it: ui-patterns'
        GlassPanel takes an optional `showIconBg`, and its IconBackground is
        `bg-surface-75 border w-8 h-8 flex items-center justify-center
        rounded-sm` around an `img className="w-5"` — read from source, a
        NEUTRAL surface chip, 20px of mark in a 32px box at the default border.
        None of that geometry is imported here and the fill is the part that is
        ours: brand-default at 10% behind a --brand-500 stroke and a
        brand-tinted glyph, where upstream's is a surface tint behind a logo.
        Every other upstream icon in a card sits bare, tinted with a text class,
        which is what icons.mdx prescribes and all it prescribes. The plate is a
        landing-page accent, kept deliberately, and measured safe on the surface
        it actually paints: brand-default at 10% over --card gives 4.87:1
        against --brand-600 in light and 8.83:1 in dark, both clear of 1.4.11's
        3:1.
        The empty-state panel does NOT use it, because a plate is a Card
        affordance and that panel is not a card.

     6. The hero is centred, single-column, with full-width buttons below 640px
        and a rule under the band. supabase.com's hero is start-aligned, splits
        into two columns at lg, stretches nothing and carries no border. Centring
        is language-neutral, so the band needs no RTL work at all; an asymmetric
        two-column hero needs more copy than one dataset justifies; and on a page
        with three sections one rule closes the band that the padding alone would
        leave open. Its padding IS theirs: 48/32 below md, 160/64 from it.

     7. The header bar and the WHOLE footer stay put in Arabic. Asked for
        directly: a control that relocates itself, along with the wordmark,
        reads as the page jumping. Both language and theme now sit in the footer
        bar, which was pinned for the same reason and measures identically in
        both languages — the group at 1008-1169 at 1280 either way — so the rule
        followed them rather than being restated. The header bar keeps the
        wordmark and nothing else. The skip link keeps its physical `left`
        because it still sits inside that bar's footprint. The page body between
        them mirrors as usual.

        This said "footer" from the start and only ever covered its bottom bar.
        .nav__inner and .footer__bar carried direction: ltr; .footer__grid did
        not, so in Arabic the brand block crossed 778px to the right (left 96 to
        874 at 1280), the link block came back the other way (407 to 96) and the
        two columns swapped (439/563 to 718/593), all above a copyright strip
        that had not moved. The grid is pinned now, and measured in both
        languages at 375, 768 and 1280 every box in the footer sits at the same
        x: wordmark 24/24/96, channel row and form 24-298/24-298/96-370, columns
        @24/@24+@385/@439+@563, the copyright at 24/24/96 and GitHub at
        329/707/1147.

        The footer is also translated now, which is the other half of the same
        request, and pinning the layout is not the same as typesetting the text
        LTR: every text-bearing block inside it turns RTL in Arabic so
        punctuation lands on the correct side, inside a box that has not moved.
        The wordmark, the channel row and the copyright's Latin head do not
        turn — a name, a lockup, and a mixed run whose Arabic clause is isolated
        so the © and the year stay at the left. Nor does the form: it is a
        column flex at align-items: flex-start, and flex-start is
        direction-relative, so turning the form put the Subscribe button at the
        right edge (96-176 in English against 320-370 in Arabic). Its two text
        children turn instead — both 274px wide, so they fill the block and
        nothing moves when they do. The button also holds its width, 80px in
        both languages, against an intrinsic 79.63 for "Subscribe" and 46.88
        for "اشترك"; the longer label sets the box and the shorter centres in
        it. Measured: 24-104 at 375 and 768, 96-176 at 1280, in both.

     8. The metric row compresses MetricCard: 14px figure inline with its label,
        where upstream is an 18px tabular figure above a smaller one. Five
        metrics in one card is not a shape upstream has — it lays one metric per
        Card — so the row is ours, built from MetricCard's parts. tabular-nums,
        items-start and the --muted-foreground icon tint are all its.

     9. The empty-state panel keeps a 16px title and a --card background where
        EmptyStatePresentational has neither an explicit title size nor a fill.
        Everything else about it is the fragment: dashed border, no shadow,
        px-4 py-10, a bare 24px icon at stroke 1.5, a 14px description at 640px,
        12px down the stack.

    10. The dataset card is their product card on a wider span. Theirs is
        col-span-3 at xl — 259.25px, four across; ours is span 4 at 1024 —
        354.66px at the capped container, three across — because theirs holds a
        title, two lines and an illustration while this one holds a paragraph,
        five metrics and an action. h-[400px] becomes a min-height for the same
        reason it always does here: the Arabic body sets longer than the English
        and a fixed height would clip it. The 12px radius and 24/16 padding are
        NOT taken; every card on this page keeps the family's 8px and 24px,
        because the dataset card and the three service cards sit one section
        apart in the same scroll and a different corner would read as a mistake.
        The title weight is the one place the difference says something — 600
        against .card h3's 500, the object against the work — so that is taken.

    11. The icon-beside-title row is on ALL FOUR cards, and upstream would put
        it on one. Their product cards use it; their feature cards — the family
        the three service cards belong to — do not, on two independent readings.
        Measured across all 81 cards on supabase.com/features: every one centres
        a 20px mark in a tinted panel ABOVE the heading, none shares the
        heading's line. And read from source, the closest analogue there is to
        these three — a bordered card at p-6 holding an icon, a title and one
        line — is Highlights.tsx under
        apps/www/app/(products)/edge-functions/_components:
        `flex flex-col gap-3`, then a BARE 20px mark in
        --foreground-lighter, then `h3 text-base font-medium`, then a text-sm
        paragraph. It stacks. It also settles two things this file already had
        right: the title at font-medium, which is .card h3's 500, and the plate
        as ours alone — upstream's mark there has no plate at all (deviation 5).
        Asked for directly, and the reason holds: this page is one scroll with
        four cards in two sections, where upstream's two families live on
        different pages and never appear together. What the service cards had
        before was neither idiom anyway — a 32px plate stacked above a title,
        which upstream has nowhere — so this moves them ONTO an upstream shape
        rather than away from one.

        Two things fall out of sharing the row. The plate is 24px with an 18px
        mark for every card, and the 18px is the painted hairline, not a box
        rule: stroke-width 2 over a 24-unit viewBox at 18px paints 1.5px, which
        is the figure the stroke note below pins everything else to. It is NOT
        icons.mdx's 18-in-24 — that rule governs custom SVGs, as this header
        says two paragraphs down, and all four plate marks are Lucide drawings.
        The 32px plate was sized to be stacked, and beside a 24px line it
        overhung by 4px each side. And .card became the flex column their content column
        is — `flex flex-col gap-2 sm:gap-4`, so 8px here and 16px on the
        dataset card — which let `.card h3`'s margin go: inside a title row
        that margin belongs to the title's box, not under it. Highlights.tsx
        sets its card gap to 12px, and that is not taken: 8px and 16px are one
        component's `gap-2` and `sm:gap-4`, and importing 12px from a third
        component would break a pairing that comes from one.

        align-items is flex-start, not their center. Their product-card titles
        are single words that never wrap; one of ours does. Measured at 768px,
        each service card is 227px with a 177px text box, the plate and gap take
        32px, and "Cleaning & structuring" needs 168.88px of the 145px left — so
        it runs to two lines below 250.88px of card width, which in this pane is
        a viewport of 839px against 840px. items-center would
        centre the plate against that 48px box and drop it 12px below the line
        it labels. flex-start pins it to the first line, and since the plate is
        24px and the line-height is 24px the two are concentric either way for
        a single-line title — measured, plate centre 12 and line centre 12 in
        both languages at every width.

    12. Arabic sets Arabic-Indic digits, with the Arabic punctuation that goes
        with them: U+066C for thousands, U+066B for the decimal, U+066A for
        percent. So ١٧٬٣٠٤ and ٩٩٫٥٪ where English has 17,304 and 99.5%. Asked
        for directly. Worth knowing that Saudi Arabia, which is the market this
        dataset covers, uses Western digits far more widely than Egypt does; if
        that turns out to matter, the change is transcribing five values and two
        labels in the Arabic block back, not a setting.

        All five glyphs are real in IBM Plex Sans Arabic, not fallbacks:
        measured at 12px, ٠ sets 3.38px, ٩ 6.10, ٪ 6.79, ٫ 3.26 and ٬ 3.38,
        each against the 6.31/3.83 the same characters get from the font stack
        with Plex removed. Different advances mean Plex is drawing them.

        The digits are written into the Arabic dictionary as literals rather
        than converted at render time, because that is what they are. The one
        number the page computes is the year, and that goes through a per-
        language digits() so the two dictionaries have the same shape and the
        caller never branches. Machine-readable numbers — the JSON-LD figures,
        the aria-labels — stay Western on purpose.

   Not a deviation, though it may look like one: WhatsApp green is simply
   absent, and the brand token is the compliant choice for the CTA. The Lucide
   icons vary from 75% to 83% of their box in whichever dimension leads — that
   is Lucide's own optical grid, and icons.mdx's 18-in-24 rule governs custom
   SVGs, not Lucide's drawings. Only the footer's channel marks are normalised
   to their box, because those come from six different designers and share no
   optical system.

   Nor is the stroke width, though the attributes disagree: Lucide's own 2 sits
   on the smaller icons and 1.5 on the panel's. What upstream fixes is the
   PAINTED hairline — 1.5 user units at a 24px box, so 1.5 device px — and a
   stroke scales with its box. Measured: 2 at 18px paints 1.5px on the card
   plates, 1.5 at 24px paints 1.5px on the panel, and the small ones come in
   under — 1.333px at the metric row's 16px and 1.167px at the 14px of the
   language and theme controls, which .ctl svg states outright. This is what pins the plate's
   mark at 18px: it briefly went to 14px, which painted 1.167px — thinner than
   anything else on the page, including the metric icons it sits above. Setting every attribute to
   1.5 for the look of consistency would paint 1.125px and 1.0px instead —
   thinner than upstream everywhere but the panel, and less uniform than what
   is here.

   Unused but kept: --secondary and --elevation-3 complete the elevation ladder
   whose other two rungs are painted; --border-stronger completes the border
   ladder; --text-3xl, --lh-lg, --lh-2xl and --lh-3xl complete contiguous type
   steps. Machinery that produced a colour nothing painted has been deleted, not
   kept — the --accent chain went that way.

   Spacing follows Tailwind's 4px base, which neither globals.css overrides.
   Section rhythm and the container ladder come from apps/www's SectionContainer;
   the section-header-to-content gap is PageSection's gap-6, the gaps inside the
   header are SectionHeader's space-y-4 and mt-4, and action-group gaps are
   Button's space-x-2. Button sizes in use are tiny (the footer's Subscribe,
   the theme control and each language option) and medium (everything else);
   small has no user left since the controls stepped down to match their
   header's own right-hand group.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Shared scalars and derived colours — formulas verbatim from semantic.css.
   Declared FIRST so a theme can override any knob, and so a theme that omits
   --elevation-step inherits the derived default, exactly as classic-dark does.
   -------------------------------------------------------------------------- */
:root {
  --contrast: 0.5;
  --contrast-delta: calc((var(--contrast) - 0.5) / 0.5);
  --contrast-up: max(0, var(--contrast-delta));
  --contrast-down: max(0, calc(var(--contrast-delta) * -1));
  --contrast-text-floor: 0.9;
  --contrast-border-floor: 0.05;
  --contrast-text: calc(1 - (1 - var(--contrast-text-floor)) * var(--contrast-down));
  --contrast-border-linear: calc(
    var(--contrast-border-floor) + (1 - var(--contrast-border-floor)) * var(--contrast)
  );
  --contrast-border: calc(var(--contrast-border-linear) * var(--contrast-border-linear));

  --elevation-1: 1;
  --elevation-2: 1.5;
  --elevation-3: 2;

  /* Signed span surface -> foreground: negative in light, positive in dark */
  --tone-span: calc(var(--foreground-lightness) - var(--surface));

  /* Default step. A theme may override it; classic-dark does not. */
  --elevation-step: calc(var(--tone-span) * 0.04);

  --surface-hue: var(--hue);
  --foreground-chroma: calc(var(--chroma) * 0.55);

  /* --- Derived colours (no relative colour syntax needed) --------------- */
  --background: oklch(var(--surface) calc(var(--chroma) * 0.5) var(--surface-hue));

  --foreground: oklch(
    calc(var(--surface) + var(--tone-span) * var(--contrast-text)) var(--foreground-chroma)
      var(--surface-hue)
  );

  --muted-foreground-mix: calc(
    var(--muted-foreground-level) + (1 - var(--muted-foreground-level)) * var(--contrast-up) -
      var(--muted-foreground-level) * 0.5 * var(--contrast-down)
  );
  --muted-foreground: oklch(
    calc(var(--surface) + var(--tone-span) * var(--muted-foreground-mix)) var(--foreground-chroma)
      var(--surface-hue)
  );

  --tertiary-foreground-mix: calc(
    var(--tertiary-foreground-level) + (1 - var(--tertiary-foreground-level)) * var(--contrast-up) -
      var(--tertiary-foreground-level) * 0.5 * var(--contrast-down)
  );
  --tertiary-foreground: oklch(
    calc(var(--surface) + var(--tone-span) * var(--tertiary-foreground-mix))
      var(--foreground-chroma) var(--surface-hue)
  );

  /* --brand-link is tuned for brand text on the page background (4.75:1 on
     --card in light). On a --brand-plate fill it drops to 4.14:1, under the AA
     floor for normal text, so text sitting on a plate uses the next step down
     the same upstream scale: --brand-600 measures 4.57:1 there. */
  --brand-on-plate: var(--brand-600);

  /* The focus ring. Upstream builds it as the accent at 55%:
       --ring: oklch(from var(--primary) l c h / 55%)
     --primary is mode-invariant at L 0.76, so in the dark theme that lands on
     3.52:1 against the background — over the 3:1 that WCAG 1.4.11 asks of a
     focus indicator. In the light theme the same colour composites to a pale
     mint on a near-white page and measures 1.47:1. Not dim: invisible. And
     light is this page's default, so that is what most people would get.

     Every other brand step was measured against the light background too:
     --primary opaque 1.99, --brand-default 1.97, --brand-500 2.60. Only
     --brand-600 and --brand-link clear the bar, and --brand-600 clears it in
     both themes with room — 5.16:1 light, 11.36:1 dark — because that step of
     the scale flips light-on-dark with the theme. It stays one shared colour
     that no variant changes, which is the rule accessibility.mdx actually sets.

     Being a plain hsl() it also needs no relative colour syntax, so the ring
     leaves the @supports blocks entirely. */
  --ring: var(--brand-600);

  /* --- Radius ----------------------------------------------------------- */
  /* --radius-panel is 6px (theme.css) and buttons are rounded-md.
     Cards use rounded-lg; global.css sets --borderradius-lg: 8px. */
  --radius: 6px;
  --radius-lg: 8px;

  /* Card is `py-4 px-(--card-padding-x)`. Both of these are HORIZONTAL tokens
     in global.css — --card-padding-x is --padding-x-sm (px-4) and
     --card-padding-x-md is --padding-x-md (px-6). The vertical padding is py-4
     in every card, roomy or not. */
  --card-padding-x: 1rem;
  --card-padding-x-md: 1.5rem;

  /* Input backgrounds. semantic.css: `--field: oklch(0 0 0 / var(--field-alpha))`,
     a soft always-sink through black. 0.015 is the base value; dark.css raises it
     to 0.12, but this page follows classic-dark, which leaves it alone — so the
     border carries the field and the wash only softens it. */
  --field-alpha: 0.015;
  --field: oklch(0 0 0 / var(--field-alpha));

  /* The Card carries shadow-xs — Tailwind's own value, unchanged upstream. */
  --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);

  /* Tailwind's shadow-lg, for the one thing on the page that floats over the
     rest: the language menu. A popover needs to separate from what it covers,
     which shadow-xs — tuned to lift a card a hair off the page — cannot do. */
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);

  /* --- Type scale ------------------------------------------------------
     Two apps ship this scale and they differ in one place. Both override
     --text-base upward to 15px and set --font-weight-normal: 450, but
     apps/design-system also pulls --text-sm down to 13px for Studio density,
     while apps/www — supabase.com — leaves --text-sm and --text-xs at
     Tailwind's own 14px and 12px. This is a landing page, so it follows
     supabase.com. Measured there at 1280px: body and lead 16/24, card copy
     14/20, medium buttons 14/20, hero 46/46, section headings 34.
     ---------------------------------------------------------------------- */
  --text-xs: 0.75rem;      /* 12px */
  --text-sm: 0.875rem;     /* 14px */
  --text-base: 0.9375rem;  /* 15px */
  --text-lg: 1rem;         /* 16px */
  --text-xl: 1.125rem;     /* 18px */
  --text-2xl: 1.375rem;    /* 22px */
  --text-3xl: 1.75rem;     /* 28px */
  --text-4xl: 2.125rem;    /* 34px */
  --text-5xl: 2.875rem;    /* 46px */

  /* Line heights pair to each step. This table used to hard-code Tailwind's
     absolute defaults — 24px against text-base, 40px against text-4xl — on the
     stated grounds that globals.css overrides the sizes and "leaves Tailwind's
     paired line-heights untouched". The second half is true and the conclusion
     did not follow: Tailwind v4 states each pairing as a unitless RATIO, so
     overriding a size rescales its line-height with it. Six of the nine were
     wrong by 1.5 to 4px.

     Measured on supabase.com to settle it, and every ratio came back exact:
       text-xs    12 / 16      1.3333
       text-base  15 / 22.5    1.5        (this table said 24)
       text-xl    18 / 25.2    1.4        (said 28)
       text-2xl   22 / 29.333  1.3333     (said 32)
       text-4xl   34 / 37.778  1.1111     (said 40)
     So the ratios are written out here instead of their products, which also
     means a size change can no longer leave its line-height behind. */
  --lh-xs: calc(var(--text-xs) * 1 / 0.75);          /* 16px    */
  --lh-sm: calc(var(--text-sm) * 1.25 / 0.875);      /* 20px    */
  --lh-base: calc(var(--text-base) * 1.5);           /* 22.5px  */
  --lh-lg: calc(var(--text-lg) * 1.75 / 1.125);      /* 24.89px */
  --lh-xl: calc(var(--text-xl) * 1.75 / 1.25);       /* 25.2px  */
  --lh-2xl: calc(var(--text-2xl) * 2 / 1.5);         /* 29.33px */
  --lh-3xl: calc(var(--text-3xl) * 2.25 / 1.875);    /* 33.6px  */
  --lh-4xl: calc(var(--text-4xl) * 2.5 / 2.25);      /* 37.78px */
  --lh-5xl: 1;                                       /* leading-none */

  /* globals.css sets font-normal to 450 for prose, 400 inside mono/code */
  --font-weight-normal: 450;

  /* Button heights run tiny 26 / small 34 / medium 38 / large 42 / xlarge 50.
     Every button here is medium, which is the size supabase.com gives its own
     hero calls to action; .ctl is small and carries its 34px inline. */
  --btn-h-medium: 38px;

  /* SIZE.text in constants.ts gives the small and medium sizes
     `text-base md:text-sm`: text-base below the md breakpoint, text-sm from it
     up. Mobile-first, so the base value here is text-base and the media query
     below steps it down. large and xlarge are plain `text-base` at every width. */
  --text-responsive-sm: var(--text-base);

  /* --- Fonts: --font-sans is Inter, --font-heading is Manrope ----------- */
  --font: "Inter", "IBM Plex Sans Arabic", "Helvetica Neue", Helvetica,
    ui-sans-serif, system-ui, sans-serif;
  --font-heading: "Manrope", "IBM Plex Sans Arabic", "Helvetica Neue", Helvetica,
    ui-sans-serif, system-ui, sans-serif;

  /* The wordmark only. Kept off --font-sans so the logo never drifts with the
     body type. */
  --font-brand: "Space Grotesk", "Inter", "Helvetica Neue", Helvetica,
    ui-sans-serif, system-ui, sans-serif;

  /* h-16 plus the 1px bottom border, which is what supabase.com's own bar
     measures. 60px was a number with nothing behind it. */
  /* The channel row, described rather than hard-coded, because the subscribe
     field is sized from it: their field and their icon row come out within 3%
     of each other, and the row reads as one block only when they agree. Change
     the count when a channel is added or removed and the field follows. */
  --social-icon: 22px;
  --social-gap: 20px;
  --social-count: 7;
  --social-row: calc(
    var(--social-count) * var(--social-icon) +
    (var(--social-count) - 1) * var(--social-gap)
  );

  --nav-h: 65px;
  --container: 1280px;
  /* Tailwind ease-out, which the Button base class names. cubic-bezier(.4,0,.2,1)
     is ease-in-out and made every control feel a beat behind the cursor. */
  --ease: cubic-bezier(0, 0, 0.2, 1);
  /* Anything that just says `transition` or `transition-colors` — the footer
     links and icons do — gets Tailwind's default curve instead, which is not
     ease-out. Measured on supabase.com's footer: cubic-bezier(0.4, 0, 0.2, 1)
     over 150ms. Only Button asks for ease-out, and only Button gets it. */
  --ease-default: cubic-bezier(0.4, 0, 0.2, 1);
}

/* --------------------------------------------------------------------------
   Light theme — knobs from light.css

   The selector must NOT include a bare `:root`. `:root` matches <html> in every
   theme, so any knob the dark block does not itself declare would leak out of
   here — which is how --surface-hue and --elevation-step ended up wrong in dark.
   Upstream avoids this by scoping light to [data-theme='light'], .light only.
   `:not([data-theme="dark"])` keeps light as the default when no attribute is set.
   -------------------------------------------------------------------------- */
:root:not([data-theme="dark"]) {
  --hue: 159;
  --surface-hue: 34;
  --chroma: 0;
  --surface: 0.995;
  --elevation-step: 0.024;
  --contrast: 0.53;
  --foreground-lightness: 0.1;
  --muted-foreground-level: 0.65;
  --tertiary-foreground-level: 0.5;

  /* Brand scale: per-theme literals upstream, so literal here too.
     Exact HSL values from light.css. */
  --brand-default: hsl(152.9 60% 52.9%);
  --brand-link: hsl(153.4 86.5% 27.8%);
  --brand-200: hsl(147.6 72.5% 90%);
  --brand-400: hsl(151.3 66.9% 66.9%);
  --brand-500: hsl(155.3 78.4% 40%);
  --brand-600: hsl(156.5 86.5% 26.1%);

  /* Button primary, light: bg-brand-400 / hover:bg-brand/80 / border-brand-500/75 */
  --brand-fill: var(--brand-400);
  --brand-fill-hover: color-mix(in srgb, var(--brand-default) 80%, transparent);
  --brand-fill-border: color-mix(in srgb, var(--brand-500) 75%, transparent);
  /* hover:border-brand-600 */
  --brand-fill-border-hover: var(--brand-600);

  /* default variant: bg-background in light, dark:bg-card in dark */
  --btn-default-bg: var(--background);
  --brand-plate: var(--brand-200);
  --brand-text: var(--brand-link);

  color-scheme: light;
}

/* --------------------------------------------------------------------------
   Dark theme — knobs from classic-dark.css.
   It sets no --elevation-step and no --contrast, so both fall back to the
   derived defaults above, exactly as upstream.
   -------------------------------------------------------------------------- */
[data-theme="dark"] .nav {
  background: color-mix(in srgb, var(--background) 95%, transparent);
}

[data-theme="dark"] {
  --hue: 159;
  --chroma: 0.014;
  --surface: 0.205;
  --foreground-lightness: 0.97;
  --muted-foreground-level: 0.72;
  --tertiary-foreground-level: 0.52;

  /* Exact HSL values from classic-dark.css */
  --brand-default: hsl(153.1 60.2% 52.7%);
  --brand-link: hsl(153.1 60.2% 52.7%);
  --brand-200: hsl(152.5 75% 6.3%);
  --brand-400: hsl(153.3 65.2% 13.5%);
  --brand-500: hsl(153.5 61.8% 21.6%);
  --brand-600: hsl(153 59.5% 70%);

  /* Button primary, dark: bg-brand-500 / hover:bg-brand/50 / border-brand/30 */
  --brand-fill: var(--brand-500);
  --brand-fill-hover: color-mix(in srgb, var(--brand-default) 50%, transparent);
  --brand-fill-border: color-mix(in srgb, var(--brand-default) 30%, transparent);
  /* dark:hover:border-brand */
  --brand-fill-border-hover: var(--brand-default);

  --btn-default-bg: var(--card);
  --brand-plate: var(--brand-400);
  --brand-text: var(--brand-link);

  color-scheme: dark;
}

/* --------------------------------------------------------------------------
   Derived colours that need relative colour syntax.

   These sit on a bare `:root`, which the theme selectors outrank: `:root:not(
   [data-theme="dark"])` scores (0,2,0) against this block's (0,1,0), and
   `@supports` adds nothing. A literal declared beside the knobs therefore BEAT
   its derived counterpart — which is exactly what happened: light ran entirely
   on hand-written literals while dark ran on the formulas, in the same browser.
   So the fallbacks now live in the mutually exclusive block below instead, and
   only one of the two can ever apply.
   -------------------------------------------------------------------------- */
@supports (color: oklch(from white l c h)) {
  :root {
    /* Each surface steps its lightness away from --background by
       --elevation-step * ratio. The step is signed, so "higher = lighter"
       holds in both themes. */
    --card: oklch(from var(--background) calc(l + var(--elevation-step) * var(--elevation-1)) c h);
    --popover: oklch(from var(--background) calc(l + var(--elevation-step) * var(--elevation-2)) c h);
    --secondary: oklch(from var(--background) calc(l + var(--elevation-step) * var(--elevation-3)) c h);

    /* The same ladder again, as an alpha over whatever is behind instead of a
       lightness step away from --background. semantic.css, verbatim:
         --surface-overlay-unit: elevation-step / |tone-span|
         --muted-alpha:  unit * --elevation-1
         --accent-alpha: unit * --elevation-2
         --muted:  oklch(from var(--foreground) l c h / var(--muted-alpha))
         --accent: oklch(from var(--foreground) l c h / var(--accent-alpha))
       This chain had been deleted for producing colours nothing painted. The
       language toggle paints both: --accent is its checked fill and --muted its
       hover, which is what toggleVariants asks for. */
    --surface-overlay-unit: calc(
      var(--elevation-step) / max(var(--tone-span), calc(var(--tone-span) * -1))
    );
    --muted-alpha: calc(var(--surface-overlay-unit) * var(--elevation-1));
    --accent-alpha: calc(var(--surface-overlay-unit) * var(--elevation-2));
    --muted: oklch(from var(--foreground) l c h / var(--muted-alpha));
    --accent: oklch(from var(--foreground) l c h / var(--accent-alpha));

    /* The border ladder, verbatim. Every step is the foreground at an alpha
       ramped by --contrast-border; none of these numbers are ours.
         --border               semantic.css
         --border-strong        compat.css aliases it to --input, so --input's formula
         --border-stronger      compat.css, its own alpha
         --border-control-hover semantic.css, the control hover stroke
       --border-muted is `var(--border)` upstream, an identical legacy alias, so
       it is simply absent here. */
    --border: oklch(
      from var(--foreground) l calc(c * 0.54) h / calc(2% + 20% * var(--contrast-border))
    );
    --border-strong: oklch(
      from var(--foreground) l calc(c * 0.5) h / calc(3% + 38% * var(--contrast-border))
    );
    --border-stronger: oklch(
      from var(--foreground) l calc(c * 0.5) h / calc(5% + 45% * var(--contrast-border))
    );
    /* Not an alias of --border-stronger, though it sits next to it: semantic.css
       gives it its own ramp, and the default Button hovers to this one. */
    --border-control-hover: oklch(
      from var(--foreground) l calc(c * 0.5) h / calc(12% + 58% * var(--contrast-border))
    );
  }
}

/* --------------------------------------------------------------------------
   The same colours for browsers without relative colour syntax, resolved to
   sRGB from the formulas above. `not` makes this block and the one above
   mutually exclusive, so neither can outrank the other.
   -------------------------------------------------------------------------- */
@supports not (color: oklch(from white l c h)) {
  :root:not([data-theme="dark"]) {
    --card: #ffffff;
    --popover: #ffffff;
    --secondary: #ffffff;
    --muted: rgba(3, 3, 3, 0.027);
    --accent: rgba(3, 3, 3, 0.040);
    --border: rgba(3, 3, 3, 0.081);
    --border-strong: rgba(3, 3, 3, 0.146);
    --border-stronger: rgba(3, 3, 3, 0.188);
    --border-control-hover: rgba(3, 3, 3, 0.298);
  }

  [data-theme="dark"] {
    --card: #1c1f1d;
    --popover: #1f2321;
    --secondary: #232724;
    --muted: rgba(243, 246, 244, 0.040);
    --accent: rgba(243, 246, 244, 0.060);
    --border: rgba(243, 246, 244, 0.075);
    --border-strong: rgba(243, 246, 244, 0.135);
    --border-stronger: rgba(243, 246, 244, 0.174);
    --border-control-hover: rgba(243, 246, 244, 0.280);
  }
}


/* --------------------------------------------------------------------------
   Arabic runs one weight lighter, and has to.

   Inter ships a real 450 face, which is why the system asks for it. IBM Plex
   Sans Arabic ships whole hundreds only, and CSS font matching resolves a
   desired weight between 400 and 500 upward first — so 450 lands on the 500
   face. Measured at 100px: 450 and 500 both set `سجل ومدينة` at 498.00px while
   400 sets it at 485.81px. Two things followed from that. Arabic body copy
   rendered a full step heavier than the English beside it, and `strong`, which
   is font-medium at 500, became literally the same face as its own paragraph —
   emphasis with nothing to show for it.

   400 is the Regular face, so body copy reads as body copy and strong reads as
   strong. Latin text inside an Arabic page drops from Inter 450 to Inter 400,
   a 0.7% difference in set width. That is the whole cost.
   -------------------------------------------------------------------------- */
:root[lang="ar"] { --font-weight-normal: 400; }

/* The md half of `text-base md:text-sm`. Tailwind's md breakpoint is 768px. */
@media (min-width: 768px) {
  :root { --text-responsive-sm: var(--text-sm); }
}

/* ==========================================================================
   Reset
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 16px);
}

body {
  font-family: var(--font);
  background: var(--background);
  color: var(--foreground);
  font-size: var(--text-lg);
  font-weight: var(--font-weight-normal);
  /* Not --lh-base. supabase.com's body measures 16/24 — ratio 1.5, which is
     Tailwind preflight's `html { line-height: 1.5 }`, not text-base's pairing.
     Their unclassed paragraphs, the hero lead among them, all inherit it, so
     the 16px prose below inherits this rather than naming a token. */
  line-height: 1.5;
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  transition: background-color 0.2s var(--ease), color 0.2s var(--ease);
}

/* globals.css: h1–h6 get font-heading and font-semibold. All of them. */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  font-weight: 600;
}

img, svg { display: block; max-width: 100%; }
/* Preflight gives `strong` font-weight: bolder, which against a 450 body
   resolves to 700 — a jump the page has no use for. typography.css would make
   it font-medium, but apps/www does not import typography.css, and measuring
   their product card settles it: the <strong> in its body computes to 450, the
   inherited weight, with `[&_strong]:text-foreground!` lifting the colour and
   nothing else. So the two-tone body is a tone change, not a weight change.
   .metrics strong keeps its explicit 500 — that pairing is MetricCard's. */
strong { font-weight: inherit; }
button { background: none; border: none; cursor: pointer; font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; }

/* --------------------------------------------------------------------------
   Focus ring. One shared indicator, never variant-coloured, never animated.

   Upstream's `focus-ring` utility is a box-shadow:
     outline-hidden focus-visible:ring-2 focus-visible:ring-ring
     focus-visible:ring-offset-2 focus-visible:ring-offset-background
   An outline draws the same two rings with two differences that both favour it
   here. It survives forced-colors mode, where box-shadows are dropped and a
   keyboard user would be left with no focus indicator at all. And its 2px gap
   shows whatever is actually behind the control, so a button inside a card
   reads correctly; ring-offset-background paints the page background there.
   -------------------------------------------------------------------------- */
:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

::selection { background: var(--brand-plate); color: var(--foreground); }

/* The skip link's target takes focus programmatically, so it must not paint the
   shared ring round the whole page body. This is the one place accessibility.mdx's
   "never use outline-none without a replacement" does not apply, and the reason
   is that its own skip-to-content contract asks for it: the landmark is a
   destination, not a control, and the ring belongs to the link that sent you. */
main:focus, main:focus-visible { outline: none; }

/* The design system's own sr-only, for text that only a screen reader needs.
   Named in accessibility.mdx under Scaffolding. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border-width: 0;
}

/* --------------------------------------------------------------------------
   Latin-only tracking. Arabic is cursive: negative tracking damages the joins
   and uppercase means nothing, so both stay scoped to LTR.
   -------------------------------------------------------------------------- */
/* supabase.com sets no tracking on any heading, including the hero h1. */

/* ==========================================================================
   Skip link — first focusable element, per accessibility.mdx
   ========================================================================== */

.skip-link {
  position: fixed;
  top: 10px;
  /* Physical `left`, not inset-inline-start. This sits inside the header bar's
     footprint at z-index 100, and that bar is pinned — it does not mirror in
     Arabic (deviation 7). With the logical property the skip link crossed to
     the right in RTL and landed on top of the two controls it exists to let you
     skip past. Upstream uses `left-[10px]` for the same reason. */
  left: 10px;
  z-index: 100;
  padding: 8px 14px;
  border-radius: var(--radius);
  background: var(--card);
  border: 1px solid var(--border-strong);
  color: var(--foreground);
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  transform: translateY(-250%);
}
.skip-link:focus-visible { transform: none; }

/* ==========================================================================
   Layout
   ========================================================================== */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: 24px;
}

/* px-6 / lg:px-12 / xl:px-24 — measured on supabase.com at 768, 1024 and 1280:
   24px, 48px, 96px. The middle step was missing, so everything between 1024 and
   1279 sat half as far from the edge as the reference. */
@media (min-width: 1024px) {
  .container { padding-inline: 48px; }
}

@media (min-width: 1280px) {
  .container { padding-inline: 96px; }
}

/* PageSection is `pt-12 last:pb-12` — 48px on top, nothing underneath except
   on the last one. That is the Studio fragment, where sections are panels in an
   app. supabase.com pads its own sections `py-16 sm:py-18 md:py-24`, symmetric,
   and every one of the four that keeps the base measures 96/96 at desktop.

   This is the same fork as --text-sm, the container padding, the hero band and
   the footer: two apps, and a landing page follows the landing page. Measured
   what it costs before taking it — the page goes from 2,897px to 3,281px, 3.6
   viewports to 4.0. There was room for their rhythm.

   Symmetric padding means no `last-of-type` rule: every section carries its own
   bottom, so the space between two of them is the sum of both. */
.section { padding-block: 64px; }

@media (min-width: 640px) {
  .section { padding-block: 72px; }
}

@media (min-width: 768px) {
  .section { padding-block: 96px; }
}

/* ==========================================================================
   Buttons — Button.tsx + constants.ts
   Base: rounded-md, a border on every variant, font-regular, 200ms ease-out
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: var(--btn-h-medium);
  padding-inline: 16px;
  border-radius: var(--radius);
  border: 1px solid transparent;
  font-size: var(--text-responsive-sm);
  font-weight: var(--font-weight-normal);
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.2s var(--ease), border-color 0.2s var(--ease),
    color 0.2s var(--ease);
}

/* primary: bg-brand-400 dark:bg-brand-500 / text-foreground / brand border */
.btn--primary {
  background: var(--brand-fill);
  color: var(--foreground);
  border-color: var(--brand-fill-border);
}
.btn--primary:hover {
  background: var(--brand-fill-hover);
  border-color: var(--brand-fill-border-hover);
}

/* default: bg-background dark:bg-card / border-strong / hover:bg-popover */
.btn--default {
  background: var(--btn-default-bg);
  color: var(--foreground);
  border-color: var(--border-strong);
}
/* size "tiny": h-[26px] text-xs px-2.5. The heights run tiny 26 / small 34 /
   medium 38 / large 42 / xlarge 50; only these two and medium are used here. */
.btn--tiny { height: 26px; padding-inline: 10px; font-size: var(--text-xs); }

.btn--default:hover {
  background: var(--popover); /* hover:bg-popover */
  border-color: var(--border-control-hover);
}

/* ==========================================================================
   Nav
   ========================================================================== */

.nav {
  position: sticky;
  top: 0;
  z-index: 40;
  height: var(--nav-h);
  border-bottom: 1px solid var(--border);
  /* `bg-background/90 dark:bg-background/95`, on a layer behind a transparent
     <nav> that carries only `backdrop-blur-xs`. I had written a paragraph here
     claiming their bar has no background at all and arguing at length for
     keeping a tint they already have — I had looked for the fill inside the nav
     and their layer is a sibling. Both numbers come from source; the tint is
     stronger in dark because the same opacity shows more through a dark
     surface. Painted on the element rather than on a layer of its own: at these
     opacities the paint order does not show. */
  background: color-mix(in srgb, var(--background) 90%, transparent);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

/* The header chrome stays put across languages: logo and wordmark on one side,
   language and theme controls on the other, in the same order either way.
   Mirroring the whole bar is the usual RTL convention, but toggling the
   language then moves every control at once, which reads as the page jumping.
   The page body below still mirrors normally. */
/* One child now — the wordmark. The 12px gap that used to separate it from the
   controls went with them; flex and align-items are what still earn their
   place, centring the wordmark in a 65px bar. */
/* Two children now — the wordmark and the theme control, one at each end.

   direction: ltr is what pins them there: it is set on this row for the
   wordmark's sake (a logo lockup never mirrors), and it holds both sides
   steady through a language switch as a consequence. Without it the control
   would cross the bar in Arabic, which is exactly what it must not do.

   The auto margin is what carries the control to the far edge; there are two
   children and no third, so justify-content: space-between would do the same
   thing today and quietly stop doing it the moment anything else joins the
   row. */
.nav__inner {
  height: 100%;
  display: flex;
  align-items: center;
  gap: 12px;
  direction: ltr;
}
.nav__inner #theme-toggle { margin-left: auto; }

/* Wordmark. One word set in two weights of Space Grotesk: "Data" light and
   muted, "Souq" medium and full contrast. No mark, no space, no separator —
   the weight and tone shift carry the break. direction: ltr keeps it upright
   in Arabic; a logo lockup never mirrors. */
.brand {
  display: inline-flex;
  align-items: baseline;
  font-family: var(--font-brand);
  font-size: var(--text-lg);
  letter-spacing: -0.01em;
  direction: ltr;
  white-space: nowrap;
}

.brand__data { font-weight: 300; color: var(--muted-foreground); }
.brand__souq { font-weight: 500; color: var(--foreground); }

/* The bar's right-hand side: language, theme, source. 8px is Button's
   `space-x-2`, which the header note already names as this page's action-group
   gap, and it clears WCAG 2.5.8 without leaning on the spacing exception —
   the 26px theme control and the 22px GitHub mark put their centres 32px
   apart, against the 24px the rule asks for. */
.footer__bar-actions { display: flex; align-items: center; gap: 8px; }

/* size "small": h-[34px] px-3 text-sm */
/* SIZE.tiny in constants.ts — 26px with `px-2.5` — not SIZE.small's 34/px-3
   these were built at. Measured on supabase.com at 1280: every button in the
   right-hand group of their header is 26px tall with 10px of side padding and a
   12px/16px label — the GitHub star count, Sign in and Start your project all
   of them. Ours were a size step up. The control this still styles has since
   moved to the footer bar, where 26px reads as the right size for a different
   reason: that bar's own type is 12px, the same 12px this label is.
   The variant is unchanged and was already right: Button.tsx's outline is
   `text-foreground bg-transparent border-strong hover:border-foreground-muted`,
   read from source, and that is exactly what is below. Their Sign in button is
   the `default` variant instead — `bg-background dark:bg-card` behind the same
   border — and that is not taken: these two are settings, not a call to
   action, and a filled control in the bar would compete with the hero's. */
.ctl {
  height: 26px;
  min-width: 26px;
  padding-inline: 10px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border-radius: var(--radius);
  border: 1px solid var(--border-strong);
  background: transparent;
  /* outline variant: text-foreground at rest and on hover; only the border moves */
  color: var(--foreground);
  font-size: var(--text-xs);
  line-height: var(--lh-xs); /* 16px, which is what their tiny buttons measure */
  font-weight: var(--font-weight-normal);
  transition: border-color 0.2s var(--ease);
}
.ctl:hover { border-color: var(--tertiary-foreground); }
.ctl--icon { padding-inline: 0; justify-content: center; }

/* Language picker — one button naming the current language, opening a menu.

   Modelled on the control at claude.com, measured from it rather than
   guessed: its trigger is a bordered pill holding a 20px globe, the language
   name, and a chevron, `display:flex` with `gap:6px`, and it opens a menu of
   the languages, each written in its own script. That last part is the point
   of the pattern — a reader who cannot read the current language still finds
   their own in the list.

   Two things are deliberately not copied. Their 12px radius and 12px/16px
   padding belong to a 45px-tall control on a page whose type runs larger;
   this bar's type is 12px and every other control in it is .ctl at 26px, so
   the trigger IS a .ctl and takes that geometry. And the swap for two
   languages stays a menu rather than collapsing to a toggle: the previous
   control was a two-option toggle reading "EN AR", which says what it is only
   to someone who already reads one of them.

   The whole bar carries direction: ltr (deviation 7), so the menu is pinned
   to the trigger's right edge with a physical property — there is no logical
   edge to track here. */
.langpicker { position: relative; display: inline-flex; }

/* Wider than a .ctl icon button because it carries a word — the endonym,
   "English" or "العربية". The BUTTON no longer sizes itself to that word: it
   used to, and switching language visibly narrowed it, 103px to 90px
   measured, because "العربية" (29px) is shorter than "English" (41px) in
   this font at this size. That 13px jump moved the chevron and the edge of
   the pill under a control the reader had just used — the one moment they
   are looking straight at it.

   Fixed by giving the WORD a fixed box instead of the button: see
   .langpicker__label below. The globe and the chevron then never move,
   whichever language is live. */
.langpicker__trigger { cursor: pointer; }

/* 42px, not measured-and-left: "English" is 41.27px in this font at 12px
   (canvas measureText, cross-checked against the rendered box), and 42
   rounds that up by less than a pixel of slack. "العربية" is 29px, so it
   centres in the same box with room either side rather than filling it.
   Re-measure this if a label ever changes or a third language is added —
   the number is the point, not the formula. */
.langpicker__label {
  display: inline-block;
  inline-size: 42px;
  text-align: center;
  flex-shrink: 0;
}
.langpicker__trigger[aria-expanded="true"] { border-color: var(--tertiary-foreground); }

/* The chevron turns over when the menu is open, which is the only motion in
   this control. */
.langpicker__chevron { transition: transform 0.2s var(--ease); }
.langpicker__trigger[aria-expanded="true"] .langpicker__chevron { transform: rotate(180deg); }

/* Opens upward: the control sits in the last bar on the page, so downward
   would render the menu off-screen. */
.langpicker__menu {
  position: absolute;
  bottom: calc(100% + 6px);
  right: 0;
  min-width: 100%;
  padding: 4px;
  background: var(--popover);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  z-index: 20;
  display: grid;
  gap: 2px;
}

/* Each language is written in its own script and never translated: a menu
   that renders "Arabic" to an English reader and "الإنجليزية" to an Arabic one
   is a menu neither can use to leave the language they are stuck in. */
.langpicker__item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px 10px;
  border-radius: var(--radius);
  background: transparent;
  color: var(--foreground);
  font-size: var(--text-xs);
  line-height: var(--lh-sm);
  text-align: start;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.2s var(--ease);
}
.langpicker__item:hover { background: var(--muted); }
.langpicker__item[aria-checked="true"] { background: var(--accent); }

/* A check marks the live option instead of colour alone, so the state does
   not rest on a background tint. Its slot is held open on the others, or the
   labels would jump left and right as the selection moves. */
.langpicker__check { width: 14px; height: 14px; flex-shrink: 0; opacity: 0; }
.langpicker__item[aria-checked="true"] .langpicker__check { opacity: 1; }

/* .ctl's 26px min-width is for the icon-only control, which is 16px of icon and
   border without it. 26x26 clears WCAG 2.5.8's 24x24 minimum outright, so the
   spacing exception the footer's channel row leans on is not needed here. */

/* IconContainerVariants in Button.tsx, read from source:
     tiny:  '[&_svg]:h-[14px] [&_svg]:w-[14px]'
     small: '[&_svg]:h-[18px] [&_svg]:w-[18px]'
   so the icon steps down with the button. The tint is the outline variant's
   `text-foreground-lighter`, which compat.css aliases to --tertiary-foreground
   — a tone below the label, which stays --foreground. It reaches the glyphs
   because the SVGs are stroke="currentColor".

   These are the one place on the page where the painted hairline lands under
   1.5px on purpose: stroke-width 2 over a 24-unit viewBox at 14px paints
   1.167px. That is what upstream's tiny buttons paint too, and the alternative
   would be an icon that does not match its own button size.

   flex-shrink:0 is required — without it the icon collapses to 0 width when
   the label pushes the button past the space available in the nav. */
.ctl svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--tertiary-foreground); }

[data-theme="dark"] .icon-sun { display: block; }
[data-theme="dark"] .icon-moon { display: none; }
:root:not([data-theme="dark"]) .icon-sun { display: none; }
:root:not([data-theme="dark"]) .icon-moon { display: block; }

/* ==========================================================================
   Hero
   ========================================================================== */

.hero {
  /* Hero.tsx is `pt-12 pb-8 md:pt-40! md:pb-16` — 48/32 below md, 160/64 from
     it. The 96px that used to be here rested on the claim that their nav floats
     over the band, so 160px was "really" 95px of clear space. That claim was
     wrong: their bar is a sticky wrapper in normal flow and reserves its own
     65px exactly as ours does. So 160px was already clear space and we were
     64px short — and flat, with no mobile step at all, spending 96px of empty
     band on a phone. */
  padding-block: 48px 32px;
  text-align: center;
  border-bottom: 1px solid var(--border);
}

/* Hero heading, measured on supabase.com: 46/46 at desktop and 34 at mobile,
   weight 500, no tracking. `heading-title` (text-2xl, semibold, tracking-tight)
   is the Studio page-title role and reads far too small on a landing hero. */
.hero h1 {
  font-size: var(--text-4xl);
  line-height: var(--lh-4xl);
  font-weight: 500;
  margin-bottom: 16px;
  /* Their h1 needs no balancing because it dictates its own line breaks with a
     span per line. A bilingual heading cannot — the Arabic breaks elsewhere —
     so balance is the equivalent control. Their lead carries `text-balance`
     outright. */
  text-wrap: balance;
}

/* `text-4xl sm:text-5xl sm:leading-none` — the step is Tailwind's sm, 640px.
   It had been at 768px, so between 640 and 767 the heading held 34px where
   theirs is already 46. This is a different breakpoint from the button text
   step, which really is `md:text-sm` at 768px; two queries, not one. */
@media (min-width: 640px) {
  .hero h1 { font-size: var(--text-5xl); line-height: var(--lh-5xl); }
}

@media (min-width: 768px) {
  .hero { padding-block: 160px 64px; }
}

@media (min-width: 1024px) {
  .hero__lead { margin-bottom: 32px; }
}

/* PageHeaderDescription is `heading-subSection text-foreground-light`
   = text-base on --muted-foreground. PageHeader stacks its children gap-4. */
.hero__lead {
  font-size: var(--text-lg);
  /* line-height inherited: 1.5, as upstream's hero lead takes it */
  color: var(--muted-foreground);
  /* px, not ch. `ch` is the advance of the '0' glyph in the computed font, and
     Inter is first in the stack for both languages — so an Arabic paragraph,
     every glyph of which is actually set in IBM Plex Sans Arabic, gets its
     measure from a font it never uses. 56ch in Inter was 448px. */
  max-width: 448px;
  /* Hero.tsx nests a `flex flex-col gap-6 lg:gap-8` around the copy pair and the
     button row, so the gap down to the actions is 24px and 32px from lg. The
     16px that was here is the inner grid's gap-4, which is the heading-to-lead
     gap — using it for both made the three hero rows one undifferentiated
     stack. */
  margin: 0 auto 24px;
  text-wrap: balance;
}

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

/* ==========================================================================
   Section heading
   ========================================================================== */

.head { margin-bottom: 24px; }

/* The eyebrow — deviation 3. It is not an invention, which the note here used
   to imply: apps/www
   defines it three times over — SectionHeader's subtitle span, the `.label`
   utility in its globals.css, and typography.css's `heading-meta` — and all
   three agree on 12px, mono, uppercase, tracking-wider, --foreground-lighter.

   Three of those five properties cannot come along. Uppercase carries no
   meaning in Arabic, IBM Plex Sans Arabic has no mono companion, and positive
   tracking breaks Arabic joins — the same reasoning deviation 2 gives for
   CardTitle. What is left of a label once case, family and tracking are gone is
   nothing, so two things compensate: 14px instead of 12, and the brand colour
   instead of the neutral. The colour is the arguable half — brand green is also
   the call-to-action colour — but with the other four signals unavailable it is
   what marks this line as a label rather than a dropped first line of the
   title. The alternative, --tertiary-foreground at 5.42:1 light and 4.59:1
   dark, is upstream's value and would pass; it would also make the eyebrow
   indistinguishable from the lead two lines below it.

   The 16px below it is SectionHeader's `space-y-4`. It was 4px, inherited from
   PageSectionSummary's gap-1 — a slot that has no eyebrow in it. */
.head__label {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--brand-text);
  margin-bottom: 16px;
}

/* 34px, Manrope 600, no tracking, balanced — measured on supabase.com's section
   headings, which hold 34px at every width. text-2xl (22px) is `heading-title`,
   the Studio page-title role, and reads as a panel label under a 46px hero. */
.head h2 {
  font-size: var(--text-4xl);
  line-height: var(--lh-4xl);
  text-wrap: balance;
  /* `mt-4` on the description in both live homepage sections, and `sm:mt-4` in
     SectionHeader. The 4px here was PageSectionSummary's gap-1, which pairs
     with an 18px title, not a 34px one — under this heading the lead read as a
     subtitle glued to it. */
  margin-bottom: 16px;
}
/* Upstream is --foreground-lighter, i.e. --tertiary-foreground, in every
   apps/www reference. I took it and then took it back: on --background that
   computes to 5.42:1 light and 4.59:1 dark, and this is 16px at weight 450, so
   the floor is 4.5:1 for normal text — 0.09 of headroom, about 2%, on a colour
   that moves with any knob edit. --muted-foreground is 9.29:1 and 8.22:1. The
   same reasoning keeps .hero__lead where it is; the honest part of the finding
   was that the old justification cited PageHeaderDescription, a Studio
   component the hero and the sections both explicitly do not follow. */
.head p { font-size: var(--text-lg); color: var(--muted-foreground); max-width: 512px; }

/* ==========================================================================
   Cards — depth from surface layers and borders, never shadows
   ========================================================================== */

/* `grid-cols-1 md:grid-cols-3 gap-3` — an explicit 1-to-3 step at 768px, which
   is what supabase.com's bordered three-up rows do, and 12px between them. Read
   from source: the four product FeaturesSections carry that exact string —
   apps/www/app/(products)/{auth,storage,realtime,database}/_components/fs-*.tsx
   — and so does each product HighlightsSection. A repo-wide search finds
   auto-fit only in apps/docs and apps/studio, never in apps/www.

   auto-fit with a 210px floor was the problem it looks like it solves: two
   tracks need only 436px of content, so from a 484px viewport to a 709px one
   the row rendered two cards and orphaned the third at half width. An explicit
   step never produces that state.

   The :has() guard was there because .cards also wrapped the catalogue's lone
   card, which would otherwise have taken a third of a row. The catalogue now
   runs on twelve tracks of its own, and the guard alone would not have been
   enough: :has() carries the specificity of its argument, so
   `.cards:has(> .card + .card)` scores (0,2,0) against `.cards--catalogue`'s
   (0,1,0) and would have won on specificity, not source order. Adding the
   second dataset would have snapped the catalogue back to three equal columns
   — the one thing this section is now built not to do. Hence the :not(). */
.cards { display: grid; grid-template-columns: 1fr; gap: 12px; }

@media (min-width: 768px) {
  .cards:not(.cards--catalogue):has(> .card + .card) {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* Card: rounded-lg border bg-surface-100 shadow-xs, and p-6 rather than
   card.tsx's py-4 px-(--card-padding-x) — deviation 2 and the www fork.
   CardHeader / CardContent / CardFooter are not split out: the three small
   cards hold a title and two lines, so there is nothing to divide. The dataset
   card does divide, once, and paints that rule on .metrics instead. */
.card {
  /* card.tsx opens with `overflow-hidden rounded-lg border bg-surface-100
     text-card-foreground shadow-xs`. The overflow was the one part missing. */
  overflow: hidden;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xs);
  /* 16px is Studio's Card — `py-4 px-(--card-padding-x)`. Every bordered card
     on supabase.com is p-6, 24px, stepping to p-8 at md on the wide ones. Same
     www-versus-Studio fork the file already resolves toward www for the type
     scale, the container, the hero, the section rhythm and the footer. */
  padding: var(--card-padding-x-md);
  /* Their card's content column is `flex flex-col gap-2 sm:gap-4` — 8px, 16px
     from sm. Every card here is that column now, so the gap owns the vertical
     spacing and no child inside a card carries a margin. This replaces
     `.card h3 { margin-bottom: 8px }`, which produced the same 8px for the
     three service cards and had to be undone inside the title row. */
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Deviation 5: a landing-page accent whose FILL has no upstream source —
   GlassPanel's optional IconBackground is a neutral `bg-surface-75 border`
   chip, not a brand one. Measured on the surface it actually paints:
   brand-default at 10% over --card, 4.87:1 light and 8.83:1 dark against
   --brand-600. */
/* 24px with an 18px mark. The mark's size is the painted hairline and nothing
   else: stroke-width 2 over a 24-unit viewBox at 18px paints 2 × 0.75 = 1.5px,
   the figure the stroke note pins the rest of the page to. Not icons.mdx's
   18-in-24, which governs custom SVGs — these four marks are Lucide. The
   32px plate this replaces was sized for an icon stacked ABOVE a title; in a
   row beside a 24px line it would have overhung the line by 4px each side.
   The 24px box and the 24px line-height being equal is what lets flex-start
   on .card__head align the two without an offset. */
.card__icon {
  width: 24px;
  height: 24px;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--brand-default) 10%, transparent);
  border: 1px solid var(--brand-500);
  display: grid;
  place-items: center;
  color: var(--brand-on-plate);
  /* A flex item with a definite width still shrinks below it unless told not
     to, and the title beside it will take every pixel it is given. */
  flex-shrink: 0;
}
.card__icon svg { width: 18px; height: 18px; }

/* Their product row is `grid grid-cols-12 gap-3` — twelve tracks of 79.66px
   inside the 1088px container at 1280, 12px apart — and each card is
   `col-span-3`, so four sit across at 263px each with a fixed `sm:h-[400px]`.

   Ours spans four of the twelve rather than three: 355px at 1280, three across.
   Theirs holds a title, two lines and an illustration; this one holds a
   paragraph, five metrics and an action, and 263px is not enough for that.

   The span is the whole point. A full-width card has nowhere to put the second
   dataset; a card on a fixed span leaves the rest of the row waiting for it. */
.cards--catalogue { grid-template-columns: 1fr; }

@media (min-width: 768px) {
  .cards--catalogue { grid-template-columns: repeat(12, minmax(0, 1fr)); }
  .card--dataset { grid-column: span 6; }
}

@media (min-width: 1024px) {
  .card--dataset { grid-column: span 4; }
}

/* Their content column is `flex flex-col gap-2 sm:gap-4` and the block under
   the title row is `flex-1 justify-between`, so the card's foot stays put
   however long the copy runs. h-[400px] is a floor here rather than a fixed
   height: the Arabic body sets longer than the English and a fixed one would
   clip it. */
.card--dataset { gap: 16px; }

@media (min-width: 768px) {
  .card--dataset { min-height: 400px; }
}

.card--dataset .card__actions { margin-top: auto; }

/* `flex items-center gap-2`, with one change: flex-start, not center.
   Upstream's product-card titles are single words that never wrap, so center
   costs them nothing. One of ours does wrap — measured at 768px, where each
   service card is 227px and its text box 177px, the plate and gap take 32px
   and leave 145px: "Cleaning & structuring" needs 168.88px and goes to two
   lines. The durable threshold is the CARD width, not the viewport: it wraps
   below 250.88px of card, which is 168.88 + the 32px the plate and gap take +
   the 50px of padding and border. Measured in this pane, that is a viewport of
   839px (card 250.67, two lines) against 840px (card 251, one line), and the
   viewport figure moves with the scrollbar. In an items-center row a two-line title
   is a 48px box, so the plate would centre against it and sit 12px below the
   line it belongs to. flex-start pins it to the first line, and because the
   plate is 24px and the line-height is 24px the two are already concentric —
   for a single-line title flex-start and center compute identically.
   The other five titles fit: 131.63 and 130.16 in English, 116.38, 77.11 and
   97.64 in Arabic. */
.card__head { display: flex; align-items: flex-start; gap: 8px; }

/* 600 against .card h3's 500, and scoped to the dataset card only. That 500 is
   right for the three service cards — it traces to HighlightsSection,
   ObservabilitySection and the design-system feature cards, which all put
   font-medium back — but the product cards the dataset card is modelled on
   measure Manrope 600 at 16px/24px. Upstream's card families differ here; so
   do ours, on the same split: the product card is the object, the other three
   describe the work. */
.card--dataset .card__head h3 { font-weight: 600; }

/* The two-tone body: `[&_strong]:text-foreground!` upstream, colour only.
   Theirs dims the base to --foreground-lighter and lifts the lead clause to
   --foreground; ours cannot dim that far (deviation 4), so the pair is
   --muted-foreground with the clause at --foreground. Measured on --card:
   --foreground-lighter 4.27:1 dark, against --muted-foreground's 7.64:1 dark
   and 9.44:1 light, with the clause itself at 15.31:1 and 20.62:1. */
.card__body strong { color: var(--foreground); }

/* Two measured differences from their product card, both left alone.
   Radius: theirs is `rounded-lg md:rounded-xl`, 12px from md; card.tsx says
   rounded-lg and --borderradius-lg is 8px, which is what every card here uses.
   Padding: theirs is 24px 16px, because at 259px wide it needs the horizontal
   room; this card is 350px and does not.
   Upstream those two card families live on different pages. Here the dataset
   card and the three service cards sit in the same scroll, one section apart,
   so a different corner or a different gutter would read as a mistake rather
   than as hierarchy. The title weight is the one property where the difference
   says something, and it is the one property taken. */

/* One metric per line in a 355px card. The five-across row was for a card that
   filled the container. */
.card--dataset .metrics { grid-template-columns: 1fr; }

/* Metrics — the Metric Card fragment's icon-and-figure pairing, laid out as a
   row inside the dataset card. Icons are tinted like the text beside them, the
   way icons.mdx describes. */
.metrics {
  display: grid;
  /* 180px, measured both ways rather than reasoned. Inside the card's 1024px at
     1440 it yields five tracks of 192px: in English every one of the five
     labels wraps to two lines, in Arabic none of them do — uniform either way,
     one row, 57px tall. Raising the floor to 200px or more drops to four tracks
     and puts the fifth metric alone on a second row, which is worse in both
     languages. The wrapping was never the defect; the icon drifting to the
     middle of a wrapped cell was, and align-items below fixes that. */
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 8px 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

/* MetricCardContent is `items-start`; items-center is only used for a single
   line CardTitle that cannot wrap. With centre alignment and a two-line label
   the icon drifted to the middle of the cell, so the icon row broke apart the
   moment any label wrapped. */
.metrics li {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--muted-foreground);
}

/* MetricCardIcon is `text-foreground-light`, which compat.css aliases to
   --muted-foreground — the same value as the label beside it. It had been
   --tertiary-foreground, one notch lighter, which is what made the icons read
   as a different weight from their own text and contradicted the comment above
   claiming they are tinted like it. margin-top optically centres the 16px mark
   on the first 20px line rather than on the whole cell. */
.metrics svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  margin-top: 2px;
  color: var(--muted-foreground);
}
/* typography.css: strong is font-medium. tabular-nums is MetricCardValue's, and
   it does its work in English only. Measured in this box: "1111" and "7777"
   both set 36.30px, while Arabic-Indic "١١١١" sets 15.19px against "٧٧٧٧"'s
   30.08px — Plex Arabic's Arabic-Indic digits are proportional and tnum does
   not cover them. Kept regardless: it costs nothing where it does not apply,
   and the row no longer leans on it. One metric per line means every figure
   starts at the same x by layout, in both directions — measured, all five at
   145 in English and all five ending at 1120 in Arabic — where the
   multi-column row this was written for depended on the digits themselves. */
.metrics strong {
  color: var(--foreground);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

/* PageSectionAside spacing for actions inside a card */
.card__actions { display: flex; flex-wrap: wrap; gap: 8px; }

/* CardTitle upstream is `text-xs font-mono uppercase` — a dense Studio-panel
   idiom. Kept as a heading here: uppercase carries no meaning in Arabic, and
   12px mono would read as a dashboard label on a public page.
   CardDescription is `text-sm text-foreground-lighter`, but on --card in the
   dark theme that measures 4.27:1, under the AA floor, so this keeps
   --muted-foreground: one notch stronger, 7.65:1. */
/* Every card call site overrides the base layer's font-semibold back to
   font-medium — HighlightsSection, ObservabilitySection and all six
   design-system feature cards. The blanket h1-h6 rule gives 600, so this puts
   500 back on the card selector only; the borderless feature-list items keep
   semibold upstream, so the element rule stays as it is. */
.card h3 { font-size: var(--text-lg); font-weight: 500; }
.card p { font-size: var(--text-sm); line-height: var(--lh-sm); color: var(--muted-foreground); }

/* ==========================================================================
   Panel
   ========================================================================== */

 /* EmptyStatePresentational is deliberately NOT a Card. It is a dashed 1px
    outline with no shadow, `px-4 py-10`, a bare 24px icon at strokeWidth 1.5 in
    --foreground-muted, a 14px description capped at 640px, and 12px gaps down
    the stack. This panel had been a copy of .card with a brand plate borrowed
    from .card__icon, which is why the page read as four cards instead of three
    cards and an invitation — the dashed stroke is the fragment's whole point.

    The two padding axes were also swapped: an empty state is mostly air around
    one idea, so it is tall and narrow-padded, not the squat 16px band of a
    dense card. */
.panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  background: var(--card);
  border: 1px dashed var(--border);
  border-radius: var(--radius-lg);
  padding: 40px var(--card-padding-x);
  text-align: center;
}

.panel h2 {
  font-size: var(--text-xl);
  line-height: var(--lh-xl);
  text-wrap: balance;
}

.panel p {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--muted-foreground);
  max-width: 640px;
  text-wrap: balance;
}

/* Its own icon, not .card__icon's plate: bare, 24px, stroke 1.5, muted. A plate
   is a Card affordance and pulls the eye to the icon, which is the opposite of
   what an empty state wants.

   It used to carry `border: 0; border-radius: 0; background: none; margin: 0`
   as well, from when this panel was a copy of .card and borrowed the plate.
   Nothing gives .panel__icon a border, a radius or a fill to undo, and the
   reset zeroes every margin already, so all four reset their own initial
   values. */
.panel__icon {
  width: 24px;
  height: 24px;
  color: var(--tertiary-foreground);
}
.panel__icon svg { width: 24px; height: 24px; stroke-width: 1.5; }

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

/* ==========================================================================
   Footer
   ========================================================================== */

/* The footer's grid is wrapped in the same SectionContainer as every marketing
   section, so it takes the same three stops the sections do rather than a flat
   96px — which was 32px too much on a phone and made the footer the only block
   on the page ignoring the rhythm .section establishes. No border of its own
   either: the rule belongs to the bar inside it. */
.footer { padding-block: 64px; }

@media (min-width: 640px) {
  .footer { padding-block: 72px; }
}

@media (min-width: 768px) {
  .footer { padding-block: 96px; }
}

/* xl:grid xl:grid-cols-7 xl:gap-4, with the brand block on two of the seven
   columns and the links on five. Below xl it stacks, gap-8.

   direction: ltr pins the track order, which is deviation 7 finally applied to
   the thing it names. That deviation said the footer does not mirror, and only
   its two bars ever did: .nav__inner and .footer__bar carried the property, the
   grid did not. Measured at 1280 in Arabic, the brand block crossed 778px to
   the right (left 96 to 874), the link block came back the other way (407 to
   96), and the two columns swapped (439/563 to 718/593) while the bar below
   them stayed exactly where it was. Half a pinned footer reads as a bug, which
   is what it was. */
.footer__grid { display: grid; gap: 32px; direction: ltr; }

@media (min-width: 1280px) {
  .footer__grid { grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 16px; }
  .footer__brand { grid-column: span 2; }
  .footer__nav { grid-column: span 5; }

  /* Their columns clear the brand block by about 72px, and none of it is
     deliberate: their icon row is 232px inside a 299px track, so 67px of slack
     plus the grid's 16px does it for free. Seven channels make our row 274px in
     the same track, which leaves 25px — the columns end up sitting almost
     against the field. This puts the separation back explicitly. */
  .footer__nav { padding-inline-start: 32px; }
}

/* flex flex-col gap-8. direction comes from .footer__grid now, which is also
   what keeps the channel row's icon order fixed — six brand marks are a lockup
   and a lockup does not reverse. */
.footer__brand {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 32px;
}

/* Their footer logo is 160x30 where the header's is 124x24 — the brand is
   deliberately larger down here. 22px against the nav's 16px is 1.375x, the
   closest the scale gets to their 1.29x. */
.brand--footer { font-size: var(--text-2xl); }

/* The confirmation that replaces the form, at the size of the label it stands
   in for: the first line in brand text, the second in the body tone. */
.footer__thanks { display: flex; flex-direction: column; gap: 4px; }
/* An author `display` beats the UA rule behind the hidden attribute, so without
   this the confirmation shows on load. Same trap for anything given a display
   value and toggled with [hidden]. */
[hidden] { display: none !important; }
.footer__thanks p {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--tertiary-foreground);
}
.footer__thanks-title { color: var(--brand-text); }

/* flex space-x-5 — 20px between icons */
.footer__social { display: flex; flex-wrap: wrap; gap: var(--social-gap); }

/* form is `flex flex-col gap-2`, and the label above it is a <p> at 14/20 in
   --tertiary-foreground. Its width is the icon row's, so the two read as one
   block instead of two of different lengths. */
.footer__form {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  width: var(--social-row);
  max-width: 100%;
}

.footer__form p {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--tertiary-foreground);
}

/* Input at size small — h-[34px] rounded-md border-control bg-field — with the
   `text-xs px-2` the footer instance narrows it to. Where they cap it at
   `xl:max-w-[80%]` of the track, this fills the form, which is already the
   width of the icon row above it. */
.field {
  width: 100%;
  height: 34px;
  padding-inline: 8px;
  border-radius: var(--radius);
  border: 1px solid var(--border-strong);
  background: var(--field);
  color: var(--foreground);
  font-family: inherit;
  font-size: var(--text-xs);
  font-weight: var(--font-weight-normal);
  transition: border-color 0.2s var(--ease);
}

/* placeholder:text-foreground-muted, which compat.css aliases to tertiary */
.field::placeholder { color: var(--tertiary-foreground); }

/* hover:border-control-hover and focus:border-control-hover, both the same */
.field:hover, .field:focus { border-color: var(--border-control-hover); }

/* Their grid, not a flex row sized to its content. Six equal tracks across the
   five-column span works out at 115px each, which is what theirs measures, and
   long labels wrap inside a track exactly as `Events & Webinars` does over
   there. Laying these out by content width instead gave two columns of 182px
   and 150px that lined up with nothing.

   Two of the six tracks are filled. The rest is empty because this site has
   four footer links and theirs has fifty; the grid is right and the content
   will grow into it. */
.footer__cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 48px 16px;
}

@media (min-width: 1280px) {
  .footer__cols { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}

/* Column headings are h6 upstream — Manrope 600 at 15/22.5 in --foreground.
   These are h2 so the document's heading order stays unbroken; the role is
   what matters, not the level. */
.footer__col h2 {
  font-size: var(--text-base);
  line-height: 1.5;
  font-weight: 600;
  color: var(--foreground);
}

/* ul is `mt-4 space-y-2` */
.footer__col ul { margin-top: 16px; display: flex; flex-direction: column; gap: 8px; }

/* `text-sm transition-colors text-foreground-lighter hover:text-foreground` */
.footer__col a {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--tertiary-foreground);
  transition: color 0.15s var(--ease-default);
}
.footer__col a:hover { color: var(--foreground); }

/* Pinning the layout is not the same as typesetting the text LTR. Direction on
   a leaf is what puts Arabic punctuation on the correct side of a run, and the
   alignment that follows from it is the one an Arabic reader expects — so every
   text-bearing block inside the pinned footer turns RTL in Arabic, inside a box
   that has not moved. The columns read right to left; the columns themselves do
   not swap, because .footer__cols stays LTR above them.

   Three things deliberately do NOT turn: the wordmark (a name, and already
   pinned at .brand), the channel row (a lockup), and the copyright's Latin
   head. That last one is a mixed run — "© 2026 DataSouq · " and then an Arabic
   clause — so only the clause becomes its own isolated RTL run. Turning the
   whole line would have carried the © and the year to the right of it. */
:root[lang="ar"] .footer__form p,
:root[lang="ar"] .footer__form .field,
:root[lang="ar"] .footer__thanks,
:root[lang="ar"] .footer__col {
  direction: rtl;
}

/* The form itself stays LTR so its own children keep their places. It is a
   column flex at align-items: flex-start, and flex-start is direction-relative
   — turning the form turned that into the right edge and took the Subscribe
   button with it, 96-176 in English against 320-370 in Arabic. The two text
   children turn instead: both are 274px wide and fill the block, so nothing
   moves when they do, and the paragraph and the placeholder still read right.

   The button also holds its size. It is sized by its label, and the two labels
   are not the same length: measured at 12px in the same box, "Subscribe" sets
   57.63px of text for a 79.63px control and "اشترك" sets 24.88px for 46.88px.
   80px is the English width rounded up, so the longer label sets the box and
   the shorter one centres in it — .btn is already inline-flex with
   justify-content: center. Change either label and this needs re-measuring. */
.footer__form .btn { min-width: 80px; }

:root[lang="ar"] .footer__rights {
  direction: rtl;
  unicode-bidi: isolate;
}

/* The source link now opens the copyright line rather than sitting at the far
   end of the bar with the settings. It belongs with the line that names who
   owns the work — it points at the work itself, not at a way to reach us or a
   thing to change — and the bar's right end is left to the one control that
   is genuinely a setting.

   The line is a flex row so the 22px mark sits on the text's centre instead of
   its baseline, where a glyph that tall would drag the line box down. */
.footer__copy {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

/* The bar: `border-default mt-32 flex justify-between border-t pt-8`, which in
   Tailwind's 4px units is a 1px --border rule, 128px above it and 32px below.
   The 128px is calibrated to a 615px-tall footer block; ours is a third of
   that, so this takes the page's own 48px rhythm instead and keeps their 32px.

   The copyright is a <small> at 12/16 upstream — typography.css's
   `small { text-compact }` showing through. */
.footer__bar {
  direction: ltr;
  margin-top: 48px;
  padding-top: 32px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: center;
  justify-content: space-between;
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
  color: var(--tertiary-foreground);
}

/* Bare 22px icons, no box behind them. These had been built as small text-
   variant Buttons — 34px, rounded, hover:bg-accent — but that variant is for
   controls, and these are links in a footer. Upstream writes its own as
     text-foreground-lighter hover:text-foreground transition
   at 22px with a 20px gap and no padding, radius or border. Only the colour
   moves, over 150ms on Tailwind's default curve.

   22px is under the 24px WCAG 2.5.8 asks of a target, but the rule's spacing
   exception carries it: a 24px circle centred on each icon needs 24px between
   centres, and the 20px gap puts them 42px apart. */
.social {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--tertiary-foreground);
  transition: color 0.15s var(--ease-default);
}
.social:hover { color: var(--foreground); }
/* site.js sets data-copied when the Discord handle is copied. Without this the
   only confirmation was the sr-only live region and a title that needs a fresh
   hover — a sighted mouse user got nothing at all. */
.social[data-copied] { color: var(--brand-text); }
/* flex-shrink:0 for the same reason .ctl svg needs it: the reset gives every
   svg `max-width: 100%`, and inside a flex parent with no definite width that
   resolves against nothing and collapses the icon to zero. */
.social svg { width: var(--social-icon); height: var(--social-icon); flex-shrink: 0; }

/* ==========================================================================
   Dataset page — the catalogue card's destination
   ========================================================================== */

/* The whole card opens the dataset page, but only ONE link is in the
   accessibility tree for it: the title. The overlay is the title link's own
   ::after stretched over the card, which is the pattern that gives a mouse a
   card-sized target without giving a screen reader a second, duplicate link
   (a wrapping <a> would also be invalid here — the card already contains the
   WhatsApp link, and <a> inside <a> is not allowed).

   The WhatsApp button then has to be lifted above that overlay or the card
   link would swallow its clicks; position + z-index is the whole fix. */
.card--dataset { position: relative; }

.card--dataset .card__head h3 a {
  color: inherit;
  text-decoration: none;
}
.card--dataset .card__head h3 a::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}
.card--dataset:hover .card__head h3 a { color: var(--brand-text); }
/* The focus ring belongs on the card, not on the two words of the title:
   the whole card is what the link activates. :focus-visible keeps it off
   mouse clicks, the way every other control here behaves. */
.card--dataset:has(.card__head h3 a:focus-visible) {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
.card--dataset .card__actions { position: relative; z-index: 1; }

/* Back to the catalogue. The arrow is Lucide's arrow-left, so it has to be
   mirrored in RTL — the glyph points at the reading origin, not at "left". */
.backlink {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 20px;
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--muted-foreground);
  text-decoration: none;
  transition: color 0.15s var(--ease-default);
}
.backlink:hover { color: var(--foreground); }
.backlink__icon { width: 16px; height: 16px; flex-shrink: 0; }
:root[dir="rtl"] .backlink__icon { transform: scaleX(-1); }

/* The dataset's own header: icon plate, title, the same two-tone body the
   card carries, then its five measured metrics. The metrics reuse .metrics
   wholesale — same component, same row, one definition. */
.detail__head { padding-bottom: 0; }

.detail__title {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 16px;
}
.detail__title h1 {
  font-size: var(--text-3xl);
  line-height: var(--lh-3xl);
  font-weight: 600;
  margin: 0;
}
.detail__lead {
  max-width: 68ch;
  color: var(--muted-foreground);
  margin-bottom: 24px;
}
.detail__lead strong { color: var(--foreground); }

/* A closing rule under the metrics, and only that: .metrics already paints
   its own rule above itself — that divider is the component's, carried over
   from the card, so adding a border-top here drew the same line twice. */
.detail__metrics {
  border-bottom: 1px solid var(--border);
  padding-bottom: 20px;
}

/* The "not found" state — a bad or missing ?id=. Presentational empty state,
   same shape as the catalogue's, so a mistyped URL lands somewhere designed
   rather than on a blank page. */
.detail__missing { text-align: center; padding: 32px 0; }
.detail__missing h1 {
  font-size: var(--text-2xl);
  line-height: var(--lh-2xl);
  margin-bottom: 8px;
}
.detail__missing p { color: var(--muted-foreground); margin-bottom: 20px; }

/* Shown where the charts or the dictionary would be when a dataset has been
   added to datasets.js but not yet measured. Built like a chart card so the
   section keeps its shape — Supabase: an empty state "should provide the
   initial state in the same presentation as the list when there is data". */
.emptystate {
  padding: 32px 20px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  text-align: center;
}
.emptystate__title {
  font-size: var(--text-base);
  line-height: var(--lh-base);
  font-weight: 600;
  margin: 0 0 6px;
}
.emptystate__body {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--muted-foreground);
  margin: 0 auto 20px;
  max-inline-size: 60ch;
}

/* ==========================================================================
   Charts — horizontal bars, built from HTML rather than SVG
   ==========================================================================

   Every chart on this page plots ONE series: a count (or a share) per
   category. That decides almost everything about them:

   - One series means one colour. Colouring each bar by its own value would
     double-encode the length as hue and spend the only free channel on
     information the bar already carries, so every bar is the same brand
     step. It also means no legend: with a single colour there is nothing to
     key, and the chart's own title says what is plotted.
   - Every bar is directly labelled with its value, so there are no
     gridlines and no y-axis to read against — the number is simply there.
     That is also what keeps the values out of a tooltip, where a keyboard
     or screen-reader user would never reach them.
   - HTML, not SVG: the labels and values are then real text — selectable,
     searchable, translated by the same render pass as the rest of the page,
     and laid out RTL by the same engine. An SVG would need manual
     text-anchor flipping for Arabic and would hide every value from
     find-in-page.

   The two bar colours are the site's own brand steps, checked against the
   card surface they sit on in each theme: #097C4F is --brand-600 in light,
   #2BA770 sits between --brand-500 and --brand-default in dark. Both clear
   3:1 against their surface and sit inside the lightness band their mode
   asks for; the lighter --brand-default (#3ECF8E) does not in dark, which
   is why the bar is not simply the accent. */
/* docs/charts.md is the written-down version of everything below: which form
   answers which question, where that rule comes from (the Supabase design
   system documents chart components but not which chart to pick, so the
   choice comes from the FT Visual Vocabulary), and what each palette was
   checked against. Change a rule there and here together.

   Three treatments, because the charts answer three different questions —
   not for variety's own sake:

   1. NOMINAL bar (--chart-bar). Categories with no inherent order: regions,
      cities, facility types. One colour for every bar; colouring them by
      their own value would encode the length twice and spend the only free
      channel on what the bar already says.
   2. ORDINAL ramp (--chart-ramp-1..6). Categories that ARE a scale:
      classification grades 1-6. The ramp carries
      the rung, so the reader sees the shape of the ladder even where the
      bars are sorted by it rather than by size. Off-scale values —
      Unclassified, Not recorded — take --chart-null, a neutral grey,
      because they are the absence of a grade, not a further grade.
   3. COVERAGE meter (--chart-bar on --chart-track). A share of a fixed
      100%, where the unfilled remainder is itself the finding.

   The ramp is one hue, light to dark, monotone in OKLCH lightness with a
   visible step between neighbours and a lightest step that still clears its
   surface — checked in both themes rather than eyeballed. The dark ramp is
   its own set of steps against the dark card, not the light one inverted. */
:root {
  --chart-bar: #097C4F;
  --chart-track: rgba(3, 3, 3, 0.055);
  --chart-null: rgba(3, 3, 3, 0.26);
  --dots-opacity: 0.88;
  --chart-ramp-1: #2dcc87;
  --chart-ramp-2: #27b175;
  --chart-ramp-3: #219764;
  --chart-ramp-4: #1c7e53;
  --chart-ramp-5: #166543;
  --chart-ramp-6: #114c33;
}
[data-theme="dark"] {
  --chart-bar: #2BA770;
  --chart-track: rgba(243, 246, 244, 0.06);
  --chart-null: rgba(243, 246, 244, 0.24);
  --dots-opacity: 0.62;
  --chart-ramp-1: #afe6ce;
  --chart-ramp-2: #76d4ab;
  --chart-ramp-3: #3cbf87;
  --chart-ramp-4: #33a272;
  --chart-ramp-5: #2b885f;
  --chart-ramp-6: #236d4d;
}

/* One column, always. There was a two-column layout above 900px — this
   used `column-count: 2` rather than grid specifically to avoid stretching
   every card to the tallest in its row (a four-row chart beside an
   eighteen-row one was padded to 670px, 400 of it blank). Two cards side by
   side read as a comparison between them, which most adjacent pairs here do
   not intend — "Records by region" beside "Top 10 cities" is two unrelated
   answers, not two halves of one. **House** — reversed at the reader's
   request; see rule 7.3.

   `column-gap` stays: inert at one column, harmless to leave, and the
   number to restore first if this is ever revisited. `break-inside: avoid`
   on `.chart` (below) is what actually keeps a card whole; multi-column
   layout itself no longer does anything a plain block stack would not. */
.charts { column-gap: 12px; }

/* Groups, because seven cards in one flow have no hierarchy and nothing to
   separate them.

   Carbon's dashboard page is the source for both halves of this. On order:
   "Place the most important at the top of the page and follow the F-pattern
   for the remaining elements, finishing with the least important
   information." On the space between them: white space "either sets elements
   apart or brings them together to distinguish a point's priority" — so the
   gap between two groups is deliberately larger than the 12px between two
   cards, and that difference is the whole mechanism. Cards that touch belong
   together; groups that stand apart do not.

   Which charts land in which group is decided in
   tools/build_dataset_details.py, not here: it is a judgement about the data,
   and it belongs with the data. */
.chartgroup + .chartgroup { margin-top: 40px; }

/* A quiet label. It names the group without competing with the section's own
   h2 above it — same size and tone as the eyebrow that heads each section,
   which is the established way this page labels a band of content. */
.chartgroup__title {
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
  font-weight: 500;
  color: var(--muted-foreground);
  margin: 0 0 2px;
}

/* The line Supabase pairs with every section title. Same size and colour as
   the title — it is the same voice continuing, not a second level. */
.chartgroup__note {
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
  color: var(--muted-foreground);
  margin: 0 0 12px;
  max-inline-size: 58ch;
}

/* The map no longer needs to span anything: every chart is already full
   width (see .charts above), so the map has no more area than a bar chart
   has by virtue of the column layout alone. Its size cap (620px, below)
   carries the hierarchy on its own now — capped and centred rather than
   filling the row, which is still what keeps the drawing at a sensible
   height (left uncapped it comes out 880px tall) without it needing a
   column count to be capped AGAINST. `column-span: all` was removed here:
   spanning all columns of a one-column layout was already a no-op, and
   leaving it in would have described a page that no longer exists. */
@media (min-width: 900px) {
  /* Drawing and legend side by side. Stacked, the legend sat under a centred
     drawing with the span's width empty on both flanks — the card had asked
     for the full row and then used a third of it. Beside the drawing the
     legend reads as part of the same figure and the flanks shrink.

     Carbon's consistency rule ("legends in the same position relative to the
     charting area") stays satisfied: this is the page's only legend. The
     split's swatch list is a key, not a legend — it carries label, share and
     count, so it is a readout of the bar above it. Rule 6.3 keeps the two
     apart, and rule 7.7 records the position.

     The pair is capped and centred rather than stretched. The drawing's
     height follows its width, and left to fill the row it came out 880px
     tall — not hierarchy but a map eating the page. 780px puts the drawing
     near the 560px that reads well and gives the legend its column. */
  .chart--map .map__wrap {
    display: flex;
    align-items: center;
    gap: 32px;
    inline-size: fit-content;
    max-inline-size: 100%;
    margin-inline: auto;
  }
  /* A fixed drawing width, not a flexible one. Flexible, the drawing was
     whatever the legend left it: the choropleth's legend grew from 91px to
     271px when it started naming regions and the map shrank from 657 to 477
     in the same edit, while the dot map beside it kept its width. Two maps
     on the same site at two sizes for no reason the reader can see is
     exactly what Carbon's "all charts should use the same layout" is about.
     620 is the widest that keeps the country under ~510px tall. */
  .chart--map .map {
    flex: 0 0 auto;
    inline-size: 620px;
    max-inline-size: none;
    margin-block-end: 0;
  }
  /* One key per line, low band at the top, so the column reads as a scale. */
  .chart--map .map__legend {
    flex: 0 0 auto;
    flex-direction: column;
    gap: 10px;
  }
}

/* The 12px below each card is the gap between cards stacked in a column, and
   it matches .charts' own column-gap and the 12px .cards uses between the
   catalogue's cards — one rhythm for every card grid on the site.

   It belongs in THIS rule and not beside break-inside above, which is where
   it was: `margin: 0` here is later in the source at the same specificity, so
   it silently won and the cards sat flush against each other with no gap at
   all. A figure's default margin is what `margin: 0` was there to clear, so
   the shorthand stays and carries the 12px itself. */
.chart {
  margin: 0 0 12px;
  break-inside: avoid;
  padding: 20px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
/* Heading on one side, metric on the other, note across the bottom — which
   is Supabase's ChartHeader (a title beside a ChartMetric) with our note
   under it. The metric never wraps under the heading on a narrow card
   because it is a short number; `align-items: baseline` sits it on the same
   line as the title rather than floating above it. */
.chart__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: 16px;
  margin-bottom: 16px;
}
.chart__heading { min-inline-size: 0; }

/* The subject, so a page of findings can still be scanned for one chart. */
.chart__kicker {
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
  color: var(--muted-foreground);
  margin: 0 0 2px;
}
.chart__title {
  font-size: var(--text-base);
  line-height: var(--lh-base);
  font-weight: 600;
  margin: 0;
  text-wrap: balance;
}

/* Supabase's ChartMetric: the value large, its label small underneath,
   aligned to the end of the header. */
.chart__metric {
  margin: 0;
  text-align: end;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.chart__metric-value {
  display: block;
  font-size: var(--text-base);
  line-height: var(--lh-base);
  font-weight: 600;
}
.chart__metric-label {
  display: block;
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
  color: var(--muted-foreground);
}

/* Capped at 80 characters. Uncapped, the note under the full-width map card
   ran 1,031px at 12px — 180 characters a line, where the comfortable range
   is 45 to 75 and WCAG 1.4.8 stops at 80. Every other note on the page was
   already inside that by accident of sitting in a column; the map's was the
   one the full-width span broke.

   58ch, not 80ch: `ch` is the width of a "0", and a "0" is wider than the
   average letter, so 80ch measured out at 107 real characters. 58ch lands
   on 78. The number is calibrated against this font at this size, which is
   why it is written down rather than guessed at again later. */
.chart__note {
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
  color: var(--muted-foreground);
  margin: 0;
  grid-column: 1 / -1;
  padding-block-start: 8px;
  max-inline-size: 58ch;
}

/* ONE grid for the whole chart, not a grid per row — the three columns have
   to line up down the card, and a row-by-row grid cannot do that.

   It was a row-by-row grid, and the bars visibly failed to share a baseline:
   the value column was `auto`, so a row reading 910 sized that column
   narrower than one reading 2,974, which left the two fr columns a different
   width to divide and shifted where each bar began. Column widths have to be
   resolved once, for the whole chart, which is what subgrid does.

   fit-content(38%) on the labels fixes the second half of it. As 0.4fr the
   label column claimed its share whatever was in it, so a tier chart whose
   labels are "A" to "D" pushed its bars 200px clear of them with nothing in
   between. fit-content takes only the width the longest label needs and
   stops at 38%, so short labels sit beside their bars and a long Arabic
   category still cannot squeeze the bars to stubs. */
.chart__rows {
  display: grid;
  grid-template-columns: fit-content(38%) minmax(0, 1fr) auto;
  gap: 10px;
  align-items: center;
}
.chart__row {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
}

/* Without subgrid the row keeps its own three columns. They no longer track
   the longest label in the card, but a fixed value column at least holds the
   bars to one baseline, which is the defect that mattered. */
@supports not (grid-template-columns: subgrid) {
  .chart__rows { display: block; }
  .chart__row {
    display: grid;
    grid-template-columns: minmax(0, 0.32fr) minmax(0, 1fr) 4.5rem;
    gap: 10px;
    margin-bottom: 10px;
  }
  .chart__row:last-child { margin-bottom: 0; }
  .chart__value { text-align: end; }
}
.chart__label {
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
  color: var(--muted-foreground);
  overflow-wrap: anywhere;
}
.chart__value {
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
  font-weight: 500;
  color: var(--foreground);
  /* Tabular figures here and only here: these are a column of numbers that
     have to align vertically. */
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The track is only drawn where the scale is a fixed 0-100%: on a coverage
   chart the empty remainder is meaningful (it is the share of records
   missing that channel). On a counted chart the bars are scaled to the
   largest value, where a track would imply a maximum that does not exist. */
.chart__track { min-height: 10px; }
.chart--coverage .chart__track {
  background: var(--chart-track);
  border-radius: 999px;
}

/* 10px of bar, well under the 24px cap, because these run 13-20 rows deep and
   the row gap has to stay bigger than the bar for the list to read as a list.
   Square where it leaves the baseline, 4px round at the data end — written
   with logical corners so RTL mirrors it without a second rule. */
.chart__bar {
  display: block;
  block-size: 10px;
  min-inline-size: 2px;
  background: var(--chart-bar);
  border-start-start-radius: 0;
  border-end-start-radius: 0;
  border-start-end-radius: 4px;
  border-end-end-radius: 4px;
}

/* The map. Colour is the value channel here and nowhere else on this page,
   so it is the one chart that carries a scale legend.

   The borders between regions are drawn in the card colour, not in a border
   tone: the same rule the bars follow, where white does the separating and a
   stroke would add ink that is not data. At this size a 1.5-unit stroke on a
   1000-unit viewBox paints well under a pixel, which is what keeps thirteen
   shapes reading as one country rather than thirteen outlined tiles.

   The SVG is left to scale with its box; it carries no fixed height, so the
   viewBox's own aspect ratio decides how tall the card gets. */
/* The cap lives HERE, not only inside the 900px query where it started.
   Between 768 and 899 the card is already full width but the two-column
   query has not fired, so the drawing had nothing stopping it: 663x544 at a
   768px viewport and 755x619 at 860px — both TALLER than the 657x539 it
   gets at 1280px, which is the opposite of what a smaller screen wants.
   Above 900px .map__wrap overrides this and lets the drawing take the width
   the legend leaves it. */
.map {
  display: block;
  width: 100%;
  max-inline-size: 560px;
  height: auto;
  margin-block-end: 14px;
  margin-inline: auto;
}
.map__region {
  stroke: var(--card);
  stroke-width: 1.5;
  stroke-linejoin: round;
  transition: opacity 0.15s var(--ease);
}
/* Hovering lifts one region out of the ramp without recolouring it — the
   shade still means what the legend says it means. */
.map:hover .map__region { opacity: 0.55; }
.map .map__region:hover { opacity: 1; }

/* The dot map. The country is drawn first as a quiet ground and the dots
   sit on top of it: without the outline the shape of the place — which is
   the only reason a map beats a ranked list — is left to be inferred from
   where the dots stop.

   The ground is a flat fill, not a ramp. It carries no value at all here;
   its whole job is to say "this is Saudi Arabia" and then get out of the
   way, so it takes the same neutral the off-scale bars take. */
.dots__ground {
  fill: var(--chart-track);
  stroke: var(--border);
  stroke-width: 1;
  stroke-linejoin: round;
}

/* Partly transparent so overlap reads as density: two dots on top of each
   other are darker than one, which is what a dot map is for in a city.

   The opacity differs by theme because the same 0.55 does not mean the same
   thing on the two cards. Rule 4.5 wants a mark at 3:1 against its surface,
   and a LONE dot is the worst case: at 0.55 the light bar (#097C4F blending
   toward white) fell to 1.80:1 while the dark one (#2BA770 blending toward
   near-black) still measured 3.44:1. Solved rather than nudged — light
   needs at least 0.824 to clear 3:1 and dark only 0.452, so 0.88 and 0.62
   give both a margin (3.48:1 and 3.62:1). Density then rides mostly on the
   radius, which is fine: that is the channel FT prescribes for totals. */
.dots__dot {
  fill: var(--chart-bar);
  fill-opacity: var(--dots-opacity);
}
.dots__key { display: inline-flex; align-items: center; gap: 6px; }
.dots__swatch {
  inline-size: 18px;
  block-size: 18px;
  flex-shrink: 0;
  overflow: visible;
}

/* Low to high, left to right, each band naming the range it covers. */
.map__legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
  color: var(--muted-foreground);
  font-variant-numeric: tabular-nums;
}
/* align-items: start, not center: a band can hold two or three regions and
   the swatch belongs beside the FIRST line of them, not floating against the
   middle of a wrapped block. */
.map__key { display: flex; align-items: start; gap: 6px; }

/* The places in one band. Each name keeps its count next to it and the pair
   never breaks across lines — a region separated from its figure is worse
   than no figure. */
.map__places { display: flex; flex-wrap: wrap; gap: 2px 10px; }
.map__place { white-space: nowrap; }
.map__place b { font-weight: 500; color: var(--foreground); font-variant-numeric: tabular-nums; }
.map__swatch {
  inline-size: 10px;
  block-size: 10px;
  border-radius: 2px;
  flex-shrink: 0;
  margin-block-start: 3px;
}

/* The split: one bar, segmented, for "what is the share of each class".
   Reached for when one class holds most of the file — as separate bars that
   is one full-width bar and three hairlines, which answers nothing.

   The 2px separator is a gap in the card colour, not a stroke: a border
   around each segment would add ink that is not data, and at these segment
   widths it would read as a thicker mark rather than a boundary. */
.split {
  display: flex;
  gap: 2px;
  margin-bottom: 14px;
}
.split__seg {
  display: block;
  block-size: 14px;
  min-inline-size: 3px;
  border-radius: 2px;
}
.split__seg:first-child {
  border-start-start-radius: 4px;
  border-end-start-radius: 4px;
}
.split__seg:last-child {
  border-start-end-radius: 4px;
  border-end-end-radius: 4px;
}

/* The key carries the identity, so nothing depends on matching a colour to a
   segment by eye — and it carries both the share and the count, because a
   percentage alone hides how few records a 0.1% slice is. */
.split__key { display: grid; gap: 6px; }
.split__row {
  display: grid;
  grid-template-columns: 10px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 8px;
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
}
.split__swatch { inline-size: 10px; block-size: 10px; border-radius: 3px; }
.split__name { color: var(--muted-foreground); overflow-wrap: anywhere; }
.split__share { font-weight: 500; font-variant-numeric: tabular-nums; }
.split__count {
  color: var(--muted-foreground);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ==========================================================================
   Data dictionary
   ========================================================================== */

/* One grid for the whole table, with the rows as subgrid — the same
   mechanism the charts use, and for the same reason: the field column has to
   size itself to the longest name ONCE and every row has to share that
   width. As a grid per row it was a flat 0.28fr, which handed
   `organization_mobile_number` and `row` the same 224px and left most rows
   with a gap of nothing between the name and what it means.

   fit-content(26%) takes the width the longest identifier needs and stops
   there, so a name like `row` sits right beside its description. */
.dict {
  display: grid;
  grid-template-columns: 1fr;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--card);
}
.dict__row,
.dict__head {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  gap: 4px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.dict__row:last-child { border-bottom: 0; }

/* A header row, because the two halves of a row are a name and a meaning and
   nothing said so. It also gives the table the same shape the design
   system's own Table has: a muted caption row in the small size, over rows
   separated by a hairline. */
/* --muted-foreground, not --tertiary-foreground, and for the reason
   deviation 4 at the top of this file already gives: that token fails AA on
   these surfaces. Measured in dark, composited through the translucent
   --muted strip: 2.72:1 where 12px text needs 4.5. The same swap fixes
   .split__count and .dict__notes, both 4.27:1 on --card. In light all three
   already passed; dark is where the token gives out. */
.dict__head {
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
  color: var(--muted-foreground);
  background: var(--muted);
  padding-block: 10px;
}

@media (min-width: 768px) {
  .dict { grid-template-columns: fit-content(26%) minmax(0, 1fr); }
  .dict__row,
  .dict__head { gap: 16px; align-items: baseline; }
}

/* Without subgrid every row keeps its own two tracks. They stop tracking the
   longest name across the table, but the columns still line up, which is the
   part that matters. */
@supports not (grid-template-columns: subgrid) {
  .dict { display: block; }
  .dict__row,
  .dict__head { grid-template-columns: minmax(0, 0.26fr) minmax(0, 1fr); }
}

/* The field name is the literal column header a buyer will see in the file,
   so it is never translated — including on the Arabic side, where the
   columns still ship with these English names.

   Set in the sans, not a mono face: this page follows the same call the card
   titles already made upstream-versus-here — supabase's own CardTitle is
   `text-xs font-mono`, and this build ships no mono companion, so weight
   carries the "this is an identifier" signal instead of a second family
   loaded for one column.

   It took --brand-text until the table had 27 rows of it, at which point the
   accent was doing nothing but shouting: the brand tone is for the things a
   reader can act on, and these are labels. --foreground at 500 against a
   --muted-foreground description is the contrast that actually orders the
   row — the name is the anchor, and it was the smallest, palest thing in it. */
.dict__field {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  font-weight: 500;
  color: var(--foreground);
  overflow-wrap: anywhere;
  /* Column names are ASCII identifiers even in the Arabic view, so they are
     pinned LTR — otherwise a name ending in a digit or underscore reorders. */
  direction: ltr;
  unicode-bidi: isolate;
  text-align: start;
}
/* Same 80-character cap as the chart note, in the same calibrated `ch`: the
   description column is 830px wide at 1280px, which ran 129 characters a
   line. The cap stops the line, the column keeps its width — so the table
   still fills the card and the sentence still reads. */
.dict__desc {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--muted-foreground);
  margin: 0;
  max-inline-size: 56ch;
}

/* The values a column takes, which the file's own dictionary carries in a
   third column. Kept under the description rather than beside it: some of
   them run to a paragraph, and a third track would squeeze both. */
.dict__notes {
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
  color: var(--muted-foreground);
  margin: 4px 0 0;
}

/* A count beside the section heading, so the reader knows the list's size
   before scrolling it. */
.dict__count {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 16px;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: var(--text-xs);
  line-height: var(--lh-xs);
  color: var(--muted-foreground);
}

/* ==========================================================================
   Responsive
   ========================================================================== */

/* Tailwind's `max-sm:` boundary. At exactly 640px this and the min-width: 640px
   rule above both matched. */
@media (max-width: 639.98px) {
  /* The panel holds one button inside an items-center column, so stretching it
     to a full-bleed bar contradicted the centring the rest of the panel is
     built on. Upstream stretches neither; the hero pair stays as a landing-page
     convention. */
  .hero__cta .btn { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
