/* ══════════════════════════════════════════════════════════════════════════
   TOOL PORTAL — the drawer/rail. Loaded only when TOOL_PORTAL_ENABLED is on.

   COLOUR: tokens only, no raw hex, no gradients. This is not merely guard
   compliance — it is what makes the three theme states work. `:root` (the
   un-stamped default, reachable with JS off because the theme is stamped by an
   inline head script), `html[data-theme="light"]` and `html[data-theme="dark"]`
   each redefine the same token names in style.css, so a stylesheet that names
   only tokens is correct in all three without a single theme selector of its
   own. There is exactly one theme-conditional rule in this file, and it is
   commented where it sits.

   GEOMETRY, AND WHERE IT CAME FROM. The reference measures a 250px rail with
   `padding: 20px 12px 70px`, a 226x24 group header over a 24x24 icon, and
   206x37 items at 14/400 with `padding: 8px 12px` and `radius: 6px`
   (research/zipments/pars-app-screen-spec.md §9-§10). The numbers below are
   those numbers snapped to this system's ramps:

     rail width        250      = 250      exact
     rail padding      24/12/24 vs 20/12/70 — 20 and 70 are off the spacing
                                ramp {0,4,8,12,16,24,32,48,60,80,120}; 70 was
                                also dead space that cut the last link off the
                                bottom of the rail on first paint
     header box        224x24   vs 226x24  — 2px narrower, because the rail's
                                own 2px right border comes out of the 250
     icon box          24x24    = 24x24    exact
     item box          206x37   = 206x37   exact
     item type         14/400   = 14/400   exact
     item colour       --tp-ink = #475467  exact in both light states
     item radius       6        = 6        exact, and on this system's radius
                                ramp {0,6,8,12,16,20,24,9999}
     item padding      8px 12px = 8px 12px exact
     chip ink          #182230  = --ink     same ROLE (the primary ink, a rung
                                darker than the rows), token value #020618

   Two deltas, both the ramp or the border width asserting itself over the
   reference, which is the correct precedence. Everything else is exact.

   `!important` appears in exactly one block in this file — the cascade
   override near the foot — and in eight declarations inside it, one per
   property that an ancestor sheet fights for. Nowhere else.
   ══════════════════════════════════════════════════════════════════════════ */

/* The rail's governing width, stated once. On :root rather than on `.qf-tp`
   because the desktop content offset below is a SIBLING of the rail, not a
   descendant, and must resolve the same number. */
:root { --tp-w: 250px; }

/* THE PORTAL'S OWN INKS — one indirection, and it is load-bearing.
   The rail reads `--tp-ink` / `--tp-surface` / `--tp-line`, never the shared
   tokens directly, because the shared ones get re-pointed out from under it.
   `landing-wefixtrades-cleanup.css` sets `--qf-wft-muted: var(--ink-soft)
   !important` on an ANCESTOR, and a custom property resolved on an ancestor is
   simply the value the descendant inherits — no amount of `!important` on the
   descendant's `color` can undo it, because there is nothing to out-rank.
   Measured: the homepage rendered its rail at #314158 while every other page
   rendered #475467.

   So the default is the standard token, which is correct in both STAMPED
   themes, and the only override is the un-stamped state below. */
#qf-tool-portal {
  --tp-surface: var(--surface);
  --tp-line: var(--border);
  --tp-ink: var(--muted);
  /* The chip's own ink. The reference measures #182230 on its chip — its
     PRIMARY ink, deliberately darker than the #475467 its rows use — so the
     chip is the one place in the rail that steps up the ramp. `--ink` is this
     system's equivalent rung (#020618 light). The role matches exactly; the
     value is 14 steps darker because it is a token and not a new hex. */
  --tp-ink-strong: var(--ink);
}

/* UN-STAMPED — no `data-theme` at all, which is what a visitor with scripting
   off gets, because the theme is stamped by an inline head script. On this one
   page family `body.qf-public-wft` paints a dark canvas while every token in
   style.css still reads LIGHT, so the rail has to take its values from the
   `--qf-wft-*` namespace, which is dark by default. Scoped to exactly the
   condition that causes it: if that page bug is ever fixed at source, this
   block stops matching on its own. */
:root:not([data-theme]) body.qf-public-wft #qf-tool-portal {
  --tp-surface: var(--qf-wft-panel);
  --tp-line: var(--qf-wft-line);
  --tp-ink: var(--qf-wft-muted);
  --tp-ink-strong: var(--qf-wft-text);
}

.qf-tp {
  --tp-pad-x: var(--space-1-5);      /* 12 — the reference's own side padding */

  position: fixed;
  /* THE FALLBACK IS PER-BREAKPOINT BECAUSE IT HAS TO SURVIVE WITH NO SCRIPT.
     `--qf-menu-top` is written by script from the real bar; with scripting off
     it is never set and only this fallback stands. One value cannot serve both
     bars — measured 64px at mobile, 83px at desktop — and the old single 70px
     put the rail 13px behind the desktop header. The desktop value is set in
     the desktop block at the foot of this file. */
  top: var(--qf-menu-top, 64px);
  left: 0;
  bottom: 0;
  width: var(--tp-w);
  box-sizing: border-box;
  padding: var(--space-3) var(--tp-pad-x) var(--space-3);
  background: var(--tp-surface);
  /* The reference separates rail from page with a shadow alone — but its page
     ground is #F2F4F7 against a white rail. Ours is #F8FAFC, and the measured
     rail-to-page contrast was 1.05:1: no perceptible edge. A 2px hairline (the
     house border width) restores it without adding a colour. */
  border-right: 2px solid var(--tp-line);
  box-shadow: var(--shadow-lg);
  z-index: 2147483003;               /* under the pinned header, over the page */
  overflow: hidden;                  /* the inner column scrolls, not the shell */
}

/* THE SCROLLER. The panel is the fixed shell; this is the only thing that
   scrolls, so the rail's edges stay put while a long catalogue moves. */
.qf-tp__scroll {
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;      /* a rail at its end must not scroll the page */
  -webkit-overflow-scrolling: touch;
}

/* ── GROUP ───────────────────────────────────────────────────────────────── */

.qf-tp__group + .qf-tp__group { margin-top: var(--space-2); }

/* THE HEADER — 226x24, matching the reference exactly at desktop.
   `min-height` rather than `height`: a label that wraps at 320px must be
   allowed to grow instead of overflowing its own button. */
.qf-tp__head {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  width: 100%;                       /* = 226px inside the padded rail */
  min-height: var(--tap-min);        /* 24 — the reference's measured height */
  padding: 0;
  margin: 0;
  background: none;
  border: 0;
  border-radius: 6px;
  color: var(--tp-ink);
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 500;                  /* measured 500, not 600 */
  line-height: 1.5;
  text-align: left;                  /* headings left-aligned — never centred */
  cursor: pointer;
}

.qf-tp__ico {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  display: block;
  color: var(--accent);              /* where the reference puts its icon blue */
}
.qf-tp__ico svg { width: 24px; height: 24px; display: block; }

.qf-tp__label { flex: 1 1 auto; min-width: 0; }

.qf-tp__chev {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  color: var(--muted);
  transition: transform var(--qf-dur, 200ms) var(--qf-ease, cubic-bezier(.2, .7, .2, 1));
}
/* Expanded points UP, which is the state the reference ships and screenshots. */
.qf-tp__head[aria-expanded="true"] .qf-tp__chev { transform: rotate(180deg); }

.qf-tp__head:hover { color: var(--accent); }
.qf-tp__head:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ── FOLD ────────────────────────────────────────────────────────────────── */

/* `grid-template-rows: 1fr -> 0fr` resolves to the content's INTRINSIC height,
   so every group animates to exactly its own size. No max-height guess to
   out-grow, and a two-item group does not coast through empty space. */
.qf-tp__fold {
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  transition: grid-template-rows var(--qf-dur, 200ms) var(--qf-ease, cubic-bezier(.2, .7, .2, 1));
}
/* `minmax(0, 0fr)`, not `0fr`. An `fr` track is `minmax(auto, <fr>)`, and that
   `auto` minimum is the content's min-content size — which includes the inner
   box's padding. That is why a collapsed group measured 4px tall however the
   4px gap was expressed. An explicit 0 minimum is the only true zero. */
.qf-tp__fold[data-state="closed"] { grid-template-rows: minmax(0, 0fr); }
.qf-tp__fold > .qf-tp__fold-in {
  min-height: 0;
  overflow: hidden;
  visibility: visible;
  transition: visibility 0s;
}
/* A COLLAPSED GROUP MUST NOT BE TABBABLE. The track really does reach 0px, but
   a zero-height box with `overflow:hidden` still contains focusable links, so
   a keyboard visitor tabbed into a group they had just closed and followed
   links they could not see. `visibility: hidden` is the property that takes an
   element out of the tab order; it is delayed by the fold's own duration so it
   lands at the END of the close and never clips the animation. */
.qf-tp__fold[data-state="closed"] > .qf-tp__fold-in {
  visibility: hidden;
  transition: visibility 0s linear var(--qf-dur, 200ms);
}

/* THE LEFT HAIRLINE — a real border, running the height of the group, with the
   items indented off it. The reference draws 1px; this system's borders are
   2px, so it is 2px and the items are 208 rather than 206. */
.qf-tp__fold-in {
  border-left: 2px solid var(--tp-line);
  padding-left: var(--space-2);
  padding-top: var(--space-0-5);
}

/* ── ITEM ────────────────────────────────────────────────────────────────── */

.qf-tp__item {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-1-5);   /* 8px 12px — the reference exactly */
  border-radius: 6px;                          /* the reference exactly */
  color: var(--tp-ink);
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 400;
  line-height: 1.5;                            /* 21px -> a 37px box */
  text-decoration: none;
  transition:
    background-color var(--qf-dur-fast, 140ms) var(--qf-ease, cubic-bezier(.2, .7, .2, 1)),
    color var(--qf-dur-fast, 140ms) var(--qf-ease, cubic-bezier(.2, .7, .2, 1));
}

.qf-tp__item-t { flex: 1 1 auto; min-width: 0; }

.qf-tp__item:hover {
  background: var(--accent-soft);
  color: var(--accent);
}
.qf-tp__item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
/* CURRENT PAGE = OUTLINE, NOT A BRIGHT FILL. A filled accent row would read as
   the one button on the surface and pull the eye off the tool the visitor came
   for; the accent-soft ground plus accent ink says "you are here" without
   shouting it. */
.qf-tp__item[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}

/* ── COMING SOON ───────────────────────────────────────────── */

/* A PLANNED TOOL IS A <span>, NOT A DISABLED LINK, so there is no hover state
   and no focus state to write here — not because they are suppressed, but
   because a span with no href has neither. That is the point: the element
   choice does the work, and this block only has to make it LOOK as unavailable
   as it is. See `itemHtml` in toolPortal.ts.

   THE STATE IS CARRIED IN A WORD, not only in ink. The row keeps the muted
   `--tp-ink` of every other row but gains a "Soon" chip that is real text in
   the accessible name, so the distinction survives both a screen reader and a
   visitor who cannot resolve the contrast step. `cursor: default` stops the
   row claiming to be clickable under the pointer. */
.qf-tp__item--soon {
  cursor: default;
  /* THE LABEL KEEPS THE ROW INK. `--muted-soft` was tried here, to step the
     planned tools back a rung from the built ones, and measured 2.63:1 against
     the rail's surface in both light states - a contrast failure on body text,
     not a subtlety. There is no token between it and `--muted` that passes, so
     the de-emphasis is dropped rather than the legibility.

     Nothing is lost that mattered: the state was never supposed to be carried
     by ink in the first place. The chip says "Soon" in a word, the row is a
     <span> with no pointer affordance, and both of those work for a visitor
     who cannot resolve a one-rung grey step at all. */
  color: var(--muted);
}

/* ── CHIP ────────────────────────────────────────────────────────────────── */

/* The reference outlines its chip with a GRADIENT. This system ships zero CSS
   gradients, so the chip is a flat 2px accent outline on a pill radius — the
   same idiom every other pill on the site uses. */
.qf-tp__chip {
  flex: 0 0 auto;
  padding: 0 var(--space-1);
  border: 2px solid var(--accent);
  border-radius: var(--radius-pill);
  /* THE CHIP'S TEXT IS INK, NOT ACCENT — which is also what the reference
     does (measured #182230, its near-black, against its #3356EE outline).
     It is the safer choice as well: `--accent` is re-pointed by ancestor
     sheets on some pages (landing-wefixtrades-cleanup.css forces #3356EE with
     `!important` even in dark), and measured 3.07:1 on the homepage's dark
     rail. The outline keeps the accent — a border is a non-text element and
     answers to 3:1, which it clears — while the word itself rides the same
     ink as every other row and cannot fall below it. */
  color: var(--tp-ink-strong);
  font-size: 12px;
  font-weight: 600;
  /* 17px + the 2px outline top and bottom = a 21px box, which is exactly the
     item's own line box. Measured before: a 24px chip forced its row to 40px
     while every other row was 37 — a 3px step on the rail's very first row. */
  line-height: 17px;
  white-space: nowrap;               /* a chip must never wrap off its own row */
}

/* The "Soon" variant drops the accent outline. An accent border is this
   system's way of saying "look here", and the one thing a not-yet-built tool
   must not do is out-rank the built ones beside it. Neutral outline, muted
   ink — still a chip, still the same 21px box, just not an invitation. */
.qf-tp__chip--soon {
  border-color: var(--border-strong);
  color: var(--muted);
}

/* ── LOOSE LINKS ─────────────────────────────────────────────────────────── */

/* Same row as an item, but sitting at the rail's own left edge rather than
   behind a group's hairline — which is exactly what "loose" means here. */
.qf-tp__loose {
  display: block;
  margin-top: var(--space-3);
  /* 12 (rail) + 32 = 44, the same left edge as a group label, so the rail has
     one text column and not three. */
  padding: var(--space-1) var(--space-1-5) var(--space-1) var(--space-4);
  border-radius: 6px;
  color: var(--tp-ink);
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 500;                  /* measured 500 on the reference's flat rows */
  line-height: 1.5;
  text-decoration: none;
  transition: background-color var(--qf-dur-fast, 140ms) var(--qf-ease, cubic-bezier(.2, .7, .2, 1));
}
.qf-tp__loose + .qf-tp__loose { margin-top: var(--space-0-5); }
.qf-tp__loose:hover { background: var(--accent-soft); color: var(--accent); }
.qf-tp__loose:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* A loose link is a destination like any other, so it gets the same "you are
   here" treatment — and /pricing IS one of them, which is the row a visitor on
   the pricing page is most likely to look for. */
.qf-tp__loose[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}

/* ══════════════════════════════════════════════════════════════════════════
   THE CASCADE OVERRIDE — addressed by ID, and the one place `!important` is
   used in this file.

   MEASURED DEFECT: the portal's links rendered `#314158`, not the `#475467`
   the reference measures, because public-pages-wefixtrades.css ships
   `html[data-theme="light"] body.qf-public-wft a { color: var(--ink-soft)
   !important }` — and `!important` is not something a class selector can
   outrank, however specific. Several sheets carry a rule like it; the drawer
   sits inside `<header class="site-header">`, so every one of them applies to
   its links.

   This is the same problem nav-unify.css documents for `#site-mobile-menu`,
   and it takes the same answer: address the panel by ID and declare the
   properties that are being fought over. Scoped to this one panel, listing
   only `color`, so it cannot leak into anything else on the page.
   ══════════════════════════════════════════════════════════════════════════ */

/* THE RAIL FOLLOWS THE PAGE'S OWN SKIN, AND THAT IS WHAT MAKES THE THIRD
   THEME STATE CORRECT.

   MEASURED DEFECT: with JavaScript off — the UN-STAMPED state, because the
   theme is stamped by an inline head script — `body.qf-public-wft` paints
   `#0C111D` while every token in style.css still reads LIGHT. The rail came
   out #FFFFFF welded onto a near-black page, 18.9:1 the wrong way. (The same
   bug makes the header wordmark invisible there; it is pre-existing, site-wide
   and NOT introduced by this drawer.)

   The answer is not to re-derive a palette here. `public-pages-wefixtrades.css`
   already ships a `--qf-wft-*` namespace that is dark by default and re-pointed
   to the light tokens under `html[data-theme="light"]` — so it is correct in
   all THREE states by construction, which `--surface` is not. The rail reads
   those, and falls back to the standard tokens on pages that do not carry the
   skin. `--accent` is deliberately NOT overridden: that sheet already resolves
   it per state (#8DA2F9 on dark, #3356EE on light), and an earlier blanket
   override here pinned it to the light value on a dark ground.
   ══════════════════════════════════════════════════════════════════════════ */

#qf-tool-portal .qf-tp__item { color: var(--tp-ink) !important; }
#qf-tool-portal .qf-tp__loose { color: var(--tp-ink) !important; }
#qf-tool-portal .qf-tp__head { color: var(--tp-ink) !important; }
#qf-tool-portal .qf-tp__ico { color: var(--accent) !important; }
#qf-tool-portal .qf-tp__chip { color: var(--tp-ink-strong) !important; }
/* The coming-soon pair, for the same ancestor-sheet reason as every other line
   in this block: `landing-wefixtrades-cleanup.css` re-points the shared inks on
   an ancestor, and a custom property resolved on an ancestor is simply what the
   descendant inherits. Without these two the muted rows rendered at the live
   rows' ink on the skinned pages and the distinction vanished. */
#qf-tool-portal .qf-tp__item--soon { color: var(--tp-ink) !important; }
#qf-tool-portal .qf-tp__chip--soon { color: var(--muted) !important; }
#qf-tool-portal .qf-tp__item:hover,
#qf-tool-portal .qf-tp__loose:hover { color: var(--accent) !important; }
#qf-tool-portal .qf-tp__item[aria-current="page"],
#qf-tool-portal .qf-tp__loose[aria-current="page"] { color: var(--accent) !important; }
#qf-tool-portal .qf-tp__head:hover { color: var(--accent) !important; }

/* ══════════════════════════════════════════════════════════════════════════
   MOBILE — off canvas, sliding in from x = -250.
   ══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1023px) {
  /* THE PANEL IS PRESENT BUT OFF-SCREEN, which is why `hidden` is undone here
     and the position is a transform. Undoing `hidden` with a class selector
     works because `.qf-tp[hidden]` (0,2,0) outranks the UA's `[hidden]`. The
     controller still adds and removes the attribute; the transform is what the
     eye sees, and it is the property that animates on the compositor. */
  .qf-tp[hidden] {
    display: block;
    transform: translateX(-100%);
    visibility: hidden;              /* off-screen AND out of the tab order */
  }
  .qf-tp {
    transform: translateX(-100%);
    visibility: hidden;
    transition:
      transform var(--qf-dur, 200ms) var(--qf-ease, cubic-bezier(.2, .7, .2, 1)),
      visibility 0s linear var(--qf-dur, 200ms);
  }
  .qf-tp[data-state="open"] {
    transform: translateX(0);
    visibility: visible;
    transition:
      transform var(--qf-dur, 200ms) var(--qf-ease, cubic-bezier(.2, .7, .2, 1)),
      visibility 0s;
  }

  /* A 24px row is the reference's measurement, not a good tap target. On the
     viewport where it is actually tapped it gets the full 44. */
  .qf-tp__head { min-height: var(--tap-target); }

  /* The reference steps the rail's top padding 20 -> 16 on a phone. */
  .qf-tp { padding-top: var(--space-2); }
}

/* ══════════════════════════════════════════════════════════════════════════
   DESKTOP — the permanent rail, and the offset it forces on the page.
   ══════════════════════════════════════════════════════════════════════════ */

@media (min-width: 1024px) {
  /* Permanent: always laid out, never transformed, `hidden` overridden. The
     burger is already `display:none` above this breakpoint in nav-unify.css,
     so nothing is left pointing at a panel that no longer opens or closes. */
  .qf-tp,
  .qf-tp[hidden] {
    display: block;
    transform: none;
    visibility: visible;
    top: var(--qf-menu-top, 83px);   /* the measured desktop bar — see above */
  }

  /* THE CONTENT OFFSET — ON THE PAGE, NOT ON A LIST OF ITS PARTS.
     This rule enumerated `main`, then `main, section, footer`, and both times
     something was left behind it: first the footer and the hero, then
     `<nav class="qtt-crumbs">` on every hub page. Enumerating siblings IS the
     bug — the list is only ever as right as the last page somebody looked at,
     and a new top-level element is un-offset by default, silently, under an
     opaque rail.

     So the padding goes on `body`, which contains every top-level element
     there will ever be, and the ONE element that must stay full-width pulls
     itself back out. Position:fixed children (the chat FAB, the scrim, the
     rail itself) resolve against the viewport and are untouched by body
     padding, which is exactly what they need. Nothing has to be enumerated
     again, and a new sibling is offset the day it is added. */
  body:has(#qf-tool-portal) { padding-left: var(--tp-w); }

  /* The header spans the full width ABOVE the rail, as the reference has it,
     so it is the one thing that undoes the page padding. It is `position:
     sticky` — in-flow, therefore shifted by that padding — so a negative
     margin plus a width that re-adds the same token puts it back at x=0
     without either number being written down twice. */
  body:has(#qf-tool-portal) > .site-header {
    margin-left: calc(-1 * var(--tp-w));
    width: calc(100% + var(--tp-w));
  }
  /* `100%` AND NOT `100vw`, AND THE DIFFERENCE IS THE SCROLLBAR. A percentage
     resolves against the containing block, which already excludes a classic
     space-taking scrollbar; `100vw` includes it and would push the header
     wider than the page by the scrollbar's width, producing horizontal scroll
     on exactly the platforms that still have one.

     VERIFICATION GAP, RECORDED RATHER THAN PAPERED OVER: headless Chromium
     reports a 0px scrollbar (overlay scrollbars) and forcing a classic one did
     not take, so this is verified BY CONSTRUCTION and by asserting the
     header's used width equals the body's client width — never against a real
     space-taking scrollbar in a browser that has one. The derivation is sound
     and the risk is low, but nobody has watched it. */
}

/* ══════════════════════════════════════════════════════════════════════════
   REDUCED MOTION — the state changes, the movement does not.
   ══════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  .qf-tp,
  .qf-tp[data-state="open"],
  .qf-tp__fold,
  .qf-tp__chev,
  .qf-tp__item,
  .qf-tp__loose { transition: none; }
}
