/*
 * Bidlo dark palette.
 *
 * NOT generated by `npm run port` — this is the first deliberate divergence
 * from the capture. The captured dark mode is Cursor's warm near-black
 * (#14120b, olive-tinted), which reads muddy next to the Bidlo product. These
 * values are the dark tokens from bidlo-light-1.0/app/globals.css (`.dark`),
 * converted from oklch to sRGB hex:
 *
 *   --background      oklch(0.2134 0 274.03)      #191919
 *   --muted / --card  oklch(0.2435 0 274.03)      #202020
 *   --popover         oklch(0.26 0 0)             #242424
 *   --hover-opaque    oklch(0.3 0 0)              #2e2e2e
 *   --foreground      oklch(1 0.0001 274.03/81%)  #d3d3d3 over --background
 *
 * Everything else in the captured theme derives from --color-theme-bg and
 * --color-theme-fg through color-mix(), so retuning these base tokens carries
 * borders, secondary text, scrollbars and card gradients along with it. That
 * is why this file is short: the capture's token graph already matches the
 * shape of the app's, only the anchors differ.
 *
 * Selector is `:root[data-theme="dark"]` rather than `[data-theme="dark"]` so
 * it outranks the captured rules on specificity, not just source order — the
 * page stylesheets are 100KB+ of generated output and their bundle position
 * should not be load-bearing.
 */

:root[data-theme="dark"] {
  --color-theme-bg: #191919;
  --color-theme-fg: #d3d3d3;
  --color-theme-fg-02: #bfbfbf;

  /* Card ramp. The capture steps bg -> card -> 04 in roughly +7/+2/+2/+6/+5
     luminance; the same steps are kept, re-anchored on the app's neutrals so
     card/popover/hover land exactly on --muted/--popover/--hover-opaque. */
  --color-theme-card-hex: #202020;
  --color-theme-card-rgb: 32 32 32;
  --color-theme-card-01-hex: #222222;
  --color-theme-card-02-hex: #242424;
  --color-theme-card-03-hex: #2a2a2a;
  --color-theme-card-04-hex: #2e2e2e;
  --color-theme-card-hover-hex: #242424;
  --color-theme-card-hover-light-hex: #222222;

  /* The capture tints this one warm to separate it from --card-hex. The app's
     dark mode has no warm axis, so it separates by luminance instead. */
  --color-theme-card-warm-hex: #1e1e1e;
}

/* The phone mockup carries its own token set. Only the two warm anchors move;
   its status/accent hues are illustration, not chrome. */
:root[data-theme="dark"] .mobile-phone-screen {
  --mobile-phone-bg: #202020;
  --mobile-phone-text: #e8e8e8;
}
/* Base UI-style navigation menu — the shared morphing viewport.
 *
 * Hand-maintained, not emitted by `npm run port` (the pipeline never wipes
 * src/styles). It pairs with src/lib/nav-menu.js, which restructures the
 * captured header at runtime: the two `.mega-panel` dropdowns move out of their
 * per-item `.nav__sub-nav` positioners and into one shared popup, so switching
 * from Product to Resources morphs a single box instead of cross-fading two.
 *
 * Nothing here changes how a panel *looks*. No colour, radius, shadow, size or
 * spacing is declared: the popup and the panel both keep the `.mega-panel`
 * class, whose look lives in the hand-maintained src/styles/nav-panel.css, and
 * this file only moves the chrome from one to the other and drives the
 * transitions.
 */

.nav-menu {
  /* Morph is slower than the captured --duration (.14s): the box is travelling
     and resizing at once, and .14s reads as a jump rather than a transition. */
  --nav-menu-morph: 0.28s;
  --nav-menu-fade: 0.16s;
  --nav-menu-ease: var(--ease-out-spring, cubic-bezier(0.25, 1, 0.5, 1));
  --nav-menu-shift: 28px;
  position: absolute;
  top: 0;
  left: 0;
}

/* This root must never carry a transform: it would become the containing block
   for the fixed positioner below, and the whole menu would hang off the header
   instead of the viewport. It is a direct div child of #site-header, so any
   header rule written as `#site-header > div` catches it — header-pinned.css
   did exactly that once. The doubled class outscores `#site-header.is-x > div`. */
#site-header > .nav-menu.nav-menu {
  transform: none;
  transition: none;
}

/* Fixed, like the captured `--mega-wide` positioner. --nav-menu-y is the nav
   item's bottom edge; --nav-menu-gap is the original positioner's padding-top,
   read off the capture at init so the trigger-to-panel gap is unchanged. */
.nav-menu__positioner {
  position: fixed;
  top: 0;
  left: 0;
  padding-top: var(--nav-menu-gap, 5.6px);
  transform: translate3d(var(--nav-menu-x, 0px), var(--nav-menu-y, 0px), 0);
  pointer-events: none;
  transition: transform var(--nav-menu-morph) var(--nav-menu-ease);
}

/* The popup carries the `mega-panel` class, so background, radius and the
   hairline ring come from src/styles/nav-panel.css rather than being copied
   here — retuning that file carries to the popup. Only the box itself is
   overridden: size is driven by the measured panel, and the padding belongs to
   the panel inside. That the ring is a `box-shadow` and not a border is what
   lets the measurement hold: a border would put the popup's content box 2px
   inside the panel it wraps, and `.nav-menu__viewport` would clip the
   difference. #site-header outscores the plain `.mega-panel` rules. */
#site-header .nav-menu__popup {
  width: var(--nav-menu-w, 0px);
  height: var(--nav-menu-h, 0px);
  padding: 0;
  opacity: 0;
  visibility: hidden;
  /* -.4rem is the captured `.nav__sub-nav` entrance, kept as-is. It is also a
     translate rather than a scale on purpose: a scaled ancestor would shrink
     the panel rects nav-menu.js measures to size this box. */
  transform: translateY(-0.4rem);
  transition:
    width var(--nav-menu-morph) var(--nav-menu-ease),
    height var(--nav-menu-morph) var(--nav-menu-ease),
    opacity var(--nav-menu-fade) ease,
    transform var(--nav-menu-fade) var(--nav-menu-ease),
    visibility 0s linear var(--nav-menu-fade);
}

/* #site-header here is not decoration: the base rule above needs that prefix to
   outscore `.mega-panel`, so every rule that overrides it needs one too or the
   popup opens still holding its closed transform. */
#site-header .nav-menu[data-open] .nav-menu__popup {
  opacity: 1;
  visibility: visible;
  transform: none;
  pointer-events: auto;
  transition-delay: 0s;
}

/* Reposition and resize without animating when the menu opens from closed —
   only the fade should be visible. Set for one frame by nav-menu.js. */
.nav-menu[data-instant] .nav-menu__positioner,
#site-header .nav-menu[data-instant] .nav-menu__popup,
#site-header .nav-menu[data-instant] .nav-menu__content {
  transition: none;
}

.nav-menu__viewport {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

/* The moved `.mega-panel`. Chrome is stripped (it lives on the popup now);
   width, padding and contents stay. Absolute so the panel never reflows while
   the popup animates around it. */
#site-header .nav-menu__content {
  position: absolute;
  top: 0;
  left: 0;
  background: none;
  border-radius: 0;
  box-shadow: none;
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--nav-menu-fade) ease,
    transform var(--nav-menu-fade) var(--nav-menu-ease),
    visibility 0s linear var(--nav-menu-fade);
}

#site-header .nav-menu__content[data-active] {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}

/* Directional slide. `data-leaving` marks the panel being replaced, so the
   outgoing panel exits the way the incoming one entered from. */
.nav-menu[data-direction='right'] .nav-menu__content {
  transform: translateX(var(--nav-menu-shift));
}
.nav-menu[data-direction='right'] .nav-menu__content[data-leaving] {
  transform: translateX(calc(var(--nav-menu-shift) * -1));
}
.nav-menu[data-direction='left'] .nav-menu__content {
  transform: translateX(calc(var(--nav-menu-shift) * -1));
}
.nav-menu[data-direction='left'] .nav-menu__content[data-leaving] {
  transform: translateX(var(--nav-menu-shift));
}

/* A leaving panel is mid-fade, so it must still paint. */
#site-header .nav-menu__content[data-leaving] {
  visibility: visible;
}

/* The captured chevron rule keys off `li:hover` / `li:focus-within`, which stops
   matching once the pointer is over the popup. Follow the open item instead. */
#site-header nav li[data-nav-open] > .nav__btn .clone-chevron {
  transform: rotate(180deg);
  opacity: 0.85;
}

@media (prefers-reduced-motion: reduce) {
  .nav-menu {
    --nav-menu-morph: 0s;
    --nav-menu-fade: 0s;
    --nav-menu-shift: 0px;
  }
}
/* Header dropdown panels — the Product and Resources menus.
 *
 * Hand-maintained, like src/styles/nav-menu.css and src/styles/theme-dark.css:
 * tools/04-generate.mjs imports it into every page but never rewrites it from
 * the capture. It is the *only* source of truth for how these panels look —
 * tools/fragments/nav-panel.mjs strips the captured `.mega-*` rules out of the
 * tail block, so there is nothing here to override and nothing there to drift.
 *
 * The card blocks are deliberate and were kept when the rest was not: they are
 * the one place on the site where a nav item gets a picture and a sentence, and
 * that is worth the space in a marketing header. What went is the vocabulary
 * they were drawn in — a white 18px sheet, pastel `--blue`/`--pink`/`--yellow`
 * fills, six-hue cartoon art on `#191918` ink and Notion's `#2383e2` links,
 * none of which appears anywhere else on this site or in Bidlo Lite. The
 * cartoon art is gone outright as of 2026-08-31: Product's three cards end on a
 * window mockup instead, in the block headed "Card windows" below.
 * Every
 * value below is a token the capture already ships, so the panel derives from
 * the same graph as `.card`, `.btn` and the footer's language flyout, and
 * retuning that graph (theme-dark.css, say) carries here.
 *
 * That token discipline is also what fixes dark mode. The captured rules had a
 * `[data-theme=dark]` variant for the panel and the foot and none at all for
 * the cards, so a dark header opened onto pastel cards holding near-black ink.
 * Nothing here needs a dark variant except the drop shadow — the one value that
 * cannot come off `--color-theme-fg`, since that token inverts between themes
 * and a shadow must stay black in both.
 *
 * Three structural notes:
 *   - **Two panels, two shapes.** Product is still three picture-cards across
 *     720px — it held four in a 480px 2x2 until 2026-08-30, then three at
 *     Product's own width. Resources is not a card grid at all as of
 *     2026-08-31: two labelled columns of icon-plate rows beside a tinted rail
 *     of article thumbnails, 880px wide, in the block at the foot of this file.
 *     `unpinResourcesPanel` in the fragment drops the class that pinned that
 *     panel to the window centre, so it opens under its own trigger like the
 *     other one.
 *   - The panel's hairline is a `box-shadow` ring, not a border.
 *     src/lib/nav-menu.js measures the panel to size the shared morphing popup
 *     and both carry `.mega-panel`; a real border would leave the popup's
 *     content box 2px inside the panel and `.nav-menu__viewport` would clip it.
 *   - `.mega-card` is styled unscoped on purpose. src/lib/mobile-menu.js clones
 *     these cards into the phone sheet, which mounts on <body>, so a
 *     `#site-header` prefix would leave that sheet behind. The sheet turns each
 *     card into a full-width row, drops the window mockup Product's cards end
 *     on and shows the lucide glyph the fragment ships instead — see
 *     `.mega-card__glyph` at the bottom of this file and the layout half in
 *     src/styles/mobile-menu.css.
 *
 * Nothing here is visible at rest — the panels are absolutely positioned and
 * hidden until a menu opens — so this file costs `pnpm run verify` no pixels.
 */

/* ------------------------------------------------------------------ *
 * Panel
 * ------------------------------------------------------------------ */

.mega-panel {
  /* The one value not derived from a token. See the note above. */
  --mega-panel-shadow: 0 0.75rem 2rem rgb(0 0 0 / 8%), 0 0.125rem 0.5rem rgb(0 0 0 / 5%);
  /* Narrower than the captured 872. That width was drawn around 110px of
     cartoon art per card; three columns of the blocks below sit comfortably in
     720. */
  width: min(720px, calc(100vw - 32px));
  padding: var(--spacing-g1\.5);
  border-radius: var(--radius-md);
  /* The page's own surface, so the sheet reads as a piece of the site rather
     than a white card floating over it. The cards inside then sit a step down
     on `--color-theme-card-hex`, which is the relationship every other card on
     the site has to the background behind it. */
  background: var(--color-theme-bg);
  box-shadow: 0 0 0 1px var(--color-theme-border-02), var(--mega-panel-shadow);
  color: var(--color-theme-text);
}

:root[data-theme='dark'] .mega-panel {
  --mega-panel-shadow: 0 0.75rem 2rem rgb(0 0 0 / 45%), 0 0.125rem 0.5rem rgb(0 0 0 / 30%);
}

/* Resources, which is not a grid of cards at all — see the block at the foot of
   this file. 800 is what its three columns need: two `--mega-col-w` link
   columns beside a ~300px article rail, plus the gaps and the panel's own
   padding. It was 880 until the descriptions were cut on 2026-08-31 — the
   widest description sets the column and the column sets the panel, so shorter
   copy is what buys a narrower sheet. The desktop nav only exists from 900px
   up, so the `100vw - 32px` clamp bites at exactly one width (868px at a 900px
   viewport) and nowhere else, and no longer even there. */
.mega-panel--resources {
  /* **No width of its own as of 2026-09-01: it inherits `.mega-panel`'s 720**,
     which is the Product panel's width, so the two sheets are one object again
     — a reader moving between the triggers sees the contents change and not the
     sheet. It was 1040 in the capture, 880 when it was rebuilt, and 800 when
     the descriptions came out; each of those was the sum of what the columns
     then needed. 720 is that sum now: two 160px link columns and a ~320px
     Explore card, plus a 10px inside gutter, a 40px outside one and the panel's
     own `g1.5` a side.

     The order to derive it in is columns first, panel second — the column
     widths are set by their contents (see `--mega-col-w`) and the card takes
     what is left, so a wider column is a narrower card at a fixed panel width
     and there is no third thing to absorb it. */

  /* **The panel is the grid, not `.mega-panel__cols`, and that is the whole of
     the 2026-09-01 change.** The foot used to be a block under a three-column
     grid, so its hairline ran the full width of the sheet and the Explore card
     stopped ~50px above it — a filled card floating in the middle of a column
     with a rule drawn under it.

     Now the two rows are one grid: the three columns sit in row 1, the foot in
     row 2 spanning **columns 1 and 2 only**, and the Explore column spans both
     rows. Two things follow, and both are what was asked for. The card runs the
     full height of the panel and closes on the same line the foot does, so the
     third column reads as its own object rather than as a short column. And
     the hairline ends at the second column's track edge — one `g1.5` gutter
     short of the card — which says outright that it is the footer for the
     destinations and not for the articles.

     `.mega-panel__cols` is `display: contents` for this: it keeps the markup
     (and `addResourcesFoot`'s shape assertions, which read three closing tags
     in a row) while its three children become items of this grid. */
  display: grid;
  /* The link columns are sized to their content and the rail takes what is
     left. See `--mega-col-w` below for where 224 comes from. */
  grid-template-columns: var(--mega-col-w) var(--mega-col-w) 1fr;
  /* **The two gutters are not the same as of 2026-09-01**, at the owner's
     direction, and that is a deliberate break from the one-gap grid this was.
     The three columns are two groups, not three peers: About/Careers/Legal and
     Research/Documentation/Help are one list of destinations split over two
     tracks, and the Explore card is a different kind of thing. One uniform
     `g1.5` said the opposite — the link columns read as far apart as the card
     was from them, so the sheet read as three separate things and the two lists
     did not group.

     So the gutter *inside* the pair is `g1` (10px) and the one before the card
     is 40px — `g1` of grid gutter plus `g3` of margin on the Explore column
     itself, since a grid cannot state two column gaps. Keep them in that ratio
     rather than at these exact values: the inside gutter has to read as clearly
     smaller than the outside one, which is the same argument the row/column gap
     ladder makes one level down.

     The foot's hairline stops at the second column's track edge, so it now ends
     **40px** short of the card rather than 15 — the gutter between two columns
     is still exactly the air a rule should leave, and the rule is the two link
     columns' footer, so it takes their outside gutter. */
  column-gap: var(--spacing-g1);
  /* The foot carries its own `margin-top`, so the rows need no gap of their
     own — and the Explore column spans both, so a row gap would appear inside
     the card as a stripe of panel. */
  row-gap: 0;

  /* A link column. **224 -> 200 -> 176 over 2026-09-01**, at the owner's
     direction, in two passes on the same day. 224 was set by the widest
     *description*, and the descriptions came out on 2026-08-31 — see `linkRow`
     in tools/fragments/nav-panel.mjs — which left these columns 60-odd pixels
     wider than anything in them needed.

     What sets 176 is the widest *title*: "Documentation" is ~105px of
     `--text-base` ink, plus the 2rem plate and the 0.75rem beside it, i.e.
     ~149 in a 176px column. That is ~27px of slack, which is about two
     characters — a longer name than "Documentation" needs re-measuring here
     before it ships, since a title that wraps takes its row 20px taller than
     every other row in the panel and breaks the shared pitch.

     The first pass held the panel at 800 and gave the 24px a column to the
     Explore card (292 -> 340); the second took the panel down to 720 instead,
     because the card was then the thing that read as too wide. Both columns and
     the card are narrower than they were this morning. Put the descriptions
     back and this has to go back to 224 and the panel to 800 with it.

     **176 -> 160 later the same day**, in the pass that split the two gutters
     above: with the pair now 10px apart rather than 15 they read as one list,
     and a list of six short names in two 176px tracks has visible air after
     every one of them. 160 leaves "Documentation" — the widest title, ~105px of
     `--text-base` ink beside its 2rem plate and 0.75rem gap, i.e. ~149 — about
     11px of slack, which is one character and change. That is the floor: a name
     any longer than "Documentation" has to be measured here before it ships,
     because a title that wraps takes its row 20px taller than the rest and
     breaks the pitch across the whole sheet. */
  --mega-col-w: 160px;

  /* How tall a Resources column's content block is. **It is this panel's own
     number now**, and that is a change of 2026-08-31: it used to be a *measured
     Product card*, so the two panels' content occupied the same box and a
     reader moving between them saw one sheet whose contents changed rather than
     two sheets of different proportions.

     That rule was dropped at the owner's direction. It held while the two
     panels wanted the same height anyway; it stopped being free the moment the
     Product card started getting shorter, because every step the card took down
     — 208, 173, 148 as the window was cropped and the sentences came and went —
     dragged this panel's three columns down with it, and a Resources column
     squeezed to a Product card's height is a rail of 28px thumbnails and 13px
     titles. The two panels are still the same *width* and still share a foot;
     they are allowed to differ in height.

     So 173 is solved from what this panel holds: three link rows at
     `--mega-row-h` plus their gaps come to 167, and an Explore rail whose rows
     take a 36px thumbnail beside a two-line 14px title wants ~170. 173 leaves
     both a little slack. `--mega-art-row-h` and the thumbnail are solved
     against it and move with it. */
  --mega-col-h: 173px;
  /* The Explore thumbnail, and **the direction this arithmetic runs was
     reversed on 2026-09-01**. It used to be solved backwards out of the card —
     `--mega-col-h` less the card's pads and gaps, divided by three, less the
     row's own padding — because the card was exactly `--mega-col-h` tall and
     three rows had to fit it. The card is not that any more: it spans the
     foot's row as well as the columns' and takes whatever the grid comes to,
     so nothing forces a thumbnail to be small. It is stated directly now and
     the row follows it.

     48px, up from the 36 the old arithmetic yielded, at the owner's direction —
     these are the only pictures in the header and at 36 they were smaller than
     the glyph plates in the columns beside them. The width follows from the
     40:21 ratio (91px), which leaves ~190px of the ~308px card for the title:
     two lines at `--text-sm`, which is what these titles take. Grow it further
     and the card, not the link columns, is what sets the panel's height — the
     rows are 60 each at this size against ~180px of link column, so there is
     little headroom left. */
  --mega-art-thumb-h: 3rem;
  /* One row of the rail: the thumbnail plus the row's own 0.375rem a side. It
     is *not* `--mega-row-h` — a link row is sized by its text and an article
     row by its picture, so the two only ever agreed by coincidence. */
  --mega-art-row-h: calc(var(--mega-art-thumb-h) + 2 * 0.375rem);
  /* Every row in this panel is this tall and every row gap is `g0.5`, so all
     three columns step down on one pitch and the rows line up across the sheet
     rather than each column keeping its own rhythm. It was 66px link rows at a
     5px gap beside 52px article rows at a 4px gap — three columns that agreed
     on nothing below their heads. 58px is what a two-line link row needs
     (21 + 21 of text in 12 of padding) and what a 5rem thumbnail at 40:21
     comes to (42 in 12).

     **The gap ladder is the other half, and it is what the panel was reported
     for twice.** Whitespace is the only thing grouping this sheet — nothing is
     boxed except the rail — so the gap separating two groups has to be visibly
     larger than the gap inside one. Measured on 2026-08-31 it was 23px under a
     column label, 29px between rows and 43px between columns: three numbers
     close enough to read as one, so twelve links grouped into nothing and the
     reader had to scan all twelve. Drawn now at roughly

       label -> its list    ~9px   (the label belongs to the rows under it)
       row -> row          ~18px   (2x the above)
       column -> column    ~50px   (nearly 3x that again)

     Each of those three is set somewhere else in this file — the label's at
     `.mega-panel__col-head`, the rows' at `.mega-panel__cards--list` and
     `.mega-panel__articles`, the columns' at `.mega-panel__cols`. Move one and
     re-measure the other two; the ratios are the point, not the values. */
  --mega-row-h: 2.75rem;
}

/* ------------------------------------------------------------------ *
 * Head and foot
 *
 * Both were Notion blue with a hover underline. Neither colour nor underline
 * appears anywhere else on this site: links in chrome are ink and shift tone on
 * hover, and the only accent it spends is one orange, on the home page.
 * ------------------------------------------------------------------ */

.mega-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-g1);
  margin: 0 0 var(--spacing-g1\.25);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  letter-spacing: var(--tracking-sm);
}

.mega-panel__head > span {
  font-weight: var(--font-weight-medium);
  color: var(--color-theme-text-tertiary);
}

.mega-panel__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-g1\.5);
  /* The captured foot was a filled bar bled into the panel's padding. A
     hairline says the same thing without a third surface in the box. */
  margin: var(--spacing-g1\.5) 0 0;
  padding: var(--spacing-g1\.25) 0 0;
  border-top: 1px solid var(--color-theme-border-01-5);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  letter-spacing: var(--tracking-sm);
  color: var(--color-theme-text-tertiary);
}

.mega-panel__head a,
.mega-panel__foot a {
  color: var(--color-theme-text);
  font-weight: var(--font-weight-normal);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--duration) var(--ease-out-spring);
}

/* Except the dead ones: Base.astro paints `[data-dead-link]` red site-wide so
   the remaining rebrand work stays visible, and the foot holds one of them.

   Ink at rest and muted on hover, which is the footer's columns backwards. The
   footer's links are whole rows with nothing beside them to be distinguished
   from; these two sit inside a sentence, so if the link and its sentence are
   both muted the link stops being one. The sentence takes the tertiary step
   instead. */
.mega-panel__head a:hover,
.mega-panel__foot a:hover {
  color: var(--color-theme-text-sec);
}

/* The changelog closer is two tones as of 2026-09-01, at the owner's
   direction, and it is what is left of a longer line. The foot read
   "Updates every week. See what's new →": a tertiary sentence saying what the
   changelog is, then a full-ink link. The sentence went to shorten the line,
   and the link took its job instead — "See what's" keeps the sentence's own
   tertiary ink and normal weight, so it reads as the lead-in it replaced,
   while "new →" is the only full-ink thing in the line and is what the eye
   lands on. The lede is only in the foot's left line; the app closer beside it
   is one destination and takes one colour. On hover the whole link mutes to
   secondary and the two halves meet, which is what makes the hover read as the
   line answering rather than half of it. */
.mega-panel__foot a .mega-panel__foot-lede {
  color: var(--color-theme-text-tertiary);
}

/* ------------------------------------------------------------------ *
 * Cards
 *
 * `.card` in the captured stylesheet, restated: it is frozen Tailwind output
 * emitted per page, and the class was never used in the header, so the header
 * cannot simply borrow it.
 * ------------------------------------------------------------------ */

.mega-panel__cards {
  display: grid;
  /* Product: one row of four as of 2026-09-01, where it was three. Resources
     overrides this below.

     **The panel's width did not move with it**, which is the whole of the
     change: 720 less its own `g1.5` a side and three 10px gutters divided four
     ways is a **165px card** where three ways gave 223. Everything else about
     the card follows from that one number — the content box is 127.5, the
     window's visible width is that plus one `g2` pad, and `--mega-win-seen-h`
     went 94 -> 122 to take the card from landscape to portrait rather than
     leaving it square. Adding a fifth card means deriving the panel from the
     columns and not the other way round; see the note on `.mega-panel--resources`. */
  grid-template-columns: repeat(4, 1fr);
  /* The gap the home page's own card grids use. */
  gap: var(--spacing-g1);
}

/* Resources' two link columns are a single stack each. The gap is wider than a
   card grid's because these rows carry no plate of their own at rest: what
   separates them is space, so it has to be visible — and it is the same `g0.5`
   the Explore rail's articles take, which is what puts all three columns on one
   pitch. Step 2 of the gap ladder; change it in both places or nothing lines
   up.

   The block is at *least* `--mega-col-h` tall — a Product card's height, so a
   column is never shorter than the cards in the panel beside it — and its rows
   sit at their own `--mega-row-h` and stack from the top. `align-content` has
   to say `start` outright: a grid with room to spare stretches its auto tracks
   by default, which is exactly the justified-looking spread this replaced on
   2026-08-31. Rows are a fixed size and the slack goes to the bottom of the
   column, where it is invisible. */
.mega-panel__cards--list {
  grid-template-columns: 1fr;
  grid-auto-rows: min-content;
  align-content: start;
  min-height: var(--mega-col-h);
  /* 6px, where the Explore rail's rows still take `g0.25`. The two were the
     same while a link row was two lines of text in 12px of padding: at 54px
     tall the row's own padding *was* the air between rows, and a real gap on
     top of that would have doubled it. With the description gone the row is 44
     around a 32px plate, and 2.5px between two 44px boxes reads as one thick
     block of six rather than six destinations. The rail keeps the tight gap
     because its rows are pictures with a visible edge of their own; these have
     none at rest.

     **What this has to be measured against is plate to plate, not box to
     box.** A row carries 6px of its own above and below the plate, so the ink
     gap is `6 + gap + 6`. This went in at 16 and was reported as too big
     straight away, which is 28px of nothing between two 32px icons; 6 puts it
     at 18, which is where the panel's own gap ladder says a row-to-row gap
     belongs (see `--mega-row-h`, step 2 of it). Re-derive it that way if
     `--mega-row-h` or the plate moves.

     The consequence is that the two link columns end ~29px above the Explore
     rail, and that is accepted rather than solved. Closing it means either a
     bigger gap, which is what was just rejected, or a shorter `--mega-col-h`,
     which shrinks the rail's thumbnails — also rejected. These columns carry no
     fill, so where their last row ends is not an edge the reader sees; the
     panel's bottom boundary is the foot's hairline, well below both. */
  gap: 0.375rem;
}

.mega-card {
  --mega-card-paper: var(--color-theme-card-hex);
  /* A step *lighter* than the card, so the window reads as lifted off it. In
     light that is the page's own surface, one above `--card-hex`. Dark inverts
     that ladder — `--bg` is the darkest token there — so it continues the
     ladder by hand instead, the way `--hc-doc-paper` does in
     src/styles/hero-chat.css and for the same reason. It has to stay clear of
     `--card-hover-hex` (#242424 in dark) or the frame vanishes on hover. */
  --mega-win-paper: var(--color-theme-bg);
  position: relative;
  /* The window is cut by the card's own edges. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* g1.5 until 2026-08-31. The extra step is air the copy and the screen both
     wanted; the window's negative margins below have to move with it, or the
     crop stops landing on the card's own edges. */
  padding: var(--spacing-g2);
  border-radius: var(--radius-xs);
  background-color: var(--mega-card-paper);
  color: var(--color-theme-text);
  text-decoration: none;
  transition: background-color var(--duration) var(--ease-out-spring);
}

/* `.card`'s hairline, which it draws as a pseudo-element rather than a border
   so the 4px radius stays true against the fill. */
.mega-card::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 30;
  pointer-events: none;
  border: 1px solid var(--color-theme-border-01);
  border-radius: inherit;
}

.mega-card:hover {
  --mega-card-paper: var(--color-theme-card-hover-hex);
}

:root[data-theme='dark'] .mega-card {
  --mega-win-paper: #2a2a2a;
}

/* `medium` (500). It was taken to `semibold` on 2026-08-31, on the argument
   that at 500 over a `normal` description the title and its sentence read as
   one grey block, and put back the same day at the owner's direction: 600 at
   `--text-base` across nine rows is a heavier header than this one wants, and
   the title already separates on colour (`--text` over `--text-sec`). If the
   hierarchy is ever reported as weak again, the sentence's colour is the place
   to look before the title's weight. */
.mega-card__title {
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  letter-spacing: var(--tracking-base);
  font-weight: var(--font-weight-medium);
  color: var(--color-theme-text);
}

/* A step under the 16px title, where the capture set both at `--text-base`.
   Two things pay for it. A sentence set the same size as the title it sits
   under separates from it on colour alone, and this panel's titles are
   `medium` rather than `semibold` (see `.mega-card__title`), so colour was
   carrying the whole hierarchy. And the card's content box is 183px: at 16px
   "Clear project management." measures 201 and wraps to two lines, which takes
   the card 172 -> 196 and turns it back into three stacked blocks. At 14px it
   is 176 and fits, with the card a further 3px shorter than it was.
   ~183px at `--text-sm` is about 26 characters — measure rather than count if a
   new sentence is close, since these are proportional and "management" is not
   "24/7". */
.mega-card__desc {
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  letter-spacing: var(--tracking-sm);
  color: var(--color-theme-text-sec);
  text-wrap: pretty;
  /* See the note on `.mega-card__win` below: this is what holds every card's
     window on the same line when the descriptions wrap to different heights. */
  margin-bottom: auto;
}

/* ------------------------------------------------------------------ *
 * Card windows
 *
 * A Product card ends on the top-left corner of the screen that route is about
 * — a map for Bid, a schedule for Build, a flow canvas for Automate. It
 * replaces the captured 120x90 drawing, which drew another product's software
 * and could not be made to say otherwise; the markup half is
 * `swapProductWindows` in tools/fragments/nav-panel.mjs, whose header carries
 * the reasoning.
 *
 * **The two boxes do different jobs and the split is the whole trick.**
 * `.mega-card__win` is a **viewport**: a fixed height, `overflow: hidden`, and
 * negative margins that carry it out to the card's right and bottom edges.
 * `.mega-card__win-frame` is the **sheet**: `--mega-win-w` x `--mega-win-h`
 * (260x190), bigger than that viewport on both of those axes, so it is cut by
 * them and the screen reads as carrying on past the card. What the card shows
 * is **204x94** of it, which at the 0.5 the screen is scaled by is 408x156 of
 * app pixels against a sheet of 520x348.
 *
 * **Two rounds got here.** The first cropped the same 260x190 sheet to 204x93
 * at a 0.7 scale with no title bar, and it was reported as busy: three cards
 * each ending on a bare pane of dense app UI, every pane a different texture,
 * and — the other half of the same report — the components inside reading
 * oversized, because a 1:1 crop shows app UI at app scale in a box a fifth of
 * an app window's size (a 32px map marker took 11% of the visible width where
 * the app draws it at about 3%). The second dropped the crop for a whole
 * 186x110 sheet at 0.5, which fixed the scale and lost the effect; the bleed
 * was what the owner had liked. This is both — the 0.5 scale and the title bar
 * from the second, the bleed from the first — and the marker is 8.6% now.
 *
 * The trade is that app type lands at 7px rather than 9.8 and stops being
 * readable. That is the point rather than a cost: the card's own title and
 * sentence say what the screen is, and a graphic whose text is texture asks
 * nothing of the reader. If a screen ever needs a word read, it belongs in the
 * copy, not in the sheet.
 *
 * **The card's own size did not change across either round** and must not: the
 * viewport's height is `--mega-win-seen-h` plus shadow room, and its negative
 * bottom margin takes the card's `g2` pad straight back off, so what it
 * contributes to the card is what the uncropped version contributed. The card
 * is 172px against 223 wide, deliberately landscape rather than square — see
 * the note on `--mega-win-seen-h`. Moving `--mega-win-seen-h` or
 * `--mega-win-w` means recomposing all three drawings, since each is drawn to
 * fill exactly the box the viewport shows.
 * ------------------------------------------------------------------ */

.mega-card {
  /* The sheet's own height, chrome included, and the height of that chrome.
     Both are *rendered* lengths, not app pixels: the bar is this site's mockup
     frame and not a bar the app draws, and the sheet's height is what decides
     the card's. Declared here rather than on the box so the two numbers the
     card's height comes off are visible from the card. The width is not stated
     at all — the sheet is `width: 100%` of the card's content box, so it lines
     up with the title and the sentence above it by construction. */
  --mega-win-w: 260px;
  --mega-win-h: 190px;
  --mega-win-bar: 16px;
  /* How much of that sheet the card shows down its own height. The width is not
     stated: the viewport below runs out to the card's right edge, so it comes to
     the content box plus one `g2` pad (185.5 + 18.75 = 204.25) on its own.

     It was 129 until 2026-08-31, which drew a 223x207 card — square, and
     reported as such. A card is a title, a sentence and a picture, and the
     picture is the only part of it with a free dimension, so the whole of that
     report lands here: 94 takes the card to 223x172, i.e. landscape at about
     1.3, and it costs the three drawings nothing but bottom bleed. What it
     does cost is `--mega-col-h`, which is a Product card's height by
     definition and has to move with it. */
  /* **122 as of 2026-09-01**, where it was 94 and 129 before that. The panel
     went to four columns in the same pass, so the card came 223 -> 165 wide,
     and at 94 that would have drawn a 165x172 card — square, which is the exact
     report the 129 -> 94 pass was answering one width up. The picture is the
     only part of a card with a free dimension, so a change to the card's shape
     always lands here: 122 draws a **165x200** card, or 165x221 where the
     description takes two lines, i.e. portrait at about 1.2 to 1.3.

     It is the card's height control and the four drawings are composed against
     what it leaves — see the note on `SEEN_W`/`SEEN_H` in
     tools/fragments/nav-panel.mjs. Moving it means walking all four. */
  --mega-win-seen-h: 122px;
}

/* The viewport. It shows `204.25 x --mega-win-seen-h` of a sheet that is
   `--mega-win-w` x `--mega-win-h`, so the sheet runs off the card's right edge
   and its bottom edge and is cut by both. The negative right and bottom margins
   are the card's own `g2` padding negated, so the three have to move together;
   the -5px on the left and the 5px of padding are the sheet's drop shadow, which
   needs room on the two sides that are not cut.

   Its height is `--mega-win-seen-h` plus that shadow room, and the negative
   bottom margin takes the `g2` straight back off, so what this box contributes
   to the card's height is exactly what the uncropped version contributed:
   94 + 5 + 4.69 - 18.75 = 84.9. The card is exactly `--mega-win-seen-h` less
   than the sum of its other parts, which is what makes that one variable the
   card's height control. */
.mega-card__win {
  box-sizing: border-box;
  /* `margin-bottom: auto` on the description above is what pins this to the
     foot of the card. At 127.5px of content box the four descriptions no
     longer all fit on one line — "More efficient bids" does and "Clear project
     management" does not — and a grid row stretches every card to the tallest,
     so without it a one-line card would draw its window 21px above its
     neighbours'. The slack goes between the description and the window, which
     is the one gap in the card that can carry it. */
  height: calc(var(--mega-win-seen-h) + 5px);
  padding: 5px 0 0 5px;
  margin: var(--spacing-g0\.5) calc(-1 * var(--spacing-g2)) calc(-1 * var(--spacing-g2)) -5px;
  overflow: hidden;
}

.mega-card__win-frame {
  /* The sheet is the screen's positioning context; the screen inside is drawn
     at the app's own pixel values and scaled once. See the block below. */
  position: relative;
  --mega-win-scale: 0.5;
  /* The app's surface tokens, mapped onto this site's the way
     src/styles/automate-mockup.css maps them:

       --popover / --background   --np-raise   a bar, a node card
       --muted                    --np-sink    a canvas, a map's loading fill
       --border                   --np-line
       --foreground               --np-text
       --muted-foreground         --np-text-sec
       --primary                  --np-accent  (blue, not the logo's green)

     Dark inverts the light ladder — `--bg` is the darkest token there — so the
     two surfaces continue it by hand, the way `--hc-doc-paper` does in
     src/styles/hero-chat.css and for the same reason. */
  --np-paper: var(--mega-win-paper);
  --np-sink: var(--color-theme-card-hex);
  --np-raise: #fff;
  --np-line: var(--color-theme-border-02);
  --np-text: var(--color-theme-product-text);
  --np-text-sec: var(--color-theme-product-text-sec);
  --np-accent: #2483e2;
  /* `--red`, converted from the app's oklch. A shadow cannot come off a token
     that inverts between themes and neither can this: the app carries a lighter
     red in dark, so both are stated. */
  --np-red: #e1584d;
  /* The flow canvas' dot grid. The app draws it at `text-foreground/15` and
     this drew 18 to hold up at a 0.5 scale; **11 as of 2026-09-01**, at the
     owner's direction — at 18 the grid read as the loudest thing on the
     Automate card, over the node cards it is meant to sit behind. Its own
     comment in the app calls it "texture rather than an engineering grid". */
  --np-dot: color-mix(in srgb, var(--color-theme-fg) 11%, transparent);
  --np-wire: color-mix(in srgb, var(--color-theme-fg) 26%, transparent);
  /* `CATEGORY_TILE`: Triggers yellow, Data blue. */
  --np-trigger-bg: #f5efe5;
  --np-trigger-fg: #c98c00;
  --np-data-bg: #e8f0f3;
  --np-data-fg: #0067a5;
  display: block;
  /* Bigger than the viewport above on both of the axes it is cut on. */
  width: var(--mega-win-w);
  height: var(--mega-win-h);
  border: 1px solid var(--color-theme-border-02);
  border-radius: 8px;
  overflow: hidden;
  background: var(--mega-win-paper);
  /* A contact shadow plus one soft lift, and the lift carries a **negative
     spread** — that is the whole of the fix. The pair before this was
     `0 1px 2px / 8%` over `0 6px 18px / 14%`: an 18px blur at full spread
     throws ink as far sideways as it does downward, so on a card fill barely a
     step darker than the sheet it read as a grey halo round three sides of the
     window rather than as the window sitting on the card. Pulling the blur in
     by 8px puts the ink under the sheet, where a shadow belongs, and lets the
     opacity go up rather than down. */
  box-shadow: 0 1px 2px rgb(0 0 0 / 5%), 0 10px 24px -8px rgb(0 0 0 / 20%);
  /* Ink is `currentColor` at an opacity and nothing else, so the three screens
     follow the theme with no dark variant and no second palette to keep in step
     — which is precisely what the captured drawings, keyed by hex, could not
     do. */
  color: var(--color-theme-fg);
}

/* The one value that cannot come off a token: `--color-theme-fg` inverts
   between themes and a shadow has to stay black in both, so dark deepens it
   by hand exactly as `.mega-panel` does. */
:root[data-theme='dark'] .mega-card__win-frame {
  box-shadow: 0 1px 2px rgb(0 0 0 / 35%), 0 10px 24px -8px rgb(0 0 0 / 65%);
  --np-sink: #232323;
  --np-raise: #333333;
  --np-red: #ee685a;
  --np-trigger-bg: #674a26;
  --np-trigger-fg: #c99347;
  --np-data-bg: #00254b;
  --np-data-fg: #2d87cc;
}

/* ------------------------------------------------------------------ *
 * The title bar
 *
 * Chrome for this site rather than anything the app draws — a map view, a
 * timeline and a flow canvas all open straight onto their own content — so
 * every length here is a rendered pixel and none of it is scaled with the
 * screen below.
 *
 * It exists to make the sheet read as *a picture of software* rather than as
 * the card's subject. Three cards each ending on a bare pane of dense app UI
 * gave the panel three unrelated textures and no obvious frame around any of
 * them; a bar at the top says "window" in one glance and costs nothing to read.
 *
 * **It carries no words**, which is the difference between this and the version
 * that was tried and pulled on 2026-08-31. That one set the view's name in the
 * bar, which named a screen the card's own title had already named and cost a
 * quarter of a 93px crop to do it.
 *
 * **The blank address pill came out later the same day, at the owner's
 * direction.** It was there to say "browser", and it said "unfilled search box"
 * instead: an empty rounded field 42% of the way across a 204px crop is a
 * control, and a control the reader cannot use in a window with no browser
 * round it is the one thing in these three screens that looked like a mistake.
 * Three lights alone say "window" in one glance, which was the pill's job as
 * much as theirs.
 *
 * **The three lights are the `empty-mockups.js` trap and dropping the pill took
 * one of the three guards off.** That script finds a window by a `<div>`
 * holding exactly three small round children over 4px wide, and these panels
 * are hidden with `visibility`, so their boxes are measurable and it really
 * would reach them. The bar now *is* three round children. What still saves it
 * is that every box in here is a `<span>` and the script's selector is
 * `querySelectorAll('div')`, and that the lights are 4px, which fails its
 * `b.width > 4` test. Do not turn any of these into divs and do not grow the
 * lights.
 * ------------------------------------------------------------------ */

.mega-card__win-bar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 3px;
  height: var(--mega-win-bar);
  padding: 0 5px;
  border-bottom: 1px solid var(--np-line);
  background: var(--np-raise);
}

.mega-card__win-dot {
  flex: none;
  width: 4px;
  height: 4px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--color-theme-fg) 20%, transparent);
}

/* ------------------------------------------------------------------ *
 * The screen inside the sheet
 *
 * What sits in the sheet is Bidlo Lite's own markup at Bidlo Lite's own pixel
 * values, scaled **once** by `--mega-win-scale`. That is the opposite of what
 * src/styles/hero-chat.css and src/styles/hero-mockup.css do — they bake 0.75
 * and 0.86 into every length, because they are drawn beside captured chrome and
 * have to land on the capture's own type ladder. Nothing here is beside
 * anything, so the cheaper and more faithful thing is to write the app's
 * numbers and scale the result, the way `.ig-scaler` already does in
 * src/styles/integrations.css.
 *
 * **So every length and type size below is the app's own value in the app's own
 * pixels. Do not scale one by hand.** The app's number is in the comment beside
 * anything that is not obvious.
 *
 * At 0.5 the visible 204x78 of screen is **408x156 of app pixels**, and that
 * is the box tools/fragments/nav-panel.mjs composes all three screens against;
 * the full screen behind it is 520x348. The visible width is the card's content
 * box plus one `g2` pad, since the viewport runs out to the card's right edge,
 * and the visible height is `--mega-win-seen-h` less `--mega-win-bar`.
 * ------------------------------------------------------------------ */

.mega-card__screen {
  position: absolute;
  /* Under the title bar, which is the only thing above it in the sheet. */
  top: var(--mega-win-bar);
  left: 0;
  width: calc(var(--mega-win-w) / var(--mega-win-scale));
  height: calc((var(--mega-win-h) - var(--mega-win-bar)) / var(--mega-win-scale));
  transform: scale(var(--mega-win-scale));
  transform-origin: 0 0;
  overflow: hidden;
  font-family: var(--font-system);
  color: var(--np-text);
}

.mega-card__screen span,
.mega-card__screen img {
  box-sizing: border-box;
}

/* -------------------------------------------------------------------------
   Bid — the map view.

   A real Mapbox render of the project coordinate src/styles/hero-mockup.css
   already paints, cropped to this sheet and re-encoded small: the header is on
   every page, and 35KB of banner per theme is not a thing to load for a menu
   nobody has opened. The app picks its map style off the resolved theme
   (`getMapStyle`), so this does too, and only one of the two is ever painted.
   ------------------------------------------------------------------------- */

.np-map {
  position: absolute;
  inset: 0;
  background: var(--np-sink); /* the app's own fill under a loading map */
}

.np-map__tiles {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.np-map__tiles[data-theme-dark] {
  display: none;
}

:root[data-theme='dark'] .np-map__tiles[data-theme-light] {
  display: none;
}

:root[data-theme='dark'] .np-map__tiles[data-theme-dark] {
  display: block;
}

/* Every marker is anchored on its own centre, which is `anchor="center"`. */
.np-pin {
  position: absolute;
  display: block;
  width: 32px; /* size-8 */
  height: 32px;
  margin: -16px 0 0 -16px;
}

/* A **pinned** document: a plate with a 3px white ring, holding its avatar. */
.np-pin--plate {
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border: 3px solid #fff;
  border-radius: 6px; /* rounded-md */
  background: #fff;
  box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}

.np-pin--plate img {
  width: 26px;
  height: 26px;
  object-fit: cover;
}

/* An ordinary point, and a cluster: the same teardrop. `rounded-2xl` with one
   square-ish corner, rotated 45deg so that corner points down. */
.np-pin__kite {
  position: absolute;
  inset: 0;
  border-radius: 16px 16px 5px 16px; /* rounded-2xl rounded-br-[5px] */
  transform: rotate(45deg);
  background: #fff;
  box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}

.np-pin--cluster .np-pin__kite {
  background: var(--np-accent);
}

/* The app counter-rotates the disc inside the rotated square; drawn here as a
   sibling on the same centre, which lands in the same place with one transform
   instead of two. */
.np-pin__disc {
  position: absolute;
  top: 1px;
  left: 1px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px; /* size-7.5 */
  height: 30px;
  overflow: hidden;
  border: 2px solid #fff;
  border-radius: 999px;
  background: #fff;
}

.np-pin__disc img {
  width: 26px;
  height: 26px;
  object-fit: cover;
}

.np-pin__count {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px; /* text-sm */
  line-height: 20px;
  font-weight: 700;
  color: #fff; /* text-primary-foreground */
}

/* -------------------------------------------------------------------------
   Build — the timeline view.

   The axis header over a canvas of bars. At the 1px/day zoom the fragment
   picks, `getTimelineGridStepPx` returns null and `weekendBackgroundStyle` is
   gated on day ticks, so the canvas carries **no grid rules and no weekend
   bands** — bars and the today line and nothing else. That is the app.
   ------------------------------------------------------------------------- */

.np-tl {
  position: absolute;
  inset: 0;
  background: var(--np-paper);
}

.np-tl__axis {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 32px; /* TICK_HEIGHT */
  border-bottom: 1px solid var(--np-line);
}

.np-tl__tick {
  position: absolute;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px; /* text-xs */
  line-height: 16px;
  color: var(--np-text-sec);
}

/* `size-[22px] rounded-full bg-red text-[11px] font-medium text-white`, on the
   line rather than beside it, which is what `-translate-x-1/2` gives. */
.np-tl__today {
  position: absolute;
  top: 50%;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border-radius: 999px;
  background: var(--np-red);
  font-size: 11px;
  line-height: 14px;
  font-weight: 500;
  color: #fff;
}

.np-tl__canvas {
  position: absolute;
  top: 32px;
  left: 0;
  right: 0;
  bottom: 0;
}

.np-tl__line {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--np-red);
}

/* `h-full w-full rounded-[8px] border border-border/60 bg-popover shadow-sm`,
   `flex items-center gap-1.5 px-2`. */
.np-tl__bar {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 6px; /* gap-1.5 */
  height: 34px; /* BAR_HEIGHT */
  padding: 0 8px; /* px-2 */
  overflow: hidden;
  border: 1px solid var(--np-line);
  border-radius: 8px;
  background: var(--np-raise);
  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}

.np-tl__av {
  flex: none;
  width: 16px; /* size-4 */
  height: 16px;
  object-fit: cover;
}

/* `min-w-0 shrink truncate text-sm font-medium` — every bar here is wider than
   `COMPACT_BAR_PX`, so all three carry their own name and all three truncate,
   which is the app rather than a fault. */
.np-tl__name {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 14px; /* text-sm */
  line-height: 20px;
  font-weight: 500;
  color: var(--np-text);
}

/* -------------------------------------------------------------------------
   Automate — the flow canvas.

   `Background variant={Dots} gap={10} size={1}` in `text-foreground/15`, whose
   own comment calls a 10px pitch "texture rather than an engineering grid" —
   and the node card out of `flow-node-card.tsx`, which documents itself as
   "deliberately shallow: one 14px title, one muted secondary line, and nothing
   else".
   ------------------------------------------------------------------------- */

.np-fl {
  position: absolute;
  inset: 0;
  background-color: var(--np-sink);
  background-image: radial-gradient(var(--np-dot) 0.6px, transparent 0.7px);
  background-size: 10px 10px;
  background-position: 5px 5px;
}

/* The connector: one path with two rounded turns, plus the triangle
   `arrow_end_kind: "triangle-filled"` gives an edge by default. It runs *under*
   the cards, which is what the port ring's own fill is for, so it is the first
   child of the canvas and takes no z-index.

   It was three absolutely positioned 1px boxes until 2026-08-31 and the corners
   were the reason it stopped being: boxes can only meet at a hard 90 degrees,
   and a hard corner on a 1px rule is the one place in these three screens where
   a drawing reads as a diagram rather than as software. The radius lives in the
   fragment (`FL_R`), since it is a path command and not a style.

   The stroke has to be `non-scaling-stroke` for the same reason every lucide
   glyph in here does: the whole screen is scaled by `--mega-win-scale`, and a
   1px rule scaled to 0.5 is a grey smear rather than a line. */
.np-fl__edge {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  fill: var(--np-wire);
  stroke: none;
}

.np-fl__edge path {
  fill: none;
  stroke: var(--np-wire);
  stroke-width: 1;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

.np-fl__node {
  position: absolute;
  width: 200px; /* FLOW_NODE_WIDTH */
  height: 70px; /* FLOW_NODE_HEIGHT */
}

.np-fl__shell {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding-bottom: 6px; /* pb-1.5 */
  border-radius: 14px;
  background: var(--np-raise);
  box-shadow:
    inset 0 0 0 1px var(--np-line),
    0 0 2px rgb(0 0 0 / 0.04),
    0 1px 3px rgb(0 0 0 / 0.08);
}

.np-fl__head {
  display: flex;
  flex: none;
  align-items: center;
  gap: 6px; /* gap-1.5 */
  height: 34px;
  padding: 10px 10px 0; /* px-2.5 pt-2.5 */
}

.np-fl__tile {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 24px; /* size-6 */
  height: 24px;
  border: 1px solid transparent;
  border-radius: 7px;
}

.np-fl__tile svg {
  display: block;
  width: 16px; /* size-4 */
  height: 16px;
}

/* `CATEGORY_TILE`, converted from the app's oklch the way src/styles/items-mockup.css
   converts its badge hues: Triggers yellow, Data blue. */
.np-fl__tile[data-tone='trigger'] {
  background: var(--np-trigger-bg);
  border-color: color-mix(in srgb, var(--np-trigger-fg) 20%, transparent);
  color: var(--np-trigger-fg);
}

.np-fl__tile[data-tone='data'] {
  background: var(--np-data-bg);
  border-color: color-mix(in srgb, var(--np-data-fg) 20%, transparent);
  color: var(--np-data-fg);
}

.np-fl__name {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 14px; /* text-sm */
  line-height: 20px;
  font-weight: 500;
}

.np-fl__sub {
  margin: 0 10px; /* mx-2.5 */
  padding: 4px 0; /* py-1 */
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 12px; /* text-xs */
  line-height: 16px;
  font-weight: 500;
  color: var(--np-text-sec);
}

/* The trigger tab, painted before the shell so the card's own background clips
   its lower half — which is what makes it read as tucked in behind. */
.np-fl__tab {
  position: absolute;
  top: -24px; /* -top-6 */
  left: 0;
  display: flex;
  align-items: flex-start;
  height: 40px; /* h-10 */
  padding: 4px 8px 0; /* px-2 pt-1 */
  border-radius: 10px 10px 0 0;
  background: color-mix(in srgb, var(--np-accent) 12%, transparent);
  font-size: 12px; /* text-xs */
  line-height: 16px;
  font-weight: 500;
  color: var(--np-accent);
}

/* `PortRing`: a 12px ring centred on the card edge, filled with the card's own
   surface so the wire runs under it. Only **exits** are ringed — `InputPort`
   passes `hidden` unless it is labelled, and a node with one input never is. */
.np-fl__port {
  position: absolute;
  top: 50%;
  right: -6px;
  width: 12px; /* size-3 */
  height: 12px;
  margin-top: -6px;
  border: 1px solid var(--np-accent);
  border-radius: 999px;
  background: var(--np-raise);
}

/* -------------------------------------------------------------------------
   Create — a custom app.

   The fourth card's screen, and the only one of the four that draws a surface
   a *reader* built rather than one the product ships: custom apps are described
   in plain language from the chat bar and previewed in the same workspace (see
   the four changelog entries the fragment header names). What is drawn is the
   finished app, not the builder — the site already draws that chat three times
   over, and a fourth would say "chat" where this card has to say "app".

   Every length is the app's own, as in the three screens above. Two are worth
   naming because Tailwind's unanchored defaults give something else: the app
   sets `--radius: 0.625rem`, so `rounded-md` is **8** and not 6, and its
   `size-sm` button is `h-8 px-3 text-sm`.
   ------------------------------------------------------------------------- */

.np-fm {
  position: absolute;
  inset: 0;
  background: var(--np-paper);
}

/* The document header: the app's own name, and the one action that closes a
   round of edits — "App edits stay in Working changes until you save them". */
.np-fm__head {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  padding: 0 16px;
  border-bottom: 1px solid var(--np-line);
}

.np-fm__title {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 15px; /* text-[15px], the app's document title at this density */
  line-height: 20px;
  font-weight: 500;
  color: var(--np-text);
}

/* There is no Save button here. It shipped with the app's `size-sm` primary in
   the header and came out the same day at the owner's direction: it was the
   only saturated colour in the four screens, which is the same reason the map's
   exact-overlap cluster was cut on 2026-08-31. See the note in
   tools/fragments/nav-panel.mjs. */

/* `text-xs font-medium text-muted-foreground` over its control. */
.np-fm__label {
  position: absolute;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 12px; /* text-xs */
  line-height: 16px;
  font-weight: 500;
  color: var(--np-text-sec);
}

/* The control: `h-9 w-full rounded-md border bg-background px-3 text-sm`. */
.np-fm__input {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 8px; /* gap-2 */
  padding: 0 12px; /* px-3 */
  overflow: hidden;
  border: 1px solid var(--np-line);
  border-radius: 8px;
  background: var(--np-raise);
}

.np-fm__av {
  flex: none;
  width: 16px; /* size-4 */
  height: 16px;
  object-fit: cover;
}

.np-fm__val {
  min-width: 0;
  flex: 1 1 auto;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 14px; /* text-sm */
  line-height: 20px;
  color: var(--np-text);
}

/* lucide `chevron-down` on a select. `vector-effect` for the reason every glyph
   in these screens carries it: the whole screen is scaled by
   `--mega-win-scale`, and a 1px rule at 0.5 is a smear. */
.np-fm__caret {
  flex: none;
  width: 16px; /* size-4 */
  height: 16px;
  color: var(--np-text-sec);
}

.np-fm__caret * {
  vector-effect: non-scaling-stroke;
}

/* ------------------------------------------------------------------ *
 * The phone's glyph
 *
 * tools/fragments/nav-panel.mjs ships a 16px lucide glyph beside every drawing,
 * for the sheet src/lib/mobile-menu.js builds out of these same cards: a card
 * becomes a full-width row there, where the 120x90 drawing would either take
 * the row over or shrink to mush. Nothing shows it in the Product panel;
 * src/styles/mobile-menu.css turns it on and hides the drawing, and the
 * Resources panel below is drawn out of the glyph alone.
 * ------------------------------------------------------------------ */

.mega-card__glyph {
  display: none;
}

/* ------------------------------------------------------------------ *
 * The Resources panel
 *
 * Three columns: Company, Resources, and a tinted "Explore" rail of the three
 * newest articles. The shape is the one two references agree on — Hyperstart's
 * Features menu and Warpspeed's Resources menu, both of which put two columns
 * of destinations beside one column of content — and the reason it is worth a
 * second panel shape is that the third column is the only part of this header
 * that changes on its own, every time `pnpm run blog:sync` runs.
 *
 * The rows are still `.mega-card`s, on purpose: src/lib/mobile-menu.js clones
 * that class into the phone sheet and src/styles/mobile-menu.css already draws
 * it as exactly this row. What is restated here is the desktop half, which used
 * to draw a block and now draws the same row the phone does — with one real
 * difference, that a phone row sits on its own plate where these sit on the
 * panel and take a fill only on hover. Seven flat plates in two columns read as
 * a table of contents rather than as a menu.
 *
 * Everything is scoped under `.mega-panel--resources`, so the Product panel's
 * three picture-cards are untouched by all of it.
 * ------------------------------------------------------------------ */

.mega-panel__cols {
  /* The grid moved up onto `.mega-panel--resources` so the foot could join it —
     see the long note there. This wrapper stays in the markup because
     `addResourcesFoot` anchors on it (articles close, rail closes, *grid*
     closes, then the foot) and because `buildResourcesPanel` is what emits it;
     `display: contents` hands its three children straight to the panel's grid
     and draws nothing itself. */
  display: contents;
}

/* The Explore column spans the foot's row as well as its own, which is what
   makes its card run to the bottom of the panel. Everything else about it is
   unchanged. */
.mega-panel--resources .mega-panel__explore {
  grid-column: 3;
  grid-row: 1 / 3;
  min-width: 0;
  /* The second half of the outside gutter — see the `column-gap` note on
     `.mega-panel--resources`. The grid's own gap is `g1`, which is what the two
     link columns want between them; this adds `g3` in front of the card so the
     card sits 40px clear of them instead. It is a margin rather than a wider
     track because a grid has one column gap and this panel needs two.

     It comes out of the card's width, the panel being a fixed 720: 690 of
     content less 160 + 10 + 160 + 40 leaves the card ~320. Widen the gutter and
     the card narrows by the same amount; there is no third thing to absorb
     it. */
  margin-left: var(--spacing-g3);
}

/* ...and the foot stops at the second column, so its hairline is the two link
   columns' footer and not the card's. */
.mega-panel--resources .mega-panel__foot {
  grid-column: 1 / 3;
  grid-row: 2;
}

/* All three columns are the same box, and that box has **no padding** — its
   content starts at the panel's own padding edge, which is exactly where the
   Product panel's "Features" label starts. That is the whole point: the two
   panels share a trigger row, so a reader moving between them should see the
   first label stay put rather than jump. It measured (30, 27) against Product's
   (15, 17) while the columns carried `g1` of their own; it is (15, 17) now.

   The rail is still the same box with a fill — it takes that padding back and
   negates it with an equal margin, so its *content* sits on the same two lines
   as the other columns' and only the tint bleeds outward. `border-radius` goes
   with the fill, so it lives on the rail alone. */
.mega-panel__col,
.mega-panel__explore {
  padding: 0;
}

/* Byte for byte the Product panel's own head (`.mega-panel__head > span`, the
   "Features" label) — same size, same tracking, same weight, same colour, same
   space under it. It was uppercase at 0.06em until 2026-08-31, which made the
   two panels' labels read as two different systems. Sentence case, from the
   markup, is what the Product panel already sets. */
.mega-panel__col-head {
  /* `--spacing-g1.25`, which is the Product panel head's own bottom margin, so
     the first row's box lands where the first Product card's does (48.5px from
     the panel top) exactly as the label lands where Product's label does. The
     two panels share a trigger row, so what a reader sees moving between them
     is one sheet whose contents change, not two sheets of different
     proportions.

     This is the one place the gap ladder is overridden and it is deliberate.
     Step 1 wants a label tight to the list it names (~9px, which is what a
     `margin: 0` and the row's own 6px of top padding give) so a column reads as
     a group; matching Product costs that, taking the label-to-list gap past the
     18px between rows. Product gets away with the same number because its one
     label names the whole panel rather than one of three columns. The columns
     here still group — 50px apart, three rows each — but if this panel is ever
     reported as loose again, this margin is the first thing to look at. */
  margin: 0 0 var(--spacing-g1\.25);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  letter-spacing: var(--tracking-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-theme-text-tertiary);
}

/* ---- A link row --------------------------------------------------- */

.mega-panel--resources .mega-card {
  display: grid;
  /* Areas rather than order: the glyph is first in the markup and has to span
     both lines so the plate stays centred however the description wraps. */
  grid-template-columns: auto 1fr;
  grid-template-areas: 'glyph body';
  align-items: center;
  /* 11px. At the `g0.75` this was, the plate sat 7px off its title — closer
     than the two lines of the title block are to each other, so the icon read
     as stuck to the word rather than beside it. The column above has ~3px of
     slack left after this. */
  column-gap: 0.75rem;
  /* Taller than it is wide-padded, so a one-line description still gives the
     row a hover fill worth seeing and the three of them come nearer the
     Explore rail's height. */
  min-height: var(--mega-row-h);
  padding: 0.375rem var(--spacing-g0\.5);
  /* The fill wants padding and the plate wants to line up with the column
     label above it, which is at the column's own edge. Negating the horizontal
     padding gives both: the row's box bleeds `g0.5` either side into the
     panel's own padding, and its contents land flush with the label. */
  margin-inline: calc(-1 * var(--spacing-g0\.5));
  border-radius: var(--radius-xs);
  /* No plate at rest — see the note above. The token still resolves for the
     drawing's knockouts, which this panel has none of. */
  --mega-card-paper: transparent;
  background-color: transparent;
}

/* `.card`'s hairline belongs to a card, and these are rows. */
.mega-panel--resources .mega-card::before {
  content: none;
}

.mega-panel--resources .mega-card:hover {
  --mega-card-paper: var(--color-theme-card-hex);
  background-color: var(--color-theme-card-hex);
}

.mega-panel--resources .mega-card__glyph {
  grid-area: glyph;
  display: flex;
  align-items: center;
  /* Both axes. Dropping this in the 32 -> 40 pass left the glyph flush to the
     left edge of its plate — `justify-content` defaults to `flex-start`, and a
     20px mark in a 40px box shows that as 20px of empty plate on one side. */
  justify-content: center;
  /* 32 -> 40 -> 32 over 2026-08-31, and the round trip is the description's.
     40 was the largest plate a 54px row would take, and it was worth taking
     while the row was two lines of text: an icon has to hold its own beside a
     title *and* a sentence, and a 16-in-32 plate next to that block read as a
     bullet. With the sentence gone the row is one 21px line, and a 40px plate
     beside it is the largest thing in the row by a distance — the eye lands on
     six identical grey squares before it reads a word. 32 with a 16px glyph
     sits just above the line's own height, which is what a row of destinations
     wants.

     **The row's height deliberately did not follow the sentence out.**
     `--mega-row-h` is still 54, so a plate has 11px of air above and below it
     rather than 7. Shrinking the row instead would end the two link columns
     ~36px above the Explore rail, which is a card with two-line titles and its
     own padding and cannot follow them down; the three columns ending together
     is what this panel was rebuilt for. */
  width: 2rem;
  height: 2rem;
  flex: none;
  border-radius: var(--radius-xs);
  /* The plate's fill AND the glyph's ink are both mixed off this, so a change
     to one is a change to both. */
  --mega-glyph-ground: var(--color-theme-card-hex);
  /* The plate is the step the row does not take: it is card-coloured at rest,
     which is what makes it read as an object sitting on the panel, and it holds
     that colour under hover so the row lights up around it rather than with
     it. */
  background-color: var(--mega-glyph-ground);
  box-shadow: inset 0 0 0 1px var(--color-theme-border-01);
  /* FLATTENED, and deliberately not `--color-theme-text-sec`. That token is the
     foreground at 60 % ALPHA, and a lucide glyph is several separate `<path>`
     elements sharing one `stroke` — so wherever two of them cross (the users
     mark's two heads, the scales' beam and post, the briefcase's handle and
     lid, the life-buoy's spokes) the two translucent strokes composite and the
     intersection paints a step darker than the lines running into it. It read
     as an icon drawn in two colours. `color-mix(in srgb, …)` against the
     plate's own ground is exactly what that alpha would have composited to, so
     nothing changes colour — only the crossings stop doubling. Same call
     `bid-surfaces.css` makes, and the same one the app makes by carrying
     `text-secondary-opaque` beside `text-secondary`.

     It has to be mixed against `--mega-glyph-ground` rather than a stated
     colour because the plate holds that fill through hover and through
     `aria-current`, so the two can never drift. */
  color: color-mix(in srgb, var(--color-theme-fg) 60%, var(--mega-glyph-ground));
  transition: color var(--duration) var(--ease-out-spring);
}

.mega-panel--resources .mega-card:hover .mega-card__glyph {
  color: var(--color-theme-text);
}

.mega-panel--resources .mega-card__glyph svg {
  display: block;
  /* Half the plate, which is the ratio every size of it has had. */
  width: 1rem;
  height: 1rem;
}

/* A step down from the 16px title, which is `--text-base` and shared with
   Product's cards. Both references set the row's sentence smaller than its
   title, and at 16px in a 215px column these wrapped to three lines each — six
   rows of that is the wall of grey the panel was reported as. 14px is also what
   the phone sheet and the Explore rail already set. */
/* A title and its description are one object, so they sit tighter than the
   panel's default 1.5 leading puts them — which is also what fits the pair
   inside `--mega-row-h` without the row growing. */
.mega-panel--resources .mega-card__title {
  line-height: 1.3;
}

.mega-panel--resources .mega-card__desc {
  font-size: var(--text-sm);
  line-height: 1.4;
  letter-spacing: var(--tracking-sm);
}

.mega-card__body {
  grid-area: body;
  display: flex;
  flex-direction: column;
  gap: 0.0625rem;
  min-width: 0;
}

/* ---- The Explore rail --------------------------------------------- */

/* The rail carries nothing of its own any more. Its label sits on the panel
   like the other two columns' labels and the fill moved down onto
   `.mega-panel__articles`, which is what makes all three columns one shape: a
   label, then a block of content `--mega-col-h` tall starting on the same line.
   The third column's block is a card and the other two's is not, which is the
   whole difference between them.

   It carried `g0.75` of padding negated on three sides until 2026-08-31, to
   drag a tint that wrapped its own label back into line with the columns beside
   it. Taking the label out of the fill deletes the problem instead of
   compensating for it. */
.mega-panel__explore {
  display: flex;
  flex-direction: column;
}

/* The same `g0.25` and the same `--mega-col-h` the link columns take — see
   `.mega-panel__cards--list` — plus the fill, the corner and the hairline that
   make this block a card. All four values are `.mega-card`'s own, so the third
   column's block and a Product card are the same object drawn at two widths:
   `--color-theme-card-hex`, `--radius-xs`, and a 1px `--color-theme-border-01`
   ring painted by a `::before` at `inset: 0` rather than by a border, so the
   ring costs the content no width. Its padding is `g1` and not `.mega-card`'s
   `g2` — a card holding three rows of its own is not a card holding one title,
   one sentence and a picture. */
.mega-panel__articles {
  position: relative;
  display: flex;
  flex-direction: column;
  /* The column spans the foot's row as well as its own, so the card takes
     whatever height that comes to and `--mega-col-h` is only a floor — see
     `.mega-panel--resources`. The three articles keep their own row height and
     stack from the top; the slack goes to the closer, which is pinned to the
     card's bottom by its own `margin-top: auto`. */
  flex: 1;
  min-height: var(--mega-col-h);
  gap: var(--spacing-g0\.25);
  padding: var(--spacing-g1);
  border-radius: var(--radius-xs);
  background-color: var(--color-theme-card-hex);
}

/* The Explore label moved inside the card on 2026-09-01 — see `explore()` in
   tools/fragments/nav-panel.mjs. It is the same `.mega-panel__col-head` the two
   link columns set on the panel, so it keeps their size, weight, tracking and
   colour; only its box changes.

   Two adjustments. **The label's own space under it is gone**, and the card's
   top padding is why: the label sits `g1` (10px) lower than the two link
   columns' labels, which sit on the panel's own padding edge, so everything
   under it started 10px low and the first article landed a row-inset below the
   first link row. Measured at 1440 it was exactly 10px. So the gap this rule
   sets is Product's `g1.25` less the card's flex `g0.25` less that `g1`, which
   comes to zero — the `g0.25` gap is the whole of what separates the label from
   the first thumbnail now, and the first article's box top matches the first
   link row's to the pixel. It is solved rather than typed, so moving the card's
   padding or the panel's gap ladder re-solves it; if it ever comes out
   negative, the card's padding has outgrown the label gap and the padding is
   what to look at. And it takes no horizontal inset of its own: its left edge
   is the card's content edge, which is where the thumbnails and the closer
   start, so the three things in this card share one left rail. */
.mega-panel__articles > .mega-panel__col-head {
  margin-bottom: calc(
    var(--spacing-g1\.25) - var(--spacing-g0\.25) - var(--spacing-g1)
  );
}

.mega-panel__articles::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border: 1px solid var(--color-theme-border-01);
  border-radius: inherit;
}


.mega-article {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  column-gap: var(--spacing-g0\.75);
  /* The rail's three rows divide the card between them — see
     `--mega-art-row-h`. The thumbnail is smaller than that, so the row is this
     height exactly and the slack is the 6px a side below. */
  min-height: var(--mega-art-row-h);
  padding: 0.375rem var(--spacing-g0\.5);
  /* The fill wants padding and the plate wants to line up with the column
     label above it, which is at the column's own edge. Negating the horizontal
     padding gives both: the row's box bleeds `g0.5` either side into the
     panel's own padding, and its contents land flush with the label. */
  margin-inline: calc(-1 * var(--spacing-g0\.5));
  border-radius: var(--radius-xs);
  text-decoration: none;
  color: var(--color-theme-text);
  transition: background-color var(--duration) var(--ease-out-spring);
}

.mega-article:hover {
  background-color: var(--color-theme-card-hover-hex);
}

/* 40:21, which is `aspect-[40/21]` — the ratio the blog's own article cards are
   drawn at (tools/fragments/blog.mjs) and the ratio the files themselves are:
   every thumbnail in the snapshot is 1200x630 or 1920x1080. It was 4:3 until
   2026-08-31, which is a 1.9:1 picture cropped to 1.33 — a third of every image
   cut off the sides, which on the two thumbnails carrying a face cut the face.
   Fixed rather than fluid: three thumbnails in a column only read as a set if
   their left edges and their widths agree. */
.mega-article__thumb {
  display: block;
  /* The **height** is what is stated and the width follows from the ratio: the
     thumbnail's job is to be the picture in its row, and the row is sized by
     it. It reads `--mega-art-thumb-h` rather than a number of its own so the
     row height above cannot drift from the picture inside it — see that token
     for the 36 -> 48 move and what limits it. */
  height: var(--mega-art-thumb-h);
  width: auto;
  aspect-ratio: 40 / 21;
  flex: none;
  overflow: hidden;
  border-radius: var(--radius-2xs);
  background-color: var(--color-theme-bg);
  box-shadow: inset 0 0 0 1px var(--color-theme-border-01);
}

.mega-article__thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.mega-article__title {
  font-size: var(--text-sm);
  /* 1.3 rather than `--leading-normal`, and it is solved rather than styled:
     two lines have to fit `--mega-art-row-h` less the row's own 6px a side —
     48px at the current 3rem thumbnail — and a two-line title has to leave a
     one-line title's row the same height, or the rail's three rows stop
     agreeing and the column ends ragged. At 14px, 1.3 gives 36.4 and 1.5 gives
     42; both fit today, and 1.3 is what keeps a title reading as one object
     beside its picture. Re-check it if `--mega-art-thumb-h` or `--text-sm`
     moves; the pair spent part of 2026-08-31 at 13px/1.1, which is what a 148px
     column forced and is too tight to want. */
  line-height: 1.3;
  letter-spacing: var(--tracking-sm);
  color: var(--color-theme-text);
  /* Two lines is what every one of these titles takes at 4.75rem of thumbnail
     out of a 280px rail, and it is also the row's picture height — a third line
     would grow the row past its own thumbnail and leave the column ragged. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  text-wrap: pretty;
}

/* The rule that lit the Resources foot's right-hand closer a step down is gone
   with the closer itself (2026-09-01): that foot is one span now — "Updates
   every week. See what's new" — and `:last-child` would have pointed the
   secondary colour straight at it, muting the one link the foot still has.
   `.mega-panel__foot a` styles it, exactly as it styles Product's. */

/* The rail's closer, back inside the card as of 2026-09-01 — see
   `EXPLORE_CLOSER` in tools/fragments/nav-panel.mjs for why it moved and why
   it can now live here at all. `margin-top: auto` is the whole trick: the card
   is as tall as the panel and the three articles are not, so the closer takes
   the slack and lands on the card's bottom padding, on the same line the foot's
   text sits on beside it.

   `align-self: flex-start` keeps its hit area to the words. Its left edge is
   the card's content edge, which is where the article thumbnails start — the
   articles bleed `g0.5` outward and take it straight back as padding, so no
   negative margin is needed here to line the two up.

   Secondary ink, matching what the foot's own closer took: at full ink a lone
   phrase under three thumbnails pulls the eye to the corner of the panel. */
.mega-panel--resources .mega-panel__more {
  align-self: flex-start;
  margin-top: auto;
  /* Half a row gap of air above it, so it reads as the card's closer rather
     than as a fourth article. */
  padding-top: var(--spacing-g0\.5);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  letter-spacing: var(--tracking-sm);
  color: var(--color-theme-text-sec);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--duration) var(--ease-out-spring);
}

.mega-panel--resources .mega-panel__more:hover {
  color: var(--color-theme-text);
}

/* ==================================================================== *
 * "You are here"
 * ====================================================================
 *
 * Both panels are lists of destinations and neither said which one the reader
 * was already looking at — opening Product on /manage drew four identical
 * cards. `tools/fragments/nav-active.mjs` puts `aria-current="page"` on the
 * entry whose href is the page being rendered (an Astro expression, since
 * Header.astro is one component shared by every page); this is the paint.
 *
 * ONE MECHANISM, THREE SHAPES, and it is deliberately monochrome — the note at
 * the top of this file about colour being thin here holds. An entry that is the
 * current page takes **the fill it would have under the pointer, held**, plus
 * **a ring the pointer never draws**. The fill alone would be indistinguishable
 * from hover; the ring alone reads as a focus outline. Together they say
 * "already there".
 *
 * The ring is `--color-theme-border-02-5` (the foreground at 20%), which is
 * eight times the 2.5% hairline a card draws at rest — enough to read as a
 * drawn edge at a glance rather than as a slightly darker card, which is what
 * 10% gave. It is the one value to tune if the mark is ever reported as too
 * loud or too quiet; tune it here and all three shapes follow.
 *
 * Every one of these rules is deliberately LAST in the file, after the hover
 * rules it has to outrank: `.mega-card[aria-current]` and `.mega-card:hover`
 * score the same (0,2,0), so source order is what decides that hovering the
 * current entry leaves it looking current.
 *
 * The phone sheet takes its own copy — src/lib/mobile-menu.js clones these
 * nodes and `cloneNode` brings the attribute with them, but
 * `.mobile-menu .mega-card` paints a fill of its own at a specificity this
 * cannot reach. See the matching block in src/styles/mobile-menu.css.
 */

:root {
  --mega-current-ring: var(--color-theme-border-02-5);
}

/* A Product card already carries a fill and a hairline, so both simply move a
   step: `.mega-card::before` is the hairline (a pseudo-element rather than a
   border, so the 4px radius stays true against the fill), and it is the ring. */
.mega-card[aria-current='page'] {
  --mega-card-paper: var(--color-theme-card-hover-hex);
}

.mega-card[aria-current='page']::before {
  border-color: var(--mega-current-ring);
}

/* A Resources row is transparent at rest and its `::before` is `content: none`
   — a hairline belongs to a card and these are rows — so the fill is the
   row's own hover fill and the ring has to be an inset shadow instead. It
   costs no width, which matters here: the row's box already bleeds `g0.5`
   either side into the panel's padding so its contents land flush with the
   column label above, and a real border would push them 1px off it. */
.mega-panel--resources .mega-card[aria-current='page'] {
  --mega-card-paper: var(--color-theme-card-hex);
  background-color: var(--color-theme-card-hex);
  box-shadow: inset 0 0 0 1px var(--mega-current-ring);
}

/* The plate holds its own colour under hover so the row lights up around it
   rather than with it (see `.mega-card__glyph`); the *glyph* is what moves, and
   the current row keeps it at full ink the way a hovered one does. */
.mega-panel--resources .mega-card[aria-current='page'] .mega-card__glyph {
  color: var(--color-theme-text);
}

/* An Explore article is the row shape again with a picture instead of a plate,
   and it hovers to `card-hover-hex` rather than `card-hex` — so its held fill
   is that one. A reader really can be on one of these: they are this site's own
   /research/<slug> pages, and the rail carries the three newest. */
.mega-article[aria-current='page'] {
  background-color: var(--color-theme-card-hover-hex);
  box-shadow: inset 0 0 0 1px var(--mega-current-ring);
}
/* Mobile navigation menu.
 *
 * Hand-maintained, like src/styles/nav-menu.css and src/styles/header-pinned.css:
 * tools/04-generate.mjs imports it into every page but never rewrites it. Pairs
 * with src/lib/mobile-menu.js, which builds the trigger and the sheet at load
 * out of the header's own captured nav.
 *
 * The breakpoint is 900px because that is what the capture's `lg:` compiles to
 * (`@media (min-width:900px){.lg\:block{...}}`) — the header hides its whole
 * `<nav>` below it. Tailwind's default 1024 would leave a 124px band with both
 * navs showing.
 *
 * Colour is deliberately thin here. The rows keep the `.mega-card` rules from
 * src/styles/nav-panel.css, so retuning that one file carries to both. What is
 * restated is the *shape*: a desktop card is a block in a three-column grid —
 * drawing on top, then title, then copy — and on a phone that is three
 * full-width bands of nothing. Here it is a row, the 120x90 drawing is hidden
 * and the lucide glyph the fragment ships beside it takes over on its own
 * plate, which is how Bidlo Lite draws a row that is a whole destination.
 */

/* ------------------------------------------------------------------ *
 * Trigger
 * ------------------------------------------------------------------ */

/* Measured off cursor.com's own `button[aria-label="Open navigation"]` at
   390/500/768/899 — every value below is theirs, and constant across all four.
   Their box is `w-v1.5 h-v1.5`, which is a token this capture also carries and
   resolves identically (31.5px, off the same 15px root), so it is written as
   the token rather than as the number it happens to equal today. */
.mobile-menu-trigger {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--spacing-v1\.5);
  height: var(--spacing-v1\.5);
  /* Their `ml-g1` on top of the cluster's own gap, and their `-mr-[0.3rem]`
     optical pull — the icon's viewBox insets the bars ~4px from the box edge,
     so the box has to hang out past the row for the ink to land on it. */
  margin-left: var(--spacing-g1);
  margin-right: -0.3rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-theme-fg);
  cursor: pointer;
}

/* Their `after:inset-[-0.5rem]`: the layout box stays exactly the icon and the
   touch area grows underneath it, rather than padding the button out and
   pushing the row around. */
.mobile-menu-trigger::after {
  position: absolute;
  inset: -0.5rem;
  content: '';
}

@media (min-width: 900px) {
  .mobile-menu-trigger {
    display: none;
  }
}

/* Both glyphs fill the button and are crossfaded, where cursor.com swaps one
   whole button for another (`Open navigation` / `Close navigation`, the close
   one lifted to z-51 over its overlay). One button is enough here — the header
   already outranks the sheet — and `display` cannot transition, so a stroke
   this thin popping in would read as a flicker.

   Both SVGs are `width=height=100%` on a 24 viewBox, so the box is the only
   size control: the menu path spans just 16.5 of those units and renders
   21.66px of visible bars inside 31.5px, which is why sizing the box to the
   ink came out a third too small. */
.mobile-menu-trigger__icons {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
}

.mobile-menu-trigger__icon {
  position: absolute;
  inset: 0;
  transition: opacity 0.15s ease;
}

.mobile-menu-trigger__icon[data-icon='close'],
.mobile-menu-trigger[aria-expanded='true'] .mobile-menu-trigger__icon[data-icon='menu'] {
  opacity: 0;
}

.mobile-menu-trigger[aria-expanded='true'] .mobile-menu-trigger__icon[data-icon='close'] {
  opacity: 1;
}

/* ------------------------------------------------------------------ *
 * Sheet
 * ------------------------------------------------------------------ */

/* Below the header's fixed `z-50`, so the trigger stays live as the close X.
   Top edge is the header's own height, which the body already pads by. */
.mobile-menu {
  position: fixed;
  top: var(--site-header-height);
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  background-color: var(--color-theme-bg);
  opacity: 0;
  transform: translateY(-0.5rem);
  /* Transparent but still laid out for the length of the fade-out, so it must
     not keep swallowing clicks meant for the page behind it. */
  pointer-events: none;
  transition:
    opacity 0.18s ease,
    transform 0.22s var(--ease-out-spring, ease);
}

/* Author `display` beats the UA sheet's `[hidden]{display:none}`, so the rule
   above would leave a transparent full-screen sheet swallowing every click on
   the page behind it. It has to be restated here. */
.mobile-menu[hidden] {
  display: none;
}

.mobile-menu[data-open] {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

/* Scroll is locked on the page, not the sheet, so a long Resources drawer still
   scrolls. `overscroll-behavior` stops that scroll chaining to the frozen body,
   which iOS otherwise treats as a rubber-band on the whole page. */
.mobile-menu__scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 1rem var(--spacing-g2, 1rem) 1.5rem;
}

.mobile-menu__cards {
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
}

/* Grid areas rather than order, because the source order is graphic-first. The
   plate spans both lines so it stays centred against the row however the copy
   wraps. */
.mobile-menu .mega-card {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas:
    'art title'
    'art desc';
  align-items: center;
  column-gap: 0.875rem;
  padding: 0.75rem;
  border-radius: var(--radius-md);
  background-color: var(--color-theme-card-hex);
}

/* ------------------------------------------------------------------ *
 * Product rows end on their window, AS OF 2026-09-11.
 *
 * The row is the same glyph-and-copy row every drawer draws; a Product card
 * adds a third column at the row's right end holding the desktop card's own
 * window crop, bleeding off the row's right and bottom edges the way it
 * bleeds off the desktop card's. Only rows carrying a window take the column
 * (`:has(.mega-card__win)`); Resources rows are untouched.
 *
 * Two stacked versions were tried first the same day and both came out at
 * the owner's direction — a two-column grid of the desktop cards ("too
 * busy") and a four-column one (an 81px card, "Automate" cut off). What is
 * kept from the desktop window is everything: `--mega-win-w`,
 * `--mega-win-scale` and the drawings are unmoved, so the row shows the
 * top-left corner of the same 260px sheet — the title bar and about 40px of
 * screen — which is a glimpse rather than a picture, and is meant to be.
 * `overflow: hidden` on the row is what clips the bleed at the rounded corner.
 * ------------------------------------------------------------------ */
.mobile-menu .mega-card:has(.mega-card__win) {
  /* Room for the window on the right; the window itself is taken out of the
     grid below so its 190px sheet cannot grow the row. */
  padding-right: 8.25rem;
  overflow: hidden;
}

.mobile-menu .mega-card__win {
  position: absolute;
  top: 0.5rem;
  right: 0;
  bottom: 0;
  display: block;
  box-sizing: content-box;
  width: 7.5rem;
  height: auto;
  margin: 0;
  /* The desktop's 5px of shadow room on the left is kept. */
  padding: 0 0 0 5px;
}

/* No title bar on the phone: at ~40px of visible screen the three lights were
   most of the picture. `--mega-win-bar` is what the screen's top and height
   are solved off in src/styles/nav-panel.css, so zeroing it here moves the
   screen up under the hidden bar rather than leaving a 16px band. */
.mobile-menu .mega-card__win-frame {
  --mega-win-bar: 0px;
}

.mobile-menu .mega-card__win-bar {
  display: none;
}

/* Product's cards put the title and the description straight into the card;
   the Resources panel's rows wrap them in a body element, because on desktop
   that pair has to sit beside a plate rather than under a drawing (see
   `.mega-panel--resources` in src/styles/nav-panel.css). Spanning it across
   both rows of the same column lands the two shapes in the same place. */
.mobile-menu .mega-card__body {
  grid-area: title / title / desc / desc;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* The Resources drawer is two labelled groups — src/lib/mobile-menu.js clones
   the panel's column heads along with its rows so the sheet reads Company,
   three rows, Resources, four rows. The label is a caption on the group below
   it, so it takes its air from the group above instead. */
/* The desktop head is inset by the row padding beside it; a phone row is a
   full-width plate, so there is nothing to line up with. */
.mobile-menu .mega-panel__col-head {
  margin: 0;
  padding-left: 0;
}

.mobile-menu .mega-panel__col-head:not(:first-child) {
  margin-top: 0.75rem;
}

/* The second link column's head is an empty spacer that only exists so the
   desktop panel's columns start their rows on the same line (see `colHead` in
   tools/fragments/nav-panel.mjs). Stacked into one drawer there are no columns
   to align, and a blank line between two rows reads as a label that failed to
   render. */
.mobile-menu .mega-panel__col-head--empty {
  display: none;
}

.mobile-menu .mega-card__glyph {
  grid-area: art;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--radius-sm);
  /* This plate sits on the page ground rather than the panel's card, so the
     ground it flattens against is that one — see the long note on the same
     rule in src/styles/nav-panel.css for why the ink is opaque at all. */
  --mega-glyph-ground: var(--color-theme-bg);
  background-color: var(--mega-glyph-ground);
  color: color-mix(in srgb, var(--color-theme-fg) 60%, var(--mega-glyph-ground));
}

/* The glyph keeps roughly its 16px ink inside the larger plate. */
.mobile-menu .mega-card__glyph svg {
  display: block;
  width: 1.125rem;
  height: 1.125rem;
}

.mobile-menu .mega-card__title {
  grid-area: title;
  font-size: var(--text-base);
}

.mobile-menu .mega-card__desc {
  grid-area: desc;
  font-size: var(--text-sm);
}

/* Tap, not hover: a phone fires :hover on touch and leaves it stuck on the last
   row pressed. The plate below is the pressed state instead. */
.mobile-menu .mega-card:hover {
  background-color: var(--color-theme-card-hex);
}

.mobile-menu .mega-card:active {
  background-color: var(--color-theme-card-hover-hex);
}

/* Restates `.mega-panel__head a`, which no longer matches now that the link
   sits outside a panel: the site's secondary-link treatment, muted and rising
   to full ink rather than underlining. */
.mobile-menu__more {
  display: inline-block;
  margin: 0.875rem 0 0 0.125rem;
  color: var(--color-theme-text-sec);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-sm);
  text-decoration: none;
  transition: color var(--duration) var(--ease-out-spring);
}

.mobile-menu__more:hover {
  color: var(--color-theme-text);
}

/* ------------------------------------------------------------------ *
 * Rows
 * ------------------------------------------------------------------ */

/* The whole sheet body — every menu is a row here, panel-backed or not — so it
   sits flush against the scroller's own padding. */
.mobile-menu__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.mobile-menu__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  padding: 0.75rem 0.125rem;
  border: 0;
  background: none;
  color: var(--color-theme-fg);
  font: inherit;
  font-size: 1.5rem;
  line-height: 1.25;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.mobile-menu__chevron {
  width: 0.9rem;
  height: 0.9rem;
  flex: none;
  opacity: 0.5;
  transition:
    transform 0.2s var(--ease-out-spring, ease),
    opacity 0.2s ease;
}

.mobile-menu__row[aria-expanded='true'] .mobile-menu__chevron {
  transform: rotate(180deg);
  opacity: 0.85;
}

/* `height: auto` is not interpolable, so the drawer animates a 0fr -> 1fr grid
   track and the inner element does the clipping. `min-height: 0` on the inner
   is what lets the track actually reach zero. */
.mobile-menu__drawer {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.26s var(--ease-out-spring, ease);
}

.mobile-menu__drawer[data-open] {
  grid-template-rows: 1fr;
}

.mobile-menu__drawer-inner {
  min-height: 0;
  overflow: hidden;
}

.mobile-menu__drawer .mobile-menu__cards {
  padding: 0.25rem 0 0.5rem;
}

.mobile-menu__drawer .mobile-menu__more {
  margin-bottom: 0.75rem;
}

/* ------------------------------------------------------------------ *
 * Footer actions
 * ------------------------------------------------------------------ */

.mobile-menu__foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding: 0.875rem var(--spacing-g2, 1rem) max(0.875rem, env(safe-area-inset-bottom));
  border-top: 1px solid var(--color-theme-border-02);
  background-color: var(--color-theme-bg);
}

.mobile-menu__cta.mobile-menu__cta {
  display: flex;
  width: 100%;
  padding-top: 0.75rem;
  padding-bottom: 0.75rem;
}

/* ------------------------------------------------------------------ *
 * Page state while open
 * ------------------------------------------------------------------ */

/* The header's own float -> pin state is left exactly as header-pinned.css set
   it: opening the menu must not pin an unpinned header, and closing it must not
   unpin a pinned one. The sheet's top edge is the header's height either way.
   The one thing suppressed is the divider — while the sheet is up the header is
   its lid, and a hairline plus a drop shadow across the seam reads as two
   surfaces stacked rather than one. `html[data-menu-open] #site-header::before`
   outscores header-pinned.css's `#site-header::before` on element count, so this
   holds wherever the two files land in the bundle.

   It also has to survive the fade-out, or the hairline snaps back under a sheet
   still on screen — which is why mobile-menu.js holds `data-menu-open` until the
   sheet has finished leaving rather than dropping it on the click. */
html[data-menu-open] #site-header::before {
  border-bottom-color: transparent;
  box-shadow: none;
}

html[data-menu-open],
html[data-menu-open] body {
  overflow: hidden;
}

/* Locking the scroll takes the scrollbar with it, and the fixed header is
   width-100% — without this its contents slide right by the gutter's width the
   moment the menu opens. Only a desktop-width browser under 900px has one;
   mobile overlay scrollbars leave --mobile-menu-scrollbar unset, so this is the
   captured `px-g2` unchanged. Restated rather than added to, since the class
   sets `padding-inline` and there is no way to add to one side of that. */
html[data-menu-open] #site-header,
.mobile-menu__scroll,
.mobile-menu__foot {
  padding-right: calc(var(--spacing-g2, 1rem) + var(--mobile-menu-scrollbar, 0px));
}

/* Above the capture's `md` (660px) the sheet spans a full tablet and a card
   stretched across it is mostly empty fill. Hold the column to roughly the
   width the phone layout was drawn at. The foot keeps its full width so its
   top border still reads as the sheet's own edge; only the buttons narrow —
   and via max-width rather than an auto margin, which on a flex item would
   shrink-wrap the foot and take the border with it. Last in the file because
   these all lose to the margins and widths set above on source order. */
@media (min-width: 660px) {
  .mobile-menu__list {
    max-width: 32rem;
    margin-inline: auto;
  }

  .mobile-menu__cta.mobile-menu__cta {
    max-width: 32rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .mobile-menu,
  .mobile-menu__drawer,
  .mobile-menu__chevron,
  .mobile-menu-trigger__bars span {
    transition-duration: 0.01ms;
  }
}

/* ---- "You are here" ------------------------------------------------- *
 *
 * src/lib/mobile-menu.js CLONES the desktop panels' cards, and `cloneNode`
 * brings `aria-current` with them, so the sheet inherits the mark for free —
 * see the block at the foot of src/styles/nav-panel.css for what it means and
 * why the ring is the value to tune.
 *
 * Only the FILL has to be restated. A phone row is a full-width plate painted
 * by `.mobile-menu .mega-card` at (0,2,0), which ties with the unscoped
 * `.mega-card[aria-current='page']` and wins on source order — this sheet is
 * imported after that one. The RING is not restated and must not be: these
 * rows are outside `.mega-panel--resources`, so unlike their desktop selves
 * they keep `.mega-card::before`, and that pseudo-element is already recoloured
 * to the ring by nav-panel.css. Adding a shadow here would draw it twice.
 *
 * Nothing is lost to `:active` sitting above it. That rule and this one score
 * the same (0,3,0), so this wins on order — and they ask for the same fill, a
 * press on the current row being the one place the two states coincide. */
.mobile-menu .mega-card[aria-current='page'] {
  background-color: var(--color-theme-card-hover-hex);
}
/* Header float -> pin.
 *
 * Hand-maintained, like src/styles/theme-dark.css and src/styles/nav-menu.css:
 * tools/04-generate.mjs imports it into every page but never rewrites it. Pairs
 * with src/lib/header-pinned.js.
 *
 * Ported from bidlo-client-v3's <Header>. At the top of the page the header has
 * no surface of its own and its contents ride a few pixels low, so the chrome
 * reads as floating over the hero. Past 16px of scroll a background layer fades
 * in behind it with a hairline bottom border and a soft shadow, and the contents
 * settle up into place — the page now has a lid.
 *
 * The background is a ::before rather than the header's own background so it
 * can cross-fade without taking the nav text with it. The captured markup gives
 * the inner container `relative z-2`, so it already sits above the layer.
 *
 * The settle targets `> .container`, not `> div`. src/lib/nav-menu.js mounts the
 * dropdown popup as a second direct child of the header, and a transform on it
 * would make it the containing block for the fixed positioner inside — the menu
 * would hang 6px low and animate on every pin.
 *
 * Note the capture paints the header the same colour as the body, which means
 * the unpinned state is only visibly different in the 6px of settle — the
 * transparent background is a no-op until something coloured scrolls under it.
 */

#site-header {
  /* Beats the captured `.bg-theme-bg` on specificity, not source order. */
  background-color: transparent;
}

#site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  box-sizing: border-box;
  background-color: var(--color-theme-bg);
  border-bottom: 1px solid var(--color-theme-border-02);
  box-shadow: 0 1px 2px 0 color-mix(in oklab, var(--color-theme-fg) 6%, transparent);
  opacity: 0;
}

#site-header > .container {
  transform: translateY(var(--site-header-float, 6px));
}

#site-header.is-pinned::before {
  opacity: 1;
}

#site-header.is-pinned > .container {
  transform: none;
}

/* Transitions are held back until after the first paint so a page that loads
   already scrolled — a refresh, a back-navigation, an in-page anchor — snaps
   straight to the pinned state instead of animating into it. The script adds
   this class on the frame after it sets the opening state. */
#site-header.is-ready::before {
  transition: opacity 0.2s ease-out;
}

#site-header.is-ready > .container {
  transition: transform 0.2s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  #site-header.is-ready::before,
  #site-header.is-ready > .container {
    transition-duration: 0.01ms;
  }
}
/*
 * The home page's twelve integration cards.
 *
 * NOT generated by `pnpm run port`. Hand-maintained, like
 * src/styles/theme-dark.css: 04-generate.mjs imports it into every page but
 * never rewrites it. The markup half is tools/fragments/integrations.mjs, which
 * the generator swaps in for the capture's "Stay connected" section.
 *
 * WHAT CHANGED
 * ------------
 * This sheet used to dress six cards, three of which carried line-art SVGs in a
 * 3:2 `.integration-art` box. Those three cards are now one card carrying the
 * connection cluster, and `.integration-art` is gone with them — every one of
 * the nine graphics is drawn in the one `.integration-mockup-box` panel that
 * the MCP and Mobile cards already used, which is what the API card needing
 * "the same aspect ratio as the rest" resolved to. That panel was square from
 * 2026-08-10 until 2026-08-30, when it became **4:5 portrait** at the owner's
 * direction, matching cursor.com's own home cards — see the panel rule below.
 *
 * ONE SCALE, MEASURED
 * -------------------
 * The panel is 4:5 portrait and its width is measured, not assumed:
 *
 *     viewport   1440  1280  1024   960   900 | 768   640   390
 *     panel       392   372   286   265   245 | 328   570   320
 *     height      490   465   358   331   306 | 410   713   400
 *
 * (900 is the narrowest the three-column grid gets; 768 and below are two and
 * one column.) Every graphic is drawn at **340px wide** and up to 344 tall —
 * with one exception, the model picker, whose scene is the 426px chat composer
 * the popover opens over and which is therefore 426 square on a 474 divisor —
 * and `.ig-scaler` fits it on *both* axes — the smaller of the width ratio and
 * the height ratio wins, capped at 1. In a portrait panel the width is the one
 * that binds, so from 1280 up the 340px graphics render 1:1 — the app's 14px
 * type is 14px — and at the 245px worst case they fall to 0.66. The 426px one
 * never reaches 1:1: it draws at 0.86 at 1440 and 0.54 at 900. That is exactly the recipe
 * `.integration-phone__scaler` has always used for the phone; `--ig-fit` just
 * names the divisor instead of hard-coding it. The height term is there so a
 * future ratio change cannot clip a graphic; it never binds at 4:5.
 *
 * Because the graphics are 1:1 rather than scaled *contents*, every length and
 * every type size in here is the app's own value in the app's own pixels. This
 * is the opposite of hero-chat.css and hero-mockup.css, which scale their
 * contents by 0.75 and 0.86 and therefore pick type off the capture's
 * `--text-product-*` ladder. Do not import that ladder here: 11px is not what
 * the app sets, and these panels are wide enough not to need it.
 *
 * COLOUR
 * ------
 * The same map the three window mockups use, since theme-dark.css already
 * re-anchored the capture's dark palette on the app's neutrals:
 *
 *     app token            this sheet
 *     --background         --color-theme-bg
 *     --muted              --color-theme-card-hex
 *     --card                --color-theme-card-02-hex
 *     --border             --color-theme-border-02
 *     --foreground         --color-theme-product-text
 *     --muted-foreground   --color-theme-product-text-sec
 *     --secondary-opaque   --color-theme-product-text-tertiary
 *     --hover              --color-theme-card-hover-hex
 *     --primary            #2483e2  (blue, NOT the Bidlo green of the logo)
 *
 * Two colours are deliberately fixed hexes in both themes, both because the app
 * fixes them: `--primary`, and Claude's `#D97757`, which
 * `features/chats/components/model-picker/provider-icons.tsx` pins on the
 * Anthropic mark alone while every other provider mark renders in currentColor.
 */

/* ------------------------------------------------------------------ *
 * The panel, and the stage inside it
 * ------------------------------------------------------------------ */

/* The media panel: **4:5 portrait** at every width, since 2026-08-30. It was
 * square from 2026-08-10, and before that the three line-art cards ran a 3:2
 * `.integration-art` box. 4:5 is the capture's own `aspect-4/5-box` token
 * (`padding-top:125%`), the tallest it emits, and the shape cursor.com's home
 * cards use for a graphic under a title, blurb and link.
 *
 * The capture ran its product mockups at `aspect-4/3-box md:aspect-1/1-box`;
 * here the ratio is the same at every width. That cannot be written with the
 * captured utilities: `src/styles/index.css` is frozen Tailwind output and its
 * unprefixed `.aspect-1/1-box` only emits the `::after` spacer — the
 * `display:grid` half lives on `.aspect-4/3-box` and `.md:aspect-1/1-box`
 * alone, and stacking the two loses anyway because `.aspect-4/3-box::after` is
 * declared later in the file. So both halves are restated here under one name. */
.integration-mockup-box {
  display: grid;
  grid-template-rows: repeat(1, minmax(0, 1fr));
  grid-template-columns: repeat(1, minmax(0, 1fr));
  position: relative;
  width: 100%;
  background-color: transparent;
}

.integration-mockup-box::after {
  content: "";
  pointer-events: none;
  grid-area: 1 / 1 / -1 / -1;
  padding-top: 125%; /* 4:5, the capture's own `.aspect-4/5-box` value */
}

/* The stage is the container the scaler measures. `container-type: size` (both
   axes, so the scaler can fit the height as well as the width) has to sit on an
   element that is exactly the panel's box, which is why it is here and not on
   the panel itself — the panel is a grid whose ::after spacer would otherwise
   be inside the containment. The stage is `absolute inset-0`, so it has the
   explicit height size containment needs. */
.ig-stage {
  container-type: size;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

/* `100cqw / var(--ig-fit)` is a plain number — a length over a length — so the
   scale tracks the panel exactly rather than stepping at breakpoints, and the
   `100cqh` term does the same against the panel's height; the smaller wins.
   Both read `--ig-fit`, which is the graphic's width plus air — the tallest
   graphic (the model picker, 344) is within 4px of the widest, so one divisor
   serves both axes. Capped at 1 so nothing ever renders larger than it was
   drawn. The unconditional `scale(0.66)` above it is the fallback for engines
   without that division: 0.66 is the narrowest panel's width ratio, so it fits
   everywhere rather than being right in one place. */
.ig-scaler {
  width: var(--ig-w, 340px);
  flex: none;
  transform-origin: center center;
  transform: scale(0.66);
  transform: scale(min(1, 100cqw / var(--ig-fit, 372px), 100cqh / var(--ig-fit, 372px)));
  font-family: var(--font-system);
  font-size: 14px; /* text-sm */
  line-height: 1.35;
  color: var(--color-theme-product-text);
  text-align: left;
  -webkit-font-smoothing: antialiased;
}

/* Every glyph in here is a lucide icon on a 24 viewBox at stroke-width 1.5.
   Reading that width in *device* pixels is what stops the stroke drifting
   between 1.0 and 1.5px as the stage scales — the same reason the three window
   mockups set it. It does not inherit, hence the descendant selector. */
.ig-scaler svg {
  display: block;
}

.ig-scaler svg * {
  vector-effect: non-scaling-stroke;
}

.ig-glyph {
  width: 16px;
  height: 16px;
  flex: none;
  color: var(--color-theme-product-text-sec);
}

/* The app's card: `bg-card`, a hairline and `rounded-lg`. Its ladder is
   anchored on `--radius: 0.625rem`, so `rounded-lg` is **10** and not the 8
   Tailwind's unanchored default gives. */
.ig-surface {
  background: var(--color-theme-card-02-hex);
  border: 1px solid var(--color-theme-border-02);
  border-radius: 10px;
  overflow: hidden;
}

/* ------------------------------------------------------------------ *
 * 1 — The corpus
 * ------------------------------------------------------------------ */

.ig-stat__head {
  padding: 12px 14px 6px;
  font-size: 12px;
  color: var(--color-theme-product-text-sec);
}

.ig-stat__row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 14px;
  height: 30px;
}

.ig-stat__label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Right-aligned and tabular so the commas stack: 1,654,636 over 12,842 only
   reads as two orders of magnitude when the digit columns line up. */
.ig-stat__figure {
  font-variant-numeric: tabular-nums;
  color: var(--color-theme-product-text);
}

/* The rule is the app's own separator weight, and the growth block sits on the
   page surface rather than the card so it reads as a footer rather than a
   sixth row. */
.ig-stat__growth {
  margin-top: 10px;
  padding: 12px 14px 14px;
  border-top: 1px solid var(--color-theme-border-02);
  background: var(--color-theme-bg);
}

/* The one figure drawn large — it is the card's claim, and everything above it
   is the evidence. 30px is the app's `text-3xl`. */
.ig-stat__delta {
  display: block;
  font-size: 30px;
  line-height: 1.1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--color-theme-product-text);
}

.ig-stat__caption {
  display: block;
  margin-top: 2px;
  font-size: 12px;
  color: var(--color-theme-product-text-sec);
}

/* ------------------------------------------------------------------ *
 * 2 — Reading what nobody structured
 * ------------------------------------------------------------------ */

.ig-parse__file {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px;
}

/* `ReturnedFileMessage`'s plated glyph: a `size-9` well at `rounded-md` (8). */
.ig-parse__plate {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: 8px;
  background: var(--color-theme-card-hover-hex);
}

.ig-parse__meta {
  min-width: 0;
  flex: 1;
}

.ig-parse__name {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ig-parse__format {
  display: block;
  margin-top: 1px;
  font-size: 12px;
  color: var(--color-theme-product-text-sec);
}

/* The seam is the only chrome on this card that says an operation happened: a
   hairline with a label centred on it. Drawn with two flex rules rather than a
   background gradient so it stays a true 1px at every scale. */
.ig-parse__seam {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 14px 2px;
  color: var(--color-theme-product-text-sec);
  font-size: 12px;
}

.ig-parse__seam::before,
.ig-parse__seam::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--color-theme-border-02);
}

.ig-parse__seam-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}

.ig-parse__seam-label .ig-glyph {
  width: 13px;
  height: 13px;
  color: #2483e2; /* --primary */
}

.ig-parse__fields {
  padding: 4px 0;
}

/* The app's property rows: a muted label in a fixed column, the value beside
   it. 118px is wide enough for "Letting Date" without wrapping, which is the
   longest of the four. */
.ig-parse__row {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 30px;
  padding: 0 14px;
}

.ig-parse__label {
  width: 118px;
  flex: none;
  font-size: 12px;
  color: var(--color-theme-product-text-sec);
}

.ig-parse__value {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ------------------------------------------------------------------ *
 * 3 — Search
 * ------------------------------------------------------------------ */

/* The app's `SearchInput` row is `h-16` with a `size-5` glyph. 56 rather than
   64 because this palette is drawn at 340 rather than at the dialog's 672 (see
   the fragment's note on why), and 64px of chrome over a 340px palette reads as
   a different component. */
.ig-search__input {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 56px;
  padding: 0 16px;
  border-bottom: 1px solid var(--color-theme-border-02);
}

.ig-search__icon {
  width: 20px;
  height: 20px;
  opacity: 0.5;
  color: var(--color-theme-product-text);
}

/* `text-base` — the palette's input is the one place in the app's chrome that
   is not `text-sm`. */
.ig-search__query {
  font-size: 16px;
  color: var(--color-theme-product-text);
}

/* cmdk's group heading: `px-2 font-medium text-muted-foreground`, 12px. */
.ig-search__heading {
  padding: 12px 16px 4px;
  font-size: 12px;
  font-weight: 500;
  color: var(--color-theme-product-text-sec);
}

.ig-search__list {
  padding-bottom: 6px;
}

/* `ResultRow` is `p-2` inside a `px-2` group. The first row carries cmdk's
   `data-[selected=true]` fill; a palette with nothing highlighted reads as a
   screenshot taken mid-blur. */
.ig-search__row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin: 0 8px;
  padding: 8px;
  border-radius: 8px;
}

.ig-search__row.is-selected {
  background: var(--color-theme-card-hover-hex);
}

/* `size-9 rounded bg-hover` — a bare `rounded` is 4, not the ladder's 6. */
.ig-search__plate {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: 4px;
  background: var(--color-theme-card-hover-hex);
}

.ig-search__row.is-selected .ig-search__plate {
  background: var(--color-theme-bg);
}

.ig-search__plate .ig-glyph {
  width: 18px;
  height: 18px;
  color: #2483e2; /* SearchAvatar renders text-primary */
}

.ig-search__text {
  min-width: 0;
  flex: 1;
}

/* The app middle-truncates these (`MiddleTruncatedSearchTitle`), which needs a
   measurement pass and a ResizeObserver. A still frame does not: an end
   ellipsis on a title that already carries its job number in the first eight
   characters loses nothing a reader needs. */
.ig-search__title {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ig-search__sub {
  display: block;
  margin-top: 1px;
  font-size: 12px;
  color: var(--color-theme-product-text-sec);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ------------------------------------------------------------------ *
 * 4 — The connection cluster
 * ------------------------------------------------------------------ */

.ig-cluster {
  position: relative;
  width: 340px;
  height: 340px;
}

/* Each slot is placed by rotating the whole arm and translating out along it,
   then the mark counter-rotates so it sits upright. That keeps all the
   geometry in one custom property per slot — `--ig-a`, set in the markup —
   with no trigonometry anywhere. */
.ig-cluster__slot {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: rotate(var(--ig-a)) translateY(-147px);
}

/* The slot is a 52x45 **rectangle**, not a square, and that is what makes the
   set read as one size. Four of the eight are lockups rather than marks — HCSS
   is 1.42:1, B2W 1.74:1, Salesforce 1.43:1 — so a square box sized them by
   their height and left them as thin scribbles beside the globe. Letting the
   wide ones use the extra width, with `preserveAspectRatio` containing each
   inside the box, gives every mark roughly the same ink area.

   The three numbers this rule shares with the ring — mark box, ring radius and
   core — are measured against cursor.com/enterprise's own card, the treatment
   this figure is modelled on, as fractions of the panel they are drawn in —
   the panel, not the stage: the 340px stage sits inside a 392px panel at 1440,
   so a fraction of the stage is not the same number.

   **2026-09-02: they are measured off a RENDER of both cards now, rather than
   off their source numbers, and the earlier estimates were wrong about the
   mark.** Two screenshots at the same panel scale, segmented into connected
   components, give — theirs first — ring radius **38.17% / 34.51%**, mark ink
   **11.79% / 11.26%**, core ink 24.6-28.2% (a cube is taller than it is wide)
   / 26.28%. So the mark box and the core were already right and only the
   radius was short: the note this replaces read the box as 8.9% of the panel
   by dividing the *box* rather than measuring the *ink* in it, and every mark
   here is contained inside its box rather than filling it.

   What the three rules carry now is a 54x47 box, a **147px** radius and a
   105px core. The radius is drawn rather than rendered — `--ig-fit` is 404, so
   at a 392px panel the whole graphic renders at 0.970 and 147 lands at 142.6,
   i.e. 36.4% of the panel. **It stops a little inside the reference's 38.17%,
   at the owner's direction**: matched exactly, the ring reads as eight marks
   pinned round the edge of the card rather than gathered on the one in the
   middle, because their eight are compact brand glyphs where four of ours are
   wide lockups that carry their ink further out. 147 pulls each mark ~7px in
   and gives the panel edge ~27px of air instead of 20 — the smallest step that
   changes the reading. The old note said 37.6% would put the side marks
   through the edge; that is true of the 340px *stage* and false of the panel,
   which is what clips (`.ig-stage` is the only `overflow: hidden` here and it
   is the panel's own box). The sum that matters is
   `(radius + half the mark box) / --ig-fit < 0.5` — 174 / 404 = 0.431 — since
   the scale is `panel / --ig-fit` capped at 1, so that ratio is the graphic's
   half-width as a fraction of the panel at *every* width. Re-tune the three
   together, never one at a time, and re-check that sum. */
.ig-cluster__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 54px;
  height: 47px;
  transform: rotate(calc(-1 * var(--ig-a))) scale(var(--ig-s, 1));
  /* The foreground at 42%. This sheet puts a secondary mark at 80% everywhere
     else and did here too until 2026-08-31, when the whole figure was measured
     against the cursor.com card above: there the vendor marks are visibly a
     step lighter than the product mark at the centre, which is what makes the
     ring read as *their* logos around *ours* rather than as nine peers. At 80%
     a solid mark like Microsoft's four squares came within a hair of the core.
     It was 55% until 2026-09-02, when the ring was reported as too prominent
     and stepped down again — and the same pass gave two of the slots real
     wordmarks (Box and Sage) in place of two lucide glyphs, which carry more
     ink than a stroked outline does. `color` rather than `opacity`, so the two
     masked marks (which paint currentColor) follow it too.

     **It is 12% as of 2026-09-02, and that number is measured rather than
     judged.** The reference card was segmented alongside a render of this one:
     its ring ink is one flat value across all eight marks that comes to
     **7.8% alpha over white**, where 42% of this site's `--color-theme-fg`
     (#d3d3d3 in dark) over the panel's #2a2a2a came to 33.3% — four times the
     ink. Two measures bracket the replacement and 12 is between them. Matching
     the *value* wants `42 + 0.078 x 213 = 59`, i.e. 10% of fg. Matching the
     *ink mass* — glyph area x alpha, which is the fairer test, since half of
     these are wordmarks where all eight of theirs are chunky brand glyphs —
     wants 13%: at 12% a glyph here carries 3.9 alpha-px per 1e4 of panel
     against their 4.33. Both are a long way below what looks right in
     isolation, because the ring is meant to read as texture around the product
     mark rather than as eight logos. Re-measure against the reference rather
     than nudging this up because one mark looks faint on its own. */
  color: #26251e1f;
}

.ig-cluster__mark > svg {
  width: 100%;
  height: 100%;
}

@supports (color: color-mix(in lab, red, red)) {
  .ig-cluster__mark {
    color: color-mix(in oklab, var(--color-theme-fg) 12%, transparent);
  }
}

/* B2W and Procore have no vector source — see the fragment's note on the
   marks — so they are alpha masks painting currentColor, the same treatment
   logo-garden.mjs gives Tolbert. Both live under public/brand, never
   public/assets, which 01-assets.mjs wipes on every run. */
.ig-cluster__mask {
  display: block;
  width: 100%;
  height: 100%;
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}

.ig-cluster__mask--b2w {
  -webkit-mask-image: url("/brand/integrations/b2w.png");
  mask-image: url("/brand/integrations/b2w.png");
}

.ig-cluster__mask--procore {
  -webkit-mask-image: url("/brand/integrations/procore.png");
  mask-image: url("/brand/integrations/procore.png");
}

/* The Bidlo mark at full foreground, so the ring reads as vendors around a
   product rather than as nine peers. 105px is 26.7% of the panel, near the 28%
   cursor.com's card spends on its cube — see the note on the mark box above. */
.ig-cluster__core {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-theme-product-text);
}

/* The mark is lifted off the panel by a soft shadow, and this is the one place
   the figure knowingly departs from the reference rather than matching it.
   Their centre is an isometric cube whose faces run 41 / 75 / 104 / 209 / 255
   on a 37 ground — a lit object, which is what reads as "it has a shadow" —
   and the panel around it is measurably flat, 37 to the edge on every side:
   there is no cast shadow there at all. This mark is one path and cannot have
   faces, so the lift is a cast shadow instead.

   **It is a contact shadow and not a glow, as of 2026-09-02.** It shipped for
   one build at `0 5px 18px / 30%` and was reported as gimmicky, which it was:
   an 18px blur on a 105px mark throws ink half the mark's own width in every
   direction, so on a flat panel with nothing else raised off it the mark wore
   a visible dark pool rather than sitting on a surface. What is there now is
   a **6px blur at 18%** — under a tenth of the mark's width, so the ink stays
   under the glyph and only the edge nearest the panel darkens. Read it as the
   floor: a shadow this figure notices is already too big. Both themes take it —
   on a light card it is an ordinary contact shadow, and on #2a2a2a it pools to
   about #1f1f1f. */
.ig-cluster__core > svg {
  width: 105px;
  height: 105px;
  filter: drop-shadow(0 2px 6px rgb(0 0 0 / 0.18));
}

/* ------------------------------------------------------------------ *
 * 5 — The model picker
 * ------------------------------------------------------------------ */

/* The scene: the chat composer with the model popover open over it. 426 square
   — the field's own width (FLOATING_CHAT_WIDTH 450 less the wrapper's px-3) by
   82 + 344 of height — against the 1.25 x 474 a 4:5 panel divided by --ig-fit
   allows (474 being 426 plus 48 of air, this card's one departure from the
   file's default 32 — see the card entry in the fragment). This is the one graphic in the file not drawn at 340; see the art's
   comment in the fragment for why the picker's width could not be the scene's. */
.ig-pick {
  position: relative;
  height: 426px;
}

/* The composer's `InputGroup`: `rounded-[18px] border shadow-xs bg-background`
   over an `h-12` footer. Every value is the app's own pixel — this graphic is
   1:1, unlike hero-chat.css and automate-mockup.css, which draw the same field
   at 0.75 and 0.86 and therefore halve these numbers. It sits a stacking level
   above the picker so its shadow and its bottom edge stay drawn over the menu
   that tucks under it. */
.ig-cmp {
  border: 1px solid var(--color-theme-border-02);
  border-radius: 18px;
  /* `bg-background`, deliberately not the `--card` the picker below takes: in
     the app the field is the page's own ground and the popover is a card on
     top of it, and drawing both on one fill is what made an earlier pass read
     as a single tall box with a rule across it. */
  background: var(--color-theme-bg);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.05); /* shadow-xs */
}

/* `pt-4 px-4`, and the empty field's placeholder colour. 16px because chat
   prose is `.font-message`, a step above the 14px `text-sm` the picker's rows
   below it are set in — the app's own two tiers, not a hierarchy invented
   here. */
.ig-cmp__text {
  padding: 16px 16px 0;
  font-size: 16px;
  line-height: 1.375;
  color: var(--color-theme-product-text-tertiary);
}

/* `h-12 px-2 pb-2` over the addon's own `py-1.5`. */
.ig-cmp__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 48px;
  padding: 6px 8px 8px;
}

.ig-cmp__group {
  display: flex;
  align-items: center;
}

/* `size-8 rounded-full`, glyph at `size-5`. */
.ig-cmp__round {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: 999px;
  color: var(--color-theme-product-text-sec);
}

.ig-cmp__round .ig-glyph {
  width: 20px;
  height: 20px;
}

/* The submit button. It is the field's only right-hand control here — the app's
   own resting pair (a context-usage ring and a dictation button) is out at the
   owner's direction; see the note on `composer` in the fragment.

   The app draws this as `variant="default"`, i.e. the primary blue, and this
   card deliberately does not. The popover below is the subject of the card, and
   a saturated blue disc is the only chromatic mark in the frame — it out-shouts
   the menu it is meant to be hanging off. So the plate is the foreground ink and
   the arrow is the field's own surface, which reads as a submit button without
   competing: near-black on paper in light, and — since both tokens invert — pale
   on near-black in dark. It is the one control in this graphic painted off
   tokens rather than off the app's fixed hex. */
.ig-cmp__round--primary {
  background: var(--color-theme-product-text);
  color: var(--color-theme-bg);
}

/* `.ig-glyph` sets a muted `color` on the svg itself, which beats the colour
   inherited from the button, so the arrow has to be repainted here. */
.ig-cmp__round--primary .ig-glyph {
  color: var(--color-theme-bg);
}

/* `ProviderModelPicker`'s trigger: the provider mark, the model name and a
   chevron on a `h-7 rounded-full px-2` pill. Drawn in its **open** state —
   `data-state="open"` takes `bg-hover` — because the popover below is anchored
   to it, and a lit trigger is what makes the two read as one gesture. */
.ig-cmp__model {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  margin-left: 2px;
  padding: 0 8px;
  border-radius: 999px;
  white-space: nowrap;
  color: var(--color-theme-product-text);
}

.ig-cmp__model.is-open {
  background: var(--color-theme-card-hover-hex);
}

.ig-cmp__model > svg {
  width: 14px;
  height: 14px;
  flex: none;
}

.ig-cmp__caret {
  width: 14px;
  height: 14px;
  color: var(--color-theme-product-text-sec);
}

/* `ModelPickerContent` declares its canvas inline as exactly 340 x 344, with a
   comment saying why it is not a utility class. This is the one graphic drawn
   at the app's size rather than at a scale of it, and the reason every other
   graphic here is 340 wide. */
.ig-mp {
  display: flex;
  width: 340px; /* the other half of that inline canvas, and load-bearing now
                   that the popover is out of flow — see .ig-pick .ig-mp */
  height: 344px;
  /* ---- the one deliberate divergence inside the popover ----
     The app sets this menu in `text-sm` (14) over a `text-xs` (12) provider
     line, on `py-2` rows. At those values Anthropic's four rows come to 212 of
     the canvas's 275px list, and a third of the popover is empty — which in the
     app is nothing (Favorites and OpenAI fill it) and in a still is a hole. So
     the popover alone steps up one tier: 16/13 on `py-3` rows, which brings the
     four to 264 and leaves 11. 16 is also what the field above it sets, and in
     the reference this card was drawn against the menu rows and the field's
     placeholder are the same size. Everything else here is still the app's own
     pixel. To go back, drop this block and the four sizes marked "tier". */
  font-size: 16px;
  /* `--popover` is `--card`, which is what the composer above deliberately is
     not — and until 2026-08-31 this read `--color-theme-card-02-hex`, which on
     this site resolves to the *same* #ebeae5 as `--color-theme-card-hover-hex`.
     So the popover was painted in its own hover fill and the highlighted row
     was invisible: a lit row and an unlit one were the same colour. Fixing it
     is what lets the live half below register at all. */
  background: var(--color-theme-card-hex);
  border: 1px solid var(--color-theme-border-02);
  border-radius: 10px;
  overflow: hidden;
}

/* Drawn as what it is: a Radix popover on the model chip. `left` is exactly
   where that chip starts — the footer's 8px pad, the 32px action button and the
   chip's own 2px margin — and `top` puts it 6px over the field's 88px bottom
   edge, so it covers the corner it opened from rather than butting against it.
   The shadow is what carries the layering: two boxes at the same elevation read
   as one panel with a rule across it however they overlap. */
.ig-pick .ig-mp {
  position: absolute;
  left: 42px;
  top: 82px;
  z-index: 1;
  box-shadow:
    0 8px 24px rgb(0 0 0 / 0.10),
    0 2px 6px rgb(0 0 0 / 0.06);
}

/* `w-[52px] ... border-r border-border p-1.5`, gap-1. */
.ig-mp__rail {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 52px;
  flex: none;
  padding: 6px;
  border-right: 1px solid var(--color-theme-border-02);
}

/* `size-10 rounded-md`, muted until active. */
.ig-mp__tab {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: 8px;
  color: var(--color-theme-product-text-sec);
}

.ig-mp__tab > svg,
.ig-mp__tab .ig-glyph {
  width: 18px; /* size-4.5 */
  height: 18px;
}

.ig-mp__tab.is-active {
  color: var(--color-theme-product-text);
}

/* The floating divider between Favorites and the provider tabs: `h-px w-8`
   with `mt-0.5 mb-1.5`. */
.ig-mp__divider {
  width: 32px;
  height: 1px;
  flex: none;
  margin: 2px 0 6px;
  background: var(--color-theme-border-02);
}

/* The active-tab pill is one element that slides between tabs, and it sits
   *on* the divider rather than behind the icons — `right: -1`, `width: 3`. Its
   top here is the Anthropic tab's centre: 6px pad + 40 (favorites) + 4 gap +
   2 + 1 + 6 (divider block) + 4 gap + 40 + 4 (openai + gap) + 20 = 127. An
   earlier value of 123 dropped the gap *below* the divider — the rail's `gap`
   applies on both sides of it, on top of its own margins — and drew the pill
   4px above the icon it marks. */
.ig-mp__pill {
  position: absolute;
  top: 127px;
  right: -1px;
  width: 3px;
  height: 24px;
  border-radius: 999px;
  background: #2483e2;
  transform: translateY(-50%);
}

.ig-mp__body {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
  padding: 6px;
}

/* `mx-2 flex h-10` — the search row mirrors the rail's favorites square so the
   two align, and its `pl-2` puts the placeholder on the same vertical line as
   the model names below. */
.ig-mp__search {
  display: flex;
  align-items: center;
  height: 40px;
  margin: 0 8px;
  color: var(--color-theme-product-text-sec);
}

.ig-mp__search .ig-glyph {
  width: 16px;
  height: 16px;
}

.ig-mp__placeholder {
  padding-left: 8px;
  color: var(--color-theme-product-text-tertiary); /* placeholder:text-secondary */
}

/* Unfocused, so `bg-border` rather than `bg-primary`. `mx-1 mt-1.5`. */
.ig-mp__underline {
  display: block;
  height: 1px;
  margin: 6px 4px 0;
  background: var(--color-theme-border-02);
}

/* `mt-2.5` lands the first row's top on the first provider tab's top. */
.ig-mp__list {
  margin-top: 10px;
  min-height: 0;
  flex: 1;
}

/* `ModelListRow` is `rounded-md px-2 py-2`, lit by `bg-hover` when
   highlighted. */
.ig-mp__row {
  position: relative;
  padding: 12px 8px; /* tier: py-2 -> py-3, see .ig-mp */
  border-radius: 8px;
}

.ig-mp__row.is-highlighted {
  background: var(--color-theme-card-hover-hex);
}

.ig-mp__title {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 500;
  padding-right: 56px; /* pr-14 — the row's out-of-flow action buttons */
}

.ig-mp__check {
  width: 16px; /* tier: size-3.5 */
  height: 16px;
  color: #2483e2;
}

/* The provider line: `mt-0.5 text-xs text-secondary` with a `size-3` mark. */
.ig-mp__meta {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 2px;
  font-size: 13px; /* tier: text-xs */
  color: var(--color-theme-product-text-tertiary);
}

.ig-mp__meta > svg {
  width: 13px; /* tier */
  height: 13px;
  flex: none;
}

.ig-mp__dot {
  opacity: 0.6;
}

/* The row's two trailing actions: the reasoning-level button and the star.
   `absolute right-0 top-0 flex h-5 items-center` in the app, where `top-0` is
   the top of the row's *content* box and `h-5` is the title's own line — which
   is what keeps the pair centred on the model name rather than drifting to the
   middle of a two-line row. Both are drawn on every row now, which is what
   `.ig-mp__title`'s `pr-14` has always been reserving room for. */
.ig-mp__actions {
  position: absolute;
  top: 12px; /* tier: follows the row's padding, i.e. the app's `top-0` */
  right: 8px;
  display: flex;
  align-items: center;
  height: 20px; /* h-5 */
}

/* `rowActionClassName`: `rounded-md p-1.5 text-muted-foreground opacity-0`.
   Hidden at rest and revealed by the row — see the live-half block below. */
.ig-mp__action {
  display: flex;
  align-items: center;
  padding: 6px; /* p-1.5 */
  border-radius: 6px; /* rounded-md */
  color: var(--color-theme-product-text-sec);
  opacity: 0;
}

.ig-mp__action .ig-glyph {
  width: 14px; /* size-3.5 */
  height: 14px;
}

/* A favourited star stays lit unhovered — it is the only signal in the list
   that a model is favourited, which is why `rowActionClassName` takes
   `isHighlighted || isFavorite` for the star and `isHighlighted` alone for the
   settings button. So this is the one action visible on a row at rest. */
.ig-mp__star.is-filled {
  opacity: 1;
  color: var(--color-theme-product-text);
}

.ig-mp__star.is-filled .ig-glyph {
  fill: var(--color-theme-product-text);
}

/* The highlighted row shows both of its actions. */
.ig-mp__row.is-highlighted .ig-mp__action {
  opacity: 1;
}

/* ---- the live half ------------------------------------------------------
   Everything above draws the popover at rest; this is what the pointer does to
   it, and it is the only figure in this sheet that has such a half. The reason
   is that the card's claim is that you *pick* the model, and a menu that does
   not light under the cursor reads as a picture of a menu.

   None of it is invented. `ModelListRow` is `cursor-pointer` with
   `onMouseMove={onHighlight}`, so in the app the highlight is the pointer: it
   moves to whatever row is under it and leaves wherever it was. That is the
   pair of rules below — a hovered row lights, and the resting row gives the
   highlight up the moment the list is hovered at all. Keying the handover off
   `.ig-mp__list:hover` rather than off each row is deliberate: the 10px gaps
   between rows are inside the list but outside every row, and a row-keyed rule
   would flick Sonnet 5 back on every time the pointer crossed one.

   The rail tabs are `hover:bg-hover hover:text-foreground`, and the two row
   actions are `hover:text-foreground` — their `hover:bg-hover` is in the app
   too and is left out here because it is inert: an action is only ever on
   screen while its own row is already painted in that exact fill.

   GUARDED ON `hover: hover`, and that is load-bearing rather than tidy. A
   touch device fires hover on tap and leaves it stuck, so a phone would end up
   with two rows lit — the resting one and the last one touched — which is a
   state the app never has. Touch readers get the still frame, which is what
   this card was until now. */
@media (hover: hover) {
  .ig-mp__row {
    cursor: pointer;
  }

  .ig-mp__row:hover {
    background: var(--color-theme-card-hover-hex);
  }

  .ig-mp__row:hover .ig-mp__action {
    opacity: 1;
  }

  /* The highlight is the pointer: once the list has one, the resting row is
     not highlighted any more, and its settings button goes with the fill. Its
     star stays, being state rather than affordance. */
  .ig-mp__list:hover .ig-mp__row.is-highlighted:not(:hover) {
    background: transparent;
  }

  .ig-mp__list:hover .ig-mp__row.is-highlighted:not(:hover) .ig-mp__action:not(.is-filled) {
    opacity: 0;
  }

  .ig-mp__action:hover,
  .ig-mp__effort:hover {
    color: var(--color-theme-product-text);
  }

  /* `onExpandFromLevel` — the level label opens the reasoning slider, so it is
     a button in its own right and not part of the provider line's text. */
  .ig-mp__effort {
    cursor: pointer;
  }

  .ig-mp__tab {
    cursor: pointer;
  }

  .ig-mp__tab:hover {
    background: var(--color-theme-card-hover-hex);
    color: var(--color-theme-product-text);
  }

  /* The search field is an `<input>` in the app; here it is a placeholder span,
     so the cursor is the only part of that this frame can honour. */
  .ig-mp__search {
    cursor: text;
  }
}

/* ------------------------------------------------------------------ *
 * 6 — Output formats
 * ------------------------------------------------------------------ */

.ig-export__head,
.ig-alerts__head,
.ig-code__head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--color-theme-border-02);
  font-size: 12px;
  color: var(--color-theme-product-text-sec);
}

.ig-export__row {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 38px;
  padding: 0 14px;
}

.ig-export__label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ig-export__ext {
  font-size: 12px;
  color: var(--color-theme-product-text-tertiary);
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------------ *
 * 7 — Alerts
 * ------------------------------------------------------------------ */

.ig-alert {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--color-theme-border-02);
}

.ig-alert:last-child {
  border-bottom: 0;
}

.ig-alert.is-unread {
  background: var(--color-theme-bg);
}

/* The unread dot and the fill are the only difference between a read row and
   an unread one, which is what makes the state legible without a label. */
.ig-alert__dot {
  width: 6px;
  height: 6px;
  flex: none;
  margin-top: 6px;
  border-radius: 999px;
  background: transparent;
}

.ig-alert.is-unread .ig-alert__dot {
  background: #2483e2;
}

.ig-alert__text {
  min-width: 0;
  flex: 1;
}

.ig-alert__title {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ig-alert__body {
  display: block;
  margin-top: 1px;
  font-size: 12px;
  color: var(--color-theme-product-text-sec);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ig-alert__when {
  flex: none;
  font-size: 12px;
  color: var(--color-theme-product-text-tertiary);
}

/* ------------------------------------------------------------------ *
 * 8 — The API
 * ------------------------------------------------------------------ */

.ig-code__body {
  padding: 12px 14px 14px;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 12px;
  line-height: 18px;
}

.ig-code__line {
  white-space: pre;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--color-theme-product-text);
}

/* An empty line still has to take its 18px, or the request and the body run
   together. */
.ig-code__line:empty::before {
  content: "\00a0";
}

.ig-code__line.is-verb {
  color: #2483e2;
}

.ig-code__line.is-muted {
  color: var(--color-theme-product-text-sec);
}

.ig-code__line.is-indent {
  padding-left: 14px;
}

.ig-code__line.is-indent2 {
  padding-left: 28px;
}

/* ------------------------------------------------------------------ *
 * 9 — Unit price history
 *
 * The one chart in the section. Head, item line, the median drawn large, twelve
 * bars, an axis and a footer — read top to bottom that is claim, then evidence,
 * which is the corpus card's arrangement turned the right way up: there the
 * counts are the evidence and the growth figure the claim, and it sits at the
 * foot because the counts come first in the reading. Here the median is what a
 * reader wants and the distribution is why they should believe it.
 * ------------------------------------------------------------------ */

.ig-price__head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 14px 0;
  font-size: 12px;
  color: var(--color-theme-product-text-sec);
}

.ig-price__head .ig-glyph {
  width: 14px;
  height: 14px;
}

/* The item's own name, in the app's caption colour. It runs 47 characters and
   the panel takes about 45 at this size, so it truncates by a word — which is
   correct rather than tolerated: the code is the identifier and it leads. */
.ig-price__item {
  padding: 6px 14px 0;
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-theme-product-text-tertiary);
}

/* 30px is the app's `text-3xl`, the same step the corpus card's growth figure
   takes, so the two cards' one large number is one size. */
.ig-price__median {
  display: flex;
  align-items: baseline;
  gap: 5px;
  padding: 8px 14px 0;
  font-size: 30px;
  line-height: 1.1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--color-theme-product-text);
}

.ig-price__unit {
  font-size: 13px;
  letter-spacing: 0;
  color: var(--color-theme-product-text-sec);
}

.ig-price__caption {
  padding: 2px 14px 0;
  font-size: 12px;
  color: var(--color-theme-product-text-sec);
}

/* The bars sit on the page surface rather than the card so the chart reads as a
   panel inside the panel — the same separation the corpus card gives its growth
   block, and the reason neither needs a frame of its own. */
.ig-price__chart {
  position: relative;
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 104px;
  margin-top: 14px;
  padding: 0 14px;
}

.ig-price__bar {
  flex: 1;
  height: var(--ig-h);
  min-height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--color-theme-fg-15, rgb(0 0 0 / 0.15));
}

/* The median, positioned by price rather than by bucket — see the fragment.
   `--ig-x` is a unitless share of the chart's *data* width (a percentage would
   not do: calc cannot multiply a length by one), so the rule is inset by the
   same 14px the bars are and offset by half its own width. */
.ig-price__marker {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(14px + (100% - 28px) * var(--ig-x));
  width: 1px;
  margin-left: -0.5px;
  background: #2483e2; /* --primary */
}

.ig-price__axis {
  display: flex;
  justify-content: space-between;
  padding: 6px 14px 0;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--color-theme-product-text-tertiary);
}

.ig-price__foot {
  margin-top: 12px;
  padding: 10px 14px 12px;
  border-top: 1px solid var(--color-theme-border-02);
  background: var(--color-theme-bg);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--color-theme-product-text-sec);
}

/* ------------------------------------------------------------------ *
 * 10 — The bid tab
 *
 * A table without a table's chrome: no column headers, no rules between rows,
 * three columns held by width alone. The app's Bid Tabs view has all of that
 * chrome and it is drawn one card up on this page in the items window; a second
 * ruled grid here would read as the same picture twice.
 * ------------------------------------------------------------------ */

.ig-bids__head {
  padding: 12px 14px 8px;
}

.ig-bids__project {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-theme-product-text);
}

.ig-bids__sub {
  display: block;
  margin-top: 1px;
  font-size: 12px;
  color: var(--color-theme-product-text-sec);
}

.ig-bid {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 30px;
  padding: 0 14px;
}

.ig-bid__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-theme-product-text-sec);
}

/* Right-aligned and tabular for the corpus card's reason: seven seven-figure
   numbers only read as a spread when the commas stack. 92px is what
   `$14,130,847` takes at 14px without the column moving between rows. */
.ig-bid__amount {
  width: 92px;
  flex: none;
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--color-theme-product-text);
}

.ig-bid__delta {
  width: 46px;
  flex: none;
  text-align: right;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--color-theme-product-text-tertiary);
}

/* The low bid: the app's selected-row fill, and the only row whose name and
   figure are both at full strength. Its delta reads "Low" rather than +0.0%. */
.ig-bid.is-low {
  background: var(--color-theme-card-hover-hex);
}

.ig-bid.is-low .ig-bid__name {
  color: var(--color-theme-product-text);
}

.ig-bid.is-low .ig-bid__delta {
  color: #2483e2; /* --primary */
}

/* The estimate is not a bidder. Dropping its amount to the caption colour is
   what separates it from the six rows around it without a second treatment. */
.ig-bid.is-estimate .ig-bid__amount {
  color: var(--color-theme-product-text-sec);
}

.ig-bids__foot {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  padding: 11px 14px 13px;
  border-top: 1px solid var(--color-theme-border-02);
  background: var(--color-theme-bg);
  font-size: 12px;
  color: var(--color-theme-product-text-sec);
}

.ig-bids__foot .ig-glyph {
  width: 14px;
  height: 14px;
}

/* ------------------------------------------------------------------ *
 * 11 — Agents
 *
 * Five list rows over an add row. The add row is the app's own affordance and
 * is the reason the card works: without it this is a list of five features, and
 * with it it is a list of five things a customer made.
 * ------------------------------------------------------------------ */

.ig-agents__head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 14px 8px;
  font-size: 12px;
  color: var(--color-theme-product-text-sec);
}

.ig-agents__head .ig-glyph {
  width: 14px;
  height: 14px;
}

.ig-agent {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 40px;
  padding: 0 14px;
}

/* `AgentAvatar` at `size-6`: the name's first letter on the badge colour that
   agent's icon document stores. 24px rather than the 16 the hero window's
   pinned agents draw — see the fragment on why, and on the round-child
   detector that makes the size load-bearing rather than aesthetic. */
.ig-agent__avatar {
  width: 24px;
  height: 24px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  font-size: 11px;
  line-height: 1;
  font-weight: var(--font-weight-medium);
  background: var(--ig-tone-bg);
  color: var(--ig-tone-fg);
}

.ig-agent__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ig-agent__status {
  flex: none;
  font-size: 12px;
  color: var(--color-theme-product-text-tertiary);
}

/* The running row. The fill is the app's hover-opaque, the status takes the
   accent, and nothing spins: this is a still. */
.ig-agent.is-busy {
  background: var(--color-theme-card-hover-hex);
}

.ig-agent.is-busy .ig-agent__status {
  color: #2483e2; /* --primary */
}

.ig-agents__foot {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  padding: 11px 14px 13px;
  border-top: 1px solid var(--color-theme-border-02);
  background: var(--color-theme-bg);
  color: var(--color-theme-product-text-tertiary);
}

.ig-agents__foot .ig-glyph {
  width: 16px;
  height: 16px;
  color: inherit;
}

/* -------------------------------------------------------------------------
   The badge palette, and the only fixed hexes in this file besides `--primary`
   and Claude's orange.

   These are the app's named badge colours at their real alpha, converted from
   oklch, and they cannot come off the theme tokens: the hue is stored on each
   agent's own icon document, not derived from the surface it sits on. The five
   below are copied from the two window mockups that already carry them —
   `hero-mockup.css` has red, purple and gray; `items-mockup.css` has blue,
   green, yellow and orange — by arithmetic rather than by sharing, exactly as
   those two carry the same values as each other. Adding a hue means converting
   it, not guessing. Dark mode re-fills at the higher alpha the app uses there
   and collapses every foreground to white at 87%, which is what stops a badge
   going muddy on a dark surface.
   ------------------------------------------------------------------------- */

.ig-agent__avatar[data-tone='red'] {
  --ig-tone-bg: rgb(255 171 158 / 0.4);
  --ig-tone-fg: #71000b;
}

.ig-agent__avatar[data-tone='blue'] {
  --ig-tone-bg: rgb(65 139 198 / 0.27);
  --ig-tone-fg: #001f43;
}

.ig-agent__avatar[data-tone='green'] {
  --ig-tone-bg: rgb(80 158 114 / 0.27);
  --ig-tone-fg: #002f23;
}

.ig-agent__avatar[data-tone='purple'] {
  --ig-tone-bg: rgb(155 120 201 / 0.27);
  --ig-tone-fg: #362348;
}

.ig-agent__avatar[data-tone='orange'] {
  --ig-tone-bg: rgb(226 146 18 / 0.27);
  --ig-tone-fg: #4e2b0d;
}

:root[data-theme='dark'] .ig-agent__avatar {
  --ig-tone-fg: rgb(255 255 255 / 0.87);
}

:root[data-theme='dark'] .ig-agent__avatar[data-tone='red'] {
  --ig-tone-bg: rgb(238 107 93 / 0.45);
}

:root[data-theme='dark'] .ig-agent__avatar[data-tone='blue'] {
  --ig-tone-bg: rgb(0 103 165 / 0.5);
}

:root[data-theme='dark'] .ig-agent__avatar[data-tone='green'] {
  --ig-tone-bg: rgb(0 128 85 / 0.5);
}

:root[data-theme='dark'] .ig-agent__avatar[data-tone='purple'] {
  --ig-tone-bg: rgb(138 103 229 / 0.34);
}

:root[data-theme='dark'] .ig-agent__avatar[data-tone='orange'] {
  --ig-tone-bg: rgb(239 146 0 / 0.45);
}

/* ------------------------------------------------------------------ *
 * 12 — Mobile
 * ------------------------------------------------------------------ */

/* Unchanged, and predates this pass.
 *
 * The phone is a fixed 300x650 in the capture and every length inside it is a
 * pixel value, so it cannot be made fluid by restyling; it is scaled instead.
 * `100cqw / 340px` is a plain number — a length over a length — so the scale
 * tracks the panel exactly; the divisor is the 300px device plus ~12% of panel
 * so the bezel keeps air on both sides at the narrowest column, and the whole
 * thing is capped at 1 so the phone never renders larger than the capture drew
 * it. The unconditional `scale(0.72)` above it is the fallback for engines that
 * do not do that division. */
.integration-phone {
  container-type: inline-size;
  /* Only the top of the device is in frame, as in the capture's own card — the
     phone runs off the bottom of the square panel rather than sitting in it. */
  padding-top: 8%;
}

.integration-phone__scaler {
  width: 300px;
  flex: none;
  transform-origin: top center;
  transform: scale(0.72);
  transform: scale(min(1, 100cqw / 340px));
}

/* ------------------------------------------------------------------ *
 * 13 — The services card's photograph
 * ------------------------------------------------------------------ *
 *
 * The one panel in this section that holds a picture rather than a drawing, so
 * it is the one that never touches `.ig-stage` / `.ig-scaler` — those exist to
 * hold drawn markup at its own scale, and a photograph shrunk to 340px in the
 * middle of a 4:5 panel is fifteen faces at thumbnail size. The panel's own box
 * is the frame instead, and `cover` does the cropping.
 *
 * `object-position` is off-centre on purpose, and **which axis it is spending
 * flipped on 2026-08-31** when the source went from a 4:3 landscape frame to a
 * 900x1600 portrait one. At 0.5625 the photograph is now *narrower* than the
 * 4:5 box, so cover fills the width and crops the height instead: 1125 of its
 * 1600 rows survive and the 475 left over are the only slack there is. The
 * horizontal is therefore a plain 50% — there is nothing to move along — and
 * 32% down the vertical is what leaves ~80px of air over the presenter's head
 * while keeping the seated crew in, rather than centring and shaving his crown.
 *
 * The file is `public/brand/consulting-kickoff.webp`. The panel is 392x490 at
 * its largest, so 900 wide clears the 784 a 2x render wants. Never
 * `public/assets`, which `01-assets.mjs` wipes on every run.
 */
.ig-photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 32%;
}

/* ------------------------------------------------------------------ *
 * 14 — The card's own layout: copy over figure, and the link between
 * ------------------------------------------------------------------ *
 *
 * Since 2026-08-30 every card draws its title, blurb and link *above* the
 * graphic, the way cursor.com's home cards do. `.ig-figure` is the air between
 * the copy and the panel — the capture's own `g1.75` rhythm, restated here
 * rather than as a `pt-g1.75` utility because only `index.css` emits those and
 * a relocated card has to bring its own spacing. `.ig-cta` is the capture's
 * accent (`--color-theme-accent`, the one orange on the site) on the site's
 * base type, with the arrow riding a hair up and right on hover.
 */
.ig-figure {
  padding-top: var(--spacing-g1\.75);
}

.ig-cta {
  display: inline-flex;
  align-items: baseline;
  gap: 0.15em;
  align-self: flex-start;
  margin-top: var(--spacing-g1);
  color: var(--color-theme-accent);
  text-decoration: none;
}

.ig-cta:hover {
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.ig-cta__arrow {
  display: inline-block;
  transition: transform 0.15s ease;
}

.ig-cta:hover .ig-cta__arrow {
  transform: translate(0.1em, -0.1em);
}
/* Header "Get started" button — slide, don't snap.
 *
 * Hand-maintained, like src/styles/theme-dark.css: tools/04-generate.mjs
 * imports it into every page but never rewrites it, and it leaves
 * src/scripts/header-cta.js alone for the same reason.
 *
 * The button enters from the right once the hero CTA row scrolls away and
 * leaves to the left when the row comes back. Two directions means two hidden
 * states — `clone-cta-enter` (parked to the right, about to arrive) and
 * `clone-cta-hide` (on its way out to the left) — which is why the script
 * teleports the button to the enter state before showing it.
 *
 * These are new class names rather than the capture's `.clone-cta-off` on
 * purpose: that rule is `display:none!important`, inlined after this file in
 * the tail styles, and nothing animates out of `display:none`.
 *
 * Both hidden states collapse to zero width so the rest of the header sits
 * exactly where the capture puts it — the button contributes no space at all,
 * gap included.
 */

#site-header .clone-cta-anim {
  max-width: var(--clone-cta-w, 12rem);
  overflow: hidden;
  white-space: nowrap;
  transition:
    max-width 0.34s var(--ease-out-spring, ease),
    padding 0.34s var(--ease-out-spring, ease),
    margin 0.34s var(--ease-out-spring, ease),
    border-width 0.34s var(--ease-out-spring, ease),
    transform 0.34s var(--ease-out-spring, ease),
    opacity 0.22s var(--ease-out, ease);
}

/* Held for the one frame where the button is teleported to its start position,
   or measured at its natural width. Never with a transition in flight. */
#site-header .clone-cta-still {
  transition: none !important;
}

#site-header .clone-cta-hide,
#site-header .clone-cta-enter {
  max-width: 0;
  padding-left: 0;
  padding-right: 0;
  border-left-width: 0;
  border-right-width: 0;
  /* Swallows the flex row's gap, which would otherwise leave a hole. */
  margin-left: calc(-1 * var(--clone-cta-gap, 0px));
  opacity: 0;
  pointer-events: none;
}

#site-header .clone-cta-hide {
  transform: translateX(-1.25rem);
}

#site-header .clone-cta-enter {
  transform: translateX(1.25rem);
}

/* At rest, hand the button back to the capture's `display:none`. An element
   parked at opacity 0 still builds a stacking context, and that alone changes
   how the browser antialiases the rest of the header row — enough for
   `npm run verify` to see a few hundred differing pixels on text that never
   moved. The script adds this once the hide transition has finished. */
#site-header .clone-cta-gone {
  display: none !important;
}

@media (prefers-reduced-motion: reduce) {
  #site-header .clone-cta-anim {
    transition-duration: 0.01ms;
  }
}
/* Auth-aware CTAs.
 *
 * Hand-maintained: `pnpm run port` never writes this file, and
 * tools/04-generate.mjs imports it into every page.
 *
 * Both halves of the header CTA cluster ship in the HTML — the signed-out row
 * (Log In / Demo / Get started) and the signed-in one ("Open Bidlo") — and this
 * hides whichever does not apply. `data-auth` lands on <html> from an inline
 * bootstrap in <head>, so the choice is made before first paint: no flash, and
 * nothing is built at runtime. The mobile sheet gets it for free, because
 * src/lib/mobile-menu.js carries `data-auth-only` onto the copies it makes.
 *
 * Hide-only, deliberately. Restating a display value here would have to guess
 * between the `.btn`'s inline-flex and the `hidden lg:inline-flex` ladder the
 * Demo button rides; leaving the shown side alone lets its own classes decide.
 *
 * `!important` because src/styles/<page>.css is frozen Tailwind that hides the
 * same elements at some breakpoints — this has to win in both directions, not
 * just on specificity. */

html:not([data-auth='in']) [data-auth-only='in'],
html[data-auth='in'] [data-auth-only='out'] {
  display: none !important;
}
/* The route indicator in the header lockup — [Bidlo] /documentation.
 *
 * Hand-maintained, like src/styles/theme-dark.css and src/styles/auth-cta.css:
 * tools/04-generate.mjs imports it into every page but never rewrites it. Pairs
 * with tools/fragments/header-route.mjs, which appends the span and resolves
 * its text per page as an Astro expression.
 *
 * THE SLASH IS A PSEUDO-ELEMENT, and that is load-bearing rather than tidy: the
 * fragment emits the span unconditionally and this file hides it with `:empty`
 * on the home page and the 404, where the label resolves to "". `:empty` counts
 * child NODES, so a `::before` leaves the span empty and a text "/" would not.
 *
 * THE CELL IS A FLEX ROW, which is a change to captured markup and is why the
 * class is this file's own rather than a Tailwind utility — src/styles/*.css is
 * frozen Tailwind output, so `flex` on that div would have a rule only where
 * some other element on that page happened to need one. Cost against a
 * same-tree build without it: 0 differing pixels at 1440/768/390 on the home
 * page, where the label is hidden and the anchor is the row's only item.
 *
 * Every colour is one of the site's own tokens, all of which are an alpha over
 * `--color-theme-fg` — that token inverts between themes, so this file needs no
 * dark block.
 */

.hdr-lockup {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

#site-header .hdr-route {
  /* The nav beside it is --text-sm; this is one step up, so the lockup reads as
     one object rather than as the brand with a nav item stuck to it. */
  font-size: var(--text-base);
  line-height: 1;
  font-weight: var(--font-weight-normal);
  color: var(--color-theme-text-sec);
  white-space: nowrap;

  /* THE TWO x-HEIGHT BANDS ARE CENTRED ON EACH OTHER, and the number is solved
     rather than picked. Two other rules shipped first and both were reported.
     `align-items: center` ALONE (a 3.20px offset) reads HIGH: it centres the
     16px line box on the 22.64px row, which is the middle of the ICON, and the
     wordmark beside it is not centred in that box. Matching the two BASELINES
     (7.04px) reads LOW, which is what baseline alignment always does when a
     small UI sans sits beside a much larger serif logotype — the eye takes the
     two x-height bands for the line rather than the baseline.
     MEASURE THE "o" AND NOTHING ELSE: every other glyph in either word carries
     an ascender, a descender or a dot, so only "o" gives a clean x-height band.
     Measured off the built page at 1440 in Chromium at 16x:
       "o" of Bidlo          32.625 .. 44.063   centre 38.344
       "o" of documentation  28.250 .. 37.188   centre 32.719  at top: 0
     which asks for 5.63px.
     BUT CHROMIUM SNAPS PAINTED TEXT TO WHOLE CSS PIXELS, so the only offsets
     that exist here are 5, 6 and 7 — landing that centre at 37.719, 38.719 and
     39.719 against the wordmark's 38.344. 6 is the nearest, 0.34px low, and 7
     is the baseline match that was reported as too low. So the value is 6px
     written exactly (0.375rem); anything from ~0.34 to ~0.40rem renders
     identically and NUDGING BY HUNDREDTHS DOES NOTHING.
     It is a paint offset, not a layout one, so the logo's box does not move —
     which is what keeps this free against the capture. RE-SOLVE rather than
     nudging if the lockup's 80px width or this label's font-size moves; both
     ends of that subtraction depend on them. */
  position: relative;
  top: 0.375rem;
  /* A label, not a destination. */
  pointer-events: none;
  user-select: none;
}

#site-header .hdr-route::before {
  content: '/';
  color: var(--color-theme-text-tertiary);
  margin-right: 0.1em;
}

/* Home and 404: the brand IS the location, so there is nothing to add. */
#site-header .hdr-route:empty {
  display: none;
}

/* Below sm the header is the logo, one CTA and the hamburger, and a
   thirteen-character label is what pushes that row into wrapping. The mobile
   sheet names the page well enough on its own. */
@media (max-width: 639px) {
  #site-header .hdr-route {
    display: none;
  }
}
