/* portal.css - Portal v2 shared component styles
   All monetary figures use tabular-nums for clean column alignment.
   No em dashes. Sentence case labels throughout.

   Two token families live in this file (2026-07-13 visual-design-system build):
   - --pv2-primary / --pv2-font-body / --pv2-font-disp: CLIENT-DERIVED (set per client by
     theme.sty-js's applyTheme()). Used ONLY by the "Customer quote" preview panel this page embeds
     (estimate-render.css) - that preview deliberately shows the real client brand, since it is a
     preview of what the customer sees. Nothing else in the portal should reference these.
   - --portal-*: WAS "PORTAL-ONLY, fixed values, the same for every client" (Notion
     39ca396f-af42-817b8fc1da6515d50494). NO LONGER TRUE and left here because the change is
     worth seeing: as of 2026-08-07 applyClientColourOverrides() sets --portal-accent and
     --portal-accent-dk from the client's primary_color and brand_color, so the portal is
     themed per client. The values declared below are only what shows before the clients row
     loads. That note also said "when real Bottleneck branding is ready, this block is the one
     place to change" - it is ready, and the defaults are Bottleneck tokens now
     (../brand-tokens.css) rather than Oceanside Alloy's hexes. */

/* portal-v2.html does not load index.css, so the tokens are imported here too. */
@import url("../brand-tokens.css");

:root {
  --pv2-primary:     var(--client-primary, var(--bn-neutral));
  /* --client-primary-dk WAS NEVER SET BY ANYTHING (found 2026-08-08 while sweeping the
     literals). This fallback therefore always fired, so the embedded "Customer quote"
     preview - the one panel in the portal that deliberately shows the real client brand -
     showed Oceanside Alloy's dark teal to every client. applyClientColourOverrides() now
     sets it from brand_color alongside --portal-accent-dk. */
  --pv2-primary-dk:  var(--client-primary-dk, var(--bn-neutral-dk));
  --pv2-font-body:   var(--font-body, 'Jost', 'Barlow', sans-serif);
  --pv2-font-disp:   var(--font-display, 'Cormorant Garamond', serif);

  /* --portal-accent is CLIENT-DERIVED as of 2026-08-07: applyClientColourOverrides() sets it
     from primary_color, and the portal is themed per client. The block comment above still
     describing it as fixed is now wrong on that point; these are only the pre-load defaults.

     They used to BE Oceanside Alloy's primary and brand colour, written as literals, which is
     what the note above meant by "when real Bottleneck branding is ready, this block is the one
     place to change". It is ready, and this is that change. */
  --portal-font:       -apple-system, 'Inter', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --portal-accent:     var(--bn-neutral);
  --portal-accent-dk:  var(--bn-neutral-dk);

  /* Portal neutrals. --pv2-bg was Oceanside Alloy's surface_color; the portal canvas is our
     chrome, so it takes Bottleneck's cream. */
  --pv2-bg:          var(--bn-cream);
  --pv2-surface:     #ffffff;
  --pv2-border:      #dde3e4;
  --pv2-border-lt:   #ecf0f1;
  --pv2-dark:        #1a1a1a;
  --pv2-mid:         #444e50;
  /* --pv2-muted IS RETIRED AS A TEXT COLOUR (R2, 2026-07-26). Measured 2.79:1 on #ffffff and 2.61:1
     on the #f7f4ef canvas, against a 4.5:1 floor. It was carrying 75 `color:` declarations - every
     help line, every meta line, every "not set" placeholder in the portal - so the smallest text on
     the page was also the hardest to read. It survives for what it is actually fit for: hairlines,
     dot fills and disabled icon tints, where no contrast floor applies. */
  --pv2-muted:       #8a9e9f;
  /* THE muted TEXT colour, and the value is what MEASURING gave rather than what arithmetic promised.
     #5b6e70 was chosen first because it clears 5.37:1 on white - and then the live check found it at
     4.48:1 on the neutral chip fill, failing by two hundredths on exactly the smallest text in the
     portal. #586a6c clears 4.5:1 on ALL SEVEN grounds this portal actually renders text on (white,
     canvas, sunken, the neutral chip, and the blue/amber/violet semantic fills), with the worst case
     4.74:1. That gap is the whole reason scripts/portal-contrast-check.cjs resolves the real ancestor
     background instead of assuming white. */
  --pv2-ink-3:       #586a6c;
  --pv2-red:         #c0392b;

  /* Portal semantic set: one hex pair (text/pale-bg) + a border tint per meaning, replacing 15+ ad
     hoc one-off colours previously scattered across cost-tree-editor.js / cost-tree-diagram.js. */
  --portal-blue:        #2d6a9f; --portal-blue-bg:    #e4f0fb; --portal-blue-bd:    #b0cce8; /* shared / wider scope */
  /* ADVISORY, DARKENED (R2). #a8743a measured 3.98:1 on white and failed the floor for 17px text; the
     pale backgrounds and borders are unchanged because #8f5f26 is the same family, four steps down.
     Now 5.51:1 on white, 5.02:1 on canvas, 5.15:1 on its own --portal-amber-bg. */
  --portal-amber:       #8f5f26; --portal-amber-bg:   #f6ead9; --portal-amber-bd:   #e6cfa0; /* advisory, non-blocking */
  --portal-green:       #2d6a4f; --portal-green-bg:   #e5f2ea; --portal-green-bd:   #b5d8c4; /* confirmed / free / min */
  /* DRAFT, the one new hue (R2, Tim's confirmation). Draft is a MODE, not a warning: the bar used
     amber, which made "unpublished" read as "something is wrong" on a product that is simply being
     worked on. Violet sits far from both the teal accent and the amber advisory, and it is always
     accompanied by the word Draft, so the hue carries no information on its own. 6.92:1 on white. */
  --portal-violet:      #5b4b8a; --portal-violet-bg:  #efecf7; --portal-violet-bd:  #cdc5e6; /* draft, not live */
  --portal-error:       #c0392b; --portal-error-bg:   #f7e6e3; --portal-error-bd:   #e0b3ab; /* problem, blocking */
  --portal-neutral-bg:  #eeeae2; /* neutral data chip, on --pv2-ink-3 text */
  /* --portal-mustard IS MERGED AWAY (R2). #8a5a00 sat one step from the amber above and carried an
     unrelated meaning, which is the layered-on-top problem in a single pair of tokens: two browns a
     reader cannot tell apart, one meaning "look at this" and one meaning "max" or "instanced". Max and
     instanced are not STATES, they are data labels, so their chips are neutral now. Do not add a
     second brown back; if a new meaning needs a colour, it needs a hue nobody else is using. */

  /* ── THE type scale (R1, 2026-07-26). Seven steps. This is the ONLY set. ──────────────────────
     Replaces --portal-size-* (six roles, all px, plus a second copy of four of them re-declared at
     different values inside .cte2-md-detail). The old names are GONE rather than aliased: two names
     for one step is the duplication this redesign exists to remove, and an alias is how the second
     set comes back.

     BASE MOVES 14px -> 17px, Tim's decision. Measured before the change: 31 distinct rendered sizes
     between 9px and 27.2px, 6% of them on any scale at all. The row label a fabricator actually reads
     gains 21%, help text 25%, chips 30%, the page title 60%.

     EVERY STEP IS A rem MULTIPLE OF ONE BASE, and the root font-size is never overridden, for two
     reasons. Browser and OS text scaling keeps working (a phone in a workshop with accessibility text
     turned up gets bigger type, not a broken layout). And if 17 still reads small on Tim's own phone,
     the next stop is 18px: change --t-body and re-derive, ONE token, not a re-cut of the ratio. That
     is why these are written as rem literals with the px in a comment rather than as calc() off a
     variable - a calc chain reads as adjustable but silently changes seven things at once.

     NO WEBFONT, on purpose: this loads on workshop wifi, so the scale is chosen to hold up in Segoe UI
     and Roboto rather than to depend on Inter being present. */
  --t-micro:  0.8125rem;  /* 13 - state chips, data chips, column headers, timestamps. THE FLOOR. */
  --t-small:  0.9375rem;  /* 15 - help text, meta lines, unit suffixes, dense table body */
  --t-body:   1.0625rem;  /* 17 - BASE. Row labels, values, inputs, buttons, nav rows, options */
  --t-lead:   1.25rem;    /* 20 - block heading inside the detail pane; an item's headline rate */
  --t-title:  1.5625rem;  /* 25 - the name of the thing being edited; dialog title */
  --t-page:   2rem;       /* 32 - product name, page title. One per screen. */
  --t-figure: 2.5rem;     /* 40 - the single big number: a sell price, a price-impact delta */

  /* Line heights, paired with the steps above rather than set per selector. */
  --lh-tight: 1.2; --lh-snug: 1.35; --lh-body: 1.5; --lh-prose: 1.55;

  /* Measure. Prose never exceeds 68 characters, help text never 60. Before R1 the widest run on the
     portal was 185ch, which is unreadable at any size and gets worse as the type grows. */
  --measure-prose: 68ch;
  --measure-help:  60ch;
}

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

body { font-family: var(--portal-font); }

/* ── R1 base layer (2026-07-26) ──────────────────────────────────────────────────────────────────
   Three rules that belong to the scale rather than to any component, so no component has to remember
   them. Deliberately the ONLY place any of the three is declared. */

/* 1. CONTROLS TAKE THE BASE. A form control does not inherit font by default - every browser
   substitutes its own, which is why <option> measured 13.33px on this portal while everything around
   it was on the scale, and why buttons drifted between 13.6 and 14.08. Setting the family and size
   here means a control is on the scale by existing, not by being styled. */
input, select, textarea, button, option {
  font-family: inherit;
  font-size: var(--t-body);
}
/* A <select> sizes itself to its LONGEST option, so at 17px the interpolation-source picker - whose
   options carry a full driver name and its dimensions - pushed its row 570px past the pane and forced a
   horizontal scrollbar. Capping it here rather than at that one picker: any select with long options
   has the same problem, and the next one added would have hit it again. */
select { max-width: 100%; }

/* 2. TABULAR NUMERALS wherever digits line up in a column: money, dimensions, counts. Proportional
   digits make a column of prices ragged, which is the one thing a price list must not be. Applied by
   ROLE (number inputs, table cells, the money/dimension/count classes) rather than sprinkled per
   component - the previous 17 scattered declarations missed the job table's dimension column and the
   calculations table entirely. */
input[type="number"],
td, th,
.pv2-value, .pv2-price, .pv2-total, .pv2-rollup-total, .pv2-tbl-total, .pv2-computed-value,
.cte2-jt-cost, .cte2-jt-dim, .cte2-ct-value, .sty-cte2-flat-input, .cte2-num4, .sty-cte2-qrule-input,
.cte2-spc-amount, .sty-cte2-upoa-input {
  font-variant-numeric: tabular-nums;
}

/* 3. MEASURE. Prose stops at 68 characters, help text at 60. Measured before R1 the widest run on the
   portal was 185ch; bigger type makes a long line worse, not better, so this lands with the scale
   rather than after it. Applied to the prose ROLES, not to every <p>: a table cell or a chip that
   happens to be a paragraph must keep filling its column. */
.pv2-welcome, .pv2-help-intro p, .pv2-help-body p,
.pv2-lib-cat-intro, .pv2-section-desc, .pv2-draft-note,
/* .cte2-desc-line LEFT THIS LIST ON 2026-09-14 (THE QUEUE, "A description box fills half the width
   of its block"). TIM, twice: "Descriptions only take up half the width of the box" and then, after
   the 2026-09-12 fix lifted the cap for the OPEN editor only, "Description boxes on fields are still
   half width. Needs to fill whole box."
   THIS LIST'S OWN RULE IS WHY IT DOES NOT BELONG: "Applied to the prose ROLES, not to every <p>: a
   table cell or a chip that happens to be a paragraph must keep filling its column." A description
   line on an editor pane is a CONTROL - it is the click target that opens the editor, and it is the
   same box in both states - so it is that exception rather than a paragraph. MEASURED at 1700px: it
   drew 558px inside a 904px block, 62 percent, while every other block on the field and section
   panes filled its block. */
.cte2-nav-hint, .cte2-calc-ref-cat-intro, .cte2-calc-ref-empty,
/* .pv2-field-hint is THE explanatory-paragraph class of the shell: the powder-fee explanation, the
   Library intro, the driver-shape intro. It was rendering at 184ch. Found by measuring, not by
   reading - the audit reports every run over 60ch with its width, which is why it is a script. */
.pv2-field-hint {
  max-width: var(--measure-prose);
}
.cte2-section-sub, .cte2-form-hint, .cte2-stmt--muted, .cte2-dim-conv, .cte2-opt-hint,
.cte2-disclosure p, .pv2-lib-addpart-hint, .pi-sub, .pv2-setting-hint,
.cte2-price-row-note, .pv2-product-sub {
  max-width: var(--measure-help);
}
/* A textarea holding customer-facing prose is prose: the product description was 155ch wide, which is
   as hard to write in as it is to read. The lead-time input beside it stays short by nature. */
/* Narrower AND taller. Capping the measure without giving the box back its lines just moved the
   clipping from the right edge to the bottom one: the same three-line description became five lines
   inside a rows="3" box. A prose textarea is sized in lines, not pixels, so it gets a min-height in em
   that follows the type instead of a fixed height that does not. */
/* 2026-08-06: the 68ch cap is REMOVED for these two. The reasoning above is still right about reading
   prose, but it was applied when the editor body was wide; in the current pane 68ch lands at roughly a
   third of the available width, so the longest text on the page was being written through the smallest
   box on it, with a scrollbar. A full-width box that follows the pane is the lesser of the two evils,
   and the min-height in em stays because that half of the fix was correct. */
textarea.desc-input, textarea.pv2-lib-desc-input {
  width: 100%;
  min-height: 7.5em;
}

/* ── Page header ─────────────────────────────────────────────────────────── */

/* ── THE PORTAL HEADER ────────────────────────────────────────────────────────────────────────
   ONE component, five views (Products, Library, Quote requests, Settings, Help).

   EVERY COLOUR HERE IS DERIVED FROM THE CLIENT'S OWN ACCENT. Not one second hue is introduced,
   because a header that looks designed in Oceanside Alloy's teal and muddy in MORPHD's tan is not
   a design, it is a design for one client. The depth comes from translucent black and white laid
   over whatever --portal-accent happens to be, so the same rules read as intended on a dark teal,
   a mid tan and a near-black alike.

   The gradient is a two-stop wash from transparent to 14% black. On a light accent it reads as a
   soft weight at the bottom edge; on a dark one it is barely there. Either way the nav row sits on
   slightly deeper ground than the mark above it, which is what gives the two rows their hierarchy
   without a border drawn across the middle. */
.pv2-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, rgba(0, 0, 0, 0) 38%, rgba(0, 0, 0, 0.14) 100%),
    var(--portal-accent);
  /* A hairline of the client's own darker companion, then a soft drop. The page scrolls under a
     sticky header, so it needs an edge; a shadow alone smudges, a border alone looks stuck on. */
  border-bottom: 1px solid var(--portal-accent-dk);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10), 0 6px 18px -10px rgba(0, 0, 0, 0.35);
}

/* CHROME IS FULL WIDTH (2026-08-09, brief Task 20). The header is chrome, not a document: capping
   it at 1200px on a 2500px screen left the logo floating in the middle of nothing while the colour
   band ran edge to edge behind it. A gutter, not a column. */
.pv2-header-inner {
  margin: 0;
  padding: 0 24px;
  min-height: 60px;
  display: flex;
  align-items: center;
  gap: 20px;
}
/* The account controls go to the far right; the nav sits next to the mark, where a nav belongs. */
.pv2-header-account { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.pv2-header-identity { flex: none; }

/* THE MARK IS THE HEADING. There is no name beside it any more - see the note at _portalHeader -
   so the mark carries the identity alone and is given room to. When the client has no logo this
   same slot renders their name as text, which is why the text form is sized like a wordmark
   rather than like a label: it is doing a logo's job. */
.pv2-client-mark--text {
  font-family: var(--portal-font);
  font-size: var(--t-lead);
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--client-ink, #fff);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pv2-header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}

.pv2-header-btn {
  font-family: var(--portal-font);
  font-size: var(--t-body);
  font-weight: 500;
  padding: 8px 14px;
  border-radius: 6px;
  border: 1px solid rgba(var(--client-ink-rgb, 255, 255, 255), 0.35);
  background: rgba(var(--client-ink-rgb, 255, 255, 255), 0.12);
  color: var(--client-ink, #fff);
  cursor: pointer;
  text-decoration: none;
  transition: background 0.15s;
  white-space: nowrap;
  line-height: 1;
}
.pv2-header-btn:hover { background: rgba(var(--client-ink-rgb, 255, 255, 255), 0.22); }
/* THE SAME BUTTON ON THE PAGE, NOT IN THE HEADER (2026-10-02, found by portal-contrast-check). .pv2-header-btn is drawn for the dark header: the client's ink
   (white) on a translucent white fill. On the white page body it is white on white, which is how the inbox's Load more read as 1:1 and was invisible.
   This modifier takes the page's own tokens, the way the embed Preview link already does with an inline style. */
.pv2-header-btn--on-page {
  color: var(--portal-accent);
  background: var(--pv2-surface, #fff);
  border-color: var(--pv2-border);
}
.pv2-header-btn--on-page:hover { background: var(--pv2-border-lt); }
.pv2-header-btn--logout { opacity: 0.75; }
.pv2-header-btn--logout:hover { opacity: 1; background: rgba(var(--client-ink-rgb, 255, 255, 255), 0.12); }

/* ── Page body ────────────────────────────────────────────────────────────── */

/* WIDTH HELPS TOOLS AND HURTS DOCUMENTS (2026-08-09, brief Task 20). A settings row stretched to
   2500px puts a label and its control a hand-span apart; a two-pane editor at 1200px cramps the
   pane you are working in. So the cap is per surface rather than one number for the portal. */
.sty-pv2-body {
  max-width: 1200px;
  margin: 0 auto;
  padding: 32px 24px 140px;
}
/* Products is a grid of tiles, which is a tool: it is a preview of the customer page and reordering
   means what it looks like it means. Full width, with the column count doing the responsive work. */
.sty-pv2-body--products { max-width: none; }
/* Settings gets its own, WIDER cap rather than the shared 1200px reading column - the brief asks for
   "max width, wider than now". The .pv2-set-wrap cap inside it is what actually stops a row growing
   past readable; this just stops the 1200px body clipping it first. */
.sty-pv2-body--settings { max-width: 1500px; }

/* Concept B (2026-07-13): the product EDIT view is a master-detail two-pane layout (tree navigator +
   node detail) that needs the page width the 1200px reading column wastes on empty side margins. The
   edit view shares this .sty-pv2-body with the Home landing (they toggle via display), so we widen ONLY
   when this modifier is toggled on - showEditView adds it, going Back removes it - keeping Home (and
   the separately-rendered Library) at the original 1200px. min() keeps a comfortable side gutter and a
   sane cap on ultrawide screens; below the two-pane's own 900px stack breakpoint it is just full width. */
/* The editor's own width wins over the products modifier when both classes are present - going
   into the editor adds one and removes the other, but order in the stylesheet decides if they ever
   overlap, and a two-pane tool must not inherit a grid's rule. */
.sty-pv2-body--edit-wide,
.pv2-body--library {
  /* Concept L-B (2026-07-13): the Library page also adopts the master-detail two-pane layout, so it
     uses the SAME width decision as the product editor instead of the old 920px inner cap. Library
     renders its own .pv2-body (not shared with Home), so this is applied statically on that page. */
  /* FULL WIDTH, not a wider column (2026-08-09, brief Task 20). The product editor is a two-pane
     tool and the right pane was cramped; min(1680px, 95vw) still threw away a third of a 2500px
     screen. 95vw keeps a gutter and nothing else. */
  max-width: 95vw;
}

.pv2-welcome {
  font-size: var(--t-body);
  color: var(--pv2-mid);
  margin-bottom: 28px;
  line-height: 1.6;
}

/* ── Footer (save bar) ────────────────────────────────────────────────────── */

.pv2-footer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: #fff;
  border-top: 1px solid var(--pv2-border);
  box-shadow: 0 -2px 10px rgba(0,0,0,0.07);
  z-index: 40;
}

.pv2-footer-inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: 14px 24px;
  display: flex;
  align-items: center;
  gap: 12px;
  justify-content: flex-end;
}

.pv2-footer-status {
  flex: 1;
  font-size: var(--t-body);
  color: var(--pv2-ink-3);
}

.pv2-btn {
  font-family: var(--portal-font);
  font-size: var(--t-body);
  font-weight: 500;
  padding: 10px 20px;
  border-radius: 7px;
  border: none;
  cursor: pointer;
  transition: background 0.15s, opacity 0.15s;
}
.pv2-btn:disabled { opacity: 0.4; cursor: default; }

.pv2-btn--save {
  background: var(--portal-accent);
  color: var(--client-ink, #fff);
}
.pv2-btn--save:not(:disabled):hover { background: var(--portal-accent-dk); }

.pv2-btn--discard {
  background: var(--pv2-bg);
  color: var(--pv2-mid);
  border: 1px solid var(--pv2-border);
}
.pv2-btn--discard:not(:disabled):hover { background: var(--pv2-border-lt); }

/* ── Login screen ─────────────────────────────────────────────────────────── */

.pv2-login-wrap {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.pv2-login-card {
  background: #fff;
  border: 1px solid var(--pv2-border);
  border-radius: 12px;
  padding: 40px 36px;
  width: 100%;
  max-width: 380px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.pv2-login-title {
  font-family: var(--portal-font);
  font-size: var(--t-title);
  font-weight: 600;
  color: var(--pv2-dark);
  margin: 0;
}

.pv2-login-label {
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--pv2-mid);
  margin-bottom: -8px;
}

.pv2-login-input {
  width: 100%;
  padding: 11px 14px;
  border: 1.5px solid var(--pv2-border);
  border-radius: 7px;
  font-family: var(--portal-font);
  font-size: var(--t-body);
  color: var(--pv2-dark);
  outline: none;
  transition: border-color 0.15s;
}
.pv2-login-input:focus { border-color: var(--portal-accent); }

.pv2-login-error {
  font-size: var(--t-small);
  color: var(--pv2-red);
  margin: -6px 0;
}

.pv2-login-btn {
  width: 100%;
  padding: 12px;
  background: var(--portal-accent);
  color: var(--client-ink, #fff);
  border: none;
  border-radius: 7px;
  font-family: var(--portal-font);
  font-size: var(--t-body);
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s;
  margin-top: 4px;
}
.pv2-login-btn:hover:not(:disabled) { background: var(--portal-accent-dk); }
.pv2-login-btn:disabled { opacity: 0.6; cursor: default; }

/* Real logins (2026-10-13): the email is optional, so the label says so quietly; Forgot is a link-weight button under Log in. */
.pv2-login-optional { font-weight: 400; color: var(--pv2-mid); opacity: 0.8; }
/* A message that is not an error (Forgot password sent): the same slot as the error, neutral ink. */
.pv2-login-error.pv2-login-note { color: var(--pv2-mid); }
.pv2-login-forgot {
  align-self: center;
  background: none;
  border: none;
  padding: 2px 4px;
  font-family: var(--portal-font);
  font-size: var(--t-small);
  color: var(--pv2-mid);
  text-decoration: underline;
  cursor: pointer;
}
.pv2-login-forgot:hover { color: var(--pv2-dark); }

/* ── Toast ────────────────────────────────────────────────────────────────── */

.pv2-toast {
  position: fixed;
  bottom: 80px;
  left: 50%;
  transform: translateX(-50%) translateY(12px);
  background: var(--pv2-dark);
  color: #fff;
  padding: 10px 20px;
  border-radius: 8px;
  font-size: var(--t-body);
  font-weight: 500;
  opacity: 0;
  transition: opacity 0.2s, transform 0.2s;
  pointer-events: none;
  white-space: nowrap;
  z-index: 100;
}
.pv2-toast--error { background: var(--pv2-red); }
.sty-pv2-toast--visible { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ── Loading / error states ──────────────────────────────────────────────── */

.pv2-loading {
  text-align: center;
  padding: 80px 24px;
  font-size: var(--t-body);
  color: var(--pv2-ink-3);
}
.pv2-loading--error { color: var(--pv2-red); }

.pv2-center-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 24px;
}
.pv2-center-msg {
  font-size: var(--t-body);
  color: var(--pv2-ink-3);
}

/* ── Product section wrapper ───────────────────────────────────────────────── */

.pv2-product-section {
  background: var(--pv2-surface);
  border: 1px solid var(--pv2-border);
  border-radius: 10px;
  padding: 24px 28px;
  margin-bottom: 20px;
}

.pv2-product-name {
  font-family: var(--portal-font);
  font-size: var(--t-page);
  font-weight: 600;
  color: var(--pv2-dark);
  margin-bottom: 4px;
}

.pv2-product-sub {
  font-size: var(--t-body);
  color: var(--pv2-ink-3);
  margin-bottom: 20px;
}

/* ── Block (groups a section header + rows) ───────────────────────────────── */

.pv2-block {
  margin-bottom: 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--pv2-border-lt);
}
.pv2-block:last-of-type {
  border-bottom: none;
  margin-bottom: 0;
  padding-bottom: 0;
}

.pv2-block-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 10px;
}

.pv2-block-title {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 1.3px;
  color: var(--pv2-dark);
}

.pv2-block-sub {
  font-size: var(--t-micro);
  color: var(--pv2-ink-3);
  font-style: italic;
}

/* ── Row (label + input) ─────────────────────────────────────────────────── */

.pv2-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 9px 0;
  border-bottom: 1px solid var(--pv2-border-lt);
}
.pv2-row:last-child { border-bottom: none; }
.pv2-row--computed { opacity: 0.8; }

.pv2-row-label { flex: 1; min-width: 0; }
.pv2-row-input { flex-shrink: 0; display: flex; align-items: center; gap: 8px; }

.pv2-label {
  display: block;
  font-family: var(--portal-font);
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--pv2-dark);
  cursor: pointer;
}
.pv2-label--muted { color: var(--pv2-ink-3); font-weight: 400; }

.pv2-sub {
  display: block;
  font-size: var(--t-small);
  color: var(--pv2-ink-3);
  margin-top: 2px;
  line-height: 1.4;
}

/* ── Input ────────────────────────────────────────────────────────────────── */

.sty-pv2-input-wrap {
  display: flex;
  align-items: center;
  border: 1.5px solid var(--pv2-border);
  border-radius: 7px;
  overflow: hidden;
  background: var(--pv2-surface);
  transition: border-color 0.15s;
}
.sty-pv2-input-wrap:focus-within { border-color: var(--portal-accent); }
.pv2-input-wrap--sm { border-radius: 5px; }

.pv2-prefix {
  padding: 0 8px;
  font-family: var(--portal-font);
  font-size: var(--t-body);
  color: var(--pv2-ink-3);
  background: var(--pv2-bg);
  border-right: 1px solid var(--pv2-border);
  height: 100%;
  display: flex;
  align-items: center;
}

.sty-pv2-input {
  font-family: var(--portal-font);
  font-size: var(--t-body);
  font-variant-numeric: tabular-nums;
  padding: 10px 12px;
  border: none;
  outline: none;
  background: transparent;
  color: var(--pv2-dark);
  width: 110px;
}
.pv2-input--sm { width: 80px; padding: 7px 8px; font-size: var(--t-body); }
.sty-pv2-input::-webkit-inner-spin-button,
.sty-pv2-input::-webkit-outer-spin-button { opacity: 0.4; }

/* ── Static value display ─────────────────────────────────────────────────── */

.pv2-value-display {
  font-family: var(--portal-font);
  font-size: var(--t-body);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  color: var(--pv2-dark);
  padding: 10px 0;
}
.pv2-value-display--muted { color: var(--pv2-ink-3); font-weight: 400; }

.pv2-formula-info {
  font-size: var(--t-micro);
  font-weight: 700;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--pv2-border);
  color: var(--pv2-ink-3);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: help;
  flex-shrink: 0;
}

/* ── Table (PartsTable / PartsVariantTable) ────────────────────────────────── */

.pv2-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-body);
  font-variant-numeric: tabular-nums;
}

.pv2-th {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--pv2-ink-3);
  padding: 5px 8px;
  text-align: left;
  border-bottom: 1px solid var(--pv2-border);
}
.pv2-th-price, .pv2-th-qty, .pv2-th-total { text-align: right; }

.pv2-tbl-row td {
  padding: 8px;
  border-bottom: 1px solid var(--pv2-border-lt);
  vertical-align: middle;
}
.pv2-tbl-row:last-child td { border-bottom: none; }

.pv2-tbl-name { min-width: 140px; }
.pv2-tbl-sub  { display: block; font-size: var(--t-small); color: var(--pv2-ink-3); margin-top: 2px; }
.pv2-tbl-price { text-align: right; }
.pv2-tbl-qty   { text-align: right; color: var(--pv2-ink-3); }
.pv2-tbl-total { text-align: right; font-weight: 500; }

.pv2-fixed-hint {
  font-size: var(--t-micro);
  color: var(--pv2-ink-3);
  font-style: italic;
}

.pv2-rollup-row td {
  padding: 9px 8px;
  border-top: 2px solid var(--pv2-border);
  font-weight: 600;
}
.pv2-rollup-label { color: var(--pv2-mid); font-size: var(--t-body); }
.pv2-rollup-total { text-align: right; color: var(--pv2-dark); font-variant-numeric: tabular-nums; }

/* ── Computed section (used by SwapLogicBlock and ExtraPairBlock) ──────────── */

.pv2-computed-section {
  margin-top: 14px;
  padding: 12px 14px;
  background: var(--pv2-bg);
  border-radius: 7px;
  border-left: 3px solid var(--pv2-border);
}

.pv2-computed-section-label {
  display: block;
  font-size: var(--t-body);
  font-weight: 600;
  letter-spacing: 1px;
  color: var(--pv2-ink-3);
  margin-bottom: 8px;
}

.pv2-computed-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 5px 0;
  border-bottom: 1px solid var(--pv2-border-lt);
}
.pv2-computed-row:last-of-type { border-bottom: none; }

.pv2-computed-label { font-size: var(--t-body); color: var(--pv2-mid); }
.pv2-computed-value { font-size: var(--t-body); font-weight: 500; font-variant-numeric: tabular-nums; color: var(--pv2-dark); }

.pv2-formula-note {
  margin-top: 8px;
  font-size: var(--t-small);
  color: var(--pv2-ink-3);
  font-style: italic;
  line-height: 1.5;
}

/* ── Summary box ──────────────────────────────────────────────────────────── */

.pv2-summary-box {
  background: var(--pv2-bg);
  border: 1px solid var(--pv2-border);
  border-radius: 10px;
  padding: 20px 24px;
  margin-top: 20px;
}

.pv2-summary-title {
  display: block;
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 1.3px;
  color: var(--pv2-ink-3);
  margin-bottom: 16px;
}

.pv2-summary-cols {
  display: flex;
  gap: 24px;
  flex-wrap: wrap;
}

.pv2-summary-col {
  flex: 1;
  min-width: 120px;
}

.pv2-summary-col-label {
  display: block;
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--pv2-mid);
  margin-bottom: 4px;
}

.pv2-summary-col-sub {
  display: block;
  font-size: var(--t-small);
  color: var(--pv2-ink-3);
  margin-bottom: 6px;
}

.pv2-summary-price {
  display: block;
  font-family: var(--portal-font);
  font-size: var(--t-page);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--pv2-dark);
  line-height: 1;
}

.pv2-summary-gst {
  display: block;
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
  color: var(--pv2-ink-3);
  margin-top: 4px;
}

.pv2-summary-note {
  font-size: var(--t-small);
  color: var(--pv2-ink-3);
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--pv2-border);
  line-height: 1.5;
}

/* ── Error state ──────────────────────────────────────────────────────────── */

.pv2-field-error {
  font-size: var(--t-small);
  color: var(--pv2-red);
  margin-top: 5px;
}

.sty-pv2-input-wrap--error { border-color: var(--pv2-red); }

/* ── Tab navigation ───────────────────────────────────────────────────────── */

.portal-tabs {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 24px;
}

.portal-tab {
  font-family: var(--portal-font);
  font-size: var(--t-body);
  font-weight: 500;
  padding: 9px 18px;
  border-radius: 7px;
  border: 1.5px solid var(--pv2-border);
  background: var(--pv2-surface);
  color: var(--pv2-mid);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
  white-space: nowrap;
}
.portal-tab:hover { border-color: var(--portal-accent); color: var(--pv2-dark); }
.portal-tab.active {
  background: var(--portal-accent);
  border-color: var(--portal-accent);
  color: var(--client-ink, #fff);
}

/* ── Generic product panel ─────────────────────────────────────────────────── */

.portal-section {
  margin-bottom: 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--pv2-border-lt);
}
.portal-section:last-of-type { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }

.portal-section-header {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 1.2px;
  color: var(--pv2-dark);
  margin-bottom: 12px;
}

.portal-value-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 0;
  border-bottom: 1px solid var(--pv2-border-lt);
}
.portal-value-row:last-child { border-bottom: none; }

.portal-value-row label {
  flex: 1;
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--pv2-dark);
}

.portal-input-wrap {
  display: flex;
  align-items: center;
  border: 1.5px solid var(--pv2-border);
  border-radius: 7px;
  overflow: hidden;
  background: var(--pv2-surface);
  transition: border-color 0.15s;
}
.portal-input-wrap:focus-within { border-color: var(--portal-accent); }

.portal-prefix {
  padding: 0 8px;
  font-size: var(--t-body);
  color: var(--pv2-ink-3);
  background: var(--pv2-bg);
  border-right: 1px solid var(--pv2-border);
  align-self: stretch;
  display: flex;
  align-items: center;
}

.portal-input-wrap input,
.value-input,
.lead-time-input {
  font-family: var(--portal-font);
  font-size: var(--t-body);
  font-variant-numeric: tabular-nums;
  padding: 9px 11px;
  border: none;
  outline: none;
  background: transparent;
  color: var(--pv2-dark);
  width: 110px;
}
.lead-time-input {
  width: 160px;
  border: 1.5px solid var(--pv2-border);
  border-radius: 7px;
  transition: border-color 0.15s;
}
.lead-time-input:focus { border-color: var(--portal-accent); outline: none; }

.portal-unit {
  font-size: var(--t-small);
  color: var(--pv2-ink-3);
  flex-shrink: 0;
  min-width: 60px;
}

.portal-save-btn {
  display: inline-block;
  margin-top: 20px;
  padding: 11px 22px;
  background: var(--portal-accent);
  color: var(--client-ink, #fff);
  border: none;
  border-radius: 7px;
  font-family: var(--portal-font);
  font-size: var(--t-body);
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s, opacity 0.15s;
}
.portal-save-btn:hover:not(:disabled) { background: var(--portal-accent-dk); }
.portal-save-btn:disabled { opacity: 0.5; cursor: default; }

.portal-save-status {
  display: inline-block;
  margin-top: 20px;
  margin-left: 12px;
  font-size: var(--t-body);
  color: var(--pv2-ink-3);
  vertical-align: middle;
}

/* ── Parts table ───────────────────────────────────────────────────────── */

.parts-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-body);
  margin-top: 4px;
}

.parts-th {
  text-align: left;
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--pv2-ink-3);
  padding: 6px 8px 10px;
  border-bottom: 1px solid var(--pv2-border);
}

.parts-th--qty,
.parts-th--price,
.parts-th--total { text-align: right; }

.parts-row { border-bottom: 1px solid var(--pv2-border-lt); }
.parts-row:last-child { border-bottom: none; }

.parts-td {
  padding: 10px 8px;
  vertical-align: middle;
  color: var(--pv2-dark);
}

.parts-td--label { font-weight: 500; }

.parts-td--qty,
.parts-td--total { text-align: right; color: var(--pv2-mid); }

.parts-td--price { text-align: right; }

.parts-td--price .portal-input-wrap {
  display: inline-flex;
  width: auto;
}

.parts-td--price .portal-input-wrap input {
  width: 80px;
  text-align: right;
}

.parts-qty-note {
  font-size: var(--t-small);
  color: var(--pv2-ink-3);
  font-weight: 400;
}

.parts-line-total {
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ── Product card grid ────────────────────────────────────────────────────── */

/* A PREVIEW OF THE CUSTOMER PAGE (2026-08-09, brief Task 19). This grid and the configurator's
   .product-grid now share one rule, so reordering here means what it looks like it means: the same
   tile size, the same gap and the same column count at any width.

   RESPONSIVE, NOT FIXED AT FOUR. The hard repeat(4, 1fr) above 1100px is gone - it forced four
   columns onto a 1100px window and still only four onto a 2500px one, which is the worst of both.
   auto-fill with a floor lets the count follow the width, which is the whole reason the portal page
   went full width in Task 20. */
.pv2-product-grid {
  display: grid;
  /* ONE RULE, TWO SURFACES: this matches .product-grid in index.css, so the portal's grid stays a
     preview of the customer's rather than a differently-shaped list of the same products. Moved
     260 -> 320 and centred on 2026-08-18 (queue Task 21); change both or neither. */
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  justify-content: center;
  gap: 24px;
  margin-bottom: 24px;
}

/* HERO IMAGE. No image = no element, so an un-photographed product keeps exactly its old card and
   nothing renders a grey placeholder rectangle. Fixed height with object-fit so a portrait and a
   landscape photo do not give two different card heights in the same row. */
/* Same photo rule as the configurator's hero: cap by height, contain, never crop. See the note on
   --photo-fit in brand-tokens.css for why the card used to crop and the customer page did not. */
/* ── AND IT IS 4:3, THE SAME NUMBER THE CUSTOMER'S CARD USES (2026-09-19, THE QUEUE) ───────────
   It was a fixed 150px height (--photo-card-max-h), which made every card the same height and made
   none of them a stated SHAPE: at 320px wide that well is 2.13:1, and the photo inside it sat in
   whatever band was left. Tim asked for one ratio across both card surfaces, so the height comes
   from the width and the number lives in brand-tokens.css. --photo-card-max-h is left alone: it is
   still what the quote-request product card reads. */
.pv2-card-hero {
  margin: -16px -16px 12px;
  aspect-ratio: var(--photo-card-ratio, 4 / 3);
  overflow: hidden;
  border-radius: 5px 5px 0 0;
  /* WHITE, NOT THE CLIENT'S SURFACE (2026-08-10, brief Task 8). The CARD was fixed on 2026-08-09 and
     this well inside it was not, so a photograph on Luke's Products page still sat on cream -
     measured as rgb(238,230,212) behind every product tile. --photo-well-bg carries the rule in
     brand-tokens.css: "an image well is always white, never the client's surface_color... This is a
     rule, not a preference." The tile is a preview of the customer's page and the customer's ground
     is white. */
  background: var(--photo-well-bg);
}
.pv2-card-hero img {
  width: 100%;
  height: 100%;
  object-fit: var(--photo-fit);
  display: block;
}

/* ── THE TILE: A GRIP, THEN THE CARD (2026-09-19, THE QUEUE) ───────────────────────────────────
   TIM: "We need the little dots that show you can drag and drop them ... DRAG DOTS outside the
   card, top left, the same handle as the option cards, so grabbing one never opens the product."

   The grip sits ABOVE the card rather than beside it, for the same reason the photo strip's does:
   beside it would eat 14px of every card's width in a grid whose column floor is already the thing
   that decides how many fit across. The row is the grip's own line-height and nothing else. */
.pv2-product-tile {
  display: flex;
  flex-direction: column;
}
.pv2-card-grip {
  align-self: flex-start;
  cursor: grab;
  color: var(--pv2-ink-3);
  letter-spacing: -2px;
  font-size: 13px;
  line-height: 1;
  user-select: none;
  padding: 0 2px 4px;
}
.pv2-card-grip:active { cursor: grabbing; }

/* Drag to reorder. The dragged tile fades; the tile it would land before shows the insertion edge in
   the client's own accent, so the drop target is unambiguous before the mouse is released. */
.pv2-product-tile--dragging { opacity: 0.4; }
.pv2-product-tile--dropbefore { box-shadow: inset 3px 0 0 var(--portal-accent); }
.pv2-product-card--dragging { opacity: 0.4; }
.pv2-product-card--dropbefore { box-shadow: inset 3px 0 0 var(--portal-accent); }

/* WHITE, LIKE THE CUSTOMER'S CARD (brief Task 19). --pv2-surface is the portal's own panel colour;
   a tile that is a preview of the customer page should be the ground the customer sees behind the
   photograph. Same reasoning as the image wells in Task 12. */
.sty-pv2-product-card {
  background: var(--photo-well-bg);
  border: 1px solid var(--pv2-border);
  border-top: 3px solid var(--portal-accent);
  border-radius: 6px;
  padding: 16px;
  text-align: left;
  cursor: pointer;
  transition: box-shadow 0.15s;
  width: 100%;
  font-family: var(--portal-font);
  position: relative;
}

.sty-pv2-product-card:hover {
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.pv2-product-card--accessories {
  border-top-color: var(--pv2-ink-3);
  border-style: dashed;
  border-top-style: dashed;
}

/* ── A CARD WITH NO PHOTO KEEPS ITS BADGE ROW (2026-09-01) ────────────────────────────────────
   The health status sits top-right and the Link only / Not live pill top-left, both absolutely
   positioned at top 10px. On a card WITH a photo that is the photo's own strip, which is what they
   were designed against. WITH NO PHOTO the name is the first thing in the card, at exactly that
   height, so the two pills printed straight over it: measured on client 6 on Shape Fixture,
   Publishable Form Fixture, Product 1 and all seven Test fixtures cards.

   The fix is a row of its own rather than moving either pill: they are correct where they are, and
   a photo card must not change. Only the un-photographed card reserves the height its badges use,
   which is the badge's own line-height plus its offset. */
/* ── AND THAT RESERVED ROW IS NO LONGER FOR THE STATUS (2026-09-19, THE QUEUE) ─────────────────
   The status moved off the photo and under the name, in the card's own flow, so the only thing
   still absolutely positioned at top 10px is the Link only / Not live pill. An un-photographed
   card still has to reserve room for THAT, which is why this rule stays and why it is now sized to
   one pill rather than to two. */
.pv2-product-card:not(.pv2-product-card--hero) { padding-top: 34px; }

.pv2-card-name {
  font-size: var(--t-lead);
  font-weight: 600;
  color: var(--pv2-dark);
  margin-bottom: 4px;
}

.pv2-card-price {
  font-size: var(--t-small);
  color: var(--portal-green);
  margin-bottom: 4px;
}

/* ── Health status, ON THE DOT ────────────────────────────────────────────── */
/* Replaces .pv2-health-legend, which was a key at the bottom of the page explaining dots at the
   top of the page. The meaning now sits beside the dot it belongs to. */

/* UNDER THE NAME, IN THE FLOW (2026-09-19, Tim: "THE STATUS DOT moves off the photo, under the
   name"). It was absolutely positioned top right, over whatever the photograph happened to be
   there - unreadable on a dark render, and the reason an un-photographed card had to reserve a
   badge row for two pills instead of one. */
.pv2-card-health {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--t-micro);
  line-height: 1.2;
  color: var(--pv2-ink-3);
  max-width: 100%;
}

.pv2-card-health-words {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Green carries no words - see the comment at the render site. The dot still shows, so a fully
   configured product is not silently identical to one with no status at all. */

.pv2-health-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}

.pv2-health-dot--error { background: var(--portal-error); }
.pv2-health-dot--amber { background: var(--portal-amber); }
.pv2-health-dot--green { background: var(--portal-green); }
/* grey: the tree has no priced items yet, which is where every new product starts. It had no rule
   here and no entry in HEALTH, so the dot rendered as an empty span on 15 of 19 demo cards. It is a
   NEUTRAL token deliberately: not-started is not an error, and colouring it red is what trains
   people to stop reading the dot. Uses the existing ink token rather than a new colour, because a
   hardcoded grey in shared code is a client colour by another name. */
.pv2-health-dot--grey  { background: var(--pv2-ink-3, #666); }

/* Available to a screen reader, invisible on screen. Used for the green state's wording, which is
   noise as a visible label on every healthy card but is still the only thing naming what the dot
   means for anyone who cannot see its colour. */
.pv2-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Quote requests section ───────────────────────────────────────────────── */

.sty-pv2-quote-section {
  margin-top: 40px;
  padding-top: 24px;
  border-top: 2px solid var(--pv2-border);
}

.pv2-quote-section-header {
  font-size: var(--t-small);
  font-weight: 500;
  letter-spacing: 1.5px;
  color: var(--pv2-ink-3);
  margin-bottom: 0;
}

.pv2-quote-section-header--toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  padding: 4px 0;
  user-select: none;
}
.pv2-quote-section-header--toggle:hover .pv2-section-label { color: var(--portal-accent); }

.pv2-quote-new-badge {
  background: var(--portal-error);
  color: #fff;
  font-size: var(--t-micro);
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 10px;
  letter-spacing: 0.5px;
}

.pv2-quote-chevron {
  margin-left: auto;
  font-size: var(--t-micro);
  color: var(--pv2-ink-3);
  line-height: 1;
}

/* Individual quote rows (landing section) */

.sty-pv2-qrow {
  /* --pv2-border-lt (#ecf0f1) is too faint here, so consecutive quotes merged. Match
     the inbox rows' separator weight (#c4cdcf, set on .pv2-inbox-row at runtime) so each
     quote is clearly divided. Scoped to .pv2-qrow only - the shared faint token is left
     alone for its ~10 other uses where a light line is correct. */
  border-bottom: 1px solid #c4cdcf;
}

.sty-pv2-qrow-summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 0;
  cursor: pointer;
  gap: 8px;
}
.sty-pv2-qrow-summary:hover .pv2-qrow-name { color: var(--portal-accent); }

.pv2-qrow-left  { flex: 1; min-width: 0; }

.pv2-qrow-name {
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--pv2-dark);
  transition: color 0.12s;
}

.pv2-qrow-meta {
  font-size: var(--t-micro);
  color: var(--pv2-ink-3);
  margin-top: 2px;
}

.pv2-qrow-right {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.pv2-qrow-total {
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--pv2-dark);
}

.pv2-qrow-new {
  background: var(--portal-error);
  color: #fff;
  font-size: var(--t-micro);
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 8px;
}

.sty-pv2-qrow-detail { padding: 0 0 14px; }

.pv2-qrow-detail-body {
  background: var(--pv2-bg);
  border-radius: 8px;
  padding: 14px 16px;
}

.pv2-qrow-contact {
  font-size: var(--t-small);
  color: var(--pv2-mid);
  margin-bottom: 8px;
}
.pv2-qrow-contact-link { color: var(--portal-accent); text-decoration: none; }
.pv2-qrow-contact-link:hover { text-decoration: underline; }

.pv2-qrow-lines { margin-bottom: 12px; }

.pv2-qrow-line {
  font-size: var(--t-small);
  color: var(--pv2-mid);
  line-height: 1.6;
}
.pv2-qrow-line--muted { color: var(--pv2-ink-3); }

.pv2-qrow-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--pv2-border-lt);
}

.sty-pv2-qrow-delete {
  margin-left: auto;
  background: none;
  border: 1px solid #ccc;
  border-radius: 6px;
  padding: 5px 12px;
  font-size: var(--t-small);
  font-family: var(--portal-font);
  color: var(--pv2-ink-3);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}
.sty-pv2-qrow-delete:hover { border-color: var(--pv2-red); color: var(--pv2-red); }

.pv2-qrow-confirm {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: var(--t-small);
  color: var(--pv2-mid);
  margin-left: auto;
  margin-top: 8px;
  width: 100%;
}

.sty-pv2-qrow-confirm-yes {
  background: var(--pv2-red);
  color: #fff;
  border: none;
  border-radius: 6px;
  padding: 5px 12px;
  font-size: var(--t-small);
  font-family: var(--portal-font);
  cursor: pointer;
}

.sty-pv2-qrow-confirm-no {
  background: none;
  border: 1px solid var(--pv2-border);
  border-radius: 6px;
  padding: 5px 12px;
  font-size: var(--t-small);
  font-family: var(--portal-font);
  color: var(--pv2-mid);
  cursor: pointer;
}

.pv2-qrow-load-more { text-align: center; margin-top: 12px; }

.pv2-quote-load-more-btn {
  font-family: var(--portal-font);
  font-size: var(--t-body);
  font-weight: 500;
  padding: 8px 18px;
  border-radius: 6px;
  border: 1px solid var(--portal-accent);
  background: rgba(var(--portal-accent-rgb, 36, 41, 51), 0.07);
  color: var(--portal-accent);
  cursor: pointer;
  transition: background 0.15s;
}
.pv2-quote-load-more-btn:hover { background: rgba(var(--portal-accent-rgb, 36, 41, 51), 0.14); }

.pv2-quote-empty {
  margin-top: 12px;
  padding: 16px 0;
}

/* ── Back button ──────────────────────────────────────────────────────────── */

.pv2-back-btn {
  background: none;
  border: none;
  padding: 0;
  font-size: var(--t-body);
  font-family: var(--portal-font);
  color: var(--pv2-dark);
  cursor: pointer;
  margin-bottom: 16px;
  display: block;
}

.pv2-back-btn:hover { text-decoration: underline; }

/* ── Notification email section ───────────────────────────────────────────── */

.pv2-notify-section {
  margin-top: 24px;
  padding-top: 16px;
  border-top: 1px solid var(--pv2-border);
}

.pv2-section-label {
  font-size: var(--t-body);
  font-weight: 600;
  letter-spacing: 1.2px;
  color: var(--pv2-ink-3);
  margin-bottom: 8px;
}

.pv2-notify-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}

.pv2-notify-input {
  flex: 1;
  max-width: 320px;
  padding: 9px 12px;
  border: 1.5px solid var(--pv2-border);
  border-radius: 7px;
  font-family: var(--portal-font);
  font-size: var(--t-body);
  color: var(--pv2-dark);
  outline: none;
  transition: border-color 0.15s;
}
.pv2-notify-input:focus { border-color: var(--portal-accent); }

.pv2-notify-save-btn {
  padding: 9px 16px;
  background: var(--portal-accent);
  color: var(--client-ink, #fff);
  border: none;
  border-radius: 7px;
  font-family: var(--portal-font);
  font-size: var(--t-body);
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s, opacity 0.15s;
  white-space: nowrap;
}
.pv2-notify-save-btn:hover:not(:disabled) { background: var(--portal-accent-dk); }
.pv2-notify-save-btn:disabled { opacity: 0.5; cursor: default; }

.pv2-field-hint {
  font-size: var(--t-small);
  color: var(--pv2-ink-3);
  margin: 0 0 4px;
  line-height: 1.4;
}

.pv2-notify-status {
  font-size: var(--t-small);
  color: var(--pv2-ink-3);
  display: inline-block;
  margin-top: 4px;
}

/* Why a setting is unavailable. Same slot and same weight as the status line above, because it is
   the same kind of sentence: the control telling you about itself. Not red - a refusal that is
   simply how the product works is not an error (2026-08-10, brief Task 11). */
.pv2-set-why {
  font-size: var(--t-small);
  color: var(--pv2-ink-3);
  display: block;
  margin-top: 4px;
}

/* ── Inbox page (dedicated quote requests view) ───────────────────────────── */

/* THE INBOX CONTROL BAR (2026-08-11, brief Task 8): the status tabs and the search box on one
   line, wrapping to two when there is no room. */
.pv2-inbox-controls { display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
                      margin-bottom: 4px; }
.pv2-inbox-controls .pv2-inbox-tabs { margin-bottom: 0; }
.pv2-inbox-search { margin-left: auto; flex: 0 1 340px; min-width: 220px; }
.pv2-inbox-search input { width: 100%; max-width: none; }

/* Age and the to-price count sit with the date, in the same muted key: they are facts about the
   enquiry, not warnings. The to-price count is the one that earns a colour, because it is the
   reason a request cannot be quoted yet. */
.pv2-inbox-age { color: var(--pv2-ink-3); }
.pv2-inbox-poa { color: #b45309; background: #fef3c7; border-radius: 10px; padding: 1px 8px;
                 font-size: var(--t-micro); font-weight: 600; white-space: nowrap; }

.sty-pv2-inbox-tabs {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}

.sty-pv2-inbox-tab {
  background: none;
  border: 1px solid var(--pv2-border);
  border-radius: 20px;
  padding: 5px 14px;
  font-size: var(--t-body);
  font-family: var(--portal-font);
  color: var(--pv2-mid);
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.sty-pv2-inbox-tab:hover {
  border-color: var(--portal-accent);
  color: var(--portal-accent);
}

.sty-pv2-inbox-tab--active {
  background: var(--portal-accent);
  border-color: var(--portal-accent);
  color: var(--client-ink, #fff);
  font-weight: 600;
}

.sty-pv2-inbox-row {
  border-bottom: 1px solid var(--pv2-border-lt);
  padding: 14px 0;
}

.pv2-inbox-row-main {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16px;
  flex-wrap: wrap;
}

.pv2-inbox-row-left {
  flex: 1;
  min-width: 200px;
}

/* ONE LINE, HARD RIGHT (2026-08-15, queue Task 26). The value, the status pill and the three
   buttons were stacked one per line, which was reasonable when the row was capped at 1150px and is
   not now that the tab is full width: it made a tall thin tower with a wide empty band beside it.
   The row reads left to right as WHO, then WHAT IT IS WORTH, then WHAT TO DO ABOUT IT. */
.pv2-inbox-row-right {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  flex-shrink: 0;
  /* It may wrap onto a second line before it crushes: the fallback below handles the narrow case,
     and this handles the awkward width between the two where one button would otherwise be
     squeezed. */
  flex-wrap: wrap;
}

.pv2-inbox-name {
  font-size: var(--t-lead);
  font-weight: 600;
  color: var(--pv2-dark);
  margin-bottom: 2px;
}

.pv2-inbox-contact {
  font-size: var(--t-small);
  color: var(--pv2-mid);
  margin-bottom: 4px;
}

.pv2-inbox-contact-link {
  color: var(--portal-accent);
  text-decoration: none;
}
.pv2-inbox-contact-link:hover { text-decoration: underline; }

.pv2-inbox-meta {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
}

.pv2-inbox-product {
  font-size: var(--t-small);
  color: var(--pv2-mid);
}

.pv2-inbox-date {
  font-size: var(--t-small);
  color: var(--pv2-ink-3);
}

.pv2-inbox-total {
  font-size: var(--t-body);
  font-weight: 600;
  color: var(--pv2-dark);
  font-variant-numeric: tabular-nums;
}

.pv2-inbox-status-wrap {
  display: flex;
  align-items: center;
  gap: 6px;
}

.sty-pv2-inbox-status-sel {
  font-family: var(--portal-font);
  font-size: var(--t-body);
  font-weight: 700;
  padding: 3px 10px;
  border: none;
  border-radius: 12px;
  cursor: pointer;
  color: #fff;
  -webkit-appearance: none;
  appearance: none;
}

.pv2-inbox-email-btn {
  font-size: var(--t-body);
  font-family: var(--portal-font);
  color: var(--portal-accent);
  background: none;
  border: 1px solid var(--portal-accent);
  border-radius: 6px;
  padding: 4px 10px;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
  white-space: nowrap;
}
.pv2-inbox-email-btn:hover {
  background: var(--portal-accent);
  color: var(--client-ink, #fff);
}

.pv2-inbox-details {
  margin-top: 10px;
}

.pv2-inbox-expand {
  font-size: var(--t-small);
  color: var(--pv2-ink-3);
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.pv2-inbox-expand:hover { color: var(--portal-accent); }
.pv2-inbox-expand::-webkit-details-marker { display: none; }

.pv2-inbox-detail-body {
  padding: 8px 0 4px;
}

.pv2-inbox-summary-line {
  font-size: var(--t-small);
  color: var(--pv2-mid);
  line-height: 1.7;
}

/* ── Mobile ────────────────────────────────────────────────────────────────── */

@media (max-width: 600px) {
  .pv2-product-section { padding: 18px 16px; }
  .pv2-row { flex-direction: column; gap: 8px; }
  .pv2-row-input { width: 100%; }
  .sty-pv2-input-wrap { width: 100%; }
  .sty-pv2-input { width: 100%; }
  .pv2-summary-cols { flex-direction: column; gap: 16px; }
  .pv2-table { font-size: var(--t-small); }
  .pv2-input--sm { width: 65px; }
  .pv2-product-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; }
  /* NARROW: back to stacking rather than crushing (2026-08-15, queue Task 26). Below this width the
     customer's details already take the full row, so the cluster has nothing to sit beside. */
  .pv2-inbox-row-right { flex-direction: column; align-items: flex-start; justify-content: flex-start; }
}

/* THE CLUSTER STACKS BEFORE IT CRUSHES (2026-08-15, queue Task 26).
   Measured: on one line the value, the status pill and the three buttons need about 650px. The
   shared narrow breakpoint is 600px, so between roughly 600 and 1000 the cluster OVERFLOWED its own
   row - at a 700px viewport it ran 31px past the row's right edge. Its own breakpoint, set where it
   actually stops fitting rather than where the rest of the page happens to change. */
@media (max-width: 1000px) {
  .pv2-inbox-row-right {
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
  }
}

/* ── Draft / Publish chrome (2026-07-25) ─────────────────────────────────────
   Shown per product. An UNDRAFTED product gets only the quiet one-line "editing goes
   live immediately" row plus the opt-in button, so the editor Luke already knows is
   visually unchanged until he chooses otherwise. */
.pv2-draftbar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 14px; margin: 0 0 14px; border-radius: 10px;
  background: #fff; border: 1px solid #dde3e4;
}
/* DRAFT IS VIOLET, NOT AMBER (R2, Tim's confirmation). Amber is the advisory colour - "you can publish
   but look at this" - so an amber draft bar told Luke something was wrong with a product he was simply
   working on. A draft is a MODE. Violet is far enough from both the teal accent and the amber advisory
   to be unmistakable at a glance on a phone, and the word Draft is always beside it, so the hue never
   has to carry the meaning by itself. Now on the semantic tokens rather than four hardcoded hexes. */
.pv2-draftbar--on { background: var(--portal-violet-bg); border-color: var(--portal-violet-bd); }
.pv2-draft-pill {
  font-size: var(--t-micro); font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase;
  color: var(--portal-violet); background: var(--portal-violet-bg);
  border: 1px solid var(--portal-violet-bd); border-radius: 20px; padding: 3px 10px;
}
/* PUBLISHED, the other of the spec's two persisted states. Violet is the portal's "draft, not live"
   hue and carries that meaning everywhere, so the published state must not borrow it. Green is the
   portal's confirmed/live semantic and is already what a fully configured product's health dot uses. */
.pv2-draft-pill--live {
  color: var(--portal-green); background: var(--portal-green-bg);
  border-color: var(--portal-green-bd);
}
/* NEVER PUBLISHED, the third state of the absent-draft case (2026-08-31, walk finding 11).
   NEUTRAL, and that is the decision. Violet means "draft, not live" and green means "live", and this
   is neither: nothing is pending and nothing is out there. Borrowing either hue would make it read
   as one of the two states it exists to be distinguished from, and it is not a FAULT, so it must not
   borrow the portal's amber or red either. The words carry it, which is why they are the longest of
   the three labels. Semantic tokens only, no hardcoded client colour: shared code serves every
   client. */
.pv2-draft-pill--unpublished {
  color: var(--pv2-ink-3); background: transparent;
  border-color: var(--pv2-line, #d9d4c7);
}
/* THE TITLE IS A VALUE UNTIL YOU TOUCH IT (2026-08-31, walk finding 10).
   The same rule finding 23 applied to the dimension name cells in the editor's tables. It does NOT
   share that rule: .cte2-ct .cte2-flatbox is scoped to those tables on purpose, and widening a
   scoped rule so a second caller can borrow it is how a scoped rule stops being scoped. The two
   agree by intent, not by inheritance.
   The input inherits the heading's own type so the name does not resize when it becomes editable,
   which is what makes it read as the heading rather than as a form appearing over it. */
/* THE HEADING NOW WEARS THE PLATFORM'S READ STATE (2026-09-05, THE QUEUE).
   Tim, who uses this editor every day: "I just realised there is no way to change the product name
   in the portal." It could be renamed by clicking it, and the only signals were a text cursor and a
   tooltip that needs a second of dwell.
   The hover fill, the focus ring and the rounded target all come from .cte2-c2e-read in
   editor-styles.js. NOT COPIED HERE: a second copy of that rule is how the quote document, the
   editor and this heading come to disagree about what click-to-edit looks like.
   ONLY THE LAYOUT IS SAID HERE, and it is layout rather than a variant of the treatment: the read
   state is a div everywhere else and a heading is a block, so without this the hover box would run
   the full width of the pane instead of hugging the name. */
h2.pv2-product-name .pv2-prodrename-read { display: inline-block; max-width: 100%; }
h2.pv2-product-name .pv2-prodrename-read--open { display: block; width: 100%; background: none; }
.pv2-prodrename {
  font: inherit; color: inherit; width: 100%; max-width: 100%;
  border: 1px solid transparent; background: transparent; border-radius: 4px; padding: 0 4px;
}
.pv2-prodrename:focus { border-color: var(--pv2-line, #d9d4c7); background: #fff; outline: none; }
.pv2-draft-note { font-size: var(--t-small); color: var(--pv2-ink-3); }
.pv2-draft-status { font-size: var(--t-small); margin-left: auto; }
.pv2-draftbar--on .pv2-draft-status { margin-left: auto; }
/* ── THE STALE-DRAFT OFFER (2026-08-17, queue Task F) ─────────────────────────────────────────
   NOT RED, and that is the point rather than a preference. Red is the portal's fault colour, and a
   draft that is older than a row is not a fault: it is a choice, and one the button beside it makes
   for you. Amber is the portal's own advisory semantic - "you can carry on, but look at this" - which
   is exactly what this is. Tokens, never a literal, because this stylesheet serves every client.

   It wraps onto its own line because the sentence is a sentence, not a status word, and squeezing it
   into the strip beside three buttons is how it would get skipped. */
.pv2-draft-status--choice {
  margin-left: 0; flex: 1 1 100%;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px 10px; border-radius: 8px;
  background: var(--portal-amber-bg); border: 1px solid var(--portal-amber-bd);
  color: var(--pv2-ink);
}
.pv2-draft-status--choice .pv2-stale-say { flex: 1 1 22rem; line-height: 1.45; }
.pv2-draft-status--err { color: var(--red); }
.pv2-draft-btn { background: #fff; border: 1px solid #dde3e4; color: #4a6b70; }
.pv2-draft-btn:hover { background: #f6f8f8; }
.pv2-draft-preview { margin: 0 0 14px; border: 1px solid #dde3e4; border-radius: 10px; overflow: hidden; background: #fff; }
.pv2-draft-preview-head {
  font-size: var(--t-small); color: var(--pv2-ink-3); padding: 8px 12px; border-bottom: 1px solid #eef2f2;
  display: flex; justify-content: space-between; gap: 12px;
}
.pv2-draft-preview iframe { width: 100%; height: 620px; border: 0; display: block; }

/* ── SETTINGS PAGE (PORTAL SPEC, SETTINGS, 2026-08-07) ───────────────────────────────────────────
   Same side-panel shape as the product editor and the Library, so the portal has one page layout
   rather than a third one invented here. */
/* SETTINGS IS A DOCUMENT, so it gets a MAX WIDTH - wider than the 1200px body it sits in was
   giving it, but capped, because a settings row stretched to 2500px puts the label and its control
   a hand-span apart (2026-08-09, brief Task 20). The nav column is unchanged; only the detail
   column stops growing. */
.pv2-set-wrap { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 24px; align-items: start;
                max-width: 1400px; }
/* `display: grid` BEATS THE hidden ATTRIBUTE (2026-08-12, brief Task 26). The UA sheet's
   [hidden] { display: none } is a lower-specificity rule than any class that sets display, so the
   product settings panel carried hidden=true and rendered anyway, full height, above the tree.
   Task 7 believed it had hidden the old block and had not: the product showed its settings twice,
   the old one live and the tab dead. This is the whole bug and it is one line. */
.pv2-set-wrap[hidden] { display: none; }
.pv2-set-nav { display: flex; flex-direction: column; gap: 2px; }
.pv2-set-detail { min-width: 0; }
.pv2-set-block { background: var(--pv2-surface, #fff); border: 1px solid var(--pv2-border, #e0dcd4);
                 border-radius: 10px; padding: 20px 22px; }
.pv2-set-h { margin: 0 0 18px; font-size: var(--t-lead); font-weight: 700; color: var(--pv2-dark); }
.pv2-set-row { display: grid; grid-template-columns: 150px 1fr; gap: 14px; align-items: start;
               padding: 10px 0; border-bottom: 1px solid var(--pv2-border-lt, #f0ece4); }
.pv2-set-row:last-child { border-bottom: none; }
.pv2-set-label { font-size: var(--t-body); font-weight: 500; color: var(--pv2-dark); padding-top: 8px; }
.pv2-set-input { width: 100%; max-width: 380px; padding: 8px 11px; font-size: var(--t-body);
                 font-family: inherit; border: 1.5px solid var(--pv2-border, #ccc); border-radius: 7px;
                 box-sizing: border-box; }
.pv2-set-input:focus { outline: none; border-color: var(--portal-accent); }
/* A multi-line control in a row. Same box as .pv2-set-input, taller, and it must not inherit the
   input's max-width - a description is the one control that wants the column. Added with the row
   component (2026-08-09, brief Task 11): Product settings' description box used to carry nine inline
   style declarations of its own and matched nothing else on the platform. */
.pv2-set-textarea { max-width: none; resize: vertical; line-height: 1.5; }
.pv2-set-colour { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pv2-set-swatch { width: 40px; height: 34px; padding: 0; border: 1.5px solid var(--pv2-border, #ccc);
                  border-radius: 7px; background: none; cursor: pointer; flex: none; }
.pv2-set-hex { max-width: 140px; font-variant-numeric: tabular-nums; }

/* "derive from Primary" (2026-08-11, brief Task 2). Sits with the boxes it governs rather than under
   the hint, because it decides whether those boxes are editable at all. Disabled inputs keep their
   place so ticking it does not reflow the row. */
.pv2-set-derive { display: inline-flex; align-items: center; gap: 6px; margin-left: 10px;
                  font-size: var(--t-small); color: var(--pv2-ink-3); white-space: nowrap;
                  cursor: pointer; }
.pv2-set-derive input { cursor: pointer; }
.pv2-set-colour input:disabled { opacity: 0.5; cursor: not-allowed; }
.pv2-set-colour .pv2-field-hint { flex-basis: 100%; margin: 2px 0 0; }
.pv2-set-radio { margin-right: 16px; font-size: var(--t-body); color: var(--pv2-dark); }

/* LEAD TIME AND WEIGHT RULES (2026-09-13, THE QUEUE). One row per rule: the condition on top, what it
   means underneath, and Remove at the end of that line. Stacked rather than side by side because the
   condition builder is itself a row of controls and two of those across a settings pane is a form
   nobody can read at 1280px. */
.pv2-fact-pane { margin: 4px 0 6px; }
.pv2-fact-row {
  border: 1px solid var(--pv2-border, #e0dcd4);
  border-radius: 8px;
  padding: 10px 12px;
  margin-bottom: 8px;
  background: #fff;
}
.pv2-fact-then {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 8px;
  font-size: var(--t-small);
  color: var(--pv2-ink-3, #6b7280);
}
.pv2-fact-then-word { text-transform: uppercase; letter-spacing: 1px; font-size: 0.62rem; }
.pv2-fact-value {
  border: 1px solid var(--pv2-border, #e0dcd4);
  border-radius: 6px;
  padding: 5px 8px;
  font: inherit;
  font-size: var(--t-small);
  min-width: 150px;
}
.pv2-fact-remove { margin-left: auto; }
.pv2-set-logos { display: flex; gap: 14px; flex-wrap: wrap; }
.pv2-set-logo { border: 1px solid var(--pv2-border, #e0dcd4); border-radius: 8px; padding: 10px 14px;
                min-width: 140px; text-align: center; background: #fff; }
.pv2-set-logo--dark { background: var(--pv2-dark, #1c2426); }
.pv2-set-logo--dark .pv2-set-logo-cap { color: #fff; }
/* THE "NOT SET" HINT ON THE DARK SLOT (2026-10-02, portal-contrast-check): the grey page hint on the dark fill read 3.06:1. */
.pv2-set-logo--dark .pv2-field-hint { color: rgba(255, 255, 255, 0.72); }
.pv2-set-logo img { max-width: 120px; max-height: 44px; display: block; margin: 6px auto 0; }
.pv2-set-logo-cap { font-size: var(--t-micro); text-transform: uppercase; letter-spacing: 0.6px;
                    color: var(--pv2-ink-3); }

/* THE LIVE PREVIEW is not decoration (spec): without it a client picks hex codes blind. The contrast
   readout under it is this session's own addition - MORPHD's tan looked fine and failed AA on 14
   text pairs, and a number beside the swatch is what would have caught it before it shipped. */
.pv2-set-preview { margin-top: 18px; }
.pv2-set-preview-cap { font-size: var(--t-micro); text-transform: uppercase; letter-spacing: 0.6px;
                       color: var(--pv2-ink-3); margin-bottom: 6px; }
.pv2-set-preview-body { border: 1px solid var(--pv2-border, #e0dcd4); border-radius: 10px;
                        padding: 16px; }
.pv2-set-preview-bar { color: #fff; font-weight: 700; padding: 10px 14px; border-radius: 7px;
                       margin-bottom: 12px; }
.pv2-set-preview-btn { color: #fff; border: none; border-radius: 7px; padding: 9px 18px;
                       font-size: var(--t-body); font-family: inherit; cursor: pointer; }
.pv2-set-contrast { margin: 10px 0 0; font-size: var(--t-small); color: var(--pv2-ink-3);
                    font-variant-numeric: tabular-nums; }
.pv2-set-contrast--bad { color: var(--portal-error, #c0392b); font-weight: 600; }

/* THE OTHER THREE COLOURS, EACH DOING ITS OWN JOB (2026-08-14, queue Task 10). The preview painted
   Primary twice and never showed Primary (dark), Accent or Surface at all, so three of the four
   were chosen blind. Inline colours come from _paintSettingsPreview; these are the shapes. */
.pv2-set-preview-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pv2-set-preview-btn--dk { opacity: 1; }
.pv2-set-preview-chip { display: inline-flex; align-items: center; border-radius: 999px;
                        padding: 7px 15px; font-size: var(--t-small); font-weight: 600; }

/* THE FONT, SHOWN IN THE FONT. A picker that does not render its own choice still asks the client
   to imagine the answer. Sized where the risk is: a pricing platform's smallest real text is a
   dimension callout and a per-line price, so the sample carries digits and sits near that size. */
.pv2-set-fontpick { margin-bottom: 8px; }
.pv2-set-fontsample { margin: 0 0 8px; padding: 10px 12px; border-radius: 7px;
                      background: var(--pv2-surface, #fff); border: 1px solid var(--pv2-border-lt, #ecf0f1);
                      font-size: 0.95rem; color: var(--pv2-dark, #1a1a1a);
                      font-variant-numeric: tabular-nums; }
.pv2-set-fontraw { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var(--t-small); }
/* A derived swatch is disabled, and a disabled colour input greys itself in some engines. This keeps
   the DERIVED COLOUR visible, which is the entire point of showing it. */
/* A DERIVED SWATCH IS A PAINTED SPAN, not a disabled colour input, because every engine greys out
   a disabled <input type="color"> and it therefore cannot show the colour it holds. That is the
   whole reported bug: the value was right and invisible. `background: none` above is for the input,
   so this has to allow the inline background through, and `display:inline-block` is what gives a
   span the 40x34 the input got for free. */
.pv2-set-swatch--derived { display: inline-block; cursor: default; }
.pv2-set-foot { margin-top: 18px; display: flex; align-items: center; gap: 12px; }

@media (max-width: 899px) {
  .pv2-set-wrap { grid-template-columns: 1fr; }
  .pv2-set-nav { flex-direction: row; flex-wrap: wrap; }
  .pv2-set-row { grid-template-columns: 1fr; }
  .pv2-set-label { padding-top: 0; }
}

/* ── THE PORTAL SHELL (PORTAL SPEC, 2026-08-07) ──────────────────────────────────────────────────
   Two rows: identity and account on top, navigation below with a visible active state. The old
   header put a Back link, real destinations, help and Log out all at one weight, with no indication
   of where you were. */
/* The duplicate .pv2-header-inner that used to sit here is gone. It redeclared display, align-items
   and gap identically to the block above and, being later in the file, was the one that actually
   won - so the real rule was not the one anyone reading the header section would have found. */
.pv2-header-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
.pv2-header-account { margin-left: auto; display: flex; align-items: center; gap: 8px; }

/* HELP IS AN ICON, not a nav item competing with real destinations. Same treatment for Log out:
   both are account-level, neither is a place you go to do work. */
.pv2-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%;
  /* Filled from the INK at low alpha rather than outlined. An outline on a mid-tone accent like
     MORPHD's tan reads as a faint ring around nothing; a fill reads as a button on every accent,
     which is the whole test this header has to pass. It said "from white" until 2026-08-09, and
     that was the flaw: on a pale accent a white fill is invisible and white glyph on it was
     3.53:1. Both now tint with --client-ink, so the fill darkens a light ground. */
  border: 1px solid rgba(var(--client-ink-rgb, 255, 255, 255), 0.18);
  background: rgba(var(--client-ink-rgb, 255, 255, 255), 0.10);
  color: var(--client-ink, #fff); font-size: var(--t-body); line-height: 1; cursor: pointer;
  text-decoration: none; font-family: inherit; flex: none;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.pv2-icon-btn:hover { background: rgba(var(--client-ink-rgb, 255, 255, 255), 0.22); border-color: rgba(var(--client-ink-rgb, 255, 255, 255), 0.34); }
.pv2-icon-btn:active { transform: translateY(1px); }
.pv2-icon-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.pv2-icon-btn--active { background: rgba(var(--client-ink-rgb, 255, 255, 255), 0.28); border-color: rgba(var(--client-ink-rgb, 255, 255, 255), 0.42); }

/* NAVIGATION, with the active state the spec asks for: an underline under where you are. */
/* THE NAV IS INLINE, NOT A SECOND ROW (2026-08-09, brief Task 20). It used to sit under the mark on
   its own line, which is why the header was two rows tall and why the alignment note below it had to
   exist at all - two rows of one header not sharing a left margin. Inline, there is one margin. */
.pv2-header-nav {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  align-items: center;
  min-width: 0;
}
.pv2-nav-link {
  /* 0.88, NOT 0.78, AND THE NUMBER WAS MEASURED. White at 78% over Oceanside Alloy's teal is a
     comfortable 5.4:1, which is why nobody noticed - but the same declaration over MORPHD's tan
     (#77654b, a much lighter ground) came out at 4.27:1, under the 4.5 floor, on every nav link in
     the portal. That is the exact failure the header has to not have: styling tuned on one client's
     accent and quietly illegible on another's. 0.88 clears on both.
     THAT LIMIT IS LIFTED (2026-08-09, brief Task 14). This used to say "white-on-accent can only
     ever be as readable as the accent is dark, and a client who picks a genuinely pale primary
     cannot be rescued by an alpha value". True of white. The ink is no longer white: it is
     whichever of white and black wins against the client's own primary, so a pale brand gets dark
     nav links rather than illegible ones. 0.88 still applies, to whichever it turns out to be. */
  color: rgba(var(--client-ink-rgb, 255, 255, 255), 0.88);
  text-decoration: none;
  font-size: var(--t-body);
  font-weight: 500;
  padding: 9px 12px 10px;
  border-radius: 6px 6px 0 0;
  border-bottom: 2px solid transparent;
  white-space: nowrap;
  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
.pv2-nav-link:hover { color: var(--client-ink, #fff); background: rgba(var(--client-ink-rgb, 255, 255, 255), 0.10); }
.pv2-nav-link:focus-visible { outline: 2px solid var(--client-ink, #fff); outline-offset: -2px; }
/* THE ACTIVE TAB IS MARKED THREE WAYS, on purpose: a lit ground, full-strength text and a solid
   underline. Colour alone is not an accessible way to say "you are here", and on a low-contrast
   accent a 22%-opacity difference in white text is not a reliable way either. */
.pv2-nav-link--active {
  color: var(--client-ink, #fff);
  font-weight: 600;
  background: rgba(var(--client-ink-rgb, 255, 255, 255), 0.14);
  border-bottom-color: var(--client-ink, #fff);
}

/* THE ICONS ARE DRAWN (2026-10-10, THE QUEUE "Portal on a phone", page type 1 SHELL): the power button and the
   phone's tab bar carry inline SVGs in currentColor. pointer-events:none so a click on a stroke still names the
   BUTTON or the LINK as its target, which the delegated log-out handler reads. */
.pv2-icon-svg, .pv2-tab-icon { display: block; flex: none; pointer-events: none; }

/* The phone's bottom tab bar is in every header's markup and drawn only below 768px. See the block below. */
.pv2-tabbar { display: none; }

/* ── THE PORTAL ON A PHONE: THE SHELL (2026-10-10, THE QUEUE "Portal on a phone", page type 1) ──────────────

   Tim, from six screenshots of Oceanside's portal on an Android phone: "It can't get much worse." The shell
   half of what he saw, and what this block does about it:

     header two rows (mark + ? + power, then four links)  ->  ONE ROW, 56px: the mark left, ? and power right
     the ? and the power button sitting on the logo       ->  the mark is capped to the room left of them
     the four links in the header                         ->  a BOTTOM TAB BAR, icon and short label, 56px
     the Save bar (Revert / Save changes) at the bottom   ->  sits ON TOP of the tab bar, never under it

   This replaces the 2026-10-08 rule that put the four links on a second header row ("At phone width the
   portal header's links overlap the client's name"). That fixed the overlap and left a header 90px tall on
   every screen; a tab bar is where a phone keeps its destinations, and it frees the top for the page.

   DESKTOP IS UNTOUCHED: every rule here is inside this one query, and the tab bar's only rule outside it is
   display:none. Tablet (768 to 899) keeps the one-row header with the links inline, as before.

   THE TAB BAR TAKES NO CLIENT COLOUR BUT THE ACCENT, AND NEVER FOR TEXT. It is a white bar on the page's own
   tokens. The active tab is marked three ways, like the header's: a bar along its top edge and its icon in
   --portal-accent, and its label in full-strength ink at bold weight. The label is the part that must be read,
   so it never sits in the accent: a pale primary like MORPHD's tan would put it under 3:1 on white.

   THE SAFE AREAS are real numbers because portal-v2.html sets viewport-fit=cover: the tab bar grows by the home
   indicator's height and pads above it, and the header pads in from a notch in landscape. */
@media (max-width: 767px) {
  .pv2-header { padding-top: env(safe-area-inset-top); }
  .pv2-header-inner {
    min-height: 56px;
    gap: 12px;
    padding: 0 max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left));
  }
  /* The mark gets whatever the two buttons leave, and no more. Its inline max-width:340px (see _clientMark) is
     wider than a phone has room for beside them, which is how the ? and the power icon came to sit on
     Oceanside Alloy's 311px wordmark. 100% of the slot, with object-fit:contain already on the image, shrinks
     the logo to fit instead. */
  .pv2-header-identity { flex: 1 1 0; min-width: 0; overflow: hidden; }
  .pv2-header-identity .pv2-client-mark { max-width: 100% !important; min-width: 0; }
  .pv2-header-nav { display: none; }
  .pv2-header-account { flex: none; gap: 8px; }
  /* 38px circles, with a 4px invisible ring that takes the tap: a 46px target without a 46px button. */
  .pv2-icon-btn { width: 38px; height: 38px; position: relative; }
  .pv2-icon-btn::after { content: ''; position: absolute; inset: -4px; border-radius: 50%; }

  .pv2-tabbar {
    display: flex;
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 45;
    height: calc(56px + env(safe-area-inset-bottom));
    padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
    background: var(--pv2-surface);
    border-top: 1px solid var(--pv2-border);
    box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.06);
  }
  .pv2-tab {
    flex: 1 1 0;
    min-width: 0;
    min-height: 44px;
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    color: var(--pv2-ink-3);
    text-decoration: none;
    font-family: var(--portal-font);
    font-size: var(--t-micro);
    font-weight: 500;
    line-height: 1.1;
    -webkit-tap-highlight-color: transparent;
  }
  .pv2-tab-label { max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .pv2-tab:active { background: var(--pv2-border-lt); }
  .pv2-tab:focus-visible { outline: 2px solid var(--portal-accent); outline-offset: -4px; border-radius: 8px; }
  .pv2-tab--active { color: var(--pv2-dark); font-weight: 700; }
  .pv2-tab--active .pv2-tab-icon { color: var(--portal-accent); }
  .pv2-tab--active::before {
    content: '';
    position: absolute;
    top: -1px; left: 24%; right: 24%;
    height: 3px;
    border-radius: 0 0 3px 3px;
    background: var(--portal-accent);
  }

  /* NOTHING ENDS UNDER THE BAR. The body reserves its height, so the last row of every page and the Powered by
     footer scroll clear of it. :has() keeps the reservation to pages that have the bar: the sign-in screen
     renders no header and gets none. */
  body:has(.pv2-tabbar) { padding-bottom: calc(56px + env(safe-area-inset-bottom)); }
  /* THE SAVE BAR STANDS ON THE TAB BAR. It is fixed to bottom:0 everywhere else; here it moves up by the bar's
     height so Revert and Save changes are never underneath it, and the page body's own 140px bottom padding
     (.sty-pv2-body) still clears the save bar above that. */
  body:has(.pv2-tabbar) .pv2-footer { bottom: calc(56px + env(safe-area-inset-bottom)); }
  .pv2-footer-inner { padding: 10px 16px; }
  /* Toasts rise by the same height, so a "Saved" never lands on the tabs. */
  body:has(.pv2-tabbar) .pv2-toast { bottom: calc(80px + 56px + env(safe-area-inset-bottom)); }
  body:has(.pv2-tabbar) .bn-toast { bottom: calc(24px + 56px + env(safe-area-inset-bottom)); }
}
.pv2-set-inline { display: inline-flex; align-items: center; gap: 7px; }
.pv2-set-input--suffixed { max-width: 120px; }
.pv2-set-suffix { color: var(--pv2-ink-3); font-size: var(--t-body); }
/* A currency sits BEFORE the number. Same weight as the suffix so a prefixed and a suffixed row
   read as the same control, which they are. */
.pv2-set-prefix { color: var(--pv2-ink-3); font-size: var(--t-body); }

/* ADD PRODUCT: a full-width dashed affordance under the grid, not a card pretending to be a product.
   A create control shaped like the things it creates is the thing people click by accident. */
.pv2-add-product {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; margin-top: 4px; padding: 14px;
  border: 1.5px dashed var(--pv2-border, #ccc); border-radius: 8px;
  background: none; cursor: pointer; font-family: inherit;
  font-size: var(--t-body); color: var(--portal-accent);
}
.pv2-add-product:hover { background: var(--pv2-surface, #fff); border-color: var(--portal-accent); }
.pv2-add-product:disabled { opacity: 0.5; cursor: default; }
.pv2-add-product-plus { font-size: var(--t-lead); line-height: 1; }
/* THE THREE KINDS (2026-08-31, PLATFORM SPEC > PRODUCTS). They sit UNDER the affordance that opens
   them, in the spec's own order, and they are deliberately not cards: the note above this block is
   that a create control shaped like the things it creates is what people click by accident, and
   three of them would be three such controls. Each row is a name and what it is, nothing else. */
.pv2-add-product-wrap { position: relative; }
/* `display: grid` BEATS THE hidden ATTRIBUTE, and the gate caught it on the first run: the panel
   rendered open on a fresh page because [hidden]'s display:none is a UA rule any author display
   wins against. Every collapsible built this way needs this line or it is never collapsed. */
.pv2-add-product-kinds[hidden] { display: none; }
.pv2-add-product-kinds {
  display: grid; gap: 6px; margin-top: 6px; padding: 8px;
  border: 1px solid var(--pv2-border, #ccc); border-radius: 8px;
  background: var(--pv2-surface, #fff);
}
.pv2-add-product-kind {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  width: 100%; padding: 10px 12px; text-align: left;
  border: 1px solid transparent; border-radius: 6px;
  background: none; cursor: pointer; font-family: inherit;
}
.pv2-add-product-kind:hover { border-color: var(--portal-accent); background: var(--pv2-bg, #fafafa); }
.pv2-add-product-kind-name { font-size: var(--t-body); color: var(--pv2-ink, #1a1a1a); font-weight: 500; }
.pv2-add-product-kind-what { font-size: var(--t-small); color: var(--pv2-ink-3, #666); }
/* A product that is not live yet says so. It is the only difference a client can act on between a
   card that is earning money and one that is still being built. */
.pv2-card-draft {
  position: absolute; top: 10px; left: 10px;
  background: var(--portal-amber, #b7791f); color: #fff;
  font-size: var(--t-micro); font-weight: 700; letter-spacing: 0.5px;
  padding: 2px 6px; border-radius: 3px;
}
.pv2-product-card--draft { border-style: dashed; }
/* THE MIDDLE STATE (2026-08-12, brief Task 23). Reachable at its own link, just not a catalogue tile.
   It is NOT a lesser state, so it gets no dashed border: a standard part lives here permanently and
   is earning money. A quiet outline pill, so the grid reads three states at a glance without the
   middle one shouting. --pv2-mid is the portal's own ink, not a client colour. */
.pv2-card-linkonly {
  position: absolute; top: 10px; left: 10px;
  background: transparent; color: var(--pv2-mid, #55666a);
  border: 1px solid var(--pv2-border, #dde3e4);
  font-size: var(--t-micro); font-weight: 700; letter-spacing: 0.5px;
  padding: 1px 6px; border-radius: 3px;
}
.pv2-set-logo-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 10px; }


/* PRODUCT SETTINGS TABS (2026-08-09). Reuses .pv2-set-wrap / .pv2-set-nav / .pv2-set-detail from
   SETTINGS deliberately - the spec asks for the same shape, and a second set of rules is how two
   panels that should look identical start to drift. Only the outer spacing differs, because this
   one sits above a product's tree rather than filling the page. */
/* FULL WIDTH, UNLIKE THE OTHER TWO SETTINGS SURFACES (2026-08-10, brief Task 10). .pv2-set-wrap is
   capped at 1400px, which is right for the client Settings page and the Products page - a column of
   short form rows reading across a 2560px monitor is unreadable. The PRODUCT settings panel sits
   directly above the editor, which is full width, so the cap put a hard edge halfway across the page
   with the editor continuing past it. Only this wrapper is released; the other two keep the cap. */
.pv2-prodset-wrap { margin-bottom: 18px; max-width: none; }
.pv2-prodset-wrap .pv2-set-detail { min-height: 0; }
.pv2-prodset-pane[hidden] { display: none; }


/* ── THE EDITOR CANNOT HAVE A FOOTER (2026-08-13, brief Task 8) ─────────────────────────────────

   The product editor carries a sticky save bar across the bottom of the viewport. The Powered by
   footer shipped on 2026-08-12 and landed in the same strip, so on the editor the two competed and
   the footer sat half covered behind the save bar. Two things fighting for one strip is worse than
   either alone, so the editor drops the footer and the Bottleneck mark moves into the header.

   ONE CLASS DRIVES BOTH HALVES, which is Tim's rule ("the header mark appears ONLY where the footer
   cannot go") made structural rather than remembered. body.pv2-editing hides the footer and reveals
   the mark in the same breath, so the mark cannot render twice on a page that already has a footer,
   and cannot go missing on the one page that has none. */
body.pv2-editing > .page-footer { display: none; }

/* PROVENANCE, NOT A SECOND BRAND. It sits in the same bar as the client's own mark, so a wordmark at
   equal weight would read as a co-brand on their portal. Small uppercase label, an 18px mark at
   reduced opacity, pushed to the right of the nav and left of the account controls.

   BOTTLENECK'S OWN COLOUR, never the client's. --bn-charcoal, the same token the footer reads, and
   the same rule: our chrome does not take a client tint any more than a client colour may be a
   literal in shared code. */
/* EVERY PORTAL HEADER, NOT JUST THE EDITOR (2026-08-15, queue Task 27). Tim's call, reversing the
   earlier rule that the mark appears only where the footer cannot go - which had it on the product
   editor alone, because that is the one portal view with no footer. It is provenance, and provenance
   that appears on one page out of five reads as an accident rather than as a signature. */
/* ONE OPACITY AND ONE COLOUR FOR THE WHOLE MARK (2026-08-15, queue Task 10).
   Tim: "one opacity and one colour for the whole mark, so the words and the wordmark read as one
   thing." They did not: the label sat at opacity 1 and the wordmark at 0.8, so the two halves of one
   mark faded differently and read as two elements that happened to be adjacent.

   THE SINGLE VALUE IS 1, and that is the contrast finding from 2026-08-14 kept rather than
   overturned. readableInk picks whichever of white or black contrasts better with the client's
   primary, so at full opacity the ratio is the best that ground can give, and every point of fade
   spends a margin that is not the same on every client:

     opacity   oceanside-alloy   morphd   demo
     0.72          4.78           3.89     8.27     <- morphd FAILS AA
     0.85          5.93           4.70    10.93
     1.00          7.44           5.77    14.58

   0.85 would clear today's three clients and leave the next brand to fail the same way. So the mark
   is quiet by SIZE AND WEIGHT - micro caps beside a small wordmark - and not by fade. Applying the
   opacity here, on the container, is also what makes it ONE value: a child cannot now disagree. */
.pv2-header-poweredby {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* HARD RIGHT. It is a child of .pv2-header-account now, so it needs no margin-left:auto - the
     cluster is already at the right edge and this sits in it, before the two buttons. */
  margin-right: 10px;
  opacity: 1;
}
/* THE INK IS INHERITED, NOT DECLARED (2026-08-14, queue Task 9). The colour is set inline on
   .pv2-header-poweredby from readableInk(client primary), so this must NOT restate it - a
   `color: var(--bn-charcoal)` here would win over the inherited value and put the bug straight
   back. That is exactly how it got here: a fixed charcoal on a header painted with the client's
   own primary, which on Oceanside Alloy's dark teal is charcoal on near-charcoal. */
.pv2-header-poweredby span {
  /* SMALLER (queue Task 10). --t-micro is the portal's floor at 13px and this label is not body
     text, it is provenance: it should read as a caption on the mark rather than as a fifth thing
     competing with the nav. Set as a literal because it is deliberately BELOW the token floor, and
     a token that means "the smallest legible size" should not be redefined to suit one label. */
  font-size: 9.5px;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: inherit;
  /* No opacity here. The container carries the one value; see the note above. */
  white-space: nowrap;
}
.pv2-header-poweredby img {
  height: 15px;
  width: auto;
  object-fit: contain;
  /* No opacity here either. The asset already matches the ground (cream on dark, charcoal on
     light), and a second opacity is exactly what made the words and the wordmark read as two
     things. */
}
/* Below the width where the client's mark, four nav links and the account controls already crowd,
   the provenance line is the first thing to go. It is the least load-bearing item in the bar. */
@media (max-width: 900px) {
  .pv2-header-poweredby { display: none; }
}


/* ── EDITING A REVISION MOVED TO THE QUOTE DOCUMENT (2026-08-15, queue Task 1a) ─────────────────
   The pane that lived here is gone, not hidden. All that renders in the inbox now is a line saying
   where prices are edited and a link to it, because a person who opened a revision expecting to type
   a number needs telling rather than nothing. */

/* -- MARGIN, SLICE 3: COST, MARGIN AND SELL BY CATEGORY (2026-09-17) -------------------------------
   Drawn only on a manufacturer quote that carries a margin. Right-aligned money in the same column
   rhythm as the pricing blocks above it. */
.margin-table { width: 100%; border-collapse: collapse; margin: 10px 0 4px; font-size: var(--t-small, 0.85rem); }
.margin-table th { text-align: right; font-weight: 600; font-size: var(--t-micro, 0.72rem); text-transform: uppercase; letter-spacing: 0.6px; padding: 4px 6px; border-bottom: 1px solid var(--border, #e6e2da); }
.margin-table th:first-child, .margin-table td:first-child { text-align: left; }
.margin-table td { text-align: right; padding: 4px 6px; font-variant-numeric: tabular-nums; }
.margin-table .margin-total td { font-weight: 700; border-top: 1px solid var(--border, #e6e2da); }
.margin-below { color: #cc3333; }

/* ── THE QUOTE FILTER CHIPS, ONE SWIPEABLE ROW (2026-10-10, THE QUEUE "Portal on a phone", page type 2) ─────────────────────────────
   Tim: the filter chips wrapped to three rows. On a phone they stay on ONE line and swipe sideways, the way a phone app's filter row
   does. The search box takes the full width under them. Desktop is untouched. */
@media (max-width: 767px) {
  .pv2-inbox-controls { gap: 10px; }
  .sty-pv2-inbox-tabs { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none;
                        margin: 0 -16px 0; padding: 2px 16px; flex: 1 0 100%; }
  .sty-pv2-inbox-tabs::-webkit-scrollbar { display: none; }
  .sty-pv2-inbox-tab { flex: 0 0 auto; white-space: nowrap; min-height: 40px; }
  .pv2-inbox-search { flex: 1 0 100%; margin-left: 0; min-width: 0; }
}

/* ── THE EDITOR PAGE ON A PHONE (2026-10-10, THE QUEUE "Portal on a phone", page type 4) ─────────────────────────────────────────────
   The Published notice was four lines of words over every product page. The pill says it; the sentence is for a state that asks for a
   decision (a draft, a product nobody can see), which keeps its words. The page's own card edge and padding give the width back. */
@media (max-width: 767px) {
  .pv2-draftbar:not(.pv2-draftbar--on) .pv2-draft-pill--live + .pv2-draft-note { display: none; }
  .pv2-draftbar { padding: 8px 10px; margin-bottom: 10px; gap: 8px; }
  .pv2-draftbar .pv2-draft-btn, .pv2-draftbar .pv2-btn--save { min-height: 40px; }
  .pv2-product-section { padding: 12px 10px; }
}

/* ── FULL WIDTH ON A PHONE: NO BOX AROUND THE PAGE (2026-10-10, THE QUEUE "Portal on a phone: use the full width (no boxes in boxes)") ──────────────────
   The portal's page body took 34px a side (24px padding inside a 95vw box that is itself centred) and the product page's own card took another 11, so a
   390px screen showed text in about 260px. On a phone the body keeps ONE 16px margin and the product page is not a card: no border, no background, no
   padding. The pane inside it and its sections are flattened in editor-styles.js. Above 767px nothing here applies. */
@media (max-width: 767px) {
  .pv2-body.sty-pv2-body, .sty-pv2-body, .sty-pv2-body--edit-wide, .pv2-body--library, .sty-pv2-body--products, .sty-pv2-body--settings, .pv2-body--wide {
    max-width: none; margin: 0; padding-left: 16px; padding-right: 16px;
  }
  .pv2-body.sty-pv2-body { max-width: none; margin-left: 0; margin-right: 0; }
  .pv2-product-section { background: transparent; border: 0; border-radius: 0; padding: 0; }
  /* THE EDITOR'S HEAD IS ONE LINE: the product's name, smaller, with the Published chip beside it, not a bordered bar over a big title. A DRAFT keeps its
     bar (it carries Preview, Discard and Publish and a sentence that asks for a decision), on a line of its own under the title. */
  /* SETTINGS: a section is a heading and its rows, not a card with 22px of padding inside a border. QUOTE REQUESTS: a request IS a card (the one box), with a
     smaller padding so its text keeps the width. */
  .pv2-set-block { border: 0; border-radius: 0; background: transparent; padding: 0; }
  .pv2-inbox-row.pv2-qbox { padding: 10px; }
  /* THE PRODUCT CARD KEEPS ITS 16px PADDING: its photo takes a -16px margin to reach the card's edges (.pv2-card-hero), so changing one without the other
     pushed the photo past the border. The cards in a row take the height of the tallest. */
  .pv2-product-grid { align-items: stretch; }
  .pv2-product-tile { display: flex; flex-direction: column; }
  .pv2-product-tile > .pv2-product-card { flex: 1 1 auto; display: flex; flex-direction: column; justify-content: flex-start; align-items: stretch; }
  #pv2EditContent { display: flex; flex-wrap: wrap; align-items: center; column-gap: 10px; }
  #pv2EditContent > .pv2-product-name { order: 1; font-size: 20px; line-height: 1.25; margin: 0 0 8px; flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; }
  #pv2EditContent > .pv2-draftbar { order: 2; margin: 0 0 8px; }
  #pv2EditContent > .pv2-draftbar:not(.pv2-draftbar--on) { background: transparent; border: 0; padding: 0; flex: 0 0 auto; }
  #pv2EditContent > .pv2-draftbar--on { order: 3; flex: 1 0 100%; }
  #pv2EditContent > [id$="-tree-container"] { order: 4; flex: 1 0 100%; min-width: 0; }
  /* THE REVERT / SAVE BAR IS THERE WHEN THERE IS SOMETHING TO SAVE. Greyed out and empty it took about a quarter of the screen, above the tab bar, to say
     nothing; with a message to show (Saved, an error) or a change to keep it is back. */
  .pv2-footer:has(#pv2SaveBtn:disabled):has(#pv2DiscardBtn:disabled):has(.pv2-footer-status:empty) { display: none; }
}

/* ── SETTINGS TABS AND THE PRODUCTS GRID ON A PHONE (2026-10-10, THE QUEUE "Portal on a phone", page type 6) ────────────────────────
   Settings' own section list (Business, Branding, Money, Cart charges, Spend categories, Notifications) is built from the editor's
   navigator row class, whose styling is not loaded on this page, so on a phone the six names ran together as plain text. They are
   pills in one swipeable row, the same shape as the editor's tab strip, the active one filled. The Products list shows two cards across
   instead of one photo per screen; the cards keep their own proportions. */
@media (max-width: 767px) {
  .pv2-set-nav { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; gap: 6px; padding: 2px 2px 8px; }
  .pv2-set-nav::-webkit-scrollbar { display: none; }
  .pv2-set-nav > [class*="cte2-nav-row"] {
    flex: 0 0 auto; padding: 10px 14px; min-height: 44px; box-sizing: border-box; display: inline-flex; align-items: center;
    border: 1px solid var(--pv2-border, #d9d4cc); border-radius: 22px; background: var(--pv2-surface, #fff);
    font-size: var(--t-body); font-weight: 600; color: var(--pv2-ink-2, #33393b); white-space: nowrap; cursor: pointer;
  }
  .pv2-set-nav > [class*="cte2-nav-row--selected"] { background: var(--portal-accent); border-color: var(--portal-accent); color: var(--client-ink, #fff); }
  .pv2-product-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
}

/* ── SETTINGS' NAVIGATOR IS A CARD WITH GROUPS, LIKE THE PRODUCT EDITOR'S (2026-10-13, THE QUEUE "Settings: product-editor navigator,
   spend categories without offcut, delivery removed") ────────────────────────────────────────────────────────────────────────────
   Tim: "the navigator is just flat". The entries sit under BUSINESS, QUOTING and TEAM in a card, the selected one highlighted with the
   editor's own fill and accent bar. A modifier, because .pv2-set-nav is also the product settings tab strip, which is unchanged.
   On a phone the card stays, each group's entries become pills that wrap under their caption. */
.pv2-set-nav--card { flex-direction: column; gap: 8px; padding: 12px 8px; background: var(--pv2-surface, #fff);
  border: 1px solid var(--pv2-border, #e0dcd4); border-radius: 10px; }
.pv2-set-nav-title { padding: 2px 10px 4px; font-size: var(--t-lead); font-weight: 700; color: var(--pv2-dark); }
.pv2-set-nav-group { display: flex; flex-direction: column; gap: 1px; }
.pv2-set-nav-cap { padding: 6px 10px 4px; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--pv2-mid, #6b7a7d); }
.pv2-set-nav-row { padding: 7px 10px 7px 14px; border-radius: 6px; cursor: pointer; color: var(--pv2-dark); line-height: 1.3; }
.pv2-set-nav-row:hover { background: #eef3f2; }
.pv2-set-nav-row.sty-cte2-nav-row--selected { background: #e3efec; box-shadow: inset 3px 0 0 var(--portal-accent); font-weight: 600; }
.pv2-set-nav-row:focus-visible { outline: 2px solid var(--portal-accent); outline-offset: -2px; }
.pv2-set-nav-row[hidden] { display: none; }
@media (max-width: 899px) {
  .pv2-set-nav.pv2-set-nav--card { flex-direction: column; flex-wrap: nowrap; }
}
@media (max-width: 767px) {
  .pv2-set-nav.pv2-set-nav--card { overflow: visible; padding: 10px; }
  .pv2-set-nav--card .pv2-set-nav-group { flex-direction: row; flex-wrap: wrap; gap: 6px; }
  .pv2-set-nav--card .pv2-set-nav-cap { flex: 1 0 100%; padding: 4px 2px 0; }
  .pv2-set-nav--card .pv2-set-nav-row { display: inline-flex; align-items: center; min-height: 40px; padding: 8px 14px; border-radius: 20px;
    border: 1px solid var(--pv2-border, #d9d4cc); }
  .pv2-set-nav--card .pv2-set-nav-row.sty-cte2-nav-row--selected { background: var(--portal-accent); border-color: var(--portal-accent);
    color: var(--client-ink, #fff); box-shadow: none; }
  .pv2-set-nav--card .pv2-set-nav-row[hidden] { display: none; }
}

/* SPEND CATEGORIES, A COMPACT LIST (2026-10-13): the name renamed in place, how many lines use it, Remove; Remove asks in the row,
   saying how many lines will become uncategorised. */
.pv2-spendcat-list { display: flex; flex-direction: column; }
.pv2-spendcat-row { display: grid; grid-template-columns: minmax(0, 22rem) auto auto minmax(0, 1fr); gap: 12px; align-items: center;
  padding: 6px 0; border-bottom: 1px solid var(--pv2-border-lt, #f0ece4); }
.pv2-spendcat-row--new { border-bottom: none; padding-top: 12px; grid-template-columns: minmax(0, 22rem) auto minmax(0, 1fr); }
.pv2-spendcat-count { color: var(--pv2-mid, #6b7a7d); font-size: var(--t-small); white-space: nowrap; min-width: 5.5em; }
.pv2-spendcat-confirm { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 8px 10px;
  border-radius: 6px; background: var(--pv2-surface-2, #f6f4f0); }
.pv2-spendcat-confirm[hidden], .pv2-spendcat-del[hidden] { display: none; }
@media (max-width: 767px) {
  .pv2-spendcat-row { grid-template-columns: minmax(0, 1fr) auto; }
  .pv2-spendcat-row .pv2-spendcat-count { grid-column: 1; }
  .pv2-spendcat-row--new { grid-template-columns: minmax(0, 1fr) auto; }
}
.pv2-spendcat-row .pv2-spendcat-name, .pv2-spendcat-row #pv2SpendCatNew { width: 100%; box-sizing: border-box; padding: 7px 10px;
  border: 1px solid var(--pv2-border, #e0dcd4); border-radius: 6px; background: var(--pv2-surface, #fff); font: inherit; color: var(--pv2-dark); }
.pv2-spendcat-row .pv2-spendcat-name:not(:hover):not(:focus) { border-color: transparent; }
@media (max-width: 767px) {
  .pv2-spendcat-row .pv2-spendcat-name { grid-column: 1 / -1; }
}

/* THE STAFF ADDRESS'S BUSINESS PICKER (2026-10-13, app.bottleneck.au/): one row per business this login belongs to. */
.pv2-staff-pick { display: flex; flex-direction: column; gap: 8px; margin: 8px 0 14px; }
.pv2-staff-pick-row { display: block; padding: 12px 14px; border: 1px solid var(--pv2-border, #e0dcd4); border-radius: 8px;
  color: var(--pv2-dark); text-decoration: none; font-weight: 600; }
.pv2-staff-pick-row:hover, .pv2-staff-pick-row:focus-visible { border-color: var(--portal-accent); background: #eef3f2; }
